Files
paliad/internal/handlers/checklist_instances.go
m df321acb63 feat(t-paliad-097): clickable checklist references + Vorhandene Instanzen tab
Two related checklist UX gaps:

1. Checklist events in a project's Verlauf tab were unclickable — and
   nothing in the project_events row carried the originating instance ID.
   Add an `insertProjectEventWithMeta` helper, write
   {"checklist_instance_id": <uuid>} as project_events.metadata for
   checklist_created / _renamed / _linked / _unlinked / _reset (skipped
   for _deleted — instance is gone). Surface metadata on
   /api/projects/{id}/events and on dashboard recent_activity. The
   Verlauf renderer wraps the title in <a href="/checklists/instances/{id}">
   when metadata.checklist_instance_id is present, and the dashboard's
   activity feed deep-links the project ref to the instance directly for
   checklist_* events. Existing rows (metadata `{}`) stay non-clickable —
   no migration backfill needed.

2. /checklists previously demanded a template pick before any existing
   instance was reachable. Add a tab nav (Vorlagen / Vorhandene Instanzen)
   using the existing entity-tab pattern. New endpoint
   GET /api/checklist-instances and ChecklistInstanceService.ListAllVisible
   return every visible instance across templates + projects, joined with
   project ref/title and sorted by created_at DESC. Rows show template,
   instance name (linked), project link (or "Persönlich"), progress bar,
   and created date. URL state (?tab=instances) keeps the active tab
   shareable. EN + DE i18n covered for tab labels and column headers.

Also adds event.title.checklist_* localizations for the Verlauf header
that translateEvent looks up.
2026-05-01 09:48:25 +02:00

184 lines
4.3 KiB
Go

package handlers
import (
"encoding/json"
"net/http"
"github.com/google/uuid"
"mgit.msbls.de/m/paliad/internal/services"
)
// GET /api/checklists/{slug}/instances
func handleListChecklistInstancesForTemplate(w http.ResponseWriter, r *http.Request) {
if !requireDB(w) {
return
}
uid, ok := requireUser(w, r)
if !ok {
return
}
slug := r.PathValue("slug")
rows, err := dbSvc.checklistInst.ListForTemplate(r.Context(), uid, slug)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, rows)
}
// POST /api/checklists/{slug}/instances
func handleCreateChecklistInstance(w http.ResponseWriter, r *http.Request) {
if !requireDB(w) {
return
}
uid, ok := requireUser(w, r)
if !ok {
return
}
slug := r.PathValue("slug")
var input services.CreateInstanceInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
return
}
inst, err := dbSvc.checklistInst.Create(r.Context(), uid, slug, input)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusCreated, inst)
}
// GET /api/checklists/instances/{id}
func handleGetChecklistInstance(w http.ResponseWriter, r *http.Request) {
if !requireDB(w) {
return
}
uid, ok := requireUser(w, r)
if !ok {
return
}
id, err := uuid.Parse(r.PathValue("id"))
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
return
}
inst, err := dbSvc.checklistInst.GetByID(r.Context(), uid, id)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, inst)
}
// PATCH /api/checklists/instances/{id}
func handleUpdateChecklistInstance(w http.ResponseWriter, r *http.Request) {
if !requireDB(w) {
return
}
uid, ok := requireUser(w, r)
if !ok {
return
}
id, err := uuid.Parse(r.PathValue("id"))
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
return
}
var input services.UpdateInstanceInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
return
}
inst, err := dbSvc.checklistInst.Update(r.Context(), uid, id, input)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, inst)
}
// POST /api/checklists/instances/{id}/reset
func handleResetChecklistInstance(w http.ResponseWriter, r *http.Request) {
if !requireDB(w) {
return
}
uid, ok := requireUser(w, r)
if !ok {
return
}
id, err := uuid.Parse(r.PathValue("id"))
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
return
}
inst, err := dbSvc.checklistInst.Reset(r.Context(), uid, id)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, inst)
}
// DELETE /api/checklists/instances/{id}
func handleDeleteChecklistInstance(w http.ResponseWriter, r *http.Request) {
if !requireDB(w) {
return
}
uid, ok := requireUser(w, r)
if !ok {
return
}
id, err := uuid.Parse(r.PathValue("id"))
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
return
}
if err := dbSvc.checklistInst.Delete(r.Context(), uid, id); err != nil {
writeServiceError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
// GET /api/checklist-instances — every checklist instance the user can see,
// across all templates. Powers the /checklists "Vorhandene Instanzen" tab.
func handleListAllChecklistInstances(w http.ResponseWriter, r *http.Request) {
if !requireDB(w) {
return
}
uid, ok := requireUser(w, r)
if !ok {
return
}
rows, err := dbSvc.checklistInst.ListAllVisible(r.Context(), uid)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, rows)
}
// GET /api/projects/{id}/checklists
func handleListChecklistInstancesForProject(w http.ResponseWriter, r *http.Request) {
if !requireDB(w) {
return
}
uid, ok := requireUser(w, r)
if !ok {
return
}
projectID, err := uuid.Parse(r.PathValue("id"))
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
return
}
rows, err := dbSvc.checklistInst.ListForProject(r.Context(), uid, projectID)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, rows)
}