Files
udyamlinc/src/features/officer/EnterprisesPage.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

153 lines
5.4 KiB
TypeScript

import { useMemo, useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { useNavigate } from 'react-router-dom'
import { get } from '../../lib/api'
import type { Band, Enterprise, Sector } from '../../lib/api'
import { ActionChip, Empty, GhostBtn, RiskBadge, Sparkline, inputCls } from '../../components/ui/bits'
import { SECTORS, SECTOR_LIST } from '../../lib/demo/enterprises'
import { scopedEnterprises } from '../../lib/demo/ops'
import { inrCompact } from '../../lib/demo/seed'
import { exportCsv } from '../../lib/csv'
import { useScope } from './officerScope'
const BAND_FILTERS: { key: Band | 'ALL'; label: string }[] = [
{ key: 'ALL', label: 'All bands' },
{ key: 'RED', label: 'High risk' },
{ key: 'WATCH', label: 'Watch' },
{ key: 'STANDARD', label: 'Standard' },
]
/** The full enterprise register — searchable and filterable, feeding the detail page. */
export function EnterprisesPage() {
const navigate = useNavigate()
const { block } = useScope()
const [q, setQ] = useState('')
const [band, setBand] = useState<Band | 'ALL'>('ALL')
const [sector, setSector] = useState<Sector | 'ALL'>('ALL')
const { data } = useQuery({
queryKey: ['enterprises', block],
queryFn: () => get<Enterprise[]>('/officer/enterprises', { block }),
})
const all = data ?? scopedEnterprises(block)
const rows = useMemo(() => {
const needle = q.trim().toLowerCase()
return all.filter(
(e) =>
(band === 'ALL' || e.band === band) &&
(sector === 'ALL' || e.sector === sector) &&
(!needle || e.name.toLowerCase().includes(needle) || e.enterpriseId.toLowerCase().includes(needle)),
)
}, [all, band, sector, q])
function handleExport() {
exportCsv(
`udyamlinc-enterprises-${block}.csv`,
['Enterprise ID', 'Name', 'Sector', 'Block', 'District', 'Score', 'Band', 'Vintage (months)', 'Outstanding', 'Instalment'],
rows.map((e) => [
e.enterpriseId,
e.name,
SECTORS[e.sector].label,
e.block,
e.district,
e.score,
e.band,
e.vintageMonths,
e.loanOutstanding,
e.monthlyInstalment,
]),
)
}
return (
<div>
<div className="flex flex-wrap items-center gap-2 mb-3">
<input
className={`${inputCls} max-w-[260px]`}
placeholder="Search enterprise or ID…"
value={q}
onChange={(e) => setQ(e.target.value)}
/>
<select
className={`${inputCls} max-w-[170px]`}
value={sector}
onChange={(e) => setSector(e.target.value as Sector | 'ALL')}
>
<option value="ALL">All sectors</option>
{SECTOR_LIST.map((s) => (
<option key={s} value={s}>
{SECTORS[s].label}
</option>
))}
</select>
<div className="flex gap-1.5">
{BAND_FILTERS.map((b) => (
<ActionChip key={b.key} active={band === b.key} onClick={() => setBand(b.key)}>
{b.label}
</ActionChip>
))}
</div>
<span className="text-[11px] text-slate-500 ml-1">{rows.length} enterprises</span>
<GhostBtn className="ml-auto" onClick={handleExport}>
Export CSV
</GhostBtn>
</div>
<div className="bg-white border border-slate-200 rounded-[11px] overflow-hidden">
<div className="overflow-x-auto">
<table className="ul-table">
<thead>
<tr>
<th>Enterprise</th>
<th>Sector</th>
<th>Block</th>
<th className="text-right">Vintage</th>
<th className="text-right">Outstanding</th>
<th className="text-right">Instalment</th>
<th className="text-right">Score</th>
<th>Status</th>
<th>Trend</th>
</tr>
</thead>
<tbody>
{rows.map((e) => (
<tr
key={e.enterpriseId}
className="cursor-pointer"
onClick={() => navigate(`/officer/enterprises/${e.enterpriseId}`)}
>
<td>
<span className="block font-semibold text-slate-700">{e.name}</span>
<span className="block font-mono text-[10px] text-slate-400">{e.enterpriseId}</span>
</td>
<td className="text-slate-600">{SECTORS[e.sector].short}</td>
<td className="text-slate-600">{e.block}</td>
<td className="text-right text-slate-600">{e.vintageMonths} mo</td>
<td className="text-right text-slate-600">{inrCompact(e.loanOutstanding)}</td>
<td className="text-right text-slate-600">{inrCompact(e.monthlyInstalment)}</td>
<td
className={`text-right font-bold ${
e.band === 'RED' ? 'text-risk-red' : e.band === 'WATCH' ? 'text-amber' : 'text-green'
}`}
>
{e.score}
</td>
<td>
<RiskBadge band={e.band} />
</td>
<td>
<Sparkline values={e.trend} band={e.band} />
</td>
</tr>
))}
</tbody>
</table>
</div>
{!rows.length && <Empty text="No enterprise matches this filter." />}
</div>
</div>
)
}