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...

6.0 KiB

Quotation Polish + Share/Download — Design

STATUS: DELIVERED (2026-07-13). Kept as the design/task record; current state lives in STATUS.md.

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 sheetnavigator.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.tsdocument_share table (CREATE TABLE IF NOT EXISTS; no migrate).
  • apps/hq/src/repos-shares.ts — mint/validate/revoke/list shares.
  • apps/hq/src/api.tsPOST /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.