Render the real-vs-real comparable report as a parent/child category tree with subtotal rows, mirroring the budget report — rows grouped into expense/income/transfer sections with per-section and grand totals and a subtotals-on-top/bottom toggle. The compare service now builds the tree on top of the flat per-category deltas, so leaf-category figures are unchanged and the #243 transfer netting is preserved (a balanced transfer group subtotals to ~0, i.e. the group's net). - reportService: buildCompareTree() synthesizes subtotal rows from the flat leaves + category metadata; getCompareMonthOverMonth/YoY return the tree (COMPARE_DELTA_SQL and rowsToDeltas untouched). - CategoryDelta gains optional parent_id/is_parent/depth/category_type. - ComparePeriodTable: sections, depth indentation, reorderRows toggle, section/grand net totals. - Cartes top-movers and ComparePeriodChart filter to leaf rows only. - reorderRows constraint loosened to the two fields it reads. - i18n (FR+EN) section labels; CHANGELOG entries. Resolves #247 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
42 lines
1.4 KiB
TypeScript
42 lines
1.4 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.
|
|
*
|
|
* 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) {
|
|
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);
|
|
}
|