feat(hq-web): dashboard — skeletons, toned stats, tinted queue, 2-col grid

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat/client-detail-redesign
Thomas Joise 5 days ago
parent 278187631b
commit 00ce1d657e

@ -1,7 +1,7 @@
import { useState, type ReactNode } from 'react' import { useState, type ReactNode } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { formatINR } from '@sims/domain' import { formatINR } from '@sims/domain'
import { Badge, Button, DataTable, EmptyState, Notice, PageHeader, StatCard, Stats } from '@sims/ui' import { Badge, Button, DataTable, EmptyState, ErrorState, Notice, PageHeader, Skeleton, StatCard, Stats } from '@sims/ui'
import { getDashboard, sendReminder, dismissReminder, getReminderPreview, type Reminder } from '../api' import { getDashboard, sendReminder, dismissReminder, getReminderPreview, type Reminder } from '../api'
import { useData } from './Clients' import { useData } from './Clients'
@ -25,14 +25,14 @@ export function Dashboard() {
return ( return (
<div className="wf-page"> <div className="wf-page">
<PageHeader title="Dashboard" desc="Todays money — dues, renewals, follow-ups, and the reminder queue." /> <PageHeader title="Dashboard" desc="Todays money — dues, renewals, follow-ups, and the reminder queue." />
{dash.error !== undefined && <Notice tone="err">{dash.error}</Notice>} {dash.error !== undefined && <ErrorState message={dash.error} onRetry={dash.reload} />}
{err !== '' && <Notice tone="err">{err}</Notice>} {err !== '' && <Notice tone="err">{err}</Notice>}
{v === undefined ? <EmptyState>Loading</EmptyState> : ( {v === undefined && dash.error === undefined ? <Skeleton rows={5} /> : v === undefined ? null : (
<> <>
<Stats> <Stats>
<StatCard label="Overdue" value={inr(v.totals.overduePaise)} hint={`${v.overdue.length} invoice(s)`} /> <StatCard label="Overdue" value={inr(v.totals.overduePaise)} hint={`${v.overdue.length} invoice(s)`} />
<StatCard label="Queued reminders" value={String(v.totals.queued)} /> <StatCard label="Queued reminders" value={String(v.totals.queued)} />
<StatCard label="Failed sends" value={String(v.totals.failed)} /> <StatCard label="Failed sends" value={String(v.totals.failed)} {...(v.totals.failed > 0 ? { hint: 'needs attention', hintTone: 'err' as const } : {})} />
<StatCard label="Follow-ups today" value={String(v.followUpsToday.length)} /> <StatCard label="Follow-ups today" value={String(v.followUpsToday.length)} />
</Stats> </Stats>
@ -44,6 +44,10 @@ export function Dashboard() {
{ key: 'ref', label: 'Reference' }, { key: 'status', label: 'Status' }, { key: 'ref', label: 'Reference' }, { key: 'status', label: 'Status' },
{ key: 'error', label: 'Error' }, { key: 'act', label: '' }, { key: 'error', label: 'Error' }, { key: 'act', label: '' },
]} ]}
rowTone={(i) => {
const s = v.queue[i]?.status
return s === 'failed' ? 'err' : undefined
}}
rows={v.queue.map((rem) => ({ rows={v.queue.map((rem) => ({
kind: RULE_LABEL[rem.ruleKind] ?? rem.ruleKind, kind: RULE_LABEL[rem.ruleKind] ?? rem.ruleKind,
client: rem.clientName, client: rem.clientName,
@ -60,23 +64,33 @@ export function Dashboard() {
onRowClick={(i) => nav(`/documents/${v.overdue[i]!.docId}`)} onRowClick={(i) => nav(`/documents/${v.overdue[i]!.docId}`)}
map={(o) => ({ no: o.docNo ?? '—', client: o.clientName, days: o.daysOverdue, due: inr(o.outstandingPaise) })} /> map={(o) => ({ no: o.docNo ?? '—', client: o.clientName, days: o.daysOverdue, due: inr(o.outstandingPaise) })} />
<Section title="Due this week" empty="No recurring bills due this week." rows={v.dueThisWeek} <div className="dash-grid">
columns={[{ key: 'client', label: 'Client' }, { key: 'on', label: 'Runs on' }, { key: 'amt', label: 'Amount', numeric: true }]} <section>
map={(d) => ({ client: d.clientName, on: d.nextRun, amt: d.amountPaise !== null ? inr(d.amountPaise) : 'from price book' })} /> <Section title="Due this week" empty="No recurring bills due this week." rows={v.dueThisWeek}
columns={[{ key: 'client', label: 'Client' }, { key: 'on', label: 'Runs on' }, { key: 'amt', label: 'Amount', numeric: true }]}
map={(d) => ({ client: d.clientName, on: d.nextRun, amt: d.amountPaise !== null ? inr(d.amountPaise) : 'from price book' })} />
</section>
<Section title="Renewals this month" empty="No renewals this month." rows={v.renewalsThisMonth} <section>
columns={[{ key: 'client', label: 'Client' }, { key: 'on', label: 'Renews on' }]} <Section title="Renewals this month" empty="No renewals this month." rows={v.renewalsThisMonth}
onRowClick={(i) => nav(`/clients/${v.renewalsThisMonth[i]!.clientId}`)} columns={[{ key: 'client', label: 'Client' }, { key: 'on', label: 'Renews on' }]}
map={(r) => ({ client: r.clientName, on: r.nextRenewal })} /> onRowClick={(i) => nav(`/clients/${v.renewalsThisMonth[i]!.clientId}`)}
map={(r) => ({ client: r.clientName, on: r.nextRenewal })} />
</section>
<Section title="Follow-ups today" empty="No follow-ups due." rows={v.followUpsToday} <section>
columns={[{ key: 'client', label: 'Client' }, { key: 'on', label: 'Due' }, { key: 'notes', label: 'Notes' }]} <Section title="Follow-ups today" empty="No follow-ups due." rows={v.followUpsToday}
onRowClick={(i) => nav(`/clients/${v.followUpsToday[i]!.clientId}`)} columns={[{ key: 'client', label: 'Client' }, { key: 'on', label: 'Due' }, { key: 'notes', label: 'Notes' }]}
map={(f) => ({ client: f.clientName, on: f.followUpOn, notes: f.notes !== '' ? f.notes : '—' })} /> onRowClick={(i) => nav(`/clients/${v.followUpsToday[i]!.clientId}`)}
map={(f) => ({ client: f.clientName, on: f.followUpOn, notes: f.notes !== '' ? f.notes : '—' })} />
</section>
<Section title="Recent payments" empty="No payments recorded yet." rows={v.recentPayments} <section>
columns={[{ key: 'client', label: 'Client' }, { key: 'on', label: 'Received' }, { key: 'mode', label: 'Mode' }, { key: 'amt', label: 'Amount', numeric: true }]} <Section title="Recent payments" empty="No payments recorded yet." rows={v.recentPayments}
map={(p) => ({ client: p.clientName, on: p.receivedOn, mode: p.mode, amt: inr(p.amountPaise) })} /> columns={[{ key: 'client', label: 'Client' }, { key: 'on', label: 'Received' }, { key: 'mode', label: 'Mode' }, { key: 'amt', label: 'Amount', numeric: true }]}
map={(p) => ({ client: p.clientName, on: p.receivedOn, mode: p.mode, amt: inr(p.amountPaise) })} />
</section>
</div>
</> </>
)} )}
</div> </div>

@ -93,12 +93,12 @@ export function DataTable(props: {
) )
} }
export function StatCard(props: { label: string; value: ReactNode; hint?: string }) { export function StatCard(props: { label: string; value: ReactNode; hint?: string; hintTone?: 'ok' | 'warn' | 'err' }) {
return ( return (
<div className="wf-stat"> <div className="wf-stat">
<div className="label">{props.label}</div> <div className="label">{props.label}</div>
<div className="value num">{props.value}</div> <div className="value num">{props.value}</div>
{props.hint !== undefined && <div className="hint">{props.hint}</div>} {props.hint !== undefined && <div className={`hint${props.hintTone !== undefined ? ` ${props.hintTone}` : ''}`}>{props.hint}</div>}
</div> </div>
) )
} }

Loading…
Cancel
Save