extender Turso db: tablas de usuarios, celulas, project_members, ausencias, daily_logs, performance

This commit is contained in:
2026-05-26 16:33:24 -05:00
parent e2cf81757a
commit 0534817470
2 changed files with 559 additions and 0 deletions
+545
View File
@@ -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<String>,
pub seniority: Option<String>,
pub cell_id: Option<i64>,
pub is_active: bool,
pub created_at: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Cell {
pub id: i64,
pub name: String,
pub description: Option<String>,
pub created_at: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CellMember {
pub id: i64,
pub cell_id: i64,
pub user_id: i64,
pub joined_at: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ProjectMember {
pub id: i64,
pub user_id: i64,
pub initiative_id: i64,
pub initiative_name: Option<String>,
pub role: Option<String>,
pub assigned_at: Option<String>,
}
#[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<String>,
pub created_at: Option<String>,
}
#[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<i64>,
pub what_worked_on: Option<String>,
pub progress_pct: Option<f64>,
pub impediments: Option<String>,
pub plan_for_tomorrow: Option<String>,
pub hours_spent: Option<f64>,
pub created_at: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PerformanceSnapshot {
pub id: i64,
pub user_id: i64,
pub initiative_id: Option<i64>,
pub period: String,
pub planned_sp: Option<f64>,
pub completed_sp: Option<f64>,
pub planned_hours: Option<f64>,
pub actual_hours: Option<f64>,
pub velocity: Option<f64>,
pub spi: Option<f64>,
pub cpi: Option<f64>,
pub compliance_pct: Option<f64>,
pub impediment_count: Option<i64>,
pub calculated_at: Option<String>,
}
async fn get_conn(db_path: &str) -> Result<libsql::Connection, String> {
let db = libsql::Builder::new_local(db_path)
.build()
@@ -57,6 +141,89 @@ async fn get_conn(db_path: &str) -> Result<libsql::Connection, String> {
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<libsql::Connection, String> {
pub mod commands {
use super::*;
// ────────────────────────────────────────────
// Projects
// ────────────────────────────────────────────
#[tauri::command]
pub async fn get_projects(state: State<'_, Mutex<String>>) -> Result<Vec<Project>, 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<String>>, project_id: i64) -> Result<Vec<WorkItem>, 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<String>>) -> Result<Vec<AlphaUser>, 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::<i64>(0).unwrap_or(0),
email: row.get::<String>(1).unwrap_or_default(),
first_name: row.get::<String>(2).unwrap_or_default(),
last_name: row.get::<String>(3).unwrap_or_default(),
role: row.get::<String>(4).ok(),
seniority: row.get::<String>(5).ok(),
cell_id: row.get::<i64>(6).ok(),
is_active: row.get::<i64>(7).unwrap_or(1) != 0,
created_at: row.get::<String>(8).ok(),
});
}
Ok(users)
}
#[tauri::command]
pub async fn save_user(state: State<'_, Mutex<String>>, user: AlphaUser) -> Result<i64, String> {
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<String>>,
id: i64,
role: Option<String>,
seniority: Option<String>,
cell_id: Option<i64>,
) -> 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<String>>) -> Result<Vec<Cell>, 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::<i64>(0).unwrap_or(0),
name: row.get::<String>(1).unwrap_or_default(),
description: row.get::<String>(2).ok(),
created_at: row.get::<String>(3).ok(),
});
}
Ok(cells)
}
#[tauri::command]
pub async fn save_cell(state: State<'_, Mutex<String>>, cell: Cell) -> Result<i64, String> {
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<String>>,
user_id: i64,
members: Vec<ProjectMember>,
) -> 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<String>>,
user_id: i64,
) -> Result<Vec<ProjectMember>, 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::<i64>(0).unwrap_or(0),
user_id: row.get::<i64>(1).unwrap_or(0),
initiative_id: row.get::<i64>(2).unwrap_or(0),
initiative_name: row.get::<String>(3).ok(),
role: row.get::<String>(4).ok(),
assigned_at: row.get::<String>(5).ok(),
});
}
Ok(members)
}
// ────────────────────────────────────────────
// Absences
// ────────────────────────────────────────────
#[tauri::command]
pub async fn get_absences(state: State<'_, Mutex<String>>, user_id: i64) -> Result<Vec<Absence>, 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::<i64>(0).unwrap_or(0),
user_id: row.get::<i64>(1).unwrap_or(0),
start_date: row.get::<String>(2).unwrap_or_default(),
end_date: row.get::<String>(3).unwrap_or_default(),
absence_type: row.get::<String>(4).unwrap_or_else(|_| "vacation".into()),
reason: row.get::<String>(5).ok(),
created_at: row.get::<String>(6).ok(),
});
}
Ok(absences)
}
#[tauri::command]
pub async fn save_absence(state: State<'_, Mutex<String>>, absence: Absence) -> Result<i64, String> {
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<String>>, 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<String>>,
user_id: i64,
initiative_id: Option<i64>,
) -> Result<Vec<DailyLog>, 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::<i64>(0).unwrap_or(0),
user_id: row.get::<i64>(1).unwrap_or(0),
initiative_id: row.get::<i64>(2).unwrap_or(0),
date: row.get::<String>(3).unwrap_or_default(),
work_item_id: row.get::<i64>(4).ok(),
what_worked_on: row.get::<String>(5).ok(),
progress_pct: row.get::<f64>(6).ok(),
impediments: row.get::<String>(7).ok(),
plan_for_tomorrow: row.get::<String>(8).ok(),
hours_spent: row.get::<f64>(9).ok(),
created_at: row.get::<String>(10).ok(),
});
}
Ok(logs)
}
#[tauri::command]
pub async fn save_daily_log(state: State<'_, Mutex<String>>, log: DailyLog) -> Result<i64, String> {
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<String>>,
user_id: i64,
initiative_id: Option<i64>,
) -> Result<Vec<PerformanceSnapshot>, 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::<i64>(0).unwrap_or(0),
user_id: row.get::<i64>(1).unwrap_or(0),
initiative_id: row.get::<i64>(2).ok(),
period: row.get::<String>(3).unwrap_or_default(),
planned_sp: row.get::<f64>(4).ok(),
completed_sp: row.get::<f64>(5).ok(),
planned_hours: row.get::<f64>(6).ok(),
actual_hours: row.get::<f64>(7).ok(),
velocity: row.get::<f64>(8).ok(),
spi: row.get::<f64>(9).ok(),
cpi: row.get::<f64>(10).ok(),
compliance_pct: row.get::<f64>(11).ok(),
impediment_count: row.get::<i64>(12).ok(),
calculated_at: row.get::<String>(13).ok(),
});
}
Ok(snapshots)
}
#[tauri::command]
pub async fn save_performance(state: State<'_, Mutex<String>>, snap: PerformanceSnapshot) -> Result<i64, String> {
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())
}
}