initial commit
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
# PDF Generation (jspdf 4.x)
|
||||
|
||||
The v2 app uses `jspdf` (v4.2.1) to generate quote PDFs. The generator lives at `src/lib/pdf.ts`.
|
||||
|
||||
## Critical Pitfalls
|
||||
|
||||
### 1. NEVER pass string arrays to `doc.text()`
|
||||
|
||||
In jspdf 4.x, `doc.text(stringArray, x, y)` uses internal matrix-based line spacing that differs from manual `array.length * lineHeight` calculations. This causes yPos desync — every subsequent element renders at the wrong position, causing cascading text overlap.
|
||||
|
||||
**WRONG:**
|
||||
```ts
|
||||
const lines = doc.splitTextToSize(text, maxWidth);
|
||||
doc.text(lines, x, y); // jspdf 4.x spacing != manual calc
|
||||
yPos += lines.length * 6 + 15; // DOESN'T MATCH where jspdf rendered
|
||||
```
|
||||
|
||||
**RIGHT:**
|
||||
```ts
|
||||
const lines = doc.splitTextToSize(text, maxWidth);
|
||||
lines.forEach((line) => { // render each line individually
|
||||
doc.text(line, x, yPos);
|
||||
yPos += 6; // explicit tracking per line
|
||||
});
|
||||
yPos += 15;
|
||||
```
|
||||
|
||||
### 2. Page-break logic must use content-bottom + footer-height
|
||||
|
||||
Calculate max y as `PAGE_HEIGHT - MARGIN - FOOTER_HEIGHT` (not just page edge). Footer is 35pt: separator line + message + page number.
|
||||
|
||||
Before rendering a service, compute its total height and check:
|
||||
```
|
||||
if (yPos + serviceHeight > PAGE_HEIGHT - MARGIN - FOOTER_HEIGHT) {
|
||||
doc.addPage();
|
||||
yPos = MARGIN;
|
||||
// Redraw section header (APPROVED / POSTPONED) on new page
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Section headers must be redrawn after page breaks
|
||||
|
||||
Track `currentGroupLabel` and `currentGroupColor`. When a page break occurs mid-section, redraw the section header at the top of the new page.
|
||||
|
||||
### 4. Footers on EVERY page, not just the last
|
||||
|
||||
Draw footers (separator + message + page number) in a post-render loop over all pages. Do NOT draw them inline — they'll only appear on the last page.
|
||||
|
||||
### 5. Height calculation must match actual yPos increments
|
||||
|
||||
The pre-render height estimate must use the same values as actual yPos advances:
|
||||
- Customer decision badge: 5pt
|
||||
- Name+price: 6pt
|
||||
- Priority (label+reason): 6+10=16pt
|
||||
- Recommendation: 8pt
|
||||
- Explanation lines: 5pt each + 2pt end padding
|
||||
- Parts messages: 8pt each
|
||||
- Service separator+spacing: 6pt
|
||||
|
||||
### 7. Service counting for totals: `!== 'declined'`, not `=== 'approved'`
|
||||
|
||||
The PDF's `servicesTotal` accumulator and `shopChargeableSubtotal` use `customerDecision !== 'declined'`. This means **both approved AND pending services** contribute to the pricing summary.
|
||||
|
||||
The UI summary (`computeQuoteTotals`) uses `customerDecision === 'approved'` — a deliberate difference. The PDF is customer-facing, so pending services must appear in the total so the customer sees the full cost. Only services the customer has explicitly declined are excluded.
|
||||
|
||||
Do NOT change the PDF filter to `=== 'approved'` — pending services would produce $0 totals.
|
||||
|
||||
Use `let boxBottomY` (not `const`) so it can be updated when a page break changes the box position. After rendering all summary rows, set `yPos = boxBottomY + 10` for the next content.
|
||||
|
||||
## File Locations
|
||||
|
||||
- Source: `src/lib/pdf.ts`
|
||||
- Built: `dist/assets/QuoteGenerator-*.js`
|
||||
- Test script pattern: write a `.mjs` file in the project root, import jspdf, generate, save with fs.writeFileSync
|
||||
Reference in New Issue
Block a user