From 8b8d451806880d7254da2f4b5e214467efdd9f48 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 18 Feb 2026 15:12:30 +0200 Subject: [PATCH] feat: load invoice templates from JSON files in data/templates directory --- src-tauri/src/commands.rs | 244 ++++++++++++++++++++++++++++++++++++++ src-tauri/src/lib.rs | 5 +- 2 files changed, 248 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 3a4cb55..a435b08 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1152,3 +1152,247 @@ pub fn close_mini_timer(app: tauri::AppHandle) -> Result<(), String> { } Ok(()) } + +// Invoice template types and commands +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct InvoiceTemplateColors { + pub primary: String, + pub secondary: String, + pub background: String, + #[serde(rename = "headerBg")] + pub header_bg: String, + #[serde(rename = "headerText")] + pub header_text: String, + #[serde(rename = "bodyText")] + pub body_text: String, + #[serde(rename = "tableHeaderBg")] + pub table_header_bg: String, + #[serde(rename = "tableHeaderText")] + pub table_header_text: String, + #[serde(rename = "tableRowAlt")] + pub table_row_alt: String, + #[serde(rename = "tableBorder")] + pub table_border: String, + #[serde(rename = "totalHighlight")] + pub total_highlight: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct InvoiceTemplate { + pub id: String, + pub name: String, + pub layout: String, + pub category: String, + pub description: String, + pub colors: InvoiceTemplateColors, +} + +#[tauri::command] +pub fn get_invoice_templates(state: State) -> Result, String> { + let templates_dir = state.data_dir.join("templates"); + let mut templates: Vec = Vec::new(); + + if let Ok(entries) = std::fs::read_dir(&templates_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().and_then(|e| e.to_str()) == Some("json") { + if let Ok(content) = std::fs::read_to_string(&path) { + match serde_json::from_str::(&content) { + Ok(template) => templates.push(template), + Err(e) => eprintln!("Failed to parse template {:?}: {}", path, e), + } + } + } + } + } + + // Sort: essentials first, then creative, warm, premium; alphabetical within category + let cat_order = |c: &str| -> u8 { + match c { "essential" => 0, "creative" => 1, "warm" => 2, "premium" => 3, _ => 4 } + }; + templates.sort_by(|a, b| { + cat_order(&a.category).cmp(&cat_order(&b.category)).then(a.name.cmp(&b.name)) + }); + + Ok(templates) +} + +pub fn seed_default_templates(data_dir: &std::path::Path) { + let templates_dir = data_dir.join("templates"); + std::fs::create_dir_all(&templates_dir).ok(); + + // Only seed if directory is empty (no .json files) + let has_templates = std::fs::read_dir(&templates_dir) + .map(|entries| entries.flatten().any(|e| { + e.path().extension().and_then(|ext| ext.to_str()) == Some("json") + })) + .unwrap_or(false); + + if has_templates { + return; + } + + let defaults = get_default_templates(); + for template in &defaults { + let filename = format!("{}.json", template.id); + let path = templates_dir.join(&filename); + if let Ok(json) = serde_json::to_string_pretty(template) { + std::fs::write(&path, json).ok(); + } + } +} + +fn get_default_templates() -> Vec { + vec![ + InvoiceTemplate { + id: "clean".into(), name: "Clean".into(), layout: "clean".into(), + category: "essential".into(), description: "Swiss minimalism with a single blue accent".into(), + colors: InvoiceTemplateColors { + primary: "#1e293b".into(), secondary: "#3b82f6".into(), background: "#ffffff".into(), + header_bg: "#ffffff".into(), header_text: "#1e293b".into(), body_text: "#374151".into(), + table_header_bg: "#f8fafc".into(), table_header_text: "#374151".into(), + table_row_alt: "#f8fafc".into(), table_border: "#e5e7eb".into(), total_highlight: "#3b82f6".into(), + }, + }, + InvoiceTemplate { + id: "professional".into(), name: "Professional".into(), layout: "professional".into(), + category: "essential".into(), description: "Navy header band with corporate polish".into(), + colors: InvoiceTemplateColors { + primary: "#1e3a5f".into(), secondary: "#2563eb".into(), background: "#ffffff".into(), + header_bg: "#1e3a5f".into(), header_text: "#ffffff".into(), body_text: "#374151".into(), + table_header_bg: "#1e3a5f".into(), table_header_text: "#ffffff".into(), + table_row_alt: "#f3f4f6".into(), table_border: "#d1d5db".into(), total_highlight: "#1e3a5f".into(), + }, + }, + InvoiceTemplate { + id: "bold".into(), name: "Bold".into(), layout: "bold".into(), + category: "essential".into(), description: "Large indigo block with oversized typography".into(), + colors: InvoiceTemplateColors { + primary: "#4f46e5".into(), secondary: "#a5b4fc".into(), background: "#ffffff".into(), + header_bg: "#4f46e5".into(), header_text: "#ffffff".into(), body_text: "#1f2937".into(), + table_header_bg: "#4f46e5".into(), table_header_text: "#ffffff".into(), + table_row_alt: "#f5f3ff".into(), table_border: "#e0e7ff".into(), total_highlight: "#4f46e5".into(), + }, + }, + InvoiceTemplate { + id: "minimal".into(), name: "Minimal".into(), layout: "minimal".into(), + category: "essential".into(), description: "Pure monochrome centered layout".into(), + colors: InvoiceTemplateColors { + primary: "#18181b".into(), secondary: "#18181b".into(), background: "#ffffff".into(), + header_bg: "#ffffff".into(), header_text: "#18181b".into(), body_text: "#3f3f46".into(), + table_header_bg: "#ffffff".into(), table_header_text: "#18181b".into(), + table_row_alt: "#ffffff".into(), table_border: "#e4e4e7".into(), total_highlight: "#18181b".into(), + }, + }, + InvoiceTemplate { + id: "classic".into(), name: "Classic".into(), layout: "classic".into(), + category: "essential".into(), description: "Traditional layout with burgundy accents".into(), + colors: InvoiceTemplateColors { + primary: "#7f1d1d".into(), secondary: "#991b1b".into(), background: "#ffffff".into(), + header_bg: "#7f1d1d".into(), header_text: "#ffffff".into(), body_text: "#374151".into(), + table_header_bg: "#7f1d1d".into(), table_header_text: "#ffffff".into(), + table_row_alt: "#f5f5f4".into(), table_border: "#d6d3d1".into(), total_highlight: "#7f1d1d".into(), + }, + }, + InvoiceTemplate { + id: "modern".into(), name: "Modern".into(), layout: "modern".into(), + category: "creative".into(), description: "Asymmetric header with teal accents".into(), + colors: InvoiceTemplateColors { + primary: "#0d9488".into(), secondary: "#14b8a6".into(), background: "#ffffff".into(), + header_bg: "#ffffff".into(), header_text: "#0f172a".into(), body_text: "#334155".into(), + table_header_bg: "#ffffff".into(), table_header_text: "#0d9488".into(), + table_row_alt: "#f0fdfa".into(), table_border: "#99f6e4".into(), total_highlight: "#0d9488".into(), + }, + }, + InvoiceTemplate { + id: "elegant".into(), name: "Elegant".into(), layout: "elegant".into(), + category: "creative".into(), description: "Gold double-rule accents on centered layout".into(), + colors: InvoiceTemplateColors { + primary: "#a16207".into(), secondary: "#ca8a04".into(), background: "#ffffff".into(), + header_bg: "#ffffff".into(), header_text: "#422006".into(), body_text: "#57534e".into(), + table_header_bg: "#ffffff".into(), table_header_text: "#422006".into(), + table_row_alt: "#fefce8".into(), table_border: "#a16207".into(), total_highlight: "#a16207".into(), + }, + }, + InvoiceTemplate { + id: "creative".into(), name: "Creative".into(), layout: "creative".into(), + category: "creative".into(), description: "Purple sidebar with card-style rows".into(), + colors: InvoiceTemplateColors { + primary: "#7c3aed".into(), secondary: "#a78bfa".into(), background: "#ffffff".into(), + header_bg: "#ffffff".into(), header_text: "#1f2937".into(), body_text: "#374151".into(), + table_header_bg: "#faf5ff".into(), table_header_text: "#7c3aed".into(), + table_row_alt: "#faf5ff".into(), table_border: "#e9d5ff".into(), total_highlight: "#7c3aed".into(), + }, + }, + InvoiceTemplate { + id: "compact".into(), name: "Compact".into(), layout: "compact".into(), + category: "creative".into(), description: "Data-dense layout with tight spacing".into(), + colors: InvoiceTemplateColors { + primary: "#475569".into(), secondary: "#64748b".into(), background: "#ffffff".into(), + header_bg: "#ffffff".into(), header_text: "#0f172a".into(), body_text: "#334155".into(), + table_header_bg: "#f1f5f9".into(), table_header_text: "#334155".into(), + table_row_alt: "#f8fafc".into(), table_border: "#e2e8f0".into(), total_highlight: "#475569".into(), + }, + }, + InvoiceTemplate { + id: "dark".into(), name: "Dark".into(), layout: "dark".into(), + category: "warm".into(), description: "Near-black background with cyan highlights".into(), + colors: InvoiceTemplateColors { + primary: "#06b6d4".into(), secondary: "#22d3ee".into(), background: "#0f172a".into(), + header_bg: "#020617".into(), header_text: "#e2e8f0".into(), body_text: "#cbd5e1".into(), + table_header_bg: "#020617".into(), table_header_text: "#06b6d4".into(), + table_row_alt: "#1e293b".into(), table_border: "#334155".into(), total_highlight: "#06b6d4".into(), + }, + }, + InvoiceTemplate { + id: "vibrant".into(), name: "Vibrant".into(), layout: "vibrant".into(), + category: "warm".into(), description: "Coral-to-orange gradient header band".into(), + colors: InvoiceTemplateColors { + primary: "#ea580c".into(), secondary: "#f97316".into(), background: "#ffffff".into(), + header_bg: "#ea580c".into(), header_text: "#ffffff".into(), body_text: "#1f2937".into(), + table_header_bg: "#fff7ed".into(), table_header_text: "#9a3412".into(), + table_row_alt: "#fff7ed".into(), table_border: "#fed7aa".into(), total_highlight: "#ea580c".into(), + }, + }, + InvoiceTemplate { + id: "corporate".into(), name: "Corporate".into(), layout: "corporate".into(), + category: "warm".into(), description: "Deep blue header with info bar below".into(), + colors: InvoiceTemplateColors { + primary: "#1e40af".into(), secondary: "#3b82f6".into(), background: "#ffffff".into(), + header_bg: "#1e40af".into(), header_text: "#ffffff".into(), body_text: "#1f2937".into(), + table_header_bg: "#1e40af".into(), table_header_text: "#ffffff".into(), + table_row_alt: "#eff6ff".into(), table_border: "#bfdbfe".into(), total_highlight: "#1e40af".into(), + }, + }, + InvoiceTemplate { + id: "fresh".into(), name: "Fresh".into(), layout: "fresh".into(), + category: "premium".into(), description: "Oversized watermark invoice number".into(), + colors: InvoiceTemplateColors { + primary: "#0284c7".into(), secondary: "#38bdf8".into(), background: "#ffffff".into(), + header_bg: "#ffffff".into(), header_text: "#0c4a6e".into(), body_text: "#334155".into(), + table_header_bg: "#0284c7".into(), table_header_text: "#ffffff".into(), + table_row_alt: "#f0f9ff".into(), table_border: "#bae6fd".into(), total_highlight: "#0284c7".into(), + }, + }, + InvoiceTemplate { + id: "natural".into(), name: "Natural".into(), layout: "natural".into(), + category: "premium".into(), description: "Warm beige background with terracotta accents".into(), + colors: InvoiceTemplateColors { + primary: "#c2703e".into(), secondary: "#d97706".into(), background: "#fdf6ec".into(), + header_bg: "#fdf6ec".into(), header_text: "#78350f".into(), body_text: "#57534e".into(), + table_header_bg: "#c2703e".into(), table_header_text: "#ffffff".into(), + table_row_alt: "#fef3c7".into(), table_border: "#d6d3d1".into(), total_highlight: "#c2703e".into(), + }, + }, + InvoiceTemplate { + id: "statement".into(), name: "Statement".into(), layout: "statement".into(), + category: "premium".into(), description: "Total-forward design with hero amount".into(), + colors: InvoiceTemplateColors { + primary: "#18181b".into(), secondary: "#be123c".into(), background: "#ffffff".into(), + header_bg: "#ffffff".into(), header_text: "#18181b".into(), body_text: "#3f3f46".into(), + table_header_bg: "#ffffff".into(), table_header_text: "#18181b".into(), + table_row_alt: "#ffffff".into(), table_border: "#e4e4e7".into(), total_highlight: "#be123c".into(), + }, + }, + ] +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8cee7ca..c3932d7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -9,6 +9,7 @@ mod os_detection; pub struct AppState { pub db: Mutex, + pub data_dir: PathBuf, } fn get_data_dir() -> PathBuf { @@ -27,6 +28,7 @@ pub fn run() { let conn = Connection::open(&db_path).expect("Failed to open database"); database::init_db(&conn).expect("Failed to initialize database"); + commands::seed_default_templates(&data_dir); tauri::Builder::default() .plugin(tauri_plugin_window_state::Builder::new().build()) @@ -35,7 +37,7 @@ pub fn run() { .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_global_shortcut::Builder::new().build()) - .manage(AppState { db: Mutex::new(conn) }) + .manage(AppState { db: Mutex::new(conn), data_dir: data_dir.clone() }) .invoke_handler(tauri::generate_handler![ commands::get_clients, commands::create_client, @@ -90,6 +92,7 @@ pub fn run() { commands::save_binary_file, commands::open_mini_timer, commands::close_mini_timer, + commands::get_invoice_templates, ]) .setup(|app| { #[cfg(desktop)]