From 05348174708fb9c30ee6bb74819e6d71346a95d5 Mon Sep 17 00:00:00 2001 From: Ricardo Gonzalez Date: Tue, 26 May 2026 16:33:24 -0500 Subject: [PATCH] extender Turso db: tablas de usuarios, celulas, project_members, ausencias, daily_logs, performance --- src-tauri/src/db.rs | 545 ++++++++++++++++++++++++++++++++++++++++++ src-tauri/src/main.rs | 14 ++ 2 files changed, 559 insertions(+) diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index 5e1c7a5..9646316 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -26,6 +26,90 @@ pub struct WorkItem { 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)] +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, +} + async fn get_conn(db_path: &str) -> Result { let db = libsql::Builder::new_local(db_path) .build() @@ -57,6 +141,89 @@ async fn get_conn(db_path: &str) -> Result { 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) );" ) .await @@ -68,6 +235,10 @@ async fn get_conn(db_path: &str) -> Result { 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(); @@ -133,6 +304,10 @@ pub mod commands { 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(); @@ -198,4 +373,374 @@ pub mod commands { 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()) + } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 472953e..dc96d15 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -26,6 +26,20 @@ fn main() { commands::get_work_items, commands::save_work_item, commands::delete_work_item, + commands::get_users, + commands::save_user, + commands::update_user_fields, + commands::get_cells, + commands::save_cell, + commands::save_project_members, + commands::get_project_members, + commands::get_absences, + commands::save_absence, + commands::delete_absence, + commands::get_daily_logs, + commands::save_daily_log, + commands::get_performance, + commands::save_performance, ]) .run(tauri::generate_context!()) .expect("error while running tauri application");