docs: record quote-to-close funnel slice (D16)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat/client-detail-redesign
Thomas Joise 5 days ago
parent 400abc224f
commit 5a1c84f1de

@ -17,7 +17,7 @@ dashboard, an append-only audit log, an APEX importer, and a scheduler.
| Part | Where | Notes | | Part | Where | Notes |
|---|---|---| |---|---|---|
| Backend | `apps/hq/src` | Express over `better-sqlite3`, DB at `data/hq.db` (WAL) | | Backend | `apps/hq/src` | Express over `better-sqlite3`, DB at `data/hq.db` (WAL) |
| Frontend | `apps/hq-web/src` | React + Vite; pages Dashboard / Clients / ClientDetail / Documents / Modules / Reports | | Frontend | `apps/hq-web/src` | React + Vite; pages Dashboard / Pipeline / Clients / ClientDetail / Modules / Reports / NewDocument / DocumentView / Employees (owner-only) / DocumentTemplate (owner-only) |
| Repos | `apps/hq/src/repos-*.ts` | one file per domain; plain functions over a `DB` handle | | Repos | `apps/hq/src/repos-*.ts` | one file per domain; plain functions over a `DB` handle |
| Schema | `apps/hq/src/db.ts` + `apps/hq/schema.sql` | `CREATE TABLE IF NOT EXISTS` + additive `migrate()` | | Schema | `apps/hq/src/db.ts` + `apps/hq/schema.sql` | `CREATE TABLE IF NOT EXISTS` + additive `migrate()` |
| Shared packages | `packages/*` | trimmed forks — see below | | Shared packages | `packages/*` | trimmed forks — see below |
@ -37,13 +37,13 @@ distilled to what applies to HQ. A change that breaks one does not land.
|---|---|---| |---|---|---|
| Money is integer paise | Every amount is `Paise` (integer). Fractional-rupee floats never enter the domain; convert at the edge with `fromRupees`, format with `formatINR`. Every rounding step is explicit. | all `*_paise` columns; `@sims/domain/money` | | Money is integer paise | Every amount is `Paise` (integer). Fractional-rupee floats never enter the domain; convert at the edge with `fromRupees`, format with `formatINR`. Every rounding step is explicit. | all `*_paise` columns; `@sims/domain/money` |
| One billing engine, re-verified | GST is computed **only** through `@sims/billing-engine` `computeBill` — save and preview run the same path so they cannot drift. The server recomputes to the paisa; a mismatch is rejected. | `repos-documents.ts` | | One billing engine, re-verified | GST is computed **only** through `@sims/billing-engine` `computeBill` — save and preview run the same path so they cannot drift. The server recomputes to the paisa; a mismatch is rejected. | `repos-documents.ts` |
| Config over code, dated | Anything that varies — prices, tax rates/slabs, settings, doc-series prefixes, SAC codes, templates, message text — is a **DB row resolved by business date**, never a constant in code. A price or rate change is a new dated row, not a release. | `module_price_book`, `tax_class` (`effective_from`/`_to`), `doc_series`, `setting`, `template`, `reminder-templates` | | Config over code, dated | Anything that varies — prices, tax rates/slabs, settings, doc-series prefixes, SAC codes, templates, message text — is a **DB row resolved by business date**, never a constant in code. A price or rate change is a new dated row, not a release. | `module_price_book`, `tax_class` (`effective_from`/`_to`), `doc_series`, `setting`, `template`, `reminder_schedule` (reminder cadence + follow-up text, resolved by `resolveSchedule`), `reminder-templates` |
| Documents immutable once issued | Issuing assigns the number; after that the document is never edited or deleted. Corrections are **credit notes** (own per-FY series, links the invoice via `ref_doc_id`, CGST Act s.34). Cancel keeps the number consumed; only issued, unpaid, unallocated docs may cancel. QT→PI→INV convert by carrying lines forward into a new draft. | `repos-documents.ts``issueDocument`, `raiseCreditNote`, `cancelDocument`, `convertDocument` | | Documents immutable once issued | Issuing assigns the number; after that the document is never edited or deleted. Corrections are **credit notes** (own per-FY series, links the invoice via `ref_doc_id`, CGST Act s.34). Cancel keeps the number consumed; only issued, unpaid, unallocated docs may cancel. QT→PI→INV convert by carrying lines forward into a new draft. | `repos-documents.ts``issueDocument`, `raiseCreditNote`, `cancelDocument`, `convertDocument` |
| Portable-repo pattern | All data access is plain exported functions taking the `DB` handle first arg — no ORM, no repo classes, no ambient singletons. Multi-write operations wrap in `db.transaction(...)`. Keep SQL portable (SQLite today, **Postgres is the locked production engine** — D15); avoid engine-specific SQL. | every `repos-*.ts` | | Portable-repo pattern | All data access is plain exported functions taking the `DB` handle first arg — no ORM, no repo classes, no ambient singletons. Multi-write operations wrap in `db.transaction(...)`. Keep SQL portable (SQLite today, **Postgres is the locked production engine** — D15); avoid engine-specific SQL. | every `repos-*.ts` |
| Append-only audit | Every mutation calls `writeAudit(db, userId, action, entity, id, before, after)` in the **same transaction** as the change. The audit log is append-only — never update or delete rows. Previews and the public share route write nothing. | `audit.ts` | | Append-only audit | Every mutation calls `writeAudit(db, userId, action, entity, id, before, after)` in the **same transaction** as the change. The audit log is append-only — never update or delete rows. Previews and the public share route write nothing. | `audit.ts` |
| Client id is the future tenant id | `client.id` is a client-generated UUIDv7 that becomes `tenant_id` when the cloud tier stands up — same rows, promoted once. Never re-key clients. | `repos-clients.ts` | | Client id is the future tenant id | `client.id` is a client-generated UUIDv7 that becomes `tenant_id` when the cloud tier stands up — same rows, promoted once. Never re-key clients. | `repos-clients.ts` |
| No silent caps | Every list that can grow paginates or warns; never silently truncate. | reports, directory queries | | No silent caps | Every list that can grow paginates or warns; never silently truncate. | reports, directory queries |
| Strict TS, green before landing | Strict TypeScript everywhere; `npm run typecheck` and `npm test` both clean before anything lands. Money math is locked by tests — a rounding change without a test change does not land. | `tsc`, vitest (255 tests) | | Strict TS, green before landing | Strict TypeScript everywhere; `npm run typecheck` and `npm test` both clean before anything lands. Money math is locked by tests — a rounding change without a test change does not land. | `tsc`, vitest (270 tests) |
## Running it ## Running it
@ -63,7 +63,7 @@ cd apps/hq-web && npm run dev
- Env: `HQ_DATA_DIR` (DB location), `SIMS_PIN_PEPPER` (set once, never change), `HQ_SECRET_KEY` - Env: `HQ_DATA_DIR` (DB location), `SIMS_PIN_PEPPER` (set once, never change), `HQ_SECRET_KEY`
(before Gmail), `GOOGLE_*` (Gmail), `AWS_*` (Cost Explorer), `HQ_PORT` (default 5182). (before Gmail), `GOOGLE_*` (Gmail), `AWS_*` (Cost Explorer), `HQ_PORT` (default 5182).
- Health check: `GET /api/health``{ ok: true }`. - Health check: `GET /api/health``{ ok: true }`.
- Verified state: `npm install` + typecheck clean, 255 tests pass. Exercise real flows end - Verified state: `npm install` + typecheck clean, 270 tests pass. Exercise real flows end
to end before claiming done. to end before claiming done.
## Docs (kept in `docs/`) ## Docs (kept in `docs/`)

@ -19,13 +19,17 @@ document and conversation we've had with them.
- **AMC contracts** and **AWS cost recovery** — per-client cost attribution pulled monthly. - **AMC contracts** and **AWS cost recovery** — per-client cost attribution pulled monthly.
- **Payments + allocations**, **recurring plans**, and **reminders** sent via **Gmail** on - **Payments + allocations**, **recurring plans**, and **reminders** sent via **Gmail** on
the company mailbox, with **bounce handling** and a manual queue when the token dies. the company mailbox, with **bounce handling** and a manual queue when the token dies.
- **Quote-to-close funnel** — employee accounts (owner / manager / staff), a cross-client
**pipeline chase-list** (stages derived, oldest-overdue on top), and **escalating quote
follow-up** that nudges the owning employee and emails the client a share link on a dated
schedule (`reminder_schedule`), stopping automatically on accept / lose / convert.
- **Reports**, a **Dashboard**, an **append-only audit log** (every mutation), an **APEX - **Reports**, a **Dashboard**, an **append-only audit log** (every mutation), an **APEX
importer** for the initial 300-client load, and a **scheduler** (runs on boot + every 6h: importer** for the initial 300-client load, and a **scheduler** (runs on boot + every 6h:
reminders, bounce polling, AWS cost pull). reminders, bounce polling, AWS cost pull).
## Build state (start here) ## Build state (start here)
Working codebase, not a plan. `npm install` + typecheck are clean and **255 tests pass**. Working codebase, not a plan. `npm install` + typecheck are clean and **270 tests pass**.
Current state and run-vs-pending detail live in **[STATUS.md](STATUS.md)**; the go-live Current state and run-vs-pending detail live in **[STATUS.md](STATUS.md)**; the go-live
runbook (server, HTTPS, backups, Gmail/AWS/import wiring, the Postgres switch) is in runbook (server, HTTPS, backups, Gmail/AWS/import wiring, the Postgres switch) is in
**[docs/DEPLOY-HQ.md](docs/DEPLOY-HQ.md)**. **[docs/DEPLOY-HQ.md](docs/DEPLOY-HQ.md)**.
@ -38,8 +42,9 @@ runbook (server, HTTPS, backups, Gmail/AWS/import wiring, the Postgres switch) i
dev/test engine; Postgres is the locked production engine (see DEPLOY-HQ.md). DB at dev/test engine; Postgres is the locked production engine (see DEPLOY-HQ.md). DB at
**`data/hq.db`**; server on **:5182** (also serves the built web UI and the public **`data/hq.db`**; server on **:5182** (also serves the built web UI and the public
`/share/:token` route). `/share/:token` route).
- **Frontend — `apps/hq-web`**: React + Vite. Pages: Dashboard, Clients, ClientDetail, - **Frontend — `apps/hq-web`**: React + Vite. Pages: Dashboard, Pipeline, Clients,
Documents, Modules, Reports. ClientDetail, Modules, Reports, NewDocument, DocumentView, Employees (owner-only),
DocumentTemplate (owner-only).
- **Shared packages** (trimmed forks): `@sims/domain` (ids, money, business-day, doc-series, - **Shared packages** (trimmed forks): `@sims/domain` (ids, money, business-day, doc-series,
gstin, documents), `@sims/auth` (pin/scrypt), `@sims/billing-engine` (compute, tax — our gstin, documents), `@sims/auth` (pin/scrypt), `@sims/billing-engine` (compute, tax — our
invoices are GST documents, computed to the paisa), `@sims/ui` (design system). invoices are GST documents, computed to the paisa), `@sims/ui` (design system).

@ -1,7 +1,7 @@
# SiMS HQ — Build Status # SiMS HQ — Build Status
_Internal ops console for running our software business. Not shipped to clients._ _Internal ops console for running our software business. Not shipped to clients._
_Last reviewed: 2026-07-16 · Version 0.1.0_ _Last reviewed: 2026-07-17 · Version 0.1.0_
HQ is the back-office console we use to run ~300 Classic client relationships: HQ is the back-office console we use to run ~300 Classic client relationships:
the client book, our software module catalogue and dated price book, quotations / the client book, our software module catalogue and dated price book, quotations /
@ -10,6 +10,10 @@ output, shareable public document links, call & interaction tracking, AMC
contracts, AWS cost recovery, payments & allocations, recurring billing, and contracts, AWS cost recovery, payments & allocations, recurring billing, and
Gmail reminders with bounce handling — plus reports, a money dashboard, an Gmail reminders with bounce handling — plus reports, a money dashboard, an
append-only audit trail, an APEX cutover importer, and a background scheduler. append-only audit trail, an APEX cutover importer, and a background scheduler.
The 2026-07-17 quote-to-close slice (D16) added employee management with
owner / manager / staff roles, account-owner routing on clients, a cross-client
pipeline chase-list with derived stages, and escalating quote follow-up
reminders driven by the dated `reminder_schedule` table.
## Verification state ## Verification state
@ -17,11 +21,13 @@ append-only audit trail, an APEX cutover importer, and a background scheduler.
| --- | --- | | --- | --- |
| `npm install` | clean | | `npm install` | clean |
| `npm run typecheck` (root + workspaces) | clean, no errors | | `npm run typecheck` (root + workspaces) | clean, no errors |
| `npm test` (`vitest run`) | **255 tests pass across 55 files** | | `npm test` (`vitest run`) | **270 tests pass across 55 files** |
The 255 figure is the live `vitest run` count (grew with the pipeline/follow-up The 270 figure is the live `vitest run` count measured 2026-07-17, after the
and redesign work). Test files live under `apps/hq/test/`, `apps/hq-web/test/`, quote-to-close funnel slice and its review fixes. Test files live under
and `packages/{domain,auth,billing-engine,ui}/test/`. `apps/hq/test/` (49 files, including `employees`, `employees-routes`,
`client-owner`, `pipeline`, `quote-followup`, and `reminder-schedule`),
`apps/hq-web/test/`, and `packages/{domain,auth,billing-engine,ui}/test/`.
## Architecture ## Architecture
@ -52,14 +58,18 @@ and `packages/{domain,auth,billing-engine,ui}/test/`.
## Database schema (`apps/hq/src/db.ts`) ## Database schema (`apps/hq/src/db.ts`)
The schema creates 25 tables on open; `migrate()` additively backfills two columns The schema creates 26 tables on open; `migrate()` additively backfills three
(`module.quote_content`, `email_log.bounced`) on older DBs. columns (`module.quote_content`, `email_log.bounced`, `client.owner_id`) on
older DBs and widens two CHECK constraints via a guarded, transactional
table rebuild (`rebuildTable`: `staff_user.role` gains `manager`,
`reminder.rule_kind` gains `quote_followup`) — a no-op on fresh DBs and on
re-runs.
| Table | Purpose | | Table | Purpose |
| --- | --- | | --- | --- |
| `staff_user` | Console logins — owner / staff, scrypt salt+hash, active flag | | `staff_user` | Console logins / employees — owner · manager · staff role, scrypt salt+hash, active flag |
| `session` | Bearer session tokens with expiry | | `session` | Bearer session tokens with expiry |
| `client` | Client registry — code, name, GSTIN, state, contacts (JSON), status, source | | `client` | Client registry — code, name, GSTIN, state, contacts (JSON), status, account owner (`owner_id` → `staff_user`), source |
| `module` | Software module catalogue — SAC, allowed billing kinds, multi-sub flag, quote-content lines | | `module` | Software module catalogue — SAC, allowed billing kinds, multi-sub flag, quote-content lines |
| `module_price_book` | Dated prices per module/edition/kind (latest effective row wins) | | `module_price_book` | Dated prices per module/edition/kind (latest effective row wins) |
| `client_module` | A module assigned to a client — lifecycle status, install/complete/train dates, next renewal | | `client_module` | A module assigned to a client — lifecycle status, install/complete/train dates, next renewal |
@ -80,7 +90,8 @@ The schema creates 25 tables on open; `migrate()` additively backfills two colum
| `amc_contract` | AMC contracts — coverage, period, amount, reminder days, linked invoice | | `amc_contract` | AMC contracts — coverage, period, amount, reminder days, linked invoice |
| `interaction_type` | Interaction type lookup (call, site visit, training, …) | | `interaction_type` | Interaction type lookup (call, site visit, training, …) |
| `interaction` | Client interaction log — outcome, notes, follow-up date | | `interaction` | Client interaction log — outcome, notes, follow-up date |
| `reminder` | Reminder rows — rule kind, subject, due period, status; `UNIQUE(rule_kind, subject_id, due_period)` idempotency key | | `reminder` | Reminder rows — rule kind (now incl. `quote_followup`), subject, due period, status; `UNIQUE(rule_kind, subject_id, due_period)` idempotency key |
| `reminder_schedule` | Dated reminder cadence + follow-up message text per rule kind — `effective_from`/`_to` window, CSV day offsets (a cadence change is a new dated row) |
| `aws_usage` | Per-client per-month AWS storage/transfer/cost; `UNIQUE(client_id, month)` upsert key | | `aws_usage` | Per-client per-month AWS storage/transfer/cost; `UNIQUE(client_id, month)` upsert key |
## Backend capabilities ## Backend capabilities
@ -89,12 +100,26 @@ Everything below is implemented and exercised by tests via the Express router
(`apps/hq/src/api.ts`) or the scheduler. (`apps/hq/src/api.ts`) or the scheduler.
- **Auth & roles** — email + password login (scrypt via `@sims/auth`), 14-day - **Auth & roles** — email + password login (scrypt via `@sims/auth`), 14-day
bearer sessions, `requireAuth` / `requireOwner` middleware. First boot seeds an bearer sessions, `requireAuth` / `requireOwner` middleware, and **owner /
owner and prints a one-time password to stdout. There is no self-service staff manager / staff roles** with a shared server-side `ownerScope` gate: staff are
management API yet — additional staff are created in code/seed only. forced to their own rows (any widening param is ignored), owner/manager see
everything and may narrow via `?owner=`. `verifySession` requires the account
to still be active, so deactivation kills unexpired tokens immediately. First
boot seeds an owner and prints a one-time password to stdout.
- **Employee management** — staff CRUD over the existing `staff_user` table
(D16) via `repos-employees.ts` and the `/employees` routes: `GET /employees`
for any signed-in user (name/owner pickers; returned whole with a `total`),
owner-only create / rename / re-role / password-reset / deactivate /
reactivate. Guards: the last active owner can be neither demoted nor
deactivated, you cannot deactivate yourself, duplicate emails and empty
patches are rejected cleanly; deactivation **and** password reset purge the
user's sessions in the same transaction. Password hashes never leave the
repo module and are never audited.
- **Clients** — create / list / search (name·code·GSTIN) / get / patch, GSTIN - **Clients** — create / list / search (name·code·GSTIN) / get / patch, GSTIN
checksum validation, auto-generated client codes, contacts as JSON, lead → checksum validation, auto-generated client codes, contacts as JSON, lead →
active → dormant → lost status. active → dormant → lost status, and an **account owner**
(`PATCH /clients/:id/owner`, owner/manager only, audited as `set_owner`) so
bare leads are routable to an employee.
- **Modules & price book** — owner-managed catalogue, dated price book - **Modules & price book** — owner-managed catalogue, dated price book
(`priceOn` = latest effective row ≤ date), allowed billing kinds (`priceOn` = latest effective row ≤ date), allowed billing kinds
(one_time / monthly / yearly / usage), multi-subscription rule, per-module (one_time / monthly / yearly / usage), multi-subscription rule, per-module
@ -109,7 +134,13 @@ Everything below is implemented and exercised by tests via the Express router
conversion chain via `ref_doc_id`, cancel (number stays consumed; blocked once conversion chain via `ref_doc_id`, cancel (number stays consumed; blocked once
payments are allocated), full/partial credit notes recomputed at the original payments are allocated), full/partial credit notes recomputed at the original
invoice date, and a permissive preview endpoint. Issued documents are never invoice date, and a permissive preview endpoint. Issued documents are never
edited — corrections are credit notes. edited — corrections are credit notes. Conversion is hardened (funnel spec
F3/F4): a document with a live (non-cancelled) forward child refuses a second
convert — one sale, one invoice — and PROFORMA → INVOICE rebuilds the carried
lines through `computeBill` on the **invoice's own date**, so a dated tax
change between proforma and invoice lands at the rate that is law on issue
day. Accepting, losing, converting, or cancelling a quotation dismisses its
open follow-up nudges in the same transaction (one audited row each).
- **GST / billing**`@sims/billing-engine` `computeBill` with intra-state - **GST / billing**`@sims/billing-engine` `computeBill` with intra-state
CGST/SGST vs inter-state IGST split by place of supply, round-to-rupee, SAC CGST/SGST vs inter-state IGST split by place of supply, round-to-rupee, SAC
labelling. Place-of-supply is fail-loud: a missing `company.state_code` throws labelling. Place-of-supply is fail-loud: a missing `company.state_code` throws
@ -148,13 +179,37 @@ Everything below is implemented and exercised by tests via the Express router
one-click renewal-invoice generation against the seeded AMC module. one-click renewal-invoice generation against the seeded AMC module.
- **Interactions** — seven seeded interaction types, logging with outcome and an - **Interactions** — seven seeded interaction types, logging with outcome and an
optional follow-up date that feeds the daily scan. optional follow-up date that feeds the daily scan.
- **Pipeline chase-list**`GET /pipeline` (`repos-pipeline.ts`): one
cross-client ranked list with **derived** stages (Enquiry / New Project /
Quoted-Waiting / Won / Lost — never stored; computed from `client.status` +
the client's latest live quotation, so the slice added zero pipeline storage
beyond `client.owner_id`). Age anchors on the first `sent` document event;
the Waiting → Chase → Nudge → Final-nudge ladder and the green/amber/red
bands read the same dated `reminder_schedule` offsets the reminder engine
uses. Role-gated through `ownerScope` (staff see only their rows — quote
owner = `created_by`, lead owner = `client.owner_id`), oldest-overdue sorts
first, `all | mine | overdue | lost` filters (Lost hidden unless asked for),
paginated with an honest `total`.
- **Reminders & scheduler** — a deterministic daily scan (clock injected) detects - **Reminders & scheduler** — a deterministic daily scan (clock injected) detects
`invoice_overdue`, `renewal_due`, `amc_expiring`, and `follow_up`, plus recurring `invoice_overdue` (monthly bucket), `renewal_due`, `amc_expiring`, `follow_up`,
generation; every reminder goes through `INSERT OR IGNORE` on its idempotency and **`quote_followup`** — an escalating chase on sent quotations anchored on
key, so catch-up after downtime is safe. Manual queue with send / dismiss / the first `sent` event, at day milestones (default 3/7/14) resolved from the
preview; templated emails in `reminder-templates.ts`. The scheduler ticks on **dated `reminder_schedule` table** (`resolveSchedule`; message text is dated
boot and every 6h (`unref`'d), also running the bounce poll and the monthly AWS too, with code-constant fallbacks so a missing row never silences the engine).
cost pull. The queued row is the owning employee's nudge (owner derived
`doc_id → document.created_by`); sending it emails the client a public share
link — previews resolve an existing live link read-only and write nothing,
while the real send path is the only place a share is minted (60-day expiry,
live links reused; an unset `share.base_url` refuses loudly). Catch-up after
downtime enqueues only the highest crossed milestone, and every reminder goes
through `INSERT OR IGNORE` on its idempotency key. Send policy is the
`quote.followup.policy` setting (default manual); auto sends drain strictly
after the scan. The manual queue (`GET /reminders`) is now paginated with an
honest `total` and owner-scoped (doc-less rows are shared work, visible to
all), with `?status=` / `?owner=` narrowing; send / dismiss / preview and the
templated emails in `reminder-templates.ts` work as before. The scheduler
ticks on boot and every 6h (`unref`'d), also running the bounce poll and the
monthly AWS cost pull.
- **AWS cost recovery** — per-client per-month usage & cost, entered manually by - **AWS cost recovery** — per-client per-month usage & cost, entered manually by
the owner or pulled from Cost Explorer. The pull is SigV4-signed with the owner or pulled from Cost Explorer. The pull is SigV4-signed with
`node:crypto` (no SDK), grouped by the `client` cost-allocation tag, assumes `node:crypto` (no SDK), grouped by the `client` cost-allocation tag, assumes
@ -165,7 +220,8 @@ Everything below is implemented and exercised by tests via the Express router
margin), and the AWS cost ranking. margin), and the AWS cost ranking.
- **Dashboard view** — "today's money": overdue invoices, recurring due this - **Dashboard view** — "today's money": overdue invoices, recurring due this
week, renewals this month, follow-ups today, recent payments, the live reminder week, renewals this month, follow-ups today, recent payments, the live reminder
queue, and headline totals. queue (owner-scoped like `GET /reminders`, first page + honest total), and
headline totals.
- **Settings / letterhead** — owner-editable company profile (GSTIN & state-code - **Settings / letterhead** — owner-editable company profile (GSTIN & state-code
validated) and template text (terms, declaration, jurisdiction, footer, validated) and template text (terms, declaration, jurisdiction, footer,
signatory label, per-type titles), logo upload (base64 PNG/JPEG/GIF/WebP/SVG, signatory label, per-type titles), logo upload (base64 PNG/JPEG/GIF/WebP/SVG,
@ -190,13 +246,15 @@ appear in ADMIN; the warm ops-console restyle is the 2026-07-17 redesign spec. R
| --- | --- | --- | | --- | --- | --- |
| `/login` | `Login` | Email + password sign-in | | `/login` | `Login` | Email + password sign-in |
| `/` | `Dashboard` | Money headline cards, the reminder queue with send / preview / dismiss, and overdue / due-this-week / renewals / follow-ups / recent-payments tables | | `/` | `Dashboard` | Money headline cards, the reminder queue with send / preview / dismiss, and overdue / due-this-week / renewals / follow-ups / recent-payments tables |
| `/pipeline` | `Pipeline` | Cross-client chase-list: derived stage, amount, owner, age and colour band per row with an explicit next action; `All / Mine / Overdue / Lost` chips, owner narrowing (owner/manager), pagination |
| `/clients` | `Clients` | Search, list, inline new-client create; row → client 360° | | `/clients` | `Clients` | Search, list, inline new-client create; row → client 360° |
| `/clients/:id` | `ClientDetail` | Client 360°: header + status, module assignment & lifecycle, documents, payments & dues (record payment, advance), recurring plans (owner), AMC contracts (owner), interaction log with follow-ups, and AWS usage | | `/clients/:id` | `ClientDetail` | Client 360°: record header + status, **account-owner dropdown** (owner/manager), module assignment & lifecycle, documents, payments & dues (record payment, advance), recurring plans (owner), AMC contracts (owner), interaction log with follow-ups, and AWS usage |
| `/modules` | `Modules` | Module catalogue + dated price book; owner edits (create module, quote content with live sample preview, add prices), staff read-only | | `/modules` | `Modules` | Module catalogue + dated price book; owner edits (create module, quote content with live sample preview, add prices), staff read-only |
| `/reports` | `Reports` | Dues aging, module revenue, client profitability, and an AWS cost bar chart by month | | `/reports` | `Reports` | Dues aging, module revenue, client profitability, and an AWS cost bar chart by month |
| `/documents/new` | `NewDocument` | Quotation-in-minutes composer: client type-ahead, line editor, server-computed GST, and a live PDF-fidelity preview (desktop split view / mobile sheet) | | `/documents/new` | `NewDocument` | Quotation-in-minutes composer: client type-ahead, line editor, server-computed GST, and a live PDF-fidelity preview (desktop split view / mobile sheet) |
| `/documents/:id` | `DocumentView` | Document page: PDF preview, state-driven action bar (issue / send / mark / convert / cancel / credit note / record payment), share & download group (copy link / WhatsApp / native share / revoke), event timeline and email log | | `/documents/:id` | `DocumentView` | Document page: PDF preview, state-driven action bar (issue / send / mark / convert / cancel / credit note / record payment), share & download group (copy link / WhatsApp / native share / revoke), event timeline and email log |
| `/settings/template` | `DocumentTemplate` | Owner-only letterhead editor (company + boilerplate + titles + logo) with a live sample preview | | `/settings/template` | `DocumentTemplate` | Owner-only letterhead editor (company + boilerplate + titles + logo) with a live sample preview |
| `/employees` | `Employees` | Owner-only staff management: add / edit / re-role / reset password / deactivate / reactivate, surfacing the last-owner and self-deactivation guards |
The composer, quote-content, and template previews all use one `LivePreview` The composer, quote-content, and template previews all use one `LivePreview`
component that renders the server's real `documentHtml` in a sandboxed component that renders the server's real `documentHtml` in a sandboxed
@ -211,8 +269,15 @@ rasterizes for the PDF.
- **AWS cost pull is env-gated** — the scheduled and manual pulls run only when - **AWS cost pull is env-gated** — the scheduled and manual pulls run only when
`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` are configured; otherwise usage `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` are configured; otherwise usage
is owner-entered by hand. is owner-entered by hand.
- **No staff-management UI/API**`createStaff` exists but is only reachable via - **Funnel spec phases 710 are designed, not built** (see
seed/code; there is no in-app way to add or deactivate console users. `docs/superpowers/specs/2026-07-17-quote-to-close-funnel-design.md`): the
one-click `POST /documents/:id/convert-and-send` and proforma
`POST /documents/:id/supersede` endpoints do not exist yet (the underlying
convert guards landed); invoice-overdue escalation still uses the **monthly
bucket** — the dated 7/15/30 `invoice_overdue` cadence row is seeded and
`resolveSchedule` supports it, but the scan does not read it yet; and the
module → client roster (`GET /modules/:id/clients` + CSV export +
owner/manager Notify-all) is not implemented.
- **APEX import is CLI-only** — no web UI for staging/commit. - **APEX import is CLI-only** — no web UI for staging/commit.
- **Reminders live on the Dashboard** — there is no standalone reminders page, - **Reminders live on the Dashboard** — there is no standalone reminders page,
and there is no separate documents-list page (documents are reached from the and there is no separate documents-list page (documents are reached from the

@ -147,15 +147,61 @@ AWS attribution, no path to the fleet console).
dev/test engine behind the same repositories. Physical repo split stays a cheap, dev/test engine behind the same repositories. Physical repo split stays a cheap,
reversible option if practice ever demands it. reversible option if practice ever demands it.
## D16 — Employees are `staff_user` rows; the employee list is returned whole (2026-07-17) ## D16 — Quote-to-close funnel slice (2026-07-17)
The Employee entity (quote-to-close funnel, Phase 1) is **backed by the existing Founder-approved design in
`staff_user` table** — renaming it would churn `session.staff_id`, `superpowers/specs/2026-07-17-quote-to-close-funnel-design.md`; decisions locked
with it:
- **Employee foundation rides `staff_user`.** The Employee entity is **backed by the
existing `staff_user` table** — renaming it would churn `session.staff_id`,
`interaction.staff_id` and audit entity names for zero gain; "Employee" lives in the `interaction.staff_id` and audit entity names for zero gain; "Employee" lives in the
repo/API/UI vocabulary only. Roles widened to `owner | manager | staff` via a guarded, repo/API/UI vocabulary only. Roles widened to `owner | manager | staff` via a guarded,
transactional table rebuild. transactional table rebuild (SQLite cannot ALTER a CHECK); the enum is validated in
the repo. Session hardening rode along: `verifySession` requires `active=1`, and
deactivation / password reset purge sessions in the same transaction.
- **Chase-list over kanban (option B).** The pipeline is **one cross-client ranked
list** — most-overdue on top, colour bands, an explicit next action per row,
`All | Mine | Overdue | Lost` filters — not a kanban board.
- **Derived pipeline stages, zero new pipeline storage.** Stage (Enquiry / New
Project / Quoted-Waiting / Won / Lost) is **never stored** — computed from
`client.status` + the client's latest live quotation; age anchors on the first
`sent` document event. The only new column anywhere in the slice is
`client.owner_id` (a bare lead has no creator to derive an owner from — A4).
- **Escalating owner+client reminders on the dated `reminder_schedule` table.** New
sendable `quote_followup` rule kind: the queued reminder row **is** the owning
employee's nudge (owner derived `doc_id → document.created_by`), and its send **is**
the client email with a public share link. Cadence **and** message text are dated
config rows (rule 3) resolved by `resolveSchedule` — defaults 3/7/14 (quote) and
7/15/30 (invoice) ship as seeded rows plus code-constant fallbacks, so a cadence
change is a new dated row, never a release. Catch-up fires only the highest crossed
milestone; accept / lose / convert / cancel dismisses open nudges per-row (audited)
in the same transaction; previews resolve shares read-only — minting happens only in
the send path (60-day expiry, live links reused).
- **Convert recompute + duplicate-invoice guard (F3/F4).** PROFORMA → INVOICE rebuilds
the carried lines through `computeBill` on the **invoice's own `doc_date`** (a tax
invoice uses the rate that is law on its issue date), and `convertDocument` rejects
any source with a live (non-cancelled) forward child — one sale, one invoice number.
- **Proforma supersede matrix.** Corrections stay firewalled by document type: a draft
PI is simply re-drafted; an issued, unallocated PI may cancel (number stays consumed)
and be recreated as a `ref_doc_id`-linked supersede; an issued INVOICE is **never**
recreated — cancel-if-unpaid or credit note only.
- **Assumptions A1A7 (defaulted, flagged in the spec):** A1 module roster balances
support and revenue; A2 employees are a plain table (no org chart); A3 document
owner = `created_by` for v1 (no `document.owner_id` until a handoff feature is
real); A4 `client.owner_id` is the one genuinely new owner column; A5 notify-all
text is owner-typed ad-hoc, not dated config; A6 roster visible to all staff,
notify-all owner/manager only; A7 invoice-overdue anchors on `doc_date` (no
due-date column exists).
**Recorded deviation from the funnel spec's blanket pagination rule (§5 / rule 8):** **Recorded deviation from the funnel spec's blanket pagination rule (§5 / rule 8):**
`GET /employees` returns the full set with `total` and **no `page`/`pageSize`** `GET /employees` returns the full set with `total` and **no `page`/`pageSize`**
console users are a small bounded set (single-digit headcount), the count is displayed, console users are a small bounded set (single-digit headcount), the count is displayed,
and nothing is silently truncated, which is the intent of rule 8. If the team ever and nothing is silently truncated, which is the intent of rule 8. If the team ever
grows past a screenful, paginate it like every other list. grows past a screenful, paginate it like every other list.
**Delivery state (2026-07-17):** phases 16 landed (employee foundation + management
surface, owner plumbing, dated schedule, pipeline chase-list, escalating quote
follow-up) along with the F3/F4 convert hardening. The `convert-and-send` and
`supersede` endpoints, invoice `dN` milestone escalation (scan still uses the monthly
bucket), and the module → client roster (phases 710) are decided here but not yet
implemented — tracked in STATUS.md Known gaps.

Loading…
Cancel
Save