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...

258 lines
16 KiB
Markdown

# 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.ts` `milestoneTemplate(db, moduleCode?)` resolves in order:
`project.milestone_template:<MODULE_CODE>` 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 stepstatus 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-livestatus.
- `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`; stepstatus 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.