Files
glance/finance.js
Tanmay Karande 492e0ec47c feat: add finance widget and CSS/template updates
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 00:36:43 -04:00

179 lines
7.4 KiB
JavaScript

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 = `<tr><td colspan='4' style='text-align:center; padding: 2rem;' class='color-subdue'>No accounts.</td></tr>`;
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 = `<input style="${inStyle}" value="${acc.name}" onfocus="this.style.background='var(--color-widget-background-highlight)'" onblur="this.style.background='transparent'" onchange="financeApp.update(${i},'name',this.value)">`;
const td2 = document.createElement('td');
td2.style.padding = '0';
let amountCol = acc.balance < 0 ? 'color-negative' : 'color-positive';
td2.innerHTML = `<input type="number" step="0.01" style="${inStyle}" class="${amountCol}" value="${acc.balance}" onfocus="this.style.background='var(--color-widget-background-highlight)'" onblur="this.style.background='transparent'" onchange="financeApp.update(${i},'balance',parseFloat(this.value))">`;
const td3 = document.createElement('td');
td3.style.padding = '0';
td3.innerHTML = `<input style="${inStyle}" value="${acc.currency}" onfocus="this.style.background='var(--color-widget-background-highlight)'" onblur="this.style.background='transparent'" onchange="financeApp.update(${i},'currency',this.value)">`;
const td4 = document.createElement('td');
td4.style.textAlign = 'right';
td4.style.padding = '1rem';
td4.innerHTML = `<button onclick="financeApp.delete(${i})" style="padding: 0.4rem 0.8rem; border-radius: 6px; border: none; background: rgba(255, 50, 50, 0.15); color: var(--color-negative); font-weight: 600; cursor: pointer;">Delete</button>`;
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();
}
}