1(function () {
2 'use strict';
3 const D = window.FinanceData;
4 const C = window.FinanceCharts;
5 let transactions = D.loadTransactions();
6
7 const $ = id => document.getElementById(id);
8 const els = {
9 form: $('transactionForm'), formTitle: $('formTitle'), txId: $('txId'), date: $('date'), description: $('description'),
10 category: $('category'), amount: $('amount'), rows: $('transactionRows'), search: $('search'), categoryFilter: $('categoryFilter'),
11 monthFilter: $('monthFilter'), budgets: $('budgetBars'), chart: $('spendingChart'), income: $('incomeTotal'), expense: $('expenseTotal'),
12 net: $('netTotal'), count: $('txCount'), theme: $('themeToggle'), reset: $('resetBtn'), export: $('exportBtn'), import: $('importInput')
13 };
14
15 function money(n) { return Number(n || 0).toLocaleString(undefined, { style: 'currency', currency: 'USD' }); }
16 function currentFilters() { return { category: els.categoryFilter.value, query: els.search.value.trim(), month: els.monthFilter.value }; }
17 function persist() { D.saveTransactions(transactions); }
18
19 function fillSelects() {
20 const cats = [...new Set([...D.DEFAULT_CATEGORIES, ...transactions.map(t => t.category)])].sort();
21 els.category.innerHTML = cats.map(c => `<option>${c}</option>`).join('');
22 els.categoryFilter.innerHTML = ['All', ...cats].map(c => `<option>${c}</option>`).join('');
23 }
24
25 function renderSummary(filtered) {
26 const allSummary = D.summarize(transactions);
27 const filteredSummary = D.summarize(filtered);
28 els.income.textContent = money(allSummary.income);
29 els.expense.textContent = money(allSummary.expenses);
30 els.net.textContent = money(allSummary.net);
31 els.net.className = allSummary.net >= 0 ? 'amount-pos' : 'amount-neg';
32 els.count.textContent = `${filteredSummary.count}/${allSummary.count}`;
33 }
34
35 function renderBudgets() {
36 const month = els.monthFilter.value || new Date().toISOString().slice(0, 7);
37 const statuses = D.monthlyBudgetStatus(transactions, D.DEFAULT_BUDGETS, month);
38 els.budgets.innerHTML = statuses.map(b => {
39 const cls = b.percent >= 100 ? 'over' : b.percent >= 80 ? 'warn' : '';
40 return `<div class="budget"><div class="budget-top"><strong>${b.category}</strong><span>${money(b.spent)} / ${money(b.budget)} (${b.percent}%)</span></div><div class="bar"><span class="${cls}" style="width:${Math.min(100, b.percent)}%"></span></div></div>`;
41 }).join('');
42 }
43
44 function renderRows(filtered) {
45 if (!filtered.length) {
46 els.rows.innerHTML = '<tr><td colspan="5" class="empty">No transactions match your filters.</td></tr>';
47 return;
48 }
49 els.rows.innerHTML = D.sortTransactions(filtered).map(tx => `<tr>
50 <td>${tx.date}</td><td>${escapeHTML(tx.description)}</td><td>${escapeHTML(tx.category)}</td>
51 <td class="num ${tx.amount >= 0 ? 'amount-pos' : 'amount-neg'}">${money(tx.amount)}</td>
52 <td><div class="row-actions"><button class="ghost" data-action="edit" data-id="${tx.id}">Edit</button><button class="ghost" data-action="delete" data-id="${tx.id}">Delete</button></div></td>
53 </tr>`).join('');
54 }
55
56 function escapeHTML(text) {
57 return String(text).replace(/[&<>'"]/g, ch => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[ch]));
58 }
59
60 function render() {
61 fillSelects();
62 const filtered = D.filterTransactions(transactions, currentFilters());
63 renderSummary(filtered);
64 renderBudgets();
65 renderRows(filtered);
66 C.drawSpendingChart(els.chart, D.sortTransactions(filtered), { dark: document.documentElement.classList.contains('dark') });
67 }
68
69 function resetForm() {
70 els.form.reset();
71 els.txId.value = '';
72 els.date.value = D.todayISO();
73 els.category.value = 'Food';
74 els.formTitle.textContent = 'Add transaction';
75 els.description.focus();
76 }
77
78 els.form.addEventListener('submit', event => {
79 event.preventDefault();
80 const tx = { date: els.date.value, description: els.description.value, category: els.category.value, amount: els.amount.value };
81 try {
82 transactions = els.txId.value ? D.updateTransaction(transactions, els.txId.value, tx) : D.addTransaction(transactions, tx);
83 persist(); resetForm(); render();
84 } catch (error) { alert(error.message); }
85 });
86
87 els.rows.addEventListener('click', event => {
88 const btn = event.target.closest('button[data-action]');
89 if (!btn) return;
90 const tx = transactions.find(t => t.id === btn.dataset.id);
91 if (!tx) return;
92 if (btn.dataset.action === 'delete') {
93 if (confirm(`Delete "${tx.description}"?`)) { transactions = D.deleteTransaction(transactions, tx.id); persist(); render(); }
94 } else {
95 els.txId.value = tx.id; els.date.value = tx.date; els.description.value = tx.description; els.category.value = tx.category; els.amount.value = tx.amount;
96 els.formTitle.textContent = 'Edit transaction'; els.description.focus();
97 }
98 });
99
100 [els.search, els.categoryFilter, els.monthFilter].forEach(el => el.addEventListener('input', render));
101 els.reset.addEventListener('click', resetForm);
102 els.theme.addEventListener('click', () => {
103 document.documentElement.classList.toggle('dark');
104 localStorage.setItem('finance_dashboard_theme', document.documentElement.classList.contains('dark') ? 'dark' : 'light');
105 render();
106 });
107 els.export.addEventListener('click', () => {
108 const blob = new Blob([D.toCSV(transactions)], { type: 'text/csv' });
109 const a = document.createElement('a');
110 a.href = URL.createObjectURL(blob); a.download = 'transactions.csv'; a.click(); URL.revokeObjectURL(a.href);
111 });
112 els.import.addEventListener('change', async event => {
113 const file = event.target.files[0]; if (!file) return;
114 try {
115 const imported = D.parseCSV(await file.text());
116 const byId = new Map(transactions.map(t => [t.id, t]));
117 imported.forEach(t => byId.set(t.id, t));
118 transactions = D.sortTransactions([...byId.values()]); persist(); render(); event.target.value = '';
119 } catch (error) { alert('CSV import failed: ' + error.message); }
120 });
121 document.addEventListener('keydown', event => {
122 if (event.target.matches('input,select,textarea') && event.key !== 'Escape') return;
123 if (event.key === '/') { event.preventDefault(); els.search.focus(); }
124 if (event.key.toLowerCase() === 'n') resetForm();
125 if (event.key.toLowerCase() === 'd') els.theme.click();
126 if (event.key.toLowerCase() === 'e') els.export.click();
127 if (event.key === 'Escape') resetForm();
128 });
129 window.addEventListener('resize', render);
130
131 if (localStorage.getItem('finance_dashboard_theme') === 'dark') document.documentElement.classList.add('dark');
132 els.monthFilter.value = new Date().toISOString().slice(0, 7);
133 resetForm();
134 render();
135})();
136
Discussion
No comments yet. Start the discussion. Recorded by @patrick-toulme.