# 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: ```ts // 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: ```ts 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: ```ts const ci = typeof data.customerInfo === 'string' ? JSON.parse(data.customerInfo) : data.customerInfo; ``` ## Error visibility PDF errors must show a toast — not just `console.error`: ```tsx 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.