Commit Graph

75 Commits (eaecaedd3f44d5243562939d605492b87fcf0328)

Author SHA1 Message Date
Thomas Joise eaecaedd3f feat(pipeline): cross-client chase-list — derived stages, schedule-driven bands, role-gated, paginated
Phase 5 (D-PIPE option B, spec §6b/§9): repos-pipeline.ts listPipeline unions the
latest non-cancelled QUOTATION per client with bare leads (Enquiry); stage is never
stored; age anchors on the first 'sent' document_event; next action + colour band
read resolveSchedule('quote_followup') day_offsets (no hardcoded 3/7/14); staff are
server-forced to their own rows via ownerScope; Lost hidden unless filtered; sorted
actionable-oldest-first with LIMIT/OFFSET + honest total. GET /pipeline route plus
the Pipeline page (filter chips, managerial owner dropdown, band row tones, lifecycle
action buttons, pager) wired into nav right after Dashboard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 days ago
Thomas Joise c5a524bab5 feat(hq-web): Client 360 — record header, KPI row, tabbed sections, forms extracted
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 9aa376fa2b feat(hq-web): clients — status filter chips, mono codes, skeleton/error states
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 00ce1d657e feat(hq-web): dashboard — skeletons, toned stats, tinted queue, 2-col grid
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 278187631b feat(hq-web): grouped sidebar shell, top bar, Ctrl+K palette, responsive-lite
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 0be928fbf8 fix(hq-web): Employees page no longer shows a permanent Loading state on fetch failure
employees stays undefined when getEmployees rejects, so the page rendered
the error Notice AND an eternal "Loading…" EmptyState below it. Branch on
list.error first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 days ago
Thomas Joise 56b00b60c6 fix(reminders): deterministic tie-break in resolveSchedule on equal effective_from
ORDER BY effective_from DESC alone picks an engine/plan-dependent winner
when two reminder_schedule rows for the same rule_kind share an
effective_from (a same-day cadence correction under rule 3's
new-row-never-edit makes this likely). Append id DESC — UUIDv7 ids are
time-ordered, so the latest insert wins deterministically on SQLite and
Postgres alike.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 days ago
Thomas Joise 5865aafe06 fix(employees): purge sessions on password reset; reject empty patches and duplicate emails cleanly
Review findings on the Phase 1-2 employee slice:

- setEmployeePassword now deletes the employee's sessions in the same
  transaction — resetting a suspected-compromised password must not leave
  the attacker's existing 14-day bearer token valid (deactivateEmployee
  already did this; the reset path is the natural lockout action).
- updateEmployee throws 'Nothing to update' when the patch resolves to
  zero recognized fields, instead of returning 200 and writing a no-op
  before==after audit row; PATCH /employees/:id with {} or wrongly-typed
  fields now 400s.
- createEmployee pre-checks UNIQUE(email) inside the transaction and
  throws 'Email already in use' instead of surfacing the raw
  engine-specific 'UNIQUE constraint failed: staff_user.email' text
  (would differ on Postgres, the locked production engine — D15).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 days ago
Thomas Joise dc3d651b08 feat(reminders): dated reminder_schedule table + resolveSchedule resolver (Phase 4)
reminder_schedule (id, rule_kind, effective_from/_to, day_offsets CSV,
subject, body) added to SCHEMA (CREATE TABLE IF NOT EXISTS, exec'd on every
open = the additive migration path). resolveSchedule(db, ruleKind, today)
picks the row active on the business date (from<=today, to NULL or >today,
latest from wins) with code-constant fallbacks (quote_followup 3/7/14 +
spec s7 subject/body; invoice_overdue 7/15/30). seedIfEmpty seeds one
open-ended dated row per kind, audited in the same transaction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 days ago
Thomas Joise efdb34e710 feat(clients): account-owner plumbing — client.owner_id, audited owner route, role gate
Phase 3 of the quote-to-close funnel spec: client.owner_id added additively
(SCHEMA + PRAGMA-guarded migrate) so a bare lead is routable to an employee;
setClientOwner repo fn (validates active employee, audits set_owner in-txn);
PATCH /clients/:id/owner gated inline to owner/manager; shared ownerScope role
gate (staff forced to self, widening ignored) for later phases; ClientDetail
gains an owner/manager-only Account owner dropdown fed by GET /employees.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 days ago
Thomas Joise d4388c4f63 feat(employees): management surface — /employees API routes + owner-only Employees page
Phase 2 of the quote-to-close funnel slice. Six /employees routes in
apps/hq/src/api.ts (GET requireAuth for the picker; POST/PATCH/deactivate/
reactivate/password owner-only, acting as the signed-in user); Employees
page (table + add/edit/reset-password forms, email immutable on edit,
count shown, no truncation); owner-only nav item beside Document Template;
typed client fns in hq-web api.ts. 6 new route tests (192 total).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 days ago
Thomas Joise 1af7b87aab feat(hq): employee foundation — manager role, employee repo, session hardening (phase 1)
- rebuildTable helper: guarded, txn-wrapped CHECK-constraint rebuild (SQLite-only path)
- staff_user.role CHECK widened to owner|manager|staff (SCHEMA + migrate for old DBs)
- repos-employees.ts: list/get/create/update/setPassword/deactivate/reactivate,
  audited in-txn, password columns never selected, last-active-owner + self guards
- verifySession requires active=1; deactivation purges sessions in the same txn
- 9 tests (apps/hq/test/employees.test.ts); suite now 186 green

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 days ago
Thomas Joise 7e1c93cee0 Split: make this the standalone HQ project (remove Store)
This repo keeps its 60-commit HQ history and becomes independent of the Store
product. Removed the Store apps (pos, store-server, backoffice), the Store-only
packages (config, scanning, search-core, printing — printing was already a dead
dependency in hq-web), and the Store planning docs.

Shared packages are forked and trimmed to exactly what HQ uses: domain keeps
ids/money/business-day/doc-series/gstin/documents; auth keeps pin; billing-engine
keeps compute/tax. Store-only tests for the removed modules were dropped, and
auth.test.ts was trimmed to its PIN blocks. Package renamed sims-hq; root
tsconfig/vitest paths narrowed to the four kept packages.

Verified independently: npm install + typecheck clean, 177 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 days ago
Thomas Joise 3d3487c85f wip(store-product): returns, GST returns, offline queue, security hardening + docs
Snapshot of the store-product workstream's in-progress work (apps/pos,
apps/backoffice, apps/store-server, packages): returns + credit notes, GST
returns exports, offline bill queue, rate-limiting, session policy, print
guard, batch tracking, and the SEC-C6/SEC-A5 auth fixes; plus docs/18 red-team
review. Committed to preserve work for the repo push. HQ console already
committed separately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise 4559958fa4 docs(hq): go-live runbook, env template and S3 backup script
Turnkey deployment for the AWS box: documented env vars, pm2/systemd run,
HTTPS reverse-proxy, nightly SQLite->S3 backup with rotation, and the Postgres
switch note. No secrets; unblocks go-live the moment the server/creds arrive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise 5fe86f40ba chore(hq): demo-seed script + gitignore demo data/ruvector
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise cfa1cf81f6 feat(hq-web): DocumentView share & download action group
Add a Share & download group to DocumentView: Download PDF (hits
?download=1 via a bearer-authed blob + save anchor), Create/Copy a public
/share/<token> view link with the live-share list and one-click Revoke,
Share on WhatsApp (wa.me/<client digits> prefilled with greeting + doc no
+ total + the share link), and native share (navigator.share of the link,
falling back to Download where unsupported).

Client api.ts gains the Share type, shares on getDocumentFull, createShare/
revokeShare, and downloadDocumentPdf. Vite dev proxies /share to the hq
server so a minted link opens against the dev origin exactly as in prod
(the hq server serves both the SPA and the public /share route on one
origin). Server share/download endpoints and the golden renderer are
unchanged.

Browser-verified: minted a link, opened /share/<token> in a fresh
unauthenticated context (200 application/pdf, viewer renders the doc),
revoked and confirmed a 404 "Link unavailable" page with no data leak, and
confirmed the WhatsApp deep link is prefilled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise 25b96e2514 feat(hq): attachment download filename on GET /documents/:id/pdf?download=1
Serve the document PDF as an attachment with a doc-no-derived filename
("QT/26-27-0001" -> "QT-26-27-0001.pdf", null docNo -> "draft.pdf") when
?download=1 is present; keep the default inline so the composer iframe
preview still works. renderPdf is now injectable into apiRouter so the
header logic is tested without launching Chrome (house DI pattern).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise 63e377d649 feat(hq): public GET /share/:token document view route
Mount the one unauthenticated read route OUTSIDE the /api auth router.
A live token resolves via validateShare to exactly one document, rendered
inline (application/pdf) through the existing documentHtml -> renderPdf path;
unknown/expired/revoked tokens return a plain 404 "link expired or invalid"
HTML page carrying no document data. Adds a dependency-free per-IP fixed-window
rate limiter to blunt token brute-forcing. renderPdf is injected so the route
is TDD-tested without launching Chrome. Public reads write no audit; the token
is never logged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise a1759864bf feat(hq): shareable document link backend (mint/validate/revoke/list)
Add document_share table (CREATE TABLE IF NOT EXISTS; opaque token, optional
expiry, revoke flag) and repos-shares.ts:

- mintShare: 256-bit randomBytes hex token (64 hex, unique), default +30d expiry,
  audited — the token itself is never written to the audit trail or any log.
- validateShare: resolves a live token to its one document; null on
  unknown / revoked / expired (ISO-8601 UTC lexical compare).
- revokeShare: flips revoked=1, audited.
- listShares: a document's shares, scoped and newest-first.

Wire POST /api/documents/:id/share and
POST /api/documents/:id/share/:shareId/revoke (requireAuth, document-scoped
share guard) and surface shares on GET /api/documents/:id. TDD: 14 tests cover
token shape/uniqueness, validate rejections, revoke, and audit-without-token.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise d29025189c feat(hq): beautify document template — "Slate Ledger" visual system + template.accent
Rewrite documentHtml to a restrained, print-first GST layout: hairline table
rules, tabular money aligned to the paisa, a single accent driving every
structural color (letterhead rule, title, table-header rule + color-mix tint,
docmeta edge, amount-in-words edge, filled grand-total band).

Add optional template.accent setting (validated hex-or-fallback #334155
slate-indigo) read from the settings map and interpolated only after
validation, so a raw setting string can never be injected into the CSS.
print-color-adjust:exact so the accent tint and grand-total fill survive
puppeteer. DRAFT stays: accent chip in docmeta + optional watermark for
un-issued docs. lineRow/totalsRows untouched; @media screen paper + @page
14mm preserved; renderer stays one pure function so preview===pdf identity holds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise 0d57c267ca feat(hq-web): reminder email preview in the manual queue
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise 0b340fd85f feat(hq-web): module quote-content live print preview
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise b65e03e1c4 feat(hq-web): Document Template page — editable letterhead data + live preview
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise 08e067c7cc feat(hq-web): composer split view — server-truth totals strip, live paper, responsive
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise c2eb7acba0 feat(hq-web): LivePreview — debounced double-buffer iframe + preview fetchers
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise 52e8a03c81 feat(hq): GET /reminders/:id/preview — exact email via shared reminderContext
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise 48731fdb2b feat(hq): GET/PUT /settings/template + logo upload (owner-gated, audited)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise c2f94df06d feat(hq): screen paper styles + documentHtmlSample + POST /previews/sample
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise a948a92350 feat(hq): render editable template.* letterhead fields with default fallback
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise 1ac44ae7de test(hq): golden fidelity — preview HTML byte-identical to the PDF's HTML
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise 268a7469d3 feat(hq): POST /documents/preview — permissive live preview, persists nothing
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise 90df8b0e9f feat(hq): extract pure prepareDraft from createDraft (permissive preview path)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 week ago
Thomas Joise dbccb8f954 feat(hq-web): owner company profile editor and client 360 aws usage trend
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 weeks ago
Thomas Joise 2acca18fc9 feat(hq-web): reports page — dues aging, module revenue, profitability, aws cost chart
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 weeks ago
Thomas Joise 5495b42e30 feat(hq): schedule monthly aws cost pull and owner-gated manual pull route
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 weeks ago
Thomas Joise ea8c2b23ad feat(hq): company profile settings — owner-gated GET/PUT, audited
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 weeks ago
Thomas Joise b7085ce56e feat(hq): payment receipts — own RCT/ series, zero-GST acknowledgment PDF
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 weeks ago
Thomas Joise 42cfa3d0b2 feat(hq): reports — dues aging, module revenue, client profitability with margin
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 weeks ago
Thomas Joise 51732df5e5 feat(hq): aws cost explorer ingestion — sigv4 signer and monthly tag-grouped pull
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 weeks ago
Thomas Joise 9d2eedc366 feat(hq): aws usage repo — upsert/list, cross-client cost ranking, routes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 weeks ago
Thomas Joise 3a09feab6f feat(hq): hq-3a schema — aws_usage table (per client per month)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 weeks ago
Thomas Joise 92fb5e6245 feat(hq-web): client 360 recurring, amc and interaction sections
Browser-walk fixes (Task 12 step 3): reload the reminder queue after a failed send so the row flips to 'failed' live, and reload AMC contracts after recording a payment so the Paid badge derives fresh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 weeks ago
Thomas Joise a9f78c4e3c feat(hq-web): client 360 recurring, amc and interaction sections
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 weeks ago
Thomas Joise 9e3886d0be feat(hq-web): dashboard money view with manual reminder queue as default route
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 weeks ago
Thomas Joise 06b5eb532c feat(hq): dashboard money view and 6-hourly scheduler wiring
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 weeks ago
Thomas Joise 7e1dcc1ad8 feat(hq): mailbox bounce detection with email_bounced reminders
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 weeks ago
Thomas Joise 1b14e49d86 feat(hq): recurring invoice generation with at-most-once auto-send
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 weeks ago
Thomas Joise 2d3b1a52a9 feat(hq): daily scan detection rules with idempotent reminders
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 weeks ago
Thomas Joise 3204a9af97 feat(hq): reminder email send reusing gmail, plus manual-queue routes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 weeks ago