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.
258 lines
16 KiB
Markdown
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 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.
|