fix(fristenrechner/columns): preserve sequence_order for undated events

Undated events (Urteil, Beschluss, court-set placeholders) were keyed by
the empty string and collapsed into a single trailing row, so Urteil and
Berufungseinlegung ended up adjacent even though Urteil precedes Berufung
in the proceeding's sequence_order. Each undated event now gets its own
row keyed by its index in the backend response (which is already sorted
by sequence_order), and dated/unscheduled keys are sorted into separate
buckets before concatenation so the dateless tail still sits below the
dated rows.

Refs #14 (section D).
This commit is contained in:
m
2026-05-08 15:54:35 +02:00
parent 7daa70aaad
commit 609da9e86b

View File

@@ -601,13 +601,15 @@ function renderTimelineBody(data: DeadlineResponse): string {
// (defendant). Each grid row corresponds to a distinct dueDate, so events on
// the same day line up across columns. Deadlines with party=both render in
// BOTH the Proactive and Reactive cells of their row with a "beide Seiten"
// caption so the duplication is legible as intentional. Court-set / dateless
// rows collapse into a single trailing row at the bottom.
// caption so the duplication is legible as intentional. Undated events
// (Urteil, Beschluss, court-set placeholders) trail the dated rows; each
// gets its own row in the backend's sequence_order so e.g. Urteil precedes
// Berufungseinlegung visually instead of stacking in one bucket.
function renderColumnsBody(data: DeadlineResponse): string {
type Cell = CalculatedDeadline[];
type Row = { proactive: Cell; court: Cell; reactive: Cell };
const NO_DATE = "";
const UNSCHEDULED_PREFIX = "__unscheduled__";
const rowsMap = new Map<string, Row>();
const ensureRow = (key: string): Row => {
let r = rowsMap.get(key);
@@ -618,8 +620,11 @@ function renderColumnsBody(data: DeadlineResponse): string {
return r;
};
for (const dl of data.deadlines) {
const key = dl.dueDate || NO_DATE;
data.deadlines.forEach((dl, idx) => {
// Dated rows share a row by date; undated rows each get their own row,
// keyed by index so the backend's sequence_order is preserved in the
// dateless tail.
const key = dl.dueDate || `${UNSCHEDULED_PREFIX}${String(idx).padStart(4, "0")}`;
const row = ensureRow(key);
switch (dl.party) {
case "claimant":
@@ -640,16 +645,22 @@ function renderColumnsBody(data: DeadlineResponse): string {
// Unknown party: keep visible by parking in the Court column.
row.court.push(dl);
}
}
// Sort row keys chronologically; the dateless bucket (court-set rows) sinks
// to the bottom because it has no temporal anchor.
const keys = Array.from(rowsMap.keys()).sort((a, b) => {
if (a === NO_DATE) return 1;
if (b === NO_DATE) return -1;
return a < b ? -1 : a > b ? 1 : 0;
});
// Dated keys (YYYY-MM-DD) sort chronologically by lexicographic compare.
// Unscheduled keys carry the sequence-order index in their padded suffix
// so they likewise sort by source order. Concatenate so the dateless tail
// sits below the dated rows.
const datedKeys: string[] = [];
const unscheduledKeys: string[] = [];
for (const k of rowsMap.keys()) {
if (k.startsWith(UNSCHEDULED_PREFIX)) unscheduledKeys.push(k);
else datedKeys.push(k);
}
datedKeys.sort();
unscheduledKeys.sort();
const keys = [...datedKeys, ...unscheduledKeys];
const renderCell = (items: CalculatedDeadline[]): string => {
if (items.length === 0) {
return `<div class="fr-col-cell fr-col-cell--empty"></div>`;