feat(kanban): tiempo maximo por columna con borde rojo (issue 0089)
Adds column-level max time limit. Cards whose time_in_column_ms exceeds the limit show a red border + red halo. Columns marked as Done never trigger the visual regardless of the limit (per spec). Backend: - Migration 011_column_max_time.sql adds columns.max_time_minutes INTEGER NOT NULL DEFAULT 0 (0 = no limit). Aditiva, idempotente. - Column struct + ColumnPatch + UpdateColumn handle the new field; negatives clamp to 0; listing query includes it. - handleUpdateColumn (PATCH /api/columns/:id) accepts max_time_minutes in the JSON body. Frontend: - Column TS interface + UpdateColumnInput updated. - KanbanColumn context menu: new entry "Tiempo maximo" using window.prompt for low-friction config; shows current value when >0. - KanbanCard receives columnOverdue prop calculated from the column state and card.time_in_column_ms; renders red border (var --mantine-color-red-6) with 2px width + 2px red halo when overdue. - data-card-id, data-column-overdue, data-locked attributes on the card paper element so e2e tests / scripts can query state. Tests: TestColumnMaxTimeMinutes_Defaults + _Update verify the schema default, the clamp on negative input, and that updating max_time leaves other fields untouched. Visual regression of the red border kept out of automated e2e because it requires either clock control or real cards aged > N minutes; will be verified manually after merge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@ import {
|
||||
IconCheckbox,
|
||||
IconChevronDown,
|
||||
IconChevronRight,
|
||||
IconClock,
|
||||
IconDotsVertical,
|
||||
IconGripVertical,
|
||||
IconPencil,
|
||||
@@ -47,6 +48,7 @@ interface Props {
|
||||
onMoveColumnLocation: (id: string, location: "board" | "sidebar") => void;
|
||||
onDeleteColumn: (id: string) => void;
|
||||
onSetWIPLimit: (id: string, limit: number) => void;
|
||||
onSetMaxTimeMinutes: (id: string, minutes: number) => void;
|
||||
onToggleDone: (id: string, is_done: boolean) => void;
|
||||
onEditCard: (card: Card) => void;
|
||||
onDeleteCard: (id: string) => void;
|
||||
@@ -80,6 +82,7 @@ function KanbanColumnImpl({
|
||||
onMoveColumnLocation,
|
||||
onDeleteColumn,
|
||||
onSetWIPLimit,
|
||||
onSetMaxTimeMinutes,
|
||||
onToggleDone,
|
||||
onEditCard,
|
||||
onDeleteCard,
|
||||
@@ -390,6 +393,24 @@ function KanbanColumnImpl({
|
||||
>
|
||||
{column.is_done ? "Quitar marca Done" : "Marcar como Done"}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={<IconClock size={14} />}
|
||||
data-test="column-max-time"
|
||||
onClick={() => {
|
||||
const current = column.max_time_minutes || 0;
|
||||
const raw = window.prompt(
|
||||
"Tiempo maximo en minutos (0 = sin limite). Cards que pasen este tiempo en la columna mostraran borde rojo. Columnas Done no aplican.",
|
||||
String(current)
|
||||
);
|
||||
if (raw === null) return;
|
||||
const v = parseInt(raw.trim(), 10);
|
||||
const safe = Number.isFinite(v) && v >= 0 ? v : 0;
|
||||
if (safe !== current) onSetMaxTimeMinutes(column.id, safe);
|
||||
}}
|
||||
>
|
||||
Tiempo maximo
|
||||
{column.max_time_minutes > 0 ? ` (${column.max_time_minutes} min)` : ""}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={<ArchiveIcon size={14} />}
|
||||
onClick={() => onMoveColumnLocation(column.id, isInSidebar ? "board" : "sidebar")}
|
||||
@@ -435,6 +456,11 @@ function KanbanColumnImpl({
|
||||
users={users}
|
||||
assignee={c.assignee_id ? usersById.get(c.assignee_id) : undefined}
|
||||
inDoneColumn={column.is_done}
|
||||
columnOverdue={
|
||||
!column.is_done &&
|
||||
column.max_time_minutes > 0 &&
|
||||
c.time_in_column_ms > column.max_time_minutes * 60_000
|
||||
}
|
||||
highlight={highlightCardId === c.id}
|
||||
activeSticker={activeSticker}
|
||||
onAddSticker={onAddSticker}
|
||||
|
||||
Reference in New Issue
Block a user