agregar componente HuDrafts: borradores UUID + tabla + push a KAPPA + integracion en dashboard
This commit is contained in:
@@ -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>
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user