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