"use client"; import { useState, useMemo, useEffect } from "react"; 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"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { useIsMobile } from "@/hooks/use-mobile"; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, } from "recharts"; import { List, ListOrdered } from "lucide-react"; import type { Transaction, Category } from "@/lib/types"; interface TopExpensesListProps { expensesByCategory: Array<{ categoryId: string | null; expenses: Transaction[]; }>; categories: Category[]; formatCurrency: (amount: number) => string; allTransactions?: Transaction[]; // Toutes les transactions filtrées pour calculer toutes les dépenses } export function TopExpensesList({ expensesByCategory, categories, formatCurrency, allTransactions = [], }: TopExpensesListProps) { const isMobile = useIsMobile(); const [showAllExpenses, setShowAllExpenses] = useState(false); // Filtrer les catégories qui ont des dépenses const categoriesWithExpenses = expensesByCategory.filter( (group) => group.expenses.length > 0, ); const hasExpenses = categoriesWithExpenses.length > 0; // Déterminer la valeur par défaut du premier onglet const defaultTabValue = categoriesWithExpenses.length > 0 ? categoriesWithExpenses[0].categoryId || "uncategorized" : ""; const [activeTab, setActiveTab] = useState(() => defaultTabValue); // Mettre à jour activeTab quand defaultTabValue change ou si activeTab est invalide useEffect(() => { if (!defaultTabValue) return; // Vérifier si activeTab correspond à une catégorie valide const isValidTab = categoriesWithExpenses.some( (group) => (group.categoryId || "uncategorized") === activeTab, ); // Si activeTab est vide ou invalide, utiliser defaultTabValue if (!activeTab || !isValidTab) { setActiveTab(defaultTabValue); } }, [defaultTabValue, categoriesWithExpenses, activeTab]); // Calculer les données du graphique pour la catégorie active const chartData = useMemo(() => { if (!hasExpenses) return []; // Utiliser activeTab ou defaultTabValue comme fallback const currentTab = activeTab || defaultTabValue; if (!currentTab) return []; const activeCategoryGroup = categoriesWithExpenses.find( (group) => (group.categoryId || "uncategorized") === currentTab, ); if (!activeCategoryGroup) { return []; } // Si showAllExpenses est activé et qu'on a toutes les transactions, utiliser toutes les dépenses de la catégorie let expenses: Transaction[]; if (showAllExpenses && allTransactions.length > 0) { // Filtrer toutes les transactions pour obtenir toutes les dépenses de cette catégorie parente const categoryId = activeCategoryGroup.categoryId; expenses = allTransactions .filter((t) => t.amount < 0) // Seulement les dépenses .filter((t) => { if (categoryId === null) { return !t.categoryId; } // Vérifier si la transaction appartient à cette catégorie parente ou ses sous-catégories const category = categories.find((c) => c.id === t.categoryId); if (!category) { return false; } const transactionGroupId = category.parentId || category.id; return transactionGroupId === categoryId; }); } else { // Utiliser seulement les top 10 expenses = activeCategoryGroup.expenses; } if (expenses.length === 0) { return []; } // Grouper les dépenses par période // Si moins de 30 jours, groupe par jour // Si moins de 6 mois, groupe par semaine // Sinon groupe par mois const sortedExpenses = [...expenses].sort( (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(), ); if (sortedExpenses.length === 0) return []; const firstDate = new Date(sortedExpenses[0].date); const lastDate = new Date( sortedExpenses[sortedExpenses.length - 1].date, ); const daysDiff = (lastDate.getTime() - firstDate.getTime()) / (1000 * 60 * 60 * 24); let groupBy: "day" | "week" | "month"; if (daysDiff <= 30) { groupBy = "day"; } else if (daysDiff <= 180) { groupBy = "week"; } else { groupBy = "month"; } // Fonction helper pour obtenir la clé de période d'une date const getPeriodKey = (date: Date): { key: string; dateKey: string } => { if (groupBy === "day") { return { key: date.toLocaleDateString("fr-FR", { day: "2-digit", month: "short", }), dateKey: date.toISOString().substring(0, 10), // YYYY-MM-DD }; } else if (groupBy === "week") { // Semaine commençant le lundi const weekStart = new Date(date); const day = weekStart.getDay(); const diff = weekStart.getDate() - day + (day === 0 ? -6 : 1); weekStart.setDate(diff); return { key: `Sem. ${weekStart.toLocaleDateString("fr-FR", { day: "2-digit", month: "short", })}`, dateKey: weekStart.toISOString().substring(0, 10), // YYYY-MM-DD }; } else { return { key: date.toLocaleDateString("fr-FR", { month: "short", year: "numeric", }), dateKey: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`, // YYYY-MM }; } }; // Grouper les dépenses par période const groupedData = new Map< string, { total: number; dateKey: string } >(); sortedExpenses.forEach((expense) => { const date = new Date(expense.date); const { key, dateKey } = getPeriodKey(date); const current = groupedData.get(key); if (current) { current.total += Math.abs(expense.amount); } else { groupedData.set(key, { total: Math.abs(expense.amount), dateKey, }); } }); // Générer toutes les périodes entre la première et la dernière date const allPeriodsMap = new Map< string, { period: string; montant: number; dateKey: string } >(); const currentDate = new Date(firstDate); const endDate = new Date(lastDate); // Normaliser les dates selon le type de groupement if (groupBy === "day") { currentDate.setHours(0, 0, 0, 0); endDate.setHours(23, 59, 59, 999); } else if (groupBy === "week") { // Début de la semaine de la première date const day = currentDate.getDay(); const diff = currentDate.getDate() - day + (day === 0 ? -6 : 1); currentDate.setDate(diff); currentDate.setHours(0, 0, 0, 0); // Début de la semaine de la dernière date (pour la boucle) const lastDay = endDate.getDay(); const lastDiff = endDate.getDate() - lastDay + (lastDay === 0 ? -6 : 1); endDate.setDate(lastDiff); endDate.setHours(0, 0, 0, 0); } else { // Mois : premier jour du mois currentDate.setDate(1); currentDate.setHours(0, 0, 0, 0); endDate.setDate(1); endDate.setHours(0, 0, 0, 0); } while (currentDate <= endDate) { const { key, dateKey } = getPeriodKey(currentDate); const existingData = groupedData.get(key); // Utiliser dateKey comme clé unique pour éviter les doublons if (!allPeriodsMap.has(dateKey)) { allPeriodsMap.set(dateKey, { period: key, montant: existingData ? Math.round(existingData.total) : 0, dateKey, }); } // Passer à la période suivante if (groupBy === "day") { currentDate.setDate(currentDate.getDate() + 1); } else if (groupBy === "week") { currentDate.setDate(currentDate.getDate() + 7); } else { // Mois suivant currentDate.setMonth(currentDate.getMonth() + 1); } } return Array.from(allPeriodsMap.values()) .sort((a, b) => a.dateKey.localeCompare(b.dateKey), ); }, [ hasExpenses, categoriesWithExpenses, activeTab, defaultTabValue, showAllExpenses, allTransactions, categories, ]); return ( Top 10 dépenses par top 5 catégories parentes {hasExpenses ? ( {categoriesWithExpenses.map(({ categoryId, expenses }) => { const category = categoryId ? categories.find((c) => c.id === categoryId) : null; const tabValue = categoryId || "uncategorized"; return ( {category && ( )} {category?.name || "Non catégorisé"} ); })} {categoriesWithExpenses.map(({ categoryId, expenses }) => { const category = categoryId ? categories.find((c) => c.id === categoryId) : null; const tabValue = categoryId || "uncategorized"; return (
{expenses.map((expense, index) => (
{index + 1}

{expense.description}

{formatCurrency(expense.amount)}
{new Date(expense.date).toLocaleDateString( "fr-FR", )} {expense.categoryId && (() => { const expenseCategory = categories.find( (c) => c.id === expense.categoryId, ); // Afficher seulement si c'est une sous-catégorie (a un parentId) if (expenseCategory?.parentId) { return ( {expenseCategory.name} ); } return null; })()}
))}
); })} {/* Graphique d'évolution temporelle */} {chartData.length > 0 && (() => { const currentTab = activeTab || defaultTabValue; const activeCategoryGroup = categoriesWithExpenses.find( (group) => (group.categoryId || "uncategorized") === currentTab, ); const activeCategory = activeCategoryGroup?.categoryId ? categories.find((c) => c.id === activeCategoryGroup.categoryId) : null; const barColor = activeCategory?.color || "var(--destructive)"; return (

Évolution des dépenses dans le temps

{allTransactions.length > 0 && ( )}
{ if (Math.abs(v) >= 1000) { return `${(v / 1000).toFixed(1)}k€`; } return `${Math.round(v)}€`; }} tick={{ fill: "var(--muted-foreground)" }} /> formatCurrency(value)} contentStyle={{ backgroundColor: "var(--card)", border: "1px solid var(--border)", borderRadius: "8px", }} />
); })()}
) : (
Pas de dépenses pour cette période
)}
); }