PR-1 of the Fristen+Termine unification (t-paliad-110). Backend layer
only — no frontend changes; the existing /deadlines and /appointments
pages still render the type-specific UIs.
EventService delegates to DeadlineService + AppointmentService for the
actual reads (no duplicate visibility logic, no duplicate event_type
hydration), then projects both into the discriminated EventListItem
union and merges/sorts by event_date asc. The handler exposes:
GET /api/events?type=deadline|appointment|all&status=…&project_id=…
&event_type=…&type_filter=…&from=…&to=…
GET /api/events/summary?type=…&project_id=…
Bucket model (per t-paliad-110 spec, supersedes t-106):
- four universal cards: Heute · Diese Woche · Nächste Woche · Später
- Überfällig is deadline-only, conditional, alarm-styled when > 0
- Erledigt drops from the card row; stays available as a filter option
- appointments have no completed_at — past appointments aren't bucketed
The deadline-side cutoffs reuse computeDeadlineBucketBounds so
/api/events/summary and /api/deadlines/summary can never disagree.
Existing /api/deadlines and /api/appointments stay untouched —
calendars, project-detail panes, and CalDAV consumers still call them
directly.
128 lines
3.5 KiB
Go
128 lines
3.5 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"mgit.msbls.de/m/paliad/internal/services"
|
|
)
|
|
|
|
// GET /api/events?type=deadline|appointment|all&status=…&project_id=…&event_type=…&type_filter=…&from=…&to=…
|
|
//
|
|
// type — discriminator for the union; default "all" (Beides on the
|
|
// front-end). When "deadline" or "appointment", the matching
|
|
// type-specific filters take effect; the others are ignored.
|
|
// status — DeadlineStatusFilter (deadline-only).
|
|
// project_id — single project scope.
|
|
// event_type — comma-separated event_type uuids; the literal "none"
|
|
// toggles include-untyped (deadlines only).
|
|
// type_filter — appointment_type filter (hearing/meeting/...). Named
|
|
// `type_filter` in the query string to avoid clashing with
|
|
// the `type` discriminator above.
|
|
// from / to — date window applied to the canonical event_date.
|
|
func handleListEvents(w http.ResponseWriter, r *http.Request) {
|
|
if !requireDB(w) {
|
|
return
|
|
}
|
|
uid, ok := requireUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
q := r.URL.Query()
|
|
|
|
filter := services.EventListFilter{
|
|
Type: parseEventTypeDiscriminator(q.Get("type")),
|
|
Status: services.DeadlineStatusFilter(q.Get("status")),
|
|
}
|
|
|
|
if raw := q.Get("project_id"); 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(q.Get("event_type"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
filter.EventTypeIDs = ids
|
|
filter.IncludeUntyped = untyped
|
|
|
|
if raw := q.Get("type_filter"); raw != "" {
|
|
filter.AppointmentType = &raw
|
|
}
|
|
if raw := q.Get("from"); raw != "" {
|
|
t, err := parseDateOrTime(raw)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid from"})
|
|
return
|
|
}
|
|
filter.From = &t
|
|
}
|
|
if raw := q.Get("to"); raw != "" {
|
|
t, err := parseDateOrTime(raw)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid to"})
|
|
return
|
|
}
|
|
filter.To = &t
|
|
}
|
|
|
|
rows, err := dbSvc.event.ListVisibleForUser(r.Context(), uid, filter)
|
|
if err != nil {
|
|
writeServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, rows)
|
|
}
|
|
|
|
// GET /api/events/summary?type=deadline|appointment|all&project_id=…
|
|
func handleEventsSummary(w http.ResponseWriter, r *http.Request) {
|
|
if !requireDB(w) {
|
|
return
|
|
}
|
|
uid, ok := requireUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
q := r.URL.Query()
|
|
|
|
filter := services.EventSummaryFilter{
|
|
Type: parseEventTypeDiscriminator(q.Get("type")),
|
|
}
|
|
if raw := q.Get("project_id"); raw != "" {
|
|
projectID, err := uuid.Parse(raw)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid project_id"})
|
|
return
|
|
}
|
|
filter.ProjectID = &projectID
|
|
}
|
|
|
|
c, err := dbSvc.event.SummaryCounts(r.Context(), uid, filter)
|
|
if err != nil {
|
|
writeServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, c)
|
|
}
|
|
|
|
// parseEventTypeDiscriminator maps the user-facing `type=` query value
|
|
// onto the EventTypeFilter constants. Empty / "all" / unknown all degrade
|
|
// to "both" so a typo or older client doesn't 4xx.
|
|
func parseEventTypeDiscriminator(raw string) services.EventTypeFilter {
|
|
switch raw {
|
|
case "deadline":
|
|
return services.EventTypeDeadline
|
|
case "appointment":
|
|
return services.EventTypeAppointment
|
|
default:
|
|
return services.EventTypeAll
|
|
}
|
|
}
|