Migration 030 adds paliad.event_types and paliad.deadline_event_types
junction. ~43 firm-wide seeds biased toward submissions (25 UPC
submissions + 8 UPC decisions/orders/hearings + 5 EPO + 4 DPMA/DE + 1
cross-jurisdiction). UPC-seeded rows carry a loose trigger_event_id
column (no FK constraint per Q2: event_types leads, trigger_events
follows). RLS policies are defense-in-depth — primary enforcement is
in the Go service layer. Per Q6, any authenticated user can create
firm-wide types; admins moderate via the soft-delete archive lever.
EventTypeService: List (firm-wide ∪ own-private), GetByID, Create
(slug auto-derived, supports diacritics → ASCII), Update (author OR
admin-on-firm-wide), SuggestSimilar (powers the duplicate-warning in
the add modal), AttachToDeadlineTx + ValidateForUser + ListForDeadlines
for the junction.
DeadlineService gains an EventTypeService dependency and now:
- accepts event_type_ids on Create / Update / CreateBulk
- attaches them in the same transaction as the deadline insert
- hydrates EventTypeIDs on every Get / List / ListForProject
- supports the multi-select Typ filter via ListFilter.EventTypeIDs +
IncludeUntyped (UNION semantics within types, AND-intersected with
Status/Project)
AgendaService gets the same Typ filter on its deadline side;
appointments are unaffected.
API:
- GET /api/event-types?category=&jurisdiction=
- GET /api/event-types/suggest?q=
- POST /api/event-types
- PATCH /api/event-types/{id} (set archive=true to hide)
- GET /api/deadlines?event_type=<uuid>,<uuid>,none
- GET /api/agenda?event_type=<uuid>,<uuid>,none
- POST/PATCH /api/deadlines accept event_type_ids: [uuid]
go build / go vet / go test ./... clean.
Frontend (picker + custom-add modal + multi-select filter) follows in
PR-2. Admin moderation panel deferred to t-paliad-089 follow-up.
308 lines
7.6 KiB
Go
308 lines
7.6 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"mgit.msbls.de/m/patholo/internal/services"
|
|
)
|
|
|
|
// GET /api/deadlines?status=overdue|this_week|upcoming|completed|pending|all&project_id=UUID&event_type=<uuid>,<uuid>,none
|
|
func handleListDeadlines(w http.ResponseWriter, r *http.Request) {
|
|
if !requireDB(w) {
|
|
return
|
|
}
|
|
uid, ok := requireUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
filter := services.ListFilter{
|
|
Status: services.DeadlineStatusFilter(r.URL.Query().Get("status")),
|
|
}
|
|
// Accept both project_id (new) and project_id (legacy alias).
|
|
raw := r.URL.Query().Get("project_id")
|
|
if raw == "" {
|
|
raw = r.URL.Query().Get("project_id")
|
|
}
|
|
if raw != "" {
|
|
projectID, err := uuid.Parse(raw)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid project_id"})
|
|
return
|
|
}
|
|
filter.ProjectID = &projectID
|
|
}
|
|
ids, untyped, err := parseEventTypeFilter(r.URL.Query().Get("event_type"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
filter.EventTypeIDs = ids
|
|
filter.IncludeUntyped = untyped
|
|
|
|
rows, err := dbSvc.deadline.ListVisibleForUser(r.Context(), uid, filter)
|
|
if err != nil {
|
|
writeServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, rows)
|
|
}
|
|
|
|
// parseEventTypeFilter parses the comma-separated `event_type` query
|
|
// parameter used by both /api/deadlines and /api/agenda. The literal
|
|
// keyword "none" is the toggle for "deadlines without any Event Type
|
|
// attached" (filter.IncludeUntyped). All other tokens must be valid
|
|
// UUIDs of visible event_types — the service layer validates visibility.
|
|
//
|
|
// Returns (ids, includeUntyped, err). Empty/blank input returns
|
|
// (nil, false, nil) — interpret as "no filter".
|
|
func parseEventTypeFilter(raw string) ([]uuid.UUID, bool, error) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return nil, false, nil
|
|
}
|
|
ids := []uuid.UUID{}
|
|
includeUntyped := false
|
|
for tok := range strings.SplitSeq(raw, ",") {
|
|
t := strings.TrimSpace(tok)
|
|
if t == "" {
|
|
continue
|
|
}
|
|
if t == "none" {
|
|
includeUntyped = true
|
|
continue
|
|
}
|
|
id, err := uuid.Parse(t)
|
|
if err != nil {
|
|
return nil, false, &agendaErr{msg: "invalid event_type id " + t}
|
|
}
|
|
ids = append(ids, id)
|
|
}
|
|
return ids, includeUntyped, nil
|
|
}
|
|
|
|
// GET /api/deadlines/summary?project_id=UUID
|
|
func handleDeadlinesSummary(w http.ResponseWriter, r *http.Request) {
|
|
if !requireDB(w) {
|
|
return
|
|
}
|
|
uid, ok := requireUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
var projectIDPtr *uuid.UUID
|
|
raw := r.URL.Query().Get("project_id")
|
|
if raw == "" {
|
|
raw = r.URL.Query().Get("project_id")
|
|
}
|
|
if raw != "" {
|
|
projectID, err := uuid.Parse(raw)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid project_id"})
|
|
return
|
|
}
|
|
projectIDPtr = &projectID
|
|
}
|
|
c, err := dbSvc.deadline.SummaryCounts(r.Context(), uid, projectIDPtr)
|
|
if err != nil {
|
|
writeServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, c)
|
|
}
|
|
|
|
// GET /api/projects/{id}/deadlines
|
|
func handleListDeadlinesForProject(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.deadline.ListForProject(r.Context(), uid, projectID)
|
|
if err != nil {
|
|
writeServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, rows)
|
|
}
|
|
|
|
// POST /api/projects/{id}/deadlines
|
|
func handleCreateDeadline(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
|
|
}
|
|
var input services.CreateDeadlineInput
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
|
return
|
|
}
|
|
f, err := dbSvc.deadline.Create(r.Context(), uid, projectID, input)
|
|
if err != nil {
|
|
writeServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, f)
|
|
}
|
|
|
|
// POST /api/projects/{id}/deadlines/bulk — Fristenrechner "save to Project".
|
|
func handleBulkCreateDeadlines(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
|
|
}
|
|
var body struct {
|
|
Deadlines []services.CreateDeadlineInput `json:"deadlines"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
|
return
|
|
}
|
|
rows, err := dbSvc.deadline.CreateBulk(r.Context(), uid, projectID, body.Deadlines)
|
|
if err != nil {
|
|
writeServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, rows)
|
|
}
|
|
|
|
// GET /api/deadlines/{id}
|
|
func handleGetDeadline(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
|
|
}
|
|
f, err := dbSvc.deadline.GetByID(r.Context(), uid, id)
|
|
if err != nil {
|
|
writeServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, f)
|
|
}
|
|
|
|
// PATCH /api/deadlines/{id}
|
|
func handleUpdateDeadline(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.UpdateDeadlineInput
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
|
return
|
|
}
|
|
f, err := dbSvc.deadline.Update(r.Context(), uid, id, input)
|
|
if err != nil {
|
|
writeServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, f)
|
|
}
|
|
|
|
// PATCH /api/deadlines/{id}/complete — convenience endpoint for the list-row checkbox.
|
|
func handleCompleteDeadline(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
|
|
}
|
|
f, err := dbSvc.deadline.Complete(r.Context(), uid, id)
|
|
if err != nil {
|
|
writeServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, f)
|
|
}
|
|
|
|
// PATCH /api/deadlines/{id}/reopen — admin/project-lead-only undo of complete.
|
|
func handleReopenDeadline(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
|
|
}
|
|
f, err := dbSvc.deadline.Reopen(r.Context(), uid, id)
|
|
if err != nil {
|
|
writeServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, f)
|
|
}
|
|
|
|
// DELETE /api/deadlines/{id}
|
|
func handleDeleteDeadline(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.deadline.Delete(r.Context(), uid, id); err != nil {
|
|
writeServiceError(w, err)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|