use rusqlite::Connection; pub fn init_db(conn: &Connection) -> Result<(), rusqlite::Error> { conn.execute( "CREATE TABLE IF NOT EXISTS clients ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT, address TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP )", [], )?; // Migrate clients table - add new columns (safe to re-run) let migration_columns = [ "ALTER TABLE clients ADD COLUMN company TEXT", "ALTER TABLE clients ADD COLUMN phone TEXT", "ALTER TABLE clients ADD COLUMN tax_id TEXT", "ALTER TABLE clients ADD COLUMN payment_terms TEXT", "ALTER TABLE clients ADD COLUMN notes TEXT", "ALTER TABLE clients ADD COLUMN currency TEXT", ]; for sql in &migration_columns { match conn.execute(sql, []) { Ok(_) => {} Err(e) => { let msg = e.to_string(); if !msg.contains("duplicate column") { return Err(e); } } } } conn.execute( "CREATE TABLE IF NOT EXISTS projects ( id INTEGER PRIMARY KEY AUTOINCREMENT, client_id INTEGER, name TEXT NOT NULL, hourly_rate REAL DEFAULT 0, color TEXT DEFAULT '#F59E0B', archived INTEGER DEFAULT 0, created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (client_id) REFERENCES clients(id) )", [], )?; // Migrate projects table - add budget columns (safe to re-run) let project_migrations = [ "ALTER TABLE projects ADD COLUMN budget_hours REAL DEFAULT NULL", "ALTER TABLE projects ADD COLUMN budget_amount REAL DEFAULT NULL", "ALTER TABLE projects ADD COLUMN rounding_override INTEGER DEFAULT NULL", "ALTER TABLE projects ADD COLUMN timeline_override TEXT DEFAULT NULL", "ALTER TABLE projects ADD COLUMN notes TEXT", "ALTER TABLE projects ADD COLUMN currency TEXT", ]; for sql in &project_migrations { match conn.execute(sql, []) { Ok(_) => {} Err(e) => { let msg = e.to_string(); if !msg.contains("duplicate column") { return Err(e); } } } } conn.execute( "CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER NOT NULL, name TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (project_id) REFERENCES projects(id) )", [], )?; // Migrate tasks table (safe to re-run) let task_migrations = [ "ALTER TABLE tasks ADD COLUMN estimated_hours REAL DEFAULT NULL", "ALTER TABLE tasks ADD COLUMN hourly_rate REAL DEFAULT NULL", ]; for sql in &task_migrations { match conn.execute(sql, []) { Ok(_) => {} Err(e) => { let msg = e.to_string(); if !msg.contains("duplicate column") { return Err(e); } } } } conn.execute( "CREATE TABLE IF NOT EXISTS time_entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER NOT NULL, task_id INTEGER, description TEXT, start_time TEXT NOT NULL, end_time TEXT, duration INTEGER DEFAULT 0, created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (project_id) REFERENCES projects(id), FOREIGN KEY (task_id) REFERENCES tasks(id) )", [], )?; // Migrate time_entries table let time_entry_migrations = [ "ALTER TABLE time_entries ADD COLUMN billable INTEGER DEFAULT 1", ]; for sql in &time_entry_migrations { match conn.execute(sql, []) { Ok(_) => {} Err(e) => { let msg = e.to_string(); if !msg.contains("duplicate column") { return Err(e); } } } } conn.execute( "CREATE TABLE IF NOT EXISTS invoices ( id INTEGER PRIMARY KEY AUTOINCREMENT, client_id INTEGER NOT NULL, invoice_number TEXT NOT NULL, date TEXT NOT NULL, due_date TEXT, subtotal REAL DEFAULT 0, tax_rate REAL DEFAULT 0, tax_amount REAL DEFAULT 0, discount REAL DEFAULT 0, total REAL DEFAULT 0, notes TEXT, status TEXT DEFAULT 'draft', created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (client_id) REFERENCES clients(id) )", [], )?; // Migrate invoices table - add template_id column (safe to re-run) let invoice_migrations = [ "ALTER TABLE invoices ADD COLUMN template_id TEXT DEFAULT 'clean'", ]; for sql in &invoice_migrations { match conn.execute(sql, []) { Ok(_) => {} Err(e) => { let msg = e.to_string(); if !msg.contains("duplicate column") { return Err(e); } } } } conn.execute( "CREATE TABLE IF NOT EXISTS invoice_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, invoice_id INTEGER NOT NULL, description TEXT NOT NULL, quantity REAL DEFAULT 1, rate REAL DEFAULT 0, amount REAL DEFAULT 0, time_entry_id INTEGER, FOREIGN KEY (invoice_id) REFERENCES invoices(id), FOREIGN KEY (time_entry_id) REFERENCES time_entries(id) )", [], )?; conn.execute( "CREATE TABLE IF NOT EXISTS tracked_apps ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER NOT NULL, exe_name TEXT NOT NULL, exe_path TEXT, display_name TEXT, FOREIGN KEY (project_id) REFERENCES projects(id) )", [], )?; conn.execute( "CREATE TABLE IF NOT EXISTS tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, color TEXT DEFAULT '#6B7280' )", [], )?; conn.execute( "CREATE TABLE IF NOT EXISTS entry_tags ( entry_id INTEGER NOT NULL, tag_id INTEGER NOT NULL, PRIMARY KEY (entry_id, tag_id), FOREIGN KEY (entry_id) REFERENCES time_entries(id) ON DELETE CASCADE, FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE )", [], )?; conn.execute( "CREATE TABLE IF NOT EXISTS favorites ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER NOT NULL, task_id INTEGER, description TEXT, sort_order INTEGER DEFAULT 0, FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE, FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE SET NULL )", [], )?; conn.execute( "CREATE TABLE IF NOT EXISTS recurring_entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER NOT NULL, task_id INTEGER, description TEXT, duration INTEGER DEFAULT 0, recurrence_rule TEXT NOT NULL, time_of_day TEXT DEFAULT '09:00', mode TEXT DEFAULT 'prompt', enabled INTEGER DEFAULT 1, last_triggered TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (project_id) REFERENCES projects(id), FOREIGN KEY (task_id) REFERENCES tasks(id) )", [], )?; conn.execute( "CREATE TABLE IF NOT EXISTS expenses ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER NOT NULL, client_id INTEGER, category TEXT DEFAULT 'other', description TEXT, amount REAL DEFAULT 0, date TEXT NOT NULL, receipt_path TEXT, invoiced INTEGER DEFAULT 0, created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (project_id) REFERENCES projects(id), FOREIGN KEY (client_id) REFERENCES clients(id) )", [], )?; conn.execute( "CREATE TABLE IF NOT EXISTS timeline_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER NOT NULL, exe_name TEXT, exe_path TEXT, window_title TEXT, started_at TEXT NOT NULL, ended_at TEXT, duration INTEGER DEFAULT 0, FOREIGN KEY (project_id) REFERENCES projects(id) )", [], )?; conn.execute( "CREATE TABLE IF NOT EXISTS calendar_sources ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, type TEXT NOT NULL, url TEXT, last_synced TEXT, sync_interval INTEGER DEFAULT 30, enabled INTEGER DEFAULT 1, created_at TEXT DEFAULT CURRENT_TIMESTAMP )", [], )?; conn.execute( "CREATE TABLE IF NOT EXISTS calendar_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, source_id INTEGER NOT NULL, uid TEXT, summary TEXT, start_time TEXT, end_time TEXT, duration INTEGER DEFAULT 0, location TEXT, synced_at TEXT, FOREIGN KEY (source_id) REFERENCES calendar_sources(id) ON DELETE CASCADE )", [], )?; // Migrate calendar_events table - add description column (safe to re-run) let calendar_migrations = [ "ALTER TABLE calendar_events ADD COLUMN description TEXT", ]; for sql in &calendar_migrations { match conn.execute(sql, []) { Ok(_) => {} Err(e) => { let msg = e.to_string(); if !msg.contains("duplicate column") { return Err(e); } } } } conn.execute( "CREATE TABLE IF NOT EXISTS timesheet_locks ( id INTEGER PRIMARY KEY AUTOINCREMENT, week_start TEXT NOT NULL UNIQUE, status TEXT DEFAULT 'locked', locked_at TEXT DEFAULT CURRENT_TIMESTAMP )", [], )?; conn.execute( "CREATE TABLE IF NOT EXISTS entry_templates ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, project_id INTEGER NOT NULL REFERENCES projects(id), task_id INTEGER REFERENCES tasks(id), description TEXT, duration INTEGER NOT NULL DEFAULT 0, billable INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now')) )", [], )?; conn.execute( "CREATE TABLE IF NOT EXISTS timesheet_rows ( id INTEGER PRIMARY KEY AUTOINCREMENT, week_start TEXT NOT NULL, project_id INTEGER NOT NULL REFERENCES projects(id), task_id INTEGER REFERENCES tasks(id), sort_order INTEGER NOT NULL DEFAULT 0 )", [], )?; conn.execute( "CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT )", [], )?; conn.execute( "CREATE TABLE IF NOT EXISTS invoice_payments ( id INTEGER PRIMARY KEY AUTOINCREMENT, invoice_id INTEGER NOT NULL, amount REAL NOT NULL, date TEXT NOT NULL, method TEXT, notes TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE )", [], )?; conn.execute( "CREATE TABLE IF NOT EXISTS recurring_invoices ( id INTEGER PRIMARY KEY AUTOINCREMENT, client_id INTEGER NOT NULL, template_id TEXT, line_items_json TEXT NOT NULL, tax_rate REAL DEFAULT 0, discount REAL DEFAULT 0, notes TEXT, recurrence_rule TEXT NOT NULL, next_due_date TEXT NOT NULL, enabled INTEGER DEFAULT 1, created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (client_id) REFERENCES clients(id) )", [], )?; // Insert default settings conn.execute( "INSERT OR IGNORE INTO settings (key, value) VALUES ('hourly_rate', '50')", [], )?; conn.execute( "INSERT OR IGNORE INTO settings (key, value) VALUES ('idle_detection', 'true')", [], )?; conn.execute( "INSERT OR IGNORE INTO settings (key, value) VALUES ('idle_timeout', '5')", [], )?; conn.execute( "INSERT OR IGNORE INTO settings (key, value) VALUES ('reminder_interval', '30')", [], )?; conn.execute( "INSERT OR IGNORE INTO settings (key, value) VALUES ('app_tracking_mode', 'auto')", [], )?; conn.execute( "INSERT OR IGNORE INTO settings (key, value) VALUES ('app_check_interval', '5')", [], )?; conn.execute("INSERT OR IGNORE INTO settings (key, value) VALUES ('goals_enabled', 'true')", [])?; conn.execute("INSERT OR IGNORE INTO settings (key, value) VALUES ('daily_goal_hours', '8')", [])?; conn.execute("INSERT OR IGNORE INTO settings (key, value) VALUES ('weekly_goal_hours', '40')", [])?; conn.execute("INSERT OR IGNORE INTO settings (key, value) VALUES ('rounding_enabled', 'false')", [])?; conn.execute("INSERT OR IGNORE INTO settings (key, value) VALUES ('rounding_increment', '15')", [])?; conn.execute("INSERT OR IGNORE INTO settings (key, value) VALUES ('rounding_method', 'nearest')", [])?; conn.execute("INSERT OR IGNORE INTO settings (key, value) VALUES ('theme_mode', 'dark')", [])?; conn.execute("INSERT OR IGNORE INTO settings (key, value) VALUES ('accent_color', 'amber')", [])?; conn.execute("INSERT OR IGNORE INTO settings (key, value) VALUES ('shortcut_toggle_timer', 'CmdOrCtrl+Shift+T')", [])?; conn.execute("INSERT OR IGNORE INTO settings (key, value) VALUES ('shortcut_show_app', 'CmdOrCtrl+Shift+Z')", [])?; conn.execute("INSERT OR IGNORE INTO settings (key, value) VALUES ('mini_timer_opacity', '90')", [])?; conn.execute("INSERT OR IGNORE INTO settings (key, value) VALUES ('timeline_recording', 'off')", [])?; Ok(()) }