feat(ui): split module screen into Clients | Setup tabs + prominent Add-client

A module's detail screen was one long stack mixing day-to-day (who's on it) with owner
config (details/fields/quote/prices). Now two tabs:
- Clients (default): the roster + install details, with a prominent primary
  '+ Add client to this module' button (was a quiet 'Assign client…'). Tab shows the
  client count.
- Setup (owner only): Details, Fields, Quote content, Price book.
Opening a module always lands on Clients. typecheck + build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat/client-detail-redesign
Thomas Joise 2 days ago
parent 6178e0a6a3
commit 7f740a7e2e

@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react'
import { formatINR, fromRupees } from '@sims/domain'
import {
Badge, Button, ConfirmDialog, DataTable, Dialog, EmptyState, ErrorState, Field, FormField,
FormGrid, Notice, PageHeader, Skeleton, Toolbar, useToast,
FormGrid, Notice, PageHeader, Skeleton, Tabs, Toolbar, useToast,
} from '@sims/ui'
import {
addPrice, assignClientModule, createModule, downloadModuleClientsCsv, getClients,
@ -29,7 +29,9 @@ export function Modules() {
// Master → detail: selecting a module opens its own screen (derive from fresh data so the
// header + fields reflect edits immediately after a reload).
const [selectedId, setSelectedId] = useState<string | undefined>()
const [detailTab, setDetailTab] = useState<'clients' | 'setup'>('clients')
const selected = modules.data?.find((m) => m.id === selectedId)
const openModule = (mid: string) => { setSelectedId(mid); setDetailTab('clients') }
// 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>>({})
@ -59,11 +61,24 @@ export function Modules() {
desc={`SAC ${selected.sac} · ${selected.allowedKinds.map((k) => KIND_LABEL[k]).join(' · ')} · ${selected.active ? 'active' : 'inactive'}`}
actions={<Button onClick={() => setSelectedId(undefined)}> Back to all modules</Button>}
/>
<ModuleDetails key={`md-${selected.id}`} module={selected} isOwner={isOwner} onSaved={() => modules.reload()} />
<Tabs
tabs={[
{ key: 'clients', label: 'Clients', count: clientCounts[selected.id] },
...(isOwner ? [{ key: 'setup', label: 'Setup' }] : []),
]}
active={detailTab}
onChange={(k) => setDetailTab(k as 'clients' | 'setup')}
/>
{detailTab === 'clients' ? (
<ModuleClients key={`mc-${selected.id}`} module={selected} />
) : (
<>
<ModuleDetails key={`md-${selected.id}`} module={selected} isOwner={isOwner} onSaved={() => modules.reload()} />
<FieldSpecEditor key={`fs-${selected.id}`} module={selected} isOwner={isOwner} onSaved={() => modules.reload()} />
<QuoteContent key={selected.id} module={selected} isOwner={isOwner} onSaved={() => modules.reload()} />
<PriceBook module={selected} isOwner={isOwner} />
</>
)}
</div>
)
}
@ -88,14 +103,14 @@ export function Modules() {
{ key: 'multi', label: 'Multi-sub' }, { key: 'active', label: 'Active' },
{ key: 'act', label: '' },
]}
onRowClick={(_row, i) => setSelectedId(modules.data![i]!.id)}
onRowClick={(_row, i) => openModule(modules.data![i]!.id)}
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>,
act: <span onClick={(e) => e.stopPropagation()}><Button onClick={() => setSelectedId(m.id)}>Open </Button></span>,
act: <span onClick={(e) => e.stopPropagation()}><Button onClick={() => openModule(m.id)}>Open </Button></span>,
}))}
/>
)}
@ -182,10 +197,9 @@ function ModuleClients(props: { module: Module }) {
const pages = data !== undefined ? Math.max(1, Math.ceil(data.total / data.pageSize)) : 1
return (
<section>
<h2>Clients on this module</h2>
<Toolbar>
<Button onClick={() => setAssigning((v) => !v)}>
{assigning ? 'Close assign' : 'Assign client…'}
<Button tone="primary" onClick={() => setAssigning((v) => !v)}>
{assigning ? 'Close' : '+ Add client to this module'}
</Button>
{managerial && (
<Button onClick={() => setNotifying((v) => !v)}>

Loading…
Cancel
Save