diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index c7265c6..e3b1917 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -2,6 +2,10 @@ ## [Non publié] +### Ajouté + +- Rapports → Comparaison : les deux rapports comparables hiérarchiques (réel vs réel et réel vs budget) permettent désormais de **replier ou déplier les sous-catégories de chaque catégorie parente**. Un chevron sur chaque catégorie de premier niveau masque son détail tout en gardant sa ligne de sous-total visible, et un bouton « Tout déplier / Tout replier » les bascule ensemble. Les groupes s'ouvrent **repliés** pour un aperçu compact au niveau des sous-totaux ; ce que vous dépliez est mémorisé par rapport (#254). + ## [0.12.0] - 2026-07-05 ### Modifié diff --git a/CHANGELOG.md b/CHANGELOG.md index 52dba72..665c32c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Reports → Compare: the two hierarchical comparable reports (real-vs-real and real-vs-budget) now let you **collapse or expand each parent category's sub-categories**. A chevron on every top-level category folds its breakdown away while keeping the category's subtotal row in view, and an "Expand all / Collapse all" button toggles them together. Groups start **collapsed** so the report opens on a compact, subtotal-level overview; whatever you expand is remembered per report (#254). + ## [0.12.0] - 2026-07-05 ### Changed diff --git a/src/components/reports/BudgetVsActualTable.tsx b/src/components/reports/BudgetVsActualTable.tsx index 63218a9..a750374 100644 --- a/src/components/reports/BudgetVsActualTable.tsx +++ b/src/components/reports/BudgetVsActualTable.tsx @@ -1,8 +1,10 @@ import { Fragment, useState } from "react"; import { useTranslation } from "react-i18next"; -import { ArrowUpDown } from "lucide-react"; +import { ArrowUpDown, ChevronDown, ChevronRight, ChevronsDownUp, ChevronsUpDown } from "lucide-react"; import type { BudgetVsActualRow } from "../../shared/types"; import { reorderRows } from "../../utils/reorderRows"; +import type { CollapseAccessors } from "../../utils/collapsibleRows"; +import { useCollapsibleGroups } from "../../hooks/useCollapsibleGroups"; const cadFormatter = (value: number) => new Intl.NumberFormat("en-CA", { @@ -26,6 +28,17 @@ 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. +const BVA_COLLAPSE_ACCESSORS: CollapseAccessors = { + keyOf: (row) => String(row.category_id), + depthOf: (row) => row.depth ?? (row.parent_id !== null && !row.is_parent ? 1 : 0), + isParent: (row) => row.is_parent, +}; + +const BVA_EXPANDED_STORAGE_KEY = "reports-bva-expanded"; + export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps) { const { t } = useTranslation(); const [subtotalsOnTop, setSubtotalsOnTop] = useState(() => { @@ -41,6 +54,12 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps) }); }; + // Collapse/expand of sub-category groups — collapsed by default (issue #254). + const groups = useCollapsibleGroups( + BVA_EXPANDED_STORAGE_KEY, + BVA_COLLAPSE_ACCESSORS, + ); + if (data.length === 0) { return (
@@ -88,9 +107,22 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps) const totalMonthPct = totals.monthBudget !== 0 ? totals.monthVariation / Math.abs(totals.monthBudget) : null; const totalYtdPct = totals.ytdBudget !== 0 ? totals.ytdVariation / Math.abs(totals.ytdBudget) : null; + const hasGroups = groups.groupCount(data) > 0; + const allExpanded = groups.allExpanded(data); + return (
-
+
+ {hasGroups && ( + + )} + ) : ( + + + {row.category_name} + + )} {cadFormatter(row.monthActual)} diff --git a/src/components/reports/ComparePeriodTable.tsx b/src/components/reports/ComparePeriodTable.tsx index acb7a01..e77b70c 100644 --- a/src/components/reports/ComparePeriodTable.tsx +++ b/src/components/reports/ComparePeriodTable.tsx @@ -1,8 +1,10 @@ import { Fragment, useState } from "react"; import { useTranslation } from "react-i18next"; -import { ArrowUpDown } from "lucide-react"; +import { ArrowUpDown, ChevronDown, ChevronRight, ChevronsDownUp, ChevronsUpDown } from "lucide-react"; import type { CategoryDelta } from "../../shared/types"; import { reorderRows } from "../../utils/reorderRows"; +import type { CollapseAccessors } from "../../utils/collapsibleRows"; +import { useCollapsibleGroups } from "../../hooks/useCollapsibleGroups"; import { type SectionType, type Totals, sumLeaves, pct, computeResults } from "./compareResults"; export interface ComparePeriodTableProps { @@ -57,6 +59,16 @@ function deltaColor(value: number, higherIsBetter: boolean): string { const STORAGE_KEY = "compare-subtotals-position"; +// Collapse groups keyed by category id; depth/parent mirror the render logic +// below so the hidden rows are exactly a group's indented descendants. +const COMPARE_COLLAPSE_ACCESSORS: CollapseAccessors = { + keyOf: (row) => String(row.categoryId), + depthOf: (row) => row.depth ?? 0, + isParent: (row) => row.is_parent ?? false, +}; + +const COMPARE_EXPANDED_STORAGE_KEY = "reports-compare-expanded"; + const COL_COUNT = 9; export default function ComparePeriodTable({ @@ -81,6 +93,12 @@ export default function ComparePeriodTable({ }); }; + // Collapse/expand of sub-category groups — collapsed by default (issue #254). + const groups = useCollapsibleGroups( + COMPARE_EXPANDED_STORAGE_KEY, + COMPARE_COLLAPSE_ACCESSORS, + ); + const monthPrevLabel = previousLabel; const monthCurrLabel = currentLabel; const ytdPrevLabel = cumulativePreviousLabel ?? previousLabel; @@ -131,11 +149,12 @@ export default function ComparePeriodTable({ {sectionLabels[section.type]} - {reorderRows(section.rows, subtotalsOnTop).map((row) => { + {reorderRows(groups.visible(section.rows), subtotalsOnTop).map((row) => { const isParent = row.is_parent ?? false; const depth = row.depth ?? 0; const isTopParent = isParent && depth === 0; const isIntermediateParent = isParent && depth >= 1; + const collapsed = isTopParent && groups.isCollapsed(row); const paddingClass = depth >= 3 ? "pl-20" : depth === 2 ? "pl-14" : depth === 1 ? "pl-8" : "px-3"; return ( @@ -158,13 +177,33 @@ export default function ComparePeriodTable({ : `${paddingClass} bg-[var(--card)]` }`} > - - - {row.categoryName} - + {isTopParent ? ( + + ) : ( + + + {row.categoryName} + + )} {/* Monthly block */} @@ -301,9 +340,22 @@ export default function ComparePeriodTable({ ); }; + const hasGroups = groups.groupCount(rows) > 0; + const allExpanded = groups.allExpanded(rows); + return (
-
+
+ {hasGroups && ( + + )}