window.initFinanceApp = function() { window.financeApp = { data: null, charts: {}, async load() { try { let res = await fetch('/api/finance'); let json = await res.json(); this.data = json; if (!this.data.monthly || !this.data.monthly.labels) { this.data.monthly = {labels:['Jan'], income:[0], expenses:[0], spend:[0]}; } if (!this.data.accounts) this.data.accounts = []; this.renderCharts(); this.renderTable(); } catch (e) { console.error("Finance App Load Error:", e); } }, async save() { await fetch('/api/finance', { method: 'POST', body: JSON.stringify(this.data), headers: {'Content-Type': 'application/json'} }); }, renderCharts() { const resolveColor = (cssVar, fallback) => { const div = document.createElement('div'); div.style.color = fallback; div.style.color = `var(${cssVar})`; div.style.display = 'none'; document.body.appendChild(div); const color = getComputedStyle(div).color; document.body.removeChild(div); return color; }; const cPos = resolveColor('--color-positive', 'hsl(150, 60%, 50%)'); const cNeg = resolveColor('--color-negative', 'hsl(0, 70%, 65%)'); const cPri = resolveColor('--color-primary', 'hsl(12, 70%, 60%)'); const cText = resolveColor('--color-text-subdue', 'rgba(255,255,255,0.6)'); const resolveGridColor = () => { const div = document.createElement('div'); div.style.borderColor = 'rgba(255,255,255,0.05)'; div.style.borderColor = 'var(--color-widget-content-border)'; div.style.display = 'none'; document.body.appendChild(div); const color = getComputedStyle(div).borderColor; document.body.removeChild(div); return color; }; const cGrid = resolveGridColor(); const addAlpha = (rgbStr, alpha) => { let m = rgbStr.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)/); if (m) { return `rgba(${m[1]}, ${m[2]}, ${m[3]}, ${alpha})`; } return rgbStr; }; const cPosAlpha = addAlpha(cPos, 0.6); const cNegAlpha = addAlpha(cNeg, 0.6); const cPriAlpha = addAlpha(cPri, 0.2); Chart.defaults.color = cText; Chart.defaults.font.family = 'Outfit, -apple-system, sans-serif'; const c1 = document.getElementById('incomeExpenseChart'); if(c1) { if(this.charts.incomeExpense) this.charts.incomeExpense.destroy(); this.charts.incomeExpense = new Chart(c1.getContext('2d'), { type: 'bar', data: { labels: this.data.monthly.labels, datasets: [ { label: 'Income', data: this.data.monthly.income, backgroundColor: cPosAlpha, borderColor: cPos, borderWidth: 1, borderRadius: 3, barPercentage: 0.6 }, { label: 'Expenses', data: this.data.monthly.expenses, backgroundColor: cNegAlpha, borderColor: cNeg, borderWidth: 1, borderRadius: 3, barPercentage: 0.6 } ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, layout: { padding: 10 }, scales: { y: { display: false, beginAtZero: true }, x: { display: false } } } }); } const c2 = document.getElementById('spendChart'); if(c2) { if(this.charts.spend) this.charts.spend.destroy(); this.charts.spend = new Chart(c2.getContext('2d'), { type: 'line', data: { labels: this.data.monthly.labels, datasets: [{ label: 'Spend', data: this.data.monthly.spend, borderColor: cPri, borderWidth: 2, tension: 0.4, fill: false, pointRadius: 0, pointHoverRadius: 5 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, layout: { padding: 10 }, scales: { y: { display: false, beginAtZero: true }, x: { display: false } } } }); } }, renderTable() { const t = document.getElementById('finance-tbody'); if(!t) return; t.innerHTML = ''; if(!this.data.accounts || this.data.accounts.length === 0) { t.innerHTML = `