# PDF Generation — ShopProQuote v2

Source: `src/lib/pdf.ts` in the v2 SPA.

## Critical pitfalls (from real debugging sessions)

### 1. NEVER pass string arrays to `doc.text()` in jspdf 4.x

jspdf 4.x uses internal matrix-based line spacing when you pass a string array to `doc.text()`. The manual yPos calculation (`array.length * lineHeight`) does NOT match where jspdf actually placed the text. This causes a cascading yPos desync and text overlap throughout the entire document.

```typescript
// WRONG — causes overlap
const lines = doc.splitTextToSize(text, maxWidth);
doc.text(lines, x, y);
yPos += lines.length * 6;  // does NOT match jspdf's internal spacing

// CORRECT — render each line individually
doc.splitTextToSize(text, maxWidth).forEach((line) => {
  doc.text(line, x, yPos);
  yPos += 6;
});
```

### 2. Pagination must be proactive, not reactive

The page-break condition must check whether the NEXT service fits before rendering it:

```typescript
// Calculate full service height (badge + name + price + priority + recommendation + explanation + parts + separator)
let svcH = 0;
if (svc.customerDecision === 'approved' || svc.customerDecision === 'declined') svcH += 5;
svcH += 6; // name + price
if (svc.priority && svc.priorityReason) svcH += 16;
if (svc.recommendation) svcH += 8;
if (svc.explanation) svcH += splitLines * 5 + 2;
if (svc.partsNotInStock) { svcH += 8; if (svc.partsDeliveryTime) svcH += 8; }
if (svc.aftermarketAvailable) { svcH += 8; if (svc.aftermarketPartsList) svcH += 8; }
svcH += 6; // separator

// Break BEFORE rendering
if (y + svcH > CONTENT_BOTTOM - FOOTER_HEIGHT) {
  y = newPage(doc);
  // Redraw section header on new page
}
```

Key rules:
- Use `CONTENT_BOTTOM - FOOTER_HEIGHT` (not `PAGE_HEIGHT * 0.7`) as the threshold
- Redraw section headers (SERVICES APPROVED / POSTPONED SERVICES) after page breaks
- Draw footer (separator + messages + page numbers) on EVERY page, not just the last

### 3. Font size must exceed line spacing

The legacy code used very tight spacing (font size 10 with only 5pt line advance). This works in jspdf 2.x but may cause visible overlap in jspdf 4.x if font metrics differ. The current values match the legacy exactly and produce correct output in jspdf 4.2.1:
- Explanation lines: font 10, advance 5pt per line, +2pt end padding
- Parts messages: font 9, advance 8pt
- Service name: font 10, advance 6pt
- Recommendation: font 9, advance 8pt

### 4. Customer info loading bug

When editing a saved quote, `reset()` was called AFTER `setCustomerInfo()`, wiping the just-loaded customer data:

```typescript
// WRONG — reset() wipes customerInfo
setCustomerInfo(data.customerInfo);
reset();

// CORRECT
reset();
setCustomerInfo(data.customerInfo);
```

Also, PocketBase may return JSON fields as strings or parsed objects. Defensively handle both:
```typescript
const ci = typeof data.customerInfo === 'string' ? JSON.parse(data.customerInfo) : data.customerInfo;
```

### 5. File locations

- Source: `/mnt/seagate8tb/Websites/ShopProQuote.backup-20260626-2058/spq-v2/src/lib/pdf.ts`
- Built: `/mnt/seagate8tb/Websites/ShopProQuote.backup-20260626-2058/spq-v2/dist/assets/QuoteGenerator-*.js`
- Legacy reference: `/mnt/seagate8tb/Websites/ShopProQuote/quote-tab-manager.js` (function `generateQuotePDF`)
