From 278d2bf07525e95abebb2cc809b2320961dd0642 Mon Sep 17 00:00:00 2001 From: Ricardo Gonzalez Date: Wed, 27 May 2026 23:03:14 -0500 Subject: [PATCH] agregar sprint, has_impairment, tabla impairments + sync pendings KAPPA --- src-tauri/src/db.rs | 151 +++++++++++++++++++++++++----------- src-tauri/src/main.rs | 3 + src/services/kappa-api.ts | 10 +++ src/services/tauri-db.ts | 29 +++++++ src/stores/workitems.ts | 42 +++++++++- src/types/kappa.ts | 30 +++++++ src/views/DashboardView.vue | 7 +- 7 files changed, 220 insertions(+), 52 deletions(-) diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index d369111..35245fb 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -147,12 +147,28 @@ pub struct UserStory { pub estimated_hours: Option, pub actual_hours: Option, pub assigned_to: Option, + pub sprint: Option, + pub has_impairment: bool, pub created_at: Option, pub item_type: Option, pub hierarchy_path: Option, pub parent_code: Option, } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Impairment { + pub id: i64, + pub hu_id: i64, + pub responsible: Option, + pub pending_activity: Option, + pub pending_type: Option, + pub type_impediment: bool, + pub delivery_date: Option, + pub status: bool, + pub created_at: Option, + pub updated_at: Option, +} + async fn get_conn(db_path: &str) -> Result { let db = libsql::Builder::new_local(db_path) .build() @@ -271,45 +287,18 @@ async fn get_conn(db_path: &str) -> Result { 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, - item_type TEXT DEFAULT 'E', - hierarchy_path 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, - item_type TEXT DEFAULT 'U', - hierarchy_path TEXT, - parent_code TEXT, - created_at TEXT DEFAULT (datetime('now')), - FOREIGN KEY (initiative_id) REFERENCES projects(id), - FOREIGN KEY (epic_id) REFERENCES epics(id) + CREATE TABLE IF NOT EXISTS impairments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + hu_id INTEGER NOT NULL, + responsible TEXT, + pending_activity TEXT, + pending_type TEXT, + type_impediment INTEGER DEFAULT 0, + delivery_date TEXT, + status INTEGER DEFAULT 0, + created_at TEXT, + updated_at TEXT, + FOREIGN KEY (hu_id) REFERENCES user_stories(id) );" ) .await @@ -331,6 +320,8 @@ async fn get_conn(db_path: &str) -> Result { "ALTER TABLE work_items ADD COLUMN estimated_hours REAL", "ALTER TABLE work_items ADD COLUMN actual_hours REAL", "ALTER TABLE work_items ADD COLUMN assigned_to INTEGER", + "ALTER TABLE user_stories ADD COLUMN sprint TEXT", + "ALTER TABLE user_stories ADD COLUMN has_impairment INTEGER DEFAULT 0", ] { let _ = conn.execute(alter, ()).await; } @@ -962,9 +953,9 @@ pub mod commands { 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" + "SELECT id, initiative_id, epic_id, code, title, description, acceptance_criteria, status, priority, story_points, estimated_hours, actual_hours, assigned_to, sprint, has_impairment, item_type, hierarchy_path, parent_code, 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" + "SELECT id, initiative_id, epic_id, code, title, description, acceptance_criteria, status, priority, story_points, estimated_hours, actual_hours, assigned_to, sprint, has_impairment, item_type, hierarchy_path, parent_code, created_at FROM user_stories WHERE initiative_id = ?1 ORDER BY created_at DESC" }; let mut rows = if let Some(eid) = epic_id { @@ -989,10 +980,12 @@ pub mod commands { estimated_hours: row.get::(10).ok(), actual_hours: row.get::(11).ok(), assigned_to: row.get::(12).ok(), - created_at: row.get::(13).ok(), - item_type: None, - hierarchy_path: None, - parent_code: None, + sprint: row.get::(13).ok(), + has_impairment: row.get::(14).unwrap_or(0) != 0, + item_type: row.get::(15).ok(), + hierarchy_path: row.get::(16).ok(), + parent_code: row.get::(17).ok(), + created_at: row.get::(18).ok(), }); } Ok(stories) @@ -1004,14 +997,15 @@ pub mod commands { 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, item_type, hierarchy_path, parent_code) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16)", + "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, sprint, has_impairment, item_type, hierarchy_path, parent_code) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18)", 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, + story.sprint, story.has_impairment, story.item_type, story.hierarchy_path, story.parent_code, ], ) @@ -1032,4 +1026,67 @@ pub mod commands { Ok(()) } + + // ──────────────────────────────────────────── + // Impairments + // ──────────────────────────────────────────── + + #[tauri::command] + pub async fn get_impairments(state: State<'_, Mutex>, hu_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, hu_id, responsible, pending_activity, pending_type, type_impediment, delivery_date, status, created_at, updated_at FROM impairments WHERE hu_id = ?1 ORDER BY created_at DESC", libsql::params![hu_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(Impairment { + id: row.get::(0).unwrap_or(0), + hu_id: row.get::(1).unwrap_or(0), + responsible: row.get::(2).ok(), + pending_activity: row.get::(3).ok(), + pending_type: row.get::(4).ok(), + type_impediment: row.get::(5).unwrap_or(0) != 0, + delivery_date: row.get::(6).ok(), + status: row.get::(7).unwrap_or(0) != 0, + created_at: row.get::(8).ok(), + updated_at: row.get::(9).ok(), + }); + } + Ok(items) + } + + #[tauri::command] + pub async fn save_impairment(state: State<'_, Mutex>, imp: Impairment) -> 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 impairments (id, hu_id, responsible, pending_activity, pending_type, type_impediment, delivery_date, status) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + libsql::params![ + imp.id, imp.hu_id, imp.responsible, imp.pending_activity, + imp.pending_type, imp.type_impediment, imp.delivery_date, imp.status, + ], + ) + .await + .map_err(|e| format!("Insert: {e}"))?; + + Ok(conn.last_insert_rowid()) + } + + #[tauri::command] + pub async fn delete_impairment(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 impairments 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 56bc4dc..83aa44b 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -47,6 +47,9 @@ fn main() { commands::get_user_stories, commands::save_user_story, commands::delete_user_story, + commands::get_impairments, + commands::save_impairment, + commands::delete_impairment, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/services/kappa-api.ts b/src/services/kappa-api.ts index 86303e1..1080361 100644 --- a/src/services/kappa-api.ts +++ b/src/services/kappa-api.ts @@ -12,6 +12,8 @@ import type { KappaBusinessRule, KappaRequirement, KappaEmployee, + KappaPending, + KappaTypeImpediment, PaginatedResponse, } from '@/types/kappa' @@ -197,6 +199,14 @@ class KappaAPI { async createRequirement(data: KappaRequirement): Promise { return this.request('POST', '/functionalrequirements/create/', data) } + + async getPendings(huId: number, page = 1): Promise> { + return this.request>('GET', `/pendings/?hu=${huId}&page=${page}`) + } + + async getTypeImpediments(): Promise { + return this.request('GET', '/typeimpediments/') + } } export const kappa = new KappaAPI() diff --git a/src/services/tauri-db.ts b/src/services/tauri-db.ts index 1170de7..468e8d0 100644 --- a/src/services/tauri-db.ts +++ b/src/services/tauri-db.ts @@ -52,9 +52,27 @@ export interface UserStoryRecord { estimated_hours: number | null actual_hours: number | null assigned_to: number | null + sprint: string | null + has_impairment: boolean + item_type: string | null + hierarchy_path: string | null + parent_code: string | null created_at: string | null } +export interface ImpairmentRecord { + id: number + hu_id: number + responsible: string | null + pending_activity: string | null + pending_type: string | null + type_impediment: boolean + delivery_date: string | null + status: boolean + created_at: string | null + updated_at: string | null +} + export interface WorkItemRecord { id: number project_id: number @@ -236,4 +254,15 @@ export const tauriDb = { savePerformance(snap: PerformanceRecord): Promise { return safeInvoke('save_performance', { snap }) }, + + // Impairments + getImpairments(huId: number): Promise { + return safeInvoke('get_impairments', { huId }) + }, + saveImpairment(imp: ImpairmentRecord): Promise { + return safeInvoke('save_impairment', { imp }) + }, + deleteImpairment(id: number): Promise { + return safeInvoke('delete_impairment', { id }) + }, } diff --git a/src/stores/workitems.ts b/src/stores/workitems.ts index 66762b1..c131cfb 100644 --- a/src/stores/workitems.ts +++ b/src/stores/workitems.ts @@ -1,17 +1,18 @@ import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { kappa } from '@/services/kappa-api' -import { tauriDb, type UserStoryRecord, type EpicRecord } from '@/services/tauri-db' +import { tauriDb, type UserStoryRecord, type EpicRecord, type ImpairmentRecord } from '@/services/tauri-db' import { stripHtml } from '@/services/clean-html' import { criteriaToJson, parseQuillList } from '@/services/clean-html' import { parseHierarchy, stripHierarchy, getItemType, type ItemType } from '@/services/hierarchy' -import type { KappaUserStory, KappaLogbookEntry, KappaPlanningEntry, KappaEpicDevelopment } from '@/types/kappa' +import type { KappaUserStory, KappaLogbookEntry, KappaPlanningEntry, KappaEpicDevelopment, KappaPending } from '@/types/kappa' export interface EnrichedUserStory extends KappaUserStory { _itemType: ItemType _hierarchyPath: string | null _cleanTitle: string _criteriaList: string[] + has_impairment: boolean } export interface EnrichedEpic extends KappaEpicDevelopment { @@ -71,6 +72,7 @@ export const useWorkItemsStore = defineStore('workitems', () => { _hierarchyPath: h?.fullPath ?? null, _cleanTitle: h ? stripHierarchy(hu.title || '') : (hu.title || ''), _criteriaList: criteriaList, + has_impairment: false, } } @@ -166,8 +168,19 @@ export const useWorkItemsStore = defineStore('workitems', () => { console.log(`[Alpha] Syncing ${hus.length} HUs to Turso for project ${projectId}`) for (const hu of hus) { try { + const huId = Number(hu.id) || 0 + + // Consultar impedimentos en KAPPA + let hasImpairment = false + let impairments: KappaPending[] = [] + try { + const pendingResp = await kappa.getPendings(huId) + impairments = pendingResp.results + hasImpairment = impairments.some(p => !p.status) + } catch {} + await tauriDb.saveUserStory({ - id: Number(hu.id) || 0, + id: huId, initiative_id: projectId, epic_id: null, code: safeStr(hu.code), @@ -176,12 +189,33 @@ export const useWorkItemsStore = defineStore('workitems', () => { acceptance_criteria: (hu.acceptance_criteria || hu.criterios_aceptacion || null) as string | null, status: safeStr(hu.status), priority: safeStr(hu.priority), - story_points: null, + story_points: hu.story_points ?? null, estimated_hours: null, actual_hours: null, assigned_to: null, + sprint: safeStr(hu.sprint), + has_impairment: hasImpairment, + item_type: null, + hierarchy_path: null, + parent_code: null, created_at: null, }) + + // Guardar impedimentos en Turso + for (const p of impairments) { + await tauriDb.saveImpairment({ + id: p.id, + hu_id: huId, + responsible: p.responsible || null, + pending_activity: p.pending_activity || null, + pending_type: p.type || null, + type_impediment: p.type_impediment, + delivery_date: p.delivery_date || null, + status: p.status, + created_at: p.created_at || null, + updated_at: p.updated_at || null, + }).catch(() => {}) + } } catch (e) { console.error(`[Alpha] Failed to save HU ${hu.id}:`, e) } diff --git a/src/types/kappa.ts b/src/types/kappa.ts index ce4c128..200cc6f 100644 --- a/src/types/kappa.ts +++ b/src/types/kappa.ts @@ -46,6 +46,8 @@ export interface KappaUserStory { status?: string priority?: string initiative: number | string + story_points?: number + sprint?: string created_at?: string } @@ -151,6 +153,34 @@ export interface KappaEmployee { created_at?: string } +export interface KappaPending { + id: number + historia_title: string[] + created_at: string + updated_at: string + responsible: string + pending_activity: string + delivery_date: string + real_date: string | null + type: string + type_impediment: boolean + status_pending: string | null + status: boolean + pending_client_type: string | null + pending_general_client: boolean + solution_date: string | null + which: string + client: string | null + initiative: string | null + hu: number[] + initiatives_client: string[] +} + +export interface KappaTypeImpediment { + id: number + name: string +} + export interface PaginatedResponse { count: number next: string | null diff --git a/src/views/DashboardView.vue b/src/views/DashboardView.vue index ddb0f50..8eacca0 100644 --- a/src/views/DashboardView.vue +++ b/src/views/DashboardView.vue @@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n' import { useProjectsStore } from '@/stores/projects' import { useWorkItemsStore } from '@/stores/workitems' import { getTypeLabel, getTypeColor, getTypeIcon } from '@/services/hierarchy' -import { Activity, FileText, Layers, Clock, Info } from 'lucide-vue-next' +import { Activity, FileText, Layers, Clock, Info, AlertTriangle } from 'lucide-vue-next' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Skeleton } from '@/components/ui/skeleton' @@ -209,6 +209,11 @@ const statusLabel = (status: unknown) => { + {{ hu._cleanTitle || hu.title }}