feat: implement clickable category links in charts and lists; handle uncategorized transactions in URL parameters
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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 (
|
||||
<text
|
||||
x={x}
|
||||
y={y}
|
||||
fill="var(--muted-foreground)"
|
||||
fontSize={12}
|
||||
textAnchor="end"
|
||||
dominantBaseline="middle"
|
||||
>
|
||||
{categoryName}
|
||||
</text>
|
||||
);
|
||||
}
|
||||
|
||||
const href =
|
||||
item.categoryId === null || item.categoryId === undefined
|
||||
? "/transactions?includeUncategorized=true"
|
||||
: `/transactions?categoryIds=${item.categoryId}`;
|
||||
|
||||
return (
|
||||
<Link href={href}>
|
||||
<text
|
||||
x={x}
|
||||
y={y}
|
||||
fill="var(--muted-foreground)"
|
||||
fontSize={12}
|
||||
className="hover:opacity-80 transition-opacity cursor-pointer"
|
||||
textAnchor="end"
|
||||
dominantBaseline="middle"
|
||||
>
|
||||
{categoryName}
|
||||
</text>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -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}
|
||||
/>
|
||||
<Tooltip
|
||||
content={({ active, payload }) => {
|
||||
|
||||
@@ -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,10 +164,17 @@ export function CategoryPieChart({
|
||||
Légende
|
||||
</div>
|
||||
<div className="max-h-[300px] overflow-y-auto overflow-x-hidden pr-2 space-y-2">
|
||||
{currentData.map((item, index) => (
|
||||
<div
|
||||
{currentData.map((item, index) => {
|
||||
const href =
|
||||
item.categoryId === null || item.categoryId === undefined
|
||||
? "/transactions?includeUncategorized=true"
|
||||
: `/transactions?categoryIds=${item.categoryId}`;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={`legend-${index}`}
|
||||
className="flex items-center gap-2 text-sm"
|
||||
href={href}
|
||||
className="flex items-center gap-2 text-sm hover:opacity-80 transition-opacity cursor-pointer"
|
||||
>
|
||||
<CategoryIcon
|
||||
icon={item.icon}
|
||||
@@ -178,8 +187,9 @@ export function CategoryPieChart({
|
||||
<span className="text-foreground font-semibold tabular-nums whitespace-nowrap">
|
||||
{formatCurrency(item.value)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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,9 +54,13 @@ export function TopExpensesList({
|
||||
{new Date(expense.date).toLocaleDateString("fr-FR")}
|
||||
</span>
|
||||
{category && (
|
||||
<Link
|
||||
href={`/transactions?categoryIds=${category.id}`}
|
||||
className="inline-block"
|
||||
>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-[10px] md:text-xs px-1.5 md:px-2 py-0.5 inline-flex items-center gap-1 shrink-0"
|
||||
className="text-[10px] md:text-xs px-1.5 md:px-2 py-0.5 inline-flex items-center gap-1 shrink-0 hover:opacity-80 transition-opacity cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: `${category.color}20`,
|
||||
color: category.color,
|
||||
@@ -71,6 +76,7 @@ export function TopExpensesList({
|
||||
{category.name}
|
||||
</span>
|
||||
</Badge>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user