feat: add tags table, CRUD commands, and Pinia store
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
76
src/stores/tags.ts
Normal file
76
src/stores/tags.ts
Normal file
@@ -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<Tag[]>([])
|
||||
|
||||
async function fetchTags() {
|
||||
try {
|
||||
tags.value = await invoke<Tag[]>('get_tags')
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch tags:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function createTag(tag: Tag): Promise<number | null> {
|
||||
try {
|
||||
const id = await invoke<number>('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<boolean> {
|
||||
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<boolean> {
|
||||
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<Tag[]> {
|
||||
try {
|
||||
return await invoke<Tag[]>('get_entry_tags', { entryId })
|
||||
} catch (error) {
|
||||
console.error('Failed to get entry tags:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
async function setEntryTags(entryId: number, tagIds: number[]): Promise<boolean> {
|
||||
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 }
|
||||
})
|
||||
Reference in New Issue
Block a user