Files
Alpha/src-tauri/src/db.rs
T

973 lines
39 KiB
Rust

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<String>,
pub description: Option<String>,
pub status: String,
pub start_date: Option<String>,
pub end_date: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct WorkItem {
pub id: i64,
pub project_id: i64,
pub code: Option<String>,
pub title: String,
pub description: Option<String>,
#[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<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)]
#[allow(dead_code)]
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>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Epic {
pub id: i64,
pub initiative_id: i64,
pub code: Option<String>,
pub name: String,
pub description: Option<String>,
pub status: Option<String>,
pub client_taker: Option<i64>,
pub stimated_start_date: Option<String>,
pub stimated_end_date: Option<String>,
pub start_date: Option<String>,
pub end_date: Option<String>,
pub created_at: Option<String>,
pub updated_at: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UserStory {
pub id: i64,
pub initiative_id: i64,
pub epic_id: Option<i64>,
pub code: Option<String>,
pub title: String,
pub description: Option<String>,
pub acceptance_criteria: Option<String>,
pub status: Option<String>,
pub priority: Option<String>,
pub story_points: Option<f64>,
pub estimated_hours: Option<f64>,
pub actual_hours: Option<f64>,
pub assigned_to: Option<i64>,
pub created_at: Option<String>,
}
async fn get_conn(db_path: &str) -> Result<libsql::Connection, String> {
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<String>>) -> Result<Vec<Project>, 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::<i64>(0).unwrap_or(0),
name: row.get::<String>(1).unwrap_or_default(),
key: row.get::<String>(2).ok(),
description: row.get::<String>(3).ok(),
status: row.get::<String>(4).unwrap_or_else(|_| "active".into()),
start_date: row.get::<String>(5).ok(),
end_date: row.get::<String>(6).ok(),
});
}
Ok(projects)
}
#[tauri::command]
pub async fn save_project(state: State<'_, Mutex<String>>, project: Project) -> 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 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<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 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<String>>, project_id: i64) -> Result<Vec<WorkItem>, 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::<i64>(0).unwrap_or(0),
project_id: row.get::<i64>(1).unwrap_or(0),
code: row.get::<String>(2).ok(),
title: row.get::<String>(3).unwrap_or_default(),
description: row.get::<String>(4).ok(),
wi_type: row.get::<String>(5).unwrap_or_else(|_| "task".into()),
status: row.get::<String>(6).unwrap_or_else(|_| "backlog".into()),
priority: row.get::<String>(7).unwrap_or_else(|_| "medium".into()),
});
}
Ok(items)
}
#[tauri::command]
pub async fn save_work_item(state: State<'_, Mutex<String>>, item: WorkItem) -> 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 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<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 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<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())
}
// ────────────────────────────────────────────
// Epics
// ────────────────────────────────────────────
#[tauri::command]
pub async fn get_epics(state: State<'_, Mutex<String>>, initiative_id: i64) -> Result<Vec<Epic>, 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::<i64>(0).unwrap_or(0),
initiative_id: row.get::<i64>(1).unwrap_or(0),
code: row.get::<String>(2).ok(),
name: row.get::<String>(3).unwrap_or_default(),
description: row.get::<String>(4).ok(),
status: row.get::<String>(5).ok(),
client_taker: row.get::<i64>(6).ok(),
stimated_start_date: row.get::<String>(7).ok(),
stimated_end_date: row.get::<String>(8).ok(),
start_date: row.get::<String>(9).ok(),
end_date: row.get::<String>(10).ok(),
created_at: row.get::<String>(11).ok(),
updated_at: row.get::<String>(12).ok(),
});
}
Ok(epics)
}
#[tauri::command]
pub async fn save_epic(state: State<'_, Mutex<String>>, epic: Epic) -> 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 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<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 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<String>>, initiative_id: i64, epic_id: Option<i64>) -> Result<Vec<UserStory>, 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::<i64>(0).unwrap_or(0),
initiative_id: row.get::<i64>(1).unwrap_or(0),
epic_id: row.get::<i64>(2).ok(),
code: row.get::<String>(3).ok(),
title: row.get::<String>(4).unwrap_or_default(),
description: row.get::<String>(5).ok(),
acceptance_criteria: row.get::<String>(6).ok(),
status: row.get::<String>(7).ok(),
priority: row.get::<String>(8).ok(),
story_points: row.get::<f64>(9).ok(),
estimated_hours: row.get::<f64>(10).ok(),
actual_hours: row.get::<f64>(11).ok(),
assigned_to: row.get::<i64>(12).ok(),
created_at: row.get::<String>(13).ok(),
});
}
Ok(stories)
}
#[tauri::command]
pub async fn save_user_story(state: State<'_, Mutex<String>>, story: UserStory) -> 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 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<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 user_stories WHERE id = ?1", libsql::params![id])
.await
.map_err(|e| format!("Delete: {e}"))?;
Ok(())
}
}