geocoding / README.md
huayllas's picture
Create README.md
999b7fa verified
|
raw
history blame
68 kB
<!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>