From 084fc0ebba7ae1e9befa351da930468777a2c873 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sat, 16 May 2026 23:42:40 +0200 Subject: [PATCH] feat(16-03): restyle kanban board with etape grouping (tasks.templ) - Add EtapeGroup type and groupTasksByEtape helper (etape declaration order, unassigned last) - Add EtapeGroupHeader templ component with color dot and muted label for unassigned - Update KanbanBoard and KanbanColumn signatures to accept etapes []sqlc.Etape (5th param) - Restyle KanbanColumn: kanban-column > tasks-section > tasks-section-header/task-list layout - Restyle TaskCard: task-row task-card with task-check + task-body + @ui.IconButton(trash) - Restyle AddTaskTrigger: tasks-add-button class (replaces ui-button compound classes) - Remove @EtapeStrip OOB calls from TaskCardGone and TaskCardOOB; keep params with TODO --- backend/templates/tasks.templ | 176 ++++++++++++++++++++++++---------- 1 file changed, 125 insertions(+), 51 deletions(-) diff --git a/backend/templates/tasks.templ b/backend/templates/tasks.templ index 3883fd0..808af5f 100644 --- a/backend/templates/tasks.templ +++ b/backend/templates/tasks.templ @@ -17,10 +17,76 @@ func groupTasksByStatus(tasks []sqlc.Task) map[sqlc.TaskStatus][]sqlc.Task { return result } +// EtapeGroup holds tasks for one etape within a kanban column. +type EtapeGroup struct { + EtapeID string // "" for unassigned + EtapeTitle string // "No etape" for unassigned + EtapeColor string // "" for unassigned + Tasks []sqlc.Task +} + +// groupTasksByEtape groups tasks by etape in declaration order; unassigned last. +// Model: groupTasksByStatus above. Uses uuid.UUID(id.Bytes).String() same as taskEtapeIDString. +func groupTasksByEtape(tasks []sqlc.Task, etapes []sqlc.Etape) []EtapeGroup { + // Collect tasks per etape ID string + tasksByEtapeID := make(map[string][]sqlc.Task) + var unassigned []sqlc.Task + + for _, t := range tasks { + if !t.EtapeID.Valid { + unassigned = append(unassigned, t) + } else { + keyStr := uuid.UUID(t.EtapeID.Bytes).String() + tasksByEtapeID[keyStr] = append(tasksByEtapeID[keyStr], t) + } + } + + var groups []EtapeGroup + for _, etape := range etapes { + id := etape.ID.String() + ts := tasksByEtapeID[id] + if len(ts) == 0 { + continue + } + groups = append(groups, EtapeGroup{ + EtapeID: id, + EtapeTitle: etape.Title, + EtapeColor: "", // Etape struct has no Color field in this schema + Tasks: ts, + }) + } + + if len(unassigned) > 0 { + groups = append(groups, EtapeGroup{ + EtapeID: "", + EtapeTitle: "No etape", + EtapeColor: "", + Tasks: unassigned, + }) + } + + return groups +} + +// EtapeGroupHeader renders the sub-heading row for an etape group within a kanban column. +// "No etape" / unassigned group omits the color dot and uses muted label style. +templ EtapeGroupHeader(group EtapeGroup) { +
+ if group.EtapeColor != "" { + + } + if group.EtapeID == "" { + { group.EtapeTitle } + } else { + { group.EtapeTitle } + } +
+} + // KanbanBoard renders the outer board container with 4 columns and a hidden // reorder form. Used by TabloDetailPage below the tablo header section. // UI-SPEC §1 and D-08. -templ KanbanBoard(tabloID uuid.UUID, csrfToken string, tasks []sqlc.Task, filter EtapeFilter) { +templ KanbanBoard(tabloID uuid.UUID, csrfToken string, tasks []sqlc.Task, filter EtapeFilter, etapes []sqlc.Etape) { {{ grouped := groupTasksByStatus(tasks) }}
for _, status := range TaskColumns { - @KanbanColumn(tabloID, status, grouped[status], csrfToken, filter) + @KanbanColumn(tabloID, status, grouped[status], csrfToken, filter, etapes) }