From 48adb3db772a7f31ffbf573c2ccc8cad5ffe7dc2 Mon Sep 17 00:00:00 2001 From: le king fu Date: Wed, 15 Jul 2026 20:58:18 -0400 Subject: [PATCH] feat(reports): collapse category hierarchy at every level (socle + 3 reports) Generalize the report category collapse from level-1-only to every hierarchy level, on the three hierarchical report tables (real-vs-real Compare, real-vs-budget Compare, Trends by category). Visibility is now decided by an ancestor walk, not by row adjacency, so it is independent of row order (the level-ordered budget grid emits a non-DFS order). - collapsibleRows: rewrite visibleRows as an ancestor walk (a row is hidden iff any ancestor is collapsed); add parentKeyOf + injective `p:` keys; collapsibleKeys returns all parents (any depth); extract the pure, tested isCollapsedFor polarity helper; MAX_TREE_DEPTH cycle guard. - useCollapsibleGroups: persist in user_preferences (per-profile, destroyed with the profile) instead of localStorage; storageKey nullable (no persistence); options.defaultExpanded; async hydration (no flash); collapseAll(rows). - 3 tables: fix BOTH gates (collapsed flag + button) isTopParent -> isParent, add parentKeyOf accessors, aria-level on parent rows. - Delete dead CategoryTable.tsx (0 imports). - Tests: rewrite collapsibleRows.test.ts (BFS==DFS masking, cycle guard, cross-section ancestor, "(direct)" leaf, polarity); extend overTimeTableModel fixture to 3 levels with cascade assertions. Collapse stays purely visual: subtotals and result lines are computed from raw rows, never from visible rows. Resolves #288 Generated autonomously by /autopilot run of 2026-07-15 Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.fr.md | 4 + CHANGELOG.md | 4 + .../reports/BudgetVsActualTable.tsx | 15 +- .../reports/CategoryOverTimeTable.tsx | 7 +- src/components/reports/CategoryTable.tsx | 74 ------ src/components/reports/ComparePeriodTable.tsx | 14 +- .../reports/overTimeTableModel.test.ts | 96 +++++-- src/components/reports/overTimeTableModel.ts | 7 +- src/hooks/useCollapsibleGroups.ts | 106 +++++--- src/utils/collapsibleRows.test.ts | 246 ++++++++++++++---- src/utils/collapsibleRows.ts | 111 +++++--- 11 files changed, 434 insertions(+), 250 deletions(-) delete mode 100644 src/components/reports/CategoryTable.tsx diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index 1a34346..7c0ae1c 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -2,6 +2,10 @@ ## [Non publié] +### Modifié + +- Rapports : les trois tableaux de rapport hiérarchiques (Comparaison réel-vs-réel, réel-vs-budget, et Tendances par catégorie) permettent désormais de **replier ou déplier chaque niveau de la hiérarchie de catégories**, plus seulement le premier. Chaque catégorie parente — y compris les intermédiaires — porte son propre chevron ; en déplier une révèle ses enfants directs (eux-mêmes repliés), pour descendre un niveau à la fois. Les tableaux s'ouvrent entièrement repliés (seules les catégories de premier niveau visibles), « Tout déplier » ouvre tous les niveaux d'un coup, et vos choix de repli sont désormais enregistrés **par profil** — dans la base de données du profil, donc détruits avec lui — au lieu du navigateur. Les sous-totaux et les lignes de résultat restent rigoureusement inchangés quel que soit ce que vous repliez (#288). + ## [0.13.0] - 2026-07-12 ### Ajouté diff --git a/CHANGELOG.md b/CHANGELOG.md index baa699f..9131ae0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- Reports: the three hierarchical report tables (real-vs-real Compare, real-vs-budget Compare, and Trends by category) now let you **collapse or expand every level of the category hierarchy**, not just the top level. Every parent category — including the intermediate ones — carries its own chevron; expanding one reveals its direct children (themselves collapsed), so you drill down one level at a time. The tables open fully collapsed (only the top-level categories visible), "Expand all" opens every level at once, and your collapse choices are now saved **per profile** — in the profile's own database, so they are destroyed with the profile — instead of in the browser. Subtotals and result lines stay exactly the same whatever you fold away (#288). + ## [0.13.0] - 2026-07-12 ### Added diff --git a/src/components/reports/BudgetVsActualTable.tsx b/src/components/reports/BudgetVsActualTable.tsx index bd8f1b0..2257db5 100644 --- a/src/components/reports/BudgetVsActualTable.tsx +++ b/src/components/reports/BudgetVsActualTable.tsx @@ -29,11 +29,11 @@ interface BudgetVsActualTableProps { const STORAGE_KEY = "subtotals-position"; -// Collapse groups keyed by category id; depth mirrors the render logic below -// (a missing depth is derived from parent_id) so hidden rows are exactly a -// group's indented descendants. +// Collapse groups keyed by category id; a row is hidden when any ancestor +// (walking parent_id) is collapsed, so every parent level is collapsible. const BVA_COLLAPSE_ACCESSORS: CollapseAccessors = { - keyOf: (row) => String(row.category_id), + keyOf: (row) => `p:${row.category_id}`, + parentKeyOf: (row) => (row.parent_id != null ? `p:${row.parent_id}` : null), depthOf: (row) => row.depth ?? (row.parent_id !== null && !row.is_parent ? 1 : 0), isParent: (row) => row.is_parent, }; @@ -116,11 +116,12 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps) const depth = row.depth ?? (row.parent_id !== null && !row.is_parent ? 1 : 0); const isTopParent = isParent && depth === 0; const isIntermediateParent = isParent && depth >= 1; - const collapsed = isTopParent && groups.isCollapsed(row); + const collapsed = isParent && groups.isCollapsed(row); const paddingClass = depth >= 3 ? "pl-20" : depth === 2 ? "pl-14" : depth === 1 ? "pl-8" : "px-3"; return ( - {isTopParent ? ( + {isParent ? (