From 8485e69ff01e3bf40eaf8f3c98cf64e5809ca910 Mon Sep 17 00:00:00 2001 From: Thomas Joise Date: Sun, 19 Jul 2026 18:21:31 +0530 Subject: [PATCH] =?UTF-8?q?fix(ui):=20module=20page=20as=20master=E2=86=92?= =?UTF-8?q?detail=20screen=20(clear=20which=20module=20you're=20in)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Selecting a module used to dump a stack of panels below the list with no indication of which module — confusing (founder feedback on 'Fields — Mobile App'). Now clicking a module opens its OWN screen: a breadcrumb + big header naming the module (name · code · SAC · kinds), a Back button, then clearly-labelled sections — Details (editable), Clients on this module, Fields (plain-language: 'the details you record for each client on this module'), Quote content, Price book. The list hides while you're in a module. Row action is now 'Open →'. typecheck + build clean. Co-Authored-By: Claude Fable 5 --- apps/hq-web/src/pages/Modules.tsx | 62 ++++++++++++++++++------------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/apps/hq-web/src/pages/Modules.tsx b/apps/hq-web/src/pages/Modules.tsx index e8c1cd6..c8d6724 100644 --- a/apps/hq-web/src/pages/Modules.tsx +++ b/apps/hq-web/src/pages/Modules.tsx @@ -26,9 +26,10 @@ export function Modules() { const isOwner = role() === 'owner' const modules = useData(getModules, []) const [creating, setCreating] = useState(false) - const [selected, setSelected] = useState() - const [editingId, setEditingId] = useState() // inline "Edit module" form - const editingModule = modules.data?.find((m) => m.id === editingId) + // 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() + const selected = modules.data?.find((m) => m.id === selectedId) // 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>({}) @@ -44,11 +45,34 @@ export function Modules() { return () => { live = false } }, [modules.data]) + // --- Detail screen: one module at a time, clearly headed, with a Back button. --- + if (selected !== undefined) { + return ( +
+
+ + /{selected.code} +
+ KIND_LABEL[k]).join(' · ')} · ${selected.active ? 'active' : 'inactive'}`} + actions={} + /> + modules.reload()} /> + + modules.reload()} /> + modules.reload()} /> + +
+ ) + } + return (
setCreating(true)}>New module : read-only (staff)} @@ -64,31 +88,17 @@ export function Modules() { { key: 'multi', label: 'Multi-sub' }, { key: 'active', label: 'Active' }, { key: 'act', label: '' }, ]} - onRowClick={(_row, i) => setSelected(modules.data![i])} + onRowClick={(_row, i) => setSelectedId(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 ? yes : '—', active: m.active ? active : inactive, - act: isOwner - ? e.stopPropagation()}> - : null, + act: e.stopPropagation()}>, }))} /> )} - {editingModule !== undefined && ( - modules.reload()} onClose={() => setEditingId(undefined)} /> - )} - {selected !== undefined && ( - <> - - modules.reload()} /> - modules.reload()} /> - - - )} {isOwner && ( void; onClose: () => 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) @@ -348,7 +358,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(); props.onClose() }) + .then(() => { toast.ok('Module saved'); props.onSaved() }) .catch((e: Error) => setError(e.message)) .finally(() => setBusy(false)) } @@ -356,7 +366,7 @@ function ModuleDetails(props: { module: Module; isOwner: boolean; onSaved: () => if (!props.isOwner) return null return ( <> -

Edit module — {m.code}

+

Details

setName(e.target.value)} /> @@ -378,7 +388,7 @@ function ModuleDetails(props: { module: Module; isOwner: boolean; onSaved: () =>
- + {props.onClose !== undefined && } {error !== undefined && {error}}
@@ -402,7 +412,7 @@ function QuoteContent(props: { module: Module; isOwner: boolean; onSaved: () => return ( <> -

Quote content — {props.module.name}

+

Quote content — the “what’s included” lines printed on quotes

{props.isOwner ? (
@@ -483,7 +493,7 @@ function FieldSpecEditor(props: { module: Module; isOwner: boolean; onSaved: () return ( <> -

Fields — {props.module.name}

+

Fields — the details you record for each client on this module

{!props.isOwner ? ( props.module.fieldSpec.length === 0 ? No fields defined. : (