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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user