diff --git a/frontend/src/admin-rules-list.tsx b/frontend/src/admin-rules-list.tsx index f7ff705..a00de07 100644 --- a/frontend/src/admin-rules-list.tsx +++ b/frontend/src/admin-rules-list.tsx @@ -102,9 +102,9 @@ export function renderAdminRulesList(): string { Submission Code + Verfahren Rechtsgrundlage Name - Verfahrenstyp Priorität Lifecycle Zuletzt geändert diff --git a/frontend/src/client/admin-rules-list.ts b/frontend/src/client/admin-rules-list.ts index bfe3668..169d571 100644 --- a/frontend/src/client/admin-rules-list.ts +++ b/frontend/src/client/admin-rules-list.ts @@ -11,6 +11,13 @@ import { initSidebar } from "./sidebar"; interface Rule { id: string; proceeding_type_id?: number | null; + // proceeding_type_code is the joined paliad.proceeding_types.code + // for proceeding_type_id, populated server-side by the + // /admin/api/procedural-events LIST handler (t-paliad-321). Lets the + // table show the 3-segment proceeding code (e.g. "upc.inf.cfi") at + // a glance without depending on the FILTER-dropdown's limited + // proceeding list. NULL on event-rooted rules. + proceeding_type_code?: string | null; // submission_code is the proceeding-prefixed identifier of this rule // within its proceeding (e.g. `upc.inf.cfi.soc`), distinct from // rule_code (the legal citation, e.g. `RoP.013.1`). @@ -138,6 +145,19 @@ function proceedingLabel(id: number | null | undefined): string { return `${pt.code} · ${name}`; } +// proceedingCodeCell renders the LIST table's Proceeding column. Uses +// the server-side joined proceeding_type_code when available +// (t-paliad-321), falling back to the dropdown-lookup proceedingLabel +// for older API responses or for rules whose proceeding_type_id +// resolves but proceeding_type_code didn't (defence-in-depth). NULL +// proceeding_type_id renders as the em-dash placeholder used +// elsewhere in the admin table. +function proceedingCodeCell(r: Rule): string { + if (r.proceeding_type_code) return r.proceeding_type_code; + if (r.proceeding_type_id == null) return "—"; + return proceedingLabel(r.proceeding_type_id); +} + function buildFilterURL(): string { const qs = new URLSearchParams(); if (activeProceeding) qs.set("proceeding_type_id", activeProceeding); @@ -233,9 +253,9 @@ function renderRulesTable() { tbody.innerHTML = rules.map((r) => ` ${esc(r.submission_code || "")} + ${esc(proceedingCodeCell(r))} ${esc(r.rule_code || "")} ${esc(name(r))} - ${esc(proceedingLabel(r.proceeding_type_id ?? null))} ${esc(priorityLabel(r.priority))} ${esc(lifecycleLabel(r.lifecycle_state))} ${esc(fmtDateTime(r.updated_at))} diff --git a/frontend/src/client/i18n.ts b/frontend/src/client/i18n.ts index 641bcbd..d6e151c 100644 --- a/frontend/src/client/i18n.ts +++ b/frontend/src/client/i18n.ts @@ -3120,6 +3120,9 @@ const translations: Record> = { "admin.procedural_events.list.heading": "Verfahrensschritte verwalten", "admin.procedural_events.list.new": "+ Neuer Verfahrensschritt", "admin.procedural_events.col.code": "Code (Verfahrensschritt)", + // t-paliad-321: 3-segment proceeding-type code column (joined + // server-side); disambiguates same-named rules across proceedings. + "admin.procedural_events.col.proceeding": "Verfahren", "admin.procedural_events.edit.title": "Verfahrensschritt bearbeiten — Paliad", "admin.procedural_events.edit.breadcrumb":"← Verfahrensschritte verwalten", "admin.procedural_events.edit.field.code": "Code (Verfahrensschritt-Identifikator)", @@ -6188,6 +6191,8 @@ const translations: Record> = { "admin.procedural_events.list.heading": "Manage procedural events", "admin.procedural_events.list.new": "+ New procedural event", "admin.procedural_events.col.code": "Code (procedural event)", + // t-paliad-321: 3-segment proceeding-type code column. + "admin.procedural_events.col.proceeding": "Proceeding", "admin.procedural_events.edit.title": "Edit procedural event — Paliad", "admin.procedural_events.edit.breadcrumb":"← Manage procedural events", "admin.procedural_events.edit.field.code": "Code (procedural-event identifier)", diff --git a/frontend/src/i18n-keys.ts b/frontend/src/i18n-keys.ts index b12f182..1abfdc8 100644 --- a/frontend/src/i18n-keys.ts +++ b/frontend/src/i18n-keys.ts @@ -297,6 +297,7 @@ export type I18nKey = | "admin.partner_units.subtitle" | "admin.partner_units.title" | "admin.procedural_events.col.code" + | "admin.procedural_events.col.proceeding" | "admin.procedural_events.edit.breadcrumb" | "admin.procedural_events.edit.field.code" | "admin.procedural_events.edit.field.event_kind" diff --git a/internal/handlers/admin_rules.go b/internal/handlers/admin_rules.go index f6244b1..5296224 100644 --- a/internal/handlers/admin_rules.go +++ b/internal/handlers/admin_rules.go @@ -41,14 +41,22 @@ import ( // historical `submission_code` + `event_type` already on Rule's tags. // The embedded *models.DeadlineRule carries every existing tag through // json.Marshal unchanged; the wrapper only ADDS the two new keys. +// +// ProceedingTypeCode (t-paliad-321) is the joined paliad.proceeding_types.code +// for the row's proceeding_type_id. NULL on event-rooted rules. Lets the +// /admin/procedural-events list disambiguate same-named rules at a glance +// (e.g. "Berufungsbegründung" rows differ only by proceeding code). type adminRuleResponse struct { *models.DeadlineRule - Code *string `json:"code,omitempty"` - EventKind *string `json:"event_kind,omitempty"` + Code *string `json:"code,omitempty"` + EventKind *string `json:"event_kind,omitempty"` + ProceedingTypeCode *string `json:"proceeding_type_code,omitempty"` } // wrapRuleResponse builds the dual-emit wrapper from a service result. -// Same values, two keys per concept — no semantic change. +// Same values, two keys per concept — no semantic change. Pass a non-nil +// ptCode to populate the proceeding_type_code field; nil leaves it +// absent (e.g. on event-rooted rules with NULL proceeding_type_id). func wrapRuleResponse(r *models.DeadlineRule) adminRuleResponse { if r == nil { return adminRuleResponse{} @@ -61,11 +69,20 @@ func wrapRuleResponse(r *models.DeadlineRule) adminRuleResponse { } // wrapRuleListResponse maps a slice of service results into the -// dual-emit wrapper. Used by the LIST endpoint. -func wrapRuleListResponse(rows []models.DeadlineRule) []adminRuleResponse { +// dual-emit wrapper. Used by the LIST endpoint. ptCodes is an +// optional id → code lookup populated by handleAdminListRules from a +// single batch query against paliad.proceeding_types; nil leaves +// every row's proceeding_type_code empty (the LIST endpoint always +// passes a populated map; other callers don't need it). +func wrapRuleListResponse(rows []models.DeadlineRule, ptCodes map[int]string) []adminRuleResponse { out := make([]adminRuleResponse, len(rows)) for i := range rows { out[i] = wrapRuleResponse(&rows[i]) + if ptCodes != nil && rows[i].ProceedingTypeID != nil { + if code, ok := ptCodes[*rows[i].ProceedingTypeID]; ok { + out[i].ProceedingTypeCode = &code + } + } } return out } @@ -128,8 +145,16 @@ func handleAdminListRules(w http.ResponseWriter, r *http.Request) { writeRuleEditorError(w, err) return } + // t-paliad-321: batch-fetch proceeding_type.code for every rule + // row that carries a non-NULL proceeding_type_id, so the LIST + // response can show a Proceeding column without an N+1 join. + ptCodes, err := dbSvc.ruleEditor.LoadProceedingTypeCodes(r.Context(), rows) + if err != nil { + writeRuleEditorError(w, err) + return + } adminRuleDeprecationHeaders(w) - writeJSON(w, http.StatusOK, wrapRuleListResponse(rows)) + writeJSON(w, http.StatusOK, wrapRuleListResponse(rows, ptCodes)) } // GET /admin/api/rules/{id} diff --git a/internal/services/rule_editor_service.go b/internal/services/rule_editor_service.go index ef6c524..5fc77fd 100644 --- a/internal/services/rule_editor_service.go +++ b/internal/services/rule_editor_service.go @@ -11,6 +11,7 @@ import ( "github.com/google/uuid" "github.com/jmoiron/sqlx" + "github.com/lib/pq" "mgit.msbls.de/m/paliad/internal/models" lp "mgit.msbls.de/m/paliad/pkg/litigationplanner" @@ -677,6 +678,42 @@ func (s *RuleEditorService) ListRules(ctx context.Context, f ListRulesFilter) ([ return rows, nil } +// LoadProceedingTypeCodes returns an id → code map for every distinct +// non-NULL proceeding_type_id present in rows. Single SELECT against +// paliad.proceeding_types (firm-wide reference data, no RLS). Used by +// /admin/api/procedural-events to enrich the LIST response with a +// proceeding_type_code field so the admin UI can disambiguate +// same-named rules at a glance (t-paliad-321). +func (s *RuleEditorService) LoadProceedingTypeCodes(ctx context.Context, rows []models.DeadlineRule) (map[int]string, error) { + seen := map[int]bool{} + var ids []int + for _, r := range rows { + if r.ProceedingTypeID != nil && !seen[*r.ProceedingTypeID] { + seen[*r.ProceedingTypeID] = true + ids = append(ids, *r.ProceedingTypeID) + } + } + if len(ids) == 0 { + return nil, nil + } + type pair struct { + ID int `db:"id"` + Code string `db:"code"` + } + var pairs []pair + if err := s.db.SelectContext(ctx, &pairs, + `SELECT id, code FROM paliad.proceeding_types WHERE id = ANY($1)`, + pq.Array(ids), + ); err != nil { + return nil, fmt.Errorf("load proceeding_type codes: %w", err) + } + out := make(map[int]string, len(pairs)) + for _, p := range pairs { + out[p.ID] = p.Code + } + return out, nil +} + // GetByID returns a single rule. Exported so the handler can call it // directly without round-tripping through ListRules. func (s *RuleEditorService) GetByID(ctx context.Context, id uuid.UUID) (*models.DeadlineRule, error) {