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:
@@ -811,6 +811,19 @@ export function App() {
|
||||
}
|
||||
}, [reload]);
|
||||
|
||||
const handleSetMaxTimeMinutes = useCallback(async (id: string, max_time_minutes: number) => {
|
||||
setBoard((prev) => {
|
||||
if (!prev) return prev;
|
||||
return { ...prev, columns: prev.columns.map((c) => (c.id === id ? { ...c, max_time_minutes } : c)) };
|
||||
});
|
||||
try {
|
||||
await api.updateColumn(id, { max_time_minutes });
|
||||
} catch (e) {
|
||||
notifications.show({ color: "red", message: (e as Error).message });
|
||||
reload();
|
||||
}
|
||||
}, [reload]);
|
||||
|
||||
const handleToggleDone = useCallback(async (id: string, is_done: boolean) => {
|
||||
setBoard((prev) => {
|
||||
if (!prev) return prev;
|
||||
@@ -995,6 +1008,7 @@ export function App() {
|
||||
onMoveColumnLocation={handleMoveColumnLocation}
|
||||
onDeleteColumn={handleDeleteColumn}
|
||||
onSetWIPLimit={handleSetWIPLimit}
|
||||
onSetMaxTimeMinutes={handleSetMaxTimeMinutes}
|
||||
onToggleDone={handleToggleDone}
|
||||
onEditCard={openEditCard}
|
||||
onDeleteCard={handleDeleteCard}
|
||||
@@ -1263,6 +1277,7 @@ export function App() {
|
||||
onMoveColumnLocation={handleMoveColumnLocation}
|
||||
onDeleteColumn={handleDeleteColumn}
|
||||
onSetWIPLimit={handleSetWIPLimit}
|
||||
onSetMaxTimeMinutes={handleSetMaxTimeMinutes}
|
||||
onToggleDone={handleToggleDone}
|
||||
onEditCard={openEditCard}
|
||||
onDeleteCard={handleDeleteCard}
|
||||
|
||||
Reference in New Issue
Block a user