# Client Detail Redesign — Frontend Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Rebuild the Modules and Payments tabs of Client Detail — module details as a 50/50 read-only report with a single Edit per group, onboarding as a horizontal per-module status line, per-module Documents (incl. a bulk-SMS-purchase action), and a Payments Outstanding panel — plus a per-module template editor and a stalled-onboarding tile, verified pixel-tight on mobile via a red-team alignment pass. **Architecture:** React + Vite; all data via the typed `api.ts` calls. Styling is token-based CSS in `apps/hq-web/src/app.css` (Warm/Slate/Graphite/Zinc neutrals × 7 accents × light/dark) — new surfaces use existing tokens only. This plan consumes the backend from `2026-07-19-client-detail-redesign-backend.md` (ledger `outstanding`/`allocations`, per-module templates, `setMilestone(paymentId)`, `generateBulkSmsPurchaseQuote`, `stalledProjects`). **Tech Stack:** React 18, TypeScript (strict), Vite, `@sims/ui`. ## Global Constraints - **Prerequisite:** the backend plan is merged (its API shapes exist). If not, stop and do it first. - No new colour constants — style through CSS custom properties (`--accent`, `--border`, `--ok/--warn/--err`, `--bg-raised`, `--text`, `--text-dim`, `--radius`, `--mono`). Must work in **light and dark** and all four neutrals. - The page body never scrolls sideways; wide content scrolls inside its own `overflow-x:auto`. Numeric columns use `font-variant-numeric: tabular-nums`. - No frontend unit-test harness exists (tests are backend vitest). Each task's verification is: `npm run typecheck` clean, `npm run build --workspace @sims/hq-web` clean, and a **manual check in the running app** (backend on :5182 serving the built UI, or Vite dev on :5183). The final task is the responsive + red-team gate. - Run from repo root. Reference the approved mockup `scratchpad/client-detail-mockup.html` for exact layout/CSS. --- ## File Structure - `apps/hq-web/src/app.css` — MODIFY: `.split`/`.kv`, `.track`/`.node`, `.out` outstanding-panel, doc-label pill classes. - `apps/hq-web/src/pages/ClientDetail.tsx` — MODIFY: `ServiceDataCard` (50/50 + single edit), replace `MilestoneChecklist` with `StatusLine`, add `ModuleDocuments`, Payments tab (`OutstandingPanel` + Settled column), summary-table trim. - `apps/hq-web/src/pages/client-forms.tsx` — MODIFY: `AssignModuleForm` copy → "Add a module". - `apps/hq-web/src/api.ts` — MODIFY: add `generateBulkSmsPurchase`, `getStalledProjects`, `getModuleTemplate`/`setModuleTemplate`, and thread `paymentId` into `setMilestone`. - `apps/hq-web/src/pages/Modules.tsx` — MODIFY: per-module onboarding template editor. - `apps/hq-web/src/pages/Dashboard.tsx` — MODIFY: stalled-onboarding tile. Each new sub-component (`StatusLine`, `ModuleDocuments`, `OutstandingPanel`) is a local function component in `ClientDetail.tsx` next to the code it replaces (that file already houses `ServiceDataCard`, `MilestoneChecklist`, etc.). Keep them focused; if `ClientDetail.tsx` grows past readability, extract the module-block components into `pages/client-module-block.tsx` as a follow-up (note it, don't force it). --- ## Task 1: CSS foundation (port the mockup styles) **Files:** - Modify: `apps/hq-web/src/app.css` (append a new section) **Interfaces:** - Produces: classes `.split`, `.kv .k/.val`, `.track`/`.node` (+ `.done/.now`), `.out` (+ `.oh/.orow/.ofoot`), `.doc-pill` used by later tasks. These MUST match the class names the JSX in Tasks 2–5 references. - [ ] **Step 1: Append the styles** Add to `apps/hq-web/src/app.css` (values ported from `scratchpad/client-detail-mockup.html`, swapping the mockup's literal colours for tokens): ```css /* ---- Client Detail redesign (D-CDR) ---- */ /* 50/50 service sheet */ .cdr-split { display: grid; grid-template-columns: 1fr 1fr; gap: 0 30px; } .cdr-split > .col { padding: 13px 0; min-width: 0; } .cdr-split > .col + .col { border-left: 1px solid var(--border); padding-left: 30px; } .cdr-kv { display: flex; align-items: baseline; gap: 10px; padding: 6px 0; min-width: 0; } .cdr-kv + .cdr-kv { border-top: 1px dashed var(--border); } .cdr-kv > .k { flex: none; width: 120px; font-size: 10.5px; text-transform: uppercase; letter-spacing: .5px; color: var(--text-dim); font-weight: 600; padding-top: 2px; } .cdr-kv > .v { flex: 1; min-width: 0; display: flex; align-items: center; gap: 7px; flex-wrap: wrap; } .cdr-kv > .v .txt { overflow: hidden; text-overflow: ellipsis; } @media (max-width: 720px) { .cdr-split { grid-template-columns: 1fr; } .cdr-split > .col + .col { border-left: none; padding-left: 0; border-top: 1px solid var(--border); } } /* horizontal onboarding status line */ .cdr-track-wrap { overflow-x: auto; padding: 10px 2px 4px; } .cdr-track { display: flex; min-width: min-content; } .cdr-node { flex: 1 0 112px; display: flex; flex-direction: column; align-items: center; text-align: center; position: relative; padding: 0 4px; } .cdr-node .bar { position: absolute; top: 11px; right: 50%; width: 100%; height: 2px; background: var(--border); } .cdr-node.done .bar, .cdr-node.now .bar { background: var(--accent); } .cdr-node:first-child .bar { display: none; } .cdr-node .dot { position: relative; z-index: 1; width: 22px; height: 22px; border-radius: 50%; border: 2px solid var(--border-strong); background: var(--bg-raised); display: grid; place-items: center; font-size: 11px; color: transparent; cursor: pointer; } .cdr-node.done .dot { background: var(--ok); border-color: var(--ok); color: #fff; } .cdr-node.now .dot { border-color: var(--accent); color: var(--accent); box-shadow: 0 0 0 4px var(--accent-soft); } .cdr-node .nm { font-size: 11px; margin-top: 7px; max-width: 104px; line-height: 1.35; } .cdr-node.done .nm { color: var(--text-dim); } .cdr-node.now .nm { font-weight: 600; } .cdr-node .dt { font-size: 10px; color: var(--text-dim); font-family: var(--mono); margin-top: 3px; } /* outstanding panel */ .cdr-out { border: 1px solid color-mix(in srgb, var(--warn) 30%, var(--border)); border-radius: var(--radius); overflow: hidden; } .cdr-out .oh { display: flex; align-items: center; gap: 8px; padding: 10px 14px; border-bottom: 1px solid var(--border); } .cdr-out .orow { display: flex; align-items: center; gap: 12px; padding: 9px 14px; border-bottom: 1px solid var(--border); font-size: 12.5px; } .cdr-out .orow:last-child { border-bottom: none; } .cdr-out .orow .due { margin-left: auto; font-family: var(--mono); font-weight: 600; } .cdr-out .ofoot { display: flex; align-items: center; gap: 10px; padding: 10px 14px; background: var(--bg-inset); } /* document-type pill */ .doc-pill { font-size: 10.5px; font-weight: 600; letter-spacing: .3px; border-radius: 999px; padding: 1px 8px; border: 1px solid var(--border); color: var(--text-dim); background: var(--bg-inset); } ``` - [ ] **Step 2: Verify build** Run: `npm run build --workspace @sims/hq-web` Expected: build succeeds (CSS is valid, no selector errors). - [ ] **Step 3: Commit** ```bash git add apps/hq-web/src/app.css git commit -m "style(client-detail): CSS for 50/50 sheet, status line, outstanding panel" ``` --- ## Task 2: Module details → 50/50 report with single Edit per group **Files:** - Modify: `apps/hq-web/src/pages/ClientDetail.tsx` — `ServiceDataCard` (lines ~961–1103) and fold `ModuleFieldsForm` (lines ~1112–1209) presentation into it. **Interfaces:** - Consumes: existing `patchClientModule`, `setModuleFields`, `revealModulePassword`, `SecretField`, `cm.details`, `cm.fieldValues`, `props.fieldSpec`. - Produces: read-only 50/50 report; each group (`Access`, `Details`) has one **Edit** toggling that group's inputs; save = one audited call. - [ ] **Step 1: Replace the read view of `ServiceDataCard`** Rewrite the returned read view (the non-editing `return (...)` at ~1066) as the 50/50 report. Keep the existing edit-state machinery; change only what renders. The read view: ```tsx return (
` (audited).
**Interfaces:**
- Produces: `getModuleTemplate(code): Promise<{ key: string; label: string }[]>`, `setModuleTemplate(code, steps): Promise`.
- [ ] **Step 1: Backend routes**
In `apps/hq/src/api.ts` add owner-only routes:
- `GET /modules/:code/onboarding-template` → `milestoneTemplate(db, code)`.
- `PUT /modules/:code/onboarding-template` → validate `steps: {key,label}[]` (non-empty labels; unique keys; slugify missing keys), write the `setting` row `project.milestone_template:`, audited.
- [ ] **Step 2: Web calls + editor UI**
Add the two api calls, then in `Modules.tsx` add an "Onboarding steps" editor per module row (owner-only): a list of `{label}` rows with add/remove/reorder, Save calls `setModuleTemplate`. Keys auto-derive from labels on save (server slugifies). Note in the UI that changes reach existing projects additively (new steps appear; existing ticks untouched) and that a full re-order/rename for existing projects needs the migration script.
- [ ] **Step 3: Verify + commit**
Run: `npm run typecheck && npm test --workspace @sims/hq && npm run build --workspace @sims/hq-web`. In-app (owner): edit SMS steps, save, reload a client's SMS block → new step appears.
```bash
git add apps/hq-web/src/pages/Modules.tsx apps/hq-web/src/api.ts apps/hq/src/api.ts
git commit -m "feat(modules): owner editor for per-module onboarding templates"
```
---
## Task 8: Stalled-onboarding dashboard tile
**Files:**
- Modify: `apps/hq-web/src/api.ts` — `getStalledProjects()`.
- Modify: `apps/hq-web/src/pages/Dashboard.tsx` — a tile listing stalled projects.
**Interfaces:**
- Consumes: `GET /projects/stalled` (backend Task 8) → `{ clientModuleId, clientId, clientName, moduleCode, moduleName }[]`.
- [ ] **Step 1: Web call + tile**
Add `getStalledProjects`. In `Dashboard.tsx`, add a read-only card "Onboarding stalled" listing each stalled project as a link to `/clients/:clientId?tab=modules`, matching the existing dashboard card pattern (`.dash-card`/`.dash-row`). Empty → hide the card or show a positive empty state.
- [ ] **Step 2: Verify + commit**
Run: `npm run typecheck && npm run build --workspace @sims/hq-web`. In-app: a project parked past the threshold appears; clicking opens its Modules tab.
```bash
git add apps/hq-web/src/api.ts apps/hq-web/src/pages/Dashboard.tsx
git commit -m "feat(dashboard): stalled-onboarding tile"
```
---
## Task 9: Responsive + red-team alignment pass (landing gate)
**Files:** any of the above, as fixes require.
This task does not land new features — it hardens what Tasks 1–8 built. **Do not skip.**
- [ ] **Step 1: Build + serve the real app**
```bash
npm run build --workspace @sims/hq
node apps/hq/dist/server.cjs # serves the built UI on :5182
```
Seed demo data if needed (`apps/hq/scripts/demo-seed.ts`) so a client has SMS + RTGS + Mobile App, several documents, payments, and mid-onboarding milestones.
- [ ] **Step 2: Adversarial visual sweep (use the QA/design tooling)**
Use the browser QA tooling (the `browse` / `qa` / `design-review` skills, or chrome-devtools MCP) to load `/clients/:id?tab=modules` and `?tab=payments` at **375px, 768px, and desktop**, in **light and dark** and at least two neutral palettes (Warm + Graphite). Actively hunt for:
- 50/50 sheet: collapses to one column < 720px; labels never clip values; nothing runs together on any width.
- Status line: scrolls horizontally with no clipped nodes/labels; dots stay on the connector; the "now" ring isn't cut at the edge.
- Outstanding panel / payments table / summary table / documents: each scrolls inside its own container; **page body never scrolls sideways**; numeric columns right-aligned with tabular figures.
- No overlap, no collapsed/again-crowded rows, no theme-contrast failures (labels/badges legible on every ground).
- [ ] **Step 3: Fix every finding, then re-check**
Fix in the relevant file, rebuild, and re-run Step 2 until the sweep is clean at all three widths × both themes. Record the widths/themes checked in the commit body.
- [ ] **Step 4: Full green + commit**
```bash
npm run typecheck && npm test --workspace @sims/hq && npm run build --workspace @sims/hq-web
git add -A
git commit -m "fix(client-detail): responsive + red-team alignment pass across widths and themes"
```
---
## Frontend plan self-review
- **Spec coverage:** §1 50/50 report → F2; §2 status line → F3; §4 per-module docs → F4; §4a bulk SMS → F4; §5 payments outstanding/settled → F5; §6 summary+assign → F6; §2 template editor → F7; §7.4 stalled tile → F8; responsive+red-team gate → F9; CSS foundation → F1. §7.1 (status auto-drive) and §7.2 (payment link) are backend-driven; F3 consumes them and adds the payment picker UI.
- **Type consistency:** class names in F1 (`cdr-split`, `cdr-kv`, `cdr-track`/`cdr-node`, `cdr-out`, `doc-pill`) match the JSX in F2–F5; `generateBulkSmsPurchase`, `getStalledProjects`, `getModuleTemplate`/`setModuleTemplate`, `setMilestone(..., paymentId?)`, `ledger.outstanding`, `payment.allocations` match the backend plan's exports.
- **Placeholder scan:** F3 Step 3 and F7 Step 2 describe interactive UI at component level with the exact calls to make; the payment-picker dialog and the template-editor rows follow the file's existing dialog/list idioms (e.g. `BranchList`, `PlanDialog`) — the implementer mirrors those concrete patterns rather than inventing new ones.
- **Ordering:** F1 (CSS) first so F2–F5 render correctly; F9 last as the gate. Each task is independently reviewable and ends green.