diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index abb47f0..ba57a21 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -128,6 +128,8 @@ pub struct Epic { pub end_date: Option, pub created_at: Option, pub updated_at: Option, + pub item_type: Option, + pub hierarchy_path: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -146,6 +148,9 @@ pub struct UserStory { pub actual_hours: Option, pub assigned_to: Option, pub created_at: Option, + pub item_type: Option, + pub hierarchy_path: Option, + pub parent_code: Option, } async fn get_conn(db_path: &str) -> Result { @@ -278,6 +283,8 @@ async fn get_conn(db_path: &str) -> Result { 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) @@ -297,6 +304,9 @@ async fn get_conn(db_path: &str) -> Result { 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) diff --git a/src/services/hierarchy.ts b/src/services/hierarchy.ts new file mode 100644 index 0000000..d0addb1 --- /dev/null +++ b/src/services/hierarchy.ts @@ -0,0 +1,115 @@ +export type ItemType = 'E' | 'F' | 'U' | 'T' | 'B' + +export interface HierarchyInfo { + fullPath: string + items: { type: ItemType; number: number }[] + epicCode: string | null + featureCode: string | null + itemCode: string | null +} + +const TYPE_LABELS: Record = { + E: 'Épica', + F: 'Feature', + U: 'HU', + T: 'Tarea', + B: 'Bug', +} + +const TYPE_COLORS: Record = { + E: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400', + F: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400', + U: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400', + T: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400', + B: 'bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-400', +} + +const TYPE_ICONS: Record = { + E: '📦', + F: '⭐', + U: '📋', + T: '⚙️', + B: '🐛', +} + +export function getTypeLabel(type: ItemType): string { + return TYPE_LABELS[type] || type +} + +export function getTypeColor(type: ItemType): string { + return TYPE_COLORS[type] || 'bg-muted text-muted-foreground' +} + +export function getTypeIcon(type: ItemType): string { + return TYPE_ICONS[type] || '📄' +} + +const HIERARCHY_REGEX = /\[(([EFUTB]\d+)(?:-([EFUTB]\d+))?(?:-([EFUTB]\d+))?)\]/i + +export function parseHierarchy(title: string): HierarchyInfo | null { + const match = title.match(HIERARCHY_REGEX) + if (!match) return null + + const fullPath = match[1].toUpperCase() + const segments = [match[2]] + if (match[3]) segments.push(match[3]) + if (match[4]) segments.push(match[4]) + + const items = segments.map(s => ({ + type: s[0] as ItemType, + number: parseInt(s.slice(1), 10), + })) + + const epic = items.find(i => i.type === 'E') + const feature = items.find(i => i.type === 'F') + const item = items[items.length - 1] + + return { + fullPath, + items, + epicCode: epic ? `E${String(epic.number).padStart(2, '0')}` : null, + featureCode: feature ? `F${String(feature.number).padStart(2, '0')}` : null, + itemCode: item ? `${item.type}${String(item.number).padStart(2, '0')}` : null, + } +} + +export function stripHierarchy(title: string): string { + return title.replace(HIERARCHY_REGEX, '').trim() +} + +export function getItemType(title: string): ItemType { + const h = parseHierarchy(title) + if (h?.items.length) return h.items[h.items.length - 1].type + return 'U' +} + +export function buildHierarchyPath( + epicNum?: number, + featureNum?: number, + itemType?: ItemType, + itemNum?: number, +): string { + const parts: string[] = [] + if (epicNum !== undefined) parts.push(`E${String(epicNum).padStart(2, '0')}`) + if (featureNum !== undefined) parts.push(`F${String(featureNum).padStart(2, '0')}`) + if (itemType && itemNum !== undefined) parts.push(`${itemType}${String(itemNum).padStart(2, '0')}`) + return parts.length ? `[${parts.join('-')}]` : '' +} + +export function buildFullTitle( + name: string, + epicNum?: number, + featureNum?: number, + itemType?: ItemType, + itemNum?: number, +): string { + const path = buildHierarchyPath(epicNum, featureNum, itemType, itemNum) + return path ? `${path} ${name}` : name +} + +export function extractItemNumber(title: string, type: ItemType): number | null { + const h = parseHierarchy(title) + if (!h) return null + const found = h.items.find(i => i.type === type) + return found?.number ?? null +} diff --git a/src/stores/workitems.ts b/src/stores/workitems.ts index dbab267..5b8ce52 100644 --- a/src/stores/workitems.ts +++ b/src/stores/workitems.ts @@ -3,8 +3,21 @@ import { ref, computed } from 'vue' import { kappa } from '@/services/kappa-api' import { tauriDb, type UserStoryRecord, type EpicRecord } from '@/services/tauri-db' import { stripHtml } from '@/services/clean-html' +import { parseHierarchy, stripHierarchy, getItemType, type ItemType } from '@/services/hierarchy' import type { KappaUserStory, KappaLogbookEntry, KappaPlanningEntry, KappaEpicDevelopment } from '@/types/kappa' +export interface EnrichedUserStory extends KappaUserStory { + _itemType: ItemType + _hierarchyPath: string | null + _cleanTitle: string +} + +export interface EnrichedEpic extends KappaEpicDevelopment { + _itemType: ItemType + _hierarchyPath: string | null + _cleanName: string +} + export const useWorkItemsStore = defineStore('workitems', () => { const creating = ref(false) const loading = ref(false) @@ -12,8 +25,8 @@ export const useWorkItemsStore = defineStore('workitems', () => { const error = ref(null) const firstVisit = ref>(new Set()) - const userStories = ref([]) - const epics = ref([]) + const userStories = ref([]) + const epics = ref([]) const logbooks = ref([]) const plannings = ref([]) @@ -27,13 +40,41 @@ export const useWorkItemsStore = defineStore('workitems', () => { ) const totalSessions = computed(() => logbooks.value.length) - async function createUserStory(story: KappaUserStory): Promise { + function enrichEpic(e: { name?: string; title?: string; id?: number }, initiativeId: number): EnrichedEpic { + const title = e.name || e.title || '' + const h = parseHierarchy(title) + return { + id: e.id ?? 0, + code: undefined, + name: e.name, + title: e.name || e.title, + initiative: initiativeId, + _itemType: h?.items[h.items.length - 1]?.type || 'E', + _hierarchyPath: h?.fullPath ?? null, + _cleanName: h ? stripHierarchy(title) : title, + } + } + + function enrichHU(hu: { title?: string; id?: number }, initiativeId: number): EnrichedUserStory { + const h = parseHierarchy(hu.title || '') + return { + id: hu.id ?? 0, + title: hu.title || '', + initiative: initiativeId, + _itemType: h?.items[h.items.length - 1]?.type || 'U', + _hierarchyPath: h?.fullPath ?? null, + _cleanTitle: h ? stripHierarchy(hu.title || '') : (hu.title || ''), + } + } + + async function createUserStory(story: KappaUserStory): Promise { creating.value = true error.value = null try { const result = await kappa.createUserStory(story) - userStories.value.push(result) - return result + const enriched = enrichHU(result, Number(result.initiative)) + userStories.value.push(enriched) + return enriched } catch (e: any) { error.value = e.message return null @@ -62,27 +103,9 @@ export const useWorkItemsStore = defineStore('workitems', () => { tauriDb.getUserStories(id).catch(() => [] as UserStoryRecord[]), ]) - epics.value = localEpics.map(e => ({ - id: e.id, - code: e.code || undefined, - name: e.name, - title: e.name, - description: e.description || undefined, - status: e.status || undefined, - client_taker: e.client_taker || undefined, - initiative: id, - })) + epics.value = localEpics.map(e => enrichEpic(e, id)) - userStories.value = localHUs.map(hu => ({ - id: hu.id, - code: hu.code || undefined, - title: hu.title, - description: hu.description || undefined, - acceptance_criteria: hu.acceptance_criteria || undefined, - status: hu.status || undefined, - priority: hu.priority || undefined, - initiative: id, - })) + userStories.value = localHUs.map(hu => enrichHU(hu, id)) } // 2. Consultar KAPPA (siempre, para detectar cambios) @@ -100,11 +123,12 @@ export const useWorkItemsStore = defineStore('workitems', () => { const newEpics = epicData.filter(e => !localEpicIds.has(e.id)) // 4. Actualizar UI con datos frescos de KAPPA - userStories.value = stories + userStories.value = stories.map(hu => enrichHU(hu, id)) epics.value = epicData.map(epic => ({ ...epic, description: stripHtml(epic.description || ''), + ...enrichEpic(epic, id), })) // 5. Guardar en Turso: insertar/actualizar HUs y épicas diff --git a/src/views/DashboardView.vue b/src/views/DashboardView.vue index 6d5df0c..2772aad 100644 --- a/src/views/DashboardView.vue +++ b/src/views/DashboardView.vue @@ -3,6 +3,7 @@ import { computed, watch } from 'vue' 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 } from 'lucide-vue-next' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' @@ -152,10 +153,16 @@ const statusLabel = (status: unknown) => {
+ + {{ getTypeIcon(epic._itemType) }} {{ getTypeLabel(epic._itemType) }} + {{ epic.code || `EP-${epic.id}` }} - {{ epic.name || epic.title || t('dashboard.epicFallback', { id: epic.id }) }} + {{ epic._cleanName || epic.name || epic.title || t('dashboard.epicFallback', { id: epic.id }) }}
{{ statusLabel(epic.status || '') }} @@ -182,7 +189,8 @@ const statusLabel = (status: unknown) => { - {{ t('dashboard.code') }} + {{ t('dashboard.code') }} + {{ t('users.role') }} {{ t('dashboard.title') }} {{ t('dashboard.status') }} {{ t('dashboard.priority') }} @@ -193,8 +201,16 @@ const statusLabel = (status: unknown) => { {{ hu.code || `HU-${hu.id}` }} - - {{ hu.title }} + + + {{ getTypeLabel(hu._itemType) }} + + + + {{ hu._cleanTitle || hu.title }}