K-12 Priorizador diario: HUs vencidas, hoy, esta semana y bloqueadas
- PrioritizerCard.vue: nuevo componente con secciones de priorización - Tarjetas resumen: total HUs, en progreso, vencidas, bloqueadas - Listas agrupadas: vencidas (rojo), hoy (ámbar), esta semana, bloqueadas - Cada item: código, título, prioridad, fecha - KappaUserStory: agregados end_date, sprint, initial_date - enrichHU: pasa end_date y sprint al objeto enriquecido
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useWorkItemsStore, type EnrichedUserStory } from '@/stores/workitems'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { AlertTriangle, Calendar, Clock, ListChecks } from 'lucide-vue-next'
|
||||
|
||||
const { t } = useI18n()
|
||||
const workItems = useWorkItemsStore()
|
||||
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
|
||||
function daysUntil(dateStr: string | undefined): number | null {
|
||||
if (!dateStr) return null
|
||||
const d = new Date(dateStr)
|
||||
d.setHours(0, 0, 0, 0)
|
||||
return Math.ceil((d.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))
|
||||
}
|
||||
|
||||
function isOverdue(dateStr: string | undefined): boolean {
|
||||
const d = daysUntil(dateStr)
|
||||
return d !== null && d < 0
|
||||
}
|
||||
|
||||
function isToday(dateStr: string | undefined): boolean {
|
||||
const d = daysUntil(dateStr)
|
||||
return d === 0
|
||||
}
|
||||
|
||||
function isThisWeek(dateStr: string | undefined): boolean {
|
||||
const d = daysUntil(dateStr)
|
||||
return d !== null && d > 0 && d <= 7
|
||||
}
|
||||
|
||||
function isNextWeek(dateStr: string | undefined): boolean {
|
||||
const d = daysUntil(dateStr)
|
||||
return d !== null && d > 7 && d <= 14
|
||||
}
|
||||
|
||||
function isBlocked(hu: EnrichedUserStory): boolean {
|
||||
const s = String(hu.status ?? '').toLowerCase()
|
||||
return ['blocked', 'bloqueado', '6'].includes(s)
|
||||
}
|
||||
|
||||
function isInProgress(hu: EnrichedUserStory): boolean {
|
||||
const s = String(hu.status ?? '').toLowerCase()
|
||||
return ['in_progress', 'doing', 'wip', 'active', 'in progress', 'en progreso', 'true', '2'].includes(s)
|
||||
}
|
||||
|
||||
// Agrupar HUs
|
||||
const overdueHUs = computed(() =>
|
||||
workItems.userStories.filter(hu => hu.end_date && isOverdue(hu.end_date))
|
||||
)
|
||||
|
||||
const todayHUs = computed(() =>
|
||||
workItems.userStories.filter(hu => hu.end_date && isToday(hu.end_date))
|
||||
)
|
||||
|
||||
const thisWeekHUs = computed(() =>
|
||||
workItems.userStories.filter(hu => hu.end_date && isThisWeek(hu.end_date))
|
||||
)
|
||||
|
||||
const nextWeekHUs = computed(() =>
|
||||
workItems.userStories.filter(hu => hu.end_date && isNextWeek(hu.end_date))
|
||||
)
|
||||
|
||||
const blockedHUs = computed(() =>
|
||||
workItems.userStories.filter(hu => isBlocked(hu))
|
||||
)
|
||||
|
||||
const inProgressHUs = computed(() =>
|
||||
workItems.userStories.filter(hu => isInProgress(hu))
|
||||
)
|
||||
|
||||
function formatDate(dateStr: string | undefined): string {
|
||||
if (!dateStr) return ''
|
||||
const d = new Date(dateStr)
|
||||
return d.toLocaleDateString('es-CO', { day: 'numeric', month: 'short' })
|
||||
}
|
||||
|
||||
function priorityVariant(p: unknown) {
|
||||
const s = String(p ?? '').toLowerCase().trim()
|
||||
if (['alta', 'high', 'critical', 'urgente', '1'].includes(s)) return 'destructive'
|
||||
if (['media', 'medium', 'normal', '2'].includes(s)) return 'default'
|
||||
if (['baja', 'low', '3'].includes(s)) return 'secondary'
|
||||
return 'outline'
|
||||
}
|
||||
|
||||
const totalHUs = computed(() => workItems.userStories.length)
|
||||
const doneHUs = computed(() =>
|
||||
workItems.userStories.filter(hu => {
|
||||
const s = String(hu.status ?? '').toLowerCase()
|
||||
return ['done', 'completed', 'closed', 'finalizado', '5', 'true'].includes(s)
|
||||
}).length
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<!-- Summary cards -->
|
||||
<div class="grid gap-3 @xl:grid-cols-4">
|
||||
<Card class="bg-gradient-to-t from-primary/5 to-card shadow-xs">
|
||||
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle class="text-xs font-medium">{{ t('dashboard.hus') }}</CardTitle>
|
||||
<ListChecks class="size-3.5 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="text-xl font-bold">{{ totalHUs }}</div>
|
||||
<p class="text-[10px] text-muted-foreground">{{ doneHUs }} completadas ({{ totalHUs > 0 ? Math.round(doneHUs / totalHUs * 100) : 0 }}%)</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card class="bg-gradient-to-t from-amber-500/5 to-card shadow-xs">
|
||||
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle class="text-xs font-medium">En progreso</CardTitle>
|
||||
<Clock class="size-3.5 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="text-xl font-bold">{{ inProgressHUs.length }}</div>
|
||||
<p class="text-[10px] text-muted-foreground">activas</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card class="bg-gradient-to-t from-red-500/5 to-card shadow-xs">
|
||||
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle class="text-xs font-medium">Vencidas</CardTitle>
|
||||
<AlertTriangle class="size-3.5 text-red-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="text-xl font-bold text-red-500">{{ overdueHUs.length }}</div>
|
||||
<p class="text-[10px] text-muted-foreground">requieren atención</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card class="bg-gradient-to-t from-purple-500/5 to-card shadow-xs">
|
||||
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle class="text-xs font-medium">Bloqueadas</CardTitle>
|
||||
<AlertTriangle class="size-3.5 text-purple-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="text-xl font-bold text-purple-500">{{ blockedHUs.length }}</div>
|
||||
<p class="text-[10px] text-muted-foreground">con impedimentos</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- Overdue -->
|
||||
<Card v-if="overdueHUs.length > 0" class="border-l-3 border-l-red-500">
|
||||
<CardHeader class="pb-2">
|
||||
<CardTitle class="text-xs font-medium flex items-center gap-2 text-red-600">
|
||||
<AlertTriangle class="size-3.5" />
|
||||
Vencidas ({{ overdueHUs.length }})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-1">
|
||||
<div v-for="hu in overdueHUs" :key="hu.id" class="flex items-center gap-2 text-xs py-1 border-b last:border-0">
|
||||
<span class="font-mono text-[10px] text-muted-foreground w-16">{{ hu.code || `#${hu.id}` }}</span>
|
||||
<span class="flex-1 truncate">{{ hu._cleanTitle || hu.title }}</span>
|
||||
<Badge :variant="priorityVariant(hu.priority)" class="text-[10px]">{{ hu.priority }}</Badge>
|
||||
<span class="text-red-500 font-mono text-[10px] w-16 text-right">{{ formatDate(hu.end_date) }}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Today -->
|
||||
<Card v-if="todayHUs.length > 0" class="border-l-3 border-l-amber-500">
|
||||
<CardHeader class="pb-2">
|
||||
<CardTitle class="text-xs font-medium flex items-center gap-2">
|
||||
<Calendar class="size-3.5" />
|
||||
Hoy ({{ todayHUs.length }})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-1">
|
||||
<div v-for="hu in todayHUs" :key="hu.id" class="flex items-center gap-2 text-xs py-1 border-b last:border-0">
|
||||
<span class="font-mono text-[10px] text-muted-foreground w-16">{{ hu.code || `#${hu.id}` }}</span>
|
||||
<span class="flex-1 truncate">{{ hu._cleanTitle || hu.title }}</span>
|
||||
<Badge :variant="priorityVariant(hu.priority)" class="text-[10px]">{{ hu.priority }}</Badge>
|
||||
<Badge variant="outline" class="text-[10px]">Hoy</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- This week -->
|
||||
<Card v-if="thisWeekHUs.length > 0">
|
||||
<CardHeader class="pb-2">
|
||||
<CardTitle class="text-xs font-medium flex items-center gap-2">
|
||||
<Calendar class="size-3.5" />
|
||||
Esta semana ({{ thisWeekHUs.length }})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-1">
|
||||
<div v-for="hu in thisWeekHUs" :key="hu.id" class="flex items-center gap-2 text-xs py-1 border-b last:border-0">
|
||||
<span class="font-mono text-[10px] text-muted-foreground w-16">{{ hu.code || `#${hu.id}` }}</span>
|
||||
<span class="flex-1 truncate">{{ hu._cleanTitle || hu.title }}</span>
|
||||
<Badge :variant="priorityVariant(hu.priority)" class="text-[10px]">{{ hu.priority }}</Badge>
|
||||
<span class="font-mono text-[10px] text-muted-foreground w-16 text-right">{{ formatDate(hu.end_date) }}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Blocked -->
|
||||
<Card v-if="blockedHUs.length > 0" class="border-l-3 border-l-purple-500">
|
||||
<CardHeader class="pb-2">
|
||||
<CardTitle class="text-xs font-medium flex items-center gap-2 text-purple-600">
|
||||
<AlertTriangle class="size-3.5" />
|
||||
Bloqueadas ({{ blockedHUs.length }})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-1">
|
||||
<div v-for="hu in blockedHUs" :key="hu.id" class="flex items-center gap-2 text-xs py-1 border-b last:border-0">
|
||||
<span class="font-mono text-[10px] text-muted-foreground w-16">{{ hu.code || `#${hu.id}` }}</span>
|
||||
<span class="flex-1 truncate">{{ hu._cleanTitle || hu.title }}</span>
|
||||
<Badge variant="destructive" class="text-[10px]">Bloqueada</Badge>
|
||||
<Badge v-if="hu.has_impairment" variant="outline" class="text-[10px]">Impedimento</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -93,7 +93,7 @@ export const useWorkItemsStore = defineStore('workitems', () => {
|
||||
return { id: null, name: '', employeeId: null }
|
||||
}
|
||||
|
||||
function enrichHU(hu: { title?: string; id?: number; description?: string | null; status?: string | number | null; status_name?: string | null; priority?: string | number | null; story_points?: number | null; acceptance_criteria?: string | null; criterios_aceptacion?: string | null; assigned_to?: number | null; asignado_a?: number[] | string[] | null; asignado_a_names?: string[] | string | null; assigned_name?: string }, initiativeId: number): EnrichedUserStory {
|
||||
function enrichHU(hu: { title?: string; id?: number; description?: string | null; status?: string | number | null; status_name?: string | null; priority?: string | number | null; story_points?: number | null; end_date?: string | null; sprint?: number | string | null; acceptance_criteria?: string | null; criterios_aceptacion?: string | null; assigned_to?: number | null; asignado_a?: number[] | string[] | null; asignado_a_names?: string[] | string | null; assigned_name?: string }, initiativeId: number): EnrichedUserStory {
|
||||
const h = parseHierarchy(hu.title || '')
|
||||
const rawCriteria = hu.acceptance_criteria || hu.criterios_aceptacion || ''
|
||||
const criteriaList = rawCriteria ? parseQuillList(rawCriteria) : []
|
||||
@@ -104,6 +104,8 @@ export const useWorkItemsStore = defineStore('workitems', () => {
|
||||
status: String(hu.status ?? ''),
|
||||
priority: String(hu.priority ?? ''),
|
||||
story_points: hu.story_points ?? undefined,
|
||||
end_date: hu.end_date || undefined,
|
||||
sprint: hu.sprint != null ? String(hu.sprint) : undefined,
|
||||
description: hu.description || '',
|
||||
acceptance_criteria: rawCriteria,
|
||||
criterios_aceptacion: rawCriteria,
|
||||
|
||||
+3
-1
@@ -48,7 +48,9 @@ export interface KappaUserStory {
|
||||
priority?: string | number
|
||||
initiative: number | string
|
||||
story_points?: number
|
||||
sprint?: string
|
||||
sprint?: number | string
|
||||
end_date?: string
|
||||
initial_date?: string | null
|
||||
created_at?: string
|
||||
assigned_to?: number | null
|
||||
asignado_a?: number[] | string[] | null
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
||||
import HuDrafts from '@/components/HuDrafts.vue'
|
||||
import AiProjectChat from '@/components/AiProjectChat.vue'
|
||||
import PrioritizerCard from '@/components/PrioritizerCard.vue'
|
||||
import { analyzeProjectEpics, analyzeProjectHUs, saveEpicDrafts, saveHUDrafts } from '@/services/project-analyzer'
|
||||
import { getDrafts, deleteDraft, type HuDraftRecord } from '@/services/hu-drafts-db'
|
||||
import { kappa } from '@/services/kappa-api'
|
||||
@@ -663,6 +664,9 @@ const statusLabel = (status: unknown) => {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- Priorizador Diario -->
|
||||
<PrioritizerCard />
|
||||
|
||||
<!-- AI Chat -->
|
||||
<AiProjectChat
|
||||
:project-id="project.id"
|
||||
|
||||
Reference in New Issue
Block a user