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:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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."})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
47
internal/handlers/redirects.go
Normal file
47
internal/handlers/redirects.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 öffnen{{end}}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user