fix(ui): explicit Edit button per module row (inline form), separate from client roster

Clicking a module row shows its clients (roster) — which fought with editing. Add an
Edit button per row (owner) that opens the inline 'Edit module' form directly (name,
SAC, billing kinds, multi-sub, active), stopPropagation so it doesn't also open the
roster; Save/Cancel close it. Row-click still drills into clients + field-spec + quote
+ prices. typecheck + build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat/client-detail-redesign
Thomas Joise 2 days ago
parent 432587c7a1
commit b065332f89

@ -27,6 +27,8 @@ export function Modules() {
const modules = useData(getModules, [])
const [creating, setCreating] = useState(false)
const [selected, setSelected] = useState<Module | undefined>()
const [editingId, setEditingId] = useState<string | undefined>() // inline "Edit module" form
const editingModule = modules.data?.find((m) => m.id === editingId)
// 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>>({})
@ -46,7 +48,7 @@ export function Modules() {
<div className="wf-page">
<PageHeader
title="Modules"
desc="What we sell — each with a dated price book. Click a module to edit its details, fields, quote content and prices."
desc="What we sell — each with a dated price book. Use Edit to change a module's details; click a row to see its clients, fields, quote content and prices."
actions={isOwner
? <Button tone="primary" onClick={() => setCreating(true)}>New module</Button>
: <Badge>read-only (staff)</Badge>}
@ -60,6 +62,7 @@ export function Modules() {
{ key: 'clients', label: 'Clients', numeric: true },
{ key: 'sac', label: 'SAC' }, { key: 'kinds', label: 'Billing kinds' },
{ key: 'multi', label: 'Multi-sub' }, { key: 'active', label: 'Active' },
{ key: 'act', label: '' },
]}
onRowClick={(_row, i) => setSelected(modules.data![i])}
rows={modules.data.map((m) => ({
@ -68,12 +71,18 @@ export function Modules() {
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: isOwner
? <span onClick={(e) => e.stopPropagation()}><Button onClick={() => setEditingId(m.id)}>Edit</Button></span>
: null,
}))}
/>
)}
{editingModule !== undefined && (
<ModuleDetails key={`md-${editingModule.id}`} module={editingModule} isOwner={isOwner}
onSaved={() => modules.reload()} onClose={() => setEditingId(undefined)} />
)}
{selected !== undefined && (
<>
<ModuleDetails key={`md-${selected.id}`} module={selected} isOwner={isOwner} onSaved={() => modules.reload()} />
<ModuleClients key={`mc-${selected.id}`} module={selected} />
<QuoteContent key={selected.id} module={selected} isOwner={isOwner} onSaved={() => modules.reload()} />
<FieldSpecEditor key={`fs-${selected.id}`} module={selected} isOwner={isOwner} onSaved={() => modules.reload()} />
@ -319,7 +328,7 @@ function ModuleClients(props: { module: Module }) {
/** Edit a module's core details (owner only): name, SAC, billing kinds, multi-sub, active.
* Code is the identity and is not editable. Field-spec / quote-content / prices have their
* own panels below. */
function ModuleDetails(props: { module: Module; isOwner: boolean; onSaved: () => void }) {
function ModuleDetails(props: { module: Module; isOwner: boolean; onSaved: () => void; onClose: () => void }) {
const toast = useToast()
const m = props.module
const [name, setName] = useState(m.name)
@ -339,7 +348,7 @@ function ModuleDetails(props: { module: Module; isOwner: boolean; onSaved: () =>
if (kinds.length === 0) { setError('Pick at least one billing kind'); return }
setError(undefined); setBusy(true)
patchModule(m.id, { name: name.trim(), sac: sac.trim(), allowedKinds: kinds, multiSubscription: multi, active })
.then(() => { toast.ok('Module saved'); props.onSaved() })
.then(() => { toast.ok('Module saved'); props.onSaved(); props.onClose() })
.catch((e: Error) => setError(e.message))
.finally(() => setBusy(false))
}
@ -369,6 +378,7 @@ function ModuleDetails(props: { module: Module; isOwner: boolean; onSaved: () =>
</div>
<Toolbar>
<Button tone="primary" disabled={busy} onClick={save}>{busy ? 'Saving…' : 'Save module'}</Button>
<Button onClick={props.onClose}>Cancel</Button>
</Toolbar>
{error !== undefined && <Notice tone="err">{error}</Notice>}
</div>

Loading…
Cancel
Save