From a6d0acbcb431576d6bdb850070a856d60cd85781 Mon Sep 17 00:00:00 2001 From: mAi Date: Mon, 25 May 2026 16:51:27 +0200 Subject: [PATCH] =?UTF-8?q?mAi:=20#111=20-=20t-paliad-279=20=E2=80=94=20Ve?= =?UTF-8?q?rfahrensablauf=20form=20reorder=20+=20project=20auto-fill=20chi?= =?UTF-8?q?p?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorder Verfahrensablauf 'Browse a proceeding' so the user-input flow matches the importance hierarchy: proceeding-type → side → appellant → date / court / flags. Side was previously below the date input; it is the most-defining input after proceeding-type, so it belongs above. - frontend/src/verfahrensablauf.tsx: move .verfahrensablauf-perspective block above .date-input-group inside step-2. Wrap the side radio cluster in #side-radio-cluster and add a sibling #side-chip (hidden by default) that the client swaps in when a project pre-fills the side. Add a 1px divider between perspective and date-input groups. Update step-2 heading from "Ausgangsdatum eingeben" → "Perspektive und Datum" to honestly describe both controls now under the heading. - frontend/src/client/verfahrensablauf.ts: read ?project= on init, fetch /api/projects/, map our_side onto the side axis (mirrors fristenrechner.ts ourSideToPerspective: claimant/applicant/appellant → claimant, defendant/respondent → defendant, else null) and render the side row as a read-only chip + "Andere Seite wählen" override link. The chip respects ?side= as an explicit user pick — URL wins over project auto-fill, same precedence as fristenrechner. Override swaps back to the radio cluster and drops ?project= from the URL. Side-chip label is language-aware via onLangChange. - frontend/src/styles/global.css: .verfahrensablauf-step2-divider (1px hr between perspective and date blocks); .side-chip / -tag / -value / -override styles mirror .proceeding-summary's chip look so the two read as the same visual family. - frontend/src/client/i18n.ts + i18n-keys.ts: 3 new keys (deadlines.step2.perspective, deadlines.side.from_project, deadlines.side.override) in DE + EN. URL state stays backward-compatible: ?side= and ?appellant= survive the reorder unchanged. Adding ?project= opts in to auto-fill; without it the page behaves identically to before. No backend / projection logic change. --- frontend/src/client/i18n.ts | 6 + frontend/src/client/verfahrensablauf.ts | 141 ++++++++++++++++++++++++ frontend/src/i18n-keys.ts | 3 + frontend/src/styles/global.css | 53 +++++++++ frontend/src/verfahrensablauf.tsx | 119 ++++++++++++-------- 5 files changed, 274 insertions(+), 48 deletions(-) diff --git a/frontend/src/client/i18n.ts b/frontend/src/client/i18n.ts index 6eff7bd..c587351 100644 --- a/frontend/src/client/i18n.ts +++ b/frontend/src/client/i18n.ts @@ -207,6 +207,7 @@ const translations: Record> = { "deadlines.step1": "Verfahrensart w\u00e4hlen", "deadlines.step2": "Ausgangsdatum eingeben", + "deadlines.step2.perspective": "Perspektive und Datum", "deadlines.step3": "Ergebnis", "deadlines.upc": "UPC", "deadlines.de": "Deutsche Gerichte", @@ -421,6 +422,8 @@ const translations: Record> = { "deadlines.side.claimant": "Klägerseite", "deadlines.side.defendant": "Beklagtenseite", "deadlines.side.both": "Beide", + "deadlines.side.from_project": "Aus Akte:", + "deadlines.side.override": "Andere Seite wählen", "deadlines.appellant.label": "Berufung durch:", "deadlines.appellant.claimant": "Klägerseite", "deadlines.appellant.defendant": "Beklagtenseite", @@ -3274,6 +3277,7 @@ const translations: Record> = { "deadlines.step1": "Select Proceeding Type", "deadlines.step2": "Enter Trigger Date", + "deadlines.step2.perspective": "Perspective and Date", "deadlines.step3": "Result", "deadlines.upc": "UPC", "deadlines.de": "German Courts", @@ -3495,6 +3499,8 @@ const translations: Record> = { "deadlines.side.claimant": "Claimant", "deadlines.side.defendant": "Defendant", "deadlines.side.both": "Both", + "deadlines.side.from_project": "From case:", + "deadlines.side.override": "Choose other side", "deadlines.appellant.label": "Appeal filed by:", "deadlines.appellant.claimant": "Claimant", "deadlines.appellant.defendant": "Defendant", diff --git a/frontend/src/client/verfahrensablauf.ts b/frontend/src/client/verfahrensablauf.ts index 3dd48e5..83db643 100644 --- a/frontend/src/client/verfahrensablauf.ts +++ b/frontend/src/client/verfahrensablauf.ts @@ -38,6 +38,13 @@ let lastResponse: DeadlineResponse | null = null; let currentSide: Side = null; let currentAppellant: Side = null; +// Project-driven auto-fill state (t-paliad-279 / m/paliad#111). When the +// page is opened with ?project= and that project has our_side set, +// the side row renders as a read-only chip instead of the radio cluster. +// The user can flip to free-pick via the "Andere Seite wählen" override +// link, which clears this flag (radio cluster takes over again). +let sidePrefilledFromProject = false; + // Proceedings where one party initiates and "both" rows are role-swap // (i.e. either party files depending on who acted at the lower // instance). For these proceedings the appellant selector is meaningful @@ -388,6 +395,125 @@ function syncRadioGroup(name: string, value: string) { }); } +// Project context (t-paliad-279 / m/paliad#111). When the page is opened +// with ?project= and the project carries an our_side value, the side +// row renders as a read-only chip with an "Andere Seite wählen" override +// link. The proceeding picker + appellant axis stay untouched — only the +// side selector pre-fills. +interface ProjectOurSide { + id: string; + our_side?: + | "claimant" + | "defendant" + | "applicant" + | "appellant" + | "respondent" + | "third_party" + | "other" + | null; +} + +function readProjectFromURL(): string { + return new URLSearchParams(window.location.search).get("project") || ""; +} + +// ourSideToSide maps the project-level our_side enum (t-paliad-222) onto +// the side-selector's two-value axis. Active roles (claimant / applicant / +// appellant) collapse to "claimant"; reactive roles (defendant / +// respondent) collapse to "defendant"; everything else (third_party / +// other / NULL) returns null = no pre-fill. Mirrors fristenrechner.ts +// ourSideToPerspective() so projects render consistently across both +// surfaces. +function ourSideToSide(os: ProjectOurSide["our_side"] | undefined): Side { + switch (os) { + case "claimant": + case "applicant": + case "appellant": + return "claimant"; + case "defendant": + case "respondent": + return "defendant"; + default: + return null; + } +} + +async function fetchProjectOurSide(projectID: string): Promise { + try { + const resp = await fetch(`/api/projects/${encodeURIComponent(projectID)}`, { + credentials: "same-origin", + }); + if (!resp.ok) return null; + return (await resp.json()) as ProjectOurSide; + } catch { + return null; + } +} + +function sideLabelI18n(s: Side): string { + if (s === "claimant") return t("deadlines.side.claimant"); + if (s === "defendant") return t("deadlines.side.defendant"); + return t("deadlines.side.both"); +} + +// renderSideChip swaps the radio cluster for a read-only chip showing +// the auto-filled side + an "Andere Seite wählen" override link. Called +// after fetchProjectOurSide resolves to a side. The override link clears +// the prefilled flag and swaps back to the radio cluster — the user can +// then pick any side freely. +function renderSideChip(side: Side) { + const cluster = document.getElementById("side-radio-cluster"); + const chip = document.getElementById("side-chip"); + const value = document.getElementById("side-chip-value"); + if (!cluster || !chip || !value) return; + cluster.style.display = "none"; + chip.style.display = ""; + value.textContent = sideLabelI18n(side); +} + +function showSideRadioCluster() { + const cluster = document.getElementById("side-radio-cluster"); + const chip = document.getElementById("side-chip"); + if (!cluster || !chip) return; + cluster.style.display = ""; + chip.style.display = "none"; +} + +// applySidePrefill takes a project's our_side, maps it to the side axis, +// and locks the side row to a read-only chip if a mapping exists. URL +// wins — if ?side= is already explicit, the user (or shared link) has +// already chosen and we never overwrite. When we do prefill, write the +// derived side to the URL so reload + back/forward round-trip cleanly. +function applySidePrefill(os: ProjectOurSide["our_side"] | undefined) { + if (readSideFromURL() !== null) return; + const next = ourSideToSide(os); + if (next === null) return; + currentSide = next; + writeSideToURL(next); + syncRadioGroup("side", next); + sidePrefilledFromProject = true; + renderSideChip(next); + if (lastResponse) renderResults(lastResponse); +} + +function clearSidePrefill() { + sidePrefilledFromProject = false; + showSideRadioCluster(); + // Drop ?project= from the URL so a reload doesn't re-lock the side. + // ?side= stays — that's the user's last pick at this point. + const url = new URL(window.location.href); + url.searchParams.delete("project"); + window.history.replaceState(null, "", url.pathname + (url.search ? url.search : "") + url.hash); +} + +async function initProjectAutofill() { + const projectID = readProjectFromURL(); + if (!projectID) return; + const project = await fetchProjectOurSide(projectID); + if (!project) return; + applySidePrefill(project.our_side); +} + function applyVerfahrensablaufViewBodyClass(view: ProcedureView) { // Mirrors the events.ts pattern (body.events-view-*). The print // stylesheet keys `body.verfahrensablauf-view-timeline` to @@ -529,6 +655,15 @@ document.addEventListener("DOMContentLoaded", () => { initViewToggle(); initPerspectiveControls(); + // Override link on the prefilled side chip — swaps back to the radio + // cluster and clears ?project= from the URL. + document.getElementById("side-chip-override")?.addEventListener("click", clearSidePrefill); + + // Project autofill — runs after the radio cluster has its URL-driven + // state so we never clobber an explicit ?side= pick. Fire-and-forget; + // the chip swap happens once the project resolves. + void initProjectAutofill(); + onLangChange(() => { // Active-button name updates with language change (the data-i18n // pass swaps the inner 's text). Re-collapse the summary @@ -539,6 +674,12 @@ document.addEventListener("DOMContentLoaded", () => { const summary = document.getElementById("proceeding-summary-name"); if (summary) summary.textContent = proceedingDisplayName(activeBtn); } + // Side-chip label tracks language so a DE/EN flip while the chip is + // visible re-renders the inferred side in the active language. + if (sidePrefilledFromProject) { + const value = document.getElementById("side-chip-value"); + if (value) value.textContent = sideLabelI18n(currentSide); + } if (lastResponse) renderResults(lastResponse); syncTriggerEventLabel(); }); diff --git a/frontend/src/i18n-keys.ts b/frontend/src/i18n-keys.ts index 78421b6..5d448a3 100644 --- a/frontend/src/i18n-keys.ts +++ b/frontend/src/i18n-keys.ts @@ -1446,7 +1446,9 @@ export type I18nKey = | "deadlines.side.both" | "deadlines.side.claimant" | "deadlines.side.defendant" + | "deadlines.side.from_project" | "deadlines.side.label" + | "deadlines.side.override" | "deadlines.source.caldav" | "deadlines.source.fristenrechner" | "deadlines.source.imported" @@ -1477,6 +1479,7 @@ export type I18nKey = | "deadlines.step2.happened.desc" | "deadlines.step2.happened.title" | "deadlines.step2.heading" + | "deadlines.step2.perspective" | "deadlines.step3" | "deadlines.step3a.back" | "deadlines.step3a.draft.desc" diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 4501da9..9eabf5d 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -3572,6 +3572,59 @@ input[type="range"]::-moz-range-thumb { margin-bottom: 0; } +/* Visual divider between the perspective block (side + appellant) + and the date / court / flag knobs below. t-paliad-279 reorder put + the most-defining inputs (side, appellant) at the top of step-2; the + divider keeps the date block from reading as a continuation of the + perspective rows. */ +.verfahrensablauf-step2-divider { + height: 1px; + margin: 1rem 0; + background: var(--color-border, #e5e5e5); + border: 0; +} + +/* Read-only auto-fill chip for #side-row. Renders when ?project= + resolves a project whose our_side is set: shows the inferred side + with a small "Andere Seite wählen" override link that swaps the row + back to the radio cluster. t-paliad-279 / m/paliad#111. */ +.side-chip { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.4rem 0.75rem; + border: 1px solid var(--color-border, #e5e5e5); + border-radius: 0.5rem; + background: var(--color-bg-subtle, #fafafa); + font-size: 0.95rem; +} +.side-chip-tag { + color: var(--color-text-muted, #666); + font-size: 0.85rem; +} +.side-chip-value { + color: var(--color-text, #222); +} +.side-chip-override { + margin-left: 0.3rem; + padding: 0.15rem 0.55rem; + border: 1px solid var(--color-border, #ddd); + border-radius: 9999px; + background: var(--color-bg, #fff); + color: var(--color-text-muted, #555); + font-size: 0.8rem; + cursor: pointer; + transition: background 120ms, border-color 120ms; +} +.side-chip-override:hover { + background: var(--color-bg-subtle, #f4f4f4); + border-color: var(--color-text-muted, #aaa); +} +.side-chip-override:focus-visible { + outline: 2px solid var(--color-accent, #c6f41c); + outline-offset: 1px; +} + /* Compact note hint — sits in the timeline-meta line when the notes toggle is off. Native browser tooltip via title= attribute carries the full text on hover; tabindex=0 + aria-label make it diff --git a/frontend/src/verfahrensablauf.tsx b/frontend/src/verfahrensablauf.tsx index a3d3d7f..372d237 100644 --- a/frontend/src/verfahrensablauf.tsx +++ b/frontend/src/verfahrensablauf.tsx @@ -158,9 +158,79 @@ export function renderVerfahrensablauf(): string {