qa-analyzer: plan QA por HU generado por IA + indicadores

- services/qa-analyzer.ts: genera plan QA (tests, automatizable/manual, pasos)
- db.ts v5: tabla qa_plans
- DashboardView: push HU/épica auto-genera QA plan
- DashboardView: stats card QA con conteo de planes
- DashboardView: boton QA por draft + visualización de planes
- project-analyzer: saveAsDrafts actualizado con metadata para épicas
This commit is contained in:
2026-05-28 14:53:11 -05:00
parent 13683ec2c4
commit 292b9844c6
3 changed files with 161 additions and 4 deletions
+62 -3
View File
@@ -11,6 +11,7 @@ import AiProjectChat from '@/components/AiProjectChat.vue'
import { analyzeProject, saveAsDrafts } from '@/services/project-analyzer'
import { getDrafts, deleteDraft, type HuDraftRecord } from '@/services/hu-drafts-db'
import { kappa } from '@/services/kappa-api'
import { generateAndSavePlan, getQAPlans, type HUQAPlan } from '@/services/qa-analyzer'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton'
@@ -31,6 +32,37 @@ const emit = defineEmits<{
'navigate-settings': []
}>()
// ─── QA Plans ───────────────────────────────────────────
const qaPlans = ref<any[]>([])
const generatingQA = ref<string | null>(null)
const expandedQA = ref<string | null>(null)
async function loadQAPlans(projectId: number) {
qaPlans.value = await getQAPlans(projectId)
}
async function generateQA(d: HuDraftRecord) {
generatingQA.value = d.id
try {
await generateAndSavePlan(d.projectId, d.id, d.title, d.description || '', d.acceptanceCriteria)
await loadQAPlans(d.projectId)
} catch (e: any) {
console.error('[Alpha] QA generation error:', e)
} finally {
generatingQA.value = null
}
}
function parseQAPlan(record: any): HUQAPlan | null {
try { return JSON.parse(record.plan) as HUQAPlan } catch { return null }
}
function qaBadgeColor(a: string) {
if (a === 'SÍ') return 'text-green-600 border-green-300'
if (a === 'MANUAL') return 'text-red-600 border-red-300'
return 'text-amber-600 border-amber-300'
}
// ─── Drafts ──────────────────────────────────────────────
function getEpicLinkedHUs(d: HuDraftRecord): string[] {
try {
@@ -75,6 +107,10 @@ async function pushDraft(d: HuDraftRecord) {
})
if (res.ok) {
d.syncStatus = 'pushed'; await dbSaveDraft(d)
// Auto-generar QA plan tras push exitoso de épica
generateAndSavePlan(d.projectId, d.id, d.title, d.description || '', '').catch(e =>
console.error(`[Alpha] QA plan auto-gen failed for epic ${d.title}:`, e)
)
} else {
d.syncStatus = 'draft'; await dbSaveDraft(d)
}
@@ -99,6 +135,10 @@ async function pushDraft(d: HuDraftRecord) {
d.kappaId = created.id || undefined
d.syncStatus = 'pushed'
await dbSaveDraft(d)
// Auto-generar QA plan tras push exitoso
generateAndSavePlan(d.projectId, d.id, d.title, d.description || '', d.acceptanceCriteria).catch(e =>
console.error(`[Alpha] QA plan auto-gen failed for ${d.title}:`, e)
)
} else {
d.syncStatus = 'draft'; await dbSaveDraft(d)
}
@@ -108,6 +148,7 @@ async function pushDraft(d: HuDraftRecord) {
} finally {
pushingDraftId.value = null
await loadDrafts()
await loadQAPlans(project.value!.id)
await workItems.fetchWorkItems(project.value!.id)
}
}
@@ -117,7 +158,7 @@ async function dbSaveDraft(d: HuDraftRecord) {
await saveDraft(d)
}
watch(() => project.value?.id, () => { if (project.value) loadDrafts() }, { immediate: false })
watch(() => project.value?.id, () => { if (project.value) { loadDrafts(); loadQAPlans(project.value.id) } }, { immediate: false })
async function discardDraft(id: string) {
await deleteDraft(id)
@@ -199,7 +240,7 @@ const statusLabel = (status: unknown) => {
</div>
<!-- Stats -->
<div id="dashboard-stats" class="grid gap-3 @xl:grid-cols-2 @3xl:grid-cols-4">
<div id="dashboard-stats" class="grid gap-3 @xl:grid-cols-2 @3xl:grid-cols-5">
<Card id="dashboard-stats-epics" class="bg-gradient-to-t from-primary/5 to-card shadow-xs dark:bg-card">
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">{{ t('dashboard.epics') }}</CardTitle>
@@ -233,6 +274,17 @@ const statusLabel = (status: unknown) => {
</CardContent>
</Card>
<Card id="dashboard-stats-qa" class="bg-gradient-to-t from-primary/5 to-card shadow-xs dark:bg-card">
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">QA</CardTitle>
<CheckCircle2 class="size-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ qaPlans.length }}</div>
<p class="text-xs text-muted-foreground">Planes QA generados</p>
</CardContent>
</Card>
<Card id="dashboard-stats-sessions" class="bg-gradient-to-t from-primary/5 to-card shadow-xs dark:bg-card">
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">{{ t('dashboard.sessions') }}</CardTitle>
@@ -439,7 +491,14 @@ const statusLabel = (status: unknown) => {
>
<Loader2 v-if="pushingDraftId === d.id" class="size-3 mr-1 animate-spin" />
<Send v-else class="size-3 mr-1" />
Enviar
{{ d.type === 'E' ? 'Epica' : 'HU' }}
</Button>
<Button size="sm" variant="outline" class="text-xs h-7"
:disabled="generatingQA === d.id"
@click="generateQA(d)"
>
<Loader2 v-if="generatingQA === d.id" class="size-3 mr-1 animate-spin" />
QA
</Button>
<Button size="sm" variant="ghost" class="text-xs h-7"
@click="discardDraft(d.id)"