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 { 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<{
|
const props = defineProps<{
|
||||||
initiativeId: number
|
initiativeId: number
|
||||||
}>()
|
}>()
|
||||||
@@ -42,10 +50,10 @@ const form = ref<HuDraftRecord>({
|
|||||||
updated_at: null,
|
updated_at: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
const draftTypes: ItemType[] = ['U', 'F', 'T', 'B']
|
const draftTypes: ItemType[] = ['E', 'F', 'T', 'U', 'B']
|
||||||
|
|
||||||
const itemTypeOptions = computed(() =>
|
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(() =>
|
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="inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-bold"
|
||||||
:class="getTypeColor((d.item_type || 'U') as ItemType)"
|
: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>
|
||||||
<span class="text-sm truncate font-medium">{{ d.title }}</span>
|
<span class="text-sm truncate font-medium">{{ d.title }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -318,6 +318,13 @@
|
|||||||
"switchModel": "Switch model",
|
"switchModel": "Switch model",
|
||||||
"settings": "Settings..."
|
"settings": "Settings..."
|
||||||
},
|
},
|
||||||
|
"hierarchy": {
|
||||||
|
"epic": "Epic",
|
||||||
|
"hu": "HU",
|
||||||
|
"feature": "Feature",
|
||||||
|
"task": "Task",
|
||||||
|
"bug": "Bug"
|
||||||
|
},
|
||||||
"workitems": {
|
"workitems": {
|
||||||
"unnamedEpic": "Epic {id}"
|
"unnamedEpic": "Epic {id}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -318,6 +318,13 @@
|
|||||||
"switchModel": "Cambiar modelo",
|
"switchModel": "Cambiar modelo",
|
||||||
"settings": "Configuración..."
|
"settings": "Configuración..."
|
||||||
},
|
},
|
||||||
|
"hierarchy": {
|
||||||
|
"epic": "Épica",
|
||||||
|
"feature": "Caracteristica",
|
||||||
|
"task": "Tarea",
|
||||||
|
"hu": "HU",
|
||||||
|
"bug": "Error"
|
||||||
|
},
|
||||||
"workitems": {
|
"workitems": {
|
||||||
"unnamedEpic": "Épica {id}"
|
"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 { generateAndSavePlan, getQAPlans, type HUQAPlan } from '@/services/qa-analyzer'
|
||||||
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 {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -28,6 +27,8 @@ const { t } = useI18n()
|
|||||||
const projects = useProjectsStore()
|
const projects = useProjectsStore()
|
||||||
const workItems = useWorkItemsStore()
|
const workItems = useWorkItemsStore()
|
||||||
|
|
||||||
|
const project = computed(() => projects.selected)
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'navigate-settings': []
|
'navigate-settings': []
|
||||||
}>()
|
}>()
|
||||||
@@ -195,8 +196,6 @@ async function runAnalysis() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const project = computed(() => projects.selected)
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => projects.selectedId,
|
() => projects.selectedId,
|
||||||
async (id) => {
|
async (id) => {
|
||||||
@@ -340,122 +339,6 @@ const statusLabel = (status: unknown) => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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) -->
|
<!-- Borradores (web) -->
|
||||||
<Card v-if="drafts.length > 0" id="dashboard-drafts" class="border-dashed">
|
<Card v-if="drafts.length > 0" id="dashboard-drafts" class="border-dashed">
|
||||||
<CardHeader class="pb-3 flex flex-row items-center justify-between">
|
<CardHeader class="pb-3 flex flex-row items-center justify-between">
|
||||||
@@ -508,8 +391,86 @@ const statusLabel = (status: unknown) => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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) -->
|
<!-- Borradores (Tauri) -->
|
||||||
<HuDrafts v-if="project" :initiative-id="project.id" />
|
<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>
|
||||||
|
|
||||||
<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