fix: orden dashboard + i18n HuDrafts + tipo Epic
- DashboardView: borradores antes de épicas, HuDrafts antes de tabla HUs - DashboardView: skeletons eliminados - DashboardView: fix duplicate const project (causaba ReferenceError) - HuDrafts: i18n para tipos (Epic, Feature, Task, HU, Bug) - HuDrafts: tipo Epic (E) agregado al selector - i18n: +hierarchy section con labels en es/en
This commit is contained in:
@@ -14,6 +14,14 @@ import { Plus, Send, Pencil, CheckCircle2, Clock } from 'lucide-vue-next'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const TYPE_I18N: Record<string, string> = {
|
||||
E: 'hierarchy.epic',
|
||||
U: 'hierarchy.hu',
|
||||
F: 'hierarchy.feature',
|
||||
T: 'hierarchy.task',
|
||||
B: 'hierarchy.bug',
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
initiativeId: number
|
||||
}>()
|
||||
@@ -42,10 +50,10 @@ const form = ref<HuDraftRecord>({
|
||||
updated_at: null,
|
||||
})
|
||||
|
||||
const draftTypes: ItemType[] = ['U', 'F', 'T', 'B']
|
||||
const draftTypes: ItemType[] = ['E', 'F', 'T', 'U', 'B']
|
||||
|
||||
const itemTypeOptions = computed(() =>
|
||||
draftTypes.map(type => ({ value: type, label: getTypeLabel(type) }))
|
||||
draftTypes.map(type => ({ value: type, label: t(TYPE_I18N[type] || 'hierarchy.hu') }))
|
||||
)
|
||||
|
||||
const unPushedDrafts = computed(() =>
|
||||
@@ -188,7 +196,7 @@ onMounted(loadDrafts)
|
||||
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) }}
|
||||
{{ t(TYPE_I18N[d.item_type || 'U'] || 'hierarchy.hu') }}
|
||||
</span>
|
||||
<span class="text-sm truncate font-medium">{{ d.title }}</span>
|
||||
</div>
|
||||
|
||||
@@ -318,6 +318,13 @@
|
||||
"switchModel": "Switch model",
|
||||
"settings": "Settings..."
|
||||
},
|
||||
"hierarchy": {
|
||||
"epic": "Epic",
|
||||
"hu": "HU",
|
||||
"feature": "Feature",
|
||||
"task": "Task",
|
||||
"bug": "Bug"
|
||||
},
|
||||
"workitems": {
|
||||
"unnamedEpic": "Epic {id}"
|
||||
}
|
||||
|
||||
@@ -318,6 +318,13 @@
|
||||
"switchModel": "Cambiar modelo",
|
||||
"settings": "Configuración..."
|
||||
},
|
||||
"hierarchy": {
|
||||
"epic": "Épica",
|
||||
"feature": "Caracteristica",
|
||||
"task": "Tarea",
|
||||
"hu": "HU",
|
||||
"bug": "Error"
|
||||
},
|
||||
"workitems": {
|
||||
"unnamedEpic": "Épica {id}"
|
||||
}
|
||||
|
||||
+80
-119
@@ -14,7 +14,6 @@ import { kappa } from '@/services/kappa-api'
|
||||
import { generateAndSavePlan, getQAPlans, type HUQAPlan } from '@/services/qa-analyzer'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -28,6 +27,8 @@ const { t } = useI18n()
|
||||
const projects = useProjectsStore()
|
||||
const workItems = useWorkItemsStore()
|
||||
|
||||
const project = computed(() => projects.selected)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'navigate-settings': []
|
||||
}>()
|
||||
@@ -195,8 +196,6 @@ async function runAnalysis() {
|
||||
}
|
||||
}
|
||||
|
||||
const project = computed(() => projects.selected)
|
||||
|
||||
watch(
|
||||
() => projects.selectedId,
|
||||
async (id) => {
|
||||
@@ -340,122 +339,6 @@ const statusLabel = (status: unknown) => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Loading -->
|
||||
<template v-if="workItems.loading">
|
||||
<Skeleton class="h-8 w-1/3" />
|
||||
<div class="space-y-2">
|
||||
<Skeleton v-for="i in 4" :key="i" class="h-12 w-full" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Epics -->
|
||||
<template v-else-if="workItems.epics.length > 0">
|
||||
<div>
|
||||
<h3 id="dashboard-epics-heading" class="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-3">
|
||||
{{ t('dashboard.epicsCount', { count: workItems.totalEpics }) }}
|
||||
</h3>
|
||||
<div class="space-y-2">
|
||||
<Card
|
||||
v-for="epic in workItems.epics"
|
||||
:key="epic.id"
|
||||
class="bg-gradient-to-t from-primary/5 to-card shadow-xs dark:bg-card"
|
||||
>
|
||||
<CardHeader class="p-4 pb-2">
|
||||
<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(epic._itemType)"
|
||||
>
|
||||
{{ getTypeIcon(epic._itemType) }} {{ getTypeLabel(epic._itemType) }}
|
||||
</span>
|
||||
<span class="font-mono text-xs text-muted-foreground flex-shrink-0">
|
||||
{{ epic.code || `EP-${epic.id}` }}
|
||||
</span>
|
||||
<CardTitle class="text-sm truncate">{{ epic._cleanName || epic.name || epic.title || t('dashboard.epicFallback', { id: epic.id }) }}</CardTitle>
|
||||
</div>
|
||||
<Badge :variant="statusVariant(epic.status || '')" class="text-xs flex-shrink-0">
|
||||
{{ statusLabel(epic.status || '') }}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent v-if="epic.description" class="p-4 pt-0">
|
||||
<p class="text-xs text-muted-foreground line-clamp-2">
|
||||
{{ epic.description }}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- HUs Table -->
|
||||
<Card id="dashboard-hus-table">
|
||||
<CardHeader class="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle class="text-sm font-medium">{{ t('dashboard.userStoriesTitle') }}</CardTitle>
|
||||
<Badge variant="outline" class="text-xs">{{ t('dashboard.husCount', { count: workItems.userStories.length }) }}</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table v-if="workItems.userStories.length > 0">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="w-[80px]">{{ t('dashboard.code') }}</TableHead>
|
||||
<TableHead class="w-[60px]">{{ t('users.role') }}</TableHead>
|
||||
<TableHead>{{ t('dashboard.title') }}</TableHead>
|
||||
<TableHead class="w-[110px]">{{ t('dashboard.status') }}</TableHead>
|
||||
<TableHead class="w-[90px] text-right">{{ t('dashboard.priority') }}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="hu in workItems.userStories" :key="hu.id">
|
||||
<TableCell class="font-mono text-xs text-muted-foreground">
|
||||
{{ hu.code || `HU-${hu.id}` }}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span
|
||||
class="inline-flex items-center rounded px-1 py-0.5 text-[10px] font-bold"
|
||||
:class="getTypeColor(hu._itemType)"
|
||||
>
|
||||
{{ getTypeLabel(hu._itemType) }}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell class="text-sm max-w-[280px] truncate flex items-center gap-1">
|
||||
<AlertTriangle
|
||||
v-if="hu.has_impairment"
|
||||
class="size-3.5 text-amber-500 flex-shrink-0"
|
||||
title="Tiene impedimentos pendientes"
|
||||
/>
|
||||
<span class="truncate">{{ hu._cleanTitle || hu.title }}</span>
|
||||
<span
|
||||
v-if="hu._criteriaList?.length"
|
||||
class="group relative inline-flex flex-shrink-0 cursor-help"
|
||||
>
|
||||
<Info class="size-3.5 text-muted-foreground hover:text-foreground transition-colors" />
|
||||
<div class="absolute bottom-full left-0 mb-2 w-72 p-3 rounded-lg border bg-popover text-popover-foreground text-xs shadow-md opacity-0 group-hover:opacity-100 transition-opacity z-50 pointer-events-none">
|
||||
<p class="font-semibold mb-1.5 text-[11px] uppercase tracking-wider text-muted-foreground">Criterios de aceptación</p>
|
||||
<ol class="list-decimal list-inside space-y-1 text-[11px]">
|
||||
<li v-for="(c, i) in hu._criteriaList" :key="i">{{ c }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge :variant="statusVariant(hu.status || '')" class="text-xs capitalize">
|
||||
{{ statusLabel(hu.status || '') }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell class="text-right text-xs text-muted-foreground">
|
||||
{{ hu.priority || '—' }}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<p v-else class="text-xs text-muted-foreground/50 italic text-center py-4">
|
||||
{{ t('dashboard.noUserStories') }}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Borradores (web) -->
|
||||
<Card v-if="drafts.length > 0" id="dashboard-drafts" class="border-dashed">
|
||||
<CardHeader class="pb-3 flex flex-row items-center justify-between">
|
||||
@@ -508,8 +391,86 @@ const statusLabel = (status: unknown) => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Epics -->
|
||||
<template v-if="workItems.epics.length > 0">
|
||||
<div>
|
||||
<h3 id="dashboard-epics-heading" class="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-3">
|
||||
{{ t('dashboard.epicsCount', { count: workItems.totalEpics }) }}
|
||||
</h3>
|
||||
<div class="space-y-2">
|
||||
<Card
|
||||
v-for="epic in workItems.epics"
|
||||
:key="epic.id"
|
||||
class="bg-gradient-to-t from-primary/5 to-card shadow-xs dark:bg-card"
|
||||
>
|
||||
<CardHeader class="p-4 pb-2">
|
||||
<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(epic._itemType)"
|
||||
>
|
||||
{{ getTypeIcon(epic._itemType) }} {{ getTypeLabel(epic._itemType) }}
|
||||
</span>
|
||||
<span class="font-mono text-xs text-muted-foreground flex-shrink-0">{{ epic.code || `EP-${epic.id}` }}</span>
|
||||
<CardTitle class="text-sm truncate">{{ epic._cleanName || epic.name || epic.title || t('dashboard.epicFallback', { id: epic.id }) }}</CardTitle>
|
||||
</div>
|
||||
<Badge :variant="statusVariant(epic.status || '')" class="text-xs flex-shrink-0">{{ statusLabel(epic.status || '') }}</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent v-if="epic.description" class="p-4 pt-0">
|
||||
<p class="text-xs text-muted-foreground line-clamp-2">{{ epic.description }}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Borradores (Tauri) -->
|
||||
<HuDrafts v-if="project" :initiative-id="project.id" />
|
||||
|
||||
<!-- HUs Table -->
|
||||
<Card id="dashboard-hus-table">
|
||||
<CardHeader class="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle class="text-sm font-medium">{{ t('dashboard.userStoriesTitle') }}</CardTitle>
|
||||
<Badge variant="outline" class="text-xs">{{ t('dashboard.husCount', { count: workItems.userStories.length }) }}</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table v-if="workItems.userStories.length > 0">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="w-[80px]">{{ t('dashboard.code') }}</TableHead>
|
||||
<TableHead class="w-[60px]">{{ t('users.role') }}</TableHead>
|
||||
<TableHead>{{ t('dashboard.title') }}</TableHead>
|
||||
<TableHead class="w-[110px]">{{ t('dashboard.status') }}</TableHead>
|
||||
<TableHead class="w-[90px] text-right">{{ t('dashboard.priority') }}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="hu in workItems.userStories" :key="hu.id">
|
||||
<TableCell class="font-mono text-xs text-muted-foreground">{{ hu.code || `HU-${hu.id}` }}</TableCell>
|
||||
<TableCell>
|
||||
<span class="inline-flex items-center rounded px-1 py-0.5 text-[10px] font-bold" :class="getTypeColor(hu._itemType)">{{ getTypeLabel(hu._itemType) }}</span>
|
||||
</TableCell>
|
||||
<TableCell class="text-sm max-w-[280px] truncate flex items-center gap-1">
|
||||
<AlertTriangle v-if="hu.has_impairment" class="size-3.5 text-amber-500 flex-shrink-0" title="Tiene impedimentos pendientes" />
|
||||
<span class="truncate">{{ hu._cleanTitle || hu.title }}</span>
|
||||
<span v-if="hu._criteriaList?.length" class="group relative inline-flex flex-shrink-0 cursor-help">
|
||||
<Info class="size-3.5 text-muted-foreground hover:text-foreground transition-colors" />
|
||||
<div class="absolute bottom-full left-0 mb-2 w-72 p-3 rounded-lg border bg-popover text-popover-foreground text-xs shadow-md opacity-0 group-hover:opacity-100 transition-opacity z-50 pointer-events-none">
|
||||
<p class="font-semibold mb-1.5 text-[11px] uppercase tracking-wider text-muted-foreground">Criterios de aceptación</p>
|
||||
<ol class="list-decimal list-inside space-y-1 text-[11px]"><li v-for="(c, i) in hu._criteriaList" :key="i">{{ c }}</li></ol>
|
||||
</div>
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell><Badge :variant="statusVariant(hu.status || '')" class="text-xs capitalize">{{ statusLabel(hu.status || '') }}</Badge></TableCell>
|
||||
<TableCell class="text-right text-xs text-muted-foreground">{{ hu.priority || '—' }}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<p v-else class="text-xs text-muted-foreground/50 italic text-center py-4">{{ t('dashboard.noUserStories') }}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-1 items-center justify-center">
|
||||
|
||||
Reference in New Issue
Block a user