feat: enhance statistics page with new charts and data visualizations including savings trend, category trends, and year-over-year comparisons

This commit is contained in:
Julien Froidefond
2025-11-30 17:05:03 +01:00
parent f366ea02c5
commit 00dd8fc335
7 changed files with 902 additions and 27 deletions

View File

@@ -8,6 +8,11 @@ import {
CategoryPieChart,
BalanceLineChart,
TopExpensesList,
CategoryBarChart,
CategoryTrendChart,
SavingsTrendChart,
IncomeExpenseTrendChart,
YearOverYearChart,
} from "@/components/statistics";
import { useBankingData } from "@/lib/hooks";
import { getAccountBalance } from "@/lib/account-utils";
@@ -409,6 +414,126 @@ export default function StatisticsPage() {
return point;
});
// Savings trend data
const savingsTrendData = monthlyChartData.map((month) => ({
month: month.month,
savings: month.solde,
cumulative: 0, // Will be calculated if needed
}));
// Category trend data (monthly breakdown by category)
const categoryTrendByMonth = new Map<string, Map<string, number>>();
transactions
.filter((t) => t.amount < 0)
.forEach((t) => {
const monthKey = t.date.substring(0, 7);
const catId = t.categoryId || "uncategorized";
if (!categoryTrendByMonth.has(monthKey)) {
categoryTrendByMonth.set(monthKey, new Map());
}
const monthCategories = categoryTrendByMonth.get(monthKey)!;
const current = monthCategories.get(catId) || 0;
monthCategories.set(catId, current + Math.abs(t.amount));
});
const allCategoryMonths = Array.from(categoryTrendByMonth.keys()).sort();
const categoryTrendData = allCategoryMonths.map((monthKey) => {
const point: { month: string; [key: string]: string | number } = {
month: new Date(monthKey + "-01").toLocaleDateString("fr-FR", {
month: "short",
year: "numeric",
}),
};
const monthCategories = categoryTrendByMonth.get(monthKey)!;
monthCategories.forEach((total, catId) => {
point[catId] = Math.round(total);
});
return point;
});
// Category trend data grouped by parent category
const categoryTrendByMonthByParent = new Map<string, Map<string, number>>();
transactions
.filter((t) => t.amount < 0)
.forEach((t) => {
const monthKey = t.date.substring(0, 7);
const category = data.categories.find((c) => c.id === t.categoryId);
// Use parent category ID if category has a parent, otherwise use the category itself
// If category is null (uncategorized), use "uncategorized"
let groupId: string;
if (!category) {
groupId = "uncategorized";
} else if (category.parentId) {
groupId = category.parentId;
} else {
// Category is a parent itself
groupId = category.id;
}
if (!categoryTrendByMonthByParent.has(monthKey)) {
categoryTrendByMonthByParent.set(monthKey, new Map());
}
const monthCategories = categoryTrendByMonthByParent.get(monthKey)!;
const current = monthCategories.get(groupId) || 0;
monthCategories.set(groupId, current + Math.abs(t.amount));
});
// Use the same months as categoryTrendData
const categoryTrendDataByParent = allCategoryMonths.map((monthKey) => {
const point: { month: string; [key: string]: string | number } = {
month: new Date(monthKey + "-01").toLocaleDateString("fr-FR", {
month: "short",
year: "numeric",
}),
};
const monthCategories = categoryTrendByMonthByParent.get(monthKey);
if (monthCategories) {
monthCategories.forEach((total, groupId) => {
point[groupId] = Math.round(total);
});
}
return point;
});
// Year over year comparison (if we have at least 12 months)
const currentYear = new Date().getFullYear();
const previousYear = currentYear - 1;
const yearOverYearData: Array<{
month: string;
current: number;
previous: number;
label: string;
}> = [];
if (allCategoryMonths.length >= 12) {
const currentYearData = new Map<string, number>();
const previousYearData = new Map<string, number>();
monthlyData.forEach((values, monthKey) => {
const date = new Date(monthKey + "-01");
const monthName = date.toLocaleDateString("fr-FR", { month: "short" });
const year = date.getFullYear();
if (year === currentYear) {
currentYearData.set(monthName, values.expenses);
} else if (year === previousYear) {
previousYearData.set(monthName, values.expenses);
}
});
// Get all months from current year
const months = Array.from(currentYearData.keys());
months.forEach((month) => {
yearOverYearData.push({
month,
current: currentYearData.get(month) || 0,
previous: previousYearData.get(month) || 0,
label: month,
});
});
}
return {
monthlyChartData,
categoryChartData,
@@ -419,6 +544,10 @@ export default function StatisticsPage() {
aggregatedBalanceData,
perAccountBalanceData,
transactionCount: transactions.length,
savingsTrendData,
categoryTrendData,
categoryTrendDataByParent,
yearOverYearData,
};
}, [data, startDate, endDate, selectedAccounts, selectedCategories, excludeInternalTransfers, internalTransferCategory]);
@@ -609,34 +738,88 @@ export default function StatisticsPage() {
</CardContent>
</Card>
<StatsSummaryCards
totalIncome={stats.totalIncome}
totalExpenses={stats.totalExpenses}
avgMonthlyExpenses={stats.avgMonthlyExpenses}
formatCurrency={formatCurrency}
/>
{/* Vue d'ensemble */}
<section className="mb-8">
<h2 className="text-2xl font-semibold mb-4">Vue d'ensemble</h2>
<StatsSummaryCards
totalIncome={stats.totalIncome}
totalExpenses={stats.totalExpenses}
avgMonthlyExpenses={stats.avgMonthlyExpenses}
formatCurrency={formatCurrency}
/>
<div className="mt-6">
<BalanceLineChart
aggregatedData={stats.aggregatedBalanceData}
perAccountData={stats.perAccountBalanceData}
accounts={data.accounts}
formatCurrency={formatCurrency}
/>
</div>
<div className="mt-6">
<SavingsTrendChart
data={stats.savingsTrendData}
formatCurrency={formatCurrency}
/>
</div>
</section>
{/* Revenus et Dépenses */}
<section className="mb-8">
<h2 className="text-2xl font-semibold mb-4">Revenus et Dépenses</h2>
<div className="grid gap-6 lg:grid-cols-2">
<MonthlyChart
data={stats.monthlyChartData}
formatCurrency={formatCurrency}
/>
<IncomeExpenseTrendChart
data={stats.monthlyChartData.map((m) => ({
month: m.month,
revenus: m.revenus,
depenses: m.depenses,
}))}
formatCurrency={formatCurrency}
/>
</div>
{stats.yearOverYearData.length > 0 && (
<div className="mt-6">
<YearOverYearChart
data={stats.yearOverYearData}
formatCurrency={formatCurrency}
/>
</div>
)}
</section>
{/* Analyse par Catégorie */}
<section className="mb-8">
<h2 className="text-2xl font-semibold mb-4">Analyse par Catégorie</h2>
<div className="grid gap-6 lg:grid-cols-2">
<CategoryPieChart
data={stats.categoryChartData}
formatCurrency={formatCurrency}
/>
<CategoryBarChart
data={stats.categoryChartData}
formatCurrency={formatCurrency}
/>
</div>
<div className="mt-6">
<CategoryTrendChart
data={stats.categoryTrendData}
dataByParent={stats.categoryTrendDataByParent}
categories={data.categories}
formatCurrency={formatCurrency}
/>
</div>
<div className="mt-6">
<TopExpensesList
expenses={stats.topExpenses}
categories={data.categories}
formatCurrency={formatCurrency}
/>
</div>
</section>
<div className="grid gap-6 lg:grid-cols-2">
<MonthlyChart
data={stats.monthlyChartData}
formatCurrency={formatCurrency}
/>
<CategoryPieChart
data={stats.categoryChartData}
formatCurrency={formatCurrency}
/>
<BalanceLineChart
aggregatedData={stats.aggregatedBalanceData}
perAccountData={stats.perAccountBalanceData}
accounts={data.accounts}
formatCurrency={formatCurrency}
/>
<TopExpensesList
expenses={stats.topExpenses}
categories={data.categories}
formatCurrency={formatCurrency}
/>
</div>
</PageLayout>
);
}