|
|
# HQ Console — Go-Live Cluster Spec (Due Dates, Import UI, Reminders/Documents Pages, Shell & Profile, Client Support Data)
|
|
|
|
|
|
> **STATUS: DELIVERED 2026-07-17; reviewed same day — all 8 review findings fixed
|
|
|
> (`fix(review)` commit), suite at 345 tests.** 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.
|
|
|
>
|
|
|
> Post-review deltas vs the text below: the clients CSV also carries optional
|
|
|
> `anydesk,os,district,sector` columns (§3's "mapped from the APEX CSV" promise now holds —
|
|
|
> A4's "same two CSVs" means same files, with four optional extra headers), and the
|
|
|
> district/sector book filters match partial text (LIKE) rather than exact values.
|
|
|
|
|
|
## 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: <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.
|