package templates import ( "backend/internal/db/sqlc" "backend/internal/web/ui" "github.com/google/uuid" "strconv" ) // groupTasksByStatus groups a flat task slice into a map keyed by TaskStatus. // Returns a map; missing statuses return nil (empty) slices. func groupTasksByStatus(tasks []sqlc.Task) map[sqlc.TaskStatus][]sqlc.Task { result := make(map[sqlc.TaskStatus][]sqlc.Task, len(TaskColumns)) for _, t := range tasks { result[t.Status] = append(result[t.Status], t) } return result } // 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) { {{ grouped := groupTasksByStatus(tasks) }}
for _, status := range TaskColumns { @KanbanColumn(tabloID, status, grouped[status], csrfToken, filter) }
} // KanbanColumn renders a single kanban column: header, task list, and add-task slot. // UI-SPEC §1 and §2. templ KanbanColumn(tabloID uuid.UUID, status sqlc.TaskStatus, tasks []sqlc.Task, csrfToken string, filter EtapeFilter) {

{ TaskColumnLabels[status] }

@ui.Badge(ui.BadgeProps{Label: strconv.Itoa(len(tasks)), Variant: ui.BadgeVariantInfo})
if len(tasks) == 0 {

No tasks yet

} for _, task := range tasks { @TaskCard(tabloID, task, csrfToken) }
@AddTaskTrigger(tabloID, status, csrfToken, filter)
} // TaskCard renders a single task card. The outer wrapper carries class "task-card-zone" // and id="task-{task.ID}" so HTMX outerHTML swaps round-trip cleanly. // UI-SPEC §4 and D-08. templ TaskCard(tabloID uuid.UUID, task sqlc.Task, csrfToken string) {

{ task.Title }

} // TaskEditFragment renders the inline edit form for an existing task. // The outer wrapper carries class="task-card-zone" id="task-{task.ID}" so // HTMX outerHTML swaps round-trip cleanly with TaskCard (TASK-03). // UI-SPEC §3. templ TaskEditFragment(tabloID uuid.UUID, task sqlc.Task, form TaskUpdateForm, errs TaskUpdateErrors, csrfToken string) {
@ui.CSRFField(csrfToken)
@FieldError(errs.Title)
if errs.General != "" { @FieldError(errs.General) }
@ui.Button(ui.ButtonProps{ Label: "Save changes", Variant: ui.ButtonVariantDefault, Tone: ui.ButtonToneSolid, Size: ui.SizeMD, Type: "submit", }) @ui.Button(ui.ButtonProps{ Label: "Discard changes", Variant: ui.ButtonVariantNeutral, Tone: ui.ButtonToneSoft, Size: ui.SizeMD, Type: "button", Attrs: templ.Attributes{ "hx-get": "/tablos/" + tabloID.String() + "/tasks/" + task.ID.String() + "/show", "hx-target": "closest .task-card-zone", "hx-swap": "outerHTML", }, })
} // TaskCreateFormFragment renders the inline create form shown when a user clicks // "+ Add task". Targets #column-{status} for HTMX beforeend swap on submit. // UI-SPEC §2. templ TaskCreateFormFragment(tabloID uuid.UUID, status sqlc.TaskStatus, form TaskCreateForm, errs TaskCreateErrors, csrfToken string, filter EtapeFilter) {
@ui.CSRFField(csrfToken)
@FieldError(errs.Title)
@ui.Button(ui.ButtonProps{ Label: "Save", Variant: ui.ButtonVariantDefault, Tone: ui.ButtonToneSolid, Size: ui.SizeMD, Type: "submit", }) @ui.Button(ui.ButtonProps{ Label: "Discard", Variant: ui.ButtonVariantNeutral, Tone: ui.ButtonToneSoft, Size: ui.SizeMD, Type: "button", Attrs: templ.Attributes{ "hx-get": "/tablos/" + tabloID.String() + "/tasks/cancel-new?status=" + string(status) + filter.QuerySuffix(), "hx-target": "#add-task-slot-" + string(status), "hx-swap": "innerHTML", }, })
} // TaskDeleteConfirmFragment renders the delete confirmation dialog for a task. // Carries class "task-card-zone" so outerHTML round-trips work correctly. // UI-SPEC §5. templ TaskDeleteConfirmFragment(tabloID uuid.UUID, task sqlc.Task, csrfToken string) {

Delete task?

This cannot be undone.

@ui.CSRFField(csrfToken) @ui.Button(ui.ButtonProps{ Label: "Yes, delete", Variant: ui.ButtonVariantDanger, Tone: ui.ButtonToneSolid, Size: ui.SizeMD, Type: "submit", Attrs: templ.Attributes{ "aria-label": "Confirm delete task", }, })
@ui.Button(ui.ButtonProps{ Label: "Keep task", Variant: ui.ButtonVariantNeutral, Tone: ui.ButtonToneSoft, Size: ui.SizeMD, Type: "button", Attrs: templ.Attributes{ "hx-get": "/tablos/" + tabloID.String() + "/tasks/" + task.ID.String() + "/show", "hx-target": "closest .task-card-zone", "hx-swap": "outerHTML", "aria-label": "Keep task", }, })
} // AddTaskTrigger renders the "+ Add task" button that expands to TaskCreateFormFragment. // Targets #add-task-slot-{status} for innerHTML replacement. // UI-SPEC §2. templ AddTaskTrigger(tabloID uuid.UUID, status sqlc.TaskStatus, csrfToken string, filter EtapeFilter) { } // TaskCardGone renders an empty zone div with the task's id so HTMX outerHTML // swap removes the card from the DOM after a successful delete (TASK-06). templ TaskCardGone(taskID uuid.UUID) {
} // TaskCardOOB renders a new TaskCard AND an OOB swap that resets the add-task // slot to AddTaskTrigger. Used by TaskCreateHandler to perform both operations // in a single HTMX response. // D-08/UI-SPEC §2: OOB swap resets #add-task-slot-{status} after create. templ TaskCardOOB(status sqlc.TaskStatus, task sqlc.Task, tabloID uuid.UUID, csrfToken string, filter EtapeFilter) { @TaskCard(tabloID, task, csrfToken)
@AddTaskTrigger(tabloID, status, csrfToken, filter)
}