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