feat: entry templates CRUD backend
This commit is contained in:
@@ -2361,6 +2361,52 @@ pub fn upsert_timesheet_entry(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Entry template commands
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_entry_templates(state: State<AppState>) -> Result<Vec<serde_json::Value>, String> {
|
||||||
|
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"SELECT id, name, project_id, task_id, description, duration, billable, created_at FROM entry_templates ORDER BY name"
|
||||||
|
).map_err(|e| e.to_string())?;
|
||||||
|
let rows = stmt.query_map([], |row| {
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"id": row.get::<_, i64>(0)?,
|
||||||
|
"name": row.get::<_, String>(1)?,
|
||||||
|
"project_id": row.get::<_, i64>(2)?,
|
||||||
|
"task_id": row.get::<_, Option<i64>>(3)?,
|
||||||
|
"description": row.get::<_, Option<String>>(4)?,
|
||||||
|
"duration": row.get::<_, i64>(5)?,
|
||||||
|
"billable": row.get::<_, i64>(6)?,
|
||||||
|
"created_at": row.get::<_, String>(7)?,
|
||||||
|
}))
|
||||||
|
}).map_err(|e| e.to_string())?;
|
||||||
|
Ok(rows.filter_map(|r| r.ok()).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn create_entry_template(state: State<AppState>, template: serde_json::Value) -> Result<i64, String> {
|
||||||
|
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||||
|
let name = template.get("name").and_then(|v| v.as_str()).unwrap_or("Untitled");
|
||||||
|
let project_id = template.get("project_id").and_then(|v| v.as_i64()).ok_or("project_id required")?;
|
||||||
|
let task_id = template.get("task_id").and_then(|v| v.as_i64());
|
||||||
|
let description = template.get("description").and_then(|v| v.as_str());
|
||||||
|
let duration = template.get("duration").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||||
|
let billable = template.get("billable").and_then(|v| v.as_i64()).unwrap_or(1);
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO entry_templates (name, project_id, task_id, description, duration, billable) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||||
|
params![name, project_id, task_id, description, duration, billable],
|
||||||
|
).map_err(|e| e.to_string())?;
|
||||||
|
Ok(conn.last_insert_rowid())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn delete_entry_template(state: State<AppState>, id: i64) -> Result<(), String> {
|
||||||
|
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||||
|
conn.execute("DELETE FROM entry_templates WHERE id = ?1", params![id]).map_err(|e| e.to_string())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn format_seconds_as_time(secs: i64) -> String {
|
fn format_seconds_as_time(secs: i64) -> String {
|
||||||
let h = secs / 3600;
|
let h = secs / 3600;
|
||||||
let m = (secs % 3600) / 60;
|
let m = (secs % 3600) / 60;
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ pub fn init_db(conn: &Connection) -> Result<(), rusqlite::Error> {
|
|||||||
"ALTER TABLE projects ADD COLUMN budget_hours REAL DEFAULT NULL",
|
"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 budget_amount REAL DEFAULT NULL",
|
||||||
"ALTER TABLE projects ADD COLUMN rounding_override INTEGER DEFAULT NULL",
|
"ALTER TABLE projects ADD COLUMN rounding_override INTEGER DEFAULT NULL",
|
||||||
|
"ALTER TABLE projects ADD COLUMN timeline_override TEXT DEFAULT NULL",
|
||||||
];
|
];
|
||||||
for sql in &project_migrations {
|
for sql in &project_migrations {
|
||||||
match conn.execute(sql, []) {
|
match conn.execute(sql, []) {
|
||||||
@@ -75,6 +76,22 @@ pub fn init_db(conn: &Connection) -> Result<(), rusqlite::Error> {
|
|||||||
[],
|
[],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
// Migrate tasks table - add estimated_hours column (safe to re-run)
|
||||||
|
let task_migrations = [
|
||||||
|
"ALTER TABLE tasks ADD COLUMN estimated_hours 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(
|
conn.execute(
|
||||||
"CREATE TABLE IF NOT EXISTS time_entries (
|
"CREATE TABLE IF NOT EXISTS time_entries (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -91,6 +108,22 @@ pub fn init_db(conn: &Connection) -> Result<(), rusqlite::Error> {
|
|||||||
[],
|
[],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
// 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(
|
conn.execute(
|
||||||
"CREATE TABLE IF NOT EXISTS invoices (
|
"CREATE TABLE IF NOT EXISTS invoices (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -187,6 +220,112 @@ pub fn init_db(conn: &Connection) -> Result<(), rusqlite::Error> {
|
|||||||
[],
|
[],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
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
|
||||||
|
)",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
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(
|
conn.execute(
|
||||||
"CREATE TABLE IF NOT EXISTS settings (
|
"CREATE TABLE IF NOT EXISTS settings (
|
||||||
key TEXT PRIMARY KEY,
|
key TEXT PRIMARY KEY,
|
||||||
@@ -231,6 +370,7 @@ pub fn init_db(conn: &Connection) -> Result<(), rusqlite::Error> {
|
|||||||
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_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 ('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 ('mini_timer_opacity', '90')", [])?;
|
||||||
|
conn.execute("INSERT OR IGNORE INTO settings (key, value) VALUES ('timeline_recording', 'off')", [])?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,6 +132,9 @@ pub fn run() {
|
|||||||
commands::bulk_update_entries_project,
|
commands::bulk_update_entries_project,
|
||||||
commands::bulk_update_entries_billable,
|
commands::bulk_update_entries_billable,
|
||||||
commands::upsert_timesheet_entry,
|
commands::upsert_timesheet_entry,
|
||||||
|
commands::get_entry_templates,
|
||||||
|
commands::create_entry_template,
|
||||||
|
commands::delete_entry_template,
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
#[cfg(desktop)]
|
#[cfg(desktop)]
|
||||||
|
|||||||
Reference in New Issue
Block a user