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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th data-i18n="admin.procedural_events.col.code">Submission Code</th>
|
<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.legal_citation">Rechtsgrundlage</th>
|
||||||
<th data-i18n="admin.rules.col.name">Name</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.priority">Priorität</th>
|
||||||
<th data-i18n="admin.rules.col.lifecycle">Lifecycle</th>
|
<th data-i18n="admin.rules.col.lifecycle">Lifecycle</th>
|
||||||
<th data-i18n="admin.rules.col.modified">Zuletzt geändert</th>
|
<th data-i18n="admin.rules.col.modified">Zuletzt geändert</th>
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ import { initSidebar } from "./sidebar";
|
|||||||
interface Rule {
|
interface Rule {
|
||||||
id: string;
|
id: string;
|
||||||
proceeding_type_id?: number | null;
|
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
|
// submission_code is the proceeding-prefixed identifier of this rule
|
||||||
// within its proceeding (e.g. `upc.inf.cfi.soc`), distinct from
|
// within its proceeding (e.g. `upc.inf.cfi.soc`), distinct from
|
||||||
// rule_code (the legal citation, e.g. `RoP.013.1`).
|
// 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}`;
|
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 {
|
function buildFilterURL(): string {
|
||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
if (activeProceeding) qs.set("proceeding_type_id", activeProceeding);
|
if (activeProceeding) qs.set("proceeding_type_id", activeProceeding);
|
||||||
@@ -233,9 +253,9 @@ function renderRulesTable() {
|
|||||||
tbody.innerHTML = rules.map((r) => `
|
tbody.innerHTML = rules.map((r) => `
|
||||||
<tr data-row-id="${esc(r.id)}" class="admin-rules-row">
|
<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-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 class="admin-rules-col-legal"><code>${esc(r.rule_code || "")}</code></td>
|
||||||
<td>${esc(name(r))}</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="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><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>
|
<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.heading": "Verfahrensschritte verwalten",
|
||||||
"admin.procedural_events.list.new": "+ Neuer Verfahrensschritt",
|
"admin.procedural_events.list.new": "+ Neuer Verfahrensschritt",
|
||||||
"admin.procedural_events.col.code": "Code (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.title": "Verfahrensschritt bearbeiten — Paliad",
|
||||||
"admin.procedural_events.edit.breadcrumb":"← Verfahrensschritte verwalten",
|
"admin.procedural_events.edit.breadcrumb":"← Verfahrensschritte verwalten",
|
||||||
"admin.procedural_events.edit.field.code": "Code (Verfahrensschritt-Identifikator)",
|
"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.heading": "Manage procedural events",
|
||||||
"admin.procedural_events.list.new": "+ New procedural event",
|
"admin.procedural_events.list.new": "+ New procedural event",
|
||||||
"admin.procedural_events.col.code": "Code (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.title": "Edit procedural event — Paliad",
|
||||||
"admin.procedural_events.edit.breadcrumb":"← Manage procedural events",
|
"admin.procedural_events.edit.breadcrumb":"← Manage procedural events",
|
||||||
"admin.procedural_events.edit.field.code": "Code (procedural-event identifier)",
|
"admin.procedural_events.edit.field.code": "Code (procedural-event identifier)",
|
||||||
|
|||||||
@@ -297,6 +297,7 @@ export type I18nKey =
|
|||||||
| "admin.partner_units.subtitle"
|
| "admin.partner_units.subtitle"
|
||||||
| "admin.partner_units.title"
|
| "admin.partner_units.title"
|
||||||
| "admin.procedural_events.col.code"
|
| "admin.procedural_events.col.code"
|
||||||
|
| "admin.procedural_events.col.proceeding"
|
||||||
| "admin.procedural_events.edit.breadcrumb"
|
| "admin.procedural_events.edit.breadcrumb"
|
||||||
| "admin.procedural_events.edit.field.code"
|
| "admin.procedural_events.edit.field.code"
|
||||||
| "admin.procedural_events.edit.field.event_kind"
|
| "admin.procedural_events.edit.field.event_kind"
|
||||||
|
|||||||
@@ -41,14 +41,22 @@ import (
|
|||||||
// historical `submission_code` + `event_type` already on Rule's tags.
|
// historical `submission_code` + `event_type` already on Rule's tags.
|
||||||
// The embedded *models.DeadlineRule carries every existing tag through
|
// The embedded *models.DeadlineRule carries every existing tag through
|
||||||
// json.Marshal unchanged; the wrapper only ADDS the two new keys.
|
// 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 {
|
type adminRuleResponse struct {
|
||||||
*models.DeadlineRule
|
*models.DeadlineRule
|
||||||
Code *string `json:"code,omitempty"`
|
Code *string `json:"code,omitempty"`
|
||||||
EventKind *string `json:"event_kind,omitempty"`
|
EventKind *string `json:"event_kind,omitempty"`
|
||||||
|
ProceedingTypeCode *string `json:"proceeding_type_code,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// wrapRuleResponse builds the dual-emit wrapper from a service result.
|
// 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 {
|
func wrapRuleResponse(r *models.DeadlineRule) adminRuleResponse {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return adminRuleResponse{}
|
return adminRuleResponse{}
|
||||||
@@ -61,11 +69,20 @@ func wrapRuleResponse(r *models.DeadlineRule) adminRuleResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// wrapRuleListResponse maps a slice of service results into the
|
// wrapRuleListResponse maps a slice of service results into the
|
||||||
// dual-emit wrapper. Used by the LIST endpoint.
|
// dual-emit wrapper. Used by the LIST endpoint. ptCodes is an
|
||||||
func wrapRuleListResponse(rows []models.DeadlineRule) []adminRuleResponse {
|
// 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))
|
out := make([]adminRuleResponse, len(rows))
|
||||||
for i := range rows {
|
for i := range rows {
|
||||||
out[i] = wrapRuleResponse(&rows[i])
|
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
|
return out
|
||||||
}
|
}
|
||||||
@@ -128,8 +145,16 @@ func handleAdminListRules(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeRuleEditorError(w, err)
|
writeRuleEditorError(w, err)
|
||||||
return
|
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)
|
adminRuleDeprecationHeaders(w)
|
||||||
writeJSON(w, http.StatusOK, wrapRuleListResponse(rows))
|
writeJSON(w, http.StatusOK, wrapRuleListResponse(rows, ptCodes))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /admin/api/rules/{id}
|
// GET /admin/api/rules/{id}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/lib/pq"
|
||||||
|
|
||||||
"mgit.msbls.de/m/paliad/internal/models"
|
"mgit.msbls.de/m/paliad/internal/models"
|
||||||
lp "mgit.msbls.de/m/paliad/pkg/litigationplanner"
|
lp "mgit.msbls.de/m/paliad/pkg/litigationplanner"
|
||||||
@@ -677,6 +678,42 @@ func (s *RuleEditorService) ListRules(ctx context.Context, f ListRulesFilter) ([
|
|||||||
return rows, nil
|
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
|
// GetByID returns a single rule. Exported so the handler can call it
|
||||||
// directly without round-tripping through ListRules.
|
// directly without round-tripping through ListRules.
|
||||||
func (s *RuleEditorService) GetByID(ctx context.Context, id uuid.UUID) (*models.DeadlineRule, error) {
|
func (s *RuleEditorService) GetByID(ctx context.Context, id uuid.UUID) (*models.DeadlineRule, error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user