Files
udyamlinc/src/features/officer/ForecastsPage.tsx
T
LincPayandClaude Opus 5 f430f85202 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>
2026-08-06 13:30:24 +05:30

84 lines
3.8 KiB
TypeScript

import { Card, KpiTile } from '../../components/ui/bits'
import { ForecastFan } from '../../components/charts/ForecastFan'
import { BandMigrationBar } from '../../components/charts/BandMigrationBar'
import { bandMigration, portfolioForecast, sectorRollup } from '../../lib/demo/ops'
import { inrCompact } from '../../lib/demo/seed'
import { useScope } from './officerScope'
/**
* Portfolio-level view of the same forecast the enterprise pages show one at a
* time: the h = 1…6 aggregate, how each sector pack is carrying risk, and how
* the book migrated between watchlist bands over the last six months.
*/
export function ForecastsPage() {
const { block } = useScope()
const aggregate = portfolioForecast(block)
const sectors = sectorRollup(block)
const migration = bandMigration(block)
const worst = aggregate.reduce((a, p) => (p.p10 - p.obligation < a.p10 - a.obligation ? p : a), aggregate[0])
const covered = aggregate.filter((p) => p.p10 >= p.obligation).length
return (
<div className="space-y-4">
<div className="flex flex-wrap gap-3">
<KpiTile label="Horizon" value="6 months" delta="P10 / P50 / P90" />
<KpiTile label="Months covered at P10" value={`${covered} / 6`} tone={covered >= 5 ? 'good' : 'watch'} />
<KpiTile label="Tightest month" value={worst.label} delta={`P10 ${inrCompact(worst.p10)} vs ${inrCompact(worst.obligation)} due`} tone="red" />
<KpiTile label="Sector packs live" value={sectors.length} delta="each modelled separately" />
</div>
<div className="grid gap-4 lg:grid-cols-2">
<Card title="Portfolio net cash position — mean of the book">
<div className="p-4">
<ForecastFan points={aggregate} height={260} />
</div>
</Card>
<Card title="Band migration — last 6 months">
<div className="p-4">
<BandMigrationBar rows={migration} height={260} />
</div>
</Card>
</div>
<Card title="Sector risk packs" badge="dairy · poultry · food proc. · handicrafts · rural retail">
<div className="overflow-x-auto">
<table className="ul-table">
<thead>
<tr>
<th>Sector pack</th>
<th className="text-right">Enterprises</th>
<th className="text-right">High risk</th>
<th className="text-right">Watch</th>
<th className="text-right">Standard</th>
<th className="text-right">Avg score</th>
<th>Mix</th>
</tr>
</thead>
<tbody>
{sectors.map((s) => (
<tr key={s.sector}>
<td className="font-semibold text-slate-700">{s.label}</td>
<td className="text-right text-slate-600">{s.count}</td>
<td className="text-right text-risk-red font-semibold">{s.red}</td>
<td className="text-right text-amber font-semibold">{s.watch}</td>
<td className="text-right text-green font-semibold">{s.standard}</td>
<td className="text-right font-bold text-slate-700">{s.avgScore}</td>
<td className="w-[180px]">
<span className="flex h-[7px] rounded-full overflow-hidden bg-slate-100">
<span className="bg-risk-red" style={{ width: `${(s.red / Math.max(1, s.count)) * 100}%` }} />
<span className="bg-amber" style={{ width: `${(s.watch / Math.max(1, s.count)) * 100}%` }} />
<span className="bg-green" style={{ width: `${(s.standard / Math.max(1, s.count)) * 100}%` }} />
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
</div>
)
}