1
2(function (root, factory) {
3 const api = factory();
4 if (typeof module === 'object' && module.exports) module.exports = api;
5 root.FinanceData = api;
6})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
7 const STORAGE_KEY = 'finance-dashboard-transactions-v1';
8 const BUDGET_KEY = 'finance-dashboard-budgets-v1';
9
10 const defaultCategories = ['Housing', 'Food', 'Transport', 'Utilities', 'Health', 'Entertainment', 'Shopping', 'Income', 'Other'];
11 const defaultBudgets = { Housing: 1800, Food: 650, Transport: 350, Utilities: 300, Health: 250, Entertainment: 250, Shopping: 300, Other: 200 };
12
13 const sampleTransactions = [
14 { id: 't1', date: '2026-07-01', description: 'Paycheck', category: 'Income', amount: 4200 },
15 { id: 't2', date: '2026-07-02', description: 'Rent', category: 'Housing', amount: -1650 },
16 { id: 't3', date: '2026-07-04', description: 'Groceries', category: 'Food', amount: -126.45 },
17 { id: 't4', date: '2026-07-07', description: 'Metro card', category: 'Transport', amount: -48 },
18 { id: 't5', date: '2026-07-12', description: 'Movie night', category: 'Entertainment', amount: -38 },
19 { id: 't6', date: '2026-07-15', description: 'Freelance', category: 'Income', amount: 850 },
20 { id: 't7', date: '2026-07-18', description: 'Pharmacy', category: 'Health', amount: -22.8 },
21 { id: 't8', date: '2026-07-21', description: 'Electric bill', category: 'Utilities', amount: -95.2 },
22 { id: 't9', date: '2026-06-03', description: 'Groceries', category: 'Food', amount: -112.1 },
23 { id: 't10', date: '2026-06-14', description: 'Paycheck', category: 'Income', amount: 4200 }
24 ];
25
26 function uid() {
27 return 't-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
28 }
29
30 function clone(value) { return JSON.parse(JSON.stringify(value)); }
31
32 function parseAmount(value) {
33 if (typeof value === 'number') return Number.isFinite(value) ? value : 0;
34 const cleaned = String(value || '').replace(/[$,\s]/g, '');
35 const n = Number(cleaned);
36 return Number.isFinite(n) ? n : 0;
37 }
38
39 function normalizeTransaction(input) {
40 const tx = {
41 id: input.id || uid(),
42 date: input.date || new Date().toISOString().slice(0, 10),
43 description: String(input.description || '').trim() || 'Untitled',
44 category: String(input.category || 'Other').trim() || 'Other',
45 amount: Math.round(parseAmount(input.amount) * 100) / 100
46 };
47 if (!/^\d{4}-\d{2}-\d{2}$/.test(tx.date)) throw new Error('Date must be YYYY-MM-DD');
48 return tx;
49 }
50
51 function sortTransactions(transactions) {
52 return clone(transactions).sort((a, b) => (b.date + b.id).localeCompare(a.date + a.id));
53 }
54
55 function addTransaction(transactions, input) {
56 return sortTransactions([normalizeTransaction(input), ...transactions]);
57 }
58
59 function updateTransaction(transactions, id, patch) {
60 let found = false;
61 const updated = transactions.map(tx => {
62 if (tx.id !== id) return tx;
63 found = true;
64 return normalizeTransaction({ ...tx, ...patch, id });
65 });
66 if (!found) throw new Error('Transaction not found');
67 return sortTransactions(updated);
68 }
69
70 function deleteTransaction(transactions, id) {
71 return transactions.filter(tx => tx.id !== id);
72 }
73
74 function filterTransactions(transactions, filters) {
75 const category = filters && filters.category;
76 const month = filters && filters.month;
77 const q = (filters && filters.query || '').toLowerCase();
78 return transactions.filter(tx => {
79 const categoryOk = !category || category === 'All' || tx.category === category;
80 const monthOk = !month || tx.date.slice(0, 7) === month;
81 const queryOk = !q || tx.description.toLowerCase().includes(q) || tx.category.toLowerCase().includes(q);
82 return categoryOk && monthOk && queryOk;
83 });
84 }
85
86 function summarize(transactions) {
87 const income = transactions.filter(t => t.amount > 0).reduce((s, t) => s + t.amount, 0);
88 const expense = transactions.filter(t => t.amount < 0).reduce((s, t) => s + Math.abs(t.amount), 0);
89 return { income, expense, balance: income - expense, count: transactions.length };
90 }
91
92 function categorySpend(transactions, month) {
93 return transactions.reduce((acc, tx) => {
94 if (tx.amount >= 0) return acc;
95 if (month && tx.date.slice(0, 7) !== month) return acc;
96 acc[tx.category] = (acc[tx.category] || 0) + Math.abs(tx.amount);
97 return acc;
98 }, {});
99 }
100
101 function spendingOverTime(transactions) {
102 const byDay = {};
103 transactions.forEach(tx => {
104 if (tx.amount < 0) byDay[tx.date] = (byDay[tx.date] || 0) + Math.abs(tx.amount);
105 });
106 return Object.keys(byDay).sort().map(date => ({ date, amount: Math.round(byDay[date] * 100) / 100 }));
107 }
108
109 function escapeCsv(value) {
110 const s = String(value == null ? '' : value);
111 return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
112 }
113
114 function toCSV(transactions) {
115 const rows = [['id', 'date', 'description', 'category', 'amount'], ...sortTransactions(transactions).map(t => [t.id, t.date, t.description, t.category, t.amount])];
116 return rows.map(row => row.map(escapeCsv).join(',')).join('\n');
117 }
118
119 function parseCsvLine(line) {
120 const out = [];
121 let cur = '', quoted = false;
122 for (let i = 0; i < line.length; i++) {
123 const ch = line[i];
124 if (quoted && ch === '"' && line[i + 1] === '"') { cur += '"'; i++; }
125 else if (ch === '"') quoted = !quoted;
126 else if (!quoted && ch === ',') { out.push(cur); cur = ''; }
127 else cur += ch;
128 }
129 out.push(cur);
130 return out;
131 }
132
133 function fromCSV(csv) {
134 const lines = String(csv || '').split(/\r?\n/).filter(Boolean);
135 if (!lines.length) return [];
136 const headers = parseCsvLine(lines[0]).map(h => h.trim().toLowerCase());
137 return lines.slice(1).map(line => {
138 const cols = parseCsvLine(line);
139 const obj = {};
140 headers.forEach((h, i) => { obj[h] = cols[i]; });
141 return normalizeTransaction(obj);
142 });
143 }
144
145 function loadTransactions(storage) {
146 try {
147 const raw = storage && storage.getItem(STORAGE_KEY);
148 return raw ? JSON.parse(raw).map(normalizeTransaction) : clone(sampleTransactions);
149 } catch (_) { return clone(sampleTransactions); }
150 }
151
152 function saveTransactions(storage, transactions) {
153 if (storage) storage.setItem(STORAGE_KEY, JSON.stringify(sortTransactions(transactions)));
154 }
155
156 function loadBudgets(storage) {
157 try {
158 const raw = storage && storage.getItem(BUDGET_KEY);
159 return raw ? { ...defaultBudgets, ...JSON.parse(raw) } : clone(defaultBudgets);
160 } catch (_) { return clone(defaultBudgets); }
161 }
162
163 function saveBudgets(storage, budgets) {
164 if (storage) storage.setItem(BUDGET_KEY, JSON.stringify(budgets));
165 }
166
167 return { STORAGE_KEY, BUDGET_KEY, defaultCategories, defaultBudgets, sampleTransactions, normalizeTransaction, addTransaction, updateTransaction, deleteTransaction, filterTransactions, summarize, categorySpend, spendingOverTime, toCSV, fromCSV, loadTransactions, saveTransactions, loadBudgets, saveBudgets };
168});
169
Discussion
No comments yet. Start the discussion. Recorded by @patrick-toulme.