5.9 KiB
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 thetemplate.*fields. - Accent: one restrained accent colour for rules/headers, sourced from a new optional
template.accentsetting (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: avoidon 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:
- Download PDF — button hitting the existing
GET /api/documents/:id/pdf, served withContent-Disposition: attachment; filename="QT-26-27-0001.pdf"(doc-no slugged,/→-). Trivial; no new endpoint. - 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)viaCREATE TABLE IF NOT EXISTS. POST /api/documents/:id/share(requireAuth) → mints a cryptographically random token (≥32 bytes,randomBytes), defaultexpires_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) → setsrevoked=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/:tokenlookups to blunt token brute-forcing. No document data in the URL beyond the opaque token.
- New table
- 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. - 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_sharetable (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/pdfgains the attachment filename.apps/hq/src/server.ts— publicGET /share/:tokenmounted OUTSIDE the/apiauth router, rate-limited; renders via renderPdf.apps/hq/src/templates.ts— the beautification (thread A) + optionaltemplate.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/:tokenserves 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
- Live-preview build (in flight) completes and is verified.
- Plan this doc (writing-plans) → ultracode build → verify. Beauty pass uses the frontend-design skill for the concrete visual system.