From 032886dc1cbbea7dd26b71ec25557e0201593499 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Sun, 30 Nov 2025 17:16:47 +0100 Subject: [PATCH] feat: add category breakdown by parent to statistics page and enhance pie chart with toggle for grouping options --- app/statistics/page.tsx | 40 +++- components/statistics/category-pie-chart.tsx | 208 ++++++++++++------- 2 files changed, 174 insertions(+), 74 deletions(-) diff --git a/app/statistics/page.tsx b/app/statistics/page.tsx index c8e8e69..b070277 100644 --- a/app/statistics/page.tsx +++ b/app/statistics/page.tsx @@ -242,8 +242,39 @@ export default function StatisticsPage() { icon: category?.icon || "HelpCircle", }; }) - .sort((a, b) => b.value - a.value) - .slice(0, 8); + .sort((a, b) => b.value - a.value); + + // Category breakdown grouped by parent (expenses only) + const categoryTotalsByParent = new Map(); + transactions + .filter((t) => t.amount < 0) + .forEach((t) => { + const category = data.categories.find((c) => c.id === t.categoryId); + // Use parent category ID if exists, otherwise use the category itself + let groupId: string; + if (!category) { + groupId = "uncategorized"; + } else if (category.parentId) { + groupId = category.parentId; + } else { + // Category is a parent itself + groupId = category.id; + } + const current = categoryTotalsByParent.get(groupId) || 0; + categoryTotalsByParent.set(groupId, current + Math.abs(t.amount)); + }); + + const categoryChartDataByParent = Array.from(categoryTotalsByParent.entries()) + .map(([groupId, total]) => { + const category = data.categories.find((c) => c.id === groupId); + return { + name: category?.name || "Non catégorisé", + value: Math.round(total), + color: category?.color || "#94a3b8", + icon: category?.icon || "HelpCircle", + }; + }) + .sort((a, b) => b.value - a.value); // Top expenses - deduplicate by ID and sort by amount (most negative first) const uniqueTransactions = Array.from( @@ -537,6 +568,7 @@ export default function StatisticsPage() { return { monthlyChartData, categoryChartData, + categoryChartDataByParent, topExpenses, totalIncome, totalExpenses, @@ -793,9 +825,11 @@ export default function StatisticsPage() { {/* Analyse par Catégorie */}

Analyse par Catégorie

-
+
string; title?: string; height?: number; @@ -31,6 +37,8 @@ interface CategoryPieChartProps { export function CategoryPieChart({ data, + dataByParent, + categories = [], formatCurrency, title = "Répartition par catégorie", height = 300, @@ -38,85 +46,143 @@ export function CategoryPieChart({ outerRadius = 100, emptyMessage = "Pas de données pour cette période", }: CategoryPieChartProps) { + const [groupByParent, setGroupByParent] = useState(true); + const [isExpanded, setIsExpanded] = useState(false); + const hasParentData = dataByParent && dataByParent.length > 0; + const baseData = (groupByParent && hasParentData) ? dataByParent : data; + + // Limit to top 8 by default, show all if expanded + const maxItems = 8; + const currentData = isExpanded ? baseData : baseData.slice(0, maxItems); + return ( - + {title} +
+ {hasParentData && ( + + )} + {baseData.length > maxItems && ( + + )} +
- {data.length > 0 ? ( -
- - - - {data.map((entry, index) => ( - - ))} - - { - if (!active || !payload?.length) return null; - const item = payload[0].payload as CategoryChartData; - return ( -
-
- - - {item.name} - -
-
- {formatCurrency(item.value)} -
-
- ); - }} - /> - ( -
- {payload?.map((entry, index) => { - const item = data.find((d) => d.name === entry.value); - return ( -
+ {currentData.length > 0 ? ( +
+ {/* Graphique */} +
+ + + + {currentData.map((entry, index) => ( + + ))} + + { + if (!active || !payload?.length) return null; + const item = payload[0].payload as CategoryChartData; + return ( +
+
- {entry.value} + + {item.name} +
- ); - })} -
- )} - /> -
-
+
+ {formatCurrency(item.value)} +
+
+ ); + }} + /> + + +
+ + {/* Légende */} +
+
+ Légende +
+
+ {currentData.map((item, index) => ( +
+ + + {item.name} + + + {formatCurrency(item.value)} + +
+ ))} +
+
) : (