776 lines
20 KiB
Go
776 lines
20 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
type CalendarEvent struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
Start string `json:"start"`
|
|
End string `json:"end"`
|
|
AllDay bool `json:"allDay"`
|
|
Location string `json:"location"`
|
|
Description string `json:"description"`
|
|
Color string `json:"color"`
|
|
Recurrence string `json:"recurrence"`
|
|
// recurrenceWeekday verwendet time.Weekday/JavaScript-Date.getDay(): 0=Sonntag.
|
|
RecurrenceWeekday *int `json:"recurrenceWeekday,omitempty"`
|
|
RecurrenceOrdinal *int `json:"recurrenceOrdinal,omitempty"`
|
|
RecurrenceExceptions []string `json:"recurrenceExceptions"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
}
|
|
|
|
type CalendarEventRequest struct {
|
|
Title string `json:"title"`
|
|
Start string `json:"start"`
|
|
End string `json:"end"`
|
|
AllDay bool `json:"allDay"`
|
|
Location string `json:"location"`
|
|
Description string `json:"description"`
|
|
Color string `json:"color"`
|
|
Recurrence string `json:"recurrence"`
|
|
RecurrenceWeekday *int `json:"recurrenceWeekday"`
|
|
RecurrenceOrdinal *int `json:"recurrenceOrdinal"`
|
|
RecurrenceExceptions []string `json:"recurrenceExceptions"`
|
|
}
|
|
|
|
type CalendarEventOccurrenceRequest struct {
|
|
OccurrenceStart string `json:"occurrenceStart"`
|
|
Event CalendarEventRequest `json:"event"`
|
|
}
|
|
|
|
const calendarEventDateTimeLayout = "2006-01-02T15:04"
|
|
|
|
var allowedCalendarEventColors = map[string]bool{
|
|
"indigo": true,
|
|
"blue": true,
|
|
"emerald": true,
|
|
"amber": true,
|
|
"rose": true,
|
|
"slate": true,
|
|
}
|
|
|
|
var allowedCalendarEventRecurrences = map[string]bool{
|
|
"none": true,
|
|
"daily": true,
|
|
"weekly": true,
|
|
"monthly": true,
|
|
"monthlyNthWeekday": true,
|
|
"yearly": true,
|
|
}
|
|
|
|
func (s *Server) ensureCalendarEventsTable(ctx context.Context) error {
|
|
queries := []string{
|
|
`CREATE EXTENSION IF NOT EXISTS pgcrypto`,
|
|
`
|
|
CREATE TABLE IF NOT EXISTS calendar_events (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
title TEXT NOT NULL,
|
|
start_at TIMESTAMP NOT NULL,
|
|
end_at TIMESTAMP NOT NULL,
|
|
all_day BOOLEAN NOT NULL DEFAULT false,
|
|
location TEXT NOT NULL DEFAULT '',
|
|
description TEXT NOT NULL DEFAULT '',
|
|
color TEXT NOT NULL DEFAULT 'indigo'
|
|
CHECK (color IN ('indigo', 'blue', 'emerald', 'amber', 'rose', 'slate')),
|
|
recurrence TEXT NOT NULL DEFAULT 'none',
|
|
recurrence_weekday INTEGER,
|
|
recurrence_ordinal INTEGER,
|
|
recurrence_exceptions JSONB NOT NULL DEFAULT '[]'::JSONB,
|
|
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
updated_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
CONSTRAINT calendar_events_valid_range CHECK (end_at > start_at)
|
|
)
|
|
`,
|
|
`
|
|
DO $$
|
|
DECLARE
|
|
constraint_name TEXT;
|
|
BEGIN
|
|
FOR constraint_name IN
|
|
SELECT c.conname
|
|
FROM pg_constraint c
|
|
JOIN pg_class t ON t.oid = c.conrelid
|
|
WHERE t.relname = 'calendar_events'
|
|
AND c.contype = 'c'
|
|
AND pg_get_constraintdef(c.oid) ILIKE '%recurrence%'
|
|
LOOP
|
|
EXECUTE format('ALTER TABLE calendar_events DROP CONSTRAINT %I', constraint_name);
|
|
END LOOP;
|
|
END
|
|
$$
|
|
`,
|
|
`ALTER TABLE IF EXISTS calendar_events ADD COLUMN IF NOT EXISTS recurrence_weekday INTEGER`,
|
|
`ALTER TABLE IF EXISTS calendar_events ADD COLUMN IF NOT EXISTS recurrence_ordinal INTEGER`,
|
|
`ALTER TABLE IF EXISTS calendar_events ADD COLUMN IF NOT EXISTS recurrence_exceptions JSONB NOT NULL DEFAULT '[]'::JSONB`,
|
|
`UPDATE calendar_events SET recurrence_exceptions = '[]'::JSONB WHERE recurrence_exceptions IS NULL`,
|
|
`CREATE INDEX IF NOT EXISTS idx_calendar_events_start_at ON calendar_events(start_at)`,
|
|
}
|
|
|
|
for _, query := range queries {
|
|
if _, err := s.db.Exec(ctx, query); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseCalendarEventDateTime(value string) (time.Time, error) {
|
|
value = strings.TrimSpace(value)
|
|
layouts := []string{
|
|
calendarEventDateTimeLayout,
|
|
"2006-01-02T15:04:05",
|
|
time.RFC3339,
|
|
}
|
|
|
|
for _, layout := range layouts {
|
|
parsed, err := time.ParseInLocation(layout, value, time.Local)
|
|
if err == nil {
|
|
return parsed, nil
|
|
}
|
|
}
|
|
|
|
return time.Time{}, errors.New("invalid datetime")
|
|
}
|
|
|
|
func cleanCalendarEventExceptions(values []string) ([]string, error) {
|
|
result := []string{}
|
|
seen := map[string]bool{}
|
|
|
|
for _, value := range values {
|
|
parsed, err := parseCalendarEventDateTime(value)
|
|
if err != nil {
|
|
return nil, errors.New("Eine Wiederholungs-Ausnahme ist ungültig")
|
|
}
|
|
|
|
normalized := formatCalendarEventDateTime(parsed)
|
|
if seen[normalized] {
|
|
continue
|
|
}
|
|
|
|
seen[normalized] = true
|
|
result = append(result, normalized)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func appendCalendarEventException(values []string, occurrenceStart string) ([]string, error) {
|
|
next := append([]string{}, values...)
|
|
next = append(next, occurrenceStart)
|
|
return cleanCalendarEventExceptions(next)
|
|
}
|
|
|
|
func normalizeCalendarEventRequest(input CalendarEventRequest) (CalendarEventRequest, time.Time, time.Time, error) {
|
|
input.Title = strings.TrimSpace(input.Title)
|
|
input.Location = strings.TrimSpace(input.Location)
|
|
input.Description = strings.TrimSpace(input.Description)
|
|
input.Color = strings.TrimSpace(input.Color)
|
|
input.Recurrence = strings.TrimSpace(input.Recurrence)
|
|
|
|
if input.Title == "" {
|
|
return input, time.Time{}, time.Time{}, errors.New("Bitte gib einen Titel ein")
|
|
}
|
|
|
|
if len([]rune(input.Title)) > 120 {
|
|
return input, time.Time{}, time.Time{}, errors.New("Der Titel darf maximal 120 Zeichen lang sein")
|
|
}
|
|
|
|
if len([]rune(input.Location)) > 160 {
|
|
return input, time.Time{}, time.Time{}, errors.New("Der Ort darf maximal 160 Zeichen lang sein")
|
|
}
|
|
|
|
if len([]rune(input.Description)) > 1000 {
|
|
return input, time.Time{}, time.Time{}, errors.New("Die Beschreibung darf maximal 1000 Zeichen lang sein")
|
|
}
|
|
|
|
if input.Color == "" {
|
|
input.Color = "indigo"
|
|
}
|
|
if !allowedCalendarEventColors[input.Color] {
|
|
return input, time.Time{}, time.Time{}, errors.New("Ungültige Terminfarbe")
|
|
}
|
|
|
|
if input.Recurrence == "" {
|
|
input.Recurrence = "none"
|
|
}
|
|
if !allowedCalendarEventRecurrences[input.Recurrence] {
|
|
return input, time.Time{}, time.Time{}, errors.New("Ungültige Wiederholung")
|
|
}
|
|
|
|
if input.Recurrence == "monthlyNthWeekday" {
|
|
if input.RecurrenceWeekday == nil ||
|
|
*input.RecurrenceWeekday < 0 ||
|
|
*input.RecurrenceWeekday > 6 {
|
|
return input, time.Time{}, time.Time{}, errors.New("Bitte wähle einen gültigen Wochentag für die Wiederholung")
|
|
}
|
|
|
|
if input.RecurrenceOrdinal == nil ||
|
|
*input.RecurrenceOrdinal < 1 ||
|
|
*input.RecurrenceOrdinal > 5 {
|
|
return input, time.Time{}, time.Time{}, errors.New("Bitte wähle eine gültige Woche im Monat")
|
|
}
|
|
} else {
|
|
input.RecurrenceWeekday = nil
|
|
input.RecurrenceOrdinal = nil
|
|
}
|
|
|
|
exceptions, err := cleanCalendarEventExceptions(input.RecurrenceExceptions)
|
|
if err != nil {
|
|
return input, time.Time{}, time.Time{}, err
|
|
}
|
|
input.RecurrenceExceptions = exceptions
|
|
|
|
start, err := parseCalendarEventDateTime(input.Start)
|
|
if err != nil {
|
|
return input, time.Time{}, time.Time{}, errors.New("Der Startzeitpunkt ist ungültig")
|
|
}
|
|
|
|
end, err := parseCalendarEventDateTime(input.End)
|
|
if err != nil {
|
|
return input, time.Time{}, time.Time{}, errors.New("Der Endzeitpunkt ist ungültig")
|
|
}
|
|
|
|
if !end.After(start) {
|
|
return input, time.Time{}, time.Time{}, errors.New("Das Ende muss nach dem Beginn liegen")
|
|
}
|
|
|
|
return input, start, end, nil
|
|
}
|
|
|
|
func formatCalendarEventDateTime(value time.Time) string {
|
|
return value.Format(calendarEventDateTimeLayout)
|
|
}
|
|
|
|
func scanCalendarEvent(row pgx.Row) (CalendarEvent, error) {
|
|
var event CalendarEvent
|
|
var start time.Time
|
|
var end time.Time
|
|
var rawExceptions []byte
|
|
|
|
err := row.Scan(
|
|
&event.ID,
|
|
&event.Title,
|
|
&start,
|
|
&end,
|
|
&event.AllDay,
|
|
&event.Location,
|
|
&event.Description,
|
|
&event.Color,
|
|
&event.Recurrence,
|
|
&event.RecurrenceWeekday,
|
|
&event.RecurrenceOrdinal,
|
|
&rawExceptions,
|
|
&event.CreatedAt,
|
|
&event.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return CalendarEvent{}, err
|
|
}
|
|
|
|
event.Start = formatCalendarEventDateTime(start)
|
|
event.End = formatCalendarEventDateTime(end)
|
|
event.RecurrenceExceptions = []string{}
|
|
if len(rawExceptions) > 0 {
|
|
_ = json.Unmarshal(rawExceptions, &event.RecurrenceExceptions)
|
|
}
|
|
|
|
return event, nil
|
|
}
|
|
|
|
const calendarEventReturningColumns = `
|
|
id::TEXT,
|
|
title,
|
|
start_at,
|
|
end_at,
|
|
all_day,
|
|
COALESCE(location, ''),
|
|
COALESCE(description, ''),
|
|
color,
|
|
recurrence,
|
|
recurrence_weekday,
|
|
recurrence_ordinal,
|
|
recurrence_exceptions,
|
|
created_at,
|
|
updated_at
|
|
`
|
|
|
|
type calendarEventRowQueryer interface {
|
|
QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
|
|
}
|
|
|
|
func loadCalendarEventByID(ctx context.Context, queryer calendarEventRowQueryer, eventID string) (CalendarEvent, error) {
|
|
return scanCalendarEvent(queryer.QueryRow(
|
|
ctx,
|
|
`
|
|
SELECT
|
|
`+calendarEventReturningColumns+`
|
|
FROM calendar_events
|
|
WHERE id = $1
|
|
`,
|
|
eventID,
|
|
))
|
|
}
|
|
|
|
func (s *Server) handleListCalendarEvents(w http.ResponseWriter, r *http.Request) {
|
|
if err := s.ensureCalendarEventsTable(r.Context()); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Kalendereinträge konnten nicht vorbereitet werden")
|
|
return
|
|
}
|
|
|
|
rows, err := s.db.Query(
|
|
r.Context(),
|
|
`
|
|
SELECT
|
|
`+calendarEventReturningColumns+`
|
|
FROM calendar_events
|
|
ORDER BY start_at ASC, created_at ASC
|
|
`,
|
|
)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Kalendereinträge konnten nicht geladen werden")
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
events := []CalendarEvent{}
|
|
for rows.Next() {
|
|
event, err := scanCalendarEvent(rows)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Kalendereinträge konnten nicht gelesen werden")
|
|
return
|
|
}
|
|
|
|
events = append(events, event)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Kalendereinträge konnten nicht vollständig gelesen werden")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"events": events,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleCreateCalendarEvent(w http.ResponseWriter, r *http.Request) {
|
|
if err := s.ensureCalendarEventsTable(r.Context()); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Kalendereinträge konnten nicht vorbereitet werden")
|
|
return
|
|
}
|
|
|
|
var input CalendarEventRequest
|
|
if err := readJSON(r, &input); err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungültige Anfrage")
|
|
return
|
|
}
|
|
|
|
input, start, end, err := normalizeCalendarEventRequest(input)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
userID, _ := s.currentUserIDFromRequest(r)
|
|
exceptionsRaw, err := json.Marshal(input.RecurrenceExceptions)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "Wiederholungs-Ausnahmen sind ungültig")
|
|
return
|
|
}
|
|
|
|
event, err := scanCalendarEvent(s.db.QueryRow(
|
|
r.Context(),
|
|
`
|
|
INSERT INTO calendar_events (
|
|
title,
|
|
start_at,
|
|
end_at,
|
|
all_day,
|
|
location,
|
|
description,
|
|
color,
|
|
recurrence,
|
|
recurrence_weekday,
|
|
recurrence_ordinal,
|
|
recurrence_exceptions,
|
|
created_by,
|
|
updated_by
|
|
)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::JSONB, NULLIF($12, '')::UUID, NULLIF($12, '')::UUID)
|
|
RETURNING
|
|
`+calendarEventReturningColumns,
|
|
input.Title,
|
|
start,
|
|
end,
|
|
input.AllDay,
|
|
input.Location,
|
|
input.Description,
|
|
input.Color,
|
|
input.Recurrence,
|
|
input.RecurrenceWeekday,
|
|
input.RecurrenceOrdinal,
|
|
string(exceptionsRaw),
|
|
userID,
|
|
))
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Kalendereintrag konnte nicht erstellt werden")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusCreated, map[string]any{
|
|
"event": event,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleUpdateCalendarEvent(w http.ResponseWriter, r *http.Request) {
|
|
eventID := strings.TrimSpace(r.PathValue("id"))
|
|
if eventID == "" {
|
|
writeError(w, http.StatusBadRequest, "Kalendereintrag fehlt")
|
|
return
|
|
}
|
|
|
|
if err := s.ensureCalendarEventsTable(r.Context()); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Kalendereinträge konnten nicht vorbereitet werden")
|
|
return
|
|
}
|
|
|
|
var input CalendarEventRequest
|
|
if err := readJSON(r, &input); err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungültige Anfrage")
|
|
return
|
|
}
|
|
|
|
input, start, end, err := normalizeCalendarEventRequest(input)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
userID, _ := s.currentUserIDFromRequest(r)
|
|
exceptionsRaw, err := json.Marshal(input.RecurrenceExceptions)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "Wiederholungs-Ausnahmen sind ungültig")
|
|
return
|
|
}
|
|
|
|
event, err := scanCalendarEvent(s.db.QueryRow(
|
|
r.Context(),
|
|
`
|
|
UPDATE calendar_events
|
|
SET
|
|
title = $2,
|
|
start_at = $3,
|
|
end_at = $4,
|
|
all_day = $5,
|
|
location = $6,
|
|
description = $7,
|
|
color = $8,
|
|
recurrence = $9,
|
|
recurrence_weekday = $10,
|
|
recurrence_ordinal = $11,
|
|
recurrence_exceptions = $12::JSONB,
|
|
updated_by = NULLIF($13, '')::UUID,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING
|
|
`+calendarEventReturningColumns,
|
|
eventID,
|
|
input.Title,
|
|
start,
|
|
end,
|
|
input.AllDay,
|
|
input.Location,
|
|
input.Description,
|
|
input.Color,
|
|
input.Recurrence,
|
|
input.RecurrenceWeekday,
|
|
input.RecurrenceOrdinal,
|
|
string(exceptionsRaw),
|
|
userID,
|
|
))
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
writeError(w, http.StatusNotFound, "Kalendereintrag wurde nicht gefunden")
|
|
return
|
|
}
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Kalendereintrag konnte nicht gespeichert werden")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"event": event,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleUpdateCalendarEventOccurrence(w http.ResponseWriter, r *http.Request) {
|
|
eventID := strings.TrimSpace(r.PathValue("id"))
|
|
if eventID == "" {
|
|
writeError(w, http.StatusBadRequest, "Kalendereintrag fehlt")
|
|
return
|
|
}
|
|
|
|
if err := s.ensureCalendarEventsTable(r.Context()); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Kalendereinträge konnten nicht vorbereitet werden")
|
|
return
|
|
}
|
|
|
|
var payload CalendarEventOccurrenceRequest
|
|
if err := readJSON(r, &payload); err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungültige Anfrage")
|
|
return
|
|
}
|
|
|
|
occurrenceStart, err := parseCalendarEventDateTime(payload.OccurrenceStart)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "Der gewählte Serientermin ist ungültig")
|
|
return
|
|
}
|
|
occurrenceStartValue := formatCalendarEventDateTime(occurrenceStart)
|
|
|
|
payload.Event.Recurrence = "none"
|
|
payload.Event.RecurrenceWeekday = nil
|
|
payload.Event.RecurrenceOrdinal = nil
|
|
payload.Event.RecurrenceExceptions = nil
|
|
|
|
input, start, end, err := normalizeCalendarEventRequest(payload.Event)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
tx, err := s.db.Begin(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Kalendereintrag konnte nicht vorbereitet werden")
|
|
return
|
|
}
|
|
defer tx.Rollback(r.Context())
|
|
|
|
series, err := loadCalendarEventByID(r.Context(), tx, eventID)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
writeError(w, http.StatusNotFound, "Kalendereintrag wurde nicht gefunden")
|
|
return
|
|
}
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Kalendereintrag konnte nicht geladen werden")
|
|
return
|
|
}
|
|
if series.Recurrence == "none" {
|
|
writeError(w, http.StatusBadRequest, "Der Kalendereintrag ist keine Serie")
|
|
return
|
|
}
|
|
|
|
nextExceptions, err := appendCalendarEventException(series.RecurrenceExceptions, occurrenceStartValue)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
exceptionsRaw, err := json.Marshal(nextExceptions)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "Wiederholungs-Ausnahmen sind ungültig")
|
|
return
|
|
}
|
|
|
|
userID, _ := s.currentUserIDFromRequest(r)
|
|
|
|
updatedSeries, err := scanCalendarEvent(tx.QueryRow(
|
|
r.Context(),
|
|
`
|
|
UPDATE calendar_events
|
|
SET
|
|
recurrence_exceptions = $2::JSONB,
|
|
updated_by = NULLIF($3, '')::UUID,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING
|
|
`+calendarEventReturningColumns,
|
|
eventID,
|
|
string(exceptionsRaw),
|
|
userID,
|
|
))
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Serientermin konnte nicht aktualisiert werden")
|
|
return
|
|
}
|
|
|
|
event, err := scanCalendarEvent(tx.QueryRow(
|
|
r.Context(),
|
|
`
|
|
INSERT INTO calendar_events (
|
|
title,
|
|
start_at,
|
|
end_at,
|
|
all_day,
|
|
location,
|
|
description,
|
|
color,
|
|
recurrence,
|
|
recurrence_weekday,
|
|
recurrence_ordinal,
|
|
recurrence_exceptions,
|
|
created_by,
|
|
updated_by
|
|
)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, 'none', NULL, NULL, '[]'::JSONB, NULLIF($8, '')::UUID, NULLIF($8, '')::UUID)
|
|
RETURNING
|
|
`+calendarEventReturningColumns,
|
|
input.Title,
|
|
start,
|
|
end,
|
|
input.AllDay,
|
|
input.Location,
|
|
input.Description,
|
|
input.Color,
|
|
userID,
|
|
))
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Ausgewählter Termin konnte nicht gespeichert werden")
|
|
return
|
|
}
|
|
|
|
if err := tx.Commit(r.Context()); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Kalendereintrag konnte nicht gespeichert werden")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"series": updatedSeries,
|
|
"event": event,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleDeleteCalendarEventOccurrence(w http.ResponseWriter, r *http.Request) {
|
|
eventID := strings.TrimSpace(r.PathValue("id"))
|
|
if eventID == "" {
|
|
writeError(w, http.StatusBadRequest, "Kalendereintrag fehlt")
|
|
return
|
|
}
|
|
|
|
if err := s.ensureCalendarEventsTable(r.Context()); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Kalendereinträge konnten nicht vorbereitet werden")
|
|
return
|
|
}
|
|
|
|
var payload struct {
|
|
OccurrenceStart string `json:"occurrenceStart"`
|
|
}
|
|
if err := readJSON(r, &payload); err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungültige Anfrage")
|
|
return
|
|
}
|
|
|
|
occurrenceStart, err := parseCalendarEventDateTime(payload.OccurrenceStart)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "Der gewählte Serientermin ist ungültig")
|
|
return
|
|
}
|
|
occurrenceStartValue := formatCalendarEventDateTime(occurrenceStart)
|
|
|
|
tx, err := s.db.Begin(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Kalendereintrag konnte nicht vorbereitet werden")
|
|
return
|
|
}
|
|
defer tx.Rollback(r.Context())
|
|
|
|
series, err := loadCalendarEventByID(r.Context(), tx, eventID)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
writeError(w, http.StatusNotFound, "Kalendereintrag wurde nicht gefunden")
|
|
return
|
|
}
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Kalendereintrag konnte nicht geladen werden")
|
|
return
|
|
}
|
|
if series.Recurrence == "none" {
|
|
writeError(w, http.StatusBadRequest, "Der Kalendereintrag ist keine Serie")
|
|
return
|
|
}
|
|
|
|
nextExceptions, err := appendCalendarEventException(series.RecurrenceExceptions, occurrenceStartValue)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
exceptionsRaw, err := json.Marshal(nextExceptions)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "Wiederholungs-Ausnahmen sind ungültig")
|
|
return
|
|
}
|
|
|
|
userID, _ := s.currentUserIDFromRequest(r)
|
|
|
|
updatedSeries, err := scanCalendarEvent(tx.QueryRow(
|
|
r.Context(),
|
|
`
|
|
UPDATE calendar_events
|
|
SET
|
|
recurrence_exceptions = $2::JSONB,
|
|
updated_by = NULLIF($3, '')::UUID,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING
|
|
`+calendarEventReturningColumns,
|
|
eventID,
|
|
string(exceptionsRaw),
|
|
userID,
|
|
))
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Serientermin konnte nicht aktualisiert werden")
|
|
return
|
|
}
|
|
|
|
if err := tx.Commit(r.Context()); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Kalendereintrag konnte nicht gelöscht werden")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"event": updatedSeries,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleDeleteCalendarEvent(w http.ResponseWriter, r *http.Request) {
|
|
eventID := strings.TrimSpace(r.PathValue("id"))
|
|
if eventID == "" {
|
|
writeError(w, http.StatusBadRequest, "Kalendereintrag fehlt")
|
|
return
|
|
}
|
|
|
|
if err := s.ensureCalendarEventsTable(r.Context()); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Kalendereinträge konnten nicht vorbereitet werden")
|
|
return
|
|
}
|
|
|
|
result, err := s.db.Exec(
|
|
r.Context(),
|
|
`DELETE FROM calendar_events WHERE id = $1`,
|
|
eventID,
|
|
)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Kalendereintrag konnte nicht gelöscht werden")
|
|
return
|
|
}
|
|
|
|
if result.RowsAffected() == 0 {
|
|
writeError(w, http.StatusNotFound, "Kalendereintrag wurde nicht gefunden")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]string{
|
|
"message": "Kalendereintrag gelöscht",
|
|
})
|
|
}
|