You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sims-hq/docs/superpowers/specs/2026-07-19-client-detail-re...

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 writeAudit in 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 in db.transaction.
  • Strict TS, green before landing: npm run typecheck + npm test clean; money math stays test-locked (this work does not touch computeBill or allocation math).

Locked decisions

  1. Module details → 50/50 sheet. Access (credentials) left half, Details right half; aligned label → value rows, 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.
  2. Onboarding → horizontal continuous status line (point-by-point), per-module step templates, owner-editable (config). SMS / RTGS / Mobile App each get their own list.
  3. Lifecycle: keep the coarse client_module.status field (reports/roster read it); the status line is the detailed view. Ticking the go-live step auto-flips status to live. "Add module" still starts at quoted.
  4. Migration: swap SMS (and RTGS / Mobile App) projects from the old generic checklist to their new per-module list, preserving mapped ticks (dates carried).
  5. Per-module Documents: each block lists documents whose line items include that module.
  6. 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 fieldSpec fields plus the free-form details rows, shown as label → value (dash when empty). Includes SMS balance / reseller.
  • CSS: new .split (grid 1fr 1fr, centre divider, stacks < 720px) and .kv (fixed-width label column so values align). Reuse existing tokens; add to app.css near .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.tsmilestoneTemplate(db, moduleCode?) resolves in order: project.milestone_template:<MODULE_CODE> setting → global project.milestone_templateFALLBACK_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 via ensureProjectMilestones (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:

  1. Materialise the module's new template (unticked).
  2. 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.

  1. 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 / resolveUsageRate tiered 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.ts clientLedgerClientLedger gains:
    • outstanding: { docId; docNo; docType; label; outstandingPaise }[] — issued invoices with outstandingPaise > 0 plus open (sent/accepted) proformas, oldest first. label is derived from the doc's line module names ("SMS annual renewal", "Bulk SMS top-up").
    • each payment gains allocations: { docNo; amountPaise }[] (from payment_allocation), empty ⇒ shows "advance".
  • api.ts types (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 quoted and 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

  1. Status line auto-drives coarse status. A step→status map advances client_module.status when a step is ticked (never downgrades on untick): installation → installed, training → trained, go_live → live (intermediate ordered/installing remain 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.
  2. Payment steps ↔ real payments. Ticking advance_payment / balance_payment opens the record-payment form pre-filled (or links an existing payment); the step's done_on mirrors the payment's receivedOn, so the line and the ledger never disagree on the payment date. The link is stored on the milestone row (nullable payment_id).
  3. 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 generator source (renewal vs bulk-purchase stamp distinct sources) so a bulk SMS top-up reads distinctly from a subscription renewal.
  4. 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.tsxAssignModuleForm copy; any shared status control.
  • apps/hq-web/src/app.css.split, .kv, .track/.node, outstanding-panel styles.
  • apps/hq-web/src/api.tsLedger/Payment types.
  • apps/hq/src/repos-milestones.ts — per-module template resolution; go-live→status.
  • apps/hq/src/repos-payments.tsclientLedger outstanding + allocations.
  • apps/hq/src/seed.ts — seed SMS/RTGS/Mobile App template settings.
  • apps/hq/src/db.ts (migrate()) or a scripts/ pass — the milestone migration; project_milestone gains a nullable payment_id; step→status auto-advance in setMilestone.
  • apps/hq-web/src/pages/Modules.tsx — per-module template editor.
  • apps/hq/src/repos-documents.ts — bulk-SMS-purchase generator source tag (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_live ticked → new list with Installation + Go-live ticked (dates carried), five new steps unticked, old setup_done/amc_start gone; a module with no custom template is untouched. Idempotent on re-run.
  • Go-live status: ticking go_live sets status live + writes audit; unticking leaves it.
  • Ledger: outstanding lists exactly the open invoices/proformas with correct outstandingPaise; a fully-paid client shows none; each payment's allocations match payment_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 with tabular-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_module directly, 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.