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, role, getRecurringPlans, createRecurringPlan, deactivateRecurringPlan, getAmc, createAmc, deactivateAmc, generateAmcRenewalInvoice, getInteractions, getInteractionTypes, createInteraction, updateInteraction, CLIENT_MODULE_STATUSES, CLIENT_STATUSES, KIND_LABEL, PAYMENT_MODES, type AmcPaidStatus, type ClientModule, type ClientModuleStatus, type ClientStatus, type InteractionType, type Kind, type Module, type Outcome, 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) const AMC_TONE: Record = { paid: 'ok', unpaid: 'err', unbilled: 'warn' } const OUTCOME_TONE: Record = { positive: 'ok', neutral: 'warn', negative: 'err' } /** 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 plans = useData(() => getRecurringPlans(id), [id]) const amc = useData(() => getAmc(id), [id]) const interactions = useData(() => getInteractions(id), [id]) const types = useData(getInteractionTypes, []) const isOwner = role() === 'owner' 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) : '—', }))} /> )}

Recurring plans

{isOwner && cms.data !== undefined && ( cm.active).map((cm) => ({ id: cm.id, label: `${moduleName(cm.moduleId)} · ${KIND_LABEL[cm.kind]}`, }))} onDone={() => plans.reload()} /> )} {plans.error !== undefined && {plans.error}} {plans.data === undefined || plans.data.length === 0 ? No recurring plans. : ( { const cm = cms.data?.find((x) => x.id === plan.clientModuleId) return { module: cm !== undefined ? moduleName(cm.moduleId) : '—', cadence: plan.cadence, amount: plan.amountPaise !== null ? inr(plan.amountPaise) : 'price book', next: plan.nextRun, policy: {plan.policy}, active: plan.active ? 'yes' : 'no', act: plan.active ? ( ) : '—', } })} /> )}

AMC contracts

{isOwner && amc.reload()} />} {amc.error !== undefined && {amc.error}} {amc.data === undefined || amc.data.length === 0 ? No AMC contracts. : ( ({ coverage: a.coverage !== '' ? a.coverage : '—', period: `${a.periodFrom} → ${a.periodTo}`, amount: inr(a.amountPaise), days: a.renewalReminderDays, paid: {a.paidStatus}, act: (
{a.paidStatus !== 'unpaid' && ( )} {a.active && ( )}
), }))} /> )}

Interactions

{types.data !== undefined && ( interactions.reload()} /> )} {interactions.error !== undefined && {interactions.error}} {interactions.data === undefined || interactions.data.length === 0 ? No interactions logged yet. : ( ({ on: i.onDate, type: types.data?.find((t) => t.code === i.typeCode)?.label ?? i.typeCode, notes: i.notes !== '' ? i.notes : '—', outcome: i.outcome !== null ? {i.outcome} : '—', follow: ( { setActionErr(undefined) updateInteraction(i.id, { followUpOn: e.target.value !== '' ? e.target.value : null }) .then(() => interactions.reload()).catch((err: Error) => setActionErr(err.message)) }} /> ), }))} /> )}
) } /** Owner-only: create a recurring plan on one of the client's modules. */ function NewRecurringForm(props: { clientId: string; options: { id: string; label: string }[]; onDone: () => void }) { const [f, setF] = useState({ clientModuleId: '', cadence: 'monthly', amountRs: '', nextRun: today(), policy: 'manual' }) 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 = () => { if (f.clientModuleId === '') { setError('Pick a subscribed module'); return } const amount = f.amountRs === '' ? undefined : Number(f.amountRs) if (amount !== undefined && (!Number.isFinite(amount) || amount <= 0)) { setError('Amount must be a positive rupee value (or blank for the price book)'); return } setError(undefined) setSaving(true) createRecurringPlan(props.clientId, { clientModuleId: f.clientModuleId, cadence: f.cadence, nextRun: f.nextRun, policy: f.policy, ...(amount !== undefined ? { amountPaise: fromRupees(amount) } : {}), }) .then(() => { setF({ clientModuleId: '', cadence: 'monthly', amountRs: '', nextRun: today(), policy: 'manual' }); props.onDone() }) .catch((e: Error) => setError(e.message)) .finally(() => setSaving(false)) } return (
{error !== undefined && {error}}
) } /** Owner-only: create an AMC contract — amount entered in rupees, stored in paise. */ function NewAmcForm(props: { clientId: string; onDone: () => void }) { const [f, setF] = useState({ coverage: '', periodFrom: today(), periodTo: '', amountRs: '', reminderDays: '30' }) 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 the contract amount in rupees'); return } if (f.periodTo === '') { setError('Pick the period end date'); return } const days = Number(f.reminderDays) if (!Number.isInteger(days) || days < 0) { setError('Reminder days must be a non-negative integer'); return } setError(undefined) setSaving(true) createAmc(props.clientId, { coverage: f.coverage, periodFrom: f.periodFrom, periodTo: f.periodTo, amountPaise: fromRupees(amount), renewalReminderDays: days, }) .then(() => { setF({ coverage: '', periodFrom: today(), periodTo: '', amountRs: '', reminderDays: '30' }); props.onDone() }) .catch((e: Error) => setError(e.message)) .finally(() => setSaving(false)) } return (
{error !== undefined && {error}}
) } /** All roles: log a call / visit / training …; a follow-up date feeds the daily scan. */ function LogInteractionForm(props: { clientId: string; types: InteractionType[]; onDone: () => void }) { const [f, setF] = useState({ typeCode: '', onDate: today(), notes: '', outcome: '', followUpOn: '' }) 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 = () => { if (f.typeCode === '') { setError('Pick an interaction type'); return } setError(undefined) setSaving(true) createInteraction(props.clientId, { typeCode: f.typeCode, onDate: f.onDate, notes: f.notes, ...(f.outcome !== '' ? { outcome: f.outcome } : {}), ...(f.followUpOn !== '' ? { followUpOn: f.followUpOn } : {}), }) .then(() => { setF({ typeCode: '', onDate: today(), notes: '', outcome: '', followUpOn: '' }); props.onDone() }) .catch((e: Error) => setError(e.message)) .finally(() => setSaving(false)) } return (