From b141be345ab03700c97036ce23b3f8d5406e7257 Mon Sep 17 00:00:00 2001 From: Ricardo Gonzalez Date: Wed, 27 May 2026 17:32:19 -0500 Subject: [PATCH] agregar tablas epics y user_stories con relacion + Rust commands + frontend bridge --- src-tauri/src/db.rs | 217 +++++++++++++++++++++++++++++++++++++++ src-tauri/src/main.rs | 6 ++ src/services/tauri-db.ts | 55 ++++++++++ 3 files changed, 278 insertions(+) diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index 8b520ae..0595542 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -111,6 +111,41 @@ pub struct PerformanceSnapshot { pub calculated_at: Option, } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Epic { + pub id: i64, + pub initiative_id: i64, + pub code: Option, + pub name: String, + pub description: Option, + pub status: Option, + pub client_taker: Option, + pub stimated_start_date: Option, + pub stimated_end_date: Option, + pub start_date: Option, + pub end_date: Option, + pub created_at: Option, + pub updated_at: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct UserStory { + pub id: i64, + pub initiative_id: i64, + pub epic_id: Option, + pub code: Option, + pub title: String, + pub description: Option, + pub acceptance_criteria: Option, + pub status: Option, + pub priority: Option, + pub story_points: Option, + pub estimated_hours: Option, + pub actual_hours: Option, + pub assigned_to: Option, + pub created_at: Option, +} + async fn get_conn(db_path: &str) -> Result { let db = libsql::Builder::new_local(db_path) .build() @@ -225,6 +260,42 @@ async fn get_conn(db_path: &str) -> Result { 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 @@ -744,4 +815,150 @@ pub mod commands { Ok(conn.last_insert_rowid()) } + + // ──────────────────────────────────────────── + // Epics + // ──────────────────────────────────────────── + + #[tauri::command] + pub async fn get_epics(state: State<'_, Mutex>, initiative_id: i64) -> Result, String> { + let db_path = state.lock().map_err(|e| e.to_string())?.clone(); + let conn = get_conn(&db_path).await?; + + let mut rows = conn + .query("SELECT id, initiative_id, code, name, description, status, client_taker, stimated_start_date, stimated_end_date, start_date, end_date, created_at, updated_at FROM epics WHERE initiative_id = ?1 ORDER BY created_at DESC", libsql::params![initiative_id]) + .await + .map_err(|e| format!("Query: {e}"))?; + + let mut epics = Vec::new(); + while let Some(row) = rows.next().await.map_err(|e| format!("Row: {e}"))? { + epics.push(Epic { + id: row.get::(0).unwrap_or(0), + initiative_id: row.get::(1).unwrap_or(0), + code: row.get::(2).ok(), + name: row.get::(3).unwrap_or_default(), + description: row.get::(4).ok(), + status: row.get::(5).ok(), + client_taker: row.get::(6).ok(), + stimated_start_date: row.get::(7).ok(), + stimated_end_date: row.get::(8).ok(), + start_date: row.get::(9).ok(), + end_date: row.get::(10).ok(), + created_at: row.get::(11).ok(), + updated_at: row.get::(12).ok(), + }); + } + Ok(epics) + } + + #[tauri::command] + pub async fn save_epic(state: State<'_, Mutex>, epic: Epic) -> Result { + let db_path = state.lock().map_err(|e| e.to_string())?.clone(); + let conn = get_conn(&db_path).await?; + + conn.execute( + "INSERT OR REPLACE INTO epics (id, initiative_id, code, name, description, status, client_taker, stimated_start_date, stimated_end_date, start_date, end_date, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, datetime('now'))", + libsql::params![ + epic.id, epic.initiative_id, epic.code, epic.name, epic.description, + epic.status, epic.client_taker, epic.stimated_start_date, + epic.stimated_end_date, epic.start_date, epic.end_date, + ], + ) + .await + .map_err(|e| format!("Insert: {e}"))?; + + Ok(conn.last_insert_rowid()) + } + + #[tauri::command] + pub async fn delete_epic(state: State<'_, Mutex>, id: i64) -> Result<(), String> { + let db_path = state.lock().map_err(|e| e.to_string())?.clone(); + let conn = get_conn(&db_path).await?; + + conn.execute("DELETE FROM user_stories WHERE epic_id = ?1", libsql::params![id]) + .await.map_err(|e| format!("Delete HUs: {e}"))?; + + conn.execute("DELETE FROM epics WHERE id = ?1", libsql::params![id]) + .await.map_err(|e| format!("Delete: {e}"))?; + + Ok(()) + } + + // ──────────────────────────────────────────── + // User Stories + // ──────────────────────────────────────────── + + #[tauri::command] + pub async fn get_user_stories(state: State<'_, Mutex>, initiative_id: i64, epic_id: Option) -> Result, String> { + let db_path = state.lock().map_err(|e| e.to_string())?.clone(); + let conn = get_conn(&db_path).await?; + + let query = if epic_id.is_some() { + "SELECT id, initiative_id, epic_id, code, title, description, acceptance_criteria, status, priority, story_points, estimated_hours, actual_hours, assigned_to, created_at FROM user_stories WHERE initiative_id = ?1 AND epic_id = ?2 ORDER BY created_at DESC" + } else { + "SELECT id, initiative_id, epic_id, code, title, description, acceptance_criteria, status, priority, story_points, estimated_hours, actual_hours, assigned_to, created_at FROM user_stories WHERE initiative_id = ?1 ORDER BY created_at DESC" + }; + + let mut rows = if let Some(eid) = epic_id { + conn.query(query, libsql::params![initiative_id, eid]).await.map_err(|e| format!("Query: {e}"))? + } else { + conn.query(query, libsql::params![initiative_id]).await.map_err(|e| format!("Query: {e}"))? + }; + + let mut stories = Vec::new(); + while let Some(row) = rows.next().await.map_err(|e| format!("Row: {e}"))? { + stories.push(UserStory { + id: row.get::(0).unwrap_or(0), + initiative_id: row.get::(1).unwrap_or(0), + epic_id: row.get::(2).ok(), + code: row.get::(3).ok(), + title: row.get::(4).unwrap_or_default(), + description: row.get::(5).ok(), + acceptance_criteria: row.get::(6).ok(), + status: row.get::(7).ok(), + priority: row.get::(8).ok(), + story_points: row.get::(9).ok(), + estimated_hours: row.get::(10).ok(), + actual_hours: row.get::(11).ok(), + assigned_to: row.get::(12).ok(), + created_at: row.get::(13).ok(), + }); + } + Ok(stories) + } + + #[tauri::command] + pub async fn save_user_story(state: State<'_, Mutex>, story: UserStory) -> Result { + let db_path = state.lock().map_err(|e| e.to_string())?.clone(); + let conn = get_conn(&db_path).await?; + + conn.execute( + "INSERT OR REPLACE INTO user_stories (id, initiative_id, epic_id, code, title, description, acceptance_criteria, status, priority, story_points, estimated_hours, actual_hours, assigned_to) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", + libsql::params![ + story.id, story.initiative_id, story.epic_id, + story.code, story.title, story.description, + story.acceptance_criteria, story.status, story.priority, + story.story_points, story.estimated_hours, + story.actual_hours, story.assigned_to, + ], + ) + .await + .map_err(|e| format!("Insert: {e}"))?; + + Ok(conn.last_insert_rowid()) + } + + #[tauri::command] + pub async fn delete_user_story(state: State<'_, Mutex>, id: i64) -> Result<(), String> { + let db_path = state.lock().map_err(|e| e.to_string())?.clone(); + let conn = get_conn(&db_path).await?; + + conn.execute("DELETE FROM user_stories WHERE id = ?1", libsql::params![id]) + .await + .map_err(|e| format!("Delete: {e}"))?; + + Ok(()) + } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index dc96d15..476ccc7 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -40,6 +40,12 @@ fn main() { commands::save_daily_log, commands::get_performance, commands::save_performance, + commands::get_epics, + commands::save_epic, + commands::delete_epic, + commands::get_user_stories, + commands::save_user_story, + commands::delete_user_story, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/services/tauri-db.ts b/src/services/tauri-db.ts index d846f3a..001c241 100644 --- a/src/services/tauri-db.ts +++ b/src/services/tauri-db.ts @@ -10,6 +10,39 @@ export interface ProjectRecord { end_date: string | null } +export interface EpicRecord { + id: number + initiative_id: number + code: string | null + name: string + description: string | null + status: string | null + client_taker: number | null + stimated_start_date: string | null + stimated_end_date: string | null + start_date: string | null + end_date: string | null + created_at: string | null + updated_at: string | null +} + +export interface UserStoryRecord { + id: number + initiative_id: number + epic_id: number | null + code: string | null + title: string + description: string | null + acceptance_criteria: string | null + status: string | null + priority: string | null + story_points: number | null + estimated_hours: number | null + actual_hours: number | null + assigned_to: number | null + created_at: string | null +} + export interface WorkItemRecord { id: number project_id: number @@ -113,6 +146,28 @@ export const tauriDb = { return invoke('delete_work_item', { id }) }, + // Epics + getEpics(initiativeId: number): Promise { + return invoke('get_epics', { initiativeId }) + }, + saveEpic(epic: EpicRecord): Promise { + return invoke('save_epic', { epic }) + }, + deleteEpic(id: number): Promise { + return invoke('delete_epic', { id }) + }, + + // User Stories + getUserStories(initiativeId: number, epicId?: number): Promise { + return invoke('get_user_stories', { initiativeId, epicId: epicId ?? null }) + }, + saveUserStory(story: UserStoryRecord): Promise { + return invoke('save_user_story', { story }) + }, + deleteUserStory(id: number): Promise { + return invoke('delete_user_story', { id }) + }, + // Users getUsers(): Promise { return invoke('get_users')