const KEY_TX = "finance_tx_v2"; const KEY_ACCOUNTS = "finance_accounts_v1"; const el = (id) => document.getElementById(id); const rupiah = (n) => new Intl.NumberFormat("id-ID").format(Number(n || 0)); function nowLocalDatetimeValue() { const d = new Date(); d.setMinutes(d.getMinutes() - d.getTimezoneOffset()); return d.toISOString().slice(0, 16); } function loadTx() { return JSON.parse(localStorage.getItem(KEY_TX) || "[]"); } function saveTx(data) { localStorage.setItem(KEY_TX, JSON.stringify(data)); } function loadAccounts() { const raw = localStorage.getItem(KEY_ACCOUNTS); if (raw) return JSON.parse(raw); const defaults = [ { id: crypto.randomUUID(), name: "Cash", type: "CASH", opening: 0 }, { id: crypto.randomUUID(), name: "Bank", type: "BANK", opening: 0 }, { id: crypto.randomUUID(), name: "E-Wallet", type: "EWALLET", opening: 0 }, ]; localStorage.setItem(KEY_ACCOUNTS, JSON.stringify(defaults)); return defaults; } function saveAccounts(data) { localStorage.setItem(KEY_ACCOUNTS, JSON.stringify(data)); } let filterState = { from: "", to: "", q: "" }; function refreshAccountSelects() { const accounts = loadAccounts(); const accountSel = el("account"); const toAccountSel = el("toAccount"); accountSel.innerHTML = ""; toAccountSel.innerHTML = ``; for (const a of accounts) { const o1 = document.createElement("option"); o1.value = a.id; o1.textContent = `${a.name} (${a.type})`; accountSel.appendChild(o1); const o2 = document.createElement("option"); o2.value = a.id; o2.textContent = `${a.name} (${a.type})`; toAccountSel.appendChild(o2); } } function applyFilter(tx) { const { from, to, q } = filterState; let out = [...tx]; if (from) out = out.filter(x => new Date(x.occurredAt) >= new Date(from + "T00:00:00")); if (to) out = out.filter(x => new Date(x.occurredAt) <= new Date(to + "T23:59:59")); if (q) { const qq = q.toLowerCase(); out = out.filter(x => ([x.note,x.category,x.payMethod,(x.tags||[]).join(",")].join(" ").toLowerCase()).includes(qq)); } return out; } function computeBalances(allTx, accounts) { const bal = Object.fromEntries(accounts.map(a => [a.id, a.opening || 0])); for (const tx of allTx) { if (tx.kind === "INCOME") bal[tx.accountId] = (bal[tx.accountId] || 0) + tx.amount; if (tx.kind === "EXPENSE") bal[tx.accountId] = (bal[tx.accountId] || 0) - tx.amount; if (tx.kind === "TRANSFER") { bal[tx.accountId] = (bal[tx.accountId] || 0) - tx.amount; if (tx.toAccountId) bal[tx.toAccountId] = (bal[tx.toAccountId] || 0) + tx.amount; } } return bal; } function renderSummary(filteredTx) { let totalIn = 0, totalOut = 0, totalTransfer = 0; for (const tx of filteredTx) { if (tx.kind === "INCOME") totalIn += tx.amount; if (tx.kind === "EXPENSE") totalOut += tx.amount; if (tx.kind === "TRANSFER") totalTransfer += tx.amount; } const accounts = loadAccounts(); const bal = computeBalances(loadTx(), accounts); el("summary").innerHTML = `
Income: ${rupiah(totalIn)} | Expense: ${rupiah(totalOut)} | Net: ${rupiah(totalIn - totalOut)} | Transfer: ${rupiah(totalTransfer)}
Saldo per akun (semua data):
`; } function render() { refreshAccountSelects(); const allTx = loadTx(); const tx = applyFilter(allTx).sort((a,b) => new Date(b.occurredAt) - new Date(a.occurredAt)); const accounts = loadAccounts(); const accName = Object.fromEntries(accounts.map(a => [a.id, a.name])); const tbody = el("list"); tbody.innerHTML = ""; for (const item of tx) { const tr = document.createElement("tr"); tr.innerHTML = ` ${new Date(item.occurredAt).toLocaleString("id-ID")} ${item.kind} ${accName[item.accountId] || "-"} ${item.kind === "TRANSFER" ? (accName[item.toAccountId] || "-") : "-"} ${item.category || "-"} ${(item.tags || []).join(", ")} ${rupiah(item.amount)} ${item.payMethod || "-"} ${item.note || ""} `; tbody.appendChild(tr); } tbody.querySelectorAll("button[data-del]").forEach(btn => { btn.onclick = () => { const id = btn.getAttribute("data-del"); saveTx(loadTx().filter(x => x.id !== id)); render(); }; }); tbody.querySelectorAll("button[data-edit]").forEach(btn => { btn.onclick = () => startEdit(btn.getAttribute("data-edit")); }); renderSummary(tx); } function resetForm() { el("editingId").value = ""; el("submitBtn").textContent = "Tambah"; el("cancelEdit").style.display = "none"; el("form").reset(); el("occurredAt").value = nowLocalDatetimeValue(); } function startEdit(id) { const tx = loadTx().find(x => x.id === id); if (!tx) return; el("editingId").value = tx.id; el("submitBtn").textContent = "Simpan"; el("cancelEdit").style.display = "inline-block"; const d = new Date(tx.occurredAt); d.setMinutes(d.getMinutes() - d.getTimezoneOffset()); el("occurredAt").value = d.toISOString().slice(0, 16); el("kind").value = tx.kind; el("account").value = tx.accountId; el("toAccount").value = tx.toAccountId || ""; el("category").value = tx.category || ""; el("amount").value = tx.amount; el("tags").value = (tx.tags || []).join(", "); el("payMethod").value = tx.payMethod || ""; el("note").value = tx.note || ""; } el("form").addEventListener("submit", (e) => { e.preventDefault(); const kind = el("kind").value; const accountId = el("account").value; const toAccountId = el("toAccount").value || null; if (kind === "TRANSFER" && (!toAccountId || toAccountId === accountId)) { alert("Transfer butuh akun tujuan yang berbeda."); return; } const payload = { id: el("editingId").value || crypto.randomUUID(), occurredAt: new Date(el("occurredAt").value).toISOString(), kind, accountId, toAccountId, category: el("category").value.trim() || null, amount: Number(el("amount").value), tags: el("tags").value.split(",").map(s => s.trim()).filter(Boolean), payMethod: el("payMethod").value.trim() || null, note: el("note").value.trim() || null, createdAt: new Date().toISOString(), }; if (!payload.amount || payload.amount <= 0) { alert("Nominal harus > 0"); return; } const data = loadTx(); const idx = data.findIndex(x => x.id === payload.id); if (idx >= 0) data[idx] = payload; else data.push(payload); saveTx(data); resetForm(); render(); }); el("cancelEdit").onclick = () => resetForm(); el("applyFilter").onclick = () => { filterState = { from: el("from").value, to: el("to").value, q: el("q").value.trim() }; render(); }; el("resetAll").onclick = () => { if (!confirm("Yakin reset semua data?")) return; localStorage.removeItem(KEY_TX); localStorage.removeItem(KEY_ACCOUNTS); location.reload(); }; // Init resetForm(); render();