9f4fd85db3
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>
83 lines
1.9 KiB
Go
83 lines
1.9 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// Issue 0089: tiempo maximo por columna.
|
|
|
|
func openTestDB(t *testing.T) *DB {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "test.db")
|
|
db, err := openDB(path)
|
|
if err != nil {
|
|
t.Fatalf("openDB: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
db.Close()
|
|
_ = os.Remove(path)
|
|
})
|
|
return db
|
|
}
|
|
|
|
func TestColumnMaxTimeMinutes_Defaults(t *testing.T) {
|
|
db := openTestDB(t)
|
|
|
|
c, err := db.CreateColumn("col1")
|
|
if err != nil {
|
|
t.Fatalf("CreateColumn: %v", err)
|
|
}
|
|
if c.MaxTimeMinutes != 0 {
|
|
t.Fatalf("new column max_time_minutes = %d, want 0", c.MaxTimeMinutes)
|
|
}
|
|
|
|
cols, err := db.ListColumns()
|
|
if err != nil {
|
|
t.Fatalf("ListColumns: %v", err)
|
|
}
|
|
if len(cols) == 0 || cols[0].MaxTimeMinutes != 0 {
|
|
t.Fatalf("listed col max_time_minutes = %d, want 0", cols[0].MaxTimeMinutes)
|
|
}
|
|
}
|
|
|
|
func TestColumnMaxTimeMinutes_Update(t *testing.T) {
|
|
db := openTestDB(t)
|
|
|
|
c, _ := db.CreateColumn("c")
|
|
v := 30
|
|
if err := db.UpdateColumn(c.ID, ColumnPatch{MaxTimeMinutes: &v}); err != nil {
|
|
t.Fatalf("UpdateColumn set 30: %v", err)
|
|
}
|
|
|
|
cols, _ := db.ListColumns()
|
|
if cols[0].MaxTimeMinutes != 30 {
|
|
t.Fatalf("after set max=30 got %d", cols[0].MaxTimeMinutes)
|
|
}
|
|
|
|
// Negative clamps to 0.
|
|
neg := -5
|
|
if err := db.UpdateColumn(c.ID, ColumnPatch{MaxTimeMinutes: &neg}); err != nil {
|
|
t.Fatalf("UpdateColumn neg: %v", err)
|
|
}
|
|
cols, _ = db.ListColumns()
|
|
if cols[0].MaxTimeMinutes != 0 {
|
|
t.Fatalf("negative should clamp to 0, got %d", cols[0].MaxTimeMinutes)
|
|
}
|
|
|
|
// Other fields untouched.
|
|
w := 555
|
|
if err := db.UpdateColumn(c.ID, ColumnPatch{Width: &w}); err != nil {
|
|
t.Fatalf("UpdateColumn width: %v", err)
|
|
}
|
|
cols, _ = db.ListColumns()
|
|
if cols[0].MaxTimeMinutes != 0 {
|
|
t.Fatalf("max_time should still be 0 after width update, got %d", cols[0].MaxTimeMinutes)
|
|
}
|
|
if cols[0].Width != 555 {
|
|
t.Fatalf("width = %d, want 555", cols[0].Width)
|
|
}
|
|
}
|