import { CATEGORIES, addExpense, calculateSummary, deleteExpense, filterExpenses, getDateRange, loadExpenses, saveExpenses, updateExpense, } from './expense-model.mjs'; const euro = new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR' }); const dateFormatter = new Intl.DateTimeFormat('es-ES', { day: 'numeric', month: 'short', year: 'numeric' }); const categorySymbols = { Alimentación: 'A', Transporte: 'T', Hogar: 'H', Ocio: 'O', Salud: 'S', Educación: 'E', Otros: '•', }; const form = document.querySelector('#expense-form'); const periodSelect = document.querySelector('#period-filter'); const categoryFilter = document.querySelector('#category-filter'); const searchInput = document.querySelector('#search-filter'); const feedback = document.querySelector('#feedback'); const formFeedback = document.querySelector('#form-feedback'); const formTitle = document.querySelector('#form-title'); const submitLabel = document.querySelector('#submit-label'); const cancelEdit = document.querySelector('#cancel-edit'); let expenses = loadExpenses(); let editingId = null; function localToday() { const now = new Date(); const offset = now.getTimezoneOffset() * 60_000; return new Date(now.getTime() - offset).toISOString().slice(0, 10); } function escapeHtml(value) { return String(value).replace(/[&<>"']/g, (character) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', })[character]); } function showMessage(element, message, type = 'danger') { element.textContent = message; element.className = `alert alert-${type} py-2 small mb-3`; element.hidden = false; } function clearMessage(element) { element.hidden = true; element.textContent = ''; } function selectedRange() { return getDateRange(periodSelect.value); } function formatDate(isoDate) { const [year, month, day] = isoDate.split('-').map(Number); return dateFormatter.format(new Date(year, month - 1, day)); } function selectedPeriodName() { return periodSelect.options[periodSelect.selectedIndex].text; } function renderSummary(summary) { document.querySelector('#total-value').textContent = euro.format(summary.total); document.querySelector('#expense-count').textContent = `${summary.count} ${summary.count === 1 ? 'gasto' : 'gastos'}`; document.querySelector('#summary-period').textContent = selectedPeriodName(); const categories = Object.entries(summary.byCategory); const list = document.querySelector('#category-breakdown'); const empty = document.querySelector('#category-empty'); if (categories.length === 0) { list.innerHTML = ''; empty.hidden = false; return; } empty.hidden = true; list.innerHTML = categories.map(([category, amount]) => { const percentage = summary.total === 0 ? 0 : Math.round((amount / summary.total) * 100); const safeCategory = escapeHtml(category); return `
  • ${safeCategory} ${euro.format(amount)}
  • `; }).join(''); } function renderExpenses(summary) { const range = selectedRange(); const visibleExpenses = filterExpenses(expenses, { ...range, category: categoryFilter.value, query: searchInput.value, }); const body = document.querySelector('#expense-rows'); const empty = document.querySelector('#expenses-empty'); const table = document.querySelector('#expense-table-wrap'); document.querySelector('#movement-count').textContent = `${visibleExpenses.length} ${visibleExpenses.length === 1 ? 'movimiento' : 'movimientos'}`; if (visibleExpenses.length === 0) { body.innerHTML = ''; table.hidden = true; empty.hidden = false; const hasFilters = Boolean(categoryFilter.value || searchInput.value.trim()); empty.querySelector('strong').textContent = hasFilters ? 'No encontramos gastos con esos filtros' : 'Todavía no hay gastos en este período'; empty.querySelector('p').textContent = hasFilters ? 'Prueba a cambiar la búsqueda o la categoría.' : 'Añade tu primer gasto y aquí tendrás todos tus movimientos a mano.'; return; } empty.hidden = true; table.hidden = false; body.innerHTML = visibleExpenses.map((expense) => ` ${escapeHtml(formatDate(expense.date))} ${escapeHtml(expense.description)} ${escapeHtml(expense.category)} ${euro.format(expense.amount)} `).join(''); } function render() { const summary = calculateSummary(expenses, selectedRange()); renderSummary(summary); renderExpenses(summary); } function persist(nextExpenses, message) { try { saveExpenses(nextExpenses); expenses = nextExpenses; clearMessage(feedback); render(); if (message) showMessage(feedback, message, 'success'); return true; } catch (error) { showMessage(formFeedback, error.message); return false; } } function resetForm() { form.reset(); form.elements.date.value = localToday(); editingId = null; formTitle.textContent = 'Añadir un gasto'; submitLabel.textContent = 'Guardar gasto'; cancelEdit.hidden = true; clearMessage(formFeedback); } function beginEdit(id) { const expense = expenses.find((item) => item.id === id); if (!expense) return; editingId = id; form.elements.amount.value = expense.amount.toFixed(2); form.elements.category.value = expense.category; form.elements.date.value = expense.date; form.elements.description.value = expense.description; formTitle.textContent = 'Editar gasto'; submitLabel.textContent = 'Guardar cambios'; cancelEdit.hidden = false; clearMessage(formFeedback); document.querySelector('#entry-card').scrollIntoView({ behavior: 'smooth', block: 'center' }); form.elements.amount.focus({ preventScroll: true }); } form.elements.date.value = localToday(); render(); form.addEventListener('submit', (event) => { event.preventDefault(); clearMessage(formFeedback); const input = { amount: form.elements.amount.value, category: form.elements.category.value, date: form.elements.date.value, description: form.elements.description.value, }; try { const next = editingId ? updateExpense(expenses, editingId, input) : addExpense(expenses, input); if (persist(next, editingId ? 'Gasto actualizado.' : 'Gasto guardado.')) resetForm(); } catch (error) { showMessage(formFeedback, error.message); } }); cancelEdit.addEventListener('click', resetForm); periodSelect.addEventListener('change', render); categoryFilter.addEventListener('change', render); searchInput.addEventListener('input', render); document.querySelector('#clear-filters').addEventListener('click', () => { categoryFilter.value = ''; searchInput.value = ''; render(); }); document.querySelector('#expense-rows').addEventListener('click', (event) => { const button = event.target.closest('button[data-action]'); if (!button) return; const { action, id } = button.dataset; if (action === 'edit') { beginEdit(id); return; } const expense = expenses.find((item) => item.id === id); if (!expense || !window.confirm(`¿Eliminar el gasto «${expense.description}»?`)) return; try { const next = deleteExpense(expenses, id); if (persist(next, 'Gasto eliminado.')) { if (editingId === id) resetForm(); } } catch (error) { showMessage(feedback, error.message); } });