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.
sims-hq/docs/superpowers/specs/2026-07-13-quotation-polish...

94 lines
5.9 KiB
Markdown

# Quotation Polish + Share/Download — Design
**Status:** approved in principle (founder, 2026-07-13). Sequenced to run **after** the
live-preview build (wf live-preview) lands, so it builds on the finished `templates.ts`
rendering + the live preview (every visual change is seen instantly) and never edits the
template file concurrently with that build.
Two threads: (A) make the document **look beautiful**, (B) **share & download** options.
## A. Beautiful document (templates.ts `documentHtml`)
Restyle the one shared letterhead template — this automatically improves the PDF, the
composer live preview, and the Document Template page (one renderer). A print-quality,
professional Indian GST document, not a generic table. Direction (finalised with the
frontend-design skill at build time):
- **Typography:** a clear type hierarchy — company name prominent, a refined document
title block (TAX INVOICE / QUOTATION), tabular-figure alignment for all money columns
so rupee amounts line up to the paisa. Keep the bundled Segoe/Inter-class stack (renders
identically on the office desktops and the Windows server — no web-font fetch in print).
- **Layout:** a proper letterhead band (logo left via `template.logo`, company block +
GSTIN right), a clean bill-to / ship-to and document-meta row, a well-spaced line table
with subtle row rules (not heavy borders), a totals block visually anchored bottom-right
with the grand total emphasised, amount-in-words, then terms / declaration / jurisdiction
/ signatory / footer from the `template.*` fields.
- **Accent:** one restrained accent colour for rules/headers, sourced from a new optional
`template.accent` setting (defaults to a professional slate/indigo) so it's brandable
from the Document Template page later — no hard-coded brand colour.
- **Print discipline:** A4, 14mm margins, avoid row-splitting across page breaks
(`break-inside: avoid` on rows), repeat the table header on page 2+. All screen/print
parity rules from the live-preview design still hold (one renderer, golden test guards).
- **Fidelity:** the existing string-identity golden test continues to guarantee the
beautified preview === the beautified PDF.
## B. Share & download (all three channels + download)
On **DocumentView** (and optionally right after composing), an action group:
1. **Download PDF** — button hitting the existing `GET /api/documents/:id/pdf`, served
with `Content-Disposition: attachment; filename="QT-26-27-0001.pdf"` (doc-no slugged,
`/``-`). Trivial; no new endpoint.
2. **Shareable view link** (the load-bearing one — needs a secure public route):
- New table `document_share (id, document_id, token, created_by, created_at,
expires_at NULL, revoked INTEGER DEFAULT 0)` via `CREATE TABLE IF NOT EXISTS`.
- `POST /api/documents/:id/share` (requireAuth) → mints a **cryptographically random**
token (≥32 bytes, `randomBytes`), default `expires_at` = +30 days (configurable),
returns the public URL. Audited.
- **Public** `GET /share/:token` (NO auth, the ONLY unauthenticated read route) →
validates token exists, not revoked, not expired; renders **only that one
document's** PDF (reuse renderPdf) or an HTML view. Serves nothing else — no listing,
no client record, no API. Unknown/expired/revoked → a plain "link expired" page.
- `POST /api/documents/:id/share/:shareId/revoke` (requireAuth, audited) → sets
`revoked=1`. A document's shares are listed on DocumentView so the owner can revoke.
- **Security stance (documented, accepted):** anyone holding the link sees that one
document's contents (client name + amounts) — the intended trade-off of any share
link, mitigated by an unguessable token, optional expiry, and one-click revoke.
Rate-limit `/share/:token` lookups to blunt token brute-forcing. No document data in
the URL beyond the opaque token.
3. **WhatsApp (click-to-chat)** — a `wa.me/<client-phone>?text=<encoded>` link opening
WhatsApp with a ready message: greeting + doc type/no + total + **the shareable view
link** (so the client taps through to the actual document — this is why the link and
WhatsApp compose). Needs the client's phone (already on the client record); no WhatsApp
Business API. If no share link exists yet, minting one is offered inline.
4. **Native share sheet**`navigator.share({ title, text, url: shareLink })` where
supported (mobile), sharing the view link to any app; where the File share is
supported, share the PDF blob directly; desktop/unsupported → falls back to Download.
## Data / files (build time)
- `apps/hq/src/db.ts``document_share` table (CREATE TABLE IF NOT EXISTS; no migrate).
- `apps/hq/src/repos-shares.ts` — mint/validate/revoke/list shares.
- `apps/hq/src/api.ts``POST /documents/:id/share`, `POST …/share/:shareId/revoke`;
`GET /api/documents/:id/pdf` gains the attachment filename.
- `apps/hq/src/server.ts` — public `GET /share/:token` mounted OUTSIDE the `/api`
auth router, rate-limited; renders via renderPdf.
- `apps/hq/src/templates.ts` — the beautification (thread A) + optional
`template.accent`.
- `apps/hq-web/src/pages/DocumentView.tsx` — the action group (Download / Link / WhatsApp
/ Share) + the shares list with revoke.
- Tests (TDD, server): token mint is random + unguessable; `/share/:token` serves only
the one doc and 404s on unknown/expired/revoked; revoke works; owner-gate/audit;
download filename; beautify keeps the golden string-identity test green.
## Not this round
Real WhatsApp Business API sending (click-to-chat only); SMS; share analytics
(opened/viewed tracking) — a nice later addition on the same `document_share` row.
## Sequence
1. Live-preview build (in flight) completes and is verified.
2. Plan this doc (writing-plans) → ultracode build → verify. Beauty pass uses the
frontend-design skill for the concrete visual system.