import { useState, type ReactNode } from 'react' import { useNavigate } from 'react-router-dom' import { formatINR } from '@sims/domain' import { Badge, Button, DataTable, EmptyState, ErrorState, Notice, PageHeader, Skeleton, StatCard, Stats } from '@sims/ui' import { getDashboard, sendReminder, dismissReminder, getReminderPreview, type Reminder } from '../api' import { useData } from './Clients' const inr = (p: number) => formatINR(p) const RULE_LABEL: Record = { invoice_overdue: 'Overdue invoice', renewal_due: 'Renewal due', amc_expiring: 'AMC expiring', follow_up: 'Follow-up', recurring_generated: 'Recurring invoice', email_bounced: 'Email bounced', } const SENDABLE = new Set(['invoice_overdue', 'renewal_due', 'amc_expiring', 'recurring_generated']) const toneFor = (status: string): 'ok' | 'warn' | 'err' => status === 'failed' ? 'err' : status === 'sent' ? 'ok' : 'warn' /** Dashboard home — today's money + the manual reminder queue (spec §3). */ export function Dashboard() { const nav = useNavigate() const dash = useData(getDashboard, []) const [err, setErr] = useState('') const v = dash.data return (
{dash.error !== undefined && } {err !== '' && {err}} {v === undefined && dash.error === undefined ? : v === undefined ? null : ( <> 0 ? { hint: 'needs attention', hintTone: 'err' as const } : {})} />

Reminder queue

{v.queue.length === 0 ? Nothing waiting — all clear. : ( { const s = v.queue[i]?.status return s === 'failed' ? 'err' : undefined }} rows={v.queue.map((rem) => ({ kind: RULE_LABEL[rem.ruleKind] ?? rem.ruleKind, client: rem.clientName, ref: rem.docNo ?? rem.duePeriod, status: {rem.status}, error: rem.error ?? '—', act: , }))} /> )}
nav(`/documents/${v.overdue[i]!.docId}`)} map={(o) => ({ no: o.docNo ?? '—', client: o.clientName, days: o.daysOverdue, due: inr(o.outstandingPaise) })} />
({ client: d.clientName, on: d.nextRun, amt: d.amountPaise !== null ? inr(d.amountPaise) : 'from price book' })} />
nav(`/clients/${v.renewalsThisMonth[i]!.clientId}`)} map={(r) => ({ client: r.clientName, on: r.nextRenewal })} />
nav(`/clients/${v.followUpsToday[i]!.clientId}`)} map={(f) => ({ client: f.clientName, on: f.followUpOn, notes: f.notes !== '' ? f.notes : '—' })} />
({ client: p.clientName, on: p.receivedOn, mode: p.mode, amt: inr(p.amountPaise) })} />
)}
) } /** A titled table with an empty state — keeps the dashboard body declarative. */ function Section(props: { title: string; empty: string; rows: T[] columns: { key: string; label: string; numeric?: boolean }[] map: (row: T) => Record; onRowClick?: (i: number) => void }) { return ( <>

{props.title}

{props.rows.length === 0 ? {props.empty} : ( props.onRowClick!(i) } : {})} rows={props.rows.map(props.map)} /> )} ) } function QueueActions(props: { rem: Reminder & { docNo: string | null }; onDone: () => void; onError: (m: string) => void }) { const [busy, setBusy] = useState(false) const [mail, setMail] = useState<{ subject: string; body: string } | undefined>() const [loading, setLoading] = useState(false) const run = (p: Promise) => { setBusy(true); props.onError('') p.catch((e: Error) => props.onError(e.message)) // Reload even on failure: the server has already parked the reminder as // 'failed' with its error, and the row must flip live. .then(props.onDone) .finally(() => setBusy(false)) } const preview = () => { if (mail !== undefined) { setMail(undefined); return } // toggle closed setLoading(true); props.onError('') getReminderPreview(props.rem.id) .then(setMail) .catch((e: Error) => props.onError(e.message)) .finally(() => setLoading(false)) } return (
{SENDABLE.has(props.rem.ruleKind) && ( <> )}
{mail !== undefined && (
Subject
{mail.subject}
Body
{mail.body}
)}
) }