Script de Registro de Deudas y Pagos — Debt Tracker con Bootstrap 5

Compartir

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:

  1. index.html — Interfaz y carga de librerías.
  2. style.css — Estilos personalizados (estéticos, responsivos).
  3. 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:

  1. Al iniciar la página, carga las deudas desde localStorage (si existe).
  2. Renderiza estadísticas, lista de deudas y gráficos.
  3. Al agregar una deuda, se valida el formulario, se guarda al array y a localStorage, se actualiza la UI.
  4. Al registrar un pago, se actualiza la deuda y se añade el pago al historial (array de pagos dentro del objeto deuda).
  5. 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.
Ya tienes un esquema completo y funcional para llevar el control de deudas y abonos. Es un excelente MVP: simple, fácil de entender y con múltiples puntos de mejora para convertirlo en una solución multiusuario o integrarla a un backend real.

VER DEMOSTRACIÓN

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