feat(admin): add proceeding-type column to /admin/procedural-events list (t-paliad-321 / m/paliad#144)
Surfaces the 3-segment proceeding code (e.g. upc.inf.cfi) on the admin
rules list so the 4 legitimately-distinct same-named groups are
visually disambiguated without opening each row's edit page.
Specifically helps with:
- "Antrag auf Patentänderung" × 4 (distinct proceeding_type_ids)
- "Beginn des Hauptsacheverfahrens" × 2
- "Berufungsbegründung-R.220.1" × 2
- "Berufungsschrift-R.220.1" × 2
(The 6× "Mängelbeseitigung / Zahlung" identical clones are dedup'd by
mig 152 in the sibling commit; this column lets m verify the dedupe
landed and confirms the remaining same-named groups are intentional.)
* internal/services/rule_editor_service.go —
- LoadProceedingTypeCodes(ctx, rows) — batch SELECT id, code FROM
paliad.proceeding_types WHERE id = ANY(...) for every distinct
non-NULL proceeding_type_id in rows. Returns id → code map.
Single round-trip, firm-wide reference data (no RLS / visibility
gate). Used only by the LIST endpoint; GetByID etc. don't need it.
* internal/handlers/admin_rules.go —
- adminRuleResponse gains ProceedingTypeCode *string field
(json:"proceeding_type_code,omitempty"). Populated by
wrapRuleListResponse from the id → code map.
- handleAdminListRules calls LoadProceedingTypeCodes after fetching
rows, passes the map to wrapRuleListResponse.
* frontend/src/admin-rules-list.tsx —
- Adds Proceeding column header in position 2 (between Submission
Code and Legal Citation) per paliadin's "Place between submission-
code and the existing columns" spec. Binds to canonical i18n
key admin.procedural_events.col.proceeding (added below).
- Drops the legacy Verfahrenstyp column at position 4 — the new
code-only column at position 2 replaces it; the old column
showed `code · name` which duplicates the new content.
* frontend/src/client/admin-rules-list.ts —
- Rule type gains proceeding_type_code?: string | null.
- New proceedingCodeCell(r) helper: prefers server-side
proceeding_type_code, falls back to dropdown-lookup
proceedingLabel for defense-in-depth on older API responses
(the old behaviour broke for rules whose proceeding_type_id
pointed at non-fristenrechner category proceedings; the new
column never has that bug because the join is server-side).
- Row rendering: new <td class="admin-rules-col-proceeding"><code>
proceedingCodeCell(r) </code></td> in column 2.
* frontend/src/client/i18n.ts —
- admin.procedural_events.col.proceeding alias added for DE +
EN ("Verfahren" / "Proceeding"). Mirror style of the other
canonical aliases from Slice A.
* frontend/src/i18n-keys.ts —
- Generated key union extended with
"admin.procedural_events.col.proceeding".
Build + vet clean. No new SQL — proceeding_types is firm-wide
reference data and the join uses an existing primary key.
This commit is contained in:
@@ -102,9 +102,9 @@ export function renderAdminRulesList(): string {
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="admin.procedural_events.col.code">Submission Code</th>
|
||||
<th data-i18n="admin.procedural_events.col.proceeding">Verfahren</th>
|
||||
<th data-i18n="admin.rules.col.legal_citation">Rechtsgrundlage</th>
|
||||
<th data-i18n="admin.rules.col.name">Name</th>
|
||||
<th data-i18n="admin.rules.col.proceeding">Verfahrenstyp</th>
|
||||
<th data-i18n="admin.rules.col.priority">Priorität</th>
|
||||
<th data-i18n="admin.rules.col.lifecycle">Lifecycle</th>
|
||||
<th data-i18n="admin.rules.col.modified">Zuletzt geändert</th>
|
||||
|
||||
@@ -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) => `
|
||||
<tr data-row-id="${esc(r.id)}" class="admin-rules-row">
|
||||
<td class="admin-rules-col-code"><code>${esc(r.submission_code || "")}</code></td>
|
||||
<td class="admin-rules-col-proceeding"><code>${esc(proceedingCodeCell(r))}</code></td>
|
||||
<td class="admin-rules-col-legal"><code>${esc(r.rule_code || "")}</code></td>
|
||||
<td>${esc(name(r))}</td>
|
||||
<td>${esc(proceedingLabel(r.proceeding_type_id ?? null))}</td>
|
||||
<td><span class="admin-rules-priority admin-rules-priority-${esc(r.priority)}">${esc(priorityLabel(r.priority))}</span></td>
|
||||
<td><span class="${lifecycleClass(r.lifecycle_state)}">${esc(lifecycleLabel(r.lifecycle_state))}</span></td>
|
||||
<td class="admin-rules-col-modified">${esc(fmtDateTime(r.updated_at))}</td>
|
||||
|
||||
@@ -3120,6 +3120,9 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"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<Lang, Record<string, string>> = {
|
||||
"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)",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user