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:
+72
-32
@@ -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)"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user