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