/demo puts the officer console and the borrower's phone side by side on one screen. The right column is a real borrower sign-in — selecting a row calls /auth/app-login and every panel beside it is fetched with that borrower's own token. Nothing is mirrored across, which is the point: if the two sides agree it is because they read one record, and the page checks that rather than asserting it. It uses its own axios instance. The shared one picks its JWT from window.location.pathname, which is right for the two portals and wrong here — it would send the officer's token to /app/*, and its 401 handler would then clear both sessions and bounce to sign-in, during a live demo. The officer dashboard is reorganised around when a borrower's downside stops covering the instalment, rather than by composite score. Lead time is the thing this platform can show that a bureau score cannot, so the triage funnel counts act-now / in-window / covered and the work list ranks by it. The previous score-ranked view is preserved as a tab; they answer different questions and sort by different keys, and merging them would mean losing one ordering. StatusPill no longer white-screens on a missing status. It renders wire data on fifteen call sites; a row that arrives without one is a data gap, not grounds to take the page down. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
44 lines
1.3 KiB
TypeScript
44 lines
1.3 KiB
TypeScript
import { NavLink } from 'react-router-dom'
|
|
|
|
/**
|
|
* Two ways to read the same book.
|
|
*
|
|
* <p><b>Portfolio overview</b> ranks by composite score — the register an officer
|
|
* already knows how to read, and the view that has been demonstrated.
|
|
* <b>Early warning</b> ranks by when the downside first stops covering the
|
|
* instalment, which is the thing this platform can show that a bureau score
|
|
* cannot.
|
|
*
|
|
* <p>They are tabs rather than one merged screen because they answer different
|
|
* questions and sort by different keys. Merging them would mean picking one sort
|
|
* order and losing the other.
|
|
*/
|
|
const TABS = [
|
|
{ to: '/officer', label: 'Portfolio overview', end: true },
|
|
{ to: '/officer/early-warning', label: 'Early warning', end: false },
|
|
]
|
|
|
|
export function DashboardTabs() {
|
|
return (
|
|
<div className="flex gap-1 border-b border-line mb-4">
|
|
{TABS.map((t) => (
|
|
<NavLink
|
|
key={t.to}
|
|
to={t.to}
|
|
end={t.end}
|
|
className={({ isActive }) =>
|
|
[
|
|
'px-3.5 py-2 text-[12px] font-semibold whitespace-nowrap border-b-2 -mb-px transition-colors',
|
|
isActive
|
|
? 'text-lp-blue border-lp-blue'
|
|
: 'text-ink-3 border-transparent hover:text-ink',
|
|
].join(' ')
|
|
}
|
|
>
|
|
{t.label}
|
|
</NavLink>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|