2392 lines
117 KiB
HTML
2392 lines
117 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Gestión Punto de Venta</title>
|
|
<link rel="stylesheet" href="../css/Cantina.css" />
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.25/jspdf.plugin.autotable.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
|
|
</head>
|
|
|
|
<body>
|
|
<script>
|
|
window.__SESSION = {
|
|
usuario: '<?php echo addslashes($usuario); ?>',
|
|
rol: '<?php echo addslashes($rol); ?>'
|
|
};
|
|
</script>
|
|
|
|
<header class="topbar">
|
|
<div class="topbar__brand">
|
|
<span class="topbar__brand-title">Cap1tan - Panel de Gestión</span>
|
|
</div>
|
|
<div class="topbar__actions">
|
|
<button id="manual-button" class="topbar-button" type="button">Manual de Uso</button>
|
|
<div class="topbar__user">
|
|
<button id="user-button" class="user-button" type="button">
|
|
<span class="user-icon">👤</span>
|
|
<span id="user-name">Usuario</span>
|
|
</button>
|
|
<div id="user-menu" class="user-menu">
|
|
<button id="perfil-btn" type="button">Mi Perfil</button>
|
|
<button id="cambiar-contraseña-btn" type="button">Seguridad</button>
|
|
<button id="logout-btn" type="button">Cerrar Sesión</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="app-body">
|
|
<aside class="sidebar">
|
|
<ul class="sidebar__lista">
|
|
<?php if ($rol === 'admin'): ?>
|
|
<li><a href="#registrar-usuario" class="sidebar__link">Registrar Usuario</a></li>
|
|
<?php endif; ?>
|
|
|
|
<?php if (in_array($rol, ['admin', 'cantina'])): ?>
|
|
<li><a href="#ventas" class="sidebar__link">Registrar Ventas</a></li>
|
|
<li><a href="#ver_ventas" class="sidebar__link">Ver Ventas</a></li>
|
|
<li><a href="#productos" class="sidebar__link">Productos</a></li>
|
|
<li><a href="#orden" class="sidebar__link">Crear Orden de Compra</a></li>
|
|
<li><a href="#ordenes" class="sidebar__link">Ver Órdenes de Compra</a></li>
|
|
<li><a href="#proveedores" class="sidebar__link">Proveedores</a></li>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($rol === 'admin'): ?>
|
|
<li><a href="#gastos" class="sidebar__link">Gastos</a></li>
|
|
<li><a href="#reportes" class="sidebar__link">Reportes</a></li>
|
|
<?php endif; ?>
|
|
|
|
<?php if (in_array($rol, ['admin', 'cancha'])): ?>
|
|
<li><a href="#reserva" class="sidebar__link">Reserva</a></li>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($rol === 'admin'): ?>
|
|
<li><a href="#cancha" class="sidebar__link">Canchas (Configuración)</a></li>
|
|
<?php endif; ?>
|
|
</ul>
|
|
</aside>
|
|
|
|
<main class="contenido">
|
|
|
|
<section id="manual" class="seccion">
|
|
<h2>Manual de Uso</h2>
|
|
<p>Bienvenido al sistema Cap1tan. Este manual está diseñado para cualquier usuario del sistema y explica los flujos de trabajo completos según el rol: Trabajador Cancha, Trabajador Cantina o Administrador.</p>
|
|
|
|
<h3>1. Flujo general para cualquier usuario</h3>
|
|
<ul>
|
|
<li><strong>Mi Perfil</strong>: haz clic en el icono de usuario y selecciona "Mi Perfil". Allí podrás ver y actualizar tu nombre, correo y teléfono.</li>
|
|
<li><strong>Cambiar Contraseña</strong>: en el mismo menú selecciona "Seguridad". Ingresa tu contraseña actual, la nueva y confírmala.</li>
|
|
<li><strong>Cerrar Sesión</strong>: usa "Cerrar Sesión" para salir del sistema de forma segura.</li>
|
|
<li><strong>Acceso a secciones</strong>: el menú lateral muestra solo las opciones disponibles para tu rol.</li>
|
|
</ul>
|
|
|
|
<h3>2. Trabajador Cancha - Gestión de reservas</h3>
|
|
<p>Este rol se encarga de controlar las reservas y el estado de pago de cada cliente.</p>
|
|
<h4>2.1 Ver reservas</h4>
|
|
<ol>
|
|
<li>Abre la sección <strong>Reserva</strong> en el menú lateral.</li>
|
|
<li>Verás una tabla con las reservas activas.</li>
|
|
<li>La tabla muestra: Nombre, DNI, Teléfono, Horario, Tipo de Cancha, Número de Cancha, Descuento, Total y Estado.</li>
|
|
</ol>
|
|
<h4>2.2 Buscar reservas</h4>
|
|
<ol>
|
|
<li>Usa el filtro por <strong>fecha</strong> si quieres ver las reservas de un día específico.</li>
|
|
<li>Usa el filtro por <strong>nombre</strong> para buscar a un cliente concreto.</li>
|
|
<li>Usa el filtro por <strong>DNI</strong> para localizar una reserva por documento.</li>
|
|
<li>Usa el filtro por <strong>tipo de cancha</strong> para ver solo Fútbol, Pádel o Quincho.</li>
|
|
<li>Haz clic en "Buscar" para aplicar los filtros.</li>
|
|
</ol>
|
|
<h4>2.3 Cambiar el estado de una reserva</h4>
|
|
<ol>
|
|
<li>En la columna <strong>Estado</strong> de la tabla aparecerá un selector desplegable.</li>
|
|
<li>Haz clic en el estado actual.</li>
|
|
<li>Selecciona uno de los valores: <strong>No Pagado</strong>, <strong>Pagado</strong> o <strong>Cancelado</strong>.</li>
|
|
<li>El sistema guardará el cambio y actualizará el estado.</li>
|
|
</ol>
|
|
<p>Este flujo se usa cuando el cliente llega a la cancha, paga su turno o cuando se anula una reserva.</p>
|
|
|
|
<h3>3. Trabajador Cantina - Ventas y stock</h3>
|
|
<p>Este rol gestiona las ventas de productos, el inventario y las órdenes de compra.</p>
|
|
<h4>3.1 Registrar una venta</h4>
|
|
<ol>
|
|
<li>Abre la sección <strong>Registrar Ventas</strong>.</li>
|
|
<li>En la primera fila, ingresa el <strong>Código del Producto</strong> o <strong>Nombre del Producto</strong>.</li>
|
|
<li>Introduce la <strong>Cantidad</strong> que va a vender.</li>
|
|
<li>Haz clic en el botón <strong>+</strong> para añadir el producto a la venta actual.</li>
|
|
<li>Verifica que la lista de venta actual muestre el producto correcto, el precio y el subtotal.</li>
|
|
<li>Revisa el <strong>Total</strong> de la operación.</li>
|
|
<li>Haz clic en <strong>Registrar Venta</strong> para guardar la venta en el sistema.</li>
|
|
</ol>
|
|
<h4>3.2 Consultar ventas</h4>
|
|
<ol>
|
|
<li>Abre la sección <strong>Ver Ventas</strong>.</li>
|
|
<li>Selecciona la fecha deseada en el campo de fecha.</li>
|
|
<li>El sistema mostrará el total de ventas de ese día.</li>
|
|
<li>En la tabla verás cada venta registrada con su ID, hora y monto total.</li>
|
|
<li>Haz clic en una venta para ver el detalle de los productos incluidos.</li>
|
|
</ol>
|
|
<h4>3.3 Gestionar productos</h4>
|
|
<ol>
|
|
<li>Abre la sección <strong>Productos</strong>.</li>
|
|
<li>Completa los campos: descripción, precio de venta, precio de compra y stock.</li>
|
|
<li>Haz clic en <strong>Agregar Producto</strong> para registrar un nuevo producto.</li>
|
|
<li>En la tabla podrás ver los productos registrados y los datos de stock.</li>
|
|
<li>Puedes editar o eliminar productos según los botones de acción disponibles en cada fila.</li>
|
|
</ol>
|
|
<h4>3.4 Crear una orden de compra</h4>
|
|
<ol>
|
|
<li>Abre la sección <strong>Crear Orden de Compra</strong>.</li>
|
|
<li>En el primer bloque, ingresa el código o nombre del proveedor.</li>
|
|
<li>El sistema mostrará el nombre y teléfono del proveedor si coincide.</li>
|
|
<li>En el segundo bloque, ingresa el código del producto y la cantidad a pedir.</li>
|
|
<li>Haz clic en el botón <strong>+</strong> para agregar el producto a la orden.</li>
|
|
<li>Repite hasta tener todos los productos necesarios.</li>
|
|
<li>Finalmente, haz clic en <strong>Guardar Orden Pendiente</strong>.</li>
|
|
</ol>
|
|
<h4>3.5 Revisar órdenes de compra</h4>
|
|
<ol>
|
|
<li>Abre la sección <strong>Ver Órdenes de Compra</strong>.</li>
|
|
<li>Consulta el estado de cada orden: pendiente, aprobada, recibida, etc.</li>
|
|
<li>Haz clic en una orden para ver los productos solicitados y cantidades.</li>
|
|
</ol>
|
|
<h4>3.6 Gestionar proveedores</h4>
|
|
<ol>
|
|
<li>Abre la sección <strong>Proveedores</strong>.</li>
|
|
<li>Registra un nuevo proveedor con sus datos de contacto.</li>
|
|
<li>Consulta, edita o elimina proveedores utilizando las opciones de acción disponibles.</li>
|
|
</ol>
|
|
|
|
<h3>4. Administrador - Control completo del sistema</h3>
|
|
<p>Este rol puede ver y administrar todas las secciones del sistema, incluida la configuración de canchas, gastos y reportes.</p>
|
|
<h4>4.1 Registrar y administrar usuarios</h4>
|
|
<ol>
|
|
<li>Abre la sección <strong>Registrar Usuario</strong>.</li>
|
|
<li>Completa usuario, contraseña, nombre, apellido, DNI, teléfono y correo.</li>
|
|
<li>Selecciona el rol adecuado: <em>Administrador</em>, <em>Trabajador Cantina</em> o <em>Trabajador Cancha</em>.</li>
|
|
<li>Haz clic en <strong>Registrar Usuario</strong>.</li>
|
|
<li>En la tabla, utiliza las acciones para editar o eliminar usuarios existentes.</li>
|
|
</ol>
|
|
<h4>4.2 Controlar gastos</h4>
|
|
<ol>
|
|
<li>Abre la sección <strong>Gastos</strong>.</li>
|
|
<li>Registra el monto, la descripción y la fecha del gasto.</li>
|
|
<li>Guarda el gasto para que se refleje en los reportes.</li>
|
|
</ol>
|
|
<h4>4.3 Generar reportes</h4>
|
|
<ol>
|
|
<li>Abre la sección <strong>Reportes</strong>.</li>
|
|
<li>Selecciona el tipo de reporte: diario, semanal, mensual o anual.</li>
|
|
<li>Selecciona el período de fecha requerido.</li>
|
|
<li>Haz clic en <strong>Generar Reporte</strong>.</li>
|
|
<li>Revisa los datos de ventas, ingresos, egresos y ganancia neta.</li>
|
|
<li>Usa los botones <strong>Exportar a PDF</strong> o <strong>Exportar a Excel</strong> si necesitas descargar el reporte.</li>
|
|
</ol>
|
|
<h4>4.4 Configurar canchas y horarios</h4>
|
|
<ol>
|
|
<li>Abre la sección <strong>Canchas (Configuración)</strong>.</li>
|
|
<li>Configura los horarios de apertura y cierre para lunes a viernes, sábado y domingo.</li>
|
|
<li>Para cada tipo de cancha (Fútbol, Pádel, Quincho), ajusta:
|
|
<ul>
|
|
<li>Precio</li>
|
|
<li>Duración de turno</li>
|
|
<li>Cantidad de canchas o quinchos</li>
|
|
<li>Reservas necesarias para generar cupones</li>
|
|
<li>Porcentaje de descuento de cupones</li>
|
|
<li>Cantidad de faltas permitidas</li>
|
|
<li>Duración de los cupones en días</li>
|
|
</ul>
|
|
</li>
|
|
<li>Haz clic en <strong>Actualizar</strong> para guardar la configuración.</li>
|
|
</ol>
|
|
|
|
<h3>5. Flujos recomendados</h3>
|
|
<h4>5.1 Flujo típico de Trabajador Cancha</h4>
|
|
<ol>
|
|
<li>Al comenzar el turno, abre <strong>Reserva</strong> y filtra por el día.</li>
|
|
<li>Verifica el estado de pago de las reservas próximas.</li>
|
|
<li>Recibe al cliente y confirma la reserva.</li>
|
|
<li>Si el cliente paga en el lugar, cambia el estado a <strong>Pagado</strong>.</li>
|
|
<li>Si el cliente no paga, deja la reserva en <strong>No Pagado</strong> y notifica al administrador si es necesario.</li>
|
|
<li>Si la reserva se cancela, actualiza el estado a <strong>Cancelado</strong>.</li>
|
|
</ol>
|
|
<h4>5.2 Flujo típico de Trabajador Cantina</h4>
|
|
<ol>
|
|
<li>Antes de abrir, revisa el stock en <strong>Productos</strong>.</li>
|
|
<li>Registra ventas en <strong>Registrar Ventas</strong> durante el día.</li>
|
|
<li>Si falta stock, solicita una orden de compra desde <strong>Crear Orden de Compra</strong>.</li>
|
|
<li>Revisa y confirma proveedores desde <strong>Proveedores</strong>.</li>
|
|
<li>Al final del día, consulta <strong>Ver Ventas</strong> para ver el total y comparar con el efectivo.</li>
|
|
</ol>
|
|
<h4>5.3 Flujo típico de Administrador</h4>
|
|
<ol>
|
|
<li>Revisa el panel y verifica usuarios activos.</li>
|
|
<li>Controla inventarios, proveedores y órdenes de compra.</li>
|
|
<li>Actualiza precios y horarios de canchas cuando sea necesario.</li>
|
|
<li>Genera reportes diarios o semanales para analizar ingresos y gastos.</li>
|
|
<li>Registra los gastos administrativos en la sección <strong>Gastos</strong>.</li>
|
|
</ol>
|
|
|
|
<h3>6. Buenas prácticas</h3>
|
|
<ul>
|
|
<li>Usa tu usuario personal, no compartas credenciales.</li>
|
|
<li>Siempre confirma la información antes de guardar cambios.</li>
|
|
<li>Revisa los reportes y los estados de reserva todos los días.</li>
|
|
<li>Mantén actualizados los proveedores y el stock para evitar faltantes.</li>
|
|
<li>Protege la contraseña y cierra sesión al terminar.</li>
|
|
</ul>
|
|
</section>
|
|
|
|
<section id="registrar-usuario" class="seccion">
|
|
<h2>Registrar Usuario</h2>
|
|
<form class="formulario" id="form-registrar-usuario">
|
|
<input type="hidden" id="usuario-id" name="id" />
|
|
<div class="formulario__fila">
|
|
<input type="text" name="usuario" id="usuario-username" placeholder="Nombre de Usuario" required />
|
|
<input type="password" name="contraseña" id="usuario-password" placeholder="Contraseña" required />
|
|
</div>
|
|
<div class="formulario__fila">
|
|
<input type="password" name="confirmar_contraseña" id="usuario-password-confirm" placeholder="Confirmar Contraseña" required />
|
|
</div>
|
|
<div class="formulario__fila">
|
|
<input type="text" name="nombre" placeholder="Nombre" required />
|
|
<input type="text" name="apellido" placeholder="Apellido" required />
|
|
</div>
|
|
<div class="formulario__fila">
|
|
<input type="text" name="dni" placeholder="DNI" required />
|
|
<input type="tel" name="telefono" placeholder="Teléfono" required />
|
|
</div>
|
|
<div class="formulario__fila">
|
|
<input type="email" name="correo" placeholder="Correo Electrónico" />
|
|
<select name="rol" required>
|
|
<option value="">Seleccionar Rol</option>
|
|
<option value="admin">Administrador</option>
|
|
<option value="cantina">Trabajador Cantina</option>
|
|
<option value="cancha">Trabajador Cancha</option>
|
|
</select>
|
|
</div>
|
|
<button type="submit" id="btn-registrar-usuario" class="btn-agregar btn-registrar">Registrar Usuario</button>
|
|
<button type="button" id="btn-cancelar-edicion" class="btn-cerrar" style="display:none; margin-top:0.5rem;">Cancelar edición</button>
|
|
</form>
|
|
|
|
<h3>Usuarios Registrados</h3>
|
|
<div class="tabla-contenedor">
|
|
<table id="tabla-usuarios" class="tabla-usuarios">
|
|
<thead>
|
|
<tr>
|
|
<th>Usuario</th>
|
|
<th>Nombre Completo</th>
|
|
<th>DNI</th>
|
|
<th>Rol</th>
|
|
<th>Teléfono</th>
|
|
<th>Correo</th>
|
|
<th>Acciones</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="ventas" class="seccion">
|
|
<h2>Registrar Venta</h2>
|
|
<form class="formulario venta-formulario">
|
|
<div class="formulario__fila">
|
|
<input type="text" placeholder="Código del Producto" required />
|
|
<input type="text" placeholder="Nombre del Producto" required />
|
|
<input type="number" min="1" placeholder="Cantidad" required />
|
|
<button type="button" class="btn-mas">Agregar</button>
|
|
</div>
|
|
</form>
|
|
<h3>Lista de Venta Actual</h3>
|
|
<table id="tabla-ventas">
|
|
<thead>
|
|
<tr>
|
|
<th>Código</th>
|
|
<th>Producto</th>
|
|
<th>Precio</th>
|
|
<th>Cantidad</th>
|
|
<th>SubTotal</th>
|
|
<th>Acción</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
<div class="venta-footer">
|
|
<div class="total-box">
|
|
<strong>Total:</strong>
|
|
<input type="text" id="total" placeholder="$0.00" readonly />
|
|
</div>
|
|
<button type="button" class="btn-agregar" id="btn-registrar-venta">Registrar Venta</button>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="ver_ventas" class="seccion">
|
|
<h2>Ver Ventas</h2>
|
|
<div class="total-box">
|
|
<label>Fecha:</label>
|
|
<input type="date" id="fecha-resumen" />
|
|
<div class="venta">
|
|
<strong>Total del Día:</strong>
|
|
<input type="text" id="total-ver" placeholder="$0.00" readonly />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Vista Principal: Tabla de Ventas del Día -->
|
|
<div id="tabla-principal-contenedor">
|
|
<table id="tabla-ver-ventas" class="tabla-ventas-dia">
|
|
<thead>
|
|
<tr>
|
|
<th>ID Venta</th>
|
|
<th>Hora</th>
|
|
<th>Monto Total</th>
|
|
<th>Acciones</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Vista Detalle: Productos de la Venta -->
|
|
<div id="detalle-venta-contenedor" style="display: none;">
|
|
<div class="detalle-header">
|
|
<h3>Detalle de Venta</h3>
|
|
<button id="btn-volver-ventas" class="btn-volver" title="Volver a ventas">← Volver</button>
|
|
</div>
|
|
<table id="tabla-detalle-venta" class="tabla-detalle">
|
|
<thead>
|
|
<tr>
|
|
<th>Producto</th>
|
|
<th style="text-align: center;">Cantidad</th>
|
|
<th style="text-align: right;">Precio Unitario</th>
|
|
<th style="text-align: right;">Subtotal</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="productos" class="seccion">
|
|
<h2>Gestionar Productos</h2>
|
|
<form class="formulario producto-formulario" id="form-producto">
|
|
<div class="formulario__fila">
|
|
<input type="text" name="descripcion" placeholder="Descripción" required />
|
|
<input type="number" name="precio_venta" min="0" step="0.01" placeholder="Precio Venta" required />
|
|
<input type="number" name="precio_compra" min="0" step="0.01" placeholder="Precio Compra" required />
|
|
<input type="number" name="cantidad" min="0" placeholder="Cantidad / Stock" required />
|
|
</div>
|
|
<button type="submit" class="btn-agregar">Agregar Producto</button>
|
|
</form>
|
|
|
|
<div id="gestionar-stock" style="margin: 15px 0; padding: 12px; background: #2a2a2a; border-radius: 8px;">
|
|
<h3 style="margin:0 0 8px 0;">Gestionar Stock</h3>
|
|
<div style="display:flex; gap:10px; align-items:center;">
|
|
<label style="color:#ccc;">Límite de alerta:</label>
|
|
<input type="number" class="stock-limit-input" id="stock-limit-input" min="0" value="10" style="width:100px; padding:6px;" />
|
|
<button type="button" id="save-stock-limit" class="btn-agregar">Guardar Límite</button>
|
|
<small style="color:#999; margin-left:8px;">Cuando el stock esté por debajo del límite se mostrará una alerta.</small>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>Lista de Productos en Stock</h3>
|
|
<table id="tabla-productos">
|
|
<thead>
|
|
<tr>
|
|
<th>Código</th>
|
|
<th>Producto</th>
|
|
<th>Precio Venta</th>
|
|
<th>Precio Compra</th>
|
|
<th>Stock</th>
|
|
<th>Acción</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</section>
|
|
|
|
<section id="orden" class="seccion">
|
|
<h2>Crear Orden de Compra</h2>
|
|
<form id="form-orden" class="formulario">
|
|
<div class="formulario__fila">
|
|
<input type="text" id="codigo-proveedor" placeholder="Código del Proveedor" />
|
|
<input type="text" id="nombre-proveedor" placeholder="Proveedor" readonly />
|
|
<input type="text" id="telefono-proveedor" placeholder="Teléfono" readonly />
|
|
</div>
|
|
<div class="formulario__fila">
|
|
<input type="text" id="codigo-producto" placeholder="Código del Producto" />
|
|
<input type="number" id="cantidad-producto" min="1" placeholder="Cantidad" />
|
|
<button type="button" class="btn-mas" id="btn-agregar-producto">+</button>
|
|
</div>
|
|
</form>
|
|
|
|
<h3>Lista de Productos a Pedir</h3>
|
|
<table id="tabla-orden">
|
|
<thead>
|
|
<tr>
|
|
<th>Código</th>
|
|
<th>Producto</th>
|
|
<th>Cantidad</th>
|
|
<th>Acción</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
<div class="contenedor-btn-registrar" style="margin-top: 15px;">
|
|
<button type="button" id="btn-generar-orden" class="btn-agregar">Guardar Orden Pendiente</button>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="ordenes" class="seccion">
|
|
<h2>Ver Órdenes de Compra</h2>
|
|
<table id="tabla-ordenes">
|
|
<thead>
|
|
<tr>
|
|
<th>ID Orden</th>
|
|
<th>Fecha</th>
|
|
<th>ID Proveedor</th>
|
|
<th>Proveedor</th>
|
|
<th>Teléfono</th>
|
|
<th>Estado</th>
|
|
<th>Acciones</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
|
|
<div id="productos-orden" style="display: none; margin-top: 20px; border: 1px solid #ccc; padding: 15px; border-radius: 8px;">
|
|
<div class="detalle-header">
|
|
<h3>Productos de la Orden</h3>
|
|
<div class="detalle-header__acciones">
|
|
<button id="btn-confirmar-recepcion" class="btn-agregar" type="button">Confirmar Recepción</button>
|
|
<button id="btn-editar-orden" class="btn-editar" type="button">Editar</button>
|
|
<button id="btn-guardar-orden" class="btn-agregar" type="button" style="display:none;">Guardar</button>
|
|
<button id="btn-cancelar-orden" class="btn-cerrar" type="button" style="display:none;">Cancelar</button>
|
|
<button id="cerrar-orden" class="btn-cerrar" type="button">Cerrar Detalle</button>
|
|
</div>
|
|
</div>
|
|
<table style="width: 100%; margin-top: 10px;">
|
|
<thead>
|
|
<tr>
|
|
<th style="text-align: left;">Producto</th>
|
|
<th>Cantidad</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="productos-orden-body"></tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="proveedores" class="seccion">
|
|
<h2>Cargar Proveedor</h2>
|
|
<form class="formulario proveedor-formulario" id="form-proveedor">
|
|
<div class="formulario__fila">
|
|
<input type="text" class="nombre-proveedor" placeholder="Nombre del Proveedor" required />
|
|
<input type="text" class="telefono-proveedor" placeholder="Teléfono" required />
|
|
</div>
|
|
<button type="submit" class="btn-agregar">Agregar Proveedor</button>
|
|
</form>
|
|
|
|
<h3 id="titulo-proveedores">Lista de Proveedores</h3>
|
|
<table id="tabla-proveedores">
|
|
<thead>
|
|
<tr>
|
|
<th>Código</th>
|
|
<th>Proveedor</th>
|
|
<th>Teléfono</th>
|
|
<th>Acción</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
|
|
<div id="productos-proveedor" style="display: none; margin-top: 20px; border: 1px solid #ccc; padding: 15px; border-radius: 8px;">
|
|
<button id="cerrar-productos" class="btn-cerrar" style="float: right">Cerrar</button>
|
|
<h3>Productos del Proveedor</h3>
|
|
<table style="width: 100%; margin-bottom: 15px;">
|
|
<thead>
|
|
<tr>
|
|
<th>ID Producto</th>
|
|
<th>Nombre</th>
|
|
<th>Acción</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="productos-proveedor-body"></tbody>
|
|
</table>
|
|
|
|
<h4>Vincular nuevo producto</h4>
|
|
<form class="formulario" id="form-agregar-producto">
|
|
<div class="formulario__fila">
|
|
<input type="hidden" id="id-proveedor-producto" />
|
|
<input type="number" id="nuevo-id-producto" placeholder="ID del producto" required />
|
|
<button type="submit" class="btn-agregar">Vincular Producto</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="gastos" class="seccion">
|
|
<h2>Gestión de Gastos</h2>
|
|
<h3>Agregar Nuevo Gasto</h3>
|
|
<form id="form-agregar-tipo-gasto" class="formulario">
|
|
<div class="formulario__fila">
|
|
<input type="text" id="nombre-nuevo-gasto" placeholder="Descripción del gasto" required />
|
|
<input type="number" id="monto-nuevo-gasto" placeholder="Monto" min="0" step="0.01" required />
|
|
<button type="submit" class="btn-agregar">Registrar Gasto</button>
|
|
</div>
|
|
</form>
|
|
|
|
<h3>Historial de Gastos</h3>
|
|
<table id="tabla-gastos">
|
|
<thead>
|
|
<tr>
|
|
<th>Descripción</th>
|
|
<th>Monto</th>
|
|
<th>Acciones</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</section>
|
|
|
|
<section id="reportes" class="seccion">
|
|
<h2>Generar Reportes</h2>
|
|
<form class="formulario" id="form-filtros">
|
|
<div class="formulario__fila">
|
|
<label for="tipo-reporte-select">Tipo de Reporte:</label>
|
|
<select id="tipo-reporte-select" name="tipo_reporte_select">
|
|
<option value="financiero">Reporte Financiero</option>
|
|
<option value="ventas_cantina">Ventas de Cantina (Top Productos)</option>
|
|
<option value="ocupacion_canchas">Ocupación de Canchas</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="formulario__fila">
|
|
<label for="tipo-periodo">Periodo:</label>
|
|
<select id="tipo-periodo" name="tipo">
|
|
<option value="diario">Diario</option>
|
|
<option value="semanal">Semanal</option>
|
|
<option value="mensual">Mensual</option>
|
|
<option value="anual">Anual</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div id="filtro-tipo-cancha" class="formulario__fila" style="display: none;">
|
|
<label for="tipo-cancha-select">Tipo de Cancha:</label>
|
|
<select id="tipo-cancha-select" name="tipo_cancha">
|
|
<option value="">Todas las Canchas</option>
|
|
<option value="F">Fútbol</option>
|
|
<option value="P">Pádel</option>
|
|
</select>
|
|
</div>
|
|
<div id="periodo-diario" class="formulario__fila" style="display: none;">
|
|
<label for="fecha-dia">Día:</label>
|
|
<input type="date" id="fecha-dia" name="fecha_dia" />
|
|
</div>
|
|
|
|
<div id="periodo-semanal" class="formulario__fila" style="display: none;">
|
|
<label for="fecha-semana">Semana:</label>
|
|
<input type="week" id="fecha-semana" name="fecha_semana" />
|
|
</div>
|
|
|
|
<div id="periodo-mensual" class="formulario__fila" style="display: none;">
|
|
<label for="fecha-mes">Mes:</label>
|
|
<input type="month" id="fecha-mes" name="fecha_mes" />
|
|
</div>
|
|
|
|
<div id="periodo-anual" class="formulario__fila" style="display: none;">
|
|
<label for="fecha-anio">Año:</label>
|
|
<input type="number" id="fecha-anio" name="fecha_anio" min="2020" max="2099" />
|
|
</div>
|
|
|
|
<button type="submit" class="btn-generar">Generar Reporte</button>
|
|
</form>
|
|
|
|
<div id="resultados" class="resultados" style="display: none;">
|
|
<!-- <h3>Resultados del Reporte</h3> -->
|
|
<div id="contenido-reporte"></div>
|
|
<div class="acciones">
|
|
<button id="export-pdf" class="btn-exportar">Exportar a PDF</button>
|
|
<button id="export-excel" class="btn-exportar">Exportar a Excel</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="reserva" class="seccion">
|
|
<h2>Gestión de Reservas</h2>
|
|
<form class="formulario filtro-formulario">
|
|
<div class="formulario__fila">
|
|
<input type="date" name="fecha" placeholder="Buscar por Fecha" />
|
|
<input type="text" name="nombre" placeholder="Buscar por Nombre" />
|
|
<input type="text" name="dni" placeholder="Buscar por DNI" />
|
|
<select name="tipo_cancha">
|
|
<option value="">Todos los Tipos</option>
|
|
<option value="futbol">Fútbol</option>
|
|
<option value="padel">Pádel</option>
|
|
<option value="quincho">Quincho</option>
|
|
</select>
|
|
</div>
|
|
<div class="formulario__fila" style="justify-content: flex-end;">
|
|
<button type="button" id="btn-reestablecer-filtros" class="btn-cerrar" style="padding: 0.5rem 1rem; font-size: 0.9em;">Reestablecer Filtros</button>
|
|
</div>
|
|
</form>
|
|
|
|
<h3>Listado de Reservas</h3>
|
|
<table id="tabla-reservas">
|
|
<thead>
|
|
<tr>
|
|
<th>Nombre</th>
|
|
<th>DNI</th>
|
|
<th>Teléfono</th>
|
|
<th>Horario</th>
|
|
<th>Tipo Cancha</th>
|
|
<th>N° Cancha</th>
|
|
<th>Descuento</th>
|
|
<th>Total</th>
|
|
<th>Estado</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</section>
|
|
|
|
<section id="cancha" class="seccion">
|
|
<h2>Configuración de Canchas</h2>
|
|
|
|
<h3>Horarios del Complejo</h3>
|
|
<form class="formulario horarios-formulario" id="form-horarios">
|
|
<div class="formulario__fila">
|
|
<label>Lunes a Viernes:</label>
|
|
<input type="time" id="apertura-l" name="apertura" />
|
|
<input type="time" id="cierre-l" name="cierre" />
|
|
</div>
|
|
<div class="formulario__fila">
|
|
<label>Sábado:</label>
|
|
<input type="time" id="apertura-s" name="apertura" />
|
|
<input type="time" id="cierre-s" name="cierre" />
|
|
</div>
|
|
<div class="formulario__fila">
|
|
<label>Domingo:</label>
|
|
<input type="time" id="apertura-d" name="apertura" />
|
|
<input type="time" id="cierre-d" name="cierre" />
|
|
</div>
|
|
<button class="btn-agregar" id="actualizar" type="submit" style="margin-top: 10px;">Guardar Horarios</button>
|
|
</form>
|
|
|
|
<hr style="margin: 30px 0; border: 1px solid #ddd;">
|
|
|
|
<h3>Cancha de Fútbol</h3>
|
|
<form class="formulario cancha-formulario" data-tipo="F">
|
|
<div class="formulario__fila">
|
|
</div>
|
|
<div class="formulario__fila">
|
|
<label>Precio:</label>
|
|
<input type="number" id="precio-F" required />
|
|
<label>Duración (minutos):</label>
|
|
<input type="number" id="duracion-F" required />
|
|
<label>Cantidad de canchas:</label>
|
|
<input type="number" id="cantidad-F" required />
|
|
</div>
|
|
<div class="formulario__fila">
|
|
<label>Reservas para Cupón:</label>
|
|
<input type="number" id="reservas-cupon-F" />
|
|
<label>Descuento Cupón (%):</label>
|
|
<input type="text" id="descuento-F" inputmode="decimal" pattern="^\d+(?:[.,]\d+)?$" placeholder="Ej: 25" />
|
|
</div>
|
|
<div class="formulario__fila">
|
|
<label>Faltas permitidas:</label>
|
|
<input type="number" id="faltas-F" />
|
|
<label>Duración de Cupón (días):</label>
|
|
<input type="number" id="duracion-cupon-F" />
|
|
</div>
|
|
<button class="btn-agregar" type="submit" style="margin-top: 10px;">Actualizar Fútbol</button>
|
|
</form>
|
|
|
|
<hr style="margin: 30px 0; border: 1px solid #ddd;">
|
|
|
|
<h3>Cancha de Pádel</h3>
|
|
<form class="formulario cancha-formulario" data-tipo="P">
|
|
<div class="formulario__fila">
|
|
<label>Precio:</label>
|
|
<input type="number" id="precio-P" required />
|
|
<label>Duración (minutos):</label>
|
|
<input type="number" id="duracion-P" required />
|
|
<label>Cantidad de canchas:</label>
|
|
<input type="number" id="cantidad-P" required />
|
|
</div>
|
|
<div class="formulario__fila">
|
|
<label>Reservas para Cupón:</label>
|
|
<input type="number" id="reservas-cupon-P" />
|
|
<label>Descuento Cupón (%):</label>
|
|
<input type="text" id="descuento-P" inputmode="decimal" pattern="^\d+(?:[.,]\d+)?$" placeholder="Ej: 12,5" />
|
|
</div>
|
|
<div class="formulario__fila">
|
|
<label>Faltas permitidas:</label>
|
|
<input type="number" id="faltas-P" />
|
|
<label>Duración de Cupón (días):</label>
|
|
<input type="number" id="duracion-cupon-P" />
|
|
</div>
|
|
<button class="btn-agregar" type="submit" style="margin-top: 10px;">Actualizar Pádel</button>
|
|
</form>
|
|
|
|
<hr style="margin: 30px 0; border: 1px solid #ddd;">
|
|
|
|
<h3>Quincho</h3>
|
|
<form class="formulario cancha-formulario" data-tipo="Q">
|
|
<div class="formulario__fila">
|
|
<label>Precio:</label>
|
|
<input type="number" id="precio-Q" required />
|
|
<label>Cantidad de quinchos:</label>
|
|
<input type="number" id="cantidad-Q" required />
|
|
</div>
|
|
<div class="formulario__fila">
|
|
<label>Reservas máximas por dia:</label>
|
|
<input type="number" id="reservas-Q" />
|
|
</div>
|
|
<button class="btn-agregar" type="submit" style="margin-top: 10px;">Actualizar Quincho</button>
|
|
</form>
|
|
</section>
|
|
|
|
</main>
|
|
</div>
|
|
|
|
<div id="perfil-modal" class="modal-overlay">
|
|
<div class="modal">
|
|
<div class="modal-header">
|
|
<h3>Mi Perfil</h3>
|
|
<button id="perfil-cerrar" class="btn-cerrar" type="button">X</button>
|
|
</div>
|
|
<form id="perfil-form" class="formulario">
|
|
<input type="hidden" id="perfil-id" />
|
|
|
|
<!-- Datos no editables -->
|
|
<div class="formulario__fila">
|
|
<label><strong>Nombre: </strong> <span id="perfil-nombre-display"></span></label>
|
|
</div>
|
|
|
|
<div class="formulario__fila">
|
|
<label><strong>DNI: </strong> <span id="perfil-dni-display"></span></label>
|
|
</div>
|
|
|
|
<!-- Datos editables -->
|
|
<div class="formulario__fila">
|
|
<label for="perfil-usuario"><strong>Usuario:</strong></label>
|
|
<input type="text" id="perfil-usuario" required />
|
|
</div>
|
|
|
|
<div class="formulario__fila">
|
|
<label for="perfil-correo"><strong>Correo:</strong></label>
|
|
<input type="email" id="perfil-correo" />
|
|
</div>
|
|
|
|
<div class="formulario__fila">
|
|
<label for="perfil-telefono"><strong>Teléfono:</strong></label>
|
|
<input type="tel" id="perfil-telefono" />
|
|
</div>
|
|
|
|
<button type="submit" class="btn-agregar" style="width: 100%; margin-top: 15px;">Guardar cambios</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="seguridad-modal" class="modal-overlay">
|
|
<div class="modal">
|
|
<div class="modal-header">
|
|
<h3>Cambiar Contraseña</h3>
|
|
<button id="seguridad-cerrar" class="btn-cerrar" type="button">X</button>
|
|
</div>
|
|
<form id="seguridad-form" class="formulario">
|
|
<input type="hidden" id="seguridad-id" />
|
|
|
|
<div class="formulario__fila">
|
|
<label for="seguridad-actual"><strong>Contraseña Actual:</strong></label>
|
|
<input type="password" id="seguridad-actual" required />
|
|
</div>
|
|
|
|
<div class="formulario__fila">
|
|
<label for="seguridad-nueva"><strong>Contraseña Nueva:</strong></label>
|
|
<input type="password" id="seguridad-nueva" required />
|
|
</div>
|
|
|
|
<div class="formulario__fila">
|
|
<label for="seguridad-confirmar"><strong>Confirmar Contraseña Nueva:</strong></label>
|
|
<input type="password" id="seguridad-confirmar" required />
|
|
</div>
|
|
|
|
<button type="submit" class="btn-agregar" style="width: 100%; margin-top: 15px;">Actualizar Contraseña</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="../js/Resumen_Ventas.js"></script>
|
|
<script src="../js/Tabla_Ventas.js"></script>
|
|
<script src="../js/Tabla_Productos.js"></script>
|
|
<script src="../js/Orden_Compra.js"></script>
|
|
<script src="../js/Tabla_Proveedor.js"></script>
|
|
<script src="../js/Tabla_Gastos.js"></script>
|
|
<script src="../js/Tabla_Reservas.js"></script>
|
|
<script src="../js/Gestion_Canchas.js"></script>
|
|
<script src="../js/Gestion_Usuarios.js"></script>
|
|
|
|
<script>
|
|
const permisosSeccion = {
|
|
admin: ['registrar-usuario', 'ventas', 'ver_ventas', 'productos', 'orden', 'ordenes', 'proveedores', 'gastos', 'reportes', 'reserva', 'cancha', 'manual'],
|
|
cantina: ['ventas', 'ver_ventas', 'productos', 'orden', 'proveedores', 'manual'],
|
|
cancha: ['reserva', 'manual']
|
|
};
|
|
let permisosActuales = [];
|
|
|
|
async function cargarSesion() {
|
|
if (window.__SESSION && window.__SESSION.usuario) {
|
|
const usuarioBasico = {
|
|
Usuario: window.__SESSION.usuario,
|
|
Rol: window.__SESSION.rol
|
|
};
|
|
document.getElementById('user-name').textContent = usuarioBasico.Usuario || 'Usuario';
|
|
return usuarioBasico;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch('../php/Session_Info.php');
|
|
const data = await res.json();
|
|
|
|
if (!data.success) {
|
|
window.location.href = '../html/Login.html';
|
|
return null;
|
|
}
|
|
|
|
const usuario = data.usuario;
|
|
document.getElementById('user-name').textContent = usuario.Nombre || usuario.Usuario;
|
|
document.getElementById('perfil-id').value = usuario.ID_Usuario || '';
|
|
document.getElementById('perfil-nombre-display').textContent = (usuario.Nombre || '') + ' ' + (usuario.Apellido || '');
|
|
document.getElementById('perfil-dni-display').textContent = usuario.DNI || '';
|
|
document.getElementById('perfil-usuario').value = usuario.Usuario || '';
|
|
document.getElementById('perfil-correo').value = usuario.Correo || '';
|
|
document.getElementById('perfil-telefono').value = usuario.Telefono || '';
|
|
|
|
return usuario;
|
|
} catch (error) {
|
|
console.error('Error al obtener sesión:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function aplicarPermisos(rol) {
|
|
permisosActuales = permisosSeccion[rol] || [];
|
|
|
|
document.querySelectorAll('.sidebar__link').forEach(link => {
|
|
const href = link.getAttribute('href');
|
|
if (href && href.startsWith('#')) {
|
|
const id = href.replace('#', '');
|
|
const visible = permisosActuales.includes(id);
|
|
link.closest('li').style.display = visible ? '' : 'none';
|
|
}
|
|
});
|
|
}
|
|
|
|
function mostrarSeccion(id) {
|
|
const secciones = document.querySelectorAll('.seccion');
|
|
secciones.forEach(s => s.style.display = 'none');
|
|
|
|
let seccionId = id;
|
|
if (!permisosActuales.includes(seccionId)) {
|
|
seccionId = permisosActuales[0] || 'registrar-usuario';
|
|
}
|
|
|
|
const target = document.getElementById(seccionId);
|
|
if (target && target.classList.contains('seccion')) {
|
|
target.style.display = 'block';
|
|
}
|
|
|
|
return seccionId;
|
|
}
|
|
|
|
function activarLink(id) {
|
|
document.querySelectorAll('.sidebar__link').forEach(link => {
|
|
if (link.getAttribute('href') === '#' + id) {
|
|
link.classList.add('activo');
|
|
} else {
|
|
link.classList.remove('activo');
|
|
}
|
|
});
|
|
}
|
|
|
|
function manejarHash(hash) {
|
|
const tieneHashExplicito = Boolean(hash && hash !== '#');
|
|
const seccionSolicitada = (hash || '').replace('#', '');
|
|
const id = tieneHashExplicito && permisosActuales.includes(seccionSolicitada)
|
|
? seccionSolicitada
|
|
: permisosActuales.includes('ventas')
|
|
? 'ventas'
|
|
: permisosActuales[0] || 'registrar-usuario';
|
|
|
|
const seccionActual = mostrarSeccion(id);
|
|
activarLink(seccionActual);
|
|
|
|
if (tieneHashExplicito && window.location.hash !== '#' + seccionActual) {
|
|
history.replaceState(null, '', window.location.pathname + window.location.search + '#' + seccionActual);
|
|
} else if (!tieneHashExplicito && window.location.hash) {
|
|
history.replaceState(null, '', window.location.pathname + window.location.search);
|
|
}
|
|
|
|
// Trigger data loaders for dynamic sections
|
|
if (seccionActual === 'productos' && window.cargarProductos) {
|
|
setTimeout(() => window.cargarProductos(), 100);
|
|
}
|
|
if (seccionActual === 'reserva' && window.cargarReservas) {
|
|
setTimeout(() => window.cargarReservas(), 100);
|
|
}
|
|
if (seccionActual === 'cancha' && window.cargarConfiguracionCanchas) {
|
|
console.log('Navegando a canchas - cargando configuración...');
|
|
setTimeout(() => window.cargarConfiguracionCanchas(), 100);
|
|
}
|
|
if (seccionActual === 'cancha' && window.cargarHorarios) {
|
|
console.log('Navegando a canchas - cargando horarios...');
|
|
setTimeout(() => window.cargarHorarios(), 100);
|
|
}
|
|
}
|
|
|
|
function toggleUserMenu() {
|
|
document.getElementById('user-menu').classList.toggle('visible');
|
|
}
|
|
|
|
function cerrarPerfil() {
|
|
document.getElementById('perfil-modal').classList.remove('active');
|
|
}
|
|
|
|
function cerrarSeguridad() {
|
|
document.getElementById('seguridad-modal').classList.remove('active');
|
|
}
|
|
|
|
async function abrirSeguridad() {
|
|
try {
|
|
const res = await fetch('../php/Session_Info.php');
|
|
const data = await res.json();
|
|
|
|
if (!data.success) {
|
|
alert('Error al cargar datos del usuario');
|
|
return;
|
|
}
|
|
|
|
const usuario = data.usuario;
|
|
document.getElementById('seguridad-id').value = usuario.ID_Usuario || '';
|
|
console.log('Modal de seguridad abierto para usuario:', usuario.Usuario);
|
|
|
|
} catch (error) {
|
|
console.error('Error al cargar datos para cambio de contraseña:', error);
|
|
alert('Error al cargar datos');
|
|
return;
|
|
}
|
|
|
|
document.getElementById('seguridad-modal').classList.add('active');
|
|
document.getElementById('user-menu').classList.remove('visible');
|
|
}
|
|
|
|
async function guardarSeguridad(event) {
|
|
event.preventDefault();
|
|
|
|
const contraseniaActual = document.getElementById('seguridad-actual').value;
|
|
const contraseniaNueva = document.getElementById('seguridad-nueva').value;
|
|
const confirmarContrasenia = document.getElementById('seguridad-confirmar').value;
|
|
const usuarioId = document.getElementById('seguridad-id').value;
|
|
|
|
// Validaciones
|
|
if (!contraseniaActual || !contraseniaNueva || !confirmarContrasenia) {
|
|
alert('Todos los campos son obligatorios');
|
|
return;
|
|
}
|
|
|
|
if (contraseniaNueva !== confirmarContrasenia) {
|
|
alert('Las contraseñas nuevas no coinciden');
|
|
return;
|
|
}
|
|
|
|
if (contraseniaNueva.length < 6) {
|
|
alert('La contraseña debe tener al menos 6 caracteres');
|
|
return;
|
|
}
|
|
|
|
const payload = {
|
|
id: usuarioId,
|
|
contrasenia_actual: contraseniaActual,
|
|
contrasenia_nueva: contraseniaNueva
|
|
};
|
|
|
|
try {
|
|
const res = await fetch('../php/Cambiar_Contrasena.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
console.log('Response status:', res.status);
|
|
const responseText = await res.text();
|
|
console.log('Response text:', responseText);
|
|
|
|
let data;
|
|
try {
|
|
data = JSON.parse(responseText);
|
|
} catch (parseError) {
|
|
console.error('Error parsing JSON:', parseError);
|
|
console.error('Response was:', responseText);
|
|
alert('Error: El servidor devolvió una respuesta inválida. Verifica la consola.');
|
|
return;
|
|
}
|
|
|
|
if (data.success) {
|
|
alert('Contraseña actualizada con éxito');
|
|
document.getElementById('seguridad-form').reset();
|
|
cerrarSeguridad();
|
|
} else {
|
|
alert('Error: ' + (data.message || 'No se pudo cambiar la contraseña'));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error al cambiar contraseña:', error);
|
|
alert('Error al enviar la solicitud. Verifica la consola del navegador.');
|
|
}
|
|
}
|
|
|
|
async function abrirPerfil() {
|
|
// Siempre cargar datos completos del servidor para el perfil
|
|
try {
|
|
const res = await fetch('../php/Session_Info.php');
|
|
const data = await res.json();
|
|
|
|
if (!data.success) {
|
|
alert('Error al cargar datos del perfil');
|
|
return;
|
|
}
|
|
|
|
const usuario = data.usuario;
|
|
|
|
// Configurar todos los campos del modal de perfil
|
|
document.getElementById('perfil-id').value = usuario.ID_Usuario || '';
|
|
document.getElementById('perfil-nombre-display').textContent = (usuario.Nombre || '') + ' ' + (usuario.Apellido || '');
|
|
document.getElementById('perfil-dni-display').textContent = usuario.DNI || '';
|
|
document.getElementById('perfil-usuario').value = usuario.Usuario || '';
|
|
document.getElementById('perfil-correo').value = usuario.Correo || '';
|
|
document.getElementById('perfil-telefono').value = usuario.Telefono || '';
|
|
|
|
console.log('Datos del perfil cargados:', usuario);
|
|
|
|
} catch (error) {
|
|
console.error('Error al cargar datos del perfil:', error);
|
|
alert('Error al cargar datos del perfil');
|
|
return;
|
|
}
|
|
|
|
document.getElementById('perfil-modal').classList.add('active');
|
|
document.getElementById('user-menu').classList.remove('visible');
|
|
}
|
|
|
|
async function guardarPerfil(event) {
|
|
event.preventDefault();
|
|
const form = document.getElementById('perfil-form');
|
|
const payload = {
|
|
id: document.getElementById('perfil-id').value,
|
|
usuario: document.getElementById('perfil-usuario').value,
|
|
correo: document.getElementById('perfil-correo').value || '',
|
|
telefono: document.getElementById('perfil-telefono').value || ''
|
|
};
|
|
|
|
const res = await fetch('../php/Actualizar_Perfil.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
alert('Perfil actualizado con éxito');
|
|
cerrarPerfil();
|
|
// Recargar sesión para actualizar el nombre en el header
|
|
await cargarSesion();
|
|
} else {
|
|
alert('Error al guardar perfil: ' + (data.message || 'Error desconocido'));
|
|
}
|
|
}
|
|
|
|
async function cerrarSesion() {
|
|
await fetch('../php/Logout.php');
|
|
window.location.href = '../html/Login.html';
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
document.getElementById('user-button').addEventListener('click', toggleUserMenu);
|
|
document.getElementById('perfil-btn').addEventListener('click', abrirPerfil);
|
|
document.getElementById('cambiar-contraseña-btn').addEventListener('click', abrirSeguridad);
|
|
document.getElementById('logout-btn').addEventListener('click', cerrarSesion);
|
|
document.getElementById('manual-button').addEventListener('click', () => {
|
|
window.location.hash = '#manual';
|
|
});
|
|
document.getElementById('perfil-cerrar').addEventListener('click', cerrarPerfil);
|
|
document.getElementById('seguridad-cerrar').addEventListener('click', cerrarSeguridad);
|
|
document.getElementById('perfil-form').addEventListener('submit', guardarPerfil);
|
|
document.getElementById('seguridad-form').addEventListener('submit', guardarSeguridad);
|
|
|
|
document.addEventListener('click', (event) => {
|
|
const menu = document.getElementById('user-menu');
|
|
const button = document.getElementById('user-button');
|
|
if (!menu.contains(event.target) && !button.contains(event.target)) {
|
|
menu.classList.remove('visible');
|
|
}
|
|
});
|
|
|
|
const usuarioSesion = await cargarSesion();
|
|
if (!usuarioSesion) return;
|
|
|
|
aplicarPermisos(usuarioSesion.Rol);
|
|
document.querySelector('.sidebar__lista').classList.add('visible');
|
|
|
|
document.querySelectorAll('.sidebar__link').forEach(link => {
|
|
const href = link.getAttribute('href');
|
|
if (href && href.startsWith('#')) {
|
|
link.addEventListener('click', e => {
|
|
e.preventDefault();
|
|
const id = href.replace('#', '');
|
|
const newHash = '#' + id;
|
|
|
|
if (window.location.hash === newHash) {
|
|
mostrarSeccion(id);
|
|
activarLink(id);
|
|
manejarHash(newHash);
|
|
} else {
|
|
window.location.hash = newHash;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
manejarHash(window.location.hash);
|
|
});
|
|
|
|
window.addEventListener('hashchange', () => {
|
|
manejarHash(window.location.hash);
|
|
});
|
|
|
|
// Código para Reportes
|
|
let periodoActual = {
|
|
tipo: 'diario',
|
|
nombreArchivo: 'reporte',
|
|
datosReporte: null,
|
|
nombreMes: ''
|
|
};
|
|
|
|
const formFiltros = document.getElementById('form-filtros');
|
|
const resultados = document.getElementById('resultados');
|
|
const contenidoReporte = document.getElementById('contenido-reporte');
|
|
const exportPdf = document.getElementById('export-pdf');
|
|
const exportExcel = document.getElementById('export-excel');
|
|
|
|
// Extrae un objeto Date válido a partir de distintos campos posibles en la respuesta
|
|
function obtenerFechaDesdeDatos(data) {
|
|
if (!data) return null;
|
|
const valCandidates = [data.periodo, data.fecha, data.fecha_inicio, data.fecha_inicio_periodo, data.fecha_dia];
|
|
for (const val of valCandidates) {
|
|
if (!val) continue;
|
|
// Si viene en formato YYYY-MM-DD
|
|
if (/^\d{4}-\d{2}-\d{2}$/.test(val)) return new Date(val + 'T00:00:00');
|
|
// Si viene en formato YYYY-MM
|
|
if (/^\d{4}-\d{2}$/.test(val)) return new Date(val + '-01T00:00:00');
|
|
// Si ya es un string de fecha larga, intentar crear Date directamente
|
|
const d = new Date(val);
|
|
if (!isNaN(d.getTime())) return d;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function obtenerMesYAnioDesdeDatos(data) {
|
|
const meses = ['', 'enero','febrero','marzo','abril','mayo','junio','julio','agosto','septiembre','octubre','noviembre','diciembre'];
|
|
if (!data) return { mesNombre: 'Mensual', anio: new Date().getFullYear() };
|
|
if (data.nombre_mes) {
|
|
return { mesNombre: data.nombre_mes, anio: data.nombre_anio || (data.periodo ? data.periodo.split('-')[0] : new Date().getFullYear()) };
|
|
}
|
|
const fecha = obtenerFechaDesdeDatos(data);
|
|
if (fecha) {
|
|
return { mesNombre: fecha.toLocaleDateString('es-ES', { month: 'long' }), anio: fecha.getFullYear() };
|
|
}
|
|
if (data.periodo) {
|
|
const parts = String(data.periodo).split('-');
|
|
const anio = parts[0] || new Date().getFullYear();
|
|
const mesNum = parseInt(parts[1] || '0');
|
|
const mesNombre = meses[mesNum] || 'Mensual';
|
|
return { mesNombre, anio };
|
|
}
|
|
return { mesNombre: 'Mensual', anio: new Date().getFullYear() };
|
|
}
|
|
|
|
// Función para generar nombres de archivo dinámicos
|
|
function generarNombreArchivo(tipo, data, tipoReporte = 'financiero') {
|
|
let nombre = 'reporte';
|
|
let prefijo = tipoReporte === 'financiero' ? 'financiero' :
|
|
tipoReporte === 'ventas_cantina' ? 'ventas_cantina' :
|
|
'ocupacion_canchas';
|
|
|
|
if (tipo === 'diario') {
|
|
const fechaObj = obtenerFechaDesdeDatos(data);
|
|
if (fechaObj) {
|
|
const dd = String(fechaObj.getDate()).padStart(2, '0');
|
|
const mm = String(fechaObj.getMonth() + 1).padStart(2, '0');
|
|
const yy = String(fechaObj.getFullYear()).slice(-2);
|
|
nombre = `${prefijo}_${dd}-${mm}-${yy}`;
|
|
} else {
|
|
nombre = `${prefijo}`;
|
|
}
|
|
} else if (tipo === 'semanal') {
|
|
const inicio = new Date(data.fecha_inicio + 'T00:00:00');
|
|
const fin = new Date(data.fecha_fin + 'T00:00:00');
|
|
const diaInicio = inicio.getDate();
|
|
const diaFin = fin.getDate();
|
|
const mesNombre = inicio.toLocaleDateString('es-ES', { month: 'long' });
|
|
nombre = `${prefijo}_semana_${diaInicio}-${diaFin}_${mesNombre}`;
|
|
} else if (tipo === 'mensual') {
|
|
const mesInfo = obtenerMesYAnioDesdeDatos(data);
|
|
const mesSafe = String(mesInfo.mesNombre).replace(/\s+/g, '_');
|
|
nombre = `${prefijo}_${mesSafe}_${mesInfo.anio}`;
|
|
} else if (tipo === 'anual') {
|
|
// Para reportes anuales, preferir año explícito si viene en los datos.
|
|
let anio = '';
|
|
// Si el backend envía fecha_inicio (p. ej. '2026-01-01'), tomar el año de ahí.
|
|
if (data && data.fecha_inicio && /^\d{4}-\d{2}-\d{2}$/.test(String(data.fecha_inicio))) {
|
|
anio = String(data.fecha_inicio).substring(0,4);
|
|
} else if (data && data.periodo && /^\d{4}$/.test(String(data.periodo))) {
|
|
anio = String(data.periodo);
|
|
} else if (data && data.anio) {
|
|
anio = String(data.anio);
|
|
} else if (obtenerFechaDesdeDatos(data)) {
|
|
anio = obtenerFechaDesdeDatos(data).getFullYear();
|
|
} else {
|
|
anio = new Date().getFullYear();
|
|
}
|
|
nombre = `${prefijo}_${anio}`;
|
|
}
|
|
return nombre;
|
|
}
|
|
|
|
// Manejar cambios en tipo de reporte y período
|
|
const tipoReporteSelect = document.getElementById('tipo-reporte-select');
|
|
const tipoPeriodoSelect = document.getElementById('tipo-periodo');
|
|
|
|
if (tipoReporteSelect) {
|
|
tipoReporteSelect.addEventListener('change', function() {
|
|
const tipoReporte = this.value;
|
|
const filtroTipoCancha = document.getElementById('filtro-tipo-cancha');
|
|
|
|
// Mostrar/ocultar filtro de tipo de cancha según el reporte
|
|
if (tipoReporte === 'ocupacion_canchas') {
|
|
filtroTipoCancha.style.display = 'block';
|
|
} else {
|
|
filtroTipoCancha.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
|
|
if (tipoPeriodoSelect) {
|
|
tipoPeriodoSelect.addEventListener('change', function() {
|
|
const tipo = this.value;
|
|
const today = new Date();
|
|
|
|
// Ocultar todos los periodos
|
|
document.getElementById('periodo-diario').style.display = 'none';
|
|
document.getElementById('periodo-semanal').style.display = 'none';
|
|
document.getElementById('periodo-mensual').style.display = 'none';
|
|
document.getElementById('periodo-anual').style.display = 'none';
|
|
|
|
if (tipo === 'diario') {
|
|
document.getElementById('periodo-diario').style.display = 'block';
|
|
const fechaHoy = today.toISOString().split('T')[0];
|
|
document.getElementById('fecha-dia').value = fechaHoy;
|
|
} else if (tipo === 'semanal') {
|
|
document.getElementById('periodo-semanal').style.display = 'block';
|
|
// Calcular la semana actual (ISO 8601)
|
|
const diaSemana = today.getDay();
|
|
const lunes = new Date(today);
|
|
const diferenciaDias = diaSemana === 0 ? -6 : 1 - diaSemana;
|
|
lunes.setDate(today.getDate() + diferenciaDias);
|
|
|
|
const fechaReferencia = new Date(lunes.getFullYear(), 0, 4);
|
|
const diaSemanaRef = fechaReferencia.getDay();
|
|
const lunesRef = new Date(fechaReferencia);
|
|
lunesRef.setDate(fechaReferencia.getDate() - (diaSemanaRef === 0 ? 6 : diaSemanaRef - 1));
|
|
|
|
const diffMs = lunes - lunesRef;
|
|
const diffDias = Math.floor(diffMs / (24 * 60 * 60 * 1000));
|
|
const numeroSemana = Math.floor(diffDias / 7) + 1;
|
|
const ano = lunes.getFullYear();
|
|
|
|
const weekString = `${ano}-W${String(numeroSemana).padStart(2, '0')}`;
|
|
document.getElementById('fecha-semana').value = weekString;
|
|
} else if (tipo === 'mensual') {
|
|
document.getElementById('periodo-mensual').style.display = 'block';
|
|
const mes = today.getFullYear() + '-' + String(today.getMonth() + 1).padStart(2, '0');
|
|
document.getElementById('fecha-mes').value = mes;
|
|
} else if (tipo === 'anual') {
|
|
document.getElementById('periodo-anual').style.display = 'block';
|
|
document.getElementById('fecha-anio').value = today.getFullYear();
|
|
}
|
|
});
|
|
// Trigger change on load
|
|
tipoPeriodoSelect.dispatchEvent(new Event('change'));
|
|
}
|
|
|
|
if (formFiltros) {
|
|
formFiltros.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const tipoReporte = document.getElementById('tipo-reporte-select').value;
|
|
const tipo = document.getElementById('tipo-periodo').value;
|
|
let params = `tipo_reporte=${tipoReporte}&tipo=${tipo}`;
|
|
|
|
if (tipo === 'diario') {
|
|
const fechaDia = document.getElementById('fecha-dia').value;
|
|
params += `&fecha=${fechaDia}`;
|
|
} else if (tipo === 'semanal') {
|
|
const fechaSemana = document.getElementById('fecha-semana').value;
|
|
params += `&semana=${fechaSemana}`;
|
|
} else if (tipo === 'mensual') {
|
|
const fechaMes = document.getElementById('fecha-mes').value;
|
|
params += `&mes=${fechaMes}`;
|
|
} else if (tipo === 'anual') {
|
|
const fechaAnio = document.getElementById('fecha-anio').value;
|
|
params += `&anio=${fechaAnio}`;
|
|
}
|
|
|
|
// Agregar filtro de tipo de cancha si es reporte de ocupación
|
|
if (tipoReporte === 'ocupacion_canchas') {
|
|
const tipoCancha = document.getElementById('tipo-cancha-select').value;
|
|
if (tipoCancha) params += `&tipo_cancha=${tipoCancha}`;
|
|
}
|
|
|
|
fetch(`../php/Obtener_Reporte.php?${params}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.error) {
|
|
alert('Error: ' + data.error);
|
|
return;
|
|
}
|
|
periodoActual.tipo = tipo;
|
|
periodoActual.tipoReporte = tipoReporte;
|
|
periodoActual.nombreArchivo = generarNombreArchivo(tipo, data, tipoReporte);
|
|
periodoActual.datosReporte = data;
|
|
if (tipo === 'mensual' && data.nombre_mes) {
|
|
periodoActual.nombreMes = data.nombre_mes;
|
|
}
|
|
mostrarReporte(data, tipo, tipoReporte);
|
|
resultados.style.display = 'block';
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('Error al generar el reporte');
|
|
});
|
|
});
|
|
}
|
|
|
|
if (exportPdf) {
|
|
exportPdf.addEventListener('click', function() {
|
|
const { jsPDF } = window.jspdf;
|
|
const doc = new jsPDF();
|
|
const contenido = document.getElementById('contenido-reporte');
|
|
if (!contenido) return;
|
|
|
|
// Construir título con periodo según tipo
|
|
let tituloTexto = '';
|
|
if (periodoActual.tipo === 'diario') {
|
|
const fechaObj = obtenerFechaDesdeDatos(periodoActual.datosReporte);
|
|
if (fechaObj) {
|
|
const fechaFormato = fechaObj.toLocaleDateString('es-ES', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
|
tituloTexto = 'Reporte Diario - ' + fechaFormato;
|
|
} else {
|
|
tituloTexto = 'Reporte Diario';
|
|
}
|
|
} else if (periodoActual.tipo === 'semanal') {
|
|
const inicio = obtenerFechaDesdeDatos(periodoActual.datosReporte) || new Date(periodoActual.datosReporte?.fecha_inicio + 'T00:00:00');
|
|
const fin = new Date(periodoActual.datosReporte?.fecha_fin + 'T00:00:00');
|
|
tituloTexto = 'Reporte Semanal - ' + inicio.toLocaleDateString('es-ES') + ' al ' + fin.toLocaleDateString('es-ES');
|
|
} else if (periodoActual.tipo === 'mensual') {
|
|
const mesInfo = obtenerMesYAnioDesdeDatos(periodoActual.datosReporte);
|
|
const mesNombre = mesInfo.mesNombre.charAt(0).toUpperCase() + mesInfo.mesNombre.slice(1);
|
|
tituloTexto = `Reporte Mensual de ${mesNombre} ${mesInfo.anio}`;
|
|
} else if (periodoActual.tipo === 'anual') {
|
|
// Determinar año para el título: preferir fecha_inicio o un periodo numérico
|
|
let anio = periodoActual.datosReporte?.periodo;
|
|
if (!anio || !/^\d{4}$/.test(String(anio))) {
|
|
if (periodoActual.datosReporte?.fecha_inicio && /^\d{4}-\d{2}-\d{2}$/.test(String(periodoActual.datosReporte.fecha_inicio))) {
|
|
anio = String(periodoActual.datosReporte.fecha_inicio).substring(0,4);
|
|
} else if (periodoActual.datosReporte?.fecha_fin && /^\d{4}-\d{2}-\d{2}$/.test(String(periodoActual.datosReporte.fecha_fin))) {
|
|
anio = String(periodoActual.datosReporte.fecha_fin).substring(0,4);
|
|
} else {
|
|
anio = new Date().getFullYear();
|
|
}
|
|
}
|
|
tituloTexto = `Reporte Anual ${anio}`;
|
|
}
|
|
|
|
if (tituloTexto) {
|
|
doc.setFontSize(14);
|
|
doc.text(tituloTexto, 10, 10);
|
|
}
|
|
|
|
let yPos = 25;
|
|
|
|
// Agregar todas las tablas del contenido
|
|
const tablas = contenido.querySelectorAll('table');
|
|
tablas.forEach((tabla) => {
|
|
doc.autoTable({ html: tabla, startY: yPos });
|
|
yPos = doc.lastAutoTable ? doc.lastAutoTable.finalY + 10 : yPos + 60;
|
|
});
|
|
|
|
// Agregar resúmenes
|
|
const resumenDivs = contenido.querySelectorAll('div[style*="grid"][style*="grid-template-columns"]');
|
|
if (resumenDivs.length > 0) {
|
|
doc.setFontSize(11);
|
|
doc.text('RESUMEN', 10, yPos);
|
|
yPos += 8;
|
|
|
|
const resumenData = [];
|
|
resumenDivs.forEach(div => {
|
|
const items = div.querySelectorAll('div[style*="border"]');
|
|
items.forEach(item => {
|
|
const label = item.querySelector('p:first-child')?.textContent || '';
|
|
const valor = item.querySelector('p:last-child')?.textContent || '';
|
|
if (label && valor) {
|
|
resumenData.push([label.trim(), valor.trim()]);
|
|
}
|
|
});
|
|
});
|
|
|
|
if (resumenData.length > 0) {
|
|
doc.autoTable({
|
|
head: [['Concepto', 'Valor']],
|
|
body: resumenData,
|
|
startY: yPos,
|
|
theme: 'grid'
|
|
});
|
|
}
|
|
}
|
|
|
|
doc.save((periodoActual.nombreArchivo || 'reporte') + '.pdf');
|
|
});
|
|
}
|
|
|
|
if (exportExcel) {
|
|
exportExcel.addEventListener('click', function() {
|
|
const contenido = document.getElementById('contenido-reporte');
|
|
if (!contenido || !window.XLSX) {
|
|
alert('No se pudo cargar la exportación a Excel.');
|
|
return;
|
|
}
|
|
|
|
const rows = [];
|
|
|
|
// Construir título limpio según el tipo de reporte
|
|
let tituloTexto = '';
|
|
if (periodoActual.tipo === 'diario') {
|
|
const fechaObj = obtenerFechaDesdeDatos(periodoActual.datosReporte);
|
|
if (fechaObj) {
|
|
tituloTexto = 'Reporte Diario - ' + fechaObj.toLocaleDateString('es-ES', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
|
} else {
|
|
tituloTexto = 'Reporte Diario';
|
|
}
|
|
} else if (periodoActual.tipo === 'semanal') {
|
|
const inicio = obtenerFechaDesdeDatos(periodoActual.datosReporte) || new Date(periodoActual.datosReporte?.fecha_inicio + 'T00:00:00');
|
|
const fin = new Date(periodoActual.datosReporte?.fecha_fin + 'T00:00:00');
|
|
tituloTexto = 'Reporte Semanal - ' + inicio.toLocaleDateString('es-ES') + ' al ' + fin.toLocaleDateString('es-ES');
|
|
} else if (periodoActual.tipo === 'mensual') {
|
|
const mesInfo = obtenerMesYAnioDesdeDatos(periodoActual.datosReporte);
|
|
const mesNombre = mesInfo.mesNombre.charAt(0).toUpperCase() + mesInfo.mesNombre.slice(1);
|
|
tituloTexto = `Reporte Mensual de ${mesNombre} ${mesInfo.anio}`;
|
|
} else if (periodoActual.tipo === 'anual') {
|
|
const anio = periodoActual.datosReporte?.periodo || periodoActual.datosReporte?.anio || new Date().getFullYear();
|
|
tituloTexto = `Reporte Anual ${anio}`;
|
|
}
|
|
|
|
if (tituloTexto) {
|
|
rows.push([tituloTexto]);
|
|
rows.push([]);
|
|
}
|
|
|
|
contenido.querySelectorAll('table').forEach((tabla) => {
|
|
tabla.querySelectorAll('tr').forEach((fila) => {
|
|
const celdas = fila.querySelectorAll('th, td');
|
|
const valores = Array.from(celdas).map((celda) =>
|
|
celda.textContent.replace(/\s+/g, ' ').trim()
|
|
);
|
|
rows.push(valores);
|
|
});
|
|
rows.push([]);
|
|
});
|
|
|
|
const resumenBlocks = contenido.querySelectorAll('div[style*="grid"][style*="grid-template-columns"]');
|
|
if (resumenBlocks.length > 0) {
|
|
rows.push(['RESUMEN']);
|
|
resumenBlocks.forEach((bloque) => {
|
|
const items = bloque.querySelectorAll('div[style*="border"], div[style*="border-left"]');
|
|
items.forEach((item) => {
|
|
const label = item.querySelector('p:first-child')?.textContent || '';
|
|
const valor = item.querySelector('p:last-child')?.textContent || '';
|
|
if (label && valor) {
|
|
rows.push([label.trim(), valor.trim()]);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
const filasLimpias = rows.filter((fila) => fila.some((valor) => valor !== ''));
|
|
const hoja = XLSX.utils.aoa_to_sheet(filasLimpias);
|
|
|
|
const maxWidth = (arr) =>
|
|
arr.reduce((max, value) => {
|
|
const text = String(value ?? '');
|
|
return Math.max(max, text.length);
|
|
}, 0);
|
|
|
|
const maxCols = Math.max(0, ...filasLimpias.map((fila) => fila.length));
|
|
hoja['!cols'] = Array.from({ length: maxCols }, (_, index) => {
|
|
const colValues = filasLimpias.map((fila) => fila[index] ?? '');
|
|
const ancho = Math.min(50, Math.max(12, maxWidth(colValues) + 2));
|
|
return { width: ancho };
|
|
});
|
|
|
|
const libro = XLSX.utils.book_new();
|
|
XLSX.utils.book_append_sheet(libro, hoja, 'Reporte');
|
|
XLSX.writeFile(libro, `${periodoActual.nombreArchivo || 'reporte'}.xlsx`);
|
|
});
|
|
}
|
|
|
|
function mostrarReporte(data, tipo, tipoReporte = 'financiero') {
|
|
const contenido = document.getElementById('contenido-reporte');
|
|
let html = '';
|
|
|
|
// Enrutar según tipo de reporte
|
|
if (tipoReporte === 'financiero') {
|
|
html = generarReporteFinanciero(data, tipo);
|
|
} else if (tipoReporte === 'ventas_cantina') {
|
|
html = generarReporteVentasCantina(data, tipo);
|
|
} else if (tipoReporte === 'ocupacion_canchas') {
|
|
html = generarReporteOcupacionCanchas(data, tipo);
|
|
}
|
|
|
|
contenido.innerHTML = html;
|
|
}
|
|
|
|
// ============ REPORTE FINANCIERO ============
|
|
function generarReporteFinanciero(data, tipo) {
|
|
let html = '';
|
|
|
|
if (tipo === 'diario') {
|
|
// REPORTE DIARIO: Control de Operación y Cierre de Caja
|
|
const fecha = new Date(data.periodo + 'T00:00:00');
|
|
const fechaFormato = fecha.toLocaleDateString('es-ES', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
|
const fechaCorta = String(fecha.getDate()).padStart(2, '0') + '-' + String(fecha.getMonth() + 1).padStart(2, '0') + '-' + String(fecha.getFullYear()).slice(-2);
|
|
|
|
html = `
|
|
<h3 style="margin-bottom: 5px;">📅 Reporte Diario ${fechaCorta} - Cierre de Caja</h3>
|
|
<p style="margin: 0 0 20px 0; color: #999; font-size: 0.9em;">Reporte ${fechaFormato}</p>
|
|
<h4>Transacciones del Día</h4>
|
|
<table class="tabla-reportes">
|
|
<thead>
|
|
<tr>
|
|
<th>Hora</th>
|
|
<th>Tipo de Operación</th>
|
|
<th>Descripción / Detalle</th>
|
|
<th>Monto ($)</th>
|
|
<th>Usuario</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${data.transacciones.map(item => {
|
|
const isIncome = parseFloat(item.monto) >= 0;
|
|
const montoClass = isIncome ? 'text-success' : 'text-danger';
|
|
return `
|
|
<tr>
|
|
<td>${item.hora}</td>
|
|
<td><strong>${item.tipo_operacion}</strong></td>
|
|
<td>${item.descripcion}</td>
|
|
<td class="${montoClass}" style="text-align: right; font-weight: bold;">
|
|
${isIncome ? '+' : ''}$${parseFloat(item.monto).toFixed(2)}
|
|
</td>
|
|
<td>${item.usuario_registro}</td>
|
|
</tr>
|
|
`;
|
|
}).join('')}
|
|
</tbody>
|
|
</table>
|
|
|
|
<h4 style="margin-top: 30px;">Resumen del Día</h4>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-top: 15px;">
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #4CAF50;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Ingresos por Reservas</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #4CAF50;">$${parseFloat(data.resumen.ingresos_reservas || 0).toFixed(2)}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #2196F3;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Ingresos por Cantina</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #2196F3;">$${parseFloat(data.resumen.ingresos_cantina || 0).toFixed(2)}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #FF9800;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Egresos</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #FF9800;">-$${parseFloat(data.resumen.egresos || 0).toFixed(2)}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #9C27B0;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Saldo Neto</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #9C27B0;">$${parseFloat(data.resumen.saldo_neto || 0).toFixed(2)}</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
else if (tipo === 'semanal') {
|
|
// REPORTE SEMANAL: Flujo de Caja y Rendimiento
|
|
const inicio = new Date(data.fecha_inicio + 'T00:00:00');
|
|
const fin = new Date(data.fecha_fin + 'T00:00:00');
|
|
const diaInicio = inicio.getDate();
|
|
const diaFin = fin.getDate();
|
|
const inicioCorta = String(diaInicio).padStart(2, '0') + '-' + String(inicio.getMonth() + 1).padStart(2, '0');
|
|
const finCorta = String(diaFin).padStart(2, '0') + '-' + String(fin.getMonth() + 1).padStart(2, '0') + '-' + String(fin.getFullYear()).slice(-2);
|
|
|
|
html = `
|
|
<h3 style="margin-bottom: 5px;">📆 Reporte Semanal ${inicioCorta} al ${finCorta} - Flujo de Caja</h3>
|
|
<p style="margin: 0 0 20px 0; color: #999; font-size: 0.9em;">Reporte Semana del ${inicio.toLocaleDateString('es-ES', { month: 'short', day: 'numeric' })} al ${fin.toLocaleDateString('es-ES', { month: 'short', day: 'numeric' })}</p>
|
|
<table class="tabla-reportes">
|
|
<thead>
|
|
<tr>
|
|
<th>Día de la Semana</th>
|
|
<th>Ingresos Reservas ($)</th>
|
|
<th>Ingresos Cantina ($)</th>
|
|
<th>Costo de Mercadería ($)</th>
|
|
<th>Saldo Neto del Día ($)</th>
|
|
<th>% Ocupación Promedio</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${data.dias.map(item => {
|
|
const totalIngresos = parseFloat(item.ingresos_reservas || 0) + parseFloat(item.ingresos_cantina || 0);
|
|
const costo = parseFloat(item.costo_mercaderia || 0);
|
|
const saldoNeto = totalIngresos - costo;
|
|
const ocupacion = item.ocupacion_promedio ? parseFloat(item.ocupacion_promedio).toFixed(1) : 0;
|
|
const saldoColor = saldoNeto >= 0 ? '#4CAF50' : '#f44336';
|
|
return `
|
|
<tr>
|
|
<td><strong>${item.nombre_dia}</strong></td>
|
|
<td style="text-align: right;">$${parseFloat(item.ingresos_reservas || 0).toFixed(2)}</td>
|
|
<td style="text-align: right;">$${parseFloat(item.ingresos_cantina || 0).toFixed(2)}</td>
|
|
<td style="text-align: right; color: #f44336;">-$${costo.toFixed(2)}</td>
|
|
<td style="text-align: right; font-weight: bold; color: ${saldoColor};">$${saldoNeto.toFixed(2)}</td>
|
|
<td style="text-align: right;">${ocupacion}%</td>
|
|
</tr>
|
|
`;
|
|
}).join('')}
|
|
</tbody>
|
|
</table>
|
|
|
|
<h4 style="margin-top: 30px;">Resumen Semanal</h4>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-top: 15px;">
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #4CAF50;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Total Ingresos Reservas</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #4CAF50;">$${data.resumen?.ingresos_reservas_totales ? parseFloat(data.resumen.ingresos_reservas_totales).toFixed(2) : '0.00'}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #2196F3;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Total Ingresos Cantina</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #2196F3;">$${data.resumen?.ingresos_cantina_totales ? parseFloat(data.resumen.ingresos_cantina_totales).toFixed(2) : '0.00'}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #FF9800;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Total Costos Mercadería</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #FF9800;">-$${data.resumen?.costo_mercaderia_total ? parseFloat(data.resumen.costo_mercaderia_total).toFixed(2) : '0.00'}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #9C27B0;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Saldo Neto Semana</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #9C27B0;">$${data.resumen?.saldo_neto_semana ? parseFloat(data.resumen.saldo_neto_semana).toFixed(2) : '0.00'}</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
else if (tipo === 'mensual') {
|
|
// REPORTE MENSUAL: Rentabilidad y Estado de Resultados
|
|
const mesInfo = obtenerMesYAnioDesdeDatos(data);
|
|
const mesNombre = mesInfo.mesNombre.charAt(0).toUpperCase() + mesInfo.mesNombre.slice(1) + ' ' + mesInfo.anio;
|
|
|
|
html = `
|
|
<h3 style="margin-bottom: 5px;">📈 Reporte Mensual de ${mesNombre}</h3>
|
|
<p style="margin: 0 0 20px 0; color: #999; font-size: 0.9em;">Rentabilidad y Estado de Resultados</p>
|
|
|
|
<h4>Facturación por Categoría</h4>
|
|
<table class="tabla-reportes">
|
|
<thead>
|
|
<tr>
|
|
<th>Concepto / Categoría</th>
|
|
<th style="text-align: right;">Monto ($)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${data.categorias.map(item => `
|
|
<tr>
|
|
<td><strong>${item.categoria}</strong></td>
|
|
<td style="text-align: right; color: #4CAF50; font-weight: bold;">$${parseFloat(item.monto).toFixed(2)}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
|
|
<h4 style="margin-top: 30px;">Gastos Fijos del Mes</h4>
|
|
<table class="tabla-reportes">
|
|
<thead>
|
|
<tr>
|
|
<th>Nombre del Gasto</th>
|
|
<th style="text-align: right;">Monto Gasto ($)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${data.gastos_fijos.map(item => `
|
|
<tr>
|
|
<td>${item.descripcion}</td>
|
|
<td style="text-align: right; color: #f44336; font-weight: bold;">-$${parseFloat(item.monto).toFixed(2)}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
|
|
<h4 style="margin-top: 30px;">Resumen Financiero</h4>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-top: 15px;">
|
|
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #FF9800;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Costo Mercadería + Gastos Fijos</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #FF9800;">-$${(parseFloat(data.resumen?.costo_mercaderia || 0) + parseFloat(data.resumen?.gastos_fijos_total || 0)).toFixed(2)}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #FF6B6B;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Ganancia Bruta</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #FF6B6B;">$${parseFloat(data.resumen?.ganancia_bruta || 0).toFixed(2)}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #9C27B0;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Ganancia Neta</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #9C27B0;">$${parseFloat(data.resumen?.ganancia_neta || 0).toFixed(2)}</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
else if (tipo === 'anual') {
|
|
// REPORTE ANUAL: Balance General y Visión Estratégica
|
|
const anio = data.periodo || new Date().getFullYear();
|
|
const esAnioActual = data.es_ano_actual;
|
|
const datosAMostrar = data.meses || [];
|
|
|
|
const totalIngresoAnual = datosAMostrar.reduce((sum, item) => sum + (item.ingreso_operativo ? parseFloat(item.ingreso_operativo) : 0), 0);
|
|
const totalCostoAnual = datosAMostrar.reduce((sum, item) => sum + (item.costo_operativo ? parseFloat(item.costo_operativo) : 0), 0);
|
|
const totalCostoBrutoAnual = datosAMostrar.reduce((sum, item) => sum + (item.costo_bruto ? parseFloat(item.costo_bruto) : 0), 0);
|
|
const totalGananciaBrutaAnual = datosAMostrar.reduce((sum, item) => sum + (item.ganancia_bruta ? parseFloat(item.ganancia_bruta) : 0), 0);
|
|
const totalGananciaNetaAnual = datosAMostrar.reduce((sum, item) => sum + (item.ganancia_neta ? parseFloat(item.ganancia_neta) : 0), 0);
|
|
|
|
html = `
|
|
<h3 style="margin-bottom: 5px;">Reporte Anual ${anio} - Balance General</h3>
|
|
<!--<p style="margin: 0 0 20px 0; color: #999; font-size: 0.9em;">Análisis Comparativo Mensual y Visión Estratégica${esAnioActual ? ' (Año en Curso - Datos hasta ' + (new Date().toLocaleDateString('es-ES', {month: 'long'})) + ')' : ''}</p>-->
|
|
|
|
<h4>Análisis Comparativo Mensual</h4>
|
|
<table class="tabla-reportes">
|
|
<thead>
|
|
<tr>
|
|
<th>Mes</th>
|
|
<th style="text-align: right;">Ingreso Operativo ($)</th>
|
|
<th style="text-align: right;">Costo Operativo ($)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${datosAMostrar.map(item => `
|
|
<tr>
|
|
<td><strong>${item.mes}</strong></td>
|
|
<td style="text-align: right;">$${parseFloat(item.ingreso_operativo || 0).toFixed(2)}</td>
|
|
<td style="text-align: right;">$${parseFloat(item.costo_operativo || 0).toFixed(2)}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
<tfoot>
|
|
<tr style="background: #333; font-weight: bold;">
|
|
<td>TOTAL</td>
|
|
<td style="text-align: right;">$${totalIngresoAnual.toFixed(2)}</td>
|
|
<td style="text-align: right;">$${totalCostoAnual.toFixed(2)}</td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
|
|
<h4 style="margin-top: 30px;">Resumen Anual</h4>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-top: 15px;">
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #4CAF50;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Costo Bruto Total</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #4CAF50;">$${totalCostoBrutoAnual.toFixed(2)}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #FF6B6B;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Ganancia Bruta Total</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #FF6B6B;">$${totalGananciaBrutaAnual.toFixed(2)}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #9C27B0;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Ganancia Neta Total</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #9C27B0;">$${totalGananciaNetaAnual.toFixed(2)}</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
// ============ REPORTE VENTAS CANTINA ============
|
|
function generarReporteVentasCantina(data, tipo) {
|
|
let html = '';
|
|
|
|
if (tipo === 'diario') {
|
|
const fecha = new Date(data.fecha_inicio + 'T00:00:00');
|
|
const fechaFormato = fecha.toLocaleDateString('es-ES', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
|
|
|
html = `
|
|
<h3 style="margin-bottom: 5px;">🍽️ Reporte Diario de Ventas Cantina</h3>
|
|
<p style="margin: 0 0 20px 0; color: #999; font-size: 0.9em;">${fechaFormato}</p>
|
|
<table class="tabla-reportes">
|
|
<thead>
|
|
<tr>
|
|
<th>Producto</th>
|
|
<th style="text-align: right;">Cantidad Vendida</th>
|
|
<th style="text-align: right;">Ingreso ($)</th>
|
|
<th style="text-align: right;">% del Total</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${data.productos.map(item => `
|
|
<tr>
|
|
<td><strong>${item.producto}</strong></td>
|
|
<td style="text-align: right;">${item.cantidad_vendida}</td>
|
|
<td style="text-align: right; color: #4CAF50; font-weight: bold;">$${parseFloat(item.ingreso_generado).toFixed(2)}</td>
|
|
<td style="text-align: right;">${parseFloat(item.porcentaje_total).toFixed(2)}%</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
|
|
<h4 style="margin-top: 30px;">Resumen del Día</h4>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-top: 15px;">
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #4CAF50;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Total Productos Vendidos</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #4CAF50;">${data.resumen.total_productos_vendidos}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #2196F3;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Cantidad Total de Unidades</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #2196F3;">${data.resumen.total_cantidad}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #FF9800;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Ingreso Total Cantina</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #FF9800;">$${parseFloat(data.resumen.total_ingreso).toFixed(2)}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #9C27B0;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Ganancia Total</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #9C27B0;">$${parseFloat(data.resumen.ganancia_total).toFixed(2)}</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
else if (tipo === 'semanal') {
|
|
const inicio = new Date(data.fecha_inicio + 'T00:00:00');
|
|
const fin = new Date(data.fecha_fin + 'T00:00:00');
|
|
|
|
html = `
|
|
<h3 style="margin-bottom: 5px;">🍽️ Reporte Semanal de Ventas Cantina</h3>
|
|
<p style="margin: 0 0 20px 0; color: #999; font-size: 0.9em;">Semana del ${inicio.toLocaleDateString('es-ES')} al ${fin.toLocaleDateString('es-ES')}</p>
|
|
<table class="tabla-reportes">
|
|
<thead>
|
|
<tr>
|
|
<th>Producto</th>
|
|
<th>Categoría</th>
|
|
<th style="text-align: right;">Cantidad Vendida</th>
|
|
<th style="text-align: right;">Ingreso ($)</th>
|
|
<th style="text-align: right;">% del Total</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${data.productos.map(item => `
|
|
<tr>
|
|
<td><strong>${item.producto}</strong></td>
|
|
<td>${item.categoria || '-'}</td>
|
|
<td style="text-align: right;">${item.cantidad_vendida}</td>
|
|
<td style="text-align: right; color: #4CAF50; font-weight: bold;">$${parseFloat(item.ingreso_generado).toFixed(2)}</td>
|
|
<td style="text-align: right;">${parseFloat(item.porcentaje_total).toFixed(2)}%</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
|
|
<h4 style="margin-top: 30px;">Resumen Semanal</h4>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-top: 15px;">
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #4CAF50;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Total Productos Diferentes</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #4CAF50;">${data.resumen.total_productos_vendidos}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #2196F3;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Unidades Vendidas</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #2196F3;">${data.resumen.total_cantidad}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #FF9800;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Ingreso Total Cantina</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #FF9800;">$${parseFloat(data.resumen.total_ingreso).toFixed(2)}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #9C27B0;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Ganancia Total Semana</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #9C27B0;">$${parseFloat(data.resumen.ganancia_total).toFixed(2)}</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
else if (tipo === 'mensual') {
|
|
const mesInfo = obtenerMesYAnioDesdeDatos(data);
|
|
const mesNombre = mesInfo.mesNombre.charAt(0).toUpperCase() + mesInfo.mesNombre.slice(1) + ' ' + mesInfo.anio;
|
|
|
|
html = `
|
|
<h3 style="margin-bottom: 5px;">🍽️ Reporte Mensual de Ventas Cantina - ${mesNombre}</h3>
|
|
<p style="margin: 0 0 20px 0; color: #999; font-size: 0.9em;">Análisis de Ventas por Producto</p>
|
|
<table class="tabla-reportes">
|
|
<thead>
|
|
<tr>
|
|
<th>Producto</th>
|
|
<th>Categoría</th>
|
|
<th style="text-align: right;">Cantidad Vendida</th>
|
|
<th style="text-align: right;">Ingreso ($)</th>
|
|
<th style="text-align: right;">% del Total</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${data.productos.map(item => `
|
|
<tr>
|
|
<td><strong>${item.producto}</strong></td>
|
|
<td>${item.categoria || '-'}</td>
|
|
<td style="text-align: right;">${item.cantidad_vendida}</td>
|
|
<td style="text-align: right; color: #4CAF50; font-weight: bold;">$${parseFloat(item.ingreso_generado).toFixed(2)}</td>
|
|
<td style="text-align: right;">${parseFloat(item.porcentaje_total).toFixed(2)}%</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
|
|
<h4 style="margin-top: 30px;">Resumen Mensual</h4>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-top: 15px;">
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #4CAF50;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Productos Diferentes</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #4CAF50;">${data.resumen.total_productos_vendidos}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #2196F3;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Total Unidades</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #2196F3;">${data.resumen.total_cantidad}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #FF9800;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Ingreso Total Mes</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #FF9800;">$${parseFloat(data.resumen.total_ingreso).toFixed(2)}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #9C27B0;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Ganancia Total Mes</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #9C27B0;">$${parseFloat(data.resumen.ganancia_total).toFixed(2)}</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
else if (tipo === 'anual') {
|
|
html = `
|
|
<h3 style="margin-bottom: 5px;">🍽️ Reporte Anual de Ventas Cantina - ${data.fecha_inicio.substring(0, 4)}</h3>
|
|
<p style="margin: 0 0 20px 0; color: #999; font-size: 0.9em;">Productos Top del Año</p>
|
|
<table class="tabla-reportes">
|
|
<thead>
|
|
<tr>
|
|
<th>Producto</th>
|
|
<th>Categoría</th>
|
|
<th style="text-align: right;">Cantidad Vendida</th>
|
|
<th style="text-align: right;">Ingreso ($)</th>
|
|
<th style="text-align: right;">% del Total</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${data.productos.map(item => `
|
|
<tr>
|
|
<td><strong>${item.producto}</strong></td>
|
|
<td>${item.categoria || '-'}</td>
|
|
<td style="text-align: right;">${item.cantidad_vendida}</td>
|
|
<td style="text-align: right; color: #4CAF50; font-weight: bold;">$${parseFloat(item.ingreso_generado).toFixed(2)}</td>
|
|
<td style="text-align: right;">${parseFloat(item.porcentaje_total).toFixed(2)}%</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
|
|
<h4 style="margin-top: 30px;">Resumen Anual</h4>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-top: 15px;">
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #4CAF50;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Productos Diferentes</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #4CAF50;">${data.resumen.total_productos_vendidos}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #2196F3;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Total Unidades Año</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #2196F3;">${data.resumen.total_cantidad}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #FF9800;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Ingreso Total Año</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #FF9800;">$${parseFloat(data.resumen.total_ingreso).toFixed(2)}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #9C27B0;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Ganancia Total Año</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #9C27B0;">$${parseFloat(data.resumen.ganancia_total).toFixed(2)}</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
// ============ REPORTE OCUPACIÓN CANCHAS ============
|
|
function generarReporteOcupacionCanchas(data, tipo) {
|
|
let html = '';
|
|
const filtroCancha = String(data?.tipo_cancha_filtro || 'Todas');
|
|
const nombreTipoCancha = filtroCancha === 'F' ? 'Fútbol' : filtroCancha === 'P' ? 'Pádel' : filtroCancha === 'Q' ? 'Quincho' : filtroCancha;
|
|
const detalleHorarios = tipo === 'diario' && filtroCancha !== 'Todas' && filtroCancha !== ''
|
|
? (data?.reservas_por_horario?.[nombreTipoCancha] || data?.reservas_por_horario?.[filtroCancha] || [])
|
|
: [];
|
|
const showPorcentaje = !(tipo === 'diario' && nombreTipoCancha === 'Quincho');
|
|
|
|
if (tipo === 'diario') {
|
|
const fecha = new Date(data.fecha_inicio + 'T00:00:00');
|
|
const fechaFormato = fecha.toLocaleDateString('es-ES', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
|
const fechaCorta = String(fecha.getDate()).padStart(2, '0') + '-' + String(fecha.getMonth() + 1).padStart(2, '0') + '-' + String(fecha.getFullYear()).slice(-2);
|
|
|
|
html = `
|
|
<div class="reporte-titulo">
|
|
<h2>📅 Reporte Diario de Ocupación de Canchas - ${fechaCorta}</h2>
|
|
<div class="reporte-periodo">Reporte ${fechaFormato}</div>
|
|
</div>
|
|
|
|
${detalleHorarios.length > 0 && nombreTipoCancha !== 'Quincho' ? `
|
|
<h4>Detalle por Horario</h4>
|
|
<table class="tabla-reportes">
|
|
<thead>
|
|
<tr>
|
|
<th>Horario</th>
|
|
<th style="text-align: right;">Reservas Quincho</th>
|
|
<th style="text-align: right;">Reservas Activas</th>
|
|
<th style="text-align: right;">Canceladas</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${detalleHorarios
|
|
.filter(item => !item.es_quincho)
|
|
.map(item => `
|
|
<tr>
|
|
<td><strong>${item.horario}</strong></td>
|
|
<td style="text-align: right;">${item.reservas_quincho ? item.reservas_quincho : 0}</td>
|
|
<td style="text-align: right;">${item.cantidad_reservas}</td>
|
|
<td style="text-align: right; color: #f44336;">${item.canceladas}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
` : ''}
|
|
|
|
<h4 style="margin-top: 30px;">Ocupación por Cancha</h4>
|
|
<table class="tabla-reportes">
|
|
<thead>
|
|
<tr>
|
|
<th>Tipo</th>
|
|
<th>Cancha</th>
|
|
<th style="text-align: right;">Reservas Activas</th>
|
|
<th style="text-align: right;">Canceladas</th>
|
|
${showPorcentaje ? '<th style="text-align: right;">% Ocupación</th>' : ''}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${data.ocupacion_cancha.map(item => `
|
|
<tr>
|
|
<td>${item.tipo}</td>
|
|
<td><strong>#${item.numero}</strong></td>
|
|
<td style="text-align: right; color: #4CAF50; font-weight: bold;">${item.activas}</td>
|
|
<td style="text-align: right; color: #f44336;">${item.canceladas}</td>
|
|
${showPorcentaje ? ('<td style="text-align: right;">' + (item.porcentaje_ocupacion === null ? '-' : parseFloat(item.porcentaje_ocupacion).toFixed(1) + '%') + '</td>') : ''}
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
|
|
<h4 style="margin-top: 30px;">Horarios</h4>
|
|
${Object.keys(data.reservas_por_horario_por_tipo.horarios_pico_por_tipo).filter(t=> t !== 'Quincho').map(tipo => `
|
|
<div style="margin-top: 20px;">
|
|
<h5 style="margin: 0 0 15px 0; color: #FFF; font-size: 1em;"> ${tipo}</h5>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #FF9800;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Horarios Con Reservas</p>
|
|
<p style="margin: 10px 0 0 0; color: #FF9800; font-size: 0.9em;">
|
|
${data.reservas_por_horario_por_tipo.horarios_pico_por_tipo[tipo].length > 0 ? data.reservas_por_horario_por_tipo.horarios_pico_por_tipo[tipo].join(', ') : 'Sin datos'}
|
|
</p>
|
|
</div>
|
|
${tipo !== 'Quincho' ? `<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #9C27B0;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Horarios Sin Reservas</p>
|
|
<p style="margin: 10px 0 0 0; color: #9C27B0; font-size: 0.9em;">
|
|
${data.reservas_por_horario_por_tipo.horarios_sin_reservas_por_tipo[tipo].length > 0 ? data.reservas_por_horario_por_tipo.horarios_sin_reservas_por_tipo[tipo].join(', ') : 'Sin datos'}
|
|
</p>
|
|
</div>` : ''}
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
|
|
<h4 style="margin-top: 30px;">Resumen del Día</h4>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-top: 15px;">
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #4CAF50;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Reservas Activas</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #4CAF50;">${data.resumen.reservas_activas}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #f44336;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Reservas Canceladas</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #f44336;">${data.resumen.total_canceladas}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #2196F3;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Ocupación Promedio</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #2196F3;">${parseFloat(data.resumen.ocupacion_promedio).toFixed(1)}%</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
else if (tipo === 'semanal') {
|
|
const inicio = new Date(data.fecha_inicio + 'T00:00:00');
|
|
const fin = new Date(data.fecha_fin + 'T00:00:00');
|
|
|
|
html = `
|
|
<div class="reporte-titulo">
|
|
<h2>📆 Reporte Semanal de Ocupación de Canchas - ${String(inicio.getDate()).padStart(2,'0')}-${String(inicio.getMonth()+1).padStart(2,'0')} al ${String(fin.getDate()).padStart(2,'0')}-${String(fin.getMonth()+1).padStart(2,'0')}-${String(fin.getFullYear()).slice(-2)}</h2>
|
|
<div class="reporte-periodo">Semana del ${inicio.toLocaleDateString('es-ES')} al ${fin.toLocaleDateString('es-ES')}</div>
|
|
</div>
|
|
|
|
<h4>Reservas por Día</h4>
|
|
<table class="tabla-reportes">
|
|
<thead>
|
|
<tr>
|
|
<th>Día</th>
|
|
<th style="text-align: right;">Cantidad de Reservas</th>
|
|
<th style="text-align: right;">Canceladas</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${data.reservas_por_dia.map(item => `
|
|
<tr>
|
|
<td><strong>${item.nombre_dia}</strong> (${item.dia})</td>
|
|
<td style="text-align: right;">${item.cantidad_reservas}</td>
|
|
<td style="text-align: right; color: #f44336;">${item.canceladas}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
|
|
<h4 style="margin-top: 30px;">Ocupación por Cancha</h4>
|
|
<table class="tabla-reportes">
|
|
<thead>
|
|
<tr>
|
|
<th>Tipo</th>
|
|
<th>Cancha</th>
|
|
<th style="text-align: right;">Reservas Semana</th>
|
|
<th style="text-align: right;">% Ocupación</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${data.ocupacion_cancha.map(item => `
|
|
<tr>
|
|
<td>${item.tipo}</td>
|
|
<td><strong>#${item.numero}</strong></td>
|
|
<td style="text-align: right; color: #4CAF50;">${item.activas}</td>
|
|
<td style="text-align: right;">${parseFloat(item.porcentaje_ocupacion).toFixed(1)}%</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
|
|
<h4 style="margin-top: 30px;">Horarios Con Mayor Demanda y Menor Demanda</h4>
|
|
${Object.keys(data.reservas_por_horario_por_tipo.horarios_pico_por_tipo).filter(t=> t !== 'Quincho').map(tipo => `
|
|
<div style="margin-top: 20px;">
|
|
<h5 style="margin: 0 0 15px 0; color: #FFF; font-size: 1em;"> ${tipo}</h5>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #FF9800;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Horarios Con Mayor Demanda</p>
|
|
<p style="margin: 10px 0 0 0; color: #FF9800; font-size: 0.9em;">
|
|
${data.reservas_por_horario_por_tipo.horarios_pico_por_tipo[tipo].length > 0 ? data.reservas_por_horario_por_tipo.horarios_pico_por_tipo[tipo].join(', ') : 'Sin datos'}
|
|
</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #9C27B0;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Horarios Con Menor Demanda</p>
|
|
<p style="margin: 10px 0 0 0; color: #9C27B0; font-size: 0.9em;">
|
|
${data.reservas_por_horario_por_tipo.horarios_bajos_por_tipo[tipo].length > 0 ? data.reservas_por_horario_por_tipo.horarios_bajos_por_tipo[tipo].join(', ') : 'Sin datos'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
|
|
<h4 style="margin-top: 30px;">Resumen Semanal</h4>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-top: 15px;">
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #4CAF50;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Reservas Activas</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #4CAF50;">${data.resumen.reservas_activas}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #f44336;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Canceladas</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #f44336;">${data.resumen.total_canceladas}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #2196F3;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Ocupación Promedio</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #2196F3;">${parseFloat(data.resumen.ocupacion_promedio).toFixed(1)}%</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #9C27B0;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Días Analizados</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #9C27B0;">${data.reservas_por_dia.length}</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
else if (tipo === 'mensual') {
|
|
const fecha = new Date(data.fecha_inicio + 'T00:00:00');
|
|
const mesNombre = fecha.toLocaleDateString('es-ES', { month: 'long', year: 'numeric' });
|
|
|
|
html = `
|
|
<div class="reporte-titulo">
|
|
<h2>📊 Reporte Mensual de Ocupación de Canchas - ${mesNombre}</h2>
|
|
<div class="reporte-periodo">Análisis de Ocupación Completo</div>
|
|
</div>
|
|
|
|
|
|
|
|
<h4 style="margin-top: 30px;">Ocupación por Cancha - Mes Completo</h4>
|
|
<table class="tabla-reportes">
|
|
<thead>
|
|
<tr>
|
|
<th>Tipo</th>
|
|
<th>Cancha</th>
|
|
<th style="text-align: right;">Total Reservas</th>
|
|
<th style="text-align: right;">Activas</th>
|
|
<th style="text-align: right;">Canceladas</th>
|
|
<th style="text-align: right;">% Ocupación</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${data.ocupacion_cancha.map(item => `
|
|
<tr>
|
|
<td>${item.tipo}</td>
|
|
<td><strong>#${item.numero}</strong></td>
|
|
<td style="text-align: right;">${item.cantidad_reservas}</td>
|
|
<td style="text-align: right; color: #4CAF50; font-weight: bold;">${item.activas}</td>
|
|
<td style="text-align: right; color: #f44336;">${item.canceladas}</td>
|
|
<td style="text-align: right;">${parseFloat(item.porcentaje_ocupacion).toFixed(1)}%</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
|
|
<h4 style="margin-top: 30px;">Horarios Con Mayor Demanda y Menor Demanda</h4>
|
|
${Object.keys(data.reservas_por_horario_por_tipo.horarios_pico_por_tipo).filter(t=> t !== 'Quincho').map(tipo => `
|
|
<div style="margin-top: 20px;">
|
|
<h5 style="margin: 0 0 15px 0; color: #FFF; font-size: 1em;"> ${tipo}</h5>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #FF9800;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Horarios Con Mayor Demanda</p>
|
|
<p style="margin: 10px 0 0 0; color: #FF9800; font-size: 0.9em;">
|
|
${data.reservas_por_horario_por_tipo.horarios_pico_por_tipo[tipo].length > 0 ? data.reservas_por_horario_por_tipo.horarios_pico_por_tipo[tipo].join(', ') : 'Sin datos'}
|
|
</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #9C27B0;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Horarios Con Menor Demanda</p>
|
|
<p style="margin: 10px 0 0 0; color: #9C27B0; font-size: 0.9em;">
|
|
${data.reservas_por_horario_por_tipo.horarios_bajos_por_tipo[tipo].length > 0 ? data.reservas_por_horario_por_tipo.horarios_bajos_por_tipo[tipo].join(', ') : 'Sin datos'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
|
|
<h4 style="margin-top: 30px;">Resumen Mensual</h4>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-top: 15px;">
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #4CAF50;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Total Reservas Activas</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #4CAF50;">${data.resumen.reservas_activas}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #f44336;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Canceladas Mes</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #f44336;">${data.resumen.total_canceladas}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #2196F3;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Ocupación Promedio</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #2196F3;">${parseFloat(data.resumen.ocupacion_promedio).toFixed(1)}%</p>
|
|
</div>
|
|
|
|
</div>
|
|
`;
|
|
}
|
|
else if (tipo === 'anual') {
|
|
html = `
|
|
<h3 style="margin-bottom: 5px;"> Reporte Anual de Ocupación de Canchas - ${data.fecha_inicio.substring(0, 4)}</h3>
|
|
<p style="margin: 0 0 20px 0; color: #999; font-size: 0.9em;">Análisis de Ocupación Anual</p>
|
|
|
|
<h4>Ocupación por Cancha - Año Completo</h4>
|
|
<table class="tabla-reportes">
|
|
<thead>
|
|
<tr>
|
|
<th>Tipo</th>
|
|
<th>Cancha</th>
|
|
<th style="text-align: right;">Total Reservas</th>
|
|
<th style="text-align: right;">Activas</th>
|
|
<th style="text-align: right;">Canceladas</th>
|
|
<th style="text-align: right;">% Ocupación</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${data.ocupacion_cancha.map(item => `
|
|
<tr>
|
|
<td>${item.tipo}</td>
|
|
<td><strong>#${item.numero}</strong></td>
|
|
<td style="text-align: right;">${item.cantidad_reservas}</td>
|
|
<td style="text-align: right; color: #4CAF50; font-weight: bold;">${item.activas}</td>
|
|
<td style="text-align: right; color: #f44336;">${item.canceladas}</td>
|
|
<td style="text-align: right;">${parseFloat(item.porcentaje_ocupacion).toFixed(1)}%</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
|
|
<h4 style="margin-top: 30px;">Horarios Con Mayor Demanda y Menor Demanda</h4>
|
|
${Object.keys(data.reservas_por_horario_por_tipo.horarios_pico_por_tipo).map(tipo => `
|
|
<div style="margin-top: 20px;">
|
|
<h5 style="margin: 0 0 15px 0; color: #FFF; font-size: 1em;"> ${tipo}</h5>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #FF9800;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Horarios Con Mayor Demanda</p>
|
|
<p style="margin: 10px 0 0 0; color: #FF9800; font-size: 0.9em;">
|
|
${data.reservas_por_horario_por_tipo.horarios_pico_por_tipo[tipo].length > 0 ? data.reservas_por_horario_por_tipo.horarios_pico_por_tipo[tipo].join(', ') : 'Sin datos'}
|
|
</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #9C27B0;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Horarios Con Menor Demanda</p>
|
|
<p style="margin: 10px 0 0 0; color: #9C27B0; font-size: 0.9em;">
|
|
${data.reservas_por_horario_por_tipo.horarios_bajos_por_tipo[tipo].length > 0 ? data.reservas_por_horario_por_tipo.horarios_bajos_por_tipo[tipo].join(', ') : 'Sin datos'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
|
|
<h4 style="margin-top: 30px;">Resumen Anual</h4>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-top: 15px;">
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #4CAF50;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Total Reservas Año</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #4CAF50;">${data.resumen.total_reservas}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #f44336;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Canceladas Año</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #f44336;">${data.resumen.total_canceladas}</p>
|
|
</div>
|
|
<div style="padding: 15px; background: #2a2a2a; border-radius: 8px; border-left: 4px solid #2196F3;">
|
|
<p style="margin: 0; font-size: 0.9em; color: #999;">Ocupación Promedio Año</p>
|
|
<p style="margin: 5px 0 0 0; font-size: 1.8em; font-weight: bold; color: #2196F3;">${parseFloat(data.resumen.ocupacion_promedio).toFixed(1)}%</p>
|
|
</div>
|
|
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
return html;
|
|
}
|
|
</script>
|
|
</body>
|