feat(fristenrechner): predefine Determinator perspective from our_side (t-paliad-164 slice 3)
Closes m's 2026-05-08 21:42 dogfood loop: when the user picks an Akte
that knows its own side, the Determinator perspective chip should be
locked to that side instead of asking the user to re-pick something
the project already knows.
ProjectOption gains our_side; the JSON already carries it from
slice 1 (ProjectService.projectColumns). New helper
applyOurSidePredefine maps project.our_side onto the chip:
claimant → "claimant" chip active
defendant → "defendant" chip active
court → null chip cleared (court actions are neutral
to the user's side, so no narrowing)
both → null explicit "Beide" intent
null/undef → no-op
URL wins: if ?role= is present at call time the user (or a shared
link) chose it explicitly and we don't overwrite. When we do predefine,
we write the same value to the URL so refresh + back/forward round-trip
correctly. Two call sites:
- selectProject: in-page Akte pick. push history (replaceURL=false) so
back-button restores the prior state.
- post-fetchProjects hydration: the deep-link / refresh path. Use
history replace so the URL stays clean.
A small "vorgegeben durch Akte" / "predefined from project" hint
renders next to the chip strip (italic muted). Visible whenever the
active perspective came from the project; cleared on any chip click
(explicit override) and on Step-1 reselect (no Akte = no hint).
popstate restores hint visibility by recomputing from
project.our_side ↔ currentPerspective so back/forward feels right.
Free-pick is preserved: clicking another chip overrides the
predefine and the cascade re-narrows immediately.
This commit is contained in:
@@ -288,6 +288,12 @@ interface ProjectOption {
|
||||
// (Slice 3b) can scope the cascade by the project's jurisdiction
|
||||
// without an extra fetch.
|
||||
proceeding_type_id?: number | null;
|
||||
// our_side carries which side the firm represents on this project
|
||||
// (t-paliad-164). When a user selects an Akte, the perspective chip
|
||||
// pre-locks to this value; a small hint above the strip flags the
|
||||
// pre-selection and the user can still click another chip to
|
||||
// override. NULL/undefined leaves the chip unset (free-pick).
|
||||
our_side?: "claimant" | "defendant" | "court" | "both" | null;
|
||||
}
|
||||
|
||||
function escAttr(s: string): string {
|
||||
@@ -2530,6 +2536,11 @@ function selectProject(project: ProjectOption) {
|
||||
writeStep1ContextToURL(currentStep1Context);
|
||||
renderStep1Summary();
|
||||
showStep2Card();
|
||||
// t-paliad-164: project.our_side predefines the perspective chip.
|
||||
// Only fires when the user hasn't already locked a perspective via
|
||||
// ?role= in the URL — the URL pick wins because it represents an
|
||||
// explicit choice (chip click or shared link).
|
||||
applyOurSidePredefine(project, /* replaceURL */ false);
|
||||
// Slice 3b: project's proceeding type narrows the B1 cascade if the
|
||||
// user reaches it via Step 2 → Etwas ist passiert. Refresh here so
|
||||
// a cascade already on screen (rare but possible via popstate) picks
|
||||
@@ -2551,6 +2562,12 @@ function clearStep1Context() {
|
||||
renderStep1Summary();
|
||||
hideStep2Card();
|
||||
triggerCascadeRefresh();
|
||||
// t-paliad-164: hint dies with the project context. We deliberately
|
||||
// leave the perspective chip itself alone — the user may want to
|
||||
// keep their pick when returning to Step 1; we only clear the
|
||||
// "vorgegeben durch Akte" annotation since there's no Akte anymore.
|
||||
const hint = document.getElementById("fristen-perspective-hint");
|
||||
if (hint) hint.hidden = true;
|
||||
}
|
||||
|
||||
function renderStep1Summary() {
|
||||
@@ -2626,6 +2643,10 @@ function initPathwayFork() {
|
||||
if (currentStep1Context.kind === "project" && currentStep1Context.projectId) {
|
||||
currentStep1Context.project = cachedAkten.find((p) => p.id === currentStep1Context.projectId);
|
||||
renderStep1Summary();
|
||||
// t-paliad-164: deep-link / refresh path. project loaded async, so
|
||||
// the predefine has to wait for cachedAkten. replace=true keeps
|
||||
// the URL clean — the user didn't navigate, they just refreshed.
|
||||
applyOurSidePredefine(currentStep1Context.project, /* replaceURL */ true);
|
||||
}
|
||||
renderAkteList("");
|
||||
// Cascade may already be on screen if the user landed with
|
||||
@@ -2657,6 +2678,11 @@ function initPathwayFork() {
|
||||
const next: Perspective = isClear ? null : ((chip.dataset.perspective as Perspective) ?? null);
|
||||
writePerspectiveToURL(next);
|
||||
applyPerspective(next);
|
||||
// t-paliad-164: any chip click is an explicit override; hide the
|
||||
// "vorgegeben durch Akte" hint so the bar reads as "user choice"
|
||||
// from here on.
|
||||
const hint = document.getElementById("fristen-perspective-hint");
|
||||
if (hint) hint.hidden = true;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2744,6 +2770,17 @@ function initPathwayFork() {
|
||||
renderStep1Summary();
|
||||
if (currentStep1Context.kind !== "none") showStep2Card(); else hideStep2Card();
|
||||
applyPerspective(readPerspectiveFromURL());
|
||||
// t-paliad-164: restore the hint visibility from URL+project state.
|
||||
// The hint shows when the active URL perspective matches what the
|
||||
// current project's our_side would have predefined — i.e. the
|
||||
// "predefined-and-not-yet-overridden" state. Approximation: hint
|
||||
// visible iff project.our_side maps to currentPerspective.
|
||||
const hint = document.getElementById("fristen-perspective-hint");
|
||||
if (hint) {
|
||||
const proj = currentStep1Context.kind === "project" ? currentStep1Context.project : undefined;
|
||||
const expected = ourSideToPerspective(proj?.our_side);
|
||||
hint.hidden = !(proj && proj.our_side && expected === currentPerspective);
|
||||
}
|
||||
const path = readPathwayFromURL();
|
||||
const mode = readBModeFromURL();
|
||||
showPathway(path, mode);
|
||||
@@ -3382,6 +3419,45 @@ function applyPerspective(p: Perspective) {
|
||||
triggerCascadeRefresh();
|
||||
}
|
||||
|
||||
// ourSideToPerspective maps the project-level "Wir vertreten" enum
|
||||
// onto the chip-strip Perspective. 'court' / 'both' map to null
|
||||
// (chip cleared) — court actions are neutral to the user's side and
|
||||
// "both" is explicit no-filter intent.
|
||||
function ourSideToPerspective(os: string | null | undefined): Perspective {
|
||||
if (os === "claimant") return "claimant";
|
||||
if (os === "defendant") return "defendant";
|
||||
return null;
|
||||
}
|
||||
|
||||
// applyOurSidePredefine locks the perspective chip from
|
||||
// project.our_side when the user hasn't already explicitly picked
|
||||
// one. The URL is the "explicit pick" signal: if ?role= is present
|
||||
// at call time, the user (or a shared link) chose it and we don't
|
||||
// overwrite. When we do predefine, we write the same value to the
|
||||
// URL so back/forward + refresh round-trip cleanly, and we show the
|
||||
// "vorgegeben durch Akte" hint so the user knows where the
|
||||
// pre-selection came from. Clicking a chip clears the hint.
|
||||
//
|
||||
// `replaceURL=true` is for the deep-link / refresh path; `false` for
|
||||
// in-page project selection so back-button restores the empty state.
|
||||
function applyOurSidePredefine(project: ProjectOption | undefined, replaceURL: boolean) {
|
||||
const hint = document.getElementById("fristen-perspective-hint");
|
||||
if (!project || !project.our_side) {
|
||||
if (hint) hint.hidden = true;
|
||||
return;
|
||||
}
|
||||
// URL wins — user has an explicit pick. Don't clobber it; also no
|
||||
// hint, since the active perspective didn't come from the project.
|
||||
if (readPerspectiveFromURL() !== null) {
|
||||
if (hint) hint.hidden = true;
|
||||
return;
|
||||
}
|
||||
const next = ourSideToPerspective(project.our_side);
|
||||
writePerspectiveToURL(next, replaceURL);
|
||||
applyPerspective(next);
|
||||
if (hint) hint.hidden = false;
|
||||
}
|
||||
|
||||
// perspectiveAllowsParty returns true when a node tagged with `party`
|
||||
// should be visible under the current perspective. Neutral nodes
|
||||
// (party undefined / empty) always pass. "both" matches every
|
||||
|
||||
@@ -376,6 +376,7 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"deadlines.perspective.claimant.title": "Klägerseite — versteckt typische Beklagten-Schriftsätze",
|
||||
"deadlines.perspective.defendant.title": "Beklagtenseite — versteckt typische Kläger-Schriftsätze",
|
||||
"deadlines.perspective.appeal_filed_by.label": "Berufung eingelegt durch:",
|
||||
"deadlines.perspective.predefined_hint": "vorgegeben durch Akte",
|
||||
"deadlines.event.composite.label": "Zusammengesetzt:",
|
||||
"deadlines.event.unit.days.one": "Tag",
|
||||
"deadlines.event.unit.days.many": "Tage",
|
||||
@@ -2530,6 +2531,7 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"deadlines.perspective.claimant.title": "Claimant side — hides typical defendant submissions",
|
||||
"deadlines.perspective.defendant.title": "Defendant side — hides typical claimant submissions",
|
||||
"deadlines.perspective.appeal_filed_by.label": "Appeal filed by:",
|
||||
"deadlines.perspective.predefined_hint": "predefined from project",
|
||||
"deadlines.event.composite.label": "Composite:",
|
||||
"deadlines.event.unit.days.one": "day",
|
||||
"deadlines.event.unit.days.many": "days",
|
||||
|
||||
@@ -273,6 +273,14 @@ export function renderFristenrechner(): string {
|
||||
<span data-i18n="deadlines.perspective.both.short">Beide</span>
|
||||
</button>
|
||||
</div>
|
||||
{/* t-paliad-164 — predefined-from-Akte hint. Hidden by
|
||||
default; client/fristenrechner.ts shows it when the
|
||||
active perspective came from project.our_side. The
|
||||
user can still click another chip to override. */}
|
||||
<span className="fristen-perspective-hint" id="fristen-perspective-hint"
|
||||
data-i18n="deadlines.perspective.predefined_hint" hidden>
|
||||
vorgegeben durch Akte
|
||||
</span>
|
||||
</div>
|
||||
<div className="fristen-inbox-bar" id="fristen-inbox-bar" role="group" aria-label="Inbox channel">
|
||||
<span className="fristen-inbox-bar-label" data-i18n="deadlines.inbox.label">Wo kam es an?</span>
|
||||
|
||||
@@ -906,6 +906,7 @@ export type I18nKey =
|
||||
| "deadlines.perspective.defendant.short"
|
||||
| "deadlines.perspective.defendant.title"
|
||||
| "deadlines.perspective.label"
|
||||
| "deadlines.perspective.predefined_hint"
|
||||
| "deadlines.print"
|
||||
| "deadlines.priority.date"
|
||||
| "deadlines.proceeding.reselect"
|
||||
|
||||
@@ -1981,6 +1981,22 @@ input[type="range"]::-moz-range-thumb {
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
/* t-paliad-164 — "vorgegeben durch Akte" hint shown next to the
|
||||
* perspective chips when project.our_side has predefined the chip.
|
||||
* Italic, muted, with a subtle leading bullet so it reads as
|
||||
* meta-info rather than a chip. The user can still click another
|
||||
* chip to override; the hint quietly disappears when they do. */
|
||||
.fristen-perspective-hint {
|
||||
font-size: 0.8rem;
|
||||
font-style: italic;
|
||||
color: var(--color-muted, #666);
|
||||
margin-left: 0.4rem;
|
||||
}
|
||||
|
||||
.fristen-perspective-hint::before {
|
||||
content: "·\00a0";
|
||||
}
|
||||
|
||||
.fristen-inbox-bar-label {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-muted, #666);
|
||||
|
||||
Reference in New Issue
Block a user