Files
paliad/frontend/src/deadlines-detail.tsx
mAi 045accc6d9 mAi: #89 - deadline rule field binary Auto/Custom + canonical rule-label display
t-paliad-258. m's verdict on t-paliad-251's rule UI: "too many options"
(4 'Oral hearings' across courts, etc.). Replace the full deadline_rules
catalog dropdown + sort selector with a binary model and unify the rule
display contract across every surface that prints a rule label.

Binary Rule field on the deadline form
- Auto (default): rule_id is derived from the chosen Type. The resolved
  rule renders read-only as 'Auto | <Name · Citation>' next to the
  field. No catalog picker, no sort options.
- Custom: free-text input. Stored as deadlines.custom_rule_text (new
  nullable column, migration 122). Mutually exclusive with rule_id at
  the persistence boundary.
- Toggle link flips between modes. Re-toggling to Auto re-resolves from
  the current Type — no stale state.

Schema + service (additive)
- migration 122 adds paliad.deadlines.custom_rule_text (nullable).
  Existing rows: empty custom_rule_text + non-null rule_id = Auto-
  equivalent. Both NULL = "keine Regel" (consistent with today).
- models.Deadline.CustomRuleText + service SELECTs include the column.
- CreateDeadlineInput accepts custom_rule_text; the service drops it
  when rule_id is set (catalog wins; simple invariant at the boundary).
- UpdateDeadlineInput grows a {RuleSet, RuleID, CustomRuleText} triple.
  RuleSet=true is the discriminator so absent fields don't overwrite
  the row (PATCH semantics). RuleID and CustomRuleText are mutually
  exclusive in one request; service rejects "both set".
- EventListItem (the /api/events union) carries CustomRuleText so list
  surfaces can render it.

Frontend: deadlines-new
- Drop the rule <select>, the by_proceeding/by_court/alpha sort
  dropdown, the override-warning slot, and the collapsed-by-Regel Typ
  view. Strip the (Rule→Type) auto-fill machinery — direction is now
  one-way (Type → Auto-resolved Rule).
- Keep Type→Rule resolution: resolveAutoRuleForType picks the canonical
  rule by project's proceeding, then jurisdiction match, then first
  candidate. Same logic, just re-aimed at the read-only display.
- Standardtitel preserves the chain (event type → Auto rule label →
  Custom text → proceeding → fallback) so the recipe still produces a
  sensible title even when Custom is used.

Frontend: deadlines-detail
- Read-only display: catalog rule → Name · Citation, else
  custom_rule_text + Custom badge, else legacy rule_code, else "—".
- Edit mode: mirror the create form with the Auto/Custom toggle.
  enterEdit initialises the mode from the persisted deadline; Save
  PATCHes with rule_set:true + the chosen rule pointer.

Rule-label addendum (m's 14:31 follow-up)
- Canonical contract everywhere: Name primary, Citation muted secondary
  ("Notice of Appeal · UPC.RoP.220.1"). Custom rules render the text
  with a "Custom" pill.
- New frontend/src/client/rule-label.ts exports formatRuleLabel /
  formatRuleLabelHTML / formatCustomRuleLabelHTML — one helper per
  shape (plain text vs muted-citation HTML).
- Wired into: deadlines-new Auto display, deadlines-detail read +
  Standardtitel, events.ts ruleDisplay (REGEL column on /events),
  projects-detail.ts Fristen table, views/shape-list.ts generic
  rule column.
- Verfahrensablauf (views/verfahrensablauf-core.ts) already renders
  name + citation chip separately and matches the canonical pattern;
  no change needed. Schriftsätze table is column-shaped (name + code
  in distinct columns) and out of scope per the addendum.

CSS
- New .rule-mode-auto / .rule-mode-custom / .rule-label-* family.
- Drop the dead .rule-sort-select rule and the .event-type-collapsed*
  family (retired with the catalog dropdown).

i18n
- DE+EN. Remove 10 stale keys (rule.none, autofill, autofill_inline,
  mismatch, override, override_warn, sort.*). Add 6 (auto_no_match,
  auto_pick_type, custom_badge, custom_placeholder,
  mode.toggle_to_auto, mode.toggle_to_custom).

Build hygiene
- go build + go test ./internal/... clean.
- frontend bun build clean (2803 keys, scan clean).

Out of scope (per issue)
- Promoting Custom entries back to the catalog ("save as new rule").
- Filtering/searching custom_rule_text in deadline lists.
- Touching the event-type browse modal (Part 1 of #82 — that stays).

Files
- internal/db/migrations/122_deadlines_custom_rule_text.{up,down}.sql
- internal/models/models.go
- internal/services/deadline_service.go (Create+Update+SELECT)
- internal/services/event_service.go (union projection)
- frontend/src/client/rule-label.ts (new helper)
- frontend/src/client/deadlines-new.ts (rewrite)
- frontend/src/client/deadlines-detail.ts (Auto/Custom editor + display)
- frontend/src/client/events.ts (REGEL column)
- frontend/src/client/projects-detail.ts (Fristen table cell)
- frontend/src/client/views/shape-list.ts (generic rule column)
- frontend/src/client/i18n.ts + i18n-keys.ts (DE+EN delta)
- frontend/src/deadlines-new.tsx (strip dropdown+sort, add toggle)
- frontend/src/deadlines-detail.tsx (Auto/Custom edit slots)
- frontend/src/styles/global.css (rule-mode + rule-label families)
2026-05-25 14:54:51 +02:00

199 lines
11 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";
export function renderDeadlinesDetail(): 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="deadlines.detail.title">Frist &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/events?type=deadline" />
<BottomNav currentPath="/events?type=deadline" />
<main>
<section className="tool-page">
<div className="container">
<a href="/events?type=deadline" className="back-link" data-i18n="deadlines.detail.back">&larr; Zur&uuml;ck zur Fristen&uuml;bersicht</a>
<div id="deadline-loading" className="entity-loading">
<p data-i18n="deadlines.detail.loading">L&auml;dt&hellip;</p>
</div>
<div id="deadline-notfound" className="entity-empty" style="display:none">
<p data-i18n="deadlines.detail.notfound">Frist nicht gefunden oder keine Berechtigung.</p>
</div>
<div id="deadline-body" style="display:none">
<header className="entity-detail-header">
<div className="entity-detail-title-row">
<div className="entity-detail-title-col">
<h1 id="deadline-title-display" />
<input type="text" id="deadline-title-edit" className="entity-title-input" style="display:none" />
{/* t-paliad-251 Part 4 — Standardtitel button only
visible in edit mode; clicking replaces the
title with a default derived from the project
and the deadline's event types / rule. */}
<button
type="button"
id="deadline-title-default-btn"
className="btn-link-action"
style="display:none"
data-i18n="deadlines.field.title.default_btn"
>
Standardtitel
</button>
<div className="entity-detail-meta">
<span id="deadline-due-chip" className="frist-due-chip" />
<span id="deadline-status-chip" className="entity-status-chip" />
<span id="deadline-pending-approval-badge" className="approval-pending-badge" style="display:none" data-i18n="approvals.pending.badge" title="">
Wartet auf Genehmigung
</span>
<a id="deadline-project-link" className="entity-ref" href="#" />
<select id="deadline-project-edit" className="entity-ref-select" style="display:none" />
</div>
</div>
<div className="entity-detail-actions">
<button id="deadline-complete-btn" type="button" className="btn-primary btn-cta-lime btn-small" data-i18n="deadlines.detail.complete">
Als erledigt markieren
</button>
<button id="deadline-reopen-btn" type="button" className="btn-primary btn-cta-lime btn-small" style="display:none" data-i18n="deadlines.detail.reopen">
Wieder &ouml;ffnen
</button>
<button id="deadline-withdraw-btn" type="button" className="btn-secondary btn-small" style="display:none" data-i18n="approvals.withdraw.cta">
Genehmigungsanfrage zur&uuml;ckziehen
</button>
<button id="deadline-edit-btn" className="btn-icon" type="button" aria-label="Bearbeiten" data-i18n-title="deadlines.detail.edit" title="Bearbeiten">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
</svg>
</button>
<button id="deadline-save-btn" className="btn-primary btn-cta-lime" type="button" style="display:none" data-i18n="deadlines.detail.save">
Speichern
</button>
</div>
</div>
</header>
<section className="entity-tab-panel frist-detail-panel">
<dl className="frist-detail-list">
<dt data-i18n="deadlines.detail.due">F&auml;lligkeitsdatum</dt>
<dd>
<span id="deadline-due-display" />
<input type="date" id="deadline-due-edit" style="display:none" />
</dd>
{/* m/paliad#56 — Verfahrenshandlung block.
Event type (parent concept) renders first; rule
sits beneath as the citation under that event
type. Editor splits them back into separate
pickers but the read-only stack reads as one
compound "Typ — Regel" surface. */}
<dt data-i18n="deadlines.field.event_type">Typ (optional)</dt>
<dd>
<span id="deadline-event-types-display">&mdash;</span>
<div id="deadline-event-types-edit" className="event-type-picker-host" style="display:none" />
</dd>
<dt data-i18n="deadlines.detail.rule">Regel</dt>
<dd>
<span id="deadline-rule-display">&mdash;</span>
{/* t-paliad-258 — Auto / Custom rule editor.
Mirrors /deadlines/new: read-only Auto display
(resolved from Type) or free-text Custom input,
with a toggle link. Hidden outside edit mode. */}
<div className="rule-edit-block" id="deadline-rule-edit" style="display:none">
<button
type="button"
id="deadline-rule-mode-toggle"
className="btn-link-action"
data-i18n="deadlines.field.rule.mode.toggle_to_custom"
>
Eigene Regel eingeben
</button>
<div className="rule-mode-auto" id="deadline-rule-auto-display">
<span className="form-hint-badge" data-i18n="deadlines.field.rule.auto_badge">Auto</span>
<span id="deadline-rule-auto-text" className="rule-auto-text">&mdash;</span>
</div>
<input
type="text"
id="deadline-rule-custom-input"
className="rule-mode-custom"
style="display:none"
placeholder="z.B. interner Review-Termin"
data-i18n-placeholder="deadlines.field.rule.custom_placeholder"
maxLength={200}
/>
</div>
</dd>
<dt data-i18n="deadlines.detail.source">Quelle</dt>
<dd id="deadline-source-display" />
<dt data-i18n="deadlines.detail.notes">Notizen</dt>
<dd>
<span id="deadline-notes-display" />
<textarea id="deadline-notes-edit" rows={3} style="display:none" />
</dd>
<dt data-i18n="deadlines.detail.created">Angelegt</dt>
<dd id="deadline-created-display" />
<dt id="deadline-completed-row-label" data-i18n="deadlines.detail.completed" style="display:none">
Erledigt am
</dt>
<dd id="deadline-completed-display" style="display:none" />
</dl>
</section>
<section className="frist-notes-section">
<h2 className="frist-section-heading" data-i18n="notes.section.title">Notizen</h2>
<div id="notes-container" className="notiz-container" data-parent-type="deadline" />
</section>
<div className="entity-detail-footer" id="deadline-delete-wrap" style="display:none">
<button id="deadline-delete-btn" className="btn-danger" type="button" data-i18n="deadlines.detail.delete">
Frist l&ouml;schen
</button>
</div>
</div>
<div className="modal-overlay" id="deadline-delete-modal" style="display:none">
<div className="modal-card">
<div className="modal-header">
<h2 data-i18n="deadlines.detail.delete.confirm.title">Frist wirklich l&ouml;schen?</h2>
<button className="modal-close" id="deadline-delete-modal-close" type="button">&times;</button>
</div>
<p data-i18n="deadlines.detail.delete.confirm.body">
Die Frist wird endg&uuml;ltig entfernt. Der Eintrag im Verlauf der Akte bleibt erhalten.
</p>
<div className="form-actions">
<button type="button" className="btn-cancel" id="deadline-delete-modal-cancel" data-i18n="deadlines.detail.delete.confirm.cancel">Abbrechen</button>
<button type="button" className="btn-danger" id="deadline-delete-modal-confirm" data-i18n="deadlines.detail.delete.confirm.ok">L&ouml;schen</button>
</div>
</div>
</div>
</div>
</section>
</main>
<Footer />
<PaliadinWidget />
<script src="/assets/deadlines-detail.js"></script>
</body>
</html>
);
}