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.
8.5 KiB
8.5 KiB
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_daysis a flat operational setting (likequote.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_KEYused for the Gmail refresh token (crypto.ts). NoHQ_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.tsfunctions 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): FilterChipsQueued · 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.dueDatewhen 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 inGET /clients/GET /clients/:idpayloads, reveal-only endpoint, one audit row per reveal (action='reveal_db_password').POST /me/passwordcannot touch role or other users; rate of change limited by the current-password requirement.- Import routes are owner-only and audited (
stage/commitactions).
8. Build order (TDD; each phase independently shippable, suite green before landing)
- 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.)
- Documents pagination + page — API page/total,
/documentsscreen + nav. (Accept: total honest; filters compose; 300-row dataset paginates.) - Reminders page —
/remindersscreen + nav; dashboard links to it. (Accept: status chips map to?status=; staff see own only.) - Profile + self password —
/meroutes + page + user-chip menu. (Accept: wrong current password rejected; other sessions purged, current kept; role untouchable.) - Sidebar collapse + My Day — shell toggle + dashboard mine/everyone. (Accept: collapse persists; staff default to mine.)
- 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.)
- Import UI — routes over existing importer +
/importscreen. (Accept: commit refuses with problems; re-stage idempotent; series seeding preserved.) - Docs refresh — STATUS/README/CLAUDE/D-number record.
Total: 8 phases. After this slice: the Postgres switch, then the real cutover.