Files
hermes-config/skills/self-hosting/shop-pro-quote/references/v2-pdf-generation.md
T
2026-07-12 10:17:17 -04:00

2.5 KiB

v2 PDF Generation (src/lib/pdf.ts)

Stack: jspdf@^4.2.1 (ESM: import { jsPDF } from 'jspdf'). Letter portrait, 20pt margins, 572pt content width.

Critical jspdf 4.x anti-patterns

DO NOT pass string arrays to doc.text(). Array-based doc.text(lines, x, y) uses internal matrix positioning whose line spacing differs from manual array.length * lineHeight calculations — cascading yPos desync causes text overlap everywhere below. Always iterate line-by-line:

// WRONG:
doc.text(messageLines, MARGIN, yPos);
yPos += messageLines.length * 6 + 15;

// RIGHT:
messageLines.forEach((line) => { doc.text(line, MARGIN, yPos); yPos += 6; });
yPos += 15;

Service height calculation

Must exactly match actual yPos increments for page-break estimates:

Element yPos advance
Decision badge (approved/declined) +5
Name + price line +6
Priority header (label + reason) +16
Recommendation +8
Explanation (per line + final gap) +5 per line, +2 final
Parts Not In Stock +8 (+8 more if delivery time)
Aftermarket Available +8 (+8 more if parts list)
Service separator + spacing +6

Page-break logic

  • CONTENT_BOTTOM = PAGE_HEIGHT - MARGIN (772)
  • FOOTER_HEIGHT = 35 reserved at bottom of every page
  • Before each service: compute total height, break if y + height > CONTENT_BOTTOM - FOOTER_HEIGHT
  • After break: redraw current section header (APPROVED/POSTPONED)
  • Footer (separator + messages + "Page N of M") rendered on EVERY page via post-render loop — NOT just the last page

Customer info loading (QuoteGenerator.tsx)

Zustand reset() wipes all state including customer info. Call reset() BEFORE setting any fields:

reset();  // first
if (data.customerInfo) setCustomerInfo(data.customerInfo);  // then set
if (data.services) { /* add services */ }

PocketBase may return JSON fields as strings. Always check and parse:

const ci = typeof data.customerInfo === 'string' ? JSON.parse(data.customerInfo) : data.customerInfo;

Error visibility

PDF errors must show a toast — not just console.error:

const { showToast } = useToast();
// ...
} catch (err: any) {
  showToast(`PDF generation failed: ${err?.message || 'Unknown error'}`, 'error');
}

User preference

If the user says "stop", "this is taking too long", or "i can test it" — stop debugging immediately and let them test. Do not do browser-based PDF verification unless explicitly asked.