From 8ec55e40fcf91a14e9de8b02040bb151d7ff4830 Mon Sep 17 00:00:00 2001 From: MarcosFlorSalcedo Date: Tue, 14 Jul 2026 18:24:27 -0300 Subject: [PATCH] =?UTF-8?q?1ra=20Versi=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 618 ++++++ css/Cantina.css | 1164 ++++++++++++ css/Login.css | 326 ++++ css/Reserva_Clientes.css | 814 ++++++++ css/img/futbol.svg | 952 ++++++++++ css/img/padel.svg | 17 + html/Cantina.html | 2391 ++++++++++++++++++++++++ html/Login.html | 272 +++ html/Reserva_Clientes.html | 154 ++ js/Gestion_Canchas.js | 168 ++ js/Gestion_Usuarios.js | 274 +++ js/Orden_Compra.js | 429 +++++ js/Reportes.js | 585 ++++++ js/Reservar_Turno.js | 621 ++++++ js/Resumen_Ventas.js | 208 +++ js/Tabla_Gastos.js | 291 +++ js/Tabla_Productos.js | 277 +++ js/Tabla_Proveedor.js | 327 ++++ js/Tabla_Reservas.js | 673 +++++++ js/Tabla_Ventas.js | 208 +++ php/Actualizar_Cancha.php | 43 + php/Actualizar_Estado_Reserva.php | 32 + php/Actualizar_Gastos.php | 106 ++ php/Actualizar_Horario_Reserva.php | 48 + php/Actualizar_Horarios.php | 18 + php/Actualizar_Orden_Compra.php | 136 ++ php/Actualizar_Perfil.php | 54 + php/Actualizar_Productos_Orden.php | 46 + php/Actualizar_Usuario.php | 55 + php/Agregar_Producto.php | 15 + php/Agregar_Producto_Proveedor.php | 18 + php/Buscar_Producto_Orden.php | 26 + php/Buscar_Producto_Ventas.php | 41 + php/Buscar_Proveedor.php | 12 + php/Buscar_Proveedor_Editar.php | 12 + php/Cambiar_Contrasena.php | 91 + php/Cantina.php | 11 + php/Editar_Gasto.php | 126 ++ php/Editar_Producto.php | 27 + php/Editar_Proveedor.php | 54 + php/Eliminar_Gasto.php | 48 + php/Eliminar_Producto.php | 68 + php/Eliminar_Producto_Proveedor.php | 15 + php/Eliminar_Proveedor.php | 73 + php/Eliminar_Usuario.php | 57 + php/Enviar_Codigo_WSP.php | 41 + php/Ganacia_Neta.php | 88 + php/Guardar_Proveedor.php | 26 + php/Guardar_Reserva.php | 225 +++ php/Guardar_Usuario.php | 106 ++ php/Login.php | 39 + php/Logout.php | 16 + php/Migrar_Gastos.php | 51 + php/Migrar_Ventas_Productos.php | 45 + php/Obtener_Canchas.php | 28 + php/Obtener_Canchas_Numeros.php | 89 + php/Obtener_Cupon.php | 136 ++ php/Obtener_Disponibilidad_Cancha.php | 83 + php/Obtener_Disponibilidad_Quincho.php | 64 + php/Obtener_Gastos.php | 40 + php/Obtener_Horarios.php | 7 + php/Obtener_Ordenes_Compra.php | 26 + php/Obtener_Productos.php | 33 + php/Obtener_Productos_Orden.php | 34 + php/Obtener_Productos_Proveedor.php | 23 + php/Obtener_Proveedores.php | 20 + php/Obtener_Reporte.php | 1205 ++++++++++++ php/Obtener_Reservas.php | 62 + php/Obtener_Usuarios.php | 23 + php/Obtener_Ventas.php | 114 ++ php/Recuperar_Contrasena_Solicitar.php | 74 + php/Recuperar_Contrasena_Verificar.php | 93 + php/Registrar_Gastos.php | 123 ++ php/Registrar_Orden_Compra.php | 20 + php/Registrar_Venta.php | 34 + php/Registro.php | 31 + php/Session_Info.php | 27 + php/Subir_Comprobante.php | 73 + php/backup.php | 109 ++ php/conexion.php | 13 + php/config_mail.php | 90 + python/Enviar_WSP.py | 51 + sql/Consultas.sql | 60 + sql/Insertar Datos.sql | 234 +++ sql/Tablas.sql | 137 ++ sql/Triggers.sql | 45 + 86 files changed, 15839 insertions(+) create mode 100644 README.md create mode 100644 css/Cantina.css create mode 100644 css/Login.css create mode 100644 css/Reserva_Clientes.css create mode 100644 css/img/futbol.svg create mode 100644 css/img/padel.svg create mode 100644 html/Cantina.html create mode 100644 html/Login.html create mode 100644 html/Reserva_Clientes.html create mode 100644 js/Gestion_Canchas.js create mode 100644 js/Gestion_Usuarios.js create mode 100644 js/Orden_Compra.js create mode 100644 js/Reportes.js create mode 100644 js/Reservar_Turno.js create mode 100644 js/Resumen_Ventas.js create mode 100644 js/Tabla_Gastos.js create mode 100644 js/Tabla_Productos.js create mode 100644 js/Tabla_Proveedor.js create mode 100644 js/Tabla_Reservas.js create mode 100644 js/Tabla_Ventas.js create mode 100644 php/Actualizar_Cancha.php create mode 100644 php/Actualizar_Estado_Reserva.php create mode 100644 php/Actualizar_Gastos.php create mode 100644 php/Actualizar_Horario_Reserva.php create mode 100644 php/Actualizar_Horarios.php create mode 100644 php/Actualizar_Orden_Compra.php create mode 100644 php/Actualizar_Perfil.php create mode 100644 php/Actualizar_Productos_Orden.php create mode 100644 php/Actualizar_Usuario.php create mode 100644 php/Agregar_Producto.php create mode 100644 php/Agregar_Producto_Proveedor.php create mode 100644 php/Buscar_Producto_Orden.php create mode 100644 php/Buscar_Producto_Ventas.php create mode 100644 php/Buscar_Proveedor.php create mode 100644 php/Buscar_Proveedor_Editar.php create mode 100644 php/Cambiar_Contrasena.php create mode 100644 php/Cantina.php create mode 100644 php/Editar_Gasto.php create mode 100644 php/Editar_Producto.php create mode 100644 php/Editar_Proveedor.php create mode 100644 php/Eliminar_Gasto.php create mode 100644 php/Eliminar_Producto.php create mode 100644 php/Eliminar_Producto_Proveedor.php create mode 100644 php/Eliminar_Proveedor.php create mode 100644 php/Eliminar_Usuario.php create mode 100644 php/Enviar_Codigo_WSP.php create mode 100644 php/Ganacia_Neta.php create mode 100644 php/Guardar_Proveedor.php create mode 100644 php/Guardar_Reserva.php create mode 100644 php/Guardar_Usuario.php create mode 100644 php/Login.php create mode 100644 php/Logout.php create mode 100644 php/Migrar_Gastos.php create mode 100644 php/Migrar_Ventas_Productos.php create mode 100644 php/Obtener_Canchas.php create mode 100644 php/Obtener_Canchas_Numeros.php create mode 100644 php/Obtener_Cupon.php create mode 100644 php/Obtener_Disponibilidad_Cancha.php create mode 100644 php/Obtener_Disponibilidad_Quincho.php create mode 100644 php/Obtener_Gastos.php create mode 100644 php/Obtener_Horarios.php create mode 100644 php/Obtener_Ordenes_Compra.php create mode 100644 php/Obtener_Productos.php create mode 100644 php/Obtener_Productos_Orden.php create mode 100644 php/Obtener_Productos_Proveedor.php create mode 100644 php/Obtener_Proveedores.php create mode 100644 php/Obtener_Reporte.php create mode 100644 php/Obtener_Reservas.php create mode 100644 php/Obtener_Usuarios.php create mode 100644 php/Obtener_Ventas.php create mode 100644 php/Recuperar_Contrasena_Solicitar.php create mode 100644 php/Recuperar_Contrasena_Verificar.php create mode 100644 php/Registrar_Gastos.php create mode 100644 php/Registrar_Orden_Compra.php create mode 100644 php/Registrar_Venta.php create mode 100644 php/Registro.php create mode 100644 php/Session_Info.php create mode 100644 php/Subir_Comprobante.php create mode 100644 php/backup.php create mode 100644 php/conexion.php create mode 100644 php/config_mail.php create mode 100644 python/Enviar_WSP.py create mode 100644 sql/Consultas.sql create mode 100644 sql/Insertar Datos.sql create mode 100644 sql/Tablas.sql create mode 100644 sql/Triggers.sql diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d5b926 --- /dev/null +++ b/README.md @@ -0,0 +1,618 @@ +# Sistema de Gestión - Complejo Cap1tan ⚽🎾 + +Sistema integral de gestión para un complejo deportivo que incluye reserva de canchas, gestión de cantina, inventario, proveedores y reportes. + +## 📋 Índice + +- [Descripción General](#descripción-general) +- [Características Principales](#características-principales) +- [Requisitos del Sistema](#requisitos-del-sistema) +- [Levantar el Proyecto](#levantar-el-proyecto) +- [Instalación](#instalación) +- [Configuración](#configuración) +- [Estructura del Proyecto](#estructura-del-proyecto) +- [Módulos Principales](#módulos-principales) +- [Base de Datos](#base-de-datos) +- [Roles y Permisos](#roles-y-permisos) +- [Uso del Sistema](#uso-del-sistema) +- [API Endpoints](#api-endpoints) +- [Tecnologías Utilizadas](#tecnologías-utilizadas) + +## 📖 Descripción General + +Sistema web desarrollado para la gestión integral del **Complejo Cap1tan**, permitiendo: +- Administración de usuarios con diferentes roles +- Reserva y gestión de canchas +- Control de inventario de la cantina +- Gestión de proveedores y órdenes de compra +- Reportes de ventas y ganancias +- Sistema de cupones de descuento +- Notificaciones vía WhatsApp + +## ✨ Características Principales + +### Gestión de Usuarios +- ✅ Sistema de autenticación seguro +- ✅ Tres roles: Administrador General, Administrador Cancha y Administrador Cantina +- ✅ Perfiles de usuarios con datos personales +- ✅ Edición rápida desde el modal de perfil del usuario conectado +- ✅ Gestión completa desde la interfaz administrativa + +### Reservas de Canchas +- ✅ Reserva de turnos en canchas +- ✅ Visualización de disponibilidad +- ✅ Sistema de cupones para clientes frecuentes +- ✅ Registro de faltas y asistencias +- ✅ Descuentos automáticos por cupones +- ✅ Verificación del cliente por WhatsApp antes de confirmar la reserva + +### Gestión de Cantina +- ✅ Catálogo de productos +- ✅ Control de stock +- ✅ Registro de ventas en tiempo real +- ✅ Alertas visuales de stock bajo +- ✅ Cálculo automático de ganancias +- ✅ Actualización dinámica del stock sin recargar completamente la página + +### Gestión de Proveedores +- ✅ Registro de proveedores +- ✅ Órdenes de compra +- ✅ Aprobación de órdenes por el administrador +- ✅ Asociación de productos por proveedor +- ✅ Historial de compras +- ✅ Envío de órdenes por WhatsApp para comunicación rápida + +### Reportes y Análisis +- ✅ Reporte de ventas +- ✅ Cálculo de ganancia neta +- ✅ Seguimiento de gastos +- ✅ Resumen por período +- ✅ **Generación de reportes por período: Diario, Semanal, Mensual y Anual** +- ✅ **Exportación de reportes a PDF y Excel con formato profesional** +- ✅ Correcciones y mejoras en la generación visual de reportes + +## 💻 Requisitos del Sistema + +### Software Necesario +- **PHP** >= 7.4 +- **MySQL/MariaDB** >= 5.7 +- **Apache** (incluido en XAMPP) +- **Python** 3.8+ (para envío de mensajes WhatsApp) +- **Navegador** moderno (Chrome, Firefox, Edge) + +### Dependencias de Python +```bash +selenium +webdriver-manager +``` + +### Servidor Local +- **XAMPP** o equivalente (Apache + MySQL + PHP) + +## ▶️ Levantar el Proyecto + + +Para usar el sistema localmente, basta con dejar el proyecto en la carpeta de XAMPP, iniciar Apache y MySQL, importar la base de datos y abrir la interfaz desde el navegador. + +### Pasos +1. Ubica el proyecto en: + ```bash + C:\xampp\htdocs\TP-Taller\ProyectoTaller + ``` +2. Inicia **Apache** y **MySQL** desde XAMPP. +3. Crea la base de datos `complejo_cap1tan` y carga los scripts SQL indicados en la sección de instalación. +4. Ajusta las credenciales en [php/conexion.php](php/conexion.php) si es necesario. +5. Abre la aplicación en: + ```text + http://localhost/TP-Taller/ProyectoTaller/html/Login.html + ``` + +### Uso inicial +Una vez abierta la aplicación, podés comenzar a gestionar reservas, ventas, productos, proveedores, órdenes y reportes desde el panel principal. + +## 📦 Instalación + +### 1. Requisitos Previos +Asegúrate de tener **XAMPP** instalado y ejecutando: +- Apache en puerto 80 +- MySQL en puerto 3306 + +### 2. Clonar/Descargar Proyecto +```bash +# Ubicar el proyecto en la carpeta htdocs de XAMPP +c:\xampp\htdocs\TP-Taller\ProyectoTaller +``` + +### 3. Crear Base de Datos +```bash +# Acceder a phpMyAdmin +# http://localhost/phpmyadmin + +# Crear base de datos: complejo_cap1tan +CREATE DATABASE complejo_cap1tan; +USE complejo_cap1tan; +``` + +### 4. Importar Tablas +1. En phpMyAdmin, selecciona la base de datos `complejo_cap1tan` +2. Ve a la pestaña **Importar** +3. Selecciona el archivo `sql/Tablas.sql` +4. Haz clic en **Ejecutar** + +### 5. Importar Datos Iniciales (Opcional) +```bash +# Para cargar datos de prueba +sql/Insertar\ Datos.sql +``` + +### 6. Importar Triggers +```bash +# Para funcionalidades avanzadas +sql/Triggers.sql +``` + +## ⚙️ Configuración + +### Conexión a Base de Datos + +Editar archivo `php/conexion.php`: + +```php +connect_error) { + die('Error de conexión: ' . $conexion->connect_error); +} +$conexion->set_charset('utf8'); +?> +``` + +### Configuración de WhatsApp + +Para habilitar notificaciones vía WhatsApp: + +1. Instalar dependencias Python: + ```bash + pip install selenium webdriver-manager + ``` + +2. Descargar ChromeDriver compatible con tu versión de Chrome + +3. Editar `python/Enviar_WSP.py` con rutas correctas + +## 📁 Estructura del Proyecto + +``` +ProyectoTaller/ +├── backups/ # Copias de seguridad +│ └── *.zip # Archivos de backup automático +├── css/ # Estilos CSS +│ ├── Cantina.css # Estilos del módulo de cantina +│ ├── Login.css # Estilos de login +│ └── Reserva_Clientes.css # Estilos de reservas +├── html/ # Archivos HTML +│ ├── Cantina.html # Página principal (cantina) +│ ├── Login.html # Página de inicio de sesión +│ └── Reserva_Clientes.html # Página de reservas +├── js/ # Archivos JavaScript +│ ├── Gestion_Canchas.js # Lógica de gestión de canchas +│ ├── Gestion_Usuarios.js # Lógica de gestión de usuarios +│ ├── Orden_Compra.js # Lógica de órdenes de compra +│ ├── Reservar_Turno.js # Lógica de reservas +│ ├── Resumen_Ventas.js # Lógica de resumen de ventas +│ ├── Tabla_Gastos.js # Lógica de tabla de gastos +│ ├── Tabla_Productos.js # Lógica de tabla de productos +│ ├── Tabla_Proveedor.js # Lógica de tabla de proveedores +│ ├── Tabla_Reservas.js # Lógica de tabla de reservas +│ └── Tabla_Ventas.js # Lógica de tabla de ventas +├── php/ # Archivos backend PHP +│ ├── conexion.php # Conexión a base de datos +│ ├── Login.php # Autenticación +│ ├── Logout.php # Cierre de sesión +│ ├── Session_Info.php # Información de sesión +│ │ +│ ├── *Usuarios* +│ ├── Guardar_Usuario.php # Crear usuario +│ ├── Obtener_Usuarios.php # Listar usuarios +│ ├── Actualizar_Usuario.php # Editar usuario +│ ├── Eliminar_Usuario.php # Eliminar usuario +│ ├── Actualizar_Perfil.php # Actualizar perfil propio +│ │ +│ ├── *Canchas* +│ ├── Obtener_Canchas.php # Listar todas las canchas +│ ├── Obtener_Canchas_Numeros.php # Obtener números de canchas +│ ├── Actualizar_Cancha.php # Actualizar datos de cancha +│ ├── Obtener_Disponibilidad_Cancha.php +│ ├── Obtener_Disponibilidad_Quincho.php +│ ├── Obtener_Horarios.php # Obtener horarios disponibles +│ ├── Actualizar_Horarios.php # Editar horarios +│ │ +│ ├── *Reservas* +│ ├── Guardar_Reserva.php # Crear reserva +│ ├── Obtener_Reservas.php # Listar reservas +│ ├── Actualizar_Estado_Reserva.php # Cambiar estado de reserva +│ ├── Tabla_Reservas.js # Frontend de reservas +│ │ +│ ├── *Productos (Cantina)* +│ ├── Agregar_Producto.php # Crear producto +│ ├── Obtener_Productos.php # Listar productos +│ ├── Editar_Producto.php # Actualizar producto +│ ├── Eliminar_Producto.php # Eliminar producto +│ ├── Buscar_Producto_Ventas.php +│ ├── Buscar_Producto_Orden.php +│ │ +│ ├── *Ventas* +│ ├── Registrar_Venta.php # Registrar venta en cantina +│ ├── Obtener_Ventas.php # Obtener histórico de ventas +│ ├── Ganacia_Neta.php # Calcular ganancias +│ │ +│ ├── *Proveedores* +│ ├── Guardar_Proveedor.php # Crear proveedor +│ ├── Obtener_Proveedores.php # Listar proveedores +│ ├── Editar_Proveedor.php # Actualizar proveedor +│ ├── Eliminar_Proveedor.php # Eliminar proveedor +│ ├── Buscar_Proveedor.php # Buscar proveedor +│ ├── Buscar_Proveedor_Editar.php +│ │ +│ ├── *Órdenes de Compra* +│ ├── Registrar_Orden_Compra.php # Crear orden +│ ├── Obtener_Ordenes_Compra.php # Listar órdenes +│ ├── Actualizar_Orden_Compra.php # Cambiar estado de orden +│ ├── Obtener_Productos_Orden.php # Productos de una orden +│ ├── Agregar_Producto_Proveedor.php +│ ├── Eliminar_Producto_Proveedor.php +│ ├── Obtener_Productos_Proveedor.php +│ │ +│ ├── *Gastos* +│ ├── Registrar_Gastos.php # Registrar gasto +│ ├── Obtener_Gastos.php # Listar gastos +│ ├── Editar_Gasto.php # Editar gasto +│ ├── Eliminar_Gasto.php # Eliminar gasto +│ │ +│ ├── *Cupones* +│ ├── Obtener_Cupon.php # Obtener datos de cupón +│ │ +│ ├── *Utilidades* +│ ├── Enviar_Codigo_WSP.php # Enviar código de verificación +│ ├── Obtener_Reporte.php # Generar reportes +│ ├── backup.php # Hacer backup de BD +│ ├── Test_Conexion.php # Prueba de conexión +│ +├── python/ # Scripts Python +│ └── Enviar_WSP.py # Envío automático de mensajes WhatsApp +├── sql/ # Scripts SQL +│ ├── Tablas.sql # Estructura de base de datos +│ ├── Insertar\ Datos.sql # Datos iniciales de prueba +│ ├── Consultas.sql # Consultas útiles +│ └── Triggers.sql # Triggers de base de datos +└── README.md # Este archivo +``` + +## 🗄️ Módulos Principales + +### 1. **Autenticación** +- `php/Login.php`: Valida credenciales de usuario +- `php/Logout.php`: Cierra sesión actual +- Sistema de sesiones con roles + +### 2. **Gestión de Usuarios** +- Crear, editar, listar y eliminar usuarios +- Asignación de roles (Admin, Cancha, Cantina) +- Perfiles con datos personales +- Edición rápida del perfil desde el panel principal + +### 3. **Gestión de Canchas** +- Gestión de canchas (CRUD) +- Gestión de horarios disponibles +- Seguimiento de disponibilidad + +### 4. **Sistema de Reservas** +- Reserva de turnos en canchas +- Sistema de cupones para descuentos +- Registro de asistencia/faltas +- Cálculo automático de descuentos +- Confirmación con verificación por WhatsApp + +### 5. **Cantina** +- Gestión de productos (CRUD) +- Control de inventario +- Registro de ventas +- Alertas de stock bajo + +### 6. **Proveedores y Compras** +- Registro de proveedores +- Órdenes de compra con aprobación +- Asociación de productos por proveedor +- Envío de órdenes y seguimiento desde la interfaz + +### 7. **Reportes** +- Resumen de ventas por período +- Cálculo de ganancias netas +- Tracking de gastos +- Exportación a PDF y Excel +- Visualización optimizada para reportes diarios, semanales, mensuales y anuales + +### 8. **Seguridad y Operaciones Avanzadas** +- Verificación de clientes por WhatsApp +- Gestión del perfil del usuario autenticado +- Copias de seguridad automáticas de la base de datos +- Alertas de productos con stock bajo +- Mejoras en el flujo de validación y control de operaciones + +## 📊 Base de Datos + +### Tablas Principales + +| Tabla | Descripción | +|-------|-----------| +| `usuarios` | Usuarios del sistema con roles | +| `clientes` | Clientes que reservan canchas | +| `canchas` | Canchas disponibles para reserva | +| `reservas` | Histórico de reservas | +| `productos` | Productos de la cantina | +| `ventas` | Registro de ventas | +| `ventas_productos` | Detalle de productos en cada venta | +| `proveedores` | Proveedores de productos | +| `ordenes_compra` | Órdenes de compra a proveedores | +| `ordenes_productos` | Detalle de productos en órdenes | +| `gastos` | Gastos operativos | +| `cupones` | Cupones de descuento | +| `alertas_stock` | Alertas de stock bajo | + +### Roles de Base de Datos +- `Admin`: Acceso total a todo el sistema +- `Cancha`: Gestión de reservas y canchas +- `Cantina`: Gestión de productos y ventas + +## 👥 Roles y Permisos + +### Administrador +- ✅ Gestión completa de usuarios +- ✅ Gestión de canchas y horarios +- ✅ Acceso a reportes +- ✅ Gestión de proveedores +- ✅ Backup de base de datos + +### Cancha +- ✅ Gestionar reservas +- ✅ Ver disponibilidad de canchas +- ✅ Consultar historial de reservas +- ❌ Gestión de usuarios +- ❌ Reportes financieros + +### Cantina +- ✅ Gestionar productos +- ✅ Registrar ventas +- ✅ Ver resumen de ventas +- ✅ Gestionar proveedores +- ✅ Crear órdenes de compra +- ❌ Gestión de usuarios +- ❌ Gestión de reservas + +## 🚀 Uso del Sistema + +### 1. Acceso al Sistema +``` +URL: http://localhost/TP-Taller/ProyectoTaller/html/Login.html +Usuario: admin +Contraseña: admin +``` + +### 2. Crear un Usuario +1. Inicia sesión como Admin +2. Ve a Gestión de Usuarios +3. Haz clic en "Nuevo Usuario" +4. Completa el formulario con: + - Usuario (nombre de login) + - Contraseña + - Nombre y Apellido + - DNI + - Email y Teléfono + - Rol + +### 3. Registrar una Reserva +1. Ve a Reservas > Nueva Reserva +2. Selecciona la cancha +3. Elige la fecha y hora disponible +4. Si el cliente tiene cupón, selecciona el descuento +5. El sistema solicita verificación por WhatsApp del cliente +6. Completa la verificación e ingresa el código recibido +7. Confirma la reserva + +### 4. Registrar una Venta +1. Ve a Cantina > Nueva Venta +2. Busca y agrega productos (el sistema alertará si el stock es bajo) +3. El sistema calcula automáticamente el total +4. Registra la venta + +### 5. Crear Orden de Compra +1. Ve a Proveedores > Nueva Orden +2. Selecciona proveedor +3. Agrega productos +4. Envía para aprobación +5. (Opcional) Envía la orden por WhatsApp desde el sistema +6. Admin aprueba o rechaza la orden + +### 6. Generar Reportes +1. Ve a Reportes > Generar Reportes +2. Selecciona el tipo de reporte: + - **Diario**: Control de operación y cierre de caja + - **Semanal**: Flujo de caja y rendimiento + - **Mensual**: Rentabilidad y estado de resultados + - **Anual**: Balance general y visión estratégica +3. Visualiza el reporte en pantalla +4. Exporta a **PDF** o **Excel** según necesites + +## 🔌 API Endpoints + +Todos los endpoints devuelven JSON y utilizan `GET` o `POST`. + +### Usuarios +``` +POST /php/Guardar_Usuario.php - Crear usuario +GET /php/Obtener_Usuarios.php - Listar usuarios +POST /php/Actualizar_Usuario.php - Editar usuario +POST /php/Eliminar_Usuario.php - Eliminar usuario +POST /php/Actualizar_Perfil.php - Actualizar perfil propio +GET /php/Session_Info.php - Obtener datos de sesión actual +``` + +### Autenticación +``` +POST /php/Login.php - Iniciar sesión +GET /php/Logout.php - Cerrar sesión +``` + +### Canchas +``` +GET /php/Obtener_Canchas.php - Listar canchas +GET /php/Obtener_Canchas_Numeros.php - Obtener números +POST /php/Actualizar_Cancha.php - Editar cancha +GET /php/Obtener_Disponibilidad_Cancha.php +GET /php/Obtener_Horarios.php - Listar horarios +POST /php/Actualizar_Horarios.php - Editar horarios +``` + +### Reservas +``` +POST /php/Guardar_Reserva.php - Crear reserva +GET /php/Obtener_Reservas.php - Listar reservas +POST /php/Actualizar_Estado_Reserva.php - Cambiar estado +GET /php/Obtener_Cupon.php - Obtener estado del cupón +POST /php/Enviar_Codigo_WSP.php - Enviar código de verificación WhatsApp +``` + +### Productos +``` +POST /php/Agregar_Producto.php - Crear producto +GET /php/Obtener_Productos.php - Listar productos +POST /php/Editar_Producto.php - Editar producto +POST /php/Eliminar_Producto.php - Eliminar producto +``` + +### Ventas +``` +POST /php/Registrar_Venta.php - Registrar venta +GET /php/Obtener_Ventas.php - Listar ventas +GET /php/Ganacia_Neta.php - Calcular ganancias +``` + +### Proveedores +``` +POST /php/Guardar_Proveedor.php - Crear proveedor +GET /php/Obtener_Proveedores.php - Listar proveedores +POST /php/Editar_Proveedor.php - Editar proveedor +POST /php/Eliminar_Proveedor.php - Eliminar proveedor +``` + +### Órdenes de Compra +``` +POST /php/Registrar_Orden_Compra.php - Crear orden +GET /php/Obtener_Ordenes_Compra.php - Listar órdenes +POST /php/Actualizar_Orden_Compra.php - Cambiar estado (aprobar/rechazar) +GET /php/Obtener_Productos_Orden.php - Productos de orden +POST /php/Agregar_Producto_Proveedor.php - Asociar producto a proveedor +``` + +### Gastos +``` +POST /php/Registrar_Gastos.php - Registrar gasto +GET /php/Obtener_Gastos.php - Listar gastos +POST /php/Editar_Gasto.php - Editar gasto +POST /php/Eliminar_Gasto.php - Eliminar gasto +``` + +### Reportes y Operaciones +``` +GET /php/Obtener_Reporte.php - Generar reportes (diario, semanal, mensual, anual) +GET /php/Ganacia_Neta.php - Ganancias por período +POST /php/backup.php - Crear backup de base de datos +GET /php/Test_Conexion.php - Probar conexión a base de datos +``` + +## 🛠️ Tecnologías Utilizadas + +### Backend +- **PHP 7.4+** - Lenguaje servidor +- **MySQL/MariaDB** - Base de datos relacional +- **MySQLi** - Conexión a base de datos +- **Prepared Statements** - Protección contra SQL injection + +### Frontend +- **HTML5** - Estructura +- **CSS3** - Estilos +- **JavaScript (Vanilla)** - Interactividad +- **AJAX** - Comunicación asíncrona + +### Utilidades +- **Python 3.8+** - Scripts de automatización +- **Selenium** - Automatización de WhatsApp Web +- **XAMPP** - Stack local (Apache, MySQL, PHP) + +## 🔒 Seguridad + +- Autenticación con sesiones +- Prepared Statements para prevenir SQL injection +- Validación de entrada en servidor +- Control de roles y permisos + +## 📝 Mantenimiento + +### Backup Automático +```bash +# Ejecutar backup manual +php/backup.php +``` + +Los backups se guardan en la carpeta `backups/` con timestamp. + +### Prueba de Conexión +```bash +# Verificar conexión a base de datos +http://localhost/TP-Taller/ProyectoTaller/php/Test_Conexion.php +``` + +## 🐛 Troubleshooting + +### Error: "Error de conexión" +- Verificar que MySQL esté corriendo +- Revisar credenciales en `php/conexion.php` +- Verificar que la base de datos existe + +### Error: "Sesión expirada" +- El usuario fue desconectado por inactividad +- Volver a iniciar sesión + +### Productos no se visualizan +- Verificar que hay productos en la base de datos +- Revisar permisos de usuario +- Inspeccionar la consola de navegador para errores + +### WhatsApp no envía mensajes +- Verificar instalación de Python y Selenium +- Verificar ruta a ChromeDriver +- Revisar formato de número telefónico + +## 📱 Integración WhatsApp + +El sistema puede enviar notificaciones vía WhatsApp: +- Códigos de verificación +- Confirmaciones de reserva +- Recordatorios + +**Nota:** Requiere ChromeDriver y sesión activa de WhatsApp Web. + +## 📄 Licencia + +Proyecto privado para uso interno del Complejo Cap1tan. + +--- + +**Última actualización:** Julio 2026 +**Versión:** 1.0 diff --git a/css/Cantina.css b/css/Cantina.css new file mode 100644 index 0000000..b7677d1 --- /dev/null +++ b/css/Cantina.css @@ -0,0 +1,1164 @@ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Segoe UI", sans-serif; +} + +body { + display: flex; + flex-direction: column; + min-height: 100vh; + color: #f0f0f0; + background: linear-gradient(135deg, #7a897a, #000); +} + +.modal-title { + margin-bottom: 0.5rem; + font-size: 1.1rem; + font-weight: 600; + color: #d9f2d9; +} + +.topbar { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + padding: 1rem 2rem; + background: rgba(6, 37, 6, 0.95); + border-bottom: 1px solid rgba(255,255,255,0.08); + position: sticky; + top: 0; + z-index: 100; + box-shadow: 0 8px 18px rgba(0, 0, 0, 0.2); +} + +.topbar__brand-title { + font-size: 1.2rem; + font-weight: 700; + letter-spacing: 0.04em; +} + +.topbar__actions { + display: inline-flex; + align-items: center; + gap: 0.75rem; +} + +.topbar__user { + position: relative; +} + +.topbar-button, +.user-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + background: rgba(255,255,255,0.08); + border: 1px solid rgba(255,255,255,0.15); + color: #fff; + padding: 0.55rem 0.9rem; + border-radius: 999px; + cursor: pointer; + transition: all 0.2s ease; +} + +.topbar-button:hover, +.user-button:hover { + background: rgba(255,255,255,0.12); +} + +.user-menu { + position: absolute; + right: 0; + top: calc(100% + 8px); + display: none; + flex-direction: column; + min-width: 180px; + background: #111; + border: 1px solid rgba(255,255,255,0.08); + border-radius: 10px; + overflow: hidden; + box-shadow: 0 12px 30px rgba(0,0,0,0.3); +} + +.user-menu.visible { + display: flex; +} + +.user-menu button { + background: transparent; + border: none; + color: #fff; + text-align: left; + width: 100%; + padding: 0.9rem 1rem; + cursor: pointer; + font-size: 0.95rem; +} + +.user-menu button:hover { + background: rgba(255,255,255,0.08); +} + +.app-body { + display: flex; + flex: 1; + gap: 1rem; +} + +.contenido { + flex: 1; + padding: 2rem; +} + +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.55); + display: none; + align-items: center; + justify-content: center; + z-index: 200; +} + +.modal-overlay.active { + display: flex; +} + +.modal { + width: min(720px, calc(100% - 32px)); + background: #141414; + border-radius: 16px; + padding: 1.5rem; + box-shadow: 0 18px 40px rgba(0,0,0,0.35); + border: 1px solid rgba(255,255,255,0.08); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.modal-header h3 { + margin: 0; + font-size: 1.2rem; + font-weight: 700; +} + +.modal .formulario__fila { + gap: 12px; + display: flex; + /*flex-direction: column;*/ +} + +.modal .formulario__fila label { + font-size: 1rem; + color: #333; + margin-bottom: 8px; + display: flex; + align-items: center; +} + +.modal .formulario__fila label strong { + color: #ffffff; + font-weight: 600; + margin: 5px; +} + +.modal .formulario__fila span { + color: #ffffff; + font-size: 1rem; +} + +.modal .formulario__fila input[type="text"], +.modal .formulario__fila input[type="email"], +.modal .formulario__fila input[type="tel"] { + padding: 10px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 1rem; + font-family: inherit; + margin-top: 5px; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 8px; +} + +.modal .formulario__fila input[type="text"]:focus, +.modal .formulario__fila input[type="email"]:focus, +.modal .formulario__fila input[type="tel"]:focus { + outline: none; + border-color: #145214; + box-shadow: 0 0 5px rgba(20, 82, 20, 0.3); +} + +.modal .btn-agregar { + width: 100%; +} + +.modal .btn-cerrar { + background: transparent; + color: #fff; + border: 1px solid rgba(255,255,255,0.14); + padding: 0.5rem 0.9rem; + border-radius: 6px; +} + +/* Sidebar */ +.sidebar { + width: 240px; + background: linear-gradient(180deg, #145214, #0b3d0b); + padding: 1.5rem; + color: white; + box-shadow: 2px 0 12px rgba(0,0,0,0.3); +} + +.sidebar__titulo { + margin-bottom: 1rem; + font-size: 1.6rem; + text-align: center; + font-weight: 700; + border-bottom: 2px solid #1e7d1e; + padding-bottom: 0.5rem; +} + +.sidebar__lista { + list-style: none; + display: flex; + flex-direction: column; + gap: 0.8rem; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease; +} + +.sidebar__lista.visible { + opacity: 1; + pointer-events: auto; +} + +.sidebar__link { + color: white; + text-decoration: none; + padding: 0.7rem 1rem; + border-radius: 8px; + display: block; + font-weight: 500; + transition: all 0.3s ease; +} + +.sidebar__link:hover { + background-color: #d9f2d9; + color: #145214; + transform: translateX(6px); +} + +/* Contenido */ +.contenido { + flex: 1; + padding: 2rem; +} + +.seccion { + display: none; + background: #181818; + padding: 1.5rem; + border-radius: 12px; + backdrop-filter: blur(10px); + box-shadow: 0 4px 16px rgba(0,0,0,0.3); + margin-bottom: 2rem; + animation: fadeIn 0.4s ease; + +} + +.seccion h2 { + color: #d9f2d9; + margin-bottom: 1rem; + border-left: 5px solid #1e7d1e; + padding-left: 10px; + font-weight: 700; +} + +/* Formularios */ +form.formulario { + display: flex; + flex-direction: column; + /*gap: 1rem; + max-width: 900px;*/ +} + +/* Fila de inputs */ +.formulario__fila { + display: flex; + gap: 10px; + flex-wrap: wrap; + margin-top: 8px; + margin-bottom: 8px; +} + +.formulario__fila input { + flex: 1; + min-width: 150px; + padding: 0.6rem; + border: 1px solid rgba(255,255,255,0.2); + border-radius: 8px; + font-size: 0.95rem; + background: rgba(255,255,255,0.05); + color: #fff; + transition: 0.3s; +} + +.formulario__fila input:focus { + outline: none; + border-color: #1e7d1e; + box-shadow: 0 0 6px rgba(30,125,30,0.6); +} + +.formulario_producto { + margin-top: 8px; + margin-bottom: 8px; +} + +.formulario_producto input { + flex: 1; + min-width: 150px; + padding: 0.6rem; + border: 1px solid rgba(255,255,255,0.2); + border-radius: 8px; + font-size: 0.95rem; + background: rgba(255,255,255,0.05); + color: #fff; + transition: 0.3s; +} + +/* Formularios en columna */ +.formulario__columna { + display: flex; + flex-direction: column; + gap: 12px; + margin-bottom: 1rem; +} + +.formulario__columna label { + font-weight: 600; + color: #d9f2d9; + margin-bottom: 3px; +} + +.formulario__columna input { + width: 100%; + padding: 0.6rem; + border: 1px solid rgba(255,255,255,0.2); + border-radius: 8px; + background: rgba(255,255,255,0.05); + color: #fff; +} + +/* Botón Agregar */ +.btn-agregar { + background-color: #145214; + color: white; + padding: 0.5rem 1rem; + border: none; + border-radius: 6px; + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 3px 8px rgba(0,0,0,0.25); + margin-top: 10px; + margin-bottom: 15px; +} + +.btn-agregar:hover { + background-color: #1e7d1e; + transform: scale(1.05); +} + +.btn-agregar:disabled, +.btn-editar:disabled, +.btn-cerrar:disabled { + background-color: #555; + cursor: not-allowed; + opacity: 0.6; + transform: none; + box-shadow: none; +} + +.btn-agregar:disabled:hover, +.btn-editar:disabled:hover, +.btn-cerrar:disabled:hover { + background-color: #555; + transform: none; +} + +.btn-cerrar { + background-color: #962a2a; + color: white; + padding: 0.5rem 1rem; + border: none; + border-radius: 6px; + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 3px 8px rgba(0,0,0,0.25); + margin-bottom: 10px; +} +.btn-ver-productos { + background-color: #3c4350; + color: white; + padding: 0.4rem 0.9rem; + border: none; + border-radius: 4px; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 3px 6px rgba(0,0,0,0.25); + margin-right: 4px; +} + +.btn-ver-productos:hover { + background-color: #4d5461; + transform: scale(1.05); +} + +.btn-guardar, +.btn-actualizar-gasto, +.guardar-btn, +.editar-btn, +.btn-editar-gasto, +.btn-editar, +.btn-editar-proveedor { + background-color: #145214; + color:rgb(255, 255, 255); + padding: 0.4rem 0.9rem; + border: none; + border-radius: 6px; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + color: #fff; + box-shadow: 0 3px 6px rgba(0,0,0,0.25); +} + +.btn-guardar, +.btn-actualizar-gasto, +.guardar-btn, +.editar-btn:hover, +.btn-editar:hover, +.btn-editar-proveedor:hover { + background-color: #1e7d1e; +} + +.btn-eliminar-gasto, +.btn-eliminar-producto, +.cancelar-btn, +.btn-eliminar-usuario, +.btn-cancelar, +.eliminar-btn, +.btn-eliminar, +.btn-eliminar-proveedor { + background-color: #962a2a; + margin-left: 6px; + color:rgb(255, 255, 255); + padding: 0.4rem 0.9rem; + border: none; + border-radius: 6px; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + color: #fff; + box-shadow: 0 3px 6px rgba(0,0,0,0.25); +} + +.btn-eliminar-gasto, +.btn-eliminar-producto, +.cancelar-btn, +.btn-cancelar, +.eliminar-btn:hover, +.btn-eliminar:hover, +.btn-eliminar-proveedor:hover { + background-color: #962a2a; +} + +.btn-cargar { + background-color: #255825; + color: white; + padding: 0.4rem 1rem; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 12px rgba(0,0,0,0.25); +} + +.btn-mas { + background-color: #255825; + color: rgb(255, 255, 255); + padding: 0.4rem 0.8rem; + border: none; + border-radius: 4px; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 3px 8px rgba(0,0,0,0.25); +} + +/* Botones dinámicos de órdenes */ +.btn-ver-orden { + background-color: #3c4350; + color: white; + padding: 0.4rem 0.9rem; + border: none; + border-radius: 4px; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 3px 6px rgba(0,0,0,0.25); + margin-right: 4px; +} + +.btn-ver-orden:hover { + background-color: #4d5461; + transform: scale(1.05); +} + +.btn-aprobar { + background-color: #1e7d1e; + color: white; + padding: 0.4rem 0.9rem; + border: none; + border-radius: 4px; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 3px 6px rgba(0,0,0,0.25); + margin-right: 4px; +} + +.btn-aprobar:hover { + background-color: #2a9d2a; + transform: scale(1.05); +} + +.btn-aprobar:disabled { + background-color: #555; + cursor: not-allowed; + opacity: 0.6; +} + +.btn-denegar { + background-color: #962a2a; + color: white; + padding: 0.4rem 0.9rem; + border: none; + border-radius: 4px; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 3px 6px rgba(0,0,0,0.25); +} + +.btn-denegar:hover { + background-color: #b93a3a; + transform: scale(1.05); +} + +.btn-denegar:disabled { + background-color: #555; + cursor: not-allowed; + opacity: 0.6; +} + +/* Tablas */ +table { + width: 100%; + border-collapse: collapse; + margin-top: 1rem; + border-radius: 8px; + overflow: visible; + box-shadow: 0 4px 12px rgba(0,0,0,0.25); +} + +th, td { + padding: 0.8rem; + text-align: left; + overflow: visible; +} + +th { + background-color: #145214; + color: white; + font-weight: 600; +} + +td { + background-color: transparent; + overflow: visible; + position: relative; +} + +tbody tr { + overflow: visible; + z-index: 1; + background-color: #161616; +} + +tbody tr:nth-child(even) { + background-color: #202020; +} + +/* Ventas */ +.venta-formulario .formulario__fila { + display: flex; + gap: 1rem; + align-items: center; +} + +tfoot input#total { + width: 120px; + text-align: right; + padding: 6px; + border: 1px solid #ccc; + border-radius: 6px; + background-color: #e0e0e0; + font-weight: bold; + color: #000; +} + +/* Contenedor del total y botón */ +.venta-footer { + margin-top: 1rem; + display: flex; + justify-content: space-between; /* total a la izq, botón a la der */ + align-items: center; + gap: 1rem; +} + +/* Caja del total */ +.total-box { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 1rem; + margin-bottom: 10px; + margin-top: 10px; +} + +.venta { + margin-left: 20px; +} + +.gasto-monto, +.gasto-nombre, +.input-descripcion, +.input-precio-venta, +.input-precio-compra, +.input-stock, +.input-telefono, +.input-nombre, +.input-descripcion-gasto, +.input-monto-gasto, +.total-box input { + width: 120px; + text-align: right; + padding: 6px; + border: 1px solid #ccc; + border-radius: 6px; + background-color: #e0e0e0; + font-weight: bold; + color: #000; +} + +/* Estilos para Reportes */ +.btn-generar { + background-color: #145214; + color: rgb(255, 255, 255); + padding: 0.4rem 0.9rem; + border: none; + border-radius: 6px; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + color: #fff; + box-shadow: 0 3px 6px rgba(0,0,0,0.25); +} + +.btn-generar:hover { + background-color: #1e7d1e; +} + +.resultados { + margin-top: 2rem; +} + +.tabla-contenedor { + overflow-x: auto; + overflow-y: visible; +} + +.tabla-reportes { + width: 100%; + border-collapse: collapse; + background: rgba(255,255,255,0.05); + border-radius: 8px; + overflow: visible; +} + +.tabla-reportes th, +.tabla-reportes td { + padding: 0.75rem; + text-align: left; + border-bottom: 1px solid rgba(255,255,255,0.08); +} + +.tabla-reportes th { + background: rgba(255,255,255,0.1); + font-weight: 600; +} + +.acciones { + margin-top: 1rem; + display: flex; + gap: 1rem; +} + +.btn-exportar { + background-color: #1e7d1e; + color: white; + padding: 0.4rem 0.9rem; + border: none; + border-radius: 4px; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 3px 6px rgba(0,0,0,0.25); + margin-right: 4px; +} + +.btn-exportar:hover { + background-color: #4d5461; + transform: scale(1.05); + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 3px 6px rgba(0,0,0,0.25); + margin-right: 4px; +} + +.btn-exportar:hover { + background-color: #4d5461; + transform: scale(1.05); +} + +/* Estilos extra para Reservas, Canchas y Cupones */ + +/* Select estilizado */ +form select { + padding: 0.6rem; + border: 1px solid rgba(255,255,255,0.2); + border-radius: 8px; + background: #2a2a2a; + color: #fff; + font-size: 0.95rem; +} + +form select option { + background-color: #2a2a2a; + color: #fff; + padding: 0.5rem; +} + +form select:focus { + outline: none; + border-color: #1e7d1e; + box-shadow: 0 0 6px rgba(30,125,30,0.6); +} + +/* Tablas nuevas */ +#tabla-reservas, #tabla-cupones { + margin-top: 1rem; +} + +#tabla-reservas th, #tabla-cupones th { + background-color: #145214; + color: white; + text-align: center; +} + +#tabla-reservas td, #tabla-cupones td { + text-align: center; +} + +/* Botón bloquear */ +.btn-bloquear { + background-color: #a51616; + padding: 0.4rem 0.9rem; + border: none; + border-radius: 6px; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + color: #fff; + transition: 0.3s; +} + +.btn-bloquear:hover { + background-color: #d11a1a; +} + +#registrar-usuario:target, +#ventas:target, +#ver_ventas:target, +#productos:target, +#inventario:target, +#orden:target, +#proveedores:target, +#reportes:target, +#gastos:target, +#reserva:target, +#cancha:target { + display: block; + background-color: #181818; /*fondo*/ +} + +/* Animación */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(15px); } + to { opacity: 1; transform: translateY(0); } +} + +/* Estilos para la vista de Ventas del Día */ +#tabla-principal-contenedor { + animation: fadeIn 0.3s ease-in; +} + +.tabla-ventas-dia { + width: 100%; + margin-top: 1.5rem; + border-collapse: collapse; + background: rgba(255,255,255,0.02); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 8px; + overflow: hidden; +} + +.tabla-ventas-dia thead { + background-color: #145214; + color: white; + font-weight: 600; +} + +.tabla-ventas-dia th { + padding: 1rem; + text-align: left; + border-bottom: 2px solid rgba(255,255,255,0.1); + font-size: 0.95rem; +} + +.tabla-ventas-dia tbody tr { + border-bottom: 1px solid rgba(255,255,255,0.08); + transition: background-color 0.2s ease; +} + +.tabla-ventas-dia tbody tr:hover { + background-color: rgba(255,255,255,0.05); +} + +.tabla-ventas-dia td { + padding: 1rem; + color: #e0e0e0; + font-size: 0.95rem; +} + +/* Botón "Ver Detalle" */ +.btn-ver-detalle { + background-color: #3c4350; + color: white; + padding: 0.5rem 1rem; + border: none; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.btn-ver-detalle:hover { + background-color: #4a5260; +} + +/* Dropdown de Estado */ +.estado-dropdown { + position: relative; + display: inline-block; + width: 100%; +} + +.estado-btn { + width: 100%; + padding: 0.6rem 0.9rem; + background: rgba(255, 255, 255, 0.1); + color: #f0f0f0; + border: 1px solid rgba(30, 125, 30, 0.4); + border-radius: 6px; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.9rem; + font-weight: 600; + transition: all 0.3s ease; +} + +.estado-btn:hover { + background: rgba(30, 125, 30, 0.15); + border-color: #1e7d1e; + box-shadow: 0 2px 8px rgba(30, 125, 30, 0.3); +} + +.estado-btn:active { + background: rgba(30, 125, 30, 0.25); + border-color: #1e7d1e; +} + +.estado-btn.disabled { + background: rgba(100, 100, 100, 0.2); + color: #888; + border-color: rgba(100, 100, 100, 0.4); + cursor: not-allowed; + opacity: 0.6; +} + +.estado-btn.disabled:hover { + background: rgba(100, 100, 100, 0.2); + border-color: rgba(100, 100, 100, 0.4); + box-shadow: none; +} + +.estado-btn.disabled .estado-texto { + color: #888 !important; +} + +.estado-texto { + flex: 1; + text-align: left; +} + +.estado-flecha { + margin-left: 0.5rem; + transition: transform 0.3s ease; + font-size: 0.7rem; +} + +.estado-btn[data-estado="0"] .estado-texto { + color: #f0a060; +} + +.estado-btn[data-estado="1"] .estado-texto { + color: #60d960; +} + +.estado-btn[data-estado="2"] .estado-texto { + color: #ff6b6b; +} + +/* Opciones del Dropdown */ +.estado-opciones { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: #2a2a2a; + border: 1px solid rgba(30, 125, 30, 0.4); + border-top: none; + border-radius: 0 0 6px 6px; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4); + z-index: 1000; + min-width: 200px; + overflow: hidden; +} + +.estado-opcion { + padding: 0.8rem 0.9rem; + color: #f0f0f0; + cursor: pointer; + font-size: 0.9rem; + transition: all 0.2s ease; + border-bottom: 1px solid rgba(30, 125, 30, 0.2); +} + +.estado-opcion:last-child { + border-bottom: none; +} + +.estado-opcion:hover { + background: rgba(30, 125, 30, 0.2); + padding-left: 1.2rem; +} + +.estado-opcion[data-valor="0"] { + color: #f0a060; +} + +.estado-opcion[data-valor="0"]:hover { + background: rgba(240, 160, 96, 0.15); +} + +.estado-opcion[data-valor="1"] { + color: #60d960; +} + +.estado-opcion[data-valor="1"]:hover { + background: rgba(96, 217, 96, 0.15); +} + +.estado-opcion[data-valor="2"] { + color: #ff6b6b; +} + +.estado-opcion[data-valor="2"]:hover { + background: rgba(255, 107, 107, 0.15); + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 2px 6px rgba(0,0,0,0.25); + display: inline-flex; + align-items: center; + gap: 0.5rem; +} + +.btn-ver-detalle:hover { + background-color: #3b3f3b; + transform: translateY(-2px); + box-shadow: 0 4px 10px rgba(0,0,0,0.35); +} + +.btn-ver-detalle:active { + transform: translateY(0); +} + +/* Contenedor de Detalle de Venta */ +#detalle-venta-contenedor { + animation: fadeIn 0.3s ease-in; +} + +.detalle-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + padding-bottom: 1rem; + border-bottom: 2px solid rgba(255,255,255,0.1); +} + +.detalle-header h3 { + font-size: 1.3rem; + color: #fff; + margin: 0; +} + +/* Botón Volver */ +.btn-volver { + background-color: #3c4350; + color: white; + padding: 0.5rem 1rem; + border: none; + border-radius: 6px; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 2px 6px rgba(0,0,0,0.25); + display: inline-flex; + align-items: center; + gap: 0.5rem; +} + +.btn-volver:hover { + background-color: #4d5461; + transform: translateY(-2px); + box-shadow: 0 4px 10px rgba(0,0,0,0.35); +} + +.btn-volver:active { + transform: translateY(0); +} + +/* Tabla de Detalle */ +.tabla-detalle { + width: 100%; + border-collapse: collapse; + background: rgba(255,255,255,0.02); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 8px; + overflow: hidden; + margin-top: 1rem; +} + +.tabla-detalle thead { + background-color: #1e7d1e; + color: white; + font-weight: 600; +} + +.tabla-detalle th { + padding: 1.2rem; + text-align: left; + border-bottom: 2px solid rgba(255,255,255,0.1); + font-size: 0.95rem; +} + +.tabla-detalle tbody tr { + border-bottom: 1px solid rgba(255,255,255,0.08); + transition: background-color 0.2s ease; +} + +.tabla-detalle tbody tr:hover { + background-color: rgba(255,255,255,0.05); +} + +.tabla-detalle tbody tr:last-child { + background-color: rgba(30,125,30,0.15); + font-weight: 600; + color: #fff; +} + +.tabla-detalle td { + padding: 1.2rem; + color: #e0e0e0; + font-size: 0.95rem; +} + +.tabla-detalle tbody tr:last-child td { + color: #fff; + border-top: 2px solid rgba(255,255,255,0.2); +} + +.stock-limit-input { + min-width: 150px; + padding: 0.6rem; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 8px; + font-size: 0.95rem; + background: rgba(255, 255, 255, 0.05); + color: #fff; + transition: 0.3s; +} \ No newline at end of file diff --git a/css/Login.css b/css/Login.css new file mode 100644 index 0000000..86f5226 --- /dev/null +++ b/css/Login.css @@ -0,0 +1,326 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Segoe UI", sans-serif; +} + +body { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #7a897a, #000); + +} + +.container { + border: 1px solid rgba(255,255,255,0.15); + width: 90%; + max-width: 420px; + padding: 40px 35px; + border-radius: 12px; + color: #fff; + backdrop-filter: blur(20px); + box-shadow: 0 6px 20px rgb(36 255 91 / 40%); + background: rgba(0,0,0,0.6); +} + +.container h1 { + text-align: center; + margin-bottom: 30px; + font-size: 2rem; + color: #1e7d1e; + font-weight: 700; +} + +/* Formularios */ +form { + display: flex; + flex-direction: column; + gap: 25px; +} + +.input-box { + border: 1px solid rgba(255,255,255,0.2); + display: flex; + align-items: center; + height: 55px; + border-radius: 35px; + padding: 0 20px; + background: rgba(255,255,255,0.05); + transition: all 0.3s ease; +} + +.input-box input { + flex: 1; + background: transparent; + border: 0; + outline: 0; + font-size: 16px; + color: #fff; + min-width: 0; +} + +.input-box input::placeholder { + color: rgba(255,255,255,0.7); +} + +.input-box:focus-within { + border-color: #1e7d1e; + box-shadow: 0 0 8px rgba(30,125,30,0.7); +} + +/* Recordarme y link */ +.remember-password { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + gap: 15px; + font-size: 14px; +} + +.remember-password a { + color: #d9f2d9; + text-decoration: none; + transition: 0.3s; +} + +.remember-password a:hover { + text-decoration: underline; + color: #1e7d1e; +} + +/* Botón */ +.btnI { + border-radius: 30px; + border: none; + outline: none; + font-size: 16px; + font-weight: 600; + padding: 12px; + cursor: pointer; + background-color: #145214; + color: white; + transition: all 0.3s ease; + box-shadow: 0 4px 10px rgba(0,0,0,0.3); +} + +.btnI:hover { + background-color: #1e7d1e; + transform: scale(1.05); +} + +/* Registro */ +.register { + text-align: center; + font-size: 14px; + margin-top: 5px; +} + +.register a { + color: #1e7d1e; + text-decoration: none; + font-weight: 600; + margin-left: 5px; + transition: 0.3s; +} + +.register a:hover { + text-decoration: underline; +} + +/* Estilos del Modal de Recuperación */ +.modal { + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.modal-content { + background: rgba(0, 0, 0, 0.6); + border: 2px solid rgba(30, 125, 30, 0.5); + border-radius: 12px; + padding: 40px 40px; + width: 90%; + max-width: 500px; + box-shadow: 0 6px 20px rgb(36 255 91 / 40%); + position: relative; + backdrop-filter: blur(20px); + animation: slideIn 0.3s ease; + color: #fff; +} + +@keyframes slideIn { + from { + transform: translateY(-50px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +.close { + position: absolute; + right: 20px; + top: 20px; + font-size: 28px; + font-weight: bold; + color: rgba(255, 255, 255, 0.7); + cursor: pointer; + transition: color 0.3s ease; +} + +.close:hover { + color: #1e7d1e; +} + +.paso-recuperacion { + display: flex; + flex-direction: column; + gap: 15px; +} + +.paso-recuperacion h2 { + text-align: center; + color: #1e7d1e; + font-size: 1.5rem; + margin-bottom: 5px; + font-weight: 700; +} + +.paso-recuperacion p { + text-align: center; + color: rgba(255, 255, 255, 0.8); + font-size: 14px; + margin-bottom: 10px; +} + +.paso-recuperacion input { + border: 2px solid rgba(255, 255, 255, 0.3); + background: rgba(255, 255, 255, 0.05); + border-radius: 8px; + padding: 12px 20px; + color: #fff; + font-size: 16px; + transition: all 0.3s ease; + outline: none; + height: 55px; + width: 100%; + margin-top: 15px; +} + +.paso-recuperacion input::placeholder { + color: rgba(255, 255, 255, 0.5); +} + +.paso-recuperacion input:focus { + border-color: #1e7d1e; + box-shadow: 0 0 8px rgba(30, 125, 30, 0.7); +} + +/* Contenedor de botones para paso 2 */ +.paso-recuperacion .botones-grupo { + display: flex; + gap: 15px; + margin-top: 30px; +} + +.paso-recuperacion .botones-grupo button { + flex: 1; +} + +.btnRecuperar { + border-radius: 8px; + border: none; + outline: none; + font-size: 16px; + font-weight: 600; + padding: 12px; + cursor: pointer; + background-color: #145214; + color: white; + transition: all 0.3s ease; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); + height: 55px; + width: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.btnRecuperar:hover:not(:disabled) { + background-color: #1e7d1e; + transform: scale(1.05); +} + +.btnRecuperar:disabled { + background-color: #0a5a0a; + cursor: not-allowed; + opacity: 0.7; +} + +.btnVolver { + border-radius: 8px; + border: 2px solid rgba(255, 255, 255, 0.3); + outline: none; + font-size: 16px; + font-weight: 600; + padding: 12px; + cursor: pointer; + background-color: transparent; + color: rgba(255, 255, 255, 0.8); + transition: all 0.3s ease; + height: 55px; + width: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.btnVolver:hover { + border-color: #1e7d1e; + color: #1e7d1e; + background-color: rgba(30, 125, 30, 0.1); +} + +.mensaje { + padding: 12px; + border-radius: 8px; + text-align: center; + font-size: 14px; + min-height: 20px; + margin-top: 10px; +} + +.mensaje.success { + background-color: rgba(30, 125, 30, 0.3); + color: #90ee90; + border: 1px solid rgba(30, 125, 30, 0.5); +} + +.mensaje.error { + background-color: rgba(255, 0, 0, 0.2); + color: #ff6b6b; + border: 1px solid rgba(255, 0, 0, 0.5); +} + diff --git a/css/Reserva_Clientes.css b/css/Reserva_Clientes.css new file mode 100644 index 0000000..dcd5c89 --- /dev/null +++ b/css/Reserva_Clientes.css @@ -0,0 +1,814 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Segoe UI", sans-serif; +} + +body { + background: linear-gradient(135deg, #7a897a, #000); + color: #f0f0f0; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +/* Header */ +.header { + background: linear-gradient(180deg, #145214, #0b3d0b); + padding: 1rem 1rem; + text-align: center; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + margin-bottom: 2rem; +} + +.header-content { + max-width: 1200px; + margin: 0 auto; +} + +.header-title { + font-size: 2.5rem; + color: #d9f2d9; + margin-bottom: 0.5rem; + font-weight: 700; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); +} + +.header-subtitle { + font-size: 1.1rem; + color: #b8d9b8; + font-weight: 500; +} + +/* Contenedor principal */ +.contenedor-principal { + flex: 1; + max-width: 900px; + margin: 0 auto; + width: 100%; + padding: 0 1rem 2rem; +} + +/* Secciones */ +.seccion { + background: rgba(255, 255, 255, 0.08); + padding: 2rem; + border-radius: 12px; + backdrop-filter: blur(10px); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); + margin-bottom: 2rem; + border: 1px solid rgba(30, 125, 30, 0.2); + animation: fadeIn 0.4s ease; + background-color: #181818; +} + +.seccion h2, +.seccion h3 { + color: #d9f2d9; + margin-bottom: 1.5rem; + font-weight: 700; + border-left: 5px solid #1e7d1e; + padding-left: 10px; +} + +.seccion h2 { + font-size: 1.8rem; + border-bottom: 2px solid rgba(30, 125, 30, 0.3); + padding-bottom: 1rem; +} + +.seccion h3 { + font-size: 1.3rem; +} + +/* Botones de tipo de cancha */ +.botones-cancha { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 20px; +} + +.btn-cancha-tipo { + padding: 2rem; + border: 3px solid rgba(255, 255, 255, 0.2); + border-radius: 12px; + background: rgba(255, 255, 255, 0.05); + color: #fff; + font-size: 1.1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} + +.btn-cancha-tipo .icono { + font-size: 3rem; + width: 80px; + height: 80px; + object-fit: contain; + display: block; +} + +.btn-cancha-tipo .nombre { + display: block; +} + +.btn-cancha-tipo:hover { + border-color: #1e7d1e; + background: rgba(30, 125, 30, 0.15); + transform: translateY(-4px); + box-shadow: 0 6px 20px rgba(30, 125, 30, 0.3); +} + +.btn-cancha-tipo.activo { + background: linear-gradient(135deg, #1e7d1e, #145214); + border-color: #1e7d1e; + box-shadow: 0 8px 25px rgba(30, 125, 30, 0.6); + transform: translateY(-2px); +} + +/* Calendario de días */ +.dias-disponibles { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); + gap: 12px; +} + +.btn-dia { + padding: 1rem; + border: 2px solid rgba(255, 255, 255, 0.2); + border-radius: 10px; + background: rgba(255, 255, 255, 0.05); + color: #fff; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + text-align: center; + font-size: 0.9rem; +} + +.btn-dia div:first-child { + font-size: 0.85rem; + color: #b8d9b8; + margin-bottom: 5px; +} + +.btn-dia div:last-child { + font-size: 1.2rem; +} + +.btn-dia:hover { + border-color: #1e7d1e; + background: rgba(30, 125, 30, 0.15); + transform: translateY(-3px); +} + +.btn-dia.activo { + background: linear-gradient(135deg, #1e7d1e, #145214); + border-color: #1e7d1e; + box-shadow: 0 6px 20px rgba(30, 125, 30, 0.6); +} + +.btn-dia.deshabilitado { + opacity: 0.4; + cursor: not-allowed; + background: rgba(150, 42, 42, 0.15); + border-color: rgba(150, 42, 42, 0.4); +} + +/* Grid de horarios */ +#grid-horarios { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); + gap: 12px; +} + +.horario-slot { + padding: 1.2rem; + border: 2px solid rgba(255, 255, 255, 0.2); + border-radius: 10px; + background: rgba(255, 255, 255, 0.05); + color: #fff; + font-weight: 600; + text-align: center; + cursor: pointer; + transition: all 0.3s ease; + white-space: pre-line; + font-size: 0.9rem; + line-height: 1.4; +} + +.horario-slot:hover:not(.ocupado) { + background: linear-gradient(135deg, #1e7d1e, #145214); + border-color: #1e7d1e; + box-shadow: 0 8px 25px rgba(30, 125, 30, 0.8); + transform: translateY(-4px); +} + +.horario-slot.ocupado +.horario-slot.no-disponible { + background: rgba(150, 42, 42, 0.25); + border-color: rgba(150, 42, 42, 0.5); + cursor: not-allowed; + opacity: 0.6; + color: #ccc; +} + +.horario-slot.activo { + background: linear-gradient(135deg, #1e7d1e, #145214); + border-color: #1e7d1e; + box-shadow: 0 8px 25px rgba(30, 125, 30, 0.8); +} + +/* Botones de selección de número de cancha */ +.botones-canchas-numeros { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); + gap: 12px; +} + +.btn-numero-cancha { + padding: 1rem; + border: 2px solid rgba(255, 255, 255, 0.2); + border-radius: 10px; + background: rgba(255, 255, 255, 0.05); + color: #fff; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + text-align: center; + font-size: 0.95rem; +} + +.btn-numero-cancha:hover:not(:disabled) { + border-color: #1e7d1e; + background: rgba(30, 125, 30, 0.15); + transform: translateY(-3px); +} + +.btn-numero-cancha.activo { + background: linear-gradient(135deg, #1e7d1e, #145214); + border-color: #1e7d1e; + box-shadow: 0 6px 20px rgba(30, 125, 30, 0.6); +} + +.btn-numero-cancha:disabled { + opacity: 0.4; + cursor: not-allowed; + background: rgba(150, 42, 42, 0.15); + border-color: rgba(150, 42, 42, 0.4); +} + +/* Resumen de información */ +.resumen-info { + background: rgba(30, 125, 30, 0.1); + padding: 1.5rem; + border-radius: 10px; + margin-bottom: 2rem; + border-left: 5px solid #1e7d1e; +} + +.info-item { + display: flex; + justify-content: space-between; + padding: 0.8rem 0; + border-bottom: 1px solid rgba(30, 125, 30, 0.2); +} + +.info-item:last-child { + border-bottom: none; +} + +.info-item.total { + background: rgba(30, 125, 30, 0.15); + padding: 1rem; + margin-top: 0.5rem; + border-radius: 8px; + font-size: 1.1rem; +} + +.info-item .label { + color: #d9f2d9; + font-weight: 600; +} + +.info-item .valor { + color: #f0f0f0; + text-align: right; +} + +.info-item.total .valor { + color: #d9f2d9; + font-size: 1.3rem; + font-weight: 700; +} + +/* Opciones adicionales */ +.opciones-adicionales { + background: rgba(255, 255, 255, 0.05); + padding: 1.5rem; + border-radius: 10px; + margin-bottom: 2rem; +} + +.opciones-adicionales h3 { + margin-bottom: 1.5rem; +} + +.checkbox-container { + display: flex; + align-items: center; + gap: 10px; + padding: 1rem; + border-radius: 8px; + cursor: pointer; + transition: background 0.3s; +} + +.checkbox-container:hover { + background: rgba(30, 125, 30, 0.1); +} + +.checkbox-container input[type="checkbox"] { + width: 20px; + height: 20px; + cursor: pointer; + accent-color: #1e7d1e; +} + +.checkbox-text { + color: #f0f0f0; + flex: 1; +} + +.precio-quincho { + color: #1e7d1e; + font-weight: 700; + margin-left: auto; + font-size: 1.1rem; +} + +.quincho-info { + margin-top: 1rem; + padding: 0.8rem; + background: rgba(30, 125, 30, 0.15); + border-radius: 6px; + color: #d9f2d9; +} + +.quincho-alerta { + margin-top: 1rem; + padding: 0.8rem; + background: rgba(200, 100, 30, 0.25); + border-radius: 6px; + color: #f0a060; + border-left: 3px solid #d97020; +} + +/* Datos del cliente */ +.datos-cliente { + background: rgba(255, 255, 255, 0.05); + /*padding: 1.5rem;*/ + border-radius: 10px; + margin-bottom: 2rem; + background-color: #181818; +} + +.datos-cliente h3 { + margin-bottom: 1.5rem; +} + +.form-cliente { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.form-cliente input { + width: 100%; + padding: 0.9rem; + border: 2px solid rgba(255, 255, 255, 0.2); + border-radius: 8px; + background: rgba(255, 255, 255, 0.05); + color: #fff; + font-size: 0.95rem; + transition: all 0.3s; +} + +/* Reprogramar modal styles */ +.reprogramar-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} +.reprogramar-modal { + width: 900px; + max-width: 95%; + background: #121212; + padding: 20px; + border-radius: 12px; + border: 1px solid rgba(30,125,30,0.25); + box-shadow: 0 10px 30px rgba(0,0,0,0.6); +} +.reprogramar-modal h3 { + margin: 10px 0; + color: #d9f2d9; + border-left: 4px solid #1e7d1e; + padding-left: 10px; +} +.dias-grid, .horarios-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 12px; + margin-bottom: 12px; +} +.dia-btn, .horario-btn { + padding: 14px; + background: rgba(255,255,255,0.03); + border: 2px solid rgba(255,255,255,0.06); + color: #fff; + border-radius: 10px; + cursor: pointer; + white-space: pre-line; + font-weight: 600; + transition: all 0.2s ease; +} +.dia-btn:hover, .horario-btn:hover { transform: translateY(-4px); border-color: #1e7d1e; background: rgba(30,125,30,0.12); } +.dia-btn.selected, .horario-btn.selected { background: linear-gradient(135deg,#1e7d1e,#145214); border-color: #1e7d1e; box-shadow: 0 8px 20px rgba(30,125,30,0.5); } +.reprogramar-actions { display:flex; gap:10px; justify-content:flex-end; margin-top:8px; } +.reprogramar-actions .btn-cancelar, .reprogramar-actions .btn-confirmar { padding:10px 16px; border-radius:8px; border: none; cursor:pointer; font-weight:700; } +.reprogramar-actions .btn-cancelar { background: rgba(255,255,255,0.06); color:#fff; } +.reprogramar-actions .btn-confirmar { background: #1e7d1e; color:#fff; } +.reprogramar-actions .btn-confirmar[disabled] { opacity:0.5; cursor:not-allowed; } + +/* Reprogramar button in table */ +.reprogramar-btn { margin-left:8px; padding:6px 8px; border-radius:6px; background: rgba(30,125,30,0.12); border:1px solid rgba(30,125,30,0.2); color:#d9f2d9; cursor:pointer; font-weight:600; } +.reprogramar-btn.disabled { opacity:0.5; cursor:not-allowed; } + +/* Estilos para reprogramar desde tabla de reservas */ +.reprogramar-btn { + margin-left: 10px; + padding: 0.45rem 0.7rem; + border-radius: 8px; + border: 2px solid rgba(255,255,255,0.12); + background: rgba(255,255,255,0.03); + color: #d9f2d9; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + margin-top: 5px; + margin-bottom: 5px; +} +.reprogramar-btn:hover:not(:disabled) { + background: rgba(30,125,30,0.12); + border-color: #1e7d1e; + transform: translateY(-2px); +} +.reprogramar-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Overlay modal */ +.reprogramar-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + padding: 20px; +} +.reprogramar-modal { + width: min(980px, 96%); + max-height: 90vh; + overflow: auto; + background: #111; + border-radius: 12px; + padding: 18px; + border: 1px solid rgba(30,125,30,0.18); + box-shadow: 0 8px 30px rgba(0,0,0,0.6); +} +.reprogramar-modal h3 { + color: #d9f2d9; + margin: 8px 0 12px; + border-left: 4px solid #1e7d1e; + padding-left: 10px; +} +.dias-grid { + display: flex; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 14px; +} +.dia-btn { + width: 110px; + height: 70px; + border-radius: 10px; + padding: 8px; + background: rgba(255,255,255,0.03); + border: 2px solid rgba(255,255,255,0.08); + color: #fff; + font-weight: 700; + cursor: pointer; + white-space: pre-line; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + transition: all 0.18s ease; +} +.dia-btn:hover:not(.deshabilitado) { + border-color: #1e7d1e; + background: rgba(30,125,30,0.12); + transform: translateY(-4px); +} +.dia-btn.selected { + background: linear-gradient(135deg,#1e7d1e,#145214); + border-color: #1e7d1e; + box-shadow: 0 8px 22px rgba(30,125,30,0.45); +} + +.horarios-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(130px,1fr)); + gap: 12px; + margin-bottom: 14px; +} +.horario-btn { + padding: 12px; + border-radius: 10px; + background: rgba(255,255,255,0.03); + border: 2px solid rgba(255,255,255,0.08); + color: #fff; + font-weight: 700; + cursor: pointer; + white-space: pre-line; + transition: all 0.18s ease; +} +.horario-btn:hover:not(.ocupado) { + background: rgba(30,125,30,0.12); + border-color: #1e7d1e; + transform: translateY(-4px); +} +.horario-btn.selected { + background: linear-gradient(135deg,#1e7d1e,#145214); + border-color: #1e7d1e; + box-shadow: 0 8px 22px rgba(30,125,30,0.45); +} + +.reprogramar-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 10px; +} +.reprogramar-actions .btn-cancelar, +.reprogramar-actions .btn-confirmar { + padding: 10px 14px; + border-radius: 8px; + font-weight: 700; + border: 2px solid rgba(255,255,255,0.08); + background: rgba(255,255,255,0.03); + color: #fff; + cursor: pointer; +} +.reprogramar-actions .btn-cancelar:hover { background: rgba(255,255,255,0.06); } +.reprogramar-actions .btn-confirmar:hover:not(:disabled) { background: rgba(30,125,30,0.14); border-color: #1e7d1e; } +.reprogramar-actions .btn-confirmar:disabled { opacity: 0.5; cursor: not-allowed; } + +@media (max-width:600px) { + .dias-grid { justify-content: center; } + .dia-btn { width: 90px; height: 64px; } +} + +.form-cliente input:focus { + outline: none; + border-color: #1e7d1e; + box-shadow: 0 0 8px rgba(30, 125, 30, 0.6); + background: rgba(30, 125, 30, 0.1); +} + +.form-cliente input::placeholder { + color: #888; +} + +/* Botones de reserva */ +.botones-reserva { + display: flex; + gap: 15px; + justify-content: flex-end; + flex-wrap: wrap; +} + +.btn-verificar, +.btn-volver, +.btn-confirmar { + padding: 0.9rem 2rem; + border: none; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + font-size: 1rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.btn-volver { + background: rgba(255, 255, 255, 0.1); + color: #fff; + border: 2px solid rgba(255, 255, 255, 0.2); +} + +.btn-volver:hover { + background: rgba(255, 255, 255, 0.15); + border-color: rgba(255, 255, 255, 0.4); + transform: translateY(-2px); +} + +.btn-verificar, +.btn-confirmar { + background: linear-gradient(135deg, #1e7d1e, #145214); + color: #fff; + box-shadow: 0 4px 15px rgba(30, 125, 30, 0.4); + min-width: 200px; +} + +.btn-verificar:hover, +.btn-confirmar:hover { + background: linear-gradient(135deg, #2a9d2a, #1e7d1e); + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(30, 125, 30, 0.6); +} + +.btn-verificar:active, +.btn-confirmar:active { + transform: translateY(0); +} + +/* Footer */ +.footer { + background: rgba(0, 0, 0, 0.3); + padding: 2rem 1rem; + text-align: center; + color: #b8d9b8; + border-top: 1px solid rgba(30, 125, 30, 0.2); + margin-top: auto; +} + +/* Responsive */ +@media (max-width: 768px) { + .header-title { + font-size: 1.8rem; + } + + .seccion { + padding: 1.5rem; + } + + .botones-cancha { + grid-template-columns: 1fr; + } + + .btn-cancha-tipo { + padding: 1.5rem; + } + + .botones-reserva { + flex-direction: column-reverse; + } + + .btn-verificar, + .btn-confirmar { + min-width: 100%; + } + + .btn-volver { + width: 100%; + } + + #grid-horarios { + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + } + + .info-item { + flex-direction: column; + gap: 0.5rem; + } + + .info-item .valor { + text-align: left; + } +} + +/* Dropdown de Estado en Tabla */ +.estado-dropdown { + position: relative; + display: inline-block; + width: 100%; + z-index: 10; +} + +.estado-btn { + width: 100%; + padding: 0.6rem 1rem; + border: 2px solid rgba(30, 125, 30, 0.5); + border-radius: 6px; + background: rgba(30, 125, 30, 0.1); + color: #fff; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; + font-size: 0.95rem; + z-index: 11; +} + +.estado-btn:hover { + background: rgba(30, 125, 30, 0.2); + border-color: #1e7d1e; + box-shadow: 0 2px 8px rgba(30, 125, 30, 0.3); +} + +.estado-texto { + flex: 1; + text-align: left; +} + +.estado-flecha { + font-size: 0.8rem; + transition: transform 0.3s ease; +} + +.estado-btn[aria-expanded="true"] .estado-flecha { + transform: rotate(180deg); +} + +.estado-opciones { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: #2a2a2a; + border: 2px solid #1e7d1e; + border-top: none; + border-radius: 0 0 6px 6px; + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.5), 0 0 0 9999px rgba(0, 0, 0, 0.3); + z-index: 1001; + max-height: 200px; + overflow-y: auto; + margin-top: -2px; + min-width: 100%; + width: max-content; +} + +.estado-opcion { + padding: 0.8rem 1rem; + color: #fff; + cursor: pointer; + transition: all 0.2s ease; + border-bottom: 1px solid rgba(30, 125, 30, 0.2); + white-space: nowrap; +} + +.estado-opcion:last-child { + border-bottom: none; +} + +.estado-opcion:hover { + background: rgba(30, 125, 30, 0.3); + padding-left: 1.3rem; +} + +.estado-opcion.activo { + background: rgba(30, 125, 30, 0.5); + font-weight: 700; + color: #d9f2d9; + border-left: 3px solid #1e7d1e; + padding-left: 0.8rem; +} + +/* Animaciones */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(15px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/css/img/futbol.svg b/css/img/futbol.svg new file mode 100644 index 0000000..8afe7a0 --- /dev/null +++ b/css/img/futbol.svg @@ -0,0 +1,952 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/css/img/padel.svg b/css/img/padel.svg new file mode 100644 index 0000000..cc94309 --- /dev/null +++ b/css/img/padel.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + diff --git a/html/Cantina.html b/html/Cantina.html new file mode 100644 index 0000000..8e6a1c0 --- /dev/null +++ b/html/Cantina.html @@ -0,0 +1,2391 @@ + + + + + + Gestión Punto de Venta + + + + + + + + + + +
+
+ Cap1tan - Panel de Gestión +
+
+ +
+ +
+ + + +
+
+
+
+ +
+ + +
+ +
+

Manual de Uso

+

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.

+ +

1. Flujo general para cualquier usuario

+
    +
  • Mi Perfil: haz clic en el icono de usuario y selecciona "Mi Perfil". Allí podrás ver y actualizar tu nombre, correo y teléfono.
  • +
  • Cambiar Contraseña: en el mismo menú selecciona "Seguridad". Ingresa tu contraseña actual, la nueva y confírmala.
  • +
  • Cerrar Sesión: usa "Cerrar Sesión" para salir del sistema de forma segura.
  • +
  • Acceso a secciones: el menú lateral muestra solo las opciones disponibles para tu rol.
  • +
+ +

2. Trabajador Cancha - Gestión de reservas

+

Este rol se encarga de controlar las reservas y el estado de pago de cada cliente.

+

2.1 Ver reservas

+
    +
  1. Abre la sección Reserva en el menú lateral.
  2. +
  3. Verás una tabla con las reservas activas.
  4. +
  5. La tabla muestra: Nombre, DNI, Teléfono, Horario, Tipo de Cancha, Número de Cancha, Descuento, Total y Estado.
  6. +
+

2.2 Buscar reservas

+
    +
  1. Usa el filtro por fecha si quieres ver las reservas de un día específico.
  2. +
  3. Usa el filtro por nombre para buscar a un cliente concreto.
  4. +
  5. Usa el filtro por DNI para localizar una reserva por documento.
  6. +
  7. Usa el filtro por tipo de cancha para ver solo Fútbol, Pádel o Quincho.
  8. +
  9. Haz clic en "Buscar" para aplicar los filtros.
  10. +
+

2.3 Cambiar el estado de una reserva

+
    +
  1. En la columna Estado de la tabla aparecerá un selector desplegable.
  2. +
  3. Haz clic en el estado actual.
  4. +
  5. Selecciona uno de los valores: No Pagado, Pagado o Cancelado.
  6. +
  7. El sistema guardará el cambio y actualizará el estado.
  8. +
+

Este flujo se usa cuando el cliente llega a la cancha, paga su turno o cuando se anula una reserva.

+ +

3. Trabajador Cantina - Ventas y stock

+

Este rol gestiona las ventas de productos, el inventario y las órdenes de compra.

+

3.1 Registrar una venta

+
    +
  1. Abre la sección Registrar Ventas.
  2. +
  3. En la primera fila, ingresa el Código del Producto o Nombre del Producto.
  4. +
  5. Introduce la Cantidad que va a vender.
  6. +
  7. Haz clic en el botón + para añadir el producto a la venta actual.
  8. +
  9. Verifica que la lista de venta actual muestre el producto correcto, el precio y el subtotal.
  10. +
  11. Revisa el Total de la operación.
  12. +
  13. Haz clic en Registrar Venta para guardar la venta en el sistema.
  14. +
+

3.2 Consultar ventas

+
    +
  1. Abre la sección Ver Ventas.
  2. +
  3. Selecciona la fecha deseada en el campo de fecha.
  4. +
  5. El sistema mostrará el total de ventas de ese día.
  6. +
  7. En la tabla verás cada venta registrada con su ID, hora y monto total.
  8. +
  9. Haz clic en una venta para ver el detalle de los productos incluidos.
  10. +
+

3.3 Gestionar productos

+
    +
  1. Abre la sección Productos.
  2. +
  3. Completa los campos: descripción, precio de venta, precio de compra y stock.
  4. +
  5. Haz clic en Agregar Producto para registrar un nuevo producto.
  6. +
  7. En la tabla podrás ver los productos registrados y los datos de stock.
  8. +
  9. Puedes editar o eliminar productos según los botones de acción disponibles en cada fila.
  10. +
+

3.4 Crear una orden de compra

+
    +
  1. Abre la sección Crear Orden de Compra.
  2. +
  3. En el primer bloque, ingresa el código o nombre del proveedor.
  4. +
  5. El sistema mostrará el nombre y teléfono del proveedor si coincide.
  6. +
  7. En el segundo bloque, ingresa el código del producto y la cantidad a pedir.
  8. +
  9. Haz clic en el botón + para agregar el producto a la orden.
  10. +
  11. Repite hasta tener todos los productos necesarios.
  12. +
  13. Finalmente, haz clic en Guardar Orden Pendiente.
  14. +
+

3.5 Revisar órdenes de compra

+
    +
  1. Abre la sección Ver Órdenes de Compra.
  2. +
  3. Consulta el estado de cada orden: pendiente, aprobada, recibida, etc.
  4. +
  5. Haz clic en una orden para ver los productos solicitados y cantidades.
  6. +
+

3.6 Gestionar proveedores

+
    +
  1. Abre la sección Proveedores.
  2. +
  3. Registra un nuevo proveedor con sus datos de contacto.
  4. +
  5. Consulta, edita o elimina proveedores utilizando las opciones de acción disponibles.
  6. +
+ +

4. Administrador - Control completo del sistema

+

Este rol puede ver y administrar todas las secciones del sistema, incluida la configuración de canchas, gastos y reportes.

+

4.1 Registrar y administrar usuarios

+
    +
  1. Abre la sección Registrar Usuario.
  2. +
  3. Completa usuario, contraseña, nombre, apellido, DNI, teléfono y correo.
  4. +
  5. Selecciona el rol adecuado: Administrador, Trabajador Cantina o Trabajador Cancha.
  6. +
  7. Haz clic en Registrar Usuario.
  8. +
  9. En la tabla, utiliza las acciones para editar o eliminar usuarios existentes.
  10. +
+

4.2 Controlar gastos

+
    +
  1. Abre la sección Gastos.
  2. +
  3. Registra el monto, la descripción y la fecha del gasto.
  4. +
  5. Guarda el gasto para que se refleje en los reportes.
  6. +
+

4.3 Generar reportes

+
    +
  1. Abre la sección Reportes.
  2. +
  3. Selecciona el tipo de reporte: diario, semanal, mensual o anual.
  4. +
  5. Selecciona el período de fecha requerido.
  6. +
  7. Haz clic en Generar Reporte.
  8. +
  9. Revisa los datos de ventas, ingresos, egresos y ganancia neta.
  10. +
  11. Usa los botones Exportar a PDF o Exportar a Excel si necesitas descargar el reporte.
  12. +
+

4.4 Configurar canchas y horarios

+
    +
  1. Abre la sección Canchas (Configuración).
  2. +
  3. Configura los horarios de apertura y cierre para lunes a viernes, sábado y domingo.
  4. +
  5. Para cada tipo de cancha (Fútbol, Pádel, Quincho), ajusta: +
      +
    • Precio
    • +
    • Duración de turno
    • +
    • Cantidad de canchas o quinchos
    • +
    • Reservas necesarias para generar cupones
    • +
    • Porcentaje de descuento de cupones
    • +
    • Cantidad de faltas permitidas
    • +
    • Duración de los cupones en días
    • +
    +
  6. +
  7. Haz clic en Actualizar para guardar la configuración.
  8. +
+ +

5. Flujos recomendados

+

5.1 Flujo típico de Trabajador Cancha

+
    +
  1. Al comenzar el turno, abre Reserva y filtra por el día.
  2. +
  3. Verifica el estado de pago de las reservas próximas.
  4. +
  5. Recibe al cliente y confirma la reserva.
  6. +
  7. Si el cliente paga en el lugar, cambia el estado a Pagado.
  8. +
  9. Si el cliente no paga, deja la reserva en No Pagado y notifica al administrador si es necesario.
  10. +
  11. Si la reserva se cancela, actualiza el estado a Cancelado.
  12. +
+

5.2 Flujo típico de Trabajador Cantina

+
    +
  1. Antes de abrir, revisa el stock en Productos.
  2. +
  3. Registra ventas en Registrar Ventas durante el día.
  4. +
  5. Si falta stock, solicita una orden de compra desde Crear Orden de Compra.
  6. +
  7. Revisa y confirma proveedores desde Proveedores.
  8. +
  9. Al final del día, consulta Ver Ventas para ver el total y comparar con el efectivo.
  10. +
+

5.3 Flujo típico de Administrador

+
    +
  1. Revisa el panel y verifica usuarios activos.
  2. +
  3. Controla inventarios, proveedores y órdenes de compra.
  4. +
  5. Actualiza precios y horarios de canchas cuando sea necesario.
  6. +
  7. Genera reportes diarios o semanales para analizar ingresos y gastos.
  8. +
  9. Registra los gastos administrativos en la sección Gastos.
  10. +
+ +

6. Buenas prácticas

+
    +
  • Usa tu usuario personal, no compartas credenciales.
  • +
  • Siempre confirma la información antes de guardar cambios.
  • +
  • Revisa los reportes y los estados de reserva todos los días.
  • +
  • Mantén actualizados los proveedores y el stock para evitar faltantes.
  • +
  • Protege la contraseña y cierra sesión al terminar.
  • +
+
+ +
+

Registrar Usuario

+
+ +
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ +

Usuarios Registrados

+
+ + + + + + + + + + + + + +
UsuarioNombre CompletoDNIRolTeléfonoCorreoAcciones
+
+
+ +
+

Registrar Venta

+
+
+ + + + +
+
+

Lista de Venta Actual

+ + + + + + + + + + + + +
CódigoProductoPrecioCantidadSubTotalAcción
+ +
+ +
+

Ver Ventas

+
+ + +
+ Total del Día: + +
+
+ + +
+ + + + + + + + + + +
ID VentaHoraMonto TotalAcciones
+
+ + + +
+ +
+

Gestionar Productos

+
+
+ + + + +
+ +
+ +
+

Gestionar Stock

+
+ + + + Cuando el stock esté por debajo del límite se mostrará una alerta. +
+
+ +

Lista de Productos en Stock

+ + + + + + + + + + + + +
CódigoProductoPrecio VentaPrecio CompraStockAcción
+
+ +
+

Crear Orden de Compra

+
+
+ + + +
+
+ + + +
+
+ +

Lista de Productos a Pedir

+ + + + + + + + + + +
CódigoProductoCantidadAcción
+
+ +
+
+ +
+

Ver Órdenes de Compra

+ + + + + + + + + + + + + +
ID OrdenFechaID ProveedorProveedorTeléfonoEstadoAcciones
+ + +
+ +
+

Cargar Proveedor

+
+
+ + +
+ +
+ +

Lista de Proveedores

+ + + + + + + + + + +
CódigoProveedorTeléfonoAcción
+ + +
+ +
+

Gestión de Gastos

+

Agregar Nuevo Gasto

+
+
+ + + +
+
+ +

Historial de Gastos

+ + + + + + + + + +
DescripciónMontoAcciones
+
+ +
+

Generar Reportes

+
+
+ + +
+ +
+ + +
+ + + + + + + + + + + +
+ + +
+ +
+

Gestión de Reservas

+
+
+ + + + +
+
+ +
+
+ +

Listado de Reservas

+ + + + + + + + + + + + + + + +
NombreDNITeléfonoHorarioTipo CanchaN° CanchaDescuentoTotalEstado
+
+ +
+

Configuración de Canchas

+ +

Horarios del Complejo

+
+
+ + + +
+
+ + + +
+
+ + + +
+ +
+ +
+ +

Cancha de Fútbol

+
+
+
+
+ + + + + + +
+
+ + + + +
+
+ + + + +
+ +
+ +
+ +

Cancha de Pádel

+
+
+ + + + + + +
+
+ + + + +
+
+ + + + +
+ +
+ +
+ +

Quincho

+
+
+ + + + +
+
+ + +
+ +
+
+ +
+
+ + + + + + + + + + + + + + + + + diff --git a/html/Login.html b/html/Login.html new file mode 100644 index 0000000..8ad09a8 --- /dev/null +++ b/html/Login.html @@ -0,0 +1,272 @@ + + + + + + Document + + + +
+

Iniciar Sesión

+
+
+ +
+
+ +
+ + +
+
+ + + + + + + \ No newline at end of file diff --git a/html/Reserva_Clientes.html b/html/Reserva_Clientes.html new file mode 100644 index 0000000..3a29384 --- /dev/null +++ b/html/Reserva_Clientes.html @@ -0,0 +1,154 @@ + + + + + + Reservar Turno - Complejo de Canchas + + + +
+
+

Complejo Deportivo Cap1tan

+

Reserva tu turno ahora

+
+
+ +
+ +
+

Selecciona el tipo de cancha

+
+ + +
+
+ + + + + + + + + + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/js/Gestion_Canchas.js b/js/Gestion_Canchas.js new file mode 100644 index 0000000..2a9d8a8 --- /dev/null +++ b/js/Gestion_Canchas.js @@ -0,0 +1,168 @@ +document.addEventListener("DOMContentLoaded", () => { + cargarConfiguracionCanchas(); + cargarHorarios(); + + // Configurar el guardado para cada formulario + const formularios = document.querySelectorAll( + ".cancha-formulario[data-tipo]" + ); + + formularios.forEach((form) => { + form.addEventListener("submit", (e) => { + e.preventDefault(); + const T = form.dataset.tipo; // Obtiene 'F', 'P' o 'Q' + + // Capturamos los valores dinámicamente usando el prefijo del tipo + const datos = { + tipo: T, + precio: document.getElementById(`precio-${T}`)?.value || 0, + duracion: document.getElementById(`duracion-${T}`)?.value || 0, + cantidad: document.getElementById(`cantidad-${T}`)?.value || 0, + reservas_cupon: + document.getElementById(`reservas-cupon-${T}`)?.value || 0, + descuento: convertirDescuentoAPorcentaje( + document.getElementById(`descuento-${T}`)?.value || 0 + ), + faltas: document.getElementById(`faltas-${T}`)?.value || 0, + duracion_cupon: document.getElementById(`duracion-cupon-${T}`)?.value || 0, + }; + + // Caso especial para Quincho que llamaste al input "reservas-Q" en lugar de "reservas-cupon-Q" + if (T === "Q") { + datos.reservas_cupon = + document.getElementById(`reservas-Q`)?.value || 0; + } + actualizarCancha(datos); + }); + }); +}); + +// Guardar configuración de Horarios +const formHorarios = document.getElementById("form-horarios"); + if (formHorarios) { + formHorarios.addEventListener("submit", (e) => { + e.preventDefault(); + const datosHorarios = [ + { + dia: "Lunes a Viernes", + ape: document.getElementById("apertura-l").value, + cie: document.getElementById("cierre-l").value, + }, + { + dia: "Sábado", + ape: document.getElementById("apertura-s").value, + cie: document.getElementById("cierre-s").value, + }, + { + dia: "Domingo", + ape: document.getElementById("apertura-d").value, + cie: document.getElementById("cierre-d").value, + }, + ]; + actualizarHorarios(datosHorarios); + }); + } + +function cargarHorarios() { + console.log('Cargando horarios...'); + fetch("../php/Obtener_Horarios.php") + .then((res) => res.json()) + .then((data) => { + console.log('Datos de horarios recibidos:', data); + data.forEach((h) => { + if (h.Dia === "Lunes a Viernes") { + setVal("apertura-l", h.Hora_Apertura); + setVal("cierre-l", h.Hora_Cierre); + } else if (h.Dia === "Sábado") { + setVal("apertura-s", h.Hora_Apertura); + setVal("cierre-s", h.Hora_Cierre); + } else if (h.Dia === "Domingo") { + setVal("apertura-d", h.Hora_Apertura); + setVal("cierre-d", h.Hora_Cierre); + } + }); + console.log('Horarios cargados exitosamente'); + }) + .catch((err) => console.error("Error al cargar horarios:", err)); +} + +window.cargarHorarios = cargarHorarios; + +function normalizarNumero(valor) { + if (valor === null || valor === undefined || valor === "") return 0; + const texto = String(valor).trim().replace(/\s+/g, ""); + if (!texto) return 0; + const numero = parseFloat(texto.replace(/,/g, ".")); + return Number.isFinite(numero) ? numero : 0; +} + +function convertirDescuentoAPorcentaje(valor) { + const numero = normalizarNumero(valor); + if (numero === 0) return 0; + return numero > 1 ? numero / 100 : numero; +} + +function convertirDescuentoAFormulario(valor) { + const numero = normalizarNumero(valor); + if (numero === 0) return ""; + return numero > 1 ? numero : numero * 100; +} + +function actualizarHorarios(datos) { + fetch("../php/Actualizar_Horarios.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(datos), + }) + .then((res) => res.text()) + .then((msg) => alert(msg)) + .catch((err) => console.error("Error:", err)); +} + +function cargarConfiguracionCanchas() { + console.log('Cargando configuración de canchas...'); + fetch("../php/Obtener_Canchas.php") + .then((res) => res.json()) + .then((data) => { + console.log('Datos de canchas recibidos:', data); + data.forEach((cancha) => { + const T = cancha.Tipo; // 'F', 'P', 'Q' + + // Usamos el operador ?. para no romper el código si un input no existe en un tipo + setVal(`precio-${T}`, cancha.Precio); + setVal(`duracion-${T}`, cancha.Duracion); + setVal(`cantidad-${T}`, cancha.Cant_Canchas); + setVal(`reservas-cupon-${T}`, cancha.Cant_Reservas_Cupon); + setVal(`descuento-${T}`, convertirDescuentoAFormulario(cancha.Descuento_Cupon)); + setVal(`faltas-${T}`, cancha.Cant_Faltas); + setVal(`duracion-cupon-${T}`, cancha.Duracion_Cupon); + + // Ajuste para ID específico de Quincho + if (T === "Q") setVal(`reservas-Q`, cancha.Cant_Reservas_Cupon); + }); + console.log('Configuración de canchas cargada exitosamente'); + }) + .catch((err) => console.error("Error al cargar configuración:", err)); +} + +window.cargarConfiguracionCanchas = cargarConfiguracionCanchas; + +// Función auxiliar para asignar valores de forma segura +function setVal(id, valor) { + const el = document.getElementById(id); + if (el) el.value = valor; +} + +function actualizarCancha(datos) { + fetch("../php/Actualizar_Cancha.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(datos), + }) + .then((res) => res.text()) + .then((msg) => { + alert(msg); + cargarConfiguracionCanchas(); // Recargar para confirmar cambios + }) + .catch((err) => console.error("Error al actualizar:", err)); +} diff --git a/js/Gestion_Usuarios.js b/js/Gestion_Usuarios.js new file mode 100644 index 0000000..8f9dc80 --- /dev/null +++ b/js/Gestion_Usuarios.js @@ -0,0 +1,274 @@ +// Gestion de Usuarios +let usuarioEditando = null; + +document.addEventListener('DOMContentLoaded', function() { + console.log('DOM cargado - Inicializando Gestion_Usuarios.js'); + cargarUsuarios(); + + // Escuchar el envío del formulario + const formulario = document.getElementById('form-registrar-usuario'); + if (formulario) { + formulario.addEventListener('submit', function(e) { + e.preventDefault(); + console.log('Formulario enviado'); + if (usuarioEditando) { + actualizarUsuario(); + } else { + registrarUsuario(); + } + }); + } else { + console.error('No se encontró el formulario con ID form-registrar-usuario'); + } + + const btnCancelar = document.getElementById('btn-cancelar-edicion'); + btnCancelar.addEventListener('click', cancelarEdicion); +}); + +function cargarUsuarios() { + console.log('Cargando usuarios...'); + + fetch('../php/Obtener_Usuarios.php') + .then(response => { + console.log('Respuesta status:', response.status); + if (!response.ok) { + throw new Error('Error HTTP: ' + response.status); + } + return response.json(); + }) + .then(data => { + console.log('Datos recibidos:', data); + + if (data.success) { + const tbody = document.querySelector('#tabla-usuarios tbody'); + tbody.innerHTML = ''; + + if (data.usuarios.length === 0) { + tbody.innerHTML = 'No hay usuarios registrados'; + return; + } + + data.usuarios.forEach(usuario => { + const row = document.createElement('tr'); + row.dataset.user = JSON.stringify(usuario); + row.dataset.userId = usuario.ID_Usuario; + const esSupremo = usuario.ID_Usuario === 1; + const botonEliminar = esSupremo + ? `` + : ``; + row.innerHTML = ` + ${usuario.Usuario} + ${usuario.Nombre} ${usuario.Apellido} + ${usuario.DNI} + ${usuario.Rol === 'admin' ? 'Administrador' : usuario.Rol === 'cantina' ? 'Trabajador Cantina' : 'Trabajador Cancha'} + ${usuario.Telefono || '-'} + ${usuario.Correo || '-'} + + + + + `; + tbody.appendChild(row); + }); + } else { + console.error('Error al cargar usuarios:', data.message); + const tbody = document.querySelector('#tabla-usuarios tbody'); + tbody.innerHTML = 'Error al cargar usuarios'; + } + }) + .catch(error => { + console.error('Error en fetch:', error); + const tbody = document.querySelector('#tabla-usuarios tbody'); + tbody.innerHTML = 'Error de conexión'; + }); +} + +function registrarUsuario() { + const formulario = document.getElementById('form-registrar-usuario'); + const formData = new FormData(formulario); + const contraseña = formData.get('contraseña')?.toString().trim() || ''; + const confirmarContraseña = formData.get('confirmar_contraseña')?.toString().trim() || ''; + + // Validar que el rol esté seleccionado + if (!formData.get('rol')) { + alert('Por favor selecciona un rol'); + return; + } + + if (contraseña.length < 6) { + alert('La contraseña debe tener al menos 6 carácteres'); + return; + } + + if (contraseña !== confirmarContraseña) { + alert('Las contraseñas no coinciden'); + return; + } + + console.log('Registrando usuario:', { + usuario: formData.get('usuario'), + rol: formData.get('rol'), + nombre: formData.get('nombre') + }); + + fetch('../php/Guardar_Usuario.php', { + method: 'POST', + body: formData + }) + .then(response => { + console.log('Respuesta status:', response.status); + if (!response.ok) { + throw new Error('Error HTTP: ' + response.status); + } + return response.json(); + }) + .then(data => { + console.log('Respuesta del servidor:', data); + + if (data.success) { + alert('✅ Usuario registrado exitosamente'); + formulario.reset(); + cargarUsuarios(); + } else { + alert('❌ Error: ' + (data.message || 'No se pudo registrar el usuario')); + } + }) + .catch(error => { + console.error('Error en fetch:', error); + alert('❌ Error al registrar el usuario: ' + error.message); + }); +} + +function editarUsuario(id) { + console.log('Editando usuario con ID:', id); + + // Buscar la fila por el ID del usuario de forma más robusta + let row = null; + const filas = document.querySelectorAll('#tabla-usuarios tbody tr'); + + for (let fila of filas) { + if (fila.dataset.userId == id) { // Comparación flexible de tipos + row = fila; + break; + } + } + + if (!row || !row.dataset.user) { + console.error('No se encontró el usuario con ID:', id); + alert('❌ Error: No se pudo encontrar el usuario. Por favor recarga la página e intenta de nuevo.'); + return; + } + + try { + const usuario = JSON.parse(row.dataset.user); + + document.getElementById('usuario-id').value = usuario.ID_Usuario; + document.getElementById('usuario-username').value = usuario.Usuario; + document.getElementById('usuario-password').value = usuario.Contraseña; + document.getElementById('usuario-password-confirm').value = usuario.Contraseña; + document.querySelector('input[name="nombre"]').value = usuario.Nombre || ''; + document.querySelector('input[name="apellido"]').value = usuario.Apellido || ''; + document.querySelector('input[name="dni"]').value = usuario.DNI || ''; + document.querySelector('input[name="telefono"]').value = usuario.Telefono || ''; + document.querySelector('input[name="correo"]').value = usuario.Correo || ''; + document.querySelector('select[name="rol"]').value = usuario.Rol; + + usuarioEditando = id; + document.getElementById('btn-registrar-usuario').textContent = 'Guardar Cambios'; + document.getElementById('btn-cancelar-edicion').style.display = 'inline-block'; + + console.log('Usuario cargado para edición:', usuario.Usuario); + } catch (error) { + console.error('Error al parsear datos del usuario:', error); + alert('❌ Error: Los datos del usuario están corruptos. Por favor recarga la página.'); + } +} + +function cancelarEdicion() { + usuarioEditando = null; + document.getElementById('form-registrar-usuario').reset(); + document.getElementById('usuario-id').value = ''; + document.getElementById('btn-registrar-usuario').textContent = 'Registrar Usuario'; + document.getElementById('btn-cancelar-edicion').style.display = 'none'; +} + +function actualizarUsuario() { + const formulario = document.getElementById('form-registrar-usuario'); + const formData = new FormData(formulario); + const contraseña = formData.get('contraseña')?.toString().trim() || ''; + const confirmarContraseña = formData.get('confirmar_contraseña')?.toString().trim() || ''; + + if (!usuarioEditando) { + alert('No hay usuario seleccionado para editar'); + return; + } + + if (contraseña.length < 6) { + alert('La contraseña debe tener al menos 6 carácteres'); + return; + } + + if (contraseña !== confirmarContraseña) { + alert('Las contraseñas no coinciden'); + return; + } + + fetch('../php/Actualizar_Usuario.php', { + method: 'POST', + body: formData + }) + .then(response => { + if (!response.ok) { + throw new Error('Error HTTP: ' + response.status); + } + return response.json(); + }) + .then(data => { + if (data.success) { + alert('✅ Usuario actualizado exitosamente'); + cancelarEdicion(); + cargarUsuarios(); + } else { + alert('❌ Error: ' + (data.message || 'No se pudo actualizar el usuario')); + } + }) + .catch(error => { + console.error('Error en fetch:', error); + alert('❌ Error al actualizar el usuario: ' + error.message); + }); +} + +function eliminarUsuario(id) { + if (confirm('¿Estás seguro de que deseas eliminar este usuario? Esta acción no se puede deshacer.')) { + console.log('Eliminando usuario con ID:', id); + + fetch('../php/Eliminar_Usuario.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: 'id=' + id + }) + .then(response => { + console.log('Respuesta status:', response.status); + if (!response.ok) { + throw new Error('Error HTTP: ' + response.status); + } + return response.json(); + }) + .then(data => { + console.log('Respuesta del servidor:', data); + + if (data.success) { + alert('✅ Usuario eliminado exitosamente'); + cargarUsuarios(); + } else { + alert('❌ Error: ' + (data.message || 'No se pudo eliminar el usuario')); + } + }) + .catch(error => { + console.error('Error en fetch:', error); + alert('❌ Error al eliminar el usuario: ' + error.message); + }); + } +} \ No newline at end of file diff --git a/js/Orden_Compra.js b/js/Orden_Compra.js new file mode 100644 index 0000000..91e9817 --- /dev/null +++ b/js/Orden_Compra.js @@ -0,0 +1,429 @@ +const productosOrden = []; + +document.addEventListener("DOMContentLoaded", () => { + const codigoProveedor = document.getElementById("codigo-proveedor"); + const nombreProveedor = document.getElementById("nombre-proveedor"); + const telefonoProveedor = document.getElementById("telefono-proveedor"); + const codigoProducto = document.getElementById("codigo-producto"); + const cantidadProducto = document.getElementById("cantidad-producto"); + const btnAgregar = document.getElementById("btn-agregar-producto"); + const btnGenerar = document.getElementById("btn-generar-orden"); + const tabla = document.querySelector("#tabla-orden tbody"); + const tablaOrdenes = document.querySelector("#tabla-ordenes tbody"); + + let ordenDetalleActual = null; + let modoEdicionDetalle = false; + + if (codigoProveedor) { + codigoProveedor.addEventListener("blur", () => { + fetch(`../php/Buscar_Proveedor.php?codigo=${codigoProveedor.value.trim()}`) + .then((res) => res.json()) + .then((data) => { + if (data && data.Nombre && data.Telefono) { + nombreProveedor.value = data.Nombre; + telefonoProveedor.value = data.Telefono; + } else { + nombreProveedor.value = ""; + telefonoProveedor.value = ""; + alert("Proveedor no encontrado."); + } + }); + }); + } + + if (btnAgregar) { + btnAgregar.addEventListener("click", () => { + const codigo = codigoProducto.value.trim(); + const cantidad = parseInt(cantidadProducto.value, 10); + + if (!codigo || cantidad <= 0) { + alert("Código y cantidad válidos requeridos."); + return; + } + + fetch(`../php/Buscar_Producto_Orden.php?codigo=${codigo}`) + .then((res) => { + if (!res.ok) { + throw new Error("Error en la solicitud al servidor."); + } + return res.json(); + }) + .then((producto) => { + if (!producto || !producto.ID_Producto) { + alert("Producto no encontrado."); + return; + } + + productosOrden.push({ + id: producto.ID_Producto, + descripcion: producto.Descripcion, + cantidad: cantidad, + }); + + actualizarTabla(); + codigoProducto.value = ""; + cantidadProducto.value = ""; + }) + .catch((err) => { + console.error("Error al obtener el producto:", err); + }); + }); + } + + if (btnGenerar) { + btnGenerar.addEventListener("click", () => { + if (!codigoProveedor.value || productosOrden.length === 0) { + alert("Proveedor y al menos un producto son necesarios."); + return; + } + + fetch("../php/Registrar_Orden_Compra.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + proveedor: codigoProveedor.value, + productos: productosOrden, + }), + }) + .then((res) => res.text()) + .then((msg) => { + alert(msg); + productosOrden.length = 0; + actualizarTabla(); + document.getElementById("form-orden").reset(); + cargarOrdenesCompra(); + }); + }); + } + + function actualizarTabla() { + if (!tabla) return; + tabla.innerHTML = ""; + productosOrden.forEach((p, i) => { + const fila = document.createElement("tr"); + fila.innerHTML = ` + ${p.id} + ${p.descripcion} + ${p.cantidad} + + + + `; + tabla.appendChild(fila); + }); + } + + function cargarOrdenesCompra() { + if (!tablaOrdenes) return; + fetch("../php/Obtener_Ordenes_Compra.php") + .then((res) => res.json()) + .then((ordenes) => { + tablaOrdenes.innerHTML = ""; + ordenes.forEach((orden) => { + const fila = document.createElement("tr"); + const estadoNumerico = parseInt(orden.Estado, 10); + const estaProcesada = estadoNumerico !== 0; + + let textoEstado = "Pendiente"; + if (estadoNumerico === 1) textoEstado = "Aprobada"; + if (estadoNumerico === 2) textoEstado = "Denegada"; + if (estadoNumerico === 3) textoEstado = "Confirmada"; + + fila.innerHTML = ` + ${orden.ID_Orden} + ${orden.Fecha} + ${orden.ID_Proveedor || "N/A"} + ${orden.Proveedor || "Sin nombre"} + ${orden.Telefono || "Sin registrar"} + ${textoEstado} + + + + + + `; + tablaOrdenes.appendChild(fila); + }); + + tablaOrdenes.querySelectorAll(".btn-aprobar").forEach((btn) => { + btn.addEventListener("click", () => { + cambiarEstadoOrden(btn.dataset.id, true, btn.dataset.telefono, btn.dataset.proveedor); + }); + }); + + tablaOrdenes.querySelectorAll(".btn-denegar").forEach((btn) => { + btn.addEventListener("click", () => cambiarEstadoOrden(btn.dataset.id, false)); + }); + + tablaOrdenes.querySelectorAll(".btn-ver-orden").forEach((btn) => { + btn.addEventListener("click", () => verProductosOrden(btn.dataset.id, btn.dataset.estado)); + }); + }) + .catch((err) => { + console.error("Error al cargar órdenes de compra:", err); + }); + } + + function cambiarEstadoOrden(idOrden, esAprobacion, telefono = "", nombreProv = "") { + const nuevoEstado = esAprobacion ? 1 : 2; + + fetch("../php/Actualizar_Orden_Compra.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id: parseInt(idOrden, 10), estado: nuevoEstado }), + }) + .then(async (res) => { + const text = await res.text(); + if (!res.ok) { + throw new Error(text || `HTTP ${res.status}`); + } + try { + return JSON.parse(text); + } catch (jsonErr) { + throw new Error(`Respuesta inválida del servidor: ${text}`); + } + }) + .then((data) => { + if (data.success) { + if (esAprobacion && telefono) { + enviarOrdenPorWhatsApp(idOrden, telefono, nombreProv); + } + cargarOrdenesCompra(); + } else { + alert("Error: " + (data.error || "No se pudo actualizar")); + } + }) + .catch((err) => { + console.error("Error al actualizar la orden:", err); + alert("Error al actualizar la orden: " + err.message); + }); + } + + function enviarOrdenPorWhatsApp(idOrden, telefono, nombreProv) { + const telefonoLimpio = telefono.replace(/[^0-9]/g, ""); + + fetch(`../php/Obtener_Productos_Orden.php?id=${idOrden}`) + .then((res) => res.json()) + .then((productos) => { + let mensaje = `*Orden de Compra #${idOrden}*\n`; + mensaje += `Proveedor: ${nombreProv}\n`; + mensaje += `--------------------------\n`; + + productos.forEach((p) => { + mensaje += `- ${p.Cantidad} x ${p.Descripcion}\n`; + }); + + mensaje += `--------------------------\n`; + mensaje += `Favor de confirmar recepción.`; + + const mensajeUrl = encodeURIComponent(mensaje); + const urlWhatsApp = `https://web.whatsapp.com/send?phone=${telefonoLimpio}&text=${mensajeUrl}`; + + window.open(urlWhatsApp, "_blank"); + }) + .catch((err) => console.error("Error al obtener productos para WhatsApp:", err)); + } + + function renderDetalleOrden() { + const cuerpo = document.getElementById("productos-orden-body"); + if (!cuerpo || !ordenDetalleActual) { + return; + } + + cuerpo.innerHTML = ""; + + if (ordenDetalleActual.productos.length === 0) { + const filaVacia = document.createElement("tr"); + filaVacia.innerHTML = 'No hay productos registrados.'; + cuerpo.appendChild(filaVacia); + actualizarBotonesDetalleOrden(); + return; + } + + ordenDetalleActual.productos.forEach((prod) => { + const fila = document.createElement("tr"); + const cantidadActual = prod.Cantidad || 0; + const celdaCantidad = modoEdicionDetalle + ? `` + : `${cantidadActual}`; + + fila.innerHTML = ` + ${prod.Descripcion} + ${celdaCantidad} + `; + cuerpo.appendChild(fila); + }); + + actualizarBotonesDetalleOrden(); + } + + function actualizarBotonesDetalleOrden() { + const btnConfirmar = document.getElementById("btn-confirmar-recepcion"); + const btnEditar = document.getElementById("btn-editar-orden"); + const btnGuardar = document.getElementById("btn-guardar-orden"); + const btnCancelar = document.getElementById("btn-cancelar-orden"); + const btnCerrar = document.getElementById("cerrar-orden"); + + if (!btnConfirmar || !btnEditar || !btnGuardar || !btnCancelar || !btnCerrar) { + return; + } + + const estado = parseInt(ordenDetalleActual?.estado || 0, 10); + const esConfirmada = estado === 3; + const esBloqueada = estado === 2 || estado === 3; + + if (modoEdicionDetalle) { + btnConfirmar.style.display = "none"; + btnEditar.style.display = "none"; + btnCerrar.style.display = "none"; + btnGuardar.style.display = "inline-block"; + btnCancelar.style.display = "inline-block"; + } else { + btnConfirmar.style.display = "inline-block"; + btnEditar.style.display = "inline-block"; + btnCerrar.style.display = "inline-block"; + btnGuardar.style.display = "none"; + btnCancelar.style.display = "none"; + } + + btnConfirmar.disabled = esConfirmada || esBloqueada; + btnEditar.disabled = esConfirmada || esBloqueada; + } + + function verProductosOrden(idOrden, estadoActual = 0) { + fetch(`../php/Obtener_Productos_Orden.php?id=${idOrden}`) + .then((res) => res.json()) + .then((productos) => { + ordenDetalleActual = { + id: parseInt(idOrden, 10), + estado: parseInt(estadoActual, 10), + productos: productos || [], + }; + modoEdicionDetalle = false; + renderDetalleOrden(); + + document.getElementById("tabla-ordenes").style.display = "none"; + document.getElementById("productos-orden").style.display = "block"; + }) + .catch((err) => { + console.error("Error al cargar productos de la orden:", err); + }); + } + + async function confirmarRecepcionOrden() { + if (!ordenDetalleActual) return; + + try { + const res = await fetch("../php/Actualizar_Orden_Compra.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id: ordenDetalleActual.id, estado: 3 }), + }); + const data = await res.json(); + + if (data.success) { + ordenDetalleActual.estado = 3; + cargarOrdenesCompra(); + if (window.cargarProductos) { + window.cargarProductos(); + } + renderDetalleOrden(); + } else { + alert("Error: " + (data.error || "No se pudo confirmar la recepción")); + } + } catch (err) { + console.error("Error al confirmar recepción:", err); + } + } + + function activarEdicionDetalleOrden() { + modoEdicionDetalle = true; + renderDetalleOrden(); + } + + function cancelarEdicionDetalleOrden() { + modoEdicionDetalle = false; + renderDetalleOrden(); + } + + async function guardarEdicionDetalleOrden() { + if (!ordenDetalleActual) return; + + const productosActualizados = ordenDetalleActual.productos.map((prod) => { + const input = document.querySelector(`input[data-product-id="${prod.ID_Producto}"]`); + const cantidad = input ? parseInt(input.value, 10) : parseInt(prod.Cantidad, 10); + + if (!Number.isInteger(cantidad) || cantidad < 1) { + throw new Error(`La cantidad para ${prod.Descripcion} debe ser un número mayor a 0.`); + } + + return { + ...prod, + Cantidad: cantidad, + }; + }); + + try { + const res = await fetch("../php/Actualizar_Productos_Orden.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + id: ordenDetalleActual.id, + productos: productosActualizados.map((prod) => ({ + id: prod.ID_Producto, + cantidad: prod.Cantidad, + })), + }), + }); + const data = await res.json(); + + if (data.success) { + ordenDetalleActual.productos = productosActualizados; + modoEdicionDetalle = false; + renderDetalleOrden(); + } else { + alert("Error: " + (data.error || "No se pudieron guardar los cambios")); + } + } catch (err) { + console.error("Error al guardar edición:", err); + alert(err.message || "Ocurrió un error al guardar los cambios."); + } + } + + cargarOrdenesCompra(); + + document.getElementById("cerrar-orden").addEventListener("click", () => { + document.getElementById("tabla-ordenes").style.display = "table"; + document.getElementById("productos-orden").style.display = "none"; + ordenDetalleActual = null; + modoEdicionDetalle = false; + }); + + document.getElementById("btn-confirmar-recepcion").addEventListener("click", confirmarRecepcionOrden); + document.getElementById("btn-editar-orden").addEventListener("click", activarEdicionDetalleOrden); + document.getElementById("btn-guardar-orden").addEventListener("click", guardarEdicionDetalleOrden); + document.getElementById("btn-cancelar-orden").addEventListener("click", cancelarEdicionDetalleOrden); +}); + +function eliminarProductoOrden(index) { + productosOrden.splice(index, 1); + document.querySelector("#tabla-orden tbody").innerHTML = ""; + productosOrden.forEach((p, i) => { + const fila = document.createElement("tr"); + fila.innerHTML = ` + ${p.id} + ${p.descripcion} + ${p.cantidad} + + + + `; + document.querySelector("#tabla-orden tbody").appendChild(fila); + }); +} diff --git a/js/Reportes.js b/js/Reportes.js new file mode 100644 index 0000000..d39a94e --- /dev/null +++ b/js/Reportes.js @@ -0,0 +1,585 @@ +document.addEventListener('DOMContentLoaded', () => { + const tipoReporte = document.getElementById('tipo-reporte'); + const parametros = document.getElementById('parametros'); + const generarBtn = document.getElementById('generar-reporte'); + const exportarBtn = document.getElementById('exportar-pdf'); + const reporteContainer = document.getElementById('reporte-container'); + const reporteContenido = document.getElementById('reporte-contenido'); + const spinner = document.getElementById('spinner'); + const errorMessage = document.getElementById('error-message'); + + // Variable global para almacenar el período actual + let periodoActual = { + tipo: 'diario', + fecha: new Date().toISOString().split('T')[0], + nombreArchivo: 'reporte' + }; + + // ========== FUNCIONES AUXILIARES ========== + + function formatoMoneda(valor) { + return new Intl.NumberFormat('es-AR', { + style: 'currency', + currency: 'ARS' + }).format(valor); + } + + function obtenerSemanaActual() { + const hoy = new Date(); + const diaSemana = hoy.getDay(); + const lunes = new Date(hoy); + const diferenciaDias = diaSemana === 0 ? -6 : 1 - diaSemana; + lunes.setDate(hoy.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(); + + return `${ano}-W${String(numeroSemana).padStart(2, '0')}`; + } + + function mostrarSpinner() { + spinner.style.display = 'flex'; + } + + function ocultarSpinner() { + spinner.style.display = 'none'; + } + + function mostrarError(mensaje) { + errorMessage.textContent = mensaje; + errorMessage.style.display = 'block'; + } + + function ocultarError() { + errorMessage.style.display = 'none'; + } + + function generarNombresYMetadatos(tipo, data) { + let nombreArchivo = 'reporte'; + + if (tipo === 'diario') { + const fecha = new Date(data.periodo + 'T00:00:00'); + const fechaCorta = fecha.getDate().toString().padStart(2, '0') + '-' + + (fecha.getMonth() + 1).toString().padStart(2, '0') + '-' + + fecha.getFullYear().toString().slice(-2); + nombreArchivo = 'reporte_' + fechaCorta; + } 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().toString().padStart(2, '0'); + const diaFin = fin.getDate().toString().padStart(2, '0'); + const mesNombre = inicio.toLocaleDateString('es-ES', { month: 'long' }); + nombreArchivo = `reporte_semana_${diaInicio}-${diaFin}_${mesNombre}`; + } else if (tipo === 'mensual') { + const mesesNombres = ['', 'enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre']; + let nombreMes = 'mensual'; + let anio = new Date().getFullYear(); + if (data.nombre_mes) { + nombreMes = data.nombre_mes; + anio = data.nombre_anio || anio; + } else if (data.fecha_inicio) { + const f = new Date(data.fecha_inicio + 'T00:00:00'); + if (!isNaN(f.getTime())) { + nombreMes = f.toLocaleDateString('es-ES', { month: 'long' }); + anio = f.getFullYear(); + } + } else if (data.periodo && /^\d{4}-\d{2}$/.test(String(data.periodo))) { + const parts = String(data.periodo).split('-'); + nombreMes = mesesNombres[parseInt(parts[1] || '0')] || nombreMes; + anio = parts[0] || anio; + } + nombreArchivo = 'reporte_' + nombreMes + '_' + anio; + } else if (tipo === 'anual') { + const anio = data.periodo || data.anio || (data.fecha_inicio ? (new Date(data.fecha_inicio + 'T00:00:00')).getFullYear() : new Date().getFullYear()); + nombreArchivo = 'reporte_' + anio; + } + + periodoActual.tipo = tipo; + periodoActual.nombreArchivo = nombreArchivo; + } + + function renderizarDiario(data) { + const fecha = new Date(data.periodo + 'T00:00:00'); + const opciones = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }; + const fechaFormato = fecha.toLocaleDateString('es-ES', opciones); + const fechaCorta = fecha.getDate().toString().padStart(2, '0') + '-' + + (fecha.getMonth() + 1).toString().padStart(2, '0') + '-' + + fecha.getFullYear().toString().slice(-2); + + let html = ` +
+

📅 Reporte Diario ${fechaCorta} - Cierre de Caja

+
Reporte ${fechaFormato}
+
+ + + + + + + + + + + + + `; + + if (data.transacciones && data.transacciones.length > 0) { + data.transacciones.forEach(tx => { + const monto = parseFloat(tx.monto); + const claseNegativo = monto < 0 ? 'negativo' : ''; + html += ` + + + + + + + + `; + }); + } else { + html += ``; + } + + html += ` + +
HoraTipo de OperaciónDescripción / DetalleMonto ($)Usuario
${tx.hora || '--:--'}${tx.tipo_operacion}${tx.descripcion || '--'}${formatoMoneda(monto)}${tx.usuario_registro || 'Sistema'}
Sin transacciones registradas
+ +
+

Resumen del Día

+
+ Ingresos Reservas: + ${formatoMoneda(data.resumen.ingresos_reservas)} +
+
+ Ingresos Cantina: + ${formatoMoneda(data.resumen.ingresos_cantina)} +
+
+ Total Ingresos: + ${formatoMoneda(data.resumen.total_ingresos)} +
+
+ Egresos Totales: + -${formatoMoneda(data.resumen.egresos)} +
+
+
+ 💰 SALDO NETO: + ${formatoMoneda(data.resumen.saldo_neto)} +
+
+ `; + + reporteContenido.innerHTML = html; + } + + function renderizarSemanal(data) { + const inicio = new Date(data.fecha_inicio + 'T00:00:00'); + const fin = new Date(data.fecha_fin + 'T00:00:00'); + const opciones = { month: 'short', day: 'numeric' }; + const inicioCorta = inicio.getDate().toString().padStart(2, '0') + '-' + + (inicio.getMonth() + 1).toString().padStart(2, '0'); + const finCorta = fin.getDate().toString().padStart(2, '0') + '-' + + (fin.getMonth() + 1).toString().padStart(2, '0') + '-' + + fin.getFullYear().toString().slice(-2); + + let html = ` +
+

📆 Reporte Semanal ${inicioCorta} al ${finCorta} - Flujo de Caja

+
Reporte Semana del ${inicio.toLocaleDateString('es-ES', opciones)} al ${fin.toLocaleDateString('es-ES', opciones)}
+
+ + + + + + + + + + + + + `; + + if (data.dias && data.dias.length > 0) { + data.dias.forEach(dia => { + const nombreDia = new Date(dia.dia + 'T00:00:00').toLocaleDateString('es-ES', { weekday: 'long' }); + const saldoNeto = dia.saldo_neto; + const claseNegativo = saldoNeto < 0 ? 'negativo' : ''; + + html += ` + + + + + + + + `; + }); + } + + html += ` + +
Día de la SemanaIngresos Reservas ($)Ingresos Cantina ($)Costo Mercadería ($)Saldo Neto del Día ($)
${nombreDia.charAt(0).toUpperCase() + nombreDia.slice(1)} (${dia.dia})${formatoMoneda(dia.ingresos_reservas)}${formatoMoneda(dia.ingresos_cantina)}${formatoMoneda(dia.costo_mercaderia)}${formatoMoneda(saldoNeto)}
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Resumen de la Semana
Ingresos Reservas Total:${formatoMoneda(data.resumen.ingresos_reservas_totales)}
Ingresos Cantina Total:${formatoMoneda(data.resumen.ingresos_cantina_totales)}
Costo Mercadería Total:${formatoMoneda(data.resumen.costo_mercaderia_total)}
💰 Saldo Neto Semana:${formatoMoneda(data.resumen.saldo_neto_semana)}
+ +
+

% Ocupación Promedio

+
+ Turnos Ocupados: + ${data.resumen.turnos_ocupados} / ${data.resumen.turnos_disponibles} +
+
+ Porcentaje Ocupación: + ${data.resumen.ocupacion_promedio}% +
+
+ `; + + reporteContenido.innerHTML = html; + } + + function renderizarMensual(data) { + const fechaInicio = new Date(data.fecha_inicio + 'T00:00:00'); + const mesCorta = (fechaInicio.getMonth() + 1).toString().padStart(2, '0') + '-' + + fechaInicio.getFullYear().toString().slice(-2); + const nombreMes = data.nombre_mes || fechaInicio.toLocaleDateString('es-ES', { month: 'long' }); + + let html = ` +
+

📊 Reporte Mensual ${mesCorta} - Rentabilidad

+
Reporte de ${nombreMes.charAt(0).toUpperCase() + nombreMes.slice(1)}
+
+ +

Ingreso por Categoría

+ + + + + + + + + + `; + + if (data.categorias && data.categorias.length > 0) { + data.categorias.forEach(cat => { + html += ` + + + + + + `; + }); + } + + html += ` + +
Concepto / CategoríaOperacionesMonto ($)
${cat.categoria}${cat.operaciones}${formatoMoneda(cat.monto)}
+ +

Gastos Fijos

+ + + + + + + + + `; + + if (data.gastos_fijos && data.gastos_fijos.length > 0) { + data.gastos_fijos.forEach(gasto => { + html += ` + + + + + `; + }); + } else { + html += ` + + + + `; + } + + html += ` + +
Nombre del GastoMonto Gasto ($)
${gasto.descripcion}${formatoMoneda(gasto.monto)}
Sin gastos registrados
+ +
+

Estado de Resultados

+
+ Ingresos Totales: + ${formatoMoneda(data.resumen.ingresos_totales)} +
+
+ Costo de Mercadería: + -${formatoMoneda(data.resumen.costo_mercaderia)} +
+
+
+ Ganancia Bruta: + ${formatoMoneda(data.resumen.ganancia_bruta)} +
+
+ Gastos Fijos: + -${formatoMoneda(data.resumen.gastos_fijos_total)} +
+
+
+ 💰 GANANCIA NETA: + ${formatoMoneda(data.resumen.ganancia_neta)} +
+
+ `; + + reporteContenido.innerHTML = html; + } + + function renderizarAnual(data) { + let notaAnoActual = ''; + if (data.es_ano_actual) { + notaAnoActual = ` +
+ ℹ️ Nota: Este es el año en curso. El reporte muestra datos hasta el ${data.mes_actual}° mes (mes actual). +
+ `; + } + + let html = ` +
+

📈 Reporte Anual ${data.periodo} - Visión Estratégica

+
Reporte Año ${data.periodo}
+
+ ${notaAnoActual} + + + + + + + + + + + + + + `; + + if (data.meses && data.meses.length > 0) { + data.meses.forEach(mes => { + const gananciaNeta = parseFloat(mes.ganancia_neta); + const claseNegativo = gananciaNeta < 0 ? 'negativo' : ''; + + html += ` + + + + + + + + + `; + }); + } + + html += ` + +
MesIngreso Operativo ($)Costo Bruto ($)Costo Operativo ($)Ganancia Bruta ($)Ganancia Neta ($)
${mes.mes}${formatoMoneda(mes.ingreso_operativo)}${formatoMoneda(mes.costo_bruto)}${formatoMoneda(mes.costo_operativo)}${formatoMoneda(mes.ganancia_bruta)}${formatoMoneda(gananciaNeta)}
+ +
+

Totales Anuales

+
+ Ingreso Operativo Total: + ${formatoMoneda(data.totales.ingreso_operativo_total)} +
+
+ Costo Bruto Total: + -${formatoMoneda(data.totales.costo_bruto_total)} +
+
+ Costo Operativo Total: + -${formatoMoneda(data.totales.costo_operativo_total)} +
+
+
+ Ganancia Bruta Total: + ${formatoMoneda(data.totales.ganancia_bruta_total)} +
+
+
+ 💰 GANANCIA NETA ANUAL: + ${formatoMoneda(data.totales.ganancia_neta_total)} +
+
+ `; + + reporteContenido.innerHTML = html; + } + + // ========== INICIALIZACIÓN ========== + + // Establecer valores por defecto + const hoy = new Date(); + document.getElementById('fecha-diario').valueAsDate = hoy; + + const semanaActual = obtenerSemanaActual(); + document.getElementById('semana-input').value = semanaActual; + + const mesActual = hoy.toISOString().slice(0, 7); + document.getElementById('mes-input').value = mesActual; + + document.getElementById('anio-input').value = hoy.getFullYear(); + + // Cambiar parámetros según tipo de reporte + tipoReporte.addEventListener('change', () => { + document.querySelectorAll('.param-section').forEach(el => el.style.display = 'none'); + + const tipo = tipoReporte.value; + if (tipo === 'diario') { + document.getElementById('param-diario').style.display = 'block'; + } else if (tipo === 'semanal') { + document.getElementById('param-semanal').style.display = 'block'; + } else if (tipo === 'mensual') { + document.getElementById('param-mensual').style.display = 'block'; + } else if (tipo === 'anual') { + document.getElementById('param-anual').style.display = 'block'; + } + }); + + // Generar reporte + generarBtn.addEventListener('click', async () => { + const tipo = tipoReporte.value; + let url = '../php/Obtener_Reporte.php?tipo_reporte=' + tipo; + + // Agregar parámetros según el tipo + if (tipo === 'diario') { + const fecha = document.getElementById('fecha-diario').value; + if (!fecha) { + mostrarError('Por favor selecciona una fecha'); + return; + } + url += '&fecha=' + fecha; + } else if (tipo === 'semanal') { + const semana = document.getElementById('semana-input').value; + if (!semana) { + mostrarError('Por favor selecciona una semana'); + return; + } + url += '&semana=' + semana; + } else if (tipo === 'mensual') { + const mes = document.getElementById('mes-input').value; + if (!mes) { + mostrarError('Por favor selecciona un mes'); + return; + } + url += '&mes=' + mes; + } else if (tipo === 'anual') { + const anio = document.getElementById('anio-input').value; + if (!anio) { + mostrarError('Por favor selecciona un año'); + return; + } + url += '&anio=' + anio; + } + + ocultarError(); + mostrarSpinner(); + + try { + const response = await fetch(url); + const data = await response.json(); + + if (data.error) { + mostrarError(data.error); + ocultarSpinner(); + return; + } + + ocultarSpinner(); + + // Generar nombres de archivo + generarNombresYMetadatos(tipo, data); + + // Renderizar según tipo + if (tipo === 'diario') { + renderizarDiario(data); + } else if (tipo === 'semanal') { + renderizarSemanal(data); + } else if (tipo === 'mensual') { + renderizarMensual(data); + } else if (tipo === 'anual') { + renderizarAnual(data); + } + + reporteContainer.style.display = 'block'; + exportarBtn.style.display = 'inline-block'; + } catch (error) { + mostrarError('Error al cargar el reporte: ' + error.message); + ocultarSpinner(); + } + }); + + // Exportar a PDF + exportarBtn.addEventListener('click', () => { + const elemento = document.getElementById('reporte-contenido'); + const nombreArchivo = periodoActual.nombreArchivo; + + const opciones = { + margin: 10, + filename: nombreArchivo + '.pdf', + image: { type: 'jpeg', quality: 0.98 }, + html2canvas: { scale: 2, useCORS: true }, + jsPDF: { orientation: 'portrait', unit: 'mm', format: 'a4', compress: true } + }; + + html2pdf() + .set(opciones) + .from(elemento) + .save(nombreArchivo + '.pdf'); + }); +}); diff --git a/js/Reservar_Turno.js b/js/Reservar_Turno.js new file mode 100644 index 0000000..49b96dd --- /dev/null +++ b/js/Reservar_Turno.js @@ -0,0 +1,621 @@ +// Estado global de la reserva +let estadoReserva = { + tipoCancha: null, + fecha: null, + horario: null, + numeroCancha: null, + quincho: false, + descuento: 0, + cuponeado: false, + reservasRestantes: null, + cantidadReservas: 0, + umbral: 0 +}; + +// Obtener datos de las canchas al cargar +let datoscCanchas = { futbol: null, padel: null, quincho: null }; + +// Variable global para guardar el código de WhatsApp temporalmente +let codigoCorrecto = null; + +// Cargar datos de las canchas desde la base de datos +async function cargarDatosCanchas() { + try { + const response = await fetch('../php/Obtener_Canchas.php'); + const data = await response.json(); + console.log('Datos recibidos de Obtener_Canchas.php:', data); + + const canchas = Array.isArray(data) + ? data + : (data && Array.isArray(data.canchas) ? data.canchas : []); + + if (!canchas.length) { + console.warn('No se recibieron canchas válidas desde el servidor:', data); + return; + } + + canchas.forEach(cancha => { + const rawTipo = String(cancha.Tipo ?? cancha.tipo ?? '').trim().toUpperCase(); + const tipo = rawTipo === 'F' || rawTipo === 'FUTBOL' ? 'F' + : rawTipo === 'P' || rawTipo === 'PADEL' ? 'P' + : rawTipo === 'Q' || rawTipo === 'QUINCHO' ? 'Q' + : null; + + if (!tipo) { + console.warn('Tipo de cancha desconocido en la respuesta:', rawTipo, cancha); + return; + } + + const duracion = parseInt(cancha.Duracion ?? cancha.duracion, 10) || 0; + const precio = parseFloat(cancha.Precio ?? cancha.precio_venta ?? cancha.precio) || 0; + const apertura = cancha.Hora_Apertura ?? cancha.apertura ?? null; + const cierre = cancha.Hora_Cierre ?? cancha.cierre ?? null; + const cantidad = parseInt(cancha.Cant_Canchas ?? cancha.cantidad, 10) || 0; + const reservas = parseInt(cancha.Cant_Reservas_Cupon ?? cancha.reservas, 10) || 0; + const descuento = parseFloat(cancha.Descuento_Cupon ?? cancha.descuento_cupon) || 0; + const maxFaltas = parseInt(cancha.Cant_Faltas ?? cancha.cant_faltas, 10) || 0; + const duracionCupon = parseInt(cancha.Duracion_Cupon ?? cancha.duracion_cupon, 10) || 0; + + if (tipo === 'F') { + datoscCanchas.futbol = { + duracion, + precio, + apertura, + cierre, + cantidad, + reservasParaCupon: reservas, + descuentoCupon: descuento, + maxFaltas, + duracionCupon + }; + } else if (tipo === 'P') { + datoscCanchas.padel = { + duracion, + precio, + apertura, + cierre, + cantidad, + reservasParaCupon: reservas, + descuentoCupon: descuento, + maxFaltas, + duracionCupon + }; + } else if (tipo === 'Q') { + datoscCanchas.quincho = { + precio, + reservasMax: reservas + }; + } + }); + + console.log('Datos de canchas cargados:', datoscCanchas); + } catch (error) { + console.error('Error al cargar datos de canchas desde BD:', error); + } +} + +// Inicializar la interfaz +function inicializarReserva() { + const botonesCancha = document.querySelectorAll('.btn-cancha-tipo'); + + botonesCancha.forEach(btn => { + btn.addEventListener('click', (e) => { + botonesCancha.forEach(b => b.classList.remove('activo')); + btn.classList.add('activo'); + estadoReserva.tipoCancha = btn.dataset.tipo; + estadoReserva.fecha = null; + estadoReserva.horario = null; + + document.querySelector('.selector-fecha').style.display = 'block'; + document.querySelector('.selector-horario').style.display = 'none'; + document.querySelector('.datos-cliente').style.display = 'none'; + document.querySelector('.resumen-reserva').style.display = 'none'; + + generarDiasDisponibles(); + }); + }); + + // Botón volver + document.querySelector('.btn-volver').addEventListener('click', () => { + document.querySelector('.resumen-reserva').style.display = 'none'; + }); + + // --- LÓGICA DE DATOS DEL CLIENTE --- + document.querySelector('.btn-verificar').addEventListener('click', async () => { + const nombre = document.getElementById('cliente-nombre').value.trim(); + const telefono = document.getElementById('cliente-telefono').value.trim(); + const dni = document.getElementById('cliente-dni').value.trim(); + + if (!nombre || !telefono || !dni) { + alert('Por favor completa todos tus datos antes de continuar'); + return; + } + + if (!estadoReserva.numeroCancha) { + alert('Selecciona primero el número de cancha disponible.'); + return; + } + + await obtenerEstadoCupon(dni, estadoReserva.tipoCancha); + actualizarResumen(); + document.querySelector('.resumen-reserva').style.display = 'block'; + }); + + // Botón "Confirmar Reserva" (Pide el código) + const btnSolicitar = document.getElementById('btn-solicitar-reserva'); + if (btnSolicitar) { + btnSolicitar.addEventListener('click', async () => { + await solicitarCodigoWhatsApp(); + }); + } + + // Botón "Validar y Reservar" (Verifica el código) + const btnVerificarWA = document.getElementById('btn-verificar-codigo'); + if (btnVerificarWA) { + btnVerificarWA.addEventListener('click', async () => { + const codigoIngresado = document.getElementById('codigo-verificacion').value; + if (codigoIngresado === codigoCorrecto) { + await guardarReservaFinal(); + } else { + alert("Código incorrecto. Por favor, inténtelo de nuevo."); + } + }); + } + + // Checkbox quincho + document.getElementById('incluir-quincho').addEventListener('change', (e) => { + estadoReserva.quincho = e.target.checked; + const precioQuincho = datoscCanchas.quincho?.precio || 0; + + if (e.target.checked && precioQuincho > 0) { + document.querySelector('.precio-quincho').style.display = 'inline-block'; + document.getElementById('quincho-reservas').style.display = 'block'; + document.getElementById('precio-quincho-val').textContent = precioQuincho.toFixed(2); + } else { + estadoReserva.quincho = false; + document.querySelector('.precio-quincho').style.display = 'none'; + document.getElementById('quincho-reservas').style.display = 'none'; + e.target.checked = false; + } + actualizarPrecioTotal(); + }); + + const formComprobante = document.getElementById('form-comprobante'); + if (formComprobante) { + formComprobante.addEventListener('submit', async (e) => { + e.preventDefault(); + await enviarComprobantePago(); + }); + } +} + +// Generar días disponibles +function generarDiasDisponibles() { + const contenedor = document.getElementById('dias-disponibles'); + contenedor.innerHTML = ''; + const hoy = new Date(); + const diasSemana = ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sab']; + + for (let i = 0; i < 7; i++) { + const fecha = new Date(hoy); + fecha.setDate(fecha.getDate() + i); + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'btn-dia'; + + btn.innerHTML = `
${diasSemana[fecha.getDay()]}
${fecha.getDate()}/${fecha.getMonth() + 1}
`; + btn.dataset.fecha = fecha.toISOString().split('T')[0]; + + btn.addEventListener('click', async () => { + document.querySelectorAll('.btn-dia').forEach(b => b.classList.remove('activo')); + btn.classList.add('activo'); + estadoReserva.fecha = btn.dataset.fecha; + estadoReserva.horario = null; + + generarHorarios(); + await actualizarDisponibilidadQuincho(btn.dataset.fecha); + document.querySelector('.selector-horario').style.display = 'block'; + document.querySelector('.datos-cliente').style.display = 'none'; + document.querySelector('.resumen-reserva').style.display = 'none'; + }); + contenedor.appendChild(btn); + } +} + +// Obtener disponibilidad de quinchos +async function obtenerDisponibilidadQuincho(fecha) { + try { + const response = await fetch(`../php/Obtener_Disponibilidad_Quincho.php?fecha=${fecha}`); + const data = await response.json(); + return data.success ? data.disponibles : 0; + } catch (error) { return 0; } +} + +// Actualizar disponibilidad de quincho en UI +async function actualizarDisponibilidadQuincho(fecha) { + const disponibles = await obtenerDisponibilidadQuincho(fecha); + const checkboxQuincho = document.getElementById('incluir-quincho'); + const quinchoInfo = document.getElementById('quincho-reservas'); + const quinchoAlerta = document.getElementById('quincho-no-disponible'); + + document.getElementById('quincho-disp').textContent = disponibles; + + if (disponibles === 0) { + checkboxQuincho.disabled = true; + checkboxQuincho.checked = false; + estadoReserva.quincho = false; + quinchoInfo.style.display = 'none'; + quinchoAlerta.style.display = 'block'; + document.querySelector('.precio-quincho').style.display = 'none'; + } else { + checkboxQuincho.disabled = false; + quinchoAlerta.style.display = 'none'; + quinchoInfo.style.display = 'block'; + } + actualizarPrecioTotal(); +} + +// Obtener disponibilidad de cancha +async function obtenerDisponibilidadCancha(fecha, horario, duracion, tipo) { + try { + const response = await fetch(`../php/Obtener_Disponibilidad_Cancha.php?fecha=${fecha}&horario=${horario}&duracion=${duracion}&tipo=${tipo === 'futbol' ? 'F' : 'P'}`); + const data = await response.json(); + return data.success ? data.disponibles : 0; + } catch (error) { return 0; } +} + +// Obtener números de canchas +async function obtenerNumerosCanchas(fecha, horario, duracion, tipo) { + try { + const response = await fetch(`../php/Obtener_Canchas_Numeros.php?fecha=${fecha}&horario=${horario}&duracion=${duracion}&tipo=${tipo === 'futbol' ? 'F' : 'P'}`); + const data = await response.json(); + return data.success ? data.canchas : []; + } catch (error) { return []; } +} + +// Generar selección de número de cancha +async function generarSeleccionNumeroCancha(horarioInicio) { + const contenedor = document.getElementById('botones-canchas-numeros'); + contenedor.innerHTML = ''; + const tipo = estadoReserva.tipoCancha; + const canchas = await obtenerNumerosCanchas(estadoReserva.fecha, horarioInicio, datoscCanchas[tipo].duracion, tipo); + + canchas.forEach(cancha => { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'btn-numero-cancha'; + btn.textContent = `Cancha ${cancha.numero}`; + btn.dataset.numero = cancha.numero; + + if (!cancha.disponible) { + btn.disabled = true; + btn.classList.add('no-disponible'); + btn.style.opacity = '0.5'; + btn.style.cursor = 'not-allowed'; + } else { + btn.addEventListener('click', () => { + document.querySelectorAll('.btn-numero-cancha').forEach(b => b.classList.remove('activo')); + btn.classList.add('activo'); + estadoReserva.numeroCancha = cancha.numero; + + document.querySelector('.datos-cliente').style.display = 'block'; + }); + } + contenedor.appendChild(btn); + }); +} + +// Generar horarios +async function generarHorarios() { + const contenedor = document.getElementById('grid-horarios'); + contenedor.innerHTML = ''; + + const tipo = estadoReserva.tipoCancha; + const datos = datoscCanchas[tipo]; + + if (!datos || typeof datos.duracion !== 'number' || !datos.apertura || !datos.cierre) { + console.error('No hay datos de configuración válidos para la cancha seleccionada:', tipo, datos); + alert('Error al cargar los horarios. Revisa la configuración de la cancha.'); + return; + } + + const duracion = datos.duracion; + + const [aperturaH, aperturaM] = datos.apertura.split(':').map(Number); + const [cierreH, cierreM] = datos.cierre.split(':').map(Number); + const aperturaTotalMin = aperturaH * 60 + aperturaM; + const cierreTotalMin = cierreH * 60 + cierreM; + + const ahora = new Date(); + const [selYear, selMonth, selDay] = estadoReserva.fecha.split('-').map(Number); + const fechaSeleccionada = new Date(selYear, selMonth - 1, selDay); + const hoy = new Date(ahora.getFullYear(), ahora.getMonth(), ahora.getDate()); + const fechaSelSoloFecha = new Date(fechaSeleccionada.getFullYear(), fechaSeleccionada.getMonth(), fechaSeleccionada.getDate()); + const esHoy = fechaSelSoloFecha.getTime() === hoy.getTime(); + const minutoActual = ahora.getHours() * 60 + ahora.getMinutes(); + + let horaActualMin = aperturaTotalMin; + const horarios = []; + + while (horaActualMin + duracion <= cierreTotalMin) { + const horas = Math.floor(horaActualMin / 60); + const minutos = horaActualMin % 60; + const horaFin = Math.floor((horaActualMin + duracion) / 60); + const minutesFin = (horaActualMin + duracion) % 60; + + horarios.push({ + inicio: `${String(horas).padStart(2, '0')}:${String(minutos).padStart(2, '0')}`, + fin: `${String(horaFin).padStart(2, '0')}:${String(minutesFin).padStart(2, '0')}`, + minutos: horaActualMin + }); + horaActualMin += duracion; + } + + for (const horario of horarios) { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'horario-slot'; + btn.textContent = `${horario.inicio}\n-\n${horario.fin}`; + + const inicioArr = horario.inicio.split(':').map(Number); + const inicioMin = inicioArr[0] * 60 + inicioArr[1]; + const esPasado = fechaSelSoloFecha.getTime() < hoy.getTime() || (esHoy && inicioMin < minutoActual); + + let disponibles = 0; + if (!esPasado) { + disponibles = await obtenerDisponibilidadCancha(estadoReserva.fecha, genericNormalizeTipo(horario.inicio), duracion, tipo); + } + + if (esPasado || disponibles === 0) { + btn.disabled = true; + btn.classList.add('no-disponible'); + btn.style.opacity = '0.5'; + btn.style.cursor = 'not-allowed'; + } else { + btn.addEventListener('click', async () => { + document.querySelectorAll('.horario-slot').forEach(b => b.classList.remove('activo')); + btn.classList.add('activo'); + estadoReserva.horario = `${horario.inicio} - ${horario.fin}`; + estadoReserva.numeroCancha = null; + + document.getElementById('incluir-quincho').checked = false; + estadoReserva.quincho = false; + document.querySelector('.precio-quincho').style.display = 'none'; + document.getElementById('quincho-reservas').style.display = 'none'; + + await generarSeleccionNumeroCancha(horario.inicio); + document.querySelector('.selector-numero-cancha').style.display = 'block'; + document.querySelector('.datos-cliente').style.display = 'none'; + document.querySelector('.resumen-reserva').style.display = 'none'; + }); + } + contenedor.appendChild(btn); + } +} + +function genericNormalizeTipo(inicio) { + return inicio; +} + +// Actualizar resumen +function actualizarResumen() { + const tipo = estadoReserva.tipoCancha; + const datos = datoscCanchas[tipo]; + + document.getElementById('res-cancha').textContent = `${tipo === 'futbol' ? 'Fútbol' : 'Pádel'} - Cancha ${estadoReserva.numeroCancha}`; + document.getElementById('res-fecha').textContent = new Date(estadoReserva.fecha).toLocaleDateString('es-AR', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); + document.getElementById('res-horario').textContent = estadoReserva.horario; + + const duracionHoras = Math.floor(datos.duracion / 60); + const duracionMinutos = datos.duracion % 60; + document.getElementById('res-duracion').textContent = duracionHoras > 0 ? (duracionMinutos > 0 ? `${duracionHoras}h ${duracionMinutos}m` : `${duracionHoras}h`) : `${duracionMinutos}m`; + + if (estadoReserva.cuponeado) { + document.getElementById('res-cupon').textContent = `Cupón activo: ${(estadoReserva.descuento * 100).toFixed(0)}% aplicado`; + } else if (estadoReserva.reservasRestantes !== null) { + document.getElementById('res-cupon').textContent = estadoReserva.reservasRestantes > 0 ? `Faltan ${estadoReserva.reservasRestantes} reserva(s) para cupón` : 'Sin cupón activo'; + } else { + document.getElementById('res-cupon').textContent = '-'; + } + + actualizarPrecioTotal(); +} + +function actualizarPrecioTotal() { + const tipo = estadoReserva.tipoCancha; + const datos = datoscCanchas[tipo]; + + if (!datos || typeof datos.precio !== 'number') { + console.error('No se puede actualizar el precio porque no se cargaron los datos de cancha:', tipo, datos); + return; + } + + const basePrecio = datos.precio; + const descuentoMonto = basePrecio * (estadoReserva.descuento || 0); + const precioCancha = basePrecio - descuentoMonto; + const totalPrecio = precioCancha + (estadoReserva.quincho ? (datoscCanchas.quincho?.precio || 0) : 0); + + const precioOriginalElement = document.getElementById('precio-original'); + const precioOriginalValor = document.getElementById('res-precio-original'); + const labelPrecio = document.getElementById('label-precio'); + + if (estadoReserva.descuento > 0) { + // Mostrar precio original tachado + precioOriginalValor.textContent = `$${basePrecio.toFixed(2)}`; + precioOriginalElement.style.display = 'block'; + labelPrecio.textContent = 'Precio Final:'; + } else { + // Ocultar precio original + precioOriginalElement.style.display = 'none'; + labelPrecio.textContent = 'Precio:'; + } + + document.getElementById('res-precio').textContent = `$${totalPrecio.toFixed(2)}`; +} + +async function obtenerEstadoCupon(dni, tipo) { + try { + const response = await fetch(`../php/Obtener_Cupon.php?dni=${encodeURIComponent(dni)}&tipo=${encodeURIComponent(tipo)}`); + const data = await response.json(); + + if (!data.success) { + throw new Error(data.message || 'No se pudo obtener el estado del cupón'); + } + + estadoReserva.cuponeado = data.aplicarDescuento; + estadoReserva.descuento = data.aplicarDescuento ? parseFloat(data.descuentoCanchas) : 0; + estadoReserva.cantidadReservas = data.cantidadReservas; + estadoReserva.umbral = data.umbral; + estadoReserva.reservasRestantes = data.reservasRestantes; + + if (!estadoReserva.cuponeado && data.reservasRestantes === 0 && data.umbral === 0) { + estadoReserva.reservasRestantes = null; + } + + return data; + } catch (error) { + console.error('Error al obtener estado de cupón:', error); + alert('No se pudo determinar el estado del cupón. Intenta nuevamente.'); + estadoReserva.cuponeado = false; + estadoReserva.descuento = 0; + estadoReserva.reservasRestantes = null; + return null; + } +} + +// --- FUNCIONES DE WHATSAPP Y GUARDADO --- + +async function solicitarCodigoWhatsApp() { + const nombre = document.getElementById('cliente-nombre').value.trim(); + const telefono = document.getElementById('cliente-telefono').value.trim(); + + const btnSolicitar = document.getElementById('btn-solicitar-reserva'); + btnSolicitar.disabled = true; + btnSolicitar.textContent = 'Enviando código...'; + + try { + const response = await fetch('../php/Enviar_Codigo_WSP.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ telefono: telefono, nombre: nombre }) + }); + + const data = await response.json(); + + if (data.success) { + codigoCorrecto = data.codigo; + + // Ocultar botones de Confirmar/Volver y mostrar campo de validación + document.querySelector('.botones-reserva').style.display = 'none'; + document.getElementById('lbl-telefono').textContent = telefono; + document.getElementById('seccion-verificacion').style.display = 'block'; + + console.log('Código enviado a Selenium:', data.codigo); + } else { + alert('Error al enviar el mensaje de WhatsApp.'); + btnSolicitar.disabled = false; + btnSolicitar.textContent = 'Confirmar Reserva'; + } + } catch (error) { + console.error('Error:', error); + alert('Error de conexión con el servidor.'); + btnSolicitar.disabled = false; + btnSolicitar.textContent = 'Confirmar Reserva'; + } +} + +async function guardarReservaFinal() { + const tipo = estadoReserva.tipoCancha; + const datos = { + nombre: document.getElementById('cliente-nombre').value.trim(), + telefono: document.getElementById('cliente-telefono').value.trim(), + dni: document.getElementById('cliente-dni').value.trim(), + fecha: estadoReserva.fecha, + horario: estadoReserva.horario.split(' - ')[0], + tipo: tipo === 'futbol' ? 'F' : 'P', + numeroCancha: estadoReserva.numeroCancha, + monto: datoscCanchas[tipo].precio, + incluirQuincho: estadoReserva.quincho, + montoQuincho: estadoReserva.quincho ? datoscCanchas.quincho.precio : 0 + }; + + try { + const response = await fetch('../php/Guardar_Reserva.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(datos) + }); + + const result = await response.json(); + + if (result.success) { + alert('¡Reserva confirmada exitosamente!'); + + // ADAPTACIÓN: Ocultamos la sección de reserva en vez de refrescar la pantalla + document.getElementById('seccion-verificacion').style.display = 'none'; + document.querySelector('.resumen-reserva').style.display = 'none'; + + // Cargamos el ID retornado por tu servidor en el input oculto del formulario + // Nota: Tu php/Guardar_Reserva.php debe retornar un campo id_reserva (ej: result.id_reserva) + const idNuevaReserva = result.id_reserva || result.id || 0; + document.getElementById('comprobante-id-reserva').value = idNuevaReserva; + + // Hacemos visible la ventana de carga para la imagen + document.getElementById('seccion-comprobante').style.display = 'block'; + } else { + alert('Error al guardar: ' + result.message); + } + } catch (error) { + console.error('Error:', error); + alert('Ocurrió un error crítico al intentar guardar la reserva.'); + } +} + +// --- FUNCIÓN ASÍNCRONA PARA SUBIR EL COMPROBANTE DE PAGO --- +async function enviarComprobantePago() { + const idReserva = document.getElementById('comprobante-id-reserva').value; + const fileInput = document.getElementById('input-file-comprobante'); + const btnSubir = document.querySelector('#form-comprobante button[type="submit"]'); + + if (fileInput.files.length === 0) { + alert("Por favor, selecciona una foto de tu comprobante."); + return; + } + + btnSubir.disabled = true; + btnSubir.textContent = "Subiendo archivo..."; + + // Construimos el FormData binario + const formData = new FormData(); + formData.append('id_reserva', idReserva); + formData.append('comprobante', fileInput.files[0]); + + try { + const response = await fetch('../php/Subir_Comprobante.php', { + method: 'POST', + body: formData // El navegador setea automáticamente el multipart/form-data + }); + + const data = await response.json(); + + if (data.success) { + alert("¡Comprobante enviado con éxito! El administrador verificará su pago en la brevedad."); + location.reload(); // Ahora sí, reiniciamos la vista limpia del complejo + } else { + alert("Error: " + data.error); + btnSubir.disabled = false; + btnSubir.textContent = "Enviar Comprobante"; + } + } catch (error) { + console.error("Error al subir el comprobante:", error); + alert("Error de conexión al procesar el archivo."); + btnSubir.disabled = false; + btnSubir.textContent = "Enviar Comprobante"; + } +} + +// Inicializar cuando el DOM esté listo +document.addEventListener('DOMContentLoaded', async () => { + await cargarDatosCanchas(); + inicializarReserva(); +}); \ No newline at end of file diff --git a/js/Resumen_Ventas.js b/js/Resumen_Ventas.js new file mode 100644 index 0000000..24aa84d --- /dev/null +++ b/js/Resumen_Ventas.js @@ -0,0 +1,208 @@ +document.addEventListener('DOMContentLoaded', () => { + const fechaInput = document.getElementById('fecha-resumen'); + const totalVerInput = document.getElementById('total-ver'); + const tablaVentas = document.getElementById('tabla-ver-ventas'); + const tablaDetalle = document.getElementById('tabla-detalle-venta'); + const detalleContenedor = document.getElementById('detalle-venta-contenedor'); + const tablaPrincipal = document.getElementById('tabla-principal-contenedor'); + + // --- Estado de la aplicación --- + let state = { + vistaActual: 'principal', // 'principal' o 'detalle' + ventaSeleccionada: null, + ventasDelDia: [] + }; + + // --- Funciones de utilidad --- + function formatMoney(v) { + return '$' + Number(v).toFixed(2); + } + + function formatDate(dateString) { + const date = new Date(dateString); + return date.toLocaleString('es-AR'); + } + + // --- Función para cambiar vistas --- + function mostrarVistaPrincipal() { + state.vistaActual = 'principal'; + state.ventaSeleccionada = null; + if (tablaPrincipal) tablaPrincipal.style.display = 'block'; + if (detalleContenedor) detalleContenedor.style.display = 'none'; + } + + function mostrarVistaDetalle(idVenta) { + state.vistaActual = 'detalle'; + state.ventaSeleccionada = idVenta; + if (tablaPrincipal) tablaPrincipal.style.display = 'none'; + if (detalleContenedor) detalleContenedor.style.display = 'block'; + } + + // --- Carga de Ventas --- + function cargarVentasActuales() { + const fechaSeleccionada = fechaInput.value; + if (fechaSeleccionada) { + console.log("Actualizando vista de ventas para:", fechaSeleccionada); + fetchVentas({ start: fechaSeleccionada, end: fechaSeleccionada }); + } + } + + function fetchVentas(params) { + const query = new URLSearchParams(params).toString(); + fetch(`../php/Obtener_Ventas.php?${query}`) + .then(res => res.json()) + .then(data => { + state.ventasDelDia = data || []; + renderTablaVentas(data); + calcularTotalDia(data); + console.log('Ventas cargadas:', data); + }) + .catch(err => { + console.error('Error al cargar ventas:', err); + if (tablaVentas) { + tablaVentas.querySelector('tbody').innerHTML = + 'Error al cargar ventas'; + } + }); + } + + function renderTablaVentas(ventas) { + if (!tablaVentas) return; + + const tbody = tablaVentas.querySelector('tbody'); + tbody.innerHTML = ''; + + if (!ventas || ventas.length === 0) { + tbody.innerHTML = 'No hay ventas en el período seleccionado'; + return; + } + + ventas.forEach(venta => { + const row = document.createElement('tr'); + row.innerHTML = ` + ${venta.ID_Venta || '-'} + ${venta.fecha_hora ? formatDate(venta.fecha_hora) : '-'} + ${formatMoney(venta.monto_total)} + + + + `; + tbody.appendChild(row); + }); + } + + function calcularTotalDia(ventas) { + let total = 0; + if (ventas && Array.isArray(ventas) && ventas.length > 0) { + total = ventas.reduce((sum, v) => sum + Number(v.monto_total || 0), 0); + } + if (totalVerInput) totalVerInput.value = formatMoney(total); + } + + // --- Funcionalidad de Detalle --- + function cargarDetalleVenta(idVenta) { + console.log('Cargando detalle para venta:', idVenta); + + fetch(`../php/Obtener_Ventas.php?id_venta=${idVenta}`) + .then(res => { + console.log('Respuesta status:', res.status); + if (!res.ok) { + throw new Error(`HTTP Error: ${res.status}`); + } + return res.json(); + }) + .then(data => { + console.log('Detalle cargado:', data); + + // Verificar si hay error en la respuesta + if (data.error) { + console.error('Error del servidor:', data.error); + alert('Error: ' + data.error); + return; + } + + // Si la respuesta es un array vacío, mostrar mensaje + if (Array.isArray(data) && data.length === 0) { + console.warn('No hay productos para esta venta'); + } + + renderDetalleVenta(data); + mostrarVistaDetalle(idVenta); + }) + .catch(err => { + console.error('Error completo:', err); + alert('❌ Error al cargar el detalle de la venta: ' + err.message); + }); + } + + function renderDetalleVenta(data) { + if (!tablaDetalle) return; + + // Asumimos que data contiene los productos de la venta + const productos = Array.isArray(data) ? data : (data.productos || []); + + let totalVenta = 0; + const tbody = tablaDetalle.querySelector('tbody'); + tbody.innerHTML = ''; + + if (productos.length === 0) { + tbody.innerHTML = 'No hay productos en esta venta'; + return; + } + + productos.forEach(prod => { + const cantidad = Number(prod.cantidad || 0); + const precio = Number(prod.precio_unitario || prod.precio || 0); + const subtotal = cantidad * precio; + totalVenta += subtotal; + + const row = document.createElement('tr'); + row.innerHTML = ` + ${prod.nombre || prod.descripcion || '-'} + ${cantidad} + ${formatMoney(precio)} + ${formatMoney(subtotal)} + `; + tbody.appendChild(row); + }); + + // Agregar fila de total + const rowTotal = document.createElement('tr'); + rowTotal.style.fontWeight = 'bold'; + rowTotal.style.borderTop = '2px solid #333'; + rowTotal.innerHTML = ` + TOTAL GENERAL: + ${formatMoney(totalVenta)} + `; + tbody.appendChild(rowTotal); + } + + // --- Exponemos función global para el onclick --- + window.verDetalleVenta = function(idVenta) { + cargarDetalleVenta(idVenta); + }; + + // --- Botón Volver --- + const btnVolver = document.getElementById('btn-volver-ventas'); + if (btnVolver) { + btnVolver.addEventListener('click', mostrarVistaPrincipal); + } + + // --- Inicialización --- + const hoyStr = new Date().toISOString().slice(0, 10); + if (fechaInput) { + fechaInput.value = hoyStr; + cargarVentasActuales(); + fechaInput.addEventListener('change', () => { + mostrarVistaPrincipal(); + cargarVentasActuales(); + }); + } + + window.addEventListener("hashchange", () => { + if (window.location.hash === "#ver_ventas") { + mostrarVistaPrincipal(); + cargarVentasActuales(); + } + }); +}); \ No newline at end of file diff --git a/js/Tabla_Gastos.js b/js/Tabla_Gastos.js new file mode 100644 index 0000000..7192e35 --- /dev/null +++ b/js/Tabla_Gastos.js @@ -0,0 +1,291 @@ +let gastosActuales = {}; +let columnasGastos = []; // Columnas dinámicas de gastos + +document.addEventListener('DOMContentLoaded', () => { + cargarEstructuraGastos(); + setupFormListeners(); +}); + +function setupFormListeners() { + // Formulario para agregar nuevo tipo de gasto + const formAgregarTipo = document.getElementById('form-agregar-tipo-gasto'); + if (formAgregarTipo) { + formAgregarTipo.addEventListener('submit', handleAgregarTipoGasto); + } +} + +// ========== CARGAR ESTRUCTURA DINÁMICA ========== + +async function cargarEstructuraGastos() { + try { + const response = await fetch('../php/Obtener_Gastos.php'); + const data = await response.json(); + + if (data.success) { + columnasGastos = data.columnas || []; + gastosActuales = data.gasto || {}; + + construirFormularioDinamico(); + } else { + console.log('No hay gastos registrados:', data.message); + columnasGastos = []; + construirFormularioDinamico(); + } + } catch (error) { + console.error('Error al cargar gastos:', error); + } +} + +// ========== CONSTRUIR FORMULARIO DINÁMICO ========== + +function construirFormularioDinamico() { + // Solo mostrar tabla de gastos + mostrarTablaGastos(); +} + +// ========== AGREGAR NUEVO TIPO DE GASTO ========== + +async function handleAgregarTipoGasto(e) { + e.preventDefault(); + + const nombre = document.getElementById('nombre-nuevo-gasto').value.trim(); + const monto = parseFloat(document.getElementById('monto-nuevo-gasto').value); + + if (!nombre || monto < 0) { + alert('Por favor completa los datos correctamente'); + return; + } + + // Validar nombre (solo letras y espacios) + if (!/^[a-zA-Z\s]+$/.test(nombre)) { + alert('El nombre del gasto solo puede contener letras y espacios'); + return; + } + + try { + const response = await fetch('../php/Registrar_Gastos.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + nuevo_gasto: nombre, + monto: monto + }) + }); + + const data = await response.json(); + + if (data.success) { + alert('Nuevo tipo de gasto agregado correctamente'); + document.getElementById('form-agregar-tipo-gasto').reset(); + cargarEstructuraGastos(); + } else { + alert('Error al agregar gasto: ' + data.message); + } + } catch (error) { + console.error('Error:', error); + alert('Error de conexión'); + } +} + +// ========== MOSTRAR TABLA DE GASTOS ========== + +function mostrarTablaGastos() { + const tabla = document.getElementById('tabla-gastos'); + if (!tabla) return; + + const tbody = tabla.querySelector('tbody'); + tbody.innerHTML = ''; + + if (columnasGastos.length === 0) { + const tr = document.createElement('tr'); + const td = document.createElement('td'); + td.colSpan = 3; + td.textContent = 'No hay gastos configurados'; + td.style.textAlign = 'center'; + td.style.color = '#999'; + tr.appendChild(td); + tbody.appendChild(tr); + return; + } + + columnasGastos.forEach(columna => { + const tr = document.createElement('tr'); + const monto = gastosActuales[columna] || 0; + const descripcionFormato = columna.charAt(0).toUpperCase() + columna.slice(1); + + // Descripción + const tdDesc = document.createElement('td'); + tdDesc.textContent = descripcionFormato; + tr.appendChild(tdDesc); + + // Monto + const tdMonto = document.createElement('td'); + tdMonto.textContent = `$${parseFloat(monto).toFixed(2)}`; + tr.appendChild(tdMonto); + + // Acciones + const tdAcciones = document.createElement('td'); + tdAcciones.style.display = 'flex'; + tdAcciones.style.gap = '10px'; + + // Botón Editar + const btnEditar = document.createElement('button'); + btnEditar.type = 'button'; + btnEditar.className = 'btn-editar-gasto'; + btnEditar.textContent = 'Editar'; + btnEditar.addEventListener('click', () => activarEdicionGasto(btnEditar, columna)); + tdAcciones.appendChild(btnEditar); + + // Botón Eliminar + const btnEliminar = document.createElement('button'); + btnEliminar.type = 'button'; + btnEliminar.className = 'btn-eliminar-gasto'; + btnEliminar.textContent = 'Eliminar'; + btnEliminar.addEventListener('click', () => abrirModalEliminar(columna)); + tdAcciones.appendChild(btnEliminar); + + tr.appendChild(tdAcciones); + tbody.appendChild(tr); + }); +} + +// ========== EDITAR GASTO ========== + +function activarEdicionGasto(btnEditar, columnaActual) { + const fila = btnEditar.closest('tr'); + const celdas = fila.querySelectorAll('td'); + const descripcionActual = celdas[0].textContent; + const montoActual = parseFloat(celdas[1].textContent.replace('$', '')); + + // Convertir descripción a input + celdas[0].innerHTML = ``; + + // Convertir monto a input + celdas[1].innerHTML = ``; + + // Crear botones Guardar y Cancelar + const btnGuardar = document.createElement('button'); + btnGuardar.type = 'button'; + btnGuardar.textContent = 'Guardar'; + btnGuardar.className = 'btn-guardar'; + btnGuardar.addEventListener('click', () => guardarEdicionGasto(fila, columnaActual, montoActual, descripcionActual)); + + const btnCancelar = document.createElement('button'); + btnCancelar.type = 'button'; + btnCancelar.textContent = 'Cancelar'; + btnCancelar.className = 'btn-cancelar'; + btnCancelar.addEventListener('click', () => cancelarEdicionGasto(fila, columnaActual, montoActual, descripcionActual)); + + // Reemplazar botones en la celda de acciones + celdas[2].innerHTML = ''; + celdas[2].appendChild(btnGuardar); + celdas[2].appendChild(btnCancelar); +} + +function cancelarEdicionGasto(fila, columnaActual, montoActual, descripcionActual) { + const celdas = fila.querySelectorAll('td'); + + // Restaurar descripción + celdas[0].textContent = descripcionActual; + + // Restaurar monto + celdas[1].textContent = `$${parseFloat(montoActual).toFixed(2)}`; + + // Restaurar botones Editar y Eliminar + const btnEditar = document.createElement('button'); + btnEditar.type = 'button'; + btnEditar.className = 'btn-editar-gasto'; + btnEditar.textContent = 'Editar'; + btnEditar.addEventListener('click', () => activarEdicionGasto(btnEditar, columnaActual)); + + const btnEliminar = document.createElement('button'); + btnEliminar.type = 'button'; + btnEliminar.className = 'btn-eliminar-gasto'; + btnEliminar.textContent = 'Eliminar'; + btnEliminar.addEventListener('click', () => abrirModalEliminar(columnaActual)); + + celdas[2].innerHTML = ''; + celdas[2].appendChild(btnEditar); + celdas[2].appendChild(btnEliminar); +} + +async function guardarEdicionGasto(fila, columnaActual, montoActual, descripcionActual) { + const nuevoNombre = fila.querySelector('.input-descripcion-gasto').value.trim(); + const nuevoMonto = parseFloat(fila.querySelector('.input-monto-gasto').value); + + if (!nuevoNombre) { + alert('El nombre no puede estar vacío'); + return; + } + + if (isNaN(nuevoMonto) || nuevoMonto < 0) { + alert('El monto debe ser un número válido y mayor o igual a 0'); + return; + } + + try { + const response = await fetch('../php/Editar_Gasto.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + columna_actual: columnaActual, + columna_nueva: nuevoNombre, + monto: nuevoMonto + }) + }); + + const data = await response.json(); + + if (data.success) { + alert('Gasto editado correctamente'); + cargarEstructuraGastos(); + } else { + alert('Error al editar gasto: ' + data.message); + cancelarEdicionGasto(fila, columnaActual, montoActual, descripcionActual); + } + } catch (error) { + console.error('Error:', error); + alert('Error de conexión'); + cancelarEdicionGasto(fila, columnaActual, montoActual, descripcionActual); + } +} + +// ========== ELIMINAR GASTO ========== + +function abrirModalEliminar(columna) { + const confirmar = confirm(`¿Deseas eliminar el gasto "${columna}"? Esta acción no se puede deshacer.`); + + if (confirmar) { + eliminarGasto(columna); + } +} + +async function eliminarGasto(columna) { + try { + const response = await fetch('../php/Eliminar_Gasto.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + columna: columna + }) + }); + + const data = await response.json(); + + if (data.success) { + alert('Gasto eliminado correctamente'); + cargarEstructuraGastos(); + } else { + alert('Error al eliminar gasto: ' + data.message); + } + } catch (error) { + console.error('Error:', error); + alert('Error de conexión'); + } +} diff --git a/js/Tabla_Productos.js b/js/Tabla_Productos.js new file mode 100644 index 0000000..4b45550 --- /dev/null +++ b/js/Tabla_Productos.js @@ -0,0 +1,277 @@ +let tablaProductosInitDone = false; + +function initTablaProductos() { + if (tablaProductosInitDone) return; + tablaProductosInitDone = true; + + const form = document.getElementById("form-producto"); + const tablaBody = document.querySelector("#tabla-productos tbody"); + + form.addEventListener("submit", (e) => { + e.preventDefault(); + + const datos = new FormData(form); + const producto = { + descripcion: datos.get("descripcion"), + precio_venta: parseFloat(datos.get("precio_venta")), + precio_compra: parseFloat(datos.get("precio_compra")), + cantidad: parseInt(datos.get("cantidad")), + }; + + const esEdicion = !!form.dataset.editando; + if (esEdicion) { + producto.id = form.dataset.editando; + } + + const url = esEdicion + ? "../php/Editar_Producto.php" + : "../php/Agregar_Producto.php"; + + fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(producto), + }) + .then((res) => res.text()) + .then((msg) => { + alert(msg); + form.reset(); + delete form.dataset.editando; + cargarProductos(); + verificarStockBajoMenu(); + }) + .catch((err) => console.error("Error al guardar producto:", err)); + }); + + verificarStockBajoMenu(); +} + +// Stock limit helpers (stored in localStorage) +function getStockLimit() { + const val = localStorage.getItem('stock_limit_alert'); + const n = parseInt(val, 10); + return isNaN(n) ? 10 : n; +} + +function setStockLimit(limit) { + localStorage.setItem('stock_limit_alert', String(parseInt(limit, 10) || 0)); + verificarStockBajoMenu(); + if (typeof cargarProductos === 'function') cargarProductos(); +} + +document.addEventListener("DOMContentLoaded", initTablaProductos); +if (document.readyState !== "loading") { + initTablaProductos(); +} + +const lowStockIconHTML = ``; + +function actualizarAlertaStockEnMenu(tieneStockBajo) { + const linkProductos = document.querySelector('.sidebar__link[href="#productos"]'); + if (!linkProductos) return; + + const alertaExistente = linkProductos.querySelector('.low-stock-alert'); + if (tieneStockBajo) { + if (!alertaExistente) { + linkProductos.insertAdjacentHTML('beforeend', lowStockIconHTML); + } + } else if (alertaExistente) { + alertaExistente.remove(); + } +} + +function verificarStockBajoMenu() { + const limit = getStockLimit(); + fetch("../php/Obtener_Productos.php") + .then((res) => res.json()) + .then((productos) => { + const tieneStockBajo = productos.some((p) => parseInt(p.Stock_Disponible, 10) < limit); + actualizarAlertaStockEnMenu(tieneStockBajo); + }) + .catch((err) => console.error("Error al consultar stock bajo:", err)); +} + +function cargarProductos() { + fetch("../php/Obtener_Productos.php") + .then((res) => res.json()) + .then((productos) => { + const tablaBody = document.querySelector("#tabla-productos tbody"); + tablaBody.innerHTML = ""; + const limit = getStockLimit(); + const tieneStockBajo = productos.some((p) => parseInt(p.Stock_Disponible, 10) < limit); + + productos.forEach((p) => { + const stockBajo = parseInt(p.Stock_Disponible, 10) < limit; + const fila = document.createElement("tr"); + fila.innerHTML = ` + ${p.ID_Producto} + ${p.Descripcion} + $${parseFloat(p.Precio_Venta).toFixed(2)} + $${parseFloat(p.Precio_Compra).toFixed(2)} + ${p.Stock_Disponible}${stockBajo ? lowStockIconHTML : ''} + + + + + `; + tablaBody.appendChild(fila); + }); + + actualizarAlertaStockEnMenu(tieneStockBajo); + + document.querySelectorAll(".btn-editar").forEach((btn) => { + btn.addEventListener("click", () => activarEdicionProducto(btn)); + }); + + document.querySelectorAll(".btn-eliminar").forEach((btn) => { + btn.addEventListener("click", () => eliminarProducto(btn.dataset.id)); + }); + }); +} + +// Init stock limit UI +document.addEventListener('DOMContentLoaded', () => { + const input = document.getElementById('stock-limit-input'); + const btn = document.getElementById('save-stock-limit'); + if (input) { + input.value = getStockLimit(); + } + if (btn) { + btn.addEventListener('click', () => { + const v = document.getElementById('stock-limit-input').value; + setStockLimit(parseInt(v, 10) || 0); + alert('Límite de stock guardado: ' + (parseInt(v, 10) || 0)); + }); + } +}); + +function eliminarProducto(id) { + if (!confirm("¿Estás seguro que querés eliminar este producto?")) return; + + fetch(`../php/Eliminar_Producto.php?id=${id}`) + .then((res) => res.json()) + .then((data) => { + alert(data.message); + if (data.success) { + cargarProductos(); + } + }) + .catch((err) => { + console.error("Error:", err); + alert("Error al eliminar el producto"); + }); +} + +function activarEdicionProducto(btnEditar) { + const fila = btnEditar.closest("tr"); + const celdas = fila.querySelectorAll("td"); + const idProducto = btnEditar.dataset.id; + + const descripcionActual = celdas[1].textContent; + const precioVentaActual = parseFloat(celdas[2].textContent.replace('$', '')); + const precioCompraActual = parseFloat(celdas[3].textContent.replace('$', '')); + const stockActual = parseInt(celdas[4].textContent); + + celdas[1].innerHTML = ``; + celdas[2].innerHTML = ``; + celdas[3].innerHTML = ``; + celdas[4].innerHTML = ``; + + const btnGuardar = document.createElement("button"); + btnGuardar.textContent = "Guardar"; + btnGuardar.classList.add("btn-guardar"); + btnGuardar.addEventListener("click", () => guardarEdicionProducto(fila, idProducto)); + + const btnCancelar = document.createElement("button"); + btnCancelar.textContent = "Cancelar"; + btnCancelar.classList.add("btn-cancelar"); + btnCancelar.addEventListener("click", () => + cancelarEdicionProducto(fila, descripcionActual, precioVentaActual, precioCompraActual, stockActual, idProducto) + ); + + const celdaBotones = celdas[5]; + celdaBotones.innerHTML = ""; + celdaBotones.appendChild(btnGuardar); + celdaBotones.appendChild(btnCancelar); +} + +function cancelarEdicionProducto(fila, descripcion, venta, compra, stock, id) { + const celdas = fila.querySelectorAll("td"); + celdas[1].textContent = descripcion; + celdas[2].textContent = `$${parseFloat(venta).toFixed(2)}`; + celdas[3].textContent = `$${parseFloat(compra).toFixed(2)}`; + celdas[4].textContent = stock; + + const btnEditar = document.createElement("button"); + btnEditar.textContent = "Editar"; + btnEditar.classList.add("btn-editar"); + btnEditar.dataset.id = id; + btnEditar.addEventListener("click", () => activarEdicionProducto(btnEditar)); + + const btnEliminar = document.createElement("button"); + btnEliminar.textContent = "Eliminar"; + btnEliminar.classList.add("btn-eliminar"); + btnEliminar.dataset.id = id; + btnEliminar.addEventListener("click", () => eliminarProducto(id)); + + celdas[5].innerHTML = ""; + celdas[5].appendChild(btnEditar); + celdas[5].appendChild(btnEliminar); +} + +function guardarEdicionProducto(fila, idProducto) { + const descripcion = fila.querySelector(".input-descripcion").value.trim(); + const precioVenta = parseFloat(fila.querySelector(".input-precio-venta").value); + const precioCompra = parseFloat(fila.querySelector(".input-precio-compra").value); + const stock = parseInt(fila.querySelector(".input-stock").value); + + if (!descripcion || isNaN(precioVenta) || isNaN(precioCompra) || isNaN(stock)) { + alert("Por favor, complete todos los campos correctamente."); + return; + } + + fetch("../php/Editar_Producto.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + id: idProducto, + descripcion, + precio_venta: precioVenta, + precio_compra: precioCompra, + cantidad: stock + }) + }) + .then(res => res.json()) + .then(data => { + if (data.success) { + cargarProductos(); + } else { + alert("Error al guardar producto: " + data.error); + } + }) + .catch(err => { + console.error("Error:", err); + alert("Ocurrió un error al editar el producto."); + }); +} + +// Actualizar Stock sin recargar la pagina +window.addEventListener("hashchange", () => { + if (window.location.hash === "#productos") { + // Esperar un poco para que el DOM actualice la sección visible + setTimeout(() => { + const tabla = document.querySelector("#tabla-productos"); + if (tabla) { + cargarProductos(); + } else { + console.warn("Tabla de productos no encontrada."); + } + }, 100); // pequeño retardo para esperar que el DOM muestre la sección + } +}); + +window.cargarProductos = cargarProductos; diff --git a/js/Tabla_Proveedor.js b/js/Tabla_Proveedor.js new file mode 100644 index 0000000..1dbeaa4 --- /dev/null +++ b/js/Tabla_Proveedor.js @@ -0,0 +1,327 @@ +document.addEventListener("DOMContentLoaded", () => { + const form = document.getElementById("form-proveedor"); + const tablaBody = document.querySelector("#tabla-proveedores tbody"); + + form.addEventListener("submit", (e) => { + e.preventDefault(); + + const nombre = document.querySelector(".nombre-proveedor").value; + const telefono = document.querySelector(".telefono-proveedor").value; + + const proveedor = { + id: form.dataset.editando || null, + nombre, + telefono, + }; + + const url = proveedor.id + ? "../php/Editar_Proveedor.php" + : "../php/Guardar_Proveedor.php"; + + fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(proveedor), + }) + .then((res) => res.json()) + .then((data) => { + if (data.success) { + alert(data.message || "Proveedor guardado"); + form.reset(); + delete form.dataset.editando; + cargarProveedores(); + } else { + alert("Error: " + (data.error || "Error desconocido")); + } + }); + }); + + cargarProveedores(); + + // Delegación de eventos para la tabla + document.addEventListener("click", (e) => { + if (e.target.classList.contains("btn-cancelar")) { + e.preventDefault(); + e.stopPropagation(); + const fila = e.target.closest("tr"); + if (fila) { + cargarProveedores(); + } + } + }); +}); + +function cargarProveedores() { + fetch("../php/Obtener_Proveedores.php") + .then((res) => res.json()) + .then((proveedores) => { + const tablaBody = document.querySelector("#tabla-proveedores tbody"); + tablaBody.innerHTML = ""; + + proveedores.forEach((p) => { + const fila = document.createElement("tr"); + fila.setAttribute("data-id", p.ID_Proveedor); + fila.innerHTML = ` + ${p.ID_Proveedor} + ${p.Nombre} + ${p.Telefono} + + + + + + `; + tablaBody.appendChild(fila); + }); + + document.querySelectorAll("#tabla-proveedores .btn-editar-proveedor").forEach((btn) => { + btn.addEventListener("click", () => activarModoEdicion(btn)); + }); + + document.querySelectorAll("#tabla-proveedores .btn-eliminar-proveedor").forEach((btn) => { + btn.addEventListener("click", () => eliminarProveedor(btn.dataset.id)); + }); + + document.querySelectorAll(".btn-ver-productos").forEach((btn) => { + btn.addEventListener("click", () => cargarProductosDelProveedor(btn.dataset.id)); + }); + }); +} + +function editarProveedor(id) { + fetch(`../php/Buscar_Proveedor_Editar.php?id=${id}`) + .then((res) => res.json()) + .then((data) => { + console.log("Respuesta del servidor:", data); + if (!data || !data.ID_Proveedor) { + alert("Proveedor no encontrado"); + return; + } + + const form = document.getElementById("form-proveedor"); + document.querySelector(".nombre-proveedor").value = data.Nombre; + document.querySelector(".telefono-proveedor").value = data.Telefono; + form.dataset.editando = data.ID_Proveedor; + }); +} + +function eliminarProveedor(id) { + if (!confirm("¿Estás seguro que querés eliminar este proveedor?")) return; + + fetch("../php/Eliminar_Proveedor.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id: parseInt(id, 10) }), + }) + .then((res) => res.text()) + .then((text) => { + if (!text) { + throw new Error("Respuesta vacía del servidor."); + } + + let data; + try { + data = JSON.parse(text); + } catch (error) { + console.warn("Respuesta de Eliminar_Proveedor no es JSON:", text); + throw new Error("Respuesta inválida del servidor."); + } + + if (data.success) { + alert(data.message || "Proveedor eliminado"); + cargarProveedores(); + } else { + alert("Error: " + (data.error || data.message || "Error desconocido")); + } + }) + .catch((error) => { + console.error("Error al eliminar proveedor:", error); + alert(error.message || "Error de conexión al eliminar proveedor."); + }); +} + +function cargarProductosDelProveedor(idProveedor) { + fetch(`../php/Obtener_Productos_Proveedor.php?id=${idProveedor}`) + .then((res) => res.json()) + .then((productos) => { + const contenedor = document.getElementById("productos-proveedor"); + const cuerpo = document.getElementById("productos-proveedor-body"); + cuerpo.innerHTML = ""; + + productos.forEach((prod) => { + const fila = document.createElement("tr"); + fila.setAttribute("data-id", idProveedor); + fila.innerHTML = ` + ${prod.ID_Producto} + ${prod.Descripcion} + + + + `; + cuerpo.appendChild(fila); + }); + + contenedor.style.display = "block"; + + document.querySelectorAll(".btn-eliminar-producto").forEach((btn) => { + btn.addEventListener("click", () => { + const idProd = btn.dataset.idProducto; + const idProv = btn.dataset.idProveedor; + + fetch( + `../php/Eliminar_Producto_Proveedor.php?id_proveedor=${idProv}&id_producto=${idProd}` + ) + .then((res) => res.json()) + .then((data) => { + if (data.success) { + cargarProductosDelProveedor(idProv); + } else { + alert("Error: " + data.error); + } + }); + }); + }); + }); +} + +// Agregar producto al proveedor +document.addEventListener("DOMContentLoaded", () => { + const formAgregarProducto = document.getElementById("form-agregar-producto"); + + formAgregarProducto.addEventListener("submit", function (e) { + e.preventDefault(); + + const idProveedor = document.getElementById("id-proveedor-producto").value; + const idProducto = document.getElementById("nuevo-id-producto").value; + + fetch("../php/Agregar_Producto_Proveedor.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + id_proveedor: parseInt(idProveedor), + id_producto: parseInt(idProducto), + }), + }) + .then((res) => res.json()) + .then((data) => { + if (data.success) { + cargarProductosDelProveedor(idProveedor); + document.getElementById("nuevo-id-producto").value = ""; + } else { + alert("Error: " + data.error); + } + }); + }); +}); + +document.addEventListener("click", function (e) { + if (e.target.classList.contains("btn-ver-productos")) { + const idProveedor = e.target.dataset.id; + + document.getElementById("id-proveedor-producto").value = idProveedor; + cargarProductosDelProveedor(idProveedor); + + // Mostrar tabla productos + document.getElementById("productos-proveedor").style.display = "block"; + document.getElementById("tabla-proveedores").style.display = "none"; + document.getElementById("titulo-proveedores").style.display = "none"; + } +}); + +document.getElementById("cerrar-productos").addEventListener("click", () => { + document.getElementById("productos-proveedor").style.display = "none"; + document.getElementById("tabla-proveedores").style.display = "table"; + document.getElementById("titulo-proveedores").style.display = "block"; +}); + +function activarModoEdicion(btnEditar) { + const fila = btnEditar.closest("tr"); + const celdas = fila.querySelectorAll("td"); + + // Obtener ID desde el botón + const idProveedor = btnEditar.getAttribute("data-id"); + + if (!idProveedor) { + console.error("ID DE PROVEEDOR NO ENCONTRADO"); + alert("ID de proveedor no encontrado."); + return; + } + + // Obtener valores actuales ANTES de cambiar a inputs + const nombreActual = celdas[1].textContent.trim(); + const telefonoActual = celdas[2].textContent.trim(); + + // Reemplazar con inputs + celdas[1].innerHTML = ``; + celdas[2].innerHTML = ``; + + // Botón Guardar + const btnGuardar = document.createElement("button"); + btnGuardar.type = "button"; + btnGuardar.textContent = "Guardar"; + btnGuardar.className = "btn-guardar"; + btnGuardar.setAttribute("data-id", idProveedor); + btnGuardar.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + guardarEdicion(fila, idProveedor); + }); + + // Botón Cancelar + const btnCancelar = document.createElement("button"); + btnCancelar.type = "button"; + btnCancelar.textContent = "Cancelar"; + btnCancelar.className = "btn-cancelar"; + btnCancelar.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + // Simplemente recargar la tabla cancela todos los cambios + cargarProveedores(); + }); + + // Reemplazar celda de botones + const celdaBotones = celdas[3]; + celdaBotones.innerHTML = ""; + celdaBotones.appendChild(btnGuardar); + celdaBotones.appendChild(btnCancelar); +} + +function guardarEdicion(fila, idProveedor) { + const celdas = fila.querySelectorAll("td"); + const nuevoNombre = fila.querySelector(".input-nombre").value.trim(); + const nuevoTelefono = fila.querySelector(".input-telefono").value.trim(); + + if (nuevoNombre === "" || nuevoTelefono === "") { + alert("Por favor, complete todos los campos."); + return; + } + console.log("Datos enviados:", { + id: idProveedor, + nombre: nuevoNombre, + telefono: nuevoTelefono, + }); + + // Enviar los nuevos datos al servidor para actualizarlos en la base de datos + fetch("../php/Editar_Proveedor.php", { + method: "POST", + body: JSON.stringify({ + id: idProveedor, + nombre: nuevoNombre, + telefono: nuevoTelefono, + }), + headers: { "Content-Type": "application/json" }, + }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + alert("Proveedor actualizado correctamente"); + cargarProveedores(); + } else { + alert("Error al actualizar el proveedor."); + } + }) + .catch((error) => { + console.error("Error al actualizar el proveedor:", error); + alert("Ocurrió un error al actualizar el proveedor."); + }); +} diff --git a/js/Tabla_Reservas.js b/js/Tabla_Reservas.js new file mode 100644 index 0000000..119e14d --- /dev/null +++ b/js/Tabla_Reservas.js @@ -0,0 +1,673 @@ +// Variables globales para mantener el auto-refresh +let filtrosActuales = {}; +let intervaloActualizacion = null; + +// Datos de las canchas (similar a Reservar_Turno.js) +let datosCanchas = { futbol: null, padel: null, quincho: null }; + +// Cargar datos de las canchas +async function cargarDatosCanchastabla() { + try { + const response = await fetch('../php/Obtener_Canchas.php'); + const data = await response.json(); + + const canchas = Array.isArray(data) + ? data + : (data && Array.isArray(data.canchas) ? data.canchas : []); + + if (!canchas.length) return; + + canchas.forEach(cancha => { + const rawTipo = String(cancha.Tipo ?? cancha.tipo ?? '').trim().toUpperCase(); + const tipo = rawTipo === 'F' || rawTipo === 'FUTBOL' ? 'F' + : rawTipo === 'P' || rawTipo === 'PADEL' ? 'P' + : rawTipo === 'Q' || rawTipo === 'QUINCHO' ? 'Q' + : null; + + if (!tipo) return; + + const duracion = parseInt(cancha.Duracion ?? cancha.duracion, 10) || 0; + const apertura = cancha.Hora_Apertura ?? cancha.apertura ?? null; + const cierre = cancha.Hora_Cierre ?? cancha.cierre ?? null; + + if (tipo === 'F') { + datosCanchas.futbol = { duracion, apertura, cierre }; + } else if (tipo === 'P') { + datosCanchas.padel = { duracion, apertura, cierre }; + } + }); + } catch (error) { + console.error('Error al cargar datos de canchas:', error); + } +} + +document.addEventListener("DOMContentLoaded", async () => { + // Cargar datos de canchas + await cargarDatosCanchastabla(); + + // Establecer la fecha de hoy por defecto + const hoy = new Date().toISOString().split('T')[0]; + const inputFecha = document.querySelector(".filtro-formulario input[name='fecha']"); + if (inputFecha) { + inputFecha.value = hoy; + } + + cargarReservas({ fecha: hoy }); // Cargar reservas del día actual + iniciarActualizacionAutomatica(); // Inicia el auto-refresh + + const formFiltro = document.querySelector(".filtro-formulario"); + const btnReestablecer = document.getElementById("btn-reestablecer-filtros"); + + // Asegurar estilos para reprogramar (inyección en head si el css no está presente) + ensureReprogramStyles(); + + // Agregar event listeners para búsqueda dinámica + if (formFiltro) { + const inputs = formFiltro.querySelectorAll("input, select"); + inputs.forEach(input => { + input.addEventListener("change", () => { + aplicarFiltrosDinamicos(); + }); + input.addEventListener("input", () => { + aplicarFiltrosDinamicos(); + }); + }); + } + + // Event listener para botón reestablecer + if (btnReestablecer) { + btnReestablecer.addEventListener("click", (e) => { + e.preventDefault(); + reestablecerFiltros(); + }); + } +}); + +function aplicarFiltrosDinamicos() { + const formFiltro = document.querySelector(".filtro-formulario"); + const formData = new FormData(formFiltro); + const filtros = { + fecha: formData.get("fecha"), + nombre: formData.get("nombre"), + dni: formData.get("dni"), + tipo: formData.get("tipo_cancha") + }; + + cargarReservas(filtros); +} + +function reestablecerFiltros() { + const formFiltro = document.querySelector(".filtro-formulario"); + + // Limpiar todos los inputs + const inputs = formFiltro.querySelectorAll("input, select"); + inputs.forEach(input => { + if (input.name === "fecha") { + // Establecer fecha de hoy + input.value = new Date().toISOString().split('T')[0]; + } else { + input.value = ""; + } + }); + + // Cargar reservas del día actual + const hoy = new Date().toISOString().split('T')[0]; + cargarReservas({ fecha: hoy }); +} + +function cargarReservas(filtros = {}) { + // Guardar los filtros actuales para el auto-refresh (solo los que tienen valor) + const filtrosConValor = {}; + for (const [key, value] of Object.entries(filtros)) { + if (value) { + filtrosConValor[key] = value; + } + } + + if (Object.keys(filtrosConValor).length > 0) { + filtrosActuales = filtrosConValor; + } + + let url = "../php/Obtener_Reservas.php?"; + const params = new URLSearchParams(); + + // Usar los filtros guardados si no se proporcionan nuevos + const filtrosAUsar = Object.keys(filtrosConValor).length > 0 ? filtrosConValor : filtrosActuales; + + // Solo agregamos parámetros si tienen valor + if (filtrosAUsar.fecha) params.append("fecha", filtrosAUsar.fecha); + if (filtrosAUsar.nombre) params.append("nombre", filtrosAUsar.nombre); + if (filtrosAUsar.dni) params.append("dni", filtrosAUsar.dni); + if (filtrosAUsar.tipo) params.append("tipo", filtrosAUsar.tipo); + + fetch(url + params.toString()) + .then(res => res.json()) + .then(reservas => { + const tablaBody = document.querySelector("#tabla-reservas tbody"); + tablaBody.innerHTML = ""; + + if (reservas.length === 0) { + tablaBody.innerHTML = "No se encontraron resultados."; + return; + } + + reservas.forEach(r => { + const tipoTexto = r.Tipo === 'F' ? 'Fútbol' : (r.Tipo === 'P' ? 'Pádel' : (r.Tipo === 'Q' ? 'Quincho' : 'Otro')); + + // Convertir estado numérico a texto + let estadoTexto = ''; + let estadoValor = parseInt(r.Estado); + + if (estadoValor === 0) { + estadoTexto = 'No Pagado'; + } else if (estadoValor === 1) { + estadoTexto = 'Pagado'; + } else if (estadoValor === 2) { + estadoTexto = 'Cancelado'; + } + + // Verificar si la reserva es del pasado + const fechaReserva = new Date(r.Fecha_Hora); + const hoy = new Date(); + hoy.setHours(0, 0, 0, 0); + fechaReserva.setHours(0, 0, 0, 0); + + const esDelPasado = fechaReserva < hoy; + const disabledAttr = esDelPasado ? 'disabled' : ''; + const disabledClass = esDelPasado ? 'disabled' : ''; + + const fila = document.createElement("tr"); + fila.setAttribute("data-id-reserva", r.ID_Reserva); + fila.setAttribute("data-tipo-cancha", r.Tipo); + fila.setAttribute("data-numero-cancha", r.Numero); + fila.setAttribute("data-fecha-reserva", r.Fecha_Hora); + fila.innerHTML = ` + ${r.Nombre} + ${r.DNI} + ${r.Telefono} + ${new Date(r.Fecha_Hora).toLocaleString()} + ${tipoTexto} + ${r.Numero} + ${parseFloat(r.Descuento_Cupon) > 0 ? (parseFloat(r.Descuento_Cupon) * 100).toFixed(0) + '%' : '0%'} + $${parseFloat(r.Monto).toFixed(2)} + +
+ + +
+ + + + `; + tablaBody.appendChild(fila); + }); + + // Agregar event listeners al dropdown + agregarListenersDropdown(); + agregarListenersReprogramar(); + }) + .catch(err => console.error("Error en fetch:", err)); +} + +function agregarListenersDropdown() { + const botonesEstado = document.querySelectorAll(".estado-btn"); + + botonesEstado.forEach(btn => { + // Marcar la opción activa al inicio + const estadoActual = btn.getAttribute("data-estado"); + const dropdown = btn.closest(".estado-dropdown"); + const opciones = dropdown.querySelectorAll(".estado-opcion"); + + opciones.forEach(op => { + if (op.getAttribute("data-valor") === estadoActual) { + op.classList.add("activo"); + } else { + op.classList.remove("activo"); + } + }); + + btn.addEventListener("click", (e) => { + e.stopPropagation(); + + // Si el botón está deshabilitado, no hacer nada + if (btn.hasAttribute("disabled") || btn.classList.contains("disabled")) { + return; + } + + const opcionesDiv = dropdown.querySelector(".estado-opciones"); + + // Cerrar otros dropdowns abiertos + document.querySelectorAll(".estado-opciones").forEach(op => { + if (op !== opcionesDiv) op.style.display = "none"; + }); + + // Toggle del dropdown actual + const estaAbierto = opcionesDiv.style.display === "block"; + opcionesDiv.style.display = estaAbierto ? "none" : "block"; + btn.setAttribute("aria-expanded", !estaAbierto); + }); + }); + + // Click en opciones + const opcionesEstado = document.querySelectorAll(".estado-opcion"); + opcionesEstado.forEach(opcion => { + opcion.addEventListener("click", (e) => { + e.stopPropagation(); + const dropdown = opcion.closest(".estado-dropdown"); + const btn = dropdown.querySelector(".estado-btn"); + const textoSpan = btn.querySelector(".estado-texto"); + const opcionesDiv = dropdown.querySelector(".estado-opciones"); + + const nuevoValor = opcion.getAttribute("data-valor"); + const textoOpcion = opcion.textContent; + + // Remover clase activo de todas las opciones + dropdown.querySelectorAll(".estado-opcion").forEach(op => { + op.classList.remove("activo"); + }); + + // Agregar clase activo a la opción seleccionada + opcion.classList.add("activo"); + + // Actualizar el botón + textoSpan.textContent = textoOpcion; + btn.setAttribute("data-estado", nuevoValor); + + // Ocultar dropdown + opcionesDiv.style.display = "none"; + btn.setAttribute("aria-expanded", false); + + // Guardar cambio en BD + const fila = opcion.closest("tr"); + const idReserva = fila.getAttribute("data-id-reserva"); + actualizarEstadoReserva(idReserva, parseInt(nuevoValor)); + }); + }); + + // Cerrar dropdown si se hace clic fuera + document.addEventListener("click", (e) => { + if (!e.target.closest(".estado-dropdown")) { + document.querySelectorAll(".estado-opciones").forEach(op => { + op.style.display = "none"; + }); + document.querySelectorAll(".estado-btn").forEach(btn => { + btn.setAttribute("aria-expanded", false); + }); + } + }); +} + +function actualizarEstadoReserva(idReserva, nuevoEstado) { + fetch("../php/Actualizar_Estado_Reserva.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id_reserva: idReserva, estado: nuevoEstado }) + }) + .then(res => res.json()) + .then(data => { + if (data.success) { + console.log("Estado actualizado correctamente"); + } else { + alert("Error al actualizar: " + data.error); + // Recargar para mostrar el estado anterior + cargarReservas(); + } + }) + .catch(err => { + console.error("Error:", err); + alert("Error al actualizar el estado"); + cargarReservas(); + }); +} + +// Función para iniciar la actualización automática +function iniciarActualizacionAutomatica() { + // Actualizar cada 5 segundos (5000 milisegundos) + intervaloActualizacion = setInterval(() => { + cargarReservas(); + }, 5000); +} + +// Función para detener la actualización automática (opcional, si es necesario) +function detenerActualizacionAutomatica() { + if (intervaloActualizacion) { + clearInterval(intervaloActualizacion); + intervaloActualizacion = null; + } +} + +// Listeners y modal para reprogramar +function agregarListenersReprogramar() { + const botones = document.querySelectorAll('.reprogramar-btn'); + botones.forEach(btn => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + if (btn.hasAttribute('disabled') || btn.classList.contains('disabled')) return; + const fila = btn.closest('tr'); + const idReserva = fila.getAttribute('data-id-reserva'); + crearModalReprogramar(idReserva, fila); + }); + }); +} + +function crearModalReprogramar(idReserva, fila) { + // Obtener datos de la reserva + const tipoCancha = fila.getAttribute('data-tipo-cancha'); // 'F' o 'P' + const fechaActualReserva = fila.getAttribute('data-fecha-reserva'); + + // Crear overlay + const overlay = document.createElement('div'); + overlay.className = 'reprogramar-overlay'; + + const modal = document.createElement('div'); + modal.className = 'reprogramar-modal'; + + modal.innerHTML = ` + +
+ +
+ +
+
+ + +
+ `; + + overlay.appendChild(modal); + document.body.appendChild(overlay); + + // Generar los próximos 7 días + const diasGrid = modal.querySelector('.dias-grid'); + const hoy = new Date(); + + for (let i = 0; i < 7; i++) { + const dia = new Date(hoy); + dia.setDate(hoy.getDate() + i); + const btn = document.createElement('button'); + btn.className = 'dia-btn'; + const options = { weekday: 'short', day: 'numeric', month: 'numeric' }; + btn.textContent = `${dia.toLocaleDateString(undefined, { weekday: 'short' })}\n${dia.getDate()}/${dia.getMonth()+1}`; + btn.dataset.iso = dia.toISOString().split('T')[0]; + diasGrid.appendChild(btn); + } + + let diaSeleccionado = null; + let horaSeleccionada = null; + let canchaSeleccionada = null; + + // Handlers selección de día + modal.querySelectorAll('.dia-btn').forEach(d => { + d.addEventListener('click', async () => { + modal.querySelectorAll('.dia-btn').forEach(x => x.classList.remove('selected')); + d.classList.add('selected'); + diaSeleccionado = d.dataset.iso; + + // Generar horarios cuando se selecciona un día + await generarHorariosReprogramar(modal, tipoCancha, diaSeleccionado); + + // Limpiar selección de horario y cancha + horaSeleccionada = null; + canchaSeleccionada = null; + actualizarEstadoBotonConfirm(modal, diaSeleccionado, horaSeleccionada, canchaSeleccionada); + }); + }); + + // Delegation para horarios (se generan dinámicamente) + modal.addEventListener('click', async (e) => { + if (e.target.classList.contains('horario-btn') && !e.target.disabled) { + modal.querySelectorAll('.horario-btn').forEach(x => x.classList.remove('selected')); + e.target.classList.add('selected'); + horaSeleccionada = e.target.dataset.hora; + canchaSeleccionada = null; + + // Generar selección de canchas + await generarSeleccionCanchasReprogramar(modal, tipoCancha, diaSeleccionado, horaSeleccionada); + + actualizarEstadoBotonConfirm(modal, diaSeleccionado, horaSeleccionada, canchaSeleccionada); + } + }); + + // Delegation para canchas + modal.addEventListener('click', (e) => { + if (e.target.classList.contains('cancha-btn') && !e.target.disabled) { + modal.querySelectorAll('.cancha-btn').forEach(x => x.classList.remove('selected')); + e.target.classList.add('selected'); + canchaSeleccionada = e.target.dataset.numero; + actualizarEstadoBotonConfirm(modal, diaSeleccionado, horaSeleccionada, canchaSeleccionada); + } + }); + + // Cancelar + modal.querySelector('.btn-cancelar').addEventListener('click', () => { + closeModalReprogramar(overlay); + }); + + // Confirmar + modal.querySelector('.btn-confirmar').addEventListener('click', () => { + if (!diaSeleccionado || !horaSeleccionada || !canchaSeleccionada) return; + const fechaHora = diaSeleccionado + ' ' + horaSeleccionada; + reprogramarReserva(idReserva, fechaHora, overlay); + }); + + // Cerrar al hacer click fuera del modal + overlay.addEventListener('click', (e) => { + if (e.target === overlay) closeModalReprogramar(overlay); + }); +} + +// Generar horarios para reprogramación +async function generarHorariosReprogramar(modal, tipoCancha, fechaSeleccionada) { + const horariosGrid = modal.querySelector('.horarios-grid'); + horariosGrid.innerHTML = ''; + + const tipoCode = tipoCancha === 'F' ? 'futbol' : 'padel'; + const datos = datosCanchas[tipoCode]; + + if (!datos || !datos.duracion || !datos.apertura || !datos.cierre) { + horariosGrid.innerHTML = '

Error: No se pudieron cargar los datos de las canchas

'; + return; + } + + const duracion = datos.duracion; + const [aperturaH, aperturaM] = datos.apertura.split(':').map(Number); + const [cierreH, cierreM] = datos.cierre.split(':').map(Number); + const aperturaTotalMin = aperturaH * 60 + aperturaM; + const cierreTotalMin = cierreH * 60 + cierreM; + + const ahora = new Date(); + const [selYear, selMonth, selDay] = fechaSeleccionada.split('-').map(Number); + const fechaSelDate = new Date(selYear, selMonth - 1, selDay); + const hoy = new Date(ahora.getFullYear(), ahora.getMonth(), ahora.getDate()); + const esHoy = fechaSelDate.getTime() === hoy.getTime(); + const minutoActual = ahora.getHours() * 60 + ahora.getMinutes(); + + let horaActualMin = aperturaTotalMin; + + while (horaActualMin + duracion <= cierreTotalMin) { + const horas = Math.floor(horaActualMin / 60); + const minutos = horaActualMin % 60; + const horaFin = Math.floor((horaActualMin + duracion) / 60); + const minutosFin = (horaActualMin + duracion) % 60; + + const inicio = `${String(horas).padStart(2, '0')}:${String(minutos).padStart(2, '0')}`; + const fin = `${String(horaFin).padStart(2, '0')}:${String(minutosFin).padStart(2, '0')}`; + + const inicioMin = horas * 60 + minutos; + const esPasado = fechaSelDate < hoy || (esHoy && inicioMin < minutoActual); + + const btn = document.createElement('button'); + btn.className = 'horario-btn'; + btn.textContent = `${inicio}\n-\n${fin}`; + btn.dataset.hora = `${String(horas).padStart(2, '0')}:${String(minutos).padStart(2, '0')}:00`; + + // Verificar disponibilidad + let disponibles = 0; + if (!esPasado) { + disponibles = await obtenerDisponibilidadCanchaReprogramar( + fechaSeleccionada, + inicio, + duracion, + tipoCancha + ); + } + + // Desactivar si es pasado o no hay canchas disponibles + if (esPasado || disponibles === 0) { + btn.disabled = true; + btn.classList.add('no-disponible'); + btn.style.opacity = '0.5'; + btn.style.cursor = 'not-allowed'; + } + + horariosGrid.appendChild(btn); + horaActualMin += duracion; + } + + // Limpiar grid de canchas + modal.querySelector('.canchas-grid').innerHTML = ''; +} + +// Obtener disponibilidad de cancha para reprogramación +async function obtenerDisponibilidadCanchaReprogramar(fecha, horario, duracion, tipo) { + try { + const response = await fetch( + `../php/Obtener_Disponibilidad_Cancha.php?fecha=${fecha}&horario=${horario}&duracion=${duracion}&tipo=${tipo}` + ); + const data = await response.json(); + return data.success ? data.disponibles : 0; + } catch (error) { + console.error('Error obteniendo disponibilidad:', error); + return 0; + } +} + +// Generar selección de canchas disponibles +async function generarSeleccionCanchasReprogramar(modal, tipoCancha, fecha, horario) { + const canchasGrid = modal.querySelector('.canchas-grid'); + canchasGrid.innerHTML = ''; + + const tipoCode = tipoCancha === 'F' ? 'futbol' : 'padel'; + const datos = datosCanchas[tipoCode]; + + if (!datos || !datos.duracion) { + canchasGrid.innerHTML = '

Error: No se pudieron cargar los datos

'; + return; + } + + try { + const response = await fetch( + `../php/Obtener_Canchas_Numeros.php?fecha=${fecha}&horario=${horario}&duracion=${datos.duracion}&tipo=${tipoCancha}` + ); + const data = await response.json(); + + if (data.success && data.canchas) { + data.canchas.forEach(cancha => { + const btn = document.createElement('button'); + btn.className = 'cancha-btn'; + btn.textContent = `Cancha ${cancha.numero}`; + btn.dataset.numero = cancha.numero; + + if (!cancha.disponible) { + btn.disabled = true; + btn.classList.add('no-disponible'); + btn.style.opacity = '0.5'; + btn.style.cursor = 'not-allowed'; + } + + canchasGrid.appendChild(btn); + }); + } + } catch (error) { + console.error('Error generando selección de canchas:', error); + canchasGrid.innerHTML = '

Error al cargar canchas disponibles

'; + } +} + +function actualizarEstadoBotonConfirm(modal, dia, hora, cancha) { + const btn = modal.querySelector('.btn-confirmar'); + if (dia && hora && cancha) { + btn.disabled = false; + } else { + btn.disabled = true; + } +} + +function closeModalReprogramar(overlay) { + if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay); +} + +function reprogramarReserva(idReserva, fechaHora, overlay) { + fetch('../php/Actualizar_Horario_Reserva.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id_reserva: idReserva, fecha_hora: fechaHora }) + }) + .then(async res => { + if (!res.ok) throw new Error('HTTP ' + res.status); + const text = await res.text(); + try { + return JSON.parse(text); + } catch (error) { + throw new Error('Respuesta no JSON: ' + text.trim().replace(/\s+/g, ' ')); + } + }) + .then(data => { + if (data.success) { + closeModalReprogramar(overlay); + cargarReservas(); + alert('Reserva reprogramada correctamente'); + } else { + alert('Error al reprogramar: ' + (data.error || 'Error desconocido')); + } + }) + .catch(err => { + console.error('Error reprogramando:', err); + alert('Error al reprogramar la reserva: ' + err.message); + }); +} + +// Inyecta estilos mínimos si no están presentes en el CSS cargado +function ensureReprogramStyles() { + if (document.getElementById('reprogramar-styles')) return; + const css = ` + .reprogramar-btn{margin-left:8px;padding:6px 8px;border-radius:6px;background:rgba(30,125,30,0.12);border:1px solid rgba(30,125,30,0.2);color:#d9f2d9;cursor:pointer;font-weight:600} + .reprogramar-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;z-index:9999;padding:20px} + .reprogramar-modal{width:min(980px,96%);max-height:90vh;overflow:auto;background:#111;border-radius:12px;padding:18px;border:1px solid rgba(30,125,30,0.18)} + .dias-grid{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:14px}.dia-btn{width:110px;height:70px;border-radius:10px;padding:8px;background:rgba(255,255,255,0.03);border:2px solid rgba(255,255,255,0.08);color:#fff;font-weight:700;cursor:pointer;white-space:pre-line;display:flex;flex-direction:column;align-items:center;justify-content:center} + .dia-btn.selected{background:linear-gradient(135deg,#1e7d1e,#145214);border-color:#1e7d1e} + .horarios-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:12px;margin-bottom:14px}.horario-btn{padding:12px;border-radius:10px;background:rgba(255,255,255,0.03);border:2px solid rgba(255,255,255,0.08);color:#fff;font-weight:700;cursor:pointer;white-space:pre-line} + .horario-btn.selected{background:linear-gradient(135deg,#1e7d1e,#145214);border-color:#1e7d1e} + .horario-btn.no-disponible{opacity:0.4 !important;cursor:not-allowed !important} + .canchas-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:12px;margin-bottom:14px}.cancha-btn{padding:12px;border-radius:10px;background:rgba(255,255,255,0.03);border:2px solid rgba(255,255,255,0.08);color:#fff;font-weight:700;cursor:pointer} + .cancha-btn.selected{background:linear-gradient(135deg,#1e7d1e,#145214);border-color:#1e7d1e} + .cancha-btn.no-disponible{opacity:0.4 !important;cursor:not-allowed !important} + .reprogramar-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}.reprogramar-actions .btn-cancelar,.reprogramar-actions .btn-confirmar{padding:10px 14px;border-radius:8px;font-weight:700;border:2px solid rgba(255,255,255,0.08);background:rgba(255,255,255,0.03);color:#fff} + .reprogramar-actions .btn-confirmar:disabled{opacity:0.5;cursor:not-allowed} + `; + const style = document.createElement('style'); + style.id = 'reprogramar-styles'; + style.appendChild(document.createTextNode(css)); + document.head.appendChild(style); +} + +// Delegación: por si los listeners directos no se añadieron (tabla re-renderizada) +document.addEventListener('click', (e) => { + const btn = e.target.closest && e.target.closest('.reprogramar-btn'); + if (!btn) return; + e.stopPropagation(); + if (btn.hasAttribute('disabled') || btn.classList.contains('disabled')) return; + // evitar crear múltiples overlays + if (document.querySelector('.reprogramar-overlay')) return; + const fila = btn.closest('tr'); + const idReserva = fila ? fila.getAttribute('data-id-reserva') : null; + if (idReserva) crearModalReprogramar(idReserva, fila); +}); \ No newline at end of file diff --git a/js/Tabla_Ventas.js b/js/Tabla_Ventas.js new file mode 100644 index 0000000..666adf0 --- /dev/null +++ b/js/Tabla_Ventas.js @@ -0,0 +1,208 @@ +const productos = []; + +document.addEventListener("DOMContentLoaded", () => { + const btnMas = document.querySelector(".btn-mas"); + const btnRegistrar = document.querySelector("#btn-registrar-venta"); + const codigoInput = document.querySelector( + 'input[placeholder="Código del Producto"]' + ); + const nombreInput = document.querySelector( + 'input[placeholder="Nombre del Producto"]' + ); + const cantidadInput = document.querySelector( + 'input[placeholder="Cantidad"]' + ); + + const buscarProducto = (terminoBusqueda) => { + return fetch( + `../php/Buscar_Producto_Ventas.php?codigo=${encodeURIComponent(terminoBusqueda)}` + ).then((response) => response.json()); + }; + + const completarProductoDesdeInputs = (event) => { + // Identificamos cuál de los dos inputs disparó la acción + const inputActual = event ? event.target : null; + + // 1. Si el usuario vació el input en el que está trabajando, limpiamos ambos y salimos + if (inputActual && inputActual.value.trim() === "") { + codigoInput.value = ""; + nombreInput.value = ""; + return; + } + + const codigo = codigoInput.value.trim(); + const nombre = nombreInput.value.trim(); + + // 2. Buscamos priorizando el input que el usuario acaba de modificar + let terminoBusqueda = ""; + if (inputActual === codigoInput) { + terminoBusqueda = codigo; + } else if (inputActual === nombreInput) { + terminoBusqueda = nombre; + } else { + terminoBusqueda = codigo || nombre; + } + + // Si por alguna razón ambos están vacíos, aseguramos la limpieza + if (!terminoBusqueda) { + codigoInput.value = ""; + nombreInput.value = ""; + return; + } + + buscarProducto(terminoBusqueda) + .then((data) => { + if (!data || !data.ID_Producto) { + return; + } + + codigoInput.value = data.ID_Producto; + nombreInput.value = data.Descripcion; + }) + .catch((error) => { + console.error("Error al autocompletar el producto:", error); + }); + }; + + [codigoInput, nombreInput].forEach((input) => { + // Cuando sale del campo (hace clic afuera) + input.addEventListener("blur", completarProductoDesdeInputs); + + // Cuando presiona Enter + input.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + event.preventDefault(); + completarProductoDesdeInputs(event); + } + }); + + // NUEVO: Cuando borra el contenido en tiempo real + input.addEventListener("input", (event) => { + if (event.target.value.trim() === "") { + codigoInput.value = ""; + nombreInput.value = ""; + } + }); + }); + btnMas.addEventListener("click", () => { + const codigo = codigoInput.value.trim(); + const nombre = nombreInput.value.trim(); + const cantidad = parseInt(cantidadInput.value); + + if (!codigo && !nombre) { + alert("Debe ingresar el Código o el Nombre del producto."); + return; + } + + if (isNaN(cantidad) || cantidad <= 0) { + alert("Ingrese una cantidad válida mayor a 0."); + return; + } + + const terminoBusqueda = codigo || nombre; + + buscarProducto(terminoBusqueda) + .then((data) => { + console.log("Producto obtenido:", data); + if (!data || !data.ID_Producto) { + alert("Producto no encontrado."); + return; + } + + const stockDisponible = parseInt(data.Stock_Disponible); + + // Calculamos cuánto de este producto ya tenemos en la lista actual + const cantidadEnLista = productos + .filter((p) => p.id === data.ID_Producto) + .reduce((acc, p) => acc + p.cantidad, 0); + + const cantidadTotalIntentada = cantidad + cantidadEnLista; + + if (cantidadTotalIntentada > stockDisponible) { + alert(`Stock insuficiente. + Disponible: ${stockDisponible} unidades. + En lista: ${cantidadEnLista} unidades. + No puedes agregar ${cantidad} más.`); + return; + } + + const precio = parseFloat(data.Precio_Venta); + const subtotal = precio * cantidad; + + productos.push({ + id: data.ID_Producto, + descripcion: data.Descripcion, + precio: precio, + cantidad: cantidad, + subtotal: subtotal, + stockMaximo: stockDisponible, + }); + + actualizarTabla(); + codigoInput.value = ""; + nombreInput.value = ""; + cantidadInput.value = ""; + }) + .catch((error) => { + console.error("Error al buscar el producto:", error); + }); + }); + + btnRegistrar.addEventListener("click", () => { + if (productos.length === 0) { + alert("No hay productos agregados."); + return; + } + + fetch("../php/Registrar_Venta.php", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(productos), + }) + .then((res) => res.text()) + .then((msg) => { + alert(msg); + productos.length = 0; + actualizarTabla(); + if (typeof window.verificarStockBajoMenu === 'function') { + window.verificarStockBajoMenu(); + } + }) + .catch((err) => { + console.error("Error al registrar la venta:", err); + }); + }); +}); + +function actualizarTabla() { + const tbody = document.querySelector("#tabla-ventas tbody"); + tbody.innerHTML = ""; + + let total = 0; + + productos.forEach((p, index) => { + total += p.subtotal; + + const fila = document.createElement("tr"); + fila.innerHTML = ` + ${p.id} + ${p.descripcion} + $${p.precio.toFixed(2)} + ${p.cantidad} + $${p.subtotal.toFixed(2)} + + + + `; + tbody.appendChild(fila); + }); + + document.getElementById("total").value = `$${total.toFixed(2)}`; +} + +function eliminarProductoVenta(index) { + productos.splice(index, 1); + actualizarTabla(); +} diff --git a/php/Actualizar_Cancha.php b/php/Actualizar_Cancha.php new file mode 100644 index 0000000..b0a5408 --- /dev/null +++ b/php/Actualizar_Cancha.php @@ -0,0 +1,43 @@ + 1) { + $descuento = $descuento / 100; +} +$descuento = round($descuento, 4); +$faltas = $data['faltas']; +$duracion_cupon = $data['duracion_cupon']; + +// Evitar inyección SQL básica +$tipo = mysqli_real_escape_string($conexion, $tipo); + +$sql = "UPDATE canchas SET + Precio = '$precio', + Duracion = '$duracion', + Cant_Canchas = '$cantidad', + Cant_Reservas_Cupon = '$res_cupon', + Descuento_Cupon = '$descuento', + Cant_Faltas = '$faltas', + Duracion_Cupon = '$duracion_cupon' + WHERE Tipo = '$tipo'"; + +if (mysqli_query($conexion, $sql)) { + echo "Configuración de " . $tipo . " actualizada correctamente."; +} else { + echo "Error al actualizar: " . mysqli_error($conexion); +} +?> \ No newline at end of file diff --git a/php/Actualizar_Estado_Reserva.php b/php/Actualizar_Estado_Reserva.php new file mode 100644 index 0000000..908be48 --- /dev/null +++ b/php/Actualizar_Estado_Reserva.php @@ -0,0 +1,32 @@ + false, 'error' => 'Datos incompletos']); + exit; +} + +$id_reserva = intval($data['id_reserva']); +$estado = intval($data['estado']); + +// Validar que el estado sea 0, 1 o 2 +if (!in_array($estado, [0, 1, 2])) { + echo json_encode(['success' => false, 'error' => 'Estado inválido']); + exit; +} + +// Actualizar la reserva +$sql = "UPDATE reservas SET Estado = $estado WHERE ID_Reserva = $id_reserva"; + +if (mysqli_query($conexion, $sql)) { + echo json_encode(['success' => true, 'message' => 'Estado actualizado correctamente']); +} else { + echo json_encode(['success' => false, 'error' => mysqli_error($conexion)]); +} + +mysqli_close($conexion); +?> diff --git a/php/Actualizar_Gastos.php b/php/Actualizar_Gastos.php new file mode 100644 index 0000000..79a0519 --- /dev/null +++ b/php/Actualizar_Gastos.php @@ -0,0 +1,106 @@ +query($sql_get_actual); + + if ($result_actual && $result_actual->num_rows > 0) { + $row_actual = $result_actual->fetch_assoc(); + $id_gasto_actual = $row_actual['ID_Gasto']; + + // Actualizar el Periodo_Hasta del registro actual con la fecha actual + $sql_update = "UPDATE gastos SET Periodo_Hasta = ? WHERE ID_Gasto = ?"; + $stmt_update = $conexion->prepare($sql_update); + + if (!$stmt_update) { + throw new Exception("Error preparando UPDATE: " . $conexion->error); + } + + $stmt_update->bind_param('si', $fecha_hoy, $id_gasto_actual); + + if (!$stmt_update->execute()) { + throw new Exception("Error actualizando período anterior: " . $stmt_update->error); + } + + $stmt_update->close(); + + // Obtener todos los valores del registro que acaba de cerrarse + $sql_get_values = "SELECT * FROM gastos WHERE ID_Gasto = ?"; + $stmt_get_values = $conexion->prepare($sql_get_values); + $stmt_get_values->bind_param('i', $id_gasto_actual); + $stmt_get_values->execute(); + $result_values = $stmt_get_values->get_result(); + $valores_anteriores = $result_values->fetch_assoc(); + $stmt_get_values->close(); + } else { + $valores_anteriores = null; + } + + // Construir INSERT dinámico + $columnas = ['Periodo_Desde', 'Periodo_Hasta']; + $valores = [$fecha_hoy, null]; + $placeholders = ['?', '?']; + $tipos = 'ss'; + + // Agregar columnas de gastos del request + foreach ($gastos as $col => $val) { + $columnas[] = $col; + $valores[] = floatval($val); + $placeholders[] = '?'; + $tipos .= 'd'; + } + + // Agregar columnas restantes del registro anterior (si existen) + if (!empty($valores_anteriores)) { + foreach ($valores_anteriores as $col => $val) { + if (!in_array($col, ['ID_Gasto', 'Periodo_Desde', 'Periodo_Hasta']) && !isset($gastos[$col])) { + $columnas[] = $col; + $valores[] = floatval($val); + $placeholders[] = '?'; + $tipos .= 'd'; + } + } + } + + $sql_insert = "INSERT INTO gastos (" . implode(', ', $columnas) . ") VALUES (" . implode(', ', $placeholders) . ")"; + $stmt_insert = $conexion->prepare($sql_insert); + + if (!$stmt_insert) { + throw new Exception("Error preparando INSERT: " . $conexion->error); + } + + $stmt_insert->bind_param($tipos, ...$valores); + + if (!$stmt_insert->execute()) { + throw new Exception("Error insertando registro: " . $stmt_insert->error); + } + + $nuevo_id = $stmt_insert->insert_id; + $stmt_insert->close(); + + echo json_encode([ + 'success' => true, + 'message' => 'Gastos guardados correctamente', + 'id_gasto' => $nuevo_id + ]); + +} catch (Exception $e) { + echo json_encode([ + 'success' => false, + 'message' => $e->getMessage() + ]); +} +?> diff --git a/php/Actualizar_Horario_Reserva.php b/php/Actualizar_Horario_Reserva.php new file mode 100644 index 0000000..0f9aaa1 --- /dev/null +++ b/php/Actualizar_Horario_Reserva.php @@ -0,0 +1,48 @@ + false, 'error' => 'Cuerpo vacío o no se envió JSON']); + exit; +} + +$data = json_decode($json, true); +if (!is_array($data) || json_last_error() !== JSON_ERROR_NONE) { + $rawPreview = substr($json, 0, 200); + echo json_encode([ + 'success' => false, + 'error' => 'JSON inválido en la solicitud', + 'detalle' => json_last_error_msg(), + 'raw' => $rawPreview, + 'raw_hex' => bin2hex(substr($json, 0, 50)) + ]); + exit; +} + +if (!isset($data['id_reserva']) || !isset($data['fecha_hora'])) { + echo json_encode(['success' => false, 'error' => 'Datos incompletos']); + exit; +} + +$id_reserva = intval($data['id_reserva']); +$fecha_hora = $conexion->real_escape_string($data['fecha_hora']); + +// Validación básica de formato YYYY-MM-DD HH:MM:SS +if (!preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $fecha_hora)) { + echo json_encode(['success' => false, 'error' => 'Formato de fecha_hora inválido. Use YYYY-MM-DD HH:MM:SS']); + exit; +} + +$sql = "UPDATE reservas SET Fecha_Hora = '$fecha_hora' WHERE ID_Reserva = $id_reserva"; + +if (mysqli_query($conexion, $sql)) { + echo json_encode(['success' => true, 'message' => 'Horario actualizado correctamente']); +} else { + echo json_encode(['success' => false, 'error' => mysqli_error($conexion)]); +} + +mysqli_close($conexion); +?> diff --git a/php/Actualizar_Horarios.php b/php/Actualizar_Horarios.php new file mode 100644 index 0000000..6de9e35 --- /dev/null +++ b/php/Actualizar_Horarios.php @@ -0,0 +1,18 @@ + \ No newline at end of file diff --git a/php/Actualizar_Orden_Compra.php b/php/Actualizar_Orden_Compra.php new file mode 100644 index 0000000..0df8704 --- /dev/null +++ b/php/Actualizar_Orden_Compra.php @@ -0,0 +1,136 @@ +connect_error) { + echo json_encode(['success' => false, 'error' => 'No hay conexión con la base de datos.']); + exit; +} + +if (!is_array($data) || !isset($data['id'])) { + echo json_encode(['success' => false, 'error' => 'Faltan datos requeridos.']); + exit; +} + +$idOrden = intval($data['id']); +$estado = isset($data['estado']) ? intval($data['estado']) : (isset($data['aprobada']) ? intval($data['aprobada']) : 0); + +if (!in_array($estado, [0, 1, 2, 3], true)) { + echo json_encode(['success' => false, 'error' => 'Estado no válido.']); + exit; +} + +function tableHasColumn($conexion, $table, $column) +{ + $stmt = $conexion->prepare( + "SELECT COUNT(*) AS count FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?" + ); + if (!$stmt) { + return false; + } + $stmt->bind_param('ss', $table, $column); + $stmt->execute(); + $result = $stmt->get_result(); + $row = $result ? $result->fetch_assoc() : null; + $stmt->close(); + return $row && intval($row['count']) > 0; +} + +$hasEstado = tableHasColumn($conexion, 'ordenes_compra', 'Estado'); +if (!$hasEstado) { + $alterQuery = "ALTER TABLE ordenes_compra ADD COLUMN Estado TINYINT(1) DEFAULT 0"; + if (!$conexion->query($alterQuery)) { + echo json_encode(['success' => false, 'error' => 'No se pudo corregir la estructura de la tabla: ' . $conexion->error]); + exit; + } + $hasEstado = true; +} + +$hasAprobada = tableHasColumn($conexion, 'ordenes_compra', 'Aprobada'); +if (!$hasAprobada) { + $alterQuery = "ALTER TABLE ordenes_compra ADD COLUMN Aprobada TINYINT(1) DEFAULT 0"; + if (!$conexion->query($alterQuery)) { + echo json_encode(['success' => false, 'error' => 'No se pudo corregir la estructura de la tabla: ' . $conexion->error]); + exit; + } + $hasAprobada = true; +} + +$aprobada = in_array($estado, [1, 3], true) ? 1 : 0; + +$conexion->begin_transaction(); + +try { + if ($hasEstado && $hasAprobada) { + $stmt = $conexion->prepare("UPDATE ordenes_compra SET Estado = ?, Aprobada = ? WHERE ID_Orden = ?"); + if (!$stmt) { + throw new Exception('Error en la consulta SQL: ' . $conexion->error); + } + $stmt->bind_param('iii', $estado, $aprobada, $idOrden); + } elseif ($hasEstado) { + $stmt = $conexion->prepare("UPDATE ordenes_compra SET Estado = ? WHERE ID_Orden = ?"); + if (!$stmt) { + throw new Exception('Error en la consulta SQL: ' . $conexion->error); + } + $stmt->bind_param('ii', $estado, $idOrden); + } else { + $stmt = $conexion->prepare("UPDATE ordenes_compra SET Aprobada = ? WHERE ID_Orden = ?"); + if (!$stmt) { + throw new Exception('Error en la consulta SQL: ' . $conexion->error); + } + $stmt->bind_param('ii', $aprobada, $idOrden); + } + + if (!$stmt->execute()) { + throw new Exception('No se pudo actualizar el estado: ' . $stmt->error); + } + $stmt->close(); + + if ($estado === 3) { + $productosStmt = $conexion->prepare("SELECT ID_Producto, Cantidad FROM ordenes_productos WHERE ID_Orden = ?"); + if (!$productosStmt) { + throw new Exception('No se pudieron obtener los productos de la orden.'); + } + $productosStmt->bind_param('i', $idOrden); + if (!$productosStmt->execute()) { + $productosStmt->close(); + throw new Exception('No se pudieron leer los productos de la orden.'); + } + + $productosResult = $productosStmt->get_result(); + $stockStmt = $conexion->prepare("UPDATE productos SET Stock_Disponible = Stock_Disponible + ? WHERE ID_Producto = ?"); + if (!$stockStmt) { + $productosStmt->close(); + throw new Exception('No se pudo preparar la actualización de stock.'); + } + + while ($producto = $productosResult->fetch_assoc()) { + $cantidad = intval($producto['Cantidad']); + $idProducto = intval($producto['ID_Producto']); + + if ($cantidad > 0 && $idProducto > 0) { + $stockStmt->bind_param('ii', $cantidad, $idProducto); + if (!$stockStmt->execute()) { + $stockStmt->close(); + $productosStmt->close(); + throw new Exception('No se pudo actualizar el stock de los productos.'); + } + } + } + + $stockStmt->close(); + $productosStmt->close(); + } + + $conexion->commit(); + echo json_encode(['success' => true, 'message' => 'Estado de la orden actualizado correctamente.']); +} catch (Exception $e) { + $conexion->rollback(); + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} + +$conexion->close(); +?> \ No newline at end of file diff --git a/php/Actualizar_Perfil.php b/php/Actualizar_Perfil.php new file mode 100644 index 0000000..d6f691f --- /dev/null +++ b/php/Actualizar_Perfil.php @@ -0,0 +1,54 @@ + false, 'message' => 'Sesión no iniciada']); + exit(); +} + +$input = json_decode(file_get_contents('php://input'), true); +if (!$input) { + echo json_encode(['success' => false, 'message' => 'No se recibieron datos']); + exit(); +} + +require_once 'conexion.php'; + +$id = isset($input['id']) ? intval($input['id']) : 0; +$usuario = trim($input['usuario'] ?? ''); +$correo = trim($input['correo'] ?? ''); +$telefono = trim($input['telefono'] ?? ''); + +// Validar campos obligatorios (solo usuario es obligatorio) +if ($id <= 0 || empty($usuario)) { + echo json_encode(['success' => false, 'message' => 'Usuario es obligatorio']); + exit(); +} + +// Verificar que el usuario logueado sea el que intenta modificar su perfil +if ($id !== intval($_SESSION['id_usuario'])) { + echo json_encode(['success' => false, 'message' => 'No permitido']); + exit(); +} + +// Actualizar solo usuario, correo y teléfono +$query = "UPDATE usuarios SET Usuario = ?, Correo = ?, Telefono = ? WHERE ID_Usuario = ?"; +$stmt = $conexion->prepare($query); + +if (!$stmt) { + echo json_encode(['success' => false, 'message' => 'Error en consulta: ' . $conexion->error]); + exit(); +} + +$stmt->bind_param('sssi', $usuario, $correo, $telefono, $id); + +if ($stmt->execute()) { + echo json_encode(['success' => true, 'message' => 'Perfil actualizado']); +} else { + echo json_encode(['success' => false, 'message' => 'Error al actualizar: ' . $stmt->error]); +} + +$stmt->close(); +$conexion->close(); +?> \ No newline at end of file diff --git a/php/Actualizar_Productos_Orden.php b/php/Actualizar_Productos_Orden.php new file mode 100644 index 0000000..a2379c9 --- /dev/null +++ b/php/Actualizar_Productos_Orden.php @@ -0,0 +1,46 @@ +connect_error) { + echo json_encode(['success' => false, 'error' => 'No hay conexión con la base de datos.']); + exit; +} + +if (!isset($data['id']) || !isset($data['productos']) || !is_array($data['productos'])) { + echo json_encode(['success' => false, 'error' => 'Faltan datos requeridos.']); + exit; +} + +$idOrden = intval($data['id']); + +$stmt = $conexion->prepare("UPDATE ordenes_productos SET Cantidad = ? WHERE ID_Orden = ? AND ID_Producto = ?"); +if (!$stmt) { + echo json_encode(['success' => false, 'error' => 'Error en la consulta SQL.']); + exit; +} + +foreach ($data['productos'] as $producto) { + $idProducto = intval($producto['id']); + $cantidad = intval($producto['cantidad']); + + if ($idProducto <= 0 || $cantidad <= 0) { + $stmt->close(); + echo json_encode(['success' => false, 'error' => 'Datos de productos inválidos.']); + exit; + } + + $stmt->bind_param('iii', $cantidad, $idOrden, $idProducto); + if (!$stmt->execute()) { + $stmt->close(); + echo json_encode(['success' => false, 'error' => 'No se pudieron guardar los cambios.']); + exit; + } +} + +$stmt->close(); +$conexion->close(); + +echo json_encode(['success' => true, 'message' => 'Cantidades actualizadas correctamente.']); +?> \ No newline at end of file diff --git a/php/Actualizar_Usuario.php b/php/Actualizar_Usuario.php new file mode 100644 index 0000000..ded5aad --- /dev/null +++ b/php/Actualizar_Usuario.php @@ -0,0 +1,55 @@ + false, 'message' => 'Método no permitido']); + exit(); +} + +$id = isset($_POST['id']) ? intval($_POST['id']) : 0; +$usuario = isset($_POST['usuario']) ? trim($_POST['usuario']) : ''; +$contraseña = isset($_POST['contraseña']) ? trim($_POST['contraseña']) : ''; +$confirmar_contraseña = isset($_POST['confirmar_contraseña']) ? trim($_POST['confirmar_contraseña']) : ''; +$rol = isset($_POST['rol']) ? trim($_POST['rol']) : ''; +$nombre = isset($_POST['nombre']) ? trim($_POST['nombre']) : ''; +$apellido = isset($_POST['apellido']) ? trim($_POST['apellido']) : ''; +$dni = isset($_POST['dni']) ? trim($_POST['dni']) : ''; +$correo = isset($_POST['correo']) ? trim($_POST['correo']) : ''; +$telefono = isset($_POST['telefono']) ? trim($_POST['telefono']) : ''; + +if ($id <= 0 || empty($usuario) || empty($contraseña) || !in_array($rol, ['admin', 'cantina', 'cancha']) || empty($nombre) || empty($apellido) || empty($dni)) { + echo json_encode(['success' => false, 'message' => 'Datos inválidos']); + exit(); +} + +if (strlen($contraseña) < 6) { + echo json_encode(['success' => false, 'message' => 'La contraseña debe tener al menos 6 carácteres']); + exit(); +} + +if ($contraseña !== $confirmar_contraseña) { + echo json_encode(['success' => false, 'message' => 'Las contraseñas no coinciden']); + exit(); +} + +$query = "UPDATE usuarios SET Usuario = ?, Contraseña = ?, Rol = ?, Nombre = ?, Apellido = ?, DNI = ?, Correo = ?, Telefono = ? WHERE ID_Usuario = ?"; +$stmt = $conexion->prepare($query); +if (!$stmt) { + echo json_encode(['success' => false, 'message' => 'Error en consulta: ' . $conexion->error]); + exit(); +} + +$stmt->bind_param('ssssssssi', $usuario, $contraseña, $rol, $nombre, $apellido, $dni, $correo, $telefono, $id); + +if ($stmt->execute()) { + echo json_encode(['success' => true, 'message' => 'Usuario actualizado']); +} else { + echo json_encode(['success' => false, 'message' => 'Error al actualizar: ' . $stmt->error]); +} + +$stmt->close(); +$conexion->close(); +?> \ No newline at end of file diff --git a/php/Agregar_Producto.php b/php/Agregar_Producto.php new file mode 100644 index 0000000..921cf0c --- /dev/null +++ b/php/Agregar_Producto.php @@ -0,0 +1,15 @@ +prepare("INSERT INTO productos (Descripcion, Precio_Venta, Precio_Compra, Stock_Disponible) VALUES (?, ?, ?, ?)"); +$stmt->bind_param("sddi", $descripcion, $precio_venta, $precio_compra, $cantidad); +$stmt->execute(); + +echo "Producto agregado correctamente."; +?> diff --git a/php/Agregar_Producto_Proveedor.php b/php/Agregar_Producto_Proveedor.php new file mode 100644 index 0000000..59422c3 --- /dev/null +++ b/php/Agregar_Producto_Proveedor.php @@ -0,0 +1,18 @@ +prepare("INSERT INTO proveedores_productos (ID_Proveedor, ID_Producto) VALUES (?, ?)"); +$stmt->bind_param("ii", $idProveedor, $idProducto); + +if ($stmt->execute()) { + echo json_encode(['success' => true]); +} else { + echo json_encode(['success' => false, 'error' => $stmt->error]); +} +?> \ No newline at end of file diff --git a/php/Buscar_Producto_Orden.php b/php/Buscar_Producto_Orden.php new file mode 100644 index 0000000..8278f6e --- /dev/null +++ b/php/Buscar_Producto_Orden.php @@ -0,0 +1,26 @@ + "Código no especificado"]); + exit; +} + +$codigo = $_GET['codigo']; +require_once 'conexion.php'; + +if ($conexion->connect_error) { + http_response_code(500); + echo json_encode(["error" => "Error de conexión"]); + exit; +} + +$stmt = $conexion->prepare("SELECT ID_Producto, Descripcion FROM productos WHERE ID_Producto = ?"); +$stmt->bind_param("i", $codigo); +$stmt->execute(); +$result = $stmt->get_result(); + +$producto = $result->fetch_assoc(); +echo json_encode($producto ?: []); +?> diff --git a/php/Buscar_Producto_Ventas.php b/php/Buscar_Producto_Ventas.php new file mode 100644 index 0000000..4634107 --- /dev/null +++ b/php/Buscar_Producto_Ventas.php @@ -0,0 +1,41 @@ +prepare($sql); + $busquedaInt = intval($busqueda); + $stmt->bind_param("i", $busquedaInt); +} else { + // Búsqueda por descripción (si contiene el texto) + $sql = "SELECT ID_Producto, Descripcion, Precio_Venta, Stock_Disponible + FROM productos + WHERE Descripcion LIKE ? + LIMIT 1"; + + $stmt = $conexion->prepare($sql); + $stmt->bind_param("s", $terminoLike); +} + +$stmt->execute(); +$result = $stmt->get_result(); +$producto = $result->fetch_assoc(); + +echo json_encode($producto); +?> \ No newline at end of file diff --git a/php/Buscar_Proveedor.php b/php/Buscar_Proveedor.php new file mode 100644 index 0000000..78c0c0c --- /dev/null +++ b/php/Buscar_Proveedor.php @@ -0,0 +1,12 @@ +prepare("SELECT Nombre, Telefono FROM proveedores WHERE ID_Proveedor = ?"); +$stmt->bind_param("i", $codigo); +$stmt->execute(); +$result = $stmt->get_result(); + +echo json_encode($result->fetch_assoc()); +?> diff --git a/php/Buscar_Proveedor_Editar.php b/php/Buscar_Proveedor_Editar.php new file mode 100644 index 0000000..f581bc7 --- /dev/null +++ b/php/Buscar_Proveedor_Editar.php @@ -0,0 +1,12 @@ +prepare("SELECT ID_Proveedor, Nombre, Telefono FROM proveedores WHERE ID_Proveedor = ?"); +$stmt->bind_param("i", $codigo); +$stmt->execute(); +$result = $stmt->get_result(); + +echo json_encode($result->fetch_assoc()); +?> diff --git a/php/Cambiar_Contrasena.php b/php/Cambiar_Contrasena.php new file mode 100644 index 0000000..36ee70d --- /dev/null +++ b/php/Cambiar_Contrasena.php @@ -0,0 +1,91 @@ + false, 'message' => 'Sesión no iniciada']); + exit(); +} + +$input = json_decode(file_get_contents('php://input'), true); +if (!$input) { + echo json_encode(['success' => false, 'message' => 'No se recibieron datos']); + exit(); +} + +require_once 'conexion.php'; + +$id = isset($input['id']) ? intval($input['id']) : 0; +$contrasenia_actual = $input['contrasenia_actual'] ?? ''; +$contrasenia_nueva = $input['contrasenia_nueva'] ?? ''; + +// Validar campos obligatorios +if ($id <= 0 || empty($contrasenia_actual) || empty($contrasenia_nueva)) { + echo json_encode(['success' => false, 'message' => 'Todos los campos son obligatorios']); + exit(); +} + +// Verificar que el usuario logueado sea el que intenta modificar su contraseña +if ($id !== intval($_SESSION['id_usuario'])) { + echo json_encode(['success' => false, 'message' => 'No permitido']); + exit(); +} + +// Obtener la contraseña actual del usuario +$query = "SELECT Contraseña FROM usuarios WHERE ID_Usuario = ?"; +$stmt = $conexion->prepare($query); + +if (!$stmt) { + echo json_encode(['success' => false, 'message' => 'Error en consulta: ' . $conexion->error]); + exit(); +} + +$stmt->bind_param('i', $id); +$stmt->execute(); +$result = $stmt->get_result(); +$usuario = $result->fetch_assoc(); + +if (!$usuario) { + echo json_encode(['success' => false, 'message' => 'Usuario no encontrado']); + exit(); +} + +// Verificar que la contraseña actual sea correcta +// En este sistema, las contraseñas se guardan en texto plano +if ($contrasenia_actual === $usuario['Contraseña']) { + // La contraseña actual es correcta +} else { + echo json_encode(['success' => false, 'message' => 'La contraseña actual es incorrecta']); + exit(); +} + +// Validar longitud mínima de la nueva contraseña +if (strlen($contrasenia_nueva) < 6) { + echo json_encode(['success' => false, 'message' => 'La contraseña debe tener al menos 6 caracteres']); + exit(); +} + +// Guardar la nueva contraseña en texto plano (compatible con el sistema actual) +$contrasenia_nueva_guardada = $contrasenia_nueva; + +// Actualizar la contraseña +$update_query = "UPDATE usuarios SET Contraseña = ? WHERE ID_Usuario = ?"; +$update_stmt = $conexion->prepare($update_query); + +if (!$update_stmt) { + echo json_encode(['success' => false, 'message' => 'Error en consulta: ' . $conexion->error]); + exit(); +} + +$update_stmt->bind_param('si', $contrasenia_nueva_guardada, $id); + +if ($update_stmt->execute()) { + echo json_encode(['success' => true, 'message' => 'Contraseña actualizada exitosamente']); +} else { + echo json_encode(['success' => false, 'message' => 'Error al actualizar: ' . $update_stmt->error]); +} + +$update_stmt->close(); +$stmt->close(); +$conexion->close(); +?> diff --git a/php/Cantina.php b/php/Cantina.php new file mode 100644 index 0000000..a389fef --- /dev/null +++ b/php/Cantina.php @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/php/Editar_Gasto.php b/php/Editar_Gasto.php new file mode 100644 index 0000000..9a9be60 --- /dev/null +++ b/php/Editar_Gasto.php @@ -0,0 +1,126 @@ +real_escape_string($columna_actual_formatted) . "'"; + $result_check = $conexion->query($sql_check); + + if ($result_check === false || $result_check->num_rows === 0) { + throw new Exception("La columna de gasto no existe: " . $conexion->error); + } + + // Renombrar columna (Nota: esto cambia el nombre también en los registros históricos) + $sql_rename = "ALTER TABLE gastos CHANGE COLUMN `$columna_actual_formatted` `$nombre_columna` DECIMAL(10,2)"; + + if (!$conexion->query($sql_rename)) { + throw new Exception("Error al renombrar columna: " . $conexion->error); + } + } else { + $nombre_columna = strtolower(str_replace(' ', '_', $columna_actual)); + } + + // Obtener el registro actual COMPLETO + $sql_get_actual = "SELECT * FROM gastos WHERE Periodo_Hasta IS NULL ORDER BY Periodo_Desde DESC LIMIT 1"; + $result_actual = $conexion->query($sql_get_actual); + + if ($result_actual && $result_actual->num_rows > 0) { + $valores_anteriores = $result_actual->fetch_assoc(); + $id_gasto_actual = $valores_anteriores['ID_Gasto']; + + // 1. Cerrar el periodo actual poniendo la fecha de hoy + $sql_update = "UPDATE gastos SET Periodo_Hasta = ? WHERE ID_Gasto = ?"; + $stmt_update = $conexion->prepare($sql_update); + $stmt_update->bind_param('si', $fecha_hoy, $id_gasto_actual); + + if (!$stmt_update->execute()) { + throw new Exception("Error cerrando periodo anterior: " . $stmt_update->error); + } + $stmt_update->close(); + + // 2. Preparar el nuevo registro (INSERT) + $columnas = ['Periodo_Desde', 'Periodo_Hasta']; + $valores = [$fecha_hoy, null]; + $placeholders = ['?', '?']; + $tipos = 'ss'; + + // Recorrer los valores del registro que acabamos de cerrar + foreach ($valores_anteriores as $col => $val) { + // Ignorar las columnas de control + if (in_array($col, ['ID_Gasto', 'Periodo_Desde', 'Periodo_Hasta'])) { + continue; + } + + $columnas[] = "`$col`"; // Usamos backticks por seguridad en los nombres + + // Si es la columna que estamos editando, guardamos el NUEVO monto. Si no, arrastramos el viejo. + if ($col === $nombre_columna) { + $valores[] = $monto; + } else { + $valores[] = floatval($val); + } + + $placeholders[] = '?'; + $tipos .= 'd'; + } + + // 3. Insertar la nueva "versión" de los gastos + $sql_insert = "INSERT INTO gastos (" . implode(', ', $columnas) . ") VALUES (" . implode(', ', $placeholders) . ")"; + $stmt_insert = $conexion->prepare($sql_insert); + + if (!$stmt_insert) { + throw new Exception("Error preparando INSERT: " . $conexion->error); + } + + $stmt_insert->bind_param($tipos, ...$valores); + + if (!$stmt_insert->execute()) { + throw new Exception("Error insertando el nuevo periodo: " . $stmt_insert->error); + } + + $stmt_insert->close(); + } else { + throw new Exception("No hay un periodo de gastos activo para editar."); + } + + echo json_encode([ + 'success' => true, + 'message' => 'Gasto editado y nuevo periodo registrado correctamente' + ]); + +} catch (Exception $e) { + echo json_encode([ + 'success' => false, + 'message' => $e->getMessage() + ]); +} +?> \ No newline at end of file diff --git a/php/Editar_Producto.php b/php/Editar_Producto.php new file mode 100644 index 0000000..668db6c --- /dev/null +++ b/php/Editar_Producto.php @@ -0,0 +1,27 @@ + false, "error" => "Datos inválidos"]); + exit; +} + +require_once 'conexion.php'; + +$id = $conexion->real_escape_string($data["id"]); +$descripcion = $conexion->real_escape_string($data["descripcion"]); +$precio_venta = floatval($data["precio_venta"]); +$precio_compra = floatval($data["precio_compra"]); +$cantidad = intval($data["cantidad"]); + +$query = "UPDATE productos SET Descripcion=?, Precio_Venta=?, Precio_Compra=?, Stock_Disponible=? WHERE ID_Producto=?"; +$stmt = $conexion->prepare($query); +$stmt->bind_param("sddii", $descripcion, $precio_venta, $precio_compra, $cantidad, $id); + +if ($stmt->execute()) { + echo json_encode(["success" => true]); +} else { + echo json_encode(["success" => false, "error" => $conexion->error]); +} diff --git a/php/Editar_Proveedor.php b/php/Editar_Proveedor.php new file mode 100644 index 0000000..4d530e6 --- /dev/null +++ b/php/Editar_Proveedor.php @@ -0,0 +1,54 @@ +connect_error) { + echo json_encode([ + "success" => false, + "error" => "Error de conexión: " . $conexion->connect_error + ]); + exit; +} + +$raw = file_get_contents("php://input"); +// file_put_contents("log_debug.txt", "RAW:\n" . $raw . "\n", FILE_APPEND); <-- línea comentada / eliminada + +$data = json_decode($raw, true); + +if (!$data) { + echo json_encode([ + "success" => false, + "error" => "JSON inválido", + "raw" => $raw + ]); + exit; +} + +$id = isset($data['id']) ? (int)$data['id'] : null; +$nombre = $data['nombre'] ?? null; +$telefono = $data['telefono'] ?? null; + +if (!$id || !$nombre || !$telefono) { + echo json_encode(["success" => false, "error" => "Datos incompletos"]); + exit; +} + +$stmt = $conexion->prepare("UPDATE proveedores SET Nombre = ?, Telefono = ? WHERE ID_Proveedor = ?"); +if (!$stmt) { + echo json_encode(["success" => false, "error" => "Error en prepare(): " . $conexion->error]); + exit; +} + +$stmt->bind_param("ssi", $nombre, $telefono, $id); + +if ($stmt->execute()) { + echo json_encode(["success" => true]); +} else { + echo json_encode(["success" => false, "error" => $stmt->error]); +} + +$stmt->close(); +$conexion->close(); diff --git a/php/Eliminar_Gasto.php b/php/Eliminar_Gasto.php new file mode 100644 index 0000000..7d747e6 --- /dev/null +++ b/php/Eliminar_Gasto.php @@ -0,0 +1,48 @@ +real_escape_string($nombre_columna) . "'"; + $result_check = $conexion->query($sql_check); + + if ($result_check === false || $result_check->num_rows === 0) { + throw new Exception("La columna de gasto no existe:"); + } + + // Eliminar la columna + $sql_drop = "ALTER TABLE gastos DROP COLUMN `$nombre_columna`"; + + if (!$conexion->query($sql_drop)) { + throw new Exception("Error al eliminar columna: " . $conexion->error); + } + + echo json_encode([ + 'success' => true, + 'message' => 'Gasto eliminado correctamente' + ]); + +} catch (Exception $e) { + echo json_encode([ + 'success' => false, + 'message' => $e->getMessage() + ]); +} +?> diff --git a/php/Eliminar_Producto.php b/php/Eliminar_Producto.php new file mode 100644 index 0000000..00783bf --- /dev/null +++ b/php/Eliminar_Producto.php @@ -0,0 +1,68 @@ + false, 'message' => 'ID de producto no especificado.']); + exit; +} + +$id = intval($_GET['id']); + +require_once 'conexion.php'; + +header('Content-Type: application/json'); + +// Verificar si la columna Estado existe +$checkColumn = $conexion->query("SHOW COLUMNS FROM productos WHERE Field='Estado'"); +$columnaExists = $checkColumn && $checkColumn->num_rows > 0; + +if ($columnaExists) { + // Soft delete: marcar el producto como inactivo + $conexion->query("SET FOREIGN_KEY_CHECKS=0"); + + $stmt = $conexion->prepare("UPDATE productos SET Estado = 0 WHERE ID_Producto = ?"); + $stmt->bind_param("i", $id); + $stmt->execute(); + + $conexion->query("SET FOREIGN_KEY_CHECKS=1"); + + if ($stmt->affected_rows > 0) { + echo json_encode(['success' => true, 'message' => 'Producto eliminado correctamente.']); + } else { + echo json_encode(['success' => false, 'message' => 'Error al eliminar el producto o no existe.']); + } +} else { + // Fallback: verificar si la columna Activo existe + $checkColumnActivo = $conexion->query("SHOW COLUMNS FROM productos WHERE Field='Activo'"); + $activoExists = $checkColumnActivo && $checkColumnActivo->num_rows > 0; + + if ($activoExists) { + $conexion->query("SET FOREIGN_KEY_CHECKS=0"); + + $stmt = $conexion->prepare("UPDATE productos SET Activo = 0 WHERE ID_Producto = ?"); + $stmt->bind_param("i", $id); + $stmt->execute(); + + $conexion->query("SET FOREIGN_KEY_CHECKS=1"); + + if ($stmt->affected_rows > 0) { + echo json_encode(['success' => true, 'message' => 'Producto eliminado correctamente.']); + } else { + echo json_encode(['success' => false, 'message' => 'Error al eliminar el producto o no existe.']); + } + } else { + $conexion->query("SET FOREIGN_KEY_CHECKS=0"); + + $stmt = $conexion->prepare("DELETE FROM productos WHERE ID_Producto = ?"); + $stmt->bind_param("i", $id); + $stmt->execute(); + + $conexion->query("SET FOREIGN_KEY_CHECKS=1"); + + if ($stmt->affected_rows > 0) { + echo json_encode(['success' => true, 'message' => 'Producto eliminado correctamente.']); + } else { + echo json_encode(['success' => false, 'message' => 'Error al eliminar el producto o no existe.']); + } + } +} +?> diff --git a/php/Eliminar_Producto_Proveedor.php b/php/Eliminar_Producto_Proveedor.php new file mode 100644 index 0000000..2188bde --- /dev/null +++ b/php/Eliminar_Producto_Proveedor.php @@ -0,0 +1,15 @@ +prepare("DELETE FROM proveedores_productos WHERE ID_Proveedor = ? AND ID_Producto = ?"); +$stmt->bind_param("ii", $idProveedor, $idProducto); + +if ($stmt->execute()) { + echo json_encode(['success' => true]); +} else { + echo json_encode(['success' => false, 'error' => $stmt->error]); +} diff --git a/php/Eliminar_Proveedor.php b/php/Eliminar_Proveedor.php new file mode 100644 index 0000000..1b08af5 --- /dev/null +++ b/php/Eliminar_Proveedor.php @@ -0,0 +1,73 @@ +connect_error) { + echo json_encode(['success' => false, 'error' => 'Error de conexión']); + exit; +} + +if ($id <= 0) { + echo json_encode(['success' => false, 'error' => 'ID no proporcionado']); + exit; +} + +$conexion->begin_transaction(); + +try { + $stmt = $conexion->prepare("DELETE FROM ordenes_productos WHERE ID_Orden IN (SELECT ID_Orden FROM ordenes_compra WHERE ID_Proveedor = ?)"); + $stmt->bind_param("i", $id); + $stmt->execute(); + $stmt->close(); + + $stmt = $conexion->prepare("DELETE FROM ordenes_compra WHERE ID_Proveedor = ?"); + $stmt->bind_param("i", $id); + $stmt->execute(); + $stmt->close(); + + $stmt = $conexion->prepare("DELETE FROM proveedores_productos WHERE ID_Proveedor = ?"); + $stmt->bind_param("i", $id); + $stmt->execute(); + $stmt->close(); + + $stmt = $conexion->prepare("DELETE FROM proveedores WHERE ID_Proveedor = ?"); + $stmt->bind_param("i", $id); + $stmt->execute(); + + if ($stmt->affected_rows === 0) { + $stmt->close(); + $conexion->rollback(); + echo json_encode(['success' => false, 'error' => 'Proveedor no encontrado']); + exit; + } + + $stmt->close(); + $conexion->commit(); + echo json_encode(['success' => true, 'message' => 'Proveedor eliminado']); +} catch (Exception $e) { + $conexion->rollback(); + echo json_encode(['success' => false, 'error' => 'Error al eliminar proveedor: ' . $e->getMessage()]); +} + +$conexion->close(); +?> diff --git a/php/Eliminar_Usuario.php b/php/Eliminar_Usuario.php new file mode 100644 index 0000000..467266c --- /dev/null +++ b/php/Eliminar_Usuario.php @@ -0,0 +1,57 @@ + false, 'message' => 'Método no permitido']); + exit(); +} + +// Obtener id del usuario a eliminar +$id = isset($_POST['id']) ? intval($_POST['id']) : 0; + +if ($id <= 0) { + echo json_encode(['success' => false, 'message' => 'ID inválido']); + exit(); +} + +// Prevenir la eliminación del usuario actual (si es necesario) +if (isset($_SESSION['id_usuario']) && $_SESSION['id_usuario'] == $id) { + echo json_encode(['success' => false, 'message' => 'No puedes eliminar tu propia cuenta']); + exit(); +} + +// Obtener el ID del primer usuario registrado para protegerlo +$primerUsuarioQuery = "SELECT MIN(ID_Usuario) AS primer_id FROM usuarios"; +$primerUsuarioStmt = $conexion->prepare($primerUsuarioQuery); +$primerUsuarioStmt->execute(); +$primerUsuarioResult = $primerUsuarioStmt->get_result(); +$primerUsuarioId = (int) $primerUsuarioResult->fetch_assoc()['primer_id']; +$primerUsuarioStmt->close(); + +if ($primerUsuarioId > 0 && $id === $primerUsuarioId) { + echo json_encode(['success' => false, 'message' => 'El administrador supremo no puede ser eliminado']); + exit(); +} + +// Eliminar el usuario +$query = "DELETE FROM usuarios WHERE ID_Usuario = ?"; +$stmt = $conexion->prepare($query); +$stmt->bind_param("i", $id); + +if ($stmt->execute()) { + if ($stmt->affected_rows > 0) { + echo json_encode(['success' => true, 'message' => 'Usuario eliminado exitosamente']); + } else { + echo json_encode(['success' => false, 'message' => 'El usuario no existe']); + } +} else { + echo json_encode(['success' => false, 'message' => 'Error al eliminar el usuario']); +} + +$stmt->close(); +$conexion->close(); +?> \ No newline at end of file diff --git a/php/Enviar_Codigo_WSP.php b/php/Enviar_Codigo_WSP.php new file mode 100644 index 0000000..58e7a30 --- /dev/null +++ b/php/Enviar_Codigo_WSP.php @@ -0,0 +1,41 @@ + false, 'message' => 'Falta el teléfono']); + exit; +} + +// 1. Limpiamos el número y le agregamos el 549 de Argentina si no lo tiene +$telefono = preg_replace('/[^0-9]/', '', $data['telefono']); +if (substr($telefono, 0, 3) !== '549') { + $telefono = '549' . ltrim($telefono, '0'); +} + +$nombre = $data['nombre']; + +// 2. Generamos el código +$codigoGenerado = (string) rand(1000, 9999); +$mensaje = "Hola $nombre, tu código de verificación para Complejo Cap1tan es: *$codigoGenerado*"; + +// 3. Ejecutamos Python en SEGUNDO PLANO (Asíncrono para Windows) +$ruta_script = "C:\\xampp\\htdocs\\TP-Taller\\ProyectoTaller\\python\\Enviar_WSP.py"; + +// Armamos el comando base +$comando_python = escapeshellcmd("python \"$ruta_script\" \"$telefono\" \"$mensaje\""); + +// "start /B" inicia el proceso sin abrir una nueva ventana de CMD. +// "1> NUL 2>&1" tira a la basura los mensajes de consola de Python para que PHP no se quede esperando a leerlos. +// popen() y pclose() abren el proceso y lo cierran al instante en PHP, dejando a Python trabajando solo. +pclose(popen("start /B " . $comando_python . " 1> NUL 2>&1", "r")); + +// 4. Devolvemos la respuesta INMEDIATAMENTE al JavaScript +echo json_encode([ + 'success' => true, + 'codigo' => $codigoGenerado, + 'debug_python' => 'Ejecutando en segundo plano... (No se captura salida)' +]); +?> \ No newline at end of file diff --git a/php/Ganacia_Neta.php b/php/Ganacia_Neta.php new file mode 100644 index 0000000..0292ecd --- /dev/null +++ b/php/Ganacia_Neta.php @@ -0,0 +1,88 @@ +query($sql_columnas); + + if (!$result_columnas) { + throw new Exception("Error al obtener las columnas de gastos: " . $conexion->error); + } + + $columnas_suma = []; + while ($row = $result_columnas->fetch_assoc()) { + $col = $row['Field']; + // Ignoramos las columnas de control de historial + if (!in_array($col, ['ID_Gasto', 'Periodo_Desde', 'Periodo_Hasta'])) { + // Usamos COALESCE por si algún valor es NULL, para que la suma matemática no falle + $columnas_suma[] = "COALESCE(g.`$col`, 0)"; + } + } + + // Armamos el fragmento de la suma: "COALESCE(g.sueldo,0) + COALESCE(g.luz,0)..." + $suma_gastos_sql = empty($columnas_suma) ? "0" : implode(' + ', $columnas_suma); + + // 2. CONSTRUIR LA CONSULTA PRINCIPAL + // Usamos LAST_DAY() para asegurar que tomamos la versión de gastos de fin de mes + // y evitamos duplicar los ingresos si hubo cambios a mitad de mes. + $sql_reporte = " + SELECT + ingresos.Mes, + COALESCE(ingresos.Ingreso_Bruto, 0) AS Ingreso_Bruto, + COALESCE(ingresos.Costo, 0) AS Costo, + COALESCE(ingresos.Ganancia_Bruta, 0) AS Ganancia_Bruta, + ($suma_gastos_sql) AS Gastos_Totales, + COALESCE(ingresos.Ganancia_Bruta, 0) - ($suma_gastos_sql) AS Ganancia_Neta + FROM ( + SELECT + DATE_FORMAT(v.Fecha, '%Y-%m') AS Mes, + LAST_DAY(MAX(v.Fecha)) AS Fin_De_Mes, + SUM(p.Precio_Venta * vp.Cantidad) AS Ingreso_Bruto, + SUM(p.Precio_Compra * vp.Cantidad) AS Costo, + SUM((p.Precio_Venta - p.Precio_Compra) * vp.Cantidad) AS Ganancia_Bruta + FROM ventas v + JOIN ventas_productos vp ON v.ID_Venta = vp.ID_Venta + JOIN productos p ON vp.ID_Producto = p.ID_Producto + GROUP BY Mes + ) ingresos + LEFT JOIN gastos g + ON g.Periodo_Desde <= ingresos.Fin_De_Mes + AND (g.Periodo_Hasta IS NULL OR g.Periodo_Hasta > ingresos.Fin_De_Mes) + ORDER BY ingresos.Mes DESC; + "; + + // 3. EJECUTAR LA CONSULTA + $result_reporte = $conexion->query($sql_reporte); + + if (!$result_reporte) { + throw new Exception("Error al generar el reporte: " . $conexion->error); + } + + $reporte = []; + while ($row = $result_reporte->fetch_assoc()) { + // Convertimos los valores numéricos de strings a floats para el JSON + $reporte[] = [ + 'Mes' => $row['Mes'], + 'Ingreso_Bruto' => floatval($row['Ingreso_Bruto']), + 'Costo' => floatval($row['Costo']), + 'Ganancia_Bruta' => floatval($row['Ganancia_Bruta']), + 'Gastos_Totales' => floatval($row['Gastos_Totales']), + 'Ganancia_Neta' => floatval($row['Ganancia_Neta']) + ]; + } + + echo json_encode([ + 'success' => true, + 'data' => $reporte + ]); + +} catch (Exception $e) { + echo json_encode([ + 'success' => false, + 'message' => $e->getMessage() + ]); +} +?> \ No newline at end of file diff --git a/php/Guardar_Proveedor.php b/php/Guardar_Proveedor.php new file mode 100644 index 0000000..8cce40a --- /dev/null +++ b/php/Guardar_Proveedor.php @@ -0,0 +1,26 @@ +connect_error) { + echo json_encode(['success' => false, 'error' => 'Error de conexión']); + exit; +} + +$data = json_decode(file_get_contents("php://input"), true); +$nombre = $data['nombre'] ?? ''; +$telefono = $data['telefono'] ?? ''; + +$stmt = $conexion->prepare("INSERT INTO proveedores (Nombre, Telefono) VALUES (?, ?)"); +$stmt->bind_param("ss", $nombre, $telefono); + +if ($stmt->execute()) { + echo json_encode(['success' => true]); +} else { + echo json_encode(['success' => false, 'error' => $conexion->error]); +} + +$stmt->close(); +$conexion->close(); +?> + diff --git a/php/Guardar_Reserva.php b/php/Guardar_Reserva.php new file mode 100644 index 0000000..1db8bca --- /dev/null +++ b/php/Guardar_Reserva.php @@ -0,0 +1,225 @@ + false, + 'message' => 'Datos no recibidos' + ]); + exit; +} + +$nombre = isset($data['nombre']) ? trim($data['nombre']) : ''; +$telefono = isset($data['telefono']) ? trim($data['telefono']) : ''; +$dni = isset($data['dni']) ? trim($data['dni']) : ''; +$fecha = isset($data['fecha']) ? $data['fecha'] : ''; +$horario = isset($data['horario']) ? $data['horario'] : ''; +$tipo = isset($data['tipo']) ? $data['tipo'] : ''; +$numeroCancha = isset($data['numeroCancha']) ? intval($data['numeroCancha']) : 0; +$monto = isset($data['monto']) ? floatval($data['monto']) : 0; +$incluirQuincho = isset($data['incluirQuincho']) ? $data['incluirQuincho'] : false; +$montoQuincho = isset($data['montoQuincho']) ? floatval($data['montoQuincho']) : 0; +$idCancha = isset($data['idCancha']) ? intval($data['idCancha']) : null; + +// Validar datos requeridos +if (empty($nombre) || empty($telefono) || empty($dni) || empty($fecha) || empty($horario) || empty($tipo) || $numeroCancha === 0 || $monto === 0) { + echo json_encode([ + 'success' => false, + 'message' => 'Datos incompletos' + ]); + exit; +} + +try { + // Escapar datos para evitar SQL injection + $nombre = mysqli_real_escape_string($conexion, $nombre); + $telefono = mysqli_real_escape_string($conexion, $telefono); + $dni = mysqli_real_escape_string($conexion, $dni); + $fecha = mysqli_real_escape_string($conexion, $fecha); + $horario = mysqli_real_escape_string($conexion, $horario); + $tipo = mysqli_real_escape_string($conexion, $tipo); + + // Buscar o crear cliente + $sqlBuscarCliente = "SELECT ID_Cliente FROM clientes WHERE DNI = '$dni'"; + $resBuscar = mysqli_query($conexion, $sqlBuscarCliente); + + if (!$resBuscar) { + throw new Exception("Error al buscar cliente: " . mysqli_error($conexion)); + } + + $cliente = mysqli_fetch_assoc($resBuscar); + + if ($cliente) { + // Cliente existe, actualizar datos + $idCliente = $cliente['ID_Cliente']; + $sqlActualizarCliente = "UPDATE clientes SET Nombre = '$nombre', Telefono = '$telefono' WHERE ID_Cliente = $idCliente"; + + if (!mysqli_query($conexion, $sqlActualizarCliente)) { + throw new Exception("Error al actualizar cliente: " . mysqli_error($conexion)); + } + } else { + // Crear nuevo cliente + $sqlCrearCliente = "INSERT INTO clientes (Nombre, Telefono, DNI) VALUES ('$nombre', '$telefono', '$dni')"; + + if (!mysqli_query($conexion, $sqlCrearCliente)) { + throw new Exception("Error al crear cliente: " . mysqli_error($conexion)); + } + + $idCliente = mysqli_insert_id($conexion); + } + + // Obtener los datos de la cancha usada para calcular el cupón + $sqlObtenerCancha = "SELECT ID_Cancha, Cant_Reservas_Cupon, Descuento_Cupon, Cant_Faltas, Duracion_Cupon FROM canchas WHERE Tipo = '$tipo' LIMIT 1"; + $resCancha = mysqli_query($conexion, $sqlObtenerCancha); + + if (!$resCancha) { + throw new Exception("Error al obtener cancha: " . mysqli_error($conexion)); + } + + $cancha = mysqli_fetch_assoc($resCancha); + if (!$cancha) { + throw new Exception("No existe cancha del tipo especificado"); + } + + $idCancha = $cancha['ID_Cancha']; + $umbral = intval($cancha['Cant_Reservas_Cupon']); + $descuentoCupon = floatval($cancha['Descuento_Cupon']); + $maxFaltas = intval($cancha['Cant_Faltas']); + $duracionCupon = intval($cancha['Duracion_Cupon']); + + // Buscar fila de cupón existente para el cliente y tipo de cancha + $sqlBuscarCupon = "SELECT ID_Cupon, Cantidad_Reservas, Cantidad_Faltas, Ultima_Fecha, Descuento FROM cupones WHERE ID_Cliente = $idCliente AND Tipo = '$tipo' LIMIT 1"; + $resCupon = mysqli_query($conexion, $sqlBuscarCupon); + + if (!$resCupon) { + throw new Exception("Error al buscar cupón: " . mysqli_error($conexion)); + } + + $cupon = mysqli_fetch_assoc($resCupon); + + if (!$cupon) { + $sqlCrearCupon = "INSERT INTO cupones (Cantidad_Reservas, Cantidad_Faltas, Tipo, Ultima_Fecha, Descuento, ID_Cliente) VALUES (0, 0, '$tipo', NULL, 0, $idCliente)"; + if (!mysqli_query($conexion, $sqlCrearCupon)) { + throw new Exception("Error al crear cupón: " . mysqli_error($conexion)); + } + + $cupon = [ + 'ID_Cupon' => mysqli_insert_id($conexion), + 'Cantidad_Reservas' => 0, + 'Cantidad_Faltas' => 0, + 'Ultima_Fecha' => null, + 'Descuento' => 0 + ]; + } + + $cantidadReservasCupon = intval($cupon['Cantidad_Reservas']); + $cantidadFaltasCupon = intval($cupon['Cantidad_Faltas']); + $ultimaFechaCupon = $cupon['Ultima_Fecha']; + + // Caducidad del cupón + if ($duracionCupon > 0 && !empty($ultimaFechaCupon)) { + $fechaUltima = new DateTime($ultimaFechaCupon); + $fechaUltima->modify("+{$duracionCupon} days"); + $fechaActual = new DateTime(); + + if ($fechaActual > $fechaUltima) { + $cantidadReservasCupon = 0; + $cantidadFaltasCupon = 0; + $ultimaFechaCupon = null; + $sqlResetCupon = "UPDATE cupones SET Cantidad_Reservas = 0, Cantidad_Faltas = 0, Ultima_Fecha = NULL, Descuento = 0 WHERE ID_Cupon = {$cupon['ID_Cupon']}"; + if (!mysqli_query($conexion, $sqlResetCupon)) { + throw new Exception("Error al resetear cupón vencido: " . mysqli_error($conexion)); + } + } + } + + // Si la cantidad de faltas supera el máximo permitido, reiniciamos el conteo de reservas + if ($maxFaltas > 0 && $cantidadFaltasCupon > $maxFaltas) { + $cantidadReservasCupon = 0; + $cantidadFaltasCupon = 0; + $ultimaFechaCupon = null; + $sqlResetCupon = "UPDATE cupones SET Cantidad_Reservas = 0, Cantidad_Faltas = 0, Ultima_Fecha = NULL, Descuento = 0 WHERE ID_Cupon = {$cupon['ID_Cupon']}"; + if (!mysqli_query($conexion, $sqlResetCupon)) { + throw new Exception("Error al resetear cupón por faltas: " . mysqli_error($conexion)); + } + } + + // Convertir horario a formato DATETIME + $fechaHora = $fecha . ' ' . $horario . ':00'; + + $descuentoAplicado = 0; + $montoFinal = $monto; + $valorCupon = $descuentoCupon; + + if ($umbral > 0 && ($cantidadReservasCupon + 1) >= $umbral) { + $descuentoAplicado = $valorCupon; + $montoFinal = round($monto * (1 - $descuentoAplicado), 2); + $cantidadReservasCupon = 0; + $cantidadFaltasCupon = 0; + $ultimaFechaCupon = date('Y-m-d H:i:s'); + } else { + $cantidadReservasCupon += 1; + $ultimaFechaCupon = date('Y-m-d H:i:s'); + } + + $sqlActualizarCupon = "UPDATE cupones SET Cantidad_Reservas = $cantidadReservasCupon, Cantidad_Faltas = $cantidadFaltasCupon, Ultima_Fecha = " . ($ultimaFechaCupon ? "'$ultimaFechaCupon'" : "NULL") . ", Descuento = $descuentoAplicado WHERE ID_Cupon = {$cupon['ID_Cupon']}"; + if (!mysqli_query($conexion, $sqlActualizarCupon)) { + throw new Exception("Error al actualizar cupón: " . mysqli_error($conexion)); + } + + // Obtener ID del usuario desde la sesión + $id_usuario = isset($_SESSION['id_usuario']) ? intval($_SESSION['id_usuario']) : null; + + // Insertar reserva de cancha + $sqlInsertarReserva = "INSERT INTO reservas (Fecha_Hora, Monto, Numero, Estado, ID_Cliente, ID_Cancha, Descuento, ID_Usuario) + VALUES ('$fechaHora', $montoFinal, $numeroCancha, 0, $idCliente, $idCancha, $descuentoAplicado, $id_usuario)"; + + if (!mysqli_query($conexion, $sqlInsertarReserva)) { + throw new Exception("Error al insertar reserva: " . mysqli_error($conexion)); + } + + $idReserva = mysqli_insert_id($conexion); + + // Si incluye quincho, insertar reserva de quincho + if ($incluirQuincho && $montoQuincho > 0) { + // Obtener ID_Cancha para quincho + $sqlObtenerQuincho = "SELECT ID_Cancha FROM canchas WHERE Tipo = 'Q'"; + $resQuincho = mysqli_query($conexion, $sqlObtenerQuincho); + + if (!$resQuincho) { + throw new Exception("Error al obtener quincho: " . mysqli_error($conexion)); + } + + $quincho = mysqli_fetch_assoc($resQuincho); + if ($quincho) { + $idQuincho = $quincho['ID_Cancha']; + + // El quincho se reserva por el día completo, entonces el número es 1 (hay un solo quincho) + $sqlInsertarQuincho = "INSERT INTO reservas (Fecha_Hora, Monto, Numero, Estado, ID_Cliente, ID_Cancha, Descuento, ID_Usuario) + VALUES ('$fechaHora', $montoQuincho, 1, 0, $idCliente, $idQuincho, 0, $id_usuario)"; + + if (!mysqli_query($conexion, $sqlInsertarQuincho)) { + throw new Exception("Error al insertar reserva de quincho: " . mysqli_error($conexion)); + } + } + } + + echo json_encode([ + 'success' => true, + 'message' => 'Reserva confirmada exitosamente', + 'id_reserva' => $idReserva, + 'id_cliente' => $idCliente + ]); + +} catch (Exception $e) { + echo json_encode([ + 'success' => false, + 'message' => $e->getMessage() + ]); +} +?> diff --git a/php/Guardar_Usuario.php b/php/Guardar_Usuario.php new file mode 100644 index 0000000..4b9fb84 --- /dev/null +++ b/php/Guardar_Usuario.php @@ -0,0 +1,106 @@ + false, 'message' => 'Método no permitido']); + exit(); +} + +if ($conexion->connect_error) { + echo json_encode(['success' => false, 'message' => 'Error de conexión a la base de datos: ' . $conexion->connect_error]); + exit(); +} + +// Obtener datos del formulario +$usuario = isset($_POST['usuario']) ? trim($_POST['usuario']) : ''; +$contraseña = isset($_POST['contraseña']) ? trim($_POST['contraseña']) : ''; +$confirmar_contraseña = isset($_POST['confirmar_contraseña']) ? trim($_POST['confirmar_contraseña']) : ''; +$rol = isset($_POST['rol']) ? trim($_POST['rol']) : ''; +$nombre = isset($_POST['nombre']) ? trim($_POST['nombre']) : ''; +$apellido = isset($_POST['apellido']) ? trim($_POST['apellido']) : ''; +$dni = isset($_POST['dni']) ? trim($_POST['dni']) : ''; +$correo = isset($_POST['correo']) ? trim($_POST['correo']) : ''; +$telefono = isset($_POST['telefono']) ? trim($_POST['telefono']) : ''; + +// El primer usuario registrado será el administrador supremo del sistema +$contadorQuery = "SELECT COUNT(*) AS total FROM usuarios"; +$contadorStmt = $conexion->prepare($contadorQuery); +$contadorStmt->execute(); +$contadorResult = $contadorStmt->get_result(); +$primerUsuario = $contadorResult->fetch_assoc()['total'] == 0; +$contadorStmt->close(); + +if ($primerUsuario) { + $rol = 'admin'; +} + +// Log para debugging +error_log("Datos recibidos - Usuario: $usuario, Rol: $rol, Nombre: $nombre"); + +// Validar que los campos requeridos estén presentes +if (empty($usuario) || empty($contraseña) || empty($confirmar_contraseña) || empty($rol) || empty($nombre) || empty($apellido) || empty($dni)) { + echo json_encode(['success' => false, 'message' => 'Todos los campos requeridos deben estar completos']); + exit(); +} + +if (strlen($contraseña) < 6) { + echo json_encode(['success' => false, 'message' => 'La contraseña debe tener al menos 6 carácteres']); + exit(); +} + +if ($contraseña !== $confirmar_contraseña) { + echo json_encode(['success' => false, 'message' => 'Las contraseñas no coinciden']); + exit(); +} + +// Validar que el rol sea válido +if (!in_array($rol, ['admin', 'cantina', 'cancha'])) { + echo json_encode(['success' => false, 'message' => 'Rol inválido']); + exit(); +} + +// Verificar si el usuario ya existe +$query = "SELECT ID_Usuario FROM usuarios WHERE Usuario = ?"; +$stmt = $conexion->prepare($query); + +if (!$stmt) { + echo json_encode(['success' => false, 'message' => 'Error en la consulta: ' . $conexion->error]); + exit(); +} + +$stmt->bind_param("s", $usuario); +$stmt->execute(); +$result = $stmt->get_result(); + +if ($result->num_rows > 0) { + echo json_encode(['success' => false, 'message' => 'El usuario ya existe']); + $stmt->close(); + exit(); +} +$stmt->close(); + +// Insertar el nuevo usuario +$query = "INSERT INTO usuarios (Usuario, Contraseña, Rol, Nombre, Apellido, DNI, Correo, Telefono) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; +$stmt = $conexion->prepare($query); + +if (!$stmt) { + echo json_encode(['success' => false, 'message' => 'Error en la consulta: ' . $conexion->error]); + exit(); +} + +$stmt->bind_param("ssssssss", $usuario, $contraseña, $rol, $nombre, $apellido, $dni, $correo, $telefono); + +if ($stmt->execute()) { + echo json_encode(['success' => true, 'message' => 'Usuario registrado exitosamente', 'id' => $stmt->insert_id]); +} else { + echo json_encode(['success' => false, 'message' => 'Error al registrar el usuario: ' . $stmt->error]); +} + +$stmt->close(); +$conexion->close(); +?> \ No newline at end of file diff --git a/php/Login.php b/php/Login.php new file mode 100644 index 0000000..0fd386b --- /dev/null +++ b/php/Login.php @@ -0,0 +1,39 @@ +connect_error) { + die("Error de conexión: " . $conexion->connect_error); +} + +if ($_SERVER["REQUEST_METHOD"] == "POST") { + header('Content-Type: application/json'); + $usuario = $_POST['usuario']; + $clave = $_POST['clave']; + + $query = "SELECT * FROM usuarios WHERE Usuario=? AND Contraseña=?"; + $stmt = $conexion->prepare($query); + $stmt->bind_param("ss", $usuario, $clave); + $stmt->execute(); + $result = $stmt->get_result(); + + if ($result->num_rows == 1) { + $row = $result->fetch_assoc(); + $_SESSION['id_usuario'] = $row['ID_Usuario'] ?? null; + $_SESSION['usuario'] = $row['Usuario']; + $_SESSION['rol'] = $row['Rol']; // admin, cancha o cantina + echo json_encode([ + 'success' => true, + 'message' => 'Login exitoso' + ]); + exit(); + } else { + echo json_encode([ + 'success' => false, + 'message' => 'Usuario o contraseña incorrectos' + ]); + exit(); + } +} +?> diff --git a/php/Logout.php b/php/Logout.php new file mode 100644 index 0000000..bb4d942 --- /dev/null +++ b/php/Logout.php @@ -0,0 +1,16 @@ + true]); +?> \ No newline at end of file diff --git a/php/Migrar_Gastos.php b/php/Migrar_Gastos.php new file mode 100644 index 0000000..ddaa814 --- /dev/null +++ b/php/Migrar_Gastos.php @@ -0,0 +1,51 @@ + 'DECIMAL(10,2) DEFAULT 0', + 'Luz' => 'DECIMAL(10,2) DEFAULT 0', + 'Agua' => 'DECIMAL(10,2) DEFAULT 0', + 'Impuestos' => 'DECIMAL(10,2) DEFAULT 0' + ]; + + foreach ($columnas_necesarias as $nombre_columna => $tipo_columna) { + // Verificar si la columna existe + $result = $conexion->query("SHOW COLUMNS FROM gastos LIKE '$nombre_columna'"); + + if ($result->num_rows === 0) { + // Si no existe, agregarla + $sql = "ALTER TABLE gastos ADD COLUMN `$nombre_columna` $tipo_columna"; + if ($conexion->query($sql)) { + echo "✓ Columna '$nombre_columna' agregada correctamente\n"; + } else { + throw new Exception("Error al agregar columna '$nombre_columna': " . $conexion->error); + } + } else { + echo "✓ Columna '$nombre_columna' ya existe\n"; + } + } + + // Verificar que haya al menos un registro de gastos + $result = $conexion->query("SELECT COUNT(*) as count FROM gastos"); + $count = $result->fetch_assoc()['count']; + + if ($count === 0) { + // Insertar un registro de gasto por defecto si no existe + $sql = "INSERT INTO gastos (Periodo_Desde, Periodo_Hasta, Sueldo, Luz, Agua, Impuestos) + VALUES (CURDATE(), NULL, 0, 0, 0, 0)"; + if ($conexion->query($sql)) { + echo "✓ Registro de gasto inicial creado\n"; + } else { + throw new Exception("Error al crear registro inicial: " . $conexion->error); + } + } + + echo "\n✓ Migración de gastos completada exitosamente\n"; + echo json_encode(['success' => true, 'message' => 'Migración completada']); + +} catch (Exception $e) { + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} +?> diff --git a/php/Migrar_Ventas_Productos.php b/php/Migrar_Ventas_Productos.php new file mode 100644 index 0000000..1845740 --- /dev/null +++ b/php/Migrar_Ventas_Productos.php @@ -0,0 +1,45 @@ +query("SHOW COLUMNS FROM ventas_productos LIKE 'Precio_Unitario'"); + +if ($result->num_rows === 0) { + // Si no existe, agregarla + $sql = "ALTER TABLE ventas_productos ADD COLUMN Precio_Unitario DECIMAL(10, 2) DEFAULT 0.00"; + + if ($conexion->query($sql) === TRUE) { + // Actualizar con los precios de los productos existentes + $update_sql = " + UPDATE ventas_productos vp + JOIN productos p ON vp.ID_Producto = p.ID_Producto + SET vp.Precio_Unitario = p.Precio + WHERE vp.Precio_Unitario = 0 + "; + + if ($conexion->query($update_sql) === TRUE) { + echo json_encode([ + 'success' => true, + 'message' => 'Columna Precio_Unitario agregada y actualizada correctamente' + ]); + } else { + echo json_encode([ + 'success' => false, + 'message' => 'Error al actualizar precios: ' . $conexion->error + ]); + } + } else { + echo json_encode([ + 'success' => false, + 'message' => 'Error al agregar columna: ' . $conexion->error + ]); + } +} else { + echo json_encode([ + 'success' => true, + 'message' => 'La columna Precio_Unitario ya existe' + ]); +} + +$conexion->close(); +?> diff --git a/php/Obtener_Canchas.php b/php/Obtener_Canchas.php new file mode 100644 index 0000000..6c72029 --- /dev/null +++ b/php/Obtener_Canchas.php @@ -0,0 +1,28 @@ + false, + 'message' => $e->getMessage() + ]); +} +?> \ No newline at end of file diff --git a/php/Obtener_Canchas_Numeros.php b/php/Obtener_Canchas_Numeros.php new file mode 100644 index 0000000..af99629 --- /dev/null +++ b/php/Obtener_Canchas_Numeros.php @@ -0,0 +1,89 @@ + false, + 'message' => 'Parámetros incompletos' + ]); + exit; +} + +try { + // Obtener la cantidad total de canchas del tipo especificado + $t = mysqli_real_escape_string($conexion, $tipo); + $sqlCanchas = "SELECT Cant_Canchas as cantidad_canchas, Duracion as duracion_cancha + FROM canchas + WHERE Tipo = '$t'"; + + $resCanchas = mysqli_query($conexion, $sqlCanchas); + + if (!$resCanchas) { + throw new Exception("Error al obtener información de canchas: " . mysqli_error($conexion)); + } + + $cancha = mysqli_fetch_assoc($resCanchas); + $cantidadCanchas = $cancha['cantidad_canchas']; + $duracionCancha = $cancha['duracion_cancha']; + + // Convertir horario + $horaInicioArr = explode(':', $horario_inicio); + $horaInicioMin = intval($horaInicioArr[0]) * 60 + intval($horaInicioArr[1]); + $horaFinMin = $horaInicioMin + intval($duracion); + + $horaInicio = $horario_inicio . ':00'; + $horaFin = sprintf("%02d:%02d", intval($horaFinMin / 60), intval($horaFinMin % 60)) . ':00'; + + // Obtener las canchas reservadas para este horario + $f = mysqli_real_escape_string($conexion, $fecha); + $sqlReservadasPorNumero = "SELECT r.Numero as numero_cancha + FROM reservas r + INNER JOIN canchas c ON r.ID_Cancha = c.ID_Cancha + WHERE c.Tipo = '$t' + AND DATE(r.Fecha_Hora) = '$f' + AND r.Estado != 2 + AND ( + (TIME(r.Fecha_Hora) < '$horaFin' AND ADDTIME(TIME(r.Fecha_Hora), SEC_TO_TIME($duracionCancha * 60)) > '$horaInicio') + ) + GROUP BY r.Numero"; + + $resReservadasPorNumero = mysqli_query($conexion, $sqlReservadasPorNumero); + + if (!$resReservadasPorNumero) { + throw new Exception("Error al obtener canchas reservadas: " . mysqli_error($conexion)); + } + + $canchasReservadas = []; + while ($row = mysqli_fetch_assoc($resReservadasPorNumero)) { + $canchasReservadas[] = intval($row['numero_cancha']); + } + + // Construir lista de todas las canchas con su disponibilidad + $canchas = []; + for ($i = 1; $i <= $cantidadCanchas; $i++) { + $canchas[] = [ + 'numero' => $i, + 'disponible' => !in_array($i, $canchasReservadas) + ]; + } + + echo json_encode([ + 'success' => true, + 'cantidad_total' => $cantidadCanchas, + 'canchas' => $canchas, + 'tipo' => $tipo + ]); + +} catch (Exception $e) { + echo json_encode([ + 'success' => false, + 'message' => $e->getMessage() + ]); +} +?> diff --git a/php/Obtener_Cupon.php b/php/Obtener_Cupon.php new file mode 100644 index 0000000..2a6e74c --- /dev/null +++ b/php/Obtener_Cupon.php @@ -0,0 +1,136 @@ + false, 'message' => 'DNI y tipo de cancha son requeridos']); + exit; +} + +$tipoDb = ''; +if ($tipo === 'futbol' || $tipo === 'F') { + $tipoDb = 'F'; +} elseif ($tipo === 'padel' || $tipo === 'P') { + $tipoDb = 'P'; +} + +if (empty($tipoDb)) { + echo json_encode(['success' => false, 'message' => 'Tipo de cancha inválido']); + exit; +} + +$dni = mysqli_real_escape_string($conexion, $dni); + +$sqlCliente = "SELECT ID_Cliente FROM clientes WHERE DNI = '$dni' LIMIT 1"; +$resCliente = mysqli_query($conexion, $sqlCliente); +if (!$resCliente) { + echo json_encode(['success' => false, 'message' => 'Error al buscar cliente: ' . mysqli_error($conexion)]); + exit; +} + +$cliente = mysqli_fetch_assoc($resCliente); +if (!$cliente) { + echo json_encode([ + 'success' => true, + 'clienteEncontrado' => false, + 'cantidadReservas' => 0, + 'cantidadFaltas' => 0, + 'umbral' => 0, + 'descuentoCanchas' => 0, + 'duracionCupon' => 0, + 'ultimaFecha' => null, + 'aplicarDescuento' => false, + 'reservasRestantes' => null + ]); + exit; +} + +$idCliente = intval($cliente['ID_Cliente']); + +$sqlCancha = "SELECT Cant_Reservas_Cupon, Descuento_Cupon, Cant_Faltas, Duracion_Cupon FROM canchas WHERE Tipo = '$tipoDb' LIMIT 1"; +$resCancha = mysqli_query($conexion, $sqlCancha); +if (!$resCancha) { + echo json_encode(['success' => false, 'message' => 'Error al obtener datos de la cancha: ' . mysqli_error($conexion)]); + exit; +} + +$cancha = mysqli_fetch_assoc($resCancha); +if (!$cancha) { + echo json_encode(['success' => false, 'message' => 'No se encontró el tipo de cancha']); + exit; +} + +$umbral = intval($cancha['Cant_Reservas_Cupon']); +$descuentoCanchas = floatval($cancha['Descuento_Cupon']); +$duracionCupon = intval($cancha['Duracion_Cupon']); +$maxFaltas = intval($cancha['Cant_Faltas']); + +$sqlCupon = "SELECT ID_Cupon, Cantidad_Reservas, Cantidad_Faltas, Ultima_Fecha FROM cupones WHERE ID_Cliente = $idCliente AND Tipo = '$tipoDb' LIMIT 1"; +$resCupon = mysqli_query($conexion, $sqlCupon); +if (!$resCupon) { + echo json_encode(['success' => false, 'message' => 'Error al buscar cupón: ' . mysqli_error($conexion)]); + exit; +} + +$cupon = mysqli_fetch_assoc($resCupon); +$cantidadReservas = 0; +$cantidadFaltas = 0; +$ultimaFecha = null; +$vencido = false; + +if ($cupon) { + $cantidadReservas = intval($cupon['Cantidad_Reservas']); + $cantidadFaltas = intval($cupon['Cantidad_Faltas']); + $ultimaFecha = $cupon['Ultima_Fecha']; + + if ($duracionCupon > 0 && !empty($ultimaFecha)) { + $fechaUltima = new DateTime($ultimaFecha); + $fechaUltima->modify("+{$duracionCupon} days"); + $fechaActual = new DateTime(); + + if ($fechaActual > $fechaUltima) { + $cantidadReservas = 0; + $cantidadFaltas = 0; + $ultimaFecha = null; + $vencido = true; + $sqlResetCupon = "UPDATE cupones SET Cantidad_Reservas = 0, Cantidad_Faltas = 0, Ultima_Fecha = NULL, Descuento = 0 WHERE ID_Cupon = {$cupon['ID_Cupon']}"; + mysqli_query($conexion, $sqlResetCupon); + } + } + + if ($maxFaltas > 0 && $cantidadFaltas > $maxFaltas) { + $cantidadReservas = 0; + $cantidadFaltas = 0; + $ultimaFecha = null; + $sqlResetCupon = "UPDATE cupones SET Cantidad_Reservas = 0, Cantidad_Faltas = 0, Ultima_Fecha = NULL, Descuento = 0 WHERE ID_Cupon = {$cupon['ID_Cupon']}"; + mysqli_query($conexion, $sqlResetCupon); + } +} + +$aplicarDescuento = false; +$reservasRestantes = null; +if ($umbral > 0) { + if (($cantidadReservas + 1) >= $umbral) { + $aplicarDescuento = true; + $reservasRestantes = 0; + } else { + $reservasRestantes = $umbral - $cantidadReservas - 1; + } +} + +echo json_encode([ + 'success' => true, + 'clienteEncontrado' => true, + 'cantidadReservas' => $cantidadReservas, + 'cantidadFaltas' => $cantidadFaltas, + 'umbral' => $umbral, + 'descuentoCanchas' => $descuentoCanchas, + 'duracionCupon' => $duracionCupon, + 'ultimaFecha' => $ultimaFecha, + 'aplicarDescuento' => $aplicarDescuento, + 'reservasRestantes' => $reservasRestantes, + 'vencido' => $vencido +]); diff --git a/php/Obtener_Disponibilidad_Cancha.php b/php/Obtener_Disponibilidad_Cancha.php new file mode 100644 index 0000000..57328a4 --- /dev/null +++ b/php/Obtener_Disponibilidad_Cancha.php @@ -0,0 +1,83 @@ + false, + 'message' => 'Parámetros incompletos' + ]); + exit; +} + +try { + // Obtener la cantidad total de canchas del tipo especificado y su duración + $t = mysqli_real_escape_string($conexion, $tipo); + $sqlCanchas = "SELECT Cant_Canchas as cantidad_canchas, Duracion as duracion_cancha + FROM canchas + WHERE Tipo = '$t'"; + + $resCanchas = mysqli_query($conexion, $sqlCanchas); + + if (!$resCanchas) { + throw new Exception("Error al obtener información de canchas: " . mysqli_error($conexion)); + } + + $cancha = mysqli_fetch_assoc($resCanchas); + $cantidadCanchas = $cancha['cantidad_canchas']; + $duracionCancha = $cancha['duracion_cancha']; + + // Convertir horario a minutos para comparación + $horaInicioArr = explode(':', $horario_inicio); + $horaInicioMin = intval($horaInicioArr[0]) * 60 + intval($horaInicioArr[1]); + $horaFinMin = $horaInicioMin + intval($duracion); + + // Convertir a formato HH:MM:SS para la query + $horaInicio = $horario_inicio . ':00'; + $horaFin = sprintf("%02d:%02d", intval($horaFinMin / 60), intval($horaFinMin % 60)) . ':00'; + + // Obtener la cantidad de reservas que se superponen con el horario seleccionado + $f = mysqli_real_escape_string($conexion, $fecha); + $sqlReservas = "SELECT COUNT(*) as reservas_solapadas + FROM reservas r + INNER JOIN canchas c ON r.ID_Cancha = c.ID_Cancha + WHERE c.Tipo = '$t' + AND DATE(r.Fecha_Hora) = '$f' + AND r.Estado != 2 + AND ( + (TIME(r.Fecha_Hora) < '$horaFin' AND ADDTIME(TIME(r.Fecha_Hora), SEC_TO_TIME($duracionCancha * 60)) > '$horaInicio') + )"; + + $resReservas = mysqli_query($conexion, $sqlReservas); + + if (!$resReservas) { + throw new Exception("Error al contar reservas: " . mysqli_error($conexion)); + } + + $reservas = mysqli_fetch_assoc($resReservas); + $reservasSolapadas = $reservas['reservas_solapadas']; + + // Calcular disponibilidad + $disponibles = $cantidadCanchas - $reservasSolapadas; + + echo json_encode([ + 'success' => true, + 'cantidad_total' => $cantidadCanchas, + 'reservas' => $reservasSolapadas, + 'disponibles' => max(0, $disponibles), + 'horario' => $horaInicio, + 'tipo' => $tipo + ]); + +} catch (Exception $e) { + echo json_encode([ + 'success' => false, + 'message' => $e->getMessage() + ]); +} +?> diff --git a/php/Obtener_Disponibilidad_Quincho.php b/php/Obtener_Disponibilidad_Quincho.php new file mode 100644 index 0000000..4cdeaa2 --- /dev/null +++ b/php/Obtener_Disponibilidad_Quincho.php @@ -0,0 +1,64 @@ + false, + 'message' => 'Fecha no proporcionada' + ]); + exit; +} + +try { + // Obtener la cantidad total de quinchos disponibles + $sqlCanchas = "SELECT Cant_Canchas as cantidad_quinchos + FROM canchas + WHERE Tipo = 'Q'"; + + $resCanchas = mysqli_query($conexion, $sqlCanchas); + + if (!$resCanchas) { + throw new Exception("Error al obtener información del quincho: " . mysqli_error($conexion)); + } + + $cancha = mysqli_fetch_assoc($resCanchas); + $cantidadQuinchos = $cancha['cantidad_quinchos']; + + // Obtener la cantidad de reservas de quincho para esa fecha + $f = mysqli_real_escape_string($conexion, $fecha); + $sqlReservas = "SELECT COUNT(*) as reservas_quincho + FROM reservas r + INNER JOIN canchas c ON r.ID_Cancha = c.ID_Cancha + WHERE c.Tipo = 'Q' + AND DATE(r.Fecha_Hora) = '$f' + AND r.Estado != 2"; + + $resReservas = mysqli_query($conexion, $sqlReservas); + + if (!$resReservas) { + throw new Exception("Error al contar reservas: " . mysqli_error($conexion)); + } + + $reservas = mysqli_fetch_assoc($resReservas); + $reservasQuincho = $reservas['reservas_quincho']; + + // Calcular disponibilidad + $disponibles = $cantidadQuinchos - $reservasQuincho; + + echo json_encode([ + 'success' => true, + 'cantidad_total' => $cantidadQuinchos, + 'reservas' => $reservasQuincho, + 'disponibles' => max(0, $disponibles) + ]); + +} catch (Exception $e) { + echo json_encode([ + 'success' => false, + 'message' => $e->getMessage() + ]); +} +?> diff --git a/php/Obtener_Gastos.php b/php/Obtener_Gastos.php new file mode 100644 index 0000000..37996a8 --- /dev/null +++ b/php/Obtener_Gastos.php @@ -0,0 +1,40 @@ +query($sql_columns); + + $columnas = []; + while ($row = $result_columns->fetch_assoc()) { + $col = $row['Field']; + if (!in_array($col, ['ID_Gasto', 'Periodo_Desde', 'Periodo_Hasta'])) { + $columnas[] = $col; + } + } + + // Obtener el registro actual (donde Periodo_Hasta IS NULL) + $sql_actual = "SELECT * FROM gastos WHERE Periodo_Hasta IS NULL ORDER BY Periodo_Desde DESC LIMIT 1"; + $result_actual = $conexion->query($sql_actual); + + $gasto_actual = null; + if ($result_actual && $result_actual->num_rows > 0) { + $gasto_actual = $result_actual->fetch_assoc(); + } + + echo json_encode([ + 'success' => true, + 'columnas' => $columnas, + 'gasto' => $gasto_actual, + 'message' => $gasto_actual ? 'Datos cargados' : 'No hay gastos registrados' + ]); + +} catch (Exception $e) { + echo json_encode([ + 'success' => false, + 'message' => $e->getMessage() + ]); +} +?> diff --git a/php/Obtener_Horarios.php b/php/Obtener_Horarios.php new file mode 100644 index 0000000..9f92b85 --- /dev/null +++ b/php/Obtener_Horarios.php @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/php/Obtener_Ordenes_Compra.php b/php/Obtener_Ordenes_Compra.php new file mode 100644 index 0000000..422c0c0 --- /dev/null +++ b/php/Obtener_Ordenes_Compra.php @@ -0,0 +1,26 @@ +connect_error) { + echo json_encode([]); + exit; +} + +$sql = "SELECT oc.ID_Orden, DATE(oc.Fecha) AS Fecha, oc.ID_Proveedor, p.Nombre AS Proveedor, p.Telefono, oc.Estado + FROM ordenes_compra oc + LEFT JOIN proveedores p ON oc.ID_Proveedor = p.ID_Proveedor + ORDER BY oc.Fecha DESC"; + +$result = $conexion->query($sql); +$ordenes = []; + +if ($result) { + while ($fila = $result->fetch_assoc()) { + $ordenes[] = $fila; + } +} + +echo json_encode($ordenes); +$conexion->close(); +?> \ No newline at end of file diff --git a/php/Obtener_Productos.php b/php/Obtener_Productos.php new file mode 100644 index 0000000..10859e5 --- /dev/null +++ b/php/Obtener_Productos.php @@ -0,0 +1,33 @@ +query("SHOW COLUMNS FROM productos WHERE Field='Estado'"); +$columnaExists = $checkColumn && $checkColumn->num_rows > 0; + +if ($columnaExists) { + // Solo mostrar productos activos + $result = $conexion->query("SELECT ID_Producto, Descripcion, Precio_Venta, Precio_Compra, Stock_Disponible FROM productos WHERE Estado = 1 ORDER BY ID_Producto DESC"); +} else { + // Fallback: verificar si la columna Activo existe + $checkColumnActivo = $conexion->query("SHOW COLUMNS FROM productos WHERE Field='Activo'"); + $activoExists = $checkColumnActivo && $checkColumnActivo->num_rows > 0; + + if ($activoExists) { + $result = $conexion->query("SELECT ID_Producto, Descripcion, Precio_Venta, Precio_Compra, Stock_Disponible FROM productos WHERE Activo = 1 ORDER BY ID_Producto DESC"); + } else { + $result = $conexion->query("SELECT ID_Producto, Descripcion, Precio_Venta, Precio_Compra, Stock_Disponible FROM productos ORDER BY ID_Producto DESC"); + } +} + +if (!$result) { + echo json_encode([]); + exit; +} + +$productos = []; +while ($row = $result->fetch_assoc()) { + $productos[] = $row; +} +echo json_encode($productos); +?> diff --git a/php/Obtener_Productos_Orden.php b/php/Obtener_Productos_Orden.php new file mode 100644 index 0000000..1b5ab74 --- /dev/null +++ b/php/Obtener_Productos_Orden.php @@ -0,0 +1,34 @@ +connect_error) { + echo json_encode([]); + exit; +} + +$id_orden = isset($_GET['id']) ? intval($_GET['id']) : 0; + +if ($id_orden <= 0) { + echo json_encode([]); + exit; +} + +$sql = "SELECT op.ID_Producto, op.Cantidad, p.Descripcion + FROM ordenes_productos op + INNER JOIN productos p ON op.ID_Producto = p.ID_Producto + WHERE op.ID_Orden = ?"; + +$stmt = $conexion->prepare($sql); +$stmt->bind_param("i", $id_orden); +$stmt->execute(); +$result = $stmt->get_result(); + +$productos = []; +while ($fila = $result->fetch_assoc()) { + $productos[] = $fila; +} + +echo json_encode($productos); +$conexion->close(); +?> \ No newline at end of file diff --git a/php/Obtener_Productos_Proveedor.php b/php/Obtener_Productos_Proveedor.php new file mode 100644 index 0000000..024de7e --- /dev/null +++ b/php/Obtener_Productos_Proveedor.php @@ -0,0 +1,23 @@ +prepare(" + SELECT p.ID_Producto, p.Descripcion + FROM productos p + INNER JOIN proveedores_productos pp ON p.ID_Producto = pp.ID_Producto + WHERE pp.ID_Proveedor = ? +"); +$stmt->bind_param("i", $idProveedor); +$stmt->execute(); +$resultado = $stmt->get_result(); + +$productos = []; +while ($fila = $resultado->fetch_assoc()) { + $productos[] = $fila; +} + +echo json_encode($productos); diff --git a/php/Obtener_Proveedores.php b/php/Obtener_Proveedores.php new file mode 100644 index 0000000..8ace08a --- /dev/null +++ b/php/Obtener_Proveedores.php @@ -0,0 +1,20 @@ +connect_error) { + echo json_encode([]); + exit; +} + +$resultado = $conexion->query("SELECT ID_Proveedor, Nombre, Telefono FROM proveedores"); +$proveedores = []; + +while ($fila = $resultado->fetch_assoc()) { + $proveedores[] = $fila; +} + +echo json_encode($proveedores); + +$conexion->close(); +?> diff --git a/php/Obtener_Reporte.php b/php/Obtener_Reporte.php new file mode 100644 index 0000000..a4dfd0b --- /dev/null +++ b/php/Obtener_Reporte.php @@ -0,0 +1,1205 @@ += '$fecha_inicio') + ORDER BY Periodo_Desde DESC + LIMIT 1 + "; + + $result = $conexion->query($sql); + $total_gastos = 0; + $gastos_detalles = []; + + if ($result && $result->num_rows > 0) { + $row = $result->fetch_assoc(); + + // Sumar todos los valores numéricos excepto ID_Gasto, Periodo_Desde, Periodo_Hasta + foreach ($row as $columna => $valor) { + if (!in_array($columna, ['ID_Gasto', 'Periodo_Desde', 'Periodo_Hasta']) && is_numeric($valor) && floatval($valor) > 0) { + $total_gastos += floatval($valor); + $gastos_detalles[] = [ + 'descripcion' => ucfirst($columna), + 'monto' => floatval($valor) + ]; + } + } + } + + return ['total' => $total_gastos, 'detalles' => $gastos_detalles]; +} + +function obtenerHorasDisponiblesPorPeriodo($conexion, $fecha_inicio, $fecha_fin) { + $total_horas = 0; + $fecha = new DateTime($fecha_inicio); + $fecha_fin_dt = new DateTime($fecha_fin); + + while ($fecha <= $fecha_fin_dt) { + $diaSemana = intval($fecha->format('N')); + $diaTexto = $diaSemana >= 1 && $diaSemana <= 5 ? 'Lunes a Viernes' : ($diaSemana === 6 ? 'Sábado' : 'Domingo'); + + $sqlHorario = "SELECT Hora_Apertura, Hora_Cierre FROM horarios WHERE Dia = '$diaTexto' LIMIT 1"; + $resultHorario = $conexion->query($sqlHorario); + if ($resultHorario && $rowHorario = $resultHorario->fetch_assoc()) { + $horaApertura = $rowHorario['Hora_Apertura']; + $horaCierre = $rowHorario['Hora_Cierre']; + if ($horaApertura && $horaCierre) { + $horasDia = (strtotime($horaCierre) - strtotime($horaApertura)) / 3600; + if ($horasDia > 0) { + $total_horas += $horasDia; + } + } + } + $fecha->modify('+1 day'); + } + + return $total_horas; +} + +try { + // ===== REPORTE FINANCIERO ===== + if ($tipo_reporte === 'financiero') { + + // ===== REPORTE DIARIO ===== + if ($tipo === 'diario') { + // Filtro opcional: si hay usuario logueado, mostrar solo sus operaciones + $filtro_venta = $id_usuario_logueado ? "AND v.ID_Usuario = $id_usuario_logueado" : ""; + $filtro_reserva = $id_usuario_logueado ? "AND r.ID_Usuario = $id_usuario_logueado" : ""; + $filtro_orden = $id_usuario_logueado ? "AND oc.ID_Usuario = $id_usuario_logueado" : ""; + + $sql = " + SELECT + DATE(v.Fecha) as fecha, + TIME(v.Fecha) as hora, + 'Venta Cantina' as tipo_operacion, + GROUP_CONCAT(CONCAT(vp.Cantidad, 'x ', p.Descripcion) SEPARATOR ', ') as descripcion, + COALESCE(SUM(vp.Cantidad * p.Precio_Venta), 0) as monto, + COALESCE(CONCAT(u.Nombre, ' ', u.Apellido), 'Sistema') as usuario_registro + FROM ventas v + LEFT JOIN ventas_productos vp ON v.ID_Venta = vp.ID_Venta + LEFT JOIN productos p ON vp.ID_Producto = p.ID_Producto + LEFT JOIN usuarios u ON v.ID_Usuario = u.ID_Usuario + WHERE DATE(v.Fecha) = '$fecha' $filtro_venta + GROUP BY v.ID_Venta, v.Fecha, v.ID_Usuario + + UNION ALL + + SELECT + DATE(r.Fecha_Hora) as fecha, + TIME(r.Fecha_Hora) as hora, + CONCAT('Reserva ', CASE c.Tipo WHEN 'F' THEN 'Fútbol' WHEN 'P' THEN 'Pádel' WHEN 'Q' THEN 'Quincho' ELSE 'Otro' END) as tipo_operacion, + CONCAT(cl.Nombre, ' - Cancha ', r.Numero, ' - ', TIME(r.Fecha_Hora), ' hs') as descripcion, + r.Monto as monto, + COALESCE(CONCAT(u.Nombre, ' ', u.Apellido), 'Sistema') as usuario_registro + FROM reservas r + JOIN canchas c ON r.ID_Cancha = c.ID_Cancha + JOIN clientes cl ON r.ID_Cliente = cl.ID_Cliente + LEFT JOIN usuarios u ON r.ID_Usuario = u.ID_Usuario + WHERE DATE(r.Fecha_Hora) = '$fecha' $filtro_reserva + + UNION ALL + + SELECT + DATE(oc.Fecha) as fecha, + TIME(oc.Fecha) as hora, + 'Orden de Compra' as tipo_operacion, + CONCAT('Proveedor: ', pr.Nombre) as descripcion, + -COALESCE(SUM(op.Cantidad * p.Precio_Compra), 0) as monto, + COALESCE(CONCAT(u.Nombre, ' ', u.Apellido), 'Sistema') as usuario_registro + FROM ordenes_compra oc + JOIN ordenes_productos op ON oc.ID_Orden = op.ID_Orden + JOIN productos p ON op.ID_Producto = p.ID_Producto + JOIN proveedores pr ON oc.ID_Proveedor = pr.ID_Proveedor + LEFT JOIN usuarios u ON oc.ID_Usuario = u.ID_Usuario + WHERE oc.Estado = 1 AND DATE(oc.Fecha) = '$fecha' $filtro_orden + GROUP BY oc.ID_Orden, oc.Fecha, oc.ID_Usuario + + ORDER BY hora ASC + "; + + $result = $conexion->query($sql); + if (!$result) { + throw new Exception("Error: " . $conexion->error); + } + + $transacciones = []; + $ingresos_reservas = 0; + $ingresos_cantina = 0; + $egresos = 0; + + while ($row = $result->fetch_assoc()) { + $transacciones[] = $row; + $tipo_op = $row['tipo_operacion']; + + if (strpos($tipo_op, 'Reserva') !== false) { + $ingresos_reservas += floatval($row['monto']); + } elseif ($tipo_op === 'Venta Cantina') { + $ingresos_cantina += floatval($row['monto']); + } else { + $egresos += abs(floatval($row['monto'])); + } + } + + echo json_encode([ + 'tipo_reporte' => 'diario', + 'periodo' => $fecha, + 'transacciones' => $transacciones, + 'resumen' => [ + 'ingresos_reservas' => $ingresos_reservas, + 'ingresos_cantina' => $ingresos_cantina, + 'total_ingresos' => $ingresos_reservas + $ingresos_cantina, + 'egresos' => $egresos, + 'saldo_neto' => ($ingresos_reservas + $ingresos_cantina) - $egresos + ] + ]); + } + + // ===== REPORTE SEMANAL ===== + elseif ($tipo === 'semanal') { + // Obtener el rango de fechas de la semana + if (!$semana) { + $semana = date('Y-W'); + } + + // Parsear la semana ISO (formato YYYY-Www) + $semanaPartes = explode('-W', $semana); + if (count($semanaPartes) !== 2) { + throw new Exception("Formato de semana inválido"); + } + + $anio_s = intval($semanaPartes[0]); + $semana_s = intval($semanaPartes[1]); + + // Calcular el lunes de la semana ISO especificada + // Crear una fecha para el jueves de la primera semana del año (para cálculo ISO correcto) + $fecha_base = new DateTime($anio_s . '-01-04'); // 4 de enero siempre está en semana 1 + + // Retroceder al lunes de esa semana + $dia_semana = $fecha_base->format('N'); // 1=lunes, 7=domingo + $fecha_base->modify('-' . ($dia_semana - 1) . ' days'); + + // Sumar las semanas necesarias + $fecha_base->modify('+' . ($semana_s - 1) . ' weeks'); + + $fecha_inicio = $fecha_base->format('Y-m-d'); + $fecha_fin = $fecha_base->modify('+6 days')->format('Y-m-d'); + + // Resetear fecha_base para los cálculos posteriores + $fecha_base = new DateTime($fecha_inicio); + + $sql = " + SELECT + DATE(r.Fecha_Hora) as dia, + DAYNAME(r.Fecha_Hora) as nombre_dia, + CASE c.Tipo WHEN 'F' THEN 'Fútbol' WHEN 'P' THEN 'Pádel' WHEN 'Q' THEN 'Quincho' ELSE 'Otro' END as tipo_cancha, + COUNT(r.ID_Reserva) as cantidad_reservas, + COALESCE(SUM(r.Monto), 0) as ingreso_reservas + FROM reservas r + JOIN canchas c ON r.ID_Cancha = c.ID_Cancha + WHERE DATE(r.Fecha_Hora) BETWEEN '$fecha_inicio' AND '$fecha_fin' + GROUP BY DATE(r.Fecha_Hora), c.Tipo + ORDER BY DATE(r.Fecha_Hora), tipo_cancha + "; + + $result = $conexion->query($sql); + $reservas_por_dia = []; + + $dias_map = [ + 'Monday' => 'Lunes', + 'Tuesday' => 'Martes', + 'Wednesday' => 'Miércoles', + 'Thursday' => 'Jueves', + 'Friday' => 'Viernes', + 'Saturday' => 'Sábado', + 'Sunday' => 'Domingo' + ]; + + while ($row = $result->fetch_assoc()) { + $dia = $row['dia']; + if (!isset($reservas_por_dia[$dia])) { + $reservas_por_dia[$dia] = [ + 'dia' => $dia, + 'nombre_dia' => $row['nombre_dia'], + 'ingresos_reservas' => 0, + 'detalles_cancha' => [] + ]; + } + $reservas_por_dia[$dia]['ingresos_reservas'] += floatval($row['ingreso_reservas']); + $reservas_por_dia[$dia]['detalles_cancha'][] = [ + 'tipo' => $row['tipo_cancha'], + 'cantidad' => $row['cantidad_reservas'], + 'ingreso' => $row['ingreso_reservas'] + ]; + } + + // Ingresos Cantina por día + $sql_cantina = " + SELECT + DATE(v.Fecha) as dia, + COALESCE(SUM(vp.Cantidad * p.Precio_Venta), 0) as ingreso_cantina, + COALESCE(SUM((vp.Cantidad * p.Precio_Venta) - (vp.Cantidad * p.Precio_Compra)), 0) as ganancia + FROM ventas v + JOIN ventas_productos vp ON v.ID_Venta = vp.ID_Venta + JOIN productos p ON vp.ID_Producto = p.ID_Producto + WHERE DATE(v.Fecha) BETWEEN '$fecha_inicio' AND '$fecha_fin' + GROUP BY DATE(v.Fecha) + "; + + $result_cantina = $conexion->query($sql_cantina); + $cantina_por_dia = []; + + while ($row = $result_cantina->fetch_assoc()) { + $cantina_por_dia[$row['dia']] = floatval($row['ingreso_cantina']); + } + + // Costo de Mercadería por día + $sql_compras = " + SELECT + DATE(oc.Fecha) as dia, + COALESCE(SUM(op.Cantidad * p.Precio_Compra), 0) as costo_mercaderia + FROM ordenes_compra oc + JOIN ordenes_productos op ON oc.ID_Orden = op.ID_Orden + JOIN productos p ON op.ID_Producto = p.ID_Producto + WHERE oc.Estado = 1 + AND DATE(oc.Fecha) BETWEEN '$fecha_inicio' AND '$fecha_fin' + GROUP BY DATE(oc.Fecha) + "; + + $result_compras = $conexion->query($sql_compras); + $compras_por_dia = []; + + while ($row = $result_compras->fetch_assoc()) { + $compras_por_dia[$row['dia']] = floatval($row['costo_mercaderia']); + } + + // Gastos fijos para el período + $gastos_fijos_data = obtenerGastosFijos($conexion, $fecha_inicio, $fecha_fin); + $gastos_totales = $gastos_fijos_data['total']; + + // Ocupación + $sql_ocupacion = " + SELECT + COALESCE(SUM(CASE WHEN r.Estado != 2 THEN c.Duracion ELSE 0 END), 0) as minutos_reservados + , COALESCE(SUM(CASE WHEN r.Estado != 2 THEN 1 ELSE 0 END), 0) as turnos_ocupados + FROM reservas r + JOIN canchas c ON r.ID_Cancha = c.ID_Cancha + WHERE DATE(r.Fecha_Hora) BETWEEN '$fecha_inicio' AND '$fecha_fin' + "; + + $sql_disponibilidad = " + SELECT + COUNT(*) as cantidad_canchas + FROM canchas + "; + + $result_ocu = $conexion->query($sql_ocupacion); + $result_disp = $conexion->query($sql_disponibilidad); + + $minutos_reservados = 0; + $turnos_disponibles = 1; // para evitar división por cero + $horas_disponibles_por_cancha = obtenerHorasDisponiblesPorPeriodo($conexion, $fecha_inicio, $fecha_fin); + + if ($result_ocu) { + $row = $result_ocu->fetch_assoc(); + $minutos_reservados = intval($row['minutos_reservados'] ?? 0); + $turnos_ocupados = intval($row['turnos_ocupados'] ?? 0); + } + + if ($result_disp) { + $row = $result_disp->fetch_assoc(); + $cantidad_canchas = intval($row['cantidad_canchas'] ?? 1); + $turnos_disponibles = $cantidad_canchas * $horas_disponibles_por_cancha; + } + + $horas_reservadas = $minutos_reservados / 60; + $ocupacion = ($turnos_disponibles > 0) ? round(($horas_reservadas / $turnos_disponibles) * 100, 2) : 0; + + // Consolidar datos por día + $dias_map_semana = [ + 'Monday' => 'Lunes', + 'Tuesday' => 'Martes', + 'Wednesday' => 'Miércoles', + 'Thursday' => 'Jueves', + 'Friday' => 'Viernes', + 'Saturday' => 'Sábado', + 'Sunday' => 'Domingo' + ]; + + $semana_datos = []; + for ($i = 0; $i < 7; $i++) { + $dia_actual = date('Y-m-d', strtotime($fecha_inicio . ' +' . $i . ' days')); + + $ingresos_res = isset($reservas_por_dia[$dia_actual]) ? floatval($reservas_por_dia[$dia_actual]['ingresos_reservas']) : 0; + $ingresos_cant = isset($cantina_por_dia[$dia_actual]) ? floatval($cantina_por_dia[$dia_actual]) : 0; + $costo_merc = isset($compras_por_dia[$dia_actual]) ? floatval($compras_por_dia[$dia_actual]) : 0; + + $total_ingresos = $ingresos_res + $ingresos_cant; + $saldo_neto = $total_ingresos - $costo_merc; + + $nombre_dia_ingles = date('l', strtotime($dia_actual)); + $nombre_dia_es = isset($dias_map_semana[$nombre_dia_ingles]) ? $dias_map_semana[$nombre_dia_ingles] : $nombre_dia_ingles; + + $semana_datos[] = [ + 'dia' => $dia_actual, + 'nombre_dia' => $nombre_dia_es, + 'ingresos_reservas' => $ingresos_res, + 'ingresos_cantina' => $ingresos_cant, + 'costo_mercaderia' => $costo_merc, + 'total_ingresos' => $total_ingresos, + 'saldo_neto' => $saldo_neto + ]; + } + + echo json_encode([ + 'tipo_reporte' => 'semanal', + 'periodo' => $semana, + 'fecha_inicio' => $fecha_inicio, + 'fecha_fin' => $fecha_fin, + 'dias' => $semana_datos, + 'resumen' => [ + 'ingresos_reservas_totales' => array_sum(array_column($semana_datos, 'ingresos_reservas')), + 'ingresos_cantina_totales' => array_sum(array_column($semana_datos, 'ingresos_cantina')), + 'costo_mercaderia_total' => array_sum(array_column($semana_datos, 'costo_mercaderia')), + 'total_ingresos' => array_sum(array_column($semana_datos, 'total_ingresos')), + 'saldo_neto_semana' => array_sum(array_column($semana_datos, 'saldo_neto')), + 'ocupacion_promedio' => $ocupacion, + 'turnos_ocupados' => $turnos_ocupados, + 'turnos_disponibles' => $turnos_disponibles, + 'gastos_fijos_estimados' => $gastos_totales / 4 // Aproximado a una semana + ] + ]); + } + + // ===== REPORTE MENSUAL ===== + elseif ($tipo === 'mensual') { + if (!$mes) { + $mes = date('Y-m'); + } + + list($anio_m, $mes_m) = explode('-', $mes); + $fecha_inicio = $anio_m . '-' . $mes_m . '-01'; + $fecha_fin = date('Y-m-d', strtotime($fecha_inicio . ' +1 month -1 day')); + + // Ingresos por categoría + $categorias = ['Fútbol', 'Pádel', 'Quincho', 'Cantina']; + $datos_categorias = []; + + // Reservas (Fútbol, Pádel, Quincho) + $sql_futbol = " + SELECT + 'Fútbol' as categoria, + COUNT(r.ID_Reserva) as operaciones, + COALESCE(SUM(r.Monto), 0) as monto + FROM reservas r + JOIN canchas c ON r.ID_Cancha = c.ID_Cancha + WHERE c.Tipo = 'F' AND DATE(r.Fecha_Hora) BETWEEN '$fecha_inicio' AND '$fecha_fin' + "; + + $sql_padel = " + SELECT + 'Pádel' as categoria, + COUNT(r.ID_Reserva) as operaciones, + COALESCE(SUM(r.Monto), 0) as monto + FROM reservas r + JOIN canchas c ON r.ID_Cancha = c.ID_Cancha + WHERE c.Tipo = 'P' AND DATE(r.Fecha_Hora) BETWEEN '$fecha_inicio' AND '$fecha_fin' + "; + + $sql_quincho = " + SELECT + 'Quincho' as categoria, + COUNT(r.ID_Reserva) as operaciones, + COALESCE(SUM(r.Monto), 0) as monto + FROM reservas r + JOIN canchas c ON r.ID_Cancha = c.ID_Cancha + WHERE c.Tipo = 'Q' AND DATE(r.Fecha_Hora) BETWEEN '$fecha_inicio' AND '$fecha_fin' + "; + + $sql_cantina = " + SELECT + 'Cantina' as categoria, + COUNT(DISTINCT v.ID_Venta) as operaciones, + COALESCE(SUM(vp.Cantidad * p.Precio_Venta), 0) as monto + FROM ventas v + JOIN ventas_productos vp ON v.ID_Venta = vp.ID_Venta + JOIN productos p ON vp.ID_Producto = p.ID_Producto + WHERE DATE(v.Fecha) BETWEEN '$fecha_inicio' AND '$fecha_fin' + "; + + foreach ([$sql_futbol, $sql_padel, $sql_quincho, $sql_cantina] as $sql) { + $result = $conexion->query($sql); + if ($result && $result->num_rows > 0) { + $row = $result->fetch_assoc(); + $datos_categorias[] = [ + 'categoria' => $row['categoria'], + 'operaciones' => intval($row['operaciones']), + 'monto' => floatval($row['monto']) + ]; + } else { + $datos_categorias[] = [ + 'categoria' => $row['categoria'] ?? '', + 'operaciones' => 0, + 'monto' => 0 + ]; + } + } + + // Costo de Mercadería + $sql_costo = " + SELECT + 'Costo de Mercadería' as concepto, + COALESCE(SUM(op.Cantidad * p.Precio_Compra), 0) as monto + FROM ordenes_compra oc + JOIN ordenes_productos op ON oc.ID_Orden = op.ID_Orden + JOIN productos p ON op.ID_Producto = p.ID_Producto + WHERE oc.Estado = 1 + AND DATE(oc.Fecha) BETWEEN '$fecha_inicio' AND '$fecha_fin' + "; + + $result_costo = $conexion->query($sql_costo); + $costo_mercaderia = 0; + if ($result_costo && $result_costo->num_rows > 0) { + $row = $result_costo->fetch_assoc(); + $costo_mercaderia = floatval($row['monto']); + } + + // Gastos fijos + $gastos_fijos_data = obtenerGastosFijos($conexion, $fecha_inicio, $fecha_fin); + $gastos_fijos = []; + foreach ($gastos_fijos_data['detalles'] as $gasto) { + $gastos_fijos[] = [ + 'descripcion' => $gasto['descripcion'], + 'monto' => $gasto['monto'] + ]; + } + $total_gastos_fijos = $gastos_fijos_data['total']; + + // Calcular ganancias + $ingresos_totales = array_sum(array_column($datos_categorias, 'monto')); + $ganancia_bruta = $ingresos_totales - $costo_mercaderia; + $ganancia_neta = $ganancia_bruta - $total_gastos_fijos; + + // Obtener el nombre del mes en español + $meses_nombres = [ + '01' => 'enero', '02' => 'febrero', '03' => 'marzo', '04' => 'abril', + '05' => 'mayo', '06' => 'junio', '07' => 'julio', '08' => 'agosto', + '09' => 'septiembre', '10' => 'octubre', '11' => 'noviembre', '12' => 'diciembre' + ]; + $nombre_mes = $meses_nombres[$mes_m]; + + echo json_encode([ + 'tipo_reporte' => 'mensual', + 'periodo' => $mes, + 'fecha_inicio' => $fecha_inicio, + 'fecha_fin' => $fecha_fin, + 'nombre_mes' => $nombre_mes, + 'categorias' => $datos_categorias, + 'gastos_fijos' => $gastos_fijos, + 'resumen' => [ + 'ingresos_totales' => $ingresos_totales, + 'costo_mercaderia' => $costo_mercaderia, + 'gastos_fijos_total' => $total_gastos_fijos, + 'ganancia_bruta' => $ganancia_bruta, + 'ganancia_neta' => $ganancia_neta + ] + ]); + } + + // ===== REPORTE ANUAL ===== + elseif ($tipo === 'anual') { + if (!$anio) { + $anio = date('Y'); + } + + $fecha_inicio = $anio . '-01-01'; + $fecha_fin = $anio . '-12-31'; + + // Determinar si es el año actual + $ano_actual = intval(date('Y')); + $es_ano_actual = ($anio == $ano_actual); + $mes_actual = intval(date('m')); + $mes_limite = $es_ano_actual ? $mes_actual : 12; + + $meses = [ + '01' => 'Enero', '02' => 'Febrero', '03' => 'Marzo', '04' => 'Abril', + '05' => 'Mayo', '06' => 'Junio', '07' => 'Julio', '08' => 'Agosto', + '09' => 'Septiembre', '10' => 'Octubre', '11' => 'Noviembre', '12' => 'Diciembre' + ]; + + $datos_anuales = []; + + for ($mes_num = 1; $mes_num <= $mes_limite; $mes_num++) { + $mes_str = str_pad($mes_num, 2, '0', STR_PAD_LEFT); + $mes_inicio = $anio . '-' . $mes_str . '-01'; + $mes_fin = date('Y-m-d', strtotime($mes_inicio . ' +1 month -1 day')); + + // Si es el mes actual, solo contar hasta hoy + if ($es_ano_actual && $mes_num == $mes_actual) { + $mes_fin = date('Y-m-d'); + } + + // Ingreso Operativo + $sql_ingreso = " + SELECT + COALESCE(SUM(r.Monto), 0) as reservas, + (SELECT COALESCE(SUM(vp.Cantidad * p.Precio_Venta), 0) + FROM ventas v + JOIN ventas_productos vp ON v.ID_Venta = vp.ID_Venta + JOIN productos p ON vp.ID_Producto = p.ID_Producto + WHERE DATE(v.Fecha) BETWEEN '$mes_inicio' AND '$mes_fin') as ventas + FROM reservas r + WHERE DATE(r.Fecha_Hora) BETWEEN '$mes_inicio' AND '$mes_fin' + "; + + $result_ingreso = $conexion->query($sql_ingreso); + $ingreso_operativo = 0; + + if ($result_ingreso) { + $row = $result_ingreso->fetch_assoc(); + $ingreso_operativo = floatval($row['reservas']) + floatval($row['ventas']); + } + + // Costo Bruto (Mercadería) + $sql_costo_bruto = " + SELECT COALESCE(SUM(op.Cantidad * p.Precio_Compra), 0) as mercaderia + FROM ordenes_compra oc + JOIN ordenes_productos op ON oc.ID_Orden = op.ID_Orden + JOIN productos p ON op.ID_Producto = p.ID_Producto + WHERE oc.Estado = 1 + AND DATE(oc.Fecha) BETWEEN '$mes_inicio' AND '$mes_fin' + "; + + $result_costo_bruto = $conexion->query($sql_costo_bruto); + $costo_bruto = 0; + + if ($result_costo_bruto) { + $row = $result_costo_bruto->fetch_assoc(); + $costo_bruto = floatval($row['mercaderia']); + } + + // Obtener gastos fijos dinámicamente + $gastos_fijos_data = obtenerGastosFijos($conexion, $mes_inicio, $mes_fin); + $costo_gastos_fijos = $gastos_fijos_data['total']; + + $costo_operativo_total = $costo_bruto + $costo_gastos_fijos; + $ganancia_bruta = $ingreso_operativo - $costo_bruto; + $ganancia_neta = $ingreso_operativo - $costo_operativo_total; + + $datos_anuales[] = [ + 'mes' => $meses[$mes_str], + 'ingreso_operativo' => $ingreso_operativo, + 'costo_bruto' => $costo_bruto, + 'costo_operativo' => $costo_operativo_total, + 'ganancia_bruta' => $ganancia_bruta, + 'ganancia_neta' => $ganancia_neta + ]; + } + + // Calcular totales anuales + $ingreso_operativo_total = array_sum(array_column($datos_anuales, 'ingreso_operativo')); + $costo_bruto_total = array_sum(array_column($datos_anuales, 'costo_bruto')); + $costo_operativo_total = array_sum(array_column($datos_anuales, 'costo_operativo')); + $ganancia_bruta_total = array_sum(array_column($datos_anuales, 'ganancia_bruta')); + $ganancia_neta_total = array_sum(array_column($datos_anuales, 'ganancia_neta')); + + echo json_encode([ + 'tipo_reporte' => 'anual', + 'periodo' => $anio, + 'es_ano_actual' => $es_ano_actual, + 'mes_actual' => $mes_actual, + 'meses' => $datos_anuales, + 'totales' => [ + 'ingreso_operativo_total' => $ingreso_operativo_total, + 'costo_bruto_total' => $costo_bruto_total, + 'costo_operativo_total' => $costo_operativo_total, + 'ganancia_bruta_total' => $ganancia_bruta_total, + 'ganancia_neta_total' => $ganancia_neta_total + ] + ]); + } + + } // FIN REPORTE FINANCIERO + + // ===== REPORTE VENTAS CANTINA ===== + elseif ($tipo_reporte === 'ventas_cantina') { + $fecha_inicio = ''; + $fecha_fin = ''; + + if ($tipo === 'diario') { + $fecha_inicio = $fecha; + $fecha_fin = $fecha; + } elseif ($tipo === 'semanal') { + if (!$semana) $semana = date('Y-W'); + $semanaPartes = explode('-W', $semana); + $anio_s = intval($semanaPartes[0]); + $semana_s = intval($semanaPartes[1]); + $fecha_base = new DateTime($anio_s . '-01-04'); + $dia_semana = $fecha_base->format('N'); + $fecha_base->modify('-' . ($dia_semana - 1) . ' days'); + $fecha_base->modify('+' . ($semana_s - 1) . ' weeks'); + $fecha_inicio = $fecha_base->format('Y-m-d'); + $fecha_fin = $fecha_base->modify('+6 days')->format('Y-m-d'); + } elseif ($tipo === 'mensual') { + if (!$mes) $mes = date('Y-m'); + list($anio_m, $mes_m) = explode('-', $mes); + $fecha_inicio = $anio_m . '-' . $mes_m . '-01'; + $fecha_fin = date('Y-m-d', strtotime($fecha_inicio . ' +1 month -1 day')); + } elseif ($tipo === 'anual') { + if (!$anio) $anio = date('Y'); + $fecha_inicio = $anio . '-01-01'; + $fecha_fin = $anio . '-12-31'; + } + + // Obtener productos de cantina con ventas + $sql_productos = " + SELECT + p.ID_Producto, + p.Descripcion as producto, + 'Cantina' as categoria, + SUM(vp.Cantidad) as cantidad_vendida, + SUM(vp.Cantidad * p.Precio_Venta) as ingreso_generado, + COALESCE(SUM(vp.Cantidad * (p.Precio_Venta - p.Precio_Compra)), 0) as ganancia + FROM productos p + LEFT JOIN ventas_productos vp ON p.ID_Producto = vp.ID_Producto + LEFT JOIN ventas v ON vp.ID_Venta = v.ID_Venta + WHERE DATE(v.Fecha) BETWEEN '$fecha_inicio' AND '$fecha_fin' + GROUP BY p.ID_Producto, p.Descripcion + ORDER BY cantidad_vendida DESC + "; + + $result_productos = $conexion->query($sql_productos); + if (!$result_productos) { + throw new Exception("Error en query productos: " . $conexion->error); + } + + $productos = []; + $total_ingreso = 0; + $total_cantidad = 0; + + while ($row = $result_productos->fetch_assoc()) { + $ingreso = floatval($row['ingreso_generado']) ?? 0; + $productos[] = [ + 'producto' => $row['producto'], + 'categoria' => $row['categoria'], + 'cantidad_vendida' => intval($row['cantidad_vendida']) ?? 0, + 'ingreso_generado' => $ingreso, + 'ganancia' => floatval($row['ganancia']) ?? 0 + ]; + $total_ingreso += $ingreso; + $total_cantidad += intval($row['cantidad_vendida']) ?? 0; + } + + // Calcular porcentajes sobre el total de unidades vendidas + foreach ($productos as &$producto) { + $producto['porcentaje_total'] = $total_cantidad > 0 ? round(($producto['cantidad_vendida'] / $total_cantidad) * 100, 2) : 0; + } + unset($producto); + + echo json_encode([ + 'tipo_reporte' => 'ventas_cantina', + 'periodo' => $tipo, + 'fecha_inicio' => $fecha_inicio, + 'fecha_fin' => $fecha_fin, + 'productos' => $productos, + 'resumen' => [ + 'total_productos_vendidos' => count($productos), + 'total_cantidad' => $total_cantidad, + 'total_ingreso' => $total_ingreso, + 'ganancia_total' => array_sum(array_column($productos, 'ganancia')) + ] + ]); + } + + // ===== REPORTE OCUPACIÓN CANCHAS ===== + elseif ($tipo_reporte === 'ocupacion_canchas') { + $fecha_inicio = ''; + $fecha_fin = ''; + + if ($tipo === 'diario') { + $fecha_inicio = $fecha; + $fecha_fin = $fecha; + } elseif ($tipo === 'semanal') { + if (!$semana) $semana = date('Y-W'); + $semanaPartes = explode('-W', $semana); + $anio_s = intval($semanaPartes[0]); + $semana_s = intval($semanaPartes[1]); + $fecha_base = new DateTime($anio_s . '-01-04'); + $dia_semana = $fecha_base->format('N'); + $fecha_base->modify('-' . ($dia_semana - 1) . ' days'); + $fecha_base->modify('+' . ($semana_s - 1) . ' weeks'); + $fecha_inicio = $fecha_base->format('Y-m-d'); + $fecha_fin = $fecha_base->modify('+6 days')->format('Y-m-d'); + } elseif ($tipo === 'mensual') { + if (!$mes) $mes = date('Y-m'); + list($anio_m, $mes_m) = explode('-', $mes); + $fecha_inicio = $anio_m . '-' . $mes_m . '-01'; + $fecha_fin = date('Y-m-d', strtotime($fecha_inicio . ' +1 month -1 day')); + } elseif ($tipo === 'anual') { + if (!$anio) $anio = date('Y'); + $fecha_inicio = $anio . '-01-01'; + $fecha_fin = $anio . '-12-31'; + } + $total_dias_periodo = (strtotime($fecha_fin) - strtotime($fecha_inicio)) / (60 * 60 * 24) + 1; + + // Filtro por tipo de cancha si está especificado + $tipo_cancha_filtro = $tipo_cancha ?? ''; + $filtro_cancha = ''; + if ($tipo_cancha_filtro) { + $filtro_cancha = "AND c.Tipo = '$tipo_cancha_filtro'"; + } + + // 1. Reservas por día + $sql_por_dia = " + SELECT + DATE(r.Fecha_Hora) as dia, + DAYNAME(r.Fecha_Hora) as nombre_dia, + COUNT(r.ID_Reserva) as cantidad_reservas, + COUNT(CASE WHEN r.Estado = 2 THEN 1 END) as canceladas + FROM reservas r + JOIN canchas c ON r.ID_Cancha = c.ID_Cancha + WHERE DATE(r.Fecha_Hora) BETWEEN '$fecha_inicio' AND '$fecha_fin' + $filtro_cancha + GROUP BY DATE(r.Fecha_Hora) + ORDER BY dia ASC + "; + + $result_dia = $conexion->query($sql_por_dia); + if (!$result_dia) { + throw new Exception("Error en query por día: " . $conexion->error); + } + + $reservas_por_dia = []; + $total_reservas = 0; + $total_canceladas = 0; + + $dias_map = [ + 'Monday' => 'Lunes', + 'Tuesday' => 'Martes', + 'Wednesday' => 'Miércoles', + 'Thursday' => 'Jueves', + 'Friday' => 'Viernes', + 'Saturday' => 'Sábado', + 'Sunday' => 'Domingo' + ]; + + while ($row = $result_dia->fetch_assoc()) { + $nombre_dia_original = $row['nombre_dia']; + $nombre_dia_es = isset($dias_map[$nombre_dia_original]) ? $dias_map[$nombre_dia_original] : $nombre_dia_original; + $reservas_por_dia[] = [ + 'dia' => $row['dia'], + 'nombre_dia' => $nombre_dia_es, + 'cantidad_reservas' => intval($row['cantidad_reservas']), + 'canceladas' => intval($row['canceladas']) + ]; + $total_reservas += intval($row['cantidad_reservas']); + $total_canceladas += intval($row['canceladas']); + } + + // 2. Reservas por horario - Considerar duración de cada tipo de cancha + // Primero obtener todos los tipos de canchas con su duración + $sql_tipos_canchas = " + SELECT DISTINCT + CASE c.Tipo WHEN 'F' THEN 'Fútbol' WHEN 'P' THEN 'Pádel' WHEN 'Q' THEN 'Quincho' ELSE 'Otro' END as tipo_nombre, + c.Tipo as tipo_codigo, + c.Duracion as duracion_minutos + FROM canchas c + WHERE 1=1 $filtro_cancha + ORDER BY c.Tipo + "; + + $result_tipos = $conexion->query($sql_tipos_canchas); + if (!$result_tipos) { + throw new Exception("Error al obtener tipos de canchas: " . $conexion->error); + } + + $tipos_canchas = []; + while ($row = $result_tipos->fetch_assoc()) { + $tipos_canchas[] = [ + 'tipo_nombre' => $row['tipo_nombre'], + 'tipo_codigo' => $row['tipo_codigo'], + 'duracion_minutos' => intval($row['duracion_minutos']) + ]; + } + + // Si no hay tipos de canchas, retornar estructura vacía + if (empty($tipos_canchas)) { + $tipos_canchas[] = [ + 'tipo_nombre' => 'Fútbol', + 'tipo_codigo' => 'F', + 'duracion_minutos' => 60 + ]; + } + + // Generar horarios disponibles por tipo de cancha + $horarios_por_tipo = []; + + foreach ($tipos_canchas as $tipo_cancha_info) { + $tipo_codigo = $tipo_cancha_info['tipo_codigo']; + $duracion_min = $tipo_cancha_info['duracion_minutos']; + $horarios_unicos_tipo = []; + + if ($tipo_codigo === 'Q') { + $horarios_unicos_tipo['reserva_quincho'] = [ + 'key' => 'reserva_quincho', + 'horario' => 'Reserva del Quincho', + 'inicio' => '', + 'fin' => '', + 'es_quincho' => true + ]; + } else { + $fecha_temp = new DateTime($fecha_inicio); + $fecha_fin_dt = new DateTime($fecha_fin); + + while ($fecha_temp <= $fecha_fin_dt) { + $diaSemana = intval($fecha_temp->format('N')); + $diaTexto = $diaSemana >= 1 && $diaSemana <= 5 ? 'Lunes a Viernes' : ($diaSemana === 6 ? 'Sábado' : 'Domingo'); + + $sqlHorario = "SELECT Hora_Apertura, Hora_Cierre FROM horarios WHERE Dia = '$diaTexto' LIMIT 1"; + $resultHorario = $conexion->query($sqlHorario); + if ($resultHorario && $rowHorario = $resultHorario->fetch_assoc()) { + $horaApertura = $rowHorario['Hora_Apertura']; + $horaCierre = $rowHorario['Hora_Cierre']; + if ($horaApertura && $horaCierre) { + $start_hour = intval(substr($horaApertura, 0, 2)); + $start_min = intval(substr($horaApertura, 3, 2)); + $end_hour = intval(substr($horaCierre, 0, 2)); + $end_min = intval(substr($horaCierre, 3, 2)); + + // Convertir a minutos desde medianoche + $apertura_minutos = $start_hour * 60 + $start_min; + $cierre_minutos = $end_hour * 60 + $end_min; + + // Generar horarios considerando la duración real de la cancha + $tiempo_actual = $apertura_minutos; + while ($tiempo_actual + $duracion_min <= $cierre_minutos) { + $hora_inicio = intval($tiempo_actual / 60); + $minuto_inicio = $tiempo_actual % 60; + $hora_fin = $tiempo_actual + $duracion_min; + $hora_fin_h = intval($hora_fin / 60); + $minuto_fin = $hora_fin % 60; + $inicio_texto = sprintf('%02d:%02d', $hora_inicio, $minuto_inicio); + $fin_texto = sprintf('%02d:%02d', $hora_fin_h, $minuto_fin); + $horario_formato = $inicio_texto . ' - ' . $fin_texto; + $horarios_unicos_tipo[$inicio_texto] = [ + 'key' => $inicio_texto, + 'horario' => $horario_formato, + 'inicio' => $inicio_texto, + 'fin' => $fin_texto, + 'es_quincho' => false + ]; + $tiempo_actual += $duracion_min; + } + } + } + $fecha_temp->modify('+1 day'); + } + } + + // Guardar horarios por tipo + ksort($horarios_unicos_tipo); + $horarios_por_tipo[$tipo_cancha_info['tipo_nombre']] = array_values($horarios_unicos_tipo); + } + + // Obtener reservas por horario y tipo de cancha + $sql_por_horario = " + SELECT + CASE c.Tipo WHEN 'F' THEN 'Fútbol' WHEN 'P' THEN 'Pádel' WHEN 'Q' THEN 'Quincho' ELSE 'Otro' END as tipo_nombre, + CASE WHEN c.Tipo = 'Q' THEN 'reserva_quincho' ELSE TIME_FORMAT(r.Fecha_Hora, '%H:%i') END as horario_key, + CASE WHEN c.Tipo = 'Q' THEN 'Reserva del Quincho' ELSE TIME_FORMAT(r.Fecha_Hora, '%H:%i') END as horario, + COUNT(r.ID_Reserva) as cantidad_reservas, + COUNT(CASE WHEN r.Estado = 2 THEN 1 END) as canceladas + FROM reservas r + JOIN canchas c ON r.ID_Cancha = c.ID_Cancha + WHERE DATE(r.Fecha_Hora) BETWEEN '$fecha_inicio' AND '$fecha_fin' + $filtro_cancha + GROUP BY CASE c.Tipo WHEN 'F' THEN 'Fútbol' WHEN 'P' THEN 'Pádel' WHEN 'Q' THEN 'Quincho' ELSE 'Otro' END, + CASE WHEN c.Tipo = 'Q' THEN 'reserva_quincho' ELSE TIME_FORMAT(r.Fecha_Hora, '%H:%i') END + ORDER BY tipo_nombre, horario ASC + "; + + $result_horario = $conexion->query($sql_por_horario); + if (!$result_horario) { + throw new Exception("Error en query por horario: " . $conexion->error); + } + + // Obtener conteo de reservas de Quincho por horario (agregado para mostrar en tablas de Fútbol/Pádel) + $sql_quincho_por_horario = " + SELECT + TIME_FORMAT(r.Fecha_Hora, '%H:%i') as horario_key, + COUNT(r.ID_Reserva) as cantidad + FROM reservas r + JOIN canchas c ON r.ID_Cancha = c.ID_Cancha + WHERE c.Tipo = 'Q' AND DATE(r.Fecha_Hora) BETWEEN '$fecha_inicio' AND '$fecha_fin' + GROUP BY horario_key + "; + $result_quincho_hor = $conexion->query($sql_quincho_por_horario); + $quincho_por_horario = []; + if ($result_quincho_hor) { + while ($q = $result_quincho_hor->fetch_assoc()) { + $quincho_por_horario[$q['horario_key']] = intval($q['cantidad']); + } + } + + // Obtener conteo de reservas de quincho asociadas a cada tipo de cancha (emparejando por Fecha_Hora e ID_Cliente) + $sql_quincho_asociada = " + SELECT + TIME_FORMAT(q.Fecha_Hora, '%H:%i') as horario_key, + CASE WHEN c2.Tipo = 'F' THEN 'Fútbol' WHEN c2.Tipo = 'P' THEN 'Pádel' ELSE 'Otro' END as asociado_tipo, + COUNT(q.ID_Reserva) as cantidad + FROM reservas q + JOIN canchas cq ON q.ID_Cancha = cq.ID_Cancha AND cq.Tipo = 'Q' + JOIN reservas r ON q.Fecha_Hora = r.Fecha_Hora AND q.ID_Cliente = r.ID_Cliente + JOIN canchas c2 ON r.ID_Cancha = c2.ID_Cancha + WHERE DATE(q.Fecha_Hora) BETWEEN '$fecha_inicio' AND '$fecha_fin' AND c2.Tipo IN ('F','P') + GROUP BY horario_key, asociado_tipo + "; + $result_quincho_asoc = $conexion->query($sql_quincho_asociada); + $quincho_asociada = []; + if ($result_quincho_asoc) { + while ($qa = $result_quincho_asoc->fetch_assoc()) { + $tipo_asoc = $qa['asociado_tipo']; + $hk = $qa['horario_key']; + if (!isset($quincho_asociada[$tipo_asoc])) $quincho_asociada[$tipo_asoc] = []; + $quincho_asociada[$tipo_asoc][$hk] = intval($qa['cantidad']); + } + } + + $reservas_por_horario_temp = []; + while ($row = $result_horario->fetch_assoc()) { + $tipo = $row['tipo_nombre']; + $horario_key = $row['horario_key']; + if (!isset($reservas_por_horario_temp[$tipo])) { + $reservas_por_horario_temp[$tipo] = []; + } + $reservas_por_horario_temp[$tipo][$horario_key] = [ + 'horario' => $row['horario'], + 'inicio' => $row['horario_key'], + 'fin' => '', + 'cantidad_reservas' => intval($row['cantidad_reservas']), + 'canceladas' => intval($row['canceladas']) + ]; + // Añadir conteo de reservas de quincho vinculadas por horario. + // Si existe asociación por tipo (Fútbol/Pádel), usarla; si no, usar conteo general de quincho por horario. + $reservas_por_horario_temp[$tipo][$horario_key]['reservas_quincho'] = 0; + if (isset($quincho_asociada[$tipo][$horario_key])) { + $reservas_por_horario_temp[$tipo][$horario_key]['reservas_quincho'] = intval($quincho_asociada[$tipo][$horario_key]); + } else { + $reservas_por_horario_temp[$tipo][$horario_key]['reservas_quincho'] = intval($quincho_por_horario[$horario_key] ?? 0); + } + } + + // Combinar todos los horarios disponibles con las reservas por tipo + $reservas_por_horario = []; + foreach ($horarios_por_tipo as $tipo_cancha => $horarios_disponibles) { + $reservas_por_horario[$tipo_cancha] = []; + foreach ($horarios_disponibles as $horario) { + $key = $horario['key']; + if (isset($reservas_por_horario_temp[$tipo_cancha][$key])) { + $item = $reservas_por_horario_temp[$tipo_cancha][$key]; + $item['horario'] = $horario['horario']; + $item['inicio'] = $horario['inicio']; + $item['fin'] = $horario['fin']; + $item['es_quincho'] = $horario['es_quincho'] ?? false; + $reservas_por_horario[$tipo_cancha][] = $item; + } else { + // Horario sin reservas + $reservas_por_horario[$tipo_cancha][] = [ + 'horario' => $horario['horario'], + 'inicio' => $horario['inicio'], + 'fin' => $horario['fin'], + 'cantidad_reservas' => 0, + 'canceladas' => 0, + 'es_quincho' => $horario['es_quincho'] ?? false, + 'reservas_quincho' => intval(isset($quincho_asociada[$tipo_cancha][$key]) ? $quincho_asociada[$tipo_cancha][$key] : ($quincho_por_horario[$horario['inicio']] ?? 0)) + ]; + } + } + } + + // 3. Ocupación por cancha + $sql_ocupacion_cancha = " + SELECT + c.ID_Cancha, + CASE c.Tipo WHEN 'F' THEN 'Fútbol' WHEN 'P' THEN 'Pádel' WHEN 'Q' THEN 'Quincho' ELSE 'Otro' END as tipo, + c.Duracion as duracion_minutos, + COUNT(r.ID_Reserva) as cantidad_reservas, + COUNT(CASE WHEN r.Estado != 2 THEN 1 END) as activas, + COUNT(CASE WHEN r.Estado = 2 THEN 1 END) as canceladas + FROM canchas c + LEFT JOIN reservas r ON c.ID_Cancha = r.ID_Cancha + AND DATE(r.Fecha_Hora) BETWEEN '$fecha_inicio' AND '$fecha_fin' + WHERE 1=1 $filtro_cancha + GROUP BY c.ID_Cancha, c.Tipo + ORDER BY c.Tipo, c.ID_Cancha ASC + "; + + $result_cancha = $conexion->query($sql_ocupacion_cancha); + if (!$result_cancha) { + throw new Exception("Error en query ocupación cancha: " . $conexion->error); + } + + $ocupacion_cancha = []; + $horas_disponibles_en_periodo = obtenerHorasDisponiblesPorPeriodo($conexion, $fecha_inicio, $fecha_fin); + $minutos_disponibles_por_cancha = $horas_disponibles_en_periodo * 60; + + while ($row = $result_cancha->fetch_assoc()) { + $minutos_reservados = intval($row['activas']) * intval($row['duracion_minutos']); + $ocupacion = 0; + if ($minutos_disponibles_por_cancha > 0) { + $ocupacion = round(($minutos_reservados / $minutos_disponibles_por_cancha) * 100, 2); + } + + $ocupacion_cancha[] = [ + 'numero' => intval($row['ID_Cancha']), + 'tipo' => $row['tipo'], + 'duracion_minutos' => intval($row['duracion_minutos']), + 'cantidad_reservas' => intval($row['cantidad_reservas']), + 'activas' => intval($row['activas']), + 'canceladas' => intval($row['canceladas']), + 'porcentaje_ocupacion' => $ocupacion + ]; + } + + // Para el período diario, no mostrar % de ocupación para el Quincho + if ($tipo === 'diario') { + foreach ($ocupacion_cancha as &$cancha_tmp) { + if (isset($cancha_tmp['tipo']) && $cancha_tmp['tipo'] === 'Quincho') { + $cancha_tmp['porcentaje_ocupacion'] = null; + } + } + unset($cancha_tmp); + } + + // Calcular horarios pico y bajos por tipo de cancha (considerando horarios sin reservas) + $horarios_pico_por_tipo = []; + $horarios_bajos_por_tipo = []; + $horarios_sin_reservas_por_tipo = []; + + foreach ($reservas_por_horario as $tipo_cancha => $horarios) { + $horarios_pico_por_tipo[$tipo_cancha] = []; + $horarios_bajos_por_tipo[$tipo_cancha] = []; + $horarios_sin_reservas_por_tipo[$tipo_cancha] = []; + + if (!empty($horarios)) { + // Separar horarios con reservas de los sin reservas + $horarios_con_reservas = array_filter($horarios, function($h) { + return $h['cantidad_reservas'] > 0; + }); + + $horarios_sin_reservas_temp = array_filter($horarios, function($h) { + return $h['cantidad_reservas'] == 0; + }); + + // Extraer solo los horarios sin reservas + $horarios_sin_reservas_por_tipo[$tipo_cancha] = array_values(array_map(function($h) { return $h['horario']; }, $horarios_sin_reservas_temp)); + + if ($tipo === 'diario') { + // Para diario: Horarios Con Reservas = todos los que tienen reservas + $horarios_pico_por_tipo[$tipo_cancha] = array_values(array_map(function($h) { return $h['horario']; }, $horarios_con_reservas)); + // Horarios sin reservas ya está definido arriba + } else { + // Para otros períodos: calcular promedio y separar en mayor y menor demanda + if (!empty($horarios_con_reservas)) { + $promedio_reservas = array_sum(array_column($horarios_con_reservas, 'cantidad_reservas')) / count($horarios_con_reservas); + + foreach ($horarios_con_reservas as $horario) { + if ($horario['cantidad_reservas'] >= $promedio_reservas) { + $horarios_pico_por_tipo[$tipo_cancha][] = $horario['horario']; + } else { + $horarios_bajos_por_tipo[$tipo_cancha][] = $horario['horario']; + } + } + + // Incluir horarios sin reservas en "bajos" + $horarios_bajos_por_tipo[$tipo_cancha] = array_merge($horarios_bajos_por_tipo[$tipo_cancha], $horarios_sin_reservas_por_tipo[$tipo_cancha]); + sort($horarios_bajos_por_tipo[$tipo_cancha]); + } + } + } + } + + // Para compatibilidad, crear arrays planos con todos los horarios (solo si hay datos) + $horarios_pico = []; + $horarios_bajos = []; + $horarios_sin_reservas = []; + + if (!empty($horarios_pico_por_tipo)) { + foreach ($horarios_pico_por_tipo as $horarios_tipo) { + $horarios_pico = array_merge($horarios_pico, $horarios_tipo); + } + } + if (!empty($horarios_bajos_por_tipo)) { + foreach ($horarios_bajos_por_tipo as $horarios_tipo) { + $horarios_bajos = array_merge($horarios_bajos, $horarios_tipo); + } + } + if (!empty($horarios_sin_reservas_por_tipo)) { + foreach ($horarios_sin_reservas_por_tipo as $horarios_tipo) { + $horarios_sin_reservas = array_merge($horarios_sin_reservas, $horarios_tipo); + } + } + + // Ocupación general del período (reservado horas / horas disponibles) + $ocupacion_general = 0; + $total_cancha_horas_disponibles = count($ocupacion_cancha) * $horas_disponibles_en_periodo; + $minutos_reservados_total = 0; + foreach ($ocupacion_cancha as $cancha) { + $minutos_reservados_total += intval($cancha['activas']) * intval($cancha['duracion_minutos']); + } + if ($total_cancha_horas_disponibles > 0) { + $ocupacion_general = round(($minutos_reservados_total / ($total_cancha_horas_disponibles * 60)) * 100, 2); + } + + echo json_encode([ + 'tipo_reporte' => 'ocupacion_canchas', + 'periodo' => $tipo, + 'fecha_inicio' => $fecha_inicio, + 'fecha_fin' => $fecha_fin, + 'tipo_cancha_filtro' => $tipo_cancha_filtro ?: 'Todas', + 'reservas_por_dia' => $reservas_por_dia, + 'reservas_por_horario' => $reservas_por_horario, + 'reservas_por_horario_por_tipo' => [ + 'horarios_pico_por_tipo' => $horarios_pico_por_tipo, + 'horarios_bajos_por_tipo' => $horarios_bajos_por_tipo, + 'horarios_sin_reservas_por_tipo' => $horarios_sin_reservas_por_tipo + ], + 'ocupacion_cancha' => $ocupacion_cancha, + 'horarios_pico' => $horarios_pico, + 'horarios_bajos' => $horarios_bajos, + 'horarios_sin_reservas' => $horarios_sin_reservas, + 'resumen' => [ + 'total_reservas' => $total_reservas, + 'total_canceladas' => $total_canceladas, + 'reservas_activas' => $total_reservas - $total_canceladas, + 'ocupacion_promedio' => $ocupacion_general, + 'dias_en_periodo' => intval($total_dias_periodo), + 'total_horarios' => count($reservas_por_horario), + 'total_canchas' => count($ocupacion_cancha) + ] + ]); + } + + else { + echo json_encode(['error' => 'Tipo de reporte no válido']); + } + +} catch (Exception $e) { + echo json_encode(['error' => $e->getMessage()]); +} +?> diff --git a/php/Obtener_Reservas.php b/php/Obtener_Reservas.php new file mode 100644 index 0000000..ab787ed --- /dev/null +++ b/php/Obtener_Reservas.php @@ -0,0 +1,62 @@ + mysqli_error($conexion)])); +} + +if ($resultado) { + while ($fila = mysqli_fetch_assoc($resultado)) { + $reservas[] = $fila; + } +} + +header('Content-Type: application/json'); +echo json_encode($reservas); +?> \ No newline at end of file diff --git a/php/Obtener_Usuarios.php b/php/Obtener_Usuarios.php new file mode 100644 index 0000000..21dee48 --- /dev/null +++ b/php/Obtener_Usuarios.php @@ -0,0 +1,23 @@ +query($query); + +if ($result) { + $usuarios = []; + while ($row = $result->fetch_assoc()) { + $usuarios[] = $row; + } + echo json_encode(['success' => true, 'usuarios' => $usuarios]); +} else { + echo json_encode(['success' => false, 'message' => 'Error al obtener usuarios: ' . $conexion->error]); +} + +$conexion->close(); +?> \ No newline at end of file diff --git a/php/Obtener_Ventas.php b/php/Obtener_Ventas.php new file mode 100644 index 0000000..66dbcb3 --- /dev/null +++ b/php/Obtener_Ventas.php @@ -0,0 +1,114 @@ +prepare(" + SELECT + vp.ID_Producto, + p.Descripcion as nombre, + vp.Cantidad, + COALESCE(p.Precio_Venta, 0) as precio_unitario, + (vp.Cantidad * COALESCE(p.Precio_Venta, 0)) as subtotal + FROM ventas_productos vp + LEFT JOIN productos p ON vp.ID_Producto = p.ID_Producto + WHERE vp.ID_Venta = ? + ORDER BY vp.ID_Producto + "); + + if (!$stmt) { + http_response_code(500); + echo json_encode(['error' => 'Error en la consulta preparada: ' . $conexion->error]); + exit; + } + + $stmt->bind_param("i", $id_venta); + $stmt->execute(); + + if ($stmt->error) { + http_response_code(500); + echo json_encode(['error' => 'Error al ejecutar: ' . $stmt->error]); + exit; + } + + $result = $stmt->get_result(); + + $productos = []; + while ($row = $result->fetch_assoc()) { + $productos[] = [ + 'ID_Producto' => $row['ID_Producto'], + 'nombre' => $row['nombre'], + 'cantidad' => intval($row['Cantidad']), + 'precio_unitario' => floatval($row['precio_unitario']), + 'subtotal' => floatval($row['subtotal']) + ]; + } + + header('Content-Type: application/json'); + echo json_encode($productos); + $stmt->close(); + exit; +} + +// De lo contrario, devolver las ventas individuales del período +if ($days !== null) { + $end = date('Y-m-d'); + $start = date('Y-m-d', strtotime("-{$days} days", strtotime($end))); +} else { + if (!$start && !$end) { + $start = date('Y-m-d'); + $end = $start; + } elseif ($start && !$end) { + $end = $start; + } elseif (!$start && $end) { + $start = $end; + } +} + +$startDateTime = $start . ' 00:00:00'; +$endDateTime = $end . ' 23:59:59'; + +$stmt = $conexion->prepare(" + SELECT ID_Venta, Fecha, Monto + FROM ventas + WHERE Fecha BETWEEN ? AND ? + ORDER BY Fecha DESC +"); + +if (!$stmt) { + http_response_code(500); + echo json_encode(['error' => 'Error en la consulta preparada: ' . $conexion->error]); + exit; +} + +$stmt->bind_param("ss", $startDateTime, $endDateTime); +$stmt->execute(); + +if ($stmt->error) { + http_response_code(500); + echo json_encode(['error' => 'Error al ejecutar: ' . $stmt->error]); + exit; +} + +$result = $stmt->get_result(); + +$ventas = []; +while ($row = $result->fetch_assoc()) { + $ventas[] = [ + 'ID_Venta' => intval($row['ID_Venta']), + 'fecha_hora' => $row['Fecha'], + 'monto_total' => floatval($row['Monto']) + ]; +} + +$stmt->close(); + +header('Content-Type: application/json'); +echo json_encode($ventas); +?> + diff --git a/php/Recuperar_Contrasena_Solicitar.php b/php/Recuperar_Contrasena_Solicitar.php new file mode 100644 index 0000000..6a4b4dd --- /dev/null +++ b/php/Recuperar_Contrasena_Solicitar.php @@ -0,0 +1,74 @@ + false, 'message' => 'Por favor ingresa un correo']); + exit; + } + + $correo = mysqli_real_escape_string($conexion, trim($data['correo'])); + + // Verificar si el correo existe en la base de datos + $sql = "SELECT ID_Usuario, Usuario, Nombre FROM usuarios WHERE Correo = '$correo'"; + $resultado = mysqli_query($conexion, $sql); + + if (!$resultado) { + throw new Exception('Error en la consulta: ' . mysqli_error($conexion)); + } + + if (mysqli_num_rows($resultado) === 0) { + echo json_encode(['success' => false, 'message' => 'El correo no está registrado en el sistema']); + exit; + } + + $usuario = mysqli_fetch_assoc($resultado); + $id_usuario = $usuario['ID_Usuario']; + $nombre_usuario = $usuario['Nombre']; + + // Generar código de 6 dígitos + $codigo = str_pad(rand(0, 999999), 6, '0', STR_PAD_LEFT); + + // Guardar el código con fecha de expiración (15 minutos) + $fecha_expiracion = date('Y-m-d H:i:s', strtotime('+15 minutes')); + $sql_update = "UPDATE usuarios SET Codigo_Recuperacion = '$codigo', Fecha_Exp_Codigo = '$fecha_expiracion' WHERE ID_Usuario = $id_usuario"; + + if (!mysqli_query($conexion, $sql_update)) { + throw new Exception('Error al actualizar: ' . mysqli_error($conexion)); + } + + // Construir el mensaje de email + $asunto = 'Código de Recuperación - Complejo Cap1tan'; + $mensaje = "Hola $nombre_usuario,\n\n"; + $mensaje .= "Hemos recibido una solicitud para recuperar tu contraseña en Complejo Cap1tan.\n\n"; + $mensaje .= "Tu código de verificación es: " . $codigo . "\n\n"; + $mensaje .= "Este código expira en 15 minutos.\n\n"; + $mensaje .= "Si no realizaste esta solicitud, por favor ignora este correo.\n\n"; + $mensaje .= "Saludos,\nEquipo de Complejo Cap1tan"; + + // Enviar email usando Brevo + $correo_enviado = enviarEmailBrevo($correo, $asunto, $mensaje, false); + + // Respuesta exitosa (el código se guardó en la BD de todas formas) + echo json_encode([ + 'success' => true, + 'message' => 'Se ha enviado un código de verificación a tu correo. Por favor revisa tu bandeja de entrada.', + 'test_codigo' => $codigo // Cambiar a false en producción + ]); + +} catch (Exception $e) { + echo json_encode([ + 'success' => false, + 'message' => 'Error: ' . $e->getMessage() + ]); +} +?> + diff --git a/php/Recuperar_Contrasena_Verificar.php b/php/Recuperar_Contrasena_Verificar.php new file mode 100644 index 0000000..b36fa55 --- /dev/null +++ b/php/Recuperar_Contrasena_Verificar.php @@ -0,0 +1,93 @@ + false, 'message' => 'Faltan datos requeridos']); + exit; + } + + $correo = mysqli_real_escape_string($conexion, trim($data['correo'])); + $codigo = mysqli_real_escape_string($conexion, trim($data['codigo'])); + $nueva_contrasena = mysqli_real_escape_string($conexion, $data['nueva_contrasena']); + + // Validar que la contraseña tenga al menos 6 caracteres + if (strlen($nueva_contrasena) < 6) { + echo json_encode(['success' => false, 'message' => 'La contraseña debe tener al menos 6 caracteres']); + exit; + } + + // Verificar el código y que no haya expirado + $sql = "SELECT ID_Usuario, Codigo_Recuperacion, Fecha_Exp_Codigo FROM usuarios WHERE Correo = '$correo'"; + $resultado = mysqli_query($conexion, $sql); + + if (!$resultado) { + error_log('Error en SELECT: ' . mysqli_error($conexion)); + throw new Exception('Error en la consulta: ' . mysqli_error($conexion)); + } + + if (mysqli_num_rows($resultado) === 0) { + echo json_encode(['success' => false, 'message' => 'Correo no encontrado']); + exit; + } + + $usuario = mysqli_fetch_assoc($resultado); + $id_usuario = $usuario['ID_Usuario']; + + // Verificar si el código es correcto + if ($usuario['Codigo_Recuperacion'] === null || $usuario['Codigo_Recuperacion'] === '') { + echo json_encode(['success' => false, 'message' => 'No hay código generado. Solicita uno nuevo']); + exit; + } + + if ($usuario['Codigo_Recuperacion'] !== $codigo) { + echo json_encode(['success' => false, 'message' => 'Código incorrecto']); + exit; + } + + // Verificar si el código ha expirado + if ($usuario['Fecha_Exp_Codigo'] === null) { + echo json_encode(['success' => false, 'message' => 'Error: fecha de expiración no registrada']); + exit; + } + + $fecha_actual = new DateTime(); + $fecha_exp = new DateTime($usuario['Fecha_Exp_Codigo']); + + if ($fecha_actual > $fecha_exp) { + echo json_encode(['success' => false, 'message' => 'El código ha expirado. Solicita uno nuevo']); + exit; + } + + // Actualizar la contraseña - IMPORTANTE: el nombre de la columna es "Contraseña" con acento + $sql_update = "UPDATE usuarios SET `Contraseña` = '$nueva_contrasena', `Codigo_Recuperacion` = NULL, `Fecha_Exp_Codigo` = NULL WHERE ID_Usuario = $id_usuario"; + + if (!mysqli_query($conexion, $sql_update)) { + error_log('Error en UPDATE: ' . mysqli_error($conexion) . ' | SQL: ' . $sql_update); + throw new Exception('Error al actualizar: ' . mysqli_error($conexion)); + } + + // Verificar que se actualizó correctamente + if (mysqli_affected_rows($conexion) === 0) { + error_log('Warning: UPDATE ejecutado pero no afectó filas'); + echo json_encode(['success' => false, 'message' => 'No se pudo actualizar la contraseña']); + exit; + } + + echo json_encode(['success' => true, 'message' => 'Contraseña actualizada correctamente']); + +} catch (Exception $e) { + error_log('Exception en Recuperar_Contrasena_Verificar: ' . $e->getMessage()); + echo json_encode([ + 'success' => false, + 'message' => 'Error: ' . $e->getMessage() + ]); +} +?> diff --git a/php/Registrar_Gastos.php b/php/Registrar_Gastos.php new file mode 100644 index 0000000..b046b49 --- /dev/null +++ b/php/Registrar_Gastos.php @@ -0,0 +1,123 @@ +real_escape_string($nombre_columna) . "'"; + $result_check = $conexion->query($sql_check); + + if ($result_check === false) { + throw new Exception("Error verificando columna: " . $conexion->error); + } + + if ($result_check->num_rows === 0) { + // Columna no existe, agregar con ALTER TABLE + $sql_alter = "ALTER TABLE gastos ADD COLUMN `$nombre_columna` DECIMAL(10,2) DEFAULT NULL"; + + if (!$conexion->query($sql_alter)) { + throw new Exception("Error al agregar columna: " . $conexion->error); + } + } + + // Obtener el ID del registro anterior (donde Periodo_Hasta IS NULL) + $sql_get_id = "SELECT ID_Gasto FROM gastos WHERE Periodo_Hasta IS NULL ORDER BY Periodo_Desde DESC LIMIT 1"; + $result_id = $conexion->query($sql_get_id); + + $fecha_hoy = date('Y-m-d'); + $id_anterior = null; + + if ($result_id && $result_id->num_rows > 0) { + $row = $result_id->fetch_assoc(); + $id_anterior = $row['ID_Gasto']; + + // Cerrar el período anterior + $sql_update = "UPDATE gastos SET Periodo_Hasta = ? WHERE ID_Gasto = ?"; + $stmt_update = $conexion->prepare($sql_update); + $stmt_update->bind_param('si', $fecha_hoy, $id_anterior); + + if (!$stmt_update->execute()) { + throw new Exception("Error actualizando período anterior: " . $stmt_update->error); + } + + $stmt_update->close(); + + // Obtener todos los valores del registro anterior para reutilizarlos + $sql_get_values = "SELECT * FROM gastos WHERE ID_Gasto = ?"; + $stmt_get_values = $conexion->prepare($sql_get_values); + $stmt_get_values->bind_param('i', $id_anterior); + $stmt_get_values->execute(); + $result_values = $stmt_get_values->get_result(); + $valores_anteriores = $result_values->fetch_assoc(); + $stmt_get_values->close(); + } + + // Construir INSERT dinámico + $columnas = ['Periodo_Desde', 'Periodo_Hasta', $nombre_columna]; + $valores = [date('Y-m-d'), null, $monto]; + $placeholders = ['?', '?', '?']; + $tipos = 'ssd'; + + // Si hay datos anteriores, agregar todas las columnas existentes + if (!empty($valores_anteriores)) { + foreach ($valores_anteriores as $col => $val) { + if (!in_array($col, ['ID_Gasto', 'Periodo_Desde', 'Periodo_Hasta', $nombre_columna])) { + $columnas[] = $col; + $valores[] = $val; + $placeholders[] = '?'; + $tipos .= 'd'; + } + } + } + + $sql_insert = "INSERT INTO gastos (" . implode(', ', $columnas) . ") VALUES (" . implode(', ', $placeholders) . ")"; + $stmt_insert = $conexion->prepare($sql_insert); + + if (!$stmt_insert) { + throw new Exception("Error preparando INSERT: " . $conexion->error); + } + + $stmt_insert->bind_param($tipos, ...$valores); + + if (!$stmt_insert->execute()) { + throw new Exception("Error insertando registro: " . $stmt_insert->error); + } + + $nuevo_id = $stmt_insert->insert_id; + $stmt_insert->close(); + + echo json_encode([ + 'success' => true, + 'message' => 'Gasto agregado correctamente', + 'id_gasto' => $nuevo_id + ]); + +} catch (Exception $e) { + echo json_encode([ + 'success' => false, + 'message' => $e->getMessage() + ]); +} +?> diff --git a/php/Registrar_Orden_Compra.php b/php/Registrar_Orden_Compra.php new file mode 100644 index 0000000..18aa1b1 --- /dev/null +++ b/php/Registrar_Orden_Compra.php @@ -0,0 +1,20 @@ +prepare("INSERT INTO ordenes_compra (Fecha, ID_Proveedor) VALUES (NOW(), ?)"); +$stmt->bind_param("i", $data["proveedor"]); +$stmt->execute(); +$idOrden = $stmt->insert_id; + +// Insertar productos de la orden +foreach ($data["productos"] as $p) { + $stmt = $conexion->prepare("INSERT INTO ordenes_productos (ID_Orden, ID_Producto, Cantidad) VALUES (?, ?, ?)"); + $stmt->bind_param("iii", $idOrden, $p["id"], $p["cantidad"]); + $stmt->execute(); +} + +echo "Orden registrada correctamente."; +?> diff --git a/php/Registrar_Venta.php b/php/Registrar_Venta.php new file mode 100644 index 0000000..24ddc4a --- /dev/null +++ b/php/Registrar_Venta.php @@ -0,0 +1,34 @@ +prepare("INSERT INTO ventas (Fecha, Monto, ID_Usuario) VALUES (NOW(), ?, ?)"); +$stmt->bind_param("di", $monto, $id_usuario); +$stmt->execute(); +$id_venta = $stmt->insert_id; + +// Insertar productos vendidos +foreach ($productos as $p) { + $stmt = $conexion->prepare("INSERT INTO ventas_productos (ID_Venta, ID_Producto, Cantidad) VALUES (?, ?, ?)"); + $stmt->bind_param("iii", $id_venta, $p['id'], $p['cantidad']); + $stmt->execute(); + + // Descontar stock + //$stmt = $conexion->prepare("UPDATE productos SET Stock_Disponible = Stock_Disponible - ? WHERE ID_Producto = ?"); + //$stmt->bind_param("ii", $p['cantidad'], $p['id']); + //$stmt->execute(); +} + +echo "Venta registrada correctamente."; +?> diff --git a/php/Registro.php b/php/Registro.php new file mode 100644 index 0000000..3f69515 --- /dev/null +++ b/php/Registro.php @@ -0,0 +1,31 @@ +connect_error) { + die("Error de conexión: " . $conexion->connect_error); +} + +$usuario = $_POST['usuario']; +$clave = $_POST['clave']; +$nombre = $_POST['nombre']; +$apellido = $_POST['apellido']; +$dni = $_POST['dni']; +$telefono = $_POST['telefono']; +$correo = $_POST['correo']; +$rol = $_POST['rol']; + +$sql = "INSERT INTO usuarios (DNI, Usuario, Contraseña, Rol, Nombre, Apellido, Correo, Telefono) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; + +$stmt = $conexion->prepare($sql); +$stmt->bind_param("isssssss", $dni, $usuario, $clave, $rol, $nombre, $apellido, $correo, $telefono); + +if ($stmt->execute()) { + echo "Registro exitoso."; +} else { + echo "Error: " . $stmt->error; +} + +$stmt->close(); +$conexion->close(); +?> diff --git a/php/Session_Info.php b/php/Session_Info.php new file mode 100644 index 0000000..91cabe9 --- /dev/null +++ b/php/Session_Info.php @@ -0,0 +1,27 @@ + false, 'message' => 'Sesión no iniciada']); + exit(); +} + +require_once 'conexion.php'; + +$id = intval($_SESSION['id_usuario']); +$query = "SELECT ID_Usuario, Usuario, Rol, Nombre, Apellido, DNI, Correo, Telefono FROM usuarios WHERE ID_Usuario = ?"; +$stmt = $conexion->prepare($query); +$stmt->bind_param('i', $id); +$stmt->execute(); +$result = $stmt->get_result(); + +if ($result && $row = $result->fetch_assoc()) { + echo json_encode(['success' => true, 'usuario' => $row]); +} else { + echo json_encode(['success' => false, 'message' => 'Usuario no encontrado']); +} + +$stmt->close(); +$conexion->close(); +?> \ No newline at end of file diff --git a/php/Subir_Comprobante.php b/php/Subir_Comprobante.php new file mode 100644 index 0000000..337ca97 --- /dev/null +++ b/php/Subir_Comprobante.php @@ -0,0 +1,73 @@ + false, 'error' => 'Método no permitido.']); + exit; +} + +$idReserva = isset($_POST['id_reserva']) ? intval($_POST['id_reserva']) : 0; + +if ($idReserva === 0 || !isset($_FILES['comprobante'])) { + echo json_encode(['success' => false, 'error' => 'Faltan datos obligatorios.']); + exit; +} + +$file = $_FILES['comprobante']; +$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); +$allowed = ['jpg', 'jpeg', 'png']; + +// Validar extensión +if (!in_array($ext, $allowed)) { + echo json_encode(['success' => false, 'error' => 'Formato no permitido. Solo JPG, JPEG o PNG.']); + exit; +} + +// Definir rutas +$nombreArchivo = "reserva_" . $idReserva . "_" . time() . "." . $ext; +$carpetaDestino = "C:\\xampp\\htdocs\\TP-Taller\\ProyectoTaller\\uploads\\comprobantes\\"; +$rutaCompleta = $carpetaDestino . $nombreArchivo; + +// Mover el archivo al servidor +if (move_uploaded_file($file['tmp_name'], $rutaCompleta)) { + + // Actualizar la base de datos con el nombre del archivo + $stmt = $conexion->prepare("UPDATE reservas SET Comprobante = ? WHERE ID_Reserva = ?"); + $stmt->bind_param('si', $nombreArchivo, $idReserva); + $stmt->execute(); + + if ($stmt->affected_rows > 0) { + + // --- NOTIFICACIÓN AL ADMINISTRADOR DE CANCHAS --- + // 1. Buscamos el teléfono del usuario con rol 'cancha' + $resUser = $conexion->query("SELECT Telefono FROM usuarios WHERE Rol = 'cancha' LIMIT 1"); + if ($resUser && $resUser->num_rows > 0) { + $admin = $resUser->fetch_assoc(); + $telefonoAdmin = $admin['Telefono']; // Ej: "549343..." + + // 2. Limpiamos el formato del teléfono del administrador + $telefonoLimpio = preg_replace('/[^0-9]/', '', $telefonoAdmin); + + // 3. Armamos el mensaje con un link directo para que el admin vea la foto desde su celular + // Nota: Cuando pases el sistema a producción, reemplaza 'localhost' por tu IP pública o dominio + $urlFoto = "http://localhost/TP-Taller/ProyectoTaller/uploads/comprobantes/" . $nombreArchivo; + $mensaje = "Alerta: Se ha subido un comprobante para la Reserva #$idReserva. Ver foto aquí: $urlFoto"; + + // 4. Llamamos al bot de Python de forma asíncrona para no trabar la pantalla del cliente + $scriptPython = "C:\\xampp\\htdocs\\TP-Taller\\ProyectoTaller\\python\\Enviar_WSP.py"; + $comando = "start /B python \"$scriptPython\" $telefonoLimpio \"$mensaje\""; + pclose(popen($comando, "r")); + } + + echo json_encode(['success' => true, 'message' => 'Comprobante subido y administrador notificado.']); + } else { + echo json_encode(['success' => false, 'error' => 'No se encontró la reserva para actualizar.']); + } + $stmt->close(); +} else { + echo json_encode(['success' => false, 'error' => 'No se pudo guardar el archivo en el servidor.']); +} + +$conexion->close(); +?> \ No newline at end of file diff --git a/php/backup.php b/php/backup.php new file mode 100644 index 0000000..b776d1a --- /dev/null +++ b/php/backup.php @@ -0,0 +1,109 @@ + \"$rutaSql\" 2>&1"; +exec($comando, $salida, $resultado); + +if ($resultado !== 0) { + echo "

Error al generar la base de datos. Abortando.

"; + exit; +} + +// --- EMPAQUETADO ZIP --- +$nombreArchivoZip = "backup_completo_" . $fecha . ".zip"; +$rutaZip = $carpetaDestino . $nombreArchivoZip; + +$zip = new ZipArchive(); + +if ($zip->open($rutaZip, ZipArchive::CREATE) === TRUE) { + + // Guardamos la base de datos dentro del ZIP + $zip->addFile($rutaSql, $nombreArchivoSql); + + // Guardamos el archivo de conexión + $rutaConexion = $rutaProyecto . "php\\conexion.php"; + if (file_exists($rutaConexion)) { + $zip->addFile($rutaConexion, "php/conexion.php"); + } + + // Guardamos el bot de Python + $rutaBot = $rutaProyecto . "python\\Enviar_WSP.py"; + if (file_exists($rutaBot)) { + $zip->addFile($rutaBot, "python/Enviar_WSP.py"); + } + + // Guardamos la carpeta uploads completa + $rutaUploads = $rutaProyecto . "uploads\\"; + + // Verificamos si la carpeta existe antes de intentar abrirla + if (is_dir($rutaUploads)) { + // El sabueso que busca todos los archivos dentro de la carpeta + $archivos = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($rutaUploads), + RecursiveIteratorIterator::LEAVES_ONLY + ); + + foreach ($archivos as $nombre => $archivo) { + // Saltamos las carpetas vacías o los directorios del sistema (. y ..) + if (!$archivo->isDir()) { + // Obtenemos la ruta real del archivo en Windows + $rutaReal = $archivo->getRealPath(); + + // Calculamos la ruta relativa para que adentro del ZIP quede ordenado en "uploads/comprobantes/foto.jpg" + $rutaRelativa = "uploads/" . substr($rutaReal, strlen($rutaUploads)); + + // Reemplazamos las barras invertidas de Windows por barras normales para el ZIP + $rutaRelativa = str_replace('\\', '/', $rutaRelativa); + + // Añadimos el archivo al ZIP + $zip->addFile($rutaReal, $rutaRelativa); + } + } + } + + $zip->close(); + + // Borramos el archivo .sql suelto + unlink($rutaSql); + + echo "

¡Éxito! Backup COMPLETO generado correctamente.

"; + echo "

Ruta: " . $rutaZip . "

"; +} else { + echo "

Error al intentar crear el archivo ZIP.

"; +} + +// --- AUTO-LIMPIEZA DE BACKUPS VIEJOS --- +$archivosZip = glob($carpetaDestino . "*.zip"); +$fechaActual = time(); +$borrados = 0; + +foreach ($archivosZip as $archivo) { + if (is_file($archivo)) { + $diasDeAntiguedad = ($fechaActual - filemtime($archivo)) / (60 * 60 * 24); + if ($diasDeAntiguedad >= $diasDeRetencion) { + unlink($archivo); + $borrados++; + } + } +} + +echo "
"; +if ($borrados > 0) { + echo "

Limpieza completada: Se eliminaron $borrados backup(s) viejo(s).

"; +} else { + echo "

Mantenimiento al día: Ningún backup supera los $diasDeRetencion días.

"; +} +?> \ No newline at end of file diff --git a/php/conexion.php b/php/conexion.php new file mode 100644 index 0000000..c4fac53 --- /dev/null +++ b/php/conexion.php @@ -0,0 +1,13 @@ +connect_error) { + die('Error de conexión: ' . $conexion->connect_error); +} +$conexion->set_charset('utf8'); +?> \ No newline at end of file diff --git a/php/config_mail.php b/php/config_mail.php new file mode 100644 index 0000000..ec975b6 --- /dev/null +++ b/php/config_mail.php @@ -0,0 +1,90 @@ + [ + 'name' => BREVO_FROM_NAME, + 'email' => BREVO_FROM_EMAIL + ], + 'to' => [ + [ + 'email' => $para, + 'name' => $para + ] + ], + 'subject' => $asunto, + 'htmlContent' => $esHTML ? $mensaje : nl2br(htmlspecialchars($mensaje)), + 'textContent' => $mensaje + ]; + + // Preparar headers + $headers = [ + 'Content-Type: application/json', + 'api-key: ' . BREVO_API_KEY, + 'Accept: application/json' + ]; + + // Inicializar cURL + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, BREVO_API_URL); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + + // Ejecutar + $respuesta = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + // Verificar respuesta + if ($error) { + error_log('Brevo API Error: ' . $error); + return false; + } + + if ($http_code >= 200 && $http_code < 300) { + error_log('Email enviado exitosamente a ' . $para); + return true; + } else { + error_log('Brevo API HTTP ' . $http_code . ': ' . $respuesta); + return false; + } + + } catch (Exception $e) { + error_log('Brevo Exception: ' . $e->getMessage()); + return false; + } +} + +/** + * Función fallback usando mail() nativa + */ +function enviarEmailFallback($para, $asunto, $mensaje) { + $headers = "From: " . BREVO_FROM_NAME . " <" . BREVO_FROM_EMAIL . ">\r\n"; + $headers .= "Reply-To: " . BREVO_FROM_EMAIL . "\r\n"; + $headers .= "Content-Type: text/plain; charset=UTF-8\r\n"; + + return @mail($para, $asunto, $mensaje, $headers); +} +?> + diff --git a/python/Enviar_WSP.py b/python/Enviar_WSP.py new file mode 100644 index 0000000..c01e53e --- /dev/null +++ b/python/Enviar_WSP.py @@ -0,0 +1,51 @@ +import sys +import time +from urllib.parse import quote +from selenium import webdriver +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.common.keys import Keys + +# Recibimos los datos +telefono = sys.argv[1] +mensaje = sys.argv[2] + +chrome_options = Options() +chrome_options.add_argument("user-data-dir=C:\\whatsapp_sesion") +# Activa el modo invisible +chrome_options.add_argument("--headless=new") +# Le damos un tamaño de pantalla virtual grande +chrome_options.add_argument("--window-size=1920,1080") + +driver = webdriver.Chrome(options=chrome_options) + +try: + mensaje_encoded = quote(mensaje) + url = f"https://web.whatsapp.com/send?phone={telefono}&text={mensaje_encoded}" + + driver.get(url) + wait = WebDriverWait(driver, 40) + + # 1. Esperamos a que cargue el panel principal del chat + wait.until(EC.presence_of_element_located((By.XPATH, '//div[@id="main"]'))) + + # 2. Le damos 3 segundos para que WhatsApp termine de procesar el texto en la caja + time.sleep(15) + + # 3. Buscamos la caja de texto directamente (está en el footer del chat) + caja_texto = wait.until(EC.presence_of_element_located((By.XPATH, '//footer//div[@contenteditable="true"]'))) + + # 4. Le enviamos la tecla "Enter" directamente a esa caja de texto + caja_texto.send_keys(Keys.ENTER) + + # Esperamos 4 segundos para que aparezca el tilde gris antes de cerrar + time.sleep(7) + print("Mensaje enviado con éxito") + +except Exception as e: + print(f"Error al enviar: {e}") + +finally: + driver.quit() \ No newline at end of file diff --git a/sql/Consultas.sql b/sql/Consultas.sql new file mode 100644 index 0000000..ae5ff7a --- /dev/null +++ b/sql/Consultas.sql @@ -0,0 +1,60 @@ +SELECT SUM(p.Precio_Venta * vp.Cantidad) AS Ingreso_Bruto_Total +FROM ventas_productos vp +JOIN productos p ON vp.ID_Producto = p.ID_Producto; + +SELECT SUM(p.Precio_Compra * vp.Cantidad) AS Costo_Total +FROM ventas_productos vp +JOIN productos p ON vp.ID_Producto = p.ID_Producto; + +SELECT SUM((p.Precio_Venta - p.Precio_Compra) * vp.Cantidad) AS Ganancia_Bruta +FROM ventas_productos vp +JOIN productos p ON vp.ID_Producto = p.ID_Producto; + +SELECT + v.Fecha, + SUM(p.Precio_Venta * vp.Cantidad) AS Ingreso_Bruto, + SUM(p.Precio_Compra * vp.Cantidad) AS Costo, + SUM((p.Precio_Venta - p.Precio_Compra) * vp.Cantidad) AS Ganancia_Bruta +FROM ventas v +JOIN ventas_productos vp ON v.ID_Venta = vp.ID_Venta +JOIN productos p ON vp.ID_Producto = p.ID_Producto +GROUP BY v.Fecha +ORDER BY v.Fecha DESC; + +SELECT + DATE_FORMAT(v.Fecha, '%Y-%m') AS Mes, + SUM(p.Precio_Venta * vp.Cantidad) AS Ingreso_Bruto, + SUM(p.Precio_Compra * vp.Cantidad) AS Costo, + SUM((p.Precio_Venta - p.Precio_Compra) * vp.Cantidad) AS Ganancia_Bruta +FROM ventas v +JOIN ventas_productos vp ON v.ID_Venta = vp.ID_Venta +JOIN productos p ON vp.ID_Producto = p.ID_Producto +GROUP BY Mes +ORDER BY Mes DESC; + +SELECT + g.Periodo_Desde, + g.Periodo_Hasta, + COALESCE(ingresos.Mes, DATE_FORMAT(g.Periodo_Desde, '%Y-%m')) AS Mes, + COALESCE(ingresos.Ingreso_Bruto, 0) AS Ingreso_Bruto, + COALESCE(ingresos.Costo, 0) AS Costo, + COALESCE(ingresos.Ganancia_Bruta, 0) AS Ganancia_Bruta, + (g.Sueldo + g.Luz + g.Agua + g.Impuestos) AS Gastos_Totales, + COALESCE(ingresos.Ganancia_Bruta, 0) - (g.Sueldo + g.Luz + g.Agua + g.Impuestos) AS Ganancia_Neta +FROM gastos g +LEFT JOIN ( + SELECT + DATE_FORMAT(v.Fecha, '%Y-%m') AS Mes, + MIN(v.Fecha) AS Periodo_Desde, + MAX(v.Fecha) AS Periodo_Hasta, + SUM(p.Precio_Venta * vp.Cantidad) AS Ingreso_Bruto, + SUM(p.Precio_Compra * vp.Cantidad) AS Costo, + SUM((p.Precio_Venta - p.Precio_Compra) * vp.Cantidad) AS Ganancia_Bruta + FROM ventas v + JOIN ventas_productos vp ON v.ID_Venta = vp.ID_Venta + JOIN productos p ON vp.ID_Producto = p.ID_Producto + GROUP BY Mes +) ingresos +ON g.Periodo_Desde <= ingresos.Periodo_Hasta + AND (g.Periodo_Hasta IS NULL OR g.Periodo_Hasta >= ingresos.Periodo_Desde) +ORDER BY ingresos.Mes DESC, g.Periodo_Desde; \ No newline at end of file diff --git a/sql/Insertar Datos.sql b/sql/Insertar Datos.sql new file mode 100644 index 0000000..2eadeae --- /dev/null +++ b/sql/Insertar Datos.sql @@ -0,0 +1,234 @@ +INSERT INTO proveedores (ID_Proveedor, Telefono, Nombre) VALUES + (1, '+54-11-1111-1111', 'AG Export'), + (2, '+54-11-2222-2222', 'Gastro Service'), + (3, '+54-11-3333-3333', 'Freixenet'), + (4, '+54-11-4444-4444', 'Supper Foods'), + (5, '+54-11-5555-5555', 'Bebidas Buenos Aires'); + +INSERT INTO usuarios (ID_Usuario, DNI, Usuario, `Contraseña`, Rol, Nombre, Apellido, Correo, Telefono) VALUES + (1, '11111111', 'juanp', 'admin', 'admin', 'Juan', 'Pérez', 'juanp@example.com', '+54-9-351-111-111'), + (2, '22222222', 'mariag', 'cancha', 'cancha', 'María', 'Gómez', 'mariag@example.com', '+54-9-351-222-222'), + (3, '33333333', 'carlosl', 'cantina', 'cantina', 'Carlos', 'López', 'carlosl@example.com', '+54-9-351-333-333'), + (4, '44444444', 'laurac', 'cantina', 'cantina', 'Laura', 'Castaño', 'laurac@example.com', '+54-9-351-444-555'), + (5, '55555555', 'robert', 'admin', 'admin', 'Roberto', 'Molina', 'robert@example.com', '+54-9-351-666-777'); + +INSERT INTO productos (ID_Producto, Precio_Venta, Precio_Compra, Descripcion, Stock_Disponible) VALUES + (1, 2000.00, 1500.00, 'Coca-Cola 500ml', 100), + (2, 2500.00, 2000.00, 'Pritty 3l', 50), + (3, 1600.00, 1100.00, 'Quilmes Clasica Lata', 80), + (4, 1200.00, 1000.00, 'Corona Extra Rubia Lata', 10), + (5, 1500.00, 1200.00, 'Pepsi 500ml', 200), + (6, 500.00, 250.00, 'Sprite 250ml', 90), + (7, 1800.00, 1200.00, 'Fanta 600ml', 180), + (8, 1000.00, 600.00, 'Agua Mineral 600ml', 220), + (9, 1700.00, 1300.00, 'Heineken Lata', 70), + (10, 1400.00, 900.00, 'Schweppes Tonica 500ml', 130), + (11, 1900.00, 1100.00, 'Agua Tónica 330ml', 140), + (12, 1700.00, 1000.00, 'Jugo de Naranja 500ml', 90), + (13, 1300.00, 700.00, 'Alfajor Doble', 75), + (14, 2800.00, 1700.00, 'Hamburguesa con Queso', 55), + (15, 900.00, 500.00, 'Empanada de Carne', 120); + +INSERT INTO proveedores_productos (ID_Proveedor, ID_Producto) VALUES + (1, 1), + (1, 2), + (1, 5), + (2, 3), + (2, 6), + (2, 12), + (3, 4), + (3, 9), + (3, 13), + (4, 10), + (4, 11), + (4, 14), + (5, 7), + (5, 8), + (5, 15); + +INSERT INTO clientes (ID_Cliente, DNI, Nombre, Telefono) VALUES + (1, '44444444', 'Lucas Díaz', '+54-9-351-444-444'), + (2, '55555555', 'Carla Romero', '+54-9-351-555-555'), + (3, '66666666', 'Diego Suárez', '+54-9-351-666-666'), + (4, '77777777', 'Matías Suárez', '+54-9-351-777-777'), + (5, '88888888', 'Sofía Martínez', '+54-9-351-888-888'), + (6, '99999999', 'Josefa Aguirre', '+54-9-351-999-999'), + (7, '10101010', 'Valeria Ríos', '+54-9-351-101-010'), + (8, '12121212', 'Marcos Alcántara', '+54-9-351-121-212'), + (9, '13131313', 'Nadia Funes', '+54-9-351-131-313'), + (10, '14141414', 'Hernán Ortiz', '+54-9-351-141-414'), + (11, '15151515', 'Jimena Paredes', '+54-9-351-151-515'), + (12, '16161616', 'Pablo Duarte', '+54-9-351-161-616'); + +INSERT INTO cupones (ID_Cupon, Cantidad_Reservas, Cantidad_Faltas, Tipo, Ultima_Fecha, Descuento, ID_Cliente) VALUES + (1, 5, 0, 'F', '2026-06-01 10:00:00', 0.10, 1), + (2, 3, 1, 'P', '2026-06-05 12:00:00', 0.05, 2), + (3, 1, 0, 'F', '2026-06-08 17:00:00', 0.08, 3), + (4, 4, 0, 'P', '2026-06-10 14:00:00', 0.12, 4), + (5, 2, 0, 'F', '2026-06-12 09:00:00', 0.07, 5), + (6, 6, 1, 'P', '2026-06-15 20:00:00', 0.15, 6), + (7, 5, 0, 'F', '2026-06-18 19:00:00', 0.10, 7), + (8, 4, 0, 'P', '2026-06-20 16:30:00', 0.12, 8); + +INSERT INTO canchas (ID_Cancha, Tipo, Precio, Duracion, Cant_Canchas, Cant_Reservas_Cupon, Descuento_Cupon, Cant_Faltas, Duracion_Cupon) VALUES + (1, 'F', 3000.00, 60, 2, 5, 0.25, 2, 7), + (2, 'P', 2500.00, 60, 2, 5, 0.20, 2, 7), + (3, 'Q', 1500.00, 120, 1, 0, 0.00, 0, 0); + +INSERT INTO horarios (Dia, Hora_Apertura, Hora_Cierre) VALUES + ('Lunes a Viernes', '08:00:00', '23:00:00'), + ('Sábado', '09:00:00', '22:00:00'), + ('Domingo', '09:00:00', '22:00:00'); + +INSERT INTO gastos (Periodo_Desde, Periodo_Hasta, Sueldo, Luz, Agua, Impuestos) VALUES + ('2026-04-01', '2026-04-30', 250000, 32000, 12000, 10500), + ('2026-05-01', '2026-05-31', 270000, 35000, 13000, 11000), + ('2026-06-01', '2026-06-29', 285000, 38000, 14000, 11500); + +INSERT INTO alertas_stock (ID_Producto, Mensaje) VALUES + (4, 'Stock reducido a 9 unidades.'), + (9, 'Stock reducido a 7 unidades.'), + (13, 'Stock bajo para producto de almacén.'), + (15, 'Stock crítico en empanadas. Reponer pronto.'); + +INSERT INTO ventas (ID_Venta, Fecha, Monto, ID_Usuario) VALUES + (1, '2026-06-01 10:15:00', 12500.00, 2), + (2, '2026-06-02 12:30:00', 8700.00, 2), + (3, '2026-06-04 15:45:00', 15400.00, 2), + (4, '2026-06-06 13:20:00', 10250.00, 2), + (5, '2026-06-08 17:05:00', 13600.00, 2), + (6, '2026-06-10 19:10:00', 9400.00, 2), + (7, '2026-06-12 09:35:00', 11200.00, 1), + (8, '2026-06-14 16:00:00', 7600.00, 2), + (9, '2026-06-16 22:15:00', 13300.00, 1), + (10, '2026-06-18 18:45:00', 11850.00, 1), + (11, '2026-06-20 14:00:00', 9800.00, 2), + (12, '2026-06-22 20:30:00', 14700.00, 2), + (13, '2026-06-24 11:20:00', 8200.00, 2), + (14, '2026-06-25 13:30:00', 7500.00, 2), + (15, '2026-06-26 17:40:00', 16600.00, 1), + (16, '2026-06-27 18:25:00', 13200.00, 2), + (17, '2026-06-28 16:50:00', 10150.00, 2), + (18, '2026-06-29 11:05:00', 8900.00, 1), + (19, '2026-06-30 12:40:00', 9400.00, 5), + (20, '2026-06-30 18:15:00', 12000.00, 4), + (21, '2026-06-30 20:30:00', 13200.00, 4), + (22, '2026-06-30 21:15:00', 8700.00, 5); + +INSERT INTO ordenes_compra (ID_Orden, ID_Proveedor, ID_Usuario, Fecha) VALUES + (1, 1, 2, '2026-06-01 09:00:00'), + (2, 2, 2, '2026-06-03 10:30:00'), + (3, 3, 2, '2026-06-05 08:45:00'), + (4, 1, 2, '2026-06-07 09:15:00'), + (5, 2, 2, '2026-06-09 11:00:00'), + (6, 3, 2, '2026-06-11 08:30:00'), + (7, 1, 2, '2026-06-13 12:00:00'), + (8, 2, 2, '2026-06-15 09:45:00'), + (9, 3, 2, '2026-06-17 10:20:00'), + (10, 1, 2, '2026-06-19 12:15:00'), + (11, 4, 2, '2026-06-21 09:05:00'), + (12, 5, 4, '2026-06-23 10:15:00'), + (13, 4, 4, '2026-06-24 08:50:00'), + (14, 5, 2, '2026-06-25 11:30:00'); + +INSERT INTO reservas (ID_Reserva, Fecha_Hora, Monto, Numero, Estado, ID_Cliente, ID_Cancha, ID_Usuario) VALUES + (1, '2026-06-01 18:00:00', 3000.00, 1, 1, 1, 1, 3), + (2, '2026-06-02 19:00:00', 2500.00, 1, 1, 2, 2, 3), + (3, '2026-06-03 20:00:00', 3000.00, 2, 1, 3, 1, 3), + (4, '2026-06-04 17:00:00', 2500.00, 2, 1, 1, 2, 3), + (5, '2026-06-05 17:00:00', 1500.00, 1, 1, 2, 3, 3), + (6, '2026-06-06 16:30:00', 3000.00, 1, 1, 1, 1, 1), + (7, '2026-06-07 18:30:00', 2500.00, 2, 1, 3, 2, 3), + (8, '2026-06-08 17:30:00', 3000.00, 1, 1, 2, 1, 3), + (9, '2026-06-09 17:30:00', 1500.00, 1, 1, 3, 3, 3), + (10, '2026-06-10 19:30:00', 2500.00, 1, 1, 1, 2, 1), + (11, '2026-06-11 18:00:00', 3000.00, 2, 1, 2, 1, 3), + (12, '2026-06-12 21:00:00', 1500.00, 1, 1, 3, 3, 1), + (13, '2026-06-13 16:00:00', 2500.00, 1, 1, 1, 2, 3), + (14, '2026-06-14 20:30:00', 3000.00, 2, 1, 2, 1, 3), + (15, '2026-06-15 18:45:00', 1500.00, 1, 1, 3, 3, 3), + (16, '2026-06-16 17:00:00', 2800.00, 1, 2, 4, 2, 3), + (17, '2026-06-17 19:15:00', 2200.00, 2, 1, 5, 1, 3), + (18, '2026-06-18 15:30:00', 3200.00, 1, 1, 6, 1, 3), + (19, '2026-06-20 18:45:00', 3000.00, 2, 1, 4, 2, 3), + (20, '2026-06-22 16:20:00', 1700.00, 1, 1, 5, 2, 3), + (21, '2026-06-23 19:00:00', 1500.00, 1, 1, 8, 3, 3), + (22, '2026-06-24 20:00:00', 1500.00, 1, 0, 9, 3, 3), + (23, '2026-06-25 19:30:00', 1500.00, 1, 1, 10, 3, 1), + (24, '2026-06-26 21:00:00', 1500.00, 1, 2, 11, 3, 1); + +INSERT INTO ordenes_productos (ID_Orden, ID_Producto, Cantidad) VALUES + (1, 1, 50), + (1, 4, 10), + (2, 2, 30), + (2, 5, 100), + (3, 3, 60), + (3, 6, 80), + (4, 1, 40), + (4, 3, 35), + (5, 2, 25), + (5, 6, 50), + (6, 4, 18), + (6, 5, 70), + (7, 1, 30), + (7, 2, 20), + (8, 3, 45), + (8, 6, 60), + (9, 7, 60), + (9, 8, 40), + (10, 9, 20), + (10, 10, 30), + (11, 11, 20), + (11, 13, 25), + (12, 12, 30), + (12, 14, 15), + (13, 1, 40), + (13, 15, 50), + (14, 5, 40), + (14, 8, 70); + +INSERT INTO ventas_productos (ID_Venta, ID_Producto, Cantidad) VALUES + (1, 1, 12), + (1, 2, 5), + (2, 3, 8), + (2, 5, 10), + (3, 4, 5), + (3, 6, 7), + (4, 1, 8), + (4, 3, 7), + (5, 2, 6), + (5, 5, 14), + (6, 4, 3), + (6, 6, 10), + (7, 1, 10), + (7, 2, 4), + (8, 3, 9), + (8, 5, 11), + (9, 6, 15), + (9, 7, 8), + (10, 2, 7), + (10, 4, 2), + (11, 3, 12), + (11, 5, 9), + (12, 1, 11), + (12, 6, 13), + (13, 7, 10), + (13, 8, 6), + (14, 9, 8), + (14, 5, 7), + (15, 10, 6), + (15, 1, 4), + (16, 2, 8), + (16, 4, 9), + (17, 3, 4), + (17, 9, 5), + (18, 5, 10), + (18, 8, 7), + (19, 11, 10), + (19, 13, 5), + (20, 12, 12), + (20, 14, 6), + (21, 15, 15), + (21, 8, 10), + (22, 1, 9), + (22, 11, 8); \ No newline at end of file diff --git a/sql/Tablas.sql b/sql/Tablas.sql new file mode 100644 index 0000000..b2b9b23 --- /dev/null +++ b/sql/Tablas.sql @@ -0,0 +1,137 @@ +CREATE TABLE proveedores ( + ID_Proveedor INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + Telefono VARCHAR(20), + Nombre VARCHAR(20) +); + +CREATE TABLE usuarios ( + ID_Usuario INT AUTO_INCREMENT PRIMARY KEY, + Usuario VARCHAR(50) NOT NULL, + `Contraseña` VARCHAR(100) NOT NULL, + Rol ENUM('admin', 'cancha', 'cantina') NOT NULL, + Nombre VARCHAR(50), + Apellido VARCHAR(50), + DNI VARCHAR(10), + Correo VARCHAR(100), + Telefono VARCHAR(20) +); + +CREATE TABLE ventas ( + ID_Venta INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + Fecha TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + Monto DECIMAL(10,2) NULL, + ID_Usuario INT, + CONSTRAINT fk_venta_usuario FOREIGN KEY (ID_Usuario) REFERENCES usuarios(ID_Usuario) +); + +CREATE TABLE ordenes_compra ( + ID_Orden INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + ID_Proveedor INT, + ID_Usuario INT, + Fecha TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + Estado INT DEFAULT 0, -- 0=Pendiente, 1=Aprobada, 2=Denegada, 3=Confirmada + CONSTRAINT fk_oc_proveedor FOREIGN KEY (ID_Proveedor) REFERENCES proveedores(ID_Proveedor), + CONSTRAINT fk_oc_usuario FOREIGN KEY (ID_Usuario) REFERENCES usuarios(ID_Usuario) +); + +CREATE TABLE productos ( + ID_Producto INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + Precio_Venta DECIMAL(10,2), + Precio_Compra DECIMAL(10,2), + Descripcion VARCHAR(100), + Stock_Disponible INT, + Estado INT DEFAULT TRUE -- 0=Inactivo, 1=Activo +); + +CREATE TABLE clientes ( + ID_Cliente INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + DNI VARCHAR(10), + Nombre VARCHAR(50), + Telefono VARCHAR(20) +); + +CREATE TABLE gastos ( + ID_Gasto INT AUTO_INCREMENT PRIMARY KEY, + Periodo_Desde DATE NOT NULL, + Periodo_Hasta DATE DEFAULT NULL +); + +CREATE TABLE ordenes_productos ( + ID_Orden INT NOT NULL, + ID_Producto INT NOT NULL, + Cantidad INT, + PRIMARY KEY (ID_Orden, ID_Producto), + CONSTRAINT fk_op_orden FOREIGN KEY (ID_Orden) REFERENCES ordenes_compra(ID_Orden), + CONSTRAINT fk_op_producto FOREIGN KEY (ID_Producto) REFERENCES productos(ID_Producto) +); + +CREATE TABLE ventas_productos ( + ID_Venta INT NOT NULL, + ID_Producto INT NOT NULL, + Cantidad INT, + PRIMARY KEY (ID_Venta, ID_Producto), + CONSTRAINT fk_vp_venta FOREIGN KEY (ID_Venta) REFERENCES ventas(ID_Venta), + CONSTRAINT fk_vp_producto FOREIGN KEY (ID_Producto) REFERENCES productos(ID_Producto) +); + +CREATE TABLE proveedores_productos ( + ID_Proveedor INT NOT NULL, + ID_Producto INT NOT NULL, + PRIMARY KEY (ID_Proveedor, ID_Producto), + CONSTRAINT fk_pp_proveedor FOREIGN KEY (ID_Proveedor) REFERENCES proveedores(ID_Proveedor), + CONSTRAINT fk_pp_producto FOREIGN KEY (ID_Producto) REFERENCES productos(ID_Producto) +); + +CREATE TABLE alertas_stock ( + ID_Alerta INT AUTO_INCREMENT PRIMARY KEY, + ID_Producto INT, + Mensaje VARCHAR(255), + Fecha TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_alerta_producto FOREIGN KEY (ID_Producto) REFERENCES productos(ID_Producto) +); + +CREATE TABLE cupones ( + ID_Cupon INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + Cantidad_Reservas INT, + Cantidad_Faltas INT, + Tipo VARCHAR(1), + Ultima_Fecha DATETIME, + Descuento DECIMAL(4,2), + ID_Cliente INT, + CONSTRAINT fk_cupon_cliente FOREIGN KEY (ID_Cliente) REFERENCES clientes(ID_Cliente) +); + +CREATE TABLE canchas ( + ID_Cancha INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + Tipo VARCHAR(1), -- F=Futbol, P=Padel, Q=Quincho + Precio DECIMAL(10,2), + Duracion INT, -- en minutos + Cant_Canchas INT, + Cant_Reservas_Cupon INT, + Descuento_Cupon DECIMAL(4,2), + Cant_Faltas INT, + Duracion_Cupon INT -- en dias +); + +CREATE TABLE reservas ( + ID_Reserva INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + Fecha_Hora DATETIME, + Monto DECIMAL(10,2), + Numero INT, + Estado INT, -- 0=No Pagado, 1=Pagado, 2=Cancelado + Descuento DECIMAL(4,2), + Comprobante VARCHAR(255), + ID_Cliente INT, + ID_Cancha INT, + ID_Usuario INT, + CONSTRAINT fk_reserva_cliente FOREIGN KEY (ID_Cliente) REFERENCES clientes(ID_Cliente), + CONSTRAINT fk_reserva_cancha FOREIGN KEY (ID_Cancha) REFERENCES canchas(ID_Cancha), + CONSTRAINT fk_reserva_usuario FOREIGN KEY (ID_Usuario) REFERENCES usuarios(ID_Usuario) +); + +CREATE TABLE horarios ( + ID_Horario INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + Dia ENUM('Lunes a Viernes', 'Sábado', 'Domingo'), + Hora_Apertura TIME, + Hora_Cierre TIME +); \ No newline at end of file diff --git a/sql/Triggers.sql b/sql/Triggers.sql new file mode 100644 index 0000000..0135771 --- /dev/null +++ b/sql/Triggers.sql @@ -0,0 +1,45 @@ +SHOW TRIGGERS FROM complejo_cap1tan; + +DELIMITER // +CREATE TRIGGER alerta_stock_bajo +AFTER UPDATE ON productos FOR EACH ROW +BEGIN + IF NEW.Stock_Disponible < 10 AND OLD.Stock_Disponible >= 10 THEN + INSERT INTO alertas_stock (ID_Producto, Mensaje) + VALUES (NEW.ID_Producto, CONCAT('Stock reducido a ', NEW.Stock_Disponible, ' unidades.')); + END IF; +END; +// DELIMITER ; + +-- Setear un producto por debajo de 10 +-- UPDATE productos SET Stock_Disponible = 100 WHERE ID_Producto = 1; + +DELIMITER // +CREATE TRIGGER actualizar_stock_venta +AFTER INSERT ON ventas_productos FOR EACH ROW +BEGIN + UPDATE productos + SET Stock_Disponible = Stock_Disponible - NEW.Cantidad + WHERE ID_Producto = NEW.ID_Producto; +END; +// DELIMITER ; + +DELIMITER // +CREATE TRIGGER actualizar_stock_orden_compra +AFTER UPDATE ON ordenes_compra FOR EACH ROW +BEGIN + -- Si la orden cambió a estado aprobado y antes no lo estaba + IF NEW.Estado = TRUE AND OLD.Estado = FALSE THEN + INSERT INTO alertas_stock (ID_Producto, Mensaje) + SELECT op.ID_Producto, CONCAT('Orden de compra aprobada. Se sumaron ', op.Cantidad, ' unidades al stock.') + FROM ordenes_productos op + WHERE op.ID_Orden = NEW.ID_Orden; + + -- Aumentar el stock + UPDATE productos p + JOIN ordenes_productos op ON p.ID_Producto = op.ID_Producto + SET p.Stock_Disponible = p.Stock_Disponible + op.Cantidad + WHERE op.ID_Orden = NEW.ID_Orden; + END IF; +END; +// DELIMITER ;