feat: add tags table, CRUD commands, and Pinia store

This commit is contained in:
Your Name
2026-02-18 02:01:04 +02:00
parent 6049536284
commit ee30647b44
4 changed files with 280 additions and 1 deletions

View File

@@ -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<AppState>, project: Project) -> Result<(), St
#[tauri::command]
pub fn delete_project(state: State<AppState>, 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<AppState>) -> Result<serde_json::Value, String>
pub fn clear_all_data(state: State<AppState>) -> 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<AppState>) -> Result<(), String> {
).map_err(|e| e.to_string())?;
Ok(())
}
// Tracked Apps struct
#[derive(Debug, Serialize, Deserialize)]
pub struct TrackedApp {
pub id: Option<i64>,
pub project_id: i64,
pub exe_name: String,
pub exe_path: Option<String>,
pub display_name: Option<String>,
}
// OS Detection commands
#[tauri::command]
pub fn get_idle_seconds() -> Result<u64, String> {
Ok(os_detection::get_system_idle_seconds())
}
#[tauri::command]
pub fn get_visible_windows() -> Result<Vec<os_detection::WindowInfo>, String> {
Ok(os_detection::enumerate_visible_windows())
}
#[tauri::command]
pub fn get_running_processes() -> Result<Vec<os_detection::WindowInfo>, String> {
Ok(os_detection::enumerate_running_processes())
}
// Tracked Apps CRUD commands
#[tauri::command]
pub fn get_tracked_apps(state: State<AppState>, project_id: i64) -> Result<Vec<TrackedApp>, 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::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}
#[tauri::command]
pub fn add_tracked_app(state: State<AppState>, app: TrackedApp) -> Result<i64, String> {
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<AppState>, 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<i64>,
pub name: String,
pub color: String,
}
#[tauri::command]
pub fn get_tags(state: State<AppState>) -> Result<Vec<Tag>, 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::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}
#[tauri::command]
pub fn create_tag(state: State<AppState>, tag: Tag) -> Result<i64, String> {
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<AppState>, 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<AppState>, 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<AppState>, entry_id: i64) -> Result<Vec<Tag>, 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::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}
#[tauri::command]
pub fn set_entry_tags(state: State<AppState>, entry_id: i64, tag_ids: Vec<i64>) -> 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(())
}

View File

@@ -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(())
}

View File

@@ -5,6 +5,7 @@ use tauri::Manager;
mod database;
mod commands;
mod os_detection;
pub struct AppState {
pub db: Mutex<Connection>,
@@ -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)]