import { useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { formatINR, fromRupees } from '@sims/domain' import { Badge, Button, DataTable, EmptyState, Field, Notice, PageHeader, StatCard, Stats, Toolbar } from '@sims/ui' import { assignClientModule, getClient, getClientModules, getLedger, getModules, patchClient, patchClientModule, recordPayment, CLIENT_MODULE_STATUSES, CLIENT_STATUSES, KIND_LABEL, PAYMENT_MODES, type ClientModule, type ClientModuleStatus, type ClientStatus, type Kind, type Module, type PaymentMode, } from '../api' import { CLIENT_TONE, DOC_TONE, useData } from './Clients' const inr = (p: number) => formatINR(p, { symbol: false }) const today = () => new Date().toISOString().slice(0, 10) /** Client 360° — header, modules, documents, payments & dues (14-SPEC §Client registry). */ export function ClientDetail() { const id = useParams()['id'] ?? '' const nav = useNavigate() const client = useData(() => getClient(id), [id]) const modules = useData(getModules, []) const cms = useData(() => getClientModules(id), [id]) const ledger = useData(() => getLedger(id), [id]) const [actionErr, setActionErr] = useState() const c = client.data if (client.error !== undefined) return {client.error} if (c === undefined) return Loading… const moduleName = (moduleId: string) => modules.data?.find((m) => m.id === moduleId)?.name ?? moduleId return (
{ setActionErr(undefined) patchClient(id, { status: e.target.value as ClientStatus }) .then(client.reload).catch((err: Error) => setActionErr(err.message)) }} > {CLIENT_STATUSES.map((s) => )} } /> {c.status} {c.contacts.map((ct, i) => ( {[ct.name, ct.email, ct.phone].filter((x) => x !== undefined && x !== '').join(' · ')} ))} {actionErr !== undefined && {actionErr}}

Modules

{modules.data !== undefined && ( { setActionErr(undefined) assignClientModule(id, { moduleId, kind }) .then(() => { cms.reload(); ledger.reload() }) .catch((err: Error) => setActionErr(err.message)) }} /> )} {cms.error !== undefined && {cms.error}} {cms.data === undefined || cms.data.length === 0 ? No modules assigned yet. : ( { const paid = ledger.data?.modulePaid.find((mp) => mp.moduleId === cm.moduleId) return { module: moduleName(cm.moduleId), kind: KIND_LABEL[cm.kind], status: ( cms.reload()} onError={setActionErr} /> ), installed: cms.reload()} onError={setActionErr} />, completed: cms.reload()} onError={setActionErr} />, trained: cms.reload()} onError={setActionErr} />, billed: paid !== undefined ? inr(paid.billedPaise) : '—', settled: paid !== undefined ? inr(paid.settledPaise) : '—', } })} /> )}

Documents

{ledger.error !== undefined && {ledger.error}} {ledger.data === undefined || ledger.data.documents.length === 0 ? No documents yet — compose one from New Document. : ( nav(`/documents/${ledger.data!.documents[i]!.id}`)} rows={ledger.data.documents.map((d) => ({ no: d.docNo ?? draft, type: d.docType, date: d.docDate, payable: inr(d.payablePaise), status: {d.status}, }))} /> )}

Payments & dues

{ledger.data !== undefined && ( )} { ledger.reload(); cms.reload() }} /> {ledger.data !== undefined && ledger.data.payments.length > 0 && ( ({ on: p.receivedOn, mode: p.mode, ref: p.reference !== '' ? p.reference : '—', amount: inr(p.amountPaise), tds: p.tdsPaise > 0 ? inr(p.tdsPaise) : '—', }))} /> )}
) } function AssignModuleForm(props: { modules: Module[]; onAssign: (moduleId: string, kind: Kind) => void }) { const [moduleId, setModuleId] = useState('') const mod = props.modules.find((m) => m.id === moduleId) const [kind, setKind] = useState('') return ( {mod !== undefined && ( )} ) } function ClientModuleStatusSelect(props: { cm: ClientModule; onPatched: () => void; onError: (msg: string) => void }) { return ( ) } function RowDate(props: { cm: ClientModule; field: 'installedOn' | 'completedOn' | 'trainedOn' onPatched: () => void; onError: (msg: string) => void }) { return ( { patchClientModule(props.cm.id, { [props.field]: e.target.value !== '' ? e.target.value : null }) .then(props.onPatched).catch((err: Error) => props.onError(err.message)) }} /> ) } /** * Record a payment against the client — amounts entered in rupees, stored in * integer paise via fromRupees. Shared with DocumentView's "Record payment". */ export function PaymentForm(props: { clientId: string; onDone: () => void }) { const [f, setF] = useState({ amountRs: '', tdsRs: '', mode: 'bank' as PaymentMode, reference: '', receivedOn: today() }) const [error, setError] = useState() const [saving, setSaving] = useState(false) const set = (k: keyof typeof f) => (e: { target: { value: string } }) => setF((p) => ({ ...p, [k]: e.target.value })) const save = () => { const amount = Number(f.amountRs) if (!Number.isFinite(amount) || amount <= 0) { setError('Enter a payment amount in rupees'); return } const tds = f.tdsRs === '' ? 0 : Number(f.tdsRs) if (!Number.isFinite(tds) || tds < 0) { setError('TDS must be a non-negative rupee amount'); return } setError(undefined) setSaving(true) recordPayment({ clientId: props.clientId, receivedOn: f.receivedOn, mode: f.mode, reference: f.reference, amountPaise: fromRupees(amount), tdsPaise: fromRupees(tds), }) .then(() => { setF({ amountRs: '', tdsRs: '', mode: 'bank', reference: '', receivedOn: today() }); props.onDone() }) .catch((e: Error) => setError(e.message)) .finally(() => setSaving(false)) } return (
{error !== undefined && {error}}
) }