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
-
Inicio: Se crea la instancia de la clase
FinanceTracker. -
Esperar DOMContentLoaded: Asegura que todo el HTML esté cargado.
-
Configurar eventos: Conecta los formularios de ingreso y gasto.
-
Inicializar gráficos: Se crean gráficos vacíos de ingresos y gastos.
-
Actualizar dashboard: Calcula totales, balance y colores.
-
Cargar transacciones: Muestra las transacciones guardadas en la pantalla.
-
Interacciones: El usuario puede:
-
Agregar transacción: Se guarda en
transactionsylocalStorage, se actualizan dashboard y gráficos, y se muestra notificación. -
Eliminar transacción: Se elimina de
transactionsylocalStorage, se actualiza dashboard y gráficos, y se muestra notificación.
-
-
Notificaciones: Mensajes flotantes informan al usuario sobre acciones realizadas.
-
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.
¿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.

Comentarios