feat: comprehensive export with all tables and auto-backup command
This commit is contained in:
@@ -716,6 +716,19 @@ pub fn export_data(state: State<AppState>) -> Result<serde_json::Value, String>
|
||||
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 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| {
|
||||
@@ -733,8 +746,31 @@ pub fn export_data(state: State<AppState>) -> Result<serde_json::Value, String>
|
||||
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 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| {
|
||||
Ok(serde_json::json!({
|
||||
"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)?,
|
||||
"total": row.get::<_, f64>(9)?,
|
||||
"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())?;
|
||||
rows
|
||||
@@ -757,8 +979,23 @@ pub fn export_data(state: State<AppState>) -> Result<serde_json::Value, String>
|
||||
Ok(serde_json::json!({
|
||||
"clients": clients,
|
||||
"projects": projects,
|
||||
"tasks": tasks,
|
||||
"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
|
||||
if let Some(entries) = data.get("time_entries").and_then(|v| v.as_array()) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1794,6 +2228,17 @@ pub fn get_invoice_templates(state: State<AppState>) -> Result<Vec<InvoiceTempla
|
||||
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) {
|
||||
let templates_dir = data_dir.join("templates");
|
||||
std::fs::create_dir_all(&templates_dir).ok();
|
||||
|
||||
@@ -138,6 +138,7 @@ pub fn run() {
|
||||
commands::get_timesheet_rows,
|
||||
commands::save_timesheet_rows,
|
||||
commands::get_previous_week_structure,
|
||||
commands::auto_backup,
|
||||
])
|
||||
.setup(|app| {
|
||||
#[cfg(desktop)]
|
||||
|
||||
Reference in New Issue
Block a user