Files
paliad/internal/handlers/appointments_pages.go
m 56522adffe feat(t-paliad-115): canonicalise list URL on /events; redirect old paths
PR-2 of t-paliad-115. The unified Fristen + Termine surface now lives at
/events. Old /deadlines and /appointments list URLs 301-redirect to
/events?type=deadline and /events?type=appointment so existing bookmarks
still land on the right view. Detail pages (/deadlines/{id},
/appointments/{id}) stay type-specific.

Backend (Go).
- New `GET /events` route → handleEventsListPage serves dist/events.html.
- `GET /deadlines` → handleDeadlinesListRedirect (301 → /events?type=deadline).
- `GET /appointments` → handleAppointmentsListRedirect (301 → /events?type=appointment).
- /deadlines/new, /deadlines/calendar, /deadlines/{id}, /appointments/new,
  /appointments/calendar, /appointments/{id} unchanged — type-specific
  detail / form / legacy-calendar surfaces stay where they are.

Frontend.
- build.ts now emits ONE events.html (not events-deadlines /
  events-appointments) with defaultType="all" baked in. The page reads
  ?type=… and ?view=… on hydration, so /events?type=deadline lands on
  the Fristen-only Cards view, /events?view=calendar opens the calendar,
  and bare /events shows the Beides view.
- Sidebar Fristen / Termine entries point at /events?type=deadline and
  /events?type=appointment. The SSR active-state matches exactly via
  href === currentPath, so detail/new/calendar pages that pass
  currentPath="/events?type=deadline" (resp. appointment) still
  highlight the right entry.
- events.ts hydration adds applySidebarTypeHighlight(): on bare /events
  the sidebar SSRs with neither entry lit, and we re-highlight the
  matching entry whenever the in-page chip toggle changes the active
  type. Sidebar stays in sync without a server round-trip.
- Updated every list-target reference: palette-actions.ts (Cmd-K
  navigation), deadlines-detail.ts + appointments-detail.ts (post-delete
  redirect), and the back-link / cancel hrefs in the *-new + *-detail +
  *-calendar TSX templates. Detail-page Sidebar/BottomNav currentPath
  also moved from "/deadlines" → "/events?type=deadline" so the new
  highlight contract holds end-to-end.

Out of scope (per task brief).
- A third "Ereignisse / Alle Events" sidebar entry pointing at /events
  bare. m's call: keep two entries; defer until signal.
- Removing /deadlines/calendar + /appointments/calendar standalone
  pages. The new /events?view=calendar covers the same need but the
  legacy URLs stay live for one cycle.

Build clean: `cd frontend && bun run build` + `go build/vet/test ./...`.
2026-05-04 14:40:53 +02:00

63 lines
2.5 KiB
Go

package handlers
import "net/http"
// Server-rendered page endpoints for the Phase F Appointments UI.
// HTML is generated at build time by frontend/build.ts; the per-page
// client TS bundles call /api/appointments* to populate the DOM and read
// id/project_id from window.location.
// handleAppointmentsListRedirect 301-redirects the legacy /appointments
// list URL to the canonical /events?type=appointment (t-paliad-115).
// Detail page /appointments/{id} stays type-specific. Drop this redirect
// once we're confident no caches / bookmarks / external links still hit
// the old URL.
func handleAppointmentsListRedirect(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/events?type=appointment", http.StatusMovedPermanently)
}
func handleAppointmentsNewPage(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "dist/appointments-new.html")
}
func handleAppointmentsDetailPage(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "dist/appointments-detail.html")
}
func handleAppointmentsCalendarPage(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "dist/appointments-calendar.html")
}
// handleSettingsPage serves the unified settings page with tabs for
// Profil / Benachrichtigungen / CalDAV. The active tab is picked
// client-side from ?tab=<name> so switching tabs doesn't round-trip.
func handleSettingsPage(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "dist/settings.html")
}
// settingsTabAliases maps every supported /settings/<slug> deep-link to its
// canonical ?tab=<name> value the client TS understands. Both the German tab
// IDs (profil/benachrichtigungen) and intuitive English aliases
// (profile/notifications) are accepted so bookmarks, smoke tests, and
// manually-typed URLs all land on the right tab.
var settingsTabAliases = map[string]string{
"profil": "profil",
"profile": "profil",
"benachrichtigungen": "benachrichtigungen",
"notifications": "benachrichtigungen",
"caldav": "caldav",
}
// handleSettingsTabRedirect turns /settings/<slug> into /settings?tab=<canonical>
// as 301 Moved Permanently. Unknown slugs fall back to the bare /settings page
// (the client picks the default tab) so a typo doesn't 404.
func handleSettingsTabRedirect(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("tab")
canonical, ok := settingsTabAliases[slug]
if !ok {
http.Redirect(w, r, "/settings", http.StatusMovedPermanently)
return
}
http.Redirect(w, r, "/settings?tab="+canonical, http.StatusMovedPermanently)
}