Files
patrickandClaude Sonnet 5 56ab1a2d6d
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (pull_request) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Canceled after 0s
CI / Web (dms-browser) (pull_request) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (push) Canceled after 0s
CI / Web (pdf-viewer) (pull_request) Canceled after 0s
CI / Web (shl) (pull_request) Canceled after 0s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Canceled after 0s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
TSK-04: aufgaben oberflaeche
Neue additive status-Spalte (open/in_progress/done) auf tasks,
status.go (SetStatus/ByStatus/GetStatus/ListAll) haelt completed_at
synchron fuer TSK-03s escalation.Store. Neuer HTTP-Layer taskapi
(GET /tasks, PATCH /tasks/{id}/status), Tenant-Kontext-Durchsetzung
wie backchannelapi (VOR-06). Neue Next.js-Oberflaeche
web/workflow-tasks (Liste + Kanban, Drag&Drop + Tastaturbedienung).
tasks-a11y-fixture fuer den echten Browser-Test, kein Produktions-
binary vorhanden das geaendert werden musste.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-26 23:26:56 +02:00

125 lines
3.7 KiB
Go

// TSK-04: Kanban-Status je Aufgabe, additiv zu task.go (TSK-01). completed_at
// (escalation.go, TSK-03) bleibt die fuer Eskalation massgebliche Spalte --
// SetStatus haelt sie synchron, damit escalation.Store weiterhin unveraendert
// funktioniert.
package task
import (
"context"
"errors"
"fmt"
)
type Status string
const (
StatusOpen Status = "open"
StatusInProgress Status = "in_progress"
StatusDone Status = "done"
)
var ErrInvalidStatus = errors.New("task: unzulaessiger status")
func validStatus(s Status) bool {
return s == StatusOpen || s == StatusInProgress || s == StatusDone
}
// SetStatus setzt den Kanban-Status einer Aufgabe (Akzeptanzkriterium 4).
// Beim Wechsel nach "done" wird completed_at gesetzt, beim Wechsel weg davon
// wieder auf NULL zurueckgesetzt, damit escalation.Store (TSK-03) konsistent
// bleibt.
func (s *Store) SetStatus(ctx context.Context, id string, status Status) error {
if !validStatus(status) {
return ErrInvalidStatus
}
var tag interface{ RowsAffected() int64 }
var err error
if status == StatusDone {
tag, err = s.pool.Exec(ctx, `UPDATE tasks SET status = $2, completed_at = now() WHERE id = $1`, id, string(status))
} else {
tag, err = s.pool.Exec(ctx, `UPDATE tasks SET status = $2, completed_at = NULL WHERE id = $1`, id, string(status))
}
if err != nil {
return fmt.Errorf("task: status setzen: %w", err)
}
if tag.RowsAffected() == 0 {
return ErrNotFound
}
return nil
}
// ByStatus liefert alle Aufgaben mit dem angegebenen Status, sortiert nach
// Erstellung (fuer die Kanban-Spalten, Akzeptanzkriterium 4).
func (s *Store) ByStatus(ctx context.Context, status Status) ([]Task, error) {
rows, err := s.pool.Query(ctx, `SELECT id FROM tasks WHERE status = $1 ORDER BY created_at`, string(status))
if err != nil {
return nil, fmt.Errorf("task: nach status lesen: %w", err)
}
var ids []string
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
rows.Close()
return nil, fmt.Errorf("task: id scannen: %w", err)
}
ids = append(ids, id)
}
rows.Close()
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("task: iterieren: %w", err)
}
out := make([]Task, 0, len(ids))
for _, id := range ids {
t, err := s.Get(ctx, id)
if err != nil {
return nil, err
}
out = append(out, t)
}
return out, nil
}
// GetStatus liest nur den Status einer Aufgabe (fuer Praeflug-Pruefungen in
// der API-Schicht, ohne die komplette Aufgabe inkl. Referenzen zu laden).
func (s *Store) GetStatus(ctx context.Context, id string) (Status, error) {
var status string
if err := s.pool.QueryRow(ctx, `SELECT status FROM tasks WHERE id = $1`, id).Scan(&status); err != nil {
return "", ErrNotFound
}
return Status(status), nil
}
// ListAll liefert alle Aufgaben mit Status und Faelligkeit, fuer Liste und
// Kanban-Ansicht zusammen (Akzeptanzkriterien 1-3). limit/offset ist bewusst
// nicht Teil dieser Signatur -- AC3 verlangt Nutzbarkeit bei 200 Aufgaben,
// nicht Paginierung; das Frontend filtert/sortiert clientseitig ueber diese
// vollstaendige Liste.
func (s *Store) ListAll(ctx context.Context) ([]Task, error) {
rows, err := s.pool.Query(ctx, `SELECT id FROM tasks ORDER BY created_at`)
if err != nil {
return nil, fmt.Errorf("task: alle lesen: %w", err)
}
var ids []string
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
rows.Close()
return nil, fmt.Errorf("task: id scannen: %w", err)
}
ids = append(ids, id)
}
rows.Close()
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("task: iterieren: %w", err)
}
out := make([]Task, 0, len(ids))
for _, id := range ids {
t, err := s.Get(ctx, id)
if err != nil {
return nil, err
}
out = append(out, t)
}
return out, nil
}