From f5dfbb5ad4f2fa4473becb671c74610e5f54227c Mon Sep 17 00:00:00 2001 From: medic-bot Date: Sun, 8 Mar 2026 23:03:29 -0400 Subject: [PATCH] fix: order level 4 categories under parent in budget vs actual table Rework sorting in budgetService to keep level-2 groups (subtotal + children) together under their intermediate parent instead of flat-sorting alphabetically which scattered depth-2 rows to the bottom. Also reduce pie chart size on dashboard (height 280->220, radius 100->85), change grid layout to 2/5 pie + 3/5 table, and make pie chart legend collapsible (collapsed by default) to give more space to the BVA table. Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.fr.md | 7 ++ CHANGELOG.md | 7 ++ src/components/dashboard/CategoryPieChart.tsx | 62 ++++++++++------- src/i18n/locales/en.json | 3 +- src/i18n/locales/fr.json | 3 +- src/pages/DashboardPage.tsx | 6 +- src/services/budgetService.ts | 67 +++++++++++++++++-- 7 files changed, 118 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index 7066584..3b5eece 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -5,6 +5,13 @@ ### Ajouté - Tableau de budget : colonne du total de l'année précédente affichée comme première colonne de données pour servir de référence (#16) +### Corrigé +- Tableau de bord : les catégories de niveau 4 apparaissent maintenant sous leur parent dans le tableau réel vs budget (#23) + +### Modifié +- Tableau de bord : taille du graphique circulaire réduite et plus d'espace pour le tableau réel vs budget (#23) +- Tableau de bord : la légende du graphique circulaire est maintenant repliable (repliée par défaut) pour économiser de l'espace (#23) + ## [0.6.3] ### Ajouté diff --git a/CHANGELOG.md b/CHANGELOG.md index edd44d6..f5f679e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ ### Added - Budget table: previous year total column displayed as first data column for baseline reference (#16) +### Fixed +- Dashboard: level 4 categories now appear under their parent in the budget vs actual table (#23) + +### Changed +- Dashboard: reduced pie chart size and gave more space to the budget vs actual table (#23) +- Dashboard: pie chart legend is now collapsible (collapsed by default) to save space (#23) + ## [0.6.3] ### Added diff --git a/src/components/dashboard/CategoryPieChart.tsx b/src/components/dashboard/CategoryPieChart.tsx index 2fe8195..2d5b406 100644 --- a/src/components/dashboard/CategoryPieChart.tsx +++ b/src/components/dashboard/CategoryPieChart.tsx @@ -1,7 +1,7 @@ import { useState, useRef, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from "recharts"; -import { Eye } from "lucide-react"; +import { Eye, ChevronDown, ChevronUp } from "lucide-react"; import type { CategoryBreakdownItem } from "../../shared/types"; import { ChartPatternDefs, getPatternFill, PatternSwatch } from "../../utils/chartPatterns"; import ChartContextMenu from "../shared/ChartContextMenu"; @@ -24,6 +24,7 @@ export default function CategoryPieChart({ const { t } = useTranslation(); const hoveredRef = useRef(null); const [contextMenu, setContextMenu] = useState<{ x: number; y: number; item: CategoryBreakdownItem } | null>(null); + const [legendExpanded, setLegendExpanded] = useState(false); const visibleData = data.filter((d) => !hiddenCategories.has(d.category_name)); const total = visibleData.reduce((sum, d) => sum + d.total, 0); @@ -67,7 +68,7 @@ export default function CategoryPieChart({ )}
- + {visibleData.map((item, index) => ( @@ -110,27 +111,38 @@ export default function CategoryPieChart({
-
- {data.map((item, index) => { - const isHidden = hiddenCategories.has(item.category_name); - return ( - - ); - })} +
+ + {legendExpanded && ( +
+ {data.map((item, index) => { + const isHidden = hiddenCategories.has(item.category_name); + return ( + + ); + })} +
+ )}
{contextMenu && ( diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index b1da85f..d40d25d 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -508,7 +508,8 @@ "showAll": "Show all", "total": "Total", "transactions": "transactions", - "clickToShow": "Click to show" + "clickToShow": "Click to show", + "legend": "Legend" }, "months": { "jan": "Jan", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 28abfd7..d248110 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -508,7 +508,8 @@ "showAll": "Tout afficher", "total": "Total", "transactions": "transactions", - "clickToShow": "Cliquer pour afficher" + "clickToShow": "Cliquer pour afficher", + "legend": "Légende" }, "months": { "jan": "Jan", diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx index d484fbf..7bdab5c 100644 --- a/src/pages/DashboardPage.tsx +++ b/src/pages/DashboardPage.tsx @@ -126,8 +126,8 @@ export default function DashboardPage() { ))}
-
-
+
+

{t("dashboard.expensesByCategory")}

-
+

{t("dashboard.budgetVsActual")}

diff --git a/src/services/budgetService.ts b/src/services/budgetService.ts index 8a59581..2ffbcc1 100644 --- a/src/services/budgetService.ts +++ b/src/services/budgetService.ts @@ -404,13 +404,66 @@ export async function getBudgetVsActualData( rows.push(parent); - // Sort: "(direct)" first, then subtotals with their children, then alphabetical leaves - allChildRows.sort((a, b) => { - if (a.category_id === cat.id && !a.is_parent) return -1; - if (b.category_id === cat.id && !b.is_parent) return 1; - return a.category_name.localeCompare(b.category_name); - }); - rows.push(...allChildRows); + // Sort: "(direct)" first, then keep level-2 groups (subtotal + children) together, + // sorted alphabetically by the subtotal name, with leaves sorted alphabetically too. + // Separate depth-1 leaves from depth-1 subtotal groups + const directRow = allChildRows.find((r) => r.category_id === cat.id && !r.is_parent); + const level2Groups: { subtotal: BudgetVsActualRow; children: BudgetVsActualRow[] }[] = []; + const level1Leaves: BudgetVsActualRow[] = []; + + for (const r of allChildRows) { + if (r.category_id === cat.id && !r.is_parent) continue; // skip "(direct)" — handled separately + if (r.is_parent && (r.depth ?? 0) === 1) { + // This is an intermediate parent subtotal — start a new group + level2Groups.push({ subtotal: r, children: [] }); + } else if ((r.depth ?? 0) === 2) { + // Find which group this belongs to (by parent_id) + const group = level2Groups.find((g) => g.subtotal.category_id === r.parent_id); + if (group) { + group.children.push(r); + } else { + level1Leaves.push(r); + } + } else { + level1Leaves.push(r); + } + } + + // Sort level-1 leaves alphabetically + level1Leaves.sort((a, b) => a.category_name.localeCompare(b.category_name)); + // Sort level-2 groups by subtotal name + level2Groups.sort((a, b) => a.subtotal.category_name.localeCompare(b.subtotal.category_name)); + // Sort children within each group + for (const g of level2Groups) { + g.children.sort((a, b) => { + // "(direct)" row first + if (a.category_id === g.subtotal.category_id) return -1; + if (b.category_id === g.subtotal.category_id) return 1; + return a.category_name.localeCompare(b.category_name); + }); + } + + // Reassemble: (direct) first, then interleave level-1 leaves and level-2 groups alphabetically + const sorted: BudgetVsActualRow[] = []; + if (directRow) sorted.push(directRow); + + // Merge level1Leaves and level2Groups by name + let li = 0; + let gi = 0; + while (li < level1Leaves.length || gi < level2Groups.length) { + const leafName = li < level1Leaves.length ? level1Leaves[li].category_name : null; + const groupName = gi < level2Groups.length ? level2Groups[gi].subtotal.category_name : null; + if (leafName !== null && (groupName === null || leafName.localeCompare(groupName) <= 0)) { + sorted.push(level1Leaves[li]); + li++; + } else { + const g = level2Groups[gi]; + sorted.push(g.subtotal, ...g.children); + gi++; + } + } + + rows.push(...sorted); } } -- 2.45.2