feat(determinator/slice-3b): scope B1 cascade by project's proceeding type
m's 2026-05-08 18:09 spec: "if we have the project type defined, we
should only have events available that match the type of project /
type of case." Slice 3b wires the project's proceeding_type into the
cascade narrowing alongside the inbox chip and ad-hoc context.
Three inputs feed the cascade now, in priority order:
1. Inbox chip (cms / bea / posteingang) — user override.
2. Ad-hoc Step 1 chip (upc / de / epa / dpma).
3. Project's proceeding (Step 1 picked Akte → proceeding_type_id →
proceeding_types.code → forum prefix).
activeForumOnPage() returns the first non-null value. The B1
cascade's inboxFilterAllowsForums consults this so a user landing on
/tools/fristenrechner?project=<uuid>&path=b&mode=tree gets the
narrowed cascade automatically — no chip clicks required. The chip
can still override at the top of the panel.
Pieces:
- ProjectOption gains optional proceeding_type_id (already on the
JSON; just declared so TypeScript can read it).
- cachedProceedingTypes Map<int, string> is populated once on init
via /api/proceeding-types-db and cached for the page lifetime.
- forumFromProceedingCode() maps "UPC_INF" / "DE_NULL" / "EPA_OPP"
/ "EP_GRANT" / "DPMA_OPP" → upc / de / epa / dpma. EP_ and EPA_
both hit the EPA branch since EP_GRANT belongs to the EPA forum.
- triggerCascadeRefresh() is called from selectProject /
selectAdhoc / clearStep1Context + after the async load completes
so the cascade re-renders when the context changes.
The role variants (Klägerseite vs Beklagtenseite, Berufungskläger vs
-beklagte) are Slice 3c — they require fetching the user's
project_teams.responsibility for the selected project. Project's
forum lands first; role layers on after.
Refs t-paliad-157 / m/paliad#15. Folds in part of #18 (Item A
rule-vs-event collapse) — when the project context narrows the cascade
to one jurisdiction, the rule-vs-event mismatch surface shrinks.
This commit is contained in:
@@ -283,6 +283,11 @@ interface ProjectOption {
|
||||
reference?: string | null;
|
||||
title: string;
|
||||
path: string;
|
||||
// proceeding_type_id is on every paliad.projects row; the JSON
|
||||
// already includes it. We just declare the field so the Determinator
|
||||
// (Slice 3b) can scope the cascade by the project's jurisdiction
|
||||
// without an extra fetch.
|
||||
proceeding_type_id?: number | null;
|
||||
}
|
||||
|
||||
function escAttr(s: string): string {
|
||||
@@ -2525,6 +2530,11 @@ function selectProject(project: ProjectOption) {
|
||||
writeStep1ContextToURL(currentStep1Context);
|
||||
renderStep1Summary();
|
||||
showStep2Card();
|
||||
// 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
|
||||
// up the new narrowing.
|
||||
triggerCascadeRefresh();
|
||||
}
|
||||
|
||||
function selectAdhoc(forum: AdhocForum) {
|
||||
@@ -2532,6 +2542,7 @@ function selectAdhoc(forum: AdhocForum) {
|
||||
writeStep1ContextToURL(currentStep1Context);
|
||||
renderStep1Summary();
|
||||
showStep2Card();
|
||||
triggerCascadeRefresh();
|
||||
}
|
||||
|
||||
function clearStep1Context() {
|
||||
@@ -2539,6 +2550,7 @@ function clearStep1Context() {
|
||||
writeStep1ContextToURL(currentStep1Context);
|
||||
renderStep1Summary();
|
||||
hideStep2Card();
|
||||
triggerCascadeRefresh();
|
||||
}
|
||||
|
||||
function renderStep1Summary() {
|
||||
@@ -2601,15 +2613,24 @@ function initPathwayFork() {
|
||||
try { localStorage.setItem(PATHWAY_STORAGE_KEY, initial); } catch { /* */ }
|
||||
}
|
||||
|
||||
// Step 1 — fetch projects + render filtered list. Search filters the
|
||||
// list in-place; click on a row drops the user into Step 2.
|
||||
// Step 1 — fetch projects + proceeding-types in parallel. Both are
|
||||
// small + cacheable; both are needed before the cascade narrowing
|
||||
// can fire correctly. Render the list as soon as projects come in,
|
||||
// refresh cascade once the proceeding-types map is also populated.
|
||||
void (async () => {
|
||||
cachedAkten = await fetchProjects();
|
||||
const [projects] = await Promise.all([
|
||||
fetchProjects(),
|
||||
loadProceedingTypes(),
|
||||
]);
|
||||
cachedAkten = projects;
|
||||
if (currentStep1Context.kind === "project" && currentStep1Context.projectId) {
|
||||
currentStep1Context.project = cachedAkten.find((p) => p.id === currentStep1Context.projectId);
|
||||
renderStep1Summary();
|
||||
}
|
||||
renderAkteList("");
|
||||
// Cascade may already be on screen if the user landed with
|
||||
// ?path=b&project=<uuid>; re-render to apply the now-known forum.
|
||||
triggerCascadeRefresh();
|
||||
})();
|
||||
|
||||
const akteSearch = document.getElementById("fristen-akte-search") as HTMLInputElement | null;
|
||||
@@ -3223,17 +3244,84 @@ function applyInboxFilter(ch: InboxChannel) {
|
||||
}
|
||||
}
|
||||
|
||||
// Slice 3b: cascade narrowing now flows from THREE inputs, in priority
|
||||
// order. Whichever is set first wins.
|
||||
//
|
||||
// 1. Inbox chip (cms / bea / posteingang) — user-clicked override.
|
||||
// Maps to upc / de / de.
|
||||
// 2. Ad-hoc chip (Step 1's explore-mode upc / de / epa / dpma).
|
||||
// 3. Project context (Step 1's selected Akte → proceeding_type_id →
|
||||
// proceeding_types.code → forum prefix).
|
||||
//
|
||||
// activeForumOnPage() returns the first non-null value or null when
|
||||
// nothing is set. inboxFilterAllowsForums consults this so the B1
|
||||
// cascade narrows automatically when the user enters Pathway B with a
|
||||
// project context — no extra clicks needed. The chip can still
|
||||
// override at the top of the B1 panel.
|
||||
|
||||
let cachedProceedingTypes: Map<number, string> = new Map();
|
||||
|
||||
async function loadProceedingTypes(): Promise<void> {
|
||||
try {
|
||||
const resp = await fetch("/api/proceeding-types-db");
|
||||
if (!resp.ok) return;
|
||||
const list = (await resp.json()) as Array<{ id: number; code: string }>;
|
||||
for (const pt of list) {
|
||||
cachedProceedingTypes.set(pt.id, pt.code);
|
||||
}
|
||||
} catch {
|
||||
// Anonymous visitor / network blip — leave the map empty; project
|
||||
// narrowing falls back to "no opinion" and the cascade stays
|
||||
// wide-open.
|
||||
}
|
||||
}
|
||||
|
||||
function forumFromProceedingCode(code: string): "upc" | "de" | "epa" | "dpma" | null {
|
||||
if (code.startsWith("UPC_")) return "upc";
|
||||
if (code.startsWith("DE_")) return "de";
|
||||
if (code.startsWith("EPA_") || code.startsWith("EP_")) return "epa";
|
||||
if (code.startsWith("DPMA_")) return "dpma";
|
||||
return null;
|
||||
}
|
||||
|
||||
function forumFromProject(p?: ProjectOption | null): "upc" | "de" | "epa" | "dpma" | null {
|
||||
if (!p || p.proceeding_type_id == null) return null;
|
||||
const code = cachedProceedingTypes.get(p.proceeding_type_id);
|
||||
return code ? forumFromProceedingCode(code) : null;
|
||||
}
|
||||
|
||||
function activeForumOnPage(): "upc" | "de" | "epa" | "dpma" | null {
|
||||
const chipForum = inboxChannelToForum(currentInboxChannel);
|
||||
if (chipForum !== null) return chipForum;
|
||||
if (currentStep1Context.kind === "adhoc" && currentStep1Context.adhocForum) {
|
||||
return currentStep1Context.adhocForum;
|
||||
}
|
||||
if (currentStep1Context.kind === "project") {
|
||||
return forumFromProject(currentStep1Context.project);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// inboxFilterAllowsForums returns true when a node with the given
|
||||
// forums tags should be visible under the current inbox chip. Neutral
|
||||
// nodes (forums undefined / empty) are always visible. When no inbox
|
||||
// is active, every node is visible.
|
||||
// forums tags should be visible. Neutral nodes (forums undefined /
|
||||
// empty) are always visible. When no forum is active anywhere on the
|
||||
// page, every node is visible.
|
||||
function inboxFilterAllowsForums(forums: string[] | undefined): boolean {
|
||||
if (!forums || forums.length === 0) return true;
|
||||
const active = inboxChannelToForum(currentInboxChannel);
|
||||
const active = activeForumOnPage();
|
||||
if (active === null) return true;
|
||||
return forums.includes(active);
|
||||
}
|
||||
|
||||
// triggerCascadeRefresh re-renders the B1 cascade if the panel is
|
||||
// mounted. Call after any change that affects activeForumOnPage()
|
||||
// (chip click, project selection, ad-hoc selection, clear).
|
||||
function triggerCascadeRefresh() {
|
||||
if (eventCategoryTree && document.getElementById("fristen-b1-cascade")) {
|
||||
renderB1Cascade(readB1PathFromURL());
|
||||
}
|
||||
}
|
||||
|
||||
async function persistInboxPref(ch: InboxChannel) {
|
||||
// forum_pref="" clears on the server side (NULL in the DB) — matches
|
||||
// the EscalationContactID convention in services/user_service.go.
|
||||
|
||||
Reference in New Issue
Block a user