docs: add invoice templates v2 implementation plan

This commit is contained in:
Your Name
2026-02-18 14:32:38 +02:00
parent 5e47700c93
commit 9e9c0c78f1

View File

@@ -0,0 +1,992 @@
# Invoice Templates v2 Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Replace the invoice template system with 15 genuinely beautiful, typographically distinct templates and a two-step UX flow (create invoice → full-screen template picker).
**Architecture:** The invoices view gains a third state `'template-picker'` that shows a full-screen split-pane picker after invoice creation. Each of the 15 templates has its own unique HTML layout in `InvoicePreview.vue` and its own PDF render function in `invoicePdfRenderer.ts`, following researched typography scales. A new `template_id` column on the invoices table persists the user's template choice.
**Tech Stack:** Vue 3 Composition API, Tailwind CSS v4, jsPDF, Tauri v2 (Rust/rusqlite), Pinia
---
### Task 1: Add template_id column to database and Rust backend
**Files:**
- Modify: `src-tauri/src/database.rs:94-112` (invoices table area)
- Modify: `src-tauri/src/commands.rs:52-65` (Invoice struct)
- Modify: `src-tauri/src/commands.rs:317-367` (create/get/update invoice commands)
- Modify: `src-tauri/src/lib.rs:39-89` (register new command)
**Step 1: Add migration in database.rs**
After the invoices CREATE TABLE (around line 112), add a safe migration using the same pattern as clients/projects:
```rust
// Migrate invoices table — add template_id column (safe to re-run)
let invoice_migrations = [
"ALTER TABLE invoices ADD COLUMN template_id TEXT DEFAULT 'clean'",
];
for sql in &invoice_migrations {
match conn.execute(sql, []) {
Ok(_) => {}
Err(e) => {
let msg = e.to_string();
if !msg.contains("duplicate column") {
return Err(e);
}
}
}
}
```
**Step 2: Add template_id to Invoice struct in commands.rs**
Add to the `Invoice` struct (after `status` field at line 64):
```rust
pub template_id: Option<String>,
```
**Step 3: Update get_invoices to SELECT template_id**
In `get_invoices` (line 330), change the SELECT to include `template_id` at position 12:
```rust
"SELECT id, client_id, invoice_number, date, due_date, subtotal, tax_rate, tax_amount, discount, total, notes, status, template_id
FROM invoices ORDER BY date DESC"
```
And in the row mapping, add after `status: row.get(11)?`:
```rust
template_id: row.get(12)?,
```
**Step 4: Update create_invoice to INSERT template_id**
In `create_invoice` (line 317), change the INSERT:
```rust
"INSERT INTO invoices (client_id, invoice_number, date, due_date, subtotal, tax_rate, tax_amount, discount, total, notes, status, template_id)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
params![invoice.client_id, invoice.invoice_number, invoice.date, invoice.due_date,
invoice.subtotal, invoice.tax_rate, invoice.tax_amount, invoice.discount,
invoice.total, invoice.notes, invoice.status, invoice.template_id],
```
**Step 5: Update update_invoice to SET template_id**
In `update_invoice` (line 356), change the UPDATE:
```rust
"UPDATE invoices SET client_id = ?1, invoice_number = ?2, date = ?3, due_date = ?4,
subtotal = ?5, tax_rate = ?6, tax_amount = ?7, discount = ?8, total = ?9, notes = ?10, status = ?11, template_id = ?12
WHERE id = ?13",
params![invoice.client_id, invoice.invoice_number, invoice.date, invoice.due_date,
invoice.subtotal, invoice.tax_rate, invoice.tax_amount, invoice.discount,
invoice.total, invoice.notes, invoice.status, invoice.template_id, invoice.id],
```
**Step 6: Add update_invoice_template command**
Add a new lightweight command after `delete_invoice` (after line 374):
```rust
#[tauri::command]
pub fn update_invoice_template(state: State<AppState>, id: i64, template_id: String) -> Result<(), String> {
let conn = state.db.lock().map_err(|e| e.to_string())?;
conn.execute(
"UPDATE invoices SET template_id = ?1 WHERE id = ?2",
params![template_id, id],
).map_err(|e| e.to_string())?;
Ok(())
}
```
**Step 7: Register in lib.rs**
Add `commands::update_invoice_template,` after the `commands::delete_invoice,` line in the `generate_handler!` macro.
**Step 8: Verify**
Run: `cd src-tauri && cargo build`
Expected: Compiles with no errors.
**Step 9: Commit**
```bash
git add src-tauri/src/database.rs src-tauri/src/commands.rs src-tauri/src/lib.rs
git commit -m "feat: add template_id column to invoices table and update_invoice_template command"
```
---
### Task 2: Update frontend Invoice interface and store
**Files:**
- Modify: `src/stores/invoices.ts:5-18` (Invoice interface)
- Modify: `src/stores/invoices.ts` (add updateInvoiceTemplate action)
**Step 1: Add template_id to Invoice interface**
In `src/stores/invoices.ts`, add `template_id` to the `Invoice` interface (after `status: string` at line 17):
```typescript
template_id?: string
```
**Step 2: Add updateInvoiceTemplate store action**
Add after the `deleteInvoice` function (after line 79):
```typescript
async function updateInvoiceTemplate(id: number, templateId: string): Promise<boolean> {
try {
await invoke('update_invoice_template', { id, templateId })
const index = invoices.value.findIndex(i => i.id === id)
if (index !== -1) {
invoices.value[index].template_id = templateId
}
return true
} catch (error) {
console.error('Failed to update invoice template:', error)
return false
}
}
```
**Step 3: Export the new action**
Add `updateInvoiceTemplate` to the return object (around line 111-121).
**Step 4: Commit**
```bash
git add src/stores/invoices.ts
git commit -m "feat: add template_id to Invoice interface and updateInvoiceTemplate action"
```
---
### Task 3: Rewrite invoiceTemplates.ts with new IDs and design doc colors
**Files:**
- Rewrite: `src/utils/invoiceTemplates.ts`
**Step 1: Rewrite the entire file**
Replace the entire file with new template configs matching the design doc's 15 templates. The new IDs are: `clean`, `professional`, `bold`, `minimal`, `classic`, `modern`, `elegant`, `creative`, `compact`, `dark`, `vibrant`, `corporate`, `fresh`, `natural`, `statement`.
The interface stays simple — `id`, `name`, `category`, `description`, `colors` — but the color object maps to the exact hex values from the design doc for each template.
```typescript
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 InvoiceTemplateConfig {
id: string
name: string
category: 'essential' | 'creative' | 'warm' | 'premium'
description: string
colors: InvoiceTemplateColors
}
const clean: InvoiceTemplateConfig = {
id: 'clean',
name: 'Clean',
category: 'essential',
description: 'Swiss minimalism with a single blue accent',
colors: {
primary: '#1e293b',
secondary: '#3b82f6',
background: '#ffffff',
headerBg: '#ffffff',
headerText: '#1e293b',
bodyText: '#374151',
tableHeaderBg: '#f8fafc',
tableHeaderText: '#374151',
tableRowAlt: '#f8fafc',
tableBorder: '#e5e7eb',
totalHighlight: '#3b82f6',
},
}
const professional: InvoiceTemplateConfig = {
id: 'professional',
name: 'Professional',
category: 'essential',
description: 'Navy header band with corporate polish',
colors: {
primary: '#1e3a5f',
secondary: '#2563eb',
background: '#ffffff',
headerBg: '#1e3a5f',
headerText: '#ffffff',
bodyText: '#374151',
tableHeaderBg: '#1e3a5f',
tableHeaderText: '#ffffff',
tableRowAlt: '#f3f4f6',
tableBorder: '#d1d5db',
totalHighlight: '#1e3a5f',
},
}
const bold: InvoiceTemplateConfig = {
id: 'bold',
name: 'Bold',
category: 'essential',
description: 'Large indigo block with oversized typography',
colors: {
primary: '#4f46e5',
secondary: '#a5b4fc',
background: '#ffffff',
headerBg: '#4f46e5',
headerText: '#ffffff',
bodyText: '#1f2937',
tableHeaderBg: '#4f46e5',
tableHeaderText: '#ffffff',
tableRowAlt: '#f5f3ff',
tableBorder: '#e0e7ff',
totalHighlight: '#4f46e5',
},
}
const minimal: InvoiceTemplateConfig = {
id: 'minimal',
name: 'Minimal',
category: 'essential',
description: 'Pure monochrome centered layout',
colors: {
primary: '#18181b',
secondary: '#18181b',
background: '#ffffff',
headerBg: '#ffffff',
headerText: '#18181b',
bodyText: '#3f3f46',
tableHeaderBg: '#ffffff',
tableHeaderText: '#18181b',
tableRowAlt: '#ffffff',
tableBorder: '#e4e4e7',
totalHighlight: '#18181b',
},
}
const classic: InvoiceTemplateConfig = {
id: 'classic',
name: 'Classic',
category: 'essential',
description: 'Traditional layout with burgundy accents',
colors: {
primary: '#7f1d1d',
secondary: '#991b1b',
background: '#ffffff',
headerBg: '#7f1d1d',
headerText: '#ffffff',
bodyText: '#374151',
tableHeaderBg: '#7f1d1d',
tableHeaderText: '#ffffff',
tableRowAlt: '#f5f5f4',
tableBorder: '#d6d3d1',
totalHighlight: '#7f1d1d',
},
}
const modern: InvoiceTemplateConfig = {
id: 'modern',
name: 'Modern',
category: 'creative',
description: 'Asymmetric header with teal accents',
colors: {
primary: '#0d9488',
secondary: '#14b8a6',
background: '#ffffff',
headerBg: '#ffffff',
headerText: '#0f172a',
bodyText: '#334155',
tableHeaderBg: '#ffffff',
tableHeaderText: '#0d9488',
tableRowAlt: '#f0fdfa',
tableBorder: '#99f6e4',
totalHighlight: '#0d9488',
},
}
const elegant: InvoiceTemplateConfig = {
id: 'elegant',
name: 'Elegant',
category: 'creative',
description: 'Gold double-rule accents on centered layout',
colors: {
primary: '#a16207',
secondary: '#ca8a04',
background: '#ffffff',
headerBg: '#ffffff',
headerText: '#422006',
bodyText: '#57534e',
tableHeaderBg: '#ffffff',
tableHeaderText: '#422006',
tableRowAlt: '#fefce8',
tableBorder: '#a16207',
totalHighlight: '#a16207',
},
}
const creative: InvoiceTemplateConfig = {
id: 'creative',
name: 'Creative',
category: 'creative',
description: 'Purple sidebar with card-style rows',
colors: {
primary: '#7c3aed',
secondary: '#a78bfa',
background: '#ffffff',
headerBg: '#ffffff',
headerText: '#1f2937',
bodyText: '#374151',
tableHeaderBg: '#faf5ff',
tableHeaderText: '#7c3aed',
tableRowAlt: '#faf5ff',
tableBorder: '#e9d5ff',
totalHighlight: '#7c3aed',
},
}
const compact: InvoiceTemplateConfig = {
id: 'compact',
name: 'Compact',
category: 'creative',
description: 'Data-dense layout with tight spacing',
colors: {
primary: '#475569',
secondary: '#64748b',
background: '#ffffff',
headerBg: '#ffffff',
headerText: '#0f172a',
bodyText: '#334155',
tableHeaderBg: '#f1f5f9',
tableHeaderText: '#334155',
tableRowAlt: '#f8fafc',
tableBorder: '#e2e8f0',
totalHighlight: '#475569',
},
}
const dark: InvoiceTemplateConfig = {
id: 'dark',
name: 'Dark',
category: 'warm',
description: 'Near-black background with cyan highlights',
colors: {
primary: '#06b6d4',
secondary: '#22d3ee',
background: '#0f172a',
headerBg: '#020617',
headerText: '#e2e8f0',
bodyText: '#cbd5e1',
tableHeaderBg: '#020617',
tableHeaderText: '#06b6d4',
tableRowAlt: '#1e293b',
tableBorder: '#334155',
totalHighlight: '#06b6d4',
},
}
const vibrant: InvoiceTemplateConfig = {
id: 'vibrant',
name: 'Vibrant',
category: 'warm',
description: 'Coral-to-orange gradient header band',
colors: {
primary: '#ea580c',
secondary: '#f97316',
background: '#ffffff',
headerBg: '#ea580c',
headerText: '#ffffff',
bodyText: '#1f2937',
tableHeaderBg: '#fff7ed',
tableHeaderText: '#9a3412',
tableRowAlt: '#fff7ed',
tableBorder: '#fed7aa',
totalHighlight: '#ea580c',
},
}
const corporate: InvoiceTemplateConfig = {
id: 'corporate',
name: 'Corporate',
category: 'warm',
description: 'Deep blue header with info bar below',
colors: {
primary: '#1e40af',
secondary: '#3b82f6',
background: '#ffffff',
headerBg: '#1e40af',
headerText: '#ffffff',
bodyText: '#1f2937',
tableHeaderBg: '#1e40af',
tableHeaderText: '#ffffff',
tableRowAlt: '#eff6ff',
tableBorder: '#bfdbfe',
totalHighlight: '#1e40af',
},
}
const fresh: InvoiceTemplateConfig = {
id: 'fresh',
name: 'Fresh',
category: 'premium',
description: 'Oversized watermark invoice number',
colors: {
primary: '#0284c7',
secondary: '#38bdf8',
background: '#ffffff',
headerBg: '#ffffff',
headerText: '#0c4a6e',
bodyText: '#334155',
tableHeaderBg: '#0284c7',
tableHeaderText: '#ffffff',
tableRowAlt: '#f0f9ff',
tableBorder: '#bae6fd',
totalHighlight: '#0284c7',
},
}
const natural: InvoiceTemplateConfig = {
id: 'natural',
name: 'Natural',
category: 'premium',
description: 'Warm beige background with terracotta accents',
colors: {
primary: '#c2703e',
secondary: '#d97706',
background: '#fdf6ec',
headerBg: '#fdf6ec',
headerText: '#78350f',
bodyText: '#57534e',
tableHeaderBg: '#c2703e',
tableHeaderText: '#ffffff',
tableRowAlt: '#fef3c7',
tableBorder: '#d6d3d1',
totalHighlight: '#c2703e',
},
}
const statement: InvoiceTemplateConfig = {
id: 'statement',
name: 'Statement',
category: 'premium',
description: 'Total-forward design with hero amount',
colors: {
primary: '#18181b',
secondary: '#be123c',
background: '#ffffff',
headerBg: '#ffffff',
headerText: '#18181b',
bodyText: '#3f3f46',
tableHeaderBg: '#ffffff',
tableHeaderText: '#18181b',
tableRowAlt: '#ffffff',
tableBorder: '#e4e4e7',
totalHighlight: '#be123c',
},
}
export const INVOICE_TEMPLATES: InvoiceTemplateConfig[] = [
clean, professional, bold, minimal, classic,
modern, elegant, creative, compact,
dark, vibrant, corporate,
fresh, natural, statement,
]
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: Commit**
```bash
git add src/utils/invoiceTemplates.ts
git commit -m "feat: rewrite invoice template configs with design-doc IDs and colors"
```
---
### Task 4: Rewrite InvoicePreview.vue with 15 unique HTML layouts
**Files:**
- Rewrite: `src/components/InvoicePreview.vue`
This is the largest single task. Each of the 15 templates needs a genuinely unique HTML layout following the design doc specifications. The component receives `template`, `invoice`, `client`, `items`, `businessInfo` props and renders a scaled A4 preview (~300px wide).
**Typography scale for HTML preview (from design doc):**
- "INVOICE" title: 18-24px bold
- Company name: 11-14px semi-bold
- Section headers: 8-9px uppercase, letter-spacing 0.05em
- Body text: 7.5-8px regular
- Column headers: 7-7.5px medium, uppercase
- Total amount: 12-16px bold
- Fine print: 6.5-7px regular
**Key design rules:**
- Total Due is the most prominent number in every template
- Borderless tables (thin horizontal rules, no vertical borders, no spreadsheet grids)
- Right-align monetary values, left-align descriptions
- Tables use thin bottom borders per row (border-bottom: 0.5px solid color)
- Zebra striping only where specified (alternate barely-gray #f8fafc)
- Generous whitespace between sections
**Per-template layout descriptions (from design doc):**
1. **Clean**: Logo top-left, biz name below, "INVOICE" in slate with thin accent line (30% width). Two-column From/To. Borderless table with thin gray row borders, light gray header bg. Blue accent only on total.
2. **Professional**: Full-width navy band across top ~17% height. White "INVOICE" large inside band. Biz name white right side. Navy header table row, light gray zebra stripes.
3. **Bold**: Large indigo rectangle top-left ~55% width × 17% height. "INVOICE" massive white inside. Biz info outside block to right. Indigo header row, no borders at all, generous row height.
4. **Minimal**: Everything centered. Logo centered. Thin rule. "INVOICE" centered in charcoal. No backgrounds anywhere. Whitespace-only row separation. Pure monochrome #18181b.
5. **Classic**: Traditional two-column header. Biz info left, "INVOICE" + meta right. Thin burgundy rule below. Burgundy header bg with white text. Light bordered grid. Warm gray alternating rows.
6. **Modern**: Asymmetric — "INVOICE" large top-left. Biz info pushed right. Thin teal line separator. Teal header text (no bg fill), thin teal bottom borders. Teal bg strip behind total row.
7. **Elegant**: Centered. Gold double-rule at top (two thin lines). "INVOICE" centered below. Another double-rule. Gold rules between rows. No colored header bg.
8. **Creative**: Narrow purple sidebar on left (~3% width), full height. Content offset past sidebar. Card-style rows (faint bg + padding). Purple-tinted header text.
9. **Compact**: Single-line header — biz name left, "INVOICE #XXX" right, same line. Tight zebra stripes (small row height). Efficient space usage. Slate accent.
10. **Dark**: Near-black #0f172a background. Light text #e2e8f0. "INVOICE" in cyan #06b6d4. Very dark header with cyan column names. Alternating dark rows.
11. **Vibrant**: Full-width gradient band (coral→orange) ~15% height. White text inside. Light warm-tinted table. Coral total.
12. **Corporate**: Deep blue band top ~15% with white text. Thin lighter blue info bar below. Light bordered table (thin gray on all cells). Blue header row.
13. **Fresh**: Logo left. Large invoice number right side (oversized, light sky blue as watermark). Sky blue header bg. Light blue zebra stripes.
14. **Natural**: Warm beige #fdf6ec background for entire page. "INVOICE" in terracotta #c2703e. Terracotta header. Warm cream alternating rows.
15. **Statement**: "INVOICE" normal top-left. TOTAL AMOUNT displayed massively top-right (~24px+). Whitespace-only table separation. No borders, no stripes. Rose accent on big total.
**Implementation approach:**
The template uses a `v-if`/`v-else-if` chain to switch between 15 completely different layout blocks. Each block contains its own unique HTML structure. Shared computed values (displayItems, clientName, etc.) are reused across all templates.
The script section stays lean — props, computed helpers for items/client/biz, formatCurrency/formatDate imports. All visual differentiation is in the template.
**Step 1: Write the complete component**
Write `InvoicePreview.vue` with the script setup section and all 15 template blocks. Each template block must follow its specific design doc layout. Use inline styles for template-specific colors (from `c` computed). Use Tailwind-like patterns via inline style objects.
All templates share:
- Outer `<div>` with `aspect-ratio: 210/297`, `width: 100%`, `overflow: hidden`, `font-family: -apple-system, ...sans-serif`
- Padding of roughly `6%` to simulate page margins
- The same data bindings (invoice number, dates, client info, business info, line items, totals)
**Step 2: Verify**
Run: `npm run build` (from project root)
Expected: Compiles with no errors. May have chunk size warning — that's OK.
**Step 3: Commit**
```bash
git add src/components/InvoicePreview.vue
git commit -m "feat: rewrite InvoicePreview with 15 unique typographic layouts"
```
---
### Task 5: Rewrite invoicePdfRenderer.ts with 15 unique PDF render functions
**Files:**
- Rewrite: `src/utils/invoicePdfRenderer.ts`
Each template gets its own render function that produces a unique PDF layout matching the HTML preview. Uses jsPDF directly.
**Typography scale for PDF (from design doc, in points):**
- "INVOICE" title: 24-32pt bold
- Company name: 14-18pt semi-bold
- Section headers: 11-12pt semi-bold, uppercase, letter-spacing +0.05em
- Body text: 10-11pt regular
- Column headers: 9-10pt medium, uppercase
- Total amount: 16-20pt bold
- Due date: 12-14pt semi-bold
- Footer/notes: 8-9pt regular
**PDF whitespace (in mm):**
- Page margins: 20mm all sides
- Between major sections: 8-12mm
- Table cell padding: 3mm vertical, 4mm horizontal
- Row height: 8-10mm
- Logo max height: 16mm
**Table design rules:**
- Borderless with thin horizontal rules (0.3pt, light gray)
- No vertical borders
- Column headers: weight + bottom border (1pt)
- Right-align Qty, Rate, Amount columns
- Zebra striping where specified: white / barely-gray (#f8fafc)
**Implementation approach:**
The file exports one main function `renderInvoicePdf(config, invoice, client, items, businessInfo)` that switches on `config.id` to call the appropriate per-template function. Shared helpers (hexToRgb, drawLogo, truncateText) are defined at the top.
Each per-template function creates a `new jsPDF({ unit: 'mm', format: 'a4' })` and draws its unique layout using jsPDF methods: `setFont`, `setFontSize`, `setTextColor`, `text`, `setFillColor`, `rect`, `setDrawColor`, `line`, `setLineWidth`.
The function returns the jsPDF doc instance.
**IMPORTANT jsPDF notes:**
- jsPDF uses `'helvetica'` font with styles `'normal'`, `'bold'`
- Colors must be set as RGB via `setTextColor(r, g, b)`, `setFillColor(r, g, b)`, `setDrawColor(r, g, b)` — use hexToRgb helper
- Text alignment: `doc.text(str, x, y, { align: 'right' })` for right-aligned text
- A4 size: 210mm × 297mm
- `doc.setFont('helvetica', 'normal')` for regular, `doc.setFont('helvetica', 'bold')` for bold
- For uppercase tracked text: transform the string to uppercase, use `doc.setCharSpace(0.3)` then reset with `doc.setCharSpace(0)`
- For letter-spacing simulation: jsPDF has `setCharSpace(mm)` — use 0.3mm for tracked labels
**Step 1: Write the complete renderer**
Write `invoicePdfRenderer.ts` with:
1. `BusinessInfo` export interface (name, address, email, phone, logo)
2. `hexToRgb` helper
3. `setFill`, `setText`, `setDraw` color helpers that accept hex
4. `drawLogo` helper (if businessInfo.logo is a data URL, use `doc.addImage`)
5. `truncateDesc` helper (truncates long descriptions for table cells)
6. 15 render functions: `renderClean`, `renderProfessional`, `renderBold`, `renderMinimal`, `renderClassic`, `renderModern`, `renderElegant`, `renderCreative`, `renderCompact`, `renderDark`, `renderVibrant`, `renderCorporate`, `renderFresh`, `renderNatural`, `renderStatement`
7. Main `renderInvoicePdf` switch function
Each render function must match its corresponding HTML preview layout. The typography scale must follow the design doc exactly.
**Step 2: Verify**
Run: `npm run build`
Expected: Compiles with no errors.
**Step 3: Commit**
```bash
git add src/utils/invoicePdfRenderer.ts
git commit -m "feat: rewrite PDF renderer with 15 unique typographic layouts"
```
---
### Task 6: Update invoicePdf.ts wrapper
**Files:**
- Modify: `src/utils/invoicePdf.ts`
**Step 1: Update default template ID**
Change the default `templateId` parameter from `'clean-minimal'` to `'clean'`:
```typescript
export function generateInvoicePdf(
invoice: Invoice,
client: Client,
items: InvoiceItem[],
templateId: string = 'clean',
businessInfo?: BusinessInfo,
): jsPDF {
```
**Step 2: Commit**
```bash
git add src/utils/invoicePdf.ts
git commit -m "feat: update invoicePdf wrapper with new default template ID"
```
---
### Task 7: Add template-picker view to Invoices.vue
This is the UX flow change — the core of Part 1 of the design doc.
**Files:**
- Modify: `src/views/Invoices.vue`
**Changes needed:**
1. **View type**: Change `ref<'list' | 'create'>('list')` to `ref<'list' | 'create' | 'template-picker'>('list')`
2. **New state**: Add `pickerInvoiceId = ref<number | null>(null)` to track which invoice is being styled
3. **Remove template picker from create form**: Delete the template selection section (lines 374-384 in current file)
4. **Change handleCreate**: After saving invoice + items, instead of resetting and going to list, navigate to template picker:
```typescript
pickerInvoiceId.value = invoiceId
selectedTemplateId.value = 'clean'
view.value = 'template-picker'
```
5. **Change viewInvoice**: Instead of opening a modal dialog, navigate to the template picker:
```typescript
async function viewInvoice(invoice: Invoice) {
pickerInvoiceId.value = invoice.id!
selectedTemplateId.value = invoice.template_id || 'clean'
// Load items for the picker
try {
previewItems.value = invoice.id ? await invoicesStore.getInvoiceItems(invoice.id) : []
} catch (e) {
console.error('Failed to load invoice items:', e)
previewItems.value = []
}
selectedInvoice.value = invoice
view.value = 'template-picker'
}
```
6. **Remove the old preview dialog**: Delete the "Invoice Preview Dialog" section (lines 405-447) — it's replaced by the template picker view.
7. **Add template-picker view in template**: Add a new `v-else-if="view === 'template-picker'"` block after the create view. This is a full-screen layout:
```html
<!-- Template Picker View -->
<div v-else-if="view === 'template-picker'" class="-m-6 h-[calc(100vh-2rem)] flex flex-col">
<!-- Top bar with invoice info -->
<div class="flex items-center justify-between px-6 py-3 border-b border-border-subtle bg-bg-surface shrink-0">
<div class="flex items-center gap-3">
<button
@click="handlePickerBack"
class="p-1.5 text-text-tertiary hover:text-text-secondary transition-colors"
>
<!-- back arrow SVG -->
</button>
<span class="text-[0.8125rem] font-medium text-text-primary">
{{ selectedInvoice?.invoice_number }}
</span>
<span class="text-[0.75rem] text-text-tertiary">
{{ selectedInvoice ? getClientName(selectedInvoice.client_id) : '' }}
</span>
</div>
<div class="flex items-center gap-3">
<button @click="handlePickerExport" class="px-4 py-2 bg-accent text-bg-base text-[0.75rem] font-medium rounded-lg hover:bg-accent-hover transition-colors">
Export PDF
</button>
<button @click="handlePickerSave" class="px-4 py-2 border border-border-subtle text-text-secondary text-[0.75rem] rounded-lg hover:bg-bg-elevated transition-colors">
Save & Close
</button>
</div>
</div>
<!-- Split pane: template list + preview -->
<div class="flex-1 flex overflow-hidden">
<!-- Left: Template list -->
<div class="w-56 border-r border-border-subtle overflow-y-auto bg-bg-surface shrink-0">
<div v-for="cat in TEMPLATE_CATEGORIES" :key="cat.id">
<div class="text-[0.5625rem] text-text-tertiary uppercase tracking-[0.1em] font-medium px-3 pt-3 pb-1">
{{ cat.label }}
</div>
<button
v-for="tmpl in getTemplatesByCategory(cat.id)"
:key="tmpl.id"
class="w-full flex items-center gap-2 px-3 py-1.5 text-[0.75rem] transition-colors"
:class="tmpl.id === selectedTemplateId
? 'bg-accent/10 text-accent-text'
: 'text-text-secondary hover:bg-bg-elevated hover:text-text-primary'"
@click="selectedTemplateId = tmpl.id"
>
<span class="w-2.5 h-2.5 rounded-full shrink-0 border border-black/10" :style="{ backgroundColor: tmpl.colors.primary }" />
<span class="truncate">{{ tmpl.name }}</span>
</button>
</div>
</div>
<!-- Right: Preview -->
<div class="flex-1 bg-bg-inset p-8 overflow-y-auto flex justify-center">
<div class="w-full max-w-lg">
<InvoicePreview
:template="getTemplateById(selectedTemplateId)"
:invoice="selectedInvoice!"
:client="pickerClient"
:items="previewItems"
:business-info="businessInfo"
/>
</div>
</div>
</div>
</div>
```
8. **Add picker helper functions**:
```typescript
const pickerClient = computed<Client | null>(() => {
if (!selectedInvoice.value) return null
return clientsStore.clients.find(c => c.id === selectedInvoice.value!.client_id) || null
})
async function handlePickerSave() {
if (pickerInvoiceId.value) {
await invoicesStore.updateInvoiceTemplate(pickerInvoiceId.value, selectedTemplateId.value)
}
pickerInvoiceId.value = null
view.value = 'list'
}
async function handlePickerExport() {
if (selectedInvoice.value) {
await exportPDF(selectedInvoice.value)
}
}
function handlePickerBack() {
pickerInvoiceId.value = null
view.value = 'list'
}
```
9. **Update handleCreate to navigate to picker**: After saving invoice and items:
```typescript
// Navigate to template picker instead of list
selectedInvoice.value = { ...invoice, id: invoiceId }
pickerInvoiceId.value = invoiceId
selectedTemplateId.value = 'clean'
// Load items for preview
previewItems.value = lineItems.value.map(li => ({
invoice_id: invoiceId,
description: li.description,
quantity: li.quantity,
rate: li.unit_price,
amount: li.quantity * li.unit_price,
}))
// Reset form
createForm.client_id = 0
// ... rest of form reset
lineItems.value = []
view.value = 'template-picker'
```
**IMPORTANT**: Must save the selectedClient BEFORE resetting createForm.client_id, because pickerClient depends on selectedInvoice.
10. **Update selectedTemplateId default**: Change from `'clean-minimal'` to `'clean'`.
11. **Hide tabs when in template-picker view**: Wrap the tab buttons in `v-if="view !== 'template-picker'"`.
**Step 1: Implement all changes above**
**Step 2: Verify**
Run: `npm run build`
Expected: Compiles. No errors.
**Step 3: Commit**
```bash
git add src/views/Invoices.vue
git commit -m "feat: add two-step invoice flow with full-screen template picker"
```
---
### Task 8: Update InvoiceTemplatePicker.vue for new template IDs
**Files:**
- Modify: `src/components/InvoiceTemplatePicker.vue`
The InvoiceTemplatePicker is still used in the create form (now removed) but may be referenced elsewhere. Update:
**Step 1: Update default template ID in sample data**
The component's default data and behavior should reference `'clean'` not `'clean-minimal'`. Since the old create form no longer uses this component (template picker is now in the Invoices view directly), this component may become unused. However, keep it functional in case it's used for other purposes.
No changes needed if all references already go through `getTemplateById`. Just verify it works with the new template IDs.
**Step 2: Verify no broken imports**
Check that InvoiceTemplatePicker.vue still imports from the updated invoiceTemplates.ts correctly. The import paths haven't changed, just the template IDs and config structure — which is the same interface name.
**Step 3: Commit (if any changes were needed)**
```bash
git add src/components/InvoiceTemplatePicker.vue
git commit -m "chore: update InvoiceTemplatePicker for new template IDs"
```
---
### Task 9: Full build and integration test
**Files:** None new — verification only.
**Step 1: Build the Rust backend**
Run: `cd src-tauri && cargo build`
Expected: Compiles with no errors.
**Step 2: Build the frontend**
Run: `npm run build`
Expected: Compiles with no errors (chunk size warning is acceptable).
**Step 3: Manual verification checklist**
Run: `npm run tauri dev` and verify:
1. Create an invoice with line items and a client → clicking "Create Invoice" navigates to the template picker (not back to list)
2. Template picker shows all 15 templates in the left sidebar, grouped by category
3. Clicking different templates shows genuinely different layouts in the preview
4. Each template is immediately visually distinguishable from the others
5. Total amount is the most prominent element in every template
6. "Export PDF" generates a PDF that matches the preview
7. "Save & Close" saves the template choice and returns to list
8. Viewing an existing invoice from the list opens the template picker with its saved template pre-selected
9. Tables use borderless/subtle-rule design (no spreadsheet grids)
10. Typography is readable — body text not too small, headers properly sized
**Step 4: Commit any fixes**
```bash
git add -A
git commit -m "fix: integration test fixes for invoice templates v2"
```
---
## Summary of Template ID Migration
| Old ID | New ID | Template Name |
|---|---|---|
| clean-minimal | clean | Clean |
| corporate-classic | professional | Professional |
| modern-bold | bold | Bold |
| elegant-serif | minimal | Minimal |
| simple-two-tone | classic | Classic |
| gradient-header | modern | Modern |
| sidebar-accent | elegant | Elegant |
| geometric-modern | creative | Creative |
| dark-mode | compact | Compact |
| warm-natural | dark | Dark |
| playful-color-block | vibrant | Vibrant |
| retro-professional | corporate | Corporate |
| tech-minimal | fresh | Fresh |
| executive-premium | natural | Natural |
| data-driven-clean | statement | Statement |
**Note**: Existing invoices in the DB will have old template IDs. The `getTemplateById` function falls back to the first template (Clean) when an ID is not found, so old invoices will gracefully default to Clean. No data migration is needed.