hu_drafts en Dexie + push individual a KAPPA + project-analyzer

- db.ts v4: tabla hu_drafts (id, projectId, title, description, syncStatus)
- hu-drafts-db.ts: CRUD Dexie para drafts
- project-analyzer.ts: saveAsDrafts() guarda en BD local, no crea en KAPPA
- DashboardView: borradores desde Dexie con boton Enviar individual
- pushDraft: endpoint /api/userstorys/create/ con payload exacto
- pushDraft: elimina draft solo si KAPPA responde ok
This commit is contained in:
2026-05-28 14:35:02 -05:00
parent eb4fae78b3
commit 63804a2cb6
4 changed files with 186 additions and 54 deletions
+112 -14
View File
@@ -4,11 +4,13 @@ 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, Info, AlertTriangle, Plus, Brain, Sparkles, Loader2, CheckCircle2, XCircle } from 'lucide-vue-next'
import { Activity, FileText, Layers, Clock, Info, AlertTriangle, Plus, Brain, Sparkles, Loader2, CheckCircle2, XCircle, Send } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import HuDrafts from '@/components/HuDrafts.vue'
import AiProjectChat from '@/components/AiProjectChat.vue'
import { analyzeProject, createMissingHUs } from '@/services/project-analyzer'
import { analyzeProject, saveAsDrafts } from '@/services/project-analyzer'
import { getDrafts, deleteDraft, type HuDraftRecord } from '@/services/hu-drafts-db'
import { kappa } from '@/services/kappa-api'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton'
@@ -29,9 +31,73 @@ const emit = defineEmits<{
'navigate-settings': []
}>()
// ─── Drafts ──────────────────────────────────────────────
const drafts = ref<HuDraftRecord[]>([])
const pushingDraftId = ref<string | null>(null)
async function loadDrafts() {
if (!project.value) return
drafts.value = await getDrafts(project.value.id)
}
async function pushDraft(d: HuDraftRecord) {
pushingDraftId.value = d.id
d.syncStatus = 'pushing'
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)
} else {
d.syncStatus = 'draft'
await dbSaveDraft(d)
}
} catch {
d.syncStatus = 'draft'
await dbSaveDraft(d)
} finally {
pushingDraftId.value = null
await loadDrafts()
await workItems.fetchWorkItems(project.value!.id)
}
}
async function dbSaveDraft(d: HuDraftRecord) {
const { saveDraft } = await import('@/services/hu-drafts-db')
await saveDraft(d)
}
watch(() => project.value?.id, () => { if (project.value) loadDrafts() }, { immediate: false })
async function discardDraft(id: string) {
await deleteDraft(id)
await loadDrafts()
}
// ─── Project analysis ────────────────────────────────────
const analyzing = ref(false)
const analysisResult = ref<{ created: number; skipped: number; errors: string[] } | null>(null)
const analysisResult = ref<{ saved: number; skipped: number } | null>(null)
const analysisSummary = ref('')
async function runAnalysis() {
@@ -45,14 +111,15 @@ async function runAnalysis() {
analysisSummary.value = result.summary
if (result.hus.length > 0) {
const outcome = await createMissingHUs(project.value.id, result, workItems.userStories)
const outcome = await saveAsDrafts(project.value.id, result, workItems.userStories)
analysisResult.value = outcome
await workItems.fetchWorkItems(project.value.id)
} else {
analysisResult.value = { created: 0, skipped: 0, errors: [] }
analysisResult.value = { saved: 0, skipped: 0 }
}
} catch (e: any) {
analysisResult.value = { created: 0, skipped: 0, errors: [e.message] }
console.error('[Alpha] Analysis error:', e)
analysisSummary.value = `Error: ${e.message}`
analysisResult.value = { saved: 0, skipped: 0 }
} finally {
analyzing.value = false
}
@@ -179,19 +246,16 @@ const statusLabel = (status: unknown) => {
<CardContent v-if="analysisResult" class="space-y-2 text-sm">
<p class="text-muted-foreground">{{ analysisSummary }}</p>
<div class="flex items-center gap-3 text-xs">
<span v-if="analysisResult.created > 0" class="text-green-600 dark:text-green-400 flex items-center gap-1">
<CheckCircle2 class="size-3" /> {{ analysisResult.created }} HUs creadas
<span v-if="analysisResult.saved > 0" class="text-green-600 dark:text-green-400 flex items-center gap-1">
<CheckCircle2 class="size-3" /> {{ analysisResult.saved }} borradores guardados
</span>
<span v-if="analysisResult.skipped > 0" class="text-amber-600 dark:text-amber-400 flex items-center gap-1">
{{ analysisResult.skipped }} duplicadas saltadas
</span>
<span v-if="analysisResult.created === 0 && analysisResult.skipped === 0 && analysisResult.errors.length === 0" class="text-muted-foreground">
<span v-if="analysisResult.saved === 0 && analysisResult.skipped === 0" class="text-muted-foreground">
Todo ya está cubierto. No se requieren nuevas HUs.
</span>
</div>
<div v-if="analysisResult.errors.length > 0" class="text-xs text-destructive space-y-0.5">
<p v-for="e in analysisResult.errors" :key="e"> {{ e }}</p>
</div>
</CardContent>
</Card>
@@ -311,7 +375,41 @@ const statusLabel = (status: unknown) => {
</CardContent>
</Card>
<!-- Borradores -->
<!-- Borradores (web) -->
<Card v-if="drafts.length > 0" id="dashboard-drafts" class="border-dashed">
<CardHeader class="pb-3 flex flex-row items-center justify-between">
<CardTitle class="text-sm font-medium">{{ drafts.length }} borradores pendientes</CardTitle>
</CardHeader>
<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 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>
</div>
</div>
<div class="flex gap-1 shrink-0">
<Button size="sm" variant="outline" class="text-xs h-7"
:disabled="pushingDraftId === d.id"
@click="pushDraft(d)"
>
<Loader2 v-if="pushingDraftId === d.id" class="size-3 mr-1 animate-spin" />
<Send v-else class="size-3 mr-1" />
Enviar
</Button>
<Button size="sm" variant="ghost" class="text-xs h-7"
@click="discardDraft(d.id)"
></Button>
</div>
</div>
</CardContent>
</Card>
<!-- Borradores (Tauri) -->
<HuDrafts v-if="project" :initiative-id="project.id" />
</div>