Instructions to use alvarodt/geocoding with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use alvarodt/geocoding with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("feature-extraction", model="alvarodt/geocoding")# Load model directly from transformers import AutoTokenizer, AutoModel tokenizer = AutoTokenizer.from_pretrained("alvarodt/geocoding") model = AutoModel.from_pretrained("alvarodt/geocoding", device_map="auto") - Notebooks
- Google Colab
- Kaggle
| <!DOCTYPE html> | |
| <html lang="es"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Sistema de Pendientes de Pago</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/localforage/1.10.0/localforage.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: #f8fafc; | |
| color: #1e293b; | |
| } | |
| .sidebar { | |
| transition: all 0.3s ease; | |
| } | |
| .card { | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.1); | |
| transition: all 0.3s ease; | |
| } | |
| .card:hover { | |
| box-shadow: 0 4px 6px rgba(0,0,0,0.1); | |
| transform: translateY(-2px); | |
| } | |
| .tab-content { | |
| animation: fadeIn 0.3s ease; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; } | |
| to { opacity: 1; } | |
| } | |
| .table-container { | |
| max-height: 500px; | |
| scrollbar-width: thin; | |
| scrollbar-color: #cbd5e1 #f1f5f9; | |
| } | |
| .table-container::-webkit-scrollbar { | |
| width: 8px; | |
| height: 8px; | |
| } | |
| .table-container::-webkit-scrollbar-track { | |
| background: #f1f5f9; | |
| } | |
| .table-container::-webkit-scrollbar-thumb { | |
| background-color: #cbd5e1; | |
| border-radius: 4px; | |
| } | |
| .badge { | |
| font-size: 0.75rem; | |
| padding: 0.25rem 0.5rem; | |
| } | |
| .action-btn { | |
| transition: all 0.2s ease; | |
| } | |
| .action-btn:hover { | |
| transform: scale(1.05); | |
| } | |
| .floating-btn { | |
| box-shadow: 0 4px 6px rgba(0,0,0,0.1); | |
| transition: all 0.3s ease; | |
| } | |
| .floating-btn:hover { | |
| box-shadow: 0 6px 8px rgba(0,0,0,0.15); | |
| transform: translateY(-2px); | |
| } | |
| .no-records { | |
| background-image: linear-gradient(to right, #f8fafc, #f1f5f9, #f8fafc); | |
| background-size: 200% auto; | |
| animation: shimmer 1.5s infinite linear; | |
| } | |
| @keyframes shimmer { | |
| 0% { background-position: -200% 0; } | |
| 100% { background-position: 200% 0; } | |
| } | |
| </style> | |
| </head> | |
| <body class="min-h-screen bg-gray-50"> | |
| <!-- Contenedor principal --> | |
| <div class="flex h-screen bg-gray-50" id="app"> | |
| <!-- Sidebar --> | |
| <div class="sidebar bg-white w-64 border-r border-gray-200 flex flex-col"> | |
| <div class="p-4 border-b border-gray-200 flex items-center"> | |
| <i class="fas fa-wallet text-2xl text-blue-600 mr-3"></i> | |
| <h1 class="text-xl font-bold text-gray-800">Pendientes de Pago</h1> | |
| </div> | |
| <div class="flex-1 overflow-y-auto"> | |
| <nav class="p-4 space-y-1"> | |
| <a href="#" class="tab-link flex items-center px-4 py-2 text-sm font-medium rounded-lg bg-blue-50 text-blue-700" data-tab="dashboard"> | |
| <i class="fas fa-tachometer-alt mr-3 text-blue-600"></i> Dashboard | |
| </a> | |
| <a href="#" class="tab-link flex items-center px-4 py-2 text-sm font-medium rounded-lg text-gray-700 hover:bg-gray-100" data-tab="pendientes"> | |
| <i class="fas fa-list-ul mr-3 text-gray-500"></i> Pendientes | |
| </a> | |
| <a href="#" class="tab-link flex items-center px-4 py-2 text-sm font-medium rounded-lg text-gray-700 hover:bg-gray-100" data-tab="historial"> | |
| <i class="fas fa-history mr-3 text-gray-500"></i> Historial | |
| </a> | |
| <a href="#" class="tab-link flex items-center px-4 py-2 text-sm font-medium rounded-lg text-gray-700 hover:bg-gray-100" data-tab="reportes"> | |
| <i class="fas fa-chart-bar mr-3 text-gray-500"></i> Reportes | |
| </a> | |
| <a href="#" class="tab-link flex items-center px-4 py-2 text-sm font-medium rounded-lg text-gray-700 hover:bg-gray-100" data-tab="configuracion"> | |
| <i class="fas fa-cog mr-3 text-gray-500"></i> Configuraci贸n | |
| </a> | |
| </nav> | |
| </div> | |
| <div class="p-4 border-t border-gray-200"> | |
| <button onclick="logout()" class="w-full flex items-center justify-center px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-50 rounded-lg"> | |
| <i class="fas fa-sign-out-alt mr-2"></i> Cerrar Sesi贸n | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Contenido principal --> | |
| <div class="flex-1 overflow-y-auto"> | |
| <div class="p-6"> | |
| <!-- Header --> | |
| <div class="flex justify-between items-center mb-6"> | |
| <h2 class="text-2xl font-bold text-gray-800" id="current-tab-title">Dashboard</h2> | |
| <div class="flex items-center space-x-2"> | |
| <span id="current-date" class="text-sm text-gray-500"></span> | |
| <div class="relative"> | |
| <button id="notifications-btn" class="p-2 rounded-full hover:bg-gray-100"> | |
| <i class="fas fa-bell text-gray-500"></i> | |
| <span class="absolute top-0 right-0 h-2 w-2 rounded-full bg-red-500"></span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Contenido de las pesta帽as --> | |
| <div class="tab-content" id="dashboard-content"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6"> | |
| <div class="card bg-white rounded-xl p-6"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <p class="text-sm font-medium text-gray-500">Total Pendiente</p> | |
| <h3 class="text-2xl font-bold text-gray-800 mt-1" id="total-pendiente">$0.00</h3> | |
| </div> | |
| <div class="p-3 rounded-full bg-blue-100 text-blue-600"> | |
| <i class="fas fa-clock text-xl"></i> | |
| </div> | |
| </div> | |
| <div class="mt-4 pt-4 border-t border-gray-100"> | |
| <p class="text-xs text-gray-500">脷ltima actualizaci贸n: <span id="last-update">hoy</span></p> | |
| </div> | |
| </div> | |
| <div class="card bg-white rounded-xl p-6"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <p class="text-sm font-medium text-gray-500">Total Pagado</p> | |
| <h3 class="text-2xl font-bold text-gray-800 mt-1" id="total-pagado">$0.00</h3> | |
| </div> | |
| <div class="p-3 rounded-full bg-green-100 text-green-600"> | |
| <i class="fas fa-check-circle text-xl"></i> | |
| </div> | |
| </div> | |
| <div class="mt-4 pt-4 border-t border-gray-100"> | |
| <p class="text-xs text-gray-500">Este mes: <span id="month-pagado">$0.00</span></p> | |
| </div> | |
| </div> | |
| <div class="card bg-white rounded-xl p-6"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <p class="text-sm font-medium text-gray-500">Registros Pendientes</p> | |
| <h3 class="text-2xl font-bold text-gray-800 mt-1" id="count-pendientes">0</h3> | |
| </div> | |
| <div class="p-3 rounded-full bg-yellow-100 text-yellow-600"> | |
| <i class="fas fa-file-invoice text-xl"></i> | |
| </div> | |
| </div> | |
| <div class="mt-4 pt-4 border-t border-gray-100"> | |
| <p class="text-xs text-gray-500">Vencidos: <span id="count-vencidos">0</span></p> | |
| </div> | |
| </div> | |
| <div class="card bg-white rounded-xl p-6"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <p class="text-sm font-medium text-gray-500">Registros Historial</p> | |
| <h3 class="text-2xl font-bold text-gray-800 mt-1" id="count-historial">0</h3> | |
| </div> | |
| <div class="p-3 rounded-full bg-purple-100 text-purple-600"> | |
| <i class="fas fa-archive text-xl"></i> | |
| </div> | |
| </div> | |
| <div class="mt-4 pt-4 border-t border-gray-100"> | |
| <p class="text-xs text-gray-500">Este mes: <span id="month-historial">0</span></p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> | |
| <div class="card bg-white rounded-xl p-6"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <h3 class="text-lg font-semibold text-gray-800">Evoluci贸n de Pagos</h3> | |
| <div class="flex space-x-2"> | |
| <button class="text-xs px-2 py-1 bg-gray-100 rounded hover:bg-gray-200" data-period="week">Semana</button> | |
| <button class="text-xs px-2 py-1 bg-blue-100 rounded hover:bg-blue-200" data-period="month">Mes</button> | |
| <button class="text-xs px-2 py-1 bg-gray-100 rounded hover:bg-gray-200" data-period="year">A帽o</button> | |
| </div> | |
| </div> | |
| <div class="h-64"> | |
| <canvas id="payments-chart"></canvas> | |
| </div> | |
| </div> | |
| <div class="card bg-white rounded-xl p-6"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <h3 class="text-lg font-semibold text-gray-800">Distribuci贸n por Categor铆a</h3> | |
| <div class="flex space-x-2"> | |
| <button class="text-xs px-2 py-1 bg-gray-100 rounded hover:bg-gray-200" data-type="pendientes">Pendientes</button> | |
| <button class="text-xs px-2 py-1 bg-blue-100 rounded hover:bg-blue-200" data-type="historial">Historial</button> | |
| </div> | |
| </div> | |
| <div class="h-64"> | |
| <canvas id="categories-chart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card bg-white rounded-xl p-6"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <h3 class="text-lg font-semibold text-gray-800">Pr贸ximos Vencimientos</h3> | |
| <a href="#" class="text-sm text-blue-600 hover:text-blue-800" onclick="activarTab('pendientes')">Ver todos</a> | |
| </div> | |
| <div class="overflow-x-auto"> | |
| <table class="min-w-full divide-y divide-gray-200"> | |
| <thead class="bg-gray-50"> | |
| <tr> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Fecha</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Nombre</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Descripci贸n</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Total</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Estado</th> | |
| </tr> | |
| </thead> | |
| <tbody class="bg-white divide-y divide-gray-200" id="upcoming-payments"> | |
| <tr> | |
| <td colspan="5" class="px-6 py-4 text-center text-sm text-gray-500">Cargando...</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Pesta帽a Pendientes --> | |
| <div class="tab-content hidden" id="pendientes-content"> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6"> | |
| <div class="lg:col-span-2"> | |
| <div class="card bg-white rounded-xl p-6"> | |
| <h3 class="text-lg font-semibold text-gray-800 mb-4">Nuevo Registro</h3> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label for="fecha" class="block text-sm font-medium text-gray-700 mb-1">Fecha</label> | |
| <input type="date" id="fecha" required | |
| class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"> | |
| </div> | |
| <div> | |
| <label for="nombre" class="block text-sm font-medium text-gray-700 mb-1">Nombre</label> | |
| <input type="text" id="nombre" placeholder="Ingrese nombre" required | |
| class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"> | |
| </div> | |
| <div class="md:col-span-2"> | |
| <label for="descripcion" class="block text-sm font-medium text-gray-700 mb-1">Descripci贸n</label> | |
| <textarea id="descripcion" placeholder="Ingrese descripci贸n" required rows="3" | |
| class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"></textarea> | |
| </div> | |
| <div> | |
| <label for="total" class="block text-sm font-medium text-gray-700 mb-1">Total</label> | |
| <input type="number" id="total" placeholder="0.00" step="0.01" required | |
| class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"> | |
| </div> | |
| <div class="flex items-end"> | |
| <button id="btn-agregar" | |
| class="bg-blue-600 text-white py-2 px-6 rounded-lg hover:bg-blue-700 transition flex items-center"> | |
| <i class="fas fa-plus mr-2"></i> Agregar | |
| </button> | |
| <button id="btn-cancelar-edicion" | |
| class="ml-2 bg-gray-200 text-gray-700 py-2 px-4 rounded-lg hover:bg-gray-300 transition hidden"> | |
| Cancelar | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card bg-white rounded-xl p-6"> | |
| <h3 class="text-lg font-semibold text-gray-800 mb-4">Resumen</h3> | |
| <div class="space-y-4"> | |
| <div> | |
| <p class="text-sm font-medium text-gray-500">Total Pendiente</p> | |
| <h3 class="text-2xl font-bold text-gray-800" id="pendientes-summary-total">$0.00</h3> | |
| </div> | |
| <div> | |
| <p class="text-sm font-medium text-gray-500">Registros</p> | |
| <h3 class="text-xl font-bold text-gray-800" id="pendientes-summary-count">0</h3> | |
| </div> | |
| <div class="pt-4 border-t border-gray-100"> | |
| <label for="buscar" class="block text-sm font-medium text-gray-700 mb-2">Buscar registros</label> | |
| <div class="relative"> | |
| <input type="search" id="buscar" placeholder="Buscar por nombre, fecha o descripci贸n" | |
| class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"> | |
| <div class="absolute left-3 top-2.5 text-gray-400"> | |
| <i class="fas fa-search"></i> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="pt-4 border-t border-gray-100"> | |
| <button id="exportar-json" | |
| class="w-full bg-green-600 text-white py-2 px-4 rounded-lg hover:bg-green-700 transition flex items-center justify-center mb-2"> | |
| <i class="fas fa-file-export mr-2"></i> Exportar JSON | |
| </button> | |
| <button id="exportar-excel" | |
| class="w-full bg-green-600 text-white py-2 px-4 rounded-lg hover:bg-green-700 transition flex items-center justify-center mb-2"> | |
| <i class="fas fa-file-excel mr-2"></i> Exportar CSV | |
| </button> | |
| <button id="importar-json" | |
| class="w-full bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 transition flex items-center justify-center mb-2"> | |
| <i class="fas fa-file-import mr-2"></i> Importar JSON | |
| </button> | |
| <button id="importar-excel" | |
| class="w-full bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 transition flex items-center justify-center"> | |
| <i class="fas fa-file-csv mr-2"></i> Importar CSV | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card bg-white rounded-xl p-6 mb-6"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h3 class="text-lg font-semibold text-gray-800">Registros Pendientes</h3> | |
| <div class="flex space-x-2"> | |
| <button id="descargar-busqueda" | |
| class="bg-gray-100 text-gray-700 py-1.5 px-3 rounded-lg hover:bg-gray-200 transition flex items-center text-sm"> | |
| <i class="fas fa-image mr-1"></i> Imagen | |
| </button> | |
| <button id="limpiar-datos" | |
| class="bg-red-100 text-red-700 py-1.5 px-3 rounded-lg hover:bg-red-200 transition flex items-center text-sm"> | |
| <i class="fas fa-trash mr-1"></i> Limpiar | |
| </button> | |
| </div> | |
| </div> | |
| <div class="table-container overflow-auto rounded-lg border border-gray-200"> | |
| <table class="min-w-full divide-y divide-gray-200" id="tabla-pendientes"> | |
| <thead class="bg-gray-50"> | |
| <tr> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Fecha</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Nombre</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Descripci贸n</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Total</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Acciones</th> | |
| </tr> | |
| </thead> | |
| <tbody class="bg-white divide-y divide-gray-200" id="pendientes-body"> | |
| <tr> | |
| <td colspan="5" class="px-6 py-4 text-center text-sm text-gray-500">Cargando datos...</td> | |
| </tr> | |
| </tbody> | |
| <tfoot class="bg-gray-50"> | |
| <tr> | |
| <td colspan="3" class="px-6 py-3 text-right text-sm font-medium text-gray-500">TOTAL PENDIENTE:</td> | |
| <td class="px-6 py-3 text-left text-sm font-medium text-gray-900" id="suma-total">$0.00</td> | |
| <td></td> | |
| </tr> | |
| </tfoot> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Pesta帽a Historial --> | |
| <div class="tab-content hidden" id="historial-content"> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6"> | |
| <div class="lg:col-span-2"> | |
| <div class="card bg-white rounded-xl p-6"> | |
| <h3 class="text-lg font-semibold text-gray-800 mb-4">Filtros</h3> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| <div> | |
| <label for="fecha-inicio" class="block text-sm font-medium text-gray-700 mb-1">Fecha Inicio</label> | |
| <input type="date" id="fecha-inicio" | |
| class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"> | |
| </div> | |
| <div> | |
| <label for="fecha-fin" class="block text-sm font-medium text-gray-700 mb-1">Fecha Fin</label> | |
| <input type="date" id="fecha-fin" | |
| class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"> | |
| </div> | |
| <div> | |
| <label for="buscar-historial" class="block text-sm font-medium text-gray-700 mb-1">Buscar</label> | |
| <div class="relative"> | |
| <input type="search" id="buscar-historial" placeholder="Buscar en historial" | |
| class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"> | |
| <div class="absolute left-3 top-2.5 text-gray-400"> | |
| <i class="fas fa-search"></i> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card bg-white rounded-xl p-6"> | |
| <h3 class="text-lg font-semibold text-gray-800 mb-4">Resumen</h3> | |
| <div class="space-y-4"> | |
| <div> | |
| <p class="text-sm font-medium text-gray-500">Total Pagado</p> | |
| <h3 class="text-2xl font-bold text-gray-800" id="historial-summary-total">$0.00</h3> | |
| </div> | |
| <div> | |
| <p class="text-sm font-medium text-gray-500">Registros</p> | |
| <h3 class="text-xl font-bold text-gray-800" id="historial-summary-count">0</h3> | |
| </div> | |
| <div class="pt-4 border-t border-gray-100"> | |
| <button id="exportar-historial-json" | |
| class="w-full bg-green-600 text-white py-2 px-4 rounded-lg hover:bg-green-700 transition flex items-center justify-center mb-2"> | |
| <i class="fas fa-file-export mr-2"></i> Exportar JSON | |
| </button> | |
| <button id="exportar-historial-excel" | |
| class="w-full bg-green-600 text-white py-2 px-4 rounded-lg hover:bg-green-700 transition flex items-center justify-center"> | |
| <i class="fas fa-file-excel mr-2"></i> Exportar CSV | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card bg-white rounded-xl p-6 mb-6"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h3 class="text-lg font-semibold text-gray-800">Historial de Pagos</h3> | |
| <div class="flex items-center space-x-2"> | |
| <span class="text-sm text-gray-500" id="historial-filter-info">Mostrando todos los registros</span> | |
| </div> | |
| </div> | |
| <div class="table-container overflow-auto rounded-lg border border-gray-200"> | |
| <table class="min-w-full divide-y divide-gray-200" id="tabla-historial"> | |
| <thead class="bg-gray-50"> | |
| <tr> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Fecha Pago</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Fecha Registro</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Nombre</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Descripci贸n</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Total</th> | |
| </tr> | |
| </thead> | |
| <tbody class="bg-white divide-y divide-gray-200" id="historial-body"> | |
| <tr> | |
| <td colspan="5" class="px-6 py-4 text-center text-sm text-gray-500">Cargando historial...</td> | |
| </tr> | |
| </tbody> | |
| <tfoot class="bg-gray-50"> | |
| <tr> | |
| <td colspan="4" class="px-6 py-3 text-right text-sm font-medium text-gray-500">TOTAL PAGADO:</td> | |
| <td class="px-6 py-3 text-left text-sm font-medium text-gray-900" id="suma-total-historial">$0.00</td> | |
| </tr> | |
| </tfoot> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Pesta帽a Reportes --> | |
| <div class="tab-content hidden" id="reportes-content"> | |
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> | |
| <div class="card bg-white rounded-xl p-6"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <h3 class="text-lg font-semibold text-gray-800">Pagos por Mes</h3> | |
| <select id="report-year" class="text-sm border border-gray-300 rounded px-3 py-1 focus:ring-blue-500 focus:border-blue-500"> | |
| <option value="2023">2023</option> | |
| <option value="2024" selected>2024</option> | |
| </select> | |
| </div> | |
| <div class="h-80"> | |
| <canvas id="monthly-payments-chart"></canvas> | |
| </div> | |
| </div> | |
| <div class="card bg-white rounded-xl p-6"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <h3 class="text-lg font-semibold text-gray-800">Top Clientes</h3> | |
| <select id="report-type" class="text-sm border border-gray-300 rounded px 3 py-1 focus:ring-blue-500 focus:border-blue-500"> | |
| <option value="pendientes">Pendientes</option> | |
| <option value="historial" selected>Historial</option> | |
| </select> | |
| </div> | |
| <div class="h-80"> | |
| <canvas id="top-clients-chart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card bg-white rounded-xl p-6 mb-6"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <h3 class="text-lg font-semibold text-gray-800">Resumen Anual</h3> | |
| <div class="flex space-x-2"> | |
| <button id="export-report-pdf" class="text-sm bg-red-600 text-white px-4 py-1.5 rounded hover:bg-red-700 flex items-center"> | |
| <i class="fas fa-file-pdf mr-2"></i> PDF | |
| </button> | |
| <button id="export-report-excel" class="text-sm bg-green-600 text-white px-4 py-1.5 rounded hover:bg-green-700 flex items-center"> | |
| <i class="fas fa-file-excel mr-2"></i> Excel | |
| </button> | |
| </div> | |
| </div> | |
| <div class="overflow-x-auto"> | |
| <table class="min-w-full divide-y divide-gray-200"> | |
| <thead class="bg-gray-50"> | |
| <tr> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">A帽o</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ene</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Feb</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Mar</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Abr</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">May</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Jun</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Jul</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ago</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Sep</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Oct</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Nov</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Dic</th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Total</th> | |
| </tr> | |
| </thead> | |
| <tbody class="bg-white divide-y divide-gray-200" id="annual-summary"> | |
| <tr> | |
| <td colspan="14" class="px-6 py-4 text-center text-sm text-gray-500">Cargando datos...</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Pesta帽a Configuraci贸n --> | |
| <div class="tab-content hidden" id="configuracion-content"> | |
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> | |
| <div class="card bg-white rounded-xl p-6"> | |
| <h3 class="text-lg font-semibold text-gray-800 mb-4">Respaldo Autom谩tico</h3> | |
| <p class="text-sm text-gray-600 mb-4">Selecciona una carpeta donde se guardar谩n autom谩ticamente los archivos de respaldo cada vez que agregues o actualices un registro.</p> | |
| <div class="mb-4"> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Carpeta de Respaldo</label> | |
| <div class="flex"> | |
| <input type="text" id="backup-folder" placeholder="Ninguna carpeta seleccionada" readonly | |
| class="flex-1 px-4 py-2 border border-gray-300 rounded-l-lg bg-gray-50"> | |
| <button onclick="seleccionarCarpeta()" | |
| class="bg-blue-600 text-white px-4 py-2 rounded-r-lg hover:bg-blue-700 transition"> | |
| <i class="fas fa-folder-open mr-1"></i> Seleccionar | |
| </button> | |
| </div> | |
| </div> | |
| <div class="p-4 bg-blue-50 rounded-lg mb-4" id="backup-status"> | |
| <p class="text-sm text-blue-800">Selecciona una carpeta para activar backups.</p> | |
| </div> | |
| </div> | |
| <div class="card bg-white rounded-xl p-6"> | |
| <h3 class="text-lg font-semibold text-gray-800 mb-4">Respaldo Programado</h3> | |
| <p class="text-sm text-gray-600 mb-4">Configura respaldos peri贸dicos que incluyan pendientes e historial con marca de tiempo.</p> | |
| <div class="mb-4"> | |
| <label for="backup-interval" class="block text-sm font-medium text-gray-700 mb-1">Intervalo</label> | |
| <select id="backup-interval" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500"> | |
| <option value="0">Desactivado</option> | |
| <option value="5">Cada 5 minutos</option> | |
| <option value="15">Cada 15 minutos</option> | |
| <option value="30">Cada 30 minutos</option> | |
| <option value="60">Cada 1 hora</option> | |
| <option value="120">Cada 2 horas</option> | |
| <option value="1440">Diario</option> | |
| </select> | |
| </div> | |
| <div class="mb 4"> | |
| <label for="backup-format" class="block text-sm font-medium text-gray-700 mb-1">Formato</label> | |
| <select id="backup-format" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500"> | |
| <option value="json">JSON</option> | |
| <option value="csv">CSV</option> | |
| </select> | |
| </div> | |
| <button onclick="guardarConfiguracionProgramada()" | |
| class="w-full bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 transition flex items-center justify-center"> | |
| <i class="fas fa-save mr-2"></i> Guardar Configuraci贸n | |
| </button> | |
| </div> | |
| </div> | |
| <div class="card bg-white rounded-xl p-6"> | |
| <h3 class="text-lg font-semibold text-gray-800 mb-4">Integrar C贸digo</h3> | |
| <p class="text-sm text-gray-600 mb-4">Pega aqu铆 el c贸digo JavaScript que deseas ejecutar en el sistema.</p> | |
| <textarea id="code-area" rows="8" placeholder="Pega aqu铆 el c贸digo JavaScript que deseas ejecutar..." | |
| class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition mb-4 font-mono text-sm"></textarea> | |
| <div class="flex justify-end"> | |
| <button onclick="integrarCodigo()" | |
| class="bg-purple-600 text-white py-2 px-6 rounded-lg hover:bg-purple-700 transition flex items-center"> | |
| <i class="fas fa-code mr-2"></i> Ejecutar C贸digo | |
| </button> | |
| </div> | |
| <div class="mt-4 p-3 bg-red-50 rounded-lg"> | |
| <p class="text-sm text-red-700"><i class="fas fa-exclamation-triangle mr-2"></i> ADVERTENCIA: Ejecutar c贸digo desconocido puede ser peligroso. Solo usa c贸digo de fuentes confiables.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // --- Strict mode --- | |
| "use strict"; | |
| /******************************* | |
| * Variables Globales | |
| *******************************/ | |
| let modoEdicion = false; | |
| let idEdicion = null; | |
| let backupFolderHandle = null; | |
| let paymentsChart = null; | |
| let categoriesChart = null; | |
| let monthlyPaymentsChart = null; | |
| let topClientsChart = null; | |
| let btnAgregar = document.getElementById('btn-agregar'); | |
| let btnCancelarEdicion = document.getElementById('btn-cancelar-edicion'); | |
| /******************************* | |
| * App Initialization | |
| *******************************/ | |
| async function initializeApp() { | |
| document.getElementById('fecha').valueAsDate = new Date(); | |
| configurarEventosGenerales(); | |
| // Set default tab to dashboard | |
| activarTab('dashboard'); | |
| // Load initial data | |
| try { | |
| const pendientesStored = await memoryStorage.getPendientes(); | |
| if (!pendientesStored || pendientesStored.length === 0) { | |
| await cargarDatosEjemplo(); | |
| } | |
| await cargarDatos(); | |
| await cargarHistorial(); | |
| await updateDashboard(); | |
| await loadReports(); | |
| } catch (error) { | |
| console.error("Error during initial data loading:", error); | |
| alert("Error al cargar los datos iniciales: " + error.message); | |
| } | |
| cargarConfiguracionProgramada(); | |
| } | |
| /******************************* | |
| * Almacenamiento (localForage) | |
| *******************************/ | |
| const memoryStorage = { | |
| async getPendientes() { | |
| const pendientes = await localforage.getItem('pendientes'); | |
| return pendientes || []; | |
| }, | |
| async setPendientes(newData) { | |
| if (!Array.isArray(newData)) { | |
| return Promise.reject(new Error("Invalid data format for pendientes")); | |
| } | |
| return localforage.setItem('pendientes', newData); | |
| }, | |
| async getHistorial() { | |
| const historial = await localforage.getItem('historial'); | |
| return historial || []; | |
| }, | |
| async setHistorial(newData) { | |
| if (!Array.isArray(newData)) { | |
| return Promise.reject(new Error("Invalid data format for historial")); | |
| } | |
| return localforage.setItem('historial', newData); | |
| }, | |
| async addToHistorial(pendiente) { | |
| if (!pendiente || typeof pendiente !== 'object') { | |
| return Promise.reject(new Error("Invalid item format for historial")); | |
| } | |
| let historial = await this.getHistorial(); | |
| const pagoRegistro = { | |
| ...pendiente, | |
| fechaPago: new Date().toISOString().split('T')[0] | |
| }; | |
| historial.push(pagoRegistro); | |
| return this.setHistorial(historial); | |
| }, | |
| async clearData() { | |
| await localforage.removeItem('pendientes'); | |
| await localforage.removeItem('historial'); | |
| } | |
| }; | |
| /******************************* | |
| * Configuraci贸n Eventos Generales | |
| *******************************/ | |
| function configurarEventosGenerales() { | |
| // Tabs | |
| document.querySelectorAll('.tab-link').forEach(tab => { | |
| tab.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| const tab = tab.dataset.tab; | |
| activarTab(tab); | |
| }); | |
| }); | |
| // Bot贸n Agregar/Actualizar | |
| btnAgregar?.addEventListener('click', guardarRegistro); | |
| // Bot贸n Cancelar Edici贸n | |
| btnCancelarEdicion?.addEventListener('click', resetearFormulario); | |
| // Filtros de b煤squeda | |
| document.getElementById('buscar')?.addEventListener('input', cargarDatos); | |
| document.getElementById('buscar-historial')?.addEventListener('input', cargarHistorial); | |
| document.getElementById('fecha-inicio')?.addEventListener('change', cargarHistorial); | |
| document.getElementById('fecha-fin')?.addEventListener('change', cargarHistorial); | |
| // Descargar imagen | |
| document.getElementById('descargar-busqueda')?.addEventListener('click', descargarResultadosComoImagen); | |
| // Exportar/Importar | |
| document.getElementById('exportar-json')?.addEventListener('click', exportarPendientesJSON); | |
| document.getElementById('exportar-excel')?.addEventListener('click', exportarPendientesCSV); | |
| document.getElementById('exportar-historial-json')?.addEventListener('click', exportarHistorialJSON); | |
| document.getElementById('exportar-historial-excel')?.addEventListener('click', exportarHistorialCSV); | |
| document.getElementById('importar-json')?.addEventListener('click', importarPendientesJSON); | |
| document.getElementById('importar-excel')?.addEventListener('click', importarPendientesCSV); | |
| // Limpiar datos | |
| document.getElementById('limpiar-datos')?.addEventListener('click', async () => { | |
| if (confirm('驴Est谩 seguro de eliminar todos los datos (pendientes e historial)? Esta acci贸n no se puede deshacer.')) { | |
| try { | |
| await memoryStorage.clearData(); | |
| await cargarDatos(); | |
| await cargarHistorial(); | |
| await updateDashboard(); | |
| await loadReports(); | |
| alert('Todos los datos han sido eliminados.'); | |
| await eliminarBackupsInternos(); | |
| resetearFormulario(); | |
| } catch (error) { | |
| alert("Error al limpiar los datos: " + error.message); | |
| } | |
| } | |
| }); | |
| // Reportes | |
| document.getElementById('report-year')?.addEventListener('change', loadReports); | |
| document.getElementById('report-type')?.addEventListener('change', loadReports); | |
| } | |
| function activarTab(tab) { | |
| // Update active tab in sidebar | |
| document.querySelectorAll('.tab-link').forEach(t => { | |
| t.classList.remove('bg-blue-50', 'text-blue-700'); | |
| t.classList.add('text-gray-700', 'hover:bg-gray-100'); | |
| // Update icon colors | |
| const icon = t.querySelector('i'); | |
| if (icon) { | |
| icon.classList.remove('text-blue-600'); | |
| icon.classList.add('text-gray-500'); | |
| } | |
| }); | |
| // Set active tab | |
| const activeTab = document.querySelector(`.tab-link[data-tab="${tab}"]`); | |
| if (activeTab) { | |
| activeTab.classList.add('bg-blue-50', 'text-blue-700'); | |
| activeTab.classList.remove('text-gray-700', 'hover:bg-gray-100'); | |
| // Update icon color | |
| const icon = activeTab.querySelector('i'); | |
| if (icon) { | |
| icon.classList.add('text-blue-600'); | |
| icon.classList.remove('text-gray-500'); | |
| } | |
| } | |
| // Hide all tab contents | |
| document.querySelectorAll('.tab-content').forEach(t => t.classList.add('hidden')); | |
| // Show selected tab content | |
| const contentElement = document.getElementById(`${tab}-content`); | |
| if (contentElement) { | |
| contentElement.classList.remove('hidden'); | |
| } | |
| // Update title | |
| const tabTitles = { | |
| 'dashboard': 'Dashboard', | |
| 'pendientes': 'Pendientes', | |
| 'historial': 'Historial', | |
| 'reportes': 'Reportes', | |
| 'configuracion': 'Configuraci贸n' | |
| }; | |
| document.getElementById('current-tab-title').textContent = tabTitles[tab] || tab; | |
| // Load specific tab data if needed | |
| if (tab === 'dashboard') { | |
| updateDashboard(); | |
| } else if (tab === 'reportes') { | |
| loadReports(); | |
| } | |
| } | |
| /******************************* | |
| * Funciones principales | |
| *******************************/ | |
| async function guardarRegistro() { | |
| const fecha = document.getElementById('fecha').value; | |
| const nombre = document.getElementById('nombre').value.trim(); | |
| const descripcion = document.getElementById('descripcion').value.trim(); | |
| const totalInput = document.getElementById('total').value; | |
| if (!fecha || !nombre || !descripcion || totalInput === '') { | |
| alert('Por favor, complete todos los campos.'); | |
| return; | |
| } | |
| const total = parseFloat(totalInput); | |
| if (isNaN(total)) { | |
| alert('El valor total debe ser un n煤mero v谩lido.'); | |
| return; | |
| } | |
| try { | |
| let pendientes = await memoryStorage.getPendientes(); | |
| let mensajeExito = ''; | |
| if (modoEdicion && idEdicion !== null) { | |
| const index = pendientes.findIndex(item => item.id == idEdicion); | |
| if (index !== -1) { | |
| pendientes[index] = { id: parseInt(idEdicion), fecha, nombre, descripcion, total }; | |
| mensajeExito = 'Registro actualizado correctamente.'; | |
| } else { | |
| alert("Error al actualizar: No se encontr贸 el registro."); | |
| resetearFormulario(); | |
| return; | |
| } | |
| } else { | |
| const nuevoId = Date.now(); | |
| const nuevoRegistro = { id: nuevoId, fecha, nombre, descripcion, total }; | |
| pendientes.push(nuevoRegistro); | |
| mensajeExito = 'Registro agregado correctamente.'; | |
| } | |
| await memoryStorage.setPendientes(pendientes); | |
| await generarBackupInterno().catch(err => console.error("Error during internal backup:", err)); | |
| resetearFormulario(); | |
| await cargarDatos(); | |
| await updateDashboard(); | |
| await loadReports(); | |
| } catch (error) { | |
| alert(`Error al guardar el registro: ${error.message}`); | |
| } | |
| } | |
| async function cargarDatos() { | |
| const tbody = document.getElementById('pendientes-body'); | |
| const busqueda = document.getElementById('buscar').value.toLowerCase(); | |
| tbody.innerHTML = '<tr><td colspan="5" class="px-6 py-4 text-center text-sm text-gray-500">Cargando...</td></tr>'; | |
| try { | |
| let pendientes = await memoryStorage.getPendientes(); | |
| // Filtrar | |
| let filtrados = pendientes.filter(item => | |
| !busqueda || | |
| (item.nombre && item.nombre.toLowerCase().includes(busqueda)) || | |
| (item.descripcion && item.descripcion.toLowerCase().includes(busqueda)) || | |
| (item.fecha && item.fecha.includes(busqueda)) | |
| ); | |
| // Ordenar por fecha (ascendente) | |
| filtrados.sort((a, b) => { | |
| try { return new Date(a.fecha) - new Date(b.fecha); } | |
| catch(e) { return 0; } | |
| }); | |
| tbody.innerHTML = ""; | |
| let sumaTotal = 0; | |
| if (filtrados.length === 0) { | |
| const msg = busqueda ? "No se encontraron registros con ese criterio." : "No hay registros pendientes."; | |
| tbody.innerHTML = `<tr><td colspan="5" class="px-6 py-4 text-center text-sm text-gray-500">${msg}</td></tr>`; | |
| } else { | |
| filtrados.forEach(item => { | |
| let fechaFormateada = item.fecha; | |
| try { | |
| const dateParts = item.fecha.split('-'); | |
| if (dateParts.length === 3) { | |
| const year = parseInt(dateParts[0]); | |
| const month = parseInt(dateParts[1]) - 1; | |
| const day = parseInt(dateParts[2]); | |
| fechaFormateada = new Date(year, month, day).toLocaleDateString(); | |
| } | |
| } catch (e) { console.warn(`Fecha inv谩lida: ${item.fecha}`); } | |
| const row = tbody.insertRow(); | |
| row.className = 'hover:bg-gray-50'; | |
| // Fecha | |
| const cellFecha = row.insertCell(); | |
| cellFecha.className = 'px-6 py-4 whitespace-nowrap text-sm text-gray-900'; | |
| cellFecha.textContent = fechaFormateada; | |
| // Nombre | |
| const cellNombre = row.insertCell(); | |
| cellNombre.className = 'px-6 py-4 whitespace-nowrap text-sm text-gray-900'; | |
| cellNombre.textContent = item.nombre; | |
| // Descripci贸n | |
| const cellDesc = row.insertCell(); | |
| cellDesc.className = 'px-6 py-4 text-sm text-gray-900'; | |
| cellDesc.textContent = item.descripcion; | |
| // Total | |
| const cellTotal = row.insertCell(); | |
| cellTotal.className = 'px-6 py-4 whitespace-nowrap text-sm text-right font-medium'; | |
| cellTotal.textContent = `$${item.total?.toFixed(2) ?? '0.00'}`; | |
| cellTotal.style.color = item.total < 0 ? '#dc2626' : '#1f2937'; | |
| // Acciones | |
| const cellAcciones = row.insertCell(); | |
| cellAcciones.className = 'px-6 py-4 whitespace-nowrap text-right text-sm font-medium'; | |
| const btnContainer = document.createElement('div'); | |
| btnContainer.className = 'flex space-x-2 justify-end'; | |
| // Bot贸n Editar | |
| const btnEditar = document.createElement('button'); | |
| btnEditar.className = 'action-btn text-blue-600 hover:text-blue-900'; | |
| btnEditar.title = 'Editar este registro'; | |
| btnEditar.innerHTML = '<i class="fas fa-edit"></i>'; | |
| btnEditar.addEventListener('click', () => editarRegistro(item.id)); | |
| // Bot贸n Eliminar | |
| const btnEliminar = document.createElement('button'); | |
| btnEliminar.className = 'action-btn text-red-600 hover:text-red-900'; | |
| btnEliminar.title = 'Eliminar este registro'; | |
| btnEliminar.innerHTML = '<i class="fas fa-trash"></i>'; | |
| btnEliminar.addEventListener('click', () => eliminarRegistro(item.id)); | |
| // Bot贸n Pagado | |
| const btnPagado = document.createElement('button'); | |
| btnPagado.className = 'action-btn text-green-600 hover:text-green-900'; | |
| btnPagado.title = 'Marcar como pagado'; | |
| btnPagado.innerHTML = '<i class="fas fa-check-circle"></i>'; | |
| btnPagado.addEventListener('click', () => marcarComoPagado(item.id)); | |
| btnContainer.appendChild(btnEditar); | |
| btnContainer.appendChild(btnEliminar); | |
| btnContainer.appendChild(btnPagado); | |
| cellAcciones.appendChild(btnContainer); | |
| // Sum total | |
| if (typeof item.total === 'number' && !isNaN(item.total)) { | |
| sumaTotal += item.total; | |
| } | |
| }); | |
| } | |
| document.getElementById('suma-total').textContent = `$${sumaTotal.toFixed(2)}`; | |
| document.getElementById('pendientes-summary-total').textContent = `$${sumaTotal.toFixed(2)}`; | |
| document.getElementById('pendientes-summary-count').textContent = filtrados.length; | |
| } catch (error) { | |
| tbody.innerHTML = `<tr><td colspan="5" class="px-6 py-4 text-center text-sm text-red-500">Error al cargar datos: ${error.message}</td></tr>`; | |
| } | |
| } | |
| async function cargarHistorial() { | |
| const tbody = document.getElementById('historial-body'); | |
| const busqueda = document.getElementById('buscar-historial').value.toLowerCase(); | |
| const fechaInicio = document.getElementById('fecha-inicio')?.value; | |
| const fechaFin = document.getElementById('fecha-fin')?.value; | |
| tbody.innerHTML = '<tr><td colspan="5" class="px-6 py-4 text-center text-sm text-gray-500">Cargando...</td></tr>'; | |
| try { | |
| let historial = await memoryStorage.getHistorial(); | |
| // Filtrar | |
| historial = historial.filter(item => { | |
| // Filtro de b煤squeda | |
| const matchSearch = !busqueda || | |
| (item.nombre && item.nombre.toLowerCase().includes(busqueda)) || | |
| (item.descripcion && item.descripcion.toLowerCase().includes(busqueda)) || | |
| (item.fecha && item.fecha.includes(busqueda)) || | |
| (item.fechaPago && item.fechaPago.includes(busqueda)); | |
| // Filtro por fecha | |
| let matchDate = true; | |
| if (fechaInicio) { | |
| matchDate = matchDate && item.fechaPago >= fechaInicio; | |
| } | |
| if (fechaFin) { | |
| matchDate = matchDate && item.fechaPago <= fechaFin; | |
| } | |
| return matchSearch && matchDate; | |
| }); | |
| // Ordenar por fecha de pago (descendente) | |
| historial.sort((a, b) => { | |
| try { return new Date(b.fechaPago) - new Date(a.fechaPago); } | |
| catch(e) { return 0; } | |
| }); | |
| tbody.innerHTML = ""; | |
| let sumaTotalHistorial = 0; | |
| if (historial.length === 0) { | |
| const msg = busqueda ? "No se encontraron registros con ese criterio." : "El historial de pagos est谩 vac铆o."; | |
| tbody.innerHTML = `<tr><td colspan="5" class="px-6 py-4 text-center text-sm text-gray-500">${msg}</td></tr>`; | |
| } else { | |
| historial.forEach(item => { | |
| let fechaPagoF = item.fechaPago; | |
| let fechaRegF = item.fecha; | |
| try { | |
| const dpParts = item.fechaPago?.split('-'); | |
| if (dpParts?.length === 3) fechaPagoF = new Date(parseInt(dpParts[0]), parseInt(dpParts[1]) - 1, parseInt(dpParts[2])).toLocaleDateString(); | |
| const drParts = item.fecha?.split('-'); | |
| if (drParts?.length === 3) fechaRegF = new Date(parseInt(drParts[0]), parseInt(drParts[1]) - 1, parseInt(drParts[2])).toLocaleDateString(); | |
| } catch (e) { /* Ignore date formatting errors silently */ } | |
| const row = tbody.insertRow(); | |
| row.className = 'hover:bg-gray-50'; | |
| // Fecha Pago | |
| const cellFechaPago = row.insertCell(); | |
| cellFechaPago.className = 'px-6 py-4 whitespace-nowrap text-sm text-gray-900'; | |
| cellFechaPago.textContent = fechaPagoF; | |
| // Fecha Registro | |
| const cellFechaReg = row.insertCell(); | |
| cellFechaReg.className = 'px-6 py-4 whitespace-nowrap text-sm text-gray-900'; | |
| cellFechaReg.textContent = fechaRegF; | |
| // Nombre | |
| const cellNombre = row.insertCell(); | |
| cellNombre.className = 'px-6 py-4 whitespace-nowrap text-sm text-gray-900'; | |
| cellNombre.textContent = item.nombre; | |
| // Descripci贸n | |
| const cellDesc = row.insertCell(); | |
| cellDesc.className = 'px-6 py-4 text-sm text-gray-900'; | |
| cellDesc.textContent = item.descripcion; | |
| // Total | |
| const cellTotal = row.insertCell(); | |
| cellTotal.className = 'px-6 py-4 whitespace-nowrap text-sm text-right font-medium'; | |
| cellTotal.textContent = `$${item.total?.toFixed(2) ?? '0.00'}`; | |
| cellTotal.style.color = item.total < 0 ? '#dc2626' : '#1f2937'; | |
| if (typeof item.total === 'number' && !isNaN(item.total)) { | |
| sumaTotalHistorial += item.total; | |
| } | |
| }); | |
| } | |
| document.getElementById('suma-total-historial').textContent = `$${sumaTotalHistorial.toFixed(2)}`; | |
| document.getElementById('historial-summary-total').textContent = `$${sumaTotalHistorial.toFixed(2)}`; | |
| document.getElementById('historial-summary-count').textContent = historial.length; | |
| // Update filter info | |
| let filterInfo = "Mostrando todos los registros"; | |
| if (fechaInicio || fechaFin) { | |
| filterInfo = `Filtrado por fecha: ${fechaInicio || 'inicio'} - ${fechaFin || 'fin'}`; | |
| } | |
| document.getElementById('historial-filter-info').textContent = filterInfo; | |
| } catch (error) { | |
| tbody.innerHTML = `<tr><td colspan="5, class="px-6 py-4 text-center text-sm text-red-500">Error al cargar historial: ${error.message}</td></tr>`; | |
| } | |
| } | |
| /******************************* | |
| * Funciones de edici贸n/eliminaci贸n | |
| *******************************/ | |
| async function eliminarRegistro(id) { | |
| if (!id) return; | |
| if (confirm(`驴Est谩 seguro de eliminar permanentemente el registro pendiente?`)) { | |
| try { | |
| let pendientes = await memoryStorage.getPendientes(); | |
| const originalLength = pendientes.length; | |
| pendientes = pendientes.filter(item => item.id != id); | |
| if (pendientes.length < originalLength) { | |
| await memoryStorage.setPendientes(pendientes); | |
| await cargarDatos(); | |
| await updateDashboard(); | |
| await loadReports(); | |
| await generarBackupInterno().catch(err => console.error("Backup failed after delete:", err)); | |
| if (modoEdicion && idEdicion == id) { | |
| resetearFormulario(); | |
| } | |
| } else { | |
| alert('Error: No se encontr贸 el registro para eliminar.'); | |
| } | |
| } catch (error) { | |
| alert(`Error al eliminar el registro: ${error.message}`); | |
| } | |
| } | |
| } | |
| async function marcarComoPagado(id) { | |
| if (!id) return; | |
| if (confirm(`驴Marcar este registro como pagado y moverlo al historial?`)) { | |
| try { | |
| let pendientes = await memoryStorage.getPendientes(); | |
| const index = pendientes.findIndex(item => item.id == id); | |
| if (index !== -1) { | |
| const itemPagado = pendientes[index]; | |
| await memoryStorage.addToHistorial(itemPagado); | |
| pendientes.splice(index, 1); | |
| await memoryStorage.setPendientes(pendientes); | |
| await cargarDatos(); | |
| await cargarHistorial(); | |
| await updateDashboard(); | |
| await loadReports(); | |
| await generarBackupInterno().catch(err => console.error("Backup failed after mark paid:", err)); | |
| if (modoEdicion && idEdicion == id) { | |
| resetearFormulario(); | |
| } | |
| } else { | |
| alert('Error: No se encontr贸 el registro para marcar como pagado.'); | |
| } | |
| } catch(error) { | |
| alert(`Error al marcar como pagado: ${error.message}`); | |
| } | |
| } | |
| } | |
| async function editarRegistro(id) { | |
| if (!id) return; | |
| try { | |
| let pendientes = await memoryStorage.getPendientes(); | |
| const registro = pendientes.find(item => item.id == id); | |
| if (registro) { | |
| document.getElementById('fecha').value = registro.fecha; | |
| document.getElementById('nombre').value = registro.nombre; | |
| document.getElementById('descripcion').value = registro.descripcion; | |
| document.getElementById('total').value = registro.total; | |
| modoEdicion = true; | |
| idEdicion = id; | |
| btnAgregar.innerHTML = '<i class="fas fa-save mr-2"></i> Actualizar'; | |
| btnAgregar.className = 'bg-yellow-600 text-white py-2 px-6 rounded-lg hover:bg-yellow-700 transition flex items-center'; | |
| btnCancelarEdicion.classList.remove('hidden'); | |
| // Scroll to form | |
| const nombreInput = document.getElementById('nombre'); | |
| if (nombreInput) { | |
| nombreInput.focus(); | |
| nombreInput.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| } | |
| } else { | |
| alert("Error: No se pudo encontrar el registro para editar."); | |
| resetearFormulario(); | |
| } | |
| } catch (error) { | |
| alert(`Error al preparar la edici贸n: ${error.message}`); | |
| resetearFormulario(); | |
| } | |
| } | |
| function resetearFormulario() { | |
| document.getElementById('fecha').valueAsDate = new Date(); | |
| document.getElementById('nombre').value = ''; | |
| document.getElementById('descripcion').value = ''; | |
| document.getElementById('total').value = ''; | |
| modoEdicion = false; | |
| idEdicion = null; | |
| btnAgregar.innerHTML = '<i class="fas fa-plus mr-2"></i> Agregar'; | |
| btnAgregar.className = 'bg-blue-600 text-white py-2 px-6 rounded-lg hover:bg-blue-700 transition flex items-center'; | |
| btnCancelarEdicion.classList.add('hidden'); | |
| document.getElementById('nombre').focus(); | |
| } | |
| /******************************* | |
| * Dashboard y Reportes | |
| *******************************/ | |
| async function updateDashboard() { | |
| try { | |
| const pendientes = await memoryStorage.getPendientes(); | |
| const historial = await memoryStorage.getHistorial(); | |
| // Totales | |
| const totalPendiente = pendientes.reduce((sum, item) => sum + (item.total || 0), 0); | |
| const totalPagado = historial.reduce((sum, item) => sum + (item.total || 0), 0); | |
| // Contadores | |
| const countPendientes = pendientes.length; | |
| const countHistorial = historial.length; | |
| // Mes actual | |
| const now = new Date(); | |
| const currentMonth = now.getMonth() + 1; | |
| const currentYear = now.getFullYear(); | |
| const monthPagado = historial | |
| .filter(item => { | |
| const dateParts = item.fechaPago?.split('-'); | |
| if (dateParts?.length === 3) { | |
| return parseInt(dateParts[0]) === currentYear && parseInt(dateParts[1]) === currentMonth; | |
| } | |
| return false; | |
| }) | |
| .reduce((sum, item) => sum + (item.total || 0), 0); | |
| // Vencidos (m谩s de 30 d铆as) | |
| const countVencidos = pendientes.filter(item => { | |
| try { | |
| const itemDate = new Date(item.fecha); | |
| const diffTime = now - itemDate; | |
| const diffDays = diffTime / (1000 * 60 * 60 * 24); | |
| return diffDays > 30; | |
| } catch (e) { | |
| return false; | |
| } | |
| }).length; | |
| // Actualizar UI | |
| document.getElementById('total-pendiente').textContent = `$${totalPendiente.toFixed(2)}`; | |
| document.getElementById('total-pagado').textContent = `$${totalPagado.toFixed(2)}`; | |
| document.getElementById('month-pagado').textContent = `$${monthPagado.toFixed(2)}`; | |
| document.getElementById('count-pendientes').textContent = countPendientes; | |
| document.getElementById('count-historial').textContent = countHistorial; | |
| document.getElementById('count-vencidos').textContent = countVencidos; | |
| document.getElementById('last-update').textContent = new Date().toLocaleTimeString(); | |
| // Pr贸ximos vencimientos (pr贸ximos 7 d铆as) | |
| const upcomingPayments = pendientes | |
| .filter(item => { | |
| try { | |
| const itemDate = new Date(item.fecha); | |
| const diffTime = itemDate - now; | |
| const diffDays = diffTime / (1000 * 60 * 60 * 24); | |
| return diffDays > 0 && diffDays <= 7; | |
| } catch (e) { | |
| return false; | |
| } | |
| }) | |
| .sort((a, b) => new Date(a.fecha) - new Date(b.fecha)) | |
| .slice(0, 5); | |
| const upcomingTbody = document.getElementById('upcoming-payments'); | |
| upcomingTbody.innerHTML = ''; | |
| if (upcomingPayments.length === 0) { | |
| upcomingTbody.innerHTML = '<tr><td colspan="5" class="px-6 py-4 text-center text-sm text-gray-500">No hay pagos pr贸ximos en los pr贸ximos 7 d铆as</td></tr>'; | |
| } else { | |
| upcomingPayments.forEach(item => { | |
| const row = upcomingTbody.insertRow(); | |
| row.className = 'hover:bg-gray-50'; | |
| // Fecha | |
| const cellFecha = row.insertCell(); | |
| cellFecha.className = 'px-6 py-4 whitespace-nowrap text-sm text-gray-900'; | |
| try { | |
| const dateParts = item.fecha.split('-'); | |
| if (dateParts.length === 3) { | |
| cellFecha.textContent = new Date(parseInt(dateParts[0]), parseInt(dateParts[1]) - 1, parseInt(dateParts[2])).toLocaleDateString(); | |
| } else { | |
| cellFecha.textContent = item.fecha; | |
| } | |
| } catch (e) { | |
| cellFecha.textContent = item.fecha; | |
| } | |
| // Nombre | |
| const cellNombre = row.insertCell(); | |
| cellNombre.className = 'px-6 py-4 whitespace-nowrap text-sm text-gray-900'; | |
| cellNombre.textContent = item.nombre; | |
| // Descripci贸n | |
| const cellDesc = row.insertCell(); | |
| cellDesc.className = 'px-6 py-4 text-sm text-gray-900'; | |
| cellDesc.textContent = item.descripcion; | |
| // Total | |
| const cellTotal = row.insertCell(); | |
| cellTotal.className = 'px-6 py-4 whitespace-nowrap text-sm text-right font-medium'; | |
| cellTotal.textContent = `$${item.total?.toFixed(2) ?? '0.00'}`; | |
| cellTotal.style.color = item.total < 0 ? '#dc2626' : '#1f2937'; | |
| // Estado | |
| const cellEstado = row.insertCell(); | |
| cellEstado.className = 'px-6 py-4 whitespace-nowrap text-sm text-gray-900'; | |
| const badge = document.createElement('span'); | |
| badge.className = 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium'; | |
| try { | |
| const itemDate = new Date(item.fecha); | |
| const diffTime = itemDate - now; | |
| const diffDays = diffTime / (1000 * 60 * 60 * 24); | |
| if (diffDays < 0) { | |
| badge.textContent = 'Vencido'; | |
| badge.classList.add('bg-red-100', 'text-red-800'); | |
| } else if (diffDays <= 3) { | |
| badge.textContent = 'Pr贸ximo'; | |
| badge.classList.add('bg-yellow-100', 'text-yellow-800'); | |
| } else { | |
| badge.textContent = 'Pendiente'; | |
| badge.classList.add('bg-blue-100', 'text-blue-800'); | |
| } | |
| } catch (e) { | |
| badge.textContent = 'Pendiente'; | |
| badge.classList.add('bg-blue-100', 'text-blue-800'); | |
| } | |
| cellEstado.appendChild(badge); | |
| }); | |
| } | |
| // Actualizar gr谩ficos | |
| actualizarGraficosDashboard(pendientes, historial); | |
| } catch (error) { | |
| console.error("Error updating dashboard:", error); | |
| } | |
| } | |
| function actualizarGraficosDashboard(pendientes, historial) { | |
| // Gr谩fico de evoluci贸n de pagos | |
| const ctxPayments = document.getElementById('payments-chart').getContext('2d'); | |
| // Datos de ejemplo para el gr谩fico | |
| const labels = ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic']; | |
| const currentMonth = new Date().getMonth(); | |
| const currentYear = new Date().getFullYear(); | |
| // Calcular totales por mes | |
| const monthlyTotals = Array(12).fill(0); | |
| historial.forEach(item => { | |
| try { | |
| const dateParts = item.fechaPago?.split('-'); | |
| if (dateParts?.length === 3) { | |
| const year = parseInt(dateParts[0]); | |
| const month = parseInt(dateParts[1]) - 1; | |
| if (year === currentYear && month >= 0 && month < 12) { | |
| monthlyTotals[month] += item.total || 0; | |
| } | |
| } | |
| } catch (e) { /* Ignore errors */ } | |
| }); | |
| if (paymentsChart) { | |
| paymentsChart.destroy(); | |
| } | |
| paymentsChart = new Chart(ctxPayments, { | |
| type: 'line', | |
| data: { | |
| labels: labels, | |
| datasets: [{ | |
| label: 'Pagos por mes', | |
| data: monthlyTotals, | |
| backgroundColor: 'rgba(59, 130, 246, 0.1)', | |
| borderColor: 'rgba(59, 130, 246, 1)', | |
| borderWidth: 2, | |
| tension: 0.1, | |
| fill: true | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { | |
| display: false | |
| } | |
| }, | |
| scales: { | |
| y: { | |
| beginAtZero: true, | |
| grid: { | |
| color: 'rgba(0 | |
| </html> |