From 26f1b19ddee80bba9b59998d4baa597696e49612 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 18 Feb 2026 02:01:04 +0200 Subject: [PATCH] feat: add tags table, CRUD commands, and Pinia store --- src-tauri/src/commands.rs | 152 +++++++++++++++++++++++++++++++++++++- src-tauri/src/database.rs | 40 ++++++++++ src-tauri/src/lib.rs | 13 ++++ src/stores/tags.ts | 76 +++++++++++++++++++ 4 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 src/stores/tags.ts diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index b569408..1a3614b 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1,4 +1,5 @@ use crate::AppState; +use crate::os_detection; use rusqlite::params; use serde::{Deserialize, Serialize}; use tauri::State; @@ -151,6 +152,7 @@ pub fn update_project(state: State, project: Project) -> Result<(), St #[tauri::command] pub fn delete_project(state: State, id: i64) -> Result<(), String> { let conn = state.db.lock().map_err(|e| e.to_string())?; + conn.execute("DELETE FROM tracked_apps WHERE project_id = ?1", params![id]).map_err(|e| e.to_string())?; conn.execute("DELETE FROM projects WHERE id = ?1", params![id]).map_err(|e| e.to_string())?; Ok(()) } @@ -478,7 +480,8 @@ pub fn export_data(state: State) -> Result pub fn clear_all_data(state: State) -> Result<(), String> { let conn = state.db.lock().map_err(|e| e.to_string())?; conn.execute_batch( - "DELETE FROM invoice_items; + "DELETE FROM tracked_apps; + DELETE FROM invoice_items; DELETE FROM invoices; DELETE FROM time_entries; DELETE FROM tasks; @@ -487,3 +490,150 @@ pub fn clear_all_data(state: State) -> Result<(), String> { ).map_err(|e| e.to_string())?; Ok(()) } + +// Tracked Apps struct +#[derive(Debug, Serialize, Deserialize)] +pub struct TrackedApp { + pub id: Option, + pub project_id: i64, + pub exe_name: String, + pub exe_path: Option, + pub display_name: Option, +} + +// OS Detection commands +#[tauri::command] +pub fn get_idle_seconds() -> Result { + Ok(os_detection::get_system_idle_seconds()) +} + +#[tauri::command] +pub fn get_visible_windows() -> Result, String> { + Ok(os_detection::enumerate_visible_windows()) +} + +#[tauri::command] +pub fn get_running_processes() -> Result, String> { + Ok(os_detection::enumerate_running_processes()) +} + +// Tracked Apps CRUD commands +#[tauri::command] +pub fn get_tracked_apps(state: State, project_id: i64) -> Result, String> { + let conn = state.db.lock().map_err(|e| e.to_string())?; + let mut stmt = conn.prepare( + "SELECT id, project_id, exe_name, exe_path, display_name FROM tracked_apps WHERE project_id = ?1 ORDER BY display_name" + ).map_err(|e| e.to_string())?; + let apps = stmt.query_map(params![project_id], |row| { + Ok(TrackedApp { + id: Some(row.get(0)?), + project_id: row.get(1)?, + exe_name: row.get(2)?, + exe_path: row.get(3)?, + display_name: row.get(4)?, + }) + }).map_err(|e| e.to_string())?; + apps.collect::, _>>().map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn add_tracked_app(state: State, app: TrackedApp) -> Result { + let conn = state.db.lock().map_err(|e| e.to_string())?; + conn.execute( + "INSERT INTO tracked_apps (project_id, exe_name, exe_path, display_name) VALUES (?1, ?2, ?3, ?4)", + params![app.project_id, app.exe_name, app.exe_path, app.display_name], + ).map_err(|e| e.to_string())?; + Ok(conn.last_insert_rowid()) +} + +#[tauri::command] +pub fn remove_tracked_app(state: State, id: i64) -> Result<(), String> { + let conn = state.db.lock().map_err(|e| e.to_string())?; + conn.execute("DELETE FROM tracked_apps WHERE id = ?1", params![id]).map_err(|e| e.to_string())?; + Ok(()) +} + +// Tag structs and commands +#[derive(Debug, Serialize, Deserialize)] +pub struct Tag { + pub id: Option, + pub name: String, + pub color: String, +} + +#[tauri::command] +pub fn get_tags(state: State) -> Result, String> { + let conn = state.db.lock().map_err(|e| e.to_string())?; + let mut stmt = conn.prepare("SELECT id, name, color FROM tags ORDER BY name") + .map_err(|e| e.to_string())?; + let tags = stmt.query_map([], |row| { + Ok(Tag { + id: Some(row.get(0)?), + name: row.get(1)?, + color: row.get(2)?, + }) + }).map_err(|e| e.to_string())?; + tags.collect::, _>>().map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn create_tag(state: State, tag: Tag) -> Result { + let conn = state.db.lock().map_err(|e| e.to_string())?; + conn.execute( + "INSERT INTO tags (name, color) VALUES (?1, ?2)", + params![tag.name, tag.color], + ).map_err(|e| e.to_string())?; + Ok(conn.last_insert_rowid()) +} + +#[tauri::command] +pub fn update_tag(state: State, tag: Tag) -> Result<(), String> { + let conn = state.db.lock().map_err(|e| e.to_string())?; + conn.execute( + "UPDATE tags SET name = ?1, color = ?2 WHERE id = ?3", + params![tag.name, tag.color, tag.id], + ).map_err(|e| e.to_string())?; + Ok(()) +} + +#[tauri::command] +pub fn delete_tag(state: State, id: i64) -> Result<(), String> { + let conn = state.db.lock().map_err(|e| e.to_string())?; + conn.execute("DELETE FROM entry_tags WHERE tag_id = ?1", params![id]) + .map_err(|e| e.to_string())?; + conn.execute("DELETE FROM tags WHERE id = ?1", params![id]) + .map_err(|e| e.to_string())?; + Ok(()) +} + +#[tauri::command] +pub fn get_entry_tags(state: State, entry_id: i64) -> Result, String> { + let conn = state.db.lock().map_err(|e| e.to_string())?; + let mut stmt = conn.prepare( + "SELECT t.id, t.name, t.color FROM tags t + JOIN entry_tags et ON t.id = et.tag_id + WHERE et.entry_id = ?1 ORDER BY t.name" + ).map_err(|e| e.to_string())?; + let tags = stmt.query_map(params![entry_id], |row| { + Ok(Tag { + id: Some(row.get(0)?), + name: row.get(1)?, + color: row.get(2)?, + }) + }).map_err(|e| e.to_string())?; + tags.collect::, _>>().map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn set_entry_tags(state: State, entry_id: i64, tag_ids: Vec) -> Result<(), String> { + let conn = state.db.lock().map_err(|e| e.to_string())?; + conn.execute("DELETE FROM entry_tags WHERE entry_id = ?1", params![entry_id]) + .map_err(|e| e.to_string())?; + for tag_id in tag_ids { + conn.execute( + "INSERT INTO entry_tags (entry_id, tag_id) VALUES (?1, ?2)", + params![entry_id, tag_id], + ).map_err(|e| e.to_string())?; + } + Ok(()) +} diff --git a/src-tauri/src/database.rs b/src-tauri/src/database.rs index 1f44815..f2ec011 100644 --- a/src-tauri/src/database.rs +++ b/src-tauri/src/database.rs @@ -108,6 +108,38 @@ pub fn init_db(conn: &Connection) -> Result<(), rusqlite::Error> { [], )?; + 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 settings ( key TEXT PRIMARY KEY, @@ -133,6 +165,14 @@ pub fn init_db(conn: &Connection) -> Result<(), rusqlite::Error> { "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')", + [], + )?; Ok(()) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f69dab4..fb31e3a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -5,6 +5,7 @@ use tauri::Manager; mod database; mod commands; +mod os_detection; pub struct AppState { pub db: Mutex, @@ -59,6 +60,18 @@ pub fn run() { commands::update_settings, commands::export_data, commands::clear_all_data, + commands::get_idle_seconds, + commands::get_visible_windows, + commands::get_running_processes, + commands::get_tracked_apps, + commands::add_tracked_app, + commands::remove_tracked_app, + commands::get_tags, + commands::create_tag, + commands::update_tag, + commands::delete_tag, + commands::get_entry_tags, + commands::set_entry_tags, ]) .setup(|app| { #[cfg(desktop)] diff --git a/src/stores/tags.ts b/src/stores/tags.ts new file mode 100644 index 0000000..9cafd7b --- /dev/null +++ b/src/stores/tags.ts @@ -0,0 +1,76 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { invoke } from '@tauri-apps/api/core' + +export interface Tag { + id?: number + name: string + color: string +} + +export const useTagsStore = defineStore('tags', () => { + const tags = ref([]) + + async function fetchTags() { + try { + tags.value = await invoke('get_tags') + } catch (error) { + console.error('Failed to fetch tags:', error) + } + } + + async function createTag(tag: Tag): Promise { + try { + const id = await invoke('create_tag', { tag }) + tags.value.push({ ...tag, id: Number(id) }) + return Number(id) + } catch (error) { + console.error('Failed to create tag:', error) + return null + } + } + + async function updateTag(tag: Tag): Promise { + try { + await invoke('update_tag', { tag }) + const index = tags.value.findIndex(t => t.id === tag.id) + if (index !== -1) tags.value[index] = tag + return true + } catch (error) { + console.error('Failed to update tag:', error) + return false + } + } + + async function deleteTag(id: number): Promise { + try { + await invoke('delete_tag', { id }) + tags.value = tags.value.filter(t => t.id !== id) + return true + } catch (error) { + console.error('Failed to delete tag:', error) + return false + } + } + + async function getEntryTags(entryId: number): Promise { + try { + return await invoke('get_entry_tags', { entryId }) + } catch (error) { + console.error('Failed to get entry tags:', error) + return [] + } + } + + async function setEntryTags(entryId: number, tagIds: number[]): Promise { + try { + await invoke('set_entry_tags', { entryId, tagIds }) + return true + } catch (error) { + console.error('Failed to set entry tags:', error) + return false + } + } + + return { tags, fetchTags, createTag, updateTag, deleteTag, getEntryTags, setEntryTags } +})