project-analyzer: propone épicas + vincula HUs

- AnalysisEpic: name, description, linkedHuTitles, estimated dates
- saveAsDrafts: guarda épicas como drafts tipo 'E' con metadata JSON
- HuDraftRecord: +metadata field (JSON string)
- DashboardView: push épica busca HUs vinculadas con kappaId
- DashboardView: push HU guarda kappaId del response, no elimina draft
- UI: badge tipo (Epica/HU), HUs vinculadas visibles, estado Enviada con ID
This commit is contained in:
2026-05-28 14:38:39 -05:00
parent 63804a2cb6
commit 13683ec2c4
3 changed files with 127 additions and 77 deletions
+72 -32
View File
@@ -32,6 +32,12 @@ const emit = defineEmits<{
}>()
// ─── Drafts ──────────────────────────────────────────────
function getEpicLinkedHUs(d: HuDraftRecord): string[] {
try {
const meta = JSON.parse(d.metadata || '{}')
return meta.linkedHuTitles || []
} catch { return [] }
}
const drafts = ref<HuDraftRecord[]>([])
const pushingDraftId = ref<string | null>(null)
@@ -46,36 +52,59 @@ async function pushDraft(d: HuDraftRecord) {
await dbSaveDraft(d)
try {
const token = localStorage.getItem('kappa_token')
const res = await fetch('/api/userstorys/create/', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...(token ? { 'Authorization': `Bearer ${token}` } : {}) },
body: JSON.stringify({
initiative: String(d.projectId),
title: d.title,
description: d.description ? `<p>${d.description.replace(/\n/g, '</p><p>')}</p>` : '',
criterios_aceptacion: d.acceptanceCriteria ? `<p>${d.acceptanceCriteria.replace(/\n/g, '</p><p>')}</p>` : '',
story_points: '',
priority: d.priority === 'Alta' ? '1' : d.priority === 'Baja' ? '3' : '2',
sprint: '',
asignado_a: [],
client_taker: null,
characterization_hu: '',
has_impairment: false,
epic_development: null,
feature: '',
initial_date: null,
end_date: null,
}),
})
if (res.ok) {
await deleteDraft(d.id)
const headers = { 'Content-Type': 'application/json', ...(token ? { 'Authorization': `Bearer ${token}` } : {}) }
if (d.type === 'E') {
// Push épica: buscar HUs vinculadas ya enviadas
const meta = JSON.parse(d.metadata || '{}')
const linkedHuIds: number[] = []
for (const huTitle of meta.linkedHuTitles || []) {
const pushedHu = drafts.value.find(x => x.type !== 'E' && (x.title.toLowerCase().trim() === huTitle.toLowerCase().trim()) && x.kappaId)
if (pushedHu?.kappaId) linkedHuIds.push(pushedHu.kappaId)
}
const res = await fetch('/api/epicdevelopment/create/', {
method: 'POST', headers,
body: JSON.stringify({
initiative: String(d.projectId), name: d.title,
description: d.description ? `<p>${d.description.replace(/\n/g, '</p><p>')}</p>` : '',
client_taker: null, hu: linkedHuIds,
stimated_start_date: meta.estimatedStart || null,
stimated_end_date: meta.estimatedEnd || null,
}),
})
if (res.ok) {
d.syncStatus = 'pushed'; await dbSaveDraft(d)
} else {
d.syncStatus = 'draft'; await dbSaveDraft(d)
}
} else {
d.syncStatus = 'draft'
await dbSaveDraft(d)
// Push HU individual
const res = await fetch('/api/userstorys/create/', {
method: 'POST', headers,
body: JSON.stringify({
initiative: String(d.projectId), title: d.title,
description: d.description ? `<p>${d.description.replace(/\n/g, '</p><p>')}</p>` : '',
criterios_aceptacion: d.acceptanceCriteria ? `<p>${d.acceptanceCriteria.replace(/\n/g, '</p><p>')}</p>` : '',
story_points: '',
priority: d.priority === 'Alta' ? '1' : d.priority === 'Baja' ? '3' : '2',
sprint: '', asignado_a: [], client_taker: null,
characterization_hu: '', has_impairment: false,
epic_development: null, feature: '',
initial_date: null, end_date: null,
}),
})
if (res.ok) {
const created = await res.json()
d.kappaId = created.id || undefined
d.syncStatus = 'pushed'
await dbSaveDraft(d)
} else {
d.syncStatus = 'draft'; await dbSaveDraft(d)
}
}
} catch {
d.syncStatus = 'draft'
await dbSaveDraft(d)
d.syncStatus = 'draft'; await dbSaveDraft(d)
} finally {
pushingDraftId.value = null
await loadDrafts()
@@ -383,17 +412,28 @@ const statusLabel = (status: unknown) => {
<CardContent class="space-y-2">
<div v-for="d in drafts" :key="d.id" class="flex items-start gap-3 p-2 rounded-lg border text-sm">
<div class="flex-1 min-w-0">
<p class="font-medium">{{ d.title }}</p>
<p v-if="d.description" class="text-xs text-muted-foreground truncate">{{ d.description }}</p>
<div class="flex items-center gap-2">
<Badge variant="outline" class="text-[10px]" :class="d.type === 'E' ? 'border-purple-300 text-purple-600' : ''">
{{ d.type === 'E' ? 'Épica' : 'HU' }}
</Badge>
<p class="font-medium">{{ d.title }}</p>
</div>
<div v-if="d.type === 'E' && d.metadata" class="text-xs text-muted-foreground mt-0.5">
<span v-if="getEpicLinkedHUs(d).length > 0">
HUs vinculadas: {{ getEpicLinkedHUs(d).join(', ') }}
</span>
<span v-else class="italic">Sin HUs vinculadas</span>
</div>
<p v-if="d.description && d.type !== 'E'" class="text-xs text-muted-foreground truncate">{{ d.description }}</p>
<div class="flex items-center gap-2 mt-1">
<Badge variant="outline" class="text-[10px]">{{ d.priority }}</Badge>
<Badge variant="outline" class="text-[10px] font-mono">
{{ d.syncStatus === 'pushing' ? 'Enviando...' : d.syncStatus === 'pushed' ? 'Enviada' : 'Borrador' }}
<Badge variant="outline" class="text-[10px] font-mono" :class="d.syncStatus === 'pushed' ? 'text-green-600 border-green-300' : ''">
{{ d.syncStatus === 'pushing' ? 'Enviando...' : d.syncStatus === 'pushed' ? `Enviada (ID ${d.kappaId || '?'})` : 'Borrador' }}
</Badge>
</div>
</div>
<div class="flex gap-1 shrink-0">
<Button size="sm" variant="outline" class="text-xs h-7"
<Button v-if="d.syncStatus !== 'pushed'" size="sm" variant="outline" class="text-xs h-7"
:disabled="pushingDraftId === d.id"
@click="pushDraft(d)"
>