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.
+ )}
+
);
}