|
<!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> |