diff --git a/src/services/db.ts b/src/services/db.ts index 3194c84..d8a76ff 100644 --- a/src/services/db.ts +++ b/src/services/db.ts @@ -28,37 +28,53 @@ export interface SessionRecord { export interface SessionSummaryRecord { sessionId: number summary: string - objectives: string // JSON array - tasks: string // JSON array - commitments: string // JSON array - decisions: string // JSON array - keyPoints: string // JSON array + objectives: string + tasks: string + commitments: string + decisions: string + keyPoints: string modelUsed: string } export interface ProjectStateRecord { projectId: number summary: string - objectives: string // JSON array (unificado) - tasks: string // JSON array (consolidado) - commitments: string // JSON array (consolidado) + objectives: string + tasks: string + commitments: string updatedAt: string } +export interface HuDraftRecord { + id: string + projectId: number + title: string + description: string + acceptanceCriteria: string + priority: string + type: string + sourceSessionId?: number + syncStatus: 'draft' | 'pushing' | 'pushed' + kappaId?: number + createdAt: string +} + const db = new Dexie('alpha-core') as Dexie & { settings: Dexie.Table project_docs: Dexie.Table sessions: Dexie.Table session_summaries: Dexie.Table project_state: Dexie.Table + hu_drafts: Dexie.Table } -db.version(3).stores({ +db.version(4).stores({ settings: '&key', project_docs: '&projectId, projectName, updatedAt', sessions: '++id, projectId, date', session_summaries: '&sessionId', project_state: '&projectId', + hu_drafts: '&id, projectId, syncStatus', }) export default db diff --git a/src/services/hu-drafts-db.ts b/src/services/hu-drafts-db.ts new file mode 100644 index 0000000..b8eaa71 --- /dev/null +++ b/src/services/hu-drafts-db.ts @@ -0,0 +1,26 @@ +import db, { type HuDraftRecord } from '@/services/db' +export type { HuDraftRecord } + +export async function getDrafts(projectId: number): Promise { + return db.hu_drafts.where('projectId').equals(projectId).toArray() +} + +export async function getDraft(id: string): Promise { + return db.hu_drafts.get(id) +} + +export async function saveDraft(d: HuDraftRecord) { + await db.hu_drafts.put(d) +} + +export async function deleteDraft(id: string) { + await db.hu_drafts.delete(id) +} + +export async function getUnpushedDrafts(projectId: number): Promise { + return db.hu_drafts.where({ projectId, syncStatus: 'draft' }).toArray() +} + +export function createDraftId(): string { + return crypto.randomUUID() +} diff --git a/src/services/project-analyzer.ts b/src/services/project-analyzer.ts index 1ac6eea..ac32145 100644 --- a/src/services/project-analyzer.ts +++ b/src/services/project-analyzer.ts @@ -1,9 +1,9 @@ import { callAI } from '@/services/ai' import { getSessionsByProject, getSessionSummary, getProjectState } from '@/services/transcriptions-db' -import { kappa } from '@/services/kappa-api' +import { saveDraft, createDraftId } from '@/services/hu-drafts-db' import type { EnrichedUserStory } from '@/stores/workitems' -interface AnalysisHU { +export interface AnalysisHU { title: string description: string acceptance_criteria: string[] @@ -109,51 +109,43 @@ export async function analyzeProject( } /** - * Crea las HUs propuestas en KAPPA (solo las que no existen). - * Devuelve cuántas se crearon. + * Guarda las HUs propuestas como borradores en la BD local. + * No crea nada en KAPPA — el usuario revisa y envía desde HuDrafts. */ -export async function createMissingHUs( +export async function saveAsDrafts( projectId: number, analysis: AnalysisResult, existingHUs: EnrichedUserStory[], -): Promise<{ created: number; skipped: number; errors: string[] }> { - let created = 0 + sourceSessionId?: number, +): Promise<{ saved: number; skipped: number }> { + let saved = 0 let skipped = 0 - const errors: string[] = [] - - const existingTitles = new Set(existingHUs.map(h => (h._cleanTitle || h.title).toLowerCase().trim())) for (const hu of analysis.hus) { const normalizedTitle = hu.title.toLowerCase().trim() - - // Verificar duplicado por título similar const isDuplicate = existingHUs.some(ex => { const et = (ex._cleanTitle || ex.title).toLowerCase().trim() return et === normalizedTitle || et.includes(normalizedTitle) || normalizedTitle.includes(et) }) - if (isDuplicate) { - skipped++ - continue - } + if (isDuplicate) { skipped++; continue } - try { - await kappa.createUserStory({ - title: hu.title, - description: hu.description + (hu.acceptance_criteria.length > 0 - ? `\n\n**Criterios de aceptación:**\n${hu.acceptance_criteria.map(c => `- ${c}`).join('\n')}` - : ''), - initiative: projectId, - priority: hu.priority || 'Media', - status: 'todo', - }) - created++ - } catch (e: any) { - errors.push(`${hu.title}: ${e.message}`) - } + await saveDraft({ + id: createDraftId(), + projectId, + title: hu.title, + description: hu.description, + acceptanceCriteria: hu.acceptance_criteria.join('\n'), + priority: hu.priority, + type: 'U', + sourceSessionId, + syncStatus: 'draft', + createdAt: new Date().toISOString(), + }) + saved++ } - return { created, skipped, errors } + return { saved, skipped } } function extractJSON(text: string): string { diff --git a/src/views/DashboardView.vue b/src/views/DashboardView.vue index fe1a70a..eaee303 100644 --- a/src/views/DashboardView.vue +++ b/src/views/DashboardView.vue @@ -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([]) +const pushingDraftId = ref(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 ? `

${d.description.replace(/\n/g, '

')}

` : '', + criterios_aceptacion: d.acceptanceCriteria ? `

${d.acceptanceCriteria.replace(/\n/g, '

')}

` : '', + 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) => {

{{ analysisSummary }}

- - {{ analysisResult.created }} HUs creadas + + {{ analysisResult.saved }} borradores guardados {{ analysisResult.skipped }} duplicadas saltadas - + Todo ya está cubierto. No se requieren nuevas HUs.
-
-

⚠ {{ e }}

-
@@ -311,7 +375,41 @@ const statusLabel = (status: unknown) => { - + + + + {{ drafts.length }} borradores pendientes + + +
+
+

{{ d.title }}

+

{{ d.description }}

+
+ {{ d.priority }} + + {{ d.syncStatus === 'pushing' ? 'Enviando...' : d.syncStatus === 'pushed' ? 'Enviada' : 'Borrador' }} + +
+
+
+ + +
+
+
+
+ +