Surfaces the Slice 11a admin API at /admin/rules so editors can drive
the rule lifecycle without curling. Three new pages, each gated by
adminGate on the route + sidebar reveal via /api/me:
/admin/rules — list page with filters (proceeding,
trigger event, lifecycle chips, fuzzy
search) and a second "Orphans" tab that
loads paliad.deadline_rule_backfill_orphans
via the new GET /admin/api/orphans
endpoint. Pick-chip on each candidate
fires the reason modal → POST resolve.
"+ Neue Regel" opens the same reason modal
with minimal required fields (name DE/EN
+ duration) and routes to the edit page
on success.
/admin/rules/{id}/edit — full form (37 columns grouped: identity /
proceeding / timing / party / display /
lifecycle / condition). Side panel hosts
the preview widget (trigger date + flags
→ GET .../preview, drafts only) and the
audit-log timeline (paginated, 20 per
page). Bottom action bar adapts to
lifecycle_state — save-draft + publish on
drafts, clone on published/archived,
archive on draft/published, restore on
archived. Every action opens the reason
modal with ≥10-char client-side guard per
Slice 11a edge case #4.
/admin/rules/export — minimal SQL preview + "Download as file"
/ "Copy to clipboard". Optional `since`
audit-id scopes the export window.
condition_expr ships with a raw JSON textarea + inline parse
validation; the tree-builder is out of scope for Slice 11b (raw JSON
is sufficient given the existing 172-row corpus and validates the
same grammar live). The dependency on document.querySelectorAll for
form binding follows the admin-event-types / admin-audit-log
playbook — no new component substrate needed.
Wiring:
- frontend/build.ts: 3 new entrypoints + 3 new HTML writes.
- frontend/src/admin.tsx: new "Regeln verwalten" card with ICON_TABLE.
- frontend/src/components/Sidebar.tsx: two new admin nav entries
(Regeln + Regel-Migrations).
- frontend/src/client/i18n.ts: 162 new keys (DE+EN), under
admin.rules.* and admin.rules.edit.* and admin.rules.export.*.
- frontend/src/styles/global.css: new admin-rules-* CSS block
appended (chips, pills, audit timeline, edit-grid, preview list,
orphan cards, export pre). Uses paliad's existing CSS tokens so
light/dark/auto themes inherit automatically.
Route registration:
- GET /admin/rules — list page shell
- GET /admin/rules/{id}/edit — edit page shell
- GET /admin/rules/export — export page shell
All routes adminGate + gateOnboarded, so non-admin users 404 before
the shell even loads. Backend audit and lifecycle invariants from
Slice 11a stay authoritative; the frontend never bypasses them.
353 lines
21 KiB
TypeScript
353 lines
21 KiB
TypeScript
import { h } from "./jsx";
|
|
import { Sidebar } from "./components/Sidebar";
|
|
import { PaliadinWidget } from "./components/PaliadinWidget";
|
|
import { BottomNav } from "./components/BottomNav";
|
|
import { Footer } from "./components/Footer";
|
|
import { PWAHead } from "./components/PWAHead";
|
|
|
|
// /admin/rules/{id}/edit — Slice 11b (t-paliad-192). Form for the full
|
|
// 37-column rule row plus a side panel with the preview widget and the
|
|
// audit-log timeline. Lifecycle action bar at the bottom adapts to the
|
|
// rule's current state (draft/published/archived). Every write goes
|
|
// through a reason modal that enforces the ≥10-char rule from Slice 11a
|
|
// edge case #4.
|
|
//
|
|
// The id of the rule is parsed from the URL path on hydration —
|
|
// frontend never reads it from a server-injected blob, so the static
|
|
// HTML shell is reusable for every rule. condition_expr ships with a
|
|
// raw JSON textarea + a simple AND/OR/NOT tree-builder (toggle).
|
|
export function renderAdminRulesEdit(): string {
|
|
return "<!DOCTYPE html>" + (
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
<meta name="theme-color" content="#BFF355" />
|
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
|
<PWAHead />
|
|
<title data-i18n="admin.rules.edit.title">Regel bearbeiten — Paliad</title>
|
|
<link rel="stylesheet" href="/assets/global.css" />
|
|
</head>
|
|
<body className="has-sidebar">
|
|
<Sidebar currentPath="/admin/rules" />
|
|
<BottomNav currentPath="/admin/rules" />
|
|
|
|
<main>
|
|
<section className="tool-page">
|
|
<div className="container">
|
|
<div className="tool-header admin-rules-edit-header">
|
|
<div>
|
|
<p className="admin-rules-breadcrumb">
|
|
<a href="/admin/rules" data-i18n="admin.rules.edit.breadcrumb">← Regeln verwalten</a>
|
|
</p>
|
|
<h1 id="rules-edit-heading" data-i18n="admin.rules.edit.heading.loading">Regel laden...</h1>
|
|
<div className="admin-rules-edit-meta">
|
|
<span id="rules-edit-lifecycle" className="admin-rules-pill admin-rules-pill-draft" />
|
|
<span id="rules-edit-id" className="admin-rules-edit-uuid" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="rules-edit-feedback" className="form-msg" style="display:none" />
|
|
|
|
<div className="admin-rules-edit-grid">
|
|
<form id="rules-edit-form" className="entity-form admin-rules-edit-form" autocomplete="off">
|
|
<fieldset className="admin-rules-fieldset">
|
|
<legend data-i18n="admin.rules.edit.section.identity">Identität</legend>
|
|
<div className="admin-rules-edit-row">
|
|
<div className="form-field">
|
|
<label htmlFor="f-name" data-i18n="admin.rules.edit.field.name">Name (DE)</label>
|
|
<input type="text" id="f-name" className="admin-rules-input" />
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="f-name-en" data-i18n="admin.rules.edit.field.name_en">Name (EN)</label>
|
|
<input type="text" id="f-name-en" className="admin-rules-input" />
|
|
</div>
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="f-description" data-i18n="admin.rules.edit.field.description">Beschreibung</label>
|
|
<textarea id="f-description" className="admin-rules-input" rows={2} />
|
|
</div>
|
|
<div className="admin-rules-edit-row">
|
|
<div className="form-field">
|
|
<label htmlFor="f-code" data-i18n="admin.rules.edit.field.code">Code</label>
|
|
<input type="text" id="f-code" className="admin-rules-input" />
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="f-rule-code" data-i18n="admin.rules.edit.field.rule_code">Rule-Code (zit.)</label>
|
|
<input type="text" id="f-rule-code" className="admin-rules-input" placeholder="z. B. RoP.151" />
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="f-legal-source" data-i18n="admin.rules.edit.field.legal_source">Rechtsgrundlage</label>
|
|
<input type="text" id="f-legal-source" className="admin-rules-input" />
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<fieldset className="admin-rules-fieldset">
|
|
<legend data-i18n="admin.rules.edit.section.proceeding">Verfahren & Trigger</legend>
|
|
<div className="admin-rules-edit-row">
|
|
<div className="form-field">
|
|
<label htmlFor="f-proceeding" data-i18n="admin.rules.edit.field.proceeding">Verfahrenstyp</label>
|
|
<select id="f-proceeding" className="admin-rules-select">
|
|
<option value="" data-i18n="admin.rules.edit.field.proceeding.none">—</option>
|
|
</select>
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="f-trigger" data-i18n="admin.rules.edit.field.trigger">Trigger-Ereignis</label>
|
|
<select id="f-trigger" className="admin-rules-select">
|
|
<option value="" data-i18n="admin.rules.edit.field.trigger.none">—</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div className="admin-rules-edit-row">
|
|
<div className="form-field">
|
|
<label htmlFor="f-parent" data-i18n="admin.rules.edit.field.parent">Parent-Regel (UUID)</label>
|
|
<input type="text" id="f-parent" className="admin-rules-input" placeholder="UUID oder leer" />
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="f-concept" data-i18n="admin.rules.edit.field.concept">Konzept (UUID)</label>
|
|
<input type="text" id="f-concept" className="admin-rules-input" placeholder="UUID oder leer" />
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="f-sequence" data-i18n="admin.rules.edit.field.sequence_order">Reihenfolge</label>
|
|
<input type="number" id="f-sequence" className="admin-rules-input" min="0" />
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<fieldset className="admin-rules-fieldset">
|
|
<legend data-i18n="admin.rules.edit.section.timing">Berechnung</legend>
|
|
<div className="admin-rules-edit-row">
|
|
<div className="form-field">
|
|
<label htmlFor="f-duration" data-i18n="admin.rules.edit.field.duration_value">Dauer</label>
|
|
<input type="number" id="f-duration" className="admin-rules-input" min="0" />
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="f-duration-unit" data-i18n="admin.rules.edit.field.duration_unit">Einheit</label>
|
|
<select id="f-duration-unit" className="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 className="form-field">
|
|
<label htmlFor="f-timing" data-i18n="admin.rules.edit.field.timing">Timing</label>
|
|
<select id="f-timing" className="admin-rules-select">
|
|
<option value="">—</option>
|
|
<option value="after">after</option>
|
|
<option value="before">before</option>
|
|
</select>
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="f-combine-op" data-i18n="admin.rules.edit.field.combine_op">Combine-Op</label>
|
|
<select id="f-combine-op" className="admin-rules-select">
|
|
<option value="">—</option>
|
|
<option value="max">max</option>
|
|
<option value="min">min</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div className="admin-rules-edit-row">
|
|
<div className="form-field">
|
|
<label htmlFor="f-alt-duration" data-i18n="admin.rules.edit.field.alt_duration_value">Alt-Dauer</label>
|
|
<input type="number" id="f-alt-duration" className="admin-rules-input" min="0" />
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="f-alt-duration-unit" data-i18n="admin.rules.edit.field.alt_duration_unit">Alt-Einheit</label>
|
|
<select id="f-alt-duration-unit" className="admin-rules-select">
|
|
<option value="">—</option>
|
|
<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 className="form-field">
|
|
<label htmlFor="f-alt-rule-code" data-i18n="admin.rules.edit.field.alt_rule_code">Alt-Rule-Code</label>
|
|
<input type="text" id="f-alt-rule-code" className="admin-rules-input" />
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="f-anchor-alt" data-i18n="admin.rules.edit.field.anchor_alt">Alt-Anchor</label>
|
|
<input type="text" id="f-anchor-alt" className="admin-rules-input" />
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<fieldset className="admin-rules-fieldset">
|
|
<legend data-i18n="admin.rules.edit.section.party">Partei & Ereignis</legend>
|
|
<div className="admin-rules-edit-row">
|
|
<div className="form-field">
|
|
<label htmlFor="f-primary-party" data-i18n="admin.rules.edit.field.primary_party">Primäre Partei</label>
|
|
<input type="text" id="f-primary-party" className="admin-rules-input" />
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="f-event-type" data-i18n="admin.rules.edit.field.event_type">Event-Typ (frei)</label>
|
|
<input type="text" id="f-event-type" className="admin-rules-input" />
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<fieldset className="admin-rules-fieldset">
|
|
<legend data-i18n="admin.rules.edit.section.display">Anzeige & Notizen</legend>
|
|
<div className="form-field">
|
|
<label htmlFor="f-notes" data-i18n="admin.rules.edit.field.deadline_notes">Hinweise (DE)</label>
|
|
<textarea id="f-notes" className="admin-rules-input" rows={2} />
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="f-notes-en" data-i18n="admin.rules.edit.field.deadline_notes_en">Hinweise (EN)</label>
|
|
<textarea id="f-notes-en" className="admin-rules-input" rows={2} />
|
|
</div>
|
|
</fieldset>
|
|
|
|
<fieldset className="admin-rules-fieldset">
|
|
<legend data-i18n="admin.rules.edit.section.lifecycle">Priorität & Flags</legend>
|
|
<div className="admin-rules-edit-row">
|
|
<div className="form-field">
|
|
<label htmlFor="f-priority" data-i18n="admin.rules.edit.field.priority">Priorität</label>
|
|
<select id="f-priority" className="admin-rules-select">
|
|
<option value="mandatory">mandatory</option>
|
|
<option value="recommended">recommended</option>
|
|
<option value="optional">optional</option>
|
|
<option value="informational">informational</option>
|
|
</select>
|
|
</div>
|
|
<div className="form-field admin-rules-checkbox-field">
|
|
<label>
|
|
<input type="checkbox" id="f-is-court-set" />
|
|
<span data-i18n="admin.rules.edit.field.is_court_set">Gerichtlich gesetzt</span>
|
|
</label>
|
|
</div>
|
|
<div className="form-field admin-rules-checkbox-field">
|
|
<label>
|
|
<input type="checkbox" id="f-is-spawn" />
|
|
<span data-i18n="admin.rules.edit.field.is_spawn">Spawn</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div className="admin-rules-edit-row" id="f-spawn-row" style="display:none">
|
|
<div className="form-field">
|
|
<label htmlFor="f-spawn-label" data-i18n="admin.rules.edit.field.spawn_label">Spawn-Label</label>
|
|
<input type="text" id="f-spawn-label" className="admin-rules-input" />
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="f-spawn-proceeding" data-i18n="admin.rules.edit.field.spawn_proceeding">Spawn-Verfahren</label>
|
|
<select id="f-spawn-proceeding" className="admin-rules-select">
|
|
<option value="" data-i18n="admin.rules.edit.field.spawn_proceeding.none">—</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<fieldset className="admin-rules-fieldset">
|
|
<legend data-i18n="admin.rules.edit.section.condition">Bedingung (condition_expr)</legend>
|
|
<p className="admin-rules-hint" data-i18n="admin.rules.edit.field.condition_hint">
|
|
JSON-Grammatik: <code>{"flag":"name"}</code> · <code>{"op":"and|or","args":[...]}</code> · <code>{"op":"not","args":[...]}</code>
|
|
</p>
|
|
<div className="form-field">
|
|
<textarea id="f-condition-expr" className="admin-rules-input admin-rules-code-input" rows={5} placeholder='z. B. {"flag":"with_ccr"}' />
|
|
<p className="admin-rules-hint" id="f-condition-msg" />
|
|
</div>
|
|
</fieldset>
|
|
</form>
|
|
|
|
<aside className="admin-rules-edit-side">
|
|
{/* Preview widget */}
|
|
<div className="admin-rules-edit-card">
|
|
<h3 data-i18n="admin.rules.edit.preview.heading">Preview</h3>
|
|
<p className="admin-rules-hint" data-i18n="admin.rules.edit.preview.hint">
|
|
Nur für Drafts. Berechnet die Fristenkette mit dieser Draft-Regel anstelle der publizierten Variante.
|
|
</p>
|
|
<div className="form-field">
|
|
<label htmlFor="preview-trigger-date" data-i18n="admin.rules.edit.preview.trigger_date">Trigger-Datum</label>
|
|
<input type="date" lang="de" id="preview-trigger-date" className="admin-rules-input" />
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="preview-flags" data-i18n="admin.rules.edit.preview.flags">Flags (komma-separiert)</label>
|
|
<input type="text" id="preview-flags" className="admin-rules-input" placeholder="z. B. with_ccr,is_appeal" />
|
|
</div>
|
|
<button type="button" id="preview-run" className="btn-secondary" data-i18n="admin.rules.edit.preview.run">
|
|
Preview berechnen
|
|
</button>
|
|
<div id="preview-result" className="admin-rules-preview-result" style="display:none" />
|
|
</div>
|
|
|
|
{/* Audit-log timeline */}
|
|
<div className="admin-rules-edit-card">
|
|
<h3 data-i18n="admin.rules.edit.audit.heading">Audit-Log</h3>
|
|
<ol id="rules-edit-audit" className="admin-rules-audit-list">
|
|
<li className="admin-rules-loading" data-i18n="admin.rules.edit.audit.loading">Lade...</li>
|
|
</ol>
|
|
<button type="button" id="audit-loadmore" className="btn-secondary" style="display:none" data-i18n="admin.rules.edit.audit.loadmore">
|
|
Weitere laden
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
|
|
{/* Action bar */}
|
|
<div className="admin-rules-actionbar">
|
|
<button type="button" id="action-save-draft" className="btn-primary" style="display:none" data-i18n="admin.rules.edit.action.save_draft">
|
|
Draft speichern
|
|
</button>
|
|
<button type="button" id="action-publish" className="btn-primary" style="display:none" data-i18n="admin.rules.edit.action.publish">
|
|
Publish
|
|
</button>
|
|
<button type="button" id="action-clone" className="btn-secondary" style="display:none" data-i18n="admin.rules.edit.action.clone">
|
|
Als Draft klonen
|
|
</button>
|
|
<button type="button" id="action-archive" className="btn-secondary" style="display:none" data-i18n="admin.rules.edit.action.archive">
|
|
Archivieren
|
|
</button>
|
|
<button type="button" id="action-restore" className="btn-secondary" style="display:none" data-i18n="admin.rules.edit.action.restore">
|
|
Wiederherstellen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
{/* Reason modal — shared for every lifecycle action. Action-specific
|
|
body text is set by the client at open time. */}
|
|
<div className="modal-overlay" id="rules-action-modal" style="display:none">
|
|
<div className="modal-card">
|
|
<div className="modal-header">
|
|
<h2 id="rules-action-modal-title">Aktion bestätigen</h2>
|
|
<button className="modal-close" id="rules-action-modal-close" type="button" aria-label="Close">×</button>
|
|
</div>
|
|
<p id="rules-action-modal-body" className="invite-modal-body" />
|
|
<form id="rules-action-modal-form" className="entity-form" autocomplete="off">
|
|
<div className="form-field">
|
|
<label htmlFor="rules-action-modal-reason" data-i18n="admin.rules.modal.reason">Grund</label>
|
|
<textarea
|
|
id="rules-action-modal-reason"
|
|
className="admin-rules-input"
|
|
rows={3}
|
|
required
|
|
minlength={10}
|
|
/>
|
|
<p className="admin-rules-hint" data-i18n="admin.rules.modal.reason.hint">
|
|
Mindestens 10 Zeichen.
|
|
</p>
|
|
</div>
|
|
<p className="form-msg" id="rules-action-modal-msg" style="display:none" />
|
|
<div className="form-actions">
|
|
<button type="button" className="btn-cancel" id="rules-action-modal-cancel" data-i18n="common.cancel">Abbrechen</button>
|
|
<button type="submit" className="btn-primary" id="rules-action-modal-submit" data-i18n="admin.rules.modal.confirm">
|
|
Bestätigen
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<Footer />
|
|
<PaliadinWidget />
|
|
<script src="/assets/admin-rules-edit.js"></script>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|