Código incluido: index.html style.css script.js
Aprende a implementar un pequeño sistema que registra deudas, permite registrar abonos y muestra el progreso general y por deuda. El código usa Bootstrap 5, Chart.js para gráficos y jsPDF para exportar en PDF.
Introducción
Este script es ideal para llevar un control personal o un prototipo de gestión de deudas. Guarda los datos en localStorage, permite:
- Registrar nuevas deudas (acreedor, monto, fecha límite, descripción).
- Editar la deuda (con validación para no reducir el total por debajo de lo ya pagado).
- Registrar abonos y ver historial de pagos por deuda.
- Ver progreso visual (barra y círculo) y gráfica de distribución.
- Exportar a CSV y PDF.
Tecnologías y requisitos
- Editor de texto (VSCode, Sublime, Notepad++)
- Navegador moderno (Chrome, Firefox, Edge)
- Bootstrap 5 (CDN incluido en el ejemplo)
- Chart.js para gráficos (CDN incluido)
- jsPDF para exportar a PDF (CDN incluido)
Guía paso a paso
Estructura de archivos
Crea una carpeta del proyecto y dentro agrega 3 archivos:
- index.html — Interfaz y carga de librerías.
- style.css — Estilos personalizados (estéticos, responsivos).
- script.js — Lógica completa (manejo de localStorage, modales, gráficos, export).
Paso 1 — Preparar index.html
- Incluye Bootstrap 5 (CSS + bundle JS) al final de la página. - Añade Chart.js y jsPDF (se usan para gráficos y PDF). - Crea la estructura: cabecera, estadísticas, secciones para gráficos y la lista de deudas. - Agrega los modales (Agregar, Editar, Pagar, Historial).
Consejo: carga script.js al final del <body> para que el DOM esté listo.
Paso 2 — Estilos ( style.css )
- Aquí se define colores de marca, bordes redondeados y estilos para tarjetas, progreso y botones personalizados. - Se mantiene la compatibilidad móvil con media queries; Bootstrap 5 ya cubre la mayoría de la responsividad.
Paso 3 — Lógica principal ( script.js )
El flujo general es:
- Al iniciar la página, carga las deudas desde localStorage (si existe).
- Renderiza estadísticas, lista de deudas y gráficos.
- Al agregar una deuda, se valida el formulario, se guarda al array y a localStorage, se actualiza la UI.
- Al registrar un pago, se actualiza la deuda y se añade el pago al historial (array de pagos dentro del objeto deuda).
- Funciones auxiliares importantes: validaciones, cálculo de progreso, exportar CSV/PDF, y notificaciones (alerts).
Paso 4 — Validaciones y casos especiales
- Impedir abonos mayores al saldo pendiente (el script ya tiene esa comprobación).
- Al editar una deuda, validar que el nuevo total no quede por debajo de lo ya pagado.
- Gestionar fechas vencidas y mostrar alertas para deudas próximas a vencer o vencidas.
- Evitar IDs duplicados: el script usa Date.now() como ID.
Paso 5 — Pruebas y depuración
- Abre la consola del navegador (F12) durante las pruebas para ver mensajes console.log. - Prueba los siguientes flujos:
- Agregar 3 deudas, realizar pagos parciales y ver cambios en barra y gráfico.
- Eliminar una deuda.
- Editar la deuda a un monto menor al ya pagado, debe mostrar un mensaje de error: (El monto total no puede ser menor al monto ya pagado).
- Exportar CSV y PDF con varias deudas largas para confirmar el funcionamiento del PDF y la exportación CSV.
Paso 6 — Mejoras (para próximas versiones lo que hare):
- Guardar datos en servidor con PHP/MySQL para multiusuario y persistencia real.
- Autenticación de usuarios y cuentas separadas.
- Filtros y búsquedas en la lista de deudas.
- Adicionar export a Excel (XLSX) y mejoras en el diseño de PDF (tablas).
Código (copiar/pegar)
Abajo tienes los tres archivos completos tal como funcionan en el proyecto. Pégalos en tus archivos correspondientes.
index.html
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Debt Tracker - Control de Deudas</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="style.css" rel="stylesheet">
</head>
<body>
<div class="container-fluid">
<div class="dashboard-container">
<!-- Header Section -->
<div class="header-section">
<div class="row align-items-center">
<div class="col-md-6">
<h1 class="mb-0"><i class="fas fa-chart-line me-3"></i>Debt Tracker</h1>
<p class="mb-0 opacity-75">Control inteligente de tus deudas</p>
</div>
<div class="col-md-6 text-md-end mt-3 mt-md-0">
<button class="btn btn-light btn-lg" data-bs-toggle="modal" data-bs-target="#addDebtModal">
<i class="fas fa-plus me-2"></i>Nueva Deuda
</button>
</div>
</div>
<!-- Stats Cards -->
<div class="row mt-4">
<div class="col-md-3 col-6">
<div class="stat-card">
<i class="fas fa-credit-card fa-2x mb-2"></i>
<h4 id="totalDebts">0</h4>
<small>Total Deudas</small>
</div>
</div>
<div class="col-md-3 col-6">
<div class="stat-card">
<i class="fas fa-dollar-sign fa-2x mb-2"></i>
<h4 id="totalAmount">$0</h4>
<small>Monto Total</small>
</div>
</div>
<div class="col-md-3 col-6">
<div class="stat-card">
<i class="fas fa-check-circle fa-2x mb-2"></i>
<h4 id="totalPaid">$0</h4>
<small>Total Pagado</small>
</div>
</div>
<div class="col-md-3 col-6">
<div class="stat-card">
<i class="fas fa-clock fa-2x mb-2"></i>
<h4 id="totalPending">$0</h4>
<small>Pendiente</small>
</div>
</div>
</div>
</div>
<!-- Main Content -->
<div class="p-4">
<!-- Alerts Section -->
<div id="alertsContainer"></div>
<!-- Charts Section -->
<div class="row mb-4">
<div class="col-md-6">
<div class="chart-container">
<h5><i class="fas fa-chart-pie me-2"></i>Distribución de Deudas</h5>
<canvas id="debtChart" width="400" height="200"></canvas>
</div>
</div>
<div class="col-md-6">
<div class="chart-container">
<h5><i class="fas fa-chart-bar me-2"></i>Progreso General</h5>
<div class="text-center">
<div class="progress-circle mb-3" style="width: 150px; height: 150px; margin: 0 auto;">
<!-- Círculo de progreso -->
<svg width="150" height="150">
<!-- Fondo gris -->
<circle cx="75" cy="75" r="65" fill="none" stroke="#e9ecef" stroke-width="10"/>
<!-- Barra de progreso -->
<circle id="progressCircle" cx="75" cy="75" r="65" fill="none" stroke="url(#gradient)" stroke-width="10"
stroke-dasharray="408.4" stroke-dashoffset="408.4" transform="rotate(-90 75 75)"/>
<!-- Gradiente -->
<defs>
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#667eea"/>
<stop offset="100%" style="stop-color:#764ba2"/>
</linearGradient>
</defs>
</svg>
</div>
<!-- Barra de progreso -->
<div class="progress" style="height: 30px;">
<div id="progressBar" class="progress-bar progress-bar-custom" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
0%
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Export Buttons -->
<div class="row mb-3">
<div class="col-12 text-end">
<button class="btn btn-outline-primary me-2" onclick="exportToCSV()">
<i class="fas fa-file-csv me-2"></i>Exportar CSV
</button>
<button class="btn btn-outline-danger" onclick="exportToPDF()">
<i class="fas fa-file-pdf me-2"></i>Exportar PDF
</button>
</div>
</div>
<!-- Debts List -->
<div class="row">
<div class="col-12">
<h4 class="mb-3"><i class="fas fa-list me-2"></i>Mis Deudas</h4>
<div id="debtsList"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Add Debt Modal -->
<div class="modal fade" id="addDebtModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-plus me-2"></i>Nueva Deuda</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addDebtForm">
<div class="mb-3">
<label class="form-label">Acreedor</label>
<input type="text" class="form-control" id="creditorName" required>
</div>
<div class="mb-3">
<label class="form-label">Monto Total</label>
<input type="number" class="form-control" id="newDebtAmount" step="0.01" required>
</div>
<div class="mb-3">
<label class="form-label">Fecha Límite</label>
<input type="date" class="form-control" id="dueDate" required>
</div>
<div class="mb-3">
<label class="form-label">Descripción (Opcional)</label>
<textarea class="form-control" id="description" rows="3"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn-primary-custom" onclick="addDebt()">Agregar Deuda</button>
</div>
</div>
</div>
</div>
<!-- Edit Debt Modal -->
<div class="modal fade" id="editDebtModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-edit me-2"></i>Editar Deuda</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="editDebtForm">
<div class="mb-3">
<label class="form-label">Acreedor</label>
<input type="text" class="form-control" id="editCreditorName" required>
</div>
<div class="mb-3">
<label class="form-label">Monto Total</label>
<input type="number" class="form-control" id="editTotalAmount" step="0.01" required>
</div>
<div class="mb-3">
<label class="form-label">Fecha Límite</label>
<input type="date" class="form-control" id="editDueDate" required>
</div>
<div class="mb-3">
<label class="form-label">Descripción (Opcional)</label>
<textarea class="form-control" id="editDescription" rows="3"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn-primary-custom" onclick="updateDebt()">Actualizar Deuda</button>
</div>
</div>
</div>
</div>
<!-- Payment Modal -->
<div class="modal fade" id="paymentModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-money-bill me-2"></i>Registrar Abono</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="alert alert-info mb-3">
<i class="fas fa-info-circle me-2"></i>
<span id="debtInfo">Información de la deuda</span>
</div>
<form id="paymentForm">
<div class="mb-3">
<label class="form-label">Monto del Abono</label>
<input type="number" class="form-control" id="paymentAmount" step="0.01" required>
<div class="form-text" id="remainingAmount">Saldo pendiente: $0.00</div>
</div>
<div class="mb-3">
<label class="form-label">Fecha del Abono</label>
<input type="date" class="form-control" id="paymentDate" required>
</div>
<div class="mb-3">
<label class="form-label">Notas (Opcional)</label>
<textarea class="form-control" id="paymentNotes" rows="2" placeholder="Ej: Pago parcial, abono mensual, etc."></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn-success-custom" onclick="addPayment()">Registrar Abono</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="script.js"></script>
</body>
</html>
style.css
body {
box-sizing: border-box;
font-family: 'Inter', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.dashboard-container {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
margin: 20px auto;
max-width: 1200px;
}
.header-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 20px 20px 0 0;
padding: 2rem;
}
.stat-card {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 15px;
padding: 1.5rem;
text-align: center;
transition: transform 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
}
.debt-card {
background: white;
border-radius: 15px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
border: none;
margin-bottom: 1rem;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.debt-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
}
.progress-custom {
height: 8px;
border-radius: 10px;
background-color: #e9ecef;
}
.progress-bar-custom {
border-radius: 10px;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
}
.btn-primary-custom {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 10px;
padding: 0.75rem 1.5rem;
font-weight: 500;
transition: all 0.3s ease;
}
.btn-primary-custom:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn-success-custom {
background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%);
border: none;
border-radius: 8px;
padding: 0.5rem 1rem;
font-weight: 500;
}
.btn-danger-custom {
background: linear-gradient(135deg, #ff416c 0%, #ff4b2b 100%);
border: none;
border-radius: 8px;
padding: 0.5rem 1rem;
font-weight: 500;
}
.modal-content {
border-radius: 15px;
border: none;
}
.modal-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 15px 15px 0 0;
}
.form-control, .form-select {
border-radius: 10px;
border: 2px solid #e9ecef;
padding: 0.75rem;
transition: border-color 0.3s ease;
}
.form-control:focus, .form-select:focus {
border-color: #667eea;
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
}
.alert-custom {
border-radius: 10px;
border: none;
padding: 1rem;
margin-bottom: 1rem;
}
.alert-warning-custom {
background: linear-gradient(135deg, #ffeaa7 0%, #fab1a0 100%);
color: #2d3436;
}
.chart-container {
background: white;
border-radius: 15px;
padding: 1.5rem;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
margin-bottom: 1rem;
}
@media (max-width: 768px) {
.dashboard-container {
margin: 10px;
border-radius: 15px;
}
.header-section {
padding: 1.5rem;
border-radius: 15px 15px 0 0;
}
.stat-card {
margin-bottom: 1rem;
}
}
svg {
display: block;
margin: 30px auto;
}
#debtChart {
max-width: 250px;
max-height: 250px;
margin: 0 auto; /* Centrar */
}
.chart-container{
min-height:350px;
}
script.js
let debts = JSON.parse(localStorage.getItem('debts')) || [];
let currentDebtId = null;
// Initialize app
document.addEventListener('DOMContentLoaded', function() {
updateDashboard();
// Set today's date as default for payment
document.getElementById('paymentDate').value = new Date().toISOString().split('T')[0];
});
function addDebt() {
console.log('Función addDebt ejecutada'); // Debug
try {
const creditorValue = document.getElementById('creditorName').value.trim();
const totalAmountValue = document.getElementById('newDebtAmount').value.trim();
const dueDateValue = document.getElementById('dueDate').value.trim();
const descriptionValue = document.getElementById('description').value.trim();
console.log('Valores del formulario:', {
creditor: creditorValue,
totalAmount: totalAmountValue,
dueDate: dueDateValue,
description: descriptionValue
});
// Validate that we have the required values
if (!creditorValue) {
showAlert('Por favor ingresa el nombre del acreedor', 'danger');
return;
}
if (!totalAmountValue) {
showAlert('Por favor ingresa el monto total', 'danger');
return;
}
if (!dueDateValue) {
showAlert('Por favor selecciona la fecha límite', 'danger');
return;
}
const totalAmountNumber = parseFloat(totalAmountValue);
if (isNaN(totalAmountNumber) || totalAmountNumber <= 0) {
showAlert('El monto total debe ser un número válido mayor a 0', 'danger');
return;
}
const debt = {
id: Date.now(),
creditor: creditorValue,
totalAmount: totalAmountNumber,
paidAmount: 0,
dueDate: dueDateValue,
description: descriptionValue,
payments: [],
createdAt: new Date().toISOString()
};
debts.push(debt);
localStorage.setItem('debts', JSON.stringify(debts));
console.log('Deuda guardada exitosamente:', debt);
// Close modal and reset form
const modal = bootstrap.Modal.getInstance(document.getElementById('addDebtModal'));
if (modal) {
modal.hide();
}
document.getElementById('addDebtForm').reset();
updateDashboard();
showAlert('Deuda agregada exitosamente', 'success');
} catch (error) {
console.error('Error en addDebt:', error);
showAlert('Error al procesar la deuda: ' + error.message, 'danger');
}
}
function deleteDebt(id) {
if (confirm('¿Estás seguro de que quieres eliminar esta deuda?')) {
debts = debts.filter(debt => debt.id !== id);
localStorage.setItem('debts', JSON.stringify(debts));
updateDashboard();
showAlert('Deuda eliminada exitosamente', 'success');
}
}
function openEditModal(id) {
currentDebtId = id;
const debt = debts.find(d => d.id === id);
if (!debt) {
showAlert('Error: No se encontró la deuda', 'danger');
return;
}
console.log('Deuda encontrada para editar:', debt); // Para debug
// Clear form first
const form = document.getElementById('editDebtForm');
form.reset();
// Wait a moment for form to reset, then populate
setTimeout(() => {
document.getElementById('editCreditorName').value = debt.creditor || '';
document.getElementById('editTotalAmount').value = debt.totalAmount || '';
document.getElementById('editDueDate').value = debt.dueDate || '';
document.getElementById('editDescription').value = debt.description || '';
console.log('Valores cargados en formulario:', {
creditor: document.getElementById('editCreditorName').value,
totalAmount: document.getElementById('editTotalAmount').value,
dueDate: document.getElementById('editDueDate').value,
description: document.getElementById('editDescription').value
}); // Para debug
}, 100);
const modal = new bootstrap.Modal(document.getElementById('editDebtModal'));
modal.show();
}
function updateDebt() {
const form = document.getElementById('editDebtForm');
if (!form.checkValidity()) {
form.reportValidity();
return;
}
if (!currentDebtId) {
showAlert('Error: No se ha seleccionado ninguna deuda', 'danger');
return;
}
const debtIndex = debts.findIndex(d => d.id === currentDebtId);
if (debtIndex === -1) {
showAlert('Error: No se encontró la deuda', 'danger');
return;
}
const debt = debts[debtIndex];
const newTotalAmount = parseFloat(document.getElementById('editTotalAmount').value);
const currentPaidAmount = parseFloat(debt.paidAmount) || 0;
// Check if new total amount is less than already paid amount
if (newTotalAmount < currentPaidAmount) {
showAlert(`El monto total no puede ser menor al monto ya pagado ($${currentPaidAmount.toFixed(2)})`, 'danger');
return;
}
// Update debt information directly in the array
debts[debtIndex].creditor = document.getElementById('editCreditorName').value.trim();
debts[debtIndex].totalAmount = newTotalAmount;
debts[debtIndex].dueDate = document.getElementById('editDueDate').value;
debts[debtIndex].description = document.getElementById('editDescription').value.trim();
// Save to localStorage
try {
localStorage.setItem('debts', JSON.stringify(debts));
// Close modal and reset form
const modal = bootstrap.Modal.getInstance(document.getElementById('editDebtModal'));
if (modal) {
modal.hide();
}
form.reset();
currentDebtId = null;
showAlert('Deuda actualizada exitosamente', 'success');
updateDashboard();
} catch (error) {
console.error('Error saving to localStorage:', error);
showAlert('Error al guardar los datos', 'danger');
return;
}
}
function openPaymentModal(id) {
currentDebtId = id;
const debt = debts.find(d => d.id === id);
const remaining = debt.totalAmount - debt.paidAmount;
// Update debt info in modal
document.getElementById('debtInfo').textContent = `${debt.creditor} - Total: $${debt.totalAmount.toFixed(2)}`;
document.getElementById('remainingAmount').textContent = `Saldo pendiente: $${remaining.toFixed(2)}`;
document.getElementById('paymentAmount').max = remaining;
document.getElementById('paymentAmount').placeholder = `Máximo: $${remaining.toFixed(2)}`;
const modal = new bootstrap.Modal(document.getElementById('paymentModal'));
modal.show();
}
function addPayment() {
const form = document.getElementById('paymentForm');
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const amount = parseFloat(document.getElementById('paymentAmount').value);
const date = document.getElementById('paymentDate').value;
const notes = document.getElementById('paymentNotes').value;
const debt = debts.find(d => d.id === currentDebtId);
const remaining = debt.totalAmount - debt.paidAmount;
if (amount > remaining) {
showAlert('El monto del pago no puede ser mayor al saldo pendiente', 'danger');
return;
}
const payment = {
id: Date.now(),
amount: amount,
date: date,
notes: notes
};
debt.payments.push(payment);
debt.paidAmount += amount;
localStorage.setItem('debts', JSON.stringify(debts));
// Close modal and reset form
const modal = bootstrap.Modal.getInstance(document.getElementById('paymentModal'));
modal.hide();
form.reset();
updateDashboard();
// Check if debt is fully paid
if (debt.paidAmount >= debt.totalAmount) {
showAlert(`¡Felicidades! Has pagado completamente la deuda con ${debt.creditor}`, 'success');
} else {
showAlert('Abono registrado exitosamente', 'success');
}
}
function updateDashboard() {
updateStats();
renderDebts();
checkDueDates();
renderDebtChart();
}
function updateStats() {
const totalDebts = debts.length;
const totalAmount = debts.reduce((sum, debt) => {
const amount = parseFloat(debt.totalAmount) || 0;
return sum + amount;
}, 0);
const totalPaid = debts.reduce((sum, debt) => {
const paid = parseFloat(debt.paidAmount) || 0;
return sum + paid;
}, 0);
const totalPending = totalAmount - totalPaid;
document.getElementById('totalDebts').textContent = totalDebts;
document.getElementById('totalAmount').textContent = `$${totalAmount.toFixed(2)}`;
document.getElementById('totalPaid').textContent = `$${totalPaid.toFixed(2)}`;
document.getElementById('totalPending').textContent = `$${totalPending.toFixed(2)}`;
// Actualizar barra y texto
const progressBar = document.getElementById("progressBar");
// Calcular progreso
const progress = totalAmount > 0 ? (totalPaid / totalAmount) * 100 : 0;
progressBar.style.width = progress.toFixed(0) + "%";
progressBar.setAttribute("aria-valuenow", progress.toFixed(0));
progressBar.textContent = progress.toFixed(0) + "% pagado";
updateCircularProgress(totalAmount, totalPaid);
}
function renderDebts() {
const container = document.getElementById('debtsList');
if (debts.length === 0) {
container.innerHTML = `
<div class="text-center py-5">
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No hay deudas registradas</h5>
<p class="text-muted">Agrega tu primera deuda para comenzar</p>
</div>
`;
return;
}
container.innerHTML = debts.map(debt => {
const totalAmount = parseFloat(debt.totalAmount) || 0;
const paidAmount = parseFloat(debt.paidAmount) || 0;
const progress = totalAmount > 0 ? (paidAmount / totalAmount) * 100 : 0;
const remaining = totalAmount - paidAmount;
const isOverdue = new Date(debt.dueDate) < new Date() && remaining > 0;
const daysUntilDue = Math.ceil((new Date(debt.dueDate) - new Date()) / (1000 * 60 * 60 * 24));
return `
<div class="debt-card card">
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-8">
<div class="d-flex justify-content-between align-items-start mb-2">
<h5 class="card-title mb-0">${debt.creditor}</h5>
${isOverdue ? '<span class="badge bg-danger">Vencida</span>' :
daysUntilDue <= 7 && daysUntilDue > 0 ? '<span class="badge bg-warning">Próxima a vencer</span>' : ''}
</div>
<p class="text-muted mb-2">${debt.description || 'Sin descripción'}</p>
<div class="row">
<div class="col-sm-6">
<small class="text-muted">Total: <strong>$${totalAmount.toFixed(2)}</strong></small>
</div>
<div class="col-sm-6">
<small class="text-muted">Pagado: <strong>$${paidAmount.toFixed(2)}</strong></small>
</div>
<div class="col-sm-6">
<small class="text-muted">Pendiente: <strong>$${remaining.toFixed(2)}</strong></small>
</div>
<div class="col-sm-6">
<small class="text-muted">Vence: <strong>${new Date(debt.dueDate).toLocaleDateString()}</strong></small>
</div>
</div>
<div class="mt-3">
<div class="progress progress-custom">
<div class="progress-bar progress-bar-custom" style="width: ${progress}%"></div>
</div>
<small class="text-muted">${progress.toFixed(1)}% completado</small>
</div>
</div>
<div class="col-md-4 text-md-end mt-3 mt-md-0">
<div class="btn-group-vertical d-grid gap-2">
<button class="btn btn-outline-primary btn-sm" onclick="openEditModal(${debt.id})">
<i class="fas fa-edit me-1"></i>Editar
</button>
${remaining > 0 ? `<button class="btn btn-success" onclick="openPaymentModal(${debt.id})">
<i class="fas fa-money-bill-wave me-1"></i>Abonar
</button>` : '<span class="badge bg-success fs-6 p-2"><i class="fas fa-check me-1"></i>Pagada</span>'}
<button class="btn btn-outline-info btn-sm" onclick="viewPaymentHistory(${debt.id})">
<i class="fas fa-history me-1"></i>Historial
</button>
<button class="btn btn-danger-custom btn-sm" onclick="deleteDebt(${debt.id})">
<i class="fas fa-trash me-1"></i>Eliminar
</button>
</div>
</div>
</div>
</div>
</div>
`;
}).join('');
}
function checkDueDates() {
const alertsContainer = document.getElementById('alertsContainer');
const today = new Date();
const alerts = [];
debts.forEach(debt => {
const dueDate = new Date(debt.dueDate);
const remaining = debt.totalAmount - debt.paidAmount;
if (remaining > 0) {
const daysUntilDue = Math.ceil((dueDate - today) / (1000 * 60 * 60 * 24));
if (daysUntilDue < 0) {
alerts.push({
type: 'danger',
message: `⚠️ La deuda con ${debt.creditor} está vencida (${Math.abs(daysUntilDue)} días)`
});
} else if (daysUntilDue <= 7) {
alerts.push({
type: 'warning',
message: ` La deuda con ${debt.creditor} vence en ${daysUntilDue} días`
});
}
}
});
alertsContainer.innerHTML = alerts.map(alert => `
<div class="alert alert-${alert.type} alert-custom">
${alert.message}
</div>
`).join('');
}
function viewPaymentHistory(id) {
const debt = debts.find(d => d.id === id);
const payments = debt.payments;
let historyHtml = `
<div class="modal fade" id="historyModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-history me-2"></i>Historial de Pagos - ${debt.creditor}</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
`;
if (payments.length === 0) {
historyHtml += '<p class="text-center text-muted">No hay pagos registrados</p>';
} else {
historyHtml += `
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Fecha</th>
<th>Monto</th>
<th>Notas</th>
</tr>
</thead>
<tbody>
`;
payments.forEach(payment => {
historyHtml += `
<tr>
<td>${new Date(payment.date).toLocaleDateString()}</td>
<td>$${payment.amount.toFixed(2)}</td>
<td>${payment.notes || '-'}</td>
</tr>
`;
});
historyHtml += '</tbody></table></div>';
}
historyHtml += `
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>
`;
// Remove existing modal if any
const existingModal = document.getElementById('historyModal');
if (existingModal) {
existingModal.remove();
}
document.body.insertAdjacentHTML('beforeend', historyHtml);
const modal = new bootstrap.Modal(document.getElementById('historyModal'));
modal.show();
}
function exportToCSV() {
if (debts.length === 0) {
showAlert('No hay datos para exportar', 'warning');
return;
}
let csv = 'Acreedor,Monto Total,Monto Pagado,Saldo Pendiente,Fecha Vencimiento,Descripción,Progreso\n';
debts.forEach(debt => {
const remaining = debt.totalAmount - debt.paidAmount;
const progress = (debt.paidAmount / debt.totalAmount) * 100;
csv += `"${debt.creditor}",${debt.totalAmount},${debt.paidAmount},${remaining},"${debt.dueDate}","${debt.description || ''}",${progress.toFixed(1)}%\n`;
});
const blob = new Blob([csv], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `deudas_${new Date().toISOString().split('T')[0]}.csv`;
a.click();
window.URL.revokeObjectURL(url);
showAlert('Archivo CSV descargado exitosamente', 'success');
}
function exportToPDF() {
if (debts.length === 0) {
showAlert('No hay datos para exportar', 'warning');
return;
}
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
// Header
doc.setFontSize(20);
doc.text('Reporte de Deudas', 20, 20);
doc.setFontSize(12);
doc.text(`Generado el: ${new Date().toLocaleDateString()}`, 20, 30);
let y = 50;
debts.forEach((debt, index) => {
if (y > 250) {
doc.addPage();
y = 20;
}
const remaining = debt.totalAmount - debt.paidAmount;
const progress = (debt.paidAmount / debt.totalAmount) * 100;
doc.setFontSize(14);
doc.text(`${index + 1}. ${debt.creditor}`, 20, y);
y += 10;
doc.setFontSize(10);
doc.text(`Monto Total: $${debt.totalAmount.toFixed(2)}`, 25, y);
doc.text(`Pagado: $${debt.paidAmount.toFixed(2)}`, 25, y + 7);
doc.text(`Pendiente: $${remaining.toFixed(2)}`, 25, y + 14);
doc.text(`Vencimiento: ${new Date(debt.dueDate).toLocaleDateString()}`, 25, y + 21);
doc.text(`Progreso: ${progress.toFixed(1)}%`, 25, y + 28);
if (debt.description) {
doc.text(`Descripción: ${debt.description}`, 25, y + 35);
y += 42;
} else {
y += 35;
}
y += 10;
});
doc.save(`deudas_${new Date().toISOString().split('T')[0]}.pdf`);
showAlert('Archivo PDF descargado exitosamente', 'success');
}
function showAlert(message, type) {
const alertsContainer = document.getElementById('alertsContainer');
const alertId = 'alert_' + Date.now();
const alertHtml = `
<div id="${alertId}" class="alert alert-${type} alert-dismissible fade show" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
alertsContainer.insertAdjacentHTML('afterbegin', alertHtml);
// Auto remove after 5 seconds
setTimeout(() => {
const alertElement = document.getElementById(alertId);
if (alertElement) {
const alert = bootstrap.Alert.getOrCreateInstance(alertElement);
alert.close();
}
}, 5000);
}
function updateCircularProgress(totalAmount, paidAmount) {
const circle = document.getElementById("progressCircle");
const overallProgress = document.getElementById("overallProgress");
const radius = 65;
const circumference = 2 * Math.PI * radius;
// Calcular porcentaje
const progress = totalAmount > 0 ? (paidAmount / totalAmount) * 100 : 0;
// Calcular el desplazamiento (offset)
const offset = circumference - (progress / 100) * circumference;
// Actualizar atributos del círculo
circle.style.strokeDasharray = circumference;
circle.style.strokeDashoffset = offset;
}
let debtChart = null; // referencia global
function renderDebtChart() {
const ctx = document.getElementById('debtChart');
if (!ctx) return;
const context = ctx.getContext('2d');
if (debtChart) {
debtChart.destroy();
}
const data = debts.map(debt => {
const totalAmount = parseFloat(debt.totalAmount) || 0;
const paidAmount = parseFloat(debt.paidAmount) || 0;
const remaining = totalAmount - paidAmount;
return {
label: debt.creditor,
value: remaining > 0 ? remaining : 0
};
}).filter(item => item.value > 0);
if (data.length === 0) {
context.clearRect(0, 0, ctx.width, ctx.height);
context.fillStyle = '#28a745';
context.font = '20px Inter';
context.textAlign = 'center';
context.fillText('¡Todas las deudas pagadas!', ctx.width / 2, ctx.height / 2);
return;
}
const colors = ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40'];
debtChart = new Chart(context, {
type: 'doughnut',
data: {
labels: data.map(item => item.label),
datasets: [{
label: 'Deudas',
data: data.map(item => item.value),
backgroundColor: data.map((_, i) => colors[i % colors.length]),
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: { position: 'bottom' },
tooltip: {
callbacks: {
label: function(context) {
let label = context.label || '';
let value = context.raw || 0;
return `${label}: $${value.toFixed(2)}`;
}
}
}
}
}
});
}
Explicación rápida de las funciones clave
- addDebt(): valida el formulario, crea objeto deuda (con id = Date.now()), lo guarda en debts y en localStorage, cierra modal y actualiza el dashboard.
- openEditModal(id) / updateDebt(): carga la deuda en el formulario, valida que el nuevo total no sea menor al ya pagado y actualiza en localStorage.
- openPaymentModal(id) / addPayment(): calcula saldo pendiente, limita el campo max al saldo, agrega el pago al array payments y actualiza paidAmount.
- updateStats(): calcula totales (deudas, pagado, pendiente) y actualiza la barra circular y la barra lineal de progreso.
- renderDebtChart(): construye un gráfico tipo doughnut con las deudas pendientes (Chart.js).
- exportToCSV() / exportToPDF(): exportan la información actual. jsPDF puede paginar si hay mucho contenido.
Consejos de depuración / problemas comunes
- Si los modales no abren: asegúrate de incluir bootstrap.bundle.min.js y de usar la API correcta de Bootstrap 5 (new bootstrap.Modal(...) en lugar de $('#modal').modal('show')).
- Si no ves cambios al guardar: revisa localStorage en la consola (Application → Local Storage).
- Para errores raros usa console.log() en puntos clave (ej. antes y después de guardar, al abrir modales).
- Validaciones HTML5: el script usa form.checkValidity() y form.reportValidity() para usar la validación nativa del navegador.
Comentarios