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:
@@ -0,0 +1,83 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user