feat(d30): clients-per-module count on Modules page + comment fix

- Modules page now shows a "Clients" column — how many clients sit on
  each module — via one lightweight best-effort roster count per module
  (cell shows '…' until it loads, never blocks the table). (Modules.tsx)
- Fix a stale comment: the reminder-queue badge now lives on the Reminders
  nav item, not Dashboard. (Layout.tsx)

All sidebar count badges kept as-is (Reminders / Pipeline / Tickets /
Renewals / SMS / Clients / Documents-drafts / Modules). The dead
@fontsource inter+jetbrains deps were already dropped from package.json.

Typecheck clean; web build green. Verified live: Modules "Clients" column
renders (seed AMC module -> 0), badges intact, zero console errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat/client-detail-redesign
Thomas Joise 3 days ago
parent 3e175b8da1
commit 23089a9b1f

@ -45,7 +45,7 @@ export function Layout() {
fetch('/api/health').then((r) => setHealthy(r.ok)).catch(() => setHealthy(false))
}, [])
// Reminder-queue badge (Dashboard nav item) — refreshed on every route change.
// Reminder-queue badge (Reminders nav item) — refreshed on every route change.
// Uses the honest scoped `total` (never rows.length): staff badge counts match
// their own queue, and pagination cannot under-count.
useEffect(() => {

@ -27,6 +27,20 @@ export function Modules() {
const modules = useData(getModules, [])
const [creating, setCreating] = useState(false)
const [selected, setSelected] = useState<Module | undefined>()
// How many clients sit on each module — one lightweight roster count per module,
// best-effort so a slow/failed one never blocks the table (cell shows '…' until then).
const [clientCounts, setClientCounts] = useState<Record<string, number>>({})
useEffect(() => {
const ms = modules.data
if (ms === undefined) return
let live = true
ms.forEach((m) => {
getModuleClients(m.id, 1)
.then((r) => { if (live) setClientCounts((c) => ({ ...c, [m.id]: r.total })) })
.catch(() => { /* best-effort */ })
})
return () => { live = false }
}, [modules.data])
return (
<div className="wf-page">
@ -43,12 +57,14 @@ export function Modules() {
<DataTable
columns={[
{ key: 'code', label: 'Code', mono: true }, { key: 'name', label: 'Name' },
{ key: 'clients', label: 'Clients', numeric: true },
{ key: 'sac', label: 'SAC' }, { key: 'kinds', label: 'Billing kinds' },
{ key: 'multi', label: 'Multi-sub' }, { key: 'active', label: 'Active' },
]}
onRowClick={(_row, i) => setSelected(modules.data![i])}
rows={modules.data.map((m) => ({
code: m.code, name: m.name, sac: m.sac,
clients: clientCounts[m.id] !== undefined ? clientCounts[m.id] : '…',
kinds: m.allowedKinds.map((k) => KIND_LABEL[k]).join(' · '),
multi: m.multiSubscription ? <Badge tone="accent">yes</Badge> : '—',
active: m.active ? <Badge tone="ok">active</Badge> : <Badge>inactive</Badge>,

Loading…
Cancel
Save