feat: add tags table, CRUD commands, and Pinia store
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
use crate::os_detection;
|
||||||
use rusqlite::params;
|
use rusqlite::params;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
@@ -151,6 +152,7 @@ pub fn update_project(state: State<AppState>, project: Project) -> Result<(), St
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn delete_project(state: State<AppState>, id: i64) -> Result<(), String> {
|
pub fn delete_project(state: State<AppState>, id: i64) -> Result<(), String> {
|
||||||
let conn = state.db.lock().map_err(|e| e.to_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())?;
|
conn.execute("DELETE FROM projects WHERE id = ?1", params![id]).map_err(|e| e.to_string())?;
|
||||||
Ok(())
|
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> {
|
pub fn clear_all_data(state: State<AppState>) -> Result<(), String> {
|
||||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||||
conn.execute_batch(
|
conn.execute_batch(
|
||||||
"DELETE FROM invoice_items;
|
"DELETE FROM tracked_apps;
|
||||||
|
DELETE FROM invoice_items;
|
||||||
DELETE FROM invoices;
|
DELETE FROM invoices;
|
||||||
DELETE FROM time_entries;
|
DELETE FROM time_entries;
|
||||||
DELETE FROM tasks;
|
DELETE FROM tasks;
|
||||||
@@ -487,3 +490,150 @@ pub fn clear_all_data(state: State<AppState>) -> Result<(), String> {
|
|||||||
).map_err(|e| e.to_string())?;
|
).map_err(|e| e.to_string())?;
|
||||||
Ok(())
|
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(
|
conn.execute(
|
||||||
"CREATE TABLE IF NOT EXISTS settings (
|
"CREATE TABLE IF NOT EXISTS settings (
|
||||||
key TEXT PRIMARY KEY,
|
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')",
|
"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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use tauri::Manager;
|
|||||||
|
|
||||||
mod database;
|
mod database;
|
||||||
mod commands;
|
mod commands;
|
||||||
|
mod os_detection;
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub db: Mutex<Connection>,
|
pub db: Mutex<Connection>,
|
||||||
@@ -59,6 +60,18 @@ pub fn run() {
|
|||||||
commands::update_settings,
|
commands::update_settings,
|
||||||
commands::export_data,
|
commands::export_data,
|
||||||
commands::clear_all_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| {
|
.setup(|app| {
|
||||||
#[cfg(desktop)]
|
#[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