Files
udyamlinc/src/features/nabard/NabardShell.tsx
T
LincPayandClaude Opus 5 38067c41f8 feat(nabard): supervisory portal — national view, banks, early warning
/nabard, reachable only by a NABARD role. Four screens: national view with
the India→zone→state→district drill-down, partner-bank scorecard that
expands into branches, early-warning digest, and the institution register.

Deliberately a different chrome from the bank console. A NABARD user and a
branch officer are not two ranks of one job; a portal that looked identical
would invite the assumption that the apex user is a bank user with more
rights. The sidebar states on every screen that this tier holds no borrower
identity, so a supervisor can tell a partner bank what it can see without
checking.

Response rate is the column the scorecard is built around. Stress rate is
mostly a fact about a bank's districts and sectors; response rate is a fact
about the bank, it is the one no bank self-reports, and low values are
coloured to draw the eye.

Suppressed cells keep their row and lose their numbers — dropping the row
reads as 'no enterprises here', a different and wrong claim, and the one a
reader would most easily act on.

Login routes by tier: both staff tiers share one sign-in and land in
different portals. RequireApex bounces a bank session to /officer rather
than to /login, because signed in as the wrong tier is not signed out. The
routing is a convenience only — the backend re-derives the tier from the
signed JWT and refuses the other surface with a 403.

The demo fallback includes a partner bank below the suppression threshold,
so the withholding behaviour is visible without a backend.

tsc, oxlint and npm run build clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 18:10:51 +05:30

83 lines
3.1 KiB
TypeScript

import { NavLink, Outlet, useNavigate } from 'react-router-dom'
import { Building2, Gauge, Landmark, ShieldCheck, Siren } from 'lucide-react'
import { clearSessions, officerSession } from '../../lib/api'
const NAV = [
{ label: 'National view', to: '/nabard', end: true, icon: Gauge },
{ label: 'Partner banks', to: '/nabard/banks', icon: Landmark },
{ label: 'Early warning', to: '/nabard/digest', icon: Siren },
{ label: 'Institutions', to: '/nabard/institutions', icon: Building2 },
]
/**
* The apex portal shell.
*
* <p>Deliberately a different chrome from the bank console. A NABARD user and a
* branch officer are not two ranks of one job — they see different things and
* may do different things, and a portal that looked identical would invite the
* assumption that the apex user is simply a bank user with more rights.
*/
export function NabardShell() {
const navigate = useNavigate()
const session = officerSession()
return (
<div className="min-h-screen flex bg-slate-50">
<aside className="w-[210px] shrink-0 bg-[#123c2b] text-white flex flex-col px-3 py-5">
<div className="flex items-center gap-2.5 px-2 pb-5">
<span className="w-9 h-9 rounded-[9px] bg-[#0f6b4c] grid place-items-center shrink-0">
<ShieldCheck size={19} />
</span>
<span>
<b className="text-[16px] block leading-tight">UdyamLinc</b>
<span className="text-[10.5px] text-[#bcd6cb]">NABARD Supervisory View</span>
</span>
</div>
<nav className="flex flex-col gap-0.5">
{NAV.map((n) => (
<NavLink
key={n.to}
to={n.to}
end={n.end}
className={({ isActive }) =>
`flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-[12.5px] transition-colors ${
isActive ? 'bg-white/15 text-white font-semibold' : 'text-[#cfe2da] hover:bg-white/10'
}`
}
>
<n.icon size={15} />
{n.label}
</NavLink>
))}
</nav>
{/*
Stated in the chrome, on every screen, rather than buried in a policy
page: this tier cannot reach a borrower. A supervisor should be able to
tell a partner bank what this portal can see without checking.
*/}
<div className="mt-auto pt-4 border-t border-white/15 text-[10px] text-[#9dbfb1] leading-relaxed">
<b className="text-[#dcebe4] block mb-0.5">Aggregate view only</b>
This portal holds no borrower identity. Figures are counts and rates
across refinanced portfolios; cells below the minimum size are withheld.
</div>
<button
onClick={() => {
clearSessions()
navigate('/login', { replace: true })
}}
className="mt-3 text-[11px] text-[#9dbfb1] hover:text-white text-left px-2"
>
Sign out{session ? ` · ${session.username}` : ''}
</button>
</aside>
<main className="flex-1 min-w-0 px-6 py-5 max-w-[1200px]">
<Outlet />
</main>
</div>
)
}