/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>
83 lines
3.1 KiB
TypeScript
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>
|
|
)
|
|
}
|