feat: comprehensive export with all tables and auto-backup command

This commit is contained in:
Your Name
2026-02-20 15:40:02 +02:00
parent e97bc0f640
commit 0ae431b8ac
2 changed files with 449 additions and 3 deletions

View File

@@ -716,6 +716,19 @@ pub fn export_data(state: State<AppState>) -> Result<serde_json::Value, String>
rows rows
}; };
let tasks = {
let mut stmt = conn.prepare("SELECT id, project_id, name, estimated_hours FROM tasks").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)?,
"name": row.get::<_, String>(2)?,
"estimated_hours": row.get::<_, Option<f64>>(3)?
}))
}).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
rows
};
let time_entries = { let time_entries = {
let mut stmt = conn.prepare("SELECT id, project_id, task_id, description, start_time, end_time, duration, billable FROM time_entries").map_err(|e| e.to_string())?; let mut stmt = conn.prepare("SELECT id, project_id, task_id, description, start_time, end_time, duration, billable FROM time_entries").map_err(|e| e.to_string())?;
let rows: Vec<serde_json::Value> = stmt.query_map([], |row| { let rows: Vec<serde_json::Value> = stmt.query_map([], |row| {
@@ -733,8 +746,31 @@ pub fn export_data(state: State<AppState>) -> Result<serde_json::Value, String>
rows rows
}; };
let tags = {
let mut stmt = conn.prepare("SELECT id, name, color FROM tags").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)?,
"color": row.get::<_, Option<String>>(2)?
}))
}).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
rows
};
let entry_tags = {
let mut stmt = conn.prepare("SELECT entry_id, tag_id FROM entry_tags").map_err(|e| e.to_string())?;
let rows: Vec<serde_json::Value> = stmt.query_map([], |row| {
Ok(serde_json::json!({
"entry_id": row.get::<_, i64>(0)?,
"tag_id": row.get::<_, i64>(1)?
}))
}).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
rows
};
let invoices = { 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 mut stmt = conn.prepare("SELECT id, client_id, invoice_number, date, due_date, subtotal, tax_rate, tax_amount, discount, total, notes, status, template_id FROM invoices").map_err(|e| e.to_string())?;
let rows: Vec<serde_json::Value> = stmt.query_map([], |row| { let rows: Vec<serde_json::Value> = stmt.query_map([], |row| {
Ok(serde_json::json!({ Ok(serde_json::json!({
"id": row.get::<_, i64>(0)?, "id": row.get::<_, i64>(0)?,
@@ -748,7 +784,193 @@ pub fn export_data(state: State<AppState>) -> Result<serde_json::Value, String>
"discount": row.get::<_, f64>(8)?, "discount": row.get::<_, f64>(8)?,
"total": row.get::<_, f64>(9)?, "total": row.get::<_, f64>(9)?,
"notes": row.get::<_, Option<String>>(10)?, "notes": row.get::<_, Option<String>>(10)?,
"status": row.get::<_, String>(11)? "status": row.get::<_, String>(11)?,
"template_id": row.get::<_, Option<String>>(12)?
}))
}).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
rows
};
let invoice_items = {
let mut stmt = conn.prepare("SELECT id, invoice_id, description, quantity, rate, amount, time_entry_id FROM invoice_items").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)?,
"invoice_id": row.get::<_, i64>(1)?,
"description": row.get::<_, String>(2)?,
"quantity": row.get::<_, f64>(3)?,
"rate": row.get::<_, f64>(4)?,
"amount": row.get::<_, f64>(5)?,
"time_entry_id": row.get::<_, Option<i64>>(6)?
}))
}).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
rows
};
let tracked_apps = {
let mut stmt = conn.prepare("SELECT id, project_id, exe_name, exe_path, display_name FROM tracked_apps").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)?,
"exe_name": row.get::<_, String>(2)?,
"exe_path": row.get::<_, Option<String>>(3)?,
"display_name": row.get::<_, Option<String>>(4)?
}))
}).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
rows
};
let favorites = {
let mut stmt = conn.prepare("SELECT id, project_id, task_id, description, sort_order FROM favorites").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)?,
"sort_order": row.get::<_, i32>(4)?
}))
}).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
rows
};
let recurring_entries = {
let mut stmt = conn.prepare("SELECT id, project_id, task_id, description, duration, recurrence_rule, time_of_day, mode, enabled, last_triggered FROM recurring_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)?,
"duration": row.get::<_, i64>(4)?,
"recurrence_rule": row.get::<_, String>(5)?,
"time_of_day": row.get::<_, Option<String>>(6)?,
"mode": row.get::<_, Option<String>>(7)?,
"enabled": row.get::<_, i32>(8)?,
"last_triggered": row.get::<_, Option<String>>(9)?
}))
}).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
rows
};
let expenses = {
let mut stmt = conn.prepare("SELECT id, project_id, client_id, category, description, amount, date, receipt_path, invoiced FROM expenses").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)?,
"client_id": row.get::<_, Option<i64>>(2)?,
"category": row.get::<_, Option<String>>(3)?,
"description": row.get::<_, Option<String>>(4)?,
"amount": row.get::<_, f64>(5)?,
"date": row.get::<_, String>(6)?,
"receipt_path": row.get::<_, Option<String>>(7)?,
"invoiced": row.get::<_, i32>(8)?
}))
}).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
rows
};
let timeline_events = {
let mut stmt = conn.prepare("SELECT id, project_id, exe_name, exe_path, window_title, started_at, ended_at, duration FROM timeline_events").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)?,
"exe_name": row.get::<_, Option<String>>(2)?,
"exe_path": row.get::<_, Option<String>>(3)?,
"window_title": row.get::<_, Option<String>>(4)?,
"started_at": row.get::<_, String>(5)?,
"ended_at": row.get::<_, Option<String>>(6)?,
"duration": row.get::<_, i64>(7)?
}))
}).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
rows
};
let calendar_sources = {
let mut stmt = conn.prepare("SELECT id, name, type, url, last_synced, sync_interval, enabled FROM calendar_sources").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)?,
"source_type": row.get::<_, String>(2)?,
"url": row.get::<_, Option<String>>(3)?,
"last_synced": row.get::<_, Option<String>>(4)?,
"sync_interval": row.get::<_, i32>(5)?,
"enabled": row.get::<_, i32>(6)?
}))
}).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
rows
};
let calendar_events = {
let mut stmt = conn.prepare("SELECT id, source_id, uid, summary, start_time, end_time, duration, location FROM calendar_events").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)?,
"source_id": row.get::<_, i64>(1)?,
"uid": row.get::<_, Option<String>>(2)?,
"summary": row.get::<_, Option<String>>(3)?,
"start_time": row.get::<_, Option<String>>(4)?,
"end_time": row.get::<_, Option<String>>(5)?,
"duration": row.get::<_, i64>(6)?,
"location": row.get::<_, Option<String>>(7)?
}))
}).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
rows
};
let timesheet_locks = {
let mut stmt = conn.prepare("SELECT id, week_start, status, locked_at FROM timesheet_locks").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)?,
"week_start": row.get::<_, String>(1)?,
"status": row.get::<_, Option<String>>(2)?,
"locked_at": row.get::<_, Option<String>>(3)?
}))
}).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
rows
};
let entry_templates = {
let mut stmt = conn.prepare("SELECT id, name, project_id, task_id, description, duration, billable FROM entry_templates").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)?,
"project_id": row.get::<_, i64>(2)?,
"task_id": row.get::<_, Option<i64>>(3)?,
"description": row.get::<_, Option<String>>(4)?,
"duration": row.get::<_, i64>(5)?,
"billable": row.get::<_, i32>(6)?
}))
}).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
rows
};
let timesheet_rows = {
let mut stmt = conn.prepare("SELECT id, week_start, project_id, task_id, sort_order FROM timesheet_rows").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)?,
"week_start": row.get::<_, String>(1)?,
"project_id": row.get::<_, i64>(2)?,
"task_id": row.get::<_, Option<i64>>(3)?,
"sort_order": row.get::<_, i32>(4)?
}))
}).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
rows
};
let settings = {
let mut stmt = conn.prepare("SELECT key, value FROM settings").map_err(|e| e.to_string())?;
let rows: Vec<serde_json::Value> = stmt.query_map([], |row| {
Ok(serde_json::json!({
"key": row.get::<_, String>(0)?,
"value": row.get::<_, Option<String>>(1)?
})) }))
}).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?; }).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
rows rows
@@ -757,8 +979,23 @@ pub fn export_data(state: State<AppState>) -> Result<serde_json::Value, String>
Ok(serde_json::json!({ Ok(serde_json::json!({
"clients": clients, "clients": clients,
"projects": projects, "projects": projects,
"tasks": tasks,
"time_entries": time_entries, "time_entries": time_entries,
"invoices": invoices "tags": tags,
"entry_tags": entry_tags,
"invoices": invoices,
"invoice_items": invoice_items,
"tracked_apps": tracked_apps,
"favorites": favorites,
"recurring_entries": recurring_entries,
"expenses": expenses,
"timeline_events": timeline_events,
"calendar_sources": calendar_sources,
"calendar_events": calendar_events,
"timesheet_locks": timesheet_locks,
"entry_templates": entry_templates,
"timesheet_rows": timesheet_rows,
"settings": settings
})) }))
} }
@@ -1667,6 +1904,23 @@ pub fn import_json_data(state: State<AppState>, data: serde_json::Value) -> Resu
} }
} }
// Import tasks
if let Some(tasks) = data.get("tasks").and_then(|v| v.as_array()) {
for task in tasks {
let name = task.get("name").and_then(|v| v.as_str()).unwrap_or("");
if name.is_empty() { continue; }
let project_id = task.get("project_id").and_then(|v| v.as_i64());
if let Some(pid) = project_id {
let estimated_hours = task.get("estimated_hours").and_then(|v| v.as_f64());
conn.execute(
"INSERT OR IGNORE INTO tasks (project_id, name, estimated_hours) VALUES (?1, ?2, ?3)",
params![pid, name, estimated_hours],
).map_err(|e| e.to_string())?;
counts["tasks"] = serde_json::json!(counts["tasks"].as_i64().unwrap_or(0) + 1);
}
}
}
// Import time entries // Import time entries
if let Some(entries) = data.get("time_entries").and_then(|v| v.as_array()) { if let Some(entries) = data.get("time_entries").and_then(|v| v.as_array()) {
for entry in entries { for entry in entries {
@@ -1699,6 +1953,186 @@ pub fn import_json_data(state: State<AppState>, data: serde_json::Value) -> Resu
} }
} }
// Import entry_tags
if let Some(et_list) = data.get("entry_tags").and_then(|v| v.as_array()) {
for et in et_list {
let entry_id = et.get("entry_id").and_then(|v| v.as_i64());
let tag_id = et.get("tag_id").and_then(|v| v.as_i64());
if let (Some(eid), Some(tid)) = (entry_id, tag_id) {
conn.execute(
"INSERT OR IGNORE INTO entry_tags (entry_id, tag_id) VALUES (?1, ?2)",
params![eid, tid],
).ok();
}
}
}
// Import invoices
if let Some(invoices) = data.get("invoices").and_then(|v| v.as_array()) {
for inv in invoices {
let client_id = inv.get("client_id").and_then(|v| v.as_i64()).unwrap_or(0);
let invoice_number = inv.get("invoice_number").and_then(|v| v.as_str()).unwrap_or("");
if invoice_number.is_empty() { continue; }
let date = inv.get("date").and_then(|v| v.as_str()).unwrap_or("");
let due_date = inv.get("due_date").and_then(|v| v.as_str());
let subtotal = inv.get("subtotal").and_then(|v| v.as_f64()).unwrap_or(0.0);
let tax_rate = inv.get("tax_rate").and_then(|v| v.as_f64()).unwrap_or(0.0);
let tax_amount = inv.get("tax_amount").and_then(|v| v.as_f64()).unwrap_or(0.0);
let discount = inv.get("discount").and_then(|v| v.as_f64()).unwrap_or(0.0);
let total = inv.get("total").and_then(|v| v.as_f64()).unwrap_or(0.0);
let notes = inv.get("notes").and_then(|v| v.as_str());
let status = inv.get("status").and_then(|v| v.as_str()).unwrap_or("draft");
let template_id = inv.get("template_id").and_then(|v| v.as_str());
conn.execute(
"INSERT INTO invoices (client_id, invoice_number, date, due_date, subtotal, tax_rate, tax_amount, discount, total, notes, status, template_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
params![client_id, invoice_number, date, due_date, subtotal, tax_rate, tax_amount, discount, total, notes, status, template_id],
).ok();
}
}
// Import invoice_items
if let Some(items) = data.get("invoice_items").and_then(|v| v.as_array()) {
for item in items {
let invoice_id = item.get("invoice_id").and_then(|v| v.as_i64()).unwrap_or(0);
let description = item.get("description").and_then(|v| v.as_str()).unwrap_or("");
let quantity = item.get("quantity").and_then(|v| v.as_f64()).unwrap_or(1.0);
let rate = item.get("rate").and_then(|v| v.as_f64()).unwrap_or(0.0);
let amount = item.get("amount").and_then(|v| v.as_f64()).unwrap_or(0.0);
let time_entry_id = item.get("time_entry_id").and_then(|v| v.as_i64());
conn.execute(
"INSERT INTO invoice_items (invoice_id, description, quantity, rate, amount, time_entry_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
params![invoice_id, description, quantity, rate, amount, time_entry_id],
).ok();
}
}
// Import tracked_apps
if let Some(apps) = data.get("tracked_apps").and_then(|v| v.as_array()) {
for app in apps {
let project_id = app.get("project_id").and_then(|v| v.as_i64()).unwrap_or(0);
let exe_name = app.get("exe_name").and_then(|v| v.as_str()).unwrap_or("");
if exe_name.is_empty() { continue; }
let exe_path = app.get("exe_path").and_then(|v| v.as_str());
let display_name = app.get("display_name").and_then(|v| v.as_str());
conn.execute(
"INSERT INTO tracked_apps (project_id, exe_name, exe_path, display_name) VALUES (?1, ?2, ?3, ?4)",
params![project_id, exe_name, exe_path, display_name],
).ok();
}
}
// Import favorites
if let Some(favs) = data.get("favorites").and_then(|v| v.as_array()) {
for fav in favs {
let project_id = fav.get("project_id").and_then(|v| v.as_i64()).unwrap_or(0);
let task_id = fav.get("task_id").and_then(|v| v.as_i64());
let description = fav.get("description").and_then(|v| v.as_str());
let sort_order = fav.get("sort_order").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
conn.execute(
"INSERT INTO favorites (project_id, task_id, description, sort_order) VALUES (?1, ?2, ?3, ?4)",
params![project_id, task_id, description, sort_order],
).ok();
}
}
// Import recurring_entries
if let Some(recs) = data.get("recurring_entries").and_then(|v| v.as_array()) {
for rec in recs {
let project_id = rec.get("project_id").and_then(|v| v.as_i64()).unwrap_or(0);
let task_id = rec.get("task_id").and_then(|v| v.as_i64());
let description = rec.get("description").and_then(|v| v.as_str());
let duration = rec.get("duration").and_then(|v| v.as_i64()).unwrap_or(0);
let recurrence_rule = rec.get("recurrence_rule").and_then(|v| v.as_str()).unwrap_or("");
if recurrence_rule.is_empty() { continue; }
let time_of_day = rec.get("time_of_day").and_then(|v| v.as_str());
let mode = rec.get("mode").and_then(|v| v.as_str());
let enabled = rec.get("enabled").and_then(|v| v.as_i64()).unwrap_or(1) as i32;
let last_triggered = rec.get("last_triggered").and_then(|v| v.as_str());
conn.execute(
"INSERT INTO recurring_entries (project_id, task_id, description, duration, recurrence_rule, time_of_day, mode, enabled, last_triggered) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
params![project_id, task_id, description, duration, recurrence_rule, time_of_day, mode, enabled, last_triggered],
).ok();
}
}
// Import expenses
if let Some(exps) = data.get("expenses").and_then(|v| v.as_array()) {
for exp in exps {
let project_id = exp.get("project_id").and_then(|v| v.as_i64()).unwrap_or(0);
let client_id = exp.get("client_id").and_then(|v| v.as_i64());
let category = exp.get("category").and_then(|v| v.as_str());
let description = exp.get("description").and_then(|v| v.as_str());
let amount = exp.get("amount").and_then(|v| v.as_f64()).unwrap_or(0.0);
let date = exp.get("date").and_then(|v| v.as_str()).unwrap_or("");
if date.is_empty() { continue; }
let receipt_path = exp.get("receipt_path").and_then(|v| v.as_str());
let invoiced = exp.get("invoiced").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
conn.execute(
"INSERT INTO expenses (project_id, client_id, category, description, amount, date, receipt_path, invoiced) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
params![project_id, client_id, category, description, amount, date, receipt_path, invoiced],
).ok();
}
}
// Import settings
if let Some(settings_list) = data.get("settings").and_then(|v| v.as_array()) {
for setting in settings_list {
let key = setting.get("key").and_then(|v| v.as_str()).unwrap_or("");
if key.is_empty() { continue; }
let value = setting.get("value").and_then(|v| v.as_str());
conn.execute(
"INSERT OR REPLACE INTO settings (key, value) VALUES (?1, ?2)",
params![key, value],
).ok();
}
}
// Import entry_templates
if let Some(templates) = data.get("entry_templates").and_then(|v| v.as_array()) {
for tmpl in templates {
let name = tmpl.get("name").and_then(|v| v.as_str()).unwrap_or("");
if name.is_empty() { continue; }
let project_id = tmpl.get("project_id").and_then(|v| v.as_i64()).unwrap_or(0);
let task_id = tmpl.get("task_id").and_then(|v| v.as_i64());
let description = tmpl.get("description").and_then(|v| v.as_str());
let duration = tmpl.get("duration").and_then(|v| v.as_i64()).unwrap_or(0);
let billable = tmpl.get("billable").and_then(|v| v.as_i64()).unwrap_or(1) as i32;
conn.execute(
"INSERT INTO entry_templates (name, project_id, task_id, description, duration, billable) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
params![name, project_id, task_id, description, duration, billable],
).ok();
}
}
// Import timesheet_rows
if let Some(rows) = data.get("timesheet_rows").and_then(|v| v.as_array()) {
for row in rows {
let week_start = row.get("week_start").and_then(|v| v.as_str()).unwrap_or("");
if week_start.is_empty() { continue; }
let project_id = row.get("project_id").and_then(|v| v.as_i64()).unwrap_or(0);
let task_id = row.get("task_id").and_then(|v| v.as_i64());
let sort_order = row.get("sort_order").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
conn.execute(
"INSERT INTO timesheet_rows (week_start, project_id, task_id, sort_order) VALUES (?1, ?2, ?3, ?4)",
params![week_start, project_id, task_id, sort_order],
).ok();
}
}
// Import timesheet_locks
if let Some(locks) = data.get("timesheet_locks").and_then(|v| v.as_array()) {
for lock in locks {
let week_start = lock.get("week_start").and_then(|v| v.as_str()).unwrap_or("");
if week_start.is_empty() { continue; }
let status = lock.get("status").and_then(|v| v.as_str());
let locked_at = lock.get("locked_at").and_then(|v| v.as_str());
conn.execute(
"INSERT OR IGNORE INTO timesheet_locks (week_start, status, locked_at) VALUES (?1, ?2, ?3)",
params![week_start, status, locked_at],
).ok();
}
}
Ok(counts) Ok(counts)
} }
@@ -1794,6 +2228,17 @@ pub fn get_invoice_templates(state: State<AppState>) -> Result<Vec<InvoiceTempla
Ok(templates) Ok(templates)
} }
#[tauri::command]
pub fn auto_backup(state: State<AppState>, backup_dir: String) -> Result<String, String> {
let data = export_data(state)?;
let today = chrono::Local::now().format("%Y-%m-%d").to_string();
let filename = format!("zeroclock-backup-{}.json", today);
let path = std::path::Path::new(&backup_dir).join(&filename);
let json = serde_json::to_string_pretty(&data).map_err(|e| e.to_string())?;
std::fs::write(&path, json).map_err(|e| e.to_string())?;
Ok(path.to_string_lossy().to_string())
}
pub fn seed_default_templates(data_dir: &std::path::Path) { pub fn seed_default_templates(data_dir: &std::path::Path) {
let templates_dir = data_dir.join("templates"); let templates_dir = data_dir.join("templates");
std::fs::create_dir_all(&templates_dir).ok(); std::fs::create_dir_all(&templates_dir).ok();

View File

@@ -138,6 +138,7 @@ pub fn run() {
commands::get_timesheet_rows, commands::get_timesheet_rows,
commands::save_timesheet_rows, commands::save_timesheet_rows,
commands::get_previous_week_structure, commands::get_previous_week_structure,
commands::auto_backup,
]) ])
.setup(|app| { .setup(|app| {
#[cfg(desktop)] #[cfg(desktop)]