356 lines
13 KiB
Rust
356 lines
13 KiB
Rust
use crate::AppState;
|
|
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>,
|
|
}
|
|
|
|
#[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,
|
|
}
|
|
|
|
#[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 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)?,
|
|
})
|
|
}).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) VALUES (?1, ?2, ?3)",
|
|
params![client.name, client.email, client.address],
|
|
).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 WHERE id = ?4",
|
|
params![client.name, client.email, client.address, 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 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,
|
|
})
|
|
}).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) VALUES (?1, ?2, ?3, ?4, ?5)",
|
|
params![project.client_id, project.name, project.hourly_rate, project.color, project.archived as i32],
|
|
).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 WHERE id = ?6",
|
|
params![project.client_id, project.name, project.hourly_rate, project.color, project.archived as i32, 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 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())?;
|
|
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())?.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())?;
|
|
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())?.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
|
|
let mut stmt = conn.prepare(
|
|
"SELECT 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!({
|
|
"name": row.get::<_, String>(0)?,
|
|
"color": row.get::<_, String>(1)?,
|
|
"duration": row.get::<_, i64>(2)?
|
|
}))
|
|
}).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())
|
|
}
|
|
|
|
// 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(())
|
|
}
|