Files
fintrack/app/statistics/page.tsx

234 lines
6.9 KiB
TypeScript

"use client";
import { useState, useMemo } from "react";
import { PageLayout, LoadingState, PageHeader } from "@/components/layout";
import {
StatsSummaryCards,
MonthlyChart,
CategoryPieChart,
BalanceLineChart,
TopExpensesList,
} from "@/components/statistics";
import { useBankingData } from "@/lib/hooks";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
type Period = "3months" | "6months" | "12months" | "all";
export default function StatisticsPage() {
const { data, isLoading } = useBankingData();
const [period, setPeriod] = useState<Period>("6months");
const [selectedAccount, setSelectedAccount] = useState<string>("all");
const stats = useMemo(() => {
if (!data) return null;
const now = new Date();
let startDate: Date;
switch (period) {
case "3months":
startDate = new Date(now.getFullYear(), now.getMonth() - 3, 1);
break;
case "6months":
startDate = new Date(now.getFullYear(), now.getMonth() - 6, 1);
break;
case "12months":
startDate = new Date(now.getFullYear(), now.getMonth() - 12, 1);
break;
default:
startDate = new Date(0);
}
let transactions = data.transactions.filter(
(t) => new Date(t.date) >= startDate
);
if (selectedAccount !== "all") {
transactions = transactions.filter(
(t) => t.accountId === selectedAccount
);
}
// Monthly breakdown
const monthlyData = new Map<string, { income: number; expenses: number }>();
transactions.forEach((t) => {
const monthKey = t.date.substring(0, 7);
const current = monthlyData.get(monthKey) || { income: 0, expenses: 0 };
if (t.amount >= 0) {
current.income += t.amount;
} else {
current.expenses += Math.abs(t.amount);
}
monthlyData.set(monthKey, current);
});
const monthlyChartData = Array.from(monthlyData.entries())
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([month, values]) => ({
month: new Date(month + "-01").toLocaleDateString("fr-FR", {
month: "short",
year: "2-digit",
}),
revenus: Math.round(values.income),
depenses: Math.round(values.expenses),
solde: Math.round(values.income - values.expenses),
}));
// Category breakdown (expenses only)
const categoryTotals = new Map<string, number>();
transactions
.filter((t) => t.amount < 0)
.forEach((t) => {
const catId = t.categoryId || "uncategorized";
const current = categoryTotals.get(catId) || 0;
categoryTotals.set(catId, current + Math.abs(t.amount));
});
const categoryChartData = Array.from(categoryTotals.entries())
.map(([categoryId, total]) => {
const category = data.categories.find((c) => c.id === categoryId);
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)
.slice(0, 8);
// Top expenses
const topExpenses = transactions
.filter((t) => t.amount < 0)
.sort((a, b) => a.amount - b.amount)
.slice(0, 5);
// Summary
const totalIncome = transactions
.filter((t) => t.amount >= 0)
.reduce((sum, t) => sum + t.amount, 0);
const totalExpenses = transactions
.filter((t) => t.amount < 0)
.reduce((sum, t) => sum + Math.abs(t.amount), 0);
const avgMonthlyExpenses =
monthlyData.size > 0 ? totalExpenses / monthlyData.size : 0;
// Balance evolution
const sortedTransactions = [...transactions].sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
);
let runningBalance = 0;
const balanceByDate = new Map<string, number>();
sortedTransactions.forEach((t) => {
runningBalance += t.amount;
balanceByDate.set(t.date, runningBalance);
});
const balanceChartData = Array.from(balanceByDate.entries()).map(
([date, balance]) => ({
date: new Date(date).toLocaleDateString("fr-FR", {
day: "2-digit",
month: "short",
}),
solde: Math.round(balance),
})
);
return {
monthlyChartData,
categoryChartData,
topExpenses,
totalIncome,
totalExpenses,
avgMonthlyExpenses,
balanceChartData,
transactionCount: transactions.length,
};
}, [data, period, selectedAccount]);
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("fr-FR", {
style: "currency",
currency: "EUR",
}).format(amount);
};
if (isLoading || !data || !stats) {
return <LoadingState />;
}
return (
<PageLayout>
<PageHeader
title="Statistiques"
description="Analysez vos dépenses et revenus"
actions={
<>
<Select value={selectedAccount} onValueChange={setSelectedAccount}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Compte" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Tous les comptes</SelectItem>
{data.accounts.map((account) => (
<SelectItem key={account.id} value={account.id}>
{account.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={period}
onValueChange={(v) => setPeriod(v as Period)}
>
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="Période" />
</SelectTrigger>
<SelectContent>
<SelectItem value="3months">3 mois</SelectItem>
<SelectItem value="6months">6 mois</SelectItem>
<SelectItem value="12months">12 mois</SelectItem>
<SelectItem value="all">Tout</SelectItem>
</SelectContent>
</Select>
</>
}
/>
<StatsSummaryCards
totalIncome={stats.totalIncome}
totalExpenses={stats.totalExpenses}
avgMonthlyExpenses={stats.avgMonthlyExpenses}
formatCurrency={formatCurrency}
/>
<div className="grid gap-6 lg:grid-cols-2">
<MonthlyChart
data={stats.monthlyChartData}
formatCurrency={formatCurrency}
/>
<CategoryPieChart
data={stats.categoryChartData}
formatCurrency={formatCurrency}
/>
<BalanceLineChart
data={stats.balanceChartData}
formatCurrency={formatCurrency}
/>
<TopExpensesList
expenses={stats.topExpenses}
categories={data.categories}
formatCurrency={formatCurrency}
/>
</div>
</PageLayout>
);
}