From 49c6bc75ca96aa64eb7aa7b4ed05ed05f06cb69f Mon Sep 17 00:00:00 2001 From: m Date: Mon, 20 Apr 2026 17:40:55 +0200 Subject: [PATCH] refactor(rename): handler functions, routes, legacy 301 redirects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second rename pass closing the backend cleanup: * handler functions (handleListProjekte, handleCreateFrist, …) renamed to English equivalents so every symbol in the handler package matches the URL/entity it serves. * services.FristStatusFilter + filter constants renamed to DeadlineStatusFilter / DeadlineFilterOverdue etc. * services.TerminListFilter / TerminCalDAVPusher / TerminSummaryCounts renamed to AppointmentListFilter / AppointmentCalDAVPusher / AppointmentSummaryCounts. * GlossarTerm/GlossarSuggestion/glossarTerms → Glossary*. * CourtsFeedback/CourtsResponse (formerly Gerichte*). * handlers.Services.{Projekt,Parteien,Frist,Termin,Notiz,Dezernat} → {Project,Party,Deadline,Appointment,Note,Department}; dbServices struct + consumers likewise. * email templates: {{.FristURL}} → {{.DeadlineURL}}, {{.FristenURL}} → {{.DeadlinesURL}}. * links.go category IDs: gerichte → courts. * cmd/server/main.go local vars: projektSvc/terminSvc/dezernatSvc → projectSvc/appointmentSvc/departmentSvc. Routes: * removed all /api/akten alias routes (API clients use /api/projects now). * removed /api/akten/*/deadlines, /*/notes, /*/parties, /*/appointments, /*/checklists, /*/events, /*/summary alias variants. * new internal/handlers/redirects.go registers 301 Moved Permanently redirects for every legacy German GET path: /akten, /projekte, /fristen, /termine, /notizen, /einstellungen, /checklisten, /dezernate, /parteien, /gerichte, /glossar. Sub-paths + query strings are preserved so old bookmarks keep working. Kept in German (product names, per task spec): * /tools/fristenrechner, /tools/kostenrechner, /tools/gebuehrentabellen * FristenrechnerService / KostenrechnerService types * User.Dezernat + paliad.users.dezernat free-text legacy column (separate from the new paliad.departments entity). go build / vet / test clean. --- cmd/server/main.go | 30 +-- internal/handlers/appointments.go | 16 +- internal/handlers/appointments_pages.go | 16 +- internal/handlers/checklist_instances.go | 2 +- internal/handlers/checklists.go | 6 +- internal/handlers/courts.go | 14 +- internal/handlers/deadline_rules_db.go | 2 +- internal/handlers/deadlines.go | 20 +- internal/handlers/deadlines_pages.go | 8 +- internal/handlers/departments.go | 36 +-- internal/handlers/fristenrechner.go | 2 +- internal/handlers/glossary.go | 16 +- internal/handlers/handlers.go | 220 ++++++++---------- internal/handlers/links.go | 14 +- internal/handlers/notes.go | 16 +- internal/handlers/projects.go | 32 +-- internal/handlers/projects_pages.go | 8 +- internal/handlers/redirects.go | 47 ++++ internal/handlers/teams.go | 2 +- internal/services/appointment_service.go | 26 +-- internal/services/caldav_service.go | 4 +- internal/services/deadline_service.go | 30 +-- internal/services/department_service.go | 16 +- internal/services/fristenrechner.go | 4 +- internal/services/mail_service_test.go | 4 +- internal/services/reminder_service.go | 14 +- .../templates/email/deadline_reminder.html | 2 +- internal/templates/email/deadline_weekly.html | 2 +- 28 files changed, 314 insertions(+), 295 deletions(-) create mode 100644 internal/handlers/redirects.go diff --git a/cmd/server/main.go b/cmd/server/main.go index 55b2305..6639fc0 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -52,7 +52,7 @@ func main() { // DATABASE_URL is optional during the Phase A → Phase D transition. The // existing knowledge-platform features (Kostenrechner, Glossar, etc.) work - // without a DB. Akten/Deadline endpoints return 503 until DATABASE_URL is set. + // without a DB. matter-management endpoints return 503 until DATABASE_URL is set. dbURL := os.Getenv("DATABASE_URL") var svcBundle *handlers.Services var caldavSvc *services.CalDAVService @@ -70,9 +70,9 @@ func main() { } holidays := services.NewHolidayService(pool) users := services.NewUserService(pool) - projektSvc := services.NewProjectService(pool, users) - teamSvc := services.NewTeamService(pool, projektSvc) - dezernatSvc := services.NewDepartmentService(pool, users) + projectSvc := services.NewProjectService(pool, users) + teamSvc := services.NewTeamService(pool, projectSvc) + departmentSvc := services.NewDepartmentService(pool, users) rules := services.NewDeadlineRuleService(pool) // Phase F: optional CalDAV cipher. If CALDAV_ENCRYPTION_KEY is unset @@ -89,31 +89,31 @@ func main() { log.Println("CalDAV encryption configured (AES-256-GCM)") } - terminSvc := services.NewAppointmentService(pool, projektSvc) - caldavSvc = services.NewCalDAVService(pool, cipher, terminSvc) + appointmentSvc := services.NewAppointmentService(pool, projectSvc) + caldavSvc = services.NewCalDAVService(pool, cipher, appointmentSvc) // Wire the push hook so user-driven mutations sync to the external // calendar without waiting for the next 60-second tick. - terminSvc.SetCalDAVPusher(caldavSvc) + appointmentSvc.SetCalDAVPusher(caldavSvc) baseURL := os.Getenv("PALIAD_BASE_URL") inviteSvc := services.NewInviteService(pool, mailSvc, handlers.AllowedEmailDomains, baseURL) reminderSvc := services.NewReminderService(pool, mailSvc, users, baseURL) svcBundle = &handlers.Services{ - Project: projektSvc, + Project: projectSvc, Team: teamSvc, - Dezernat: dezernatSvc, - Parties: services.NewPartyService(pool, projektSvc), - Deadline: services.NewDeadlineService(pool, projektSvc), - Appointment: terminSvc, + Department: departmentSvc, + Party: services.NewPartyService(pool, projectSvc), + Deadline: services.NewDeadlineService(pool, projectSvc), + Appointment: appointmentSvc, CalDAV: caldavSvc, Rules: rules, Calculator: services.NewDeadlineCalculator(holidays), Users: users, Fristenrechner: services.NewFristenrechnerService(rules, holidays), Dashboard: services.NewDashboardService(pool, users), - Note: services.NewNoteService(pool, projektSvc, terminSvc), - ChecklistInst: services.NewChecklistInstanceService(pool, projektSvc), + Note: services.NewNoteService(pool, projectSvc, appointmentSvc), + ChecklistInst: services.NewChecklistInstanceService(pool, projectSvc), Mail: mailSvc, Invite: inviteSvc, } @@ -132,7 +132,7 @@ func main() { caldavSvc.Stop() }() } else { - log.Println("DATABASE_URL not set — Akten/Deadline endpoints will return 503") + log.Println("DATABASE_URL not set — matter-management endpoints will return 503") } mux := http.NewServeMux() diff --git a/internal/handlers/appointments.go b/internal/handlers/appointments.go index 5ceaea8..99a7dcb 100644 --- a/internal/handlers/appointments.go +++ b/internal/handlers/appointments.go @@ -28,7 +28,7 @@ func requireCalDAV(w http.ResponseWriter) bool { } // GET /api/appointments?project_id=&from=&to=&type= -func handleListTermine(w http.ResponseWriter, r *http.Request) { +func handleListAppointments(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -37,7 +37,7 @@ func handleListTermine(w http.ResponseWriter, r *http.Request) { return } q := r.URL.Query() - filter := services.TerminListFilter{} + filter := services.AppointmentListFilter{} raw := q.Get("project_id") if raw == "" { raw = q.Get("project_id") @@ -78,7 +78,7 @@ func handleListTermine(w http.ResponseWriter, r *http.Request) { } // GET /api/appointments/summary -func handleTermineSummary(w http.ResponseWriter, r *http.Request) { +func handleAppointmentsSummary(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -95,7 +95,7 @@ func handleTermineSummary(w http.ResponseWriter, r *http.Request) { } // GET /api/projects/{id}/appointments -func handleListTermineForProjekt(w http.ResponseWriter, r *http.Request) { +func handleListAppointmentsForProject(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -117,7 +117,7 @@ func handleListTermineForProjekt(w http.ResponseWriter, r *http.Request) { } // POST /api/appointments -func handleCreateTermin(w http.ResponseWriter, r *http.Request) { +func handleCreateAppointment(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -139,7 +139,7 @@ func handleCreateTermin(w http.ResponseWriter, r *http.Request) { } // GET /api/appointments/{id} -func handleGetTermin(w http.ResponseWriter, r *http.Request) { +func handleGetAppointment(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -161,7 +161,7 @@ func handleGetTermin(w http.ResponseWriter, r *http.Request) { } // PATCH /api/appointments/{id} -func handleUpdateTermin(w http.ResponseWriter, r *http.Request) { +func handleUpdateAppointment(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -188,7 +188,7 @@ func handleUpdateTermin(w http.ResponseWriter, r *http.Request) { } // DELETE /api/appointments/{id} -func handleDeleteTermin(w http.ResponseWriter, r *http.Request) { +func handleDeleteAppointment(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } diff --git a/internal/handlers/appointments_pages.go b/internal/handlers/appointments_pages.go index 94d8e95..deded93 100644 --- a/internal/handlers/appointments_pages.go +++ b/internal/handlers/appointments_pages.go @@ -7,33 +7,33 @@ import "net/http" // client TS bundles call /api/appointments* to populate the DOM and read // id/project_id from window.location. -func handleTermineListPage(w http.ResponseWriter, r *http.Request) { +func handleAppointmentsListPage(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "dist/appointments.html") } -func handleTermineNewPage(w http.ResponseWriter, r *http.Request) { +func handleAppointmentsNewPage(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "dist/appointments-neu.html") } -func handleTermineDetailPage(w http.ResponseWriter, r *http.Request) { +func handleAppointmentsDetailPage(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "dist/appointments-detail.html") } -func handleTermineKalenderPage(w http.ResponseWriter, r *http.Request) { +func handleAppointmentsCalendarPage(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "dist/appointments-kalender.html") } -// handleEinstellungenPage serves the unified settings page with tabs for +// handleSettingsPage serves the unified settings page with tabs for // Profil / Benachrichtigungen / CalDAV. The active tab is picked client-side // from ?tab= so switching tabs doesn't round-trip. -func handleEinstellungenPage(w http.ResponseWriter, r *http.Request) { +func handleSettingsPage(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "dist/settings.html") } -// handleEinstellungenCalDAVRedirect keeps /settings/caldav working for +// handleSettingsCalDAVRedirect keeps /settings/caldav working for // bookmarks and any external links while the canonical URL moves to // /settings?tab=caldav. 301 Moved Permanently — browsers cache the hop // so the redirect only costs once per bookmark. -func handleEinstellungenCalDAVRedirect(w http.ResponseWriter, r *http.Request) { +func handleSettingsCalDAVRedirect(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/settings?tab=caldav", http.StatusMovedPermanently) } diff --git a/internal/handlers/checklist_instances.go b/internal/handlers/checklist_instances.go index bff4f30..1b85376 100644 --- a/internal/handlers/checklist_instances.go +++ b/internal/handlers/checklist_instances.go @@ -143,7 +143,7 @@ func handleDeleteChecklistInstance(w http.ResponseWriter, r *http.Request) { } // GET /api/projects/{id}/checklists -func handleListChecklistInstancesForProjekt(w http.ResponseWriter, r *http.Request) { +func handleListChecklistInstancesForProject(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } diff --git a/internal/handlers/checklists.go b/internal/handlers/checklists.go index cf7b5ec..015e03e 100644 --- a/internal/handlers/checklists.go +++ b/internal/handlers/checklists.go @@ -20,7 +20,7 @@ type ChecklistFeedback struct { Message string `json:"message"` } -func handleChecklistenPage(w http.ResponseWriter, r *http.Request) { +func handleChecklistsPage(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "dist/checklists.html") } @@ -37,7 +37,7 @@ func handleChecklistInstancePage(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "dist/checklists-instance.html") } -func handleChecklistenAPI(w http.ResponseWriter, r *http.Request) { +func handleChecklistsAPI(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, checklists.Summaries()) } @@ -51,7 +51,7 @@ func handleChecklistAPI(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, c) } -func handleChecklistenFeedback(w http.ResponseWriter, r *http.Request) { +func handleChecklistsFeedback(w http.ResponseWriter, r *http.Request) { var feedback ChecklistFeedback if err := json.NewDecoder(r.Body).Decode(&feedback); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Ungültige Anfrage."}) diff --git a/internal/handlers/courts.go b/internal/handlers/courts.go index e01a9aa..61547a8 100644 --- a/internal/handlers/courts.go +++ b/internal/handlers/courts.go @@ -606,33 +606,33 @@ var courts = []Court{ // --- Feedback --- -type GerichteFeedback struct { +type CourtsFeedback struct { FeedbackType string `json:"feedback_type"` Message string `json:"message"` CourtID string `json:"court_id,omitempty"` } -type GerichteResponse struct { +type CourtsResponse struct { Courts []Court `json:"courts"` Types []CourtType `json:"types"` } -func handleGerichtePage(w http.ResponseWriter, r *http.Request) { +func handleCourtsPage(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "dist/courts.html") } -func handleGerichteAPI(w http.ResponseWriter, r *http.Request) { +func handleCourtsAPI(w http.ResponseWriter, r *http.Request) { // Strip the internal `source` field — it is for maintainers, not for end users. public := make([]Court, len(courts)) for i, c := range courts { c.Source = "" public[i] = c } - writeJSON(w, http.StatusOK, GerichteResponse{Courts: public, Types: CourtTypes}) + writeJSON(w, http.StatusOK, CourtsResponse{Courts: public, Types: CourtTypes}) } -func handleGerichteFeedback(w http.ResponseWriter, r *http.Request) { - var feedback GerichteFeedback +func handleCourtsFeedback(w http.ResponseWriter, r *http.Request) { + var feedback CourtsFeedback if err := json.NewDecoder(r.Body).Decode(&feedback); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Ungültige Anfrage."}) return diff --git a/internal/handlers/deadline_rules_db.go b/internal/handlers/deadline_rules_db.go index 70d3c6f..733ba31 100644 --- a/internal/handlers/deadline_rules_db.go +++ b/internal/handlers/deadline_rules_db.go @@ -55,7 +55,7 @@ func handleListProceedingTypesDB(w http.ResponseWriter, r *http.Request) { // Calculates all deadlines for the proceeding type's rule tree, applying // holiday/weekend adjustment via the DB-backed HolidayService. // -// Lives at /api/deadlines/calculate (vs the existing /api/tools/deadlinesrechner +// Lives at /api/deadlines/calculate (vs the existing /api/tools/fristenrechner // which uses the in-memory rule tree). Phase C swaps the Fristenrechner UI // to this endpoint, then deletes the in-memory rule tree. func handleCalculateDeadlines(w http.ResponseWriter, r *http.Request) { diff --git a/internal/handlers/deadlines.go b/internal/handlers/deadlines.go index c89b36c..6311db2 100644 --- a/internal/handlers/deadlines.go +++ b/internal/handlers/deadlines.go @@ -10,7 +10,7 @@ import ( ) // GET /api/deadlines?status=overdue|this_week|upcoming|completed|pending|all&project_id=UUID -func handleListFristen(w http.ResponseWriter, r *http.Request) { +func handleListDeadlines(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -19,7 +19,7 @@ func handleListFristen(w http.ResponseWriter, r *http.Request) { return } filter := services.ListFilter{ - Status: services.FristStatusFilter(r.URL.Query().Get("status")), + 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") @@ -43,7 +43,7 @@ func handleListFristen(w http.ResponseWriter, r *http.Request) { } // GET /api/deadlines/summary?project_id=UUID -func handleFristenSummary(w http.ResponseWriter, r *http.Request) { +func handleDeadlinesSummary(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -73,7 +73,7 @@ func handleFristenSummary(w http.ResponseWriter, r *http.Request) { } // GET /api/projects/{id}/deadlines -func handleListFristenForProjekt(w http.ResponseWriter, r *http.Request) { +func handleListDeadlinesForProject(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -95,7 +95,7 @@ func handleListFristenForProjekt(w http.ResponseWriter, r *http.Request) { } // POST /api/projects/{id}/deadlines -func handleCreateFrist(w http.ResponseWriter, r *http.Request) { +func handleCreateDeadline(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -122,7 +122,7 @@ func handleCreateFrist(w http.ResponseWriter, r *http.Request) { } // POST /api/projects/{id}/deadlines/bulk — Fristenrechner "save to Project". -func handleBulkCreateFristen(w http.ResponseWriter, r *http.Request) { +func handleBulkCreateDeadlines(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -151,7 +151,7 @@ func handleBulkCreateFristen(w http.ResponseWriter, r *http.Request) { } // GET /api/deadlines/{id} -func handleGetFrist(w http.ResponseWriter, r *http.Request) { +func handleGetDeadline(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -173,7 +173,7 @@ func handleGetFrist(w http.ResponseWriter, r *http.Request) { } // PATCH /api/deadlines/{id} -func handleUpdateFrist(w http.ResponseWriter, r *http.Request) { +func handleUpdateDeadline(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -200,7 +200,7 @@ func handleUpdateFrist(w http.ResponseWriter, r *http.Request) { } // PATCH /api/deadlines/{id}/complete — convenience endpoint for the list-row checkbox. -func handleCompleteFrist(w http.ResponseWriter, r *http.Request) { +func handleCompleteDeadline(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -222,7 +222,7 @@ func handleCompleteFrist(w http.ResponseWriter, r *http.Request) { } // DELETE /api/deadlines/{id} -func handleDeleteFrist(w http.ResponseWriter, r *http.Request) { +func handleDeleteDeadline(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } diff --git a/internal/handlers/deadlines_pages.go b/internal/handlers/deadlines_pages.go index 3e31848..8b236e0 100644 --- a/internal/handlers/deadlines_pages.go +++ b/internal/handlers/deadlines_pages.go @@ -7,18 +7,18 @@ import "net/http" // client TS bundles call /api/deadlines* to populate the DOM and read // id/project_id from window.location. -func handleFristenListPage(w http.ResponseWriter, r *http.Request) { +func handleDeadlinesListPage(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "dist/deadlines.html") } -func handleFristenNewPage(w http.ResponseWriter, r *http.Request) { +func handleDeadlinesNewPage(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "dist/deadlines-neu.html") } -func handleFristenDetailPage(w http.ResponseWriter, r *http.Request) { +func handleDeadlinesDetailPage(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "dist/deadlines-detail.html") } -func handleFristenKalenderPage(w http.ResponseWriter, r *http.Request) { +func handleDeadlinesCalendarPage(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "dist/deadlines-kalender.html") } diff --git a/internal/handlers/departments.go b/internal/handlers/departments.go index 2442639..71565d8 100644 --- a/internal/handlers/departments.go +++ b/internal/handlers/departments.go @@ -12,14 +12,14 @@ import ( ) // GET /api/departments — list every Dezernat (readable by all authenticated users). -func handleListDezernate(w http.ResponseWriter, r *http.Request) { +func handleListDepartments(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } if _, ok := requireUser(w, r); !ok { return } - rows, err := dbSvc.dezernat.List(r.Context()) + rows, err := dbSvc.department.List(r.Context()) if err != nil { writeServiceError(w, err) return @@ -28,7 +28,7 @@ func handleListDezernate(w http.ResponseWriter, r *http.Request) { } // POST /api/departments — admin-only create. -func handleCreateDezernat(w http.ResponseWriter, r *http.Request) { +func handleCreateDepartment(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -41,7 +41,7 @@ func handleCreateDezernat(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"}) return } - d, err := dbSvc.dezernat.Create(r.Context(), uid, input) + d, err := dbSvc.department.Create(r.Context(), uid, input) if err != nil { writeServiceError(w, err) return @@ -50,7 +50,7 @@ func handleCreateDezernat(w http.ResponseWriter, r *http.Request) { } // GET /api/departments/{id} -func handleGetDezernat(w http.ResponseWriter, r *http.Request) { +func handleGetDepartment(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -62,7 +62,7 @@ func handleGetDezernat(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"}) return } - d, err := dbSvc.dezernat.GetByID(r.Context(), id) + d, err := dbSvc.department.GetByID(r.Context(), id) if err != nil { if errors.Is(err, sql.ErrNoRows) { writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"}) @@ -75,7 +75,7 @@ func handleGetDezernat(w http.ResponseWriter, r *http.Request) { } // PATCH /api/departments/{id} — admin-only. -func handleUpdateDezernat(w http.ResponseWriter, r *http.Request) { +func handleUpdateDepartment(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -93,7 +93,7 @@ func handleUpdateDezernat(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"}) return } - d, err := dbSvc.dezernat.Update(r.Context(), uid, id, input) + d, err := dbSvc.department.Update(r.Context(), uid, id, input) if err != nil { writeServiceError(w, err) return @@ -102,7 +102,7 @@ func handleUpdateDezernat(w http.ResponseWriter, r *http.Request) { } // DELETE /api/departments/{id} — admin-only. -func handleDeleteDezernat(w http.ResponseWriter, r *http.Request) { +func handleDeleteDepartment(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -115,7 +115,7 @@ func handleDeleteDezernat(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"}) return } - if err := dbSvc.dezernat.Delete(r.Context(), uid, id); err != nil { + if err := dbSvc.department.Delete(r.Context(), uid, id); err != nil { writeServiceError(w, err) return } @@ -123,7 +123,7 @@ func handleDeleteDezernat(w http.ResponseWriter, r *http.Request) { } // GET /api/departments/{id}/members -func handleListDezernatMembers(w http.ResponseWriter, r *http.Request) { +func handleListDepartmentMembers(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -135,7 +135,7 @@ func handleListDezernatMembers(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"}) return } - rows, err := dbSvc.dezernat.ListMembers(r.Context(), id) + rows, err := dbSvc.department.ListMembers(r.Context(), id) if err != nil { writeServiceError(w, err) return @@ -144,7 +144,7 @@ func handleListDezernatMembers(w http.ResponseWriter, r *http.Request) { } // POST /api/departments/{id}/members — admin-only. Body: {"user_id": ""} -func handleAddDezernatMember(w http.ResponseWriter, r *http.Request) { +func handleAddDepartmentMember(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -152,7 +152,7 @@ func handleAddDezernatMember(w http.ResponseWriter, r *http.Request) { if !ok { return } - dezernatID, err := uuid.Parse(r.PathValue("id")) + departmentID, err := uuid.Parse(r.PathValue("id")) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"}) return @@ -164,7 +164,7 @@ func handleAddDezernatMember(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"}) return } - if err := dbSvc.dezernat.AddMember(r.Context(), uid, dezernatID, body.UserID); err != nil { + if err := dbSvc.department.AddMember(r.Context(), uid, departmentID, body.UserID); err != nil { writeServiceError(w, err) return } @@ -172,7 +172,7 @@ func handleAddDezernatMember(w http.ResponseWriter, r *http.Request) { } // DELETE /api/departments/{id}/members/{user_id} — admin-only. -func handleRemoveDezernatMember(w http.ResponseWriter, r *http.Request) { +func handleRemoveDepartmentMember(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -180,7 +180,7 @@ func handleRemoveDezernatMember(w http.ResponseWriter, r *http.Request) { if !ok { return } - dezernatID, err := uuid.Parse(r.PathValue("id")) + departmentID, err := uuid.Parse(r.PathValue("id")) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid dezernat id"}) return @@ -190,7 +190,7 @@ func handleRemoveDezernatMember(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid user id"}) return } - if err := dbSvc.dezernat.RemoveMember(r.Context(), uid, dezernatID, userID); err != nil { + if err := dbSvc.department.RemoveMember(r.Context(), uid, departmentID, userID); err != nil { writeServiceError(w, err) return } diff --git a/internal/handlers/fristenrechner.go b/internal/handlers/fristenrechner.go index 3ab238e..2926a8c 100644 --- a/internal/handlers/fristenrechner.go +++ b/internal/handlers/fristenrechner.go @@ -13,7 +13,7 @@ func handleFristenrechnerPage(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "dist/deadlinesrechner.html") } -// POST /api/tools/deadlinesrechner — calculate the UI timeline for a proceeding. +// POST /api/tools/fristenrechner — calculate the UI timeline for a proceeding. // // Phase C: routes through FristenrechnerService which pulls rules from // paliad.deadline_rules. When DATABASE_URL is unset, returns 503; the page diff --git a/internal/handlers/glossary.go b/internal/handlers/glossary.go index e5db6f4..eb37531 100644 --- a/internal/handlers/glossary.go +++ b/internal/handlers/glossary.go @@ -14,14 +14,14 @@ import ( "mgit.msbls.de/m/patholo/internal/auth" ) -type GlossarTerm struct { +type GlossaryTerm struct { DE string `json:"de"` EN string `json:"en"` Definition string `json:"definition,omitempty"` Category string `json:"category"` } -type GlossarSuggestion struct { +type GlossarySuggestion struct { TermDE string `json:"term_de"` TermEN string `json:"term_en"` Definition string `json:"definition,omitempty"` @@ -30,7 +30,7 @@ type GlossarSuggestion struct { ExistingTermDE string `json:"existing_term_de,omitempty"` } -var glossarTerms = []GlossarTerm{ +var glossaryTerms = []GlossaryTerm{ // --- Litigation --- {DE: "Patentverletzung", EN: "Patent infringement", Definition: "Benutzung einer patentgeschützten Erfindung ohne Zustimmung des Patentinhabers.", Category: "Litigation"}, {DE: "Verletzungsklage", EN: "Infringement action", Definition: "Klage des Patentinhabers gegen den Verletzer auf Unterlassung, Schadensersatz und Auskunft.", Category: "Litigation"}, @@ -133,16 +133,16 @@ var glossarTerms = []GlossarTerm{ {DE: "Patent-Hold-out", EN: "Patent hold-out", Definition: "Gegenstück zum Hold-up: Implementierer verzögern oder verweigern Lizenzverhandlungen und nutzen die Standardtechnologie ohne Lizenz (\"efficient infringement\").", Category: "SEP/FRAND"}, } -func handleGlossarPage(w http.ResponseWriter, r *http.Request) { +func handleGlossaryPage(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "dist/glossary.html") } -func handleGlossarAPI(w http.ResponseWriter, r *http.Request) { - writeJSON(w, http.StatusOK, glossarTerms) +func handleGlossaryAPI(w http.ResponseWriter, r *http.Request) { + writeJSON(w, http.StatusOK, glossaryTerms) } -func handleGlossarSuggest(w http.ResponseWriter, r *http.Request) { - var suggestion GlossarSuggestion +func handleGlossarySuggest(w http.ResponseWriter, r *http.Request) { + var suggestion GlossarySuggestion if err := json.NewDecoder(r.Body).Decode(&suggestion); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Ungültige Anfrage."}) return diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index a82b4a2..7de97a4 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -11,21 +11,21 @@ import ( var authClient *auth.Client // Services bundles the Phase B + C database-backed services. Pass nil if -// DATABASE_URL was unset; the Akten/deadline endpoints will return 503. +// DATABASE_URL was unset; the matter-management endpoints will return 503. type Services struct { Project *services.ProjectService Team *services.TeamService - Dezernat *services.DepartmentService - Parties *services.PartyService - Deadline *services.DeadlineService - Appointment *services.AppointmentService + Department *services.DepartmentService + Party *services.PartyService + Deadline *services.DeadlineService + Appointment *services.AppointmentService CalDAV *services.CalDAVService Rules *services.DeadlineRuleService Calculator *services.DeadlineCalculator Users *services.UserService Fristenrechner *services.FristenrechnerService Dashboard *services.DashboardService - Note *services.NoteService + Note *services.NoteService ChecklistInst *services.ChecklistInstanceService Mail *services.MailService Invite *services.InviteService @@ -39,17 +39,17 @@ func Register(mux *http.ServeMux, client *auth.Client, giteaAPIToken string, svc dbSvc = &dbServices{ projects: svc.Project, team: svc.Team, - dezernat: svc.Dezernat, - parties: svc.Parties, - deadline: svc.Deadline, - appointment: svc.Appointment, + department: svc.Department, + parties: svc.Party, + deadline: svc.Deadline, + appointment: svc.Appointment, caldav: svc.CalDAV, rules: svc.Rules, calc: svc.Calculator, users: svc.Users, fristenrechner: svc.Fristenrechner, dashboard: svc.Dashboard, - note: svc.Note, + note: svc.Note, checklistInst: svc.ChecklistInst, mail: svc.Mail, invite: svc.Invite, @@ -76,13 +76,13 @@ func Register(mux *http.ServeMux, client *auth.Client, giteaAPIToken string, svc protected := http.NewServeMux() protected.HandleFunc("GET /tools/kostenrechner", handleKostenrechnerPage) protected.HandleFunc("POST /api/tools/kostenrechner", handleKostenrechnerAPI) - protected.HandleFunc("GET /tools/deadlinesrechner", handleFristenrechnerPage) - protected.HandleFunc("POST /api/tools/deadlinesrechner", handleFristenrechnerAPI) + protected.HandleFunc("GET /tools/fristenrechner", handleFristenrechnerPage) + protected.HandleFunc("POST /api/tools/fristenrechner", handleFristenrechnerAPI) protected.HandleFunc("GET /api/tools/proceeding-types", handleProceedingTypes) protected.HandleFunc("GET /downloads", handleDownloadsPage) - protected.HandleFunc("GET /glossary", handleGlossarPage) - protected.HandleFunc("GET /api/glossaryy", handleGlossarAPI) - protected.HandleFunc("POST /api/glossaryy/suggest", handleGlossarSuggest) + protected.HandleFunc("GET /glossary", handleGlossaryPage) + protected.HandleFunc("GET /api/glossary", handleGlossaryAPI) + protected.HandleFunc("POST /api/glossary/suggest", handleGlossarySuggest) protected.HandleFunc("GET /files/{filename}", handleFileDownload) protected.HandleFunc("POST /api/files/refresh", handleFileRefresh) protected.HandleFunc("GET /links", handleLinksPage) @@ -94,77 +94,64 @@ func Register(mux *http.ServeMux, client *auth.Client, giteaAPIToken string, svc protected.HandleFunc("GET /api/tools/gebuehrentabellen", handleGebuehrentabellenAPI) protected.HandleFunc("GET /api/tools/gebuehrentabellen/lookup", handleGebuehrentabellenLookup) protected.HandleFunc("POST /api/tools/gebuehrentabellen/feedback", handleGebuehrentabellenFeedback) - protected.HandleFunc("GET /checklists", handleChecklistenPage) + protected.HandleFunc("GET /checklists", handleChecklistsPage) protected.HandleFunc("GET /checklists/instances/{id}", handleChecklistInstancePage) protected.HandleFunc("GET /checklists/{slug}", handleChecklistDetailPage) - protected.HandleFunc("GET /api/checklists", handleChecklistenAPI) + protected.HandleFunc("GET /api/checklists", handleChecklistsAPI) protected.HandleFunc("GET /api/checklists/{slug}", handleChecklistAPI) - protected.HandleFunc("POST /api/checklists/feedback", handleChecklistenFeedback) + protected.HandleFunc("POST /api/checklists/feedback", handleChecklistsFeedback) protected.HandleFunc("GET /api/checklists/{slug}/instances", handleListChecklistInstancesForTemplate) protected.HandleFunc("POST /api/checklists/{slug}/instances", handleCreateChecklistInstance) protected.HandleFunc("GET /api/checklist-instances/{id}", handleGetChecklistInstance) protected.HandleFunc("PATCH /api/checklist-instances/{id}", handleUpdateChecklistInstance) protected.HandleFunc("POST /api/checklist-instances/{id}/reset", handleResetChecklistInstance) protected.HandleFunc("DELETE /api/checklist-instances/{id}", handleDeleteChecklistInstance) - protected.HandleFunc("GET /api/projects/{id}/checklists", handleListChecklistInstancesForProjekt) - protected.HandleFunc("GET /api/akten/{id}/checklists", handleListChecklistInstancesForProjekt) // legacy alias - protected.HandleFunc("GET /courts", handleGerichtePage) - protected.HandleFunc("GET /api/courts", handleGerichteAPI) - protected.HandleFunc("POST /api/courts/feedback", handleGerichteFeedback) + protected.HandleFunc("GET /api/projects/{id}/checklists", handleListChecklistInstancesForProject) + protected.HandleFunc("GET /courts", handleCourtsPage) + protected.HandleFunc("GET /api/courts", handleCourtsAPI) + protected.HandleFunc("POST /api/courts/feedback", handleCourtsFeedback) // Phase B (DB-backed) — return 503 if DATABASE_URL unset. protected.HandleFunc("GET /api/deadline-rules", handleListDeadlineRules) protected.HandleFunc("GET /api/proceeding-types-db", handleListProceedingTypesDB) protected.HandleFunc("POST /api/deadlines/calculate", handleCalculateDeadlines) // Projects v2 (hierarchical tree — t-paliad-024). - protected.HandleFunc("GET /api/projects", handleListProjekte) - protected.HandleFunc("POST /api/projects", handleCreateProjekt) - protected.HandleFunc("GET /api/projects/{id}", handleGetProjekt) - protected.HandleFunc("PATCH /api/projects/{id}", handleUpdateProjekt) - protected.HandleFunc("DELETE /api/projects/{id}", handleDeleteProjekt) + protected.HandleFunc("GET /api/projects", handleListProjects) + protected.HandleFunc("POST /api/projects", handleCreateProject) + protected.HandleFunc("GET /api/projects/{id}", handleGetProject) + protected.HandleFunc("PATCH /api/projects/{id}", handleUpdateProject) + protected.HandleFunc("DELETE /api/projects/{id}", handleDeleteProject) protected.HandleFunc("GET /api/projects/{id}/events", handleListProjectEvents) - protected.HandleFunc("GET /api/projects/{id}/children", handleListProjektChildren) - protected.HandleFunc("GET /api/projects/{id}/tree", handleGetProjektTree) - protected.HandleFunc("GET /api/projects/{id}/ancestors", handleListProjektAncestors) - protected.HandleFunc("GET /api/projects/{id}/parties", handleListParteien) - protected.HandleFunc("POST /api/projects/{id}/parties", handleCreatePartei) + protected.HandleFunc("GET /api/projects/{id}/children", handleListProjectChildren) + protected.HandleFunc("GET /api/projects/{id}/tree", handleGetProjectTree) + protected.HandleFunc("GET /api/projects/{id}/ancestors", handleListProjectAncestors) + protected.HandleFunc("GET /api/projects/{id}/parties", handleListParties) + protected.HandleFunc("POST /api/projects/{id}/parties", handleCreateParty) // Team membership endpoints for Project detail "Team" tab. - protected.HandleFunc("GET /api/projects/{id}/team", handleListProjektTeam) + protected.HandleFunc("GET /api/projects/{id}/team", handleListProjectTeam) protected.HandleFunc("POST /api/projects/{id}/team", handleAddProjectTeamMember) protected.HandleFunc("DELETE /api/projects/{id}/team/{user_id}", handleRemoveProjectTeamMember) // Departments (structural teams). - protected.HandleFunc("GET /api/departments", handleListDezernate) - protected.HandleFunc("POST /api/departments", handleCreateDezernat) - protected.HandleFunc("GET /api/departments/{id}", handleGetDezernat) - protected.HandleFunc("PATCH /api/departments/{id}", handleUpdateDezernat) - protected.HandleFunc("DELETE /api/departments/{id}", handleDeleteDezernat) - protected.HandleFunc("GET /api/departments/{id}/members", handleListDezernatMembers) - protected.HandleFunc("POST /api/departments/{id}/members", handleAddDezernatMember) - protected.HandleFunc("DELETE /api/departments/{id}/members/{user_id}", handleRemoveDezernatMember) + protected.HandleFunc("GET /api/departments", handleListDepartments) + protected.HandleFunc("POST /api/departments", handleCreateDepartment) + protected.HandleFunc("GET /api/departments/{id}", handleGetDepartment) + protected.HandleFunc("PATCH /api/departments/{id}", handleUpdateDepartment) + protected.HandleFunc("DELETE /api/departments/{id}", handleDeleteDepartment) + protected.HandleFunc("GET /api/departments/{id}/members", handleListDepartmentMembers) + protected.HandleFunc("POST /api/departments/{id}/members", handleAddDepartmentMember) + protected.HandleFunc("DELETE /api/departments/{id}/members/{user_id}", handleRemoveDepartmentMember) - // Legacy /api/akten aliases — map to the same Project handlers during the - // frontend transition. Remove once all clients use /api/projects. - protected.HandleFunc("GET /api/akten", handleListProjekte) - protected.HandleFunc("POST /api/akten", handleCreateProjekt) - protected.HandleFunc("GET /api/akten/{id}", handleGetProjekt) - protected.HandleFunc("PATCH /api/akten/{id}", handleUpdateProjekt) - protected.HandleFunc("DELETE /api/akten/{id}", handleDeleteProjekt) - protected.HandleFunc("GET /api/akten/{id}/events", handleListProjectEvents) - protected.HandleFunc("GET /api/akten/{id}/parties", handleListParteien) - protected.HandleFunc("POST /api/akten/{id}/parties", handleCreatePartei) - - protected.HandleFunc("DELETE /api/parties/{id}", handleDeletePartei) + protected.HandleFunc("DELETE /api/parties/{id}", handleDeleteParty) // Phase F — Appointments (appointments) - protected.HandleFunc("GET /api/appointments", handleListTermine) - protected.HandleFunc("GET /api/appointments/summary", handleTermineSummary) - protected.HandleFunc("POST /api/appointments", handleCreateTermin) - protected.HandleFunc("GET /api/appointments/{id}", handleGetTermin) - protected.HandleFunc("PATCH /api/appointments/{id}", handleUpdateTermin) - protected.HandleFunc("DELETE /api/appointments/{id}", handleDeleteTermin) - protected.HandleFunc("GET /api/projects/{id}/appointments", handleListTermineForProjekt) - protected.HandleFunc("GET /api/akten/{id}/appointments", handleListTermineForProjekt) // legacy alias + protected.HandleFunc("GET /api/appointments", handleListAppointments) + protected.HandleFunc("GET /api/appointments/summary", handleAppointmentsSummary) + protected.HandleFunc("POST /api/appointments", handleCreateAppointment) + protected.HandleFunc("GET /api/appointments/{id}", handleGetAppointment) + protected.HandleFunc("PATCH /api/appointments/{id}", handleUpdateAppointment) + protected.HandleFunc("DELETE /api/appointments/{id}", handleDeleteAppointment) + protected.HandleFunc("GET /api/projects/{id}/appointments", handleListAppointmentsForProject) // Phase F — CalDAV configuration (per-user, encrypted at rest) protected.HandleFunc("GET /api/caldav-config", handleGetCalDAVConfig) @@ -174,31 +161,25 @@ func Register(mux *http.ServeMux, client *auth.Client, giteaAPIToken string, svc protected.HandleFunc("GET /api/caldav-config/log", handleCalDAVSyncLog) // Phase E — Deadlines (persistent deadlines) - protected.HandleFunc("GET /api/deadlines", handleListFristen) - protected.HandleFunc("GET /api/deadlines/summary", handleFristenSummary) - protected.HandleFunc("GET /api/deadlines/{id}", handleGetFrist) - protected.HandleFunc("PATCH /api/deadlines/{id}", handleUpdateFrist) - protected.HandleFunc("PATCH /api/deadlines/{id}/complete", handleCompleteFrist) - protected.HandleFunc("DELETE /api/deadlines/{id}", handleDeleteFrist) - protected.HandleFunc("GET /api/projects/{id}/deadlines", handleListFristenForProjekt) - protected.HandleFunc("POST /api/projects/{id}/deadlines", handleCreateFrist) - protected.HandleFunc("POST /api/projects/{id}/deadlines/bulk", handleBulkCreateFristen) - // Legacy aliases. - protected.HandleFunc("GET /api/akten/{id}/deadlines", handleListFristenForProjekt) - protected.HandleFunc("POST /api/akten/{id}/deadlines", handleCreateFrist) - protected.HandleFunc("POST /api/akten/{id}/deadlines/bulk", handleBulkCreateFristen) + protected.HandleFunc("GET /api/deadlines", handleListDeadlines) + protected.HandleFunc("GET /api/deadlines/summary", handleDeadlinesSummary) + protected.HandleFunc("GET /api/deadlines/{id}", handleGetDeadline) + protected.HandleFunc("PATCH /api/deadlines/{id}", handleUpdateDeadline) + protected.HandleFunc("PATCH /api/deadlines/{id}/complete", handleCompleteDeadline) + protected.HandleFunc("DELETE /api/deadlines/{id}", handleDeleteDeadline) + protected.HandleFunc("GET /api/projects/{id}/deadlines", handleListDeadlinesForProject) + protected.HandleFunc("POST /api/projects/{id}/deadlines", handleCreateDeadline) + protected.HandleFunc("POST /api/projects/{id}/deadlines/bulk", handleBulkCreateDeadlines) // Phase I — Notes (polymorphic notes) - protected.HandleFunc("GET /api/projects/{id}/notes", handleListNotizenForProjekt) - protected.HandleFunc("POST /api/projects/{id}/notes", handleCreateNotizForProjekt) - protected.HandleFunc("GET /api/akten/{id}/notes", handleListNotizenForProjekt) // legacy - protected.HandleFunc("POST /api/akten/{id}/notes", handleCreateNotizForProjekt) // legacy - protected.HandleFunc("GET /api/deadlines/{id}/notes", handleListNotizenForFrist) - protected.HandleFunc("POST /api/deadlines/{id}/notes", handleCreateNotizForFrist) - protected.HandleFunc("GET /api/appointments/{id}/notes", handleListNotizenForTermin) - protected.HandleFunc("POST /api/appointments/{id}/notes", handleCreateNotizForTermin) - protected.HandleFunc("PATCH /api/notes/{id}", handleUpdateNotiz) - protected.HandleFunc("DELETE /api/notes/{id}", handleDeleteNotiz) + protected.HandleFunc("GET /api/projects/{id}/notes", handleListNotesForProject) + protected.HandleFunc("POST /api/projects/{id}/notes", handleCreateNoteForProject) + protected.HandleFunc("GET /api/deadlines/{id}/notes", handleListNotesForDeadline) + protected.HandleFunc("POST /api/deadlines/{id}/notes", handleCreateNoteForDeadline) + protected.HandleFunc("GET /api/appointments/{id}/notes", handleListNotesForAppointment) + protected.HandleFunc("POST /api/appointments/{id}/notes", handleCreateNoteForAppointment) + protected.HandleFunc("PATCH /api/notes/{id}", handleUpdateNote) + protected.HandleFunc("DELETE /api/notes/{id}", handleDeleteNote) protected.HandleFunc("GET /api/me", handleGetMe) protected.HandleFunc("PATCH /api/me", handleUpdateMe) @@ -220,48 +201,39 @@ func Register(mux *http.ServeMux, client *auth.Client, giteaAPIToken string, svc // waterfall fetch (design audit §2.3). protected.HandleFunc("GET /dashboard", gateOnboarded(handleDashboardPage)) - // /projects (v2) — temporarily serves the same pre-rendered HTML as the - // legacy /akten pages during the frontend cutover. Once the TSX rewrite - // lands, dedicated handlers will replace these aliases. - protected.HandleFunc("GET /projects", gateOnboarded(handleAktenListPage)) - protected.HandleFunc("GET /projects/new", gateOnboarded(handleAktenNewPage)) - protected.HandleFunc("GET /projects/{id}", gateOnboarded(handleAktenDetailPage)) - protected.HandleFunc("GET /projects/{id}/events", gateOnboarded(handleAktenDetailPage)) - protected.HandleFunc("GET /projects/{id}/parties", gateOnboarded(handleAktenDetailPage)) - protected.HandleFunc("GET /projects/{id}/deadlines", gateOnboarded(handleAktenDetailPage)) - protected.HandleFunc("GET /projects/{id}/appointments", gateOnboarded(handleAktenDetailPage)) - protected.HandleFunc("GET /projects/{id}/dokumente", gateOnboarded(handleAktenDetailPage)) - protected.HandleFunc("GET /projects/{id}/notes", gateOnboarded(handleAktenDetailPage)) - protected.HandleFunc("GET /projects/{id}/checklists", gateOnboarded(handleAktenDetailPage)) - protected.HandleFunc("GET /projects/{id}/team", gateOnboarded(handleAktenDetailPage)) - - // Phase D — server-rendered Akten pages (legacy aliases). - protected.HandleFunc("GET /akten", gateOnboarded(handleAktenListPage)) - protected.HandleFunc("GET /projects/new", gateOnboarded(handleAktenNewPage)) - protected.HandleFunc("GET /akten/{id}", gateOnboarded(handleAktenDetailPage)) - protected.HandleFunc("GET /akten/{id}/events", gateOnboarded(handleAktenDetailPage)) - protected.HandleFunc("GET /akten/{id}/parties", gateOnboarded(handleAktenDetailPage)) - protected.HandleFunc("GET /akten/{id}/deadlines", gateOnboarded(handleAktenDetailPage)) - protected.HandleFunc("GET /akten/{id}/appointments", gateOnboarded(handleAktenDetailPage)) - protected.HandleFunc("GET /akten/{id}/dokumente", gateOnboarded(handleAktenDetailPage)) - protected.HandleFunc("GET /akten/{id}/notes", gateOnboarded(handleAktenDetailPage)) - protected.HandleFunc("GET /akten/{id}/checklists", gateOnboarded(handleAktenDetailPage)) + // Phase D — server-rendered Projects pages. + protected.HandleFunc("GET /projects", gateOnboarded(handleProjectsListPage)) + protected.HandleFunc("GET /projects/new", gateOnboarded(handleProjectsNewPage)) + protected.HandleFunc("GET /projects/{id}", gateOnboarded(handleProjectsDetailPage)) + protected.HandleFunc("GET /projects/{id}/events", gateOnboarded(handleProjectsDetailPage)) + protected.HandleFunc("GET /projects/{id}/parties", gateOnboarded(handleProjectsDetailPage)) + protected.HandleFunc("GET /projects/{id}/deadlines", gateOnboarded(handleProjectsDetailPage)) + protected.HandleFunc("GET /projects/{id}/appointments", gateOnboarded(handleProjectsDetailPage)) + protected.HandleFunc("GET /projects/{id}/documents", gateOnboarded(handleProjectsDetailPage)) + protected.HandleFunc("GET /projects/{id}/notes", gateOnboarded(handleProjectsDetailPage)) + protected.HandleFunc("GET /projects/{id}/checklists", gateOnboarded(handleProjectsDetailPage)) + protected.HandleFunc("GET /projects/{id}/team", gateOnboarded(handleProjectsDetailPage)) + protected.HandleFunc("GET /projects/{id}/deadlines/new", gateOnboarded(handleDeadlinesNewPage)) + protected.HandleFunc("GET /projects/{id}/appointments/new", gateOnboarded(handleAppointmentsNewPage)) // Phase E — Deadlines (persistent deadline) pages - protected.HandleFunc("GET /deadlines", gateOnboarded(handleFristenListPage)) - protected.HandleFunc("GET /deadlines/new", gateOnboarded(handleFristenNewPage)) - protected.HandleFunc("GET /deadlines/calendar", gateOnboarded(handleFristenKalenderPage)) - protected.HandleFunc("GET /deadlines/{id}", gateOnboarded(handleFristenDetailPage)) - protected.HandleFunc("GET /akten/{id}/deadlines/new", gateOnboarded(handleFristenNewPage)) + protected.HandleFunc("GET /deadlines", gateOnboarded(handleDeadlinesListPage)) + protected.HandleFunc("GET /deadlines/new", gateOnboarded(handleDeadlinesNewPage)) + protected.HandleFunc("GET /deadlines/calendar", gateOnboarded(handleDeadlinesCalendarPage)) + protected.HandleFunc("GET /deadlines/{id}", gateOnboarded(handleDeadlinesDetailPage)) // Phase F — Appointments pages - protected.HandleFunc("GET /appointments", gateOnboarded(handleTermineListPage)) - protected.HandleFunc("GET /appointments/new", gateOnboarded(handleTermineNewPage)) - protected.HandleFunc("GET /appointments/calendar", gateOnboarded(handleTermineKalenderPage)) - protected.HandleFunc("GET /appointments/{id}", gateOnboarded(handleTermineDetailPage)) - protected.HandleFunc("GET /akten/{id}/appointments/new", gateOnboarded(handleTermineNewPage)) - protected.HandleFunc("GET /settings", gateOnboarded(handleEinstellungenPage)) - protected.HandleFunc("GET /settings/caldav", handleEinstellungenCalDAVRedirect) + protected.HandleFunc("GET /appointments", gateOnboarded(handleAppointmentsListPage)) + protected.HandleFunc("GET /appointments/new", gateOnboarded(handleAppointmentsNewPage)) + protected.HandleFunc("GET /appointments/calendar", gateOnboarded(handleAppointmentsCalendarPage)) + protected.HandleFunc("GET /appointments/{id}", gateOnboarded(handleAppointmentsDetailPage)) + + // Settings + protected.HandleFunc("GET /settings", gateOnboarded(handleSettingsPage)) + protected.HandleFunc("GET /settings/caldav", handleSettingsCalDAVRedirect) + + // Legacy German URLs — one 301 redirect table maps old → new. + registerLegacyRedirects(protected) // Session middleware refreshes tokens; user-id middleware extracts the // JWT sub claim into the request context for handlers that need it. diff --git a/internal/handlers/links.go b/internal/handlers/links.go index 74007d8..326bd40 100644 --- a/internal/handlers/links.go +++ b/internal/handlers/links.go @@ -34,7 +34,7 @@ type linksResponse struct { } var linkCategories = []linkCategory{ - {ID: "gerichte", NameDE: "Gerichte & Ämter", NameEN: "Courts & Offices"}, + {ID: "courts", NameDE: "Gerichte & Ämter", NameEN: "Courts & Offices"}, {ID: "recherche", NameDE: "Recherche", NameEN: "Research"}, {ID: "upc", NameDE: "UPC", NameEN: "UPC"}, {ID: "gesetze", NameDE: "Gesetze", NameEN: "Legislation"}, @@ -43,42 +43,42 @@ var linkCategories = []linkCategory{ var curatedLinks = []link{ // Gerichte & Ämter { - ID: "upc-cms", Category: "gerichte", + ID: "upc-cms", Category: "courts", Title: "UPC Case Management System", URL: "https://www.unified-patent-court.org/en/registry/case-management-system", DescDE: "Verfahrensverwaltung des Einheitlichen Patentgerichts. Klageschriften, Schriftsätze und Verfahrensstatus.", DescEN: "Case management of the Unified Patent Court. Statements of claim, written pleadings, and case status.", }, { - ID: "upc-register", Category: "gerichte", + ID: "upc-register", Category: "courts", Title: "UPC Register", URL: "https://www.unified-patent-court.org/en/registry", DescDE: "Öffentliches Register des UPC. Verfahrensdokumente und Entscheidungen.", DescEN: "Public register of the UPC. Case documents and decisions.", }, { - ID: "epo", Category: "gerichte", + ID: "epo", Category: "courts", Title: "Europäisches Patentamt (EPO)", URL: "https://www.epo.org", DescDE: "Europäisches Patentamt. Patentanmeldungen, Recherchen und Prüfungsverfahren.", DescEN: "European Patent Office. Patent applications, searches, and examination procedures.", }, { - ID: "dpma", Category: "gerichte", + ID: "dpma", Category: "courts", Title: "DPMA", URL: "https://www.dpma.de", DescDE: "Deutsches Patent- und Markenamt. Nationale Patente, Marken und Designs.", DescEN: "German Patent and Trade Mark Office. National patents, trade marks, and designs.", }, { - ID: "bpatg", Category: "gerichte", + ID: "bpatg", Category: "courts", Title: "Bundespatentgericht", URL: "https://www.bundespatentgericht.de", DescDE: "Bundespatentgericht. Nichtigkeitsverfahren und Beschwerden gegen DPMA-Entscheidungen.", DescEN: "Federal Patent Court. Nullity proceedings and appeals against DPMA decisions.", }, { - ID: "euipo", Category: "gerichte", + ID: "euipo", Category: "courts", Title: "EUIPO", URL: "https://euipo.europa.eu", DescDE: "Amt der Europäischen Union für geistiges Eigentum. EU-Marken und Gemeinschaftsgeschmacksmuster.", diff --git a/internal/handlers/notes.go b/internal/handlers/notes.go index 7a71900..eb118fb 100644 --- a/internal/handlers/notes.go +++ b/internal/handlers/notes.go @@ -10,7 +10,7 @@ import ( ) // GET /api/projects/{id}/notes -func handleListNotizenForProjekt(w http.ResponseWriter, r *http.Request) { +func handleListNotesForProject(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -32,7 +32,7 @@ func handleListNotizenForProjekt(w http.ResponseWriter, r *http.Request) { } // POST /api/projects/{id}/notes -func handleCreateNotizForProjekt(w http.ResponseWriter, r *http.Request) { +func handleCreateNoteForProject(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -59,7 +59,7 @@ func handleCreateNotizForProjekt(w http.ResponseWriter, r *http.Request) { } // GET /api/deadlines/{id}/notes -func handleListNotizenForFrist(w http.ResponseWriter, r *http.Request) { +func handleListNotesForDeadline(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -81,7 +81,7 @@ func handleListNotizenForFrist(w http.ResponseWriter, r *http.Request) { } // POST /api/deadlines/{id}/notes -func handleCreateNotizForFrist(w http.ResponseWriter, r *http.Request) { +func handleCreateNoteForDeadline(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -108,7 +108,7 @@ func handleCreateNotizForFrist(w http.ResponseWriter, r *http.Request) { } // GET /api/appointments/{id}/notes -func handleListNotizenForTermin(w http.ResponseWriter, r *http.Request) { +func handleListNotesForAppointment(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -130,7 +130,7 @@ func handleListNotizenForTermin(w http.ResponseWriter, r *http.Request) { } // POST /api/appointments/{id}/notes -func handleCreateNotizForTermin(w http.ResponseWriter, r *http.Request) { +func handleCreateNoteForAppointment(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -157,7 +157,7 @@ func handleCreateNotizForTermin(w http.ResponseWriter, r *http.Request) { } // PATCH /api/notes/{id} -func handleUpdateNotiz(w http.ResponseWriter, r *http.Request) { +func handleUpdateNote(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -184,7 +184,7 @@ func handleUpdateNotiz(w http.ResponseWriter, r *http.Request) { } // DELETE /api/notes/{id} -func handleDeleteNotiz(w http.ResponseWriter, r *http.Request) { +func handleDeleteNote(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } diff --git a/internal/handlers/projects.go b/internal/handlers/projects.go index 3bf662f..733f411 100644 --- a/internal/handlers/projects.go +++ b/internal/handlers/projects.go @@ -17,17 +17,17 @@ import ( type dbServices struct { projects *services.ProjectService team *services.TeamService - dezernat *services.DepartmentService - parties *services.PartyService - deadline *services.DeadlineService - appointment *services.AppointmentService + department *services.DepartmentService + parties *services.PartyService + deadline *services.DeadlineService + appointment *services.AppointmentService caldav *services.CalDAVService rules *services.DeadlineRuleService calc *services.DeadlineCalculator users *services.UserService fristenrechner *services.FristenrechnerService dashboard *services.DashboardService - note *services.NoteService + note *services.NoteService checklistInst *services.ChecklistInstanceService mail *services.MailService invite *services.InviteService @@ -75,7 +75,7 @@ func writeServiceError(w http.ResponseWriter, err error) { // GET /api/projects — list visible projects. // Query params: ?type=case&status=active&parent_id=&parent_null=1&search=foo -func handleListProjekte(w http.ResponseWriter, r *http.Request) { +func handleListProjects(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -112,7 +112,7 @@ func handleListProjekte(w http.ResponseWriter, r *http.Request) { // ({aktenzeichen, owning_office, court_ref}) for the frontend transition. // aktenzeichen → reference, court_ref → case_number, owning_office is dropped // (no longer part of the visibility model). Type defaults to 'case'. -func handleCreateProjekt(w http.ResponseWriter, r *http.Request) { +func handleCreateProject(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -181,7 +181,7 @@ func handleCreateProjekt(w http.ResponseWriter, r *http.Request) { } // GET /api/projects/{id} -func handleGetProjekt(w http.ResponseWriter, r *http.Request) { +func handleGetProject(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -203,7 +203,7 @@ func handleGetProjekt(w http.ResponseWriter, r *http.Request) { } // GET /api/projects/{id}/children — direct children. -func handleListProjektChildren(w http.ResponseWriter, r *http.Request) { +func handleListProjectChildren(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -225,7 +225,7 @@ func handleListProjektChildren(w http.ResponseWriter, r *http.Request) { } // GET /api/projects/{id}/tree — full subtree depth-first (path-ordered). -func handleGetProjektTree(w http.ResponseWriter, r *http.Request) { +func handleGetProjectTree(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -247,7 +247,7 @@ func handleGetProjektTree(w http.ResponseWriter, r *http.Request) { } // GET /api/projects/{id}/ancestors — ancestor chain for breadcrumbs. -func handleListProjektAncestors(w http.ResponseWriter, r *http.Request) { +func handleListProjectAncestors(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -269,7 +269,7 @@ func handleListProjektAncestors(w http.ResponseWriter, r *http.Request) { } // PATCH /api/projects/{id} -func handleUpdateProjekt(w http.ResponseWriter, r *http.Request) { +func handleUpdateProject(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -296,7 +296,7 @@ func handleUpdateProjekt(w http.ResponseWriter, r *http.Request) { } // DELETE /api/projects/{id} -func handleDeleteProjekt(w http.ResponseWriter, r *http.Request) { +func handleDeleteProject(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -358,7 +358,7 @@ func handleListProjectEvents(w http.ResponseWriter, r *http.Request) { } // GET /api/projects/{id}/parties -func handleListParteien(w http.ResponseWriter, r *http.Request) { +func handleListParties(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -380,7 +380,7 @@ func handleListParteien(w http.ResponseWriter, r *http.Request) { } // POST /api/projects/{id}/parties -func handleCreatePartei(w http.ResponseWriter, r *http.Request) { +func handleCreateParty(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } @@ -407,7 +407,7 @@ func handleCreatePartei(w http.ResponseWriter, r *http.Request) { } // DELETE /api/parties/{id} -func handleDeletePartei(w http.ResponseWriter, r *http.Request) { +func handleDeleteParty(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } diff --git a/internal/handlers/projects_pages.go b/internal/handlers/projects_pages.go index be33082..1007d84 100644 --- a/internal/handlers/projects_pages.go +++ b/internal/handlers/projects_pages.go @@ -8,19 +8,19 @@ import "net/http" // (bun run build) and served from disk; per-page client TS bundles call the // JSON APIs in akten.go to populate the DOM. // -// Sub-routes (/akten/{id}/events, /deadlines, /appointments, /dokumente, /parties, +// Sub-routes (/akten/{id}/events, /deadlines, /appointments, /documents, /parties, // /notes) all serve the same detail HTML; client JS reads window.location to // pick the initial tab. Deadlines/Appointments/Dokumente/Notes tabs currently show // a "Coming Soon — Phase X" panel in the client until later phases land. -func handleAktenListPage(w http.ResponseWriter, r *http.Request) { +func handleProjectsListPage(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "dist/akten.html") } -func handleAktenNewPage(w http.ResponseWriter, r *http.Request) { +func handleProjectsNewPage(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "dist/akten-neu.html") } -func handleAktenDetailPage(w http.ResponseWriter, r *http.Request) { +func handleProjectsDetailPage(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "dist/akten-detail.html") } diff --git a/internal/handlers/redirects.go b/internal/handlers/redirects.go new file mode 100644 index 0000000..657b12f --- /dev/null +++ b/internal/handlers/redirects.go @@ -0,0 +1,47 @@ +package handlers + +import ( + "net/http" + "strings" +) + +// registerLegacyRedirects wires every historical German URL path to its +// English successor as a 301 Moved Permanently. One entry per legacy prefix; +// sub-paths are preserved verbatim in the redirect target (e.g. +// /akten/abc/deadlines → /projects/abc/deadlines). +// +// Only GET is redirected — old POST/PATCH/DELETE API endpoints are not +// mirrored. API clients must update to the English /api/* paths. +func registerLegacyRedirects(mux *http.ServeMux) { + // Prefix pairs: when the request path starts with key, it's rewritten to + // value + whatever followed the key. Order does not matter because each + // prefix is registered as its own pattern on the mux. + prefixes := map[string]string{ + "/akten": "/projects", + "/projekte": "/projects", + "/fristen": "/deadlines", + "/termine": "/appointments", + "/notizen": "/notes", + "/einstellungen": "/settings", + "/checklisten": "/checklists", + "/dezernate": "/departments", + "/parteien": "/parties", + "/gerichte": "/courts", + "/glossar": "/glossary", + } + for oldPrefix, newPrefix := range prefixes { + mux.Handle("GET "+oldPrefix, redirectPrefix(oldPrefix, newPrefix)) + mux.Handle("GET "+oldPrefix+"/", redirectPrefix(oldPrefix, newPrefix)) + } +} + +func redirectPrefix(oldPrefix, newPrefix string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tail := strings.TrimPrefix(r.URL.Path, oldPrefix) + target := newPrefix + tail + if r.URL.RawQuery != "" { + target += "?" + r.URL.RawQuery + } + http.Redirect(w, r, target, http.StatusMovedPermanently) + }) +} diff --git a/internal/handlers/teams.go b/internal/handlers/teams.go index edbb688..418a5ca 100644 --- a/internal/handlers/teams.go +++ b/internal/handlers/teams.go @@ -11,7 +11,7 @@ import ( // GET /api/projects/{id}/team — returns direct + inherited team members. // inherited=true rows include inherited_from_id / inherited_from_title. -func handleListProjektTeam(w http.ResponseWriter, r *http.Request) { +func handleListProjectTeam(w http.ResponseWriter, r *http.Request) { if !requireDB(w) { return } diff --git a/internal/services/appointment_service.go b/internal/services/appointment_service.go index a2cbddb..846a62e 100644 --- a/internal/services/appointment_service.go +++ b/internal/services/appointment_service.go @@ -23,19 +23,19 @@ import ( // Audit: Project-attached mutations append project_events rows. Personal // Appointments never touch project_events. // -// CalDAV: optional hook (TerminCalDAVPusher) is called best-effort after +// CalDAV: optional hook (AppointmentCalDAVPusher) is called best-effort after // each mutation. type AppointmentService struct { db *sqlx.DB projects *ProjectService - caldav TerminCalDAVPusher + caldav AppointmentCalDAVPusher } -// TerminCalDAVPusher is the contract the CalDAV service implements so the +// AppointmentCalDAVPusher is the contract the CalDAV service implements so the // AppointmentService can push individual appointment changes without importing the // caldav package directly. -type TerminCalDAVPusher interface { +type AppointmentCalDAVPusher interface { OnTerminCreated(ctx context.Context, userID uuid.UUID, t *models.Appointment) OnTerminUpdated(ctx context.Context, userID uuid.UUID, t *models.Appointment) OnTerminDeleted(ctx context.Context, userID uuid.UUID, t *models.Appointment) @@ -46,7 +46,7 @@ func NewAppointmentService(db *sqlx.DB, projects *ProjectService) *AppointmentSe } // SetCalDAVPusher wires an optional CalDAV push hook. -func (s *AppointmentService) SetCalDAVPusher(p TerminCalDAVPusher) { +func (s *AppointmentService) SetCalDAVPusher(p AppointmentCalDAVPusher) { s.caldav = p } @@ -75,8 +75,8 @@ type UpdateTerminInput struct { AppointmentType *string `json:"appointment_type,omitempty"` } -// TerminListFilter narrows ListVisibleForUser results. -type TerminListFilter struct { +// AppointmentListFilter narrows ListVisibleForUser results. +type AppointmentListFilter struct { ProjectID *uuid.UUID From *time.Time To *time.Time @@ -85,7 +85,7 @@ type TerminListFilter struct { // ListVisibleForUser returns all Appointments the user can see (personal + // Project-attached they have visibility for), ordered by start_at ascending. -func (s *AppointmentService) ListVisibleForUser(ctx context.Context, userID uuid.UUID, filter TerminListFilter) ([]models.AppointmentWithProject, error) { +func (s *AppointmentService) ListVisibleForUser(ctx context.Context, userID uuid.UUID, filter AppointmentListFilter) ([]models.AppointmentWithProject, error) { user, err := s.users().GetByID(ctx, userID) if err != nil { return nil, err @@ -402,8 +402,8 @@ func (s *AppointmentService) Delete(ctx context.Context, userID, terminID uuid.U return nil } -// TerminSummaryCounts buckets visible Appointments into today / this_week / later. -type TerminSummaryCounts struct { +// AppointmentSummaryCounts buckets visible Appointments into today / this_week / later. +type AppointmentSummaryCounts struct { Today int `json:"today"` ThisWeek int `json:"this_week"` Later int `json:"later"` @@ -411,13 +411,13 @@ type TerminSummaryCounts struct { } // SummaryCounts aggregates Appointments by start-date bucket for the user's visible projects. -func (s *AppointmentService) SummaryCounts(ctx context.Context, userID uuid.UUID) (*TerminSummaryCounts, error) { +func (s *AppointmentService) SummaryCounts(ctx context.Context, userID uuid.UUID) (*AppointmentSummaryCounts, error) { user, err := s.users().GetByID(ctx, userID) if err != nil { return nil, err } if user == nil { - return &TerminSummaryCounts{}, nil + return &AppointmentSummaryCounts{}, nil } now := time.Now().UTC() today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC) @@ -442,7 +442,7 @@ func (s *AppointmentService) SummaryCounts(ctx context.Context, userID uuid.UUID } defer stmt.Close() - var c TerminSummaryCounts + var c AppointmentSummaryCounts if err := stmt.GetContext(ctx, &c, map[string]any{ "today": today, "tomorrow": tomorrow, diff --git a/internal/services/caldav_service.go b/internal/services/caldav_service.go index aa29f50..b4068cb 100644 --- a/internal/services/caldav_service.go +++ b/internal/services/caldav_service.go @@ -501,10 +501,10 @@ func (s *CalDAVService) pullAll(ctx context.Context, cli *calDAVClient, cfg *dec return pulled, nil } -// --- Push hooks (TerminCalDAVPusher) --- +// --- Push hooks (AppointmentCalDAVPusher) --- // OnTerminCreated, OnTerminUpdated, OnTerminDeleted satisfy the -// TerminCalDAVPusher interface. They schedule a one-shot best-effort sync +// AppointmentCalDAVPusher interface. They schedule a one-shot best-effort sync // for the relevant user on a fresh background goroutine so the // user-facing request returns immediately. func (s *CalDAVService) OnTerminCreated(_ context.Context, userID uuid.UUID, t *models.Appointment) { diff --git a/internal/services/deadline_service.go b/internal/services/deadline_service.go index 73bcfdb..523f4d1 100644 --- a/internal/services/deadline_service.go +++ b/internal/services/deadline_service.go @@ -54,21 +54,21 @@ type UpdateFristInput struct { Status *string `json:"status,omitempty"` } -// FristStatusFilter is a server-side bucket for ListVisibleForUser. -type FristStatusFilter string +// DeadlineStatusFilter is a server-side bucket for ListVisibleForUser. +type DeadlineStatusFilter string const ( - FristFilterAll FristStatusFilter = "all" - FristFilterOverdue FristStatusFilter = "overdue" - FristFilterThisWeek FristStatusFilter = "this_week" - FristFilterUpcoming FristStatusFilter = "upcoming" - FristFilterCompleted FristStatusFilter = "completed" - FristFilterPending FristStatusFilter = "pending" + DeadlineFilterAll DeadlineStatusFilter = "all" + DeadlineFilterOverdue DeadlineStatusFilter = "overdue" + DeadlineFilterThisWeek DeadlineStatusFilter = "this_week" + DeadlineFilterUpcoming DeadlineStatusFilter = "upcoming" + DeadlineFilterCompleted DeadlineStatusFilter = "completed" + DeadlineFilterPending DeadlineStatusFilter = "pending" ) // ListFilter narrows ListVisibleForUser results. type ListFilter struct { - Status FristStatusFilter + Status DeadlineStatusFilter ProjectID *uuid.UUID } @@ -98,21 +98,21 @@ func (s *DeadlineService) ListVisibleForUser(ctx context.Context, userID uuid.UU endOfWeek := today.AddDate(0, 0, 7) switch filter.Status { - case FristFilterOverdue: + case DeadlineFilterOverdue: conds = append(conds, `f.status = 'pending' AND f.due_date < :today`) args["today"] = today - case FristFilterThisWeek: + case DeadlineFilterThisWeek: conds = append(conds, `f.status = 'pending' AND f.due_date >= :today AND f.due_date < :endweek`) args["today"] = today args["endweek"] = endOfWeek - case FristFilterUpcoming: + case DeadlineFilterUpcoming: conds = append(conds, `f.status = 'pending' AND f.due_date >= :endweek`) args["endweek"] = endOfWeek - case FristFilterCompleted: + case DeadlineFilterCompleted: conds = append(conds, `f.status = 'completed'`) - case FristFilterPending: + case DeadlineFilterPending: conds = append(conds, `f.status = 'pending'`) - case FristFilterAll, "": + case DeadlineFilterAll, "": // no-op default: return nil, fmt.Errorf("%w: unknown status filter %q", ErrInvalidInput, filter.Status) diff --git a/internal/services/department_service.go b/internal/services/department_service.go index b85c969..b0f6455 100644 --- a/internal/services/department_service.go +++ b/internal/services/department_service.go @@ -149,14 +149,14 @@ func (s *DepartmentService) Delete(ctx context.Context, callerID, id uuid.UUID) } // AddMember inserts a (dezernat, user) membership. Admin-only. Idempotent. -func (s *DepartmentService) AddMember(ctx context.Context, callerID, dezernatID, userID uuid.UUID) error { +func (s *DepartmentService) AddMember(ctx context.Context, callerID, departmentID, userID uuid.UUID) error { if err := s.requireAdmin(ctx, callerID); err != nil { return err } _, err := s.db.ExecContext(ctx, `INSERT INTO paliad.department_members (department_id, user_id, created_at) VALUES ($1, $2, now()) ON CONFLICT (department_id, user_id) DO NOTHING`, - dezernatID, userID) + departmentID, userID) if err != nil { return fmt.Errorf("add dezernat member: %w", err) } @@ -164,13 +164,13 @@ func (s *DepartmentService) AddMember(ctx context.Context, callerID, dezernatID, } // RemoveMember deletes a (dezernat, user) membership. Admin-only. -func (s *DepartmentService) RemoveMember(ctx context.Context, callerID, dezernatID, userID uuid.UUID) error { +func (s *DepartmentService) RemoveMember(ctx context.Context, callerID, departmentID, userID uuid.UUID) error { if err := s.requireAdmin(ctx, callerID); err != nil { return err } _, err := s.db.ExecContext(ctx, `DELETE FROM paliad.department_members WHERE department_id = $1 AND user_id = $2`, - dezernatID, userID) + departmentID, userID) if err != nil { return fmt.Errorf("remove dezernat member: %w", err) } @@ -178,7 +178,7 @@ func (s *DepartmentService) RemoveMember(ctx context.Context, callerID, dezernat } // ListMembers returns users in the Dezernat, enriched with display fields. -type DezernatMember struct { +type DepartmentMember struct { UserID uuid.UUID `db:"user_id" json:"user_id"` Email string `db:"email" json:"email"` DisplayName string `db:"display_name" json:"display_name"` @@ -188,15 +188,15 @@ type DezernatMember struct { } // ListMembers returns users in the Dezernat (readable by any authenticated user). -func (s *DepartmentService) ListMembers(ctx context.Context, dezernatID uuid.UUID) ([]DezernatMember, error) { - var rows []DezernatMember +func (s *DepartmentService) ListMembers(ctx context.Context, departmentID uuid.UUID) ([]DepartmentMember, error) { + var rows []DepartmentMember err := s.db.SelectContext(ctx, &rows, `SELECT dm.user_id, dm.created_at, u.email, u.display_name, u.office, u.role FROM paliad.department_members dm LEFT JOIN paliad.users u ON u.id = dm.user_id WHERE dm.department_id = $1 - ORDER BY u.display_name`, dezernatID) + ORDER BY u.display_name`, departmentID) if err != nil { return nil, fmt.Errorf("list dezernat members: %w", err) } diff --git a/internal/services/fristenrechner.go b/internal/services/fristenrechner.go index 7915c0c..a0bbceb 100644 --- a/internal/services/fristenrechner.go +++ b/internal/services/fristenrechner.go @@ -11,7 +11,7 @@ import ( // FristenrechnerService renders the Paliad public Fristenrechner's response // shape from DB-stored rules. It sits on top of DeadlineRuleService and // HolidayService and produces the bilingual, rule-code + notes-rich payload -// that /tools/deadlinesrechner's client expects. +// that /tools/fristenrechner's client expects. // // The UI-facing response is distinct from the plain calculator in // DeadlineCalculator: it adds IsRootEvent, IsCourtSet, RuleRef, Notes, @@ -28,7 +28,7 @@ func NewFristenrechnerService(rules *DeadlineRuleService, holidays *HolidayServi } // UIDeadline matches the frontend's CalculatedDeadline TypeScript interface -// (camelCase JSON to keep /tools/deadlinesrechner byte-identical). +// (camelCase JSON to keep /tools/fristenrechner byte-identical). type UIDeadline struct { Code string `json:"code"` Name string `json:"name"` diff --git a/internal/services/mail_service_test.go b/internal/services/mail_service_test.go index b79effa..adb7b1d 100644 --- a/internal/services/mail_service_test.go +++ b/internal/services/mail_service_test.go @@ -46,7 +46,7 @@ func TestRenderTemplateDeadlineReminder(t *testing.T) { "DueDate": "2026-04-21", "AkteAktenzeichen": "2026/0042", "AkteTitle": "Mustermann ./. Musterfrau", - "FristURL": "https://paliad.de/deadlines/123", + "DeadlineURL": "https://paliad.de/deadlines/123", }, }) if err != nil { @@ -108,7 +108,7 @@ func TestRenderTemplateDeadlineWeekly(t *testing.T) { Name: "deadline_weekly", Data: map[string]any{ "Count": 2, - "FristenURL": "https://paliad.de/deadlines", + "DeadlinesURL": "https://paliad.de/deadlines", "Items": []map[string]any{ {"DueDate": "2026-04-20", "Title": "Heute f.", "AkteAktenzeichen": "2026/0001", "URL": "https://paliad.de/deadlines/a", "Overdue": true}, {"DueDate": "2026-04-24", "Title": "Später f.", "AkteAktenzeichen": "2026/0002", "URL": "https://paliad.de/deadlines/b", "Overdue": false}, diff --git a/internal/services/reminder_service.go b/internal/services/reminder_service.go index 2dc26cb..5950991 100644 --- a/internal/services/reminder_service.go +++ b/internal/services/reminder_service.go @@ -134,7 +134,7 @@ func (s *ReminderService) RunOnce(ctx context.Context) { // the preferred language and notification preferences. type fristReminderRow struct { DeadlineID uuid.UUID `db:"deadline_id"` - FristTitle string `db:"frist_title"` + DeadlineTitle string `db:"deadline_title"` DueDate time.Time `db:"due_date"` AkteAktenzeichen string `db:"akte_aktenzeichen"` AkteTitle string `db:"akte_title"` @@ -218,14 +218,14 @@ func (s *ReminderService) deliverFristReminder(ctx context.Context, kind string, lang = "en" } - subject := buildSubject(kind, lang, r.FristTitle, 0) + subject := buildSubject(kind, lang, r.DeadlineTitle, 0) data := map[string]any{ "Kind": kind, - "Title": r.FristTitle, + "Title": r.DeadlineTitle, "DueDate": r.DueDate.Format("2006-01-02"), "AkteAktenzeichen": r.AkteAktenzeichen, "AkteTitle": r.AkteTitle, - "FristURL": fmt.Sprintf("%s/deadlines/%s", s.baseURL, r.DeadlineID), + "DeadlineURL": fmt.Sprintf("%s/deadlines/%s", s.baseURL, r.DeadlineID), } if err := s.mail.SendTemplate(TemplateData{ To: r.UserEmail, @@ -250,7 +250,7 @@ type weeklyRow struct { UserEmailPreferences json.RawMessage `db:"user_email_preferences"` DeadlineID uuid.UUID `db:"deadline_id"` - FristTitle string `db:"frist_title"` + DeadlineTitle string `db:"deadline_title"` DueDate time.Time `db:"due_date"` AkteAktenzeichen string `db:"akte_aktenzeichen"` } @@ -370,7 +370,7 @@ func (s *ReminderService) deliverWeekly(ctx context.Context, today time.Time, ro for _, r := range rows { items = append(items, map[string]any{ "DueDate": r.DueDate.Format("2006-01-02"), - "Title": r.FristTitle, + "Title": r.DeadlineTitle, "AkteAktenzeichen": r.AkteAktenzeichen, "URL": fmt.Sprintf("%s/deadlines/%s", s.baseURL, r.DeadlineID), "Overdue": r.DueDate.Before(today), @@ -386,7 +386,7 @@ func (s *ReminderService) deliverWeekly(ctx context.Context, today time.Time, ro Data: map[string]any{ "Count": len(rows), "Items": items, - "FristenURL": fmt.Sprintf("%s/deadlines", s.baseURL), + "DeadlinesURL": fmt.Sprintf("%s/deadlines", s.baseURL), }, }); err != nil { return fmt.Errorf("send weekly: %w", err) diff --git a/internal/templates/email/deadline_reminder.html b/internal/templates/email/deadline_reminder.html index 6e29d5b..826a6ad 100644 --- a/internal/templates/email/deadline_reminder.html +++ b/internal/templates/email/deadline_reminder.html @@ -35,7 +35,7 @@

- + {{if eq .Lang "en"}}Open in Paliad{{else}}In Paliad öffnen{{end}}

diff --git a/internal/templates/email/deadline_weekly.html b/internal/templates/email/deadline_weekly.html index a8f6cca..4e5a486 100644 --- a/internal/templates/email/deadline_weekly.html +++ b/internal/templates/email/deadline_weekly.html @@ -31,7 +31,7 @@

- + {{if eq .Lang "en"}}All deadlines{{else}}Alle Fristen{{end}}