16 KiB
Client Detail redesign — module lifecycle, status line & payments context
Date: 2026-07-19
Area: apps/hq-web/src/pages/ClientDetail.tsx, client-forms.tsx, apps/hq-web/src/app.css,
apps/hq/src/repos-milestones.ts, repos-payments.ts, apps/hq/src/api.ts, seed + migration.
Mockup: scratchpad/client-detail-mockup.html (v2) — approved direction.
Why
The Client Detail page reads as a crowded dump. The per-module "service details" run every field
into one wrapping row (provider · username · password · balance · reseller · alerts …), so you
can't land on what you want. Onboarding is a flat checkbox grid divorced from the module's real
lifecycle. "Assign module" looks like a one-click action when in reality a module is the end of a
process (call → conversation → quotation → start → onboarding → live). And "Payments & plans" lets
you record a payment with no view of what is actually owed.
This redesign restructures the Modules and Payments tabs around how the business actually works, without changing the money engine, the audit rule, or the data model's spine.
Constraints (house rules that bind this work)
- Config over code, dated: per-module onboarding step lists are DB rows resolved by module (and
date), never constants. (
docs/16,docs/07.) - Append-only audit: every mutation (milestone toggle, module-field edit, template change,
go-live auto-status) calls
writeAuditin the same transaction. The Outstanding panel and the per-module document list are reads — they write nothing. - Portable-repo pattern: plain functions over
DB; multi-write ops indb.transaction. - Strict TS, green before landing:
npm run typecheck+npm testclean; money math stays test-locked (this work does not touchcomputeBillor allocation math).
Locked decisions
- Module details → 50/50 sheet. Access (credentials) left half, Details right half; aligned
label → valuerows, read-only, one Edit per group. No*markers, no always-on input boxes. Applies to every module, not just SMS. Reseller + SMS balance are ordinary editable Details fields, not headline columns. - Onboarding → horizontal continuous status line (point-by-point), per-module step templates, owner-editable (config). SMS / RTGS / Mobile App each get their own list.
- Lifecycle: keep the coarse
client_module.statusfield (reports/roster read it); the status line is the detailed view. Ticking the go-live step auto-flips status tolive. "Add module" still starts atquoted. - Migration: swap SMS (and RTGS / Mobile App) projects from the old generic checklist to their new per-module list, preserving mapped ticks (dates carried).
- Per-module Documents: each block lists documents whose line items include that module.
- Payments: add an Outstanding panel (open docs + amount due + advance) and a Settled → document column on each past payment. Record-payment behaviour (auto-allocate oldest invoice first) is unchanged.
1 · Module block — 50/50 sheet, report-not-boxes, single Edit
Rework ServiceDataCard + ModuleFieldsForm (in ClientDetail.tsx) so the read view is the
default and editing is behind a button.
Layout (per module block, inside the existing <details class="mod-section">):
- Header unchanged: module name, coarse status badge, kind badge.
- Body groups, each with a hairline-ruled header + an Edit button:
- Access (left half): Provider, Login (username + Copy), Password (
••••+ managerial Reveal/Copy), Portal (URL field →Open ↗). - Details (right half): the module's
fieldSpecfields plus the free-formdetailsrows, shown aslabel → value(dash when empty). Includes SMS balance / reseller.
- Access (left half): Provider, Login (username + Copy), Password (
- CSS: new
.split(grid1fr 1fr, centre divider, stacks < 720px) and.kv(fixed-width label column so values align). Reuse existing tokens; add toapp.cssnear.mod-section.
Editing: each group's Edit swaps that group's rows for inputs (the existing edit-state
machinery in ServiceDataCard, extended to also carry the fieldSpec values that
ModuleFieldsForm edits today). Save = one audited patchClientModule / setModuleFields
call, then back to read view. Secret fields keep the SecretField reveal/set pattern.
ModuleFieldsForm's always-on inputs + separate "Save fields" button are removed — folded into
the Details group's Edit.
2 · Onboarding — horizontal status line, per-module templates
Replace MilestoneChecklist's checkbox grid with a horizontal track (.track / .node CSS,
already prototyped in the mockup): ordered points = the module's template steps, filled to the
current point, current point ringed, completed dates under nodes, overflow-x:auto for narrow
screens. Clicking a node toggles done / stamps the date (reuse setMilestone); "+ Add step" keeps
appending per-project custom steps (addProjectMilestone). The toggle POST still returns the fresh
list so the block re-renders in place.
Per-module templates (config-over-code):
-
repos-milestones.ts—milestoneTemplate(db, moduleCode?)resolves in order:project.milestone_template:<MODULE_CODE>setting → globalproject.milestone_template→FALLBACK_TEMPLATE.ensureProjectMilestones(db, clientModuleId)looks up the client_module's module code and resolves the module-specific template. -
Seed settings (
seed.ts, additive) for:- SMS: Document collection · DLT registration · Account creation & registration · Installation · Testing · Training · Go-live · Advance payment · Balance payment.
- RTGS: Document collection · Server checkup · Setup & configuration · Installation · Testing · Training · Go-live · Advance payment · Balance payment.
- Mobile App (draft, editable): Requirement & setup · Configuration · Data import · Installation · Testing · Training · Go-live · Advance payment · Balance payment.
Every list ends with Advance payment → Balance payment (payment is part of onboarding for all modules). Ticking a payment step stamps its date on the line — that is the payment date of record for the onboarding view; the money itself is still recorded in the Payments tab / ledger.
-
Owner-editable on the Modules page: a per-module template editor (list of
{key,label}rows). Minimum viable: edit the JSON list per module; every mutation audited. New/changed steps reach existing projects additively viaensureProjectMilestones(which already only adds missing keys).
Go-live → status: when a step with key go_live is ticked, set client_module.status='live'
in the same transaction as the milestone write, with its own audit entry. Unticking does not
revert status (manual only).
3 · Migration — swap seeded modules to their new lists, preserve ticks
One-time, idempotent migration (in migrate() / a scripted pass like d35), for every
client_module whose module is SMS / RTGS / Mobile App:
- Materialise the module's new template (unticked).
- Carry ticks from old generic keys by mapping, preserving
done+done_on:
| old generic key | → SMS | → RTGS / Mobile App |
|---|---|---|
installed |
installation |
installation |
go_live |
go_live |
go_live |
advance_paid |
advance_payment |
advance_payment |
balance_paid |
balance_payment |
balance_payment |
setup_done |
(drop) | setup_configuration (RTGS) |
amc_start |
(drop — AMC tracked separately) | (drop) |
Every module list now ends with advance/balance payment steps, so no payment tick is ever dropped.
- Remove the unmapped old rows so a project shows exactly its new list (no doubling).
Modules with no custom template keep the generic list untouched. The mapping and the per-project result are covered by tests (below). Dropped ticks are logged in the migration summary (no silent loss).
4 · Per-module Documents
Client-side only — the ledger already returns full Docs and each payload.lines[].itemId is
the module id. In each module block add a Documents group listing
ledger.documents.filter(d => d.payload.lines.some(l => l.itemId === cm.moduleId)), newest first:
doc no · type badge · date · amount · status, row click → /documents/:id. Empty → nothing shown
(no empty card).
4a · SMS bulk-credit purchase (a document, within the module)
SMS has two distinct money events: the annual subscription/renewal and ad-hoc bulk SMS credit purchases (top-ups). The bulk purchase is not onboarding and not the subscription — it is its own transaction and must be captured as a document.
- In the SMS module block's Documents group, a "Bulk SMS purchase" action raises a proforma for
a chosen SMS quantity — the existing usage-priced path (
generateModuleRenewalQuote/resolveUsageRatetiered SMS pricing), pre-filled with the SMS line. (This is the same mechanism the SMS-balances top-up already uses; here it lives in the module block.) - The resulting proforma/invoice appears in that block's Documents list (labelled so a bulk top-up reads distinctly from a renewal), flows into the Outstanding panel, and the client's payment settles it — so "what is this payment for?" resolves to a real document.
- Generalisation: the Documents group also offers "New document for this module" (quotation pre-filled with the module line) so a block is where you start the sale — matching the quote → conversation → start flow. Bulk SMS purchase is the SMS-specific flavour of this.
5 · Payments & plans — Outstanding panel + Settled column
Extend the ledger read (no money-math change):
repos-payments.tsclientLedger→ClientLedgergains:outstanding: { docId; docNo; docType; label; outstandingPaise }[]— issued invoices withoutstandingPaise > 0plus open (sent/accepted) proformas, oldest first.labelis derived from the doc's line module names ("SMS annual renewal", "Bulk SMS top-up").- each
paymentgainsallocations: { docNo; amountPaise }[](frompayment_allocation), empty ⇒ shows "advance".
api.tstypes (Ledger,Payment) updated to match.- UI (Payments tab): an Outstanding card above the record-payment row (open docs + amount due + advance-on-account), and a Settled column on the payments table showing the doc no(s) each payment landed on (or "advance"). Record-payment form and its auto-allocation are unchanged.
6 · Active modules summary + assign reframe
- Summary table (Modules tab): dates now live in each block's status line, so simplify the table to Module · Kind · Status · Billed · Settled (drop Installed/Completed/Trained columns); right-align the numeric columns. This is the "tidy alignment / give it life" item.
- Assign form: rename "Assign module" → "Add a module"; copy notes it starts the module at
quotedand seeds its onboarding line. Behaviour otherwise unchanged (module + kind + optional creds). Coarse status stays editable via a small control in the block header (kept for reports).
7 · Adopted refinements
- Status line auto-drives coarse status. A step→status map advances
client_module.statuswhen a step is ticked (never downgrades on untick):installation → installed,training → trained,go_live → live(intermediateordered/installingremain manual). The line becomes the single driver of status — the dropdown is no longer hand-set in normal use. Each auto-advance is audited in the milestone-toggle transaction. This is the full resolution of the point-1 overlap. - Payment steps ↔ real payments. Ticking
advance_payment/balance_paymentopens the record-payment form pre-filled (or links an existing payment); the step'sdone_onmirrors the payment'sreceivedOn, so the line and the ledger never disagree on the payment date. The link is stored on the milestone row (nullablepayment_id). - Document-type labels in the block. Rows in a module's Documents list are tagged
Quote / Renewal / Bulk top-up / Invoice / Credit note, derived from
docType+ the generatorsource(renewal vs bulk-purchase stamp distinct sources) so a bulk SMS top-up reads distinctly from a subscription renewal. - Flag stalled onboarding. Dashboard/MIS surface active modules parked at the same pending step
for more than N days (config setting), reusing
milestoneBoard/projectsPendingMilestone. A read-only tile + drill-down; no new write path.
Files touched
apps/hq-web/src/pages/ClientDetail.tsx— module block (50/50 sheet, status line, documents), payments tab (outstanding + settled), summary table.apps/hq-web/src/pages/client-forms.tsx—AssignModuleFormcopy; any shared status control.apps/hq-web/src/app.css—.split,.kv,.track/.node, outstanding-panel styles.apps/hq-web/src/api.ts—Ledger/Paymenttypes.apps/hq/src/repos-milestones.ts— per-module template resolution; go-live→status.apps/hq/src/repos-payments.ts—clientLedgeroutstanding + allocations.apps/hq/src/seed.ts— seed SMS/RTGS/Mobile App template settings.apps/hq/src/db.ts(migrate()) or ascripts/pass — the milestone migration;project_milestonegains a nullablepayment_id; step→status auto-advance insetMilestone.apps/hq-web/src/pages/Modules.tsx— per-module template editor.apps/hq/src/repos-documents.ts— bulk-SMS-purchase generatorsourcetag (vs renewal).apps/hq-web/src/pages/Dashboard.tsx/Mis.tsx— stalled-onboarding tile + drill-down.
Testing
- Milestone resolution: module-specific template wins; falls back to global then code default.
- Migration: an SMS project with
installed+go_liveticked → new list with Installation + Go-live ticked (dates carried), five new steps unticked, oldsetup_done/amc_startgone; a module with no custom template is untouched. Idempotent on re-run. - Go-live status: ticking
go_livesets statuslive+ writes audit; unticking leaves it. - Ledger:
outstandinglists exactly the open invoices/proformas with correctoutstandingPaise; a fully-paid client shows none; each payment'sallocationsmatchpayment_allocation. Money totals unchanged (existing tests stay green). - Manual: exercise assign → walk the status line → go-live flips status → record payment against an outstanding invoice, in light and dark.
- Bulk SMS purchase: from the SMS block, raise a bulk-credit proforma → it appears in the SMS Documents list and in the Outstanding panel → a payment settles it.
Responsive / mobile + red-team alignment (explicit requirement)
The redesign must be pixel-tight on a full mobile view, not just desktop. Every new surface is verified at phone (~375px), tablet (~768px) and desktop widths, in both themes:
- 50/50 sheet collapses cleanly to one column < 720px; labels never clip their values; no row runs together again on any width.
- Status line scrolls horizontally on narrow screens with no clipped nodes/labels; dots stay aligned to the connector; the current-point ring isn't cut off.
- Outstanding panel, payments table, summary table, per-module Documents — tables scroll inside
their own
overflow-x:auto; the page body never scrolls sideways; numeric columns stay right-aligned withtabular-nums. - Red-team alignment pass: a deliberate adversarial review (via the browser QA / design-review tooling) that hunts for misalignment, overflow, overlap, collapsed spacing, and theme-contrast breaks across the three widths — findings fixed before landing, re-checked after. Alignment is a landing gate, not a nice-to-have.
Risks / open items
- Mobile App step list is a draft — owner can edit post-seed.
- Summary-table column drop removes inline date editing from the table; dates are edited on the
status line instead. Confirm no report depends on the table columns (they read
client_moduledirectly, not the table). - Bulk SMS pricing path: the bulk-purchase proforma relies on the existing usage/tiered SMS
pricing (
resolveUsageRate); the plan must confirm the SMS module is priced as a usage kind and the quantity input maps to that rate. No new money math — reuses the vetted path.