179 lines
7.4 KiB
JavaScript
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();
|
|
}
|
|
}
|