|
|
|
|
@@ -2390,6 +2390,9 @@ function initPathwayFork() {
|
|
|
|
|
// project.our_side ↔ currentPerspective inside the renderer, so
|
|
|
|
|
// no separate DOM hint flip is needed on popstate.
|
|
|
|
|
currentActiveRow = null;
|
|
|
|
|
// t-paliad-197: popstate is a fresh navigation — auto-walk should
|
|
|
|
|
// re-evaluate from scratch, no stale "ändern-just-clicked" cap.
|
|
|
|
|
cascadeAutoWalkStopAfter = null;
|
|
|
|
|
const path = readPathwayFromURL();
|
|
|
|
|
const mode = readBModeFromURL();
|
|
|
|
|
showPathway(path, mode);
|
|
|
|
|
@@ -2541,6 +2544,10 @@ interface RowSpec {
|
|
|
|
|
pickedValue?: string;
|
|
|
|
|
pickedLabel?: string;
|
|
|
|
|
pickedIcon?: string;
|
|
|
|
|
// is-prefilled rows can carry an "aus Akte: <reference>" annotation.
|
|
|
|
|
// Per design §11.2, only the first prefilled row in a stack shows the
|
|
|
|
|
// reference; subsequent ones show the plain tag.
|
|
|
|
|
prefilledReference?: string;
|
|
|
|
|
// Cascade-only: the slug of the ancestor node whose children populate
|
|
|
|
|
// this row's options. Used by the click handler to navigate to a
|
|
|
|
|
// sibling without losing prefix state. Empty string = cascade root.
|
|
|
|
|
@@ -2549,6 +2556,10 @@ interface RowSpec {
|
|
|
|
|
// on this answered row. Always the row's parent slug (ancestor at
|
|
|
|
|
// depth K-1), which drops descendants and re-actives this depth.
|
|
|
|
|
cascadeRevertSlug?: string;
|
|
|
|
|
// Cascade-only: the depth this row occupies in the post-auto-walk
|
|
|
|
|
// trail (0 = root bucket). Used by handleRowEdit to compute the
|
|
|
|
|
// auto-walk suppression key.
|
|
|
|
|
cascadeDepth?: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// currentActiveRow tracks which non-cascade row the user has clicked
|
|
|
|
|
@@ -2556,6 +2567,20 @@ interface RowSpec {
|
|
|
|
|
// natural active row. Cleared on every successful pick.
|
|
|
|
|
let currentActiveRow: string | null = null;
|
|
|
|
|
|
|
|
|
|
// cascadeAutoWalkStopAfter caps the auto-walk depth for the next
|
|
|
|
|
// render. Set when the user clicks "ändern" on an auto-walked cascade
|
|
|
|
|
// row at depth K — auto-walk would otherwise re-fire on render and
|
|
|
|
|
// undo the edit. The cap stays in place until the user picks a chip
|
|
|
|
|
// (which clears it via handleRowPick → renderRowStack → cleared by
|
|
|
|
|
// the picker), at which point auto-walk re-engages from the new
|
|
|
|
|
// position. null = no cap (auto-walk runs to first branching point).
|
|
|
|
|
let cascadeAutoWalkStopAfter: number | null = null;
|
|
|
|
|
|
|
|
|
|
// cascadeTooltipDismissedKey backs the "first auto-walk tooltip" flag
|
|
|
|
|
// in localStorage. Once the user dismisses the tooltip, the key stays
|
|
|
|
|
// for future sessions; we only re-prompt if the key gets cleared.
|
|
|
|
|
const cascadeTooltipDismissedKey = "paliad.fristen.cascade.autoWalkTooltipSeen";
|
|
|
|
|
|
|
|
|
|
function perspectiveOptionLabel(value: string): string {
|
|
|
|
|
if (value === "claimant") return t("deadlines.perspective.claimant.short");
|
|
|
|
|
if (value === "defendant") return t("deadlines.perspective.defendant.short");
|
|
|
|
|
@@ -2575,8 +2600,93 @@ function modeOptionLabel(value: string): string {
|
|
|
|
|
: t("deadlines.pathway.b.mode.tree");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Slice 2: cascade-segment ↔ fristenrechner-code bridge. The event_categories
|
|
|
|
|
// taxonomy uses kebab-case segments under the `cms-eingang.*` buckets to
|
|
|
|
|
// represent proceedings (`upc-inf`, `de-bgh-null`, …); paliad.projects
|
|
|
|
|
// stores the fristenrechner code in UPPER_SNAKE form (`UPC_INF`, …).
|
|
|
|
|
// Most pairs follow a direct kebab↔snake mapping; a few — particularly
|
|
|
|
|
// the DE BGH variants and the DPMA BGH Rechtsbeschwerde — were given
|
|
|
|
|
// different segment orderings and need an explicit override. Any code
|
|
|
|
|
// not in the map degrades to "no proceeding-axis narrowing" — better
|
|
|
|
|
// silent than wrong (design §11.6).
|
|
|
|
|
const fristenrechnerCodeToCascadeSegment: Record<string, string> = {
|
|
|
|
|
UPC_INF: "upc-inf",
|
|
|
|
|
UPC_REV: "upc-rev",
|
|
|
|
|
UPC_APP: "upc-app",
|
|
|
|
|
UPC_PI: "upc-pi",
|
|
|
|
|
DE_INF: "de-inf",
|
|
|
|
|
DE_NULL: "de-null",
|
|
|
|
|
DE_INF_BGH: "de-bgh-inf",
|
|
|
|
|
DE_NULL_BGH: "de-bgh-null",
|
|
|
|
|
DPMA_OPP: "dpma-opp",
|
|
|
|
|
DPMA_BGH_RB: "dpma-bgh",
|
|
|
|
|
EPA_OPP: "epa-opp",
|
|
|
|
|
EPA_APP: "epa-app",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Set of kebab segments known to be proceeding-axis values. Used to
|
|
|
|
|
// distinguish "this child is a proceeding pick" from "this child is a
|
|
|
|
|
// sender / bucket / Schriftsatz" — proceeding-axis siblings get filtered
|
|
|
|
|
// by project context, others always pass.
|
|
|
|
|
const proceedingCascadeSegments = new Set(Object.values(fristenrechnerCodeToCascadeSegment));
|
|
|
|
|
|
|
|
|
|
function fristenrechnerCodeFromProject(p?: ProjectOption | null): string | null {
|
|
|
|
|
if (!p || p.proceeding_type_id == null) return null;
|
|
|
|
|
return cachedProceedingTypes.get(p.proceeding_type_id) ?? null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function projectCascadeSegment(p?: ProjectOption | null): string | null {
|
|
|
|
|
const code = fristenrechnerCodeFromProject(p);
|
|
|
|
|
if (!code) return null;
|
|
|
|
|
return fristenrechnerCodeToCascadeSegment[code] ?? null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// cascadeChildAllowsProject narrows cascade children to those matching
|
|
|
|
|
// the project's proceeding code along the proceeding axis. Children
|
|
|
|
|
// whose terminal slug segment isn't a known proceeding segment always
|
|
|
|
|
// pass (they're orthogonal axes — sender / bucket / Schriftsatz / …).
|
|
|
|
|
// Without a project context this is a no-op.
|
|
|
|
|
function cascadeChildAllowsProject(child: EventCategoryNode, projectSegment: string | null): boolean {
|
|
|
|
|
if (!projectSegment) return true;
|
|
|
|
|
const terminal = child.slug.split(".").pop() || "";
|
|
|
|
|
if (!proceedingCascadeSegments.has(terminal)) return true;
|
|
|
|
|
return terminal === projectSegment;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function filterCascadeChildren(scope: EventCategoryNode[], projectSegment: string | null): EventCategoryNode[] {
|
|
|
|
|
return scope.filter((c) =>
|
|
|
|
|
inboxFilterAllowsForums(c.forums)
|
|
|
|
|
&& perspectiveAllowsParty(c.party)
|
|
|
|
|
&& cascadeChildAllowsProject(c, projectSegment));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// projectReferenceLabel returns the short identifier used in the "aus
|
|
|
|
|
// Akte: <reference>" tag. Falls back to a short slice of the UUID when
|
|
|
|
|
// the project has no human-readable reference yet.
|
|
|
|
|
function projectReferenceLabel(p?: ProjectOption | null): string {
|
|
|
|
|
if (!p) return "";
|
|
|
|
|
if (p.reference && p.reference.trim() !== "") return p.reference.trim();
|
|
|
|
|
return p.id ? p.id.slice(0, 8) : "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildRowStack(currentSlug: string): RowSpec[] {
|
|
|
|
|
const rows: RowSpec[] = [];
|
|
|
|
|
const proj = currentStep1Context.kind === "project" ? currentStep1Context.project : undefined;
|
|
|
|
|
const hasProject = !!proj;
|
|
|
|
|
const projectForum = hasProject ? forumFromProject(proj) : null;
|
|
|
|
|
const projectSegment = projectCascadeSegment(proj);
|
|
|
|
|
const projectRef = projectReferenceLabel(proj);
|
|
|
|
|
|
|
|
|
|
// Once a single "aus Akte: <reference>" tag has been rendered, subsequent
|
|
|
|
|
// prefilled rows omit the reference (design §11.2) — repeating it on
|
|
|
|
|
// every row is just noise.
|
|
|
|
|
let referenceTagUsed = false;
|
|
|
|
|
const consumeReferenceTag = (): string => {
|
|
|
|
|
if (!projectRef) return "";
|
|
|
|
|
if (referenceTagUsed) return "";
|
|
|
|
|
referenceTagUsed = true;
|
|
|
|
|
return projectRef;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// R0 — Mode. Always present, defaults to "tree" so a fresh visit
|
|
|
|
|
// renders as answered. ändern flips to active and lets the user
|
|
|
|
|
@@ -2598,10 +2708,9 @@ function buildRowStack(currentSlug: string): RowSpec[] {
|
|
|
|
|
|
|
|
|
|
// R1 — Perspective. Default null ("Beide") = no filter; the row
|
|
|
|
|
// renders as answered with that label. t-paliad-164 carry-over: a
|
|
|
|
|
// project-bound perspective shows as `is-prefilled` with a "aus Akte"
|
|
|
|
|
// project-bound perspective shows as `is-prefilled` with the "aus Akte"
|
|
|
|
|
// tag in the picked-answer area.
|
|
|
|
|
const perspectiveValue = currentPerspective ?? "";
|
|
|
|
|
const proj = currentStep1Context.kind === "project" ? currentStep1Context.project : undefined;
|
|
|
|
|
const expectedFromAkte = ourSideToPerspective(proj?.our_side);
|
|
|
|
|
const perspectivePrefilled =
|
|
|
|
|
!!(proj && proj.our_side && expectedFromAkte === currentPerspective && currentPerspective !== null);
|
|
|
|
|
@@ -2619,33 +2728,46 @@ function buildRowStack(currentSlug: string): RowSpec[] {
|
|
|
|
|
],
|
|
|
|
|
pickedValue: perspectiveValue,
|
|
|
|
|
pickedLabel: perspectiveOptionLabel(perspectiveValue),
|
|
|
|
|
prefilledReference: perspectivePrefilled ? consumeReferenceTag() : undefined,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// R2 — Inbox channel. Default null ("Alle") = no filter. Slice 2 will
|
|
|
|
|
// hide this row entirely for UPC projects (forum implies CMS); for
|
|
|
|
|
// Slice 1 the row is always active/answered like any other.
|
|
|
|
|
const inboxValue = currentInboxChannel ?? "";
|
|
|
|
|
rows.push({
|
|
|
|
|
kind: "inbox",
|
|
|
|
|
rowId: "inbox",
|
|
|
|
|
state: currentActiveRow === "inbox" ? "active" : "answered",
|
|
|
|
|
question: t("deadlines.inbox.label"),
|
|
|
|
|
options: [
|
|
|
|
|
{ value: "cms", label: "CMS", title: t("deadlines.inbox.cms.title") },
|
|
|
|
|
{ value: "bea", label: "beA", title: t("deadlines.inbox.bea.title") },
|
|
|
|
|
{ value: "posteingang", label: t("deadlines.inbox.posteingang"), title: t("deadlines.inbox.posteingang.title") },
|
|
|
|
|
{ value: "", label: t("deadlines.inbox.all") },
|
|
|
|
|
],
|
|
|
|
|
pickedValue: inboxValue,
|
|
|
|
|
pickedLabel: inboxOptionLabel(inboxValue),
|
|
|
|
|
});
|
|
|
|
|
// R2 — Inbox channel. Hidden entirely when the project's forum is UPC
|
|
|
|
|
// (CMS is the only valid inbox for UPC matters, so the question is
|
|
|
|
|
// moot — design §5 matrix). For non-UPC projects, ad-hoc explore mode,
|
|
|
|
|
// and the no-context case, the row renders as answered with whatever
|
|
|
|
|
// "Wo kam es an?" channel was last picked (default: "Alle").
|
|
|
|
|
const r2Hidden = projectForum === "upc" && currentActiveRow !== "inbox";
|
|
|
|
|
if (!r2Hidden) {
|
|
|
|
|
const inboxValue = currentInboxChannel ?? "";
|
|
|
|
|
rows.push({
|
|
|
|
|
kind: "inbox",
|
|
|
|
|
rowId: "inbox",
|
|
|
|
|
state: currentActiveRow === "inbox" ? "active" : "answered",
|
|
|
|
|
question: t("deadlines.inbox.label"),
|
|
|
|
|
options: [
|
|
|
|
|
{ value: "cms", label: "CMS", title: t("deadlines.inbox.cms.title") },
|
|
|
|
|
{ value: "bea", label: "beA", title: t("deadlines.inbox.bea.title") },
|
|
|
|
|
{ value: "posteingang", label: t("deadlines.inbox.posteingang"), title: t("deadlines.inbox.posteingang.title") },
|
|
|
|
|
{ value: "", label: t("deadlines.inbox.all") },
|
|
|
|
|
],
|
|
|
|
|
pickedValue: inboxValue,
|
|
|
|
|
pickedLabel: inboxOptionLabel(inboxValue),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// R3..Rn — Cascade. The trail is the chain of picked nodes from the
|
|
|
|
|
// root down to the deepest pick. Each picked node renders as an
|
|
|
|
|
// answered row whose question is the question asked at its parent
|
|
|
|
|
// scope. When the deepest pick is non-leaf with surviving children
|
|
|
|
|
// (after forum + perspective filters), an active row materialises
|
|
|
|
|
// below to demand the next pick.
|
|
|
|
|
// R3..Rn — Cascade. URL slug = user-picked deepest position. The
|
|
|
|
|
// trail walks the URL slug; each trail node renders as `answered`
|
|
|
|
|
// unless it was the sole option at its parent's filtered scope under
|
|
|
|
|
// a project context — in which case it's `prefilled` (the user
|
|
|
|
|
// effectively had no choice; project context implied this pick).
|
|
|
|
|
//
|
|
|
|
|
// After the trail, the auto-walk extends the stack with `prefilled`
|
|
|
|
|
// rows whenever the remaining scope narrows to a single option under
|
|
|
|
|
// a project context; the walk stops at the first branching point,
|
|
|
|
|
// empty scope, or leaf — which is where the active row goes (if any).
|
|
|
|
|
// Auto-walk does NOT extend the URL — the user-picked slug stays
|
|
|
|
|
// explicit. "Ändern" on an auto-walked row sets `cascadeAutoWalkStopAfter`
|
|
|
|
|
// so the next render shows the active row at that depth.
|
|
|
|
|
const trail = eventCategoryTree
|
|
|
|
|
? buildBreadcrumb(eventCategoryTree, currentSlug)
|
|
|
|
|
: [];
|
|
|
|
|
@@ -2655,47 +2777,105 @@ function buildRowStack(currentSlug: string): RowSpec[] {
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < trail.length; i++) {
|
|
|
|
|
const node = trail[i];
|
|
|
|
|
const scope = parentScope.filter((c) =>
|
|
|
|
|
inboxFilterAllowsForums(c.forums) && perspectiveAllowsParty(c.party));
|
|
|
|
|
const filteredSiblings = filterCascadeChildren(parentScope, projectSegment);
|
|
|
|
|
// "Sole option" detection: at this depth, the project-filtered scope
|
|
|
|
|
// contains only this node — the pick was implied by context.
|
|
|
|
|
const wasSoleOption = hasProject && filteredSiblings.length === 1
|
|
|
|
|
&& filteredSiblings[0].slug === node.slug;
|
|
|
|
|
const isPrefilled = wasSoleOption;
|
|
|
|
|
rows.push({
|
|
|
|
|
kind: "cascade",
|
|
|
|
|
rowId: `cascade:${i}`,
|
|
|
|
|
state: "answered",
|
|
|
|
|
state: isPrefilled ? "prefilled" : "answered",
|
|
|
|
|
question: parentQuestion,
|
|
|
|
|
options: scope.map(nodeToRowOption),
|
|
|
|
|
options: filteredSiblings.map(nodeToRowOption),
|
|
|
|
|
pickedValue: node.slug,
|
|
|
|
|
pickedLabel: nodeLabel(node),
|
|
|
|
|
pickedIcon: node.icon,
|
|
|
|
|
cascadeParentSlug: parentSlug,
|
|
|
|
|
cascadeRevertSlug: parentSlug,
|
|
|
|
|
cascadeDepth: i,
|
|
|
|
|
prefilledReference: isPrefilled ? consumeReferenceTag() : undefined,
|
|
|
|
|
});
|
|
|
|
|
parentScope = node.children || [];
|
|
|
|
|
parentSlug = node.slug;
|
|
|
|
|
parentQuestion = nodeStepQuestion(node) || parentQuestion;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Active cascade row below the trail. Skip if the deepest node is a
|
|
|
|
|
// leaf (no further pick) or if all children get filtered out.
|
|
|
|
|
const deepest = trail.length > 0 ? trail[trail.length - 1] : null;
|
|
|
|
|
if (!deepest || !deepest.is_leaf) {
|
|
|
|
|
const activeScope = parentScope.filter((c) =>
|
|
|
|
|
inboxFilterAllowsForums(c.forums) && perspectiveAllowsParty(c.party));
|
|
|
|
|
if (activeScope.length > 0) {
|
|
|
|
|
// Auto-walk extension + active row. Walk down the cascade as long as
|
|
|
|
|
// the filtered scope narrows to a single option (with a project
|
|
|
|
|
// context that justifies the inference); stop at branching, empty
|
|
|
|
|
// scope, or a leaf. The first branching point becomes the active row;
|
|
|
|
|
// an empty scope or leaf finalises the stack with no active row.
|
|
|
|
|
let walkDepth = trail.length;
|
|
|
|
|
const deepestTrail = trail.length > 0 ? trail[trail.length - 1] : null;
|
|
|
|
|
let walked: EventCategoryNode | null = null;
|
|
|
|
|
|
|
|
|
|
if (!deepestTrail || !deepestTrail.is_leaf) {
|
|
|
|
|
// eslint-disable-next-line no-constant-condition
|
|
|
|
|
while (true) {
|
|
|
|
|
const filtered = filterCascadeChildren(parentScope, projectSegment);
|
|
|
|
|
if (filtered.length === 0) break;
|
|
|
|
|
const canAutoWalk = filtered.length === 1
|
|
|
|
|
&& hasProject
|
|
|
|
|
&& (cascadeAutoWalkStopAfter === null || walkDepth < cascadeAutoWalkStopAfter);
|
|
|
|
|
if (canAutoWalk) {
|
|
|
|
|
const node = filtered[0];
|
|
|
|
|
rows.push({
|
|
|
|
|
kind: "cascade",
|
|
|
|
|
rowId: `cascade:${walkDepth}`,
|
|
|
|
|
state: "prefilled",
|
|
|
|
|
question: parentQuestion,
|
|
|
|
|
options: filtered.map(nodeToRowOption),
|
|
|
|
|
pickedValue: node.slug,
|
|
|
|
|
pickedLabel: nodeLabel(node),
|
|
|
|
|
pickedIcon: node.icon,
|
|
|
|
|
cascadeParentSlug: parentSlug,
|
|
|
|
|
// Revert slug for an auto-walked row is the URL-explicit slug
|
|
|
|
|
// (= the trail tail). ändern on this row should NOT change the
|
|
|
|
|
// URL — it suppresses auto-walk past this depth instead. See
|
|
|
|
|
// handleRowEdit's cascade branch.
|
|
|
|
|
cascadeRevertSlug: parentSlug,
|
|
|
|
|
cascadeDepth: walkDepth,
|
|
|
|
|
prefilledReference: consumeReferenceTag(),
|
|
|
|
|
});
|
|
|
|
|
walked = node;
|
|
|
|
|
parentScope = node.children || [];
|
|
|
|
|
parentSlug = node.slug;
|
|
|
|
|
parentQuestion = nodeStepQuestion(node) || parentQuestion;
|
|
|
|
|
walkDepth += 1;
|
|
|
|
|
if (node.is_leaf) break;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
// Branching or no-project — render active row at this depth.
|
|
|
|
|
rows.push({
|
|
|
|
|
kind: "cascade",
|
|
|
|
|
rowId: `cascade:${trail.length}`,
|
|
|
|
|
rowId: `cascade:${walkDepth}`,
|
|
|
|
|
state: "active",
|
|
|
|
|
question: parentQuestion,
|
|
|
|
|
options: activeScope.map(nodeToRowOption),
|
|
|
|
|
options: filtered.map(nodeToRowOption),
|
|
|
|
|
cascadeParentSlug: parentSlug,
|
|
|
|
|
cascadeRevertSlug: parentSlug,
|
|
|
|
|
cascadeDepth: walkDepth,
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Stash the effective deepest slug so renderRowStack can pass it to
|
|
|
|
|
// runB1Search — the result panel narrows on the auto-walked leaf, not
|
|
|
|
|
// the URL slug.
|
|
|
|
|
cascadeEffectiveSlug = walked ? walked.slug : (deepestTrail ? deepestTrail.slug : "");
|
|
|
|
|
|
|
|
|
|
return rows;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// cascadeEffectiveSlug is the deepest slug after auto-walk extension —
|
|
|
|
|
// used by runB1Search to narrow the concept-card results to the
|
|
|
|
|
// effective cascade position, not just the URL-explicit slug. Updated
|
|
|
|
|
// by buildRowStack on every render.
|
|
|
|
|
let cascadeEffectiveSlug = "";
|
|
|
|
|
|
|
|
|
|
function nodeToRowOption(c: EventCategoryNode): RowOption {
|
|
|
|
|
return {
|
|
|
|
|
value: c.slug,
|
|
|
|
|
@@ -2743,8 +2923,17 @@ function rowHtml(row: RowSpec, rowNumber: number): string {
|
|
|
|
|
const iconHtml = row.pickedIcon
|
|
|
|
|
? `<span class="fristen-row-answer-icon" aria-hidden="true">${escHtml(row.pickedIcon)}</span>`
|
|
|
|
|
: "";
|
|
|
|
|
// Prefilled tag carries the project reference on the first prefilled
|
|
|
|
|
// row only (design §11.2 — subsequent rows show the plain "aus Akte"
|
|
|
|
|
// tag). prefilledReference !== "" signals "this row owns the
|
|
|
|
|
// reference"; "" / undefined signals plain tag.
|
|
|
|
|
const prefilledTag = row.state === "prefilled"
|
|
|
|
|
? `<span class="fristen-row-prefilled-tag" data-i18n="deadlines.row.prefilled.from_akte">aus Akte</span>`
|
|
|
|
|
? (row.prefilledReference
|
|
|
|
|
? `<span class="fristen-row-prefilled-tag">
|
|
|
|
|
<span data-i18n="deadlines.row.prefilled.from_akte">aus Akte</span>:
|
|
|
|
|
<span class="fristen-row-prefilled-ref">${escHtml(row.prefilledReference)}</span>
|
|
|
|
|
</span>`
|
|
|
|
|
: `<span class="fristen-row-prefilled-tag" data-i18n="deadlines.row.prefilled.from_akte">aus Akte</span>`)
|
|
|
|
|
: "";
|
|
|
|
|
return `<div class="fristen-row ${stateClass}"${dataKindAttr}${dataIdAttr}>
|
|
|
|
|
<div class="fristen-row-head">
|
|
|
|
|
@@ -2768,6 +2957,7 @@ function renderRowStack(currentSlug: string) {
|
|
|
|
|
|
|
|
|
|
const rows = buildRowStack(currentSlug);
|
|
|
|
|
stack.innerHTML = rows.map((r, i) => rowHtml(r, i + 1)).join("");
|
|
|
|
|
maybeShowAutoWalkTooltip(stack, rows);
|
|
|
|
|
|
|
|
|
|
// Wire chip picks (active rows).
|
|
|
|
|
stack.querySelectorAll<HTMLButtonElement>(".fristen-row-chip").forEach((btn) => {
|
|
|
|
|
@@ -2799,7 +2989,7 @@ function renderRowStack(currentSlug: string) {
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
runB1Search(currentSlug);
|
|
|
|
|
runB1Search(cascadeEffectiveSlug);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handleRowPick routes a chip selection in an active row to the right
|
|
|
|
|
@@ -2812,6 +3002,7 @@ function handleRowPick(rowId: string, value: string) {
|
|
|
|
|
const next: BMode = value === "filter" ? "filter" : "tree";
|
|
|
|
|
setPathwayURL("b", next);
|
|
|
|
|
currentActiveRow = null;
|
|
|
|
|
cascadeAutoWalkStopAfter = null;
|
|
|
|
|
showBMode(next);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
@@ -2819,6 +3010,7 @@ function handleRowPick(rowId: string, value: string) {
|
|
|
|
|
const next: Perspective = value === "claimant" || value === "defendant" ? value : null;
|
|
|
|
|
writePerspectiveToURL(next);
|
|
|
|
|
currentActiveRow = null;
|
|
|
|
|
cascadeAutoWalkStopAfter = null;
|
|
|
|
|
applyPerspective(next);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
@@ -2827,6 +3019,7 @@ function handleRowPick(rowId: string, value: string) {
|
|
|
|
|
? value : null;
|
|
|
|
|
writeInboxToURL(next);
|
|
|
|
|
currentActiveRow = null;
|
|
|
|
|
cascadeAutoWalkStopAfter = null;
|
|
|
|
|
applyInboxFilter(next);
|
|
|
|
|
applyFineForumsFromInbox(next);
|
|
|
|
|
writeForumsToURL(true);
|
|
|
|
|
@@ -2835,6 +3028,9 @@ function handleRowPick(rowId: string, value: string) {
|
|
|
|
|
}
|
|
|
|
|
if (rowId.startsWith("cascade:")) {
|
|
|
|
|
currentActiveRow = null;
|
|
|
|
|
// A new cascade pick clears any auto-walk cap — the user has
|
|
|
|
|
// committed to this branch, so auto-walk can re-engage from here.
|
|
|
|
|
cascadeAutoWalkStopAfter = null;
|
|
|
|
|
navigateB1(value);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
@@ -2842,9 +3038,11 @@ function handleRowPick(rowId: string, value: string) {
|
|
|
|
|
|
|
|
|
|
// handleRowEdit re-activates an answered row. For non-cascade rows that
|
|
|
|
|
// flips the row to active in-place (toggle via currentActiveRow) so the
|
|
|
|
|
// user can re-pick; the cascade below stays valid. For cascade rows, it
|
|
|
|
|
// drops descendants by navigating to the row's revert slug — matching
|
|
|
|
|
// today's breadcrumb-click semantic.
|
|
|
|
|
// user can re-pick; the cascade below stays valid. For cascade rows it
|
|
|
|
|
// either drops descendants by navigating to the row's revert slug —
|
|
|
|
|
// matching today's breadcrumb-click semantic — or, when the row was
|
|
|
|
|
// auto-walked, caps the auto-walk depth so the row turns active
|
|
|
|
|
// in-place without changing the URL.
|
|
|
|
|
function handleRowEdit(rowId: string) {
|
|
|
|
|
if (rowId === "mode" || rowId === "perspective" || rowId === "inbox") {
|
|
|
|
|
currentActiveRow = rowId;
|
|
|
|
|
@@ -2852,18 +3050,64 @@ function handleRowEdit(rowId: string) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (rowId.startsWith("cascade:")) {
|
|
|
|
|
// Derive the revert slug from the trail: ändern on cascade depth K
|
|
|
|
|
// reverts to depth K-1's slug (or "" at root), which drops
|
|
|
|
|
// descendants and makes depth K the new active row.
|
|
|
|
|
const idx = parseInt(rowId.slice("cascade:".length), 10);
|
|
|
|
|
if (!Number.isFinite(idx)) return;
|
|
|
|
|
const trail = buildBreadcrumb(eventCategoryTree || [], readB1PathFromURL());
|
|
|
|
|
const revertSlug = idx > 0 ? (trail[idx - 1]?.slug || "") : "";
|
|
|
|
|
const urlSlug = readB1PathFromURL();
|
|
|
|
|
const trail = buildBreadcrumb(eventCategoryTree || [], urlSlug);
|
|
|
|
|
if (idx < trail.length) {
|
|
|
|
|
// Row K is within the user-explicit trail — ändern drops to
|
|
|
|
|
// the parent slug, which drops descendants AND clears any
|
|
|
|
|
// auto-walk suppression so the next pick can extend again.
|
|
|
|
|
const revertSlug = idx > 0 ? (trail[idx - 1]?.slug || "") : "";
|
|
|
|
|
currentActiveRow = null;
|
|
|
|
|
cascadeAutoWalkStopAfter = null;
|
|
|
|
|
navigateB1(revertSlug);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Row K is auto-walked (beyond the URL trail). Suppress auto-walk
|
|
|
|
|
// past depth K so the row materialises as active in the next
|
|
|
|
|
// render — URL stays at the trail end.
|
|
|
|
|
cascadeAutoWalkStopAfter = idx;
|
|
|
|
|
currentActiveRow = null;
|
|
|
|
|
navigateB1(revertSlug);
|
|
|
|
|
renderRowStack(urlSlug);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// maybeShowAutoWalkTooltip surfaces a one-time hint when ≥ 2 cascade
|
|
|
|
|
// rows render in the prefilled (auto-walked) state. Per design §11.3
|
|
|
|
|
// and Q11 (deferred to v2 nice-to-have), the tooltip only appears the
|
|
|
|
|
// first time the user lands in a multi-row auto-walk — once dismissed,
|
|
|
|
|
// localStorage suppresses it forever. Tooltip is inline (no portal /
|
|
|
|
|
// modal): it injects a small banner above the first prefilled row.
|
|
|
|
|
function maybeShowAutoWalkTooltip(stack: HTMLElement, rows: RowSpec[]) {
|
|
|
|
|
const prefilledCount = rows.filter((r) => r.state === "prefilled" && r.kind === "cascade").length;
|
|
|
|
|
if (prefilledCount < 2) return;
|
|
|
|
|
let dismissed = false;
|
|
|
|
|
try { dismissed = localStorage.getItem(cascadeTooltipDismissedKey) === "1"; } catch { /* private mode */ }
|
|
|
|
|
if (dismissed) return;
|
|
|
|
|
// Inject the tooltip element. It's a sibling of the row stack, slotted
|
|
|
|
|
// just above the first prefilled row via DOM insertion.
|
|
|
|
|
const firstPrefilled = stack.querySelector(".fristen-row.is-prefilled");
|
|
|
|
|
if (!firstPrefilled) return;
|
|
|
|
|
const tip = document.createElement("div");
|
|
|
|
|
tip.className = "fristen-row-autowalk-tip";
|
|
|
|
|
tip.setAttribute("role", "status");
|
|
|
|
|
tip.innerHTML = `
|
|
|
|
|
<span class="fristen-row-autowalk-tip-icon" aria-hidden="true">ⓘ</span>
|
|
|
|
|
<span class="fristen-row-autowalk-tip-text" data-i18n="deadlines.row.autowalk.tooltip">
|
|
|
|
|
${escHtml(t("deadlines.row.autowalk.tooltip"))}
|
|
|
|
|
</span>
|
|
|
|
|
<button type="button" class="fristen-row-autowalk-tip-dismiss"
|
|
|
|
|
aria-label="${escAttr(t("deadlines.row.autowalk.dismiss"))}">
|
|
|
|
|
×
|
|
|
|
|
</button>`;
|
|
|
|
|
firstPrefilled.parentElement?.insertBefore(tip, firstPrefilled);
|
|
|
|
|
tip.querySelector<HTMLButtonElement>(".fristen-row-autowalk-tip-dismiss")?.addEventListener("click", () => {
|
|
|
|
|
tip.remove();
|
|
|
|
|
try { localStorage.setItem(cascadeTooltipDismissedKey, "1"); } catch { /* private mode */ }
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// b1SearchSeq guards against out-of-order responses when the user
|
|
|
|
|
// click-cascades faster than the network. Only the latest invocation
|
|
|
|
|
@@ -2975,6 +3219,7 @@ async function initB1Cascade() {
|
|
|
|
|
if (params.get("path") === "b" && params.get("mode") === "tree") {
|
|
|
|
|
// Always re-render — tree may not have loaded yet on first popstate.
|
|
|
|
|
currentActiveRow = null;
|
|
|
|
|
cascadeAutoWalkStopAfter = null;
|
|
|
|
|
loadAndRenderB1();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|