Merge: t-paliad-302 — Verfahrensablauf duration indicator (hover + toggle, +3 lp.TimelineEntry fields) (m/paliad#133)
This commit is contained in:
@@ -312,6 +312,7 @@ const translations: Record<Lang, Record<string, string>> = {
|
|||||||
"deadlines.view.timeline": "Zeitstrahl",
|
"deadlines.view.timeline": "Zeitstrahl",
|
||||||
"deadlines.view.columns": "Spalten",
|
"deadlines.view.columns": "Spalten",
|
||||||
"deadlines.notes.show": "Hinweise anzeigen",
|
"deadlines.notes.show": "Hinweise anzeigen",
|
||||||
|
"deadlines.durations.show": "Dauern anzeigen",
|
||||||
"deadlines.col.ours": "Unsere Seite",
|
"deadlines.col.ours": "Unsere Seite",
|
||||||
"deadlines.col.court": "Gericht",
|
"deadlines.col.court": "Gericht",
|
||||||
"deadlines.col.opponent": "Gegnerseite",
|
"deadlines.col.opponent": "Gegnerseite",
|
||||||
@@ -3406,6 +3407,7 @@ const translations: Record<Lang, Record<string, string>> = {
|
|||||||
"deadlines.view.timeline": "Timeline",
|
"deadlines.view.timeline": "Timeline",
|
||||||
"deadlines.view.columns": "Columns",
|
"deadlines.view.columns": "Columns",
|
||||||
"deadlines.notes.show": "Show details",
|
"deadlines.notes.show": "Show details",
|
||||||
|
"deadlines.durations.show": "Show durations",
|
||||||
"deadlines.col.ours": "Client Side",
|
"deadlines.col.ours": "Client Side",
|
||||||
"deadlines.col.court": "Court",
|
"deadlines.col.court": "Court",
|
||||||
"deadlines.col.opponent": "Opponent Side",
|
"deadlines.col.opponent": "Opponent Side",
|
||||||
|
|||||||
@@ -276,6 +276,21 @@ function writeNotesPref(on: boolean): void {
|
|||||||
}
|
}
|
||||||
let showNotes = readNotesPref();
|
let showNotes = readNotesPref();
|
||||||
|
|
||||||
|
// Durations toggle (m/paliad#133, t-paliad-302) — when off (default),
|
||||||
|
// the per-rule duration label ("2 Mo. nach") only shows on hover via
|
||||||
|
// the date span's `title` attribute. When on, the label renders inline
|
||||||
|
// in the timeline meta row of every event card. Persisted in
|
||||||
|
// localStorage under its own key so the preference is independent of
|
||||||
|
// "Hinweise anzeigen".
|
||||||
|
const DURATIONS_PREF_KEY = "paliad.verfahrensablauf.durations-show";
|
||||||
|
function readDurationsPref(): boolean {
|
||||||
|
try { return localStorage.getItem(DURATIONS_PREF_KEY) === "1"; } catch { return false; }
|
||||||
|
}
|
||||||
|
function writeDurationsPref(on: boolean): void {
|
||||||
|
try { localStorage.setItem(DURATIONS_PREF_KEY, on ? "1" : "0"); } catch { /* no-op */ }
|
||||||
|
}
|
||||||
|
let showDurations = readDurationsPref();
|
||||||
|
|
||||||
// Jurisdiction display prefix for the proceeding-summary chip + the
|
// Jurisdiction display prefix for the proceeding-summary chip + the
|
||||||
// trigger-event placeholder. Same forum slugs the .proceeding-group
|
// trigger-event placeholder. Same forum slugs the .proceeding-group
|
||||||
// `data-forum` attribute carries in verfahrensablauf.tsx /
|
// `data-forum` attribute carries in verfahrensablauf.tsx /
|
||||||
@@ -482,6 +497,7 @@ function renderResults(data: DeadlineResponse) {
|
|||||||
? renderColumnsBody(data, {
|
? renderColumnsBody(data, {
|
||||||
editable: true,
|
editable: true,
|
||||||
showNotes,
|
showNotes,
|
||||||
|
showDurations,
|
||||||
side: currentSide,
|
side: currentSide,
|
||||||
// t-paliad-301: the appellant axis collapses into the single
|
// t-paliad-301: the appellant axis collapses into the single
|
||||||
// side picker. For role-swap proceedings, currentSide IS the
|
// side picker. For role-swap proceedings, currentSide IS the
|
||||||
@@ -490,7 +506,7 @@ function renderResults(data: DeadlineResponse) {
|
|||||||
// the appellant axis is irrelevant — pass null.
|
// the appellant axis is irrelevant — pass null.
|
||||||
appellant: hasAppellantAxis(selectedType) ? currentSide : null,
|
appellant: hasAppellantAxis(selectedType) ? currentSide : null,
|
||||||
})
|
})
|
||||||
: renderTimelineBody(data, { showParty: true, editable: true, showNotes });
|
: renderTimelineBody(data, { showParty: true, editable: true, showNotes, showDurations });
|
||||||
|
|
||||||
container.innerHTML = headerHtml + noteHtml + bodyHtml;
|
container.innerHTML = headerHtml + noteHtml + bodyHtml;
|
||||||
if (printBtn) printBtn.style.display = "block";
|
if (printBtn) printBtn.style.display = "block";
|
||||||
@@ -868,6 +884,19 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Durations toggle (m/paliad#133, t-paliad-302) — sibling of the
|
||||||
|
// notes toggle. Hover-only labels (default) become inline labels when
|
||||||
|
// the user opts in.
|
||||||
|
const durationsShowCb = document.getElementById("verfahrensablauf-durations-show") as HTMLInputElement | null;
|
||||||
|
if (durationsShowCb) {
|
||||||
|
durationsShowCb.checked = showDurations;
|
||||||
|
durationsShowCb.addEventListener("change", () => {
|
||||||
|
showDurations = durationsShowCb.checked;
|
||||||
|
writeDurationsPref(showDurations);
|
||||||
|
if (lastResponse) renderResults(lastResponse);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// t-paliad-290 — show-hidden toggle. Hydrate from URL, wire change
|
// t-paliad-290 — show-hidden toggle. Hydrate from URL, wire change
|
||||||
// to URL + recalc (the backend reshapes the response — we can't just
|
// to URL + recalc (the backend reshapes the response — we can't just
|
||||||
// re-render lastResponse since the hidden rows aren't in it when the
|
// re-render lastResponse since the hidden rows aren't in it when the
|
||||||
|
|||||||
@@ -95,6 +95,36 @@ export interface CalculatedDeadline {
|
|||||||
parentRuleCode?: string;
|
parentRuleCode?: string;
|
||||||
parentRuleName?: string;
|
parentRuleName?: string;
|
||||||
parentRuleNameEN?: string;
|
parentRuleNameEN?: string;
|
||||||
|
// durationValue / durationUnit / timing surface the rule's arithmetic
|
||||||
|
// so the timeline card can show "2 Mo. nach" on hover (and inline when
|
||||||
|
// the "Dauern anzeigen" toggle is on). Zero-duration rules (root
|
||||||
|
// event, court-set) carry durationValue=0 and the renderer suppresses
|
||||||
|
// the affordance — those don't have an explainable interval.
|
||||||
|
// (m/paliad#133, t-paliad-302)
|
||||||
|
durationValue?: number;
|
||||||
|
durationUnit?: string;
|
||||||
|
timing?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatDurationLabel renders the per-rule duration ("2 Mo. nach") for
|
||||||
|
// the Verfahrensablauf card affordance (m/paliad#133, t-paliad-302).
|
||||||
|
// Returns empty string for rules without a usable duration so the
|
||||||
|
// caller can skip the tooltip / inline span entirely.
|
||||||
|
//
|
||||||
|
// Pluralisation key naming mirrors the Fristenrechner event-mode
|
||||||
|
// renderer (deadlines.event.unit.<unit>.{one,many}) — the unit and
|
||||||
|
// timing translations already exist for /tools/fristenrechner's
|
||||||
|
// "Was kommt nach…" mode and are reused here as the single
|
||||||
|
// source of truth.
|
||||||
|
export function formatDurationLabel(dl: CalculatedDeadline): string {
|
||||||
|
const value = dl.durationValue ?? 0;
|
||||||
|
const unit = dl.durationUnit || "";
|
||||||
|
if (value <= 0 || !unit) return "";
|
||||||
|
const unitKey = `deadlines.event.unit.${unit}` + (value === 1 ? ".one" : ".many");
|
||||||
|
const unitStr = tDyn(unitKey);
|
||||||
|
const timing = dl.timing || "";
|
||||||
|
const timingStr = timing ? tDyn(`deadlines.event.timing.${timing}`) : "";
|
||||||
|
return timingStr ? `${value} ${unitStr} ${timingStr}` : `${value} ${unitStr}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// priorityRendering returns the per-priority UX hints the save-modal
|
// priorityRendering returns the per-priority UX hints the save-modal
|
||||||
@@ -321,15 +351,38 @@ export interface CardOpts {
|
|||||||
// Page shells expose a toggle ("Hinweise anzeigen") that flips this and
|
// Page shells expose a toggle ("Hinweise anzeigen") that flips this and
|
||||||
// re-renders. Default false — notes are noisy on long timelines.
|
// re-renders. Default false — notes are noisy on long timelines.
|
||||||
showNotes?: boolean;
|
showNotes?: boolean;
|
||||||
|
// showDurations controls per-rule duration rendering on event cards
|
||||||
|
// (m/paliad#133, t-paliad-302):
|
||||||
|
// true → inline `<span class="timeline-duration">2 Mo. nach</span>`
|
||||||
|
// next to the date.
|
||||||
|
// false → hover-only tooltip on the date span (browser-native
|
||||||
|
// `title` attribute). Cards without a usable
|
||||||
|
// `durationValue > 0` get neither — court-set and trigger-
|
||||||
|
// event cards have no explainable interval.
|
||||||
|
// /tools/verfahrensablauf exposes a toggle ("Dauern anzeigen") that
|
||||||
|
// flips this and re-renders; persisted via the localStorage key
|
||||||
|
// `paliad.verfahrensablauf.durations-show`. Default false.
|
||||||
|
showDurations?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deadlineCardHtml(dl: CalculatedDeadline, opts: CardOpts): string {
|
export function deadlineCardHtml(dl: CalculatedDeadline, opts: CardOpts): string {
|
||||||
const wantsEditable = !!opts.editable;
|
const wantsEditable = !!opts.editable;
|
||||||
const editable = wantsEditable && !dl.isRootEvent && dl.code !== "";
|
const editable = wantsEditable && !dl.isRootEvent && dl.code !== "";
|
||||||
const overriddenClass = dl.isOverridden ? " timeline-date--overridden" : "";
|
const overriddenClass = dl.isOverridden ? " timeline-date--overridden" : "";
|
||||||
|
// Duration affordance (m/paliad#133, t-paliad-302). Computed once so
|
||||||
|
// both the date-span tooltip and the inline meta-row span pull from
|
||||||
|
// the same string. Empty for rules without a usable duration.
|
||||||
|
const durationLabel = formatDurationLabel(dl);
|
||||||
|
// Hover affordance on the date span: prefer the duration tooltip when
|
||||||
|
// we have one, else fall back to the edit-hint when the cell is
|
||||||
|
// click-to-edit. The edit affordance still works either way — the
|
||||||
|
// title is purely advisory.
|
||||||
|
const dateTitle = durationLabel
|
||||||
|
? durationLabel
|
||||||
|
: (editable ? t("deadlines.date.edit.hint") : "");
|
||||||
const editAttrs = editable
|
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"))}"`
|
? ` data-rule-code="${escAttr(dl.code)}" data-current-date="${escAttr(dl.dueDate)}" role="button" tabindex="0"${dateTitle ? ` title="${escAttr(dateTitle)}"` : ""}`
|
||||||
: "";
|
: (dateTitle ? ` title="${escAttr(dateTitle)}"` : "");
|
||||||
// Conditional rows (t-paliad-289) replace the date column with an
|
// Conditional rows (t-paliad-289) replace the date column with an
|
||||||
// "abhängig von <parent>" chip. The chip remains click-to-edit so
|
// "abhängig von <parent>" chip. The chip remains click-to-edit so
|
||||||
// the user can pin a real date once known (e.g. once the oral
|
// the user can pin a real date once known (e.g. once the oral
|
||||||
@@ -434,9 +487,19 @@ export function deadlineCardHtml(dl: CalculatedDeadline, opts: CardOpts): string
|
|||||||
? `<span class="timeline-note-hint" tabindex="0" role="note" aria-label="${escAttr(noteText)}" title="${escAttr(noteText)}">ⓘ</span>`
|
? `<span class="timeline-note-hint" tabindex="0" role="note" aria-label="${escAttr(noteText)}" title="${escAttr(noteText)}">ⓘ</span>`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const meta = (opts.showParty || ruleRef || noteHint)
|
// Inline duration affordance (m/paliad#133, t-paliad-302). Only
|
||||||
|
// emitted when the "Dauern anzeigen" toggle is on AND the rule has a
|
||||||
|
// usable duration; the default-off hover-tooltip path is wired
|
||||||
|
// separately on the date span itself.
|
||||||
|
const showDurations = opts.showDurations === true;
|
||||||
|
const durationInline = showDurations && durationLabel
|
||||||
|
? `<span class="timeline-duration">${escHtml(durationLabel)}</span>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const meta = (opts.showParty || ruleRef || noteHint || durationInline)
|
||||||
? `<div class="timeline-meta">
|
? `<div class="timeline-meta">
|
||||||
${opts.showParty ? partyBadge(dl.party) : ""}
|
${opts.showParty ? partyBadge(dl.party) : ""}
|
||||||
|
${durationInline}
|
||||||
${ruleRef}
|
${ruleRef}
|
||||||
${noteHint}
|
${noteHint}
|
||||||
</div>`
|
</div>`
|
||||||
@@ -614,6 +677,9 @@ type ColumnPosition = "ours" | "opponent";
|
|||||||
export interface ColumnsBodyOpts {
|
export interface ColumnsBodyOpts {
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
showNotes?: boolean;
|
showNotes?: boolean;
|
||||||
|
// Forwarded to deadlineCardHtml — see CardOpts.showDurations.
|
||||||
|
// (m/paliad#133, t-paliad-302)
|
||||||
|
showDurations?: boolean;
|
||||||
// side: which side the user is on. Drives column placement;
|
// side: which side the user is on. Drives column placement;
|
||||||
// does NOT filter rows. Default null = claimant-on-the-left
|
// does NOT filter rows. Default null = claimant-on-the-left
|
||||||
// (i.e. "ours = claimant", legacy default).
|
// (i.e. "ours = claimant", legacy default).
|
||||||
@@ -724,7 +790,12 @@ export function renderColumnsBody(data: DeadlineResponse, opts: ColumnsBodyOpts
|
|||||||
const rows = bucketDeadlinesIntoColumns(data.deadlines, { side: userSide, appellant: opts.appellant });
|
const rows = bucketDeadlinesIntoColumns(data.deadlines, { side: userSide, appellant: opts.appellant });
|
||||||
const appellantPinned = opts.appellant === "claimant" || opts.appellant === "defendant";
|
const appellantPinned = opts.appellant === "claimant" || opts.appellant === "defendant";
|
||||||
|
|
||||||
const cardOpts: CardOpts = { showParty: false, editable: opts.editable, showNotes: opts.showNotes };
|
const cardOpts: CardOpts = {
|
||||||
|
showParty: false,
|
||||||
|
editable: opts.editable,
|
||||||
|
showNotes: opts.showNotes,
|
||||||
|
showDurations: opts.showDurations,
|
||||||
|
};
|
||||||
|
|
||||||
// Collapsed "both" rows lose their mirror tag — there's no longer
|
// Collapsed "both" rows lose their mirror tag — there's no longer
|
||||||
// a sibling row to mirror to, so the "↔ beide Seiten" hint would
|
// a sibling row to mirror to, so the "↔ beide Seiten" hint would
|
||||||
|
|||||||
@@ -1264,6 +1264,7 @@ export type I18nKey =
|
|||||||
| "deadlines.dpma.appeal.bgh"
|
| "deadlines.dpma.appeal.bgh"
|
||||||
| "deadlines.dpma.appeal.bpatg"
|
| "deadlines.dpma.appeal.bpatg"
|
||||||
| "deadlines.dpma.opp.dpma"
|
| "deadlines.dpma.opp.dpma"
|
||||||
|
| "deadlines.durations.show"
|
||||||
| "deadlines.empty.filtered"
|
| "deadlines.empty.filtered"
|
||||||
| "deadlines.empty.hint"
|
| "deadlines.empty.hint"
|
||||||
| "deadlines.empty.title"
|
| "deadlines.empty.title"
|
||||||
|
|||||||
@@ -3750,6 +3750,16 @@ input[type="range"]::-moz-range-thumb {
|
|||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Per-rule duration label rendered inline in the meta row when
|
||||||
|
"Dauern anzeigen" is on (m/paliad#133, t-paliad-302). Matches the
|
||||||
|
sibling .timeline-rule weight so the meta line reads as one band of
|
||||||
|
secondary metadata; non-mono so the value reads as prose ("2 Mo. nach")
|
||||||
|
rather than a code reference. */
|
||||||
|
.timeline-duration {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
.timeline-adjusted {
|
.timeline-adjusted {
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
color: var(--status-amber-fg-2);
|
color: var(--status-amber-fg-2);
|
||||||
|
|||||||
@@ -341,6 +341,13 @@ export function renderVerfahrensablauf(): string {
|
|||||||
<input type="checkbox" id="fristen-notes-show" />
|
<input type="checkbox" id="fristen-notes-show" />
|
||||||
<span data-i18n="deadlines.notes.show">Hinweise anzeigen</span>
|
<span data-i18n="deadlines.notes.show">Hinweise anzeigen</span>
|
||||||
</label>
|
</label>
|
||||||
|
{/* Durations toggle (m/paliad#133, t-paliad-302).
|
||||||
|
Default off — hover-tooltips on date spans are
|
||||||
|
the always-on path. */}
|
||||||
|
<label className="fristen-notes-option">
|
||||||
|
<input type="checkbox" id="verfahrensablauf-durations-show" />
|
||||||
|
<span data-i18n="deadlines.durations.show">Dauern anzeigen</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="timeline-container">
|
<div id="timeline-container">
|
||||||
|
|||||||
@@ -249,6 +249,10 @@ func Calculate(
|
|||||||
appellantContext[r.ID] = ctxVal
|
appellantContext[r.ID] = ctxVal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ruleTiming := ""
|
||||||
|
if r.Timing != nil {
|
||||||
|
ruleTiming = *r.Timing
|
||||||
|
}
|
||||||
d := TimelineEntry{
|
d := TimelineEntry{
|
||||||
RuleID: r.ID.String(),
|
RuleID: r.ID.String(),
|
||||||
Name: r.Name,
|
Name: r.Name,
|
||||||
@@ -258,6 +262,9 @@ func Calculate(
|
|||||||
AppellantContext: ctxVal,
|
AppellantContext: ctxVal,
|
||||||
ChoicesOffered: json.RawMessage(r.ChoicesOffered),
|
ChoicesOffered: json.RawMessage(r.ChoicesOffered),
|
||||||
IsHidden: isHidden,
|
IsHidden: isHidden,
|
||||||
|
DurationValue: r.DurationValue,
|
||||||
|
DurationUnit: r.DurationUnit,
|
||||||
|
Timing: ruleTiming,
|
||||||
}
|
}
|
||||||
if r.SubmissionCode != nil {
|
if r.SubmissionCode != nil {
|
||||||
d.Code = *r.SubmissionCode
|
d.Code = *r.SubmissionCode
|
||||||
@@ -671,6 +678,9 @@ func calculateByTriggerEvent(
|
|||||||
OriginalDate: original.Format("2006-01-02"),
|
OriginalDate: original.Format("2006-01-02"),
|
||||||
WasAdjusted: wasAdj,
|
WasAdjusted: wasAdj,
|
||||||
AdjustmentReason: reason,
|
AdjustmentReason: reason,
|
||||||
|
DurationValue: r.DurationValue,
|
||||||
|
DurationUnit: r.DurationUnit,
|
||||||
|
Timing: timing,
|
||||||
}
|
}
|
||||||
if r.SubmissionCode != nil {
|
if r.SubmissionCode != nil {
|
||||||
d.Code = *r.SubmissionCode
|
d.Code = *r.SubmissionCode
|
||||||
|
|||||||
@@ -430,6 +430,17 @@ type TimelineEntry struct {
|
|||||||
ChoicesOffered json.RawMessage `json:"choicesOffered,omitempty"`
|
ChoicesOffered json.RawMessage `json:"choicesOffered,omitempty"`
|
||||||
AppellantContext string `json:"appellantContext,omitempty"`
|
AppellantContext string `json:"appellantContext,omitempty"`
|
||||||
IsHidden bool `json:"isHidden,omitempty"`
|
IsHidden bool `json:"isHidden,omitempty"`
|
||||||
|
// DurationValue / DurationUnit / Timing surface the rule's
|
||||||
|
// arithmetic so /tools/verfahrensablauf can show "2 Mo. nach" on
|
||||||
|
// each event card (m/paliad#133, t-paliad-302). Source values from
|
||||||
|
// the Rule row (not the post-alt-swap arithmetic) — the tooltip
|
||||||
|
// reads as a property of the rule, not a recap of which branch
|
||||||
|
// fired. Zero-duration rules (root event, court-set) emit
|
||||||
|
// DurationValue=0 and the frontend suppresses the affordance.
|
||||||
|
// Timing is "before" | "after" — empty when r.Timing is NULL.
|
||||||
|
DurationValue int `json:"durationValue,omitempty"`
|
||||||
|
DurationUnit string `json:"durationUnit,omitempty"`
|
||||||
|
Timing string `json:"timing,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RuleCalculation is the single-rule calc response that backs the
|
// RuleCalculation is the single-rule calc response that backs the
|
||||||
|
|||||||
Reference in New Issue
Block a user