973 lines
39 KiB
Rust
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(())
|
|
}
|
|
}
|