1const FD = window.FinanceData;
2let transactions = FD.loadTransactions(localStorage);
3let budgets = FD.loadBudgets(localStorage);
4let filters = { category: 'All', month: new Date().toISOString().slice(0, 7), query: '' };
5
6const $ = id => document.getElementById(id);
7const money = n => (n < 0 ? '-' : '') + '$' + Math.abs(n).toLocaleString(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 2 });
8
9function init() {
10 populateCategories();
11 $('date').value = new Date().toISOString().slice(0, 10);
12 $('filterMonth').value = filters.month;
13 $('txForm').addEventListener('submit', saveForm);
14 $('resetBtn').addEventListener('click', resetForm);
15 $('filterCategory').addEventListener('change', e => { filters.category = e.target.value; render(); });
16 $('filterMonth').addEventListener('change', e => { filters.month = e.target.value; render(); });
17 $('search').addEventListener('input', e => { filters.query = e.target.value; render(); });
18 $('exportBtn').addEventListener('click', exportCsv);
19 $('importFile').addEventListener('change', importCsv);
20 $('themeBtn').addEventListener('click', toggleTheme);
21 document.addEventListener('keydown', shortcuts);
22 if (localStorage.getItem('finance-dashboard-theme') === 'dark') document.body.classList.add('dark');
23 render();
24}
25
26function populateCategories() {
27 const categories = Array.from(new Set([...FD.defaultCategories, ...transactions.map(t => t.category)])).sort();
28 $('category').innerHTML = categories.map(c => `<option>${escapeHtml(c)}</option>`).join('');
29 $('filterCategory').innerHTML = '<option>All</option>' + categories.map(c => `<option>${escapeHtml(c)}</option>`).join('');
30}
31
32function saveForm(e) {
33 e.preventDefault();
34 const input = { date: $('date').value, description: $('description').value, category: $('category').value, amount: $('amount').value };
35 const id = $('txId').value;
36 transactions = id ? FD.updateTransaction(transactions, id, input) : FD.addTransaction(transactions, input);
37 persistAndRender();
38 resetForm();
39}
40
41function resetForm() {
42 $('txId').value = '';
43 $('formTitle').textContent = 'Add transaction';
44 $('txForm').reset();
45 $('date').value = new Date().toISOString().slice(0, 10);
46 $('description').focus();
47}
48
49function editTx(id) {
50 const tx = transactions.find(t => t.id === id);
51 if (!tx) return;
52 $('txId').value = tx.id;
53 $('date').value = tx.date;
54 $('description').value = tx.description;
55 $('category').value = tx.category;
56 $('amount').value = tx.amount;
57 $('formTitle').textContent = 'Edit transaction';
58 window.scrollTo({ top: 0, behavior: 'smooth' });
59}
60
61function deleteTx(id) {
62 if (!confirm('Delete this transaction?')) return;
63 transactions = FD.deleteTransaction(transactions, id);
64 persistAndRender();
65}
66
67function persistAndRender() {
68 FD.saveTransactions(localStorage, transactions);
69 populateCategories();
70 render();
71}
72
73function render() {
74 const visible = FD.filterTransactions(transactions, filters);
75 const totals = FD.summarize(visible);
76 $('balance').textContent = money(totals.balance);
77 $('income').textContent = money(totals.income);
78 $('expenses').textContent = money(totals.expense);
79 renderTable(visible);
80 renderBudgets();
81 FinanceCharts.drawSpendingChart($('spendingChart'), FD.spendingOverTime(visible), document.body.classList.contains('dark'));
82}
83
84function renderTable(rows) {
85 $('txTable').innerHTML = rows.map(tx => `
86 <tr>
87 <td>${tx.date}</td><td>${escapeHtml(tx.description)}</td><td>${escapeHtml(tx.category)}</td>
88 <td class="num ${tx.amount >= 0 ? 'amount-pos' : 'amount-neg'}">${money(tx.amount)}</td>
89 <td><button class="small-btn ghost" onclick="editTx('${tx.id}')">Edit</button> <button class="small-btn danger" onclick="deleteTx('${tx.id}')">Delete</button></td>
90 </tr>`).join('') || '<tr><td colspan="5">No transactions match your filters.</td></tr>';
91}
92
93function renderBudgets() {
94 const month = filters.month || new Date().toISOString().slice(0, 7);
95 const spend = FD.categorySpend(transactions, month);
96 $('budgetBars').innerHTML = Object.keys(budgets).map(cat => {
97 const used = spend[cat] || 0;
98 const budget = Number(budgets[cat]) || 1;
99 const pct = Math.min(100, Math.round((used / budget) * 100));
100 const cls = pct >= 100 ? 'over' : pct >= 80 ? 'warn' : '';
101 return `<div class="budget-row"><strong>${escapeHtml(cat)}</strong><div class="bar"><div class="fill ${cls}" style="width:${pct}%"></div></div><span>${money(used)} / ${money(budget)}</span></div>`;
102 }).join('');
103}
104
105function exportCsv() {
106 const blob = new Blob([FD.toCSV(transactions)], { type: 'text/csv' });
107 const a = document.createElement('a');
108 a.href = URL.createObjectURL(blob); a.download = 'transactions.csv'; a.click(); URL.revokeObjectURL(a.href);
109}
110
111async function importCsv(e) {
112 const file = e.target.files[0];
113 if (!file) return;
114 try {
115 const imported = FD.fromCSV(await file.text());
116 transactions = FD.fromCSV(FD.toCSV([...transactions, ...imported]));
117 persistAndRender();
118 } catch (err) { alert('CSV import failed: ' + err.message); }
119 e.target.value = '';
120}
121
122function toggleTheme() {
123 document.body.classList.toggle('dark');
124 localStorage.setItem('finance-dashboard-theme', document.body.classList.contains('dark') ? 'dark' : 'light');
125 render();
126}
127
128function shortcuts(e) {
129 if (['INPUT', 'SELECT', 'TEXTAREA'].includes(document.activeElement.tagName) && e.key !== 'Escape') return;
130 if (e.key.toLowerCase() === 'n') { e.preventDefault(); resetForm(); }
131 if (e.key.toLowerCase() === 'e') { e.preventDefault(); exportCsv(); }
132 if (e.key.toLowerCase() === 'd') { e.preventDefault(); toggleTheme(); }
133 if (e.key === '/') { e.preventDefault(); $('search').focus(); }
134 if (e.key === 'Escape') resetForm();
135}
136
137function escapeHtml(s) { return String(s).replace(/[&<>'"]/g, c => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[c])); }
138
139window.editTx = editTx;
140window.deleteTx = deleteTx;
141document.addEventListener('DOMContentLoaded', init);
142
Discussion
No comments yet. Start the discussion. Recorded by @patrick-toulme.