feat(fristenrechner/inbox-chip): wire inbox into B1 cascade narrowing

Completes the #15 vision: the inbox chip now narrows the B1 decision
tree alongside Pathway A's picker and B2's fine-bucket forum filter.
Picking CMS hides DE / EPA / DPMA cascade entries; picking beA /
Posteingang hides UPC / EPA / DPMA entries. Neutral nodes (top-level
branches, Mündliche Verhandlung sub-states, court-generic events like
Ladung / Kostenfestsetzung) stay visible from every inbox setting so
the user can always reach the cross-jurisdictional middle of the tree.

Migration 065 adds paliad.event_categories.forums (text[]) with a
CHECK on {upc, de, epa, dpma}, a partial GIN index, and a two-step
backfill:

  1. Regex on slug for nodes that carry the forum token explicitly.
     Token-bounded by ^/./- so .dpma doesn't trip the de pattern.
  2. Explicit slug list for stragglers (BGH / BPatG / Versäumnisurteil /
     Hinweisbeschluss are DE-only; r116-eingaben is EPA-only).

NULL stays neutral. Migration applied to live Supabase; tracker at v65.

Backend: EventCategoryNode JSON gains an optional `forums` array;
EventCategoryService.Tree SELECT includes the column and threads it
through to the response.

Frontend: new module-level currentInboxChannel mirrors the chip state
so renderB1Cascade can ask "which forum is active?" without re-deriving
from the URL on every step. inboxFilterAllowsForums(forums) gates each
child node — neutral arrays (undefined / empty) always pass; tagged
arrays must include the active forum. applyInboxFilter re-renders the
cascade so chip clicks reflow B1 in place. Pathway A picker filter
and B2 fine-bucket sync remain orthogonal — same chip, three filters.

Refs m/paliad#15 (B1 follow-up).
This commit is contained in:
m
2026-05-08 16:54:34 +02:00
parent e87929885d
commit 6ef14ddc39
4 changed files with 123 additions and 2 deletions

View File

@@ -2409,6 +2409,9 @@ interface EventCategoryNode {
icon?: string;
sort_order: number;
is_leaf: boolean;
// #15 follow-up: coarse forum tags ('upc' | 'de' | 'epa' | 'dpma').
// Empty / undefined = neutral, node visible from every inbox setting.
forums?: string[];
children?: EventCategoryNode[];
}
@@ -2494,7 +2497,12 @@ function renderB1Cascade(currentSlug: string) {
const trail = buildBreadcrumb(eventCategoryTree, currentSlug);
const node = trail.length > 0 ? trail[trail.length - 1] : null;
const childScope = node ? (node.children || []) : eventCategoryTree;
const rawChildScope = node ? (node.children || []) : eventCategoryTree;
// #15 follow-up: drop children whose forums tag doesn't match the
// active inbox channel. Nodes with no forums (neutral) stay visible
// so the user always reaches court-event / Schriftsatz parents that
// span jurisdictions.
const childScope = rawChildScope.filter((c) => inboxFilterAllowsForums(c.forums));
const breadcrumbHtml = trail.length === 0
? ""
@@ -2804,6 +2812,13 @@ type InboxChannel = "cms" | "bea" | "posteingang" | null;
const INBOX_CHANNEL_VALUES = new Set<string>(["cms", "bea", "posteingang"]);
// currentInboxChannel mirrors the chip's active state so the B1 cascade
// renderer (which lives in a different section of this file) can ask
// "which forum is active right now?" without re-deriving from URL on
// every render. Updated by applyInboxFilter on hydrate / click /
// popstate.
let currentInboxChannel: InboxChannel = null;
function readInboxFromURL(): InboxChannel {
const raw = new URLSearchParams(window.location.search).get("inbox");
return raw && INBOX_CHANNEL_VALUES.has(raw) ? (raw as InboxChannel) : null;
@@ -2854,6 +2869,7 @@ function applyFineForumsFromInbox(ch: InboxChannel) {
}
function applyInboxFilter(ch: InboxChannel) {
currentInboxChannel = ch;
const forum = inboxChannelToForum(ch);
document.querySelectorAll<HTMLButtonElement>(".fristen-inbox-chip").forEach((btn) => {
@@ -2871,6 +2887,25 @@ function applyInboxFilter(ch: InboxChannel) {
const groupForum = g.dataset.forum || "";
g.hidden = forum !== null && groupForum !== forum;
});
// Re-render the B1 cascade so its button set picks up the new forum
// narrowing. Render is a no-op if the tree hasn't loaded yet or the
// cascade DOM isn't mounted (Pathway B not visible) — both guards
// already inside renderB1Cascade.
if (eventCategoryTree) {
renderB1Cascade(readB1PathFromURL());
}
}
// 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.
function inboxFilterAllowsForums(forums: string[] | undefined): boolean {
if (!forums || forums.length === 0) return true;
const active = inboxChannelToForum(currentInboxChannel);
if (active === null) return true;
return forums.includes(active);
}
async function persistInboxPref(ch: InboxChannel) {