diff --git a/bun.lock b/bun.lock index c3ed33c..cdba074 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "": { "name": "kappa-hub", "dependencies": { + "@internationalized/date": "^3.12.2", "@lucide/vue": "^1.16.0", "@tabler/icons-vue": "^3.44.0", "@tailwindcss/vite": "^4.3.0", @@ -243,7 +244,7 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], - "@internationalized/date": ["@internationalized/date@3.12.1", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ=="], + "@internationalized/date": ["@internationalized/date@3.12.2", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw=="], "@internationalized/number": ["@internationalized/number@3.6.6", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-iFgmQaXHE0vytNfpLZWOC2mEJCBRzcUxt53Xf/yCXG93lRvqas237i3r7X4RKMwO3txiyZD4mQjKAByFv6UGSQ=="], @@ -1309,6 +1310,8 @@ "recast-x/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "reka-ui/@internationalized/date": ["@internationalized/date@3.12.1", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ=="], + "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], "restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], diff --git a/package.json b/package.json index 69090f2..512328c 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "tauri": "npx tauri" }, "dependencies": { + "@internationalized/date": "^3.12.2", "@lucide/vue": "^1.16.0", "@tabler/icons-vue": "^3.44.0", "@tailwindcss/vite": "^4.3.0", diff --git a/src/components/HuDrafts.vue b/src/components/HuDrafts.vue index 95f1fe9..9fc9577 100644 --- a/src/components/HuDrafts.vue +++ b/src/components/HuDrafts.vue @@ -10,7 +10,9 @@ import { Input } from '@/components/ui/input' import { Badge } from '@/components/ui/badge' import { Skeleton } from '@/components/ui/skeleton' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' -import { Plus, Send, Pencil, CheckCircle2, Clock } from 'lucide-vue-next' +import { Popover } from '@/components/ui/popover' +import { Calendar } from '@/components/ui/calendar' +import { Plus, Send, Pencil, CheckCircle2, Clock, Calendar as CalendarIcon } from 'lucide-vue-next' const { t } = useI18n() @@ -136,6 +138,23 @@ async function pushAllToKappa() { } } +function parseDate(s: string): Date | undefined { + if (!s) return undefined + const d = new Date(s + 'T00:00:00') + return isNaN(d.getTime()) ? undefined : d +} + +function formatDate(d: Date | undefined): string { + if (!d) return '' + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` +} + +function updateEpicDate(value: string | number | null, idx: 0 | 1) { + const parts = (form.value.code || '').split('|') + parts[idx] = String(value ?? '') + form.value.code = parts.join('|') || null +} + onMounted(loadDrafts) @@ -154,24 +173,47 @@ onMounted(loadDrafts) -
- - -
- -
- - -
+ + + + +
diff --git a/src/components/ui/calendar/Calendar.vue b/src/components/ui/calendar/Calendar.vue new file mode 100644 index 0000000..755a431 --- /dev/null +++ b/src/components/ui/calendar/Calendar.vue @@ -0,0 +1,71 @@ + + + diff --git a/src/components/ui/calendar/index.ts b/src/components/ui/calendar/index.ts new file mode 100644 index 0000000..50f2111 --- /dev/null +++ b/src/components/ui/calendar/index.ts @@ -0,0 +1 @@ +export { default as Calendar } from './Calendar.vue' diff --git a/src/components/ui/popover/Popover.vue b/src/components/ui/popover/Popover.vue new file mode 100644 index 0000000..52619bc --- /dev/null +++ b/src/components/ui/popover/Popover.vue @@ -0,0 +1,23 @@ + + + diff --git a/src/components/ui/popover/index.ts b/src/components/ui/popover/index.ts new file mode 100644 index 0000000..2188e67 --- /dev/null +++ b/src/components/ui/popover/index.ts @@ -0,0 +1 @@ +export { default as Popover } from './Popover.vue' diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 6b43ab8..5207623 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -320,9 +320,9 @@ }, "hierarchy": { "epic": "Epic", - "hu": "HU", "feature": "Feature", "task": "Task", + "hu": "HU", "bug": "Bug" }, "workitems": { diff --git a/src/services/project-analyzer.ts b/src/services/project-analyzer.ts index c8b7b68..92db257 100644 --- a/src/services/project-analyzer.ts +++ b/src/services/project-analyzer.ts @@ -1,6 +1,7 @@ import { callAI } from '@/services/ai' import { getSessionsByProject, getSessionSummary, getProjectState } from '@/services/transcriptions-db' import { saveDraft, createDraftId } from '@/services/hu-drafts-db' +import { generateAndSavePlan } from '@/services/qa-analyzer' import type { EnrichedUserStory } from '@/stores/workitems' export interface AnalysisHU { @@ -129,12 +130,16 @@ export async function saveAsDrafts( }) if (isDuplicate) { skipped++; continue } + const draftId = createDraftId() await saveDraft({ - id: createDraftId(), projectId, title: hu.title, + id: draftId, projectId, title: hu.title, description: hu.description, acceptanceCriteria: hu.acceptance_criteria.join('\n'), priority: hu.priority, type: 'U', metadata: '{}', sourceSessionId, syncStatus: 'draft', createdAt: new Date().toISOString(), }) + // QA plan: fire-and-forget para no bloquear el guardado + generateAndSavePlan(projectId, draftId, hu.title, hu.description, hu.acceptance_criteria.join('\n')) + .catch(e => console.error(`[Alpha] QA auto-gen failed for ${hu.title}:`, e)) saved++ } diff --git a/src/views/DashboardView.vue b/src/views/DashboardView.vue index 414b628..bca73e4 100644 --- a/src/views/DashboardView.vue +++ b/src/views/DashboardView.vue @@ -108,10 +108,6 @@ 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) } @@ -136,10 +132,6 @@ 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) } @@ -168,17 +160,26 @@ async function discardDraft(id: string) { // ─── Project analysis ──────────────────────────────────── const analyzing = ref(false) +const analysisAbort = ref(null) const analysisResult = ref<{ saved: number; skipped: number } | null>(null) const analysisSummary = ref('') +function cancelAnalysis() { + analysisAbort.value?.abort() + analyzing.value = false + analysisAbort.value = null + analysisSummary.value = 'Análisis cancelado' +} + async function runAnalysis() { if (!project.value) return analyzing.value = true + analysisAbort.value = new AbortController() analysisResult.value = null analysisSummary.value = '' try { - const result = await analyzeProject(project.value.id, project.value.name || '', workItems.userStories) + const result = await analyzeProject(project.value.id, project.value.name || '', workItems.userStories, analysisAbort.value?.signal) analysisSummary.value = result.summary if (result.hus.length > 0) { @@ -188,8 +189,12 @@ async function runAnalysis() { analysisResult.value = { saved: 0, skipped: 0 } } } catch (e: any) { - console.error('[Alpha] Analysis error:', e) - analysisSummary.value = `Error: ${e.message}` + if (e.name === 'AbortError' || e.message?.includes('aborted')) { + analysisSummary.value = 'Análisis cancelado' + } else { + console.error('[Alpha] Analysis error:', e) + analysisSummary.value = `Error: ${e.message}` + } analysisResult.value = { saved: 0, skipped: 0 } } finally { analyzing.value = false @@ -313,15 +318,25 @@ const statusLabel = (status: unknown) => { Análisis completo del proyecto - +
+ + +

{{ analysisSummary }}