// Reports page JavaScript let currentPeriod = 30; let categoryFilter = ''; let trendChart = null; let categoryChart = null; let monthlyChart = null; // Load reports data async function loadReportsData() { try { const params = new URLSearchParams({ period: currentPeriod, ...(categoryFilter && { category_id: categoryFilter }) }); const data = await apiCall(`/api/reports-stats?${params}`); displayReportsData(data); } catch (error) { console.error('Failed to load reports data:', error); showToast('Failed to load reports', 'error'); } } // Display reports data function displayReportsData(data) { // Store user currency globally window.userCurrency = data.currency || 'GBP'; // Update KPI cards document.getElementById('total-spent').textContent = formatCurrency(data.total_spent, window.userCurrency); document.getElementById('total-income').textContent = formatCurrency(data.total_income, window.userCurrency); document.getElementById('profit-loss').textContent = formatCurrency(Math.abs(data.profit_loss), window.userCurrency); // Update profit/loss card color based on value const profitCard = document.getElementById('profit-loss').closest('.bg-card-light, .dark\\:bg-card-dark'); if (profitCard) { if (data.profit_loss >= 0) { profitCard.classList.add('border-green-500/20'); profitCard.classList.remove('border-red-500/20'); document.getElementById('profit-loss').classList.add('text-green-600', 'dark:text-green-400'); document.getElementById('profit-loss').classList.remove('text-red-600', 'dark:text-red-400'); } else { profitCard.classList.add('border-red-500/20'); profitCard.classList.remove('border-green-500/20'); document.getElementById('profit-loss').classList.add('text-red-600', 'dark:text-red-400'); document.getElementById('profit-loss').classList.remove('text-green-600', 'dark:text-green-400'); } } // Spending change indicator const spentChange = document.getElementById('spent-change'); const changeValue = data.percent_change; const isIncrease = changeValue > 0; spentChange.className = `flex items-center font-medium px-1.5 py-0.5 rounded ${ isIncrease ? 'text-red-500 dark:text-red-400 bg-red-500/10' : 'text-green-500 dark:text-green-400 bg-green-500/10' }`; spentChange.innerHTML = ` ${isIncrease ? 'trending_up' : 'trending_down'} ${Math.abs(changeValue).toFixed(1)}% `; // Income change indicator const incomeChange = document.getElementById('income-change'); const incomeChangeValue = data.income_percent_change || 0; const isIncomeIncrease = incomeChangeValue > 0; incomeChange.className = `flex items-center font-medium px-1.5 py-0.5 rounded ${ isIncomeIncrease ? 'text-green-500 dark:text-green-400 bg-green-500/10' : 'text-red-500 dark:text-red-400 bg-red-500/10' }`; incomeChange.innerHTML = ` ${isIncomeIncrease ? 'trending_up' : 'trending_down'} ${Math.abs(incomeChangeValue).toFixed(1)}% `; // Profit/loss change indicator const profitChange = document.getElementById('profit-change'); const profitChangeValue = data.profit_percent_change || 0; const isProfitIncrease = profitChangeValue > 0; profitChange.className = `flex items-center font-medium px-1.5 py-0.5 rounded ${ isProfitIncrease ? 'text-green-500 dark:text-green-400 bg-green-500/10' : 'text-red-500 dark:text-red-400 bg-red-500/10' }`; profitChange.innerHTML = ` ${isProfitIncrease ? 'trending_up' : 'trending_down'} ${Math.abs(profitChangeValue).toFixed(1)}% `; // Average daily document.getElementById('avg-daily').textContent = formatCurrency(data.avg_daily, data.currency); // Average change indicator const avgChange = document.getElementById('avg-change'); const avgChangeValue = data.avg_daily_change; const isAvgIncrease = avgChangeValue > 0; avgChange.className = `flex items-center font-medium px-1.5 py-0.5 rounded ${ isAvgIncrease ? 'text-red-500 dark:text-red-400 bg-red-500/10' : 'text-green-500 dark:text-green-400 bg-green-500/10' }`; avgChange.innerHTML = ` ${isAvgIncrease ? 'trending_up' : 'trending_down'} ${Math.abs(avgChangeValue).toFixed(1)}% `; // Savings rate document.getElementById('savings-rate').textContent = `${data.savings_rate.toFixed(1)}%`; // Savings rate change indicator const savingsChange = document.getElementById('savings-change'); const savingsChangeValue = data.savings_rate_change; const isSavingsIncrease = savingsChangeValue > 0; savingsChange.className = `flex items-center font-medium px-1.5 py-0.5 rounded ${ isSavingsIncrease ? 'text-green-500 dark:text-green-400 bg-green-500/10' : 'text-red-500 dark:text-red-400 bg-red-500/10' }`; savingsChange.innerHTML = ` ${isSavingsIncrease ? 'trending_up' : 'trending_down'} ${Math.abs(savingsChangeValue).toFixed(1)}% `; // Update charts updateTrendChart(data.daily_trend); updateCategoryChart(data.category_breakdown); updateIncomeChart(data.income_breakdown); updateMonthlyChart(data.monthly_comparison); } // Update trend chart - Income vs Expenses function updateTrendChart(dailyData) { const ctx = document.getElementById('trend-chart'); if (!ctx) return; // Get theme const isDark = document.documentElement.classList.contains('dark'); const textColor = isDark ? '#94a3b8' : '#64748b'; const gridColor = isDark ? '#334155' : '#e2e8f0'; if (trendChart) { trendChart.destroy(); } // Check if we have income data const hasIncome = dailyData.length > 0 && dailyData[0].hasOwnProperty('income'); const datasets = hasIncome ? [ { label: window.getTranslation ? window.getTranslation('nav.income', 'Income') : 'Income', data: dailyData.map(d => d.income || 0), borderColor: '#10b981', backgroundColor: 'rgba(16, 185, 129, 0.1)', fill: true, tension: 0.4, pointRadius: 4, pointBackgroundColor: isDark ? '#1e293b' : '#ffffff', pointBorderColor: '#10b981', pointBorderWidth: 2, pointHoverRadius: 6 }, { label: window.getTranslation ? window.getTranslation('dashboard.spending', 'Expenses') : 'Expenses', data: dailyData.map(d => d.expenses || 0), borderColor: '#ef4444', backgroundColor: 'rgba(239, 68, 68, 0.1)', fill: true, tension: 0.4, pointRadius: 4, pointBackgroundColor: isDark ? '#1e293b' : '#ffffff', pointBorderColor: '#ef4444', pointBorderWidth: 2, pointHoverRadius: 6 } ] : [{ label: window.getTranslation ? window.getTranslation('dashboard.spending', 'Spending') : 'Spending', data: dailyData.map(d => d.amount || d.expenses || 0), borderColor: '#3b82f6', backgroundColor: 'rgba(59, 130, 246, 0.1)', fill: true, tension: 0.4, pointRadius: 4, pointBackgroundColor: isDark ? '#1e293b' : '#ffffff', pointBorderColor: '#3b82f6', pointBorderWidth: 2, pointHoverRadius: 6 }]; trendChart = new Chart(ctx, { type: 'line', data: { labels: dailyData.map(d => d.date), datasets: datasets }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true, position: 'top', labels: { color: textColor, usePointStyle: true, padding: 15 } }, tooltip: { backgroundColor: isDark ? '#1e293b' : '#ffffff', titleColor: isDark ? '#f8fafc' : '#0f172a', bodyColor: isDark ? '#94a3b8' : '#64748b', borderColor: isDark ? '#334155' : '#e2e8f0', borderWidth: 1, padding: 12, displayColors: true, callbacks: { label: function(context) { return context.dataset.label + ': ' + formatCurrency(context.parsed.y, window.userCurrency || 'GBP'); } } } }, scales: { x: { grid: { color: gridColor, drawBorder: false }, ticks: { color: textColor, maxRotation: 45, minRotation: 0 } }, y: { grid: { color: gridColor, drawBorder: false }, ticks: { color: textColor, callback: function(value) { return formatCurrency(value, window.userCurrency || 'GBP'); } } } } } }); } // Update income sources pie chart function updateIncomeChart(incomeBreakdown) { const pieChart = document.getElementById('income-pie-chart'); const pieTotal = document.getElementById('income-pie-total'); const pieLegend = document.getElementById('income-legend'); if (!pieChart || !pieLegend) return; const userCurrency = window.userCurrency || 'GBP'; if (!incomeBreakdown || incomeBreakdown.length === 0) { pieChart.style.background = 'conic-gradient(#10b981 0% 100%)'; if (pieTotal) pieTotal.textContent = formatCurrency(0, userCurrency); pieLegend.innerHTML = '
' + (window.getTranslation ? window.getTranslation('dashboard.noData', 'No income data') : 'No income data') + '
'; return; } // Calculate total const total = incomeBreakdown.reduce((sum, inc) => sum + parseFloat(inc.amount || 0), 0); if (pieTotal) pieTotal.textContent = formatCurrency(total, userCurrency); // Income source colors const incomeColors = { 'Salary': '#10b981', 'Freelance': '#3b82f6', 'Investment': '#8b5cf6', 'Rental': '#f59e0b', 'Gift': '#ec4899', 'Bonus': '#14b8a6', 'Refund': '#6366f1', 'Other': '#6b7280' }; // Generate conic gradient segments let currentPercent = 0; const gradientSegments = incomeBreakdown.map(inc => { const percent = inc.percentage || 0; const color = incomeColors[inc.source] || '#10b981'; const segment = `${color} ${currentPercent}% ${currentPercent + percent}%`; currentPercent += percent; return segment; }); // Apply gradient pieChart.style.background = `conic-gradient(${gradientSegments.join(', ')})`; // Generate compact legend const legendHTML = incomeBreakdown.map(inc => { const color = incomeColors[inc.source] || '#10b981'; return `No data available
'; return; } // Calculate total const total = categories.reduce((sum, cat) => sum + parseFloat(cat.amount || 0), 0); if (pieTotal) pieTotal.textContent = formatCurrency(total, userCurrency); // Generate conic gradient segments let currentPercent = 0; const gradientSegments = categories.map(cat => { const percent = total > 0 ? (parseFloat(cat.amount || 0) / total) * 100 : 0; const segment = `${cat.color} ${currentPercent}% ${currentPercent + percent}%`; currentPercent += percent; return segment; }); // Apply gradient pieChart.style.background = `conic-gradient(${gradientSegments.join(', ')})`; // Generate compact legend const legendHTML = categories.map(cat => { const percent = total > 0 ? ((parseFloat(cat.amount || 0) / total) * 100).toFixed(1) : 0; return `No recommendations at this time
${rec.description}
Failed to load recommendations