Primer subida completa

This commit is contained in:
nickpons666
2026-01-19 15:20:36 -06:00
commit 85894619d8
146 changed files with 3620 additions and 0 deletions

10
.env Executable file
View File

@@ -0,0 +1,10 @@
DB_HOST=10.10.4.17:3391
DB_NAME=contenedor_ibiza
DB_USER=nickpons666
DB_PASS=MiPo6425@@
SITE_URL=https://contenedor-ibiza.ddns.net
TELEGRAM_BOT_TOKEN=8589698394:AAFSphFBEy1DQmOIUDyEKCMAwksTaYlatYE
SESSION_NAME=contenedor_session

10
.env.example Executable file
View File

@@ -0,0 +1,10 @@
DB_HOST=localhost
DB_NAME=contenedor_ibiza
DB_USER=root
DB_PASS=
SITE_URL=http://localhost:8080
TELEGRAM_BOT_TOKEN=tu_token_aqui
SESSION_NAME=contenedor_session

3
.gitignore vendored Executable file
View File

@@ -0,0 +1,3 @@
# Logs
logs/
*.log

256
README.md Executable file
View File

@@ -0,0 +1,256 @@
# Sistema de Administración de Apertura y Cierre de Contenedor
## Descripción
Sistema web en PHP + MySQL para administrar la apertura y cierre de un contenedor de basura, asignando turnos semanales a personas con horarios configurables.
## Requisitos
- PHP 7.4 o superior
- MySQL 5.7 o superior
- Extensión PHP PDO MySQL
- Composer (opcional)
- Servidor web (Apache/Nginx/PHP built-in)
## Instalación
### 1. Clonar o copiar los archivos
Copiar todos los archivos del proyecto al directorio del servidor web.
### 2. Configurar variables de entorno
Copiar el archivo de ejemplo:
```bash
cp .env.example .env
```
Editar el archivo `.env` con la configuración correcta:
```env
DB_HOST=localhost
DB_NAME=contenedor_ibiza
DB_USER=root
DB_PASS=tu_password
SITE_URL=http://localhost:8080
TELEGRAM_BOT_TOKEN=tu_token_de_telegram
```
### 3. Crear la base de datos
Ejecutar el script SQL:
```bash
mysql -u root -p < sql/schema.sql
```
O importar el archivo `sql/schema.sql` desde phpMyAdmin o cualquier cliente MySQL.
### 4. Configurar el servidor web
#### Con PHP built-in (desarrollo):
```bash
php -S localhost:8080 -t public
```
#### Con Apache:
Configurar el document root hacia la carpeta `public/`.
#### Con Nginx:
```nginx
server {
listen 80;
server_name localhost;
root /path/to/project/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/var/run/php/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
```
### 5. Credenciales de acceso
- **Email:** admin@ibiza.com
- **Contraseña:** admin123
## Estructura del Proyecto
```
contenedor/
├── .env.example # Variables de entorno
├── config/
│ └── config.php # Configuración general
├── public/ # Archivos públicos
│ ├── index.php # Redirección según rol
│ ├── login.php # Página de login
│ ├── logout.php # Cierre de sesión
│ ├── ayudante.php # Panel del ayudante
│ ├── admin/ # Panel de administración
│ │ ├── index.php # Dashboard
│ │ ├── usuarios.php # CRUD de usuarios
│ │ ├── horarios.php # Configuración de horarios
│ │ ├── asignaciones.php # Asignación de turnos con reordenamiento
│ │ └── webhook.php # Configuración del webhook del bot
│ └── partials/ # Compartials/
│ └── navbar.php # Barra de navegación
├── src/ # Código fuente
│ ├── Database.php # Conexión a BD
│ ├── Auth.php # Autenticación
│ ├── User.php # Modelo de usuarios
│ ├── DiasHorarios.php # Modelo de horarios
│ └── Asignacion.php # Modelo de asignaciones y rotación automática
├── bot/ # Bot de Telegram
│ ├── TelegramBot.php # Clase del bot con teclado de menú
│ ├── webhook.php # Webhook del bot
│ └── setup_webhook.php # Script CLI para configurar webhook
├── sql/
│ └── schema.sql # Esquema de base de datos
├── scripts/ # Scripts CLI
│ └── rotar_automatico.php # Script de rotación automática
└── assets/ # Recursos estáticos
├── css/
└── js/
```
## Configuración del Bot de Telegram
### 1. Crear un bot
1. Talking to @BotFather on Telegram
2. Send `/newbot` to create a new bot
3. Follow the instructions to set a name and username
4. Copy the API token
### 2. Configurar el webhook
El webhook puede configurarse de tres formas:
**Opción A: Desde el panel de administración**
1. Ir a Administración > Webhook
2. Ingresar la URL del bot
3. Click en "Configurar Webhook"
**Opción B: Con script CLI**
```bash
php bot/setup_webhook.php
```
**Opción C: Manualmente**
```
https://api.telegram.org/bot<TOKEN>/setWebhook?url=https://tu-dominio.com/bot/webhook.php
```
### 3. Usar el bot
El bot tiene un teclado de menú con las siguientes opciones:
- **📅 Ver Turnos** - Muestra todos los turnos de la semana actual
- **🔍 ¿Qué Semana Es?** - Indica la semana actual del ciclo de rotación
- **📋 Ayuda** - Muestra los comandos disponibles
**Comandos disponibles:**
- `/start` - Iniciar el bot
- `/menu` - Mostrar el teclado de menú
- `/turnos` - Ver turnos de la semana actual
- `/semana` - Ver qué semana es del ciclo de rotación
## Configuración de Rotación Automática
Para configurar la rotación automática de turnos cada semana, agregar al crontab:
```bash
# Rotar turnos cada domingo a las 00:00
0 0 * * 0 /usr/bin/php /path/to/project/scripts/rotar_automatico.php >> /var/log/rotacion.log 2>&1
```
O ejecutar manualmente:
```bash
php scripts/rotar_automatico.php
```
## Funcionalidades
### Administrador
- Crear, editar y desactivar usuarios
- Asignar turnos por semana con selector agrupado por mes
- Visualizar asignaciones con contador "Semana X de 4"
- **Reordenar rotación** - Arrastrar y soltar para cambiar el orden de la rotación
- Recálculo automático de asignaciones futuras al reordenar
- Configurar días activos de apertura
- Configurar horarios de apertura y cierre por día
- Visualizar todas las asignaciones
- Rotar turnos manualmente
- Configurar webhook del bot de Telegram
### Ayudante
- Visualizar únicamente los días y horarios que le corresponden
- Ver información de su turno actual
- **Calendario de Turnos** - Tabla con todos los turnos de las próximas 5 semanas
### Bot de Telegram
- **Teclado de menú** con botones interactivos
- `/start` - Iniciar conversación con el bot
- `/menu` - Mostrar teclado de opciones
- `/turnos` - Ver todos los turnos de la semana actual
- `/semana` - Ver qué semana es del ciclo de rotación (1-4)
- Configuración de webhook desde el panel de administración
## Personalización
### Agregar más personas
1. Ir al panel de administración > Usuarios
2. Click en "Nuevo Usuario"
3. Completar los datos
### Modificar horarios
1. Ir al panel de administración > Horarios
2. Modificar las horas de apertura y cierre
3. Activar/desactivar días según necesidad
### Cambiar configuración de turnos
Los turnos pueden asignarse manualmente desde el panel de administración > Asignaciones.
## Seguridad
- Las contraseñas se almacenan usando `password_hash()` de PHP
- Las sesiones son únicas por usuario
- Los accesos están protegidos por autenticación
- Las consultas usan prepared statements para prevenir SQL injection
## Solución de Problemas
### Error de conexión a la base de datos
- Verificar las credenciales en el archivo `.env`
- Asegurarse de que MySQL esté ejecutándose
### El bot de Telegram no responde
- Verificar que el token sea correcto en `.env`
- Confirmar que el webhook esté configurado correctamente
- Usar el panel de administración > Webhook para verificar
- Revisar los logs del servidor
### Error "Todos los campos son obligatorios" al guardar horarios
- Verificar que las variables de horario coincidan con los nombres del formulario
### Error en rotación de turnos
- Verificar que exista al menos un usuario activo
- Verificar que la tabla `asignaciones_turnos` tenga registros previos
- El ciclo de rotación inicia el domingo 28 de diciembre de 2024

206
bot/TelegramBot.php Executable file
View File

@@ -0,0 +1,206 @@
<?php
require_once __DIR__ . '/../config/config.php';
require_once __DIR__ . '/../src/Asignacion.php';
require_once __DIR__ . '/../src/User.php';
class TelegramBot {
private $token;
private $apiUrl;
public function __construct() {
$config = require __DIR__ . '/../config/config.php';
$this->token = $config['telegram_bot_token'];
$this->apiUrl = "https://api.telegram.org/bot{$this->token}";
}
private function request($method, $data = []) {
$url = "{$this->apiUrl}/{$method}";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
public function sendMessage($chatId, $text, $parseMode = 'HTML', $replyMarkup = null) {
$data = [
'chat_id' => $chatId,
'text' => $text,
'parse_mode' => $parseMode
];
if ($replyMarkup) {
$data['reply_markup'] = $replyMarkup;
}
return $this->request('sendMessage', $data);
}
public function getMe() {
return $this->request('getMe');
}
public function getUpdates($offset = 0) {
return $this->request('getUpdates', ['offset' => $offset, 'timeout' => 60]);
}
public function sendKeyboard($chatId, $text) {
$keyboard = [
'inline_keyboard' => [
[
['text' => 'Ver Turnos', 'callback_data' => 'ver_turnos'],
['text' => 'Semana Actual', 'callback_data' => 'semana_actual']
],
[
['text' => '🔍 Buscar por Nombre', 'callback_data' => 'buscar_nombre']
]
]
];
return $this->sendMessage($chatId, $text, 'HTML', json_encode($keyboard));
}
public function answerCallback($callbackId, $text, $showAlert = false) {
return $this->request('answerCallbackQuery', [
'callback_query_id' => $callbackId,
'text' => $text,
'show_alert' => $showAlert
]);
}
public function deleteMessage($chatId, $messageId) {
return $this->request('deleteMessage', [
'chat_id' => $chatId,
'message_id' => $messageId
]);
}
public function editMessage($chatId, $messageId, $text, $keyboard = null) {
$data = [
'chat_id' => $chatId,
'message_id' => $messageId,
'text' => $text,
'parse_mode' => 'HTML'
];
if ($keyboard) {
$data['reply_markup'] = $keyboard;
}
return $this->request('editMessageText', $data);
}
public function getTablaTurnos($semanas = 4) {
$asignacion = new Asignacion();
$ayudantes = $asignacion->getAyudantesPorOrden();
if (empty($ayudantes)) {
return "No hay ayudantes configurados.";
}
$hoy = new DateTime();
$diaSemana = (int)$hoy->format('w');
$domingo = clone $hoy;
$domingo->modify('-' . $diaSemana . ' days');
$tabla = "<b>TABLA DE TURNOS</b>\n\n";
$tabla .= "<code>";
$tabla .= str_pad("Semana", 10) . " | " . str_pad("Ayudante", 12) . " | Periodo\n";
$tabla .= str_repeat("-", 45) . "\n";
for ($i = 0; $i < $semanas; $i++) {
$domingoSemana = clone $domingo;
$domingoSemana->modify("+{$i} weeks");
$viernesSemana = clone $domingoSemana;
$viernesSemana->modify('+5 days');
$posicion = ($i % count($ayudantes));
$ayudante = $ayudantes[$posicion];
$semanaNum = $i + 1;
$periodo = $domingoSemana->format('d/m') . '-' . $viernesSemana->format('d/m');
$tabla .= str_pad("Sem $semanaNum", 10) . " | " .
str_pad(substr($ayudante['nombre'], 0, 10), 12) . " | $periodo\n";
}
$tabla .= "</code>";
$tabla .= "\n\nCiclo: " . implode(' -> ', array_column($ayudantes, 'nombre'));
return $tabla;
}
public function getSemanaActual() {
$asignacion = new Asignacion();
$hoy = new DateTime();
$diaSemana = (int)$hoy->format('w');
$domingo = clone $hoy;
$domingo->modify('-' . $diaSemana . ' days');
$asignacionActual = $asignacion->getAsignacionPorSemana($domingo->format('Y-m-d'));
if ($asignacionActual) {
return "<b>SEMANA ACTUAL</b>\n\n" .
"Asignado: <b>{$asignacionActual['nombre']}</b>\n" .
"Periodo: " . date('d/m/Y', strtotime($asignacionActual['semana_inicio'])) .
" - " . date('d/m/Y', strtotime($asignacionActual['semana_fin'])) . "\n" .
"Dias: Domingo a Viernes";
} else {
return "No hay asignacion para esta semana.";
}
}
public function getTurnosAyudante($nombre) {
$config = require __DIR__ . '/../config/config.php';
try {
$pdo = new PDO(
"mysql:host={$config['db']['host']};port={$config['db']['port']};dbname={$config['db']['database']};charset=utf8mb4",
$config['db']['username'],
$config['db']['password'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
} catch (Exception $e) {
return "Error de conexion.";
}
$stmt = $pdo->prepare("SELECT * FROM users WHERE nombre LIKE ? AND rol = 'ayudante' AND activo = 1 LIMIT 1");
$stmt->execute(["%$nombre%"]);
$user = $stmt->fetch();
if (!$user) {
$ayudantes = (new Asignacion())->getAyudantesPorOrden();
$nombres = implode(', ', array_map(fn($a) => $a['nombre'], $ayudantes));
return "No encontre '$nombre'.\n\nAyudantes: $nombres";
}
$stmt = $pdo->prepare("
SELECT semana_inicio, semana_fin
FROM asignaciones_turnos
WHERE user_id = ? AND semana_inicio >= CURDATE()
ORDER BY semana_inicio
LIMIT 4
");
$stmt->execute([$user['id']]);
$turnos = $stmt->fetchAll();
if (empty($turnos)) {
return "{$user['nombre']} no tiene turnos proximos.";
}
$result = "<b>TURNOS DE {$user['nombre']}</b>\n\n";
foreach ($turnos as $turno) {
$result .= date('d/m/Y', strtotime($turno['semana_inicio'])) .
" - " . date('d/m/Y', strtotime($turno['semana_fin'])) . "\n";
}
return $result;
}
public function getListaAyudantesParaBusqueda() {
$ayudantes = (new Asignacion())->getAyudantesPorOrden();
return array_map(fn($a) => $a['nombre'], $ayudantes);
}
}

49
bot/setup_webhook.php Executable file
View File

@@ -0,0 +1,49 @@
#!/usr/bin/env php
<?php
/**
* Script para configurar el webhook de Telegram
*
* Uso: php setup_webhook.php <url_webhook>
*
* Ejemplo: php setup_webhook.php https://contenedor-test.local:82/bot/webhook.php
*
* Requiere: TELEGRAM_BOT_TOKEN configurado en .env
*/
require_once __DIR__ . '/../config/config.php';
$config = require __DIR__ . '/../config/config.php';
$token = $config['telegram_bot_token'];
if (empty($token)) {
echo "ERROR: TELEGRAM_BOT_TOKEN no configurado en .env\n";
exit(1);
}
$urlWebhook = $argv[1] ?? "https://contenedor-test.local:82/bot/webhook.php";
echo "Configurando webhook...\n";
echo "URL: {$urlWebhook}\n\n";
$url = "https://api.telegram.org/bot{$token}/setWebhook";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'url' => $urlWebhook,
'allowed_updates' => ['message', 'callback_query']
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
if ($result && $result['ok']) {
echo "Webhook configurado correctamente!\n\n";
echo "Ahora ve a Telegram y envia /start al bot\n";
} else {
echo "Error al configurar webhook:\n";
print_r($result);
exit(1);
}

127
bot/webhook.php Executable file
View File

@@ -0,0 +1,127 @@
<?php
// Configurar logging de errores
error_reporting(E_ALL);
ini_set('log_errors', 1);
ini_set('error_log', __DIR__ . '/../logs/bot_error.log');
require_once __DIR__ . '/../config/config.php';
require_once __DIR__ . '/TelegramBot.php';
class TurnoBot {
private $bot;
private $config;
public function __construct() {
$this->config = require __DIR__ . '/../config/config.php';
$this->bot = new TelegramBot();
}
public function handleUpdate($update) {
try {
// Manejar callback de botones inline
if (isset($update['callback_query'])) {
$this->handleCallback($update['callback_query']);
return;
}
// Manejar mensajes normales
if (!isset($update['message'])) {
return;
}
$message = $update['message'];
$chatId = $message['chat']['id'];
$text = trim($message['text'] ?? '');
if (empty($text)) {
return;
}
$textLower = mb_strtolower($text, 'UTF-8');
// Comandos
if ($textLower === '/start' || $textLower === '/menu' || $textLower === 'menu') {
$this->sendMenu($chatId);
} elseif ($textLower === '/turnos' || $textLower === 'turnos') {
$this->bot->sendMessage($chatId, $this->bot->getTablaTurnos(8));
} elseif ($textLower === '/semana' || $textLower === 'semana' || $textLower === 'hoy') {
$this->bot->sendMessage($chatId, $this->bot->getSemanaActual());
} elseif ($textLower === '/ayudantes' || $textLower === 'ayudantes') {
$ayudantes = $this->bot->getListaAyudantesParaBusqueda();
$this->bot->sendMessage($chatId, "<b>AYUDANTES DISPONIBLES:</b>\n\n" . implode("\n", $ayudantes));
} else {
// Buscar por nombre
$this->bot->sendMessage($chatId, $this->bot->getTurnosAyudante($text));
}
} catch (Exception $e) {
error_log("Error en handleUpdate: " . $e->getMessage());
if (isset($update['message']['chat']['id'])) {
$this->bot->sendMessage($update['message']['chat']['id'], "Error: " . $e->getMessage());
}
}
}
private function handleCallback($callback) {
try {
$callbackId = $callback['id'];
$data = $callback['data'];
$message = $callback['message'];
$chatId = $message['chat']['id'];
$messageId = $message['message_id'];
switch ($data) {
case 'ver_turnos':
$this->bot->answerCallback($callbackId, 'Cargando turnos...');
$this->bot->editMessage($chatId, $messageId, $this->bot->getTablaTurnos(8));
break;
case 'semana_actual':
$this->bot->answerCallback($callbackId, 'Cargando semana actual...');
$this->bot->editMessage($chatId, $messageId, $this->bot->getSemanaActual());
break;
case 'buscar_nombre':
$this->bot->answerCallback($callbackId, '');
$this->bot->deleteMessage($chatId, $messageId);
$this->bot->sendMessage($chatId, "🔍 <b>Buscar por Nombre</b>\n\nEscribe el nombre del ayudante que buscas:");
break;
case 'mi_turno':
$this->bot->answerCallback($callbackId, 'Enviando tu turno...');
$this->bot->editMessage($chatId, $messageId, "Por favor ingresa tu nombre para ver tu turno:");
break;
default:
$this->bot->answerCallback($callbackId, 'Opcion no reconocida');
}
} catch (Exception $e) {
error_log("Error en handleCallback: " . $e->getMessage());
}
}
private function sendMenu($chatId) {
$mensaje = "<b>BOT DE TURNOS - CONTENEDOR IBIZA</b>\n\n";
$mensaje .= "Selecciona una opcion del menu:\n\n";
$mensaje .= "Ver Turnos - Tabla completa de asignaciones\n";
$mensaje .= "Semana Actual - Quien tiene turno esta semana\n";
$mensaje .= "Buscar por Nombre - Consultar un ayudante especifico\n";
$mensaje .= "Mi Turno - Ver tu proximo turno";
$this->bot->sendKeyboard($chatId, $mensaje);
}
}
// Recibir actualización
$update = json_decode(file_get_contents('php://input'), true);
// Log para debugging
error_log("Webhook recibido: " . json_encode($update));
if ($update) {
$bot = new TurnoBot();
$bot->handleUpdate($update);
} else {
http_response_code(200);
echo "Webhook activo. Usa /start para ver el menu.";
}

35
config/config.php Executable file
View File

@@ -0,0 +1,35 @@
<?php
require_once __DIR__ . '/error_logging.php';
$envFile = dirname(__DIR__) . '/.env';
if (file_exists($envFile)) {
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) continue;
if (strpos($line, '=') !== false) {
list($key, $value) = explode('=', $line, 2);
$key = trim($key);
$value = trim($value);
$_ENV[$key] = $value;
putenv("$key=$value");
}
}
}
if (!defined('BASE_PATH')) {
define('BASE_PATH', dirname(__DIR__));
}
return [
'db' => [
'host' => getenv('DB_HOST') ?: 'localhost',
'port' => getenv('DB_PORT') ?: '3306',
'database' => getenv('DB_NAME') ?: 'contenedor_ibiza',
'username' => getenv('DB_USER') ?: 'root',
'password' => getenv('DB_PASS') ?: '',
],
'site_url' => getenv('SITE_URL') ?: 'http://localhost:8080',
'telegram_bot_token' => getenv('TELEGRAM_BOT_TOKEN') ?: '',
'session_name' => getenv('SESSION_NAME') ?: 'contenedor_session',
];

58
config/error_logging.php Executable file
View File

@@ -0,0 +1,58 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
$logFile = __DIR__ . '/../logs/error.log';
if (!file_exists(dirname($logFile))) {
mkdir(dirname($logFile), 0755, true);
}
ini_set('error_log', $logFile);
set_error_handler(function($errno, $errstr, $errfile, $errline) use ($logFile) {
$timestamp = date('Y-m-d H:i:s');
$errorType = match($errno) {
E_ERROR => 'ERROR',
E_WARNING => 'WARNING',
E_PARSE => 'PARSE',
E_NOTICE => 'NOTICE',
E_CORE_ERROR => 'CORE_ERROR',
E_CORE_WARNING => 'CORE_WARNING',
E_COMPILE_ERROR => 'COMPILE_ERROR',
E_COMPILE_WARNING => 'COMPILE_WARNING',
E_USER_ERROR => 'USER_ERROR',
E_USER_WARNING => 'USER_WARNING',
E_USER_NOTICE => 'USER_NOTICE',
E_STRICT => 'STRICT',
E_RECOVERABLE_ERROR => 'RECOVERABLE_ERROR',
E_DEPRECATED => 'DEPRECATED',
E_USER_DEPRECATED => 'USER_DEPRECATED',
default => 'UNKNOWN'
};
$message = "[$timestamp] PHP $errorType: $errstr in $errfile on line $errline";
error_log($message . PHP_EOL, 3, $logFile);
return false;
});
set_exception_handler(function($exception) use ($logFile) {
$timestamp = date('Y-m-d H:i:s');
$message = "[$timestamp] UNCAUGHT EXCEPTION: " . get_class($exception) . ": " .
$exception->getMessage() . " in " . $exception->getFile() .
" on line " . $exception->getLine() . "\n" . $exception->getTraceAsString();
error_log($message . PHP_EOL, 3, $logFile);
});
register_shutdown_function(function() use ($logFile) {
$error = error_get_last();
if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
$timestamp = date('Y-m-d H:i:s');
$message = "[$timestamp] FATAL: " . $error['message'] . " in " .
$error['file'] . " on line " . $error['line'];
error_log($message . PHP_EOL, 3, $logFile);
}
});

45
crontab_config.txt Executable file
View File

@@ -0,0 +1,45 @@
# ============================================
# CONFIGURACIÓN DE CRONTAB PARA ROTACIÓN AUTOMÁTICA
# Sistema de Contenedor Ibiza
# ============================================
# Opción 1: Ejecutar todos los domingos a las 00:00 (medianoche)
# ----------------------------------------------------------------
0 0 * * 0 cd /var/www/html/contenedor && php scripts/rotar_automatico.php >> /var/log/rotacion.log 2>&1
# ============================================
# EXPLICACIÓN:
# ============================================
# 0 0 * * 0 = Segundos Minutos Horas DíaDelMes Mes DíaDeSemana
# │ │ │ │ │ │
# │ │ │ │ │ └─ Domingo (0-7, 0 y 7 son domingo)
# │ │ │ │ └────── Mes (1-12)
# │ │ │ └─────────────── Día del mes (1-31)
# │ │ └─────────────────── Hora (0-23)
# │ └──────────────────────── Minuto (0-59)
# └────────────────────────── Segundo (0-59, siempre 0)
# cd /var/www/html/contenedor = Ir al directorio del proyecto
# php scripts/rotar_automatico.php = Ejecutar el script de rotación
# >> /var/log/rotacion.log 2>&1 = Guardar salida en log
# ============================================
# PASOS PARA INSTALAR:
# ============================================
# 1. Abrir editor de crontab:
# crontab -e
#
# 2. Copiar y pegar la línea de arriba (sin el # del inicio)
#
# 3. Guardar y salir
#
# 4. Verificar que esté instalado:
# crontab -l
#
# ============================================
# ============================================
# NOTA: Si quieres probar primero, puedes
# ejecutar manualmente con:
# php /var/www/html/contenedor/scripts/rotar_automatico.php
# ============================================

559
public/admin/asignaciones.php Executable file
View File

@@ -0,0 +1,559 @@
<?php
if (!defined('BASE_PATH')) {
define('BASE_PATH', dirname(__DIR__, 2));
}
require_once BASE_PATH . '/config/config.php';
require_once BASE_PATH . '/src/Auth.php';
require_once BASE_PATH . '/src/User.php';
require_once BASE_PATH . '/src/DiasHorarios.php';
require_once BASE_PATH . '/src/Asignacion.php';
$auth = new Auth();
$auth->requireAdmin();
$userModel = new User();
$horariosModel = new DiasHorarios();
$asignacionModel = new Asignacion();
$message = '';
$messageType = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
if ($action === 'asignar') {
$userId = $_POST['user_id'] ?? 0;
$semana = $_POST['semana'] ?? '';
if ($userId && $semana) {
$asignacionModel->asignar($userId, $semana);
$message = 'Turno asignado correctamente';
$messageType = 'success';
}
} elseif ($action === 'rotar') {
$semana = $_POST['semana'] ?? '';
$asignacionActual = $asignacionModel->getAsignacionPorSemana($semana);
if ($asignacionActual) {
$proximaPersona = $asignacionModel->getProximaPersona($asignacionActual['user_id']);
if ($proximaPersona) {
$asignacionModel->asignar($proximaPersona['id'], $semana);
$message = 'Turno rotado a: ' . htmlspecialchars($proximaPersona['nombre']);
$messageType = 'success';
}
}
} elseif ($action === 'asignar_masivo') {
$userIds = $_POST['user_ids'] ?? [];
$semanaInicio = $_POST['semana_inicio'] ?? '';
$rotacionAutomatica = isset($_POST['rotacion_automatica']) ? true : false;
if (!empty($userIds) && $semanaInicio) {
$resultado = $asignacionModel->asignarMasivo($userIds, $semanaInicio, $rotacionAutomatica);
if ($resultado['success'] > 0) {
$message = "Se asignaron {$resultado['success']} turnos correctamente";
if ($rotacionAutomatica) {
$message .= " con rotación automática para la siguiente semana";
}
$messageType = 'success';
}
if (!empty($resultado['errors'])) {
$message .= "<br>Errores: " . implode('<br>', $resultado['errors']);
$messageType = 'warning';
}
} else {
$message = 'Debes seleccionar al menos un ayudante y una semana';
$messageType = 'danger';
}
}
}
$ayudantes = $userModel->getAyudantesActivos();
$horarios = $horariosModel->getActivos();
// Encontrar el domingo actual
$hoy = new DateTime();
$diaSemana = (int)$hoy->format('w'); // 0 = domingo, 6 = sábado
$domingoActual = clone $hoy;
$domingoActual->modify('-' . $diaSemana . ' days'); // Restar días para llegar al domingo
$currentWeekStart = $domingoActual->format('Y-m-d');
$asignacionActual = $asignacionModel->getAsignacionPorSemana($currentWeekStart);
// Calcular posición en el ciclo (semana X de 4)
function calcularPosicionCiclo($semanaInicio) {
// Empezamos desde el inicio del ciclo: 28 Dic 2025
$fechaInicioCiclo = new DateTime('2025-12-28');
$semanaActual = new DateTime($semanaInicio);
$diasDiferencia = $fechaInicioCiclo->diff($semanaActual)->days;
$semanasDesdeInicio = floor($diasDiferencia / 7);
// Posición en ciclo de 4 semanas (1-4)
$posicion = ($semanasDesdeInicio % 4) + 1;
return $posicion;
}
$posicionCicloActual = calcularPosicionCiclo($currentWeekStart);
// Generar semanas agrupadas por mes
$mesesEspanol = [
'January' => 'Enero', 'February' => 'Febrero', 'March' => 'Marzo',
'April' => 'Abril', 'May' => 'Mayo', 'June' => 'Junio',
'July' => 'Julio', 'August' => 'Agosto', 'September' => 'Septiembre',
'October' => 'Octubre', 'November' => 'Noviembre', 'December' => 'Diciembre'
];
$semanasAgrupadas = [];
for ($i = -4; $i <= 12; $i++) {
$semanaDomingo = clone $domingoActual;
$semanaDomingo->modify("+{$i} weeks");
$key = $semanaDomingo->format('Y-m');
$mesIngles = $semanaDomingo->format('F');
$mesEspanol = $mesesEspanol[$mesIngles] ?? $mesIngles;
$anio = $semanaDomingo->format('Y');
if (!isset($semanasAgrupadas[$key])) {
$semanasAgrupadas[$key] = [
'nombre' => "$mesEspanol $anio",
'semanas' => []
];
}
$semanasAgrupadas[$key]['semanas'][] = [
'fecha' => $semanaDomingo->format('Y-m-d'),
'posicion' => calcularPosicionCiclo($semanaDomingo->format('Y-m-d'))
];
}
$currentPage = 'asignaciones';
$pageTitle = 'Asignación de Turnos';
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Asignaciones - Contenedor Ibiza</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://code.jquery.com/ui/1.13.2/themes/base/jquery-ui.css">
</head>
<body>
<?php include BASE_PATH . '/public/partials/navbar.php'; ?>
<div class="container mt-4">
<h2 class="mb-4">Asignación de Turnos</h2>
<?php if ($message): ?>
<div class="alert alert-<?= $messageType ?>"><?= htmlspecialchars($message) ?></div>
<?php endif; ?>
<div class="row mb-4">
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">Asignación Actual (Semana <?= $posicionCicloActual ?> de 4)</h5>
</div>
<div class="card-body">
<p class="mb-3">
<strong>Fecha:</strong> <?= date('d/m/y', strtotime($currentWeekStart)) ?> (Dom) - <?= date('d/m/y', strtotime('+5 days', strtotime($currentWeekStart))) ?> (Vie)
</p>
<?php if ($asignacionActual): ?>
<div class="alert alert-success">
<strong>Asignado a:</strong> <?= htmlspecialchars($asignacionActual['nombre']) ?>
</div>
<form method="POST" class="d-flex gap-2">
<input type="hidden" name="action" value="rotar">
<input type="hidden" name="semana" value="<?= $currentWeekStart ?>">
<button type="submit" class="btn btn-outline-primary">
↻ Rotar al siguiente
</button>
</form>
<?php else: ?>
<div class="alert alert-warning">No hay asignación para esta semana</div>
<form method="POST">
<input type="hidden" name="action" value="asignar">
<input type="hidden" name="semana" value="<?= $currentWeekStart ?>">
<div class="mb-3">
<label class="form-label">Asignar a:</label>
<select class="form-select" name="user_id" required>
<option value="">Seleccionar persona...</option>
<?php foreach ($ayudantes as $a): ?>
<option value="<?= $a['id'] ?>"><?= htmlspecialchars($a['nombre']) ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-primary">Asignar</button>
</form>
<?php endif; ?>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header bg-info text-white">
<h5 class="mb-0">Horarios Activos</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-sm mb-0">
<thead>
<tr>
<th>Día</th>
<th>Hora</th>
</tr>
</thead>
<tbody>
<?php foreach ($horarios as $h): ?>
<tr>
<td><?= ucfirst($h['dia_semana']) ?></td>
<td><?= date('H:i', strtotime($h['hora_apertura'])) ?> - <?= date('H:i', strtotime($h['hora_cierre'])) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="card shadow-sm">
<div class="card-header bg-secondary text-white">
<h5 class="mb-0">Historial de Asignaciones</h5>
</div>
<div class="card-body">
<form method="GET" class="mb-3">
<label class="form-label">Seleccionar semana:</label>
<div class="d-flex gap-2">
<select class="form-select" name="semana" style="max-width: 320px;">
<?php foreach ($semanasAgrupadas as $grupo): ?>
<optgroup label="<?= $grupo['nombre'] ?>">
<?php foreach ($grupo['semanas'] as $s): ?>
<option value="<?= $s['fecha'] ?>" <?= $s['fecha'] === ($_GET['semana'] ?? $currentWeekStart) ? 'selected' : '' ?>>
Semana <?= $s['posicion'] ?> de 4 - <?= date('d/m', strtotime($s['fecha'])) ?>
</option>
<?php endforeach; ?>
</optgroup>
<?php endforeach; ?>
</select>
<button type="submit" class="btn btn-outline-primary">Ver</button>
</div>
</form>
<?php
$semanaVer = $_GET['semana'] ?? $currentWeekStart;
$asignacionVer = $asignacionModel->getAsignacionPorSemana($semanaVer);
?>
<?php if ($asignacionVer): ?>
<div class="alert alert-info">
<?php $posicionSemanaVer = calcularPosicionCiclo($semanaVer); ?>
<strong>Semana <?= $posicionSemanaVer ?> de 4 (<?= date('d/m/y', strtotime($semanaVer)) ?>):</strong>
<?= htmlspecialchars($asignacionVer['nombre']) ?>
</div>
<form method="POST" class="d-flex gap-2">
<input type="hidden" name="action" value="asignar">
<input type="hidden" name="semana" value="<?= $semanaVer ?>">
<select class="form-select" name="user_id" style="max-width: 250px;">
<option value="">Cambiar persona...</option>
<?php foreach ($ayudantes as $a): ?>
<option value="<?= $a['id'] ?>" <?= isset($asignacionVer['user_id']) && $a['id'] == $asignacionVer['user_id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($a['nombre']) ?>
</option>
<?php endforeach; ?>
</select>
<button type="submit" class="btn btn-outline-secondary">Actualizar</button>
</form>
<?php else: ?>
<div class="alert alert-secondary">
<?php $posicionSinAsignar = calcularPosicionCiclo($semanaVer); ?>
No hay asignación para la semana <?= $posicionSinAsignar ?> de 4 (<?= date('d/m/y', strtotime($semanaVer)) ?>)
</div>
<form method="POST">
<input type="hidden" name="action" value="asignar">
<input type="hidden" name="semana" value="<?= $semanaVer ?>">
<div class="d-flex gap-2">
<select class="form-select" name="user_id" style="max-width: 300px;" required>
<option value="">Seleccionar persona...</option>
<?php foreach ($ayudantes as $a): ?>
<option value="<?= $a['id'] ?>"><?= htmlspecialchars($a['nombre']) ?></option>
<?php endforeach; ?>
</select>
<button type="submit" class="btn btn-primary">Asignar</button>
</div>
</form>
<?php endif; ?>
</div>
</div>
<!-- Sección de Asignación Masiva -->
<div class="card shadow-sm mt-4">
<div class="card-header bg-success text-white">
<h5 class="mb-0">Asignación Masiva</h5>
</div>
<div class="card-body">
<form method="POST" id="asignacionMasivaForm">
<input type="hidden" name="action" value="asignar_masivo">
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Semana de inicio:</label>
<input type="date" class="form-control" name="semana_inicio" required>
<small class="text-muted">Debe ser un domingo</small>
</div>
<div class="col-md-6">
<label class="form-label">Opciones:</label>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" name="rotacion_automatica" id="rotacion_automatica">
<label class="form-check-label" for="rotacion_automatica">
Activar rotación automática para la siguiente semana
</label>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Seleccionar ayudantes:</label>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th width="50">
<input type="checkbox" class="form-check-input" id="selectAll">
</th>
<th>Nombre</th>
<th>Email</th>
<th>Username</th>
</tr>
</thead>
<tbody>
<?php foreach ($ayudantes as $a): ?>
<tr>
<td>
<input type="checkbox" class="form-check-input user-checkbox"
name="user_ids[]" value="<?= $a['id'] ?>">
</td>
<td><?= htmlspecialchars($a['nombre']) ?></td>
<td><?= htmlspecialchars($a['email']) ?></td>
<td><?= htmlspecialchars($a['username']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-success">
<i class="fas fa-users"></i> Asignar a seleccionados
</button>
<button type="button" class="btn btn-outline-secondary" onclick="selectAllUsers()">
<i class="fas fa-check-square"></i> Seleccionar todos
</button>
<button type="button" class="btn btn-outline-secondary" onclick="deselectAllUsers()">
<i class="fas fa-square"></i> Deseleccionar todos
</button>
</div>
</form>
</div>
</div>
<!-- Sección de Rotación Automática -->
<div class="card shadow-sm mt-4">
<div class="card-header bg-warning text-dark">
<h5 class="mb-0">Rotación Automática</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-8">
<h6>Orden de Rotación Actual:</h6>
<div class="d-flex flex-wrap gap-2 mb-3">
<?php
$ayudantesOrdenados = $asignacionModel->getAyudantesPorOrden();
foreach ($ayudantesOrdenados as $index => $ayudante):
?>
<span class="badge bg-primary fs-6">
<?= ($index + 1) ?>. <?= htmlspecialchars($ayudante['nombre']) ?>
</span>
<?php endforeach; ?>
</div>
<?php if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'rotacion_automatica'): ?>
<?php
$resultado = $asignacionModel->asignarSemanasFuturasAutomaticas(12);
?>
<div class="alert alert-<?= !empty($resultado['errors']) ? 'warning' : 'success' ?>">
<strong>Resultado:</strong> Se asignaron <?= $resultado['success'] ?> semanas futuras
<?php if (!empty($resultado['errores'])): ?>
<br><small>Errores: <?= implode(', ', $resultado['errores']) ?></small>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<div class="col-md-4">
<form method="POST" class="h-100 d-flex flex-column justify-content-center">
<input type="hidden" name="action" value="rotacion_automatica">
<button type="submit" class="btn btn-warning w-100">
<i class="fas fa-sync"></i> Generar Rotación Automática
</button>
<small class="text-muted mt-2">
Asigna automáticamente los próximos 12 semanas siguiendo el orden de rotación
</small>
</form>
</div>
</div>
<div class="alert alert-info mt-3 mb-0">
<strong> ¿Cómo funciona?</strong><br>
• El sistema mantiene un orden cíclico de ayudantes<br>
• Cada semana (Dom→Vie) asigna automáticamente al siguiente en la lista<br>
• Al agregar nuevos ayudantes, se integran automáticamente en el ciclo<br>
• Usa el botón para generar las próximas 12 semanas
</div>
</div>
</div>
<!-- Sección de Reordenar Rotación -->
<div class="card shadow-sm mt-4">
<div class="card-header bg-dark text-white">
<h5 class="mb-0">Reordenar Rotación</h5>
</div>
<div class="card-body">
<form method="POST" id="reordenarForm">
<input type="hidden" name="action" value="reordenar">
<p class="text-muted">
Arrastra los elementos para cambiar el orden de rotación.
Los cambios afectarán las asignaciones futuras.
</p>
<ul id="sortableList" class="list-group">
<?php
$ayudantesOrdenados = $asignacionModel->getAyudantesPorOrden();
foreach ($ayudantesOrdenados as $index => $ayudante):
?>
<li class="list-group-item d-flex align-items-center" data-id="<?= $ayudante['id'] ?>">
<input type="hidden" name="ordenes[<?= $index ?>]" value="<?= $ayudante['id'] ?>">
<span class="badge bg-primary me-2" style="cursor: grab;">☰ <?= ($index + 1) ?></span>
<span><?= htmlspecialchars($ayudante['nombre']) ?></span>
</li>
<?php endforeach; ?>
</ul>
<div class="mt-3 d-flex gap-2">
<button type="submit" class="btn btn-dark">
<i class="fas fa-save"></i> Guardar Nuevo Orden
</button>
<button type="button" class="btn btn-outline-secondary" onclick="invertirOrden()">
<i class="fas fa-exchange-alt"></i> Invertir Orden
</button>
</div>
</form>
<?php if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'reordenar'): ?>
<?php
$nuevosOrdenes = $_POST['ordenes'] ?? [];
$errores = [];
if (!empty($nuevosOrdenes)) {
foreach ($nuevosOrdenes as $index => $userId) {
$stmt = $db->prepare("
UPDATE rotacion_orden
SET orden = ?
WHERE user_id = ? AND activo = 1
");
$stmt->execute([$index + 1, $userId]);
}
// Recalcular asignaciones futuras
$resultado = $asignacionModel->recalcularAsignaciones(20);
if ($resultado['success'] > 0) {
echo '<div class="alert alert-success mt-3">';
echo 'Orden actualizado correctamente. ';
echo "Se recalcularon {$resultado['success']} semanas futuras.";
echo '</div>';
}
}
?>
<?php endif; ?>
</div>
</div>
</div>
<script src="https://code.jquery.com/ui/1.13.2/jquery-ui.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Inicializar sortable
$(function() {
$("#sortableList").sortable({
placeholder: "ui-state-highlight",
update: function(event, ui) {
actualizarNumeros();
}
}).disableSelection();
});
function actualizarNumeros() {
$("#sortableList li").each(function(index) {
$(this).find('.badge').text('☰ ' + (index + 1));
});
}
function invertirOrden() {
var items = $("#sortableList li").get().reverse();
$("#sortableList").append(items);
actualizarNumeros();
}
// Seleccionar/deseleccionar todos
document.getElementById('selectAll').addEventListener('change', function() {
const checkboxes = document.querySelectorAll('.user-checkbox');
checkboxes.forEach(checkbox => checkbox.checked = this.checked);
});
function selectAllUsers() {
document.querySelectorAll('.user-checkbox').forEach(checkbox => checkbox.checked = true);
document.getElementById('selectAll').checked = true;
}
function deselectAllUsers() {
document.querySelectorAll('.user-checkbox').forEach(checkbox => checkbox.checked = false);
document.getElementById('selectAll').checked = false;
}
// Validar que al menos un usuario esté seleccionado
document.getElementById('asignacionMasivaForm').addEventListener('submit', function(e) {
const selectedUsers = document.querySelectorAll('.user-checkbox:checked');
if (selectedUsers.length === 0) {
e.preventDefault();
alert('Debes seleccionar al menos un ayudante');
return false;
}
});
// Establecer fecha por defecto al domingo actual
document.addEventListener('DOMContentLoaded', function() {
const hoy = new Date();
const diaSemana = hoy.getDay(); // 0 = domingo, 6 = sábado
const diasParaDomingo = diaSemana === 0 ? 0 : (7 - diaSemana);
const domingoActual = new Date(hoy);
domingoActual.setDate(hoy.getDate() - diasParaDomingo);
const fechaInput = document.querySelector('input[name="semana_inicio"]');
if (fechaInput) {
fechaInput.value = domingoActual.toISOString().split('T')[0];
}
});
</script>
</body>
</html>

139
public/admin/horarios.php Executable file
View File

@@ -0,0 +1,139 @@
<?php
if (!defined('BASE_PATH')) {
define('BASE_PATH', dirname(__DIR__, 2));
}
require_once BASE_PATH . '/config/config.php';
require_once BASE_PATH . '/src/Auth.php';
require_once BASE_PATH . '/src/DiasHorarios.php';
$auth = new Auth();
$auth->requireAdmin();
$horariosModel = new DiasHorarios();
$message = '';
$messageType = '';
$diasNombres = [
'domingo' => 'Domingo',
'lunes' => 'Lunes',
'martes' => 'Martes',
'miercoles' => 'Miércoles',
'jueves' => 'Jueves',
'viernes' => 'Viernes',
'sabado' => 'Sábado'
];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$dia = $_POST['dia'] ?? '';
$hora_apertura = $_POST["hora_apertura_$dia"] ?? '';
$hora_cierre = $_POST["hora_cierre_$dia"] ?? '';
$activo = isset($_POST["activo_$dia"]) ? 1 : 0;
if (empty($dia) || empty($hora_apertura) || empty($hora_cierre)) {
$message = 'Todos los campos son obligatorios';
$messageType = 'danger';
} else {
$horariosModel->update($dia, compact('hora_apertura', 'hora_cierre', 'activo'));
$message = 'Horario actualizado correctamente';
$messageType = 'success';
}
}
$horarios = $horariosModel->getAll();
$currentPage = 'horarios';
$pageTitle = 'Configuración de Horarios';
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Horarios - Contenedor Ibiza</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<?php include BASE_PATH . '/public/partials/navbar.php'; ?>
<div class="container mt-4">
<h2 class="mb-4">Configuración de Horarios</h2>
<?php if ($message): ?>
<div class="alert alert-<?= $messageType ?>"><?= htmlspecialchars($message) ?></div>
<?php endif; ?>
<div class="card shadow-sm">
<div class="card-body">
<form method="POST" id="horariosForm">
<input type="hidden" name="dia" id="selectedDia" value="">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Día</th>
<th>Hora Apertura</th>
<th>Hora Cierre</th>
<th>Activo</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<?php foreach ($horarios as $h): ?>
<tr>
<td><strong><?= $diasNombres[$h['dia_semana']] ?? $h['dia_semana'] ?></strong></td>
<td>
<input type="time" class="form-control form-control-sm"
name="hora_apertura_<?= $h['dia_semana'] ?>"
value="<?= $h['hora_apertura'] ?>" required>
</td>
<td>
<input type="time" class="form-control form-control-sm"
name="hora_cierre_<?= $h['dia_semana'] ?>"
value="<?= $h['hora_cierre'] ?>" required>
</td>
<td>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox"
name="activo_<?= $h['dia_semana'] ?>"
id="activo_<?= $h['dia_semana'] ?>"
<?= $h['activo'] ? 'checked' : '' ?>>
</div>
</td>
<td>
<button type="button" class="btn btn-sm btn-primary"
onclick="guardarHorario('<?= $h['dia_semana'] ?>')">
Guardar
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</form>
</div>
</div>
<div class="card mt-4 shadow-sm">
<div class="card-header bg-secondary text-white">
<h5 class="mb-0">Información</h5>
</div>
<div class="card-body">
<ul class="mb-0">
<li>Los días marcados como "Activo" aparecerán en los turnos.</li>
<li>Los horarios pueden modificarse en cualquier momento.</li>
<li>La hora de cierre debe ser posterior a la hora de apertura.</li>
</ul>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
function guardarHorario(dia) {
document.getElementById('selectedDia').value = dia;
document.getElementById('horariosForm').submit();
}
</script>
</body>
</html>

120
public/admin/index.php Executable file
View File

@@ -0,0 +1,120 @@
<?php
if (!defined('BASE_PATH')) {
define('BASE_PATH', dirname(__DIR__, 2));
}
require_once BASE_PATH . '/config/config.php';
require_once BASE_PATH . '/src/Auth.php';
require_once BASE_PATH . '/src/User.php';
require_once BASE_PATH . '/src/DiasHorarios.php';
require_once BASE_PATH . '/src/Asignacion.php';
$auth = new Auth();
$auth->requireAdmin();
$userModel = new User();
$horariosModel = new DiasHorarios();
$asignacionModel = new Asignacion();
$totalUsuarios = count($userModel->getAll());
$totalAyudantes = count($userModel->getAyudantesActivos());
$totalHorarios = count($horariosModel->getAll());
$asignacionActual = $asignacionModel->getAsignacionActual();
$currentPage = 'dashboard';
$pageTitle = 'Dashboard';
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - Contenedor Ibiza</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<?php include BASE_PATH . '/public/partials/navbar.php'; ?>
<div class="container mt-4">
<h2 class="mb-4">Panel de Administración</h2>
<div class="row g-4 mb-4">
<div class="col-md-3">
<div class="card text-center shadow-sm">
<div class="card-body">
<h5 class="card-title text-primary"><?= $totalUsuarios ?></h5>
<p class="card-text">Total Usuarios</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center shadow-sm">
<div class="card-body">
<h5 class="card-title text-success"><?= $totalAyudantes ?></h5>
<p class="card-text">Ayudantes Activos</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center shadow-sm">
<div class="card-body">
<h5 class="card-title text-warning"><?= $totalHorarios ?></h5>
<p class="card-text">Días Configurados</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center shadow-sm">
<div class="card-body">
<h5 class="card-title text-info">
<?= $asignacionActual ? htmlspecialchars($asignacionActual['nombre']) : 'Sin asignar' ?>
</h5>
<p class="card-text">Turno Actual</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">Acciones Rápidas</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="/admin/usuarios.php" class="btn btn-outline-primary">Gestionar Usuarios</a>
<a href="/admin/horarios.php" class="btn btn-outline-primary">Configurar Horarios</a>
<a href="/admin/asignaciones.php" class="btn btn-outline-primary">Ver Asignaciones</a>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header bg-info text-white">
<h5 class="mb-0">Información del Sistema</h5>
</div>
<div class="card-body">
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between">
<span>Semana actual:</span>
<strong><?= date('W') ?></strong>
</li>
<li class="list-group-item d-flex justify-content-between">
<span>Inicio semana:</span>
<strong><?= date('d/m/Y', strtotime('monday this week')) ?></strong>
</li>
<li class="list-group-item d-flex justify-content-between">
<span>Rol actual:</span>
<strong><?= ucfirst($_SESSION['user_rol']) ?></strong>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

112
public/admin/logs.php Executable file
View File

@@ -0,0 +1,112 @@
<?php
if (!defined('BASE_PATH')) {
define('BASE_PATH', dirname(__DIR__, 2));
}
$config = require_once BASE_PATH . '/config/config.php';
require_once BASE_PATH . '/src/Auth.php';
require_once BASE_PATH . '/src/Database.php';
$auth = new Auth();
$auth->requireAdmin();
$logFile = BASE_PATH . '/public/logs/error.log';
$logs = [];
if (file_exists($logFile)) {
$logs = array_reverse(file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES));
$logs = array_slice($logs, 0, 100);
}
$currentPage = 'logs';
$pageTitle = 'Logs del Sistema';
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Logs - Contenedor Ibiza</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.log-entry {
font-family: monospace;
font-size: 12px;
padding: 4px 8px;
border-bottom: 1px solid #eee;
}
.log-entry:hover {
background-color: #f8f9fa;
}
.log-error { color: #dc3545; }
.log-warning { color: #ffc107; }
.log-notice { color: #0dcaf0; }
.log-info { color: #6c757d; }
.log-container {
max-height: 70vh;
overflow-y: auto;
background: #1e1e1e;
color: #d4d4d4;
}
</style>
</head>
<body>
<?php include BASE_PATH . '/public/partials/navbar.php'; ?>
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Logs del Sistema</h2>
<div>
<a href="?clear=1" class="btn btn-outline-danger btn-sm" onclick="return confirm('¿Vaciar todos los logs?')">Vaciar Logs</a>
<a href="<?= rtrim($config['site_url'] ?? '', '/') ?>/logs/error.log" target="_blank" class="btn btn-outline-secondary btn-sm">Ver Archivo Completo</a>
</div>
</div>
<?php
if (isset($_GET['clear']) && $_GET['clear'] == '1') {
file_put_contents($logFile, '');
header('Location: logs.php');
exit;
}
?>
<div class="card shadow-sm">
<div class="card-body p-0">
<div class="log-container">
<?php if (empty($logs)): ?>
<div class="p-3 text-muted">No hay logs registrados.</div>
<?php else: ?>
<?php foreach ($logs as $log): ?>
<?php
$class = 'log-info';
if (stripos($log, 'ERROR') !== false || stripos($log, 'FATAL') !== false) {
$class = 'log-error';
} elseif (stripos($log, 'WARNING') !== false) {
$class = 'log-warning';
} elseif (stripos($log, 'NOTICE') !== false) {
$class = 'log-notice';
}
?>
<div class="log-entry <?= $class ?>"><?= htmlspecialchars($log) ?></div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
<div class="card mt-4 shadow-sm">
<div class="card-header bg-secondary text-white">
<h5 class="mb-0">Información</h5>
</div>
<div class="card-body">
<ul class="mb-0">
<li>Los errores se guardan automáticamente en: <code>logs/error.log</code></li>
<li>Se registran errores de PHP, excepciones y errores fatales.</li>
<li>La configuración está en: <code>config/error_logging.php</code></li>
</ul>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

223
public/admin/usuarios.php Executable file
View File

@@ -0,0 +1,223 @@
<?php
if (!defined('BASE_PATH')) {
define('BASE_PATH', dirname(__DIR__, 2));
}
require_once BASE_PATH . '/config/config.php';
require_once BASE_PATH . '/src/Auth.php';
require_once BASE_PATH . '/src/User.php';
$auth = new Auth();
$auth->requireAdmin();
$userModel = new User();
$message = '';
$messageType = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
if ($action === 'create') {
$nombre = trim($_POST['nombre'] ?? '');
$email = trim($_POST['email'] ?? '');
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$rol = $_POST['rol'] ?? 'ayudante';
if (empty($nombre) || empty($email) || empty($password)) {
$message = 'Todos los campos son obligatorios';
$messageType = 'danger';
} elseif ($userModel->getByEmail($email)) {
$message = 'El email ya está registrado';
$messageType = 'danger';
} elseif ($username && $userModel->usernameExists($username)) {
$message = 'El username ya está en uso';
$messageType = 'danger';
} else {
$userModel->create(compact('nombre', 'email', 'username', 'password', 'rol'));
$message = 'Usuario creado exitosamente';
$messageType = 'success';
}
} elseif ($action === 'update') {
$id = $_POST['id'] ?? 0;
$nombre = trim($_POST['nombre'] ?? '');
$email = trim($_POST['email'] ?? '');
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$rol = $_POST['rol'] ?? 'ayudante';
if (empty($nombre) || empty($email)) {
$message = 'Nombre y email son obligatorios';
$messageType = 'danger';
} elseif ($userModel->usernameExists($username, $id)) {
$message = 'El username ya está en uso';
$messageType = 'danger';
} else {
$userModel->update($id, compact('nombre', 'email', 'username', 'password', 'rol'));
$message = 'Usuario actualizado exitosamente';
$messageType = 'success';
}
} elseif ($action === 'toggle') {
$id = $_POST['id'] ?? 0;
$user = $userModel->getById($id);
if ($user) {
if ($user['activo']) {
$userModel->deactivate($id);
} else {
$userModel->activate($id);
}
$message = 'Estado actualizado';
$messageType = 'success';
}
}
}
$users = $userModel->getAll(true);
$currentPage = 'usuarios';
$pageTitle = 'Gestión de Usuarios';
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Usuarios - Contenedor Ibiza</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<?php include BASE_PATH . '/public/partials/navbar.php'; ?>
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Gestión de Usuarios</h2>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#userModal" onclick="resetForm()">
+ Nuevo Usuario
</button>
</div>
<?php if ($message): ?>
<div class="alert alert-<?= $messageType ?>"><?= htmlspecialchars($message) ?></div>
<?php endif; ?>
<div class="card shadow-sm">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Username</th>
<th>Nombre</th>
<th>Email</th>
<th>Rol</th>
<th>Estado</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<?php foreach ($users as $u): ?>
<tr>
<td><?= htmlspecialchars($u['username'] ?? '-') ?></td>
<td><?= htmlspecialchars($u['nombre']) ?></td>
<td><?= htmlspecialchars($u['email']) ?></td>
<td>
<span class="badge bg-<?= $u['rol'] === 'admin' ? 'danger' : 'primary' ?>">
<?= ucfirst($u['rol']) ?>
</span>
</td>
<td>
<span class="badge bg-<?= $u['activo'] ? 'success' : 'secondary' ?>">
<?= $u['activo'] ? 'Activo' : 'Inactivo' ?>
</span>
</td>
<td>
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#userModal"
onclick="editUser(<?= $u['id'] ?>, '<?= htmlspecialchars($u['nombre']) ?>', '<?= htmlspecialchars($u['email']) ?>', '<?= htmlspecialchars($u['username'] ?? '') ?>', '<?= $u['rol'] ?>')">
Editar
</button>
<?php if ($u['id'] != $_SESSION['user_id']): ?>
<form method="POST" class="d-inline">
<input type="hidden" name="action" value="toggle">
<input type="hidden" name="id" value="<?= $u['id'] ?>">
<button type="submit" class="btn btn-sm btn-<?= $u['activo'] ? 'outline-warning' : 'outline-success' ?>">
<?= $u['activo'] ? 'Desactivar' : 'Activar' ?>
</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="modal fade" id="userModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalTitle">Nuevo Usuario</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST" id="userForm">
<input type="hidden" name="action" value="create" id="formAction">
<input type="hidden" name="id" value="" id="userId">
<div class="modal-body">
<div class="mb-3">
<label for="nombre" class="form-label">Nombre Completo</label>
<input type="text" class="form-control" id="nombre" name="nombre" required>
</div>
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" placeholder="Opcional">
<small class="text-muted">Para iniciar sesión con nombre de usuario</small>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Contraseña</label>
<input type="password" class="form-control" id="password" name="password">
<small class="text-muted">Dejar en blanco para mantener la actual</small>
</div>
<div class="mb-3">
<label for="rol" class="form-label">Rol</label>
<select class="form-select" id="rol" name="rol">
<option value="ayudante">Ayudante</option>
<option value="admin">Administrador</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-primary">Guardar</button>
</div>
</form>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
function resetForm() {
document.getElementById('modalTitle').textContent = 'Nuevo Usuario';
document.getElementById('formAction').value = 'create';
document.getElementById('userId').value = '';
document.getElementById('userForm').reset();
document.getElementById('password').required = true;
}
function editUser(id, nombre, email, username, rol) {
document.getElementById('modalTitle').textContent = 'Editar Usuario';
document.getElementById('formAction').value = 'update';
document.getElementById('userId').value = id;
document.getElementById('nombre').value = nombre;
document.getElementById('email').value = email;
document.getElementById('username').value = username;
document.getElementById('rol').value = rol;
document.getElementById('password').required = false;
}
</script>
</body>
</html>

327
public/admin/webhook.php Executable file
View File

@@ -0,0 +1,327 @@
<?php
if (!defined('BASE_PATH')) {
define('BASE_PATH', dirname(__DIR__, 2));
}
$config = require BASE_PATH . '/config/config.php';
require_once BASE_PATH . '/src/Auth.php';
require_once BASE_PATH . '/bot/TelegramBot.php';
$auth = new Auth();
$auth->requireAdmin();
$bot = new TelegramBot();
$message = '';
$messageType = '';
$webhookInfo = null;
$botInfo = null;
// Obtener información del bot
$botMe = $bot->getMe();
if ($botMe && isset($botMe['ok']) && $botMe['ok']) {
$botInfo = $botMe['result'];
}
// Verificar estado del webhook
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
if ($action === 'verificar') {
$url = "https://api.telegram.org/bot{$config['telegram_bot_token']}/getWebhookInfo";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
if ($result && isset($result['ok'])) {
$webhookInfo = $result;
$message = 'Información del webhook obtenida';
$messageType = 'success';
} else {
$message = 'Error al obtener información del webhook';
$messageType = 'danger';
}
} elseif ($action === 'borrar') {
$url = "https://api.telegram.org/bot{$config['telegram_bot_token']}/deleteWebhook";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
if ($result && isset($result['ok']) && $result['ok']) {
$message = 'Webhook eliminado correctamente';
$messageType = 'success';
$webhookInfo = null;
} else {
$message = 'Error al eliminar webhook: ' . ($result['description'] ?? 'Desconocido');
$messageType = 'danger';
}
} elseif ($action === 'configurar') {
$webhookUrl = trim($_POST['webhook_url'] ?? '');
if (empty($webhookUrl)) {
$message = 'Debes ingresar la URL del webhook';
$messageType = 'danger';
} elseif (!filter_var($webhookUrl, FILTER_VALIDATE_URL)) {
$message = 'La URL ingresada no es válida';
$messageType = 'danger';
} else {
$url = "https://api.telegram.org/bot{$config['telegram_bot_token']}/setWebhook";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'url' => $webhookUrl,
'allowed_updates' => ['message', 'callback_query']
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
if ($result && isset($result['ok']) && $result['ok']) {
$message = "Webhook configurado correctamente en:\n" . htmlspecialchars($webhookUrl);
$messageType = 'success';
} else {
$message = 'Error al configurar webhook: ' . ($result['description'] ?? 'Desconocido');
$messageType = 'danger';
}
}
}
}
// Obtener estado actual del webhook
$url = "https://api.telegram.org/bot{$config['telegram_bot_token']}/getWebhookInfo";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$webhookInfo = json_decode($response, true);
$currentPage = 'webhook';
$pageTitle = 'Administración del Bot de Telegram';
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $pageTitle ?> - Contenedor Ibiza</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<?php include BASE_PATH . '/public/partials/navbar.php'; ?>
<div class="container mt-4">
<h2 class="mb-4">🤖 Administración del Bot de Telegram</h2>
<?php if ($message): ?>
<div class="alert alert-<?= $messageType ?>"><?= nl2br(htmlspecialchars($message)) ?></div>
<?php endif; ?>
<!-- Información del Bot -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">Información del Bot</h5>
</div>
<div class="card-body">
<?php if ($botInfo): ?>
<div class="row">
<div class="col-md-6">
<p><strong>Nombre:</strong> <?= htmlspecialchars($botInfo['first_name']) ?></p>
<p><strong>Username:</strong> @<?= htmlspecialchars($botInfo['username']) ?></p>
<p><strong>ID:</strong> <?= $botInfo['id'] ?></p>
<p><strong>Estado:</strong>
<span class="badge bg-success">Conectado</span>
</p>
</div>
<div class="col-md-6 text-end">
<span class="text-muted">Token configurado correctamente</span>
</div>
</div>
<?php else: ?>
<div class="alert alert-warning">
<strong>Error:</strong> No se pudo conectar con el bot.
Verifica que el TELEGRAM_BOT_TOKEN esté configurado correctamente en .env
</div>
<?php endif; ?>
</div>
</div>
<!-- Estado del Webhook -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-info text-white">
<h5 class="mb-0">Estado del Webhook</h5>
</div>
<div class="card-body">
<?php if ($webhookInfo && isset($webhookInfo['ok']) && $webhookInfo['ok']): ?>
<?php if (!empty($webhookInfo['result']['url'])): ?>
<div class="alert alert-success">
<strong>✅ Webhook activo</strong>
<p class="mb-0 mt-2">
<strong>URL:</strong> <?= htmlspecialchars($webhookInfo['result']['url']) ?>
</p>
</div>
<ul class="list-group mb-3">
<li class="list-group-item d-flex justify-content-between">
<span>Última actualización:</span>
<span><?= date('d/m/Y H:i:s', $webhookInfo['result']['last_synchronization_unix_time'] ?? 0) ?></span>
</li>
<li class="list-group-item d-flex justify-content-between">
<span>IP permitida:</span>
<span><?= $webhookInfo['result']['ip_address'] ?? 'No disponible' ?></span>
</li>
<li class="list-group-item d-flex justify-content-between">
<span>Errores acumulados:</span>
<span><?= $webhookInfo['result']['last_error_date'] ?? 0 ?></span>
</li>
<li class="list-group-item d-flex justify-content-between">
<span>Actualizaciones pendientes:</span>
<span><?= $webhookInfo['result']['pending_update_count'] ?? 0 ?></span>
</li>
</ul>
<?php else: ?>
<div class="alert alert-warning">
<strong>⚠️ Webhook no configurado</strong>
<p class="mb-0 mt-2">No hay webhook configurado para este bot.</p>
</div>
<?php endif; ?>
<?php else: ?>
<div class="alert alert-danger">
<strong>Error:</strong> No se pudo obtener información del webhook
</div>
<?php endif; ?>
<!-- Acciones -->
<div class="d-flex gap-2 mt-3">
<form method="POST" class="d-inline">
<input type="hidden" name="action" value="verificar">
<button type="submit" class="btn btn-outline-primary">
🔄 Verificar Estado
</button>
</form>
<?php if ($webhookInfo && isset($webhookInfo['result']['url']) && !empty($webhookInfo['result']['url'])): ?>
<form method="POST" class="d-inline" onsubmit="return confirm('¿Estás seguro de eliminar el webhook?');">
<input type="hidden" name="action" value="borrar">
<button type="submit" class="btn btn-outline-danger">
🗑️ Eliminar Webhook
</button>
</form>
<?php endif; ?>
</div>
</div>
</div>
<!-- Configurar Webhook -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-success text-white">
<h5 class="mb-0">⚙️ Configurar Webhook</h5>
</div>
<div class="card-body">
<?php
// Construir URL sugerida usando SITE_URL del .env
$urlSugerida = ($config['site_url'] ?? '') . '/bot/webhook.php';
$urlActual = $webhookInfo['result']['url'] ?? '';
$urlParaInput = $urlActual ?: $urlSugerida;
?>
<form method="POST">
<input type="hidden" name="action" value="configurar">
<div class="mb-3">
<label for="webhook_url" class="form-label">URL del Webhook:</label>
<input type="url" class="form-control" id="webhook_url" name="webhook_url"
value="<?= htmlspecialchars($urlParaInput) ?>"
placeholder="https://tu-dominio.com/bot/webhook.php" required>
<?php if (!empty($config['site_url'])): ?>
<div class="form-text">
URL sugerida basada en SITE_URL: <code><?= htmlspecialchars($config['site_url']) ?></code>
</div>
<?php endif; ?>
</div>
<div class="alert alert-info mb-3">
<strong>📝 Instrucciones:</strong>
<ol class="mb-0">
<li>Asegúrate de que la URL sea accesible públicamente (no localhost)</li>
<li>El dominio debe tener certificado SSL (HTTPS)</li>
<li>Ejemplo de URL: <code>https://contenedor-test.local:82/bot/webhook.php</code></li>
</ol>
</div>
<button type="submit" class="btn btn-success">
✅ Configurar Webhook
</button>
</form>
</div>
</div>
<!-- Comandos del Bot -->
<div class="card shadow-sm">
<div class="card-header bg-secondary text-white">
<h5 class="mb-0">📋 Comandos Disponibles</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Comando</th>
<th>Descripción</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>/start</code> o <code>/menu</code></td>
<td>Muestra el menú interactivo con botones</td>
</tr>
<tr>
<td><code>/turnos</code></td>
<td>Muestra la tabla completa de asignaciones</td>
</tr>
<tr>
<td><code>/semana</code> o <code>hoy</code></td>
<td>Muestra quién tiene turno esta semana</td>
</tr>
<tr>
<td><code>/ayudantes</code></td>
<td>Lista de todos los ayudantes</td>
</tr>
<tr>
<td><code>[Nombre]</code></td>
<td>Busca los turnos de un ayudante específico</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- URLs de Referencia -->
<div class="card shadow-sm mt-4">
<div class="card-header bg-dark text-white">
<h5 class="mb-0">🔗 URLs de Referencia</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<strong>Webhook:</strong><br>
<code class="text-primary"><?= htmlspecialchars($config['site_url'] ?? 'https://tu-dominio.com') ?>/bot/webhook.php</code>
</div>
<div class="col-md-6">
<strong>Test del bot:</strong><br>
<code class="text-primary"><?= $site_url ?? 'https://tu-dominio.com' ?>/bot/test.php</code>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

231
public/ayudante.php Executable file
View File

@@ -0,0 +1,231 @@
<?php
require_once __DIR__ . '/../src/Auth.php';
require_once __DIR__ . '/../src/User.php';
require_once __DIR__ . '/../src/DiasHorarios.php';
require_once __DIR__ . '/../src/Asignacion.php';
$auth = new Auth();
$auth->requireAuth();
if ($auth->isAdmin()) {
header('Location: /admin/index.php');
exit;
}
$user = $auth->getCurrentUser();
$horariosModel = new DiasHorarios();
$asignacionModel = new Asignacion();
$horarios = $horariosModel->getActivos();
$asignacionActual = $asignacionModel->getAsignacionActual();
// Obtener todas las asignaciones de las próximas semanas
$semanasFuturas = [];
// Encontrar el domingo de esta semana
$hoy = new DateTime();
$diaSemana = (int)$hoy->format('w'); // 0 = domingo, 6 = sábado
$domingoEstaSemana = clone $hoy;
$domingoEstaSemana->modify('-' . $diaSemana . ' days'); // Restar días para llegar al domingo
for ($i = 0; $i <= 4; $i++) {
$semanaDomingo = clone $domingoEstaSemana;
$semanaDomingo->modify("+{$i} weeks");
$semanaInicio = $semanaDomingo->format('Y-m-d');
$asignacionesSemana = $asignacionModel->getTodasAsignacionesPorSemana($semanaInicio);
$semanasFuturas[] = [
'inicio' => $semanaInicio,
'fin' => date('Y-m-d', strtotime('+5 days', strtotime($semanaInicio))), // +5 días = domingo a viernes
'asignaciones' => $asignacionesSemana,
'asignacion' => !empty($asignacionesSemana) ? $asignacionesSemana[0] : null
];
}
$miTurno = $asignacionActual && $asignacionActual['id'] == $user['id'];
// También verificar si el usuario tiene turno en las próximas semanas
$misAsignacionesFuturas = [];
foreach ($semanasFuturas as $semana) {
foreach ($semana['asignaciones'] as $asignacion) {
if ($asignacion['id'] == $user['id']) {
$misAsignacionesFuturas[] = [
'semana' => $semana,
'asignacion' => $asignacion
];
}
}
}
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mis Turnos - Contenedor Ibiza</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="/ayudante.php">Contenedor Ibiza</a>
<span class="navbar-text">
Hola, <?= htmlspecialchars($user['nombre']) ?>
</span>
<a href="/logout.php" class="btn btn-outline-light btn-sm">Cerrar Sesión</a>
</div>
</nav>
<div class="container mt-4">
<h2 class="mb-4">Mis Turnos</h2>
<?php
// Verificar si tiene turno esta semana (hoy está entre domingo y viernes de la semana actual)
$hoy = new DateTime();
$diaSemana = (int)$hoy->format('w'); // 0 = domingo, 6 = sábado
$domingoActual = clone $hoy;
$domingoActual->modify('-' . $diaSemana . ' days'); // Restar días para llegar al domingo
$viernesActual = clone $domingoActual;
$viernesActual->modify('+5 days');
$asignacionEstaSemana = $asignacionModel->getAsignacionPorSemana($domingoActual->format('Y-m-d'));
$tengoTurnoEstaSemana = $asignacionEstaSemana && $asignacionEstaSemana['id'] == $user['id'];
if ($tengoTurnoEstaSemana):
?>
<div class="alert alert-success mb-4">
<strong>¡Tienes turno esta semana!</strong><br>
Del <?= date('d/m/y', strtotime($asignacionEstaSemana['semana_inicio'])) ?>
al <?= date('d/m/y', strtotime($asignacionEstaSemana['semana_fin'])) ?>
</div>
<?php elseif ($asignacionEstaSemana): ?>
<div class="alert alert-secondary mb-4">
<strong>Turno esta semana:</strong> <?= htmlspecialchars($asignacionEstaSemana['nombre']) ?><br>
<?php if (!empty($misAsignacionesFuturas)): ?>
Tu próximo turno: <?= date('d/m/y', strtotime($misAsignacionesFuturas[0]['semana']['inicio'])) ?>
al <?= date('d/m/y', strtotime($misAsignacionesFuturas[0]['semana']['fin'])) ?>
<?php else: ?>
Tu próximo turno será en las próximas semanas.
<?php endif; ?>
</div>
<?php elseif (!empty($misAsignacionesFuturas)): ?>
<div class="alert alert-info mb-4">
<strong>Próximo turno:</strong><br>
Del <?= date('d/m/y', strtotime($misAsignacionesFuturas[0]['semana']['inicio'])) ?>
al <?= date('d/m/y', strtotime($misAsignacionesFuturas[0]['semana']['fin'])) ?>
</div>
<?php else: ?>
<div class="alert alert-warning mb-4">
No hay turnos asignados para las próximas semanas.
</div>
<?php endif; ?>
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">Horarios de Apertura del Contenedor</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Día</th>
<th>Hora Apertura</th>
<th>Hora Cierre</th>
</tr>
</thead>
<tbody>
<?php foreach ($horarios as $h): ?>
<tr class="<?= $miTurno ? 'table-primary' : '' ?>">
<td><strong><?= ucfirst($h['dia_semana']) ?></strong></td>
<td><?= date('H:i', strtotime($h['hora_apertura'])) ?></td>
<td><?= date('H:i', strtotime($h['hora_cierre'])) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Tabla de Asignaciones de Turnos -->
<div class="card mt-4 shadow-sm">
<div class="card-header bg-success text-white">
<h5 class="mb-0">Calendario de Turnos</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Semana</th>
<th>Período</th>
<th>Asignado a</th>
<th>Estado</th>
</tr>
</thead>
<tbody>
<?php foreach ($semanasFuturas as $index => $semana): ?>
<tr class="<?= !empty($semana['asignaciones']) && in_array($user['id'], array_column($semana['asignaciones'], 'id')) ? 'table-success' : '' ?>">
<td>
<strong>Semana <?= date('d/m/y', strtotime($semana['inicio'])) ?></strong>
<?php if ($index === 0): ?>
<span class="badge bg-primary ms-1">Actual</span>
<?php endif; ?>
</td>
<td>
<?= date('d/m/y', strtotime($semana['inicio'])) ?> (Dom) -
<?= date('d/m/y', strtotime($semana['fin'])) ?> (Vie)
</td>
<td>
<?php if (!empty($semana['asignaciones'])): ?>
<?php foreach ($semana['asignaciones'] as $asignacion): ?>
<div class="mb-1">
<?= htmlspecialchars($asignacion['nombre']) ?>
<?php if ($asignacion['id'] == $user['id']): ?>
<span class="badge bg-success ms-1">Tú</span>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php else: ?>
<span class="text-muted">Sin asignar</span>
<?php endif; ?>
</td>
<td>
<?php if (!empty($semana['asignaciones']) && in_array($user['id'], array_column($semana['asignaciones'], 'id'))): ?>
<span class="badge bg-success">Tu turno</span>
<?php elseif (!empty($semana['asignaciones'])): ?>
<span class="badge bg-secondary">
<?= count($semana['asignaciones']) ?> asignado(s)
</span>
<?php else: ?>
<span class="badge bg-warning">Pendiente</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<div class="card mt-4 shadow-sm">
<div class="card-header bg-info text-white">
<h5 class="mb-0">Información</h5>
</div>
<div class="card-body">
<ul class="mb-0">
<li>Los turnos se asignan de forma rotativa semanalmente.</li>
<li>Cada semana inicia en lunes y termina en domingo.</li>
<li>Recuerda estar atento a tu turno para abrir y cerrar el contenedor.</li>
<li>Las filas en verde indican tus turnos asignados.</li>
</ul>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

16
public/index.php Executable file
View File

@@ -0,0 +1,16 @@
<?php
require_once __DIR__ . '/../src/Auth.php';
$auth = new Auth();
if (!$auth->isLoggedIn()) {
header('Location: /login.php');
exit;
}
if ($auth->isAdmin()) {
header('Location: /admin/index.php');
} else {
header('Location: /ayudante.php');
}
exit;

82
public/login.php Executable file
View File

@@ -0,0 +1,82 @@
<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/../src/Auth.php';
$auth = new Auth();
if ($auth->isLoggedIn()) {
if ($auth->isAdmin()) {
header('Location: /admin/index.php');
} else {
header('Location: /ayudante.php');
}
exit;
}
$error = '';
$loginInput = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$loginInput = trim($_POST['login'] ?? '');
$password = $_POST['password'] ?? '';
if (empty($loginInput) || empty($password)) {
$error = 'Por favor ingresa usuario/email y contraseña';
} else {
if ($auth->login($loginInput, $password)) {
session_write_close();
if ($auth->isAdmin()) {
header('Location: /admin/index.php');
} else {
header('Location: /ayudante.php');
}
exit;
} else {
$error = 'Usuario/email o contraseña incorrectos';
}
}
}
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - Contenedor Ibiza</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light d-flex align-items-center justify-content-center" style="min-height: 100vh;">
<div class="container">
<div class="row justify-content-center">
<div class="col-md-5">
<div class="card shadow">
<div class="card-body p-5">
<h3 class="text-center mb-4">Contenedor Ibiza</h3>
<h5 class="text-center text-muted mb-4">Iniciar Sesión</h5>
<?php if ($error): ?>
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<form method="POST">
<div class="mb-3">
<label for="login" class="form-label">Usuario o Email</label>
<input type="text" class="form-control" id="login" name="login"
value="<?= htmlspecialchars($loginInput) ?>" required>
</div>
<div class="mb-4">
<label for="password" class="form-label">Contraseña</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary w-100 py-2">Ingresar</button>
</form>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

8
public/logout.php Executable file
View File

@@ -0,0 +1,8 @@
<?php
require_once __DIR__ . '/../src/Auth.php';
$auth = new Auth();
$auth->logout();
header('Location: /login.php');
exit;

3
public/logs.php Executable file
View File

@@ -0,0 +1,3 @@
<?php
header('Location: /admin/logs.php');
exit;

48
public/partials/navbar.php Executable file
View File

@@ -0,0 +1,48 @@
<?php
require_once '/var/www/html/contenedor/src/Auth.php';
$auth = new Auth();
$user = $auth->getCurrentUser();
$currentPage = $currentPage ?? '';
?>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="/admin/index.php">Contenedor Ibiza</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link <?= $currentPage === 'dashboard' ? 'active' : '' ?>" href="/admin/index.php">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $currentPage === 'usuarios' ? 'active' : '' ?>" href="/admin/usuarios.php">Usuarios</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $currentPage === 'horarios' ? 'active' : '' ?>" href="/admin/horarios.php">Horarios</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $currentPage === 'asignaciones' ? 'active' : '' ?>" href="/admin/asignaciones.php">Asignaciones</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $currentPage === 'logs' ? 'active' : '' ?>" href="/admin/logs.php">Logs</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $currentPage === 'webhook' ? 'active' : '' ?>" href="/admin/webhook.php">🤖 Bot</a>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<?= htmlspecialchars($user['nombre'] ?? 'Usuario') ?>
</a>
<ul class="dropdown-menu">
<li><span class="dropdown-item-text"><strong>Rol:</strong> <?= htmlspecialchars(ucfirst($user['rol'] ?? '')) ?></span></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/logout.php">Cerrar Sesión</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>

24
scripts/rotar.php Executable file
View File

@@ -0,0 +1,24 @@
#!/usr/bin/env php
<?php
require_once __DIR__ . '/../src/RotacionTurnos.php';
$rotacion = new RotacionTurnos();
$resultado = $rotacion->verificarYRotar();
echo "=== Rotación de Turnos ===\n";
echo "Fecha: " . date('Y-m-d H:i:s') . "\n";
echo "Resultado: " . $resultado['message'] . "\n";
if (isset($resultado['already_assigned'])) {
echo "Estado: Ya estaba asignada\n";
exit(0);
}
if ($resultado['success']) {
echo "Estado: Éxito\n";
exit(0);
} else {
echo "Estado: Error\n";
exit(1);
}

32
scripts/rotar_automatico.php Executable file
View File

@@ -0,0 +1,32 @@
<?php
require_once __DIR__ . '/../config/config.php';
require_once __DIR__ . '/../src/Asignacion.php';
$asignacion = new Asignacion();
echo "=== Sistema de Rotación Automática ===\n\n";
// 1. Inicializar orden de rotación si no existe
echo "1. Verificando orden de rotación...\n";
$resultado = $asignacion->inicializarOrdenRotacion();
echo " - Usuarios actualizados: {$resultado['actualizados']}\n";
if (!empty($resultado['errores'])) {
echo " - Errores: " . implode(', ', $resultado['errores']) . "\n";
}
// 2. Asignar semanas futuras automáticamente
echo "\n2. Asignando semanas futuras...\n";
$resultado = $asignacion->asignarSemanasFuturasAutomaticas(12);
echo " - Semanas asignadas: {$resultado['success']}\n";
if (!empty($resultado['errores'])) {
echo " - Errores: " . implode(', ', $resultado['errores']) . "\n";
}
// 3. Mostrar orden de rotación actual
echo "\n3. Orden de rotación actual:\n";
$ayudantes = $asignacion->getAyudantesPorOrden();
foreach ($ayudantes as $index => $ayudante) {
echo " " . ($index + 1) . ". {$ayudante['nombre']} (Orden: {$ayudante['orden']})\n";
}
echo "\n=== Proceso completado ===\n";

View File

View File

View File

View File

View File

View File

View File

@@ -0,0 +1 @@
user_id|i:1;user_name|s:13:"Administrador";user_rol|s:5:"admin";logged_in|b:1;

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

@@ -0,0 +1 @@
user_id|i:1;user_name|s:13:"Administrador";user_rol|s:5:"admin";logged_in|b:1;

View File

View File

@@ -0,0 +1 @@
user_id|i:1;user_name|s:13:"Administrador";user_rol|s:5:"admin";logged_in|b:1;

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

@@ -0,0 +1 @@
user_id|i:1;user_name|s:13:"Administrador";user_rol|s:5:"admin";logged_in|b:1;

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

@@ -0,0 +1 @@
user_id|i:1;user_name|s:13:"Administrador";user_rol|s:5:"admin";logged_in|b:1;

View File

View File

View File

Some files were not shown because too many files have changed in this diff Show More