Files
zeroclock/src-tauri/src/commands.rs

738 lines
28 KiB
Rust

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<i64>,
pub name: String,
pub email: Option<String>,
pub address: Option<String>,
pub company: Option<String>,
pub phone: Option<String>,
pub tax_id: Option<String>,
pub payment_terms: Option<String>,
pub notes: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Project {
pub id: Option<i64>,
pub client_id: Option<i64>,
pub name: String,
pub hourly_rate: f64,
pub color: String,
pub archived: bool,
pub budget_hours: Option<f64>,
pub budget_amount: Option<f64>,
pub rounding_override: Option<i32>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Task {
pub id: Option<i64>,
pub project_id: i64,
pub name: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TimeEntry {
pub id: Option<i64>,
pub project_id: i64,
pub task_id: Option<i64>,
pub description: Option<String>,
pub start_time: String,
pub end_time: Option<String>,
pub duration: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Invoice {
pub id: Option<i64>,
pub client_id: i64,
pub invoice_number: String,
pub date: String,
pub due_date: Option<String>,
pub subtotal: f64,
pub tax_rate: f64,
pub tax_amount: f64,
pub discount: f64,
pub total: f64,
pub notes: Option<String>,
pub status: String,
}
// Client commands
#[tauri::command]
pub fn get_clients(state: State<AppState>) -> Result<Vec<Client>, 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::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}
#[tauri::command]
pub fn create_client(state: State<AppState>, client: Client) -> Result<i64, String> {
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<AppState>, 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<AppState>, 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<AppState>) -> Result<Vec<Project>, 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::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}
#[tauri::command]
pub fn create_project(state: State<AppState>, project: Project) -> Result<i64, String> {
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<AppState>, 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<AppState>, 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<AppState>, project_id: i64) -> Result<Vec<Task>, 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::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}
#[tauri::command]
pub fn create_task(state: State<AppState>, task: Task) -> Result<i64, String> {
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<AppState>, 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<AppState>, start_date: Option<String>, end_date: Option<String>) -> Result<Vec<TimeEntry>, 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::<Result<Vec<_>, _>>().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::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?
}
};
Ok(query)
}
#[tauri::command]
pub fn create_time_entry(state: State<AppState>, entry: TimeEntry) -> Result<i64, String> {
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<AppState>, 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<AppState>, 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<AppState>, start_date: String, end_date: String) -> Result<serde_json::Value, String> {
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<serde_json::Value> = 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::<Result<Vec<_>, _>>().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<AppState>, invoice: Invoice) -> Result<i64, String> {
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<AppState>) -> Result<Vec<Invoice>, 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::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}
#[tauri::command]
pub fn update_invoice(state: State<AppState>, 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<AppState>, 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<AppState>) -> Result<std::collections::HashMap<String, String>, 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<AppState>, 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<AppState>) -> Result<serde_json::Value, String> {
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<serde_json::Value> = stmt.query_map([], |row| {
Ok(serde_json::json!({
"id": row.get::<_, i64>(0)?,
"name": row.get::<_, String>(1)?,
"email": row.get::<_, Option<String>>(2)?,
"address": row.get::<_, Option<String>>(3)?,
"company": row.get::<_, Option<String>>(4)?,
"phone": row.get::<_, Option<String>>(5)?,
"tax_id": row.get::<_, Option<String>>(6)?,
"payment_terms": row.get::<_, Option<String>>(7)?,
"notes": row.get::<_, Option<String>>(8)?
}))
}).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().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<serde_json::Value> = stmt.query_map([], |row| {
Ok(serde_json::json!({
"id": row.get::<_, i64>(0)?,
"client_id": row.get::<_, Option<i64>>(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<f64>>(6)?,
"budget_amount": row.get::<_, Option<f64>>(7)?,
"rounding_override": row.get::<_, Option<i32>>(8)?
}))
}).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().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<serde_json::Value> = stmt.query_map([], |row| {
Ok(serde_json::json!({
"id": row.get::<_, i64>(0)?,
"project_id": row.get::<_, i64>(1)?,
"task_id": row.get::<_, Option<i64>>(2)?,
"description": row.get::<_, Option<String>>(3)?,
"start_time": row.get::<_, String>(4)?,
"end_time": row.get::<_, Option<String>>(5)?,
"duration": row.get::<_, i64>(6)?
}))
}).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().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<serde_json::Value> = 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<String>>(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<String>>(10)?,
"status": row.get::<_, String>(11)?
}))
}).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().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<AppState>) -> 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<i64>,
pub project_id: i64,
pub exe_name: String,
pub exe_path: Option<String>,
pub display_name: Option<String>,
}
// OS Detection commands
#[tauri::command]
pub fn get_idle_seconds() -> Result<u64, String> {
Ok(os_detection::get_system_idle_seconds())
}
#[tauri::command]
pub fn get_visible_windows() -> Result<Vec<os_detection::WindowInfo>, String> {
Ok(os_detection::enumerate_visible_windows())
}
#[tauri::command]
pub fn get_running_processes() -> Result<Vec<os_detection::WindowInfo>, String> {
Ok(os_detection::enumerate_running_processes())
}
// Tracked Apps CRUD commands
#[tauri::command]
pub fn get_tracked_apps(state: State<AppState>, project_id: i64) -> Result<Vec<TrackedApp>, 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::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}
#[tauri::command]
pub fn add_tracked_app(state: State<AppState>, app: TrackedApp) -> Result<i64, String> {
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<AppState>, 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<i64>,
pub name: String,
pub color: String,
}
#[tauri::command]
pub fn get_tags(state: State<AppState>) -> Result<Vec<Tag>, 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::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}
#[tauri::command]
pub fn create_tag(state: State<AppState>, tag: Tag) -> Result<i64, String> {
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<AppState>, 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<AppState>, 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<AppState>, entry_id: i64) -> Result<Vec<Tag>, 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::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}
#[tauri::command]
pub fn set_entry_tags(state: State<AppState>, entry_id: i64, tag_ids: Vec<i64>) -> 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<AppState>, project_id: i64) -> Result<serde_json::Value, String> {
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<f64>, Option<f64>, 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<i64>,
pub project_id: i64,
pub task_id: Option<i64>,
pub description: Option<String>,
pub sort_order: i32,
}
#[tauri::command]
pub fn get_favorites(state: State<AppState>) -> Result<Vec<Favorite>, 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::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}
#[tauri::command]
pub fn create_favorite(state: State<AppState>, fav: Favorite) -> Result<i64, String> {
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<AppState>, 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<AppState>, ids: Vec<i64>) -> 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(())
}