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 = `No accounts.`; return; } this.data.accounts.forEach((acc, i) => { const tr = document.createElement('tr'); tr.style.borderBottom = '1px solid var(--color-widget-content-border)'; const inStyle = `width: 100%; background: transparent; border: none; color: var(--color-text-highlight); padding: 1rem; font-family: inherit; font-size: 1.15rem; outline: none; transition: background 0.2s;`; const td1 = document.createElement('td'); td1.style.padding = '0'; td1.innerHTML = ``; const td2 = document.createElement('td'); td2.style.padding = '0'; let amountCol = acc.balance < 0 ? 'color-negative' : 'color-positive'; td2.innerHTML = ``; const td3 = document.createElement('td'); td3.style.padding = '0'; td3.innerHTML = ``; const td4 = document.createElement('td'); td4.style.textAlign = 'right'; td4.style.padding = '1rem'; td4.innerHTML = ``; tr.appendChild(td1); tr.appendChild(td2); tr.appendChild(td3); tr.appendChild(td4); t.appendChild(tr); }); }, update(i, key, val) { this.data.accounts[i][key] = val; this.renderTable(); this.save(); }, addAccount() { this.data.accounts.push({name:'New Account',balance:0,currency:'USD'}); this.renderTable(); this.save(); }, delete(i) { this.data.accounts.splice(i, 1); this.renderTable(); this.save(); } }; window.financeApp.load(); }; if (!window.financeAppLoaded) { window.financeAppLoaded = true; if (!window.Chart) { let script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/chart.js'; script.onload = () => window.initFinanceApp(); document.head.appendChild(script); } else { window.initFinanceApp(); } }