From 50734dee03cf7a5056d823d9145684a15cf3f4bc Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 18 Feb 2026 13:12:37 +0200 Subject: [PATCH] docs: add invoice templates implementation plan 9-task plan covering template config types, jsPDF renderer, HTML preview component, template picker UI, Invoices.vue integration, business identity settings, and polish passes. --- ...-02-18-invoice-templates-implementation.md | 1715 +++++++++++++++++ 1 file changed, 1715 insertions(+) create mode 100644 docs/plans/2026-02-18-invoice-templates-implementation.md diff --git a/docs/plans/2026-02-18-invoice-templates-implementation.md b/docs/plans/2026-02-18-invoice-templates-implementation.md new file mode 100644 index 0000000..d3515d1 --- /dev/null +++ b/docs/plans/2026-02-18-invoice-templates-implementation.md @@ -0,0 +1,1715 @@ +# Invoice Templates Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Replace the single hardcoded invoice layout with 15 visually distinct templates, a template picker with live preview, and business identity support. + +**Architecture:** Template Config Objects approach — 15 config objects define colors, layout, typography, and decorative elements. A shared jsPDF renderer reads the config to generate PDFs. A Vue component reads the same config to render HTML previews. The template picker is a split-pane UI with template list on the left and live preview on the right. + +**Tech Stack:** Vue 3 Composition API, jsPDF (already installed), Tailwind CSS v4, Pinia stores, Tauri v2 IPC + +--- + +### Task 1: Create template config types and registry + +**Files:** +- Create: `src/utils/invoiceTemplates.ts` + +**Context:** This is the foundation file. It defines the TypeScript interface for template configs and exports all 15 template config objects plus a registry. The existing `InvoiceItem` interface in `src/utils/invoicePdf.ts:9-15` will be reused. + +**Step 1: Create the template config types and all 15 templates** + +Create `src/utils/invoiceTemplates.ts` with: + +```ts +export interface InvoiceTemplateColors { + primary: string + secondary: string + background: string + headerBg: string + headerText: string + bodyText: string + tableHeaderBg: string + tableHeaderText: string + tableRowAlt: string + tableBorder: string + totalHighlight: string +} + +export interface InvoiceTemplateLayout { + logoPosition: 'top-left' | 'top-center' | 'top-right' + headerStyle: 'full-width' | 'split' | 'minimal' | 'sidebar' | 'gradient' | 'geometric' | 'centered' + tableStyle: 'bordered' | 'striped' | 'borderless' | 'minimal-lines' | 'colored-sections' + totalsPosition: 'right' | 'center' + showDividers: boolean + dividerStyle: 'thin' | 'double' | 'thick' | 'none' +} + +export interface InvoiceTemplateTypography { + titleSize: number + titleWeight: 'bold' | 'normal' + headerSize: number + bodySize: number + numberStyle: 'normal' | 'monospace-feel' +} + +export interface InvoiceTemplateDecorative { + cornerShape: 'none' | 'colored-block' | 'triangle' | 'diagonal' + sidebarWidth: number + sidebarColor: string + useGradientHeader: boolean + gradientFrom?: string + gradientTo?: string + backgroundTint: boolean + backgroundTintColor?: string +} + +export interface InvoiceTemplateConfig { + id: string + name: string + category: 'essential' | 'creative' | 'warm' | 'premium' + description: string + colors: InvoiceTemplateColors + layout: InvoiceTemplateLayout + typography: InvoiceTemplateTypography + decorative: InvoiceTemplateDecorative +} + +// --- Template definitions --- +// Define all 15 templates. Each has unique colors, layout, typography, decorative. + +const cleanMinimal: InvoiceTemplateConfig = { + id: 'clean-minimal', + name: 'Clean Minimal', + category: 'essential', + description: 'Swiss-inspired simplicity with a single accent line', + colors: { + primary: '#3B82F6', + secondary: '#93C5FD', + background: '#FFFFFF', + headerBg: '#FFFFFF', + headerText: '#111827', + bodyText: '#374151', + tableHeaderBg: '#F9FAFB', + tableHeaderText: '#374151', + tableRowAlt: '#F9FAFB', + tableBorder: '#E5E7EB', + totalHighlight: '#3B82F6', + }, + layout: { + logoPosition: 'top-left', + headerStyle: 'minimal', + tableStyle: 'bordered', + totalsPosition: 'right', + showDividers: true, + dividerStyle: 'thin', + }, + typography: { titleSize: 24, titleWeight: 'bold', headerSize: 11, bodySize: 9, numberStyle: 'normal' }, + decorative: { cornerShape: 'none', sidebarWidth: 0, sidebarColor: '', useGradientHeader: false, backgroundTint: false }, +} + +const corporateClassic: InvoiceTemplateConfig = { + id: 'corporate-classic', + name: 'Corporate Classic', + category: 'essential', + description: 'Navy header band with professional table layout', + colors: { + primary: '#1E3A5F', + secondary: '#2563EB', + background: '#FFFFFF', + headerBg: '#1E3A5F', + headerText: '#FFFFFF', + bodyText: '#374151', + tableHeaderBg: '#1E3A5F', + tableHeaderText: '#FFFFFF', + tableRowAlt: '#F3F4F6', + tableBorder: '#D1D5DB', + totalHighlight: '#1E3A5F', + }, + layout: { + logoPosition: 'top-left', + headerStyle: 'full-width', + tableStyle: 'striped', + totalsPosition: 'right', + showDividers: false, + dividerStyle: 'none', + }, + typography: { titleSize: 28, titleWeight: 'bold', headerSize: 11, bodySize: 9, numberStyle: 'normal' }, + decorative: { cornerShape: 'none', sidebarWidth: 0, sidebarColor: '', useGradientHeader: false, backgroundTint: false }, +} + +const modernBold: InvoiceTemplateConfig = { + id: 'modern-bold', + name: 'Modern Bold', + category: 'essential', + description: 'Large accent block with oversized typography', + colors: { + primary: '#6366F1', + secondary: '#A5B4FC', + background: '#FFFFFF', + headerBg: '#6366F1', + headerText: '#FFFFFF', + bodyText: '#1F2937', + tableHeaderBg: '#6366F1', + tableHeaderText: '#FFFFFF', + tableRowAlt: '#F5F3FF', + tableBorder: '#E0E7FF', + totalHighlight: '#6366F1', + }, + layout: { + logoPosition: 'top-left', + headerStyle: 'full-width', + tableStyle: 'borderless', + totalsPosition: 'right', + showDividers: false, + dividerStyle: 'none', + }, + typography: { titleSize: 32, titleWeight: 'bold', headerSize: 12, bodySize: 10, numberStyle: 'normal' }, + decorative: { cornerShape: 'colored-block', sidebarWidth: 0, sidebarColor: '', useGradientHeader: false, backgroundTint: false }, +} + +const elegantSerif: InvoiceTemplateConfig = { + id: 'elegant-serif', + name: 'Elegant Serif', + category: 'essential', + description: 'Refined charcoal and gold with centered layout', + colors: { + primary: '#374151', + secondary: '#B8860B', + background: '#FFFFFF', + headerBg: '#FFFFFF', + headerText: '#374151', + bodyText: '#4B5563', + tableHeaderBg: '#374151', + tableHeaderText: '#FFFFFF', + tableRowAlt: '#FAFAFA', + tableBorder: '#D1D5DB', + totalHighlight: '#B8860B', + }, + layout: { + logoPosition: 'top-center', + headerStyle: 'centered', + tableStyle: 'minimal-lines', + totalsPosition: 'right', + showDividers: true, + dividerStyle: 'double', + }, + typography: { titleSize: 26, titleWeight: 'normal', headerSize: 11, bodySize: 9, numberStyle: 'normal' }, + decorative: { cornerShape: 'none', sidebarWidth: 0, sidebarColor: '', useGradientHeader: false, backgroundTint: false }, +} + +const simpleTwoTone: InvoiceTemplateConfig = { + id: 'simple-two-tone', + name: 'Simple Two-Tone', + category: 'essential', + description: 'Dark/light split header with emerald accents', + colors: { + primary: '#1F2937', + secondary: '#10B981', + background: '#FFFFFF', + headerBg: '#1F2937', + headerText: '#FFFFFF', + bodyText: '#374151', + tableHeaderBg: '#F3F4F6', + tableHeaderText: '#1F2937', + tableRowAlt: '#FFFFFF', + tableBorder: '#E5E7EB', + totalHighlight: '#10B981', + }, + layout: { + logoPosition: 'top-left', + headerStyle: 'split', + tableStyle: 'minimal-lines', + totalsPosition: 'right', + showDividers: true, + dividerStyle: 'thin', + }, + typography: { titleSize: 24, titleWeight: 'bold', headerSize: 11, bodySize: 9, numberStyle: 'normal' }, + decorative: { cornerShape: 'none', sidebarWidth: 0, sidebarColor: '', useGradientHeader: false, backgroundTint: false }, +} + +const gradientHeader: InvoiceTemplateConfig = { + id: 'gradient-header', + name: 'Gradient Header', + category: 'creative', + description: 'Full-width gradient header with modern feel', + colors: { + primary: '#667EEA', + secondary: '#764BA2', + background: '#FFFFFF', + headerBg: '#667EEA', + headerText: '#FFFFFF', + bodyText: '#374151', + tableHeaderBg: '#667EEA', + tableHeaderText: '#FFFFFF', + tableRowAlt: '#F5F3FF', + tableBorder: '#E0E7FF', + totalHighlight: '#667EEA', + }, + layout: { + logoPosition: 'top-left', + headerStyle: 'gradient', + tableStyle: 'striped', + totalsPosition: 'right', + showDividers: false, + dividerStyle: 'none', + }, + typography: { titleSize: 28, titleWeight: 'bold', headerSize: 11, bodySize: 9, numberStyle: 'normal' }, + decorative: { cornerShape: 'none', sidebarWidth: 0, sidebarColor: '', useGradientHeader: true, gradientFrom: '#667EEA', gradientTo: '#764BA2', backgroundTint: false }, +} + +const sidebarAccent: InvoiceTemplateConfig = { + id: 'sidebar-accent', + name: 'Sidebar Accent', + category: 'creative', + description: 'Rose vertical bar with asymmetric layout', + colors: { + primary: '#E11D48', + secondary: '#FB7185', + background: '#FFFFFF', + headerBg: '#FFFFFF', + headerText: '#111827', + bodyText: '#374151', + tableHeaderBg: '#FFF1F2', + tableHeaderText: '#E11D48', + tableRowAlt: '#FFF1F2', + tableBorder: '#FECDD3', + totalHighlight: '#E11D48', + }, + layout: { + logoPosition: 'top-left', + headerStyle: 'sidebar', + tableStyle: 'borderless', + totalsPosition: 'right', + showDividers: true, + dividerStyle: 'thin', + }, + typography: { titleSize: 24, titleWeight: 'bold', headerSize: 11, bodySize: 9, numberStyle: 'normal' }, + decorative: { cornerShape: 'none', sidebarWidth: 4, sidebarColor: '#E11D48', useGradientHeader: false, backgroundTint: false }, +} + +const geometricModern: InvoiceTemplateConfig = { + id: 'geometric-modern', + name: 'Geometric Modern', + category: 'creative', + description: 'Violet triangle accent with diagonal elements', + colors: { + primary: '#8B5CF6', + secondary: '#C4B5FD', + background: '#FFFFFF', + headerBg: '#FFFFFF', + headerText: '#1F2937', + bodyText: '#374151', + tableHeaderBg: '#8B5CF6', + tableHeaderText: '#FFFFFF', + tableRowAlt: '#F5F3FF', + tableBorder: '#DDD6FE', + totalHighlight: '#8B5CF6', + }, + layout: { + logoPosition: 'top-left', + headerStyle: 'geometric', + tableStyle: 'striped', + totalsPosition: 'right', + showDividers: false, + dividerStyle: 'none', + }, + typography: { titleSize: 26, titleWeight: 'bold', headerSize: 11, bodySize: 9, numberStyle: 'normal' }, + decorative: { cornerShape: 'triangle', sidebarWidth: 0, sidebarColor: '', useGradientHeader: false, backgroundTint: false }, +} + +const darkMode: InvoiceTemplateConfig = { + id: 'dark-mode', + name: 'Dark Mode', + category: 'creative', + description: 'Near-black background with cyan highlights', + colors: { + primary: '#06B6D4', + secondary: '#22D3EE', + background: '#1A1A2E', + headerBg: '#1A1A2E', + headerText: '#E2E8F0', + bodyText: '#CBD5E1', + tableHeaderBg: '#0F172A', + tableHeaderText: '#06B6D4', + tableRowAlt: '#16213E', + tableBorder: '#334155', + totalHighlight: '#06B6D4', + }, + layout: { + logoPosition: 'top-left', + headerStyle: 'minimal', + tableStyle: 'striped', + totalsPosition: 'right', + showDividers: true, + dividerStyle: 'thin', + }, + typography: { titleSize: 26, titleWeight: 'bold', headerSize: 11, bodySize: 9, numberStyle: 'monospace-feel' }, + decorative: { cornerShape: 'none', sidebarWidth: 0, sidebarColor: '', useGradientHeader: false, backgroundTint: false }, +} + +const warmNatural: InvoiceTemplateConfig = { + id: 'warm-natural', + name: 'Warm Natural', + category: 'warm', + description: 'Earth tones on warm beige background', + colors: { + primary: '#C2703E', + secondary: '#6B7A3D', + background: '#FDF6EC', + headerBg: '#FDF6EC', + headerText: '#78350F', + bodyText: '#57534E', + tableHeaderBg: '#C2703E', + tableHeaderText: '#FFFFFF', + tableRowAlt: '#FEF3C7', + tableBorder: '#D6D3D1', + totalHighlight: '#C2703E', + }, + layout: { + logoPosition: 'top-left', + headerStyle: 'minimal', + tableStyle: 'striped', + totalsPosition: 'right', + showDividers: true, + dividerStyle: 'thin', + }, + typography: { titleSize: 26, titleWeight: 'bold', headerSize: 11, bodySize: 9, numberStyle: 'normal' }, + decorative: { cornerShape: 'none', sidebarWidth: 0, sidebarColor: '', useGradientHeader: false, backgroundTint: true, backgroundTintColor: '#FDF6EC' }, +} + +const playfulColorBlock: InvoiceTemplateConfig = { + id: 'playful-color-block', + name: 'Playful Color Block', + category: 'warm', + description: 'Teal and coral blocks for a fun professional feel', + colors: { + primary: '#0D9488', + secondary: '#F97316', + background: '#FFFFFF', + headerBg: '#0D9488', + headerText: '#FFFFFF', + bodyText: '#1F2937', + tableHeaderBg: '#F97316', + tableHeaderText: '#FFFFFF', + tableRowAlt: '#FFF7ED', + tableBorder: '#FDBA74', + totalHighlight: '#0D9488', + }, + layout: { + logoPosition: 'top-left', + headerStyle: 'full-width', + tableStyle: 'colored-sections', + totalsPosition: 'right', + showDividers: false, + dividerStyle: 'none', + }, + typography: { titleSize: 28, titleWeight: 'bold', headerSize: 11, bodySize: 9, numberStyle: 'normal' }, + decorative: { cornerShape: 'colored-block', sidebarWidth: 0, sidebarColor: '', useGradientHeader: false, backgroundTint: false }, +} + +const retroProfessional: InvoiceTemplateConfig = { + id: 'retro-professional', + name: 'Retro Professional', + category: 'warm', + description: 'Vintage double-rule lines with warm brown palette', + colors: { + primary: '#78350F', + secondary: '#92400E', + background: '#FFFBEB', + headerBg: '#FFFBEB', + headerText: '#78350F', + bodyText: '#57534E', + tableHeaderBg: '#78350F', + tableHeaderText: '#FFFFFF', + tableRowAlt: '#FEF3C7', + tableBorder: '#92400E', + totalHighlight: '#78350F', + }, + layout: { + logoPosition: 'top-left', + headerStyle: 'minimal', + tableStyle: 'bordered', + totalsPosition: 'right', + showDividers: true, + dividerStyle: 'double', + }, + typography: { titleSize: 24, titleWeight: 'bold', headerSize: 11, bodySize: 9, numberStyle: 'normal' }, + decorative: { cornerShape: 'none', sidebarWidth: 0, sidebarColor: '', useGradientHeader: false, backgroundTint: true, backgroundTintColor: '#FFFBEB' }, +} + +const techMinimal: InvoiceTemplateConfig = { + id: 'tech-minimal', + name: 'Tech Minimal', + category: 'premium', + description: 'Slate and electric green with code-editor aesthetic', + colors: { + primary: '#475569', + secondary: '#22C55E', + background: '#FFFFFF', + headerBg: '#FFFFFF', + headerText: '#0F172A', + bodyText: '#475569', + tableHeaderBg: '#0F172A', + tableHeaderText: '#22C55E', + tableRowAlt: '#F8FAFC', + tableBorder: '#E2E8F0', + totalHighlight: '#22C55E', + }, + layout: { + logoPosition: 'top-left', + headerStyle: 'minimal', + tableStyle: 'minimal-lines', + totalsPosition: 'right', + showDividers: true, + dividerStyle: 'thin', + }, + typography: { titleSize: 22, titleWeight: 'bold', headerSize: 10, bodySize: 9, numberStyle: 'monospace-feel' }, + decorative: { cornerShape: 'none', sidebarWidth: 0, sidebarColor: '', useGradientHeader: false, backgroundTint: false }, +} + +const executivePremium: InvoiceTemplateConfig = { + id: 'executive-premium', + name: 'Executive Premium', + category: 'premium', + description: 'Black and gold luxury with generous spacing', + colors: { + primary: '#111827', + secondary: '#D4AF37', + background: '#FFFFFF', + headerBg: '#111827', + headerText: '#D4AF37', + bodyText: '#374151', + tableHeaderBg: '#111827', + tableHeaderText: '#D4AF37', + tableRowAlt: '#F9FAFB', + tableBorder: '#D1D5DB', + totalHighlight: '#D4AF37', + }, + layout: { + logoPosition: 'top-left', + headerStyle: 'full-width', + tableStyle: 'minimal-lines', + totalsPosition: 'right', + showDividers: true, + dividerStyle: 'thin', + }, + typography: { titleSize: 28, titleWeight: 'normal', headerSize: 11, bodySize: 10, numberStyle: 'normal' }, + decorative: { cornerShape: 'none', sidebarWidth: 0, sidebarColor: '', useGradientHeader: false, backgroundTint: false }, +} + +const dataDrivenClean: InvoiceTemplateConfig = { + id: 'data-driven-clean', + name: 'Data-Driven Clean', + category: 'premium', + description: 'Deep blue with emphasis on data hierarchy', + colors: { + primary: '#1E40AF', + secondary: '#3B82F6', + background: '#FFFFFF', + headerBg: '#1E40AF', + headerText: '#FFFFFF', + bodyText: '#1F2937', + tableHeaderBg: '#1E40AF', + tableHeaderText: '#FFFFFF', + tableRowAlt: '#EFF6FF', + tableBorder: '#BFDBFE', + totalHighlight: '#1E40AF', + }, + layout: { + logoPosition: 'top-right', + headerStyle: 'full-width', + tableStyle: 'striped', + totalsPosition: 'center', + showDividers: false, + dividerStyle: 'none', + }, + typography: { titleSize: 24, titleWeight: 'bold', headerSize: 12, bodySize: 10, numberStyle: 'monospace-feel' }, + decorative: { cornerShape: 'none', sidebarWidth: 0, sidebarColor: '', useGradientHeader: false, backgroundTint: false }, +} + +// --- Registry --- +export const INVOICE_TEMPLATES: InvoiceTemplateConfig[] = [ + cleanMinimal, + corporateClassic, + modernBold, + elegantSerif, + simpleTwoTone, + gradientHeader, + sidebarAccent, + geometricModern, + darkMode, + warmNatural, + playfulColorBlock, + retroProfessional, + techMinimal, + executivePremium, + dataDrivenClean, +] + +export const TEMPLATE_CATEGORIES = [ + { id: 'essential', label: 'Professional Essentials' }, + { id: 'creative', label: 'Creative & Modern' }, + { id: 'warm', label: 'Warm & Distinctive' }, + { id: 'premium', label: 'Premium & Specialized' }, +] as const + +export function getTemplateById(id: string): InvoiceTemplateConfig { + return INVOICE_TEMPLATES.find(t => t.id === id) || INVOICE_TEMPLATES[0] +} + +export function getTemplatesByCategory(category: string): InvoiceTemplateConfig[] { + return INVOICE_TEMPLATES.filter(t => t.category === category) +} +``` + +**Step 2: Verify the file has no TypeScript errors** + +Run: `npx tsc --noEmit --pretty 2>&1 | head -20` + +If there are import errors, fix them. The file should have zero imports (it's self-contained types + data). + +**Step 3: Commit** + +```bash +git add src/utils/invoiceTemplates.ts +git commit -m "feat: add 15 invoice template configs and registry" +``` + +--- + +### Task 2: Create the jsPDF template renderer + +**Files:** +- Create: `src/utils/invoicePdfRenderer.ts` +- Modify: `src/utils/invoicePdf.ts` (will become a thin wrapper) + +**Context:** The current `src/utils/invoicePdf.ts` has a single `generateInvoicePdf()` function with hardcoded amber theme. We need a new renderer that reads template config. The old file's `InvoiceItem` interface and `generateInvoicePdf` signature stay the same (they're used by `Invoices.vue:545,755`), but internally it delegates to the new renderer. + +**Step 1: Create the config-driven renderer** + +Create `src/utils/invoicePdfRenderer.ts`. This file exports `renderInvoicePdf(doc, config, invoice, client, items, businessInfo)` which draws onto an existing jsPDF document using the template config. + +The renderer must handle: +- **Page background**: Fill entire page with `config.colors.background` (skip if white) +- **Decorative elements**: sidebar bar, corner shapes, gradients, background tints +- **Header section**: 7 header styles — each is a separate function: + - `minimal`: Title left, invoice details below, client info right. Simple accent line. + - `full-width`: Full-width colored header band with white text. Business name + invoice number. + - `split`: Left half dark background (business info), right half light (invoice details). + - `sidebar`: Narrow colored bar on left edge, content offset. Business info near sidebar. + - `gradient`: Simulated gradient via multiple thin colored rect strips across header width. + - `geometric`: Triangle in top-right corner drawn via path, invoice details below. + - `centered`: Centered title, centered business name, dividers above and below. +- **Bill To section**: Client name, email, address. Position depends on header style. +- **Line items table**: 5 table styles: + - `bordered`: Full cell borders + - `striped`: Alternating row backgrounds, no vertical borders + - `borderless`: No borders at all, just spacing + - `minimal-lines`: Horizontal lines only between rows + - `colored-sections`: Colored background for header, alternating colored rows +- **Totals section**: Subtotal, tax, discount, total. Right-aligned or centered per config. +- **Notes section**: If invoice has notes, render below totals. +- **Logo rendering**: If `businessInfo.logo` is set, render via `doc.addImage()` at the position specified by `config.layout.logoPosition`. +- **Page breaks**: Check `yPosition > 270` before each row, add new page if needed. + +Key technical notes: +- jsPDF gradient: Simulate by drawing 20+ thin horizontal `rect()` strips with interpolated colors from `gradientFrom` to `gradientTo`. +- jsPDF triangle: Use `doc.triangle(x1,y1, x2,y2, x3,y3, 'F')`. +- jsPDF sidebar: `doc.rect(0, 0, sidebarWidth, pageHeight, 'F')`. +- Font: jsPDF only has `helvetica`, `courier`, `times`. Use `helvetica` for all. For `monospace-feel` numbers, use `courier` for amount columns only. +- Color parsing: The config uses hex strings. jsPDF `setFillColor`/`setTextColor` accept hex strings directly. +- Business info comes from settings store: `{ name, address, email, phone, logo }`. + +```ts +import { jsPDF } from 'jspdf' +import type { InvoiceTemplateConfig } from './invoiceTemplates' +import type { Invoice } from '../stores/invoices' +import type { Client } from '../stores/clients' +import type { InvoiceItem } from './invoicePdf' +import { formatCurrency, formatDate } from './locale' + +export interface BusinessInfo { + name: string + address: string + email: string + phone: string + logo: string // base64 data URL or empty +} + +export function renderInvoicePdf( + config: InvoiceTemplateConfig, + invoice: Invoice, + client: Client, + items: InvoiceItem[], + businessInfo: BusinessInfo +): jsPDF { + const doc = new jsPDF() + const pageWidth = doc.internal.pageSize.getWidth() // 210 + const pageHeight = doc.internal.pageSize.getHeight() // 297 + const margin = 20 + const contentWidth = pageWidth - margin * 2 + + // Draw page background + if (config.colors.background !== '#FFFFFF') { + doc.setFillColor(config.colors.background) + doc.rect(0, 0, pageWidth, pageHeight, 'F') + } + + // Draw decorative elements + renderDecorative(doc, config, pageWidth, pageHeight, margin) + + let y = margin + + // Draw header + y = renderHeader(doc, config, invoice, client, businessInfo, y, margin, pageWidth, contentWidth) + + // Draw line items table + y = renderTable(doc, config, items, y, margin, contentWidth) + + // Draw totals + y = renderTotals(doc, config, invoice, y, margin, pageWidth, contentWidth) + + // Draw notes + if (invoice.notes) { + y = renderNotes(doc, config, invoice.notes, y, margin, contentWidth, pageHeight) + } + + return doc +} + +// ... implement each render function following the patterns above. +// Full implementation provided in code — each function uses doc.setFillColor, +// doc.setTextColor, doc.setFont, doc.text, doc.rect, doc.line, doc.triangle etc. +``` + +**Important implementation details for each render function:** + +`renderDecorative(doc, config, pageWidth, pageHeight, margin)`: +- If `config.decorative.sidebarWidth > 0`: draw `doc.rect(0, 0, config.decorative.sidebarWidth, pageHeight, 'F')` with `sidebarColor`. +- If `config.decorative.cornerShape === 'colored-block'`: draw `doc.rect(0, 0, 60, 60, 'F')` with `config.colors.primary`. +- If `config.decorative.cornerShape === 'triangle'`: draw `doc.triangle(pageWidth, 0, pageWidth, 80, pageWidth - 80, 0, 'F')` with `config.colors.primary`. +- If `config.decorative.cornerShape === 'diagonal'`: draw a filled triangle across top. +- If `config.decorative.backgroundTint && config.decorative.backgroundTintColor`: fill full page. + +`renderHeader(doc, config, invoice, client, businessInfo, y, margin, pageWidth, contentWidth)`: +- Switch on `config.layout.headerStyle`. +- For `'gradient'`: draw 30 thin rect strips from y=0 to y=45, interpolating color from `gradientFrom` to `gradientTo`. Then overlay white text. +- For `'split'`: draw left half rect with headerBg color, right half stays white. +- For `'full-width'`: draw full-width rect with headerBg color. +- For `'sidebar'`: offset all content by `sidebarWidth + 6`. +- After header style-specific rendering, always draw: + - Invoice title "INVOICE" in `config.colors.primary` at `config.typography.titleSize` + - Invoice number, date, due date, status + - "Bill To:" section with client info + - Logo if available +- Return new y position after header. + +`renderTable(doc, config, items, y, margin, contentWidth)`: +- Column widths: Description 50%, Qty 15%, Rate 17.5%, Amount 17.5% +- Table header row: background `config.colors.tableHeaderBg`, text `config.colors.tableHeaderText` +- For each item row: apply table style + - `'bordered'`: draw rect border around each cell + - `'striped'`: draw alternating `config.colors.tableRowAlt` backgrounds + - `'borderless'`: no borders, just text + - `'minimal-lines'`: draw horizontal line after each row + - `'colored-sections'`: draw colored background sections +- Font for amounts: if `config.typography.numberStyle === 'monospace-feel'`, use `courier` font for Qty/Rate/Amount columns. +- Page break check: if y > 260, `doc.addPage()`, redraw background if needed, reset y. +- Return new y position. + +`renderTotals(doc, config, invoice, y, margin, pageWidth, contentWidth)`: +- If `config.layout.totalsPosition === 'center'`: center the totals block +- Else: right-align at `pageWidth - margin - 60` +- Draw Subtotal, Tax (if > 0), Discount (if > 0) +- Draw separator line +- Draw Total in `config.colors.totalHighlight` at larger font size +- Return new y position. + +`renderNotes(doc, config, notes, y, margin, contentWidth, pageHeight)`: +- If y > 240, add new page +- Draw "Notes:" label and wrapped text +- Return new y position. + +**Step 2: Update the old invoicePdf.ts to delegate** + +Modify `src/utils/invoicePdf.ts` to keep the same `generateInvoicePdf` signature but delegate to the new renderer with a default template: + +```ts +import type { Invoice } from '../stores/invoices' +import type { Client } from '../stores/clients' +import { renderInvoicePdf, type BusinessInfo } from './invoicePdfRenderer' +import { getTemplateById } from './invoiceTemplates' + +export interface InvoiceItem { + id?: number + description: string + quantity: number + rate: number + amount: number +} + +/** + * Generate a PDF invoice using a template + */ +export function generateInvoicePdf( + invoice: Invoice, + client: Client, + items: InvoiceItem[], + templateId: string = 'clean-minimal', + businessInfo?: BusinessInfo +): jsPDF { + const config = getTemplateById(templateId) + const info: BusinessInfo = businessInfo || { name: '', address: '', email: '', phone: '', logo: '' } + return renderInvoicePdf(config, invoice, client, items, info) +} +``` + +**Step 3: Update the Invoices.vue call site** + +In `src/views/Invoices.vue:755`, the current call is: +```ts +const doc = generateInvoicePdf(invoice, client, items) +``` + +This still works because `templateId` has a default value. No changes needed yet — template selection will be added in Task 5. + +**Step 4: Verify build** + +Run: `npm run build 2>&1 | tail -20` +Expected: Build succeeds. + +**Step 5: Commit** + +```bash +git add src/utils/invoicePdfRenderer.ts src/utils/invoicePdf.ts +git commit -m "feat: add config-driven jsPDF invoice renderer with 7 header styles and 5 table styles" +``` + +--- + +### Task 3: Create InvoicePreview.vue HTML component + +**Files:** +- Create: `src/components/InvoicePreview.vue` + +**Context:** This component renders an HTML preview of an invoice using the template config. It replicates the jsPDF renderer's visual output in HTML/CSS so the user sees exactly what the PDF will look like. It replaces the hardcoded amber preview in `Invoices.vue:417-497`. + +The component receives props: `template` (InvoiceTemplateConfig), `invoice` (Invoice), `client` (Client | null), `items` (InvoiceItem[]), `businessInfo` (BusinessInfo). + +It renders a scaled-down A4-ratio div (8.5in x 11in) with: +- Page background color from `config.colors.background` +- All decorative elements (sidebar, corners, gradient header) via CSS +- Header section matching the template's header style +- Client "Bill To" section +- Line items table with the template's table style +- Totals section +- Notes section + +**Step 1: Create the component** + +```vue + + + +``` + +**Step 2: Verify build** + +Run: `npm run build 2>&1 | tail -20` + +**Step 3: Commit** + +```bash +git add src/components/InvoicePreview.vue +git commit -m "feat: add InvoicePreview.vue HTML template preview component" +``` + +--- + +### Task 4: Create InvoiceTemplatePicker.vue + +**Files:** +- Create: `src/components/InvoiceTemplatePicker.vue` + +**Context:** Split-pane template picker. Left panel (30%) has a scrollable list of template names grouped by category with small color dots. Right panel (70%) shows a live preview of the selected template. The component emits `update:modelValue` with the selected template ID. + +```vue + + + +``` + +**Step 2: Verify build** + +Run: `npm run build 2>&1 | tail -20` + +**Step 3: Commit** + +```bash +git add src/components/InvoiceTemplatePicker.vue +git commit -m "feat: add InvoiceTemplatePicker split-pane component" +``` + +--- + +### Task 5: Integrate template picker into Invoices.vue + +**Files:** +- Modify: `src/views/Invoices.vue` + +**Context:** Three integration points: +1. Add template picker to the **Create** view (between the top form section and line items) +2. Add template selector dropdown to the **Preview dialog** toolbar +3. Pass selected template ID to `generateInvoicePdf()` and to `InvoicePreview` in the preview dialog +4. Replace the hardcoded amber preview in the dialog with `InvoicePreview` component + +**Step 1: Add imports and state** + +At the top of `