import { useState, type ReactNode } from 'react' import { Link, useNavigate } from 'react-router-dom' import { formatINR } from '@sims/domain' import { Badge, DataTable, EmptyState, ErrorState, Notice, Skeleton, StatCard, Stats } from '@sims/ui' import { displayName, getDashboard, getSmsBalances, getStalledProjects, isManagerial } from '../api' import { QueueActions, RULE_LABEL, reminderTone } from '../components/ReminderQueue' import { useData } from './Clients' const inr = (p: number) => formatINR(p) /** 'YYYY-MM-DD' → 'Sunday 19 July 2026' (falls back to the raw string if unparseable). */ const longDate = (iso: string): string => { const d = new Date(`${iso}T00:00:00`) return Number.isNaN(d.getTime()) ? iso : d.toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }) } /** 'YYYY-MM-DD' → '19 Jul' (falls back to the raw string if unparseable). */ const shortDate = (iso: string): string => { const d = new Date(`${iso}T00:00:00`) return Number.isNaN(d.getTime()) ? iso : d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short' }) } /** * Dashboard home — the "cockpit" (D29): a Today hero strip, the four KPI stat * cards, the reminder queue, then a bento grid of the day's sections. Low-colour * on purpose — ink + neutrals + one teal accent, red only for real failures. */ export function Dashboard() { const nav = useNavigate() // My Day (D18 WS-E): owner/manager toggle Mine ↔ Everyone; staff are always // scoped to their own book server-side, so they get no toggle. const managerial = isManagerial() const [mine, setMine] = useState(false) const dash = useData(() => getDashboard(mine), [mine]) const smsLow = useData(getSmsBalances, []) // D28: low-balance alert, seen even without opening the SMS screen const stalled = useData(getStalledProjects, []) // parked onboarding — active projects with no recent progress const [err, setErr] = useState('') const v = dash.data const name = displayName() return (
{dash.error !== undefined && } {err !== '' && {err}} {v === undefined && dash.error === undefined ? : v === undefined ? null : ( <> {/* Today hero — date, greeting, the headline overdue figure, scope + quick actions. */}
{longDate(v.today)}

Welcome back{name !== '' ? `, ${name}` : ''}

Here’s what needs you today — dues to chase, renewals landing, and the reminder queue.

{managerial && (
)}
Overdue
{inr(v.totals.overduePaise)}
{/* SMS low-balance alert (D28) — surfaced here so it's seen without opening the SMS screen. */} {smsLow.data !== undefined && smsLow.data.lowCount > 0 && ( {smsLow.data.lowCount} SMS client{smsLow.data.lowCount === 1 ? '' : 's'} low on balance — review & top up → )} {/* KPIs — the app's existing stat cards (kept minimal; red only when a send failed). */} 0 ? { hint: 'needs attention', hintTone: 'err' as const } : {})} /> {/* Reminder queue — full send/preview/dismiss actions preserved. */}

Reminder queue

{v.queueTotal} View all →
{v.queue.length === 0 ? Nothing waiting — all clear. : ( (v.queue[i]?.status === 'failed' ? 'err' : undefined)} rows={v.queue.map((rem) => ({ kind: rem.label !== '' ? rem.label : RULE_LABEL[rem.ruleKind] ?? rem.ruleKind, client: rem.clientName, ref: rem.docNo ?? rem.duePeriod, status: {rem.status}, error: rem.error ?? '—', act: , }))} /> )} {v.queueTotal > v.queue.length && ( // No silent caps (rule 8): the table is the first page only — say so.

Showing {v.queue.length} of {v.queueTotal} — see the full queue.

)}
{/* Bento — the day's read-only sections. */}
{v.overdue.map((o) => ( nav(`/documents/${o.docId}`)} title={{o.docNo ?? '—'}} sub={o.clientName} right={inr(o.outstandingPaise)} rightSub={`${o.daysOverdue}d overdue`} /> ))} {v.dueThisWeek.map((d) => ( nav(`/clients/${d.clientId}`)} title={d.clientName} sub={`runs ${shortDate(d.nextRun)}`} {...(d.amountPaise !== null ? { right: inr(d.amountPaise) } : { rightSub: 'from price book' })} /> ))} {v.renewalsThisMonth.map((r) => ( nav(`/clients/${r.clientId}`)} title={r.clientName} rightSub={shortDate(r.nextRenewal)} /> ))} {stalled.error !== undefined ? : (stalled.data ?? []).map((p) => ( nav(`/clients/${p.clientId}?tab=modules`)} title={p.clientName} sub={p.moduleName} /> ))} {v.followUpsToday.map((f) => ( nav(`/clients/${f.clientId}`)} title={f.clientName} sub={f.notes !== '' ? f.notes : '—'} /> ))} {v.recentPayments.map((p) => ( nav(`/clients/${p.clientId}`)} title={p.clientName} sub={`${shortDate(p.receivedOn)} · ${p.mode}`} right={inr(p.amountPaise)} /> ))}
)}
) } /** One bento card: an uppercase-labelled header (+ count and optional link) over a * scrollable list, or an empty-state line when there's nothing to show. */ function Card(props: { title: string; count?: number; link?: { to: string; label?: string } empty: string; isEmpty: boolean; children: ReactNode }) { return (

{props.title}

{props.count !== undefined && {props.count}} {props.link !== undefined && {props.link.label ?? 'All'} →}
{props.isEmpty ?
{props.empty}
:
{props.children}
}
) } /** One list row — a title/sub on the left, an optional amount/sub on the right. * Given `onClick`, it renders as a keyboard-operable :
{body}
}