feat(verfahrensablauf): re-surface hidden optional events — show-hidden toggle + un-hide chip (t-paliad-290)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled

m/paliad#122. atlas's #96 Slice A added per-card 'Überspringen' but no
un-skip path — hidden cards just disappeared from the timeline. This
adds the missing return path:

- CalcOptions.IncludeHidden (default false) tells the calculator to
  re-surface skipRules entries as faded rows instead of dropping them.
  When true, the rule renders with UIDeadline.IsHidden=true and the
  descendant-suppression cascade is bypassed so children compute their
  dates off the un-suppressed parent.
- UIResponse.HiddenCount always reflects the projection's hide count
  (gate-passed rules whose submission_code is in skipRules) so the
  "Ausgeblendete (N)" badge stays accurate regardless of toggle state.
- /tools/verfahrensablauf gets a "Ausgeblendete anzeigen" checkbox next
  to the perspective + appellant selectors. URL-driven (?show_hidden=1)
  so the state is shareable and survives reload. The row hides itself
  on projections with zero hidden cards.
- Hidden cards render via .timeline-item--hidden / .fr-col-item--hidden
  (opacity 0.55 + dotted border, mirroring the existing
  --skipped fade) and carry an inline "Wieder einblenden" chip. Clicking
  the chip removes the skip choice via the page's existing
  attachEventCardChoices remove callback (URL state + recalc included)
  and runs through a new delegated handler in event-card-choices.ts.
- 3 new i18n keys (DE+EN): choices.show_hidden.label,
  choices.show_hidden.count, choices.unhide.chip.

The skip-choice storage shape (paliad.project_event_choices, atlas's
table) is unchanged — un-hide is just a delete of the skip row.

Tests: 3 new bun-test cases pin the chip contract (emits on isHidden=
true with submission_code, suppressed otherwise); go test ./internal/...
+ bun run build clean.
This commit is contained in:
mAi
2026-05-26 09:38:31 +02:00
parent cca5e72c57
commit 80883eaac5
10 changed files with 272 additions and 7 deletions

View File

@@ -102,6 +102,12 @@ type UIDeadline struct {
// Frontend bucketer prefers this over the page-level appellant when
// non-empty. (t-paliad-265)
AppellantContext string `json:"appellantContext,omitempty"`
// IsHidden marks a card the user has previously hidden via a
// skip choice. Only ever true when CalcOptions.IncludeHidden is
// set — the toggle re-surfaces these rows so the user can either
// keep them faded for context or un-hide them via the inline
// "Wieder einblenden" chip. (t-paliad-290 / m/paliad#122)
IsHidden bool `json:"isHidden,omitempty"`
}
// UIResponse matches the frontend's DeadlineResponse TypeScript interface.
@@ -137,6 +143,14 @@ type UIResponse struct {
// is the appealable first-instance decision (m/paliad#81).
TriggerEventLabel string `json:"triggerEventLabel,omitempty"`
TriggerEventLabelEN string `json:"triggerEventLabelEN,omitempty"`
// HiddenCount is the number of rules whose submission_code is in
// CalcOptions.SkipRules AND whose condition_expr gate passes —
// i.e. how many rows the user has hidden in this projection
// regardless of the IncludeHidden toggle state. The frontend uses
// this to render the "Ausgeblendete (N)" badge on the toggle even
// when the toggle is OFF (so users know there's something to
// re-surface). (t-paliad-290 / m/paliad#122)
HiddenCount int `json:"hiddenCount"`
}
// ErrUnknownProceedingType is returned when the UI sends an unrecognised code.
@@ -214,6 +228,19 @@ type CalcOptions struct {
PerCardAppellant map[string]string
SkipRules map[string]struct{}
IncludeCCRFor map[string]struct{}
// IncludeHidden re-surfaces rules whose submission_code is in
// SkipRules (t-paliad-290 / m/paliad#122). When true:
// - Skipped rules are NOT dropped from the result; they render
// with UIDeadline.IsHidden=true so the frontend can fade them.
// - Descendant suppression is bypassed (the skipped parent is
// present in the result, so children compute their dates off
// it as if the user had never hidden it).
// Default false preserves the original skip semantic (drop rule +
// suppress descendants). HiddenCount on UIResponse is independent
// of this flag — it always reflects the number of hide-eligible
// rows so the toggle's count badge stays accurate.
IncludeHidden bool
}
// Calculate renders the full UI timeline for a proceeding type + trigger date.
@@ -381,6 +408,13 @@ func (s *FristenrechnerService) Calculate(ctx context.Context, proceedingCode, t
// child rule's parent has already been classified — so descendant
// suppression is a one-pass parent_id lookup.
skippedIDs := make(map[uuid.UUID]struct{}, len(skipRules))
// hiddenCount counts rows whose submission_code is in skipRules
// AND that pass the condition_expr gate — i.e. rows the user has
// hidden in this projection. Surfaced on UIResponse.HiddenCount so
// the frontend's "Ausgeblendete (N)" badge stays accurate even when
// IncludeHidden is off and the rows aren't in the result list.
// (t-paliad-290 / m/paliad#122)
hiddenCount := 0
// appellantContext maps a rule UUID to the appellant value that
// applies to its descendants. A rule that has its own PerCardAppellant
// pick stamps itself with that value; a rule whose parent has a
@@ -403,10 +437,22 @@ func (s *FristenrechnerService) Calculate(ctx context.Context, proceedingCode, t
// this rule (or one of its ancestors) as "don't consider for
// this case". Drop the row entirely AND record the rule ID so
// descendants suppress too.
//
// t-paliad-290 (m/paliad#122): when opts.IncludeHidden is set,
// we re-surface the directly-skipped row (faded via IsHidden)
// instead of dropping it. Descendants are NOT cascade-suppressed
// in that mode either — the un-suppressed parent computes its
// date normally, so children compute off it as usual. Either
// way we count the hide for the toggle's badge.
var isHidden bool
if r.SubmissionCode != nil {
if _, skipped := skipRules[*r.SubmissionCode]; skipped {
skippedIDs[r.ID] = struct{}{}
continue
hiddenCount++
if !opts.IncludeHidden {
skippedIDs[r.ID] = struct{}{}
continue
}
isHidden = true
}
}
if r.ParentID != nil {
@@ -442,6 +488,7 @@ func (s *FristenrechnerService) Calculate(ctx context.Context, proceedingCode, t
ConditionExpr: json.RawMessage(r.ConditionExpr),
AppellantContext: ctxVal,
ChoicesOffered: json.RawMessage(r.ChoicesOffered),
IsHidden: isHidden,
}
if r.SubmissionCode != nil {
d.Code = *r.SubmissionCode
@@ -712,6 +759,7 @@ func (s *FristenrechnerService) Calculate(ctx context.Context, proceedingCode, t
ProceedingNameEN: pickedProceeding.NameEN,
TriggerDate: triggerDateStr,
Deadlines: deadlines,
HiddenCount: hiddenCount,
}
// Sub-track routing keeps the user-picked proceeding's identity,
// so the trigger-event label rides on `pickedProceeding` (e.g.