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 = 35reserved 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.