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 { useProjectsStore } from '@/stores/projects'
|
||||||
import { useWorkItemsStore } from '@/stores/workitems'
|
import { useWorkItemsStore } from '@/stores/workitems'
|
||||||
import { getTypeLabel, getTypeColor, getTypeIcon } from '@/services/hierarchy'
|
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 { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
@@ -244,6 +245,9 @@ const statusLabel = (status: unknown) => {
|
|||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<!-- Borradores -->
|
||||||
|
<HuDrafts v-if="project" :initiative-id="project.id" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="flex flex-1 items-center justify-center">
|
<div v-else class="flex flex-1 items-center justify-center">
|
||||||
|
|||||||
Reference in New Issue
Block a user