From cca06e851b7c253b31229ae7479931b18a6c1dff Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 18 Feb 2026 14:37:26 +0200 Subject: [PATCH] feat: add template_id column to invoices table and update_invoice_template command --- src-tauri/src/commands.rs | 83 +++++++++++++++++++++++++++++++++++---- src-tauri/src/database.rs | 16 ++++++++ src-tauri/src/lib.rs | 5 +++ 3 files changed, 97 insertions(+), 7 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index ec9a811..31bbad6 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -62,6 +62,7 @@ pub struct Invoice { pub total: f64, pub notes: Option, pub status: String, + pub template_id: Option, } // Client commands @@ -317,11 +318,11 @@ pub fn get_reports(state: State, start_date: String, end_date: String) pub fn create_invoice(state: State, invoice: Invoice) -> Result { let conn = state.db.lock().map_err(|e| e.to_string())?; conn.execute( - "INSERT INTO invoices (client_id, invoice_number, date, due_date, subtotal, tax_rate, tax_amount, discount, total, notes, status) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", + "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.total, invoice.notes, invoice.status, invoice.template_id], ).map_err(|e| e.to_string())?; Ok(conn.last_insert_rowid()) } @@ -330,7 +331,7 @@ pub fn create_invoice(state: State, invoice: Invoice) -> Result) -> Result, String> { let conn = state.db.lock().map_err(|e| e.to_string())?; let mut stmt = conn.prepare( - "SELECT id, client_id, invoice_number, date, due_date, subtotal, tax_rate, tax_amount, discount, total, notes, status + "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" ).map_err(|e| e.to_string())?; let invoices = stmt.query_map([], |row| { @@ -347,6 +348,7 @@ pub fn get_invoices(state: State) -> Result, String> { total: row.get(9)?, notes: row.get(10)?, status: row.get(11)?, + template_id: row.get(12)?, }) }).map_err(|e| e.to_string())?; invoices.collect::, _>>().map_err(|e| e.to_string()) @@ -357,11 +359,11 @@ pub fn update_invoice(state: State, invoice: Invoice) -> Result<(), St let conn = state.db.lock().map_err(|e| e.to_string())?; conn.execute( "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 - WHERE id = ?12", + 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.id], + invoice.total, invoice.notes, invoice.status, invoice.template_id, invoice.id], ).map_err(|e| e.to_string())?; Ok(()) } @@ -373,6 +375,67 @@ pub fn delete_invoice(state: State, id: i64) -> Result<(), String> { Ok(()) } +#[tauri::command] +pub fn update_invoice_template(state: State, 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(()) +} + +// Invoice items +#[derive(Debug, Serialize, Deserialize)] +pub struct InvoiceItem { + pub id: Option, + pub invoice_id: i64, + pub description: String, + pub quantity: f64, + pub rate: f64, + pub amount: f64, + pub time_entry_id: Option, +} + +#[tauri::command] +pub fn get_invoice_items(state: State, invoice_id: i64) -> Result, String> { + let conn = state.db.lock().map_err(|e| e.to_string())?; + let mut stmt = conn.prepare( + "SELECT id, invoice_id, description, quantity, rate, amount, time_entry_id + FROM invoice_items WHERE invoice_id = ?1 ORDER BY id" + ).map_err(|e| e.to_string())?; + let items = stmt.query_map(params![invoice_id], |row| { + Ok(InvoiceItem { + id: Some(row.get(0)?), + invoice_id: row.get(1)?, + description: row.get(2)?, + quantity: row.get(3)?, + rate: row.get(4)?, + amount: row.get(5)?, + time_entry_id: row.get(6)?, + }) + }).map_err(|e| e.to_string())?; + items.collect::, _>>().map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn create_invoice_item(state: State, item: InvoiceItem) -> Result { + let conn = state.db.lock().map_err(|e| e.to_string())?; + conn.execute( + "INSERT INTO invoice_items (invoice_id, description, quantity, rate, amount, time_entry_id) + VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + params![item.invoice_id, item.description, item.quantity, item.rate, item.amount, item.time_entry_id], + ).map_err(|e| e.to_string())?; + Ok(conn.last_insert_rowid()) +} + +#[tauri::command] +pub fn delete_invoice_items(state: State, invoice_id: i64) -> Result<(), String> { + let conn = state.db.lock().map_err(|e| e.to_string())?; + conn.execute("DELETE FROM invoice_items WHERE invoice_id = ?1", params![invoice_id]).map_err(|e| e.to_string())?; + Ok(()) +} + // Settings commands #[tauri::command] pub fn get_settings(state: State) -> Result, String> { @@ -1053,6 +1116,12 @@ pub fn import_json_data(state: State, data: serde_json::Value) -> Resu Ok(counts) } +// File save command (bypasses fs plugin scope for user-selected paths) +#[tauri::command] +pub fn save_binary_file(path: String, data: Vec) -> Result<(), String> { + std::fs::write(&path, &data).map_err(|e| e.to_string()) +} + // Mini timer window commands #[tauri::command] pub fn open_mini_timer(app: tauri::AppHandle) -> Result<(), String> { diff --git a/src-tauri/src/database.rs b/src-tauri/src/database.rs index 926ef23..a8f6fda 100644 --- a/src-tauri/src/database.rs +++ b/src-tauri/src/database.rs @@ -111,6 +111,22 @@ pub fn init_db(conn: &Connection) -> Result<(), rusqlite::Error> { [], )?; + // 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); + } + } + } + } + conn.execute( "CREATE TABLE IF NOT EXISTS invoice_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 974701e..8cee7ca 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -57,6 +57,10 @@ pub fn run() { commands::get_invoices, commands::update_invoice, commands::delete_invoice, + commands::update_invoice_template, + commands::get_invoice_items, + commands::create_invoice_item, + commands::delete_invoice_items, commands::get_settings, commands::update_settings, commands::export_data, @@ -83,6 +87,7 @@ pub fn run() { commands::get_timesheet_data, commands::import_entries, commands::import_json_data, + commands::save_binary_file, commands::open_mini_timer, commands::close_mini_timer, ])