const storageKey = "kostenverdeler:v1"; const defaultState = { people: ["Persoon 1", "Persoon 2"], expenses: [] }; const els = { personAName: document.querySelector("#personAName"), personBName: document.querySelector("#personBName"), personALabel: document.querySelector("#personALabel"), personBLabel: document.querySelector("#personBLabel"), shareALabel: document.querySelector("#shareALabel"), shareBLabel: document.querySelector("#shareBLabel"), paidBy: document.querySelector("#paidBy"), form: document.querySelector("#expenseForm"), description: document.querySelector("#description"), amount: document.querySelector("#amount"), shareA: document.querySelector("#shareA"), shareB: document.querySelector("#shareB"), totalSpent: document.querySelector("#totalSpent"), paidA: document.querySelector("#paidA"), paidB: document.querySelector("#paidB"), settlementText: document.querySelector("#settlementText"), balanceDetail: document.querySelector("#balanceDetail"), emptyState: document.querySelector("#emptyState"), expenseList: document.querySelector("#expenseList"), resetButton: document.querySelector("#resetButton"), exportButton: document.querySelector("#exportButton") }; let state = loadState(); function loadState() { try { const saved = JSON.parse(localStorage.getItem(storageKey)); if (!saved || !Array.isArray(saved.people) || !Array.isArray(saved.expenses)) { return structuredClone(defaultState); } return saved; } catch { return structuredClone(defaultState); } } function saveState() { localStorage.setItem(storageKey, JSON.stringify(state)); } function money(value) { return new Intl.NumberFormat("nl-NL", { style: "currency", currency: "EUR" }).format(value); } function cleanName(value, fallback) { return value.trim() || fallback; } function calculate() { const totals = { paid: [0, 0], owes: [0, 0], total: 0 }; for (const expense of state.expenses) { totals.total += expense.amount; totals.paid[expense.paidBy] += expense.amount; const participants = expense.participants.length ? expense.participants : [0, 1]; const share = expense.amount / participants.length; for (const personIndex of participants) { totals.owes[personIndex] += share; } } return totals; } function renderPeople() { const [a, b] = state.people; els.personAName.value = a; els.personBName.value = b; els.personALabel.textContent = `${a} betaalde`; els.personBLabel.textContent = `${b} betaalde`; els.shareALabel.textContent = a; els.shareBLabel.textContent = b; els.paidBy.innerHTML = ""; state.people.forEach((name, index) => { const option = document.createElement("option"); option.value = String(index); option.textContent = name; els.paidBy.append(option); }); } function renderSummary() { const totals = calculate(); els.totalSpent.textContent = money(totals.total); els.paidA.textContent = money(totals.paid[0]); els.paidB.textContent = money(totals.paid[1]); const balances = totals.paid.map((paid, index) => paid - totals.owes[index]); const cents = Math.round(Math.abs(balances[0]) * 100); if (state.expenses.length === 0) { els.settlementText.textContent = "Alles is in balans."; els.balanceDetail.textContent = "Voeg een uitgave toe om de verdeling te berekenen."; return; } if (cents === 0) { els.settlementText.textContent = "Alles is in balans."; els.balanceDetail.textContent = "Beide personen dragen precies evenveel bij aan hun eigen aandeel."; return; } const receiver = balances[0] > 0 ? 0 : 1; const payer = receiver === 0 ? 1 : 0; const amount = Math.abs(balances[receiver]); els.settlementText.textContent = `${state.people[payer]} betaalt ${money(amount)} aan ${state.people[receiver]}.`; els.balanceDetail.textContent = `${state.people[receiver]} heeft meer voorgeschoten dan diens aandeel. Na deze betaling staan jullie gelijk.`; } function renderExpenses() { els.expenseList.innerHTML = ""; els.emptyState.hidden = state.expenses.length > 0; const fragment = document.createDocumentFragment(); [...state.expenses].reverse().forEach((expense) => { const item = document.createElement("li"); item.className = "expense-item"; const details = document.createElement("div"); const title = document.createElement("p"); title.className = "expense-title"; title.textContent = expense.description; const participants = expense.participants.map((index) => state.people[index]).join(" + "); const meta = document.createElement("p"); meta.className = "expense-meta"; meta.textContent = `Betaald door ${state.people[expense.paidBy]} - verdeeld over ${participants}`; const amount = document.createElement("div"); amount.className = "expense-amount"; amount.textContent = money(expense.amount); const button = document.createElement("button"); button.className = "delete-button"; button.type = "button"; button.title = "Verwijderen"; button.setAttribute("aria-label", `${expense.description} verwijderen`); button.textContent = "x"; button.addEventListener("click", () => { state.expenses = state.expenses.filter((itemExpense) => itemExpense.id !== expense.id); saveState(); render(); }); details.append(title, meta); item.append(details, amount, button); fragment.append(item); }); els.expenseList.append(fragment); } function render() { renderPeople(); renderSummary(); renderExpenses(); } function updateNames() { state.people = [ cleanName(els.personAName.value, "Persoon 1"), cleanName(els.personBName.value, "Persoon 2") ]; saveState(); render(); } function parseAmount(value) { return Number(value.replace(",", ".")); } function createId() { if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { return crypto.randomUUID(); } return `${Date.now()}-${Math.random().toString(16).slice(2)}`; } els.personAName.addEventListener("change", updateNames); els.personBName.addEventListener("change", updateNames); els.form.addEventListener("submit", (event) => { event.preventDefault(); const participants = []; if (els.shareA.checked) participants.push(0); if (els.shareB.checked) participants.push(1); if (participants.length === 0) { els.form.classList.add("warn"); setTimeout(() => els.form.classList.remove("warn"), 500); return; } const amount = parseAmount(els.amount.value); if (!Number.isFinite(amount) || amount <= 0) return; const description = els.description.value.trim(); if (!description) { els.description.focus(); return; } state.expenses.push({ id: createId(), description, amount: Math.round(amount * 100) / 100, paidBy: Number(els.paidBy.value), participants, createdAt: new Date().toISOString() }); saveState(); els.form.reset(); els.shareA.checked = true; els.shareB.checked = true; render(); els.description.focus(); }); els.resetButton.addEventListener("click", () => { const confirmed = window.confirm("Weet je zeker dat je alle namen en uitgaven wilt wissen?"); if (!confirmed) return; state = structuredClone(defaultState); saveState(); render(); }); els.exportButton.addEventListener("click", () => { const header = ["Omschrijving", "Bedrag", "Betaald door", "Verdeeld over", "Datum"]; const rows = state.expenses.map((expense) => [ expense.description, expense.amount.toFixed(2).replace(".", ","), state.people[expense.paidBy], expense.participants.map((index) => state.people[index]).join(" + "), new Date(expense.createdAt).toLocaleString("nl-NL") ]); const csv = [header, ...rows] .map((row) => row.map((cell) => `"${String(cell).replaceAll('"', '""')}"`).join(";")) .join("\n"); const blob = new Blob([csv], { type: "text/csv;charset=utf-8" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = "kostenverdeler.csv"; link.click(); URL.revokeObjectURL(url); }); render();