add MVP monthly expenses graph
Some checks failed
CI / update (push) Failing after 5s

This commit is contained in:
2025-09-12 20:07:37 +02:00
parent e773a90f1d
commit db3de29e48
5 changed files with 437 additions and 2 deletions

View File

@@ -0,0 +1,145 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { Payment } from '../../../../models/Payment';
import { dbConnect } from '../../../../utils/db';
export const GET: RequestHandler = async ({ url, locals }) => {
const session = await locals.auth();
if (!session || !session.user?.nickname) {
return json({ error: 'Unauthorized' }, { status: 401 });
}
await dbConnect();
try {
// Get query parameters for date range (default to last 12 months)
const monthsBack = parseInt(url.searchParams.get('months') || '12');
const endDate = new Date();
const startDate = new Date();
startDate.setMonth(startDate.getMonth() - monthsBack);
// First, let's get all payments and see what we have
console.log('Searching for payments for user:', session.user.nickname);
console.log('Date range:', startDate.toISOString(), 'to', endDate.toISOString());
const totalPayments = await Payment.countDocuments();
const paymentsInRange = await Payment.countDocuments({
date: {
$gte: startDate,
$lte: endDate
}
});
const expensePayments = await Payment.countDocuments({
date: {
$gte: startDate,
$lte: endDate
},
category: { $ne: 'settlement' },
$or: [
{ paidBy: session.user.nickname },
{ createdBy: session.user.nickname }
]
});
console.log('Total payments:', totalPayments, 'In date range:', paymentsInRange, 'User expenses:', expensePayments);
// Aggregate payments by month and category
const pipeline = [
{
$match: {
date: {
$gte: startDate,
$lte: endDate
},
// Exclude settlements - only show actual expenses
category: { $ne: 'settlement' },
// Only include payments where current user is involved
$or: [
{ paidBy: session.user.nickname },
{ createdBy: session.user.nickname }
]
}
},
{
$addFields: {
// Extract year-month from date
yearMonth: {
$dateToString: {
format: '%Y-%m',
date: '$date'
}
}
}
},
{
$group: {
_id: {
yearMonth: '$yearMonth',
category: '$category'
},
totalAmount: { $sum: '$amount' },
count: { $sum: 1 }
}
},
{
$sort: {
'_id.yearMonth': 1,
'_id.category': 1
}
}
];
const results = await Payment.aggregate(pipeline);
console.log('Aggregation results:', results);
// Transform data into chart-friendly format
const monthsMap = new Map();
const categories = new Set();
// Initialize months
for (let i = 0; i < monthsBack; i++) {
const date = new Date();
date.setMonth(date.getMonth() - monthsBack + i + 1);
const yearMonth = date.toISOString().substring(0, 7);
monthsMap.set(yearMonth, {});
}
// Populate data
results.forEach((result: any) => {
const { yearMonth, category } = result._id;
const amount = result.totalAmount;
categories.add(category);
if (!monthsMap.has(yearMonth)) {
monthsMap.set(yearMonth, {});
}
monthsMap.get(yearMonth)[category] = amount;
});
// Convert to arrays for Chart.js
const months = Array.from(monthsMap.keys()).sort();
const categoryList = Array.from(categories).sort();
const datasets = categoryList.map((category: string) => ({
label: category,
data: months.map(month => monthsMap.get(month)[category] || 0)
}));
return json({
labels: months.map(month => {
const [year, monthNum] = month.split('-');
const date = new Date(parseInt(year), parseInt(monthNum) - 1);
return date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
}),
datasets,
categories: categoryList
});
} catch (error) {
console.error('Error fetching monthly expenses:', error);
return json({ error: 'Failed to fetch monthly expenses' }, { status: 500 });
}
};

View File

@@ -5,6 +5,7 @@
import ProfilePicture from '$lib/components/ProfilePicture.svelte';
import EnhancedBalance from '$lib/components/EnhancedBalance.svelte';
import DebtBreakdown from '$lib/components/DebtBreakdown.svelte';
import BarChart from '$lib/components/BarChart.svelte';
import { getCategoryEmoji, getCategoryName } from '$lib/utils/categories';
import { isSettlementPayment, getSettlementIcon, getSettlementClasses, getSettlementReceiver } from '$lib/utils/settlements';
import AddButton from '$lib/components/AddButton.svelte';
@@ -18,6 +19,8 @@
};
let loading = false; // Start as false since we have server data
let error = null;
let monthlyExpensesData = { labels: [], datasets: [] };
let expensesLoading = false;
// Component references for refreshing
let enhancedBalanceComponent;
@@ -27,7 +30,10 @@
onMount(async () => {
// Mark that JavaScript is loaded for progressive enhancement
document.body.classList.add('js-loaded');
await fetchBalance();
await Promise.all([
fetchBalance(),
fetchMonthlyExpenses()
]);
// Listen for dashboard refresh events from the layout
const handleDashboardRefresh = () => {
@@ -57,10 +63,31 @@
}
}
async function fetchMonthlyExpenses() {
try {
expensesLoading = true;
console.log('Fetching monthly expenses...');
const response = await fetch('/api/cospend/monthly-expenses');
if (!response.ok) {
throw new Error('Failed to fetch monthly expenses');
}
monthlyExpensesData = await response.json();
console.log('Monthly expenses data:', monthlyExpensesData);
} catch (err) {
console.error('Error fetching monthly expenses:', err);
// Don't show this error in the main error state
} finally {
expensesLoading = false;
}
}
// Function to refresh all dashboard components after payment deletion
async function refreshAllComponents() {
// Refresh the main balance and recent activity
await fetchBalance();
await Promise.all([
fetchBalance(),
fetchMonthlyExpenses()
]);
// Refresh the enhanced balance component if it exists and has a refresh method
if (enhancedBalanceComponent && enhancedBalanceComponent.refresh) {
@@ -138,6 +165,25 @@
<DebtBreakdown bind:this={debtBreakdownComponent} />
<!-- Monthly Expenses Chart -->
<div class="chart-section">
{#if expensesLoading}
<div class="loading">Loading monthly expenses chart...</div>
{:else if monthlyExpensesData.datasets && monthlyExpensesData.datasets.length > 0}
<BarChart
data={monthlyExpensesData}
title="Monthly Expenses by Category"
height="400px"
/>
{:else}
<div class="loading">
Debug: expensesLoading={expensesLoading},
datasets={monthlyExpensesData.datasets?.length || 0},
data={JSON.stringify(monthlyExpensesData)}
</div>
{/if}
</div>
{#if loading}
<div class="loading">Loading recent activity...</div>
{:else if error}
@@ -660,4 +706,26 @@
font-size: 1.3rem;
}
}
.chart-section {
margin-bottom: 2rem;
}
.chart-section .loading {
background: var(--nord6);
border-radius: 0.75rem;
padding: 2rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid var(--nord4);
text-align: center;
color: var(--nord2);
}
@media (prefers-color-scheme: dark) {
.chart-section .loading {
background: var(--nord1);
border-color: var(--nord2);
color: var(--nord4);
}
}
</style>