diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3eebbdf..3fda32c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -781,9 +781,9 @@ export function App() { modals.open({ title: card.title, size: "md", - children: , + children: , }); - }, []); + }, [board?.columns]); const handleToggleCardLock = useCallback(async (id: string, locked: boolean) => { setBoard((prev) => { diff --git a/frontend/src/components/HistoryModal.tsx b/frontend/src/components/HistoryModal.tsx index 50b86da..d3be475 100644 --- a/frontend/src/components/HistoryModal.tsx +++ b/frontend/src/components/HistoryModal.tsx @@ -1,8 +1,9 @@ -import { Badge, Divider, Group, Loader, Stack, Text, Timeline } from "@mantine/core"; +import { Badge, Divider, Group, Loader, Stack, Table, Text, Timeline } from "@mantine/core"; import { IconArrowsHorizontal, IconCalendarDue, IconCalendarOff, + IconCheck, IconColumns3, IconEdit, IconLock, @@ -16,11 +17,12 @@ import { } from "@tabler/icons-react"; import { useEffect, useMemo, useState } from "react"; import { cardHistory, listUsers } from "../api"; -import type { Card, CardEvent, CardHistoryResponse, User } from "../types"; +import type { Card, CardEvent, CardHistoryResponse, Column, User } from "../types"; import { formatDuration } from "./format"; interface Props { card: Card; + columns?: Column[]; } interface UnifiedEvent { @@ -31,6 +33,7 @@ interface UnifiedEvent { detail: string; icon: React.ReactNode; color: string; + doneColumn?: boolean; } function parsePayload(p: string): Record { @@ -67,10 +70,18 @@ function eventToUnified(e: CardEvent): UnifiedEvent { } } -export function HistoryModal({ card }: Props) { +export function HistoryModal({ card, columns = [] }: Props) { const [data, setData] = useState(null); const [users, setUsers] = useState([]); + const columnById = useMemo(() => { + const m = new Map(); + for (const c of columns) m.set(c.id, c); + return m; + }, [columns]); + + const isDoneColumn = (columnId: string) => columnById.get(columnId)?.is_done === true; + useEffect(() => { cardHistory(card.id) .then(setData) @@ -91,14 +102,16 @@ export function HistoryModal({ card }: Props) { const out: UnifiedEvent[] = []; for (const e of data.events || []) out.push(eventToUnified(e)); for (const h of data.column_history || []) { + const done = isDoneColumn(h.column_id); out.push({ id: "h_in_" + h.id, ts: h.entered_at, - kind: "Mueve a columna", + kind: done ? "Hecho en columna" : "Mueve a columna", actorID: h.actor_id, detail: h.column_name || h.column_id, - icon: , - color: "blue", + icon: done ? : , + color: done ? "green" : "blue", + doneColumn: done, }); } for (const p of data.lock_periods || []) { @@ -108,7 +121,7 @@ export function HistoryModal({ card }: Props) { } } return out.sort((a, b) => a.ts.localeCompare(b.ts)); - }, [data]); + }, [data, columnById]); if (!data) { return ( @@ -124,6 +137,26 @@ export function HistoryModal({ card }: Props) { return Sin historial.; } + // Per-column time stats: sum duration_ms by column_id from column_history. + // Currently-active entry (exited_at=null) gets duration_ms = now - entered_at. + const nowMs = Date.now(); + const perColumnMs = new Map(); + for (const h of column_history) { + const dur = h.exited_at ? h.duration_ms : Math.max(0, nowMs - new Date(h.entered_at).getTime()); + const key = h.column_id; + const prev = perColumnMs.get(key); + const meta = columnById.get(key); + perColumnMs.set(key, { + name: h.column_name || meta?.name || key, + isDone: meta?.is_done ?? false, + ms: (prev?.ms ?? 0) + dur, + visits: (prev?.visits ?? 0) + 1, + }); + } + const perColumnRows = Array.from(perColumnMs.entries()) + .map(([id, v]) => ({ id, ...v })) + .sort((a, b) => b.ms - a.ms); + const userLabel = (id: string | null): string => { if (!id) return ""; const u = userById.get(id); @@ -140,6 +173,7 @@ export function HistoryModal({ card }: Props) { key={e.id} bullet={e.icon} color={e.color} + lineVariant={e.doneColumn ? "solid" : undefined} title={ {e.kind} @@ -163,16 +197,47 @@ export function HistoryModal({ card }: Props) { - - - Columnas visitadas - {column_history.length} - - 0 ? "yellow" : "gray"}> - {formatDuration(total_locked_ms)} - - {currently_locked && bloqueada} - + + + + Tiempo por columna + {column_history.length} entradas + + {" "} + 0 ? "yellow" : "dimmed"}> + {formatDuration(total_locked_ms)} + {" "} + bloqueada{currently_locked ? " (en curso)" : ""} + + + {perColumnRows.length > 0 ? ( + + + + Columna + Visitas + Tiempo total + + + + {perColumnRows.map((r) => ( + + + + {r.isDone && } + {r.name} + + + {r.visits} + {formatDuration(r.ms)} + + ))} + +
+ ) : ( + Sin movimientos entre columnas. + )} +
); }