use crate::db::format_number; use crate::models::{Invoice, InvoiceInput, InvoiceItem, InvoiceSummary}; use crate::AppState; use rusqlite::{params, Connection, OptionalExtension, Row}; use tauri::State; const INVOICE_COLS: &str = "id, number, series_id, invoice_date, due_date, client_id, client_name, client_address, client_gstin, po_number, place_of_supply_state_code, subtotal, discount, tax_type, tax_rate, cgst_amount, sgst_amount, igst_amount, total, amount_in_words, bank_account_id, bank_snapshot, signature_path, notes, status, created_at, updated_at"; fn map_invoice(row: &Row) -> rusqlite::Result { Ok(Invoice { id: row.get(0)?, number: row.get(1)?, series_id: row.get(2)?, invoice_date: row.get(3)?, due_date: row.get(4)?, client_id: row.get(5)?, client_name: row.get(6)?, client_address: row.get(7)?, client_gstin: row.get(8)?, po_number: row.get(9)?, place_of_supply_state_code: row.get(10)?, subtotal: row.get(11)?, discount: row.get(12)?, tax_type: row.get(13)?, tax_rate: row.get(14)?, cgst_amount: row.get(15)?, sgst_amount: row.get(16)?, igst_amount: row.get(17)?, total: row.get(18)?, amount_in_words: row.get(19)?, bank_account_id: row.get(20)?, bank_snapshot: row.get(21)?, signature_path: row.get(22)?, notes: row.get(23)?, status: row.get(24)?, created_at: row.get(25)?, updated_at: row.get(26)?, items: Vec::new(), }) } fn fetch_items(conn: &Connection, invoice_id: i64) -> rusqlite::Result> { let mut stmt = conn.prepare( "SELECT id, description, mode, rate, unit, quantity, amount, sort_order FROM invoice_items WHERE invoice_id = ?1 ORDER BY sort_order ASC, id ASC", )?; let rows = stmt.query_map(params![invoice_id], |row| { Ok(InvoiceItem { id: Some(row.get(0)?), description: row.get(1)?, mode: row.get(2)?, rate: row.get(3)?, unit: row.get(4)?, quantity: row.get(5)?, amount: row.get(6)?, sort_order: row.get(7)?, }) })?; rows.collect() } fn fetch_invoice(conn: &Connection, id: i64) -> rusqlite::Result { let mut invoice = conn.query_row( &format!("SELECT {} FROM invoices WHERE id = ?1", INVOICE_COLS), params![id], map_invoice, )?; invoice.items = fetch_items(conn, id)?; Ok(invoice) } #[tauri::command] pub fn peek_next_invoice_number(state: State) -> Result { let conn = state.db.lock().map_err(|e| e.to_string())?; let series: Option<(String, i64, i64)> = conn .query_row( "SELECT prefix, padding, next_number FROM invoice_series WHERE is_active = 1 ORDER BY id DESC LIMIT 1", [], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)), ) .optional() .map_err(|e| e.to_string())?; match series { Some((prefix, padding, next)) => Ok(format_number(&prefix, padding, next)), None => Err("No active invoice series. Create one under Series.".into()), } } #[tauri::command] pub fn create_invoice(state: State, input: InvoiceInput) -> Result { let mut conn = state.db.lock().map_err(|e| e.to_string())?; let tx = conn.transaction().map_err(|e| e.to_string())?; let series = tx .query_row( "SELECT id, prefix, padding, next_number FROM invoice_series WHERE is_active = 1 ORDER BY id DESC LIMIT 1", [], |r| { Ok(( r.get::<_, i64>(0)?, r.get::<_, String>(1)?, r.get::<_, i64>(2)?, r.get::<_, i64>(3)?, )) }, ) .optional() .map_err(|e| e.to_string())? .ok_or_else(|| "No active invoice series. Create one under Series.".to_string())?; let (series_id, prefix, padding, next_number) = series; let number = format_number(&prefix, padding, next_number); // Persist the client first when the user asked to save a new one. let client_id = match input.client_id { Some(id) => Some(id), None if input.save_client && !input.client_name.trim().is_empty() => { tx.execute( "INSERT INTO clients (name, address, gstin, state_code, po_number, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", params![ input.client_name, input.client_address, input.client_gstin, input.place_of_supply_state_code, input.po_number, chrono::Utc::now().to_rfc3339() ], ) .map_err(|e| e.to_string())?; Some(tx.last_insert_rowid()) } None => None, }; let now = chrono::Utc::now().to_rfc3339(); tx.execute( r#"INSERT INTO invoices (number, series_id, invoice_date, due_date, client_id, client_name, client_address, client_gstin, po_number, place_of_supply_state_code, subtotal, discount, tax_type, tax_rate, cgst_amount, sgst_amount, igst_amount, total, amount_in_words, bank_account_id, bank_snapshot, signature_path, notes, status, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, 'issued', ?24, ?24)"#, params![ number, series_id, input.invoice_date, input.due_date, client_id, input.client_name, input.client_address, input.client_gstin, input.po_number, input.place_of_supply_state_code, input.subtotal, input.discount, input.tax_type, input.tax_rate, input.cgst_amount, input.sgst_amount, input.igst_amount, input.total, input.amount_in_words, input.bank_account_id, input.bank_snapshot, input.signature_path, input.notes, now, ], ) .map_err(|e| e.to_string())?; let invoice_id = tx.last_insert_rowid(); { let mut stmt = tx .prepare( "INSERT INTO invoice_items (invoice_id, description, mode, rate, unit, quantity, amount, sort_order) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", ) .map_err(|e| e.to_string())?; for (i, item) in input.items.iter().enumerate() { stmt.execute(params![ invoice_id, item.description, item.mode, item.rate, item.unit, item.quantity, item.amount, i as i64, ]) .map_err(|e| e.to_string())?; } } // Advance the serial for the next invoice in this series. tx.execute( "UPDATE invoice_series SET next_number = next_number + 1 WHERE id = ?1", params![series_id], ) .map_err(|e| e.to_string())?; tx.commit().map_err(|e| e.to_string())?; fetch_invoice(&conn, invoice_id).map_err(|e| e.to_string()) } #[tauri::command] pub fn list_invoices(state: State) -> Result, String> { let conn = state.db.lock().map_err(|e| e.to_string())?; let mut stmt = conn .prepare( "SELECT id, number, invoice_date, due_date, client_name, total, status, created_at FROM invoices ORDER BY id DESC", ) .map_err(|e| e.to_string())?; let rows = stmt .query_map([], |row| { Ok(InvoiceSummary { id: row.get(0)?, number: row.get(1)?, invoice_date: row.get(2)?, due_date: row.get(3)?, client_name: row.get(4)?, total: row.get(5)?, status: row.get(6)?, created_at: row.get(7)?, }) }) .map_err(|e| e.to_string())? .collect::>>() .map_err(|e| e.to_string())?; Ok(rows) } #[tauri::command] pub fn get_invoice(state: State, id: i64) -> Result { let conn = state.db.lock().map_err(|e| e.to_string())?; fetch_invoice(&conn, id).map_err(|e| e.to_string()) } #[tauri::command] pub fn delete_invoice(state: State, id: i64) -> Result<(), String> { let conn = state.db.lock().map_err(|e| e.to_string())?; conn.execute("DELETE FROM invoices WHERE id = ?1", params![id]) .map_err(|e| e.to_string())?; Ok(()) }