agregar sprint, has_impairment, tabla impairments + sync pendings KAPPA

This commit is contained in:
2026-05-27 23:03:14 -05:00
parent 5cc7cf309e
commit 278d2bf075
7 changed files with 220 additions and 52 deletions
+104 -47
View File
@@ -147,12 +147,28 @@ pub struct UserStory {
pub estimated_hours: Option<f64>, pub estimated_hours: Option<f64>,
pub actual_hours: Option<f64>, pub actual_hours: Option<f64>,
pub assigned_to: Option<i64>, pub assigned_to: Option<i64>,
pub sprint: Option<String>,
pub has_impairment: bool,
pub created_at: Option<String>, pub created_at: Option<String>,
pub item_type: Option<String>, pub item_type: Option<String>,
pub hierarchy_path: Option<String>, pub hierarchy_path: Option<String>,
pub parent_code: Option<String>, pub parent_code: Option<String>,
} }
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Impairment {
pub id: i64,
pub hu_id: i64,
pub responsible: Option<String>,
pub pending_activity: Option<String>,
pub pending_type: Option<String>,
pub type_impediment: bool,
pub delivery_date: Option<String>,
pub status: bool,
pub created_at: Option<String>,
pub updated_at: Option<String>,
}
async fn get_conn(db_path: &str) -> Result<libsql::Connection, String> { async fn get_conn(db_path: &str) -> Result<libsql::Connection, String> {
let db = libsql::Builder::new_local(db_path) let db = libsql::Builder::new_local(db_path)
.build() .build()
@@ -271,45 +287,18 @@ async fn get_conn(db_path: &str) -> Result<libsql::Connection, String> {
FOREIGN KEY (user_id) REFERENCES alpha_users(id) FOREIGN KEY (user_id) REFERENCES alpha_users(id)
); );
CREATE TABLE IF NOT EXISTS epics ( CREATE TABLE IF NOT EXISTS impairments (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
initiative_id INTEGER NOT NULL, hu_id INTEGER NOT NULL,
code TEXT, responsible TEXT,
name TEXT NOT NULL, pending_activity TEXT,
description TEXT, pending_type TEXT,
status TEXT DEFAULT 'active', type_impediment INTEGER DEFAULT 0,
client_taker INTEGER, delivery_date TEXT,
stimated_start_date TEXT, status INTEGER DEFAULT 0,
stimated_end_date TEXT, created_at TEXT,
start_date TEXT, updated_at TEXT,
end_date TEXT, FOREIGN KEY (hu_id) REFERENCES user_stories(id)
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)
);" );"
) )
.await .await
@@ -331,6 +320,8 @@ async fn get_conn(db_path: &str) -> Result<libsql::Connection, String> {
"ALTER TABLE work_items ADD COLUMN estimated_hours REAL", "ALTER TABLE work_items ADD COLUMN estimated_hours REAL",
"ALTER TABLE work_items ADD COLUMN actual_hours REAL", "ALTER TABLE work_items ADD COLUMN actual_hours REAL",
"ALTER TABLE work_items ADD COLUMN assigned_to INTEGER", "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; let _ = conn.execute(alter, ()).await;
} }
@@ -962,9 +953,9 @@ pub mod commands {
let conn = get_conn(&db_path).await?; let conn = get_conn(&db_path).await?;
let query = if epic_id.is_some() { 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 { } 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 { let mut rows = if let Some(eid) = epic_id {
@@ -989,10 +980,12 @@ pub mod commands {
estimated_hours: row.get::<f64>(10).ok(), estimated_hours: row.get::<f64>(10).ok(),
actual_hours: row.get::<f64>(11).ok(), actual_hours: row.get::<f64>(11).ok(),
assigned_to: row.get::<i64>(12).ok(), assigned_to: row.get::<i64>(12).ok(),
created_at: row.get::<String>(13).ok(), sprint: row.get::<String>(13).ok(),
item_type: None, has_impairment: row.get::<i64>(14).unwrap_or(0) != 0,
hierarchy_path: None, item_type: row.get::<String>(15).ok(),
parent_code: None, hierarchy_path: row.get::<String>(16).ok(),
parent_code: row.get::<String>(17).ok(),
created_at: row.get::<String>(18).ok(),
}); });
} }
Ok(stories) Ok(stories)
@@ -1004,14 +997,15 @@ pub mod commands {
let conn = get_conn(&db_path).await?; let conn = get_conn(&db_path).await?;
conn.execute( 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) "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)", VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18)",
libsql::params![ libsql::params![
story.id, story.initiative_id, story.epic_id, story.id, story.initiative_id, story.epic_id,
story.code, story.title, story.description, story.code, story.title, story.description,
story.acceptance_criteria, story.status, story.priority, story.acceptance_criteria, story.status, story.priority,
story.story_points, story.estimated_hours, story.story_points, story.estimated_hours,
story.actual_hours, story.assigned_to, story.actual_hours, story.assigned_to,
story.sprint, story.has_impairment,
story.item_type, story.hierarchy_path, story.parent_code, story.item_type, story.hierarchy_path, story.parent_code,
], ],
) )
@@ -1032,4 +1026,67 @@ pub mod commands {
Ok(()) Ok(())
} }
// ────────────────────────────────────────────
// Impairments
// ────────────────────────────────────────────
#[tauri::command]
pub async fn get_impairments(state: State<'_, Mutex<String>>, hu_id: i64) -> Result<Vec<Impairment>, 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::<i64>(0).unwrap_or(0),
hu_id: row.get::<i64>(1).unwrap_or(0),
responsible: row.get::<String>(2).ok(),
pending_activity: row.get::<String>(3).ok(),
pending_type: row.get::<String>(4).ok(),
type_impediment: row.get::<i64>(5).unwrap_or(0) != 0,
delivery_date: row.get::<String>(6).ok(),
status: row.get::<i64>(7).unwrap_or(0) != 0,
created_at: row.get::<String>(8).ok(),
updated_at: row.get::<String>(9).ok(),
});
}
Ok(items)
}
#[tauri::command]
pub async fn save_impairment(state: State<'_, Mutex<String>>, imp: Impairment) -> 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 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<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 impairments WHERE id = ?1", libsql::params![id])
.await
.map_err(|e| format!("Delete: {e}"))?;
Ok(())
}
} }
+3
View File
@@ -47,6 +47,9 @@ fn main() {
commands::get_user_stories, commands::get_user_stories,
commands::save_user_story, commands::save_user_story,
commands::delete_user_story, commands::delete_user_story,
commands::get_impairments,
commands::save_impairment,
commands::delete_impairment,
]) ])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
+10
View File
@@ -12,6 +12,8 @@ import type {
KappaBusinessRule, KappaBusinessRule,
KappaRequirement, KappaRequirement,
KappaEmployee, KappaEmployee,
KappaPending,
KappaTypeImpediment,
PaginatedResponse, PaginatedResponse,
} from '@/types/kappa' } from '@/types/kappa'
@@ -197,6 +199,14 @@ class KappaAPI {
async createRequirement(data: KappaRequirement): Promise<unknown> { async createRequirement(data: KappaRequirement): Promise<unknown> {
return this.request<unknown>('POST', '/functionalrequirements/create/', data) return this.request<unknown>('POST', '/functionalrequirements/create/', data)
} }
async getPendings(huId: number, page = 1): Promise<PaginatedResponse<KappaPending>> {
return this.request<PaginatedResponse<KappaPending>>('GET', `/pendings/?hu=${huId}&page=${page}`)
}
async getTypeImpediments(): Promise<KappaTypeImpediment[]> {
return this.request<KappaTypeImpediment[]>('GET', '/typeimpediments/')
}
} }
export const kappa = new KappaAPI() export const kappa = new KappaAPI()
+29
View File
@@ -52,9 +52,27 @@ export interface UserStoryRecord {
estimated_hours: number | null estimated_hours: number | null
actual_hours: number | null actual_hours: number | null
assigned_to: 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 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 { export interface WorkItemRecord {
id: number id: number
project_id: number project_id: number
@@ -236,4 +254,15 @@ export const tauriDb = {
savePerformance(snap: PerformanceRecord): Promise<number> { savePerformance(snap: PerformanceRecord): Promise<number> {
return safeInvoke('save_performance', { snap }) return safeInvoke('save_performance', { snap })
}, },
// Impairments
getImpairments(huId: number): Promise<ImpairmentRecord[]> {
return safeInvoke('get_impairments', { huId })
},
saveImpairment(imp: ImpairmentRecord): Promise<number> {
return safeInvoke('save_impairment', { imp })
},
deleteImpairment(id: number): Promise<void> {
return safeInvoke('delete_impairment', { id })
},
} }
+38 -4
View File
@@ -1,17 +1,18 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { kappa } from '@/services/kappa-api' 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 { stripHtml } from '@/services/clean-html'
import { criteriaToJson, parseQuillList } from '@/services/clean-html' import { criteriaToJson, parseQuillList } from '@/services/clean-html'
import { parseHierarchy, stripHierarchy, getItemType, type ItemType } from '@/services/hierarchy' 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 { export interface EnrichedUserStory extends KappaUserStory {
_itemType: ItemType _itemType: ItemType
_hierarchyPath: string | null _hierarchyPath: string | null
_cleanTitle: string _cleanTitle: string
_criteriaList: string[] _criteriaList: string[]
has_impairment: boolean
} }
export interface EnrichedEpic extends KappaEpicDevelopment { export interface EnrichedEpic extends KappaEpicDevelopment {
@@ -71,6 +72,7 @@ export const useWorkItemsStore = defineStore('workitems', () => {
_hierarchyPath: h?.fullPath ?? null, _hierarchyPath: h?.fullPath ?? null,
_cleanTitle: h ? stripHierarchy(hu.title || '') : (hu.title || ''), _cleanTitle: h ? stripHierarchy(hu.title || '') : (hu.title || ''),
_criteriaList: criteriaList, _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}`) console.log(`[Alpha] Syncing ${hus.length} HUs to Turso for project ${projectId}`)
for (const hu of hus) { for (const hu of hus) {
try { 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({ await tauriDb.saveUserStory({
id: Number(hu.id) || 0, id: huId,
initiative_id: projectId, initiative_id: projectId,
epic_id: null, epic_id: null,
code: safeStr(hu.code), 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, acceptance_criteria: (hu.acceptance_criteria || hu.criterios_aceptacion || null) as string | null,
status: safeStr(hu.status), status: safeStr(hu.status),
priority: safeStr(hu.priority), priority: safeStr(hu.priority),
story_points: null, story_points: hu.story_points ?? null,
estimated_hours: null, estimated_hours: null,
actual_hours: null, actual_hours: null,
assigned_to: null, assigned_to: null,
sprint: safeStr(hu.sprint),
has_impairment: hasImpairment,
item_type: null,
hierarchy_path: null,
parent_code: null,
created_at: 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) { } catch (e) {
console.error(`[Alpha] Failed to save HU ${hu.id}:`, e) console.error(`[Alpha] Failed to save HU ${hu.id}:`, e)
} }
+30
View File
@@ -46,6 +46,8 @@ export interface KappaUserStory {
status?: string status?: string
priority?: string priority?: string
initiative: number | string initiative: number | string
story_points?: number
sprint?: string
created_at?: string created_at?: string
} }
@@ -151,6 +153,34 @@ export interface KappaEmployee {
created_at?: string 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<T> { export interface PaginatedResponse<T> {
count: number count: number
next: string | null next: string | null
+6 -1
View File
@@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n'
import { useProjectsStore } from '@/stores/projects' import { useProjectsStore } from '@/stores/projects'
import { useWorkItemsStore } from '@/stores/workitems' import { useWorkItemsStore } from '@/stores/workitems'
import { getTypeLabel, getTypeColor, getTypeIcon } from '@/services/hierarchy' 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 { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
@@ -209,6 +209,11 @@ const statusLabel = (status: unknown) => {
</span> </span>
</TableCell> </TableCell>
<TableCell class="text-sm max-w-[280px] truncate flex items-center gap-1"> <TableCell class="text-sm max-w-[280px] truncate flex items-center gap-1">
<AlertTriangle
v-if="hu.has_impairment"
class="size-3.5 text-amber-500 flex-shrink-0"
title="Tiene impedimentos pendientes"
/>
<span class="truncate">{{ hu._cleanTitle || hu.title }}</span> <span class="truncate">{{ hu._cleanTitle || hu.title }}</span>
<span <span
v-if="hu._criteriaList?.length" v-if="hu._criteriaList?.length"