Files
Alpha/src/stores/workitems.ts
T
ricardo 3035351e6f Sistema de códigos jerárquicos 2-niveles + asignación determinista post-IA
- hierarchy.ts: Spike (S) agregado, buildHierarchyPath genera [E01-F04] (2 niveles)
  Legacy [E05-F04-U01] preservado (regex opcional 3er segmento)
- hierarchy-generator.ts (nuevo): analyzeExisting() computa contadores por épica+tipo
  assignEpicCodes() asigna E{max+1} secuencial
  assignItemCodes() asigna {epic}-{tipo}{n+1} a cada HU dentro de su épica
- project-analyzer.ts: post-procesa épicas y HUs con generador de códigos
  saveEpicDrafts usa epicCode en metadata y título con [E01]
- prompts-db.ts: prompt FASE 2 instruye a la IA a no generar códigos
- workitems.ts: EnrichedEpic._epicCode, EnrichedUserStory._epicCode/_itemCode
- DashboardView: muestra códigos en drafts y tabla de épicas
2026-05-29 18:13:17 -05:00

329 lines
12 KiB
TypeScript

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<string | null>(null)
const firstVisit = ref<Set<number>>(new Set())
const userStories = ref<EnrichedUserStory[]>([])
const epics = ref<EnrichedEpic[]>([])
const logbooks = ref<KappaLogbookEntry[]>([])
const plannings = ref<KappaPlanningEntry[]>([])
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<EnrichedUserStory | null> {
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,
}
})