import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { kappa } from '@/services/kappa-api' import { storage } from '@/services/storage' 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 { resolveStatusName, seedStatusLookups } from '@/services/statuses-db' 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 _assignedUserId: number | null _assignedName: string _statusName: string _assignedEmployeeId: number | null _epicCode: string | null _itemCode: string | null } export interface EnrichedEpic extends KappaEpicDevelopment { _itemType: ItemType _hierarchyPath: string | null _cleanName: string _epicCode: string | null } export const useWorkItemsStore = defineStore('workitems', () => { const creating = ref(false) const loading = ref(false) const syncing = ref(false) const error = ref(null) const firstVisit = ref>(new Set()) const userStories = ref([]) const epics = ref([]) const logbooks = ref([]) const plannings = ref([]) const totalHUs = computed(() => userStories.value.length) const totalEpics = computed(() => epics.value.length) const inProgressHUs = computed(() => userStories.value.filter(hu => { const s = String(hu.status ?? '').toLowerCase() return ['in_progress', 'doing', 'wip', 'active', 'in progress', 'true'].includes(s) }).length ) const totalSessions = computed(() => logbooks.value.length) 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, _epicCode: h?.epicCode ?? null, } } function parseAssignedUser(hu: any): { id: number | null; name: string; employeeId: number | null } { // 1. asignado_a → EMPLOYEE IDs (ej: [1135]). NO son user_ids. if (hu.asignado_a != null) { if (Array.isArray(hu.asignado_a)) { const first = hu.asignado_a[0] if (first != null) { const employeeId = typeof first === 'object' ? Number(first.id) : Number(first) if (!isNaN(employeeId)) { const name = Array.isArray(hu.asignado_a_names) ? (hu.asignado_a_names[0] ?? '') : (hu.asignado_a_names ?? '') return { id: null, name, employeeId } } } } } // 2. assigned_to con valor negativo → employee_id (convención Turso) if (hu.assigned_to != null && hu.assigned_to !== '') { const raw = Number(hu.assigned_to) if (!isNaN(raw)) { if (raw < 0) return { id: null, name: '', employeeId: Math.abs(raw) } return { id: raw, name: hu.assigned_name || '', employeeId: null } } } return { id: null, name: '', employeeId: null } } function enrichHU(hu: { title?: string; id?: number; description?: string | null; status?: string | number | null; status_name?: string | null; priority?: string | number | null; story_points?: number | null; acceptance_criteria?: string | null; criterios_aceptacion?: string | null; assigned_to?: number | null; asignado_a?: number[] | string[] | null; asignado_a_names?: string[] | string | null; assigned_name?: string }, initiativeId: number): EnrichedUserStory { const h = parseHierarchy(hu.title || '') const rawCriteria = hu.acceptance_criteria || hu.criterios_aceptacion || '' const criteriaList = rawCriteria ? parseQuillList(rawCriteria) : [] const { id: assignedUserId, name: assignedName, employeeId } = parseAssignedUser(hu) return { id: hu.id ?? 0, title: hu.title || '', status: String(hu.status ?? ''), priority: String(hu.priority ?? ''), story_points: hu.story_points ?? undefined, description: hu.description || '', acceptance_criteria: rawCriteria, criterios_aceptacion: rawCriteria, initiative: initiativeId, _itemType: h?.items[h.items.length - 1]?.type || 'U', _hierarchyPath: h?.fullPath ?? null, _cleanTitle: h ? stripHierarchy(hu.title || '') : (hu.title || ''), _criteriaList: criteriaList, has_impairment: false, _assignedUserId: assignedUserId, _assignedName: assignedName, _statusName: hu.status_name || resolveStatusName(hu.status), _assignedEmployeeId: employeeId, _epicCode: h?.epicCode ?? null, _itemCode: h?.itemCode ?? null, } } async function createUserStory(story: KappaUserStory): Promise { creating.value = true error.value = null try { const result = await kappa.createUserStory(story) const enriched = enrichHU(result, Number(result.initiative)) userStories.value.push(enriched) return enriched } catch (e: any) { error.value = e.message return null } finally { creating.value = false } } async function fetchWorkItems(initiativeId?: number) { loading.value = true error.value = null const id = initiativeId || Number(storage.get('kappa_last_project')) if (!id) { loading.value = false return } const isFirstVisit = !firstVisit.value.has(id) if (isFirstVisit) { seedStatusLookups().catch(() => {}) } try { // 1. Cargar desde Turso (instantáneo) if (!isFirstVisit) { const [localEpics, localHUs] = await Promise.all([ tauriDb.getEpics(id).catch(() => [] as EpicRecord[]), tauriDb.getUserStories(id).catch(() => [] as UserStoryRecord[]), ]) epics.value = localEpics.map(e => enrichEpic(e, id)) userStories.value = localHUs.map(hu => enrichHU(hu, id)) } // 2. Consultar KAPPA (siempre, para detectar cambios) syncing.value = true const [stories, epicData] = await Promise.all([ kappa.getAllUserStories(id).catch(() => [] as KappaUserStory[]), kappa.getAllEpicDevelopment(id).catch(() => [] as KappaEpicDevelopment[]), ]) // 3. Merge: detectar nuevos/cambiados const localHUIds = new Set(userStories.value.map(hu => hu.id)) const newHUs = stories.filter(s => !localHUIds.has(s.id)) const localEpicIds = new Set(epics.value.map(e => e.id)) const newEpics = epicData.filter(e => !localEpicIds.has(e.id)) // 4. Actualizar UI con datos frescos de KAPPA userStories.value = stories.map(hu => enrichHU(hu, id)) if (userStories.value.length > 0) { console.log('[Alpha DEBUG] 1ra HU desc:', { id: userStories.value[0].id, title: userStories.value[0].title?.slice(0, 40), hasDescription: !!userStories.value[0].description, descLength: userStories.value[0].description?.length, }) } epics.value = epicData.map(epic => ({ ...epic, description: stripHtml(epic.description || ''), ...enrichEpic(epic, id), })) // 5. Guardar en Turso: insertar/actualizar HUs y épicas if (isFirstVisit || newHUs.length > 0) { await syncHUsToTurso(id, isFirstVisit ? stories : newHUs) } if (isFirstVisit || newEpics.length > 0) { await syncEpicsToTurso(id, isFirstVisit ? epicData : newEpics) } // Actualizar contadores en projects await tauriDb.updateProjectCounts(id).catch(() => {}) firstVisit.value.add(id) } catch (e: any) { error.value = e.message } finally { loading.value = false syncing.value = false } } function safeStr(v: unknown): string | null { if (v === null || v === undefined) return null return String(v) } async function syncHUsToTurso(projectId: number, hus: KappaUserStory[]) { 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 {} const { id: assignedUserId, employeeId } = parseAssignedUser(hu) // Convención: employee_id se guarda como negativo para distinguirlo de user_id const assignedTo = employeeId != null ? -employeeId : assignedUserId await tauriDb.saveUserStory({ id: huId, initiative_id: projectId, epic_id: null, code: safeStr(hu.code), title: String(hu.title || ''), description: stripHtml(String(hu.description || '')), acceptance_criteria: (hu.acceptance_criteria || hu.criterios_aceptacion || null) as string | null, status: safeStr(hu.status), priority: safeStr(hu.priority), story_points: hu.story_points ?? null, estimated_hours: null, actual_hours: null, assigned_to: assignedTo, 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) } } } async function syncEpicsToTurso(projectId: number, epicsData: KappaEpicDevelopment[]) { console.log(`[Alpha] Syncing ${epicsData.length} epics to Turso for project ${projectId}`) for (const epic of epicsData) { try { await tauriDb.saveEpic({ id: Number(epic.id) || 0, initiative_id: projectId, code: safeStr(epic.code), name: String(epic.name || epic.title || `Épica ${epic.id}`), description: safeStr(epic.description), status: safeStr(epic.status), client_taker: null, stimated_start_date: null, stimated_end_date: null, start_date: null, end_date: null, created_at: null, updated_at: null, }) } catch (e) { console.error(`[Alpha] Failed to save epic ${epic.id}:`, e) } } } return { creating, loading, syncing, error, userStories, epics, logbooks, plannings, totalHUs, totalEpics, inProgressHUs, totalSessions, createUserStory, fetchWorkItems, } })