feat: load invoice templates from JSON files via backend

Templates are now loaded dynamically from data/templates/*.json
via the get_invoice_templates Tauri command instead of being
hardcoded in TypeScript. Preview and PDF renderer switch on
template.layout instead of template.id, allowing custom templates
to reuse built-in layouts with different colors.
This commit is contained in:
Your Name
2026-02-18 15:17:54 +02:00
parent 8b8d451806
commit 8c8de6a2a7
4 changed files with 51 additions and 327 deletions

View File

@@ -1825,7 +1825,7 @@ export function renderInvoicePdf(
items: InvoiceItem[],
businessInfo: BusinessInfo,
): jsPDF {
switch (config.id) {
switch (config.layout) {
case 'clean': return renderClean(config, invoice, client, items, businessInfo)
case 'professional': return renderProfessional(config, invoice, client, items, businessInfo)
case 'bold': return renderBold(config, invoice, client, items, businessInfo)

View File

@@ -1,3 +1,6 @@
import { ref } from 'vue'
import { invoke } from '@tauri-apps/api/core'
export interface InvoiceTemplateColors {
primary: string
secondary: string
@@ -15,318 +18,12 @@ export interface InvoiceTemplateColors {
export interface InvoiceTemplateConfig {
id: string
name: string
category: 'essential' | 'creative' | 'warm' | 'premium'
layout: string
category: string
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' },
@@ -334,10 +31,36 @@ export const TEMPLATE_CATEGORIES = [
{ id: 'premium', label: 'Premium & Specialized' },
] as const
const templates = ref<InvoiceTemplateConfig[]>([])
let loaded = false
export async function loadTemplates(): Promise<InvoiceTemplateConfig[]> {
if (loaded && templates.value.length > 0) return templates.value
try {
templates.value = await invoke<InvoiceTemplateConfig[]>('get_invoice_templates')
loaded = true
} catch (e) {
console.error('Failed to load invoice templates:', e)
}
return templates.value
}
export function getLoadedTemplates(): InvoiceTemplateConfig[] {
return templates.value
}
export function getTemplateById(id: string): InvoiceTemplateConfig {
return INVOICE_TEMPLATES.find(t => t.id === id) || INVOICE_TEMPLATES[0]
return templates.value.find(t => t.id === id) || templates.value[0] || {
id: 'clean', name: 'Clean', layout: 'clean', category: 'essential',
description: 'Default', colors: {
primary: '#1e293b', secondary: '#3b82f6', background: '#ffffff',
headerBg: '#ffffff', headerText: '#1e293b', bodyText: '#374151',
tableHeaderBg: '#f8fafc', tableHeaderText: '#374151',
tableRowAlt: '#f8fafc', tableBorder: '#e5e7eb', totalHighlight: '#3b82f6',
},
}
}
export function getTemplatesByCategory(category: string): InvoiceTemplateConfig[] {
return INVOICE_TEMPLATES.filter(t => t.category === category)
return templates.value.filter(t => t.category === category)
}