This commit is contained in:
@@ -27,6 +27,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/sveltekit": "^1.10.0",
|
"@auth/sveltekit": "^1.10.0",
|
||||||
"@sveltejs/adapter-node": "^5.0.0",
|
"@sveltejs/adapter-node": "^5.0.0",
|
||||||
|
"chart.js": "^4.5.0",
|
||||||
"cheerio": "1.0.0-rc.12",
|
"cheerio": "1.0.0-rc.12",
|
||||||
"mongoose": "^8.0.0",
|
"mongoose": "^8.0.0",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
|
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@@ -14,6 +14,9 @@ importers:
|
|||||||
'@sveltejs/adapter-node':
|
'@sveltejs/adapter-node':
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.3.1(@sveltejs/kit@2.37.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)))
|
version: 5.3.1(@sveltejs/kit@2.37.0(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)))(svelte@5.38.6)(vite@7.1.3(@types/node@22.18.0)))
|
||||||
|
chart.js:
|
||||||
|
specifier: ^4.5.0
|
||||||
|
version: 4.5.0
|
||||||
cheerio:
|
cheerio:
|
||||||
specifier: 1.0.0-rc.12
|
specifier: 1.0.0-rc.12
|
||||||
version: 1.0.0-rc.12
|
version: 1.0.0-rc.12
|
||||||
@@ -376,6 +379,9 @@ packages:
|
|||||||
'@jridgewell/trace-mapping@0.3.30':
|
'@jridgewell/trace-mapping@0.3.30':
|
||||||
resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==}
|
resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==}
|
||||||
|
|
||||||
|
'@kurkle/color@0.3.4':
|
||||||
|
resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==}
|
||||||
|
|
||||||
'@mongodb-js/saslprep@1.3.0':
|
'@mongodb-js/saslprep@1.3.0':
|
||||||
resolution: {integrity: sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==}
|
resolution: {integrity: sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==}
|
||||||
|
|
||||||
@@ -625,6 +631,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==}
|
resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==}
|
||||||
engines: {node: '>=16.20.1'}
|
engines: {node: '>=16.20.1'}
|
||||||
|
|
||||||
|
chart.js@4.5.0:
|
||||||
|
resolution: {integrity: sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==}
|
||||||
|
engines: {pnpm: '>=8'}
|
||||||
|
|
||||||
cheerio-select@2.1.0:
|
cheerio-select@2.1.0:
|
||||||
resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
|
resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
|
||||||
|
|
||||||
@@ -1251,6 +1261,8 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.0
|
'@jridgewell/resolve-uri': 3.1.0
|
||||||
'@jridgewell/sourcemap-codec': 1.4.15
|
'@jridgewell/sourcemap-codec': 1.4.15
|
||||||
|
|
||||||
|
'@kurkle/color@0.3.4': {}
|
||||||
|
|
||||||
'@mongodb-js/saslprep@1.3.0':
|
'@mongodb-js/saslprep@1.3.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
sparse-bitfield: 3.0.3
|
sparse-bitfield: 3.0.3
|
||||||
@@ -1455,6 +1467,10 @@ snapshots:
|
|||||||
|
|
||||||
bson@6.10.4: {}
|
bson@6.10.4: {}
|
||||||
|
|
||||||
|
chart.js@4.5.0:
|
||||||
|
dependencies:
|
||||||
|
'@kurkle/color': 0.3.4
|
||||||
|
|
||||||
cheerio-select@2.1.0:
|
cheerio-select@2.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
boolbase: 1.0.0
|
boolbase: 1.0.0
|
||||||
|
205
src/lib/components/BarChart.svelte
Normal file
205
src/lib/components/BarChart.svelte
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { Chart, registerables } from 'chart.js';
|
||||||
|
|
||||||
|
export let data = { labels: [], datasets: [] };
|
||||||
|
export let title = '';
|
||||||
|
export let height = '400px';
|
||||||
|
|
||||||
|
let canvas;
|
||||||
|
let chart;
|
||||||
|
|
||||||
|
// Register Chart.js components
|
||||||
|
Chart.register(...registerables);
|
||||||
|
|
||||||
|
// Nord theme colors for categories
|
||||||
|
const nordColors = [
|
||||||
|
'#5E81AC', // Nord Blue
|
||||||
|
'#88C0D0', // Nord Light Blue
|
||||||
|
'#81A1C1', // Nord Lighter Blue
|
||||||
|
'#A3BE8C', // Nord Green
|
||||||
|
'#EBCB8B', // Nord Yellow
|
||||||
|
'#D08770', // Nord Orange
|
||||||
|
'#BF616A', // Nord Red
|
||||||
|
'#B48EAD', // Nord Purple
|
||||||
|
'#8FBCBB', // Nord Cyan
|
||||||
|
'#ECEFF4', // Nord Light Gray
|
||||||
|
];
|
||||||
|
|
||||||
|
function getCategoryColor(category, index) {
|
||||||
|
const categoryColorMap = {
|
||||||
|
'groceries': '#A3BE8C', // Green
|
||||||
|
'restaurant': '#D08770', // Orange
|
||||||
|
'transport': '#5E81AC', // Blue
|
||||||
|
'entertainment': '#B48EAD', // Purple
|
||||||
|
'shopping': '#EBCB8B', // Yellow
|
||||||
|
'utilities': '#81A1C1', // Light Blue
|
||||||
|
'healthcare': '#BF616A', // Red
|
||||||
|
'education': '#88C0D0', // Cyan
|
||||||
|
'travel': '#8FBCBB', // Light Cyan
|
||||||
|
'other': '#4C566A' // Dark Gray
|
||||||
|
};
|
||||||
|
|
||||||
|
return categoryColorMap[category] || nordColors[index % nordColors.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
function createChart() {
|
||||||
|
if (!canvas || !data.datasets) return;
|
||||||
|
|
||||||
|
// Destroy existing chart
|
||||||
|
if (chart) {
|
||||||
|
chart.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
// Process datasets with colors
|
||||||
|
const processedDatasets = data.datasets.map((dataset, index) => ({
|
||||||
|
...dataset,
|
||||||
|
backgroundColor: getCategoryColor(dataset.label, index),
|
||||||
|
borderColor: getCategoryColor(dataset.label, index),
|
||||||
|
borderWidth: 1
|
||||||
|
}));
|
||||||
|
|
||||||
|
chart = new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: data.labels,
|
||||||
|
datasets: processedDatasets
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
stacked: true,
|
||||||
|
grid: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
color: 'var(--nord3)',
|
||||||
|
font: {
|
||||||
|
family: 'Inter, system-ui, sans-serif'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
stacked: true,
|
||||||
|
beginAtZero: true,
|
||||||
|
grid: {
|
||||||
|
color: 'var(--nord4)',
|
||||||
|
borderDash: [2, 2]
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
color: 'var(--nord3)',
|
||||||
|
font: {
|
||||||
|
family: 'Inter, system-ui, sans-serif'
|
||||||
|
},
|
||||||
|
callback: function(value) {
|
||||||
|
return 'CHF ' + value.toFixed(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'bottom',
|
||||||
|
labels: {
|
||||||
|
padding: 20,
|
||||||
|
usePointStyle: true,
|
||||||
|
color: 'var(--nord1)',
|
||||||
|
font: {
|
||||||
|
family: 'Inter, system-ui, sans-serif',
|
||||||
|
size: 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
display: !!title,
|
||||||
|
text: title,
|
||||||
|
color: 'var(--nord0)',
|
||||||
|
font: {
|
||||||
|
family: 'Inter, system-ui, sans-serif',
|
||||||
|
size: 16,
|
||||||
|
weight: 'bold'
|
||||||
|
},
|
||||||
|
padding: 20
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: 'var(--nord1)',
|
||||||
|
titleColor: 'var(--nord6)',
|
||||||
|
bodyColor: 'var(--nord6)',
|
||||||
|
borderColor: 'var(--nord3)',
|
||||||
|
borderWidth: 1,
|
||||||
|
cornerRadius: 8,
|
||||||
|
titleFont: {
|
||||||
|
family: 'Inter, system-ui, sans-serif'
|
||||||
|
},
|
||||||
|
bodyFont: {
|
||||||
|
family: 'Inter, system-ui, sans-serif'
|
||||||
|
},
|
||||||
|
callbacks: {
|
||||||
|
label: function(context) {
|
||||||
|
return context.dataset.label + ': CHF ' + context.parsed.y.toFixed(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
intersect: false,
|
||||||
|
mode: 'index'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
createChart();
|
||||||
|
|
||||||
|
// Watch for theme changes
|
||||||
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
const handleThemeChange = () => {
|
||||||
|
setTimeout(createChart, 100); // Small delay to let CSS variables update
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaQuery.addEventListener('change', handleThemeChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mediaQuery.removeEventListener('change', handleThemeChange);
|
||||||
|
if (chart) {
|
||||||
|
chart.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Recreate chart when data changes
|
||||||
|
$: if (canvas && data) {
|
||||||
|
createChart();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="chart-container" style="height: {height}">
|
||||||
|
<canvas bind:this={canvas}></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.chart-container {
|
||||||
|
background: var(--nord6);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
border: 1px solid var(--nord4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.chart-container {
|
||||||
|
background: var(--nord1);
|
||||||
|
border-color: var(--nord2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
max-width: 100%;
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
</style>
|
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 ProfilePicture from '$lib/components/ProfilePicture.svelte';
|
||||||
import EnhancedBalance from '$lib/components/EnhancedBalance.svelte';
|
import EnhancedBalance from '$lib/components/EnhancedBalance.svelte';
|
||||||
import DebtBreakdown from '$lib/components/DebtBreakdown.svelte';
|
import DebtBreakdown from '$lib/components/DebtBreakdown.svelte';
|
||||||
|
import BarChart from '$lib/components/BarChart.svelte';
|
||||||
import { getCategoryEmoji, getCategoryName } from '$lib/utils/categories';
|
import { getCategoryEmoji, getCategoryName } from '$lib/utils/categories';
|
||||||
import { isSettlementPayment, getSettlementIcon, getSettlementClasses, getSettlementReceiver } from '$lib/utils/settlements';
|
import { isSettlementPayment, getSettlementIcon, getSettlementClasses, getSettlementReceiver } from '$lib/utils/settlements';
|
||||||
import AddButton from '$lib/components/AddButton.svelte';
|
import AddButton from '$lib/components/AddButton.svelte';
|
||||||
@@ -18,6 +19,8 @@
|
|||||||
};
|
};
|
||||||
let loading = false; // Start as false since we have server data
|
let loading = false; // Start as false since we have server data
|
||||||
let error = null;
|
let error = null;
|
||||||
|
let monthlyExpensesData = { labels: [], datasets: [] };
|
||||||
|
let expensesLoading = false;
|
||||||
|
|
||||||
// Component references for refreshing
|
// Component references for refreshing
|
||||||
let enhancedBalanceComponent;
|
let enhancedBalanceComponent;
|
||||||
@@ -27,7 +30,10 @@
|
|||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
// Mark that JavaScript is loaded for progressive enhancement
|
// Mark that JavaScript is loaded for progressive enhancement
|
||||||
document.body.classList.add('js-loaded');
|
document.body.classList.add('js-loaded');
|
||||||
await fetchBalance();
|
await Promise.all([
|
||||||
|
fetchBalance(),
|
||||||
|
fetchMonthlyExpenses()
|
||||||
|
]);
|
||||||
|
|
||||||
// Listen for dashboard refresh events from the layout
|
// Listen for dashboard refresh events from the layout
|
||||||
const handleDashboardRefresh = () => {
|
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
|
// Function to refresh all dashboard components after payment deletion
|
||||||
async function refreshAllComponents() {
|
async function refreshAllComponents() {
|
||||||
// Refresh the main balance and recent activity
|
// 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
|
// Refresh the enhanced balance component if it exists and has a refresh method
|
||||||
if (enhancedBalanceComponent && enhancedBalanceComponent.refresh) {
|
if (enhancedBalanceComponent && enhancedBalanceComponent.refresh) {
|
||||||
@@ -138,6 +165,25 @@
|
|||||||
|
|
||||||
<DebtBreakdown bind:this={debtBreakdownComponent} />
|
<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}
|
{#if loading}
|
||||||
<div class="loading">Loading recent activity...</div>
|
<div class="loading">Loading recent activity...</div>
|
||||||
{:else if error}
|
{:else if error}
|
||||||
@@ -660,4 +706,26 @@
|
|||||||
font-size: 1.3rem;
|
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>
|
</style>
|
||||||
|
Reference in New Issue
Block a user