Script para registrar Ingresos y Gastos con HTML, Bootstrap 5 y JavaScript

Compartir

En este tutorial crearás un pequeño script que permite registrar ingresos y gastos, mostrarlos en un historial, calcular totales y mantener los datos en el navegador usando localStorage. Todo está separado en 3 archivos: index.html, style.css y script.js.

¿Qué vas a aprender?

  • Estructurar una interfaz simple con Bootstrap 5.
  • Manejar formularios y eventos en JavaScript.
  • Persistir datos en el navegador con localStorage.
  • Mostrar y eliminar transacciones, y calcular total de ingresos, gastos y balance.

Requisitos previos

  • Un navegador moderno (Chrome, Firefox, Edge, Safari).
  • Nada de servidor: todo funciona abriendo index.html localmente.

Estructura de archivos

/control-finanzas ├─ index.html ├─ style.css └─ script.js

1) index.html (estructura y diseño)

Aquí definimos la estructura visual: header, formulario para agregar transacciones, tabla de historial y resumen.

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>FinanceTracker - Dashboard</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
	<link href="style.css" rel="stylesheet">
</head>
<body>
    <!-- Header -->
    <header class="gradient-header text-white py-4 mb-4">
        <div class="container">
            <div class="row align-items-center">
                <div class="col">
                    <h1 class="h2 mb-0">
                        <span class="emoji-icon"></span>
                        FinanceTracker
                    </h1>
                </div>

</div> </div> </header>

<div class="container"> <!-- Resumen Principal --> <div class="row g-4 mb-4"> <div class="col-md-4"> <div class="stat-card income-card"> <div class="d-flex justify-content-between align-items-center"> <div> <p class="mb-1 opacity-75">Ingresos Totales</p> <h3 class="mb-0" id="totalIncome">$0</h3> </div> <span class="emoji-icon"></span> </div> </div> </div>

<div class="col-md-4"> <div class="stat-card expense-card"> <div class="d-flex justify-content-between align-items-center"> <div> <p class="mb-1 opacity-75">Gastos Totales</p> <h3 class="mb-0" id="totalExpenses">$0</h3> </div> <span class="emoji-icon"></span> </div> </div> </div>

<div class="col-md-4"> <div class="stat-card balance-card" id="balanceCard"> <div class="d-flex justify-content-between align-items-center"> <div> <p class="mb-1 opacity-75">Balance</p> <h3 class="mb-0" id="balance">$0</h3> </div> <span class="emoji-icon"></span> </div> </div> </div> </div>

<!-- Formularios --> <div class="row g-4 mb-4"> <!-- Agregar Ingreso --> <div class="col-lg-6"> <div class="card card-custom"> <div class="card-body"> <h5 class="card-title mb-4"> <span class="text-success me-2"></span> Agregar Ingreso </h5> <form id="incomeForm"> <div class="mb-3"> <input type="number" class="form-control" id="incomeAmount" placeholder="Cantidad" step="0.01" required> </div> <div class="mb-3"> <select class="form-select" id="incomeCategory" required> <option value="">Seleccionar categoría</option> <option value="salario"> Salario</option> <option value="freelance"> Freelance</option> <option value="inversiones"> Inversiones</option> <option value="ventas">️ Ventas</option> <option value="otros"> Otros</option> </select> </div> <div class="mb-3"> <input type="text" class="form-control" id="incomeDescription" placeholder="Descripción" required> </div> <button type="submit" class="btn btn-success w-100 btn-custom"> <i class="bi bi-plus-circle"></i> Agregar Ingreso </button> </form> </div> </div> </div>

<!-- Agregar Gasto --> <div class="col-lg-6"> <div class="card card-custom"> <div class="card-body"> <h5 class="card-title mb-4"> <span class="text-danger me-2"></span> Agregar Gasto </h5> <form id="expenseForm"> <div class="mb-3"> <input type="number" class="form-control" id="expenseAmount" placeholder="Cantidad" step="0.01" required> </div> <div class="mb-3"> <select class="form-select" id="expenseCategory" required> <option value="">Seleccionar categoría</option> <option value="comida"> Comida</option> <option value="transporte"> Transporte</option> <option value="entretenimiento"> Entretenimiento</option> <option value="servicios"> Servicios</option> <option value="compras"> Compras</option> <option value="salud">⚕️ Salud</option> <option value="otros"> Otros</option> </select> </div> <div class="mb-3"> <input type="text" class="form-control" id="expenseDescription" placeholder="Descripción" required> </div> <button type="submit" class="btn btn-danger w-100 btn-custom"> <i class="bi bi-plus-circle"></i> Agregar Gasto </button> </form> </div> </div> </div> </div>

<!-- Gráficos --> <div class="row g-4 mb-4"> <div class="col-lg-6"> <div class="card card-custom"> <div class="card-body"> <h5 class="card-title"> Ingresos por Categoría</h5> <div class="chart-container"> <canvas id="incomeChart"></canvas> </div> </div> </div> </div>

<div class="col-lg-6"> <div class="card card-custom"> <div class="card-body"> <h5 class="card-title"> Gastos por Categoría</h5> <div class="chart-container"> <canvas id="expenseChart"></canvas> </div> </div> </div> </div> </div>

<!-- Historial de Transacciones --> <div class="card card-custom mb-4"> <div class="card-body"> <div class="d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center mb-4"> <h5 class="card-title mb-2 mb-md-0"> Historial de Transacciones</h5> <div class="btn-group" role="group"> <button type="button" class="btn btn-outline-primary btn-sm filter-btn active" onclick="filterTransactions('all', this)">Todas</button> <button type="button" class="btn btn-outline-success btn-sm filter-btn" onclick="filterTransactions('income', this)">Ingresos</button> <button type="button" class="btn btn-outline-danger btn-sm filter-btn" onclick="filterTransactions('expense', this)">Gastos</button> </div> </div> <div id="transactionsList" style="max-height: 400px; overflow-y: auto;"> <div class="text-center text-muted py-5"> <span class="emoji-icon"></span> <p class="mt-2">No hay transacciones aún. ¡Agrega tu primera transacción!</p> </div> </div> </div> </div> </div>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="script.js"></script> </body> </html>

2) style.css (estilos mínimos)

En este ejemplo no necesitas mucho CSS: Bootstrap se encarga del diseño. Aquí solo añadimos detalles estéticos.

        body {
            box-sizing: border-box;
            background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }

.gradient-header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }

.card-custom { border: none; border-radius: 15px; box-shadow: 0 8px 25px rgba(0,0,0,0.1); transition: transform 0.3s ease; }

.card-custom:hover { transform: translateY(-5px); }

.stat-card { background: linear-gradient(135deg, var(--bs-primary) 0%, var(--bs-primary-dark) 100%); color: white; border-radius: 15px; padding: 1.5rem; }

.income-card { background: linear-gradient(135deg, #28a745 0%, #20c997 100%); }

.expense-card { background: linear-gradient(135deg, #dc3545 0%, #fd7e14 100%); }

.balance-card { background: linear-gradient(135deg, #007bff 0%, #6f42c1 100%); }

.balance-positive { background: linear-gradient(135deg, #28a745 0%, #20c997 100%); }

.balance-negative { background: linear-gradient(135deg, #dc3545 0%, #fd7e14 100%); }

.btn-custom { border-radius: 10px; font-weight: 600; transition: all 0.3s ease; }

.btn-custom:hover { transform: scale(1.05); }

.transaction-item { border-radius: 10px; transition: all 0.3s ease; }

.transaction-item:hover { background-color: #f8f9fa; transform: translateX(5px); }

.notification { position: fixed; top: 20px; right: 20px; z-index: 1050; border-radius: 10px; animation: slideIn 0.3s ease; }

@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }

.chart-container { position: relative; height: 300px; }

.emoji-icon { font-size: 2rem; }

.filter-btn.active { box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.25); }

3) script.js (lógica completa)

Este archivo implementa:

  • Carga y guarda en localStorage.
  • Renderizado de la lista de transacciones.
  • Agregar y eliminar transacciones.
  • Cálculo de totales y balance.
  • Notificaciones sencillas con clases de Bootstrap.
// Estado de la aplicación
        class FinanceTracker {
            constructor() {
                this.transactions = JSON.parse(localStorage.getItem('transactions')) || [];
                this.incomeChart = null;
                this.expenseChart = null;
                this.isInitialized = false;

this.init(); }

init() { if (this.isInitialized) return;

document.addEventListener('DOMContentLoaded', () => { this.setupEventListeners(); this.initCharts(); this.updateDashboard(); this.loadTransactions(); this.isInitialized = true; }); }

setupEventListeners() { // Formulario de ingresos document.getElementById('incomeForm').addEventListener('submit', (e) => { e.preventDefault(); this.addTransaction('income'); });

// Formulario de gastos document.getElementById('expenseForm').addEventListener('submit', (e) => { e.preventDefault(); this.addTransaction('expense'); }); }

addTransaction(type) { const amountId = type === 'income' ? 'incomeAmount' : 'expenseAmount'; const categoryId = type === 'income' ? 'incomeCategory' : 'expenseCategory'; const descriptionId = type === 'income' ? 'incomeDescription' : 'expenseDescription'; const formId = type === 'income' ? 'incomeForm' : 'expenseForm';

const amount = parseFloat(document.getElementById(amountId).value); const category = document.getElementById(categoryId).value; const description = document.getElementById(descriptionId).value;

if (!amount || !category || !description) return;

const transaction = { id: Date.now(), type: type, amount: amount, category: category, description: description, date: new Date().toLocaleDateString('es-ES') };

this.transactions.push(transaction); this.saveTransactions(); this.updateDashboard(); this.loadTransactions();

// Limpiar formulario document.getElementById(formId).reset();

// Mostrar notificación const message = type === 'income' ? '✅ Ingreso agregado correctamente' : '✅ Gasto agregado correctamente'; this.showNotification(message, 'success'); }

updateDashboard() { const totalIncome = this.transactions .filter(t => t.type === 'income') .reduce((sum, t) => sum + t.amount, 0);

const totalExpenses = this.transactions .filter(t => t.type === 'expense') .reduce((sum, t) => sum + t.amount, 0);

const balance = totalIncome - totalExpenses;

// Actualizar elementos del DOM const incomeEl = document.getElementById('totalIncome'); const expensesEl = document.getElementById('totalExpenses'); const balanceEl = document.getElementById('balance'); const balanceCard = document.getElementById('balanceCard');

if (incomeEl) incomeEl.textContent = `$${totalIncome.toLocaleString()}`; if (expensesEl) expensesEl.textContent = `$${totalExpenses.toLocaleString()}`; if (balanceEl) balanceEl.textContent = `$${balance.toLocaleString()}`;

// Cambiar color del balance if (balanceCard) { balanceCard.className = balanceCard.className.replace(/balance-(positive|negative)/, ''); if (balance >= 0) { balanceCard.classList.add('balance-positive'); } else { balanceCard.classList.add('balance-negative'); } }

this.updateCharts(); }

initCharts() { const incomeCtx = document.getElementById('incomeChart'); const expenseCtx = document.getElementById('expenseChart');

if (!incomeCtx || !expenseCtx) return;

this.incomeChart = new Chart(incomeCtx, { type: 'doughnut', data: { labels: [], datasets: [{ data: [], backgroundColor: [ '#28a745', '#20c997', '#17a2b8', '#6f42c1', '#fd7e14' ], borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom' } } } });

this.expenseChart = new Chart(expenseCtx, { type: 'doughnut', data: { labels: [], datasets: [{ data: [], backgroundColor: [ '#dc3545', '#fd7e14', '#ffc107', '#e83e8c', '#6f42c1', '#20c997', '#17a2b8' ], borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom' } } } }); }

updateCharts() { if (!this.incomeChart || !this.expenseChart) return;

// Gráfico de ingresos const incomeByCategory = {}; this.transactions .filter(t => t.type === 'income') .forEach(t => { incomeByCategory[t.category] = (incomeByCategory[t.category] || 0) + t.amount; });

this.incomeChart.data.labels = Object.keys(incomeByCategory); this.incomeChart.data.datasets[0].data = Object.values(incomeByCategory); this.incomeChart.update('none');

// Gráfico de gastos const expenseByCategory = {}; this.transactions .filter(t => t.type === 'expense') .forEach(t => { expenseByCategory[t.category] = (expenseByCategory[t.category] || 0) + t.amount; });

this.expenseChart.data.labels = Object.keys(expenseByCategory); this.expenseChart.data.datasets[0].data = Object.values(expenseByCategory); this.expenseChart.update('none'); }

loadTransactions() { const container = document.getElementById('transactionsList'); if (!container) return;

if (this.transactions.length === 0) { container.innerHTML = ` <div class="text-center text-muted py-5"> <span class="emoji-icon"></span> <p class="mt-2">No hay transacciones aún. ¡Agrega tu primera transacción!</p> </div> `; return; }

const sortedTransactions = [...this.transactions].sort((a, b) => b.id - a.id);

container.innerHTML = sortedTransactions.map(transaction => ` <div class="transaction-item border rounded p-3 mb-2" data-type="${transaction.type}"> <div class="d-flex justify-content-between align-items-center"> <div class="d-flex align-items-center"> <div class="me-3"> <span class="badge ${transaction.type === 'income' ? 'bg-success' : 'bg-danger'} rounded-pill"> ${transaction.type === 'income' ? '' : ''} </span> </div> <div> <h6 class="mb-1">${transaction.description}</h6> <small class="text-muted">${transaction.category} • ${transaction.date}</small> </div> </div> <div class="d-flex align-items-center"> <span class="fw-bold me-2 ${transaction.type === 'income' ? 'text-success' : 'text-danger'}"> ${transaction.type === 'income' ? '+' : '-'}$${transaction.amount.toLocaleString()} </span> <button class="btn btn-outline-danger btn-sm" onclick="app.deleteTransaction(${transaction.id})"> <i class="bi bi-trash"></i> </button> </div> </div> </div> `).join(''); }

deleteTransaction(id) { if (confirm('¿Estás seguro de que quieres eliminar esta transacción?')) { this.transactions = this.transactions.filter(t => t.id !== id); this.saveTransactions(); this.updateDashboard(); this.loadTransactions(); this.showNotification('️ Transacción eliminada', 'info'); } }

saveTransactions() { localStorage.setItem('transactions', JSON.stringify(this.transactions)); }

showNotification(message, type) { const alertClass = type === 'success' ? 'alert-success' : type === 'error' ? 'alert-danger' : 'alert-info';

const notification = document.createElement('div'); notification.className = `alert ${alertClass} notification`; notification.textContent = message;

document.body.appendChild(notification);

setTimeout(() => { notification.remove(); }, 3000); } }

// Funciones globales function filterTransactions(type, button) { const items = document.querySelectorAll('.transaction-item'); const buttons = document.querySelectorAll('.filter-btn');

// Actualizar botones activos buttons.forEach(btn => btn.classList.remove('active')); button.classList.add('active');

items.forEach(item => { if (type === 'all' || item.dataset.type === type) { item.style.display = 'block'; } else { item.style.display = 'none'; } }); }

function toggleTheme() { document.body.classList.toggle('dark-theme'); app.showNotification(' Tema cambiado', 'info'); }

// Inicializar aplicación const app = new FinanceTracker();

 

Explicación del flujo

  1. Inicio: Se crea la instancia de la clase FinanceTracker.

  2. Esperar DOMContentLoaded: Asegura que todo el HTML esté cargado.

  3. Configurar eventos: Conecta los formularios de ingreso y gasto.

  4. Inicializar gráficos: Se crean gráficos vacíos de ingresos y gastos.

  5. Actualizar dashboard: Calcula totales, balance y colores.

  6. Cargar transacciones: Muestra las transacciones guardadas en la pantalla.

  7. Interacciones: El usuario puede:

    • Agregar transacción: Se guarda en transactions y localStorage, se actualizan dashboard y gráficos, y se muestra notificación.

    • Eliminar transacción: Se elimina de transactions y localStorage, se actualiza dashboard y gráficos, y se muestra notificación.

  8. Notificaciones: Mensajes flotantes informan al usuario sobre acciones realizadas.

  9. Filtros: Opcionalmente, el usuario puede filtrar transacciones.

Este ejemplo  demuestra cómo con  HTML, CSS y JavaScript, apoyados en Bootstrap 5, se puede crear un script funcional para el control básico de gastos e ingresos. Puedes mejorar este sistema incorporando almacenamiento en una base de datos  MySQL, exportación de datos Excel o PDF para usos más avanzados.

Demo

¿Cansado del caos financiero? Necesitas un control serio.

Te ofrecemos un Sistema Web profesional para gestionar tus finanzas, desarrollado con la seguridad y robustez de PHP y MySQL.

Obed Alvarado

Sobre el autor

Artículo redactado por Obed Alvarado. Comparto experiencias prácticas sobre PHP, MySQL, aplicaciones web, instalación en hosting y herramientas para negocios.

Comentarios