This commit is contained in:
145
src/routes/api/cospend/monthly-expenses/+server.ts
Normal file
145
src/routes/api/cospend/monthly-expenses/+server.ts
Normal 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 });
|
||||
}
|
||||
};
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user