m flagged 2026-05-27 20:26: archived rules (e.g. the 5 mig 152 Mängelbeseitigung clones) clutter the /admin/procedural-events default view. They were correctly archived by mig 152 but visually noisy alongside active rules. Fix: default activeLifecycle = 'published'. The 'Alle' chip still exists for when the user wants to see drafts + archived; 'Archived' chip surfaces them on demand. Initial view shows only the active corpus.
555 lines
21 KiB
TypeScript
555 lines
21 KiB
TypeScript
import { initI18n, onLangChange, t, tDyn, getLang } from "./i18n";
|
|
import { initSidebar } from "./sidebar";
|
|
|
|
// admin-rules-list.ts — /admin/procedural-events. Drives the rule table (filterable
|
|
// by proceeding type, trigger event, lifecycle state, free-text query)
|
|
// plus the Orphans tab (Slice 10 backfill staging rows). Row click on
|
|
// a rule routes to /admin/procedural-events/{id}/edit; orphan cards have their own
|
|
// "Pick" affordance with an inline reason prompt that posts to
|
|
// /admin/api/orphans/{id}/resolve.
|
|
|
|
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`).
|
|
submission_code?: string | null;
|
|
rule_code?: string | null;
|
|
name: string;
|
|
name_en: string;
|
|
priority: string;
|
|
lifecycle_state: string;
|
|
updated_at: string;
|
|
trigger_event_id?: number | null;
|
|
duration_value: number;
|
|
duration_unit: string;
|
|
}
|
|
|
|
interface ProceedingType {
|
|
id: number;
|
|
code: string;
|
|
// `name` is the German display name on the wire; the Go `ProceedingType`
|
|
// model serialises `db:"name"` as JSON key `name` (the schema treats DE
|
|
// as primary). EN lives in `name_en`. Don't reach for `name_de` — that
|
|
// field does not exist in this payload (cf. m/paliad#113).
|
|
name: string;
|
|
name_en: string;
|
|
category: string;
|
|
}
|
|
|
|
interface TriggerEvent {
|
|
id: number;
|
|
code: string;
|
|
name: string;
|
|
name_de: string;
|
|
}
|
|
|
|
interface OrphanCandidate {
|
|
id: string;
|
|
rule_code?: string | null;
|
|
name: string;
|
|
name_en: string;
|
|
}
|
|
|
|
interface Orphan {
|
|
id: string;
|
|
deadline_id: string;
|
|
title: string;
|
|
project_id?: string | null;
|
|
project_title?: string | null;
|
|
proceeding_code?: string | null;
|
|
reason: string;
|
|
candidate_count: number;
|
|
candidate_ids: string[];
|
|
candidates: OrphanCandidate[];
|
|
created_at: string;
|
|
}
|
|
|
|
let rules: Rule[] = [];
|
|
let orphans: Orphan[] = [];
|
|
let proceedings: ProceedingType[] = [];
|
|
let triggerEvents: TriggerEvent[] = [];
|
|
|
|
let activeProceeding = "";
|
|
let activeTrigger = "";
|
|
let activeLifecycle = "published";
|
|
let activeQuery = "";
|
|
let searchDebounce: number | undefined;
|
|
|
|
function esc(s: string | null | undefined): string {
|
|
const d = document.createElement("div");
|
|
d.textContent = s ?? "";
|
|
return d.innerHTML;
|
|
}
|
|
|
|
function fmtDateTime(iso: string): string {
|
|
if (!iso) return "";
|
|
const d = new Date(iso);
|
|
if (Number.isNaN(d.getTime())) return iso;
|
|
const locale = getLang() === "de" ? "de-DE" : "en-GB";
|
|
return d.toLocaleString(locale, {
|
|
year: "numeric",
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
});
|
|
}
|
|
|
|
function showFeedback(msg: string, isError: boolean) {
|
|
const el = document.getElementById("rules-feedback") as HTMLElement | null;
|
|
if (!el) return;
|
|
el.textContent = msg;
|
|
el.className = "form-msg " + (isError ? "form-msg-error" : "form-msg-success");
|
|
el.style.display = "block";
|
|
if (!isError) {
|
|
setTimeout(() => { el.style.display = "none"; }, 4000);
|
|
}
|
|
}
|
|
|
|
function lifecycleLabel(state: string): string {
|
|
return tDyn(`admin.rules.lifecycle.${state}`) || state;
|
|
}
|
|
|
|
function lifecycleClass(state: string): string {
|
|
switch (state) {
|
|
case "draft": return "admin-rules-pill admin-rules-pill-draft";
|
|
case "published": return "admin-rules-pill admin-rules-pill-published";
|
|
case "archived": return "admin-rules-pill admin-rules-pill-archived";
|
|
default: return "admin-rules-pill";
|
|
}
|
|
}
|
|
|
|
function priorityLabel(p: string): string {
|
|
return tDyn(`admin.rules.priority.${p}`) || p;
|
|
}
|
|
|
|
function proceedingLabel(id: number | null | undefined): string {
|
|
if (id == null) return "—";
|
|
const pt = proceedings.find((p) => p.id === id);
|
|
if (!pt) return `#${id}`;
|
|
const name = getLang() === "en" ? pt.name_en : pt.name;
|
|
// Guard against a proceeding row that's missing the active-language
|
|
// name (or against a stale field-name mismatch slipping back in).
|
|
// Show the code on its own rather than "code · undefined" — that
|
|
// literal string is the smell that surfaced this bug (m/paliad#113).
|
|
if (!name) return pt.code;
|
|
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);
|
|
if (activeTrigger) qs.set("trigger_event_id", activeTrigger);
|
|
if (activeLifecycle) qs.set("lifecycle_state", activeLifecycle);
|
|
if (activeQuery) qs.set("q", activeQuery);
|
|
qs.set("limit", "500");
|
|
return "/admin/api/procedural-events?" + qs.toString();
|
|
}
|
|
|
|
async function loadProceedings(): Promise<void> {
|
|
const resp = await fetch("/api/proceeding-types-db?category=fristenrechner");
|
|
if (!resp.ok) return;
|
|
proceedings = (await resp.json()) as ProceedingType[];
|
|
const sel = document.getElementById("rules-filter-proceeding") as HTMLSelectElement | null;
|
|
if (!sel) return;
|
|
// Preserve the "Alle" placeholder option then append every proceeding.
|
|
// The placeholder is the one with empty value already in the markup.
|
|
const placeholder = sel.querySelector('option[value=""]');
|
|
sel.innerHTML = "";
|
|
if (placeholder) sel.appendChild(placeholder);
|
|
for (const pt of proceedings) {
|
|
const opt = document.createElement("option");
|
|
opt.value = String(pt.id);
|
|
const name = getLang() === "en" ? pt.name_en : pt.name;
|
|
opt.textContent = name ? `${pt.code} · ${name}` : pt.code;
|
|
sel.appendChild(opt);
|
|
}
|
|
}
|
|
|
|
async function loadTriggerEvents(): Promise<void> {
|
|
const resp = await fetch("/api/tools/trigger-events");
|
|
if (!resp.ok) return;
|
|
triggerEvents = (await resp.json()) as TriggerEvent[];
|
|
const sel = document.getElementById("rules-filter-trigger") as HTMLSelectElement | null;
|
|
if (!sel) return;
|
|
const placeholder = sel.querySelector('option[value=""]');
|
|
sel.innerHTML = "";
|
|
if (placeholder) sel.appendChild(placeholder);
|
|
for (const te of triggerEvents) {
|
|
const opt = document.createElement("option");
|
|
opt.value = String(te.id);
|
|
opt.textContent = `${te.code} · ${getLang() === "en" ? te.name : te.name_de}`;
|
|
sel.appendChild(opt);
|
|
}
|
|
}
|
|
|
|
async function loadRules(): Promise<void> {
|
|
const resp = await fetch(buildFilterURL());
|
|
if (!resp.ok) {
|
|
showFeedback(t("admin.rules.error.load") || "Konnte Regeln nicht laden.", true);
|
|
rules = [];
|
|
return;
|
|
}
|
|
const body = await resp.json();
|
|
rules = Array.isArray(body) ? body as Rule[] : [];
|
|
}
|
|
|
|
async function loadOrphans(): Promise<void> {
|
|
const resp = await fetch("/admin/api/orphans");
|
|
if (!resp.ok) {
|
|
orphans = [];
|
|
return;
|
|
}
|
|
const body = await resp.json();
|
|
orphans = Array.isArray(body) ? body as Orphan[] : [];
|
|
updateOrphansBadge();
|
|
}
|
|
|
|
function updateOrphansBadge() {
|
|
const badge = document.getElementById("rules-orphans-badge") as HTMLElement | null;
|
|
if (!badge) return;
|
|
if (orphans.length === 0) {
|
|
badge.style.display = "none";
|
|
} else {
|
|
badge.style.display = "";
|
|
badge.textContent = String(orphans.length);
|
|
}
|
|
}
|
|
|
|
function renderRulesTable() {
|
|
const tbody = document.getElementById("rules-tbody") as HTMLElement | null;
|
|
const empty = document.getElementById("rules-empty") as HTMLElement | null;
|
|
if (!tbody || !empty) return;
|
|
|
|
if (rules.length === 0) {
|
|
tbody.innerHTML = "";
|
|
empty.style.display = "block";
|
|
return;
|
|
}
|
|
empty.style.display = "none";
|
|
const name = (r: Rule) => (getLang() === "en" ? r.name_en : r.name) || r.name;
|
|
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><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>
|
|
</tr>
|
|
`).join("");
|
|
|
|
tbody.querySelectorAll<HTMLElement>(".admin-rules-row").forEach((row) => {
|
|
row.addEventListener("click", (ev) => {
|
|
const target = ev.target as HTMLElement | null;
|
|
if (target && (target.closest("a") || target.closest("button"))) return;
|
|
const id = row.dataset.rowId;
|
|
if (!id) return;
|
|
window.location.href = `/admin/procedural-events/${encodeURIComponent(id)}/edit`;
|
|
});
|
|
});
|
|
}
|
|
|
|
function renderOrphans() {
|
|
const list = document.getElementById("rules-orphans-list") as HTMLElement | null;
|
|
if (!list) return;
|
|
if (orphans.length === 0) {
|
|
list.innerHTML = `<p class="entity-empty" data-i18n="admin.rules.orphans.empty">${esc(t("admin.rules.orphans.empty") || "Keine offenen Orphans. ✔")}</p>`;
|
|
return;
|
|
}
|
|
list.innerHTML = orphans.map((o) => renderOrphanCard(o)).join("");
|
|
list.querySelectorAll<HTMLButtonElement>(".admin-rules-orphan-pick").forEach((btn) => {
|
|
btn.addEventListener("click", () => {
|
|
const orphanId = btn.dataset.orphanId!;
|
|
const ruleId = btn.dataset.ruleId!;
|
|
onPickOrphanCandidate(orphanId, ruleId);
|
|
});
|
|
});
|
|
}
|
|
|
|
function renderOrphanCard(o: Orphan): string {
|
|
const reasonLabel = tDyn(`admin.rules.orphans.reason.${o.reason}`) || o.reason;
|
|
const meta = [
|
|
o.project_title ? `<span class="admin-rules-orphan-meta">${esc(t("admin.rules.orphans.field.project") || "Projekt")}: ${esc(o.project_title)}</span>` : "",
|
|
o.proceeding_code ? `<span class="admin-rules-orphan-meta">${esc(t("admin.rules.orphans.field.proceeding") || "Verfahren")}: <code>${esc(o.proceeding_code)}</code></span>` : "",
|
|
`<span class="admin-rules-orphan-meta">${esc(t("admin.rules.orphans.field.reason") || "Grund")}: ${esc(reasonLabel)}</span>`,
|
|
].filter(Boolean).join(" · ");
|
|
|
|
let candidatesHTML = "";
|
|
if (o.candidates.length === 0) {
|
|
candidatesHTML = `<p class="admin-rules-orphan-empty">${esc(t("admin.rules.orphans.no_candidates") || "Keine Kandidaten gefunden. Bitte Regel manuell anlegen.")}</p>`;
|
|
} else {
|
|
candidatesHTML = `<div class="admin-rules-orphan-candidates">
|
|
${o.candidates.map((c) => {
|
|
const cname = getLang() === "en" ? c.name_en : c.name;
|
|
return `<button type="button" class="admin-rules-orphan-pick"
|
|
data-orphan-id="${esc(o.id)}" data-rule-id="${esc(c.id)}">
|
|
<code>${esc(c.rule_code || "")}</code>
|
|
<span class="admin-rules-orphan-pick-name">${esc(cname)}</span>
|
|
</button>`;
|
|
}).join("")}
|
|
</div>`;
|
|
}
|
|
|
|
return `
|
|
<div class="admin-rules-orphan-card" data-orphan-id="${esc(o.id)}">
|
|
<div class="admin-rules-orphan-header">
|
|
<div class="admin-rules-orphan-title">${esc(o.title)}</div>
|
|
<div class="admin-rules-orphan-metas">${meta}</div>
|
|
</div>
|
|
${candidatesHTML}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// --------------------------------------------------------------------
|
|
// Reason modal — shared between "+ Neue Regel" and orphan resolve.
|
|
// --------------------------------------------------------------------
|
|
type ModalContext =
|
|
| { kind: "new-rule" }
|
|
| { kind: "orphan-resolve"; orphanId: string; ruleId: string };
|
|
|
|
let modalCtx: ModalContext | null = null;
|
|
|
|
function openReasonModal(ctx: ModalContext) {
|
|
modalCtx = ctx;
|
|
const modal = document.getElementById("rules-reason-modal") as HTMLElement;
|
|
const title = document.getElementById("rules-reason-title") as HTMLElement;
|
|
const body = document.getElementById("rules-reason-body") as HTMLElement;
|
|
const extra = document.getElementById("rules-reason-extra") as HTMLElement;
|
|
const msg = document.getElementById("rules-reason-msg") as HTMLElement;
|
|
const reasonInput = document.getElementById("rules-reason-text") as HTMLTextAreaElement;
|
|
msg.style.display = "none";
|
|
reasonInput.value = "";
|
|
extra.innerHTML = "";
|
|
|
|
if (ctx.kind === "new-rule") {
|
|
title.textContent = t("admin.rules.modal.new.title") || "Neue Regel anlegen";
|
|
body.textContent = t("admin.rules.modal.new.body") || "Eine neue Regel wird als Draft angelegt. Bitte einen Grund angeben.";
|
|
extra.innerHTML = `
|
|
<div class="form-field">
|
|
<label for="rules-new-name" data-i18n="admin.rules.modal.field.name">Name (DE)</label>
|
|
<input type="text" id="rules-new-name" class="admin-rules-input" required minlength="2" />
|
|
</div>
|
|
<div class="form-field">
|
|
<label for="rules-new-name-en" data-i18n="admin.rules.modal.field.name_en">Name (EN)</label>
|
|
<input type="text" id="rules-new-name-en" class="admin-rules-input" required minlength="2" />
|
|
</div>
|
|
<div class="form-field">
|
|
<label for="rules-new-duration" data-i18n="admin.rules.modal.field.duration">Dauer</label>
|
|
<div class="admin-rules-duration-row">
|
|
<input type="number" id="rules-new-duration" class="admin-rules-input" min="0" value="0" required />
|
|
<select id="rules-new-unit" class="admin-rules-select">
|
|
<option value="days">days</option>
|
|
<option value="weeks">weeks</option>
|
|
<option value="months">months</option>
|
|
<option value="working_days">working_days</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
`;
|
|
} else {
|
|
title.textContent = t("admin.rules.modal.resolve.title") || "Orphan zuordnen";
|
|
body.textContent = t("admin.rules.modal.resolve.body") || "Bitte einen Grund (mind. 10 Zeichen) angeben.";
|
|
}
|
|
modal.style.display = "flex";
|
|
reasonInput.focus();
|
|
}
|
|
|
|
function closeReasonModal() {
|
|
const modal = document.getElementById("rules-reason-modal") as HTMLElement;
|
|
modal.style.display = "none";
|
|
modalCtx = null;
|
|
}
|
|
|
|
async function submitReasonModal(ev: Event) {
|
|
ev.preventDefault();
|
|
if (!modalCtx) return;
|
|
const reasonInput = document.getElementById("rules-reason-text") as HTMLTextAreaElement;
|
|
const msg = document.getElementById("rules-reason-msg") as HTMLElement;
|
|
const submit = document.getElementById("rules-reason-submit") as HTMLButtonElement;
|
|
const reason = reasonInput.value.trim();
|
|
if (reason.length < 10) {
|
|
msg.textContent = t("admin.rules.modal.reason.too_short") || "Grund muss mindestens 10 Zeichen enthalten.";
|
|
msg.className = "form-msg form-msg-error";
|
|
msg.style.display = "block";
|
|
return;
|
|
}
|
|
submit.disabled = true;
|
|
try {
|
|
if (modalCtx.kind === "new-rule") {
|
|
const name = (document.getElementById("rules-new-name") as HTMLInputElement).value.trim();
|
|
const nameEn = (document.getElementById("rules-new-name-en") as HTMLInputElement).value.trim();
|
|
const duration = parseInt((document.getElementById("rules-new-duration") as HTMLInputElement).value, 10);
|
|
const unit = (document.getElementById("rules-new-unit") as HTMLSelectElement).value;
|
|
if (!name || !nameEn) {
|
|
msg.textContent = t("admin.rules.modal.error.name_required") || "Bitte Name und Name (EN) angeben.";
|
|
msg.className = "form-msg form-msg-error";
|
|
msg.style.display = "block";
|
|
submit.disabled = false;
|
|
return;
|
|
}
|
|
const resp = await fetch("/admin/api/procedural-events", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
name,
|
|
name_en: nameEn,
|
|
duration_value: Number.isFinite(duration) ? duration : 0,
|
|
duration_unit: unit,
|
|
priority: "mandatory",
|
|
is_court_set: false,
|
|
is_spawn: false,
|
|
sequence_order: 0,
|
|
reason,
|
|
}),
|
|
});
|
|
if (!resp.ok) {
|
|
const body = await resp.json().catch(() => ({ error: resp.statusText }));
|
|
msg.textContent = body.error || t("admin.rules.modal.error.create") || "Anlegen fehlgeschlagen.";
|
|
msg.className = "form-msg form-msg-error";
|
|
msg.style.display = "block";
|
|
submit.disabled = false;
|
|
return;
|
|
}
|
|
const created = await resp.json();
|
|
window.location.href = `/admin/procedural-events/${encodeURIComponent(created.id)}/edit`;
|
|
return;
|
|
}
|
|
|
|
if (modalCtx.kind === "orphan-resolve") {
|
|
const resp = await fetch(`/admin/api/orphans/${encodeURIComponent(modalCtx.orphanId)}/resolve`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ rule_id: modalCtx.ruleId, reason }),
|
|
});
|
|
if (!resp.ok) {
|
|
const body = await resp.json().catch(() => ({ error: resp.statusText }));
|
|
msg.textContent = body.error || t("admin.rules.modal.error.resolve") || "Zuordnung fehlgeschlagen.";
|
|
msg.className = "form-msg form-msg-error";
|
|
msg.style.display = "block";
|
|
submit.disabled = false;
|
|
return;
|
|
}
|
|
closeReasonModal();
|
|
showFeedback(t("admin.rules.orphans.resolved") || "Orphan zugeordnet.", false);
|
|
await loadOrphans();
|
|
renderOrphans();
|
|
}
|
|
} finally {
|
|
submit.disabled = false;
|
|
}
|
|
}
|
|
|
|
function onPickOrphanCandidate(orphanId: string, ruleId: string) {
|
|
openReasonModal({ kind: "orphan-resolve", orphanId, ruleId });
|
|
}
|
|
|
|
// --------------------------------------------------------------------
|
|
// Tabs + filter wiring.
|
|
// --------------------------------------------------------------------
|
|
function setActiveTab(name: "rules" | "orphans") {
|
|
const paneRules = document.getElementById("rules-pane-rules") as HTMLElement;
|
|
const paneOrphans = document.getElementById("rules-pane-orphans") as HTMLElement;
|
|
const tabRules = document.getElementById("rules-tab-rules") as HTMLElement;
|
|
const tabOrphans = document.getElementById("rules-tab-orphans") as HTMLElement;
|
|
if (name === "rules") {
|
|
paneRules.style.display = "";
|
|
paneOrphans.style.display = "none";
|
|
tabRules.classList.add("active");
|
|
tabOrphans.classList.remove("active");
|
|
} else {
|
|
paneRules.style.display = "none";
|
|
paneOrphans.style.display = "";
|
|
tabRules.classList.remove("active");
|
|
tabOrphans.classList.add("active");
|
|
renderOrphans();
|
|
}
|
|
}
|
|
|
|
function wireFilters() {
|
|
const proc = document.getElementById("rules-filter-proceeding") as HTMLSelectElement;
|
|
const trig = document.getElementById("rules-filter-trigger") as HTMLSelectElement;
|
|
const search = document.getElementById("rules-filter-search") as HTMLInputElement;
|
|
proc.addEventListener("change", async () => {
|
|
activeProceeding = proc.value;
|
|
await loadRules();
|
|
renderRulesTable();
|
|
});
|
|
trig.addEventListener("change", async () => {
|
|
activeTrigger = trig.value;
|
|
await loadRules();
|
|
renderRulesTable();
|
|
});
|
|
search.addEventListener("input", () => {
|
|
window.clearTimeout(searchDebounce);
|
|
searchDebounce = window.setTimeout(async () => {
|
|
activeQuery = search.value.trim();
|
|
await loadRules();
|
|
renderRulesTable();
|
|
}, 220);
|
|
});
|
|
document.querySelectorAll<HTMLButtonElement>("#rules-filter-lifecycle .admin-rules-chip").forEach((chip) => {
|
|
chip.addEventListener("click", async () => {
|
|
document.querySelectorAll(".admin-rules-chip").forEach((c) => c.classList.remove("active"));
|
|
chip.classList.add("active");
|
|
activeLifecycle = chip.dataset.state || "";
|
|
await loadRules();
|
|
renderRulesTable();
|
|
});
|
|
});
|
|
}
|
|
|
|
function wireTabs() {
|
|
(document.getElementById("rules-tab-rules") as HTMLElement).addEventListener("click", () => setActiveTab("rules"));
|
|
(document.getElementById("rules-tab-orphans") as HTMLElement).addEventListener("click", () => setActiveTab("orphans"));
|
|
}
|
|
|
|
function wireModal() {
|
|
(document.getElementById("rules-new-btn") as HTMLElement).addEventListener("click", () => openReasonModal({ kind: "new-rule" }));
|
|
(document.getElementById("rules-reason-cancel") as HTMLElement).addEventListener("click", closeReasonModal);
|
|
(document.getElementById("rules-reason-close") as HTMLElement).addEventListener("click", closeReasonModal);
|
|
(document.getElementById("rules-reason-form") as HTMLFormElement).addEventListener("submit", submitReasonModal);
|
|
}
|
|
|
|
async function init() {
|
|
initI18n();
|
|
initSidebar();
|
|
wireFilters();
|
|
wireTabs();
|
|
wireModal();
|
|
await Promise.all([loadProceedings(), loadTriggerEvents()]);
|
|
await Promise.all([loadRules(), loadOrphans()]);
|
|
renderRulesTable();
|
|
// Re-render proceeding labels when language changes
|
|
onLangChange(() => {
|
|
renderRulesTable();
|
|
renderOrphans();
|
|
});
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", init);
|