1ra Versión

This commit is contained in:
MarcosFlorSalcedo
2026-07-14 18:24:27 -03:00
commit 8ec55e40fc
86 changed files with 15839 additions and 0 deletions
+618
View File
@@ -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
<?php
$host = 'localhost'; // Servidor MySQL
$user = 'root'; // Usuario MySQL
$password = 'admin'; // Contraseña MySQL
$db = 'complejo_cap1tan'; // Nombre de la base de datos
$conexion = new mysqli($host, $user, $password, $db);
if ($conexion->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
+1164
View File
File diff suppressed because it is too large Load Diff
+326
View File
@@ -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);
}
+814
View File
@@ -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);
}
}
+952
View File
@@ -0,0 +1,952 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2478.22" height="1948.42" viewBox="0 0 2478.22 1948.42">
<defs>
<clipPath id="clip-0">
<path clip-rule="nonzero" d="M 398 1651 L 2097 1651 L 2097 1844 L 398 1844 Z M 398 1651 "/>
</clipPath>
<clipPath id="clip-1">
<path clip-rule="nonzero" d="M 2096.269531 1747.328125 C 2096.269531 1800.339844 1716.238281 1843.308594 1247.441406 1843.308594 C 778.644531 1843.308594 398.609375 1800.339844 398.609375 1747.328125 C 398.609375 1694.320312 778.644531 1651.351562 1247.441406 1651.351562 C 1716.238281 1651.351562 2096.269531 1694.320312 2096.269531 1747.328125 Z M 2096.269531 1747.328125 "/>
</clipPath>
<clipPath id="clip-2">
<path clip-rule="nonzero" d="M 2096.269531 1747.328125 C 2096.269531 1800.339844 1716.238281 1843.308594 1247.441406 1843.308594 C 778.644531 1843.308594 398.609375 1800.339844 398.609375 1747.328125 C 398.609375 1694.320312 778.644531 1651.351562 1247.441406 1651.351562 C 1716.238281 1651.351562 2096.269531 1694.320312 2096.269531 1747.328125 "/>
</clipPath>
<radialGradient id="radial-pattern-0" gradientUnits="userSpaceOnUse" cx="0" cy="0" fx="0" fy="0" r="494.237449" gradientTransform="matrix(0.0970999, 0.995275, -0.995275, 0.0970999, 1040.48, 1525.38)">
<stop offset="0" stop-color="rgb(91.372681%, 91.215515%, 91.255188%)" stop-opacity="1"/>
<stop offset="0.5" stop-color="rgb(91.372681%, 91.215515%, 91.255188%)" stop-opacity="1"/>
<stop offset="0.625" stop-color="rgb(91.372681%, 91.215515%, 91.255188%)" stop-opacity="1"/>
<stop offset="0.628906" stop-color="rgb(91.317749%, 91.159058%, 91.19873%)" stop-opacity="1"/>
<stop offset="0.632812" stop-color="rgb(91.085815%, 90.924072%, 90.963745%)" stop-opacity="1"/>
<stop offset="0.636719" stop-color="rgb(90.734863%, 90.565491%, 90.608215%)" stop-opacity="1"/>
<stop offset="0.640625" stop-color="rgb(90.388489%, 90.213013%, 90.257263%)" stop-opacity="1"/>
<stop offset="0.644531" stop-color="rgb(90.04364%, 89.862061%, 89.907837%)" stop-opacity="1"/>
<stop offset="0.648438" stop-color="rgb(89.704895%, 89.517212%, 89.564514%)" stop-opacity="1"/>
<stop offset="0.652344" stop-color="rgb(89.36615%, 89.173889%, 89.221191%)" stop-opacity="1"/>
<stop offset="0.65625" stop-color="rgb(89.035034%, 88.835144%, 88.885498%)" stop-opacity="1"/>
<stop offset="0.660156" stop-color="rgb(88.705444%, 88.499451%, 88.549805%)" stop-opacity="1"/>
<stop offset="0.664062" stop-color="rgb(88.378906%, 88.168335%, 88.220215%)" stop-opacity="1"/>
<stop offset="0.667969" stop-color="rgb(88.056946%, 87.840271%, 87.893677%)" stop-opacity="1"/>
<stop offset="0.671875" stop-color="rgb(87.739563%, 87.516785%, 87.571716%)" stop-opacity="1"/>
<stop offset="0.675781" stop-color="rgb(87.423706%, 87.194824%, 87.251282%)" stop-opacity="1"/>
<stop offset="0.679688" stop-color="rgb(87.110901%, 86.875916%, 86.933899%)" stop-opacity="1"/>
<stop offset="0.683594" stop-color="rgb(86.804199%, 86.56311%, 86.624146%)" stop-opacity="1"/>
<stop offset="0.6875" stop-color="rgb(86.499023%, 86.253357%, 86.314392%)" stop-opacity="1"/>
<stop offset="0.691406" stop-color="rgb(86.199951%, 85.948181%, 86.010742%)" stop-opacity="1"/>
<stop offset="0.695312" stop-color="rgb(85.900879%, 85.644531%, 85.708618%)" stop-opacity="1"/>
<stop offset="0.699219" stop-color="rgb(85.60791%, 85.346985%, 85.411072%)" stop-opacity="1"/>
<stop offset="0.703125" stop-color="rgb(85.317993%, 85.050964%, 85.116577%)" stop-opacity="1"/>
<stop offset="0.707031" stop-color="rgb(85.032654%, 84.759521%, 84.82666%)" stop-opacity="1"/>
<stop offset="0.710938" stop-color="rgb(84.74884%, 84.47113%, 84.539795%)" stop-opacity="1"/>
<stop offset="0.714844" stop-color="rgb(84.468079%, 84.185791%, 84.255981%)" stop-opacity="1"/>
<stop offset="0.71875" stop-color="rgb(84.194946%, 83.906555%, 83.978271%)" stop-opacity="1"/>
<stop offset="0.722656" stop-color="rgb(83.920288%, 83.628845%, 83.700562%)" stop-opacity="1"/>
<stop offset="0.726562" stop-color="rgb(83.653259%, 83.355713%, 83.430481%)" stop-opacity="1"/>
<stop offset="0.730469" stop-color="rgb(83.389282%, 83.087158%, 83.161926%)" stop-opacity="1"/>
<stop offset="0.734375" stop-color="rgb(83.126831%, 82.820129%, 82.896423%)" stop-opacity="1"/>
<stop offset="0.738281" stop-color="rgb(82.870483%, 82.559204%, 82.637024%)" stop-opacity="1"/>
<stop offset="0.742188" stop-color="rgb(82.615662%, 82.298279%, 82.377625%)" stop-opacity="1"/>
<stop offset="0.746094" stop-color="rgb(82.365417%, 82.043457%, 82.124329%)" stop-opacity="1"/>
<stop offset="0.75" stop-color="rgb(82.119751%, 81.794739%, 81.87561%)" stop-opacity="1"/>
<stop offset="0.753906" stop-color="rgb(81.877136%, 81.547546%, 81.628418%)" stop-opacity="1"/>
<stop offset="0.757812" stop-color="rgb(81.639099%, 81.304932%, 81.387329%)" stop-opacity="1"/>
<stop offset="0.761719" stop-color="rgb(81.402588%, 81.063843%, 81.147766%)" stop-opacity="1"/>
<stop offset="0.765625" stop-color="rgb(81.169128%, 80.827332%, 80.912781%)" stop-opacity="1"/>
<stop offset="0.769531" stop-color="rgb(80.941772%, 80.595398%, 80.680847%)" stop-opacity="1"/>
<stop offset="0.773438" stop-color="rgb(80.717468%, 80.366516%, 80.453491%)" stop-opacity="1"/>
<stop offset="0.777344" stop-color="rgb(80.49469%, 80.13916%, 80.227661%)" stop-opacity="1"/>
<stop offset="0.78125" stop-color="rgb(80.278015%, 79.919434%, 80.00946%)" stop-opacity="1"/>
<stop offset="0.785156" stop-color="rgb(80.065918%, 79.702759%, 79.792786%)" stop-opacity="1"/>
<stop offset="0.789062" stop-color="rgb(79.853821%, 79.486084%, 79.577637%)" stop-opacity="1"/>
<stop offset="0.792969" stop-color="rgb(79.646301%, 79.277039%, 79.368591%)" stop-opacity="1"/>
<stop offset="0.796875" stop-color="rgb(79.444885%, 79.071045%, 79.164124%)" stop-opacity="1"/>
<stop offset="0.800781" stop-color="rgb(79.244995%, 78.868103%, 78.961182%)" stop-opacity="1"/>
<stop offset="0.804688" stop-color="rgb(79.049683%, 78.669739%, 78.764343%)" stop-opacity="1"/>
<stop offset="0.808594" stop-color="rgb(78.858948%, 78.474426%, 78.570557%)" stop-opacity="1"/>
<stop offset="0.8125" stop-color="rgb(78.671265%, 78.283691%, 78.379822%)" stop-opacity="1"/>
<stop offset="0.816406" stop-color="rgb(78.485107%, 78.094482%, 78.192139%)" stop-opacity="1"/>
<stop offset="0.820312" stop-color="rgb(78.305054%, 77.909851%, 78.009033%)" stop-opacity="1"/>
<stop offset="0.824219" stop-color="rgb(78.129578%, 77.732849%, 77.830505%)" stop-opacity="1"/>
<stop offset="0.828125" stop-color="rgb(77.955627%, 77.554321%, 77.653503%)" stop-opacity="1"/>
<stop offset="0.832031" stop-color="rgb(77.703857%, 77.297974%, 77.398682%)" stop-opacity="1"/>
<stop offset="0.839844" stop-color="rgb(77.381897%, 76.96991%, 77.072144%)" stop-opacity="1"/>
<stop offset="0.847656" stop-color="rgb(77.070618%, 76.654053%, 76.756287%)" stop-opacity="1"/>
<stop offset="0.855469" stop-color="rgb(76.777649%, 76.356506%, 76.460266%)" stop-opacity="1"/>
<stop offset="0.863281" stop-color="rgb(76.498413%, 76.069641%, 76.176453%)" stop-opacity="1"/>
<stop offset="0.871094" stop-color="rgb(76.23291%, 75.801086%, 75.907898%)" stop-opacity="1"/>
<stop offset="0.878906" stop-color="rgb(76.045227%, 75.608826%, 75.717163%)" stop-opacity="1"/>
<stop offset="0.882812" stop-color="rgb(75.923157%, 75.485229%, 75.593567%)" stop-opacity="1"/>
<stop offset="0.886719" stop-color="rgb(75.697327%, 75.254822%, 75.364685%)" stop-opacity="1"/>
<stop offset="0.898438" stop-color="rgb(75.382996%, 74.934387%, 75.045776%)" stop-opacity="1"/>
<stop offset="0.910156" stop-color="rgb(75.141907%, 74.690247%, 74.801636%)" stop-opacity="1"/>
<stop offset="0.917969" stop-color="rgb(74.969482%, 74.514771%, 74.627686%)" stop-opacity="1"/>
<stop offset="0.925781" stop-color="rgb(74.746704%, 74.287415%, 74.401855%)" stop-opacity="1"/>
<stop offset="0.941406" stop-color="rgb(74.519348%, 74.055481%, 74.171448%)" stop-opacity="1"/>
<stop offset="0.953125" stop-color="rgb(74.276733%, 73.809814%, 73.925781%)" stop-opacity="1"/>
<stop offset="1" stop-color="rgb(74.121094%, 73.649597%, 73.76709%)" stop-opacity="1"/>
</radialGradient>
<clipPath id="clip-3">
<path clip-rule="nonzero" d="M 1184 199 L 1681 199 L 1681 361 L 1184 361 Z M 1184 199 "/>
</clipPath>
<clipPath id="clip-4">
<path clip-rule="nonzero" d="M 1975.851562 980.382812 C 1975.851562 1411.570312 1626.300781 1761.121094 1195.121094 1761.121094 C 763.925781 1761.121094 414.378906 1411.570312 414.378906 980.382812 C 414.378906 549.191406 763.925781 199.648438 1195.121094 199.648438 C 1626.300781 199.648438 1975.851562 549.191406 1975.851562 980.382812 Z M 1975.851562 980.382812 "/>
</clipPath>
<clipPath id="clip-5">
<path clip-rule="nonzero" d="M 414 863 L 562 863 L 562 1421 L 414 1421 Z M 414 863 "/>
</clipPath>
<clipPath id="clip-6">
<path clip-rule="nonzero" d="M 1975.851562 980.382812 C 1975.851562 1411.570312 1626.300781 1761.121094 1195.121094 1761.121094 C 763.925781 1761.121094 414.378906 1411.570312 414.378906 980.382812 C 414.378906 549.191406 763.925781 199.648438 1195.121094 199.648438 C 1626.300781 199.648438 1975.851562 549.191406 1975.851562 980.382812 Z M 1975.851562 980.382812 "/>
</clipPath>
<clipPath id="clip-7">
<path clip-rule="nonzero" d="M 815 1663 L 1070 1663 L 1070 1761 L 815 1761 Z M 815 1663 "/>
</clipPath>
<clipPath id="clip-8">
<path clip-rule="nonzero" d="M 1975.851562 980.382812 C 1975.851562 1411.570312 1626.300781 1761.121094 1195.121094 1761.121094 C 763.925781 1761.121094 414.378906 1411.570312 414.378906 980.382812 C 414.378906 549.191406 763.925781 199.648438 1195.121094 199.648438 C 1626.300781 199.648438 1975.851562 549.191406 1975.851562 980.382812 Z M 1975.851562 980.382812 "/>
</clipPath>
<clipPath id="clip-9">
<path clip-rule="nonzero" d="M 1476 1355 L 1890 1355 L 1890 1725 L 1476 1725 Z M 1476 1355 "/>
</clipPath>
<clipPath id="clip-10">
<path clip-rule="nonzero" d="M 1975.851562 980.382812 C 1975.851562 1411.570312 1626.300781 1761.121094 1195.121094 1761.121094 C 763.925781 1761.121094 414.378906 1411.570312 414.378906 980.382812 C 414.378906 549.191406 763.925781 199.648438 1195.121094 199.648438 C 1626.300781 199.648438 1975.851562 549.191406 1975.851562 980.382812 Z M 1975.851562 980.382812 "/>
</clipPath>
<clipPath id="clip-11">
<path clip-rule="nonzero" d="M 1489 646 L 1617 646 L 1617 993 L 1489 993 Z M 1489 646 "/>
</clipPath>
<clipPath id="clip-12">
<path clip-rule="nonzero" d="M 1489.601562 685.148438 C 1489.601562 685.148438 1520.21875 765.46875 1523.691406 846.960938 C 1527.160156 928.441406 1528.699219 964.652344 1528.699219 964.652344 L 1616.050781 992.679688 C 1616.050781 992.679688 1580.730469 809.160156 1569.441406 749.78125 C 1534.378906 565.328125 1489.601562 685.148438 1489.601562 685.148438 Z M 1489.601562 685.148438 "/>
</clipPath>
<clipPath id="clip-13">
<path clip-rule="nonzero" d="M 1489.601562 685.148438 C 1489.601562 685.148438 1520.21875 765.46875 1523.691406 846.960938 C 1527.160156 928.441406 1528.699219 964.652344 1528.699219 964.652344 L 1616.050781 992.679688 C 1616.050781 992.679688 1580.730469 809.160156 1569.441406 749.78125 C 1534.378906 565.328125 1489.601562 685.148438 1489.601562 685.148438 "/>
</clipPath>
<linearGradient id="linear-pattern-0" gradientUnits="userSpaceOnUse" x1="0.664577" y1="0" x2="1.31303" y2="0" gradientTransform="matrix(307.241, -57.3093, 57.3093, 307.241, 1256.91, 877.95)">
<stop offset="0" stop-color="rgb(40.727234%, 39.648438%, 39.915466%)" stop-opacity="1"/>
<stop offset="0.00390625" stop-color="rgb(40.536499%, 39.454651%, 39.723206%)" stop-opacity="1"/>
<stop offset="0.0078125" stop-color="rgb(40.34729%, 39.26239%, 39.532471%)" stop-opacity="1"/>
<stop offset="0.0117187" stop-color="rgb(40.158081%, 39.068604%, 39.34021%)" stop-opacity="1"/>
<stop offset="0.015625" stop-color="rgb(39.968872%, 38.876343%, 39.147949%)" stop-opacity="1"/>
<stop offset="0.0195312" stop-color="rgb(39.778137%, 38.68103%, 38.954163%)" stop-opacity="1"/>
<stop offset="0.0234375" stop-color="rgb(39.587402%, 38.487244%, 38.760376%)" stop-opacity="1"/>
<stop offset="0.0273438" stop-color="rgb(39.396667%, 38.294983%, 38.568115%)" stop-opacity="1"/>
<stop offset="0.03125" stop-color="rgb(39.205933%, 38.09967%, 38.374329%)" stop-opacity="1"/>
<stop offset="0.0351563" stop-color="rgb(39.015198%, 37.905884%, 38.182068%)" stop-opacity="1"/>
<stop offset="0.0390625" stop-color="rgb(38.822937%, 37.710571%, 37.986755%)" stop-opacity="1"/>
<stop offset="0.0429688" stop-color="rgb(38.632202%, 37.515259%, 37.792969%)" stop-opacity="1"/>
<stop offset="0.046875" stop-color="rgb(38.439941%, 37.319946%, 37.597656%)" stop-opacity="1"/>
<stop offset="0.0507813" stop-color="rgb(38.247681%, 37.124634%, 37.40387%)" stop-opacity="1"/>
<stop offset="0.0546875" stop-color="rgb(38.05542%, 36.929321%, 37.208557%)" stop-opacity="1"/>
<stop offset="0.0585938" stop-color="rgb(37.863159%, 36.732483%, 37.013245%)" stop-opacity="1"/>
<stop offset="0.0625" stop-color="rgb(37.670898%, 36.53717%, 36.819458%)" stop-opacity="1"/>
<stop offset="0.0664063" stop-color="rgb(37.478638%, 36.340332%, 36.62262%)" stop-opacity="1"/>
<stop offset="0.0703125" stop-color="rgb(37.286377%, 36.14502%, 36.427307%)" stop-opacity="1"/>
<stop offset="0.0742188" stop-color="rgb(37.09259%, 35.948181%, 36.231995%)" stop-opacity="1"/>
<stop offset="0.078125" stop-color="rgb(36.898804%, 35.749817%, 36.035156%)" stop-opacity="1"/>
<stop offset="0.0820312" stop-color="rgb(36.705017%, 35.552979%, 35.839844%)" stop-opacity="1"/>
<stop offset="0.0859375" stop-color="rgb(36.509705%, 35.354614%, 35.641479%)" stop-opacity="1"/>
<stop offset="0.0898437" stop-color="rgb(36.317444%, 35.157776%, 35.446167%)" stop-opacity="1"/>
<stop offset="0.09375" stop-color="rgb(36.122131%, 34.959412%, 35.247803%)" stop-opacity="1"/>
<stop offset="0.0976562" stop-color="rgb(35.926819%, 34.761047%, 35.050964%)" stop-opacity="1"/>
<stop offset="0.101562" stop-color="rgb(35.733032%, 34.562683%, 34.854126%)" stop-opacity="1"/>
<stop offset="0.105469" stop-color="rgb(35.53772%, 34.364319%, 34.655762%)" stop-opacity="1"/>
<stop offset="0.109375" stop-color="rgb(35.342407%, 34.165955%, 34.458923%)" stop-opacity="1"/>
<stop offset="0.113281" stop-color="rgb(35.147095%, 33.966064%, 34.259033%)" stop-opacity="1"/>
<stop offset="0.117187" stop-color="rgb(34.950256%, 33.766174%, 34.060669%)" stop-opacity="1"/>
<stop offset="0.121094" stop-color="rgb(34.754944%, 33.56781%, 33.863831%)" stop-opacity="1"/>
<stop offset="0.125" stop-color="rgb(34.559631%, 33.36792%, 33.66394%)" stop-opacity="1"/>
<stop offset="0.128906" stop-color="rgb(34.362793%, 33.16803%, 33.46405%)" stop-opacity="1"/>
<stop offset="0.132813" stop-color="rgb(34.165955%, 32.96814%, 33.265686%)" stop-opacity="1"/>
<stop offset="0.136719" stop-color="rgb(33.969116%, 32.766724%, 33.065796%)" stop-opacity="1"/>
<stop offset="0.140625" stop-color="rgb(33.772278%, 32.566833%, 32.865906%)" stop-opacity="1"/>
<stop offset="0.144531" stop-color="rgb(33.575439%, 32.365417%, 32.666016%)" stop-opacity="1"/>
<stop offset="0.148438" stop-color="rgb(33.377075%, 32.165527%, 32.466125%)" stop-opacity="1"/>
<stop offset="0.152344" stop-color="rgb(33.180237%, 31.964111%, 32.266235%)" stop-opacity="1"/>
<stop offset="0.15625" stop-color="rgb(32.981873%, 31.761169%, 32.064819%)" stop-opacity="1"/>
<stop offset="0.160156" stop-color="rgb(32.783508%, 31.561279%, 31.864929%)" stop-opacity="1"/>
<stop offset="0.164062" stop-color="rgb(32.585144%, 31.358337%, 31.663513%)" stop-opacity="1"/>
<stop offset="0.167969" stop-color="rgb(32.38678%, 31.155396%, 31.462097%)" stop-opacity="1"/>
<stop offset="0.171875" stop-color="rgb(32.18689%, 30.953979%, 31.260681%)" stop-opacity="1"/>
<stop offset="0.175781" stop-color="rgb(31.988525%, 30.751038%, 31.057739%)" stop-opacity="1"/>
<stop offset="0.179687" stop-color="rgb(31.788635%, 30.54657%, 30.856323%)" stop-opacity="1"/>
<stop offset="0.183594" stop-color="rgb(31.590271%, 30.345154%, 30.653381%)" stop-opacity="1"/>
<stop offset="0.1875" stop-color="rgb(31.390381%, 30.140686%, 30.451965%)" stop-opacity="1"/>
<stop offset="0.191406" stop-color="rgb(31.188965%, 29.937744%, 30.249023%)" stop-opacity="1"/>
<stop offset="0.195312" stop-color="rgb(30.989075%, 29.733276%, 30.046082%)" stop-opacity="1"/>
<stop offset="0.199219" stop-color="rgb(30.789185%, 29.530334%, 29.84314%)" stop-opacity="1"/>
<stop offset="0.203125" stop-color="rgb(30.589294%, 29.325867%, 29.640198%)" stop-opacity="1"/>
<stop offset="0.207031" stop-color="rgb(30.387878%, 29.121399%, 29.43573%)" stop-opacity="1"/>
<stop offset="0.210937" stop-color="rgb(30.186462%, 28.915405%, 29.231262%)" stop-opacity="1"/>
<stop offset="0.214844" stop-color="rgb(29.985046%, 28.710938%, 29.026794%)" stop-opacity="1"/>
<stop offset="0.21875" stop-color="rgb(29.78363%, 28.50647%, 28.823853%)" stop-opacity="1"/>
<stop offset="0.222656" stop-color="rgb(29.582214%, 28.300476%, 28.619385%)" stop-opacity="1"/>
<stop offset="0.226563" stop-color="rgb(29.380798%, 28.096008%, 28.414917%)" stop-opacity="1"/>
<stop offset="0.230469" stop-color="rgb(29.177856%, 27.890015%, 28.208923%)" stop-opacity="1"/>
<stop offset="0.234375" stop-color="rgb(28.974915%, 27.682495%, 28.004456%)" stop-opacity="1"/>
<stop offset="0.238281" stop-color="rgb(28.773499%, 27.478027%, 27.799988%)" stop-opacity="1"/>
<stop offset="0.242188" stop-color="rgb(28.572083%, 27.272034%, 27.593994%)" stop-opacity="1"/>
<stop offset="0.246094" stop-color="rgb(28.367615%, 27.062988%, 27.388%)" stop-opacity="1"/>
<stop offset="0.25" stop-color="rgb(28.164673%, 26.856995%, 27.182007%)" stop-opacity="1"/>
<stop offset="0.253906" stop-color="rgb(27.960205%, 26.649475%, 26.974487%)" stop-opacity="1"/>
<stop offset="0.257812" stop-color="rgb(27.757263%, 26.441956%, 26.768494%)" stop-opacity="1"/>
<stop offset="0.261719" stop-color="rgb(27.554321%, 26.235962%, 26.5625%)" stop-opacity="1"/>
<stop offset="0.265625" stop-color="rgb(27.349854%, 26.026917%, 26.356506%)" stop-opacity="1"/>
<stop offset="0.269531" stop-color="rgb(27.14386%, 25.817871%, 26.147461%)" stop-opacity="1"/>
<stop offset="0.273437" stop-color="rgb(26.940918%, 25.610352%, 25.941467%)" stop-opacity="1"/>
<stop offset="0.277344" stop-color="rgb(26.734924%, 25.401306%, 25.732422%)" stop-opacity="1"/>
<stop offset="0.28125" stop-color="rgb(26.528931%, 25.192261%, 25.524902%)" stop-opacity="1"/>
<stop offset="0.285156" stop-color="rgb(26.324463%, 24.983215%, 25.315857%)" stop-opacity="1"/>
<stop offset="0.289063" stop-color="rgb(26.118469%, 24.77417%, 25.108337%)" stop-opacity="1"/>
<stop offset="0.292969" stop-color="rgb(25.912476%, 24.563599%, 24.899292%)" stop-opacity="1"/>
<stop offset="0.296875" stop-color="rgb(25.706482%, 24.354553%, 24.690247%)" stop-opacity="1"/>
<stop offset="0.300781" stop-color="rgb(25.500488%, 24.143982%, 24.481201%)" stop-opacity="1"/>
<stop offset="0.304688" stop-color="rgb(25.294495%, 23.934937%, 24.273682%)" stop-opacity="1"/>
<stop offset="0.308594" stop-color="rgb(25.088501%, 23.724365%, 24.06311%)" stop-opacity="1"/>
<stop offset="0.3125" stop-color="rgb(24.880981%, 23.513794%, 23.852539%)" stop-opacity="1"/>
<stop offset="0.316406" stop-color="rgb(24.673462%, 23.303223%, 23.643494%)" stop-opacity="1"/>
<stop offset="0.320313" stop-color="rgb(24.467468%, 23.092651%, 23.434448%)" stop-opacity="1"/>
<stop offset="0.324219" stop-color="rgb(24.258423%, 22.880554%, 23.222351%)" stop-opacity="1"/>
<stop offset="0.328125" stop-color="rgb(24.050903%, 22.668457%, 23.01178%)" stop-opacity="1"/>
<stop offset="0.332031" stop-color="rgb(23.843384%, 22.45636%, 22.801208%)" stop-opacity="1"/>
<stop offset="0.335938" stop-color="rgb(23.634338%, 22.245789%, 22.590637%)" stop-opacity="1"/>
<stop offset="0.339844" stop-color="rgb(23.426819%, 22.033691%, 22.380066%)" stop-opacity="1"/>
<stop offset="0.34375" stop-color="rgb(23.219299%, 21.821594%, 22.169495%)" stop-opacity="1"/>
<stop offset="0.347656" stop-color="rgb(23.010254%, 21.607971%, 21.955872%)" stop-opacity="1"/>
<stop offset="0.351562" stop-color="rgb(22.799683%, 21.395874%, 21.743774%)" stop-opacity="1"/>
<stop offset="0.355469" stop-color="rgb(22.590637%, 21.182251%, 21.531677%)" stop-opacity="1"/>
<stop offset="0.359375" stop-color="rgb(22.381592%, 20.968628%, 21.31958%)" stop-opacity="1"/>
<stop offset="0.363281" stop-color="rgb(22.172546%, 20.756531%, 21.107483%)" stop-opacity="1"/>
<stop offset="0.367187" stop-color="rgb(21.961975%, 20.541382%, 20.89386%)" stop-opacity="1"/>
<stop offset="0.371094" stop-color="rgb(21.75293%, 20.329285%, 20.681763%)" stop-opacity="1"/>
<stop offset="0.375" stop-color="rgb(21.542358%, 20.114136%, 20.46814%)" stop-opacity="1"/>
<stop offset="0.378906" stop-color="rgb(21.331787%, 19.898987%, 20.256042%)" stop-opacity="1"/>
<stop offset="0.382813" stop-color="rgb(21.121216%, 19.685364%, 20.042419%)" stop-opacity="1"/>
<stop offset="0.386719" stop-color="rgb(20.910645%, 19.470215%, 19.828796%)" stop-opacity="1"/>
<stop offset="0.390625" stop-color="rgb(20.698547%, 19.255066%, 19.613647%)" stop-opacity="1"/>
<stop offset="0.394531" stop-color="rgb(20.487976%, 19.039917%, 19.400024%)" stop-opacity="1"/>
<stop offset="0.398438" stop-color="rgb(20.275879%, 18.824768%, 19.184875%)" stop-opacity="1"/>
<stop offset="0.402344" stop-color="rgb(20.063782%, 18.609619%, 18.969727%)" stop-opacity="1"/>
<stop offset="0.40625" stop-color="rgb(19.851685%, 18.392944%, 18.756104%)" stop-opacity="1"/>
<stop offset="0.410156" stop-color="rgb(19.638062%, 18.17627%, 18.539429%)" stop-opacity="1"/>
<stop offset="0.414062" stop-color="rgb(19.425964%, 17.959595%, 18.32428%)" stop-opacity="1"/>
<stop offset="0.417969" stop-color="rgb(19.213867%, 17.74292%, 18.107605%)" stop-opacity="1"/>
<stop offset="0.421875" stop-color="rgb(19.000244%, 17.526245%, 17.892456%)" stop-opacity="1"/>
<stop offset="0.425781" stop-color="rgb(18.786621%, 17.308044%, 17.675781%)" stop-opacity="1"/>
<stop offset="0.429687" stop-color="rgb(18.572998%, 17.09137%, 17.459106%)" stop-opacity="1"/>
<stop offset="0.433594" stop-color="rgb(18.360901%, 16.874695%, 17.243958%)" stop-opacity="1"/>
<stop offset="0.4375" stop-color="rgb(18.145752%, 16.656494%, 17.025757%)" stop-opacity="1"/>
<stop offset="0.441406" stop-color="rgb(17.932129%, 16.438293%, 16.810608%)" stop-opacity="1"/>
<stop offset="0.445312" stop-color="rgb(17.718506%, 16.220093%, 16.592407%)" stop-opacity="1"/>
<stop offset="0.449219" stop-color="rgb(17.503357%, 16.001892%, 16.374207%)" stop-opacity="1"/>
<stop offset="0.453125" stop-color="rgb(17.288208%, 15.783691%, 16.157532%)" stop-opacity="1"/>
<stop offset="0.457031" stop-color="rgb(17.074585%, 15.565491%, 15.939331%)" stop-opacity="1"/>
<stop offset="0.460937" stop-color="rgb(16.85791%, 15.345764%, 15.72113%)" stop-opacity="1"/>
<stop offset="0.464844" stop-color="rgb(16.642761%, 15.126038%, 15.50293%)" stop-opacity="1"/>
<stop offset="0.46875" stop-color="rgb(16.427612%, 14.906311%, 15.284729%)" stop-opacity="1"/>
<stop offset="0.472656" stop-color="rgb(16.212463%, 14.686584%, 15.066528%)" stop-opacity="1"/>
<stop offset="0.476563" stop-color="rgb(15.994263%, 14.465332%, 14.845276%)" stop-opacity="1"/>
<stop offset="0.480469" stop-color="rgb(15.779114%, 14.245605%, 14.627075%)" stop-opacity="1"/>
<stop offset="0.484375" stop-color="rgb(15.562439%, 14.025879%, 14.407349%)" stop-opacity="1"/>
<stop offset="0.488281" stop-color="rgb(15.345764%, 13.804626%, 14.187622%)" stop-opacity="1"/>
<stop offset="0.492188" stop-color="rgb(15.129089%, 13.583374%, 13.967896%)" stop-opacity="1"/>
<stop offset="0.496094" stop-color="rgb(14.910889%, 13.362122%, 13.748169%)" stop-opacity="1"/>
<stop offset="0.5" stop-color="rgb(14.694214%, 13.142395%, 13.526917%)" stop-opacity="1"/>
<stop offset="0.503906" stop-color="rgb(14.477539%, 12.921143%, 13.30719%)" stop-opacity="1"/>
<stop offset="0.507812" stop-color="rgb(14.257812%, 12.698364%, 13.085938%)" stop-opacity="1"/>
<stop offset="0.511719" stop-color="rgb(14.039612%, 12.475586%, 12.864685%)" stop-opacity="1"/>
<stop offset="0.515625" stop-color="rgb(13.821411%, 12.252808%, 12.641907%)" stop-opacity="1"/>
<stop offset="0.53125" stop-color="rgb(13.775635%, 12.205505%, 12.59613%)" stop-opacity="1"/>
<stop offset="0.5625" stop-color="rgb(13.729858%, 12.159729%, 12.548828%)" stop-opacity="1"/>
<stop offset="0.625" stop-color="rgb(13.729858%, 12.159729%, 12.548828%)" stop-opacity="1"/>
<stop offset="0.75" stop-color="rgb(13.729858%, 12.159729%, 12.548828%)" stop-opacity="1"/>
<stop offset="1" stop-color="rgb(13.729858%, 12.159729%, 12.548828%)" stop-opacity="1"/>
</linearGradient>
<clipPath id="clip-14">
<path clip-rule="nonzero" d="M 966 1114 L 1274 1114 L 1274 1280 L 966 1280 Z M 966 1114 "/>
</clipPath>
<clipPath id="clip-15">
<path clip-rule="nonzero" d="M 966.808594 1114.558594 L 1270.371094 1214.238281 L 1273.390625 1279.179688 L 974.359375 1179.5 Z M 966.808594 1114.558594 "/>
</clipPath>
<clipPath id="clip-16">
<path clip-rule="nonzero" d="M 966.808594 1114.558594 L 1270.371094 1214.238281 L 1273.390625 1279.179688 L 974.359375 1179.5 L 966.808594 1114.558594 "/>
</clipPath>
<linearGradient id="linear-pattern-1" gradientUnits="userSpaceOnUse" x1="0.466075" y1="0" x2="1.500953" y2="0" gradientTransform="matrix(-64.3774, 225.321, -225.321, -64.3774, 1183.42, 975.265)">
<stop offset="0" stop-color="rgb(54.873657%, 54.052734%, 54.257202%)" stop-opacity="1"/>
<stop offset="0.00390625" stop-color="rgb(54.598999%, 53.771973%, 53.977966%)" stop-opacity="1"/>
<stop offset="0.0078125" stop-color="rgb(54.324341%, 53.492737%, 53.69873%)" stop-opacity="1"/>
<stop offset="0.0117188" stop-color="rgb(54.048157%, 53.211975%, 53.419495%)" stop-opacity="1"/>
<stop offset="0.015625" stop-color="rgb(53.771973%, 52.931213%, 53.140259%)" stop-opacity="1"/>
<stop offset="0.0195312" stop-color="rgb(53.495789%, 52.648926%, 52.859497%)" stop-opacity="1"/>
<stop offset="0.0234375" stop-color="rgb(53.218079%, 52.366638%, 52.578735%)" stop-opacity="1"/>
<stop offset="0.0273438" stop-color="rgb(52.940369%, 52.084351%, 52.296448%)" stop-opacity="1"/>
<stop offset="0.03125" stop-color="rgb(52.661133%, 51.799011%, 52.01416%)" stop-opacity="1"/>
<stop offset="0.0351562" stop-color="rgb(52.383423%, 51.516724%, 51.731873%)" stop-opacity="1"/>
<stop offset="0.0390625" stop-color="rgb(52.102661%, 51.231384%, 51.448059%)" stop-opacity="1"/>
<stop offset="0.0429688" stop-color="rgb(51.823425%, 50.947571%, 51.164246%)" stop-opacity="1"/>
<stop offset="0.046875" stop-color="rgb(51.542664%, 50.660706%, 50.878906%)" stop-opacity="1"/>
<stop offset="0.0507812" stop-color="rgb(51.260376%, 50.37384%, 50.593567%)" stop-opacity="1"/>
<stop offset="0.0546875" stop-color="rgb(50.978088%, 50.086975%, 50.308228%)" stop-opacity="1"/>
<stop offset="0.0585938" stop-color="rgb(50.697327%, 49.80011%, 50.022888%)" stop-opacity="1"/>
<stop offset="0.0625" stop-color="rgb(50.413513%, 49.511719%, 49.736023%)" stop-opacity="1"/>
<stop offset="0.0664062" stop-color="rgb(50.1297%, 49.221802%, 49.447632%)" stop-opacity="1"/>
<stop offset="0.0703125" stop-color="rgb(49.845886%, 48.931885%, 49.159241%)" stop-opacity="1"/>
<stop offset="0.0742188" stop-color="rgb(49.560547%, 48.643494%, 48.87085%)" stop-opacity="1"/>
<stop offset="0.078125" stop-color="rgb(49.275208%, 48.352051%, 48.580933%)" stop-opacity="1"/>
<stop offset="0.0820312" stop-color="rgb(48.989868%, 48.062134%, 48.292542%)" stop-opacity="1"/>
<stop offset="0.0859375" stop-color="rgb(48.703003%, 47.769165%, 48.001099%)" stop-opacity="1"/>
<stop offset="0.0898438" stop-color="rgb(48.417664%, 47.477722%, 47.711182%)" stop-opacity="1"/>
<stop offset="0.09375" stop-color="rgb(48.130798%, 47.186279%, 47.421265%)" stop-opacity="1"/>
<stop offset="0.0976562" stop-color="rgb(47.842407%, 46.893311%, 47.128296%)" stop-opacity="1"/>
<stop offset="0.101562" stop-color="rgb(47.55249%, 46.59729%, 46.835327%)" stop-opacity="1"/>
<stop offset="0.105469" stop-color="rgb(47.264099%, 46.304321%, 46.542358%)" stop-opacity="1"/>
<stop offset="0.109375" stop-color="rgb(46.974182%, 46.009827%, 46.24939%)" stop-opacity="1"/>
<stop offset="0.113281" stop-color="rgb(46.682739%, 45.713806%, 45.954895%)" stop-opacity="1"/>
<stop offset="0.117188" stop-color="rgb(46.392822%, 45.41626%, 45.658875%)" stop-opacity="1"/>
<stop offset="0.121094" stop-color="rgb(46.099854%, 45.118713%, 45.362854%)" stop-opacity="1"/>
<stop offset="0.125" stop-color="rgb(45.808411%, 44.821167%, 45.066833%)" stop-opacity="1"/>
<stop offset="0.128906" stop-color="rgb(45.515442%, 44.523621%, 44.770813%)" stop-opacity="1"/>
<stop offset="0.132812" stop-color="rgb(45.222473%, 44.226074%, 44.473267%)" stop-opacity="1"/>
<stop offset="0.136719" stop-color="rgb(44.927979%, 43.927002%, 44.17572%)" stop-opacity="1"/>
<stop offset="0.140625" stop-color="rgb(44.63501%, 43.62793%, 43.878174%)" stop-opacity="1"/>
<stop offset="0.144531" stop-color="rgb(44.340515%, 43.327332%, 43.579102%)" stop-opacity="1"/>
<stop offset="0.148438" stop-color="rgb(44.046021%, 43.026733%, 43.280029%)" stop-opacity="1"/>
<stop offset="0.152344" stop-color="rgb(43.748474%, 42.726135%, 42.979431%)" stop-opacity="1"/>
<stop offset="0.15625" stop-color="rgb(43.452454%, 42.424011%, 42.678833%)" stop-opacity="1"/>
<stop offset="0.160156" stop-color="rgb(43.154907%, 42.120361%, 42.378235%)" stop-opacity="1"/>
<stop offset="0.164062" stop-color="rgb(42.857361%, 41.818237%, 42.076111%)" stop-opacity="1"/>
<stop offset="0.167969" stop-color="rgb(42.559814%, 41.514587%, 41.773987%)" stop-opacity="1"/>
<stop offset="0.171875" stop-color="rgb(42.260742%, 41.209412%, 41.470337%)" stop-opacity="1"/>
<stop offset="0.175781" stop-color="rgb(41.96167%, 40.905762%, 41.168213%)" stop-opacity="1"/>
<stop offset="0.179688" stop-color="rgb(41.661072%, 40.59906%, 40.863037%)" stop-opacity="1"/>
<stop offset="0.183594" stop-color="rgb(41.360474%, 40.292358%, 40.557861%)" stop-opacity="1"/>
<stop offset="0.1875" stop-color="rgb(41.059875%, 39.987183%, 40.254211%)" stop-opacity="1"/>
<stop offset="0.191406" stop-color="rgb(40.757751%, 39.678955%, 39.94751%)" stop-opacity="1"/>
<stop offset="0.195312" stop-color="rgb(40.455627%, 39.372253%, 39.640808%)" stop-opacity="1"/>
<stop offset="0.199219" stop-color="rgb(40.153503%, 39.065552%, 39.335632%)" stop-opacity="1"/>
<stop offset="0.203125" stop-color="rgb(39.849854%, 38.755798%, 39.027405%)" stop-opacity="1"/>
<stop offset="0.207031" stop-color="rgb(39.546204%, 38.446045%, 38.719177%)" stop-opacity="1"/>
<stop offset="0.210938" stop-color="rgb(39.242554%, 38.136292%, 38.41095%)" stop-opacity="1"/>
<stop offset="0.214844" stop-color="rgb(38.935852%, 37.825012%, 38.101196%)" stop-opacity="1"/>
<stop offset="0.21875" stop-color="rgb(38.630676%, 37.513733%, 37.791443%)" stop-opacity="1"/>
<stop offset="0.222656" stop-color="rgb(38.323975%, 37.202454%, 37.480164%)" stop-opacity="1"/>
<stop offset="0.226562" stop-color="rgb(38.017273%, 36.889648%, 37.17041%)" stop-opacity="1"/>
<stop offset="0.230469" stop-color="rgb(37.710571%, 36.576843%, 36.859131%)" stop-opacity="1"/>
<stop offset="0.234375" stop-color="rgb(37.402344%, 36.264038%, 36.546326%)" stop-opacity="1"/>
<stop offset="0.238281" stop-color="rgb(37.094116%, 35.948181%, 36.233521%)" stop-opacity="1"/>
<stop offset="0.242188" stop-color="rgb(36.785889%, 35.635376%, 35.920715%)" stop-opacity="1"/>
<stop offset="0.246094" stop-color="rgb(36.474609%, 35.319519%, 35.606384%)" stop-opacity="1"/>
<stop offset="0.25" stop-color="rgb(36.164856%, 35.003662%, 35.292053%)" stop-opacity="1"/>
<stop offset="0.253906" stop-color="rgb(35.855103%, 34.687805%, 34.977722%)" stop-opacity="1"/>
<stop offset="0.257812" stop-color="rgb(35.542297%, 34.370422%, 34.661865%)" stop-opacity="1"/>
<stop offset="0.261719" stop-color="rgb(35.232544%, 34.05304%, 34.346008%)" stop-opacity="1"/>
<stop offset="0.265625" stop-color="rgb(34.919739%, 33.735657%, 34.030151%)" stop-opacity="1"/>
<stop offset="0.269531" stop-color="rgb(34.606934%, 33.416748%, 33.712769%)" stop-opacity="1"/>
<stop offset="0.273438" stop-color="rgb(34.294128%, 33.097839%, 33.395386%)" stop-opacity="1"/>
<stop offset="0.277344" stop-color="rgb(33.979797%, 32.777405%, 33.076477%)" stop-opacity="1"/>
<stop offset="0.28125" stop-color="rgb(33.66394%, 32.45697%, 32.757568%)" stop-opacity="1"/>
<stop offset="0.285156" stop-color="rgb(33.349609%, 32.136536%, 32.43866%)" stop-opacity="1"/>
<stop offset="0.289062" stop-color="rgb(33.033752%, 31.814575%, 32.118225%)" stop-opacity="1"/>
<stop offset="0.292969" stop-color="rgb(32.717896%, 31.494141%, 31.797791%)" stop-opacity="1"/>
<stop offset="0.296875" stop-color="rgb(32.400513%, 31.170654%, 31.47583%)" stop-opacity="1"/>
<stop offset="0.300781" stop-color="rgb(32.08313%, 30.847168%, 31.15387%)" stop-opacity="1"/>
<stop offset="0.304688" stop-color="rgb(31.764221%, 30.523682%, 30.831909%)" stop-opacity="1"/>
<stop offset="0.308594" stop-color="rgb(31.446838%, 30.198669%, 30.508423%)" stop-opacity="1"/>
<stop offset="0.3125" stop-color="rgb(31.12793%, 29.873657%, 30.186462%)" stop-opacity="1"/>
<stop offset="0.316406" stop-color="rgb(30.809021%, 29.550171%, 29.862976%)" stop-opacity="1"/>
<stop offset="0.320312" stop-color="rgb(30.488586%, 29.223633%, 29.537964%)" stop-opacity="1"/>
<stop offset="0.324219" stop-color="rgb(30.168152%, 28.897095%, 29.212952%)" stop-opacity="1"/>
<stop offset="0.328125" stop-color="rgb(29.846191%, 28.569031%, 28.886414%)" stop-opacity="1"/>
<stop offset="0.332031" stop-color="rgb(29.524231%, 28.242493%, 28.559875%)" stop-opacity="1"/>
<stop offset="0.335938" stop-color="rgb(29.202271%, 27.912903%, 28.233337%)" stop-opacity="1"/>
<stop offset="0.339844" stop-color="rgb(28.878784%, 27.583313%, 27.905273%)" stop-opacity="1"/>
<stop offset="0.34375" stop-color="rgb(28.553772%, 27.253723%, 27.577209%)" stop-opacity="1"/>
<stop offset="0.347656" stop-color="rgb(28.230286%, 26.924133%, 27.249146%)" stop-opacity="1"/>
<stop offset="0.351562" stop-color="rgb(27.905273%, 26.593018%, 26.919556%)" stop-opacity="1"/>
<stop offset="0.355469" stop-color="rgb(27.580261%, 26.261902%, 26.589966%)" stop-opacity="1"/>
<stop offset="0.359375" stop-color="rgb(27.255249%, 25.930786%, 26.260376%)" stop-opacity="1"/>
<stop offset="0.363281" stop-color="rgb(26.927185%, 25.598145%, 25.927734%)" stop-opacity="1"/>
<stop offset="0.367188" stop-color="rgb(26.600647%, 25.265503%, 25.596619%)" stop-opacity="1"/>
<stop offset="0.371094" stop-color="rgb(26.274109%, 24.932861%, 25.265503%)" stop-opacity="1"/>
<stop offset="0.375" stop-color="rgb(25.946045%, 24.598694%, 24.934387%)" stop-opacity="1"/>
<stop offset="0.378906" stop-color="rgb(25.617981%, 24.264526%, 24.60022%)" stop-opacity="1"/>
<stop offset="0.382812" stop-color="rgb(25.288391%, 23.928833%, 24.266052%)" stop-opacity="1"/>
<stop offset="0.386719" stop-color="rgb(24.957275%, 23.591614%, 23.931885%)" stop-opacity="1"/>
<stop offset="0.390625" stop-color="rgb(24.627686%, 23.25592%, 23.596191%)" stop-opacity="1"/>
<stop offset="0.394531" stop-color="rgb(24.29657%, 22.918701%, 23.262024%)" stop-opacity="1"/>
<stop offset="0.398438" stop-color="rgb(23.965454%, 22.581482%, 22.924805%)" stop-opacity="1"/>
<stop offset="0.402344" stop-color="rgb(23.632812%, 22.242737%, 22.587585%)" stop-opacity="1"/>
<stop offset="0.40625" stop-color="rgb(23.300171%, 21.905518%, 22.251892%)" stop-opacity="1"/>
<stop offset="0.410156" stop-color="rgb(22.967529%, 21.565247%, 21.914673%)" stop-opacity="1"/>
<stop offset="0.414062" stop-color="rgb(22.633362%, 21.224976%, 21.574402%)" stop-opacity="1"/>
<stop offset="0.417969" stop-color="rgb(22.299194%, 20.884705%, 21.235657%)" stop-opacity="1"/>
<stop offset="0.421875" stop-color="rgb(21.963501%, 20.542908%, 20.895386%)" stop-opacity="1"/>
<stop offset="0.425781" stop-color="rgb(21.629333%, 20.202637%, 20.556641%)" stop-opacity="1"/>
<stop offset="0.429688" stop-color="rgb(21.29364%, 19.86084%, 20.21637%)" stop-opacity="1"/>
<stop offset="0.433594" stop-color="rgb(20.956421%, 19.517517%, 19.874573%)" stop-opacity="1"/>
<stop offset="0.4375" stop-color="rgb(20.619202%, 19.174194%, 19.532776%)" stop-opacity="1"/>
<stop offset="0.441406" stop-color="rgb(20.281982%, 18.830872%, 19.192505%)" stop-opacity="1"/>
<stop offset="0.445312" stop-color="rgb(19.943237%, 18.487549%, 18.849182%)" stop-opacity="1"/>
<stop offset="0.449219" stop-color="rgb(19.604492%, 18.141174%, 18.504333%)" stop-opacity="1"/>
<stop offset="0.453125" stop-color="rgb(19.264221%, 17.7948%, 18.161011%)" stop-opacity="1"/>
<stop offset="0.457031" stop-color="rgb(18.925476%, 17.449951%, 17.816162%)" stop-opacity="1"/>
<stop offset="0.460938" stop-color="rgb(18.583679%, 17.102051%, 17.469788%)" stop-opacity="1"/>
<stop offset="0.464844" stop-color="rgb(18.243408%, 16.755676%, 17.124939%)" stop-opacity="1"/>
<stop offset="0.46875" stop-color="rgb(17.900085%, 16.40625%, 16.778564%)" stop-opacity="1"/>
<stop offset="0.472656" stop-color="rgb(17.558289%, 16.05835%, 16.430664%)" stop-opacity="1"/>
<stop offset="0.476562" stop-color="rgb(17.216492%, 15.710449%, 16.08429%)" stop-opacity="1"/>
<stop offset="0.480469" stop-color="rgb(16.874695%, 15.361023%, 15.736389%)" stop-opacity="1"/>
<stop offset="0.484375" stop-color="rgb(16.529846%, 15.011597%, 15.388489%)" stop-opacity="1"/>
<stop offset="0.488281" stop-color="rgb(16.184998%, 14.660645%, 15.039062%)" stop-opacity="1"/>
<stop offset="0.492188" stop-color="rgb(15.840149%, 14.309692%, 14.689636%)" stop-opacity="1"/>
<stop offset="0.496094" stop-color="rgb(15.493774%, 13.957214%, 14.338684%)" stop-opacity="1"/>
<stop offset="0.5" stop-color="rgb(15.148926%, 13.604736%, 13.987732%)" stop-opacity="1"/>
<stop offset="0.503906" stop-color="rgb(14.801025%, 13.250732%, 13.635254%)" stop-opacity="1"/>
<stop offset="0.507812" stop-color="rgb(14.454651%, 12.898254%, 13.284302%)" stop-opacity="1"/>
<stop offset="0.511719" stop-color="rgb(14.105225%, 12.542725%, 12.930298%)" stop-opacity="1"/>
<stop offset="0.515625" stop-color="rgb(13.757324%, 12.187195%, 12.57782%)" stop-opacity="1"/>
<stop offset="0.53125" stop-color="rgb(13.743591%, 12.173462%, 12.564087%)" stop-opacity="1"/>
<stop offset="0.5625" stop-color="rgb(13.729858%, 12.159729%, 12.548828%)" stop-opacity="1"/>
<stop offset="0.625" stop-color="rgb(13.729858%, 12.159729%, 12.548828%)" stop-opacity="1"/>
<stop offset="0.75" stop-color="rgb(13.729858%, 12.159729%, 12.548828%)" stop-opacity="1"/>
<stop offset="1" stop-color="rgb(13.729858%, 12.159729%, 12.548828%)" stop-opacity="1"/>
</linearGradient>
<clipPath id="clip-17">
<path clip-rule="nonzero" d="M 781 649 L 1008 649 L 1008 750 L 781 750 Z M 781 649 "/>
</clipPath>
<clipPath id="clip-18">
<path clip-rule="nonzero" d="M 849.007812 726.410156 L 889.78125 749.070312 L 1007.589844 649.390625 L 781.042969 699.230469 Z M 849.007812 726.410156 "/>
</clipPath>
<clipPath id="clip-19">
<path clip-rule="nonzero" d="M 849.007812 726.410156 L 889.78125 749.070312 L 1007.589844 649.390625 L 781.042969 699.230469 L 849.007812 726.410156 "/>
</clipPath>
<linearGradient id="linear-pattern-2" gradientUnits="userSpaceOnUse" x1="0.553254" y1="0" x2="1.174376" y2="0" gradientTransform="matrix(-22.6542, -199.357, 199.357, -22.6542, 916.68, 871.12)">
<stop offset="0" stop-color="rgb(48.828125%, 47.895813%, 48.127747%)" stop-opacity="1"/>
<stop offset="0.00390625" stop-color="rgb(48.655701%, 47.720337%, 47.953796%)" stop-opacity="1"/>
<stop offset="0.0078125" stop-color="rgb(48.483276%, 47.544861%, 47.77832%)" stop-opacity="1"/>
<stop offset="0.0117188" stop-color="rgb(48.312378%, 47.370911%, 47.60437%)" stop-opacity="1"/>
<stop offset="0.015625" stop-color="rgb(48.138428%, 47.193909%, 47.428894%)" stop-opacity="1"/>
<stop offset="0.0195313" stop-color="rgb(47.966003%, 47.018433%, 47.253418%)" stop-opacity="1"/>
<stop offset="0.0234375" stop-color="rgb(47.793579%, 46.842957%, 47.079468%)" stop-opacity="1"/>
<stop offset="0.0273438" stop-color="rgb(47.619629%, 46.665955%, 46.902466%)" stop-opacity="1"/>
<stop offset="0.03125" stop-color="rgb(47.445679%, 46.488953%, 46.72699%)" stop-opacity="1"/>
<stop offset="0.0351563" stop-color="rgb(47.271729%, 46.311951%, 46.549988%)" stop-opacity="1"/>
<stop offset="0.0390625" stop-color="rgb(47.097778%, 46.134949%, 46.374512%)" stop-opacity="1"/>
<stop offset="0.0429687" stop-color="rgb(46.923828%, 45.957947%, 46.199036%)" stop-opacity="1"/>
<stop offset="0.046875" stop-color="rgb(46.749878%, 45.780945%, 46.022034%)" stop-opacity="1"/>
<stop offset="0.0507813" stop-color="rgb(46.575928%, 45.603943%, 45.845032%)" stop-opacity="1"/>
<stop offset="0.0546875" stop-color="rgb(46.400452%, 45.425415%, 45.666504%)" stop-opacity="1"/>
<stop offset="0.0585938" stop-color="rgb(46.226501%, 45.248413%, 45.491028%)" stop-opacity="1"/>
<stop offset="0.0625" stop-color="rgb(46.051025%, 45.069885%, 45.314026%)" stop-opacity="1"/>
<stop offset="0.0664063" stop-color="rgb(45.877075%, 44.891357%, 45.135498%)" stop-opacity="1"/>
<stop offset="0.0703125" stop-color="rgb(45.700073%, 44.711304%, 44.95697%)" stop-opacity="1"/>
<stop offset="0.0742188" stop-color="rgb(45.524597%, 44.534302%, 44.779968%)" stop-opacity="1"/>
<stop offset="0.078125" stop-color="rgb(45.349121%, 44.354248%, 44.60144%)" stop-opacity="1"/>
<stop offset="0.0820313" stop-color="rgb(45.172119%, 44.174194%, 44.422913%)" stop-opacity="1"/>
<stop offset="0.0859375" stop-color="rgb(44.996643%, 43.995667%, 44.244385%)" stop-opacity="1"/>
<stop offset="0.0898437" stop-color="rgb(44.821167%, 43.817139%, 44.065857%)" stop-opacity="1"/>
<stop offset="0.09375" stop-color="rgb(44.642639%, 43.635559%, 43.885803%)" stop-opacity="1"/>
<stop offset="0.0976562" stop-color="rgb(44.467163%, 43.455505%, 43.707275%)" stop-opacity="1"/>
<stop offset="0.101563" stop-color="rgb(44.288635%, 43.275452%, 43.527222%)" stop-opacity="1"/>
<stop offset="0.105469" stop-color="rgb(44.113159%, 43.095398%, 43.348694%)" stop-opacity="1"/>
<stop offset="0.109375" stop-color="rgb(43.934631%, 42.913818%, 43.16864%)" stop-opacity="1"/>
<stop offset="0.113281" stop-color="rgb(43.757629%, 42.733765%, 42.987061%)" stop-opacity="1"/>
<stop offset="0.117188" stop-color="rgb(43.579102%, 42.552185%, 42.807007%)" stop-opacity="1"/>
<stop offset="0.121094" stop-color="rgb(43.400574%, 42.370605%, 42.625427%)" stop-opacity="1"/>
<stop offset="0.125" stop-color="rgb(43.222046%, 42.189026%, 42.445374%)" stop-opacity="1"/>
<stop offset="0.128906" stop-color="rgb(43.045044%, 42.008972%, 42.26532%)" stop-opacity="1"/>
<stop offset="0.132812" stop-color="rgb(42.866516%, 41.825867%, 42.08374%)" stop-opacity="1"/>
<stop offset="0.136719" stop-color="rgb(42.686462%, 41.644287%, 41.903687%)" stop-opacity="1"/>
<stop offset="0.140625" stop-color="rgb(42.507935%, 41.461182%, 41.722107%)" stop-opacity="1"/>
<stop offset="0.144531" stop-color="rgb(42.329407%, 41.279602%, 41.540527%)" stop-opacity="1"/>
<stop offset="0.148438" stop-color="rgb(42.147827%, 41.094971%, 41.357422%)" stop-opacity="1"/>
<stop offset="0.152344" stop-color="rgb(41.967773%, 40.911865%, 41.174316%)" stop-opacity="1"/>
<stop offset="0.15625" stop-color="rgb(41.789246%, 40.730286%, 40.992737%)" stop-opacity="1"/>
<stop offset="0.160156" stop-color="rgb(41.609192%, 40.545654%, 40.809631%)" stop-opacity="1"/>
<stop offset="0.164063" stop-color="rgb(41.429138%, 40.362549%, 40.626526%)" stop-opacity="1"/>
<stop offset="0.167969" stop-color="rgb(41.247559%, 40.179443%, 40.444946%)" stop-opacity="1"/>
<stop offset="0.171875" stop-color="rgb(41.065979%, 39.993286%, 40.260315%)" stop-opacity="1"/>
<stop offset="0.175781" stop-color="rgb(40.885925%, 39.810181%, 40.077209%)" stop-opacity="1"/>
<stop offset="0.179687" stop-color="rgb(40.705872%, 39.627075%, 39.894104%)" stop-opacity="1"/>
<stop offset="0.183594" stop-color="rgb(40.524292%, 39.442444%, 39.710999%)" stop-opacity="1"/>
<stop offset="0.1875" stop-color="rgb(40.342712%, 39.256287%, 39.526367%)" stop-opacity="1"/>
<stop offset="0.191406" stop-color="rgb(40.161133%, 39.071655%, 39.341736%)" stop-opacity="1"/>
<stop offset="0.195312" stop-color="rgb(39.979553%, 38.887024%, 39.15863%)" stop-opacity="1"/>
<stop offset="0.199219" stop-color="rgb(39.796448%, 38.700867%, 38.972473%)" stop-opacity="1"/>
<stop offset="0.203125" stop-color="rgb(39.614868%, 38.516235%, 38.787842%)" stop-opacity="1"/>
<stop offset="0.207031" stop-color="rgb(39.431763%, 38.328552%, 38.60321%)" stop-opacity="1"/>
<stop offset="0.210938" stop-color="rgb(39.248657%, 38.142395%, 38.417053%)" stop-opacity="1"/>
<stop offset="0.214844" stop-color="rgb(39.065552%, 37.956238%, 38.232422%)" stop-opacity="1"/>
<stop offset="0.21875" stop-color="rgb(38.882446%, 37.770081%, 38.046265%)" stop-opacity="1"/>
<stop offset="0.222656" stop-color="rgb(38.699341%, 37.583923%, 37.861633%)" stop-opacity="1"/>
<stop offset="0.226562" stop-color="rgb(38.514709%, 37.39624%, 37.67395%)" stop-opacity="1"/>
<stop offset="0.230469" stop-color="rgb(38.331604%, 37.208557%, 37.487793%)" stop-opacity="1"/>
<stop offset="0.234375" stop-color="rgb(38.148499%, 37.0224%, 37.301636%)" stop-opacity="1"/>
<stop offset="0.238281" stop-color="rgb(37.963867%, 36.834717%, 37.115479%)" stop-opacity="1"/>
<stop offset="0.242187" stop-color="rgb(37.779236%, 36.647034%, 36.927795%)" stop-opacity="1"/>
<stop offset="0.246094" stop-color="rgb(37.594604%, 36.459351%, 36.741638%)" stop-opacity="1"/>
<stop offset="0.25" stop-color="rgb(37.409973%, 36.270142%, 36.553955%)" stop-opacity="1"/>
<stop offset="0.253906" stop-color="rgb(37.225342%, 36.082458%, 36.366272%)" stop-opacity="1"/>
<stop offset="0.257813" stop-color="rgb(37.039185%, 35.89325%, 36.178589%)" stop-opacity="1"/>
<stop offset="0.261719" stop-color="rgb(36.854553%, 35.705566%, 35.990906%)" stop-opacity="1"/>
<stop offset="0.265625" stop-color="rgb(36.668396%, 35.516357%, 35.801697%)" stop-opacity="1"/>
<stop offset="0.269531" stop-color="rgb(36.482239%, 35.327148%, 35.614014%)" stop-opacity="1"/>
<stop offset="0.273438" stop-color="rgb(36.297607%, 35.137939%, 35.426331%)" stop-opacity="1"/>
<stop offset="0.277344" stop-color="rgb(36.109924%, 34.947205%, 35.237122%)" stop-opacity="1"/>
<stop offset="0.28125" stop-color="rgb(35.925293%, 34.757996%, 35.047913%)" stop-opacity="1"/>
<stop offset="0.285156" stop-color="rgb(35.73761%, 34.568787%, 34.858704%)" stop-opacity="1"/>
<stop offset="0.289062" stop-color="rgb(35.551453%, 34.378052%, 34.669495%)" stop-opacity="1"/>
<stop offset="0.292969" stop-color="rgb(35.36377%, 34.187317%, 34.480286%)" stop-opacity="1"/>
<stop offset="0.296875" stop-color="rgb(35.177612%, 33.996582%, 34.289551%)" stop-opacity="1"/>
<stop offset="0.300781" stop-color="rgb(34.988403%, 33.805847%, 34.098816%)" stop-opacity="1"/>
<stop offset="0.304688" stop-color="rgb(34.80072%, 33.613586%, 33.908081%)" stop-opacity="1"/>
<stop offset="0.308594" stop-color="rgb(34.613037%, 33.422852%, 33.718872%)" stop-opacity="1"/>
<stop offset="0.3125" stop-color="rgb(34.425354%, 33.232117%, 33.528137%)" stop-opacity="1"/>
<stop offset="0.316406" stop-color="rgb(34.237671%, 33.039856%, 33.337402%)" stop-opacity="1"/>
<stop offset="0.320313" stop-color="rgb(34.048462%, 32.849121%, 33.146667%)" stop-opacity="1"/>
<stop offset="0.324219" stop-color="rgb(33.860779%, 32.65686%, 32.955933%)" stop-opacity="1"/>
<stop offset="0.328125" stop-color="rgb(33.67157%, 32.4646%, 32.763672%)" stop-opacity="1"/>
<stop offset="0.332031" stop-color="rgb(33.480835%, 32.270813%, 32.571411%)" stop-opacity="1"/>
<stop offset="0.335937" stop-color="rgb(33.293152%, 32.078552%, 32.380676%)" stop-opacity="1"/>
<stop offset="0.339844" stop-color="rgb(33.103943%, 31.886292%, 32.188416%)" stop-opacity="1"/>
<stop offset="0.34375" stop-color="rgb(32.913208%, 31.692505%, 31.994629%)" stop-opacity="1"/>
<stop offset="0.347656" stop-color="rgb(32.722473%, 31.498718%, 31.802368%)" stop-opacity="1"/>
<stop offset="0.351562" stop-color="rgb(32.533264%, 31.306458%, 31.611633%)" stop-opacity="1"/>
<stop offset="0.355469" stop-color="rgb(32.342529%, 31.111145%, 31.417847%)" stop-opacity="1"/>
<stop offset="0.359375" stop-color="rgb(32.15332%, 30.918884%, 31.225586%)" stop-opacity="1"/>
<stop offset="0.363281" stop-color="rgb(31.962585%, 30.723572%, 31.031799%)" stop-opacity="1"/>
<stop offset="0.367188" stop-color="rgb(31.771851%, 30.529785%, 30.838013%)" stop-opacity="1"/>
<stop offset="0.371094" stop-color="rgb(31.581116%, 30.335999%, 30.644226%)" stop-opacity="1"/>
<stop offset="0.375" stop-color="rgb(31.388855%, 30.13916%, 30.450439%)" stop-opacity="1"/>
<stop offset="0.378906" stop-color="rgb(31.19812%, 29.945374%, 30.256653%)" stop-opacity="1"/>
<stop offset="0.382812" stop-color="rgb(31.005859%, 29.750061%, 30.06134%)" stop-opacity="1"/>
<stop offset="0.386719" stop-color="rgb(30.813599%, 29.554749%, 29.867554%)" stop-opacity="1"/>
<stop offset="0.390625" stop-color="rgb(30.621338%, 29.35791%, 29.672241%)" stop-opacity="1"/>
<stop offset="0.394531" stop-color="rgb(30.429077%, 29.162598%, 29.476929%)" stop-opacity="1"/>
<stop offset="0.398438" stop-color="rgb(30.236816%, 28.967285%, 29.281616%)" stop-opacity="1"/>
<stop offset="0.402344" stop-color="rgb(30.044556%, 28.771973%, 29.08783%)" stop-opacity="1"/>
<stop offset="0.40625" stop-color="rgb(29.850769%, 28.573608%, 28.890991%)" stop-opacity="1"/>
<stop offset="0.410156" stop-color="rgb(29.656982%, 28.37677%, 28.695679%)" stop-opacity="1"/>
<stop offset="0.414063" stop-color="rgb(29.464722%, 28.181458%, 28.500366%)" stop-opacity="1"/>
<stop offset="0.417969" stop-color="rgb(29.270935%, 27.984619%, 28.303528%)" stop-opacity="1"/>
<stop offset="0.421875" stop-color="rgb(29.077148%, 27.786255%, 28.106689%)" stop-opacity="1"/>
<stop offset="0.425781" stop-color="rgb(28.883362%, 27.589417%, 27.911377%)" stop-opacity="1"/>
<stop offset="0.429687" stop-color="rgb(28.689575%, 27.392578%, 27.714539%)" stop-opacity="1"/>
<stop offset="0.433594" stop-color="rgb(28.494263%, 27.194214%, 27.5177%)" stop-opacity="1"/>
<stop offset="0.4375" stop-color="rgb(28.300476%, 26.99585%, 27.319336%)" stop-opacity="1"/>
<stop offset="0.441406" stop-color="rgb(28.105164%, 26.797485%, 27.122498%)" stop-opacity="1"/>
<stop offset="0.445312" stop-color="rgb(27.909851%, 26.599121%, 26.924133%)" stop-opacity="1"/>
<stop offset="0.449219" stop-color="rgb(27.716064%, 26.400757%, 26.727295%)" stop-opacity="1"/>
<stop offset="0.453125" stop-color="rgb(27.519226%, 26.200867%, 26.528931%)" stop-opacity="1"/>
<stop offset="0.457031" stop-color="rgb(27.325439%, 26.002502%, 26.330566%)" stop-opacity="1"/>
<stop offset="0.460938" stop-color="rgb(27.128601%, 25.802612%, 26.132202%)" stop-opacity="1"/>
<stop offset="0.464844" stop-color="rgb(26.931763%, 25.602722%, 25.933838%)" stop-opacity="1"/>
<stop offset="0.46875" stop-color="rgb(26.73645%, 25.402832%, 25.733948%)" stop-opacity="1"/>
<stop offset="0.472656" stop-color="rgb(26.539612%, 25.202942%, 25.535583%)" stop-opacity="1"/>
<stop offset="0.476562" stop-color="rgb(26.344299%, 25.003052%, 25.337219%)" stop-opacity="1"/>
<stop offset="0.480469" stop-color="rgb(26.145935%, 24.803162%, 25.135803%)" stop-opacity="1"/>
<stop offset="0.484375" stop-color="rgb(25.950623%, 24.603271%, 24.937439%)" stop-opacity="1"/>
<stop offset="0.488281" stop-color="rgb(25.752258%, 24.401855%, 24.737549%)" stop-opacity="1"/>
<stop offset="0.492187" stop-color="rgb(25.55542%, 24.200439%, 24.537659%)" stop-opacity="1"/>
<stop offset="0.496094" stop-color="rgb(25.358582%, 23.999023%, 24.337769%)" stop-opacity="1"/>
<stop offset="0.5" stop-color="rgb(25.160217%, 23.799133%, 24.136353%)" stop-opacity="1"/>
<stop offset="0.503906" stop-color="rgb(24.961853%, 23.596191%, 23.934937%)" stop-opacity="1"/>
<stop offset="0.507813" stop-color="rgb(24.763489%, 23.39325%, 23.733521%)" stop-opacity="1"/>
<stop offset="0.511719" stop-color="rgb(24.565125%, 23.193359%, 23.53363%)" stop-opacity="1"/>
<stop offset="0.515625" stop-color="rgb(24.365234%, 22.988892%, 23.330688%)" stop-opacity="1"/>
<stop offset="0.519531" stop-color="rgb(24.16687%, 22.787476%, 23.129272%)" stop-opacity="1"/>
<stop offset="0.523438" stop-color="rgb(23.968506%, 22.584534%, 22.927856%)" stop-opacity="1"/>
<stop offset="0.527344" stop-color="rgb(23.768616%, 22.380066%, 22.724915%)" stop-opacity="1"/>
<stop offset="0.53125" stop-color="rgb(23.568726%, 22.17865%, 22.523499%)" stop-opacity="1"/>
<stop offset="0.535156" stop-color="rgb(23.368835%, 21.974182%, 22.320557%)" stop-opacity="1"/>
<stop offset="0.539062" stop-color="rgb(23.168945%, 21.77124%, 22.117615%)" stop-opacity="1"/>
<stop offset="0.542969" stop-color="rgb(22.969055%, 21.566772%, 21.914673%)" stop-opacity="1"/>
<stop offset="0.546875" stop-color="rgb(22.769165%, 21.363831%, 21.711731%)" stop-opacity="1"/>
<stop offset="0.550781" stop-color="rgb(22.569275%, 21.159363%, 21.510315%)" stop-opacity="1"/>
<stop offset="0.554688" stop-color="rgb(22.367859%, 20.956421%, 21.307373%)" stop-opacity="1"/>
<stop offset="0.558594" stop-color="rgb(22.166443%, 20.750427%, 21.102905%)" stop-opacity="1"/>
<stop offset="0.5625" stop-color="rgb(21.966553%, 20.545959%, 20.898438%)" stop-opacity="1"/>
<stop offset="0.566406" stop-color="rgb(21.765137%, 20.339966%, 20.69397%)" stop-opacity="1"/>
<stop offset="0.570313" stop-color="rgb(21.563721%, 20.135498%, 20.489502%)" stop-opacity="1"/>
<stop offset="0.574219" stop-color="rgb(21.362305%, 19.93103%, 20.28656%)" stop-opacity="1"/>
<stop offset="0.578125" stop-color="rgb(21.160889%, 19.725037%, 20.082092%)" stop-opacity="1"/>
<stop offset="0.582031" stop-color="rgb(20.957947%, 19.519043%, 19.876099%)" stop-opacity="1"/>
<stop offset="0.585937" stop-color="rgb(20.756531%, 19.314575%, 19.671631%)" stop-opacity="1"/>
<stop offset="0.589844" stop-color="rgb(20.553589%, 19.107056%, 19.467163%)" stop-opacity="1"/>
<stop offset="0.59375" stop-color="rgb(20.350647%, 18.901062%, 19.261169%)" stop-opacity="1"/>
<stop offset="0.597656" stop-color="rgb(20.147705%, 18.695068%, 19.055176%)" stop-opacity="1"/>
<stop offset="0.601562" stop-color="rgb(19.943237%, 18.487549%, 18.849182%)" stop-opacity="1"/>
<stop offset="0.605469" stop-color="rgb(19.741821%, 18.280029%, 18.643188%)" stop-opacity="1"/>
<stop offset="0.609375" stop-color="rgb(19.537354%, 18.07251%, 18.437195%)" stop-opacity="1"/>
<stop offset="0.613281" stop-color="rgb(19.334412%, 17.866516%, 18.231201%)" stop-opacity="1"/>
<stop offset="0.617188" stop-color="rgb(19.129944%, 17.658997%, 18.023682%)" stop-opacity="1"/>
<stop offset="0.621094" stop-color="rgb(18.927002%, 17.451477%, 17.817688%)" stop-opacity="1"/>
<stop offset="0.625" stop-color="rgb(18.722534%, 17.242432%, 17.610168%)" stop-opacity="1"/>
<stop offset="0.628906" stop-color="rgb(18.516541%, 17.033386%, 17.402649%)" stop-opacity="1"/>
<stop offset="0.632812" stop-color="rgb(18.313599%, 16.825867%, 17.195129%)" stop-opacity="1"/>
<stop offset="0.636719" stop-color="rgb(18.107605%, 16.616821%, 16.98761%)" stop-opacity="1"/>
<stop offset="0.640625" stop-color="rgb(17.901611%, 16.407776%, 16.778564%)" stop-opacity="1"/>
<stop offset="0.644531" stop-color="rgb(17.697144%, 16.19873%, 16.571045%)" stop-opacity="1"/>
<stop offset="0.648438" stop-color="rgb(17.49115%, 15.989685%, 16.363525%)" stop-opacity="1"/>
<stop offset="0.652344" stop-color="rgb(17.286682%, 15.78064%, 16.15448%)" stop-opacity="1"/>
<stop offset="0.65625" stop-color="rgb(17.079163%, 15.570068%, 15.945435%)" stop-opacity="1"/>
<stop offset="0.660156" stop-color="rgb(16.874695%, 15.361023%, 15.736389%)" stop-opacity="1"/>
<stop offset="0.664063" stop-color="rgb(16.667175%, 15.150452%, 15.527344%)" stop-opacity="1"/>
<stop offset="0.667969" stop-color="rgb(16.461182%, 14.93988%, 15.318298%)" stop-opacity="1"/>
<stop offset="0.671875" stop-color="rgb(16.253662%, 14.729309%, 15.109253%)" stop-opacity="1"/>
<stop offset="0.675781" stop-color="rgb(16.047668%, 14.520264%, 14.898682%)" stop-opacity="1"/>
<stop offset="0.679687" stop-color="rgb(15.840149%, 14.309692%, 14.689636%)" stop-opacity="1"/>
<stop offset="0.683594" stop-color="rgb(15.632629%, 14.097595%, 14.479065%)" stop-opacity="1"/>
<stop offset="0.6875" stop-color="rgb(15.42511%, 13.885498%, 14.268494%)" stop-opacity="1"/>
<stop offset="0.691406" stop-color="rgb(15.21759%, 13.674927%, 14.057922%)" stop-opacity="1"/>
<stop offset="0.695312" stop-color="rgb(15.010071%, 13.46283%, 13.847351%)" stop-opacity="1"/>
<stop offset="0.699219" stop-color="rgb(14.801025%, 13.250732%, 13.635254%)" stop-opacity="1"/>
<stop offset="0.703125" stop-color="rgb(14.593506%, 13.038635%, 13.424683%)" stop-opacity="1"/>
<stop offset="0.707031" stop-color="rgb(14.38446%, 12.826538%, 13.214111%)" stop-opacity="1"/>
<stop offset="0.710938" stop-color="rgb(14.175415%, 12.612915%, 13.002014%)" stop-opacity="1"/>
<stop offset="0.714844" stop-color="rgb(13.96637%, 12.400818%, 12.789917%)" stop-opacity="1"/>
<stop offset="0.71875" stop-color="rgb(13.757324%, 12.187195%, 12.57782%)" stop-opacity="1"/>
<stop offset="0.75" stop-color="rgb(13.743591%, 12.173462%, 12.564087%)" stop-opacity="1"/>
<stop offset="1" stop-color="rgb(13.729858%, 12.159729%, 12.548828%)" stop-opacity="1"/>
</linearGradient>
<clipPath id="clip-20">
<path clip-rule="nonzero" d="M 426 928 L 519 928 L 519 1305 L 426 1305 Z M 426 928 "/>
</clipPath>
<clipPath id="clip-21">
<path clip-rule="nonzero" d="M 427.636719 928.789062 C 427.636719 928.789062 418.578125 1073.78125 450.292969 1173.460938 C 482.007812 1273.140625 504.664062 1304.851562 504.664062 1304.851562 L 518.253906 1209.699219 C 518.253906 1209.699219 463.886719 1141.738281 463.886719 1055.660156 C 463.886719 969.570312 456.335938 942.378906 456.335938 942.378906 Z M 427.636719 928.789062 "/>
</clipPath>
<clipPath id="clip-22">
<path clip-rule="nonzero" d="M 427.636719 928.789062 C 427.636719 928.789062 418.578125 1073.78125 450.292969 1173.460938 C 482.007812 1273.140625 504.664062 1304.851562 504.664062 1304.851562 L 518.253906 1209.699219 C 518.253906 1209.699219 463.886719 1141.738281 463.886719 1055.660156 C 463.886719 969.570312 456.335938 942.378906 456.335938 942.378906 L 427.636719 928.789062 "/>
</clipPath>
<linearGradient id="linear-pattern-3" gradientUnits="userSpaceOnUse" x1="0.803755" y1="0" x2="1.055912" y2="0" gradientTransform="matrix(521.046, -45.3084, 45.3084, 521.046, -16.0863, 1158.76)">
<stop offset="0" stop-color="rgb(29.997253%, 28.723145%, 29.039001%)" stop-opacity="1"/>
<stop offset="0.0078125" stop-color="rgb(29.919434%, 28.643799%, 28.961182%)" stop-opacity="1"/>
<stop offset="0.015625" stop-color="rgb(29.760742%, 28.483582%, 28.800964%)" stop-opacity="1"/>
<stop offset="0.0234375" stop-color="rgb(29.605103%, 28.323364%, 28.642273%)" stop-opacity="1"/>
<stop offset="0.03125" stop-color="rgb(29.447937%, 28.164673%, 28.483582%)" stop-opacity="1"/>
<stop offset="0.0390625" stop-color="rgb(29.290771%, 28.00293%, 28.323364%)" stop-opacity="1"/>
<stop offset="0.046875" stop-color="rgb(29.133606%, 27.844238%, 28.164673%)" stop-opacity="1"/>
<stop offset="0.0546875" stop-color="rgb(28.974915%, 27.682495%, 28.004456%)" stop-opacity="1"/>
<stop offset="0.0625" stop-color="rgb(28.819275%, 27.523804%, 27.845764%)" stop-opacity="1"/>
<stop offset="0.0703125" stop-color="rgb(28.660583%, 27.362061%, 27.685547%)" stop-opacity="1"/>
<stop offset="0.078125" stop-color="rgb(28.504944%, 27.203369%, 27.526855%)" stop-opacity="1"/>
<stop offset="0.0859375" stop-color="rgb(28.344727%, 27.041626%, 27.365112%)" stop-opacity="1"/>
<stop offset="0.09375" stop-color="rgb(28.187561%, 26.879883%, 27.204895%)" stop-opacity="1"/>
<stop offset="0.101563" stop-color="rgb(28.02887%, 26.719666%, 27.044678%)" stop-opacity="1"/>
<stop offset="0.109375" stop-color="rgb(27.870178%, 26.556396%, 26.882935%)" stop-opacity="1"/>
<stop offset="0.117187" stop-color="rgb(27.711487%, 26.396179%, 26.722717%)" stop-opacity="1"/>
<stop offset="0.125" stop-color="rgb(27.554321%, 26.235962%, 26.5625%)" stop-opacity="1"/>
<stop offset="0.132813" stop-color="rgb(27.394104%, 26.072693%, 26.400757%)" stop-opacity="1"/>
<stop offset="0.140625" stop-color="rgb(27.236938%, 25.912476%, 26.242065%)" stop-opacity="1"/>
<stop offset="0.148437" stop-color="rgb(27.076721%, 25.750732%, 26.080322%)" stop-opacity="1"/>
<stop offset="0.15625" stop-color="rgb(26.91803%, 25.587463%, 25.918579%)" stop-opacity="1"/>
<stop offset="0.164062" stop-color="rgb(26.757812%, 25.424194%, 25.75531%)" stop-opacity="1"/>
<stop offset="0.171875" stop-color="rgb(26.597595%, 25.262451%, 25.593567%)" stop-opacity="1"/>
<stop offset="0.179688" stop-color="rgb(26.438904%, 25.100708%, 25.43335%)" stop-opacity="1"/>
<stop offset="0.1875" stop-color="rgb(26.280212%, 24.937439%, 25.271606%)" stop-opacity="1"/>
<stop offset="0.195312" stop-color="rgb(26.119995%, 24.775696%, 25.109863%)" stop-opacity="1"/>
<stop offset="0.203125" stop-color="rgb(25.959778%, 24.612427%, 24.946594%)" stop-opacity="1"/>
<stop offset="0.210937" stop-color="rgb(25.801086%, 24.450684%, 24.784851%)" stop-opacity="1"/>
<stop offset="0.21875" stop-color="rgb(25.639343%, 24.285889%, 24.623108%)" stop-opacity="1"/>
<stop offset="0.226562" stop-color="rgb(25.479126%, 24.12262%, 24.459839%)" stop-opacity="1"/>
<stop offset="0.234375" stop-color="rgb(25.318909%, 23.959351%, 24.29657%)" stop-opacity="1"/>
<stop offset="0.242187" stop-color="rgb(25.158691%, 23.796082%, 24.134827%)" stop-opacity="1"/>
<stop offset="0.25" stop-color="rgb(24.996948%, 23.632812%, 23.971558%)" stop-opacity="1"/>
<stop offset="0.257812" stop-color="rgb(24.835205%, 23.468018%, 23.806763%)" stop-opacity="1"/>
<stop offset="0.265625" stop-color="rgb(24.674988%, 23.304749%, 23.64502%)" stop-opacity="1"/>
<stop offset="0.273437" stop-color="rgb(24.514771%, 23.139954%, 23.48175%)" stop-opacity="1"/>
<stop offset="0.28125" stop-color="rgb(24.353027%, 22.975159%, 23.316956%)" stop-opacity="1"/>
<stop offset="0.289063" stop-color="rgb(24.191284%, 22.81189%, 23.155212%)" stop-opacity="1"/>
<stop offset="0.296875" stop-color="rgb(24.029541%, 22.647095%, 22.990417%)" stop-opacity="1"/>
<stop offset="0.304687" stop-color="rgb(23.867798%, 22.4823%, 22.827148%)" stop-opacity="1"/>
<stop offset="0.3125" stop-color="rgb(23.706055%, 22.317505%, 22.662354%)" stop-opacity="1"/>
<stop offset="0.320313" stop-color="rgb(23.544312%, 22.15271%, 22.499084%)" stop-opacity="1"/>
<stop offset="0.328125" stop-color="rgb(23.382568%, 21.987915%, 22.33429%)" stop-opacity="1"/>
<stop offset="0.335937" stop-color="rgb(23.220825%, 21.82312%, 22.169495%)" stop-opacity="1"/>
<stop offset="0.34375" stop-color="rgb(23.057556%, 21.656799%, 22.0047%)" stop-opacity="1"/>
<stop offset="0.351562" stop-color="rgb(22.895813%, 21.492004%, 21.839905%)" stop-opacity="1"/>
<stop offset="0.359375" stop-color="rgb(22.732544%, 21.327209%, 21.676636%)" stop-opacity="1"/>
<stop offset="0.367187" stop-color="rgb(22.569275%, 21.160889%, 21.510315%)" stop-opacity="1"/>
<stop offset="0.375" stop-color="rgb(22.407532%, 20.994568%, 21.34552%)" stop-opacity="1"/>
<stop offset="0.382812" stop-color="rgb(22.244263%, 20.829773%, 21.180725%)" stop-opacity="1"/>
<stop offset="0.390625" stop-color="rgb(22.080994%, 20.661926%, 21.014404%)" stop-opacity="1"/>
<stop offset="0.398437" stop-color="rgb(21.917725%, 20.495605%, 20.849609%)" stop-opacity="1"/>
<stop offset="0.40625" stop-color="rgb(21.754456%, 20.329285%, 20.683289%)" stop-opacity="1"/>
<stop offset="0.414062" stop-color="rgb(21.591187%, 20.162964%, 20.518494%)" stop-opacity="1"/>
<stop offset="0.421875" stop-color="rgb(21.426392%, 19.996643%, 20.350647%)" stop-opacity="1"/>
<stop offset="0.429687" stop-color="rgb(21.263123%, 19.830322%, 20.185852%)" stop-opacity="1"/>
<stop offset="0.4375" stop-color="rgb(21.098328%, 19.662476%, 20.019531%)" stop-opacity="1"/>
<stop offset="0.445312" stop-color="rgb(20.935059%, 19.496155%, 19.85321%)" stop-opacity="1"/>
<stop offset="0.453125" stop-color="rgb(20.770264%, 19.328308%, 19.68689%)" stop-opacity="1"/>
<stop offset="0.460937" stop-color="rgb(20.605469%, 19.160461%, 19.520569%)" stop-opacity="1"/>
<stop offset="0.46875" stop-color="rgb(20.4422%, 18.994141%, 19.352722%)" stop-opacity="1"/>
<stop offset="0.476562" stop-color="rgb(20.277405%, 18.826294%, 19.186401%)" stop-opacity="1"/>
<stop offset="0.484375" stop-color="rgb(20.11261%, 18.658447%, 19.020081%)" stop-opacity="1"/>
<stop offset="0.492187" stop-color="rgb(19.946289%, 18.489075%, 18.852234%)" stop-opacity="1"/>
<stop offset="0.5" stop-color="rgb(19.781494%, 18.322754%, 18.684387%)" stop-opacity="1"/>
<stop offset="0.507812" stop-color="rgb(19.616699%, 18.154907%, 18.518066%)" stop-opacity="1"/>
<stop offset="0.515625" stop-color="rgb(19.451904%, 17.985535%, 18.35022%)" stop-opacity="1"/>
<stop offset="0.523437" stop-color="rgb(19.287109%, 17.817688%, 18.182373%)" stop-opacity="1"/>
<stop offset="0.53125" stop-color="rgb(19.120789%, 17.649841%, 18.014526%)" stop-opacity="1"/>
<stop offset="0.539063" stop-color="rgb(18.954468%, 17.478943%, 17.845154%)" stop-opacity="1"/>
<stop offset="0.546875" stop-color="rgb(18.788147%, 17.30957%, 17.677307%)" stop-opacity="1"/>
<stop offset="0.554687" stop-color="rgb(18.621826%, 17.141724%, 17.50946%)" stop-opacity="1"/>
<stop offset="0.5625" stop-color="rgb(18.457031%, 16.972351%, 17.341614%)" stop-opacity="1"/>
<stop offset="0.570313" stop-color="rgb(18.29071%, 16.802979%, 17.172241%)" stop-opacity="1"/>
<stop offset="0.578125" stop-color="rgb(18.12439%, 16.635132%, 17.004395%)" stop-opacity="1"/>
<stop offset="0.585937" stop-color="rgb(17.958069%, 16.464233%, 16.835022%)" stop-opacity="1"/>
<stop offset="0.59375" stop-color="rgb(17.791748%, 16.294861%, 16.667175%)" stop-opacity="1"/>
<stop offset="0.601562" stop-color="rgb(17.623901%, 16.125488%, 16.497803%)" stop-opacity="1"/>
<stop offset="0.609375" stop-color="rgb(17.457581%, 15.95459%, 16.32843%)" stop-opacity="1"/>
<stop offset="0.617188" stop-color="rgb(17.289734%, 15.785217%, 16.159058%)" stop-opacity="1"/>
<stop offset="0.625" stop-color="rgb(17.123413%, 15.614319%, 15.989685%)" stop-opacity="1"/>
<stop offset="0.632812" stop-color="rgb(16.955566%, 15.444946%, 15.820312%)" stop-opacity="1"/>
<stop offset="0.640625" stop-color="rgb(16.789246%, 15.274048%, 15.65094%)" stop-opacity="1"/>
<stop offset="0.648437" stop-color="rgb(16.621399%, 15.104675%, 15.481567%)" stop-opacity="1"/>
<stop offset="0.65625" stop-color="rgb(16.453552%, 14.932251%, 15.310669%)" stop-opacity="1"/>
<stop offset="0.664062" stop-color="rgb(16.285706%, 14.762878%, 15.141296%)" stop-opacity="1"/>
<stop offset="0.671875" stop-color="rgb(16.117859%, 14.590454%, 14.970398%)" stop-opacity="1"/>
<stop offset="0.679687" stop-color="rgb(15.950012%, 14.421082%, 14.801025%)" stop-opacity="1"/>
<stop offset="0.6875" stop-color="rgb(15.78064%, 14.247131%, 14.628601%)" stop-opacity="1"/>
<stop offset="0.695312" stop-color="rgb(15.611267%, 14.076233%, 14.457703%)" stop-opacity="1"/>
<stop offset="0.703125" stop-color="rgb(15.44342%, 13.903809%, 14.286804%)" stop-opacity="1"/>
<stop offset="0.710937" stop-color="rgb(15.274048%, 13.73291%, 14.115906%)" stop-opacity="1"/>
<stop offset="0.71875" stop-color="rgb(15.106201%, 13.560486%, 13.945007%)" stop-opacity="1"/>
<stop offset="0.726562" stop-color="rgb(14.938354%, 13.389587%, 13.774109%)" stop-opacity="1"/>
<stop offset="0.734375" stop-color="rgb(14.768982%, 13.218689%, 13.60321%)" stop-opacity="1"/>
<stop offset="0.742187" stop-color="rgb(14.599609%, 13.044739%, 13.430786%)" stop-opacity="1"/>
<stop offset="0.75" stop-color="rgb(14.430237%, 12.87384%, 13.259888%)" stop-opacity="1"/>
<stop offset="0.757813" stop-color="rgb(14.260864%, 12.701416%, 13.088989%)" stop-opacity="1"/>
<stop offset="0.765625" stop-color="rgb(14.089966%, 12.52594%, 12.915039%)" stop-opacity="1"/>
<stop offset="0.78125" stop-color="rgb(13.867188%, 12.30011%, 12.689209%)" stop-opacity="1"/>
<stop offset="0.8125" stop-color="rgb(13.729858%, 12.159729%, 12.548828%)" stop-opacity="1"/>
<stop offset="0.875" stop-color="rgb(13.729858%, 12.159729%, 12.548828%)" stop-opacity="1"/>
<stop offset="1" stop-color="rgb(13.729858%, 12.159729%, 12.548828%)" stop-opacity="1"/>
</linearGradient>
<clipPath id="clip-23">
<path clip-rule="nonzero" d="M 1560 1386 L 1833 1386 L 1833 1627 L 1560 1627 Z M 1560 1386 "/>
</clipPath>
<clipPath id="clip-24">
<path clip-rule="nonzero" d="M 1560.351562 1626.539062 C 1560.351562 1626.539062 1723.460938 1522.328125 1777.828125 1463.429688 C 1832.199219 1404.53125 1832.199219 1386.410156 1832.199219 1386.410156 C 1832.199219 1386.410156 1768.769531 1554.050781 1560.351562 1626.539062 Z M 1560.351562 1626.539062 "/>
</clipPath>
<clipPath id="clip-25">
<path clip-rule="nonzero" d="M 1560.351562 1626.539062 C 1560.351562 1626.539062 1723.460938 1522.328125 1777.828125 1463.429688 C 1832.199219 1404.53125 1832.199219 1386.410156 1832.199219 1386.410156 C 1832.199219 1386.410156 1768.769531 1554.050781 1560.351562 1626.539062 "/>
</clipPath>
<linearGradient id="linear-pattern-4" gradientUnits="userSpaceOnUse" x1="0.865715" y1="0" x2="1.009384" y2="0" gradientTransform="matrix(2022.24, -175.847, 175.847, 2022.24, -199.675, 1671.34)">
<stop offset="0" stop-color="rgb(25.004578%, 23.638916%, 23.977661%)" stop-opacity="1"/>
<stop offset="0.015625" stop-color="rgb(24.913025%, 23.547363%, 23.886108%)" stop-opacity="1"/>
<stop offset="0.03125" stop-color="rgb(24.729919%, 23.35968%, 23.699951%)" stop-opacity="1"/>
<stop offset="0.046875" stop-color="rgb(24.546814%, 23.173523%, 23.51532%)" stop-opacity="1"/>
<stop offset="0.0625" stop-color="rgb(24.362183%, 22.98584%, 23.327637%)" stop-opacity="1"/>
<stop offset="0.078125" stop-color="rgb(24.177551%, 22.798157%, 23.139954%)" stop-opacity="1"/>
<stop offset="0.09375" stop-color="rgb(23.994446%, 22.610474%, 22.955322%)" stop-opacity="1"/>
<stop offset="0.109375" stop-color="rgb(23.809814%, 22.422791%, 22.767639%)" stop-opacity="1"/>
<stop offset="0.125" stop-color="rgb(23.625183%, 22.235107%, 22.581482%)" stop-opacity="1"/>
<stop offset="0.140625" stop-color="rgb(23.442078%, 22.04895%, 22.393799%)" stop-opacity="1"/>
<stop offset="0.15625" stop-color="rgb(23.25592%, 21.859741%, 22.206116%)" stop-opacity="1"/>
<stop offset="0.171875" stop-color="rgb(23.071289%, 21.672058%, 22.019958%)" stop-opacity="1"/>
<stop offset="0.1875" stop-color="rgb(22.886658%, 21.482849%, 21.83075%)" stop-opacity="1"/>
<stop offset="0.203125" stop-color="rgb(22.7005%, 21.29364%, 21.643066%)" stop-opacity="1"/>
<stop offset="0.21875" stop-color="rgb(22.514343%, 21.104431%, 21.455383%)" stop-opacity="1"/>
<stop offset="0.234375" stop-color="rgb(22.329712%, 20.915222%, 21.266174%)" stop-opacity="1"/>
<stop offset="0.25" stop-color="rgb(22.143555%, 20.726013%, 21.078491%)" stop-opacity="1"/>
<stop offset="0.265625" stop-color="rgb(21.957397%, 20.53833%, 20.890808%)" stop-opacity="1"/>
<stop offset="0.28125" stop-color="rgb(21.77124%, 20.347595%, 20.701599%)" stop-opacity="1"/>
<stop offset="0.296875" stop-color="rgb(21.585083%, 20.158386%, 20.51239%)" stop-opacity="1"/>
<stop offset="0.3125" stop-color="rgb(21.398926%, 19.967651%, 20.323181%)" stop-opacity="1"/>
<stop offset="0.328125" stop-color="rgb(21.211243%, 19.778442%, 20.133972%)" stop-opacity="1"/>
<stop offset="0.34375" stop-color="rgb(21.025085%, 19.587708%, 19.944763%)" stop-opacity="1"/>
<stop offset="0.359375" stop-color="rgb(20.837402%, 19.396973%, 19.755554%)" stop-opacity="1"/>
<stop offset="0.375" stop-color="rgb(20.649719%, 19.204712%, 19.564819%)" stop-opacity="1"/>
<stop offset="0.390625" stop-color="rgb(20.462036%, 19.015503%, 19.374084%)" stop-opacity="1"/>
<stop offset="0.40625" stop-color="rgb(20.275879%, 18.824768%, 19.184875%)" stop-opacity="1"/>
<stop offset="0.421875" stop-color="rgb(20.08667%, 18.632507%, 18.994141%)" stop-opacity="1"/>
<stop offset="0.4375" stop-color="rgb(19.900513%, 18.443298%, 18.804932%)" stop-opacity="1"/>
<stop offset="0.453125" stop-color="rgb(19.71283%, 18.251038%, 18.614197%)" stop-opacity="1"/>
<stop offset="0.46875" stop-color="rgb(19.522095%, 18.058777%, 18.421936%)" stop-opacity="1"/>
<stop offset="0.484375" stop-color="rgb(19.334412%, 17.866516%, 18.231201%)" stop-opacity="1"/>
<stop offset="0.5" stop-color="rgb(19.146729%, 17.674255%, 18.040466%)" stop-opacity="1"/>
<stop offset="0.515625" stop-color="rgb(18.955994%, 17.481995%, 17.848206%)" stop-opacity="1"/>
<stop offset="0.53125" stop-color="rgb(18.768311%, 17.289734%, 17.657471%)" stop-opacity="1"/>
<stop offset="0.546875" stop-color="rgb(18.579102%, 17.097473%, 17.46521%)" stop-opacity="1"/>
<stop offset="0.5625" stop-color="rgb(18.389893%, 16.903687%, 17.272949%)" stop-opacity="1"/>
<stop offset="0.578125" stop-color="rgb(18.200684%, 16.712952%, 17.082214%)" stop-opacity="1"/>
<stop offset="0.59375" stop-color="rgb(18.011475%, 16.519165%, 16.889954%)" stop-opacity="1"/>
<stop offset="0.609375" stop-color="rgb(17.819214%, 16.323853%, 16.694641%)" stop-opacity="1"/>
<stop offset="0.625" stop-color="rgb(17.630005%, 16.130066%, 16.50238%)" stop-opacity="1"/>
<stop offset="0.640625" stop-color="rgb(17.440796%, 15.937805%, 16.31012%)" stop-opacity="1"/>
<stop offset="0.65625" stop-color="rgb(17.248535%, 15.744019%, 16.117859%)" stop-opacity="1"/>
<stop offset="0.671875" stop-color="rgb(17.059326%, 15.550232%, 15.925598%)" stop-opacity="1"/>
<stop offset="0.6875" stop-color="rgb(16.868591%, 15.356445%, 15.731812%)" stop-opacity="1"/>
<stop offset="0.703125" stop-color="rgb(16.677856%, 15.161133%, 15.538025%)" stop-opacity="1"/>
<stop offset="0.71875" stop-color="rgb(16.487122%, 14.967346%, 15.344238%)" stop-opacity="1"/>
<stop offset="0.734375" stop-color="rgb(16.294861%, 14.772034%, 15.150452%)" stop-opacity="1"/>
<stop offset="0.75" stop-color="rgb(16.1026%, 14.575195%, 14.955139%)" stop-opacity="1"/>
<stop offset="0.765625" stop-color="rgb(15.911865%, 14.381409%, 14.761353%)" stop-opacity="1"/>
<stop offset="0.78125" stop-color="rgb(15.719604%, 14.186096%, 14.567566%)" stop-opacity="1"/>
<stop offset="0.796875" stop-color="rgb(15.52887%, 13.990784%, 14.373779%)" stop-opacity="1"/>
<stop offset="0.8125" stop-color="rgb(15.336609%, 13.795471%, 14.178467%)" stop-opacity="1"/>
<stop offset="0.828125" stop-color="rgb(15.144348%, 13.600159%, 13.983154%)" stop-opacity="1"/>
<stop offset="0.84375" stop-color="rgb(14.952087%, 13.404846%, 13.789368%)" stop-opacity="1"/>
<stop offset="0.859375" stop-color="rgb(14.759827%, 13.208008%, 13.594055%)" stop-opacity="1"/>
<stop offset="0.875" stop-color="rgb(14.564514%, 13.009644%, 13.397217%)" stop-opacity="1"/>
<stop offset="0.890625" stop-color="rgb(14.372253%, 12.814331%, 13.201904%)" stop-opacity="1"/>
<stop offset="0.90625" stop-color="rgb(14.179993%, 12.617493%, 13.005066%)" stop-opacity="1"/>
<stop offset="0.921875" stop-color="rgb(13.986206%, 12.420654%, 12.809753%)" stop-opacity="1"/>
<stop offset="0.9375" stop-color="rgb(13.809204%, 12.240601%, 12.631226%)" stop-opacity="1"/>
<stop offset="1" stop-color="rgb(13.729858%, 12.159729%, 12.548828%)" stop-opacity="1"/>
</linearGradient>
<clipPath id="clip-26">
<path clip-rule="nonzero" d="M 834 1349 L 1147 1349 L 1147 1548 L 834 1548 Z M 834 1349 "/>
</clipPath>
<clipPath id="clip-27">
<path clip-rule="nonzero" d="M 834.441406 1349.289062 C 834.441406 1349.289062 978.414062 1541.25 1014.410156 1547.25 C 1050.398438 1553.25 1146.378906 1511.25 1146.378906 1511.25 C 1146.378906 1511.25 1056.398438 1529.25 1008.410156 1505.261719 C 960.417969 1481.261719 834.441406 1349.289062 834.441406 1349.289062 Z M 834.441406 1349.289062 "/>
</clipPath>
<clipPath id="clip-28">
<path clip-rule="nonzero" d="M 834.441406 1349.289062 C 834.441406 1349.289062 978.414062 1541.25 1014.410156 1547.25 C 1050.398438 1553.25 1146.378906 1511.25 1146.378906 1511.25 C 1146.378906 1511.25 1056.398438 1529.25 1008.410156 1505.261719 C 960.417969 1481.261719 834.441406 1349.289062 834.441406 1349.289062 "/>
</clipPath>
<linearGradient id="linear-pattern-5" gradientUnits="userSpaceOnUse" x1="-0.780602" y1="0" x2="4.017965" y2="0" gradientTransform="matrix(30.003, -60.006, 60.006, 30.003, 933.445, 1544.2)">
<stop offset="0" stop-color="rgb(82.745361%, 82.43103%, 82.50885%)" stop-opacity="1"/>
<stop offset="0.125" stop-color="rgb(82.745361%, 82.43103%, 82.50885%)" stop-opacity="1"/>
<stop offset="0.15625" stop-color="rgb(82.745361%, 82.43103%, 82.50885%)" stop-opacity="1"/>
<stop offset="0.160156" stop-color="rgb(82.574463%, 82.25708%, 82.336426%)" stop-opacity="1"/>
<stop offset="0.164062" stop-color="rgb(82.402039%, 82.081604%, 82.16095%)" stop-opacity="1"/>
<stop offset="0.167969" stop-color="rgb(81.422424%, 81.085205%, 81.169128%)" stop-opacity="1"/>
<stop offset="0.171875" stop-color="rgb(80.429077%, 80.072021%, 80.160522%)" stop-opacity="1"/>
<stop offset="0.175781" stop-color="rgb(79.423523%, 79.049683%, 79.142761%)" stop-opacity="1"/>
<stop offset="0.179688" stop-color="rgb(78.407288%, 78.013611%, 78.111267%)" stop-opacity="1"/>
<stop offset="0.183594" stop-color="rgb(77.378845%, 76.966858%, 77.069092%)" stop-opacity="1"/>
<stop offset="0.1875" stop-color="rgb(76.33667%, 75.904846%, 76.011658%)" stop-opacity="1"/>
<stop offset="0.191406" stop-color="rgb(75.282288%, 74.833679%, 74.945068%)" stop-opacity="1"/>
<stop offset="0.195312" stop-color="rgb(74.217224%, 73.747253%, 73.864746%)" stop-opacity="1"/>
<stop offset="0.199219" stop-color="rgb(73.139954%, 72.651672%, 72.772217%)" stop-opacity="1"/>
<stop offset="0.203125" stop-color="rgb(72.050476%, 71.540833%, 71.66748%)" stop-opacity="1"/>
<stop offset="0.207031" stop-color="rgb(70.948792%, 70.420837%, 70.552063%)" stop-opacity="1"/>
<stop offset="0.210938" stop-color="rgb(69.8349%, 69.285583%, 69.421387%)" stop-opacity="1"/>
<stop offset="0.214844" stop-color="rgb(68.708801%, 68.139648%, 68.281555%)" stop-opacity="1"/>
<stop offset="0.21875" stop-color="rgb(67.56897%, 66.978455%, 67.124939%)" stop-opacity="1"/>
<stop offset="0.222656" stop-color="rgb(66.419983%, 65.808105%, 65.960693%)" stop-opacity="1"/>
<stop offset="0.226562" stop-color="rgb(65.257263%, 64.625549%, 64.782715%)" stop-opacity="1"/>
<stop offset="0.230469" stop-color="rgb(64.083862%, 63.42926%, 63.592529%)" stop-opacity="1"/>
<stop offset="0.234375" stop-color="rgb(62.895203%, 62.219238%, 62.387085%)" stop-opacity="1"/>
<stop offset="0.238281" stop-color="rgb(61.697388%, 61.000061%, 61.172485%)" stop-opacity="1"/>
<stop offset="0.242187" stop-color="rgb(60.484314%, 59.765625%, 59.944153%)" stop-opacity="1"/>
<stop offset="0.246094" stop-color="rgb(59.262085%, 58.522034%, 58.705139%)" stop-opacity="1"/>
<stop offset="0.25" stop-color="rgb(58.026123%, 57.263184%, 57.452393%)" stop-opacity="1"/>
<stop offset="0.253906" stop-color="rgb(56.77948%, 55.993652%, 56.188965%)" stop-opacity="1"/>
<stop offset="0.257813" stop-color="rgb(55.519104%, 54.708862%, 54.910278%)" stop-opacity="1"/>
<stop offset="0.261719" stop-color="rgb(54.248047%, 53.416443%, 53.622437%)" stop-opacity="1"/>
<stop offset="0.265625" stop-color="rgb(52.964783%, 52.108765%, 52.322388%)" stop-opacity="1"/>
<stop offset="0.269531" stop-color="rgb(51.669312%, 50.790405%, 51.008606%)" stop-opacity="1"/>
<stop offset="0.273438" stop-color="rgb(50.361633%, 49.458313%, 49.682617%)" stop-opacity="1"/>
<stop offset="0.277344" stop-color="rgb(49.041748%, 48.11554%, 48.345947%)" stop-opacity="1"/>
<stop offset="0.28125" stop-color="rgb(47.70813%, 46.757507%, 46.994019%)" stop-opacity="1"/>
<stop offset="0.285156" stop-color="rgb(46.365356%, 45.388794%, 45.631409%)" stop-opacity="1"/>
<stop offset="0.289062" stop-color="rgb(45.007324%, 44.006348%, 44.255066%)" stop-opacity="1"/>
<stop offset="0.292969" stop-color="rgb(43.640137%, 42.614746%, 42.869568%)" stop-opacity="1"/>
<stop offset="0.296875" stop-color="rgb(42.25769%, 41.20636%, 41.467285%)" stop-opacity="1"/>
<stop offset="0.300781" stop-color="rgb(40.866089%, 39.790344%, 40.057373%)" stop-opacity="1"/>
<stop offset="0.304688" stop-color="rgb(39.460754%, 38.35907%, 38.632202%)" stop-opacity="1"/>
<stop offset="0.308594" stop-color="rgb(38.044739%, 36.917114%, 37.19635%)" stop-opacity="1"/>
<stop offset="0.3125" stop-color="rgb(36.613464%, 35.4599%, 35.746765%)" stop-opacity="1"/>
<stop offset="0.316406" stop-color="rgb(35.173035%, 33.99353%, 34.286499%)" stop-opacity="1"/>
<stop offset="0.320312" stop-color="rgb(33.720398%, 32.513428%, 32.8125%)" stop-opacity="1"/>
<stop offset="0.324219" stop-color="rgb(32.254028%, 31.021118%, 31.32782%)" stop-opacity="1"/>
<stop offset="0.328125" stop-color="rgb(30.776978%, 29.516602%, 29.829407%)" stop-opacity="1"/>
<stop offset="0.332031" stop-color="rgb(29.28772%, 27.999878%, 28.320312%)" stop-opacity="1"/>
<stop offset="0.335938" stop-color="rgb(27.784729%, 26.469421%, 26.795959%)" stop-opacity="1"/>
<stop offset="0.339844" stop-color="rgb(26.271057%, 24.928284%, 25.262451%)" stop-opacity="1"/>
<stop offset="0.34375" stop-color="rgb(24.743652%, 23.373413%, 23.713684%)" stop-opacity="1"/>
<stop offset="0.347656" stop-color="rgb(23.205566%, 21.807861%, 22.155762%)" stop-opacity="1"/>
<stop offset="0.351562" stop-color="rgb(21.655273%, 20.230103%, 20.584106%)" stop-opacity="1"/>
<stop offset="0.355469" stop-color="rgb(20.092773%, 18.638611%, 19.000244%)" stop-opacity="1"/>
<stop offset="0.359375" stop-color="rgb(18.516541%, 17.033386%, 17.402649%)" stop-opacity="1"/>
<stop offset="0.363281" stop-color="rgb(16.931152%, 15.419006%, 15.794373%)" stop-opacity="1"/>
<stop offset="0.367187" stop-color="rgb(15.330505%, 13.790894%, 14.173889%)" stop-opacity="1"/>
<stop offset="0.371094" stop-color="rgb(14.532471%, 12.9776%, 13.363647%)" stop-opacity="1"/>
<stop offset="0.375" stop-color="rgb(13.729858%, 12.159729%, 12.548828%)" stop-opacity="1"/>
<stop offset="0.5" stop-color="rgb(13.729858%, 12.159729%, 12.548828%)" stop-opacity="1"/>
<stop offset="1" stop-color="rgb(13.729858%, 12.159729%, 12.548828%)" stop-opacity="1"/>
</linearGradient>
</defs>
<g clip-path="url(#clip-0)">
<g clip-path="url(#clip-1)">
<g clip-path="url(#clip-2)">
<path fill-rule="nonzero" fill="url(#radial-pattern-0)" d="M 2080.265625 1487.285156 L 380.058594 1653.160156 L 414.617188 2007.375 L 2114.820312 1841.5 Z M 2080.265625 1487.285156 "/>
</g>
</g>
</g>
<path fill-rule="nonzero" fill="rgb(100%, 100%, 100%)" fill-opacity="1" d="M 1975.851562 980.382812 C 1975.851562 1411.570312 1626.300781 1761.121094 1195.121094 1761.121094 C 763.925781 1761.121094 414.378906 1411.570312 414.378906 980.382812 C 414.378906 549.191406 763.925781 199.648438 1195.121094 199.648438 C 1626.300781 199.648438 1975.851562 549.191406 1975.851562 980.382812 "/>
<path fill="none" stroke-width="45.3039" stroke-linecap="butt" stroke-linejoin="miter" stroke="rgb(13.729858%, 12.159729%, 12.548828%)" stroke-opacity="1" stroke-miterlimit="10" d="M 19758.515625 9680.371875 C 19758.515625 5368.496875 16263.007812 1872.989063 11951.210938 1872.989063 C 7639.257812 1872.989063 4143.789062 5368.496875 4143.789062 9680.371875 C 4143.789062 13992.285938 7639.257812 17487.715625 11951.210938 17487.715625 C 16263.007812 17487.715625 19758.515625 13992.285938 19758.515625 9680.371875 Z M 19758.515625 9680.371875 " transform="matrix(0.1, 0, 0, -0.1, 0, 1948.42)"/>
<path fill-rule="nonzero" fill="rgb(6.266785%, 5.734253%, 5.047607%)" fill-opacity="1" d="M 755.667969 429.191406 C 755.667969 429.191406 882.519531 374.820312 967.085938 356.699219 C 1051.648438 338.578125 1063.730469 356.699219 1075.808594 380.859375 C 1087.898438 405.019531 1148.300781 559.058594 1148.300781 559.058594 C 1148.300781 559.058594 918.761719 719.128906 906.679688 767.449219 L 665.058594 685.910156 C 665.058594 685.910156 698.28125 486.570312 755.667969 429.191406 "/>
<path fill="none" stroke-width="30.2026" stroke-linecap="butt" stroke-linejoin="miter" stroke="rgb(13.729858%, 12.159729%, 12.548828%)" stroke-opacity="1" stroke-miterlimit="10" d="M 11483.007812 13893.614063 C 11483.007812 13893.614063 13174.414062 13682.207813 14624.101562 12836.504688 " transform="matrix(0.1, 0, 0, -0.1, 0, 1948.42)"/>
<path fill="none" stroke-width="30.2026" stroke-linecap="butt" stroke-linejoin="miter" stroke="rgb(13.729858%, 12.159729%, 12.548828%)" stroke-opacity="1" stroke-miterlimit="10" d="M 9066.796875 11809.707813 C 9066.796875 11809.707813 9187.617188 9846.504688 9610.46875 8577.989063 " transform="matrix(0.1, 0, 0, -0.1, 0, 1948.42)"/>
<path fill="none" stroke-width="30.2026" stroke-linecap="butt" stroke-linejoin="miter" stroke="rgb(13.729858%, 12.159729%, 12.548828%)" stroke-opacity="1" stroke-miterlimit="10" d="M 12630.703125 7490.684375 C 12630.703125 7490.684375 14654.296875 8970.60625 15228.085938 9635.059375 " transform="matrix(0.1, 0, 0, -0.1, 0, 1948.42)"/>
<path fill="none" stroke-width="30.2026" stroke-linecap="butt" stroke-linejoin="miter" stroke="rgb(13.729858%, 12.159729%, 12.548828%)" stroke-opacity="1" stroke-miterlimit="10" d="M 18369.21875 11447.207813 C 18369.21875 11447.207813 18943.007812 11779.5125 19547.109375 11084.785938 " transform="matrix(0.1, 0, 0, -0.1, 0, 1948.42)"/>
<path fill="none" stroke-width="30.2026" stroke-linecap="butt" stroke-linejoin="miter" stroke="rgb(13.729858%, 12.159729%, 12.548828%)" stroke-opacity="1" stroke-miterlimit="10" d="M 12721.289062 17004.5125 C 12721.289062 17004.5125 12117.304688 17185.684375 10486.289062 15887.0125 " transform="matrix(0.1, 0, 0, -0.1, 0, 1948.42)"/>
<path fill="none" stroke-width="30.2026" stroke-linecap="butt" stroke-linejoin="miter" stroke="rgb(13.729858%, 12.159729%, 12.548828%)" stroke-opacity="1" stroke-miterlimit="10" d="M 6861.992188 12715.684375 C 6861.992188 12715.684375 5261.289062 11719.082813 4989.453125 10239.082813 " transform="matrix(0.1, 0, 0, -0.1, 0, 1948.42)"/>
<path fill="none" stroke-width="30.2026" stroke-linecap="butt" stroke-linejoin="miter" stroke="rgb(13.729858%, 12.159729%, 12.548828%)" stroke-opacity="1" stroke-miterlimit="10" d="M 5502.890625 7249.082813 C 5502.890625 7249.082813 6318.359375 6373.184375 7919.101562 6192.0125 " transform="matrix(0.1, 0, 0, -0.1, 0, 1948.42)"/>
<path fill="none" stroke-width="30.2026" stroke-linecap="butt" stroke-linejoin="miter" stroke="rgb(13.729858%, 12.159729%, 12.548828%)" stroke-opacity="1" stroke-miterlimit="10" d="M 9519.84375 2537.48125 L 9942.695312 4168.41875 " transform="matrix(0.1, 0, 0, -0.1, 0, 1948.42)"/>
<path fill="none" stroke-width="30.2026" stroke-linecap="butt" stroke-linejoin="miter" stroke="rgb(13.729858%, 12.159729%, 12.548828%)" stroke-opacity="1" stroke-miterlimit="10" d="M 12751.484375 4651.582813 C 12751.484375 4651.582813 14140.78125 3866.3875 15288.515625 3805.996875 " transform="matrix(0.1, 0, 0, -0.1, 0, 1948.42)"/>
<path fill="none" stroke-width="30.2026" stroke-linecap="butt" stroke-linejoin="miter" stroke="rgb(13.729858%, 12.159729%, 12.548828%)" stroke-opacity="1" stroke-miterlimit="10" d="M 16617.382812 13984.2 C 16315.390625 15313.184375 15741.601562 16007.79375 15741.601562 16007.79375 " transform="matrix(0.1, 0, 0, -0.1, 0, 1948.42)"/>
<path fill="none" stroke-width="30.2026" stroke-linecap="butt" stroke-linejoin="miter" stroke="rgb(13.729858%, 12.159729%, 12.548828%)" stroke-opacity="1" stroke-miterlimit="10" d="M 17704.6875 8970.60625 C 17704.6875 8970.60625 17946.40625 8155.098438 17886.015625 6886.582813 C 17825.507812 5618.10625 17825.507812 5618.10625 17825.507812 5618.10625 " transform="matrix(0.1, 0, 0, -0.1, 0, 1948.42)"/>
<path fill="none" stroke-width="30.2026" stroke-linecap="butt" stroke-linejoin="miter" stroke="rgb(13.729858%, 12.159729%, 12.548828%)" stroke-opacity="1" stroke-miterlimit="10" d="M 6771.40625 15584.98125 C 6771.40625 15584.98125 7043.242188 15705.801563 7556.679688 15192.285938 " transform="matrix(0.1, 0, 0, -0.1, 0, 1948.42)"/>
<path fill-rule="nonzero" fill="rgb(6.266785%, 5.734253%, 5.047607%)" fill-opacity="1" d="M 1451.949219 657.351562 C 1451.949219 657.351562 1569.730469 602.988281 1596.921875 587.128906 C 1624.101562 571.28125 1671.660156 548.628906 1671.660156 548.628906 C 1671.660156 548.628906 1846.070312 766.078125 1850.601562 786.460938 C 1855.140625 806.851562 1859.671875 831.761719 1827.949219 926.898438 C 1796.238281 1022.03125 1791.710938 1053.738281 1775.859375 1060.539062 C 1760 1067.328125 1510.839844 990.316406 1510.839844 990.316406 C 1510.839844 990.316406 1508.578125 784.199219 1451.949219 657.351562 "/>
<path fill-rule="nonzero" fill="rgb(6.266785%, 5.734253%, 5.047607%)" fill-opacity="1" d="M 958.921875 1086.210938 C 958.921875 1086.210938 1044.238281 1123.960938 1134.839844 1155.671875 C 1225.441406 1187.378906 1282.070312 1189.648438 1282.070312 1189.648438 L 1288.871094 1493.171875 C 1288.871094 1493.171875 1121.25 1545.261719 1064.621094 1558.851562 C 1008 1572.441406 987.609375 1576.96875 924.1875 1513.550781 C 860.765625 1450.128906 772.425781 1364.058594 799.609375 1300.628906 C 826.789062 1237.210938 958.921875 1086.210938 958.921875 1086.210938 "/>
<g clip-path="url(#clip-3)">
<g clip-path="url(#clip-4)">
<path fill-rule="nonzero" fill="rgb(6.266785%, 5.734253%, 5.047607%)" fill-opacity="1" d="M 1214.121094 197.539062 C 1214.121094 197.539062 1250.359375 220.191406 1254.890625 226.988281 C 1259.421875 233.78125 1261.679688 249.640625 1261.679688 249.640625 C 1261.679688 249.640625 1345.488281 245.109375 1436.101562 279.078125 C 1526.699219 313.058594 1567.46875 360.628906 1567.46875 360.628906 L 1667.128906 356.101562 L 1680.71875 351.570312 L 1569.730469 263.230469 L 1374.941406 188.480469 L 1243.558594 172.621094 L 1184.671875 172.621094 L 1214.121094 197.539062 "/>
</g>
</g>
<g clip-path="url(#clip-5)">
<g clip-path="url(#clip-6)">
<path fill-rule="nonzero" fill="rgb(6.266785%, 5.734253%, 5.047607%)" fill-opacity="1" d="M 502.882812 915.570312 C 502.882812 915.570312 441.726562 899.710938 416.808594 881.589844 C 391.894531 863.46875 391.894531 863.46875 391.894531 863.46875 C 391.894531 863.46875 389.628906 1096.78125 407.75 1162.460938 C 425.871094 1228.148438 428.136719 1284.78125 428.136719 1284.78125 L 480.230469 1359.53125 L 534.59375 1420.679688 C 534.59375 1420.679688 516.472656 1323.289062 530.0625 1284.78125 C 543.652344 1246.269531 561.773438 1228.148438 561.773438 1228.148438 C 561.773438 1228.148438 493.820312 1119.429688 502.882812 915.570312 "/>
</g>
</g>
<g clip-path="url(#clip-7)">
<g clip-path="url(#clip-8)">
<path fill-rule="nonzero" fill="rgb(6.266785%, 5.734253%, 5.047607%)" fill-opacity="1" d="M 815.464844 1663.050781 C 815.464844 1663.050781 937.777344 1685.699219 955.898438 1681.171875 C 955.898438 1681.171875 992.140625 1719.671875 1030.648438 1740.058594 C 1069.148438 1760.449219 1069.148438 1760.449219 1069.148438 1760.449219 L 951.367188 1755.921875 C 951.367188 1755.921875 901.535156 1724.199219 894.742188 1721.941406 C 887.945312 1719.671875 824.523438 1692.488281 824.523438 1692.488281 L 815.464844 1663.050781 "/>
</g>
</g>
<g clip-path="url(#clip-9)">
<g clip-path="url(#clip-10)">
<path fill-rule="nonzero" fill="rgb(6.266785%, 5.734253%, 5.047607%)" fill-opacity="1" d="M 1524.429688 1552.058594 C 1524.429688 1552.058594 1531.230469 1606.421875 1515.371094 1644.929688 C 1499.519531 1683.429688 1476.871094 1724.199219 1476.871094 1724.199219 C 1476.871094 1724.199219 1687.519531 1624.539062 1746.410156 1556.589844 C 1805.300781 1488.640625 1889.109375 1359.53125 1889.109375 1359.53125 C 1889.109375 1359.53125 1855.140625 1377.648438 1780.390625 1355 C 1780.390625 1355 1653.539062 1524.878906 1524.429688 1552.058594 "/>
</g>
</g>
<g clip-path="url(#clip-11)">
<g clip-path="url(#clip-12)">
<g clip-path="url(#clip-13)">
<path fill-rule="nonzero" fill="url(#linear-pattern-0)" d="M 1412.566406 579.699219 L 1493.851562 1015.472656 L 1693.082031 978.3125 L 1611.796875 542.535156 Z M 1412.566406 579.699219 "/>
</g>
</g>
</g>
<g clip-path="url(#clip-14)">
<g clip-path="url(#clip-15)">
<g clip-path="url(#clip-16)">
<path fill-rule="nonzero" fill="url(#linear-pattern-1)" d="M 1316.875 1126.984375 L 989.945312 1033.574219 L 923.324219 1266.753906 L 1250.25 1360.164062 Z M 1316.875 1126.984375 "/>
</g>
</g>
</g>
<g clip-path="url(#clip-17)">
<g clip-path="url(#clip-18)">
<g clip-path="url(#clip-19)">
<path fill-rule="nonzero" fill="url(#linear-pattern-2)" d="M 783.929688 774.484375 L 1018.773438 747.800781 L 1004.703125 623.972656 L 769.859375 650.660156 Z M 783.929688 774.484375 "/>
</g>
</g>
</g>
<g clip-path="url(#clip-20)">
<g clip-path="url(#clip-21)">
<g clip-path="url(#clip-22)">
<path fill-rule="nonzero" fill="url(#linear-pattern-3)" d="M 386.121094 931.613281 L 419.324219 1313.453125 L 550.710938 1302.027344 L 517.507812 920.1875 Z M 386.121094 931.613281 "/>
</g>
</g>
</g>
<g clip-path="url(#clip-23)">
<g clip-path="url(#clip-24)">
<g clip-path="url(#clip-25)">
<path fill-rule="nonzero" fill="url(#linear-pattern-4)" d="M 1539.625 1388.210938 L 1562.390625 1650 L 1852.925781 1624.738281 L 1830.160156 1362.949219 Z M 1539.625 1388.210938 "/>
</g>
</g>
</g>
<g clip-path="url(#clip-26)">
<g clip-path="url(#clip-27)">
<g clip-path="url(#clip-28)">
<path fill-rule="nonzero" fill="url(#linear-pattern-5)" d="M 752.859375 1512.457031 L 1083.992188 1678.023438 L 1227.964844 1390.082031 L 896.832031 1224.515625 Z M 752.859375 1512.457031 "/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 91 KiB

+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 595.3 595.3" enable-background="new 0 0 595.3 595.3" xml:space="preserve">
<g id="Capa_1">
</g>
<g id="Capa_2">
<g>
<circle fill="#F1EA71" cx="297" cy="296" r="230.6"/>
<path fill="#CFC761" d="M180.7,96.9c0,0-183.5,109.7-85.4,310.7c0,0,50.9,116.4,201.7,119c0,0-163.1-66-178.6-214.8
C118.4,311.8,100,210.3,180.7,96.9z"/>
<path fill="none" stroke="#FFFFFF" stroke-width="23.9535" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="
M84.7,234.5c0,0,49.3-2.8,77.7,62.1c0,0,40.2,145.8,182,103.3c0,0,130.1-49.4,35.8-210.1c0,0-25.7-34.8-14.3-61.2l13.8-37.3"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 872 B

+2391
View File
File diff suppressed because it is too large Load Diff
+272
View File
@@ -0,0 +1,272 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="..\css\Login.css">
</head>
<body>
<div class="container">
<h1>Iniciar Sesión</h1>
<form id="loginForm">
<div class="input-box">
<input type="text" id="usuario" name="usuario" placeholder="Usuario" required>
</div>
<div class="input-box">
<input type="password" id="clave" name="clave" placeholder="Contraseña" required>
</div>
<div class="remember-password">
<!--<label><input type="checkbox">Recordarme</label>-->
<a href="#" id="btnOlvidoContrasena">¿Olvidaste tu contraseña?</a>
</div>
<button type="submit" class="btnI">Ingresar</button>
</form>
</div>
<!-- Modal de Recuperación de Contraseña -->
<div id="modalRecuperacion" class="modal" style="display: none;">
<div class="modal-content">
<span class="close" id="cerrarModal">&times;</span>
<!-- Paso 1: Solicitar Correo -->
<div id="paso1" class="paso-recuperacion">
<h2>Recuperar Contraseña</h2>
<p>Ingresa tu correo para recibir un código de verificación</p>
<input type="email" id="correoRecuperacion" placeholder="Correo electrónico" required>
<button id="btnEnviarCodigo" class="btnRecuperar">Enviar Código</button>
<p id="msgPaso1" class="mensaje"></p>
</div>
<!-- Paso 2: Verificar Código y Nueva Contraseña -->
<div id="paso2" class="paso-recuperacion" style="display: none;">
<h2>Verificar Código</h2>
<p>Ingresa el código que recibiste en tu correo</p>
<input type="text" id="codigoVerificacion" placeholder="Código de 6 dígitos" maxlength="6" required>
<input type="password" id="nuevaContrasena" placeholder="Nueva contraseña" required>
<input type="password" id="confirmarContrasena" placeholder="Confirmar contraseña" required>
<div class="botones-grupo">
<button id="btnCambiarContrasena" class="btnRecuperar">Cambiar Contraseña</button>
<button id="btnVolver" class="btnVolver">Volver</button>
</div>
<p id="msgPaso2" class="mensaje"></p>
</div>
</div>
</div>
<script>
// Variables globales para el flujo de recuperación
let correoRecuperacion = '';
// Elementos del modal
const modalRecuperacion = document.getElementById('modalRecuperacion');
const btnOlvidoContrasena = document.getElementById('btnOlvidoContrasena');
const cerrarModal = document.getElementById('cerrarModal');
const paso1 = document.getElementById('paso1');
const paso2 = document.getElementById('paso2');
// Botones
const btnEnviarCodigo = document.getElementById('btnEnviarCodigo');
const btnCambiarContrasena = document.getElementById('btnCambiarContrasena');
const btnVolver = document.getElementById('btnVolver');
// Inputs
const correoInput = document.getElementById('correoRecuperacion');
const codigoInput = document.getElementById('codigoVerificacion');
const nuevaContrasenaInput = document.getElementById('nuevaContrasena');
const confirmarContrasenaInput = document.getElementById('confirmarContrasena');
// Mensajes
const msgPaso1 = document.getElementById('msgPaso1');
const msgPaso2 = document.getElementById('msgPaso2');
// Abrir modal
btnOlvidoContrasena.addEventListener('click', (e) => {
e.preventDefault();
modalRecuperacion.style.display = 'block';
correoInput.focus();
});
// Cerrar modal
cerrarModal.addEventListener('click', () => {
modalRecuperacion.style.display = 'none';
limpiarModal();
});
window.addEventListener('click', (e) => {
if (e.target === modalRecuperacion) {
modalRecuperacion.style.display = 'none';
limpiarModal();
}
});
// Paso 1: Enviar Código
btnEnviarCodigo.addEventListener('click', async () => {
const correo = correoInput.value.trim();
if (!correo) {
msgPaso1.textContent = 'Por favor ingresa un correo';
msgPaso1.className = 'mensaje error';
return;
}
btnEnviarCodigo.disabled = true;
btnEnviarCodigo.textContent = 'Enviando...';
msgPaso1.textContent = '';
try {
const response = await fetch('../php/Recuperar_Contrasena_Solicitar.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ correo })
});
const data = await response.json();
if (data.success) {
correoRecuperacion = correo;
msgPaso1.textContent = data.message;
msgPaso1.className = 'mensaje success';
// Cambiar al paso 2 después de 1.5 segundos
setTimeout(() => {
paso1.style.display = 'none';
paso2.style.display = 'block';
codigoInput.focus();
}, 1500);
} else {
msgPaso1.textContent = data.message;
msgPaso1.className = 'mensaje error';
}
} catch (error) {
msgPaso1.textContent = 'Error al conectar con el servidor';
msgPaso1.className = 'mensaje error';
console.error('Error:', error);
} finally {
btnEnviarCodigo.disabled = false;
btnEnviarCodigo.textContent = 'Enviar Código';
}
});
// Paso 2: Cambiar Contraseña
btnCambiarContrasena.addEventListener('click', async () => {
const codigo = codigoInput.value.trim();
const nuevaContrasena = nuevaContrasenaInput.value;
const confirmarContrasena = confirmarContrasenaInput.value;
if (!codigo || !nuevaContrasena || !confirmarContrasena) {
msgPaso2.textContent = 'Por favor completa todos los campos';
msgPaso2.className = 'mensaje error';
return;
}
if (codigo.length !== 6 || isNaN(codigo)) {
msgPaso2.textContent = 'El código debe tener 6 dígitos';
msgPaso2.className = 'mensaje error';
return;
}
if (nuevaContrasena.length < 6) {
msgPaso2.textContent = 'La contraseña debe tener al menos 6 caracteres';
msgPaso2.className = 'mensaje error';
return;
}
if (nuevaContrasena !== confirmarContrasena) {
msgPaso2.textContent = 'Las contraseñas no coinciden';
msgPaso2.className = 'mensaje error';
return;
}
btnCambiarContrasena.disabled = true;
btnCambiarContrasena.textContent = 'Cambiando...';
msgPaso2.textContent = '';
try {
const response = await fetch('../php/Recuperar_Contrasena_Verificar.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
correo: correoRecuperacion,
codigo,
nueva_contrasena: nuevaContrasena
})
});
const data = await response.json();
if (data.success) {
msgPaso2.textContent = data.message;
msgPaso2.className = 'mensaje success';
// Cerrar modal después de 2 segundos
setTimeout(() => {
modalRecuperacion.style.display = 'none';
limpiarModal();
alert('Contraseña cambiada exitosamente. Ahora puedes iniciar sesión');
}, 2000);
} else {
msgPaso2.textContent = data.message;
msgPaso2.className = 'mensaje error';
}
} catch (error) {
msgPaso2.textContent = 'Error al conectar con el servidor';
msgPaso2.className = 'mensaje error';
console.error('Error:', error);
} finally {
btnCambiarContrasena.disabled = false;
btnCambiarContrasena.textContent = 'Cambiar Contraseña';
}
});
// Volver a paso 1
btnVolver.addEventListener('click', () => {
paso2.style.display = 'none';
paso1.style.display = 'block';
correoInput.focus();
});
// Función para limpiar el modal
function limpiarModal() {
correoInput.value = '';
codigoInput.value = '';
nuevaContrasenaInput.value = '';
confirmarContrasenaInput.value = '';
msgPaso1.textContent = '';
msgPaso2.textContent = '';
paso1.style.display = 'block';
paso2.style.display = 'none';
correoRecuperacion = '';
}
// Script original de login
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(document.getElementById('loginForm'));
try {
const response = await fetch('../php/Login.php', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
// Redirigir al panel
window.location.href = '../php/Cantina.php';
} else {
// Mostrar error sin cambiar de página
alert(data.message);
// Limpiar campos de contraseña
document.getElementById('clave').value = '';
}
} catch (error) {
alert('Error al conectar con el servidor');
console.error('Error:', error);
}
});
</script>
</body>
</html>
+154
View File
@@ -0,0 +1,154 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Reservar Turno - Complejo de Canchas</title>
<link rel="stylesheet" href="../css/Reserva_Clientes.css" />
</head>
<body>
<header class="header">
<div class="header-content">
<h1 class="header-title">Complejo Deportivo Cap1tan</h1>
<p class="header-subtitle">Reserva tu turno ahora</p>
</div>
</header>
<main class="contenedor-principal">
<!-- cancha -->
<section class="seccion selector-cancha">
<h2>Selecciona el tipo de cancha</h2>
<div class="botones-cancha">
<button type="button" class="btn-cancha-tipo" data-tipo="futbol">
<img src="../css/img/futbol.svg" alt="Fútbol" class="icono">
<span class="nombre">Fútbol</span>
</button>
<button type="button" class="btn-cancha-tipo" data-tipo="padel">
<img src="../css/img/padel.svg" alt="Pádel" class="icono">
<span class="nombre">Pádel</span>
</button>
</div>
</section>
<!-- fecha -->
<section class="seccion selector-fecha" style="display: none;">
<h2>Elige un día</h2>
<div class="calendario-mini">
<div class="dias-disponibles" id="dias-disponibles"></div>
</div>
</section>
<!-- horario -->
<section class="seccion selector-horario" style="display: none;">
<h2>Selecciona horario</h2>
<div class="grid-horarios" id="grid-horarios"></div>
</section>
<!-- número de cancha -->
<section class="seccion selector-numero-cancha" style="display: none;">
<h2>Selecciona tu cancha</h2>
<div class="botones-canchas-numeros" id="botones-canchas-numeros"></div>
</section>
<section class="seccion datos-cliente" style="display: none;">
<!-- Datos del cliente -->
<h2>Tus Datos</h2>
<div class="datos-cliente">
<form id="form-reserva" class="form-cliente">
<input type="text" id="cliente-nombre" placeholder="Nombre Completo" required />
<input type="tel" id="cliente-telefono" placeholder="Teléfono" required />
<input type="text" id="cliente-dni" placeholder="DNI" required />
</form>
</div>
<div class="botones-verificar">
<button type="button" class="btn-verificar">Verificar</button>
</div>
</section>
<!-- reserva -->
<section class="seccion resumen-reserva" style="display: none;">
<h2>Resumen de Reserva</h2>
<div class="resumen-info">
<div class="info-item">
<span class="label">Cancha:</span>
<span class="valor" id="res-cancha">-</span>
</div>
<div class="info-item">
<span class="label">Fecha:</span>
<span class="valor" id="res-fecha">-</span>
</div>
<div class="info-item">
<span class="label">Horario:</span>
<span class="valor" id="res-horario">-</span>
</div>
<div class="info-item">
<span class="label">Duración:</span>
<span class="valor" id="res-duracion">-</span>
</div>
<div class="info-item">
<span class="label">Cupón:</span>
<span class="valor" id="res-cupon">-</span>
</div>
<div class="info-item" id="precio-original" style="display: none;">
<span class="label">Precio Original:</span>
<span class="valor" id="res-precio-original" style="text-decoration: line-through; color: #888;">$0.00</span>
</div>
<div class="info-item total">
<span class="label" id="label-precio">Precio:</span>
<span class="valor" id="res-precio">$0.00</span>
</div>
</div>
<!-- quincho -->
<div class="opciones-adicionales">
<h3>Opciones Adicionales</h3>
<label class="checkbox-container">
<input type="checkbox" id="incluir-quincho" />
<span class="checkbox-text">Incluir Quincho</span>
<span class="precio-quincho" style="display: none;">
+ $<span id="precio-quincho-val">0</span>
</span>
</label>
<div id="quincho-reservas" style="display: none;" class="quincho-info">
<small>Reservas disponibles: <strong><span id="quincho-disp">-</span></strong></small>
</div>
<div id="quincho-no-disponible" style="display: none;" class="quincho-alerta">
<small>⚠️ No hay quinchos disponibles para esta fecha</small>
</div>
</div>
<div id="seccion-verificacion" class="datos-cliente" style="display: none; border: 2px solid #4CAF50; padding: 15px; border-radius: 8px; margin-top: 15px;">
<h3>Verificación por WhatsApp</h3>
<p>Enviamos un código de 4 dígitos al <strong id="lbl-telefono"></strong>.</p>
<input type="text" id="codigo-verificacion" placeholder="Ingrese el código" maxlength="4" style="text-align: center; font-size: 20px; letter-spacing: 5px; margin-bottom: 10px; width: 100%;" />
<button type="button" class="btn-confirmar" id="btn-verificar-codigo" style="width: 100%;">Validar y Reservar</button>
</div>
<div class="botones-reserva">
<button type="button" class="btn-volver">← Volver</button>
<button type="button" class="btn-confirmar" id="btn-solicitar-reserva">Confirmar Reserva</button>
</div>
</section>
<section id="seccion-comprobante" class="seccion" style="display: none; text-align: center;">
<h2>¡Reserva Confirmada!</h2>
<p>Para abonar tu turno, podes pagar en efectivo o realizar una transferencia a este alias: <strong>ejemplo.mp</strong> y adjuntar la foto o captura de tu comprobante.</p>
<div class="datos-cliente" style="margin-top: 20px;">
<form id="form-comprobante" enctype="multipart/form-data">
<input type="hidden" id="comprobante-id-reserva" value="" />
<input type="file" id="input-file-comprobante" accept="image/*" required style="margin-bottom: 15px; width: 100%; padding: 10px;" />
<button type="submit" class="btn-confirmar" style="width: 100%;">Enviar Comprobante</button>
</form>
</div>
</section>
</main>
<script src="../js/Reservar_Turno.js"></script>
</body>
</html>
+168
View File
@@ -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));
}
+274
View File
@@ -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 = '<tr><td colspan="7" style="text-align: center;">No hay usuarios registrados</td></tr>';
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
? `<button class="btn-eliminar-usuario" title="El administrador supremo no puede eliminarse" disabled>🔒</button>`
: `<button onclick="eliminarUsuario(${usuario.ID_Usuario})" class="btn-eliminar-usuario" title="Eliminar">🗑️</button>`;
row.innerHTML = `
<td>${usuario.Usuario}</td>
<td>${usuario.Nombre} ${usuario.Apellido}</td>
<td>${usuario.DNI}</td>
<td>${usuario.Rol === 'admin' ? 'Administrador' : usuario.Rol === 'cantina' ? 'Trabajador Cantina' : 'Trabajador Cancha'}</td>
<td>${usuario.Telefono || '-'}</td>
<td>${usuario.Correo || '-'}</td>
<td>
<button onclick="editarUsuario(${usuario.ID_Usuario})" class="btn-editar" title="Editar">Editar</button>
<button onclick="eliminarUsuario(${usuario.ID_Usuario})" class="btn-eliminar-usuario" title="Eliminar">Eliminar</button>
</td>
`;
tbody.appendChild(row);
});
} else {
console.error('Error al cargar usuarios:', data.message);
const tbody = document.querySelector('#tabla-usuarios tbody');
tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; color: red;">Error al cargar usuarios</td></tr>';
}
})
.catch(error => {
console.error('Error en fetch:', error);
const tbody = document.querySelector('#tabla-usuarios tbody');
tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; color: red;">Error de conexión</td></tr>';
});
}
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);
});
}
}
+429
View File
@@ -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 = `
<td>${p.id}</td>
<td>${p.descripcion}</td>
<td>${p.cantidad}</td>
<td>
<button type="button" class="btn-eliminar" onclick="eliminarProductoOrden(${i})">Eliminar</button>
</td>
`;
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 = `
<td>${orden.ID_Orden}</td>
<td>${orden.Fecha}</td>
<td>${orden.ID_Proveedor || "N/A"}</td>
<td>${orden.Proveedor || "Sin nombre"}</td>
<td>${orden.Telefono || "Sin registrar"}</td>
<td>${textoEstado}</td>
<td>
<button type="button" class="btn-ver-orden" data-id="${orden.ID_Orden}" data-estado="${orden.Estado || 0}">Ver Orden</button>
<button type="button" class="btn-aprobar"
data-id="${orden.ID_Orden}"
data-telefono="${orden.Telefono || ''}"
data-proveedor="${orden.Proveedor || ''}"
${estaProcesada ? "disabled" : ""}>Aprobar</button>
<button type="button" class="btn-denegar"
data-id="${orden.ID_Orden}"
${estaProcesada ? "disabled" : ""}>Denegar</button>
</td>
`;
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 = '<td colspan="2">No hay productos registrados.</td>';
cuerpo.appendChild(filaVacia);
actualizarBotonesDetalleOrden();
return;
}
ordenDetalleActual.productos.forEach((prod) => {
const fila = document.createElement("tr");
const cantidadActual = prod.Cantidad || 0;
const celdaCantidad = modoEdicionDetalle
? `<td><input type="number" min="1" value="${cantidadActual}" data-product-id="${prod.ID_Producto}" class="input-cantidad-orden" /></td>`
: `<td>${cantidadActual}</td>`;
fila.innerHTML = `
<td>${prod.Descripcion}</td>
${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 = `
<td>${p.id}</td>
<td>${p.descripcion}</td>
<td>${p.cantidad}</td>
<td>
<button type="button" onclick="eliminarProductoOrden(${i})">Eliminar</button>
</td>
`;
document.querySelector("#tabla-orden tbody").appendChild(fila);
});
}
+585
View File
@@ -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 = `
<div class="reporte-titulo">
<h2>📅 Reporte Diario ${fechaCorta} - Cierre de Caja</h2>
<div class="reporte-periodo">Reporte ${fechaFormato}</div>
</div>
<table class="reporte-tabla">
<thead>
<tr>
<th>Hora</th>
<th>Tipo de Operación</th>
<th>Descripción / Detalle</th>
<th style="text-align: right;">Monto ($)</th>
<th>Usuario</th>
</tr>
</thead>
<tbody>
`;
if (data.transacciones && data.transacciones.length > 0) {
data.transacciones.forEach(tx => {
const monto = parseFloat(tx.monto);
const claseNegativo = monto < 0 ? 'negativo' : '';
html += `
<tr>
<td>${tx.hora || '--:--'}</td>
<td><strong>${tx.tipo_operacion}</strong></td>
<td>${tx.descripcion || '--'}</td>
<td class="monto ${claseNegativo}">${formatoMoneda(monto)}</td>
<td>${tx.usuario_registro || 'Sistema'}</td>
</tr>
`;
});
} else {
html += `<tr><td colspan="5" style="text-align: center;">Sin transacciones registradas</td></tr>`;
}
html += `
</tbody>
</table>
<div class="resumen-reporte">
<h3>Resumen del Día</h3>
<div class="resumen-fila">
<span class="resumen-label">Ingresos Reservas:</span>
<span class="resumen-valor">${formatoMoneda(data.resumen.ingresos_reservas)}</span>
</div>
<div class="resumen-fila">
<span class="resumen-label">Ingresos Cantina:</span>
<span class="resumen-valor">${formatoMoneda(data.resumen.ingresos_cantina)}</span>
</div>
<div class="resumen-fila">
<span class="resumen-label">Total Ingresos:</span>
<span class="resumen-valor">${formatoMoneda(data.resumen.total_ingresos)}</span>
</div>
<div class="resumen-fila">
<span class="resumen-label">Egresos Totales:</span>
<span class="resumen-valor negativo">-${formatoMoneda(data.resumen.egresos)}</span>
</div>
<hr style="border: none; border-top: 2px solid #2c3e50; margin: 15px 0;">
<div class="resumen-fila">
<span class="resumen-label">💰 SALDO NETO:</span>
<span class="resumen-valor ${data.resumen.saldo_neto < 0 ? 'negativo' : ''}">${formatoMoneda(data.resumen.saldo_neto)}</span>
</div>
</div>
`;
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 = `
<div class="reporte-titulo">
<h2>📆 Reporte Semanal ${inicioCorta} al ${finCorta} - Flujo de Caja</h2>
<div class="reporte-periodo">Reporte Semana del ${inicio.toLocaleDateString('es-ES', opciones)} al ${fin.toLocaleDateString('es-ES', opciones)}</div>
</div>
<table class="reporte-tabla">
<thead>
<tr>
<th>Día de la Semana</th>
<th style="text-align: right;">Ingresos Reservas ($)</th>
<th style="text-align: right;">Ingresos Cantina ($)</th>
<th style="text-align: right;">Costo Mercadería ($)</th>
<th style="text-align: right;">Saldo Neto del Día ($)</th>
</tr>
</thead>
<tbody>
`;
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 += `
<tr>
<td><strong>${nombreDia.charAt(0).toUpperCase() + nombreDia.slice(1)} (${dia.dia})</strong></td>
<td class="monto">${formatoMoneda(dia.ingresos_reservas)}</td>
<td class="monto">${formatoMoneda(dia.ingresos_cantina)}</td>
<td class="monto negativo">${formatoMoneda(dia.costo_mercaderia)}</td>
<td class="monto ${claseNegativo}"><strong>${formatoMoneda(saldoNeto)}</strong></td>
</tr>
`;
});
}
html += `
</tbody>
</table>
<table class="reporte-tabla">
<thead>
<tr>
<th colspan="5" style="background: #34495e; color: white; text-align: center;">Resumen de la Semana</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;"><strong>Ingresos Reservas Total:</strong></td>
<td style="text-align: right;" colspan="4" class="monto encabezado">${formatoMoneda(data.resumen.ingresos_reservas_totales)}</td>
</tr>
<tr>
<td style="text-align: right;"><strong>Ingresos Cantina Total:</strong></td>
<td style="text-align: right;" colspan="4" class="monto encabezado">${formatoMoneda(data.resumen.ingresos_cantina_totales)}</td>
</tr>
<tr>
<td style="text-align: right;"><strong>Costo Mercadería Total:</strong></td>
<td style="text-align: right;" colspan="4" class="monto encabezado negativo">${formatoMoneda(data.resumen.costo_mercaderia_total)}</td>
</tr>
<tr>
<td style="text-align: right;"><strong>💰 Saldo Neto Semana:</strong></td>
<td style="text-align: right;" colspan="4" class="monto encabezado ${data.resumen.saldo_neto_semana < 0 ? 'negativo' : ''}">${formatoMoneda(data.resumen.saldo_neto_semana)}</td>
</tr>
</tbody>
</table>
<div class="resumen-reporte">
<h3>% Ocupación Promedio</h3>
<div class="resumen-fila">
<span class="resumen-label">Turnos Ocupados:</span>
<span class="resumen-valor neutral">${data.resumen.turnos_ocupados} / ${data.resumen.turnos_disponibles}</span>
</div>
<div class="resumen-fila">
<span class="resumen-label">Porcentaje Ocupación:</span>
<span class="resumen-valor neutral">${data.resumen.ocupacion_promedio}%</span>
</div>
</div>
`;
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 = `
<div class="reporte-titulo">
<h2>📊 Reporte Mensual ${mesCorta} - Rentabilidad</h2>
<div class="reporte-periodo">Reporte de ${nombreMes.charAt(0).toUpperCase() + nombreMes.slice(1)}</div>
</div>
<h3 style="margin-top: 30px; color: #2c3e50;">Ingreso por Categoría</h3>
<table class="reporte-tabla">
<thead>
<tr>
<th>Concepto / Categoría</th>
<th style="text-align: right;">Operaciones</th>
<th style="text-align: right;">Monto ($)</th>
</tr>
</thead>
<tbody>
`;
if (data.categorias && data.categorias.length > 0) {
data.categorias.forEach(cat => {
html += `
<tr>
<td><strong>${cat.categoria}</strong></td>
<td style="text-align: right;">${cat.operaciones}</td>
<td class="monto">${formatoMoneda(cat.monto)}</td>
</tr>
`;
});
}
html += `
</tbody>
</table>
<h3 style="margin-top: 30px; color: #2c3e50;">Gastos Fijos</h3>
<table class="reporte-tabla">
<thead>
<tr>
<th>Nombre del Gasto</th>
<th style="text-align: right;">Monto Gasto ($)</th>
</tr>
</thead>
<tbody>
`;
if (data.gastos_fijos && data.gastos_fijos.length > 0) {
data.gastos_fijos.forEach(gasto => {
html += `
<tr>
<td>${gasto.descripcion}</td>
<td class="monto negativo">${formatoMoneda(gasto.monto)}</td>
</tr>
`;
});
} else {
html += `
<tr>
<td colspan="2" style="text-align: center; color: #7f8c8d;">Sin gastos registrados</td>
</tr>
`;
}
html += `
</tbody>
</table>
<div class="resumen-reporte" style="margin-top: 30px;">
<h3>Estado de Resultados</h3>
<div class="resumen-fila">
<span class="resumen-label">Ingresos Totales:</span>
<span class="resumen-valor">${formatoMoneda(data.resumen.ingresos_totales)}</span>
</div>
<div class="resumen-fila">
<span class="resumen-label">Costo de Mercadería:</span>
<span class="resumen-valor negativo">-${formatoMoneda(data.resumen.costo_mercaderia)}</span>
</div>
<hr style="border: none; border-top: 1px solid #2c3e50; margin: 15px 0;">
<div class="resumen-fila">
<span class="resumen-label">Ganancia Bruta:</span>
<span class="resumen-valor">${formatoMoneda(data.resumen.ganancia_bruta)}</span>
</div>
<div class="resumen-fila">
<span class="resumen-label">Gastos Fijos:</span>
<span class="resumen-valor negativo">-${formatoMoneda(data.resumen.gastos_fijos_total)}</span>
</div>
<hr style="border: none; border-top: 2px solid #2c3e50; margin: 15px 0;">
<div class="resumen-fila">
<span class="resumen-label">💰 GANANCIA NETA:</span>
<span class="resumen-valor ${data.resumen.ganancia_neta < 0 ? 'negativo' : ''}" style="font-size: 1.3em;">${formatoMoneda(data.resumen.ganancia_neta)}</span>
</div>
</div>
`;
reporteContenido.innerHTML = html;
}
function renderizarAnual(data) {
let notaAnoActual = '';
if (data.es_ano_actual) {
notaAnoActual = `
<div style="background: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; padding: 12px; margin-bottom: 20px; color: #856404;">
<strong>️ Nota:</strong> Este es el año en curso. El reporte muestra datos hasta el ${data.mes_actual}° mes (mes actual).
</div>
`;
}
let html = `
<div class="reporte-titulo">
<h2>📈 Reporte Anual ${data.periodo} - Visión Estratégica</h2>
<div class="reporte-periodo">Reporte Año ${data.periodo}</div>
</div>
${notaAnoActual}
<table class="reporte-tabla">
<thead>
<tr>
<th>Mes</th>
<th style="text-align: right;">Ingreso Operativo ($)</th>
<th style="text-align: right;">Costo Bruto ($)</th>
<th style="text-align: right;">Costo Operativo ($)</th>
<th style="text-align: right;">Ganancia Bruta ($)</th>
<th style="text-align: right;">Ganancia Neta ($)</th>
</tr>
</thead>
<tbody>
`;
if (data.meses && data.meses.length > 0) {
data.meses.forEach(mes => {
const gananciaNeta = parseFloat(mes.ganancia_neta);
const claseNegativo = gananciaNeta < 0 ? 'negativo' : '';
html += `
<tr>
<td><strong>${mes.mes}</strong></td>
<td class="monto">${formatoMoneda(mes.ingreso_operativo)}</td>
<td class="monto negativo">${formatoMoneda(mes.costo_bruto)}</td>
<td class="monto negativo">${formatoMoneda(mes.costo_operativo)}</td>
<td class="monto">${formatoMoneda(mes.ganancia_bruta)}</td>
<td class="monto ${claseNegativo}"><strong>${formatoMoneda(gananciaNeta)}</strong></td>
</tr>
`;
});
}
html += `
</tbody>
</table>
<div class="resumen-reporte" style="margin-top: 30px;">
<h3>Totales Anuales</h3>
<div class="resumen-fila">
<span class="resumen-label">Ingreso Operativo Total:</span>
<span class="resumen-valor">${formatoMoneda(data.totales.ingreso_operativo_total)}</span>
</div>
<div class="resumen-fila">
<span class="resumen-label">Costo Bruto Total:</span>
<span class="resumen-valor negativo">-${formatoMoneda(data.totales.costo_bruto_total)}</span>
</div>
<div class="resumen-fila">
<span class="resumen-label">Costo Operativo Total:</span>
<span class="resumen-valor negativo">-${formatoMoneda(data.totales.costo_operativo_total)}</span>
</div>
<hr style="border: none; border-top: 1px solid #2c3e50; margin: 15px 0;">
<div class="resumen-fila">
<span class="resumen-label">Ganancia Bruta Total:</span>
<span class="resumen-valor">${formatoMoneda(data.totales.ganancia_bruta_total)}</span>
</div>
<hr style="border: none; border-top: 2px solid #2c3e50; margin: 15px 0;">
<div class="resumen-fila">
<span class="resumen-label">💰 GANANCIA NETA ANUAL:</span>
<span class="resumen-valor ${data.totales.ganancia_neta_total < 0 ? 'negativo' : ''}" style="font-size: 1.3em;">${formatoMoneda(data.totales.ganancia_neta_total)}</span>
</div>
</div>
`;
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');
});
});
+621
View File
@@ -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 = `<div>${diasSemana[fecha.getDay()]}</div><div>${fecha.getDate()}/${fecha.getMonth() + 1}</div>`;
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();
});
+208
View File
@@ -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 =
'<tr><td colspan="4" style="text-align: center; color: red;">Error al cargar ventas</td></tr>';
}
});
}
function renderTablaVentas(ventas) {
if (!tablaVentas) return;
const tbody = tablaVentas.querySelector('tbody');
tbody.innerHTML = '';
if (!ventas || ventas.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" style="text-align: center;">No hay ventas en el período seleccionado</td></tr>';
return;
}
ventas.forEach(venta => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${venta.ID_Venta || '-'}</td>
<td>${venta.fecha_hora ? formatDate(venta.fecha_hora) : '-'}</td>
<td>${formatMoney(venta.monto_total)}</td>
<td>
<button class="btn-ver-detalle" onclick="window.verDetalleVenta(${venta.ID_Venta})" title="Ver detalle">Ver Productos</button>
</td>
`;
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 = '<tr><td colspan="4" style="text-align: center;">No hay productos en esta venta</td></tr>';
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 = `
<td>${prod.nombre || prod.descripcion || '-'}</td>
<td style="text-align: center;">${cantidad}</td>
<td style="text-align: right;">${formatMoney(precio)}</td>
<td style="text-align: right;">${formatMoney(subtotal)}</td>
`;
tbody.appendChild(row);
});
// Agregar fila de total
const rowTotal = document.createElement('tr');
rowTotal.style.fontWeight = 'bold';
rowTotal.style.borderTop = '2px solid #333';
rowTotal.innerHTML = `
<td colspan="3" style="text-align: right;">TOTAL GENERAL:</td>
<td style="text-align: right;">${formatMoney(totalVenta)}</td>
`;
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();
}
});
});
+291
View File
@@ -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 = `<input type="text" value="${columnaActual}" class="input-descripcion-gasto">`;
// Convertir monto a input
celdas[1].innerHTML = `<input type="number" value="${montoActual}" step="0.01" min="0" class="input-monto-gasto">`;
// 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');
}
}
+277
View File
@@ -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 = `<i class="fa-solid fa-triangle-exclamation fa-beat low-stock-alert" style="color: rgb(255, 212, 59); margin-left: 0.35rem;" aria-hidden="true" title="Stock bajo"></i>`;
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 = `
<td>${p.ID_Producto}</td>
<td>${p.Descripcion}</td>
<td>$${parseFloat(p.Precio_Venta).toFixed(2)}</td>
<td>$${parseFloat(p.Precio_Compra).toFixed(2)}</td>
<td>${p.Stock_Disponible}${stockBajo ? lowStockIconHTML : ''}</td>
<td>
<button class="btn-editar" data-id="${
p.ID_Producto
}">Editar</button>
<button class="btn-eliminar" data-id="${
p.ID_Producto
}">Eliminar</button>
</td>
`;
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 = `<input type="text" value="${descripcionActual}" class="input-descripcion">`;
celdas[2].innerHTML = `<input type="number" value="${precioVentaActual}" class="input-precio-venta">`;
celdas[3].innerHTML = `<input type="number" value="${precioCompraActual}" class="input-precio-compra">`;
celdas[4].innerHTML = `<input type="number" value="${stockActual}" class="input-stock">`;
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;
+327
View File
@@ -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 = `
<td>${p.ID_Proveedor}</td>
<td>${p.Nombre}</td>
<td>${p.Telefono}</td>
<td>
<button class="btn-ver-productos" data-id="${p.ID_Proveedor}">Productos</button>
<button class="btn-editar-proveedor" data-id="${p.ID_Proveedor}">Editar</button>
<button class="btn-eliminar-proveedor" data-id="${p.ID_Proveedor}">Eliminar</button>
</td>
`;
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 = `
<td>${prod.ID_Producto}</td>
<td>${prod.Descripcion}</td>
<td>
<button class="btn-eliminar-producto" data-id-producto="${prod.ID_Producto}" data-id-proveedor="${idProveedor}">Eliminar</button>
</td>
`;
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 = `<input type="text" value="${nombreActual}" class="input-nombre">`;
celdas[2].innerHTML = `<input type="text" value="${telefonoActual}" class="input-telefono">`;
// 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.");
});
}
+673
View File
@@ -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 = "<tr><td colspan='9'>No se encontraron resultados.</td></tr>";
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 = `
<td>${r.Nombre}</td>
<td>${r.DNI}</td>
<td>${r.Telefono}</td>
<td>${new Date(r.Fecha_Hora).toLocaleString()}</td>
<td>${tipoTexto}</td>
<td>${r.Numero}</td>
<td>${parseFloat(r.Descuento_Cupon) > 0 ? (parseFloat(r.Descuento_Cupon) * 100).toFixed(0) + '%' : '0%'}</td>
<td>$${parseFloat(r.Monto).toFixed(2)}</td>
<td>
<div class="estado-dropdown">
<button class="estado-btn ${disabledClass}" data-estado="${estadoValor}" ${disabledAttr} title="${esDelPasado ? 'No se pueden modificar reservas pasadas' : ''}">
<span class="estado-texto">${estadoTexto}</span>
<span class="estado-flecha">▼</span>
</button>
<div class="estado-opciones" style="display: none;">
<div class="estado-opcion" data-valor="0">No Pagado</div>
<div class="estado-opcion" data-valor="1">Pagado</div>
<div class="estado-opcion" data-valor="2">Cancelado</div>
</div>
</div>
<button class="reprogramar-btn ${disabledClass}" ${disabledAttr} title="Reprogramar reserva">Reprogramar</button>
<div class="reprogramar-container" style="display:none;"></div>
</td>
`;
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 = `
<h3 class="modal-title">Elige un día</h3>
<div class="dias-grid"></div>
<h3 class="modal-title">Selecciona horario</h3>
<div class="horarios-grid"></div>
<h3 class="modal-title">Selecciona cancha</h3>
<div class="canchas-grid"></div>
<div class="reprogramar-actions">
<button class="btn-cancelar">Cancelar</button>
<button class="btn-confirmar" disabled>Confirmar</button>
</div>
`;
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 = '<p>Error: No se pudieron cargar los datos de las canchas</p>';
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 = '<p>Error: No se pudieron cargar los datos</p>';
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 = '<p>Error al cargar canchas disponibles</p>';
}
}
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);
});
+208
View File
@@ -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 = `
<td>${p.id}</td>
<td>${p.descripcion}</td>
<td>$${p.precio.toFixed(2)}</td>
<td>${p.cantidad}</td>
<td>$${p.subtotal.toFixed(2)}</td>
<td>
<button class="btn-eliminar" onclick="eliminarProductoVenta(${index})">Eliminar</button>
</td>
`;
tbody.appendChild(fila);
});
document.getElementById("total").value = `$${total.toFixed(2)}`;
}
function eliminarProductoVenta(index) {
productos.splice(index, 1);
actualizarTabla();
}
+43
View File
@@ -0,0 +1,43 @@
<?php
include 'conexion.php';
// Leer el JSON enviado por JS
$json = file_get_contents('php://input');
$data = json_decode($json, true);
if (!$data) {
die("No se recibieron datos.");
}
$tipo = $data['tipo'];
$precio = $data['precio'];
$duracion = $data['duracion'];
$cantidad = $data['cantidad'];
$res_cupon = $data['reservas_cupon'];
$descuento = floatval(str_replace(',', '.', (string)($data['descuento'] ?? 0)));
if ($descuento > 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);
}
?>
+32
View File
@@ -0,0 +1,32 @@
<?php
include 'conexion.php';
// Leer el JSON enviado
$json = file_get_contents('php://input');
$data = json_decode($json, true);
if (!$data || !isset($data['id_reserva']) || !isset($data['estado'])) {
echo json_encode(['success' => 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);
?>
+106
View File
@@ -0,0 +1,106 @@
<?php
require_once 'conexion.php';
header('Content-Type: application/json');
try {
$data = json_decode(file_get_contents('php://input'), true);
// Validar datos
if (!isset($data['gastos']) || empty($data['gastos'])) {
throw new Exception("No hay datos de gastos para guardar");
}
$gastos = $data['gastos'];
$fecha_hoy = date('Y-m-d');
// Buscar el registro actual (donde Periodo_Hasta IS NULL)
$sql_get_actual = "SELECT ID_Gasto 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) {
$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()
]);
}
?>
+48
View File
@@ -0,0 +1,48 @@
<?php
include 'conexion.php';
header('Content-Type: application/json; charset=UTF-8');
$json = trim(file_get_contents('php://input'));
if ($json === '') {
echo json_encode(['success' => 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);
?>
+18
View File
@@ -0,0 +1,18 @@
<?php
include 'conexion.php';
$datos = json_decode(file_get_contents("php://input"), true);
$error = false;
foreach ($datos as $h) {
$dia = $h['dia'];
$ape = $h['ape'];
$cie = $h['cie'];
$sql = "UPDATE horarios SET Hora_Apertura = '$ape', Hora_Cierre = '$cie' WHERE Dia = '$dia'";
if (!mysqli_query($conexion, $sql)) {
$error = true;
}
}
echo $error ? "Error al actualizar algunos horarios" : "Horarios actualizados correctamente";
?>
+136
View File
@@ -0,0 +1,136 @@
<?php
header('Content-Type: application/json');
mysqli_report(MYSQLI_REPORT_OFF);
$data = json_decode(file_get_contents('php://input'), true);
require_once 'conexion.php';
if ($conexion->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();
?>
+54
View File
@@ -0,0 +1,54 @@
<?php
session_start();
header('Content-Type: application/json');
if (!isset($_SESSION['id_usuario'])) {
echo json_encode(['success' => 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();
?>
+46
View File
@@ -0,0 +1,46 @@
<?php
header('Content-Type: application/json');
$data = json_decode(file_get_contents('php://input'), true);
require_once 'conexion.php';
if ($conexion->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.']);
?>
+55
View File
@@ -0,0 +1,55 @@
<?php
session_start();
require_once 'conexion.php';
header('Content-Type: application/json');
if ($_SERVER["REQUEST_METHOD"] != "POST") {
echo json_encode(['success' => 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();
?>
+15
View File
@@ -0,0 +1,15 @@
<?php
$data = json_decode(file_get_contents("php://input"), true);
require_once 'conexion.php';
$descripcion = $data['descripcion'];
$precio_venta = $data['precio_venta'];
$precio_compra = $data['precio_compra'];
$cantidad = $data['cantidad'];
$stmt = $conexion->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.";
?>
+18
View File
@@ -0,0 +1,18 @@
<?php
header('Content-Type: application/json');
require_once 'conexion.php';
$data = json_decode(file_get_contents("php://input"), true);
$idProveedor = $data['id_proveedor'] ?? 0;
$idProducto = $data['id_producto'] ?? 0;
$stmt = $conexion->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]);
}
?>
+26
View File
@@ -0,0 +1,26 @@
<?php
header('Content-Type: application/json');
if (!isset($_GET['codigo'])) {
http_response_code(400);
echo json_encode(["error" => "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 ?: []);
?>
+41
View File
@@ -0,0 +1,41 @@
<?php
require_once 'conexion.php';
if (!isset($_GET['codigo']) || empty($_GET['codigo'])) {
echo json_encode(null);
exit;
}
$busqueda = trim($_GET['codigo']);
$terminoLike = "%" . $busqueda . "%";
// Detectamos si la búsqueda es un número (ID) o texto (descripción)
$esNumero = is_numeric($busqueda);
if ($esNumero) {
// Búsqueda por ID exacto
$sql = "SELECT ID_Producto, Descripcion, Precio_Venta, Stock_Disponible
FROM productos
WHERE ID_Producto = ?
LIMIT 1";
$stmt = $conexion->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);
?>
+12
View File
@@ -0,0 +1,12 @@
<?php
$codigo = $_GET['codigo'];
require_once 'conexion.php';
$stmt = $conexion->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());
?>
+12
View File
@@ -0,0 +1,12 @@
<?php
$codigo = $_GET['id'];
require_once 'conexion.php';
$stmt = $conexion->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());
?>
+91
View File
@@ -0,0 +1,91 @@
<?php
session_start();
header('Content-Type: application/json');
if (!isset($_SESSION['id_usuario'])) {
echo json_encode(['success' => 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();
?>
+11
View File
@@ -0,0 +1,11 @@
<?php
session_start();
if (!isset($_SESSION['rol'])) {
header("Location: ../html/Login.html");
exit();
}
$rol = $_SESSION['rol']; // admin, cancha, cantina
$usuario = $_SESSION['usuario'] ?? '';
$usuarioId = $_SESSION['id_usuario'] ?? null;
include __DIR__ . '/../html/Cantina.html';
?>
+126
View File
@@ -0,0 +1,126 @@
<?php
require_once 'conexion.php';
header('Content-Type: application/json');
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['columna_actual']) || !isset($data['columna_nueva']) || !isset($data['monto'])) {
throw new Exception("Datos incompletos");
}
$columna_actual = trim($data['columna_actual']);
$columna_nueva = trim($data['columna_nueva']);
$monto = floatval($data['monto']);
$fecha_hoy = date('Y-m-d'); // Necesitamos la fecha de hoy para los periodos
// Validar datos
if (empty($columna_actual) || empty($columna_nueva) || $monto < 0) {
throw new Exception("Datos inválidos");
}
// Convertir nuevo nombre a formato de columna
$nombre_columna = strtolower(str_replace(' ', '_', $columna_nueva));
// Validar que sea un nombre válido para columna SQL
if (!preg_match('/^[a-z_][a-z0-9_]*$/', $nombre_columna)) {
throw new Exception("Nombre de gasto inválido");
}
// Si el nombre cambió, renombrar la columna primero
if (strtolower(str_replace(' ', '_', $columna_actual)) !== $nombre_columna) {
$columna_actual_formatted = strtolower(str_replace(' ', '_', $columna_actual));
// Verificar que la columna actual existe
$sql_check = "SHOW COLUMNS FROM gastos LIKE '" . $conexion->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()
]);
}
?>
+27
View File
@@ -0,0 +1,27 @@
<?php
header("Content-Type: application/json");
$data = json_decode(file_get_contents("php://input"), true);
if (!$data || !isset($data["id"])) {
echo json_encode(["success" => 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]);
}
+54
View File
@@ -0,0 +1,54 @@
<?php
header('Content-Type: application/json');
error_reporting(E_ALL);
ini_set("display_errors", 1);
require_once 'conexion.php';
if ($conexion->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();
+48
View File
@@ -0,0 +1,48 @@
<?php
require_once 'conexion.php';
header('Content-Type: application/json');
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['columna'])) {
throw new Exception("Columna no especificada");
}
$columna = trim($data['columna']);
// Validar que sea un nombre válido
if (empty($columna)) {
throw new Exception("Columna inválida");
}
// Convertir nombre a formato de columna
$nombre_columna = strtolower(str_replace(' ', '_', $columna));
// Verificar que la columna existe
$sql_check = "SHOW COLUMNS FROM gastos LIKE '" . $conexion->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()
]);
}
?>
+68
View File
@@ -0,0 +1,68 @@
<?php
if (!isset($_GET['id'])) {
http_response_code(400);
echo json_encode(['success' => 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.']);
}
}
}
?>
+15
View File
@@ -0,0 +1,15 @@
<?php
header('Content-Type: application/json');
require_once 'conexion.php';
$idProveedor = $_GET['id_proveedor'] ?? 0;
$idProducto = $_GET['id_producto'] ?? 0;
$stmt = $conexion->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]);
}
+73
View File
@@ -0,0 +1,73 @@
<?php
ini_set('display_errors', '0');
error_reporting(0);
header('Content-Type: application/json; charset=utf-8');
require_once 'conexion.php';
$rawInput = file_get_contents('php://input');
$id = 0;
if ($rawInput) {
$data = json_decode($rawInput, true);
if (json_last_error() === JSON_ERROR_NONE && isset($data['id'])) {
$id = intval($data['id']);
}
}
if (!$id && isset($_POST['id'])) {
$id = intval($_POST['id']);
}
if (!$id && isset($_GET['id'])) {
$id = intval($_GET['id']);
}
if ($conexion->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();
?>
+57
View File
@@ -0,0 +1,57 @@
<?php
session_start();
require_once 'conexion.php';
// Respuesta por defecto
header('Content-Type: application/json');
if ($_SERVER["REQUEST_METHOD"] != "POST") {
echo json_encode(['success' => 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();
?>
+41
View File
@@ -0,0 +1,41 @@
<?php
set_time_limit(120);
header('Content-Type: application/json');
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['telefono']) || empty($data['telefono'])) {
echo json_encode(['success' => 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)'
]);
?>
+88
View File
@@ -0,0 +1,88 @@
<?php
require_once 'conexion.php';
header('Content-Type: application/json');
try {
// 1. OBTENER LAS COLUMNAS DINÁMICAMENTE
// Le preguntamos a la base de datos qué gastos existen actualmente
$sql_columnas = "SHOW COLUMNS FROM gastos";
$result_columnas = $conexion->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()
]);
}
?>
+26
View File
@@ -0,0 +1,26 @@
<?php
header('Content-Type: application/json');
require_once 'conexion.php';
if ($conexion->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();
?>
+225
View File
@@ -0,0 +1,225 @@
<?php
session_start();
require_once 'conexion.php';
header('Content-Type: application/json');
// Obtener datos del POST
$data = json_decode(file_get_contents("php://input"), true);
if (!$data) {
echo json_encode([
'success' => 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()
]);
}
?>
+106
View File
@@ -0,0 +1,106 @@
<?php
session_start();
require_once 'conexion.php';
// Respuesta por defecto
header('Content-Type: application/json');
if ($_SERVER["REQUEST_METHOD"] != "POST") {
echo json_encode(['success' => 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();
?>
+39
View File
@@ -0,0 +1,39 @@
<?php
session_start();
require_once 'conexion.php';
if ($conexion->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();
}
}
?>
+16
View File
@@ -0,0 +1,16 @@
<?php
session_start();
header('Content-Type: application/json');
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params['path'], $params['domain'],
$params['secure'], $params['httponly']
);
}
session_destroy();
echo json_encode(['success' => true]);
?>
+51
View File
@@ -0,0 +1,51 @@
<?php
require_once 'conexion.php';
try {
// Verificar y agregar las columnas necesarias a la tabla gastos
$columnas_necesarias = [
'Sueldo' => '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()]);
}
?>
+45
View File
@@ -0,0 +1,45 @@
<?php
require_once 'conexion.php';
// Verificar si la columna ya existe
$result = $conexion->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();
?>
+28
View File
@@ -0,0 +1,28 @@
<?php
require_once 'conexion.php';
header('Content-Type: application/json');
try {
$sql = "SELECT c.ID_Cancha, c.Tipo, c.Precio, c.Duracion, c.Cant_Canchas,
c.Cant_Reservas_Cupon, c.Descuento_Cupon, c.Cant_Faltas, c.Duracion_Cupon,
h.Hora_Apertura, h.Hora_Cierre
FROM canchas c
LEFT JOIN horarios h ON h.Dia = 'Lunes a Viernes'
WHERE c.Tipo IN ('F', 'P', 'Q')";
$res = mysqli_query($conexion, $sql);
if (!$res) {
throw new Exception("Error en la consulta de canchas: " . mysqli_error($conexion));
}
$canchas = mysqli_fetch_all($res, MYSQLI_ASSOC);
echo json_encode($canchas);
} catch (Exception $e) {
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
?>
+89
View File
@@ -0,0 +1,89 @@
<?php
require_once 'conexion.php';
header('Content-Type: application/json');
$fecha = isset($_GET['fecha']) ? $_GET['fecha'] : '';
$horario_inicio = isset($_GET['horario']) ? $_GET['horario'] : '';
$duracion = isset($_GET['duracion']) ? $_GET['duracion'] : 60;
$tipo = isset($_GET['tipo']) ? $_GET['tipo'] : '';
if (empty($fecha) || empty($horario_inicio) || empty($tipo)) {
echo json_encode([
'success' => 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()
]);
}
?>
+136
View File
@@ -0,0 +1,136 @@
<?php
require_once 'conexion.php';
header('Content-Type: application/json');
$dni = isset($_GET['dni']) ? trim($_GET['dni']) : '';
$tipo = isset($_GET['tipo']) ? trim($_GET['tipo']) : '';
if (empty($dni) || empty($tipo)) {
echo json_encode(['success' => 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
]);
+83
View File
@@ -0,0 +1,83 @@
<?php
require_once 'conexion.php';
header('Content-Type: application/json');
$fecha = isset($_GET['fecha']) ? $_GET['fecha'] : '';
$horario_inicio = isset($_GET['horario']) ? $_GET['horario'] : '';
$duracion = isset($_GET['duracion']) ? $_GET['duracion'] : 60; // duracion en minutos
$tipo = isset($_GET['tipo']) ? $_GET['tipo'] : ''; // 'F' para futbol, 'P' para padel
if (empty($fecha) || empty($horario_inicio) || empty($tipo)) {
echo json_encode([
'success' => 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()
]);
}
?>
+64
View File
@@ -0,0 +1,64 @@
<?php
require_once 'conexion.php';
header('Content-Type: application/json');
$fecha = isset($_GET['fecha']) ? $_GET['fecha'] : '';
if (empty($fecha)) {
echo json_encode([
'success' => 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()
]);
}
?>
+40
View File
@@ -0,0 +1,40 @@
<?php
require_once 'conexion.php';
header('Content-Type: application/json');
try {
// Obtener columnas dinámicas (excluyendo ID_Gasto, Periodo_Desde, Periodo_Hasta)
$sql_columns = "SHOW COLUMNS FROM gastos";
$result_columns = $conexion->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()
]);
}
?>
+7
View File
@@ -0,0 +1,7 @@
<?php
include 'conexion.php';
$res = mysqli_query($conexion, "SELECT * FROM horarios");
$horarios = mysqli_fetch_all($res, MYSQLI_ASSOC);
header('Content-Type: application/json');
echo json_encode($horarios);
?>
+26
View File
@@ -0,0 +1,26 @@
<?php
header('Content-Type: application/json');
require_once 'conexion.php';
if ($conexion->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();
?>
+33
View File
@@ -0,0 +1,33 @@
<?php
require_once 'conexion.php';
// Verificar si la columna Estado existe
$checkColumn = $conexion->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);
?>
+34
View File
@@ -0,0 +1,34 @@
<?php
header('Content-Type: application/json');
require_once 'conexion.php';
if ($conexion->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();
?>
+23
View File
@@ -0,0 +1,23 @@
<?php
header('Content-Type: application/json');
require_once 'conexion.php';
$idProveedor = $_GET['id'] ?? 0;
$stmt = $conexion->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);
+20
View File
@@ -0,0 +1,20 @@
<?php
header('Content-Type: application/json');
require_once 'conexion.php';
if ($conexion->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();
?>
File diff suppressed because it is too large Load Diff
+62
View File
@@ -0,0 +1,62 @@
<?php
require_once 'conexion.php';
header('Content-Type: application/json');
$fecha = isset($_GET['fecha']) ? $_GET['fecha'] : '';
$nombre = isset($_GET['nombre']) ? $_GET['nombre'] : '';
$dni = isset($_GET['dni']) ? $_GET['dni'] : '';
$tipo = isset($_GET['tipo']) ? $_GET['tipo'] : '';
$sql = "SELECT r.ID_Reserva, c.Nombre, c.DNI, c.Telefono, r.Fecha_Hora, ca.Tipo, IFNULL(r.Descuento, 0) as Descuento_Cupon, r.Monto, r.Numero, r.Estado
FROM reservas r
INNER JOIN clientes c ON r.ID_Cliente = c.ID_Cliente
LEFT JOIN canchas ca ON r.ID_Cancha = ca.ID_Cancha
WHERE 1=1";
//Filtro de Fecha
if (!empty($fecha)) {
$f = mysqli_real_escape_string($conexion, $fecha);
$sql .= " AND DATE(r.Fecha_Hora) = '$f'";
}
//Filtro de Nombre
if (!empty($nombre)) {
$n = mysqli_real_escape_string($conexion, $nombre);
$sql .= " AND c.Nombre LIKE '%$n%'";
}
// Filtro de DNI
if (!empty($dni)) {
$d = mysqli_real_escape_string($conexion, $dni);
$sql .= " AND c.DNI LIKE '%$d%'";
}
//Filtro de Tipo
if (!empty($tipo)) {
if ($tipo === 'futbol') {
$sql .= " AND ca.Tipo = 'F'";
} elseif ($tipo === 'padel') {
$sql .= " AND ca.Tipo = 'P'";
} elseif ($tipo === 'quincho') {
$sql .= " AND ca.Tipo = 'Q'";
}
}
$sql .= " ORDER BY r.Fecha_Hora DESC";
$resultado = mysqli_query($conexion, $sql);
$reservas = [];
if (!$resultado) {
die(json_encode(["error" => mysqli_error($conexion)]));
}
if ($resultado) {
while ($fila = mysqli_fetch_assoc($resultado)) {
$reservas[] = $fila;
}
}
header('Content-Type: application/json');
echo json_encode($reservas);
?>
+23
View File
@@ -0,0 +1,23 @@
<?php
session_start();
require_once 'conexion.php';
// Respuesta por defecto
header('Content-Type: application/json');
// Obtener todos los usuarios
$query = "SELECT ID_Usuario, Usuario, Rol, Nombre, Apellido, DNI, Correo, Telefono FROM usuarios ORDER BY Nombre ASC";
$result = $conexion->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();
?>
+114
View File
@@ -0,0 +1,114 @@
<?php
require_once 'conexion.php';
$start = isset($_GET['start']) ? $_GET['start'] : null;
$end = isset($_GET['end']) ? $_GET['end'] : null;
$id_venta = isset($_GET['id_venta']) ? intval($_GET['id_venta']) : null;
$days = isset($_GET['days']) ? intval($_GET['days']) : null;
// Si se solicita una venta específica, devolver sus productos
if ($id_venta !== null) {
$stmt = $conexion->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);
?>
+74
View File
@@ -0,0 +1,74 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
try {
require_once 'conexion.php';
require_once 'config_mail.php';
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['correo']) || empty($data['correo'])) {
echo json_encode(['success' => 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()
]);
}
?>
+93
View File
@@ -0,0 +1,93 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
header('Content-Type: application/json');
try {
require_once 'conexion.php';
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['correo']) || !isset($data['codigo']) || !isset($data['nueva_contrasena'])) {
echo json_encode(['success' => 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()
]);
}
?>
+123
View File
@@ -0,0 +1,123 @@
<?php
require_once 'conexion.php';
header('Content-Type: application/json');
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['nuevo_gasto']) || !isset($data['monto'])) {
throw new Exception("Datos incompletos");
}
$nuevo_gasto = trim($data['nuevo_gasto']);
$monto = floatval($data['monto']);
// Validar nombre del gasto
if (empty($nuevo_gasto) || $monto < 0) {
throw new Exception("Datos inválidos");
}
// Convertir nombre a formato de columna (minúsculas, sin espacios, con guion bajo)
$nombre_columna = strtolower(str_replace(' ', '_', $nuevo_gasto));
// Validar que sea un nombre válido para columna SQL
if (!preg_match('/^[a-z_][a-z0-9_]*$/', $nombre_columna)) {
throw new Exception("Nombre de gasto inválido");
}
// Verificar si la columna ya existe (sin prepared statement porque SHOW COLUMNS no lo soporta bien)
$sql_check = "SHOW COLUMNS FROM gastos LIKE '" . $conexion->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()
]);
}
?>
+20
View File
@@ -0,0 +1,20 @@
<?php
$data = json_decode(file_get_contents("php://input"), true);
require_once 'conexion.php';
// Insertar orden
$stmt = $conexion->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.";
?>
+34
View File
@@ -0,0 +1,34 @@
<?php
session_start();
require_once 'conexion.php';
$productos = json_decode(file_get_contents("php://input"), true);
// Calcular monto total
$monto = 0;
foreach ($productos as $p) {
$monto += $p['subtotal'];
}
// Obtener ID del usuario desde la sesión
$id_usuario = isset($_SESSION['id_usuario']) ? intval($_SESSION['id_usuario']) : null;
// Insertar venta
$stmt = $conexion->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.";
?>
+31
View File
@@ -0,0 +1,31 @@
<?php
require_once 'conexion.php';
if ($conexion->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();
?>
+27
View File
@@ -0,0 +1,27 @@
<?php
session_start();
header('Content-Type: application/json');
if (!isset($_SESSION['id_usuario'])) {
echo json_encode(['success' => 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();
?>
+73
View File
@@ -0,0 +1,73 @@
<?php
header('Content-Type: application/json');
require_once 'conexion.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => 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();
?>
+109
View File
@@ -0,0 +1,109 @@
<?php
set_time_limit(0);
require_once 'conexion.php';
// Rutas base de tu proyecto
$rutaProyecto = "C:\\xampp\\htdocs\\TP-Taller\\ProyectoTaller\\";
$carpetaDestino = $rutaProyecto . "backups\\";
$mysqldump = "C:\\xampp\\mysql\\bin\\mysqldump.exe";
$diasDeRetencion = 30;
// --- GENERACIÓN DEL BACKUP SQL ---
$fecha = date('Y-m-d_H-i-s');
$nombreArchivoSql = "bd_cap1tan_" . $fecha . ".sql";
$rutaSql = $carpetaDestino . $nombreArchivoSql;
$comando = "$mysqldump -h $host -u $user $db > \"$rutaSql\" 2>&1";
exec($comando, $salida, $resultado);
if ($resultado !== 0) {
echo "<h2 style='color: red;'>Error al generar la base de datos. Abortando.</h2>";
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 "<h2 style='color: green;'>¡Éxito! Backup COMPLETO generado correctamente.</h2>";
echo "<p>Ruta: <b>" . $rutaZip . "</b></p>";
} else {
echo "<h2 style='color: red;'>Error al intentar crear el archivo ZIP.</h2>";
}
// --- 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 "<hr>";
if ($borrados > 0) {
echo "<p>Limpieza completada: Se eliminaron $borrados backup(s) viejo(s).</p>";
} else {
echo "<p>Mantenimiento al día: Ningún backup supera los $diasDeRetencion días.</p>";
}
?>
+13
View File
@@ -0,0 +1,13 @@
<?php
$host = 'localhost';
$user = 'root';
$password = 'admin';
$db = 'complejo_cap1tan';
$conexion = new mysqli($host, $user, $password, $db);
if ($conexion->connect_error) {
die('Error de conexión: ' . $conexion->connect_error);
}
$conexion->set_charset('utf8');
?>
+90
View File
@@ -0,0 +1,90 @@
<?php
/**
* Configuración de Brevo SMTP
* Este archivo contiene las credenciales para enviar emails vía Brevo
*/
// Credenciales de Brevo
define('BREVO_API_KEY', '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');
define('BREVO_API_URL', 'https://api.brevo.com/v3/smtp/email');
define('BREVO_FROM_EMAIL', 'lumicaela2.987@gmail.com');
define('BREVO_FROM_NAME', 'Complejo Cap1tan');
/**
* Función para enviar emails usando API REST de Brevo
* Esta es más confiable que SMTP directo
*/
function enviarEmailBrevo($para, $asunto, $mensaje, $esHTML = false) {
try {
// Preparar el payload
$payload = [
'sender' => [
'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);
}
?>
+51
View File
@@ -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()
+60
View File
@@ -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;
+234
View File
@@ -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);
+137
View File
@@ -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
);
+45
View File
@@ -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 ;