use serde::{Deserialize, Serialize}; use std::sync::Mutex; use tauri::State; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Project { pub id: i64, pub name: String, pub key: Option, pub description: Option, pub status: String, pub start_date: Option, pub end_date: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct WorkItem { pub id: i64, pub project_id: i64, pub code: Option, pub title: String, pub description: Option, #[serde(rename = "type")] pub wi_type: String, pub status: String, pub priority: String, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct AlphaUser { pub id: i64, pub email: String, pub first_name: String, pub last_name: String, pub role: Option, pub seniority: Option, pub cell_id: Option, pub is_active: bool, pub created_at: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Cell { pub id: i64, pub name: String, pub description: Option, pub created_at: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] #[allow(dead_code)] pub struct CellMember { pub id: i64, pub cell_id: i64, pub user_id: i64, pub joined_at: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ProjectMember { pub id: i64, pub user_id: i64, pub initiative_id: i64, pub initiative_name: Option, pub role: Option, pub assigned_at: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Absence { pub id: i64, pub user_id: i64, pub start_date: String, pub end_date: String, #[serde(rename = "type")] pub absence_type: String, pub reason: Option, pub created_at: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct DailyLog { pub id: i64, pub user_id: i64, pub initiative_id: i64, pub date: String, pub work_item_id: Option, pub what_worked_on: Option, pub progress_pct: Option, pub impediments: Option, pub plan_for_tomorrow: Option, pub hours_spent: Option, pub created_at: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct PerformanceSnapshot { pub id: i64, pub user_id: i64, pub initiative_id: Option, pub period: String, pub planned_sp: Option, pub completed_sp: Option, pub planned_hours: Option, pub actual_hours: Option, pub velocity: Option, pub spi: Option, pub cpi: Option, pub compliance_pct: Option, pub impediment_count: Option, pub calculated_at: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Epic { pub id: i64, pub initiative_id: i64, pub code: Option, pub name: String, pub description: Option, pub status: Option, pub client_taker: Option, pub stimated_start_date: Option, pub stimated_end_date: Option, pub start_date: Option, pub end_date: Option, pub created_at: Option, pub updated_at: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct UserStory { pub id: i64, pub initiative_id: i64, pub epic_id: Option, pub code: Option, pub title: String, pub description: Option, pub acceptance_criteria: Option, pub status: Option, pub priority: Option, pub story_points: Option, pub estimated_hours: Option, pub actual_hours: Option, pub assigned_to: Option, pub created_at: Option, } async fn get_conn(db_path: &str) -> Result { let db = libsql::Builder::new_local(db_path) .build() .await .map_err(|e| format!("DB open: {e}"))?; let conn = db.connect().map_err(|e| format!("Connect: {e}"))?; conn.execute_batch( "CREATE TABLE IF NOT EXISTS projects ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, key TEXT, description TEXT, status TEXT DEFAULT 'active', start_date TEXT, end_date TEXT, created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS work_items ( id INTEGER PRIMARY KEY, project_id INTEGER NOT NULL, code TEXT, title TEXT NOT NULL, description TEXT, type TEXT DEFAULT 'task', status TEXT DEFAULT 'backlog', priority TEXT DEFAULT 'medium', created_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (project_id) REFERENCES projects(id) ); CREATE TABLE IF NOT EXISTS alpha_users ( id INTEGER PRIMARY KEY, email TEXT NOT NULL, first_name TEXT DEFAULT '', last_name TEXT DEFAULT '', role TEXT, seniority TEXT, cell_id INTEGER, is_active INTEGER DEFAULT 1, created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS cells ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT, created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS cell_members ( id INTEGER PRIMARY KEY AUTOINCREMENT, cell_id INTEGER NOT NULL, user_id INTEGER NOT NULL, joined_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (cell_id) REFERENCES cells(id), FOREIGN KEY (user_id) REFERENCES alpha_users(id) ); CREATE TABLE IF NOT EXISTS project_members ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, initiative_id INTEGER NOT NULL, initiative_name TEXT, role TEXT, assigned_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (user_id) REFERENCES alpha_users(id) ); CREATE TABLE IF NOT EXISTS absences ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, start_date TEXT NOT NULL, end_date TEXT NOT NULL, type TEXT DEFAULT 'vacation', reason TEXT, created_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (user_id) REFERENCES alpha_users(id) ); CREATE TABLE IF NOT EXISTS daily_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, initiative_id INTEGER NOT NULL, date TEXT NOT NULL, work_item_id INTEGER, what_worked_on TEXT, progress_pct REAL, impediments TEXT, plan_for_tomorrow TEXT, hours_spent REAL, created_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (user_id) REFERENCES alpha_users(id), FOREIGN KEY (work_item_id) REFERENCES work_items(id) ); CREATE TABLE IF NOT EXISTS performance_snapshots ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, initiative_id INTEGER, period TEXT NOT NULL DEFAULT 'weekly', planned_sp REAL, completed_sp REAL, planned_hours REAL, actual_hours REAL, velocity REAL, spi REAL, cpi REAL, compliance_pct REAL, impediment_count INTEGER, calculated_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (user_id) REFERENCES alpha_users(id) ); CREATE TABLE IF NOT EXISTS epics ( id INTEGER PRIMARY KEY, initiative_id INTEGER NOT NULL, code TEXT, name TEXT NOT NULL, description TEXT, status TEXT DEFAULT 'active', client_taker INTEGER, stimated_start_date TEXT, stimated_end_date TEXT, start_date TEXT, end_date TEXT, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (initiative_id) REFERENCES projects(id) ); CREATE TABLE IF NOT EXISTS user_stories ( id INTEGER PRIMARY KEY, initiative_id INTEGER NOT NULL, epic_id INTEGER, code TEXT, title TEXT NOT NULL, description TEXT, acceptance_criteria TEXT, status TEXT DEFAULT 'backlog', priority TEXT DEFAULT 'medium', story_points REAL, estimated_hours REAL, actual_hours REAL, assigned_to INTEGER, created_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (initiative_id) REFERENCES projects(id), FOREIGN KEY (epic_id) REFERENCES epics(id) );" ) .await .map_err(|e| format!("Migration: {e}"))?; // Copiar DB al Desktop para acceso rápido con DBeaver let _ = std::fs::copy( db_path, dirs_next::desktop_dir() .unwrap_or_else(|| std::path::PathBuf::from(".")) .join("alpha.db"), ); Ok(conn) } pub mod commands { use super::*; // ──────────────────────────────────────────── // Projects // ──────────────────────────────────────────── #[tauri::command] pub async fn get_projects(state: State<'_, Mutex>) -> Result, String> { let db_path = state.lock().map_err(|e| e.to_string())?.clone(); let conn = get_conn(&db_path).await?; let mut rows = conn .query("SELECT id, name, key, description, status, start_date, end_date FROM projects ORDER BY name", ()) .await .map_err(|e| format!("Query: {e}"))?; let mut projects = Vec::new(); while let Some(row) = rows.next().await.map_err(|e| format!("Row: {e}"))? { projects.push(Project { id: row.get::(0).unwrap_or(0), name: row.get::(1).unwrap_or_default(), key: row.get::(2).ok(), description: row.get::(3).ok(), status: row.get::(4).unwrap_or_else(|_| "active".into()), start_date: row.get::(5).ok(), end_date: row.get::(6).ok(), }); } Ok(projects) } #[tauri::command] pub async fn save_project(state: State<'_, Mutex>, project: Project) -> Result { let db_path = state.lock().map_err(|e| e.to_string())?.clone(); let conn = get_conn(&db_path).await?; conn.execute( "INSERT OR REPLACE INTO projects (id, name, key, description, status, start_date, end_date) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", libsql::params![ project.id, project.name, project.key, project.description, project.status, project.start_date, project.end_date, ], ) .await .map_err(|e| format!("Insert: {e}"))?; Ok(conn.last_insert_rowid()) } #[tauri::command] pub async fn delete_project(state: State<'_, Mutex>, id: i64) -> Result<(), String> { let db_path = state.lock().map_err(|e| e.to_string())?.clone(); let conn = get_conn(&db_path).await?; conn.execute("DELETE FROM work_items WHERE project_id = ?1", libsql::params![id]) .await .map_err(|e| format!("Delete WIs: {e}"))?; conn.execute("DELETE FROM projects WHERE id = ?1", libsql::params![id]) .await .map_err(|e| format!("Delete project: {e}"))?; Ok(()) } // ──────────────────────────────────────────── // Work Items // ──────────────────────────────────────────── #[tauri::command] pub async fn get_work_items(state: State<'_, Mutex>, project_id: i64) -> Result, String> { let db_path = state.lock().map_err(|e| e.to_string())?.clone(); let conn = get_conn(&db_path).await?; let mut rows = conn .query( "SELECT id, project_id, code, title, description, type, status, priority FROM work_items WHERE project_id = ?1 ORDER BY created_at DESC", libsql::params![project_id], ) .await .map_err(|e| format!("Query: {e}"))?; let mut items = Vec::new(); while let Some(row) = rows.next().await.map_err(|e| format!("Row: {e}"))? { items.push(WorkItem { id: row.get::(0).unwrap_or(0), project_id: row.get::(1).unwrap_or(0), code: row.get::(2).ok(), title: row.get::(3).unwrap_or_default(), description: row.get::(4).ok(), wi_type: row.get::(5).unwrap_or_else(|_| "task".into()), status: row.get::(6).unwrap_or_else(|_| "backlog".into()), priority: row.get::(7).unwrap_or_else(|_| "medium".into()), }); } Ok(items) } #[tauri::command] pub async fn save_work_item(state: State<'_, Mutex>, item: WorkItem) -> Result { let db_path = state.lock().map_err(|e| e.to_string())?.clone(); let conn = get_conn(&db_path).await?; conn.execute( "INSERT OR REPLACE INTO work_items (id, project_id, code, title, description, type, status, priority) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", libsql::params![ item.id, item.project_id, item.code, item.title, item.description, item.wi_type, item.status, item.priority, ], ) .await .map_err(|e| format!("Insert: {e}"))?; Ok(conn.last_insert_rowid()) } #[tauri::command] pub async fn delete_work_item(state: State<'_, Mutex>, id: i64) -> Result<(), String> { let db_path = state.lock().map_err(|e| e.to_string())?.clone(); let conn = get_conn(&db_path).await?; conn.execute("DELETE FROM work_items WHERE id = ?1", libsql::params![id]) .await .map_err(|e| format!("Delete: {e}"))?; Ok(()) } // ──────────────────────────────────────────── // Users (Alpha) // ──────────────────────────────────────────── #[tauri::command] pub async fn get_users(state: State<'_, Mutex>) -> Result, String> { let db_path = state.lock().map_err(|e| e.to_string())?.clone(); let conn = get_conn(&db_path).await?; let mut rows = conn .query("SELECT id, email, first_name, last_name, role, seniority, cell_id, is_active, created_at FROM alpha_users ORDER BY first_name", ()) .await .map_err(|e| format!("Query: {e}"))?; let mut users = Vec::new(); while let Some(row) = rows.next().await.map_err(|e| format!("Row: {e}"))? { users.push(AlphaUser { id: row.get::(0).unwrap_or(0), email: row.get::(1).unwrap_or_default(), first_name: row.get::(2).unwrap_or_default(), last_name: row.get::(3).unwrap_or_default(), role: row.get::(4).ok(), seniority: row.get::(5).ok(), cell_id: row.get::(6).ok(), is_active: row.get::(7).unwrap_or(1) != 0, created_at: row.get::(8).ok(), }); } Ok(users) } #[tauri::command] pub async fn save_user(state: State<'_, Mutex>, user: AlphaUser) -> Result { let db_path = state.lock().map_err(|e| e.to_string())?.clone(); let conn = get_conn(&db_path).await?; conn.execute( "INSERT OR REPLACE INTO alpha_users (id, email, first_name, last_name, role, seniority, cell_id, is_active) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", libsql::params![ user.id, user.email, user.first_name, user.last_name, user.role, user.seniority, user.cell_id, user.is_active as i64, ], ) .await .map_err(|e| format!("Insert: {e}"))?; Ok(conn.last_insert_rowid()) } #[tauri::command] pub async fn update_user_fields( state: State<'_, Mutex>, id: i64, role: Option, seniority: Option, cell_id: Option, ) -> Result<(), String> { let db_path = state.lock().map_err(|e| e.to_string())?.clone(); let conn = get_conn(&db_path).await?; conn.execute( "UPDATE alpha_users SET role = COALESCE(?1, role), seniority = COALESCE(?2, seniority), cell_id = COALESCE(?3, cell_id) WHERE id = ?4", libsql::params![role, seniority, cell_id, id], ) .await .map_err(|e| format!("Update: {e}"))?; Ok(()) } // ──────────────────────────────────────────── // Cells // ──────────────────────────────────────────── #[tauri::command] pub async fn get_cells(state: State<'_, Mutex>) -> Result, String> { let db_path = state.lock().map_err(|e| e.to_string())?.clone(); let conn = get_conn(&db_path).await?; let mut rows = conn .query("SELECT id, name, description, created_at FROM cells ORDER BY name", ()) .await .map_err(|e| format!("Query: {e}"))?; let mut cells = Vec::new(); while let Some(row) = rows.next().await.map_err(|e| format!("Row: {e}"))? { cells.push(Cell { id: row.get::(0).unwrap_or(0), name: row.get::(1).unwrap_or_default(), description: row.get::(2).ok(), created_at: row.get::(3).ok(), }); } Ok(cells) } #[tauri::command] pub async fn save_cell(state: State<'_, Mutex>, cell: Cell) -> Result { let db_path = state.lock().map_err(|e| e.to_string())?.clone(); let conn = get_conn(&db_path).await?; if cell.id != 0 { conn.execute( "UPDATE cells SET name = ?1, description = ?2 WHERE id = ?3", libsql::params![cell.name, cell.description, cell.id], ).await.map_err(|e| format!("Update: {e}"))?; Ok(cell.id) } else { conn.execute( "INSERT INTO cells (name, description) VALUES (?1, ?2)", libsql::params![cell.name, cell.description], ).await.map_err(|e| format!("Insert: {e}"))?; Ok(conn.last_insert_rowid()) } } // ──────────────────────────────────────────── // Project Members // ──────────────────────────────────────────── #[tauri::command] pub async fn save_project_members( state: State<'_, Mutex>, user_id: i64, members: Vec, ) -> Result<(), String> { let db_path = state.lock().map_err(|e| e.to_string())?.clone(); let conn = get_conn(&db_path).await?; conn.execute("DELETE FROM project_members WHERE user_id = ?1", libsql::params![user_id]) .await .map_err(|e| format!("Delete: {e}"))?; for m in &members { conn.execute( "INSERT INTO project_members (user_id, initiative_id, initiative_name, role) VALUES (?1, ?2, ?3, ?4)", libsql::params![user_id, m.initiative_id, m.initiative_name.clone(), m.role.clone()], ) .await .map_err(|e| format!("Insert: {e}"))?; } Ok(()) } #[tauri::command] pub async fn get_project_members( state: State<'_, Mutex>, user_id: i64, ) -> Result, String> { let db_path = state.lock().map_err(|e| e.to_string())?.clone(); let conn = get_conn(&db_path).await?; let mut rows = conn .query("SELECT id, user_id, initiative_id, initiative_name, role, assigned_at FROM project_members WHERE user_id = ?1 ORDER BY assigned_at DESC", libsql::params![user_id]) .await .map_err(|e| format!("Query: {e}"))?; let mut members = Vec::new(); while let Some(row) = rows.next().await.map_err(|e| format!("Row: {e}"))? { members.push(ProjectMember { id: row.get::(0).unwrap_or(0), user_id: row.get::(1).unwrap_or(0), initiative_id: row.get::(2).unwrap_or(0), initiative_name: row.get::(3).ok(), role: row.get::(4).ok(), assigned_at: row.get::(5).ok(), }); } Ok(members) } // ──────────────────────────────────────────── // Absences // ──────────────────────────────────────────── #[tauri::command] pub async fn get_absences(state: State<'_, Mutex>, user_id: i64) -> Result, String> { let db_path = state.lock().map_err(|e| e.to_string())?.clone(); let conn = get_conn(&db_path).await?; let mut rows = conn .query("SELECT id, user_id, start_date, end_date, type, reason, created_at FROM absences WHERE user_id = ?1 ORDER BY start_date DESC", libsql::params![user_id]) .await .map_err(|e| format!("Query: {e}"))?; let mut absences = Vec::new(); while let Some(row) = rows.next().await.map_err(|e| format!("Row: {e}"))? { absences.push(Absence { id: row.get::(0).unwrap_or(0), user_id: row.get::(1).unwrap_or(0), start_date: row.get::(2).unwrap_or_default(), end_date: row.get::(3).unwrap_or_default(), absence_type: row.get::(4).unwrap_or_else(|_| "vacation".into()), reason: row.get::(5).ok(), created_at: row.get::(6).ok(), }); } Ok(absences) } #[tauri::command] pub async fn save_absence(state: State<'_, Mutex>, absence: Absence) -> Result { let db_path = state.lock().map_err(|e| e.to_string())?.clone(); let conn = get_conn(&db_path).await?; if absence.id != 0 { conn.execute( "UPDATE absences SET start_date = ?1, end_date = ?2, type = ?3, reason = ?4 WHERE id = ?5", libsql::params![absence.start_date, absence.end_date, absence.absence_type, absence.reason, absence.id], ).await.map_err(|e| format!("Update: {e}"))?; Ok(absence.id) } else { conn.execute( "INSERT INTO absences (user_id, start_date, end_date, type, reason) VALUES (?1, ?2, ?3, ?4, ?5)", libsql::params![absence.user_id, absence.start_date, absence.end_date, absence.absence_type, absence.reason], ).await.map_err(|e| format!("Insert: {e}"))?; Ok(conn.last_insert_rowid()) } } #[tauri::command] pub async fn delete_absence(state: State<'_, Mutex>, id: i64) -> Result<(), String> { let db_path = state.lock().map_err(|e| e.to_string())?.clone(); let conn = get_conn(&db_path).await?; conn.execute("DELETE FROM absences WHERE id = ?1", libsql::params![id]) .await .map_err(|e| format!("Delete: {e}"))?; Ok(()) } // ──────────────────────────────────────────── // Daily Logs // ──────────────────────────────────────────── #[tauri::command] pub async fn get_daily_logs( state: State<'_, Mutex>, user_id: i64, initiative_id: Option, ) -> Result, String> { let db_path = state.lock().map_err(|e| e.to_string())?.clone(); let conn = get_conn(&db_path).await?; let query = if let Some(_pid) = initiative_id { "SELECT id, user_id, initiative_id, date, work_item_id, what_worked_on, progress_pct, impediments, plan_for_tomorrow, hours_spent, created_at FROM daily_logs WHERE user_id = ?1 AND initiative_id = ?2 ORDER BY date DESC" } else { "SELECT id, user_id, initiative_id, date, work_item_id, what_worked_on, progress_pct, impediments, plan_for_tomorrow, hours_spent, created_at FROM daily_logs WHERE user_id = ?1 ORDER BY date DESC" }; let mut rows = if let Some(pid) = initiative_id { conn.query(query, libsql::params![user_id, pid]).await.map_err(|e| format!("Query: {e}"))? } else { conn.query(query, libsql::params![user_id]).await.map_err(|e| format!("Query: {e}"))? }; let mut logs = Vec::new(); while let Some(row) = rows.next().await.map_err(|e| format!("Row: {e}"))? { logs.push(DailyLog { id: row.get::(0).unwrap_or(0), user_id: row.get::(1).unwrap_or(0), initiative_id: row.get::(2).unwrap_or(0), date: row.get::(3).unwrap_or_default(), work_item_id: row.get::(4).ok(), what_worked_on: row.get::(5).ok(), progress_pct: row.get::(6).ok(), impediments: row.get::(7).ok(), plan_for_tomorrow: row.get::(8).ok(), hours_spent: row.get::(9).ok(), created_at: row.get::(10).ok(), }); } Ok(logs) } #[tauri::command] pub async fn save_daily_log(state: State<'_, Mutex>, log: DailyLog) -> Result { let db_path = state.lock().map_err(|e| e.to_string())?.clone(); let conn = get_conn(&db_path).await?; if log.id != 0 { conn.execute( "UPDATE daily_logs SET what_worked_on = ?1, progress_pct = ?2, impediments = ?3, plan_for_tomorrow = ?4, hours_spent = ?5 WHERE id = ?6", libsql::params![log.what_worked_on, log.progress_pct, log.impediments, log.plan_for_tomorrow, log.hours_spent, log.id], ).await.map_err(|e| format!("Update: {e}"))?; Ok(log.id) } else { conn.execute( "INSERT INTO daily_logs (user_id, initiative_id, date, work_item_id, what_worked_on, progress_pct, impediments, plan_for_tomorrow, hours_spent) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", libsql::params![log.user_id, log.initiative_id, log.date, log.work_item_id, log.what_worked_on, log.progress_pct, log.impediments, log.plan_for_tomorrow, log.hours_spent], ).await.map_err(|e| format!("Insert: {e}"))?; Ok(conn.last_insert_rowid()) } } // ──────────────────────────────────────────── // Performance // ──────────────────────────────────────────── #[tauri::command] pub async fn get_performance( state: State<'_, Mutex>, user_id: i64, initiative_id: Option, ) -> Result, String> { let db_path = state.lock().map_err(|e| e.to_string())?.clone(); let conn = get_conn(&db_path).await?; let query = if let Some(_pid) = initiative_id { "SELECT id, user_id, initiative_id, period, planned_sp, completed_sp, planned_hours, actual_hours, velocity, spi, cpi, compliance_pct, impediment_count, calculated_at FROM performance_snapshots WHERE user_id = ?1 AND initiative_id = ?2 ORDER BY calculated_at DESC" } else { "SELECT id, user_id, initiative_id, period, planned_sp, completed_sp, planned_hours, actual_hours, velocity, spi, cpi, compliance_pct, impediment_count, calculated_at FROM performance_snapshots WHERE user_id = ?1 ORDER BY calculated_at DESC" }; let mut rows = if let Some(pid) = initiative_id { conn.query(query, libsql::params![user_id, pid]).await.map_err(|e| format!("Query: {e}"))? } else { conn.query(query, libsql::params![user_id]).await.map_err(|e| format!("Query: {e}"))? }; let mut snapshots = Vec::new(); while let Some(row) = rows.next().await.map_err(|e| format!("Row: {e}"))? { snapshots.push(PerformanceSnapshot { id: row.get::(0).unwrap_or(0), user_id: row.get::(1).unwrap_or(0), initiative_id: row.get::(2).ok(), period: row.get::(3).unwrap_or_default(), planned_sp: row.get::(4).ok(), completed_sp: row.get::(5).ok(), planned_hours: row.get::(6).ok(), actual_hours: row.get::(7).ok(), velocity: row.get::(8).ok(), spi: row.get::(9).ok(), cpi: row.get::(10).ok(), compliance_pct: row.get::(11).ok(), impediment_count: row.get::(12).ok(), calculated_at: row.get::(13).ok(), }); } Ok(snapshots) } #[tauri::command] pub async fn save_performance(state: State<'_, Mutex>, snap: PerformanceSnapshot) -> Result { let db_path = state.lock().map_err(|e| e.to_string())?.clone(); let conn = get_conn(&db_path).await?; conn.execute( "INSERT INTO performance_snapshots (user_id, initiative_id, period, planned_sp, completed_sp, planned_hours, actual_hours, velocity, spi, cpi, compliance_pct, impediment_count) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", libsql::params![ snap.user_id, snap.initiative_id, snap.period, snap.planned_sp, snap.completed_sp, snap.planned_hours, snap.actual_hours, snap.velocity, snap.spi, snap.cpi, snap.compliance_pct, snap.impediment_count, ], ).await.map_err(|e| format!("Insert: {e}"))?; Ok(conn.last_insert_rowid()) } // ──────────────────────────────────────────── // Epics // ──────────────────────────────────────────── #[tauri::command] pub async fn get_epics(state: State<'_, Mutex>, initiative_id: i64) -> Result, String> { let db_path = state.lock().map_err(|e| e.to_string())?.clone(); let conn = get_conn(&db_path).await?; let mut rows = conn .query("SELECT id, initiative_id, code, name, description, status, client_taker, stimated_start_date, stimated_end_date, start_date, end_date, created_at, updated_at FROM epics WHERE initiative_id = ?1 ORDER BY created_at DESC", libsql::params![initiative_id]) .await .map_err(|e| format!("Query: {e}"))?; let mut epics = Vec::new(); while let Some(row) = rows.next().await.map_err(|e| format!("Row: {e}"))? { epics.push(Epic { id: row.get::(0).unwrap_or(0), initiative_id: row.get::(1).unwrap_or(0), code: row.get::(2).ok(), name: row.get::(3).unwrap_or_default(), description: row.get::(4).ok(), status: row.get::(5).ok(), client_taker: row.get::(6).ok(), stimated_start_date: row.get::(7).ok(), stimated_end_date: row.get::(8).ok(), start_date: row.get::(9).ok(), end_date: row.get::(10).ok(), created_at: row.get::(11).ok(), updated_at: row.get::(12).ok(), }); } Ok(epics) } #[tauri::command] pub async fn save_epic(state: State<'_, Mutex>, epic: Epic) -> Result { let db_path = state.lock().map_err(|e| e.to_string())?.clone(); let conn = get_conn(&db_path).await?; conn.execute( "INSERT OR REPLACE INTO epics (id, initiative_id, code, name, description, status, client_taker, stimated_start_date, stimated_end_date, start_date, end_date, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, datetime('now'))", libsql::params![ epic.id, epic.initiative_id, epic.code, epic.name, epic.description, epic.status, epic.client_taker, epic.stimated_start_date, epic.stimated_end_date, epic.start_date, epic.end_date, ], ) .await .map_err(|e| format!("Insert: {e}"))?; Ok(conn.last_insert_rowid()) } #[tauri::command] pub async fn delete_epic(state: State<'_, Mutex>, id: i64) -> Result<(), String> { let db_path = state.lock().map_err(|e| e.to_string())?.clone(); let conn = get_conn(&db_path).await?; conn.execute("DELETE FROM user_stories WHERE epic_id = ?1", libsql::params![id]) .await.map_err(|e| format!("Delete HUs: {e}"))?; conn.execute("DELETE FROM epics WHERE id = ?1", libsql::params![id]) .await.map_err(|e| format!("Delete: {e}"))?; Ok(()) } // ──────────────────────────────────────────── // User Stories // ──────────────────────────────────────────── #[tauri::command] pub async fn get_user_stories(state: State<'_, Mutex>, initiative_id: i64, epic_id: Option) -> Result, String> { let db_path = state.lock().map_err(|e| e.to_string())?.clone(); let conn = get_conn(&db_path).await?; let query = if epic_id.is_some() { "SELECT id, initiative_id, epic_id, code, title, description, acceptance_criteria, status, priority, story_points, estimated_hours, actual_hours, assigned_to, created_at FROM user_stories WHERE initiative_id = ?1 AND epic_id = ?2 ORDER BY created_at DESC" } else { "SELECT id, initiative_id, epic_id, code, title, description, acceptance_criteria, status, priority, story_points, estimated_hours, actual_hours, assigned_to, created_at FROM user_stories WHERE initiative_id = ?1 ORDER BY created_at DESC" }; let mut rows = if let Some(eid) = epic_id { conn.query(query, libsql::params![initiative_id, eid]).await.map_err(|e| format!("Query: {e}"))? } else { conn.query(query, libsql::params![initiative_id]).await.map_err(|e| format!("Query: {e}"))? }; let mut stories = Vec::new(); while let Some(row) = rows.next().await.map_err(|e| format!("Row: {e}"))? { stories.push(UserStory { id: row.get::(0).unwrap_or(0), initiative_id: row.get::(1).unwrap_or(0), epic_id: row.get::(2).ok(), code: row.get::(3).ok(), title: row.get::(4).unwrap_or_default(), description: row.get::(5).ok(), acceptance_criteria: row.get::(6).ok(), status: row.get::(7).ok(), priority: row.get::(8).ok(), story_points: row.get::(9).ok(), estimated_hours: row.get::(10).ok(), actual_hours: row.get::(11).ok(), assigned_to: row.get::(12).ok(), created_at: row.get::(13).ok(), }); } Ok(stories) } #[tauri::command] pub async fn save_user_story(state: State<'_, Mutex>, story: UserStory) -> Result { let db_path = state.lock().map_err(|e| e.to_string())?.clone(); let conn = get_conn(&db_path).await?; conn.execute( "INSERT OR REPLACE INTO user_stories (id, initiative_id, epic_id, code, title, description, acceptance_criteria, status, priority, story_points, estimated_hours, actual_hours, assigned_to) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", libsql::params![ story.id, story.initiative_id, story.epic_id, story.code, story.title, story.description, story.acceptance_criteria, story.status, story.priority, story.story_points, story.estimated_hours, story.actual_hours, story.assigned_to, ], ) .await .map_err(|e| format!("Insert: {e}"))?; Ok(conn.last_insert_rowid()) } #[tauri::command] pub async fn delete_user_story(state: State<'_, Mutex>, id: i64) -> Result<(), String> { let db_path = state.lock().map_err(|e| e.to_string())?.clone(); let conn = get_conn(&db_path).await?; conn.execute("DELETE FROM user_stories WHERE id = ?1", libsql::params![id]) .await .map_err(|e| format!("Delete: {e}"))?; Ok(()) } }