refactor(rename): handler functions, routes, legacy 301 redirects

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.
This commit is contained in:
m
2026-04-20 17:40:55 +02:00
parent 3faec6c526
commit 49c6bc75ca
28 changed files with 314 additions and 295 deletions

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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=<name> 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)
}

View File

@@ -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
}

View File

@@ -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."})

View File

@@ -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

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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")
}

View File

@@ -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": "<uuid>"}
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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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.",

View File

@@ -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
}

View File

@@ -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=<uuid>&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
}

View File

@@ -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")
}

View File

@@ -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)
})
}

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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"`

View File

@@ -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},

View File

@@ -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)

View File

@@ -35,7 +35,7 @@
</table>
<p style="margin:20px 0 0 0;">
<a href="{{.FristURL}}" style="display:inline-block;background:#1c1917;color:#ffffff;padding:10px 20px;border-radius:6px;text-decoration:none;font-weight:600;font-size:14px;">
<a href="{{.DeadlineURL}}" style="display:inline-block;background:#1c1917;color:#ffffff;padding:10px 20px;border-radius:6px;text-decoration:none;font-weight:600;font-size:14px;">
{{if eq .Lang "en"}}Open in Paliad{{else}}In Paliad &ouml;ffnen{{end}}
</a>
</p>

View File

@@ -31,7 +31,7 @@
</table>
<p style="margin:20px 0 0 0;">
<a href="{{.FristenURL}}" style="display:inline-block;background:#1c1917;color:#ffffff;padding:10px 20px;border-radius:6px;text-decoration:none;font-weight:600;font-size:14px;">
<a href="{{.DeadlinesURL}}" style="display:inline-block;background:#1c1917;color:#ffffff;padding:10px 20px;border-radius:6px;text-decoration:none;font-weight:600;font-size:14px;">
{{if eq .Lang "en"}}All deadlines{{else}}Alle Fristen{{end}}
</a>
</p>