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).
This commit is contained in:
@@ -991,6 +991,42 @@ pub fn export_data(state: State<AppState>) -> Result<serde_json::Value, String>
|
||||
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<serde_json::Value> = 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<String>>(4)?,
|
||||
"notes": row.get::<_, Option<String>>(5)?,
|
||||
"created_at": row.get::<_, Option<String>>(6)?
|
||||
}))
|
||||
}).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().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<serde_json::Value> = stmt.query_map([], |row| {
|
||||
Ok(serde_json::json!({
|
||||
"id": row.get::<_, i64>(0)?,
|
||||
"client_id": row.get::<_, i64>(1)?,
|
||||
"template_id": row.get::<_, Option<String>>(2)?,
|
||||
"line_items_json": row.get::<_, String>(3)?,
|
||||
"tax_rate": row.get::<_, f64>(4)?,
|
||||
"discount": row.get::<_, f64>(5)?,
|
||||
"notes": row.get::<_, Option<String>>(6)?,
|
||||
"recurrence_rule": row.get::<_, String>(7)?,
|
||||
"next_due_date": row.get::<_, String>(8)?,
|
||||
"enabled": row.get::<_, i32>(9)?,
|
||||
"created_at": row.get::<_, Option<String>>(10)?
|
||||
}))
|
||||
}).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| {
|
||||
@@ -1011,6 +1047,8 @@ pub fn export_data(state: State<AppState>) -> Result<serde_json::Value, String>
|
||||
"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<AppState>, 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<AppState>, 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<i64> = 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<AppState>, 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<AppState>, 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<i64> = 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<AppState>, 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<InvoiceTemplate> {
|
||||
]
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn seed_sample_data(state: State<AppState>) -> Result<String, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
crate::seed::seed(&conn)?;
|
||||
Ok("Sample data loaded".to_string())
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use tauri::Manager;
|
||||
mod database;
|
||||
mod commands;
|
||||
mod os_detection;
|
||||
mod seed;
|
||||
|
||||
pub struct AppState {
|
||||
pub db: Mutex<Connection>,
|
||||
@@ -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)]
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -26,11 +26,20 @@ const entityLabels: Record<string, string> = {
|
||||
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',
|
||||
}
|
||||
|
||||
@@ -1402,7 +1402,7 @@
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-[0.8125rem] font-medium text-text-primary">Import Data</h3>
|
||||
<button @click="showJsonImportWizard = true" class="px-3 py-1.5 text-[0.6875rem] border border-border-subtle text-text-secondary rounded-lg hover:bg-bg-elevated transition-colors">
|
||||
<button @click="showJsonImportWizard = true" class="px-3 py-1.5 text-[0.8125rem] border border-border-subtle text-text-secondary rounded-lg hover:bg-bg-elevated transition-colors">
|
||||
Restore from Backup
|
||||
</button>
|
||||
</div>
|
||||
@@ -1463,19 +1463,6 @@
|
||||
<div class="rounded-xl border border-status-error/20 p-5">
|
||||
<h3 class="text-xs text-status-error-text uppercase tracking-[0.08em] font-medium mb-4">Danger Zone</h3>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-[0.8125rem] text-text-primary">Load Sample Data</p>
|
||||
<p class="text-[0.6875rem] text-text-tertiary mt-0.5">Populate with demo data for screenshots (clears existing data first)</p>
|
||||
</div>
|
||||
<button
|
||||
@click="showSeedDialog = true"
|
||||
:disabled="isSeedingData"
|
||||
class="px-4 py-1.5 border border-status-warning text-status-warning text-[0.8125rem] font-medium rounded-lg hover:bg-status-warning/10 transition-colors duration-150 disabled:opacity-40"
|
||||
>
|
||||
{{ isSeedingData ? 'Loading...' : 'Load Demo' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-[0.8125rem] text-text-primary">Clear All Data</p>
|
||||
@@ -1530,38 +1517,6 @@
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- Seed Data Confirmation Dialog -->
|
||||
<Transition name="modal">
|
||||
<div
|
||||
v-if="showSeedDialog"
|
||||
class="fixed inset-0 bg-black/70 backdrop-blur-[4px] flex items-center justify-center p-4 z-50"
|
||||
@click.self="showSeedDialog = false"
|
||||
>
|
||||
<div role="alertdialog" aria-modal="true" aria-labelledby="seed-data-title" aria-describedby="seed-data-desc" class="bg-bg-surface border border-border-subtle rounded-lg shadow-[0_1px_3px_rgba(0,0,0,0.3)] w-full max-w-sm p-6">
|
||||
<h2 id="seed-data-title" class="text-[1.125rem] font-semibold font-[family-name:var(--font-heading)] text-text-primary mb-2">Load Sample Data</h2>
|
||||
<p id="seed-data-desc" class="text-[0.75rem] text-text-secondary mb-4">
|
||||
This will clear all existing data and replace it with demo content.
|
||||
</p>
|
||||
<p class="text-[0.6875rem] text-status-warning mb-6">
|
||||
All current entries, projects, clients, and invoices will be replaced.
|
||||
</p>
|
||||
<div class="flex justify-end gap-3">
|
||||
<button
|
||||
@click="showSeedDialog = false"
|
||||
class="px-4 py-2 border border-border-subtle text-text-secondary rounded-lg hover:bg-bg-elevated transition-colors duration-150"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="seedSampleData"
|
||||
class="px-4 py-2 bg-status-warning text-white font-medium rounded-lg hover:bg-status-warning/80 transition-colors duration-150"
|
||||
>
|
||||
Load Demo Data
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<JsonImportWizard
|
||||
:show="showJsonImportWizard"
|
||||
@@ -1965,9 +1920,6 @@ const showClearDataDialog = ref(false)
|
||||
const clearDialogRef = ref<HTMLElement | null>(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()
|
||||
|
||||
Reference in New Issue
Block a user