162 lines
6.9 KiB
TypeScript
162 lines
6.9 KiB
TypeScript
"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";
|
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
|
import { useIsMobile } from "@/hooks/use-mobile";
|
|
import type { Transaction, Category } from "@/lib/types";
|
|
|
|
interface TopExpensesListProps {
|
|
expensesByCategory: Array<{
|
|
categoryId: string | null;
|
|
expenses: Transaction[];
|
|
}>;
|
|
categories: Category[];
|
|
formatCurrency: (amount: number) => string;
|
|
}
|
|
|
|
export function TopExpensesList({
|
|
expensesByCategory,
|
|
categories,
|
|
formatCurrency,
|
|
}: TopExpensesListProps) {
|
|
const isMobile = useIsMobile();
|
|
|
|
// 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"
|
|
: "";
|
|
|
|
return (
|
|
<Card className="card-hover">
|
|
<CardHeader>
|
|
<CardTitle className="text-sm md:text-base">
|
|
Top 10 dépenses par top 5 catégories parentes
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{hasExpenses ? (
|
|
<Tabs defaultValue={defaultTabValue} className="w-full">
|
|
<TabsList className="w-full flex-wrap h-auto p-1 mb-4">
|
|
{categoriesWithExpenses.map(({ categoryId, expenses }) => {
|
|
const category = categoryId
|
|
? categories.find((c) => c.id === categoryId)
|
|
: null;
|
|
const tabValue = categoryId || "uncategorized";
|
|
|
|
return (
|
|
<TabsTrigger
|
|
key={tabValue}
|
|
value={tabValue}
|
|
className="flex items-center gap-1.5 md:gap-2 text-xs md:text-sm"
|
|
>
|
|
{category && (
|
|
<CategoryIcon
|
|
icon={category.icon}
|
|
color={category.color}
|
|
size={isMobile ? 12 : 14}
|
|
/>
|
|
)}
|
|
<span className="truncate max-w-[100px] md:max-w-none">
|
|
{category?.name || "Non catégorisé"}
|
|
</span>
|
|
</TabsTrigger>
|
|
);
|
|
})}
|
|
</TabsList>
|
|
{categoriesWithExpenses.map(({ categoryId, expenses }) => {
|
|
const category = categoryId
|
|
? categories.find((c) => c.id === categoryId)
|
|
: null;
|
|
const tabValue = categoryId || "uncategorized";
|
|
|
|
return (
|
|
<TabsContent key={tabValue} value={tabValue}>
|
|
<div className="space-y-2 md:space-y-3">
|
|
{expenses.map((expense, index) => (
|
|
<div
|
|
key={expense.id}
|
|
className="flex items-start gap-2 md:gap-3"
|
|
>
|
|
<div className="w-5 h-5 md:w-6 md:h-6 rounded-full bg-muted flex items-center justify-center text-[10px] md:text-xs font-semibold shrink-0">
|
|
{index + 1}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-start justify-between gap-2 mb-1">
|
|
<p className="font-medium text-xs md:text-sm truncate flex-1">
|
|
{expense.description}
|
|
</p>
|
|
<div className="text-destructive font-semibold tabular-nums text-xs md:text-sm shrink-0">
|
|
{formatCurrency(expense.amount)}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-1.5 md:gap-2 flex-wrap">
|
|
<span className="text-[10px] md:text-xs text-muted-foreground">
|
|
{new Date(expense.date).toLocaleDateString(
|
|
"fr-FR",
|
|
)}
|
|
</span>
|
|
{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 (
|
|
<Link
|
|
href={`/transactions?categoryIds=${expenseCategory.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 hover:opacity-80 transition-opacity cursor-pointer"
|
|
style={{
|
|
backgroundColor: `${expenseCategory.color}20`,
|
|
color: expenseCategory.color,
|
|
borderColor: `${expenseCategory.color}30`,
|
|
}}
|
|
>
|
|
<CategoryIcon
|
|
icon={expenseCategory.icon}
|
|
color={expenseCategory.color}
|
|
size={isMobile ? 8 : 10}
|
|
/>
|
|
<span className="truncate max-w-[100px] md:max-w-none">
|
|
{expenseCategory.name}
|
|
</span>
|
|
</Badge>
|
|
</Link>
|
|
);
|
|
}
|
|
return null;
|
|
})()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</TabsContent>
|
|
);
|
|
})}
|
|
</Tabs>
|
|
) : (
|
|
<div className="h-[200px] flex items-center justify-center text-muted-foreground text-xs md:text-sm">
|
|
Pas de dépenses pour cette période
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|