feat(fristenrechner): "unbestimmt" for chained court-set rules (m's R.151 case)
m's 2026-05-08 17:50 feedback: 'Antrag auf Kostenentscheidung' (RoP.151)
labels itself "wird vom Gericht bestimmt" but the rule is actually
"1 Monat ab Hauptentscheidung". The court doesn't directly determine
this date — it determines the parent's date (Hauptentscheidung) and
this rule chains off that. Calling it "vom Gericht bestimmt" overstates
the relationship; "unbestimmt" reads correctly: derived from a
not-yet-known anchor.
Two failure modes split:
- Direct court-set rule itself is hearing / decision / order
(or primary_party='court'). Label stays
"wird vom Gericht bestimmt" — strictly correct.
- Indirect court-set rule has a real duration but its anchor is a
court-set parent (RoP.151 case), or it's a
zero-duration rule whose parent is court-set
without a real date. Label flips to
"unbestimmt".
Backend: new `IsCourtSetIndirect bool` on UIDeadline, set on the three
indirect cases inside FristenrechnerService.Calculate. Direct cases
keep IsCourtSetIndirect=false so their label stays unchanged. JSON
omits the field when false, no consumer churn.
Frontend: deadlineCardHtml + the save-modal row both consult
IsCourtSetIndirect to pick between two i18n keys (deadlines.court.set
"vom Gericht bestimmt" and deadlines.court.indirect "unbestimmt"; EN
falls back to "set by court" / "tbd"). The override edit affordance
keeps working unchanged — user types the actual parent date, downstream
re-flows.
Refs m/paliad#15 (m's 2026-05-08 17:50 feedback Item 1).
This commit is contained in:
@@ -37,6 +37,11 @@ interface CalculatedDeadline {
|
||||
adjustmentReason?: AdjustmentReason;
|
||||
isRootEvent: boolean;
|
||||
isCourtSet: boolean;
|
||||
// True when isCourtSet is "unbestimmt" — the rule chains off a
|
||||
// court-determined parent (e.g. RoP.151 = 1 Monat ab
|
||||
// Hauptentscheidung) rather than being itself court-set. The UI
|
||||
// renders "unbestimmt" instead of "wird vom Gericht bestimmt".
|
||||
isCourtSetIndirect?: boolean;
|
||||
isOverridden?: boolean;
|
||||
}
|
||||
|
||||
@@ -377,8 +382,12 @@ async function openSaveModal() {
|
||||
const isCourtDetermined = dl.isCourtSet || dl.party === "court";
|
||||
const disabled = isCourtDetermined || !dl.dueDate;
|
||||
const checked = !disabled;
|
||||
// Same direct-vs-indirect split as the timeline date cell —
|
||||
// chained court-set rules read as "unbestimmt" rather than
|
||||
// "wird vom Gericht bestimmt".
|
||||
const courtLabelKey = dl.isCourtSetIndirect ? "deadlines.court.indirect" : "deadlines.court.set";
|
||||
const meta = isCourtDetermined
|
||||
? `<span class="frist-save-meta">${escHtml(t("deadlines.court.set"))}</span>`
|
||||
? `<span class="frist-save-meta">${escHtml(t(courtLabelKey))}</span>`
|
||||
: `<span class="frist-save-meta">${escHtml(formatDate(dl.dueDate))}</span>`;
|
||||
return `<li class="frist-save-row">
|
||||
<label>
|
||||
@@ -536,8 +545,16 @@ function deadlineCardHtml(dl: CalculatedDeadline, opts: { showParty: boolean }):
|
||||
const editAttrs = editable
|
||||
? ` data-rule-code="${escAttr(dl.code)}" data-current-date="${escAttr(dl.dueDate)}" role="button" tabindex="0" title="${escAttr(t("deadlines.date.edit.hint"))}"`
|
||||
: "";
|
||||
// "wird vom Gericht bestimmt" only fits direct court-set rules
|
||||
// (Urteil / Beschluss / Anordnung). Indirect rules (chained off a
|
||||
// court-set parent, e.g. RoP.151) render "unbestimmt" instead — the
|
||||
// date isn't directly determined by the court, it's derived from
|
||||
// the parent's date that the court will set. m's 2026-05-08 call.
|
||||
const courtLabelKey = dl.isCourtSetIndirect
|
||||
? "deadlines.court.indirect"
|
||||
: "deadlines.court.set";
|
||||
const dateStr = dl.isCourtSet
|
||||
? `<span class="timeline-court-set frist-date-edit"${editAttrs}>${t("deadlines.court.set")}</span>`
|
||||
? `<span class="timeline-court-set frist-date-edit"${editAttrs}>${t(courtLabelKey)}</span>`
|
||||
: `<span class="timeline-date${overriddenClass} frist-date-edit"${editAttrs}>${formatDate(dl.dueDate)}</span>`;
|
||||
|
||||
const mandatoryBadge = dl.isMandatory
|
||||
|
||||
@@ -243,6 +243,7 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"deadlines.party.both": "Beide",
|
||||
"deadlines.party.both.label": "beide Seiten",
|
||||
"deadlines.court.set": "vom Gericht bestimmt",
|
||||
"deadlines.court.indirect": "unbestimmt",
|
||||
"deadlines.date.edit.hint": "Datum bearbeiten — Folgefristen werden neu berechnet",
|
||||
"deadlines.view.label": "Ansicht:",
|
||||
"deadlines.view.timeline": "Zeitstrahl",
|
||||
@@ -2307,6 +2308,7 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"deadlines.party.both": "Both",
|
||||
"deadlines.party.both.label": "both parties",
|
||||
"deadlines.court.set": "set by court",
|
||||
"deadlines.court.indirect": "tbd",
|
||||
"deadlines.date.edit.hint": "Edit date — downstream deadlines will recalculate",
|
||||
"deadlines.view.label": "View:",
|
||||
"deadlines.view.timeline": "Timeline",
|
||||
|
||||
@@ -722,6 +722,7 @@ export type I18nKey =
|
||||
| "deadlines.col.status"
|
||||
| "deadlines.col.title"
|
||||
| "deadlines.complete.action"
|
||||
| "deadlines.court.indirect"
|
||||
| "deadlines.court.label"
|
||||
| "deadlines.court.set"
|
||||
| "deadlines.date.edit.hint"
|
||||
|
||||
@@ -51,6 +51,18 @@ type UIDeadline struct {
|
||||
AdjustmentReason *AdjustmentReason `json:"adjustmentReason,omitempty"`
|
||||
IsRootEvent bool `json:"isRootEvent"`
|
||||
IsCourtSet bool `json:"isCourtSet"`
|
||||
// IsCourtSetIndirect is true when IsCourtSet is true because the
|
||||
// rule chains off a court-determined parent (e.g. RoP.151
|
||||
// Kostenentscheidung is "1 Monat ab Hauptentscheidung", and the
|
||||
// Hauptentscheidung itself is the court-set anchor). Direct
|
||||
// court-determined rules (Urteil / Beschluss / Anordnung
|
||||
// themselves) keep IsCourtSet=true with IsCourtSetIndirect=false.
|
||||
// The frontend uses this to render "unbestimmt" for indirect
|
||||
// cases instead of "wird vom Gericht bestimmt", which is only
|
||||
// strictly correct for the direct ones — the indirect deadline
|
||||
// is computed off a parent date that the COURT sets, not by the
|
||||
// court itself.
|
||||
IsCourtSetIndirect bool `json:"isCourtSetIndirect,omitempty"`
|
||||
IsOverridden bool `json:"isOverridden,omitempty"`
|
||||
}
|
||||
|
||||
@@ -293,7 +305,11 @@ func (s *FristenrechnerService) Calculate(ctx context.Context, proceedingCode, t
|
||||
// If parent is court-set, we have nothing to inherit —
|
||||
// fall through to court-set marking.
|
||||
if parentIsCourtSet {
|
||||
// Indirect: this rule isn't itself court-determined,
|
||||
// it's blocked because its parent is. UI should say
|
||||
// "unbestimmt", not "wird vom Gericht bestimmt".
|
||||
d.IsCourtSet = true
|
||||
d.IsCourtSetIndirect = true
|
||||
d.DueDate = ""
|
||||
d.OriginalDate = ""
|
||||
courtSet[r.ID] = true
|
||||
@@ -321,14 +337,23 @@ func (s *FristenrechnerService) Calculate(ctx context.Context, proceedingCode, t
|
||||
computed[*r.Code] = parentDate
|
||||
}
|
||||
} else {
|
||||
// Parent not yet computed (defensive — shouldn't
|
||||
// happen given sequence_order). Treat as indirect
|
||||
// court-set: the date is unknown but the rule
|
||||
// itself isn't a court action.
|
||||
d.IsCourtSet = true
|
||||
d.IsCourtSetIndirect = true
|
||||
d.DueDate = ""
|
||||
d.OriginalDate = ""
|
||||
courtSet[r.ID] = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Buckets 2 + 3: court-determined.
|
||||
// Buckets 2 + 3: court-determined directly (the rule
|
||||
// itself is a hearing / decision / order or has
|
||||
// primary_party='court'). The label "wird vom Gericht
|
||||
// bestimmt" is strictly correct here — keep
|
||||
// IsCourtSetIndirect=false.
|
||||
d.IsCourtSet = true
|
||||
d.DueDate = ""
|
||||
d.OriginalDate = ""
|
||||
@@ -343,8 +368,16 @@ func (s *FristenrechnerService) Calculate(ctx context.Context, proceedingCode, t
|
||||
// than fabricating one off the trigger date. The user can re-run
|
||||
// with the actual decision date once the court issues it (or
|
||||
// supplied via AnchorOverrides).
|
||||
//
|
||||
// This is the RoP.151 case (Antrag auf Kostenentscheidung is
|
||||
// "1 Monat ab Hauptentscheidung") — the rule has a real
|
||||
// duration but its anchor is the court-set parent. The UI
|
||||
// should say "unbestimmt", not "wird vom Gericht bestimmt":
|
||||
// the date isn't directly determined by the court, it's
|
||||
// derived from a date the court sets.
|
||||
if parentIsCourtSet {
|
||||
d.IsCourtSet = true
|
||||
d.IsCourtSetIndirect = true
|
||||
d.DueDate = ""
|
||||
d.OriginalDate = ""
|
||||
courtSet[r.ID] = true
|
||||
|
||||
Reference in New Issue
Block a user