feat: implement top expenses categorization and enhance UI with tabs; display top 10 expenses per top 5 parent categories, improving data organization and user navigation in the statistics page

This commit is contained in:
Julien Froidefond
2025-12-23 11:07:15 +01:00
parent e0597b0dcb
commit 407486a109
2 changed files with 170 additions and 69 deletions

View File

@@ -360,22 +360,53 @@ export default function StatisticsPage() {
})
.sort((a, b) => b.value - a.value);
// Top expenses - deduplicate by ID and sort by amount (most negative first)
// Top expenses by top parent categories - deduplicate by ID
const uniqueTransactions = Array.from(
new Map(transactions.map((t) => [t.id, t])).values(),
);
const topExpenses = uniqueTransactions
.filter((t) => t.amount < 0)
.sort((a, b) => {
// Sort by amount (most negative first)
if (a.amount !== b.amount) {
return a.amount - b.amount;
}
// If same amount, sort by date (most recent first) for stable sorting
return new Date(b.date).getTime() - new Date(a.date).getTime();
})
const expenses = uniqueTransactions.filter((t) => t.amount < 0);
// Get top 5 parent categories by total expenses
const topParentCategories = Array.from(categoryTotalsByParent.entries())
.map(([groupId, total]) => ({
groupId: groupId === "uncategorized" ? null : groupId,
total,
}))
.sort((a, b) => b.total - a.total)
.slice(0, 5);
// Get top 10 expenses per top parent category (from all its subcategories)
const topExpensesByCategory = topParentCategories.map(({ groupId }) => {
const categoryExpenses = expenses
.filter((t) => {
if (groupId === null) {
return !t.categoryId;
}
// Check if transaction belongs to this parent category or its subcategories
const category = data.categories.find((c) => c.id === t.categoryId);
if (!category) {
return false;
}
// Use parent category ID if exists, otherwise use the category itself
const transactionGroupId = category.parentId || category.id;
return transactionGroupId === groupId;
})
.sort((a, b) => {
// Sort by amount (most negative first)
if (a.amount !== b.amount) {
return a.amount - b.amount;
}
// If same amount, sort by date (most recent first) for stable sorting
return new Date(b.date).getTime() - new Date(a.date).getTime();
})
.slice(0, 10);
return {
categoryId: groupId,
expenses: categoryExpenses,
};
});
// Summary
const totalIncome = transactions
.filter((t) => t.amount >= 0)
@@ -653,7 +684,7 @@ export default function StatisticsPage() {
monthlyChartData,
categoryChartData,
categoryChartDataByParent,
topExpenses,
topExpensesByCategory,
totalIncome,
totalExpenses,
avgMonthlyExpenses,
@@ -1200,7 +1231,7 @@ export default function StatisticsPage() {
</div>
<div className="mt-4 md:mt-6">
<TopExpensesList
expenses={stats.topExpenses}
expensesByCategory={stats.topExpensesByCategory}
categories={data.categories}
formatCurrency={formatCurrency}
/>