Revert: stats cards a 5 originales + sidecar Python pospuesto

This commit is contained in:
2026-05-30 01:04:24 -05:00
parent 1f39c4df7a
commit a45893a9bc
+44 -103
View File
@@ -13,7 +13,7 @@ import HuDrafts from '@/components/HuDrafts.vue'
import AiProjectChat from '@/components/AiProjectChat.vue' import AiProjectChat from '@/components/AiProjectChat.vue'
import PrioritizerCard from '@/components/PrioritizerCard.vue' import PrioritizerCard from '@/components/PrioritizerCard.vue'
import SCurveChart from '@/components/SCurveChart.vue' import SCurveChart from '@/components/SCurveChart.vue'
import { generateReportDocx } from '@/services/report-export' // import { generateReportDocx } from '@/services/report-export' // Sidecar Python pending
import { analyzeProjectEpics, analyzeProjectHUs, saveEpicDrafts, saveHUDrafts } from '@/services/project-analyzer' import { analyzeProjectEpics, analyzeProjectHUs, saveEpicDrafts, saveHUDrafts } from '@/services/project-analyzer'
import { getDrafts, deleteDraft, type HuDraftRecord } from '@/services/hu-drafts-db' import { getDrafts, deleteDraft, type HuDraftRecord } from '@/services/hu-drafts-db'
import { kappa } from '@/services/kappa-api' import { kappa } from '@/services/kappa-api'
@@ -411,57 +411,7 @@ async function discardDraft(id: string) {
await loadDrafts() await loadDrafts()
} }
// ─── Export report ────────────────────────────────── // ─── Export report (pending Python sidecar) ────────
async function exportReport() {
if (!project.value) return
showNotif('info', 'Generando informe DOCX...')
try {
const doneCount = workItems.userStories.filter(hu => {
const s = String(hu.status ?? '').toLowerCase()
return ['done', 'completed', 'closed', 'finalizado', '5', '6', '7', 'qa-client', 'ready to deploy'].includes(s)
}).length
const blob = await generateReportDocx({
projectId: project.value.id,
projectName: project.value.name || '',
epicCount: workItems.totalEpics,
huCount: workItems.totalHUs,
inProgressCount: workItems.inProgressHUs,
doneCount,
blockedCount: workItems.userStories.filter(hu => {
const s = String(hu.status ?? '').toLowerCase()
return ['blocked', 'bloqueado'].includes(s)
}).length,
epics: workItems.epics,
hus: workItems.userStories,
totalSessions: workItems.totalSessions,
metrics: {
totalSpPlanned: workItems.userStories.reduce((s, h) => s + (h.story_points || 0), 0),
totalSpCompleted: workItems.userStories.filter(hu => {
const s = String(hu.status ?? '').toLowerCase()
return ['done', 'completed', 'closed', 'finalizado', '5', '6', '7', 'qa-client', 'ready to deploy'].includes(s)
}).reduce((s, h) => s + (h.story_points || 0), 0),
velocityPerWeek: 0,
spi: 1,
estimatedEndDate: '',
clientBlockedHours: 0,
totalBlockedHours: 0,
},
generatedAt: new Date().toLocaleDateString('es-CO', { year: 'numeric', month: 'long', day: 'numeric' }),
})
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `informe_${project.value.name?.replace(/[^a-z0-9]/gi, '_').toLowerCase() || 'proyecto'}.docx`
a.click()
URL.revokeObjectURL(url)
showNotif('success', 'Informe descargado')
} catch (e: any) {
console.error('[Alpha] Export error:', e)
showNotif('error', `Error al generar informe: ${e.message}`)
}
}
// ─── Project analysis — Two-phase ───────────────────────── // ─── Project analysis — Two-phase ─────────────────────────
const phase = ref<'idle' | 'epics' | 'hus' | 'done'>('idle') const phase = ref<'idle' | 'epics' | 'hus' | 'done'>('idle')
@@ -652,56 +602,57 @@ const statusLabel = (status: unknown) => {
</h1> </h1>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Badge v-if="project.key" variant="outline" class="text-xs">{{ project.key }}</Badge> <Badge v-if="project.key" variant="outline" class="text-xs">{{ project.key }}</Badge>
<button <!-- Export button pending Python sidecar
class="ml-auto text-[11px] text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1" <button class="ml-auto text-[11px] text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1" title="Exportar informe DOCX (próximamente)">
@click="exportReport" <FileText class="size-3.5" /> Exportar
title="Exportar informe DOCX" </button> -->
>
<FileText class="size-3.5" />
Exportar
</button>
</div> </div>
</div> </div>
<!-- Stats: 4 cards --> <!-- 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"> <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"> <CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-xs font-medium">{{ t('dashboard.epics') }} · {{ t('dashboard.hus') }} · {{ t('dashboard.inProgress') }}</CardTitle> <CardTitle class="text-sm font-medium">{{ t('dashboard.epics') }}</CardTitle>
<Layers class="size-3.5 text-muted-foreground" /> <Layers class="size-4 text-muted-foreground" />
</CardHeader> </CardHeader>
<CardContent class="flex items-center gap-4"> <CardContent>
<div class="flex flex-col items-center"> <div class="text-2xl font-bold">{{ workItems.totalEpics }}</div>
<span class="text-xl font-bold">{{ workItems.totalEpics }}</span> <p class="text-xs text-muted-foreground">{{ t('dashboard.epicsSubtitle') }}</p>
<span class="text-[10px] text-muted-foreground">{{ t('dashboard.epics') }}</span> </CardContent>
</div> </Card>
<div class="flex flex-col items-center">
<span id="dashboard-stats-hus-count" class="text-xl font-bold">{{ workItems.totalHUs }}</span> <Card id="dashboard-stats-hus" class="bg-gradient-to-t from-primary/5 to-card shadow-xs dark:bg-card">
<span class="text-[10px] text-muted-foreground">HUs</span> <CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
</div> <CardTitle class="text-sm font-medium">{{ t('dashboard.hus') }}</CardTitle>
<div class="flex flex-col items-center"> <FileText class="size-4 text-muted-foreground" />
<span class="text-xl font-bold">{{ workItems.inProgressHUs }}</span> </CardHeader>
<span class="text-[10px] text-muted-foreground">En prog.</span> <CardContent>
</div> <div id="dashboard-stats-hus-count" class="text-2xl font-bold">{{ workItems.totalHUs }}</div>
<div class="flex-1 h-10 flex items-end gap-0.5"> <p class="text-xs text-muted-foreground">{{ t('dashboard.husSubtitle') }}</p>
<div v-for="(val, i) in [workItems.totalEpics, workItems.totalHUs, workItems.inProgressHUs]" :key="i" </CardContent>
class="flex-1 rounded-t-sm" </Card>
:class="['bg-primary/20', 'bg-primary/40', 'bg-primary/60'][i]"
:style="{ height: Math.max(8, (val / Math.max(workItems.totalHUs, 1)) * 100) + '%' }" <Card id="dashboard-stats-inprogress" 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">
</div> <CardTitle class="text-sm font-medium">{{ t('dashboard.inProgress') }}</CardTitle>
<Activity class="size-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ workItems.inProgressHUs }}</div>
<p class="text-xs text-muted-foreground">{{ t('dashboard.activeHus') }}</p>
</CardContent> </CardContent>
</Card> </Card>
<Card id="dashboard-stats-qa" class="bg-gradient-to-t from-primary/5 to-card shadow-xs dark:bg-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"> <CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-xs font-medium">QA</CardTitle> <CardTitle class="text-sm font-medium">QA</CardTitle>
<CheckCircle2 class="size-3.5 text-muted-foreground" /> <CheckCircle2 class="size-4 text-muted-foreground" />
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div class="text-xl font-bold">{{ qaMetrics.total }}</div> <div class="text-2xl font-bold">{{ qaMetrics.total }}</div>
<p class="text-[10px] text-muted-foreground">Planes QA · {{ qaMetrics.totalCases }} casos</p> <p class="text-xs text-muted-foreground">Planes QA · {{ qaMetrics.totalCases }} casos</p>
<div class="flex gap-2 mt-1 text-[10px]"> <div class="flex gap-2 mt-1.5 text-[10px]">
<span class="text-green-600 dark:text-green-400">{{ qaMetrics.auto }} auto</span> <span class="text-green-600 dark:text-green-400">{{ qaMetrics.auto }} auto</span>
<span class="text-amber-600 dark:text-amber-400">{{ qaMetrics.parcial }} parcial</span> <span class="text-amber-600 dark:text-amber-400">{{ qaMetrics.parcial }} parcial</span>
<span class="text-red-600 dark:text-red-400">{{ qaMetrics.manual }} manual</span> <span class="text-red-600 dark:text-red-400">{{ qaMetrics.manual }} manual</span>
@@ -711,22 +662,12 @@ const statusLabel = (status: unknown) => {
<Card id="dashboard-stats-sessions" class="bg-gradient-to-t from-primary/5 to-card shadow-xs dark:bg-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"> <CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-xs font-medium">{{ t('dashboard.sessions') }}</CardTitle> <CardTitle class="text-sm font-medium">{{ t('dashboard.sessions') }}</CardTitle>
<Clock class="size-3.5 text-muted-foreground" /> <Clock class="size-4 text-muted-foreground" />
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div class="text-xl font-bold">{{ workItems.totalSessions }}</div> <div class="text-2xl font-bold">{{ workItems.totalSessions }}</div>
<p class="text-[10px] text-muted-foreground">{{ t('dashboard.sessionsSubtitle') }}</p> <p class="text-xs text-muted-foreground">{{ t('dashboard.sessionsSubtitle') }}</p>
</CardContent>
</Card>
<Card id="dashboard-stats-sc" 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-xs font-medium">Curva S</CardTitle>
<TrendingUp class="size-3.5 text-muted-foreground" />
</CardHeader>
<CardContent class="p-2">
<SCurveChart v-if="project" :project-id="project.id" :compact="true" />
</CardContent> </CardContent>
</Card> </Card>
</div> </div>