agregar paginacion a /userstorys/ + redisenar dashboard de proyecto con epicas y HUs
This commit is contained in:
@@ -99,9 +99,23 @@ class KappaAPI {
|
||||
return this.request<PaginatedResponse<KappaEmployee>>('GET', `/employees/?page=${page}`)
|
||||
}
|
||||
|
||||
async getUserStories(initiativeId?: number): Promise<KappaUserStory[]> {
|
||||
const path = initiativeId ? `/userstorys/?initiative=${initiativeId}` : '/userstorys/'
|
||||
return this.request<KappaUserStory[]>('GET', path)
|
||||
async getUserStories(initiativeId?: number, page = 1, pageSize = 100): Promise<PaginatedResponse<KappaUserStory>> {
|
||||
const params = new URLSearchParams()
|
||||
if (initiativeId) params.set('initiative', String(initiativeId))
|
||||
params.set('page', String(page))
|
||||
params.set('page_size', String(pageSize))
|
||||
return this.request<PaginatedResponse<KappaUserStory>>('GET', `/userstorys/?${params.toString()}`)
|
||||
}
|
||||
|
||||
async getAllUserStories(initiativeId: number): Promise<KappaUserStory[]> {
|
||||
const first = await this.getUserStories(initiativeId, 1, 100)
|
||||
const all = [...first.results]
|
||||
const totalPages = Math.ceil(first.count / 100)
|
||||
for (let p = 2; p <= totalPages; p++) {
|
||||
const page = await this.getUserStories(initiativeId, p, 100)
|
||||
all.push(...page.results)
|
||||
}
|
||||
return all
|
||||
}
|
||||
|
||||
async getEpicDevelopment(initiativeId: number, page = 1, pageSize = 50): Promise<PaginatedResponse<KappaEpicDevelopment>> {
|
||||
|
||||
@@ -51,18 +51,15 @@ export const useWorkItemsStore = defineStore('workitems', () => {
|
||||
|
||||
try {
|
||||
const [stories, logs, plans, epicData] = await Promise.all([
|
||||
kappa.getUserStories(id).catch(() => [] as KappaUserStory[]),
|
||||
kappa.getAllUserStories(id).catch(() => [] as KappaUserStory[]),
|
||||
kappa.getLogbooks(id).catch(() => [] as KappaLogbookEntry[]),
|
||||
kappa.getPlannings(id).catch(() => [] as KappaPlanningEntry[]),
|
||||
kappa.getAllEpicDevelopment(id).catch(() => [] as KappaEpicDevelopment[]),
|
||||
])
|
||||
|
||||
const storiesData = stories as KappaUserStory[] | { results?: KappaUserStory[] }
|
||||
const logsData = logs as KappaLogbookEntry[] | { results?: KappaLogbookEntry[] }
|
||||
const plansData = plans as KappaPlanningEntry[] | { results?: KappaPlanningEntry[] }
|
||||
userStories.value = Array.isArray(storiesData) ? storiesData : (storiesData.results ?? [])
|
||||
logbooks.value = Array.isArray(logsData) ? logsData : (logsData.results ?? [])
|
||||
plannings.value = Array.isArray(plansData) ? plansData : (plansData.results ?? [])
|
||||
userStories.value = stories
|
||||
logbooks.value = Array.isArray(logs) ? logs : ((logs as any).results ?? [])
|
||||
plannings.value = Array.isArray(plans) ? plans : ((plans as any).results ?? [])
|
||||
|
||||
epics.value = epicData.map(epic => ({
|
||||
...epic,
|
||||
|
||||
+136
-96
@@ -2,9 +2,10 @@
|
||||
import { computed, watch } from 'vue'
|
||||
import { useProjectsStore } from '@/stores/projects'
|
||||
import { useWorkItemsStore } from '@/stores/workitems'
|
||||
import { Activity, CreditCard, FileText, Users } from 'lucide-vue-next'
|
||||
import { Activity, FileText, Layers, Clock, ChevronRight } from 'lucide-vue-next'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -29,42 +30,70 @@ watch(
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const recentSessions = computed(() => {
|
||||
return [...workItems.logbooks]
|
||||
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||
.slice(0, 5)
|
||||
})
|
||||
const statusVariant = (status: string) => {
|
||||
const s = status?.toLowerCase() || ''
|
||||
if (['done', 'completed', 'closed', 'finalizado'].includes(s)) return 'secondary'
|
||||
if (['in_progress', 'doing', 'wip', 'active', 'in progress', 'en progreso'].includes(s)) return 'default'
|
||||
if (['blocked', 'bloqueado'].includes(s)) return 'destructive'
|
||||
return 'outline'
|
||||
}
|
||||
|
||||
const statusLabel = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
backlog: 'Backlog',
|
||||
todo: 'Por hacer',
|
||||
in_progress: 'En progreso',
|
||||
'in progress': 'En progreso',
|
||||
doing: 'Haciendo',
|
||||
wip: 'WIP',
|
||||
done: 'Hecho',
|
||||
completed: 'Completado',
|
||||
blocked: 'Bloqueado',
|
||||
review: 'Revisión',
|
||||
testing: 'Pruebas',
|
||||
}
|
||||
return map[status?.toLowerCase() || ''] || status || '—'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="project" class="@container/main flex flex-1 flex-col gap-2">
|
||||
<div class="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">{{ project.name }}</h1>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<Badge v-if="project.key" variant="outline" class="text-xs">{{ project.key }}</Badge>
|
||||
<Badge v-if="project.status" variant="secondary" class="text-xs capitalize">{{ project.status }}</Badge>
|
||||
</div>
|
||||
<div v-if="project" class="@container/main flex flex-1 flex-col gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">{{ project.name }}</h1>
|
||||
<div class="flex items-center gap-2">
|
||||
<Badge v-if="project.key" variant="outline" class="text-xs">{{ project.key }}</Badge>
|
||||
<Badge v-if="project.status" variant="secondary" class="text-xs capitalize">{{ project.status }}</Badge>
|
||||
</div>
|
||||
<Badge variant="outline" class="text-xs">v0.1.0</Badge>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<Card>
|
||||
<!-- Stats -->
|
||||
<div class="grid gap-3 @xl:grid-cols-2 @3xl:grid-cols-4">
|
||||
<Card class="bg-gradient-to-t from-primary/5 to-card shadow-xs dark:bg-card">
|
||||
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle class="text-sm font-medium">Historias de Usuario</CardTitle>
|
||||
<FileText class="h-4 w-4 text-muted-foreground" />
|
||||
<CardTitle class="text-sm font-medium">Épicas</CardTitle>
|
||||
<Layers class="size-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="text-2xl font-bold">{{ workItems.totalHUs }}</div>
|
||||
<p class="text-xs text-muted-foreground">Total HUs del proyecto</p>
|
||||
<div class="text-2xl font-bold">{{ workItems.totalEpics }}</div>
|
||||
<p class="text-xs text-muted-foreground">Actividades del cronograma</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Card class="bg-gradient-to-t from-primary/5 to-card shadow-xs dark:bg-card">
|
||||
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle class="text-sm font-medium">HUs</CardTitle>
|
||||
<FileText class="size-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="text-2xl font-bold">{{ workItems.totalHUs }}</div>
|
||||
<p class="text-xs text-muted-foreground">Historias de usuario</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card class="bg-gradient-to-t from-primary/5 to-card shadow-xs dark:bg-card">
|
||||
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle class="text-sm font-medium">En progreso</CardTitle>
|
||||
<Activity class="h-4 w-4 text-muted-foreground" />
|
||||
<Activity class="size-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="text-2xl font-bold">{{ workItems.inProgressHUs }}</div>
|
||||
@@ -72,99 +101,110 @@ const recentSessions = computed(() => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Card class="bg-gradient-to-t from-primary/5 to-card shadow-xs dark:bg-card">
|
||||
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle class="text-sm font-medium">Sesiones</CardTitle>
|
||||
<Users class="h-4 w-4 text-muted-foreground" />
|
||||
<Clock class="size-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="text-2xl font-bold">{{ workItems.totalSessions }}</div>
|
||||
<p class="text-xs text-muted-foreground">Bitácoras registradas</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle class="text-sm font-medium">Planeaciones</CardTitle>
|
||||
<CreditCard class="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="text-2xl font-bold">{{ workItems.plannings.length }}</div>
|
||||
<p class="text-xs text-muted-foreground">Entradas de planeación</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-sm font-medium">Historia clínica</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p v-if="project.description" class="text-sm text-muted-foreground leading-relaxed">
|
||||
{{ project.description }}
|
||||
</p>
|
||||
<p v-else class="text-sm text-muted-foreground/50 italic">Sin descripción</p>
|
||||
|
||||
<div v-if="project.start_date" class="mt-4 space-y-2">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">Ingreso</span>
|
||||
<span class="font-medium">{{ project.start_date }}</span>
|
||||
</div>
|
||||
<div v-if="project.end_date" class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">Alta prevista</span>
|
||||
<span class="font-medium">{{ project.end_date }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-sm font-medium">Actividad reciente</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div v-if="recentSessions.length > 0" class="space-y-4">
|
||||
<div v-for="session in recentSessions" :key="session.id" class="flex items-center gap-4">
|
||||
<div class="h-2 w-2 rounded-full bg-muted-foreground/30" />
|
||||
<div class="flex-1 space-y-1">
|
||||
<p class="text-sm font-medium leading-none truncate">{{ session.description?.slice(0, 50) || 'Sin descripción' }}</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ new Date(session.date).toLocaleDateString('es-CO', { month: 'long', day: 'numeric', year: 'numeric' }) }}
|
||||
<span v-if="session.hours"> · {{ session.hours }}h</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="text-xs text-muted-foreground/50 italic">Sin actividad reciente</p>
|
||||
<p class="text-xs text-muted-foreground">Bitácoras</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<Card>
|
||||
<CardHeader class="flex flex-row items-center justify-between">
|
||||
<CardTitle class="text-sm font-medium">Work Items</CardTitle>
|
||||
<Badge v-if="workItems.loading" variant="outline" class="text-[10px]">Cargando...</Badge>
|
||||
<Badge v-else variant="outline" class="text-[10px]">{{ workItems.userStories.length }} HUs</Badge>
|
||||
<CardHeader class="pb-2">
|
||||
<CardTitle class="text-sm font-medium">Descripción</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p v-if="project.description" class="text-sm text-muted-foreground leading-relaxed line-clamp-4">
|
||||
{{ project.description }}
|
||||
</p>
|
||||
<p v-else class="text-sm text-muted-foreground/50 italic">Sin descripción</p>
|
||||
<div v-if="project.start_date || project.end_date" class="flex items-center gap-4 mt-3 text-xs text-muted-foreground">
|
||||
<span v-if="project.start_date">📅 {{ project.start_date }}</span>
|
||||
<span v-if="project.end_date">→ {{ project.end_date }}</span>
|
||||
</div>
|
||||
</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 class="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-3">
|
||||
Épicas · {{ 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="font-mono text-xs text-muted-foreground flex-shrink-0">
|
||||
{{ epic.code || `EP-${epic.id}` }}
|
||||
</span>
|
||||
<CardTitle class="text-sm truncate">{{ epic.name || epic.title || `Épica ${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>
|
||||
<CardHeader class="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle class="text-sm font-medium">Historias de Usuario</CardTitle>
|
||||
<Badge variant="outline" class="text-xs">{{ workItems.userStories.length }} HUs</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table v-if="workItems.userStories.length > 0">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Código</TableHead>
|
||||
<TableHead class="w-[100px]">Código</TableHead>
|
||||
<TableHead>Título</TableHead>
|
||||
<TableHead>Estado</TableHead>
|
||||
<TableHead class="text-right">Prioridad</TableHead>
|
||||
<TableHead class="w-[110px]">Estado</TableHead>
|
||||
<TableHead class="w-[90px] text-right">Prioridad</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="story in workItems.userStories.slice(0, 10)" :key="story.id">
|
||||
<TableCell class="font-mono text-xs text-muted-foreground">{{ story.code || `HU-${story.id}` }}</TableCell>
|
||||
<TableCell class="text-sm truncate max-w-[200px]">{{ story.title }}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline" class="text-[10px] capitalize">{{ story.status || 'backlog' }}</Badge>
|
||||
<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 class="text-sm max-w-[300px] truncate">
|
||||
{{ hu.title }}
|
||||
</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>
|
||||
<TableCell class="text-right text-muted-foreground/70">{{ story.priority || '—' }}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
Reference in New Issue
Block a user