# 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 `
`):** - 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.ts` — `milestoneTemplate(db, moduleCode?)` resolves in order: `project.milestone_template:` setting → global `project.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 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. 3. 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 `Doc`s 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` `clientLedger` → `ClientLedger` 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.tsx` — `AssignModuleForm` copy; any shared status control. - `apps/hq-web/src/app.css` — `.split`, `.kv`, `.track`/`.node`, outstanding-panel styles. - `apps/hq-web/src/api.ts` — `Ledger`/`Payment` types. - `apps/hq/src/repos-milestones.ts` — per-module template resolution; go-live→status. - `apps/hq/src/repos-payments.ts` — `clientLedger` 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.