agregar componente HuDrafts: borradores UUID + tabla + push a KAPPA + integracion en dashboard

This commit is contained in:
2026-05-27 23:21:03 -05:00
parent 53c6d4325c
commit fd7a171a72
2 changed files with 241 additions and 1 deletions
+236
View File
@@ -0,0 +1,236 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { tauriDb, type HuDraftRecord } from '@/services/tauri-db'
import { kappa } from '@/services/kappa-api'
import { stripHtml, parseQuillList } from '@/services/clean-html'
import { getTypeLabel, getTypeColor, ItemType } from '@/services/hierarchy'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
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'
const { t } = useI18n()
const props = defineProps<{
initiativeId: number
}>()
const drafts = ref<HuDraftRecord[]>([])
const loading = ref(false)
const showForm = ref(false)
const editingId = ref<string | null>(null)
const form = ref<HuDraftRecord>({
id: '',
initiative_id: props.initiativeId,
code: null,
title: '',
description: null,
acceptance_criteria: null,
item_type: 'U',
hierarchy_path: null,
story_points: null,
sprint: null,
assigned_to: null,
sync_status: 'draft',
kappa_id: null,
created_at: null,
updated_at: null,
})
const draftTypes: ItemType[] = ['U', 'F', 'T', 'B']
const itemTypeOptions = computed(() =>
draftTypes.map(type => ({ value: type, label: getTypeLabel(type) }))
)
async function loadDrafts() {
loading.value = true
try {
drafts.value = await tauriDb.getHuDrafts(props.initiativeId)
} catch (e) {
console.error('[Alpha] Failed to load drafts:', e)
} finally {
loading.value = false
}
}
function resetForm() {
form.value = {
id: crypto.randomUUID(),
initiative_id: props.initiativeId,
code: null,
title: '',
description: null,
acceptance_criteria: null,
item_type: 'U',
hierarchy_path: null,
story_points: null,
sprint: null,
assigned_to: null,
sync_status: 'draft',
kappa_id: null,
created_at: null,
updated_at: null,
}
editingId.value = null
showForm.value = true
}
function editDraft(d: HuDraftRecord) {
form.value = { ...d }
editingId.value = d.id
showForm.value = true
}
async function saveDraft() {
if (!form.value.title) return
try {
await tauriDb.saveHuDraft(form.value)
showForm.value = false
await loadDrafts()
} catch (e) {
console.error('[Alpha] Failed to save draft:', e)
}
}
async function deleteDraft(id: string) {
try {
await tauriDb.deleteHuDraft(id)
await loadDrafts()
} catch (e) {
console.error('[Alpha] Failed to delete draft:', e)
}
}
async function pushToKappa(d: HuDraftRecord) {
if (!d.title) return
try {
const result = await kappa.createUserStory({
title: d.hierarchy_path
? `[${d.hierarchy_path}] ${d.title}`
: d.title,
description: d.description || '',
initiative: props.initiativeId,
priority: 'medium',
})
d.sync_status = 'pushed'
d.kappa_id = result.id ?? null
await tauriDb.saveHuDraft(d)
await loadDrafts()
} catch (e) {
console.error('[Alpha] Failed to push draft to KAPPA:', e)
}
}
onMounted(loadDrafts)
</script>
<template>
<div>
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-semibold text-muted-foreground uppercase tracking-wider">
Borradores · {{ drafts.length }}
</h3>
<Button size="sm" @click="resetForm">
<Plus class="size-3.5 mr-1" />
Nuevo
</Button>
</div>
<!-- Form -->
<Card v-if="showForm" class="mb-4">
<CardContent class="p-4 space-y-3">
<div class="grid grid-cols-2 gap-3">
<Select v-model="form.item_type">
<SelectTrigger class="w-full">
<SelectValue :placeholder="t('users.role')" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="opt in itemTypeOptions" :key="opt.value" :value="opt.value">
{{ opt.label }}
</SelectItem>
</SelectContent>
</Select>
<Input :model-value="form.story_points ?? ''" @update:model-value="form.story_points = $event ? Number($event) : null" type="number" placeholder="SP" step="0.5" min="0" />
</div>
<Input v-model="form.title" :placeholder="t('dashboard.title')" />
<div class="grid grid-cols-2 gap-3">
<Input :model-value="form.sprint ?? ''" @update:model-value="form.sprint = String($event ?? '') || null" placeholder="Sprint" />
<Input :model-value="form.assigned_to ?? ''" @update:model-value="form.assigned_to = String($event ?? '') || null" placeholder="Asignado a" />
</div>
<div class="flex gap-2">
<Button size="sm" @click="saveDraft">Guardar</Button>
<Button size="sm" variant="outline" @click="showForm = false">Cancelar</Button>
</div>
</CardContent>
</Card>
<!-- Loading -->
<Skeleton v-if="loading" class="h-20 w-full" />
<!-- Drafts list -->
<div v-else-if="drafts.length === 0" class="text-center py-6 text-xs text-muted-foreground/50 italic">
Sin borradores. Creá uno para refinar antes de enviar a KAPPA.
</div>
<div v-else class="space-y-2">
<Card v-for="d in drafts" :key="d.id" class="bg-gradient-to-t from-primary/5 to-card shadow-xs dark:bg-card">
<CardContent class="p-3">
<div class="flex items-start justify-between gap-2">
<div class="flex items-center gap-2 min-w-0">
<span
class="inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-bold"
:class="getTypeColor((d.item_type || 'U') as ItemType)"
>
{{ getTypeLabel((d.item_type || 'U') as ItemType) }}
</span>
<span class="text-sm truncate font-medium">{{ d.title }}</span>
</div>
<div class="flex items-center gap-1 flex-shrink-0">
<Badge
v-if="d.sync_status === 'pushed'"
variant="secondary"
class="text-[10px] gap-1"
>
<CheckCircle2 class="size-3" />
Enviada
</Badge>
<Badge v-else variant="outline" class="text-[10px] gap-1">
<Clock class="size-3" />
Borrador
</Badge>
</div>
</div>
<div v-if="d.story_points != null || d.sprint" class="flex gap-2 mt-1.5">
<span v-if="d.story_points != null" class="text-[10px] text-muted-foreground">{{ d.story_points }} SP</span>
<span v-if="d.sprint" class="text-[10px] text-muted-foreground">{{ d.sprint }}</span>
</div>
<div class="flex gap-1 mt-2">
<Button
v-if="d.sync_status !== 'pushed'"
size="sm"
variant="default"
class="h-7 text-xs"
@click="pushToKappa(d)"
>
<Send class="size-3 mr-1" />
Enviar a KAPPA
</Button>
<Button size="sm" variant="outline" class="h-7 text-xs" @click="editDraft(d)">
<Pencil class="size-3 mr-1" />
Editar
</Button>
<Button size="sm" variant="ghost" class="h-7 text-xs text-destructive" @click="deleteDraft(d.id)">
Eliminar
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
</template>
+5 -1
View File
@@ -4,7 +4,8 @@ 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 } from 'lucide-vue-next'
import { Activity, FileText, Layers, Clock, Info, AlertTriangle, Plus } from 'lucide-vue-next'
import HuDrafts from '@/components/HuDrafts.vue'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton'
@@ -244,6 +245,9 @@ const statusLabel = (status: unknown) => {
</p>
</CardContent>
</Card>
<!-- Borradores -->
<HuDrafts v-if="project" :initiative-id="project.id" />
</div>
<div v-else class="flex flex-1 items-center justify-center">