feat: add client billing fields to database and Rust backend
This commit is contained in:
@@ -9,6 +9,11 @@ pub struct Client {
|
||||
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)]
|
||||
@@ -59,13 +64,20 @@ pub struct Invoice {
|
||||
#[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 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())
|
||||
@@ -75,8 +87,8 @@ pub fn get_clients(state: State<AppState>) -> Result<Vec<Client>, String> {
|
||||
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],
|
||||
"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())
|
||||
}
|
||||
@@ -85,8 +97,8 @@ pub fn create_client(state: State<AppState>, client: Client) -> Result<i64, Stri
|
||||
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],
|
||||
"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(())
|
||||
}
|
||||
@@ -187,7 +199,7 @@ pub fn get_time_entries(state: State<AppState>, start_date: Option<String>, end_
|
||||
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| {
|
||||
let rows = stmt.query_map(params![start, end], |row| {
|
||||
Ok(TimeEntry {
|
||||
id: Some(row.get(0)?),
|
||||
project_id: row.get(1)?,
|
||||
@@ -197,14 +209,15 @@ pub fn get_time_entries(state: State<AppState>, start_date: Option<String>, end_
|
||||
end_time: row.get(5)?,
|
||||
duration: row.get(6)?,
|
||||
})
|
||||
}).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?
|
||||
}).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())?;
|
||||
stmt.query_map([], |row| {
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok(TimeEntry {
|
||||
id: Some(row.get(0)?),
|
||||
project_id: row.get(1)?,
|
||||
@@ -214,7 +227,8 @@ pub fn get_time_entries(state: State<AppState>, start_date: Option<String>, end_
|
||||
end_time: row.get(5)?,
|
||||
duration: row.get(6)?,
|
||||
})
|
||||
}).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?
|
||||
}).map_err(|e| e.to_string())?;
|
||||
rows.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?
|
||||
}
|
||||
};
|
||||
Ok(query)
|
||||
@@ -262,9 +276,9 @@ pub fn get_reports(state: State<AppState>, start_date: String, end_date: String)
|
||||
|row| row.get(0),
|
||||
).map_err(|e| e.to_string())?;
|
||||
|
||||
// By project
|
||||
// By project - include project_id so frontend can look up name/color/rate
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT p.name, p.color, SUM(t.duration) as total_duration
|
||||
"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)
|
||||
@@ -274,9 +288,10 @@ pub fn get_reports(state: State<AppState>, start_date: String, end_date: 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)?
|
||||
"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())?;
|
||||
@@ -327,6 +342,27 @@ pub fn get_invoices(state: State<AppState>) -> Result<Vec<Invoice>, 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> {
|
||||
@@ -353,3 +389,101 @@ pub fn update_settings(state: State<AppState>, key: String, value: String) -> Re
|
||||
).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 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
|
||||
}))
|
||||
}).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 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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user