# HQ Console — Go-Live Cluster Spec (Due Dates, Import UI, Reminders/Documents Pages, Shell & Profile, Client Support Data) > **STATUS: DRAFT — awaiting founder review.** 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 | 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). - **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. ## 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. **Docs refresh** — STATUS/README/CLAUDE/D-number record. **Total: 8 phases.** After this slice: the **Postgres switch**, then the real cutover.