chore: initial commit — UdyamLinc UI, phases A-E

Vite/React 19 console and enterprise PWA. Officer console screens, offline
ledger and sync, data feeds and trip-wire rules screens with freshness chip.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
LincPay
2026-08-06 13:30:24 +05:30
co-authored by Claude Opus 5
commit f430f85202
55 changed files with 8691 additions and 0 deletions
@@ -0,0 +1,339 @@
import { useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { officerSession } from '../../lib/api'
import type { Enterprise } from '../../lib/api'
import {
ActionChip,
Card,
DriverBar,
Empty,
GhostBtn,
PrimaryBtn,
RiskBadge,
ScoreDial,
Sparkline,
StatusPill,
} from '../../components/ui/bits'
import { ForecastFan } from '../../components/charts/ForecastFan'
import { SubScoreRadar } from '../../components/charts/SubScoreRadar'
import {
DRIVER_META,
SECTORS,
breachPoint,
findEnterprise,
forecastFor,
reasonCodesFor,
stressScoreFor,
subScoresFor,
tripWiresFor,
} from '../../lib/demo/enterprises'
import { inr, inrCompact, monthRef } from '../../lib/demo/seed'
import { loadLedger } from '../../lib/ledgerStore'
import { CATEGORIES } from '../../lib/ledgerStore'
import { useVisits } from '../../lib/visitStore'
import { ScheduleVisitModal } from './ScheduleVisitModal'
type Tab = 'OVERVIEW' | 'FORECAST' | 'DRIVERS' | 'LEDGER' | 'VISITS'
const TABS: { key: Tab; label: string }[] = [
{ key: 'OVERVIEW', label: 'Overview' },
{ key: 'FORECAST', label: 'Forecast' },
{ key: 'DRIVERS', label: 'Drivers' },
{ key: 'LEDGER', label: 'Ledger' },
{ key: 'VISITS', label: 'Visits' },
]
/** Everything the officer needs about one enterprise, in five tabs. */
export function EnterpriseDetailPage() {
const { enterpriseId = '' } = useParams()
const navigate = useNavigate()
const [tab, setTab] = useState<Tab>('OVERVIEW')
const [scheduling, setScheduling] = useState(false)
const e = findEnterprise(enterpriseId)
if (!e) return <Empty text={`No enterprise found for ${enterpriseId}.`} />
const points = forecastFor(e)
const breach = breachPoint(points)
const breachRef = breach ? monthRef(new Date(`${breach.month}-01T00:00:00`)) : null
return (
<div>
{/* ── header ─────────────────────────────────────────── */}
<div className="flex items-start gap-4 mb-4">
<button onClick={() => navigate(-1)} className="text-[11.5px] text-slate-400 hover:text-forest mt-1">
← Back
</button>
<div className="min-w-0">
<div className="flex items-center gap-2.5">
<h1 className="text-[19px] font-bold text-forest">{e.name}</h1>
<RiskBadge band={e.band} />
</div>
<div className="text-[11.5px] text-slate-500">
{e.enterpriseId} · {SECTORS[e.sector].label} · {e.block} block, {e.district} district ·{' '}
{e.vintageMonths} months on book
</div>
</div>
<div className="ml-auto flex gap-2">
<PrimaryBtn onClick={() => setScheduling(true)}>Schedule visit</PrimaryBtn>
<GhostBtn onClick={() => setTab('FORECAST')}>Full forecast</GhostBtn>
</div>
</div>
{breach && (
<div className="bg-risk-soft border border-risk-red/20 rounded-lg px-4 py-2.5 mb-4 flex items-center gap-3">
<span className="w-6 h-6 rounded-full bg-risk-red text-white grid place-items-center text-[12px] font-bold shrink-0">
!
</span>
<p className="text-[12px] text-slate-700">
Downside case fails to cover obligations from <b>{breachRef?.fullEn}</b> — projected gap{' '}
<b className="text-risk-red">{inr(breach.obligation - breach.p10)}</b> at P10.
</p>
</div>
)}
<div className="flex gap-1.5 mb-4">
{TABS.map((t) => (
<ActionChip key={t.key} active={tab === t.key} onClick={() => setTab(t.key)}>
{t.label}
</ActionChip>
))}
</div>
{tab === 'OVERVIEW' && <OverviewTab e={e} />}
{tab === 'FORECAST' && <ForecastTab e={e} />}
{tab === 'DRIVERS' && <DriversTab e={e} />}
{tab === 'LEDGER' && <LedgerTab e={e} />}
{tab === 'VISITS' && <VisitsTab e={e} />}
{scheduling && <ScheduleVisitModal enterprise={e} onClose={() => setScheduling(false)} />}
</div>
)
}
function OverviewTab({ e }: { e: Enterprise }) {
const score = stressScoreFor(e)
const reasons = reasonCodesFor(e)
return (
<div className="grid gap-4 lg:grid-cols-3">
<Card title="Composite Stress Score" badge={score.modelVersion}>
<div className="p-4 grid place-items-center">
<ScoreDial score={e.score} band={e.band} size={132} />
<div className="mt-3 w-full">
<Sparkline values={e.trend} band={e.band} width={240} height={44} />
<div className="text-[10px] text-slate-400 text-center mt-1">6-month score history</div>
</div>
</div>
</Card>
<Card title="Institutional record">
<dl className="p-4 space-y-2.5 text-[12px]">
{[
['Loan outstanding', inr(e.loanOutstanding)],
['Monthly instalment', inr(e.monthlyInstalment)],
['Sector pack', SECTORS[e.sector].label],
['Block / district', `${e.block} / ${e.district}`],
['Owners on record', String(e.ownerCount)],
['Vintage', `${e.vintageMonths} months`],
].map(([k, v]) => (
<div key={k} className="flex justify-between gap-3">
<dt className="text-slate-500">{k}</dt>
<dd className="font-semibold text-slate-700">{v}</dd>
</div>
))}
</dl>
</Card>
<Card title="Why this score" badge="SHAP">
<div className="p-4 space-y-3">
{reasons.map((r) => (
<div key={r.driver} className="border-l-[3px] border-amber pl-2.5">
<div className="text-[11px] font-semibold text-slate-700">
{r.driver} <span className="font-mono text-slate-400">· {r.weight}</span>
</div>
<p className="text-[11.5px] text-slate-600 leading-snug">{r.sentenceEn}</p>
<p className="text-[11px] text-slate-400 leading-snug">{r.sentenceHi}</p>
</div>
))}
</div>
</Card>
</div>
)
}
function ForecastTab({ e }: { e: Enterprise }) {
const points = forecastFor(e)
return (
<div className="grid gap-4 lg:grid-cols-3">
<Card title="Net cash position — 6-month quantile forecast" className="lg:col-span-2">
<div className="p-4">
<ForecastFan points={points} height={280} />
</div>
</Card>
<Card title="Month by month">
<div className="overflow-x-auto">
<table className="ul-table">
<thead>
<tr>
<th>Month</th>
<th className="text-right">P10</th>
<th className="text-right">P50</th>
<th className="text-right">Due</th>
</tr>
</thead>
<tbody>
{points.map((p) => {
const short = p.p10 < p.obligation
return (
<tr key={p.month} className={short ? '!bg-risk-soft' : ''}>
<td className="font-semibold text-slate-700">{p.label}</td>
<td className={`text-right ${short ? 'text-risk-red font-bold' : 'text-slate-600'}`}>
{inrCompact(p.p10)}
</td>
<td className="text-right text-slate-600">{inrCompact(p.p50)}</td>
<td className="text-right text-slate-500">{inrCompact(p.obligation)}</td>
</tr>
)
})}
</tbody>
</table>
</div>
<p className="px-3.5 py-2 text-[10.5px] text-slate-400 border-t border-slate-100">
Rows in red are months where the downside case does not cover what is due.
</p>
</Card>
</div>
)
}
function DriversTab({ e }: { e: Enterprise }) {
const sub = subScoresFor(e)
const wires = tripWiresFor(e)
return (
<div className="grid gap-4 lg:grid-cols-2">
<Card title="Five risk families" badge={`${SECTORS[e.sector].label} weights`}>
<div className="p-4">
<SubScoreRadar sub={sub} band={e.band} />
<div className="space-y-2 mt-4">
{DRIVER_META.map((d) => (
<DriverBar key={d.key} label={d.label} weight={sub[d.key]} />
))}
</div>
</div>
</Card>
<Card title="Deterministic trip-wires" badge="rule engine">
<div className="overflow-x-auto">
<table className="ul-table">
<thead>
<tr>
<th>Code</th>
<th>Rule</th>
<th>Threshold</th>
<th>State</th>
</tr>
</thead>
<tbody>
{wires.map((w) => (
<tr key={w.code}>
<td className="font-mono text-[10.5px] text-slate-500">{w.code}</td>
<td>
<span className="block font-semibold text-slate-700">{w.label}</span>
<span className="block text-[10.5px] text-slate-400">{w.detail}</span>
</td>
<td className="font-mono text-[10.5px] text-slate-500">{w.threshold}</td>
<td>
<span className={`pill ${w.fired ? 'pill-bad' : 'pill-mut'}`}>{w.fired ? 'Fired' : 'Clear'}</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="px-3.5 py-2 text-[10.5px] text-slate-400 border-t border-slate-100">
Trip-wires fire independently of the model, so every alert stays auditable.
</p>
</Card>
</div>
)
}
function LedgerTab({ e }: { e: Enterprise }) {
const entries = loadLedger(e.enterpriseId).slice(0, 40)
const label = (kind: string, code: string) =>
CATEGORIES[kind as keyof typeof CATEGORIES]?.find((c) => c.code === code)?.en ?? code
return (
<Card title="Three-tap ledger — last 40 entries" badge="owner-reported">
<div className="overflow-x-auto max-h-[520px] overflow-y-auto">
<table className="ul-table">
<thead className="sticky top-0">
<tr>
<th>Date</th>
<th>Type</th>
<th>Category</th>
<th className="text-right">Amount</th>
<th>Sync</th>
</tr>
</thead>
<tbody>
{entries.map((l) => (
<tr key={l.id}>
<td className="text-slate-600">{l.entryDate}</td>
<td>
<span className={`pill ${l.kind === 'IN' ? 'pill-ok' : l.kind === 'OUT' ? 'pill-bad' : 'pill-info'}`}>
{l.kind === 'IN' ? 'Money in' : l.kind === 'OUT' ? 'Money out' : 'Stock'}
</span>
</td>
<td className="text-slate-600">{label(l.kind, l.category)}</td>
<td className="text-right font-semibold text-slate-700">{inr(l.amount)}</td>
<td>
<StatusPill status={l.syncState} />
</td>
</tr>
))}
</tbody>
</table>
</div>
{!entries.length && <Empty text="This enterprise has not recorded any entries yet." />}
</Card>
)
}
function VisitsTab({ e }: { e: Enterprise }) {
const { visits } = useVisits('ALL', officerSession()?.fullName ?? 'Field Officer')
const mine = visits.filter((v) => v.enterpriseId === e.enterpriseId)
return (
<Card title="Field visits and interventions">
<div className="overflow-x-auto">
<table className="ul-table">
<thead>
<tr>
<th>Visit</th>
<th>Action</th>
<th>Due</th>
<th>State</th>
<th>Outcome</th>
</tr>
</thead>
<tbody>
{mine.map((v) => (
<tr key={v.visitId}>
<td className="font-mono text-[10.5px] text-slate-500">{v.visitId}</td>
<td className="text-slate-700 whitespace-normal">{v.action}</td>
<td className="text-slate-600">{v.dueDate}</td>
<td>
<StatusPill status={v.state} />
</td>
<td className="text-slate-500 whitespace-normal max-w-[320px]">{v.outcome ?? '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
{!mine.length && <Empty text="No visit scheduled for this enterprise yet." />}
</Card>
)
}