From 7b118c1a1c055ac1212acf99603f1bfd707da7e0 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 21 Feb 2026 01:34:26 +0200 Subject: [PATCH] feat: complete export/import cycle and remove sample data Export now includes invoice_payments and recurring_invoices tables. Import restored to use ID-based lookups and all fields for clients, projects, tasks, and time entries. Added missing import support for timeline_events, calendar_sources, calendar_events, invoice_payments, and recurring_invoices. Export uses native save dialog instead of blob download. Removed sample data seeding (seed.rs, UI, command). --- src-tauri/src/commands.rs | 206 ++++++--- src-tauri/src/lib.rs | 2 - src-tauri/src/seed.rs | 672 ---------------------------- src/components/JsonImportWizard.vue | 9 + src/views/Settings.vue | 84 +--- 5 files changed, 177 insertions(+), 796 deletions(-) delete mode 100644 src-tauri/src/seed.rs diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 62dc214..0898c0c 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -991,6 +991,42 @@ pub fn export_data(state: State) -> Result rows }; + let invoice_payments = { + let mut stmt = conn.prepare("SELECT id, invoice_id, amount, date, method, notes, created_at FROM invoice_payments").map_err(|e| e.to_string())?; + let rows: Vec = stmt.query_map([], |row| { + Ok(serde_json::json!({ + "id": row.get::<_, i64>(0)?, + "invoice_id": row.get::<_, i64>(1)?, + "amount": row.get::<_, f64>(2)?, + "date": row.get::<_, String>(3)?, + "method": row.get::<_, Option>(4)?, + "notes": row.get::<_, Option>(5)?, + "created_at": row.get::<_, Option>(6)? + })) + }).map_err(|e| e.to_string())?.collect::, _>>().map_err(|e| e.to_string())?; + rows + }; + + let recurring_invoices = { + let mut stmt = conn.prepare("SELECT id, client_id, template_id, line_items_json, tax_rate, discount, notes, recurrence_rule, next_due_date, enabled, created_at FROM recurring_invoices").map_err(|e| e.to_string())?; + let rows: Vec = stmt.query_map([], |row| { + Ok(serde_json::json!({ + "id": row.get::<_, i64>(0)?, + "client_id": row.get::<_, i64>(1)?, + "template_id": row.get::<_, Option>(2)?, + "line_items_json": row.get::<_, String>(3)?, + "tax_rate": row.get::<_, f64>(4)?, + "discount": row.get::<_, f64>(5)?, + "notes": row.get::<_, Option>(6)?, + "recurrence_rule": row.get::<_, String>(7)?, + "next_due_date": row.get::<_, String>(8)?, + "enabled": row.get::<_, i32>(9)?, + "created_at": row.get::<_, Option>(10)? + })) + }).map_err(|e| e.to_string())?.collect::, _>>().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 = stmt.query_map([], |row| { @@ -1011,6 +1047,8 @@ pub fn export_data(state: State) -> Result "entry_tags": entry_tags, "invoices": invoices, "invoice_items": invoice_items, + "invoice_payments": invoice_payments, + "recurring_invoices": recurring_invoices, "tracked_apps": tracked_apps, "favorites": favorites, "recurring_entries": recurring_entries, @@ -1902,9 +1940,15 @@ pub fn import_json_data(state: State, data: serde_json::Value) -> Resu if name.is_empty() { continue; } let email = client.get("email").and_then(|v| v.as_str()); let address = client.get("address").and_then(|v| v.as_str()); + let company = client.get("company").and_then(|v| v.as_str()); + let phone = client.get("phone").and_then(|v| v.as_str()); + let tax_id = client.get("tax_id").and_then(|v| v.as_str()); + let payment_terms = client.get("payment_terms").and_then(|v| v.as_str()); + let notes = client.get("notes").and_then(|v| v.as_str()); + let currency = client.get("currency").and_then(|v| v.as_str()); conn.execute( - "INSERT OR IGNORE INTO clients (name, email, address) VALUES (?1, ?2, ?3)", - params![name, email, address], + "INSERT OR IGNORE INTO clients (name, email, address, company, phone, tax_id, payment_terms, notes, currency) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", + params![name, email, address, company, phone, tax_id, payment_terms, notes, currency], ).map_err(|e| e.to_string())?; counts["clients"] = serde_json::json!(counts["clients"].as_i64().unwrap_or(0) + 1); } @@ -1915,34 +1959,20 @@ pub fn import_json_data(state: State, data: serde_json::Value) -> Resu for project in projects { let name = project.get("name").and_then(|v| v.as_str()).unwrap_or(""); if name.is_empty() { continue; } + let client_id = project.get("client_id").and_then(|v| v.as_i64()); let hourly_rate = project.get("hourly_rate").and_then(|v| v.as_f64()).unwrap_or(0.0); let color = project.get("color").and_then(|v| v.as_str()).unwrap_or("#F59E0B"); - - // Find client_id if client_name is provided - let client_id: Option = project.get("client_name") - .and_then(|v| v.as_str()) - .and_then(|client_name| { - conn.query_row( - "SELECT id FROM clients WHERE name = ?1", - params![client_name], - |row| row.get(0), - ).ok() - }); - - // Check if project already exists - let exists: bool = conn.query_row( - "SELECT COUNT(*) FROM projects WHERE name = ?1", - params![name], - |row| row.get::<_, i64>(0), - ).map_err(|e| e.to_string())? > 0; - - if !exists { - conn.execute( - "INSERT INTO projects (client_id, name, hourly_rate, color, archived) VALUES (?1, ?2, ?3, ?4, 0)", - params![client_id, name, hourly_rate, color], - ).map_err(|e| e.to_string())?; - counts["projects"] = serde_json::json!(counts["projects"].as_i64().unwrap_or(0) + 1); - } + let archived = if project.get("archived").and_then(|v| v.as_bool()).unwrap_or(false) { 1 } else { 0 }; + let budget_hours = project.get("budget_hours").and_then(|v| v.as_f64()); + let budget_amount = project.get("budget_amount").and_then(|v| v.as_f64()); + let rounding_override = project.get("rounding_override").and_then(|v| v.as_i64()).map(|v| v as i32); + let notes = project.get("notes").and_then(|v| v.as_str()); + let currency = project.get("currency").and_then(|v| v.as_str()); + conn.execute( + "INSERT OR IGNORE INTO projects (client_id, name, hourly_rate, color, archived, budget_hours, budget_amount, rounding_override, notes, currency) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", + params![client_id, name, hourly_rate, color, archived, budget_hours, budget_amount, rounding_override, notes, currency], + ).map_err(|e| e.to_string())?; + counts["projects"] = serde_json::json!(counts["projects"].as_i64().unwrap_or(0) + 1); } } @@ -1968,9 +1998,10 @@ pub fn import_json_data(state: State, data: serde_json::Value) -> Resu 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()); + let hourly_rate = task.get("hourly_rate").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], + "INSERT OR IGNORE INTO tasks (project_id, name, estimated_hours, hourly_rate) VALUES (?1, ?2, ?3, ?4)", + params![pid, name, estimated_hours, hourly_rate], ).map_err(|e| e.to_string())?; counts["tasks"] = serde_json::json!(counts["tasks"].as_i64().unwrap_or(0) + 1); } @@ -1980,29 +2011,19 @@ pub fn import_json_data(state: State, data: serde_json::Value) -> Resu // Import time entries if let Some(entries) = data.get("time_entries").and_then(|v| v.as_array()) { for entry in entries { - let project_name = entry.get("project_name").and_then(|v| v.as_str()).unwrap_or(""); let start_time = entry.get("start_time").and_then(|v| v.as_str()).unwrap_or(""); if start_time.is_empty() { continue; } - - let project_id: Option = if !project_name.is_empty() { - conn.query_row( - "SELECT id FROM projects WHERE name = ?1", - params![project_name], - |row| row.get(0), - ).ok() - } else { - entry.get("project_id").and_then(|v| v.as_i64()) - }; - + let project_id = entry.get("project_id").and_then(|v| v.as_i64()); if let Some(pid) = project_id { + let task_id = entry.get("task_id").and_then(|v| v.as_i64()); let description = entry.get("description").and_then(|v| v.as_str()); let end_time = entry.get("end_time").and_then(|v| v.as_str()); let duration = entry.get("duration").and_then(|v| v.as_i64()).unwrap_or(0); let billable = entry.get("billable").and_then(|v| v.as_i64()).unwrap_or(1); conn.execute( - "INSERT INTO time_entries (project_id, description, start_time, end_time, duration, billable) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", - params![pid, description, start_time, end_time, duration, billable], + "INSERT INTO time_entries (project_id, task_id, description, start_time, end_time, duration, billable) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + params![pid, task_id, description, start_time, end_time, duration, billable], ).map_err(|e| e.to_string())?; counts["entries"] = serde_json::json!(counts["entries"].as_i64().unwrap_or(0) + 1); } @@ -2130,6 +2151,97 @@ pub fn import_json_data(state: State, data: serde_json::Value) -> Resu } } + // Import timeline_events + if let Some(events) = data.get("timeline_events").and_then(|v| v.as_array()) { + for evt in events { + let project_id = evt.get("project_id").and_then(|v| v.as_i64()).unwrap_or(0); + let exe_name = evt.get("exe_name").and_then(|v| v.as_str()); + let exe_path = evt.get("exe_path").and_then(|v| v.as_str()); + let window_title = evt.get("window_title").and_then(|v| v.as_str()); + let started_at = evt.get("started_at").and_then(|v| v.as_str()).unwrap_or(""); + if started_at.is_empty() { continue; } + let ended_at = evt.get("ended_at").and_then(|v| v.as_str()); + let duration = evt.get("duration").and_then(|v| v.as_i64()).unwrap_or(0); + conn.execute( + "INSERT INTO timeline_events (project_id, exe_name, exe_path, window_title, started_at, ended_at, duration) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + params![project_id, exe_name, exe_path, window_title, started_at, ended_at, duration], + ).ok(); + } + } + + // Import calendar_sources + if let Some(sources) = data.get("calendar_sources").and_then(|v| v.as_array()) { + for src in sources { + let name = src.get("name").and_then(|v| v.as_str()).unwrap_or(""); + if name.is_empty() { continue; } + let source_type = src.get("source_type").and_then(|v| v.as_str()).unwrap_or(""); + let url = src.get("url").and_then(|v| v.as_str()); + let last_synced = src.get("last_synced").and_then(|v| v.as_str()); + let sync_interval = src.get("sync_interval").and_then(|v| v.as_i64()).unwrap_or(3600) as i32; + let enabled = src.get("enabled").and_then(|v| v.as_i64()).unwrap_or(1) as i32; + conn.execute( + "INSERT INTO calendar_sources (name, type, url, last_synced, sync_interval, enabled) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + params![name, source_type, url, last_synced, sync_interval, enabled], + ).ok(); + } + } + + // Import calendar_events + if let Some(cal_events) = data.get("calendar_events").and_then(|v| v.as_array()) { + for evt in cal_events { + let source_id = evt.get("source_id").and_then(|v| v.as_i64()).unwrap_or(0); + let uid = evt.get("uid").and_then(|v| v.as_str()); + let summary = evt.get("summary").and_then(|v| v.as_str()); + let start_time = evt.get("start_time").and_then(|v| v.as_str()); + let end_time = evt.get("end_time").and_then(|v| v.as_str()); + let duration = evt.get("duration").and_then(|v| v.as_i64()).unwrap_or(0); + let location = evt.get("location").and_then(|v| v.as_str()); + conn.execute( + "INSERT INTO calendar_events (source_id, uid, summary, start_time, end_time, duration, location) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + params![source_id, uid, summary, start_time, end_time, duration, location], + ).ok(); + } + } + + // Import invoice_payments + if let Some(payments) = data.get("invoice_payments").and_then(|v| v.as_array()) { + for pay in payments { + let invoice_id = pay.get("invoice_id").and_then(|v| v.as_i64()).unwrap_or(0); + let amount = pay.get("amount").and_then(|v| v.as_f64()).unwrap_or(0.0); + let date = pay.get("date").and_then(|v| v.as_str()).unwrap_or(""); + if date.is_empty() { continue; } + let method = pay.get("method").and_then(|v| v.as_str()); + let notes = pay.get("notes").and_then(|v| v.as_str()); + let created_at = pay.get("created_at").and_then(|v| v.as_str()); + conn.execute( + "INSERT INTO invoice_payments (invoice_id, amount, date, method, notes, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + params![invoice_id, amount, date, method, notes, created_at], + ).ok(); + } + } + + // Import recurring_invoices + if let Some(rec_invs) = data.get("recurring_invoices").and_then(|v| v.as_array()) { + for ri in rec_invs { + let client_id = ri.get("client_id").and_then(|v| v.as_i64()).unwrap_or(0); + let template_id = ri.get("template_id").and_then(|v| v.as_str()); + let line_items_json = ri.get("line_items_json").and_then(|v| v.as_str()).unwrap_or("[]"); + let tax_rate = ri.get("tax_rate").and_then(|v| v.as_f64()).unwrap_or(0.0); + let discount = ri.get("discount").and_then(|v| v.as_f64()).unwrap_or(0.0); + let notes = ri.get("notes").and_then(|v| v.as_str()); + let recurrence_rule = ri.get("recurrence_rule").and_then(|v| v.as_str()).unwrap_or(""); + if recurrence_rule.is_empty() { continue; } + let next_due_date = ri.get("next_due_date").and_then(|v| v.as_str()).unwrap_or(""); + if next_due_date.is_empty() { continue; } + let enabled = ri.get("enabled").and_then(|v| v.as_i64()).unwrap_or(1) as i32; + let created_at = ri.get("created_at").and_then(|v| v.as_str()); + conn.execute( + "INSERT INTO recurring_invoices (client_id, template_id, line_items_json, tax_rate, discount, notes, recurrence_rule, next_due_date, enabled, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", + params![client_id, template_id, line_items_json, tax_rate, discount, notes, recurrence_rule, next_due_date, enabled, created_at], + ).ok(); + } + } + // Import settings if let Some(settings_list) = data.get("settings").and_then(|v| v.as_array()) { for setting in settings_list { @@ -3642,9 +3754,3 @@ fn get_default_templates() -> Vec { ] } -#[tauri::command] -pub fn seed_sample_data(state: State) -> Result { - let conn = state.db.lock().map_err(|e| e.to_string())?; - crate::seed::seed(&conn)?; - Ok("Sample data loaded".to_string()) -} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index fd431e2..c06b0a8 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -6,7 +6,6 @@ use tauri::Manager; mod database; mod commands; mod os_detection; -mod seed; pub struct AppState { pub db: Mutex, @@ -155,7 +154,6 @@ pub fn run() { commands::update_recurring_invoice, commands::delete_recurring_invoice, commands::check_recurring_invoices, - commands::seed_sample_data, ]) .setup(|app| { #[cfg(desktop)] diff --git a/src-tauri/src/seed.rs b/src-tauri/src/seed.rs deleted file mode 100644 index 2f07e58..0000000 --- a/src-tauri/src/seed.rs +++ /dev/null @@ -1,672 +0,0 @@ -use rusqlite::Connection; - -fn hash(n: u32) -> u32 { - let x = n.wrapping_mul(2654435761); - let y = (x ^ (x >> 16)).wrapping_mul(2246822519); - y ^ (y >> 13) -} - -fn offset_to_ymd(offset: u32) -> (u32, u32, u32) { - static MONTHS: [(u32, u32, u32); 12] = [ - (2025, 3, 31), - (2025, 4, 30), - (2025, 5, 31), - (2025, 6, 30), - (2025, 7, 31), - (2025, 8, 31), - (2025, 9, 30), - (2025, 10, 31), - (2025, 11, 30), - (2025, 12, 31), - (2026, 1, 31), - (2026, 2, 28), - ]; - let mut rem = offset; - for &(y, m, d) in &MONTHS { - if rem < d { - return (y, m, rem + 1); - } - rem -= d; - } - (2026, 2, 28) -} - -struct ProjPeriod { - project_id: i64, - task_ids: &'static [i64], - desc_pool: usize, - start_day: u32, - end_day: u32, - billable: i64, -} - -static PROJ_PERIODS: &[ProjPeriod] = &[ - ProjPeriod { project_id: 1, task_ids: &[1, 2, 3, 4], desc_pool: 0, start_day: 2, end_day: 75, billable: 1 }, - ProjPeriod { project_id: 2, task_ids: &[5, 6, 7, 8], desc_pool: 1, start_day: 2, end_day: 155, billable: 1 }, - ProjPeriod { project_id: 3, task_ids: &[9, 10, 11, 12], desc_pool: 1, start_day: 33, end_day: 122, billable: 1 }, - ProjPeriod { project_id: 4, task_ids: &[13, 14, 15, 16], desc_pool: 0, start_day: 63, end_day: 183, billable: 1 }, - ProjPeriod { project_id: 5, task_ids: &[17, 18], desc_pool: 7, start_day: 2, end_day: 356, billable: 0 }, - ProjPeriod { project_id: 6, task_ids: &[19, 20, 21], desc_pool: 3, start_day: 93, end_day: 155, billable: 1 }, - ProjPeriod { project_id: 7, task_ids: &[22, 23, 24, 25], desc_pool: 5, start_day: 122, end_day: 183, billable: 1 }, - ProjPeriod { project_id: 8, task_ids: &[26, 27, 28, 29], desc_pool: 0, start_day: 155, end_day: 214, billable: 1 }, - ProjPeriod { project_id: 9, task_ids: &[30, 31, 32, 33], desc_pool: 4, start_day: 184, end_day: 244, billable: 1 }, - ProjPeriod { project_id: 10, task_ids: &[34, 35, 36], desc_pool: 1, start_day: 214, end_day: 244, billable: 1 }, - ProjPeriod { project_id: 11, task_ids: &[37, 38, 39, 40], desc_pool: 5, start_day: 214, end_day: 275, billable: 1 }, - ProjPeriod { project_id: 12, task_ids: &[41, 42, 43], desc_pool: 3, start_day: 245, end_day: 336, billable: 1 }, - ProjPeriod { project_id: 13, task_ids: &[44, 45, 46, 47], desc_pool: 1, start_day: 275, end_day: 356, billable: 1 }, - ProjPeriod { project_id: 14, task_ids: &[48, 49, 50], desc_pool: 1, start_day: 306, end_day: 356, billable: 1 }, - ProjPeriod { project_id: 15, task_ids: &[51, 52], desc_pool: 0, start_day: 306, end_day: 336, billable: 1 }, - ProjPeriod { project_id: 16, task_ids: &[53, 54, 55], desc_pool: 1, start_day: 306, end_day: 356, billable: 1 }, - ProjPeriod { project_id: 17, task_ids: &[56, 57, 58], desc_pool: 1, start_day: 93, end_day: 356, billable: 1 }, - ProjPeriod { project_id: 18, task_ids: &[59, 60], desc_pool: 7, start_day: 214, end_day: 244, billable: 0 }, -]; - -static DESC_POOLS: &[&[&str]] = &[ - // 0: Logo/brand - &[ - "Concept sketches - exploring directions", - "Color palette tests on paper", - "Wordmark spacing and kerning", - "Symbol refinement - tightening curves", - "Client presentation deck", - "Applying revision notes from call", - "Final vector cleanup and export", - "Brand guidelines page layout", - "Moodboard assembly", - "Scanning hand-drawn letterforms", - ], - // 1: Illustration - &[ - "Thumbnail compositions", - "Reference gathering and mood board", - "Rough pencil sketches", - "Inking line art", - "Flat color blocking", - "Rendering pass - light and shadow", - "Background and texture work", - "Final cleanup and detail pass", - "Scanning and color correction", - "Exploring alternate compositions", - "Detail work on foreground elements", - "Adding halftone textures", - ], - // 2: Typography/layout - &[ - "Typography pairing tests", - "Page layout drafts", - "Grid and margin adjustments", - "Hierarchy and scale refinement", - "Print proof review", - "Spread layout and flow", - ], - // 3: Web/digital - &[ - "Wireframe sketches on paper", - "Homepage hero illustration", - "Responsive layout mockups", - "Icon set - first batch", - "Custom divider illustrations", - "Gallery page layout", - "Color theme adjustments for screen", - "Asset export for dev handoff", - ], - // 4: Packaging - &[ - "Die-cut template measurements", - "Repeating pattern tile design", - "Label artwork - front panel", - "Box mockup rendering", - "Press-ready PDF export", - "Color proofing adjustments", - "Tissue paper pattern", - "Sticker sheet layout", - ], - // 5: Book cover - &[ - "Reading manuscript excerpts for feel", - "Cover thumbnail sketches", - "Main illustration - rough draft", - "Color composition study", - "Title lettering and spine layout", - "Full cover rendering", - "Back cover synopsis layout", - "Author photo placement and bio", - ], - // 6: Meeting/admin - &[ - "Client call - project kickoff", - "Reviewing feedback document", - "Scope and timeline email", - "Invoice prep and send", - "File organization and archiving", - ], - // 7: Personal - &[ - "Selecting portfolio pieces", - "Writing case study notes", - "Photographing finished prints", - "Updating website gallery", - "Sketching for fun", - "Ink drawing - daily prompt", - "Scanning and posting work", - "Reorganizing reference library", - ], -]; - -pub fn seed(conn: &Connection) -> Result<(), String> { - let e = |err: rusqlite::Error| err.to_string(); - - conn.execute_batch( - "PRAGMA foreign_keys = OFF; - DELETE FROM entry_tags; - DELETE FROM invoice_payments; - DELETE FROM invoice_items; - DELETE FROM recurring_invoices; - DELETE FROM invoices; - DELETE FROM favorites; - DELETE FROM recurring_entries; - DELETE FROM entry_templates; - DELETE FROM timesheet_rows; - DELETE FROM timesheet_locks; - DELETE FROM timeline_events; - DELETE FROM expenses; - DELETE FROM tracked_apps; - DELETE FROM time_entries; - DELETE FROM tasks; - DELETE FROM projects; - DELETE FROM clients; - DELETE FROM tags; - DELETE FROM calendar_events; - DELETE FROM calendar_sources; - DELETE FROM sqlite_sequence; - PRAGMA foreign_keys = ON;", - ) - .map_err(e)?; - - // ========================================== - // CLIENTS - // ========================================== - conn.execute_batch( - "INSERT INTO clients (id, name, email, company, phone, payment_terms, notes) VALUES - (1, 'Anna Kowalski', 'anna@moonlightbakery.com', 'Moonlight Bakery', '555-0142', 'net_30', 'Longtime client. Loves warm earth tones and hand-drawn feel.'), - (2, 'James Okonkwo', 'james@riverandstone.com', 'River & Stone Pottery', '555-0238', 'net_15', 'Prefers email. Needs high-res for print catalog.'), - (3, 'Rosa Delgado', 'rosa@velvetsparrow.com', 'The Velvet Sparrow', '555-0319', 'net_30', 'Band manager. Quick feedback, clear direction.'), - (4, 'Tom Brennan', 'tom@fernandwillow.com', 'Fern & Willow Cafe', '555-0421', 'net_30', 'Very responsive. The cafe on Elm St has great coffee.'), - (5, 'Marcus Chen', 'marcus@marcuschen.com', NULL, '555-0517', 'due_on_receipt', 'Photographer. Good referral source.'), - (6, 'Diane Huang', 'diane@wildfieldpress.com', 'Wildfield Press', '555-0634', 'net_45', 'Publisher - steady ongoing work. Pays reliably.'), - (7, 'Kai Nishimura', 'kai@sableandco.com', 'Sable & Co Tattoo', '555-0728', 'net_15', 'Expects fast turnaround. Loves bold linework.');", - ) - .map_err(e)?; - - // ========================================== - // PROJECTS - // ========================================== - conn.execute_batch( - "INSERT INTO projects (id, client_id, name, hourly_rate, color, archived, budget_hours, notes) VALUES - (1, 1, 'Moonlight Logo Redesign', 65, '#F59E0B', 1, 50, 'Modernizing the logo. Keep the crescent moon motif.'), - (2, 2, 'Product Catalog', 70, '#8B5CF6', 1, 130, '48-page catalog for spring/summer pottery collection.'), - (3, 3, 'Album Cover - Quiet Hours', 75, '#EF4444', 1, 65, 'Debut album. Dreamy watercolor feel, night sky theme.'), - (4, 4, 'Fern & Willow Rebrand', 70, '#10B981', 1, 110, 'Full rebrand - logo, menu boards, signage, socials.'), - (5, NULL, 'Portfolio Update', 0, '#6B7280', 0, NULL, 'Ongoing portfolio maintenance and case studies.'), - (6, 5, 'Portfolio Website', 60, '#3B82F6', 1, 55, 'Custom illustrations for photography portfolio.'), - (7, 6, 'Tide Pool Dreams - Cover', 75, '#06B6D4', 1, 60, 'Middle-grade novel cover. Lush underwater scene.'), - (8, 7, 'Sable & Co Brand Kit', 80, '#A855F7', 1, 55, 'Full identity - logo, cards, signage, flash sheet.'), - (9, 1, 'Seasonal Packaging', 60, '#EC4899', 1, 70, 'Holiday gift box designs and labels.'), - (10, 3, 'Tour Poster - West Coast', 60, '#DC2626', 1, 35, 'Screenprint poster for 12-city tour.'), - (11, 6, 'Moth & Lantern - Cover', 75, '#0EA5E9', 1, 60, 'YA fantasy novel cover. Moths, lantern light, forest.'), - (12, 2, 'Website Illustrations', 65, '#6366F1', 0, 85, 'Custom spot illustrations for new e-commerce site.'), - (13, 4, 'Mural Design', 65, '#34D399', 0, 75, 'Interior mural - botanical garden theme, 8ft x 12ft.'), - (14, 1, 'Menu Illustrations', 55, '#F97316', 0, 45, 'Hand-drawn food illos for seasonal menu refresh.'), - (15, 5, 'Business Cards', 50, '#60A5FA', 1, 18, 'Custom illustrated business card with foil stamp.'), - (16, 3, 'Merch Designs', 55, '#F43F5E', 0, 40, 'T-shirt, sticker, and tote bag art for online store.'), - (17, 6, 'Monthly Spot Illustrations', 50, '#14B8A6', 0, 100, 'Recurring spot illos for chapter headers in books.'), - (18, NULL, 'Inktober 2025', 0, '#1F2937', 1, NULL, 'Personal daily ink drawing challenge.');", - ) - .map_err(e)?; - - // ========================================== - // TASKS (60 tasks across 18 projects) - // ========================================== - conn.execute_batch( - "INSERT INTO tasks (id, project_id, name, estimated_hours) VALUES - (1, 1, 'Research', 8), - (2, 1, 'Sketching', 15), - (3, 1, 'Refinement', 15), - (4, 1, 'Final Delivery', 10), - (5, 2, 'Photography Layout', 30), - (6, 2, 'Illustration', 50), - (7, 2, 'Typography', 25), - (8, 2, 'Print Prep', 20), - (9, 3, 'Concept Art', 15), - (10, 3, 'Main Illustration', 25), - (11, 3, 'Lettering', 12), - (12, 3, 'File Prep', 8), - (13, 4, 'Brand Strategy', 15), - (14, 4, 'Logo Design', 35), - (15, 4, 'Collateral', 35), - (16, 4, 'Signage', 20), - (17, 5, 'Curation', NULL), - (18, 5, 'Photography', NULL), - (19, 6, 'Wireframes', 12), - (20, 6, 'Visual Design', 25), - (21, 6, 'Asset Creation', 15), - (22, 7, 'Reading', 8), - (23, 7, 'Sketches', 15), - (24, 7, 'Cover Art', 25), - (25, 7, 'Layout', 10), - (26, 8, 'Research', 10), - (27, 8, 'Concepts', 15), - (28, 8, 'Refinement', 18), - (29, 8, 'Brand Kit', 12), - (30, 9, 'Template Setup', 10), - (31, 9, 'Pattern Design', 20), - (32, 9, 'Label Art', 25), - (33, 9, 'Press Files', 12), - (34, 10, 'Layout', 10), - (35, 10, 'Illustration', 18), - (36, 10, 'Print Prep', 5), - (37, 11, 'Reading', 8), - (38, 11, 'Sketches', 15), - (39, 11, 'Cover Art', 25), - (40, 11, 'Layout', 10), - (41, 12, 'Page Illustrations', 30), - (42, 12, 'Icon Set', 25), - (43, 12, 'Banner Art', 20), - (44, 13, 'Concept', 12), - (45, 13, 'Scale Drawing', 20), - (46, 13, 'Color Studies', 18), - (47, 13, 'Detail Work', 22), - (48, 14, 'Food Illustrations', 20), - (49, 14, 'Layout', 12), - (50, 14, 'Spot Art', 10), - (51, 15, 'Design', 12), - (52, 15, 'Print Prep', 5), - (53, 16, 'T-shirt Art', 15), - (54, 16, 'Sticker Designs', 12), - (55, 16, 'Tote Bag Art', 10), - (56, 17, 'Sketching', 35), - (57, 17, 'Inking', 35), - (58, 17, 'Coloring', 25), - (59, 18, 'Daily Prompts', NULL), - (60, 18, 'Scanning', NULL);", - ) - .map_err(e)?; - - // ========================================== - // TAGS - // ========================================== - conn.execute_batch( - "INSERT INTO tags (id, name, color) VALUES - (1, 'rush', '#EF4444'), - (2, 'revision', '#F59E0B'), - (3, 'pro-bono', '#10B981'), - (4, 'personal', '#6B7280'), - (5, 'concept', '#8B5CF6'), - (6, 'final', '#3B82F6'), - (7, 'meeting', '#EC4899'), - (8, 'admin', '#6366F1');", - ) - .map_err(e)?; - - // ========================================== - // TIME ENTRIES (generated) - // ========================================== - let mut stmt = conn - .prepare( - "INSERT INTO time_entries (project_id, task_id, description, start_time, end_time, duration, billable) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", - ) - .map_err(e)?; - - let session_starts: [(u32, u32); 4] = [(9, 0), (11, 0), (13, 30), (16, 0)]; - let session_maxmins: [u32; 4] = [120, 150, 150, 120]; - - let mut entry_count: i64 = 0; - - for day_offset in 0u32..357 { - let dow = (6 + day_offset) % 7; - if dow == 0 || dow == 6 { - // Weekend: only Inktober gets weekend work - if day_offset >= 214 && day_offset <= 244 { - let h = hash(day_offset); - if h % 3 == 0 { - let (y, m, d) = offset_to_ymd(day_offset); - let date = format!("{:04}-{:02}-{:02}", y, m, d); - let di = (h / 7) as usize % DESC_POOLS[7].len(); - let ti = if h % 2 == 0 { 59i64 } else { 60 }; - let dur_mins = 30 + (h % 60); - let start = format!("{}T10:{:02}:00", date, h % 45); - let dur_secs = (dur_mins * 60) as i64; - let end_mins = 10 * 60 + (h % 45) + dur_mins; - let end = format!("{}T{:02}:{:02}:00", date, end_mins / 60, end_mins % 60); - stmt.execute(rusqlite::params![18i64, ti, DESC_POOLS[7][di], start, end, dur_secs, 0i64]).map_err(e)?; - entry_count += 1; - } - } - continue; - } - - let h = hash(day_offset); - // Skip ~5% of weekdays (sick/vacation) - if h % 20 == 0 { - continue; - } - - let (y, m, d) = offset_to_ymd(day_offset); - let date = format!("{:04}-{:02}-{:02}", y, m, d); - - // Collect active projects - let active: Vec<&ProjPeriod> = PROJ_PERIODS - .iter() - .filter(|p| day_offset >= p.start_day && day_offset <= p.end_day) - .filter(|p| { - // Personal/portfolio only shows up ~15% of days - if p.project_id == 5 { - return hash(day_offset.wrapping_mul(5)) % 7 == 0; - } - true - }) - .collect(); - - if active.is_empty() { - continue; - } - - let n_sessions = 2 + (h % 2) as usize; // 2-3 sessions - let n_sessions = n_sessions.min(active.len().max(2)); - - for s in 0..n_sessions { - if s >= 4 { - break; - } - let sh = hash(day_offset * 100 + s as u32); - let proj_idx = (sh as usize) % active.len(); - let proj = active[proj_idx]; - - let task_idx = (sh / 3) as usize % proj.task_ids.len(); - let task_id = proj.task_ids[task_idx]; - let pool = DESC_POOLS[proj.desc_pool]; - let desc_idx = (sh / 7) as usize % pool.len(); - let desc = pool[desc_idx]; - - let (base_h, base_m) = session_starts[s]; - let max_mins = session_maxmins[s]; - let dur_mins = 45 + sh % (max_mins - 44); - let start_offset_mins = (sh / 11) % 20; - let start_h = base_h + (base_m + start_offset_mins) / 60; - let start_m = (base_m + start_offset_mins) % 60; - let end_total = start_h * 60 + start_m + dur_mins; - let end_h = end_total / 60; - let end_m = end_total % 60; - - if end_h >= 19 { - continue; - } - - let start = format!("{}T{:02}:{:02}:00", date, start_h, start_m); - let end = format!("{}T{:02}:{:02}:00", date, end_h, end_m); - let dur_secs = (dur_mins * 60) as i64; - - stmt.execute(rusqlite::params![ - proj.project_id, - task_id, - desc, - start, - end, - dur_secs, - proj.billable - ]) - .map_err(e)?; - entry_count += 1; - } - - // Occasional admin/meeting entry (~20% of days) - if h % 5 == 0 && !active.is_empty() { - let sh = hash(day_offset * 200); - let proj = active[0]; - let admin_descs = DESC_POOLS[6]; - let di = (sh / 3) as usize % admin_descs.len(); - let dur_mins = 15 + sh % 30; - let start = format!("{}T08:{:02}:00", date, 30 + sh % 25); - let end_total_mins = 8 * 60 + 30 + (sh % 25) + dur_mins; - let end = format!( - "{}T{:02}:{:02}:00", - date, - end_total_mins / 60, - end_total_mins % 60 - ); - stmt.execute(rusqlite::params![ - proj.project_id, - proj.task_ids[0], - admin_descs[di], - start, - end, - (dur_mins * 60) as i64, - proj.billable - ]) - .map_err(e)?; - entry_count += 1; - } - } - - drop(stmt); - - // ========================================== - // ENTRY TAGS (tag ~15% of entries) - // ========================================== - let total_entries = entry_count; - let tag_assignments: Vec<(i64, i64)> = (1..=total_entries) - .filter_map(|id| { - let h = hash(id as u32 * 31); - if h % 7 != 0 { - return None; - } - let tag = match h % 40 { - 0..=5 => 1, // rush - 6..=15 => 2, // revision - 16..=20 => 5, // concept - 21..=28 => 6, // final - 29..=33 => 7, // meeting - 34..=37 => 8, // admin - _ => 2, // revision - }; - Some((id, tag)) - }) - .collect(); - - let mut tag_stmt = conn - .prepare("INSERT OR IGNORE INTO entry_tags (entry_id, tag_id) VALUES (?1, ?2)") - .map_err(e)?; - for (eid, tid) in &tag_assignments { - tag_stmt.execute(rusqlite::params![eid, tid]).map_err(e)?; - } - drop(tag_stmt); - - // ========================================== - // EXPENSES - // ========================================== - conn.execute_batch( - "INSERT INTO expenses (project_id, client_id, category, description, amount, date, invoiced) VALUES - -- Software subscriptions (monthly) - (5, NULL, 'software', 'Clip Studio Paint Pro - annual', 49.99, '2025-03-15', 0), - (5, NULL, 'software', 'Affinity Designer 2 license', 69.99, '2025-04-02', 0), - (5, NULL, 'software', 'Dropbox Plus - annual renewal', 119.88, '2025-06-01', 0), - (5, NULL, 'software', 'Squarespace portfolio site - annual', 192.00, '2025-07-15', 0), - (17, 6, 'software', 'Font license - Recoleta family', 45.00, '2025-08-20', 1), - - -- Art supplies - (1, 1, 'supplies', 'Copic markers (12 pack, warm grays)', 89.99, '2025-03-08', 0), - (3, 3, 'supplies', 'Winsor & Newton watercolor set', 124.50, '2025-04-10', 0), - (2, 2, 'supplies', 'A3 hot press watercolor paper (50 sheets)', 42.00, '2025-05-05', 0), - (4, 4, 'supplies', 'Posca paint markers (8 pack)', 34.99, '2025-06-18', 0), - (18, NULL,'supplies', 'India ink - Sumi (3 bottles)', 27.50, '2025-10-01', 0), - (18, NULL,'supplies', 'Micron pen set (8 widths)', 22.99, '2025-10-03', 0), - (13, 4, 'supplies', 'Acrylic paint (mural - bulk order)', 187.00, '2025-12-20', 0), - (14, 1, 'supplies', 'Brush pen set for menu illos', 18.50, '2026-01-12', 0), - - -- Printing - (2, 2, 'printing', 'Test prints - catalog spreads', 85.00, '2025-07-22', 1), - (9, 1, 'printing', 'Packaging prototypes (6 units)', 120.00, '2025-10-15', 1), - (10, 3, 'printing', 'Poster screenprint run (50 copies)', 275.00, '2025-11-01', 1), - (15, 5, 'printing', 'Business card print run (250)', 65.00, '2026-01-28', 1), - - -- Reference materials - (5, NULL, 'other', 'Illustration annual 2025', 38.00, '2025-04-22', 0), - (5, NULL, 'other', 'Color and Light by James Gurney', 28.50, '2025-05-30', 0), - (7, 6, 'other', 'Marine biology reference photos (stock)', 29.00, '2025-07-10', 1), - - -- Travel - (4, 4, 'travel', 'Bus pass - client site visits (monthly)', 45.00, '2025-06-01', 0), - (4, 4, 'travel', 'Bus pass - client site visits (monthly)', 45.00, '2025-07-01', 0), - (8, 7, 'travel', 'Transit to tattoo parlor for measurements', 8.50, '2025-08-12', 0), - (13, 4, 'travel', 'Transit to cafe for mural measurements', 8.50, '2025-12-15', 0), - (13, 4, 'travel', 'Transit to cafe - mural install day', 8.50, '2026-02-10', 0), - - -- Equipment - (5, NULL, 'equipment', 'Tablet screen protector replacement', 24.99, '2025-09-05', 0), - (5, NULL, 'equipment', 'Desk lamp (daylight bulb)', 45.00, '2025-11-20', 0);", - ) - .map_err(e)?; - - // ========================================== - // INVOICES - // ========================================== - conn.execute_batch( - "INSERT INTO invoices (id, client_id, invoice_number, date, due_date, subtotal, tax_rate, tax_amount, discount, total, notes, status) VALUES - (1, 1, 'INV-2025-001', '2025-05-28', '2025-06-27', 3120.00, 0, 0, 0, 3120.00, 'Logo redesign - concept through final delivery', 'paid'), - (2, 2, 'INV-2025-002', '2025-06-15', '2025-06-30', 4550.00, 0, 0, 0, 4550.00, 'Product catalog - first milestone (layout and illustrations)', 'paid'), - (3, 3, 'INV-2025-003', '2025-06-30', '2025-07-30', 4500.00, 0, 0, 0, 4500.00, 'Album cover art - Quiet Hours', 'paid'), - (4, 4, 'INV-2025-004', '2025-07-31', '2025-08-30', 3850.00, 0, 0, 0, 3850.00, 'Rebrand milestone 1 - logo and primary collateral', 'paid'), - (5, 2, 'INV-2025-005', '2025-08-15', '2025-08-30', 3850.00, 0, 0, 0, 3850.00, 'Product catalog - final milestone (print prep)', 'paid'), - (6, 5, 'INV-2025-006', '2025-08-20', '2025-08-20', 3000.00, 0, 0, 0, 3000.00, 'Portfolio website illustrations', 'paid'), - (7, 6, 'INV-2025-007', '2025-09-10', '2025-10-25', 4125.00, 0, 0, 0, 4125.00, 'Tide Pool Dreams - cover art', 'paid'), - (8, 4, 'INV-2025-008', '2025-09-30', '2025-10-30', 3500.00, 0, 0, 0, 3500.00, 'Rebrand milestone 2 - signage and social templates', 'paid'), - (9, 7, 'INV-2025-009', '2025-10-20', '2025-11-04', 4160.00, 0, 0, 0, 4160.00, 'Sable & Co - full brand kit', 'paid'), - (10, 6, 'INV-2025-010', '2025-09-30', '2025-11-14', 2250.00, 0, 0, 0, 2250.00, 'Monthly spot illustrations - Q3 (Jul-Sep)', 'overdue'), - (11, 1, 'INV-2025-011', '2025-11-25', '2025-12-25', 3780.00, 0, 0, 0, 3780.00, 'Seasonal packaging - holiday gift line', 'paid'), - (12, 3, 'INV-2025-012', '2025-11-20', '2025-12-20', 1860.00, 0, 0, 0, 1860.00, 'Tour poster - West Coast (design + print mgmt)', 'paid'), - (13, 6, 'INV-2025-013', '2025-12-20', '2026-02-03', 4275.00, 0, 0, 0, 4275.00, 'Moth & Lantern - cover art', 'sent'), - (14, 6, 'INV-2025-014', '2025-12-31', '2026-02-14', 2500.00, 0, 0, 0, 2500.00, 'Monthly spot illustrations - Q4 (Oct-Dec)', 'sent'), - (15, 2, 'INV-2026-001', '2026-01-31', '2026-02-14', 4225.00, 0, 0, 0, 4225.00, 'Website illustrations - first half', 'sent'), - (16, 4, 'INV-2026-002', '2026-02-15', '2026-03-17', 2600.00, 0, 0, 0, 2600.00, 'Mural design - concept and scale drawing', 'draft'), - (17, 1, 'INV-2026-003', '2026-02-20', '2026-03-22', 1100.00, 0, 0, 0, 1100.00, 'Menu illustrations - in progress', 'draft'), - (18, 5, 'INV-2026-004', '2026-01-25', '2026-01-25', 750.00, 0, 0, 0, 750.00, 'Business card design and print coordination', 'paid');", - ) - .map_err(e)?; - - // ========================================== - // INVOICE ITEMS - // ========================================== - conn.execute_batch( - "INSERT INTO invoice_items (invoice_id, description, quantity, rate, amount) VALUES - (1, 'Logo redesign - research and concepts', 12, 65, 780), - (1, 'Logo refinement and final artwork', 24, 65, 1560), - (1, 'Brand guidelines document', 12, 65, 780), - (2, 'Catalog layout - 24 spreads', 30, 70, 2100), - (2, 'Product illustrations', 35, 70, 2450), - (3, 'Album cover art - concept through final', 48, 75, 3600), - (3, 'File preparation and print variants', 12, 75, 900), - (4, 'Brand strategy and logo design', 35, 70, 2450), - (4, 'Collateral design (menu, cards, social)', 20, 70, 1400), - (5, 'Catalog - typography and print preparation', 25, 70, 1750), - (5, 'Final revisions and press files', 30, 70, 2100), - (6, 'Website illustrations and icons', 50, 60, 3000), - (7, 'Cover illustration - concept to final', 45, 75, 3375), - (7, 'Layout, spine, and back cover', 10, 75, 750), - (8, 'Signage designs (3 pieces)', 25, 70, 1750), - (8, 'Social media template set', 25, 70, 1750), - (9, 'Logo and brand identity development', 32, 80, 2560), - (9, 'Brand kit - cards, signage, flash style', 20, 80, 1600), - (10, 'Spot illustrations - July', 15, 50, 750), - (10, 'Spot illustrations - August', 15, 50, 750), - (10, 'Spot illustrations - September', 15, 50, 750), - (11, 'Packaging design - 4 box sizes', 40, 60, 2400), - (11, 'Label art and tissue paper pattern', 23, 60, 1380), - (12, 'Poster illustration and layout', 24, 60, 1440), - (12, 'Print management and color proofing', 7, 60, 420), - (13, 'Cover illustration - Moth & Lantern', 45, 75, 3375), - (13, 'Layout and final files', 12, 75, 900), - (14, 'Spot illustrations - October', 18, 50, 900), - (14, 'Spot illustrations - November', 16, 50, 800), - (14, 'Spot illustrations - December', 16, 50, 800), - (15, 'Page illustrations - 12 pieces', 40, 65, 2600), - (15, 'Icon set - first batch (20 icons)', 25, 65, 1625), - (16, 'Mural concept sketches', 15, 65, 975), - (16, 'Scale drawing and color studies', 25, 65, 1625), - (17, 'Food illustrations (8 of 15)', 20, 55, 1100), - (18, 'Business card design - illustration + layout', 12, 50, 600), - (18, 'Print coordination', 3, 50, 150);", - ) - .map_err(e)?; - - // ========================================== - // INVOICE PAYMENTS - // ========================================== - conn.execute_batch( - "INSERT INTO invoice_payments (invoice_id, amount, date, method, notes) VALUES - (1, 3120.00, '2025-06-20', 'bank_transfer', 'Paid in full'), - (2, 4550.00, '2025-06-28', 'bank_transfer', NULL), - (3, 4500.00, '2025-07-25', 'bank_transfer', NULL), - (4, 3850.00, '2025-08-28', 'bank_transfer', NULL), - (5, 3850.00, '2025-08-29', 'bank_transfer', NULL), - (6, 3000.00, '2025-08-20', 'bank_transfer', 'Paid same day'), - (7, 4125.00, '2025-10-22', 'bank_transfer', NULL), - (8, 3500.00, '2025-10-28', 'bank_transfer', NULL), - (9, 4160.00, '2025-11-02', 'bank_transfer', 'Paid early'), - (11, 3780.00, '2025-12-18', 'bank_transfer', NULL), - (12, 1860.00, '2025-12-15', 'bank_transfer', NULL), - (18, 750.00, '2026-01-25', 'bank_transfer', 'Paid on receipt');", - ) - .map_err(e)?; - - // ========================================== - // FAVORITES - // ========================================== - conn.execute_batch( - "INSERT INTO favorites (project_id, task_id, description, sort_order) VALUES - (13, 44, 'Mural concept work', 0), - (14, 48, 'Menu food illustrations', 1), - (17, 56, 'Monthly spot illo', 2), - (16, 53, 'Merch design session', 3);", - ) - .map_err(e)?; - - // ========================================== - // ENTRY TEMPLATES - // ========================================== - conn.execute_batch( - "INSERT INTO entry_templates (name, project_id, task_id, description, duration, billable) VALUES - ('Quick sketch session', 13, 44, 'Concept sketching', 5400, 1), - ('Spot illustration', 17, 57, 'Inking spot illustration', 7200, 1), - ('Portfolio photo session', 5, 18, 'Photographing prints', 3600, 0), - ('Menu illo', 14, 48, 'Food illustration', 5400, 1);", - ) - .map_err(e)?; - - // ========================================== - // TRACKED APPS - // ========================================== - conn.execute_batch( - "INSERT INTO tracked_apps (project_id, exe_name, display_name) VALUES - (13, 'clip_studio_paint.exe', 'Clip Studio Paint'), - (14, 'clip_studio_paint.exe', 'Clip Studio Paint'), - (12, 'affinity_designer.exe', 'Affinity Designer'), - (16, 'affinity_designer.exe', 'Affinity Designer');", - ) - .map_err(e)?; - - // ========================================== - // BUSINESS IDENTITY (for invoice previews) - // ========================================== - conn.execute_batch( - "INSERT OR REPLACE INTO settings (key, value) VALUES - ('business_name', 'Mika Sato Illustration'), - ('business_address', '47 Brush & Ink Lane\nPortland, OR 97205'), - ('business_email', 'hello@mikasato.art'), - ('business_phone', '(503) 555-0147'), - ('hourly_rate', '95');", - ) - .map_err(e)?; - - Ok(()) -} diff --git a/src/components/JsonImportWizard.vue b/src/components/JsonImportWizard.vue index 2b4432e..90b69a0 100644 --- a/src/components/JsonImportWizard.vue +++ b/src/components/JsonImportWizard.vue @@ -26,11 +26,20 @@ const entityLabels: Record = { tasks: 'Tasks', time_entries: 'Time Entries', tags: 'Tags', + entry_tags: 'Entry Tags', invoices: 'Invoices', invoice_items: 'Invoice Items', + invoice_payments: 'Invoice Payments', + recurring_invoices: 'Recurring Invoices', expenses: 'Expenses', favorites: 'Favorites', recurring_entries: 'Recurring Entries', + tracked_apps: 'Tracked Apps', + timeline_events: 'Timeline Events', + calendar_sources: 'Calendar Sources', + calendar_events: 'Calendar Events', + timesheet_locks: 'Timesheet Locks', + timesheet_rows: 'Timesheet Rows', entry_templates: 'Entry Templates', settings: 'Settings', } diff --git a/src/views/Settings.vue b/src/views/Settings.vue index 9399d8a..ec9d043 100644 --- a/src/views/Settings.vue +++ b/src/views/Settings.vue @@ -1402,7 +1402,7 @@

Import Data

-
@@ -1463,19 +1463,6 @@

Danger Zone

-
-
-

Load Sample Data

-

Populate with demo data for screenshots (clears existing data first)

-
- -

Clear All Data

@@ -1530,38 +1517,6 @@
- - -
-
-

Load Sample Data

-

- This will clear all existing data and replace it with demo content. -

-

- All current entries, projects, clients, and invoices will be replaced. -

-
- - -
-
-
-
(null) const { activate: activateClearTrap, deactivate: deactivateClearTrap } = useFocusTrap() -const showSeedDialog = ref(false) -const isSeedingData = ref(false) - watch(showClearDataDialog, (val) => { if (val) { setTimeout(() => { @@ -2450,20 +2402,23 @@ async function executeImport() { // Export all data async function exportData() { try { - const data = await invoke('export_data') + const { save } = await import('@tauri-apps/plugin-dialog') + const { writeTextFile } = await import('@tauri-apps/plugin-fs') + const filePath = await save({ + defaultPath: `zeroclock-export-${new Date().toISOString().split('T')[0]}.json`, + filters: [{ name: 'JSON', extensions: ['json'] }], + }) + if (!filePath) return + + const data = await invoke('export_data') const json = JSON.stringify(data, null, 2) - const blob = new Blob([json], { type: 'application/json' }) - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - a.download = `zeroclock-export-${new Date().toISOString().split('T')[0]}.json` - a.click() - URL.revokeObjectURL(url) + await writeTextFile(filePath, json) const now = new Date().toISOString() await settingsStore.updateSetting('last_exported', now) lastExported.value = now + toastStore.success('Data exported successfully') } catch (error) { console.error('Failed to export data:', error) toastStore.error('Failed to export data') @@ -2482,21 +2437,6 @@ async function clearAllData() { } } -async function seedSampleData() { - isSeedingData.value = true - showSeedDialog.value = false - try { - await invoke('seed_sample_data') - toastStore.success('Sample data loaded successfully') - setTimeout(() => { window.location.reload() }, 500) - } catch (error) { - console.error('Failed to load sample data:', error) - toastStore.error('Failed to load sample data') - } finally { - isSeedingData.value = false - } -} - // Load settings on mount onMounted(async () => { await settingsStore.fetchSettings()