Simpl-Resultat/src/utils/reorderRows.ts
le king fu 1c427a1b63 fix(budget): emit the grid rows depth-first and close subtotal blocks
buildBudgetYearRows sorted each top group by depth, so an expanded
subcategory showed at the end of its group instead of under its parent.
It now builds one block per child (lone leaf, or [subtotal, ...children]),
sorts the blocks once ("(direct)" first, then by name, leaves and
sub-groups mixed, as the Budget vs Actual report) and concatenates them;
top groups are sorted by type, sort_order and name.

reorderRows put a leaf that follows a sibling sub-group into that
sub-group's block. A row at the subtotal's depth or shallower now closes
the block, which also fixes "subtotals at the bottom" in the Budget vs
Actual and Compare reports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:42:27 -04:00

56 lines
2.2 KiB
TypeScript

/**
* Shared utility for reordering budget table rows.
* Recursively moves subtotal (parent) rows below their children
* at every depth level when "subtotals on bottom" is enabled.
*
* Expects a depth-first list (each parent followed by its whole subtree), the
* order `buildBudgetYearRows` and the report tree builders emit. A subtotal's
* block ends at the next row of its own depth or shallower, so a leaf that
* follows a sibling sub-group stays outside that sub-group (issue #424).
*
* The generic constraint only requires the two fields the algorithm actually
* reads (`is_parent`, `depth`) so it works for both the budget rows (required
* `is_parent`) and the Compare `CategoryDelta` rows (optional `is_parent`).
*/
export function reorderRows<
T extends { is_parent?: boolean; depth?: number },
>(rows: T[], subtotalsOnTop: boolean): T[] {
if (subtotalsOnTop) return rows;
function reorderGroup(groupRows: T[], parentDepth: number): T[] {
const result: T[] = [];
let currentParent: T | null = null;
let currentChildren: T[] = [];
for (const row of groupRows) {
if (row.is_parent && (row.depth ?? 0) === parentDepth) {
// Flush previous group
if (currentParent) {
result.push(...reorderGroup(currentChildren, parentDepth + 1), currentParent);
currentChildren = [];
}
currentParent = row;
} else if (currentParent && (row.depth ?? 0) <= parentDepth) {
// A row at this level (or above) that is not a parent of this level
// closes the current block: it is a sibling of the subtotal, not one of
// its children (issue #424 — a leaf after a sibling sub-group used to
// fall into that sub-group's block).
result.push(...reorderGroup(currentChildren, parentDepth + 1), currentParent);
currentChildren = [];
currentParent = null;
result.push(row);
} else if (currentParent) {
currentChildren.push(row);
} else {
result.push(row);
}
}
// Flush last group
if (currentParent) {
result.push(...reorderGroup(currentChildren, parentDepth + 1), currentParent);
}
return result;
}
return reorderGroup(rows, 0);
}