use crate::AppState; use crate::os_detection; use rusqlite::params; use serde::{Deserialize, Serialize}; use tauri::State; #[derive(Debug, Serialize, Deserialize)] pub struct Client { pub id: Option, pub name: String, pub email: Option, pub address: Option, pub company: Option, pub phone: Option, pub tax_id: Option, pub payment_terms: Option, pub notes: Option, } #[derive(Debug, Serialize, Deserialize)] pub struct Project { pub id: Option, pub client_id: Option, pub name: String, pub hourly_rate: f64, pub color: String, pub archived: bool, pub budget_hours: Option, pub budget_amount: Option, pub rounding_override: Option, } #[derive(Debug, Serialize, Deserialize)] pub struct Task { pub id: Option, pub project_id: i64, pub name: String, } #[derive(Debug, Serialize, Deserialize)] pub struct TimeEntry { pub id: Option, pub project_id: i64, pub task_id: Option, pub description: Option, pub start_time: String, pub end_time: Option, pub duration: i64, } #[derive(Debug, Serialize, Deserialize)] pub struct Invoice { pub id: Option, pub client_id: i64, pub invoice_number: String, pub date: String, pub due_date: Option, pub subtotal: f64, pub tax_rate: f64, pub tax_amount: f64, pub discount: f64, pub total: f64, pub notes: Option, pub status: String, } // Client commands #[tauri::command] pub fn get_clients(state: State) -> Result, String> { let conn = state.db.lock().map_err(|e| e.to_string())?; let mut stmt = conn.prepare( "SELECT id, name, email, address, company, phone, tax_id, payment_terms, notes FROM clients ORDER BY name" ).map_err(|e| e.to_string())?; let clients = stmt.query_map([], |row| { Ok(Client { id: Some(row.get(0)?), name: row.get(1)?, email: row.get(2)?, address: row.get(3)?, company: row.get(4)?, phone: row.get(5)?, tax_id: row.get(6)?, payment_terms: row.get(7)?, notes: row.get(8)?, }) }).map_err(|e| e.to_string())?; clients.collect::, _>>().map_err(|e| e.to_string()) } #[tauri::command] pub fn create_client(state: State, client: Client) -> Result { let conn = state.db.lock().map_err(|e| e.to_string())?; conn.execute( "INSERT INTO clients (name, email, address, company, phone, tax_id, payment_terms, notes) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", params![client.name, client.email, client.address, client.company, client.phone, client.tax_id, client.payment_terms, client.notes], ).map_err(|e| e.to_string())?; Ok(conn.last_insert_rowid()) } #[tauri::command] pub fn update_client(state: State, client: Client) -> Result<(), String> { let conn = state.db.lock().map_err(|e| e.to_string())?; conn.execute( "UPDATE clients SET name = ?1, email = ?2, address = ?3, company = ?4, phone = ?5, tax_id = ?6, payment_terms = ?7, notes = ?8 WHERE id = ?9", params![client.name, client.email, client.address, client.company, client.phone, client.tax_id, client.payment_terms, client.notes, client.id], ).map_err(|e| e.to_string())?; Ok(()) } #[tauri::command] pub fn delete_client(state: State, id: i64) -> Result<(), String> { let conn = state.db.lock().map_err(|e| e.to_string())?; conn.execute("DELETE FROM clients WHERE id = ?1", params![id]).map_err(|e| e.to_string())?; Ok(()) } // Project commands #[tauri::command] pub fn get_projects(state: State) -> Result, String> { let conn = state.db.lock().map_err(|e| e.to_string())?; let mut stmt = conn.prepare( "SELECT id, client_id, name, hourly_rate, color, archived, budget_hours, budget_amount, rounding_override FROM projects ORDER BY name" ).map_err(|e| e.to_string())?; let projects = stmt.query_map([], |row| { Ok(Project { id: Some(row.get(0)?), client_id: row.get(1)?, name: row.get(2)?, hourly_rate: row.get(3)?, color: row.get(4)?, archived: row.get::<_, i32>(5)? != 0, budget_hours: row.get(6)?, budget_amount: row.get(7)?, rounding_override: row.get(8)?, }) }).map_err(|e| e.to_string())?; projects.collect::, _>>().map_err(|e| e.to_string()) } #[tauri::command] pub fn create_project(state: State, project: Project) -> Result { let conn = state.db.lock().map_err(|e| e.to_string())?; conn.execute( "INSERT INTO projects (client_id, name, hourly_rate, color, archived, budget_hours, budget_amount, rounding_override) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", params![project.client_id, project.name, project.hourly_rate, project.color, project.archived as i32, project.budget_hours, project.budget_amount, project.rounding_override], ).map_err(|e| e.to_string())?; Ok(conn.last_insert_rowid()) } #[tauri::command] pub fn update_project(state: State, project: Project) -> Result<(), String> { let conn = state.db.lock().map_err(|e| e.to_string())?; conn.execute( "UPDATE projects SET client_id = ?1, name = ?2, hourly_rate = ?3, color = ?4, archived = ?5, budget_hours = ?6, budget_amount = ?7, rounding_override = ?8 WHERE id = ?9", params![project.client_id, project.name, project.hourly_rate, project.color, project.archived as i32, project.budget_hours, project.budget_amount, project.rounding_override, project.id], ).map_err(|e| e.to_string())?; Ok(()) } #[tauri::command] pub fn delete_project(state: State, id: i64) -> Result<(), String> { let conn = state.db.lock().map_err(|e| e.to_string())?; conn.execute("DELETE FROM tracked_apps WHERE project_id = ?1", params![id]).map_err(|e| e.to_string())?; conn.execute("DELETE FROM projects WHERE id = ?1", params![id]).map_err(|e| e.to_string())?; Ok(()) } // Task commands #[tauri::command] pub fn get_tasks(state: State, project_id: i64) -> Result, String> { let conn = state.db.lock().map_err(|e| e.to_string())?; let mut stmt = conn.prepare("SELECT id, project_id, name FROM tasks WHERE project_id = ?1 ORDER BY name").map_err(|e| e.to_string())?; let tasks = stmt.query_map(params![project_id], |row| { Ok(Task { id: Some(row.get(0)?), project_id: row.get(1)?, name: row.get(2)?, }) }).map_err(|e| e.to_string())?; tasks.collect::, _>>().map_err(|e| e.to_string()) } #[tauri::command] pub fn create_task(state: State, task: Task) -> Result { let conn = state.db.lock().map_err(|e| e.to_string())?; conn.execute( "INSERT INTO tasks (project_id, name) VALUES (?1, ?2)", params![task.project_id, task.name], ).map_err(|e| e.to_string())?; Ok(conn.last_insert_rowid()) } #[tauri::command] pub fn delete_task(state: State, id: i64) -> Result<(), String> { let conn = state.db.lock().map_err(|e| e.to_string())?; conn.execute("DELETE FROM tasks WHERE id = ?1", params![id]).map_err(|e| e.to_string())?; Ok(()) } // Time entry commands #[tauri::command] pub fn get_time_entries(state: State, start_date: Option, end_date: Option) -> Result, String> { let conn = state.db.lock().map_err(|e| e.to_string())?; let query = match (start_date, end_date) { (Some(start), Some(end)) => { let mut stmt = conn.prepare( "SELECT id, project_id, task_id, description, start_time, end_time, duration FROM time_entries WHERE date(start_time) >= date(?1) AND date(start_time) <= date(?2) ORDER BY start_time DESC" ).map_err(|e| e.to_string())?; let rows = stmt.query_map(params![start, end], |row| { Ok(TimeEntry { id: Some(row.get(0)?), project_id: row.get(1)?, task_id: row.get(2)?, description: row.get(3)?, start_time: row.get(4)?, end_time: row.get(5)?, duration: row.get(6)?, }) }).map_err(|e| e.to_string())?; rows.collect::, _>>().map_err(|e| e.to_string())? } _ => { let mut stmt = conn.prepare( "SELECT id, project_id, task_id, description, start_time, end_time, duration FROM time_entries ORDER BY start_time DESC LIMIT 100" ).map_err(|e| e.to_string())?; let rows = stmt.query_map([], |row| { Ok(TimeEntry { id: Some(row.get(0)?), project_id: row.get(1)?, task_id: row.get(2)?, description: row.get(3)?, start_time: row.get(4)?, end_time: row.get(5)?, duration: row.get(6)?, }) }).map_err(|e| e.to_string())?; rows.collect::, _>>().map_err(|e| e.to_string())? } }; Ok(query) } #[tauri::command] pub fn create_time_entry(state: State, entry: TimeEntry) -> Result { let conn = state.db.lock().map_err(|e| e.to_string())?; conn.execute( "INSERT INTO time_entries (project_id, task_id, description, start_time, end_time, duration) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", params![entry.project_id, entry.task_id, entry.description, entry.start_time, entry.end_time, entry.duration], ).map_err(|e| e.to_string())?; Ok(conn.last_insert_rowid()) } #[tauri::command] pub fn update_time_entry(state: State, entry: TimeEntry) -> Result<(), String> { let conn = state.db.lock().map_err(|e| e.to_string())?; conn.execute( "UPDATE time_entries SET project_id = ?1, task_id = ?2, description = ?3, start_time = ?4, end_time = ?5, duration = ?6 WHERE id = ?7", params![entry.project_id, entry.task_id, entry.description, entry.start_time, entry.end_time, entry.duration, entry.id], ).map_err(|e| e.to_string())?; Ok(()) } #[tauri::command] pub fn delete_time_entry(state: State, id: i64) -> Result<(), String> { let conn = state.db.lock().map_err(|e| e.to_string())?; conn.execute("DELETE FROM time_entries WHERE id = ?1", params![id]).map_err(|e| e.to_string())?; Ok(()) } // Reports #[tauri::command] pub fn get_reports(state: State, start_date: String, end_date: String) -> Result { let conn = state.db.lock().map_err(|e| e.to_string())?; // Total hours let total: i64 = conn.query_row( "SELECT COALESCE(SUM(duration), 0) FROM time_entries WHERE date(start_time) >= date(?1) AND date(start_time) <= date(?2)", params![start_date, end_date], |row| row.get(0), ).map_err(|e| e.to_string())?; // By project - include project_id so frontend can look up name/color/rate let mut stmt = conn.prepare( "SELECT p.id, p.name, p.color, SUM(t.duration) as total_duration FROM time_entries t JOIN projects p ON t.project_id = p.id WHERE date(t.start_time) >= date(?1) AND date(t.start_time) <= date(?2) GROUP BY p.id ORDER BY total_duration DESC" ).map_err(|e| e.to_string())?; let by_project: Vec = stmt.query_map(params![start_date, end_date], |row| { Ok(serde_json::json!({ "project_id": row.get::<_, i64>(0)?, "name": row.get::<_, String>(1)?, "color": row.get::<_, String>(2)?, "total_seconds": row.get::<_, i64>(3)? })) }).map_err(|e| e.to_string())? .collect::, _>>().map_err(|e| e.to_string())?; Ok(serde_json::json!({ "totalSeconds": total, "byProject": by_project })) } // Invoice commands #[tauri::command] 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)", 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], ).map_err(|e| e.to_string())?; Ok(conn.last_insert_rowid()) } #[tauri::command] pub fn get_invoices(state: State) -> 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 FROM invoices ORDER BY date DESC" ).map_err(|e| e.to_string())?; let invoices = stmt.query_map([], |row| { Ok(Invoice { id: Some(row.get(0)?), client_id: row.get(1)?, invoice_number: row.get(2)?, date: row.get(3)?, due_date: row.get(4)?, subtotal: row.get(5)?, tax_rate: row.get(6)?, tax_amount: row.get(7)?, discount: row.get(8)?, total: row.get(9)?, notes: row.get(10)?, status: row.get(11)?, }) }).map_err(|e| e.to_string())?; invoices.collect::, _>>().map_err(|e| e.to_string()) } #[tauri::command] pub fn update_invoice(state: State, invoice: Invoice) -> Result<(), String> { 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", 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], ).map_err(|e| e.to_string())?; Ok(()) } #[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(()) } // Settings commands #[tauri::command] pub fn get_settings(state: State) -> Result, String> { let conn = state.db.lock().map_err(|e| e.to_string())?; let mut stmt = conn.prepare("SELECT key, value FROM settings").map_err(|e| e.to_string())?; let settings = stmt.query_map([], |row| { Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) }).map_err(|e| e.to_string())?; let mut result = std::collections::HashMap::new(); for setting in settings { let (key, value) = setting.map_err(|e| e.to_string())?; result.insert(key, value); } Ok(result) } #[tauri::command] pub fn update_settings(state: State, key: String, value: String) -> Result<(), String> { let conn = state.db.lock().map_err(|e| e.to_string())?; conn.execute( "INSERT OR REPLACE INTO settings (key, value) VALUES (?1, ?2)", params![key, value], ).map_err(|e| e.to_string())?; Ok(()) } // Export all data as JSON #[tauri::command] pub fn export_data(state: State) -> Result { let conn = state.db.lock().map_err(|e| e.to_string())?; let clients = { let mut stmt = conn.prepare("SELECT id, name, email, address, company, phone, tax_id, payment_terms, notes FROM clients").map_err(|e| e.to_string())?; let rows: Vec = stmt.query_map([], |row| { Ok(serde_json::json!({ "id": row.get::<_, i64>(0)?, "name": row.get::<_, String>(1)?, "email": row.get::<_, Option>(2)?, "address": row.get::<_, Option>(3)?, "company": row.get::<_, Option>(4)?, "phone": row.get::<_, Option>(5)?, "tax_id": row.get::<_, Option>(6)?, "payment_terms": row.get::<_, Option>(7)?, "notes": row.get::<_, Option>(8)? })) }).map_err(|e| e.to_string())?.collect::, _>>().map_err(|e| e.to_string())?; rows }; let projects = { let mut stmt = conn.prepare("SELECT id, client_id, name, hourly_rate, color, archived, budget_hours, budget_amount, rounding_override FROM projects").map_err(|e| e.to_string())?; let rows: Vec = stmt.query_map([], |row| { Ok(serde_json::json!({ "id": row.get::<_, i64>(0)?, "client_id": row.get::<_, Option>(1)?, "name": row.get::<_, String>(2)?, "hourly_rate": row.get::<_, f64>(3)?, "color": row.get::<_, String>(4)?, "archived": row.get::<_, i32>(5)? != 0, "budget_hours": row.get::<_, Option>(6)?, "budget_amount": row.get::<_, Option>(7)?, "rounding_override": row.get::<_, Option>(8)? })) }).map_err(|e| e.to_string())?.collect::, _>>().map_err(|e| e.to_string())?; rows }; let time_entries = { let mut stmt = conn.prepare("SELECT id, project_id, task_id, description, start_time, end_time, duration FROM time_entries").map_err(|e| e.to_string())?; let rows: Vec = stmt.query_map([], |row| { Ok(serde_json::json!({ "id": row.get::<_, i64>(0)?, "project_id": row.get::<_, i64>(1)?, "task_id": row.get::<_, Option>(2)?, "description": row.get::<_, Option>(3)?, "start_time": row.get::<_, String>(4)?, "end_time": row.get::<_, Option>(5)?, "duration": row.get::<_, i64>(6)? })) }).map_err(|e| e.to_string())?.collect::, _>>().map_err(|e| e.to_string())?; rows }; let invoices = { let mut stmt = conn.prepare("SELECT id, client_id, invoice_number, date, due_date, subtotal, tax_rate, tax_amount, discount, total, notes, status FROM invoices").map_err(|e| e.to_string())?; let rows: Vec = stmt.query_map([], |row| { Ok(serde_json::json!({ "id": row.get::<_, i64>(0)?, "client_id": row.get::<_, i64>(1)?, "invoice_number": row.get::<_, String>(2)?, "date": row.get::<_, String>(3)?, "due_date": row.get::<_, Option>(4)?, "subtotal": row.get::<_, f64>(5)?, "tax_rate": row.get::<_, f64>(6)?, "tax_amount": row.get::<_, f64>(7)?, "discount": row.get::<_, f64>(8)?, "total": row.get::<_, f64>(9)?, "notes": row.get::<_, Option>(10)?, "status": row.get::<_, String>(11)? })) }).map_err(|e| e.to_string())?.collect::, _>>().map_err(|e| e.to_string())?; rows }; Ok(serde_json::json!({ "clients": clients, "projects": projects, "time_entries": time_entries, "invoices": invoices })) } // Clear all data #[tauri::command] pub fn clear_all_data(state: State) -> Result<(), String> { let conn = state.db.lock().map_err(|e| e.to_string())?; conn.execute_batch( "DELETE FROM tracked_apps; DELETE FROM invoice_items; DELETE FROM invoices; DELETE FROM time_entries; DELETE FROM tasks; DELETE FROM projects; DELETE FROM clients;" ).map_err(|e| e.to_string())?; Ok(()) } // Tracked Apps struct #[derive(Debug, Serialize, Deserialize)] pub struct TrackedApp { pub id: Option, pub project_id: i64, pub exe_name: String, pub exe_path: Option, pub display_name: Option, } // OS Detection commands #[tauri::command] pub fn get_idle_seconds() -> Result { Ok(os_detection::get_system_idle_seconds()) } #[tauri::command] pub fn get_visible_windows() -> Result, String> { Ok(os_detection::enumerate_visible_windows()) } #[tauri::command] pub fn get_running_processes() -> Result, String> { Ok(os_detection::enumerate_running_processes()) } // Tracked Apps CRUD commands #[tauri::command] pub fn get_tracked_apps(state: State, project_id: i64) -> Result, String> { let conn = state.db.lock().map_err(|e| e.to_string())?; let mut stmt = conn.prepare( "SELECT id, project_id, exe_name, exe_path, display_name FROM tracked_apps WHERE project_id = ?1 ORDER BY display_name" ).map_err(|e| e.to_string())?; let apps = stmt.query_map(params![project_id], |row| { Ok(TrackedApp { id: Some(row.get(0)?), project_id: row.get(1)?, exe_name: row.get(2)?, exe_path: row.get(3)?, display_name: row.get(4)?, }) }).map_err(|e| e.to_string())?; apps.collect::, _>>().map_err(|e| e.to_string()) } #[tauri::command] pub fn add_tracked_app(state: State, app: TrackedApp) -> Result { let conn = state.db.lock().map_err(|e| e.to_string())?; conn.execute( "INSERT INTO tracked_apps (project_id, exe_name, exe_path, display_name) VALUES (?1, ?2, ?3, ?4)", params![app.project_id, app.exe_name, app.exe_path, app.display_name], ).map_err(|e| e.to_string())?; Ok(conn.last_insert_rowid()) } #[tauri::command] pub fn remove_tracked_app(state: State, id: i64) -> Result<(), String> { let conn = state.db.lock().map_err(|e| e.to_string())?; conn.execute("DELETE FROM tracked_apps WHERE id = ?1", params![id]).map_err(|e| e.to_string())?; Ok(()) } // Tag structs and commands #[derive(Debug, Serialize, Deserialize)] pub struct Tag { pub id: Option, pub name: String, pub color: String, } #[tauri::command] pub fn get_tags(state: State) -> Result, String> { let conn = state.db.lock().map_err(|e| e.to_string())?; let mut stmt = conn.prepare("SELECT id, name, color FROM tags ORDER BY name") .map_err(|e| e.to_string())?; let tags = stmt.query_map([], |row| { Ok(Tag { id: Some(row.get(0)?), name: row.get(1)?, color: row.get(2)?, }) }).map_err(|e| e.to_string())?; tags.collect::, _>>().map_err(|e| e.to_string()) } #[tauri::command] pub fn create_tag(state: State, tag: Tag) -> Result { let conn = state.db.lock().map_err(|e| e.to_string())?; conn.execute( "INSERT INTO tags (name, color) VALUES (?1, ?2)", params![tag.name, tag.color], ).map_err(|e| e.to_string())?; Ok(conn.last_insert_rowid()) } #[tauri::command] pub fn update_tag(state: State, tag: Tag) -> Result<(), String> { let conn = state.db.lock().map_err(|e| e.to_string())?; conn.execute( "UPDATE tags SET name = ?1, color = ?2 WHERE id = ?3", params![tag.name, tag.color, tag.id], ).map_err(|e| e.to_string())?; Ok(()) } #[tauri::command] pub fn delete_tag(state: State, id: i64) -> Result<(), String> { let conn = state.db.lock().map_err(|e| e.to_string())?; conn.execute("DELETE FROM entry_tags WHERE tag_id = ?1", params![id]) .map_err(|e| e.to_string())?; conn.execute("DELETE FROM tags WHERE id = ?1", params![id]) .map_err(|e| e.to_string())?; Ok(()) } #[tauri::command] pub fn get_entry_tags(state: State, entry_id: i64) -> Result, String> { let conn = state.db.lock().map_err(|e| e.to_string())?; let mut stmt = conn.prepare( "SELECT t.id, t.name, t.color FROM tags t JOIN entry_tags et ON t.id = et.tag_id WHERE et.entry_id = ?1 ORDER BY t.name" ).map_err(|e| e.to_string())?; let tags = stmt.query_map(params![entry_id], |row| { Ok(Tag { id: Some(row.get(0)?), name: row.get(1)?, color: row.get(2)?, }) }).map_err(|e| e.to_string())?; tags.collect::, _>>().map_err(|e| e.to_string()) } #[tauri::command] pub fn set_entry_tags(state: State, entry_id: i64, tag_ids: Vec) -> Result<(), String> { let conn = state.db.lock().map_err(|e| e.to_string())?; conn.execute("DELETE FROM entry_tags WHERE entry_id = ?1", params![entry_id]) .map_err(|e| e.to_string())?; for tag_id in tag_ids { conn.execute( "INSERT INTO entry_tags (entry_id, tag_id) VALUES (?1, ?2)", params![entry_id, tag_id], ).map_err(|e| e.to_string())?; } Ok(()) } #[tauri::command] pub fn get_project_budget_status(state: State, project_id: i64) -> Result { let conn = state.db.lock().map_err(|e| e.to_string())?; let total_seconds: i64 = conn.query_row( "SELECT COALESCE(SUM(duration), 0) FROM time_entries WHERE project_id = ?1", params![project_id], |row| row.get(0), ).map_err(|e| e.to_string())?; let project_row: (Option, Option, f64) = conn.query_row( "SELECT budget_hours, budget_amount, hourly_rate FROM projects WHERE id = ?1", params![project_id], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)), ).map_err(|e| e.to_string())?; let hours_used = total_seconds as f64 / 3600.0; let amount_used = hours_used * project_row.2; Ok(serde_json::json!({ "hours_used": hours_used, "amount_used": amount_used, "budget_hours": project_row.0, "budget_amount": project_row.1, "percent_hours": project_row.0.map(|b| if b > 0.0 { (hours_used / b) * 100.0 } else { 0.0 }), "percent_amount": project_row.1.map(|b| if b > 0.0 { (amount_used / b) * 100.0 } else { 0.0 }) })) } // Favorite structs and commands #[derive(Debug, Serialize, Deserialize)] pub struct Favorite { pub id: Option, pub project_id: i64, pub task_id: Option, pub description: Option, pub sort_order: i32, } #[tauri::command] pub fn get_favorites(state: State) -> Result, String> { let conn = state.db.lock().map_err(|e| e.to_string())?; let mut stmt = conn.prepare( "SELECT id, project_id, task_id, description, sort_order FROM favorites ORDER BY sort_order" ).map_err(|e| e.to_string())?; let favs = stmt.query_map([], |row| { Ok(Favorite { id: Some(row.get(0)?), project_id: row.get(1)?, task_id: row.get(2)?, description: row.get(3)?, sort_order: row.get(4)?, }) }).map_err(|e| e.to_string())?; favs.collect::, _>>().map_err(|e| e.to_string()) } #[tauri::command] pub fn create_favorite(state: State, fav: Favorite) -> Result { let conn = state.db.lock().map_err(|e| e.to_string())?; conn.execute( "INSERT INTO favorites (project_id, task_id, description, sort_order) VALUES (?1, ?2, ?3, ?4)", params![fav.project_id, fav.task_id, fav.description, fav.sort_order], ).map_err(|e| e.to_string())?; Ok(conn.last_insert_rowid()) } #[tauri::command] pub fn delete_favorite(state: State, id: i64) -> Result<(), String> { let conn = state.db.lock().map_err(|e| e.to_string())?; conn.execute("DELETE FROM favorites WHERE id = ?1", params![id]) .map_err(|e| e.to_string())?; Ok(()) } #[tauri::command] pub fn reorder_favorites(state: State, ids: Vec) -> Result<(), String> { let conn = state.db.lock().map_err(|e| e.to_string())?; for (i, id) in ids.iter().enumerate() { conn.execute( "UPDATE favorites SET sort_order = ?1 WHERE id = ?2", params![i as i32, id], ).map_err(|e| e.to_string())?; } Ok(()) }