From 4f7a80de1c7e66c28192c827c6fffb5c4ac9726b Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Sat, 20 Dec 2025 11:07:35 +0100 Subject: [PATCH] feat: implement clickable category links in charts and lists; handle uncategorized transactions in URL parameters --- app/statistics/page.tsx | 2 + components/statistics/category-bar-chart.tsx | 49 ++++++++++++++++++-- components/statistics/category-pie-chart.tsx | 48 +++++++++++-------- components/statistics/top-expenses-list.tsx | 40 +++++++++------- hooks/use-transactions-page.ts | 15 ++++++ 5 files changed, 113 insertions(+), 41 deletions(-) diff --git a/app/statistics/page.tsx b/app/statistics/page.tsx index 0e741fb..83f01a9 100644 --- a/app/statistics/page.tsx +++ b/app/statistics/page.tsx @@ -281,6 +281,7 @@ export default function StatisticsPage() { value: Math.round(total), color: category?.color || "#94a3b8", icon: category?.icon || "HelpCircle", + categoryId: categoryId === "uncategorized" ? null : categoryId, }; }) .sort((a, b) => b.value - a.value); @@ -315,6 +316,7 @@ export default function StatisticsPage() { value: Math.round(total), color: category?.color || "#94a3b8", icon: category?.icon || "HelpCircle", + categoryId: groupId === "uncategorized" ? null : groupId, }; }) .sort((a, b) => b.value - a.value); diff --git a/components/statistics/category-bar-chart.tsx b/components/statistics/category-bar-chart.tsx index 0631138..031f0ee 100644 --- a/components/statistics/category-bar-chart.tsx +++ b/components/statistics/category-bar-chart.tsx @@ -1,5 +1,6 @@ "use client"; +import Link from "next/link"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { CategoryIcon } from "@/components/ui/category-icon"; import { @@ -29,6 +30,48 @@ export function CategoryBarChart({ }: CategoryBarChartProps) { const displayData = data.slice(0, maxItems).reverse(); // Reverse pour avoir le plus grand en haut + // Custom tick component for clickable labels + const CustomYAxisTick = ({ x, y, payload }: any) => { + const categoryName = payload.value; + const item = displayData.find((d) => d.name === categoryName); + + if (!item) { + return ( + + {categoryName} + + ); + } + + const href = + item.categoryId === null || item.categoryId === undefined + ? "/transactions?includeUncategorized=true" + : `/transactions?categoryIds=${item.categoryId}`; + + return ( + + + {categoryName} + + + ); + }; + return ( @@ -60,11 +103,7 @@ export function CategoryBarChart({ dataKey="name" className="text-xs" width={90} - tick={{ fill: "var(--muted-foreground)" }} - tickFormatter={(value) => { - const item = displayData.find((d) => d.name === value); - return item ? value : ""; - }} + tick={CustomYAxisTick} /> { diff --git a/components/statistics/category-pie-chart.tsx b/components/statistics/category-pie-chart.tsx index a0914c3..8607b26 100644 --- a/components/statistics/category-pie-chart.tsx +++ b/components/statistics/category-pie-chart.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import Link from "next/link"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { CategoryIcon } from "@/components/ui/category-icon"; @@ -13,7 +14,8 @@ export interface CategoryChartData { value: number; color: string; icon: string; - [key: string]: string | number; + categoryId?: string | null; // null for "Non catégorisé" + [key: string]: string | number | null | undefined; } interface CategoryPieChartProps { @@ -162,24 +164,32 @@ export function CategoryPieChart({ Légende
- {currentData.map((item, index) => ( -
- - - {item.name} - - - {formatCurrency(item.value)} - -
- ))} + {currentData.map((item, index) => { + const href = + item.categoryId === null || item.categoryId === undefined + ? "/transactions?includeUncategorized=true" + : `/transactions?categoryIds=${item.categoryId}`; + + return ( + + + + {item.name} + + + {formatCurrency(item.value)} + + + ); + })}
diff --git a/components/statistics/top-expenses-list.tsx b/components/statistics/top-expenses-list.tsx index 4bd9a40..0174fef 100644 --- a/components/statistics/top-expenses-list.tsx +++ b/components/statistics/top-expenses-list.tsx @@ -1,5 +1,6 @@ "use client"; +import Link from "next/link"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { CategoryIcon } from "@/components/ui/category-icon"; import { Badge } from "@/components/ui/badge"; @@ -53,24 +54,29 @@ export function TopExpensesList({ {new Date(expense.date).toLocaleDateString("fr-FR")} {category && ( - - - - {category.name} - - + + + + {category.name} + + + )} diff --git a/hooks/use-transactions-page.ts b/hooks/use-transactions-page.ts index a4bf54f..e28551c 100644 --- a/hooks/use-transactions-page.ts +++ b/hooks/use-transactions-page.ts @@ -69,6 +69,21 @@ export function useTransactionsPage() { } }, [searchParams]); + // Handle categoryIds and includeUncategorized from URL params + useEffect(() => { + const categoryIdsParam = searchParams.get("categoryIds"); + const includeUncategorizedParam = searchParams.get("includeUncategorized"); + + if (categoryIdsParam) { + const categoryIds = categoryIdsParam.split(","); + setSelectedCategories(categoryIds); + setPage(0); + } else if (includeUncategorizedParam === "true") { + setSelectedCategories(["uncategorized"]); + setPage(0); + } + }, [searchParams]); + // Calculate start date based on period const startDate = useMemo(() => { const now = new Date();