diff --git a/backend/internal/web/handlers_tasks.go b/backend/internal/web/handlers_tasks.go index d23bd4e..e92e161 100644 --- a/backend/internal/web/handlers_tasks.go +++ b/backend/internal/web/handlers_tasks.go @@ -40,6 +40,19 @@ func parseTaskStatus(s string) sqlc.TaskStatus { return sqlc.TaskStatusTodo } +func loadEtapesForTaskForm(w http.ResponseWriter, r *http.Request, q *sqlc.Queries, tabloID uuid.UUID) ([]sqlc.Etape, bool) { + etapes, err := q.ListEtapesByTablo(r.Context(), tabloID) + if err != nil { + slog.Default().Error("tasks form: ListEtapesByTablo failed", "tablo_id", tabloID, "err", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return nil, false + } + if etapes == nil { + etapes = []sqlc.Etape{} + } + return etapes, true +} + // loadOwnedTabloForTask is the shared preamble for all /tablos/{id}/tasks/{task_id}* // handlers. It calls loadOwnedTablo for tablo ownership verification, then parses // the {task_id} URL param and fetches the task (verifying it belongs to the tablo). @@ -85,6 +98,10 @@ func TaskNewFormHandler(deps TasksDeps) http.HandlerFunc { } statusStr := r.URL.Query().Get("status") status := parseTaskStatus(statusStr) + etapes, ok := loadEtapesForTaskForm(w, r, deps.Queries, tablo.ID) + if !ok { + return + } filter := templates.EtapeFilter{Kind: templates.EtapeFilterAll} if rawEtape := strings.TrimSpace(r.URL.Query().Get("etape")); rawEtape != "" { if rawEtape == "unassigned" { @@ -104,6 +121,7 @@ func TaskNewFormHandler(deps TasksDeps) http.HandlerFunc { templates.TaskCreateErrors{}, csrf.Token(r), filter, + etapes, ).Render(r.Context(), w) } } @@ -169,6 +187,10 @@ func TaskCreateHandler(deps TasksDeps) http.HandlerFunc { if etapeID.Valid { filter = templates.EtapeFilter{Kind: templates.EtapeFilterEtape, EtapeID: uuid.UUID(etapeID.Bytes)} } + etapes, ok := loadEtapesForTaskForm(w, r, deps.Queries, tablo.ID) + if !ok { + return + } var errs templates.TaskCreateErrors if title == "" { @@ -192,6 +214,7 @@ func TaskCreateHandler(deps TasksDeps) http.HandlerFunc { errs, csrf.Token(r), filter, + etapes, ).Render(ctx, w) return } @@ -217,6 +240,7 @@ func TaskCreateHandler(deps TasksDeps) http.HandlerFunc { errs, csrf.Token(r), filter, + etapes, ).Render(ctx, w) return } @@ -239,6 +263,7 @@ func TaskCreateHandler(deps TasksDeps) http.HandlerFunc { errs, csrf.Token(r), filter, + etapes, ).Render(ctx, w) return } @@ -267,6 +292,7 @@ func TaskCreateHandler(deps TasksDeps) http.HandlerFunc { errs, csrf.Token(r), filter, + etapes, ).Render(ctx, w) return } @@ -354,10 +380,15 @@ func TaskEditHandler(deps TasksDeps) http.HandlerFunc { if !ok { return } + etapes, ok := loadEtapesForTaskForm(w, r, deps.Queries, tablo.ID) + if !ok { + return + } w.Header().Set("Content-Type", "text/html; charset=utf-8") _ = templates.TaskEditFragment( tablo.ID, task, + etapes, templates.TaskUpdateForm{ Title: task.Title, Description: task.Description.String, @@ -392,6 +423,19 @@ func TaskUpdateHandler(deps TasksDeps) http.HandlerFunc { title := strings.TrimSpace(r.PostFormValue("title")) description := strings.TrimSpace(r.PostFormValue("description")) + etapes, ok := loadEtapesForTaskForm(w, r, deps.Queries, tablo.ID) + if !ok { + return + } + etapeID, _, err := parseOwnedEtapeID(r, deps.Queries, tablo.ID, r.PostFormValue("etape_id")) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + http.NotFound(w, r) + return + } + http.Error(w, "bad request", http.StatusBadRequest) + return + } var errs templates.TaskUpdateErrors if title == "" { @@ -407,6 +451,7 @@ func TaskUpdateHandler(deps TasksDeps) http.HandlerFunc { _ = templates.TaskEditFragment( tablo.ID, task, + etapes, templates.TaskUpdateForm{Title: title, Description: description}, errs, csrf.Token(r), @@ -424,7 +469,7 @@ func TaskUpdateHandler(deps TasksDeps) http.HandlerFunc { Description: pgtype.Text{String: description, Valid: description != ""}, Status: task.Status, Position: task.Position, - EtapeID: task.EtapeID, + EtapeID: etapeID, }) if err != nil { slog.Default().Error("tasks update: UpdateTask failed", "id", task.ID, "err", err) @@ -435,6 +480,7 @@ func TaskUpdateHandler(deps TasksDeps) http.HandlerFunc { _ = templates.TaskEditFragment( tablo.ID, task, + etapes, templates.TaskUpdateForm{Title: title, Description: description}, errs, csrf.Token(r), diff --git a/backend/templates/tasks.templ b/backend/templates/tasks.templ index 8ddb10f..390bc9d 100644 --- a/backend/templates/tasks.templ +++ b/backend/templates/tasks.templ @@ -170,8 +170,9 @@ templ TaskCard(tabloID uuid.UUID, task sqlc.Task, csrfToken string) { // 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) { +templ TaskEditFragment(tabloID uuid.UUID, task sqlc.Task, etapes []sqlc.Etape, form TaskUpdateForm, errs TaskUpdateErrors, csrfToken string) {
+ {{ selectedEtapeID := taskEtapeIDString(task.EtapeID) }}
@FieldError(errs.Title)
+
+ + +