# HQ Console — Go-Live Cluster Spec (Due Dates, Import UI, Reminders/Documents Pages, Shell & Profile, Client Support Data) > **STATUS: DELIVERED 2026-07-17.** Successor slice to the delivered D16 funnel > (`2026-07-17-quote-to-close-funnel-design.md`). The Postgres switch (D15) is deliberately > NOT in this slice — it is the dedicated task queued immediately after it, before the > real-data import commit. ## 1. Overview & goals Six workstreams that take the console from "feature-complete" to "safe to go live on": | WS | What | Why now | |---|---|---| | **A** | Invoice **due dates** — dated default terms + per-document override | Overdue chase currently counts from issue date (A7); the client email should say the honest thing | | **B** | **APEX import web UI** (owner-only) | The 300-client cutover is one-shot; CLI-only staging invites a mistake | | **C** | Standalone **Reminders page** | Queue volume outgrows the dashboard card at 300 live clients | | **D** | **Documents list page** + honest pagination on `GET /documents` | Documents are only reachable via client ledger/pipeline today; the endpoint returns ALL rows (rule-8 violation at scale) | | **E** | **Shell & profile**: collapsible sidebar, self-service profile (name + own password), "My Day" dashboard mode, minimal employee extras (phone/title) | Self password change doesn't exist; the old app's most-used surface was the per-person workbench | | **F** | **Client support data** from APEX: AnyDesk/OS/DB-password (encrypted), district + sector, WhatsApp + secretary contacts | AnyDesk was referenced 44× in the old app — support opens it daily; district/sector are the team's main list filters | | **G** | **Editable module→clients roster** + one-click *Convert & send* from list views | Founder call: both directions (client→modules, module→clients) must be editable, and proforma→invoice must be one click from anywhere | | **H** | **Section-per-module quotation PDF** | Founder call: a quote spanning all 5 modules prints each module as its own titled section (name, inclusions, price) with clean page breaks, totals + terms on the last page | Hard rules apply throughout (paise, one billing engine, dated config, immutable documents, portable repos, audited mutations in-txn, no silent caps, strict TS + green suite). ## 2. Assumptions & non-goals - **A1** `billing.payment_terms_days` is a flat operational setting (like `quote.followup.policy`): the *dated* fact is the due date **stamped on each document at issue**, so history never rewrites when terms change. - **A2** Due dates apply to INVOICE only (quotes/proformas are not payable paper). - **A3** DB passwords encrypt with the same AES-256-GCM + `HQ_SECRET_KEY` used for the Gmail refresh token (`crypto.ts`). No `HQ_SECRET_KEY` ⇒ the field refuses to save, loudly. - **A4** Import UI accepts the same two CSVs the CLI does; parsing/verification/commit logic is **reused, not reimplemented** (`import-apex.ts` functions get routes, not a rewrite). - **A5** Roster editing (WS-G) reuses the SAME audited repo calls Client 360 uses (`updateClientModule`, `assignModule`) with the same gating — no second write path, so the two views can never disagree. - **A6** The sectioned layout (WS-H) applies to **QUOTATION** documents only; invoices and proformas keep the compact billing table (they are payment paper, not proposals). It is rendered inside the ONE shared `documentHtml` renderer, so the preview-fidelity guarantee (screen HTML ≡ PDF HTML, byte-identical) is preserved by the existing golden test. - **Non-goals:** Postgres switch (next task); support ticket desk; WhatsApp *sending*; HR fields (DOB/joining/leave/payroll — permanently out per D16); bulk actions; kanban board. ## 3. Data model (all additive; no new tables) | Change | Where | Notes | |---|---|---| | `document.due_date TEXT NULL` | SCHEMA + `migrate()` (PRAGMA-guarded) | A column, not payload JSON — the scan needs portable SQL | | setting `billing.payment_terms_days` | seed `'15'` | Default terms; owner-editable | | `staff_user.phone TEXT NULL`, `staff_user.title TEXT NULL` | additive | Shown on Employees + profile; nothing routes on them | | `client.anydesk TEXT NULL`, `client.os TEXT NULL`, `client.db_password_enc TEXT NULL` | additive | `db_password_enc` stored encrypted; NEVER returned by list/get payloads | | `client.district TEXT NULL`, `client.sector TEXT NULL` | additive | Filterable on the client book; mapped from the APEX CSV at import | | contacts JSON | none | WhatsApp + secretary captured as typed contact rows (`role: 'whatsapp' \| 'secretary'`) — UI labels only | ## 4. API (`[N]` new · `[C]` changed) | Route | Gate | Notes | |---|---|---| | `[C]` POST `/documents` (draft) | auth | accepts optional `dueDate` on INVOICE drafts | | `[C]` `issueDocument` | — | stamps `due_date = doc_date + terms` when null and type is INVOICE (covers convert-and-send + recurring generation for free) | | `[N]` PATCH `/me` | auth (self) | `{ displayName?, phone? }` — audited, role untouchable | | `[N]` POST `/me/password` | auth (self) | requires `currentPassword`; purges the user's OTHER sessions (keeps the current one) | | `[N]` POST `/import/stage` | owner | body: `{ clientsCsv, invoicesCsv }` text; runs existing staging; audited | | `[N]` GET `/import/status` | owner | staging rows + per-row problems + verification report | | `[N]` POST `/import/commit` | owner | refuses while ANY problem exists (existing guard); audited | | `[C]` GET `/documents` | auth | gains `page/pageSize` + returns `total` (rule 8); keeps type/status/clientId filters | | `[C]` PATCH `/clients/:id` | auth | accepts `anydesk/os/district/sector`; `dbPassword` (plaintext in, encrypted at rest) — owner/manager only for `dbPassword` | | `[N]` POST `/clients/:id/reveal-db-password` | owner/manager | returns the decrypted value once; **every reveal writes an audit row** | ## 5. Screens - **`/import`** (ADMIN, owner-only): two CSV pickers → staging tables with per-row problem chips → verification summary → **Commit** enabled only at zero problems, behind a confirm that states "APEX goes read-only after cutover". Re-stage freely. - **`/reminders`** (WORK): FilterChips `Queued · Sent · Failed · Dismissed` + `Mine/All` (managerial), table (kind · client · doc · due · status), Send/Preview/Dismiss reusing the dashboard components, pager. Dashboard keeps its short queue and links here. - **`/documents`** (WORK): type chips (All/QT/PI/INV/CN/RCT) + status filter, mono numbers, amount column, pager; row → `/documents/:id`. - **`/profile`** (user-chip menu): email + role read-only; edit display name/phone; change own password (current password required). - **Sidebar collapse**: footer ⟨/⟩ toggle to an icon rail, remembered in `localStorage`; the <900px hamburger behavior is unchanged. - **"My Day" dashboard**: staff land on *mine* (my pipeline due, my queue, my follow-ups); owner/manager get a Mine/Everyone toggle. Composition of existing scoped queries only. - **Client 360 support card**: AnyDesk (copy button) · OS · district · sector · DB password as `••••` with owner/manager **Reveal** (audited) · contacts list gains WhatsApp/secretary typed rows. - **Composer**: Due date field on INVOICE drafts, auto-filled from terms, editable. - **Invoice PDF**: prints "Due date: " when present. - **Module roster — full edit (WS-G)**: each row gains inline controls for kind, edition, next renewal and lifecycle status (saving through `updateClientModule`, audited); an ✕ unassigns (sets the link inactive); an **Assign client…** type-ahead (existing client search) adds a client to the module via `assignModule`. Same gating as Client 360. - **One-click convert everywhere (WS-G)**: issued proformas show a **Convert & send** quick action on the Documents page rows and Pipeline rows — the same `POST /documents/:id/convert-and-send` behind the same confirm dialog. - **Quotation PDF — section per module (WS-H)**: page 1 = letterhead + client + a one-line summary of the modules quoted; then one titled section per line — module name (+ pack/ edition), its "what's included" block, its line price — with `page-break-inside: avoid` and a break before each section so no module splits mid-section; final page = totals, GST split, terms, jurisdiction, signature. Modules without content print a tight section. ## 6. Due-date mechanics (WS-A detail) - Scan anchor becomes `COALESCE(due_date, doc_date)`; milestone math (d7/d15/d30, F7 highest-only) is unchanged. - `reminderContext`: `ctx.dueDate` when present; template renders *"was due on {dueDate} and is now {N} day(s) overdue"*, falling back to today's *"past due"* wording for legacy invoices without one. - Editing terms changes **future** stamps only — issued documents are immutable (rule 4). ## 7. Security notes (WS-E/F) - `db_password_enc`: AES-256-GCM, never in `GET /clients`/`GET /clients/:id` payloads, reveal-only endpoint, one audit row per reveal (`action='reveal_db_password'`). - `POST /me/password` cannot touch role or other users; rate of change limited by the current-password requirement. - Import routes are owner-only and audited (`stage`/`commit` actions). ## 8. Build order (TDD; each phase independently shippable, suite green before landing) 1. **Due dates** — column + terms setting + issue-stamp + scan COALESCE + template + composer field + PDF line. *(Accept: stamp on all three issue paths; overdue math honors due_date; legacy invoices unaffected.)* 2. **Documents pagination + page** — API page/total, `/documents` screen + nav. *(Accept: total honest; filters compose; 300-row dataset paginates.)* 3. **Reminders page** — `/reminders` screen + nav; dashboard links to it. *(Accept: status chips map to `?status=`; staff see own only.)* 4. **Profile + self password** — `/me` routes + page + user-chip menu. *(Accept: wrong current password rejected; other sessions purged, current kept; role untouchable.)* 5. **Sidebar collapse + My Day** — shell toggle + dashboard mine/everyone. *(Accept: collapse persists; staff default to mine.)* 6. **Client support fields** — columns + encrypted password + reveal + Client 360 card + district/sector filters + contact types. *(Accept: reveal audited; password absent from all list payloads; saving without HQ_SECRET_KEY fails loudly.)* 7. **Import UI** — routes over existing importer + `/import` screen. *(Accept: commit refuses with problems; re-stage idempotent; series seeding preserved.)* 8. **Editable roster + quick convert** — inline edit/assign/unassign on the module roster; Convert & send on Documents + Pipeline rows. *(Accept: roster edits round-trip through the same audited repo fns as Client 360; unassign hides the row and shrinks totals; quick convert = exactly one invoice + one email.)* 9. **Sectioned quotation PDF** — section-per-module layout in `documentHtml` for QUOTATION. *(Accept: 5-module quote renders 5 titled sections + totals page; no section splits across pages; preview-fidelity golden test still passes; INVOICE/PI layout unchanged.)* 10. **Docs refresh** — STATUS/README/CLAUDE/D-number record. **Total: 10 phases.** After this slice: the **Postgres switch**, then the real cutover.