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:
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Reverse t-paliad-157 / m/paliad#15 follow-up: drop the forums column.
|
||||
|
||||
DROP INDEX IF EXISTS paliad.event_categories_forums_gin;
|
||||
|
||||
ALTER TABLE paliad.event_categories
|
||||
DROP CONSTRAINT IF EXISTS event_categories_forums_check;
|
||||
|
||||
ALTER TABLE paliad.event_categories
|
||||
DROP COLUMN IF EXISTS forums;
|
||||
67
internal/db/migrations/065_event_categories_forums.up.sql
Normal file
67
internal/db/migrations/065_event_categories_forums.up.sql
Normal file
@@ -0,0 +1,67 @@
|
||||
-- t-paliad-157 / m/paliad#15 follow-up: tag event_categories with the
|
||||
-- coarse forums they belong to so the inbox-channel chip can narrow the
|
||||
-- B1 cascade entry-points (the chip already narrows Pathway A's picker
|
||||
-- and B2's fine-bucket forum filter; B1 was the last hold-out).
|
||||
--
|
||||
-- Allowed values: upc | de | epa | dpma. NULL = "neutral" — node stays
|
||||
-- reachable from every inbox setting. Mixed-jurisdiction nodes (top-
|
||||
-- level branches, generic court actions like Ladung / Kostenfestsetzung,
|
||||
-- mündliche Verhandlung sub-states) intentionally stay NULL.
|
||||
--
|
||||
-- Two-step backfill:
|
||||
--
|
||||
-- 1. Regex on slug for nodes that carry the forum token explicitly
|
||||
-- (`upc-`, `.upc`, `-upc`, `.de-`, `-de-`, `.epa-`, `dpma`, …).
|
||||
-- Tokens are bounded by `^` / dot / dash / `$` so e.g. `.dpma`
|
||||
-- doesn't accidentally match the `de` rule.
|
||||
--
|
||||
-- 2. Explicit slug list for stragglers whose name doesn't carry the
|
||||
-- token (BGH / BPatG / Versäumnisurteil / Hinweisbeschluss are
|
||||
-- DE-only; r116-eingaben is EPA-only).
|
||||
--
|
||||
-- Anything still NULL after both passes is intentionally neutral.
|
||||
|
||||
ALTER TABLE paliad.event_categories
|
||||
ADD COLUMN forums text[];
|
||||
|
||||
ALTER TABLE paliad.event_categories
|
||||
ADD CONSTRAINT event_categories_forums_check
|
||||
CHECK (forums IS NULL OR forums <@ ARRAY['upc','de','epa','dpma']::text[]);
|
||||
|
||||
COMMENT ON COLUMN paliad.event_categories.forums IS
|
||||
'Coarse forum tags driving the #15 inbox-channel chip narrowing. '
|
||||
'Allowed: upc, de, epa, dpma. NULL = neutral (visible from every '
|
||||
'inbox setting). Empty array is treated the same as NULL by the '
|
||||
'frontend filter (CHECK constraint allows it for forward '
|
||||
'compatibility but the migration writes NULL where neutral).';
|
||||
|
||||
CREATE INDEX event_categories_forums_gin
|
||||
ON paliad.event_categories USING GIN (forums)
|
||||
WHERE forums IS NOT NULL;
|
||||
|
||||
UPDATE paliad.event_categories SET forums = ARRAY['upc']
|
||||
WHERE forums IS NULL AND slug ~ '(^|[\.-])upc([\.-]|$)';
|
||||
|
||||
UPDATE paliad.event_categories SET forums = ARRAY['de']
|
||||
WHERE forums IS NULL AND slug ~ '(^|[\.-])de([\.-]|$)';
|
||||
|
||||
UPDATE paliad.event_categories SET forums = ARRAY['epa']
|
||||
WHERE forums IS NULL AND slug ~ '(^|[\.-])epa([\.-]|$)';
|
||||
|
||||
UPDATE paliad.event_categories SET forums = ARRAY['dpma']
|
||||
WHERE forums IS NULL AND slug ~ '(^|[\.-])dpma([\.-]|$)';
|
||||
|
||||
UPDATE paliad.event_categories SET forums = ARRAY['de']
|
||||
WHERE forums IS NULL AND slug IN (
|
||||
'beschluss-entscheidung.beschluss-bpatg-beschwerde',
|
||||
'beschluss-entscheidung.versaeumnisurteil',
|
||||
'cms-eingang.gericht.endentscheidung.beschluss-bpatg-beschwerde',
|
||||
'cms-eingang.gericht.endentscheidung.versaeumnisurteil',
|
||||
'cms-eingang.gericht.hinweisbeschluss',
|
||||
'ich-moechte-einreichen.berufung.bgh-rb',
|
||||
'ich-moechte-einreichen.berufung.bpatg-beschwerde'
|
||||
);
|
||||
|
||||
UPDATE paliad.event_categories SET forums = ARRAY['epa']
|
||||
WHERE forums IS NULL
|
||||
AND slug = 'ich-moechte-einreichen.spaetere-schriftsaetze.r116-eingaben';
|
||||
@@ -33,6 +33,11 @@ func NewEventCategoryService(db *sqlx.DB) *EventCategoryService {
|
||||
// EventCategoryNode is one row in the taxonomy with its children attached.
|
||||
// JSON shape is what the frontend consumes from
|
||||
// GET /api/tools/fristenrechner/event-categories.
|
||||
//
|
||||
// Forums carries the optional coarse forum tags (#15) — `upc` / `de` /
|
||||
// `epa` / `dpma`. Empty / nil = "neutral", node stays reachable from
|
||||
// every inbox-channel chip setting. The frontend uses this to hide
|
||||
// non-matching subtrees when an inbox is active.
|
||||
type EventCategoryNode struct {
|
||||
ID string `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
@@ -45,6 +50,7 @@ type EventCategoryNode struct {
|
||||
Icon *string `json:"icon,omitempty"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
IsLeaf bool `json:"is_leaf"`
|
||||
Forums []string `json:"forums,omitempty"`
|
||||
Children []EventCategoryNode `json:"children,omitempty"`
|
||||
}
|
||||
|
||||
@@ -70,6 +76,7 @@ type categoryRow struct {
|
||||
Icon sql.NullString `db:"icon"`
|
||||
SortOrder int `db:"sort_order"`
|
||||
IsLeaf bool `db:"is_leaf"`
|
||||
Forums pq.StringArray `db:"forums"`
|
||||
}
|
||||
|
||||
// Tree returns the full taxonomy as a list of root nodes with children
|
||||
@@ -82,7 +89,7 @@ func (s *EventCategoryService) Tree(ctx context.Context) ([]EventCategoryNode, e
|
||||
SELECT id, parent_id, slug, label_de, label_en,
|
||||
description_de, description_en,
|
||||
step_question_de, step_question_en,
|
||||
icon, sort_order, is_leaf
|
||||
icon, sort_order, is_leaf, forums
|
||||
FROM paliad.event_categories
|
||||
WHERE is_active = true
|
||||
ORDER BY sort_order ASC, slug ASC
|
||||
@@ -118,6 +125,9 @@ SELECT id, parent_id, slug, label_de, label_en,
|
||||
if r.Icon.Valid {
|
||||
n.Icon = &r.Icon.String
|
||||
}
|
||||
if len(r.Forums) > 0 {
|
||||
n.Forums = []string(r.Forums)
|
||||
}
|
||||
nodes[r.ID] = &n
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user