Compare commits

...

13 Commits

Author SHA1 Message Date
nickpons666
b000e7f665 Corrección de búsqueda por nombre: inclusión del rol coordinador en el filtro 2026-01-31 02:23:45 -06:00
nickpons666
fefd3a8d78 Cambio de respuesta de PDF a tabla de texto en búsqueda por nombre del bot 2026-01-31 02:19:46 -06:00
nickpons666
a02e77d09f Actualización del bot de Telegram: Renombrado de botón PDF y eliminación de menú al solicitarlo 2026-01-31 02:17:53 -06:00
nickpons666
959b5e3596 Implementación de actualización instantánea de roles sin cierre de sesión 2026-01-31 02:12:51 -06:00
nickpons666
9b219c7856 Restricción de permisos para rol coordinador en asignaciones y navbar 2026-01-31 02:08:38 -06:00
nickpons666
67c4d8173f feat: Implementar rol Coordinador con permisos granulares
- Crear nuevo rol Coordinador con permisos específicos de gestión
- Modificar Auth.php para soportar isCoordinador() y requireCoordinador()
- Actualizar User.php con método getUsuariosGestion() para incluir coordinadores
- Corregir Asignacion.php para que getAyudantesPorOrden() incluya coordinadores
- Crear panel especial para coordinadores en coordinador.php
- Implementar restricciones granulares en usuarios.php
  • Coordinadores no pueden ver/editar/desactivar administradores
  • No pueden crear otros administradores (se convierte a coordinador)
  • Solo pueden gestionar ayudantes y otros coordinadores
- Actualizar navbar para mostrar rol específico con badges
- Mejorar ayudante.php para que coordinadores puedan usar navbar completo
- Añadir secciones especiales de gestión para coordinadores
- Actualizar todos los PDFs y bot de Telegram para incluir coordinadores
- Mantener retrocompatibilidad con usuarios y administradores existentes

Permisos Coordinador:
 Ver/editar usuarios y ayudantes
 Gestionar turnos y orden de rotación
 Generar turnos automáticamente
 Exportar PDFs y usar bot de Telegram
 Acceder a configuración general, logs, webhook
 Administrar otros administradores
2026-01-31 01:54:14 -06:00
nickpons666
8bd34c8ddb fix: Incluir coordinadores en todas las listas de gestión
- Crear método getUsuariosGestion() que incluye ayudantes y coordinadores
- Actualizar asignaciones_completo.php para mostrar coordinadores
- Actualizar todas las páginas de gestión para usar nuevo método
- Corregir que coordinadores aparezcan en asignaciones masivas
- Mantener consistencia en todos los módulos de administración
2026-01-31 01:18:28 -06:00
nickpons666
acc0033e63 feat: Añadir panel especial para coordinadores en ayudante.php
- Añadir sección especial para coordinadores con estadísticas y acciones rápidas
- Integrar botones de gestión directa en todas las secciones de turnos
- Permitir acceso coordinador sin redirección
- Mostrar acciones específicas según rol (admin vs coordinador vs ayudante)
- Añadir barra final de acciones para coordinadores
2026-01-31 01:05:14 -06:00
nickpons666
6a5a1d48ef feat: Crear interfaz simple de gestión y unificar vista de ayudante
- Crear versión ultra simple de admin/asignaciones.php con solo funcionalidades esenciales
- Mover asignaciones complejas a asignaciones_completo.php para acceso avanzado
- Modificar ayudante.php para permitir acceso a administradores sin redirigir
- Añadir detección automática: usuarios ven navbar simple, admins ven navbar completo
- Eliminar archivo duplicado vista-ayudante.php para evitar mantener código duplicado
- Corregir rutas BASE_PATH para compatibilidad entre archivos en /admin/ y /public/
- Añadir enlace 👥 Vista Ayudante en navbar de admin
- Mejorar experiencia de usuario con diseño limpio y minimalista
2026-01-31 00:29:01 -06:00
nickpons666
ffda892859 fix: Corregir orden de ayudantes en PDFs y bot de Telegram
- Cambiar getAyudantesActivos() por getAyudantesPorOrden() en todos los PDFs y bot
- Añadir tabla 'Turnos de Ayudantes' en ambos PDFs (ayudante y admin)
- Mostrar 4 próximas fechas para cada ayudante en orden correcto
- En PDF de ayudante: resaltar fila del usuario con badge 'Tu'
- Corregir sendPDF() y sendPDFGeneral() en bot de Telegram
- Asegurar consistencia: Ana → Esperanza → Mary → Bety → Mariela
- Sincronizar completamente web, PDFs y bot con nuevo orden de rotación
2026-01-30 22:52:32 -06:00
nickpons666
6823a5d6d3 fix: Mostrar orden correcto y asignaciones en ayudante.php
- Cambiar getAyudantesActivos() por getAyudantesPorOrden() para mostrar orden correcto
- Añadir columna 'Ayudante' en tabla de horarios para mostrar quién está asignado cada semana
- Mostrar badges con nombres de ayudantes asignados (verde para usuario actual, gris para otros)
- Añadir badge 'Tú' para identificar fácilmente al usuario actual
- Mejorar visualización de asignaciones en tabla semanal
2026-01-30 22:39:26 -06:00
nickpons666
76b0584667 fix: Corregir funcionalidad de reordenamiento y generación automática de turnos
- Añadir jQuery para resolver error 'jQuery is not defined'
- Corregir variable $db indefinida en reordenamiento de rotación
- Añadir método getDb() a clase Asignacion para acceso a base de datos
- Implementar procesamiento correcto de acción 'rotacion_automatica'
- Actualizar inputs ocultos al reordenar elementos visuales
- Añadir redirección para recargar datos actualizados después de guardar orden
- Mejorar manejo de mensajes de éxito/errores en generación automática
2026-01-30 22:30:55 -06:00
nickpons666
4c7f298acd refactor: Centralizar y corregir la comunicación con la API de Telegram
Se refactoriza toda la comunicación con la API de Telegram para solucionar un problema de latencia severa en el entorno Docker. El problema era causado por un retraso en la resolución de red.

- Se mejora la función  en  para forzar el uso de IPv4, añadir timeouts y soportar métodos GET/POST.
- Se centraliza la lógica de la API en la clase , añadiendo los métodos ,  y .
- Se modifica  para que utilice los nuevos métodos centralizados, eliminando código cURL duplicado y aplicando la solución de red.
- Se mantiene la instrumentación en  para futuros diagnósticos, según lo solicitado.
2026-01-20 20:20:59 -06:00
24 changed files with 1593 additions and 596 deletions

0
000-default.conf Normal file → Executable file
View File

View File

@@ -0,0 +1,30 @@
# Plan de Diagnóstico de Rendimiento del Bot de Telegram
Este archivo rastrea los pasos para diagnosticar la latencia en las respuestas del bot cuando se ejecuta en Docker.
## Análisis Final
- **Diagnóstico Definitivo:** El problema no era la base de datos, sino 100% la capa de red. Cada petición (`cURL`) desde el contenedor Docker a la API de Telegram (`api.telegram.org`) sufría una demora de ~5.6 segundos. Esto se debía probablemente a un intento fallido de conexión por IPv6 antes de recurrir a IPv4.
- **Solución Aplicada:** Se modificó la función `private function request` en `bot/TelegramBot.php` para forzar el uso de IPv4 en todas las peticiones `cURL`, además de añadir timeouts de seguridad. Se refactorizó `public/admin/webhook.php` para usar los métodos centralizados en `TelegramBot.php`, asegurando que la solución se aplique de manera consistente en toda la aplicación.
## Pasos de Diagnóstico (Historial)
- [x] **Paso 1: Medir Tiempos dentro del Script (Instrumentación)**
- **Estado:** Completado.
- **Análisis:** Los logs revelaron que cada llamada a la API de Telegram tardaba ~5.6s, y las acciones con dos llamadas (como `ver_turnos`) tardaban ~11.2s.
- [x] **Paso 2: Diagnóstico de Red y DNS desde DENTRO del Contenedor**
- **Estado:** Revisado.
- **Análisis:** La prueba inicial con `curl` ya apuntaba a una conexión lenta (~0.8s), lo que fue el primer indicio del problema de red.
- [ ] **Paso 3: Análisis de Recursos del Contenedor**
- **Estado:** Cancelado.
- **Análisis:** Se determinó que el problema era de red, no de consumo de CPU/Memoria, por lo que este paso ya no es necesario.
- [x] **Paso 4: Revisión y Corrección de la Capa de Red (TelegramBot.php y admin/webhook.php)**
- **Estado:** Completado.
- **Acción:** Se analizó y mejoró `bot/TelegramBot.php` y se refactorizó `public/admin/webhook.php` para usar la lógica de comunicación centralizada y corregida.
- [ ] **Paso 5: Verificación de la Solución**
- **Estado:** En progreso.
- **Acción:** Esperando los resultados de `logs/webhook_timing.log` y la confirmación visual de la página `admin/webhook.php` después de reiniciar el contenedor y probar el bot con el parche aplicado.

0
Dockerfile Normal file → Executable file
View File

0
apache2.conf Normal file → Executable file
View File

View File

@@ -14,14 +14,41 @@ class TelegramBot {
$this->apiUrl = "https://api.telegram.org/bot{$this->token}"; $this->apiUrl = "https://api.telegram.org/bot{$this->token}";
} }
private function request($method, $data = []) { private function request($method, $data = [], $httpMethod = 'POST') {
$url = "{$this->apiUrl}/{$method}"; $url = "{$this->apiUrl}/{$method}";
$ch = curl_init($url); $ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// --- MEJORAS ---
// 1. Forzar el uso de IPv4.
curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
// 2. Añadir timeouts de seguridad.
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); // 5 segundos para conectar
curl_setopt($ch, CURLOPT_TIMEOUT, 10); // 10 segundos para la transferencia total
// 3. (ÚLTIMO INTENTO) Forzar versión de TLS.
curl_setopt($ch, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2);
if ($httpMethod === 'POST') {
curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_URL, $url);
} else { // GET request
$url .= empty($data) ? '' : '?' . http_build_query($data);
curl_setopt($ch, CURLOPT_URL, $url);
}
$response = curl_exec($ch); $response = curl_exec($ch);
// Añadir manejo de errores de cURL
if (curl_errno($ch)) {
error_log("cURL Error en {$method}: " . curl_error($ch));
curl_close($ch);
return null;
}
curl_close($ch); curl_close($ch);
return json_decode($response, true); return json_decode($response, true);
} }
@@ -39,13 +66,30 @@ class TelegramBot {
} }
public function getMe() { public function getMe() {
return $this->request('getMe'); return $this->request('getMe', [], 'GET');
} }
public function getUpdates($offset = 0) { public function getUpdates($offset = 0) {
return $this->request('getUpdates', ['offset' => $offset, 'timeout' => 60]); return $this->request('getUpdates', ['offset' => $offset, 'timeout' => 60], 'GET');
} }
// --- NUEVOS MÉTODOS PARA CENTRALIZAR COMUNICACIONES ---
public function getWebhookInfo() {
return $this->request('getWebhookInfo', [], 'GET');
}
public function deleteWebhook() {
return $this->request('deleteWebhook', []);
}
public function setWebhook($webhookUrl, $allowedUpdates = ['message', 'callback_query']) {
return $this->request('setWebhook', [
'url' => $webhookUrl,
'allowed_updates' => $allowedUpdates
]);
}
public function sendKeyboard($chatId, $text) { public function sendKeyboard($chatId, $text) {
$keyboard = [ $keyboard = [
'inline_keyboard' => [ 'inline_keyboard' => [
@@ -54,7 +98,7 @@ class TelegramBot {
['text' => 'Semana Actual', 'callback_data' => 'semana_actual'] ['text' => 'Semana Actual', 'callback_data' => 'semana_actual']
], ],
[ [
['text' => '📄 Mi PDF', 'callback_data' => 'mi_pdf'] ['text' => '📄 Horarios PDF', 'callback_data' => 'mi_pdf']
], ],
[ [
['text' => '🔍 Buscar por Nombre', 'callback_data' => 'buscar_nombre'] ['text' => '🔍 Buscar por Nombre', 'callback_data' => 'buscar_nombre']
@@ -168,7 +212,7 @@ class TelegramBot {
return "Error de conexion."; return "Error de conexion.";
} }
$stmt = $pdo->prepare("SELECT * FROM users WHERE nombre LIKE ? AND rol = 'ayudante' AND activo = 1 LIMIT 1"); $stmt = $pdo->prepare("SELECT * FROM users WHERE nombre LIKE ? AND (rol = 'ayudante' OR rol = 'coordinador') AND activo = 1 LIMIT 1");
$stmt->execute(["%$nombre%"]); $stmt->execute(["%$nombre%"]);
$user = $stmt->fetch(); $user = $stmt->fetch();
@@ -227,7 +271,7 @@ class TelegramBot {
$db = \Database::getInstance()->getConnection(); $db = \Database::getInstance()->getConnection();
$horarios = $horariosModel->getActivos(); $horarios = $horariosModel->getActivos();
$ayudantes = $userModel->getAyudantesActivos(); $ayudantes = $asignacionModel->getAyudantesPorOrden();
$semanasFuturas = []; $semanasFuturas = [];
$hoy = new DateTime(); $hoy = new DateTime();
@@ -356,7 +400,7 @@ class TelegramBot {
$db = \Database::getInstance()->getConnection(); $db = \Database::getInstance()->getConnection();
$horarios = $horariosModel->getActivos(); $horarios = $horariosModel->getActivos();
$ayudantes = $userModel->getAyudantesActivos(); $ayudantes = $asignacionModel->getAyudantesPorOrden();
$semanasFuturas = []; $semanasFuturas = [];
$hoy = new DateTime(); $hoy = new DateTime();

View File

@@ -1,5 +1,35 @@
<?php <?php
// ---- INICIO DE INSTRUMENTACIÓN ----
$startTime = microtime(true);
function log_timing($message) {
static $lastTime = null;
global $startTime;
if ($lastTime === null) {
$lastTime = $startTime;
}
$now = microtime(true);
$elapsed = $now - $lastTime;
$totalElapsed = $now - $startTime;
$logMessage = sprintf(
"[%s] Total: %.4fs | Step: %.4fs | %s\n",
date('Y-m-d H:i:s'),
$totalElapsed,
$elapsed,
$message
);
// Usar __DIR__ para asegurar la ruta correcta
file_put_contents(__DIR__ . '/../logs/webhook_timing.log', $logMessage, FILE_APPEND);
$lastTime = $now;
}
// ---- FIN DE INSTRUMENTACIÓN ----
// Configurar logging de errores // Configurar logging de errores
error_reporting(E_ALL); error_reporting(E_ALL);
ini_set('log_errors', 1); ini_set('log_errors', 1);
@@ -13,20 +43,25 @@ class TurnoBot {
private $config; private $config;
public function __construct() { public function __construct() {
log_timing("TurnoBot: __construct start");
$this->config = require __DIR__ . '/../config/config.php'; $this->config = require __DIR__ . '/../config/config.php';
$this->bot = new TelegramBot(); $this->bot = new TelegramBot();
log_timing("TurnoBot: __construct end");
} }
public function handleUpdate($update) { public function handleUpdate($update) {
log_timing("handleUpdate: start");
try { try {
// Manejar callback de botones inline // Manejar callback de botones inline
if (isset($update['callback_query'])) { if (isset($update['callback_query'])) {
log_timing("handleUpdate: detected callback_query");
$this->handleCallback($update['callback_query']); $this->handleCallback($update['callback_query']);
return; return;
} }
// Manejar mensajes normales // Manejar mensajes normales
if (!isset($update['message'])) { if (!isset($update['message'])) {
log_timing("handleUpdate: no message found, exiting");
return; return;
} }
@@ -35,64 +70,85 @@ class TurnoBot {
$text = trim($message['text'] ?? ''); $text = trim($message['text'] ?? '');
if (empty($text)) { if (empty($text)) {
log_timing("handleUpdate: empty text, exiting");
return; return;
} }
$textLower = mb_strtolower($text, 'UTF-8'); $textLower = mb_strtolower($text, 'UTF-8');
log_timing("handleUpdate: processing command '{$textLower}'");
// Comandos // Comandos
if ($textLower === '/start' || $textLower === '/menu' || $textLower === 'menu') { if ($textLower === '/start' || $textLower === '/menu' || $textLower === 'menu') {
$this->sendMenu($chatId); $this->sendMenu($chatId);
} elseif ($textLower === '/turnos' || $textLower === 'turnos') { } elseif ($textLower === '/turnos' || $textLower === 'turnos') {
log_timing("handleUpdate: /turnos command start");
$this->bot->sendMessage($chatId, $this->bot->getTablaTurnos(8)); $this->bot->sendMessage($chatId, $this->bot->getTablaTurnos(8));
log_timing("handleUpdate: /turnos command end");
} elseif ($textLower === '/semana' || $textLower === 'semana' || $textLower === 'hoy') { } elseif ($textLower === '/semana' || $textLower === 'semana' || $textLower === 'hoy') {
log_timing("handleUpdate: /semana command start");
$this->bot->sendMessage($chatId, $this->bot->getSemanaActual()); $this->bot->sendMessage($chatId, $this->bot->getSemanaActual());
log_timing("handleUpdate: /semana command end");
} elseif ($textLower === '/ayudantes' || $textLower === 'ayudantes') { } elseif ($textLower === '/ayudantes' || $textLower === 'ayudantes') {
log_timing("handleUpdate: /ayudantes command start");
$ayudantes = $this->bot->getListaAyudantesParaBusqueda(); $ayudantes = $this->bot->getListaAyudantesParaBusqueda();
$this->bot->sendMessage($chatId, "<b>AYUDANTES DISPONIBLES:</b>\n\n" . implode("\n", $ayudantes)); $this->bot->sendMessage($chatId, "<b>AYUDANTES DISPONIBLES:</b>\n\n" . implode("\n", $ayudantes));
log_timing("handleUpdate: /ayudantes command end");
} elseif ($textLower === '/pdf' || $textLower === 'pdf' || $textLower === 'mi pdf') { } elseif ($textLower === '/pdf' || $textLower === 'pdf' || $textLower === 'mi pdf') {
log_timing("handleUpdate: /pdf command start");
$this->bot->sendPDFGeneral($chatId); $this->bot->sendPDFGeneral($chatId);
log_timing("handleUpdate: /pdf command end");
} else { } else {
// Buscar por nombre - verificar si existe el usuario log_timing("handleUpdate: searching by name '{$text}'");
$config = require __DIR__ . '/../config/config.php'; $config = require __DIR__ . '/../config/config.php';
try { try {
log_timing("handleUpdate: DB connection start");
$pdo = new PDO( $pdo = new PDO(
"mysql:host={$config['db']['host']};port={$config['db']['port']};dbname={$config['db']['database']};charset=utf8mb4", "mysql:host={$config['db']['host']};port={$config['db']['port']};dbname={$config['db']['database']};charset=utf8mb4",
$config['db']['username'], $config['db']['username'],
$config['db']['password'], $config['db']['password'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
); );
log_timing("handleUpdate: DB connection end");
} catch (Exception $e) { } catch (Exception $e) {
log_timing("handleUpdate: DB connection FAILED");
$this->bot->sendMessage($chatId, "Error de conexion."); $this->bot->sendMessage($chatId, "Error de conexion.");
return; return;
} }
$stmt = $pdo->prepare("SELECT * FROM users WHERE (nombre LIKE ? OR username LIKE ?) AND rol = 'ayudante' AND activo = 1 LIMIT 1"); log_timing("handleUpdate: DB query start");
$stmt = $pdo->prepare("SELECT * FROM users WHERE (nombre LIKE ? OR username LIKE ?) AND (rol = 'ayudante' OR rol = 'coordinador') AND activo = 1 LIMIT 1");
$stmt->execute(["%$text%", "%$text%"]); $stmt->execute(["%$text%", "%$text%"]);
$user = $stmt->fetch(); $user = $stmt->fetch();
log_timing("handleUpdate: DB query end");
if ($user) { if ($user) {
$this->bot->sendMessage($chatId, "Generando PDF de turnos..."); log_timing("handleUpdate: user found, sending text turnos");
$this->bot->sendPDF($chatId, $user['id']); $this->bot->sendMessage($chatId, $this->bot->getTurnosAyudante($user['nombre']));
} else { } else {
log_timing("handleUpdate: user not found, getting plain text turnos");
$this->bot->sendMessage($chatId, $this->bot->getTurnosAyudante($text)); $this->bot->sendMessage($chatId, $this->bot->getTurnosAyudante($text));
log_timing("handleUpdate: plain text turnos sent");
} }
} }
} catch (Exception $e) { } catch (Exception $e) {
error_log("Error en handleUpdate: " . $e->getMessage()); error_log("Error en handleUpdate: " . $e->getMessage());
log_timing("handleUpdate: EXCEPTION: " . $e->getMessage());
if (isset($update['message']['chat']['id'])) { if (isset($update['message']['chat']['id'])) {
$this->bot->sendMessage($update['message']['chat']['id'], "Error: " . $e->getMessage()); $this->bot->sendMessage($update['message']['chat']['id'], "Error: " . $e->getMessage());
} }
} }
log_timing("handleUpdate: end");
} }
private function handleCallback($callback) { private function handleCallback($callback) {
log_timing("handleCallback: start");
try { try {
$callbackId = $callback['id']; $callbackId = $callback['id'];
$data = $callback['data']; $data = $callback['data'];
$message = $callback['message']; $message = $callback['message'];
$chatId = $message['chat']['id']; $chatId = $message['chat']['id'];
$messageId = $message['message_id']; $messageId = $message['message_id'];
log_timing("handleCallback: processing data '{$data}'");
switch ($data) { switch ($data) {
case 'ver_turnos': case 'ver_turnos':
@@ -107,6 +163,7 @@ class TurnoBot {
case 'mi_pdf': case 'mi_pdf':
$this->bot->answerCallback($callbackId, 'Generando PDF...'); $this->bot->answerCallback($callbackId, 'Generando PDF...');
$this->bot->deleteMessage($chatId, $messageId);
$this->bot->sendPDFGeneral($chatId); $this->bot->sendPDFGeneral($chatId);
break; break;
@@ -124,34 +181,44 @@ class TurnoBot {
default: default:
$this->bot->answerCallback($callbackId, 'Opcion no reconocida'); $this->bot->answerCallback($callbackId, 'Opcion no reconocida');
} }
log_timing("handleCallback: data '{$data}' processed");
} catch (Exception $e) { } catch (Exception $e) {
error_log("Error en handleCallback: " . $e->getMessage()); error_log("Error en handleCallback: " . $e->getMessage());
log_timing("handleCallback: EXCEPTION: " . $e->getMessage());
} }
log_timing("handleCallback: end");
} }
private function sendMenu($chatId) { private function sendMenu($chatId) {
log_timing("sendMenu: start");
$mensaje = "<b>BOT DE TURNOS - CONTENEDOR IBIZA</b>\n\n"; $mensaje = "<b>BOT DE TURNOS - CONTENEDOR IBIZA</b>\n\n";
$mensaje .= "Selecciona una opcion del menu:\n\n"; $mensaje .= "Selecciona una opcion del menu:\n\n";
$mensaje .= "Ver Turnos - Tabla completa de asignaciones\n"; $mensaje .= "Ver Turnos - Tabla completa de asignaciones\n";
$mensaje .= "Semana Actual - Quien tiene turno esta semana\n"; $mensaje .= "Semana Actual - Quien tiene turno esta semana\n";
$mensaje .= "Mi PDF - Descargar horarios en PDF\n"; $mensaje .= "Horarios PDF - Descargar horarios en PDF\n";
$mensaje .= "Buscar por Nombre - Consultar un ayudante especifico\n"; $mensaje .= "Buscar por Nombre - Consultar un ayudante especifico\n";
$mensaje .= "Mi Turno - Ver tu proximo turno"; $mensaje .= "Mi Turno - Ver tu proximo turno";
$this->bot->sendKeyboard($chatId, $mensaje); $this->bot->sendKeyboard($chatId, $mensaje);
log_timing("sendMenu: end");
} }
} }
// Recibir actualización // Recibir actualización
$update = json_decode(file_get_contents('php://input'), true); $update = json_decode(file_get_contents('php://input'), true);
log_timing("Webhook invoked");
// Log para debugging // Log para debugging
error_log("Webhook recibido: " . json_encode($update)); // error_log("Webhook recibido: " . json_encode($update)); // Se puede comentar para no llenar el log de errores
if ($update) { if ($update) {
log_timing("Update received, initializing bot");
$bot = new TurnoBot(); $bot = new TurnoBot();
$bot->handleUpdate($update); $bot->handleUpdate($update);
log_timing("Script finished");
} else { } else {
http_response_code(200); http_response_code(200);
echo "Webhook activo. Usa /start para ver el menu."; log_timing("Webhook checked (no update provided)");
// echo "Webhook activo. Usa /start para ver el menu."; // No es necesario en producción
} }

0
config.yaml Normal file → Executable file
View File

0
docker-compose.yml Normal file → Executable file
View File

0
docker/contenedor-ibiza.conf Normal file → Executable file
View File

598
public/admin/asignaciones.php Executable file → Normal file
View File

@@ -10,7 +10,7 @@ require_once BASE_PATH . '/src/Asignacion.php';
require_once BASE_PATH . '/src/CSRF.php'; require_once BASE_PATH . '/src/CSRF.php';
$auth = new Auth(); $auth = new Auth();
$auth->requireAdmin(); $auth->requireCoordinador();
$userModel = new User(); $userModel = new User();
$horariosModel = new DiasHorarios(); $horariosModel = new DiasHorarios();
@@ -19,6 +19,16 @@ $asignacionModel = new Asignacion();
$message = ''; $message = '';
$messageType = ''; $messageType = '';
if (isset($_GET['success']) && $_GET['success'] === 'orden_actualizado') {
$message = 'Orden actualizado correctamente. Se recalcularon las asignaciones futuras.';
$messageType = 'success';
}
if (isset($_GET['success']) && $_GET['success'] === 'automatica') {
$message = 'Turnos generados automáticamente correctamente.';
$messageType = 'success';
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!CSRF::isValidRequest()) { if (!CSRF::isValidRequest()) {
$message = 'Error de validación del formulario'; $message = 'Error de validación del formulario';
@@ -26,519 +36,247 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} else { } else {
$action = $_POST['action'] ?? ''; $action = $_POST['action'] ?? '';
if ($action === 'asignar') { if ($action === 'asignar_actual') {
$userId = $_POST['user_id'] ?? 0; $userId = $_POST['user_id'] ?? 0;
$semana = $_POST['semana'] ?? '';
if ($userId && $semana) { // Encontrar el domingo actual
$asignacionModel->asignar($userId, $semana); $hoy = new DateTime();
$message = 'Turno asignado correctamente'; $diaSemana = (int)$hoy->format('w');
$domingoActual = clone $hoy;
$domingoActual->modify('-' . $diaSemana . ' days');
$currentWeekStart = $domingoActual->format('Y-m-d');
if ($userId) {
$asignacionModel->asignar($userId, $currentWeekStart);
$message = 'Turno actual asignado correctamente';
$messageType = 'success'; $messageType = 'success';
header("Location: " . $_SERVER['PHP_SELF'] . "?success=actual");
exit;
} }
} elseif ($action === 'rotar') { } elseif ($action === 'rotar_automatica') {
$semana = $_POST['semana'] ?? ''; $resultado = $asignacionModel->asignarSemanasFuturasAutomaticas(12);
$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) { if ($resultado['success'] > 0) {
$message = "Se asignaron {$resultado['success']} turnos correctamente"; header("Location: " . $_SERVER['PHP_SELF'] . "?success=automatica");
if ($rotacionAutomatica) { exit;
$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 { } else {
$message = 'Debes seleccionar al menos un ayudante y una semana'; $message = 'No se pudieron generar turnos: ' . implode(', ', $resultado['errors']);
$messageType = 'danger'; $messageType = 'danger';
} }
} }
} }
} }
$ayudantes = $userModel->getAyudantesActivos(); // Obtener datos principales
$horarios = $horariosModel->getActivos();
// Encontrar el domingo actual
$hoy = new DateTime(); $hoy = new DateTime();
$diaSemana = (int)$hoy->format('w'); // 0 = domingo, 6 = sábado $diaSemana = (int)$hoy->format('w');
$domingoActual = clone $hoy; $domingoActual = clone $hoy;
$domingoActual->modify('-' . $diaSemana . ' days'); // Restar días para llegar al domingo $domingoActual->modify('-' . $diaSemana . ' days');
$currentWeekStart = $domingoActual->format('Y-m-d'); $currentWeekStart = $domingoActual->format('Y-m-d');
$asignacionActual = $asignacionModel->getAsignacionPorSemana($currentWeekStart); $asignacionActual = $asignacionModel->getAsignacionPorSemana($currentWeekStart);
// Para gestión de turnos, incluir ayudantes y coordinadores
$ayudantes = $userModel->getUsuariosGestion();
// Calcular posición en el ciclo (semana X de 4) // Obtener próximos turnos (siguientes 3 semanas)
function calcularPosicionCiclo($semanaInicio) { $proximosTurnos = [];
// Empezamos desde el inicio del ciclo: 28 Dic 2025 for ($i = 1; $i <= 3; $i++) {
$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 = clone $domingoActual;
$semanaDomingo->modify("+{$i} weeks"); $semanaDomingo->modify("+{$i} weeks");
$semanaInicio = $semanaDomingo->format('Y-m-d');
$asignacion = $asignacionModel->getAsignacionPorSemana($semanaInicio);
$key = $semanaDomingo->format('Y-m'); $proximosTurnos[] = [
$mesIngles = $semanaDomingo->format('F'); 'semana' => $semanaInicio,
$mesEspanol = $mesesEspanol[$mesIngles] ?? $mesIngles; 'fin' => date('Y-m-d', strtotime('+5 days', strtotime($semanaInicio))),
$anio = $semanaDomingo->format('Y'); 'asignacion' => $asignacion
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'; $currentPage = 'asignaciones';
$pageTitle = 'Asignación de Turnos'; $pageTitle = 'Gestión de Turnos';
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="es"> <html lang="es">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Asignaciones - Contenedor Ibiza</title> <title>Turnos - Contenedor Ibiza</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <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"> <style>
.turno-card {
transition: transform 0.2s;
}
.turno-card:hover {
transform: translateY(-2px);
}
.btn-accion {
min-width: 120px;
}
.turno-actual {
border-left: 5px solid #198754;
}
</style>
</head> </head>
<body> <body>
<?php include BASE_PATH . '/public/partials/navbar.php'; ?> <?php include BASE_PATH . '/public/partials/navbar.php'; ?>
<div class="container mt-4"> <div class="container mt-4">
<h2 class="mb-4">Asignación de Turnos</h2> <div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0">📅 Gestión de Turnos</h2>
<a href="/admin/asignaciones_completo.php" class="btn btn-outline-secondary btn-sm">
⚙️ Vista Avanzada
</a>
</div>
<?php if ($message): ?> <?php if ($message): ?>
<div class="alert alert-<?= $messageType ?>"><?= htmlspecialchars($message) ?></div> <div class="alert alert-<?= $messageType ?> alert-dismissible fade show">
<?= htmlspecialchars($message) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?> <?php endif; ?>
<!-- TURNO ACTUAL -->
<div class="row mb-4"> <div class="row mb-4">
<div class="col-md-6"> <div class="col-12">
<div class="card shadow-sm"> <div class="card turno-actual shadow-sm">
<div class="card-header bg-primary text-white"> <div class="card-header bg-success text-white">
<h5 class="mb-0">Asignación Actual (Semana <?= $posicionCicloActual ?> de 4)</h5> <h5 class="mb-0">📍 Turno Actual</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<p class="mb-3"> <div class="row align-items-center">
<strong>Fecha:</strong> <?= date('d/m/y', strtotime($currentWeekStart)) ?> (Dom) - <?= date('d/m/y', strtotime('+5 days', strtotime($currentWeekStart))) ?> (Vie) <div class="col-md-6">
<p class="mb-2">
<strong>📆 Semana:</strong>
<?= date('d/m/Y', strtotime($currentWeekStart)) ?>
al <?= date('d/m/Y', strtotime('+5 days', strtotime($currentWeekStart))) ?>
</p> </p>
<?php if ($asignacionActual): ?> <?php if ($asignacionActual): ?>
<div class="alert alert-success"> <div class="alert alert-success mb-3">
<strong>Asignado a:</strong> <?= htmlspecialchars($asignacionActual['nombre']) ?> <h6 class="mb-1">✅ Asignado a:</h6>
<strong class="fs-5"><?= htmlspecialchars($asignacionActual['nombre']) ?></strong>
</div> </div>
<form method="POST" class="d-flex gap-2">
<?= CSRF::getTokenField() ?>
<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: ?> <?php else: ?>
<div class="alert alert-warning">No hay asignación para esta semana</div> <div class="alert alert-warning mb-3">
<h6 class="mb-1">⚠️ Sin asignar</h6>
<form method="POST"> <span class="fs-5">No hay ayudante asignado esta semana</span>
<input type="hidden" name="action" value="asignar">
<?= CSRF::getTokenField() ?>
<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> </div>
<button type="submit" class="btn btn-primary">Asignar</button>
</form>
<?php endif; ?> <?php endif; ?>
</div> </div>
</div>
</div>
<div class="col-md-6"> <div class="col-md-6 text-center">
<div class="card shadow-sm"> <form method="POST" class="d-flex justify-content-center gap-2">
<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">
<?= CSRF::getTokenField() ?> <?= CSRF::getTokenField() ?>
<input type="hidden" name="action" value="asignar"> <input type="hidden" name="action" value="asignar_actual">
<input type="hidden" name="semana" value="<?= $semanaVer ?>">
<select class="form-select" name="user_id" style="max-width: 250px;"> <select class="form-select" name="user_id" style="max-width: 200px;" required>
<option value="">Cambiar persona...</option> <option value="">Seleccionar...</option>
<?php foreach ($ayudantes as $a): ?> <?php foreach ($ayudantes as $a): ?>
<option value="<?= $a['id'] ?>" <?= isset($asignacionVer['user_id']) && $a['id'] == $asignacionVer['user_id'] ? 'selected' : '' ?>> <option value="<?= $a['id'] ?>"
<?= $asignacionActual && $a['id'] == $asignacionActual['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($a['nombre']) ?> <?= htmlspecialchars($a['nombre']) ?>
</option> </option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </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"> <button type="submit" class="btn btn-success btn-accion">
<input type="hidden" name="action" value="asignar"> 💾 Asignar
<input type="hidden" name="semana" value="<?= $semanaVer ?>"> </button>
<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> </form>
<?php endif; ?> </div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Sección de Asignación Masiva --> <!-- ACCIONES RÁPIDAS -->
<div class="card shadow-sm mt-4"> <div class="row mb-4">
<div class="card-header bg-success text-white"> <div class="col-12">
<h5 class="mb-0">Asignación Masiva</h5> <div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">⚡ Acciones Rápidas</h5>
</div> </div>
<div class="card-body"> <div class="card-body text-center">
<form method="POST" id="asignacionMasivaForm"> <div class="row">
<div class="col-md-6 mb-3">
<div class="p-3">
<h6 class="mb-3">🔄 Generar Turnos Automáticos</h6>
<p class="text-muted mb-3">Crea automáticamente las próximas 12 semanas siguiendo el orden actual</p>
<form method="POST" style="display: inline;">
<?= CSRF::getTokenField() ?> <?= CSRF::getTokenField() ?>
<input type="hidden" name="action" value="asignar_masivo"> <input type="hidden" name="action" value="rotar_automatica">
<button type="submit" class="btn btn-primary btn-lg btn-accion">
<div class="row mb-3"> 🚀 Generar Automático
<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>
<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> </form>
</div> </div>
</div> </div>
<!-- Sección de Rotación Automática --> <div class="col-md-6 mb-3">
<div class="card shadow-sm mt-4"> <div class="p-3">
<div class="card-header bg-warning text-dark"> <h6 class="mb-3">👥 Gestionar Orden</h6>
<h5 class="mb-0">Rotación Automática</h5> <p class="text-muted mb-3">Modifica el orden de rotación de los ayudantes</p>
<a href="/admin/asignaciones_completo.php#reordenar" class="btn btn-outline-primary btn-lg btn-accion">
⚙️ Orden de Rotación
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- PRÓXIMOS TURNOS -->
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-info text-white">
<h5 class="mb-0">📋 Próximos Turnos</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row"> <div class="row">
<div class="col-md-8"> <?php foreach ($proximosTurnos as $index => $turno): ?>
<h6>Orden de Rotación Actual:</h6> <div class="col-md-4 mb-3">
<div class="d-flex flex-wrap gap-2 mb-3"> <div class="card turno-card h-100">
<?php <div class="card-body text-center">
$ayudantesOrdenados = $asignacionModel->getAyudantesPorOrden(); <h6 class="card-title">
foreach ($ayudantesOrdenados as $index => $ayudante): 📅 <?= date('d/m', strtotime($turno['semana'])) ?> -
?> <?= date('d/m', strtotime($turno['fin'])) ?>
<span class="badge bg-primary fs-6"> </h6>
<?= ($index + 1) ?>. <?= htmlspecialchars($ayudante['nombre']) ?>
</span> <?php if ($turno['asignacion']): ?>
<div class="alert alert-success mb-2">
<strong>✅ <?= htmlspecialchars($turno['asignacion']['nombre']) ?></strong>
</div>
<?php else: ?>
<div class="alert alert-warning mb-2">
<strong>⚠️ Sin asignar</strong>
</div>
<?php endif; ?>
<a href="/admin/asignaciones_completo.php" class="btn btn-sm btn-outline-primary">
Modificar
</a>
</div>
</div>
</div>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<?php if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'rotacion_automatica'): ?> <div class="text-center mt-3">
<?php <a href="/admin/asignaciones_completo.php" class="btn btn-outline-info">
$resultado = $asignacionModel->asignarSemanasFuturasAutomaticas(12); 📊 Ver todos los turnos
?> </a>
<div class="alert alert-<?= !empty($resultado['errors']) ? 'warning' : 'success' ?>"> </div>
<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">
<?= CSRF::getTokenField() ?>
<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> </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> </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">
<?= CSRF::getTokenField() ?>
<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 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> </body>
</html> </html>

View File

@@ -0,0 +1,568 @@
<?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';
require_once BASE_PATH . '/src/CSRF.php';
$auth = new Auth();
$auth->requireCoordinador();
$userModel = new User();
$horariosModel = new DiasHorarios();
$asignacionModel = new Asignacion();
$message = '';
$messageType = '';
if (isset($_GET['success']) && $_GET['success'] === 'orden_actualizado') {
$message = 'Orden actualizado correctamente. Se recalcularon las asignaciones futuras.';
$messageType = 'success';
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!CSRF::isValidRequest()) {
$message = 'Error de validación del formulario';
$messageType = 'danger';
} else {
$action = $_POST['action'] ?? '';
if ($auth->isAdmin()) {
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';
}
}
}
// Acciones permitidas tanto para Admin como para Coordinador
if ($action === 'rotacion_automatica') {
$resultado = $asignacionModel->asignarSemanasFuturasAutomaticas(12);
if ($resultado['success'] > 0) {
$message = "Se generaron {$resultado['success']} semanas futuras correctamente";
if (!empty($resultado['errors'])) {
$message .= ". Errores: " . implode(', ', $resultado['errors']);
$messageType = 'warning';
} else {
$messageType = 'success';
}
} else {
$message = 'No se pudieron generar asignaciones: ' . implode(', ', $resultado['errors']);
$messageType = 'danger';
}
}
}
}
// Para asignaciones masiva, mostrar todos los ayudantes y coordinadores
$ayudantes = $userModel->getUsuariosGestion();
$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; ?>
<?php if ($auth->isAdmin()): ?>
<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">
<?= CSRF::getTokenField() ?>
<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">
<?= CSRF::getTokenField() ?>
<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-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">
<?= CSRF::getTokenField() ?>
<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>
</div>
</div>
<?php endif; ?>
<!-- Sección de Asignación Masiva -->
<?php if ($auth->isAdmin()): ?>
<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">
<?= CSRF::getTokenField() ?>
<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>
<?php endif; ?>
<!-- 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>
</div>
<div class="col-md-4">
<form method="POST" class="h-100 d-flex flex-column justify-content-center">
<?= CSRF::getTokenField() ?>
<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">
<?= CSRF::getTokenField() ?>
<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 = $asignacionModel->getDb()->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) {
// Redireccionar para recargar los datos actualizados
header("Location: " . $_SERVER['PHP_SELF'] . "?success=orden_actualizado");
exit;
}
}
?>
<?php endif; ?>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<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));
// Actualizar el valor del input oculto con el nuevo orden
var userId = $(this).data('id');
$(this).find('input[type="hidden"]').attr('name', 'ordenes[' + index + ']').val(userId);
});
}
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>

View File

@@ -0,0 +1,203 @@
<?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->requireCoordinador();
$userModel = new User();
$horariosModel = new DiasHorarios();
$asignacionModel = new Asignacion();
// Estadísticas básicas
$totalUsuarios = count($userModel->getUsuariosGestion());
$totalHorarios = count($horariosModel->getActivos());
$asignacionActual = $asignacionModel->getAsignacionActual();
// Obtener orden actual de rotación
$ayudantesOrdenados = $asignacionModel->getAyudantesPorOrden();
$currentPage = 'dashboard';
$pageTitle = 'Panel Coordinador';
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Panel Coordinador - Contenedor Ibiza</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.stat-card {
transition: transform 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
}
.rol-badge {
font-size: 0.8em;
}
</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">
<div>
<h2 class="mb-1">🎯 Panel Coordinador</h2>
<p class="text-muted mb-0">Gestión de turnos y ayudantes</p>
</div>
<span class="badge bg-info rol-badge">Coordinador</span>
</div>
<div class="row mb-4">
<div class="col-md-4 mb-3">
<div class="card stat-card shadow-sm border-primary">
<div class="card-body text-center">
<h3 class="text-primary"><?= $totalUsuarios ?></h3>
<p class="mb-0">Ayudantes Activos</p>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card stat-card shadow-sm border-success">
<div class="card-body text-center">
<h3 class="text-success"><?= $totalHorarios ?></h3>
<p class="mb-0">Días Configurados</p>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card stat-card shadow-sm border-warning">
<div class="card-body text-center">
<h3 class="text-warning"><?= count($ayudantesOrdenados) ?></h3>
<p class="mb-0">Orden Rotación</p>
</div>
</div>
</div>
</div>
<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">📋 Turno Actual</h5>
</div>
<div class="card-body">
<?php if ($asignacionActual): ?>
<div class="alert alert-success mb-3">
<h6 class="mb-1">✅ Esta semana:</h6>
<strong><?= htmlspecialchars($asignacionActual['nombre']) ?></strong>
</div>
<?php else: ?>
<div class="alert alert-warning mb-3">
<h6 class="mb-1">⚠️ Sin asignar</h6>
<span>No hay ayudante asignado esta semana</span>
</div>
<?php endif; ?>
<div class="d-grid gap-2">
<a href="/admin/asignaciones.php" class="btn btn-primary">
🔄 Gestionar Turnos
</a>
<a href="/ayudante.php" class="btn btn-outline-primary">
👥 Vista Ayudante
</a>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header bg-success text-white">
<h5 class="mb-0">👥 Ayudantes</h5>
</div>
<div class="card-body">
<h6 class="mb-3">Orden de Rotación:</h6>
<div class="d-flex flex-wrap gap-2 mb-3">
<?php foreach ($ayudantesOrdenados as $index => $ayudante): ?>
<span class="badge bg-secondary">
<?= ($index + 1) ?>. <?= htmlspecialchars($ayudante['nombre']) ?>
</span>
<?php endforeach; ?>
</div>
<div class="d-grid gap-2">
<a href="/admin/usuarios.php" class="btn btn-success">
Agregar Ayudante
</a>
<a href="/admin/asignaciones_completo.php#reordenar" class="btn btn-outline-success">
🔄 Modificar Orden
</a>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="card shadow-sm">
<div class="card-header bg-info text-white">
<h5 class="mb-0">🎯 Acciones Rápidas</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3 mb-3">
<div class="text-center p-3">
<div class="mb-3">🔄</div>
<h6>Generar Turnos</h6>
<p class="text-muted small">Crear próximas semanas automáticamente</p>
<a href="/admin/asignaciones_completo.php" class="btn btn-sm btn-primary">
Generar
</a>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="text-center p-3">
<div class="mb-3">📄</div>
<h6>Exportar PDF</h6>
<p class="text-muted small">Descargar turnos en formato PDF</p>
<a href="/export-pdf.php" target="_blank" class="btn btn-sm btn-danger">
Exportar
</a>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="text-center p-3">
<div class="mb-3">👥</div>
<h6>Ver Turnos</h6>
<p class="text-muted small">Ver asignaciones actuales</p>
<a href="/ayudante.php" class="btn btn-sm btn-info">
Ver Turnos
</a>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="text-center p-3">
<div class="mb-3">🔧</div>
<h6>Gestionar</h6>
<p class="text-muted small">Administrar todo</p>
<a href="/admin/asignaciones_completo.php" class="btn btn-sm btn-secondary">
Gestionar
</a>
</div>
</div>
</div>
</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>

View File

@@ -18,11 +18,11 @@ $asignacionModel = new Asignacion();
$db = \Database::getInstance()->getConnection(); $db = \Database::getInstance()->getConnection();
$totalUsuarios = count($userModel->getAll()); $totalUsuarios = count($userModel->getAll());
$totalAyudantes = count($userModel->getAyudantesActivos()); $totalAyudantes = count($userModel->getUsuariosGestion());
$totalHorarios = count($horariosModel->getAll()); $totalHorarios = count($horariosModel->getAll());
$asignacionActual = $asignacionModel->getAsignacionActual(); $asignacionActual = $asignacionModel->getAsignacionActual();
$ayudantes = $userModel->getAyudantesActivos(); $ayudantes = $asignacionModel->getAyudantesPorOrden();
$horarios = $horariosModel->getAll(); $horarios = $horariosModel->getAll();
$asignaciones = $asignacionModel->getTodasAsignaciones(); $asignaciones = $asignacionModel->getTodasAsignaciones();
@@ -89,6 +89,40 @@ foreach ($asignacionesLimit as $a) {
$html .= '</tbody></table>'; $html .= '</tbody></table>';
$html .= '<h2>Turnos de Ayudantes</h2>';
$html .= '<table>';
$html .= '<thead><tr><th>Ayudante</th><th class="text-center">Fecha 1</th><th class="text-center">Fecha 2</th><th class="text-center">Fecha 3</th><th class="text-center">Fecha 4</th></tr></thead>';
$html .= '<tbody>';
foreach ($ayudantes as $ayudante) {
$stmt = $db->prepare("
SELECT semana_inicio, semana_fin
FROM asignaciones_turnos
WHERE user_id = ? AND semana_inicio >= CURDATE()
ORDER BY semana_inicio
LIMIT 4
");
$stmt->execute([$ayudante['id']]);
$turnos = $stmt->fetchAll();
$html .= '<tr>';
$html .= '<td>' . htmlspecialchars($ayudante['nombre']) . '</td>';
for ($i = 0; $i < 4; $i++) {
$html .= '<td class="text-center">';
if (isset($turnos[$i])) {
$html .= date('d/m/Y', strtotime($turnos[$i]['semana_inicio'])) . ' - ';
$html .= date('d/m/Y', strtotime($turnos[$i]['semana_fin']));
} else {
$html .= '-';
}
$html .= '</td>';
}
$html .= '</tr>';
}
$html .= '</tbody></table>';
$html .= PDFGenerator::getFooter(); $html .= PDFGenerator::getFooter();
$pdf = new PDFGenerator(); $pdf = new PDFGenerator();

View File

@@ -16,7 +16,7 @@ $horariosModel = new DiasHorarios();
$asignacionModel = new Asignacion(); $asignacionModel = new Asignacion();
$totalUsuarios = count($userModel->getAll()); $totalUsuarios = count($userModel->getAll());
$totalAyudantes = count($userModel->getAyudantesActivos()); $totalAyudantes = count($userModel->getUsuariosGestion());
$totalHorarios = count($horariosModel->getAll()); $totalHorarios = count($horariosModel->getAll());
$asignacionActual = $asignacionModel->getAsignacionActual(); $asignacionActual = $asignacionModel->getAsignacionActual();

View File

@@ -9,7 +9,7 @@ require_once BASE_PATH . '/src/CSRF.php';
require_once BASE_PATH . '/src/Session.php'; require_once BASE_PATH . '/src/Session.php';
$auth = new Auth(); $auth = new Auth();
$auth->requireAdmin(); $auth->requireCoordinador();
$userModel = new User(); $userModel = new User();
$message = ''; $message = '';
@@ -51,13 +51,27 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$password = $_POST['password'] ?? ''; $password = $_POST['password'] ?? '';
$rol = $_POST['rol'] ?? 'ayudante'; $rol = $_POST['rol'] ?? 'ayudante';
if (empty($nombre) || empty($email)) { // Obtener usuario actual para verificar su rol
$usuarioActual = $userModel->getById($id);
// Prevenir que coordinadores editen administradores
if ($usuarioActual && $usuarioActual['rol'] === 'admin' && !$auth->isAdmin()) {
$message = 'No tienes permisos para editar administradores';
$messageType = 'danger';
}
// Solo los administradores pueden asignar rol de administrador
elseif ($rol === 'admin' && !$auth->isAdmin()) {
$rol = 'coordinador';
}
elseif (empty($nombre) || empty($email)) {
$message = 'Nombre y email son obligatorios'; $message = 'Nombre y email son obligatorios';
$messageType = 'danger'; $messageType = 'danger';
} elseif ($userModel->usernameExists($username, $id)) { }
elseif ($userModel->usernameExists($username, $id)) {
$message = 'El username ya está en uso'; $message = 'El username ya está en uso';
$messageType = 'danger'; $messageType = 'danger';
} else { }
else {
$userModel->update($id, compact('nombre', 'email', 'username', 'password', 'rol')); $userModel->update($id, compact('nombre', 'email', 'username', 'password', 'rol'));
$message = 'Usuario actualizado exitosamente'; $message = 'Usuario actualizado exitosamente';
$messageType = 'success'; $messageType = 'success';
@@ -65,6 +79,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} elseif ($action === 'toggle') { } elseif ($action === 'toggle') {
$id = $_POST['id'] ?? 0; $id = $_POST['id'] ?? 0;
$user = $userModel->getById($id); $user = $userModel->getById($id);
// Prevenir que coordinadores desactiven administradores
if ($user && $user['rol'] === 'admin' && !$auth->isAdmin()) {
$message = 'No tienes permisos para modificar administradores';
$messageType = 'danger';
}
elseif ($user && $user['id'] == Session::get('user_id')) {
$message = 'No puedes desactivar tu propio usuario';
$messageType = 'danger';
}
else {
if ($user) { if ($user) {
if ($user['activo']) { if ($user['activo']) {
$userModel->deactivate($id); $userModel->deactivate($id);
@@ -77,8 +102,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} }
} }
} }
}
$users = $userModel->getAll(true); $users = $userModel->getAll(true);
// Filtrar administradores para coordinadores
if ($auth->isCoordinador() && !$auth->isAdmin()) {
$users = array_filter($users, function($user) {
return $user['rol'] !== 'admin';
});
}
$currentPage = 'usuarios'; $currentPage = 'usuarios';
$pageTitle = 'Gestión de Usuarios'; $pageTitle = 'Gestión de Usuarios';
?> ?>
@@ -126,8 +159,19 @@ $pageTitle = 'Gestión de Usuarios';
<td><?= htmlspecialchars($u['nombre']) ?></td> <td><?= htmlspecialchars($u['nombre']) ?></td>
<td><?= htmlspecialchars($u['email']) ?></td> <td><?= htmlspecialchars($u['email']) ?></td>
<td> <td>
<span class="badge bg-<?= $u['rol'] === 'admin' ? 'danger' : 'primary' ?>"> <?php
<?= ucfirst($u['rol']) ?> $badgeClass = 'primary';
if ($u['rol'] === 'admin') $badgeClass = 'danger';
elseif ($u['rol'] === 'coordinador') $badgeClass = 'success';
?>
<span class="badge bg-<?= $badgeClass ?>">
<?php
if ($u['rol'] === 'coordinador') {
echo '🎯 Coordinador';
} else {
echo ucfirst($u['rol']);
}
?>
</span> </span>
</td> </td>
<td> <td>
@@ -140,7 +184,12 @@ $pageTitle = 'Gestión de Usuarios';
onclick="editUser(<?= $u['id'] ?>, '<?= htmlspecialchars($u['nombre']) ?>', '<?= htmlspecialchars($u['email']) ?>', '<?= htmlspecialchars($u['username'] ?? '') ?>', '<?= $u['rol'] ?>')"> onclick="editUser(<?= $u['id'] ?>, '<?= htmlspecialchars($u['nombre']) ?>', '<?= htmlspecialchars($u['email']) ?>', '<?= htmlspecialchars($u['username'] ?? '') ?>', '<?= $u['rol'] ?>')">
Editar Editar
</button> </button>
<?php if ($u['id'] != Session::get('user_id')): ?> <?php
// Determinar si puede desactivar este usuario
$puedeDesactivar = ($u['id'] != Session::get('user_id')) && $u['rol'] !== 'admin';
?>
<?php if ($puedeDesactivar): ?>
<form method="POST" class="d-inline"> <form method="POST" class="d-inline">
<?= CSRF::getTokenField() ?> <?= CSRF::getTokenField() ?>
<input type="hidden" name="action" value="toggle"> <input type="hidden" name="action" value="toggle">
@@ -194,7 +243,10 @@ $pageTitle = 'Gestión de Usuarios';
<label for="rol" class="form-label">Rol</label> <label for="rol" class="form-label">Rol</label>
<select class="form-select" id="rol" name="rol"> <select class="form-select" id="rol" name="rol">
<option value="ayudante">Ayudante</option> <option value="ayudante">Ayudante</option>
<option value="coordinador">🎯 Coordinador</option>
<?php if ($auth->isAdmin()): ?>
<option value="admin">Administrador</option> <option value="admin">Administrador</option>
<?php endif; ?>
</select> </select>
</div> </div>
</div> </div>

View File

@@ -17,7 +17,7 @@ $botInfo = null;
// Obtener información del bot // Obtener información del bot
$botMe = $bot->getMe(); $botMe = $bot->getMe();
if ($botMe && isset($botMe['ok']) && $botMe['ok']) { if ($botMe && $botMe['ok']) { // Simplificado
$botInfo = $botMe['result']; $botInfo = $botMe['result'];
} }
@@ -26,14 +26,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? ''; $action = $_POST['action'] ?? '';
if ($action === 'verificar') { if ($action === 'verificar') {
$url = "https://api.telegram.org/bot{$config['telegram_bot_token']}/getWebhookInfo"; $result = $bot->getWebhookInfo(); // Usar el método centralizado
$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'])) { if ($result && $result['ok']) { // Simplificado
$webhookInfo = $result; $webhookInfo = $result;
$message = 'Información del webhook obtenida'; $message = 'Información del webhook obtenida';
$messageType = 'success'; $messageType = 'success';
@@ -42,17 +37,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$messageType = 'danger'; $messageType = 'danger';
} }
} elseif ($action === 'borrar') { } elseif ($action === 'borrar') {
$url = "https://api.telegram.org/bot{$config['telegram_bot_token']}/deleteWebhook"; $result = $bot->deleteWebhook(); // Usar el método centralizado
$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']) { if ($result && $result['ok']) { // Simplificado
$message = 'Webhook eliminado correctamente'; $message = 'Webhook eliminado correctamente';
$messageType = 'success'; $messageType = 'success';
$webhookInfo = null; $webhookInfo = null;
@@ -70,20 +57,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$message = 'La URL ingresada no es válida'; $message = 'La URL ingresada no es válida';
$messageType = 'danger'; $messageType = 'danger';
} else { } else {
$url = "https://api.telegram.org/bot{$config['telegram_bot_token']}/setWebhook"; $result = $bot->setWebhook($webhookUrl); // Usar el método centralizado
$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']) { if ($result && $result['ok']) { // Simplificado
$message = "Webhook configurado correctamente en:\n" . htmlspecialchars($webhookUrl); $message = "Webhook configurado correctamente en:\n" . htmlspecialchars($webhookUrl);
$messageType = 'success'; $messageType = 'success';
} else { } else {
@@ -94,13 +70,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} }
} }
// Obtener estado actual del webhook // Obtener estado actual del webhook al cargar la página
$url = "https://api.telegram.org/bot{$config['telegram_bot_token']}/getWebhookInfo"; $webhookInfo = $bot->getWebhookInfo(); // Usar el método centralizado
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$webhookInfo = json_decode($response, true);
$currentPage = 'webhook'; $currentPage = 'webhook';
$pageTitle = 'Administración del Bot de Telegram'; $pageTitle = 'Administración del Bot de Telegram';

View File

@@ -1,23 +1,25 @@
<?php <?php
require_once __DIR__ . '/../src/Auth.php'; if (!defined('BASE_PATH')) {
require_once __DIR__ . '/../src/User.php'; define('BASE_PATH', dirname(__DIR__));
require_once __DIR__ . '/../src/DiasHorarios.php'; }
require_once __DIR__ . '/../src/Asignacion.php'; require_once BASE_PATH . '/src/Auth.php';
require_once __DIR__ . '/../src/Database.php'; require_once BASE_PATH . '/src/User.php';
require_once BASE_PATH . '/src/DiasHorarios.php';
require_once BASE_PATH . '/src/Asignacion.php';
require_once BASE_PATH . '/src/Database.php';
$auth = new Auth(); $auth = new Auth();
$auth->requireAuth(); $auth->requireAuth();
if ($auth->isAdmin()) { // Permitir acceso a administradores para que puedan ver la vista de ayudante
header('Location: /admin/index.php');
exit;
}
$user = $auth->getCurrentUser(); $user = $auth->getCurrentUser();
$horariosModel = new DiasHorarios(); $horariosModel = new DiasHorarios();
$asignacionModel = new Asignacion(); $asignacionModel = new Asignacion();
$db = Database::getInstance()->getConnection(); $db = Database::getInstance()->getConnection();
$horarios = $horariosModel->getActivos(); $horarios = $horariosModel->getActivos();
$asignacionActual = $asignacionModel->getAsignacionActual(); $asignacionActual = $asignacionModel->getAsignacionActual();
@@ -58,7 +60,7 @@ foreach ($semanasFuturas as $semana) {
} }
$userModel = new User(); $userModel = new User();
$ayudantes = $userModel->getAyudantesActivos(); $ayudantes = $asignacionModel->getAyudantesPorOrden();
$domingo = new DateTime(); $domingo = new DateTime();
$domingo->modify('-' . (int)$domingo->format('w') . ' days'); $domingo->modify('-' . (int)$domingo->format('w') . ' days');
@@ -72,6 +74,13 @@ $domingo->modify('-' . (int)$domingo->format('w') . ' days');
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head> </head>
<body> <body>
<?php
// Si es administrador o coordinador, usar el navbar del admin
if ($auth->isAdmin() || $auth->isCoordinador()) {
include BASE_PATH . '/public/partials/navbar.php';
} else {
// Si es ayudante, usar navbar simple
?>
<nav class="navbar navbar-dark bg-primary"> <nav class="navbar navbar-dark bg-primary">
<div class="container"> <div class="container">
<a class="navbar-brand" href="/ayudante.php">Contenedor Ibiza</a> <a class="navbar-brand" href="/ayudante.php">Contenedor Ibiza</a>
@@ -82,14 +91,78 @@ $domingo->modify('-' . (int)$domingo->format('w') . ' days');
<a href="/logout.php" class="btn btn-outline-light btn-sm">Cerrar Sesión</a> <a href="/logout.php" class="btn btn-outline-light btn-sm">Cerrar Sesión</a>
</div> </div>
</nav> </nav>
<?php } ?>
<div class="container mt-4"> <div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<div>
<?php if ($auth->isAdmin() || $auth->isCoordinador()): ?>
<h2 class="mb-1">👥 Vista de Ayudante</h2>
<p class="text-muted mb-0">Viendo la interfaz que ven los ayudantes</p>
<?php else: ?>
<h2 class="mb-0">Mis Turnos</h2> <h2 class="mb-0">Mis Turnos</h2>
<?php endif; ?>
</div>
<div>
<?php if ($auth->isAdmin() || $auth->isCoordinador()): ?>
<a href="/admin/asignaciones.php" class="btn btn-outline-secondary btn-sm me-2">
⚙️ Gestionar Turnos
</a>
<?php endif; ?>
<a href="/export-pdf.php" target="_blank" class="btn btn-danger btn-sm"> <a href="/export-pdf.php" target="_blank" class="btn btn-danger btn-sm">
📄 Exportar PDF 📄 Exportar PDF
</a> </a>
</div> </div>
</div>
<?php if ($auth->isCoordinador()): ?>
<!-- SECCIÓN ESPECIAL PARA COORDINADORES -->
<div class="card shadow-sm mb-4 border-success">
<div class="card-header bg-success text-white">
<h5 class="mb-0">🎯 Panel de Coordinador</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<h6 class="mb-2">⚡ Acciones Rápidas</h6>
<div class="d-grid gap-2">
<a href="/admin/asignaciones.php" class="btn btn-primary btn-sm">
🔄 Gestionar Turnos
</a>
<a href="/admin/usuarios.php" class="btn btn-success btn-sm">
👥 Agregar Ayudante
</a>
<a href="/admin/asignaciones_completo.php#reordenar" class="btn btn-outline-primary btn-sm">
📋 Modificar Orden
</a>
</div>
</div>
<div class="col-md-6 mb-3">
<h6 class="mb-2">📈 Estadísticas</h6>
<div class="row text-center">
<div class="col-6">
<div class="border rounded p-2 mb-2">
<div class="h4 mb-0 text-primary"><?= count($ayudantes) ?></div>
<small class="text-muted">Ayudantes</small>
</div>
</div>
<div class="col-6">
<div class="border rounded p-2 mb-2">
<div class="h4 mb-0 text-success"><?= count($misAsignacionesFuturas) ?></div>
<small class="text-muted">Tus turnos</small>
</div>
</div>
</div>
<div class="text-center">
<a href="/export-pdf.php" target="_blank" class="btn btn-danger btn-sm">
📄 Exportar PDF
</a>
</div>
</div>
</div>
</div>
</div>
<?php endif; ?>
<?php <?php
$hoy = new DateTime(); $hoy = new DateTime();
@@ -109,6 +182,8 @@ $domingo->modify('-' . (int)$domingo->format('w') . ' days');
</div> </div>
<?php elseif ($asignacionEstaSemana): ?> <?php elseif ($asignacionEstaSemana): ?>
<div class="alert alert-secondary mb-4"> <div class="alert alert-secondary mb-4">
<div class="d-flex justify-content-between align-items-start">
<div>
<strong>Turno esta semana:</strong> <?= htmlspecialchars($asignacionEstaSemana['nombre']) ?><br> <strong>Turno esta semana:</strong> <?= htmlspecialchars($asignacionEstaSemana['nombre']) ?><br>
<?php if (!empty($misAsignacionesFuturas)): ?> <?php if (!empty($misAsignacionesFuturas)): ?>
Tu proximo turno: <?= date('d/m/y', strtotime($misAsignacionesFuturas[0]['semana']['inicio'])) ?> Tu proximo turno: <?= date('d/m/y', strtotime($misAsignacionesFuturas[0]['semana']['inicio'])) ?>
@@ -117,16 +192,47 @@ $domingo->modify('-' . (int)$domingo->format('w') . ' days');
Tu proximo turno sera en las proximas semanas. Tu proximo turno sera en las proximas semanas.
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php if ($auth->isCoordinador()): ?>
<div class="text-end">
<a href="/admin/asignaciones.php" class="btn btn-sm btn-outline-primary">
🔄 Cambiar
</a>
</div>
<?php endif; ?>
</div>
</div>
<?php elseif (!empty($misAsignacionesFuturas)): ?> <?php elseif (!empty($misAsignacionesFuturas)): ?>
<div class="alert alert-info mb-4"> <div class="alert alert-info mb-4">
<div class="d-flex justify-content-between align-items-start">
<div>
<strong>Proximo turno:</strong><br> <strong>Proximo turno:</strong><br>
Del <?= date('d/m/y', strtotime($misAsignacionesFuturas[0]['semana']['inicio'])) ?> Del <?= date('d/m/y', strtotime($misAsignacionesFuturas[0]['semana']['inicio'])) ?>
al <?= date('d/m/y', strtotime($misAsignacionesFuturas[0]['semana']['fin'])) ?> al <?= date('d/m/y', strtotime($misAsignacionesFuturas[0]['semana']['fin'])) ?>
</div> </div>
<?php if ($auth->isCoordinador()): ?>
<div class="text-end">
<a href="/admin/asignaciones_completo.php" class="btn btn-sm btn-outline-primary">
📋 Gestionar
</a>
</div>
<?php endif; ?>
</div>
</div>
<?php else: ?> <?php else: ?>
<div class="alert alert-warning mb-4"> <div class="alert alert-warning mb-4">
<div class="d-flex justify-content-between align-items-start">
<div>
No hay turnos asignados para las proximas semanas. No hay turnos asignados para las proximas semanas.
</div> </div>
<?php if ($auth->isCoordinador()): ?>
<div class="text-end">
<a href="/admin/asignaciones.php" class="btn btn-sm btn-success">
🚀 Generar Turnos
</a>
</div>
<?php endif; ?>
</div>
</div>
<?php endif; ?> <?php endif; ?>
<div class="card shadow-sm"> <div class="card shadow-sm">
@@ -151,6 +257,7 @@ $domingo->modify('-' . (int)$domingo->format('w') . ' days');
<thead class="table-light"> <thead class="table-light">
<tr> <tr>
<th class="text-center" style="min-width: 120px;">Semana</th> <th class="text-center" style="min-width: 120px;">Semana</th>
<th class="text-center" style="min-width: 120px;">Ayudante</th>
<?php foreach ($diasOrden as $dia): ?> <?php foreach ($diasOrden as $dia): ?>
<th class="text-center"><?= $diasNombres[$dia] ?></th> <th class="text-center"><?= $diasNombres[$dia] ?></th>
<?php endforeach; ?> <?php endforeach; ?>
@@ -172,6 +279,21 @@ $domingo->modify('-' . (int)$domingo->format('w') . ' days');
<span class="badge bg-success ms-1">Tu turno</span> <span class="badge bg-success ms-1">Tu turno</span>
<?php endif; ?> <?php endif; ?>
</td> </td>
<td class="text-center align-middle">
<?php if (!empty($semana['asignaciones'])): ?>
<?php foreach ($semana['asignaciones'] as $asignacion): ?>
<span class="badge <?= $asignacion['id'] == $user['id'] ? 'bg-success' : 'bg-secondary' ?> mb-1">
<?= htmlspecialchars($asignacion['nombre']) ?>
</span>
<?php if ($asignacion['id'] == $user['id']): ?>
<span class="badge bg-warning text-dark">Tú</span>
<?php endif; ?>
<br>
<?php endforeach; ?>
<?php else: ?>
<span class="text-muted">Sin asignación</span>
<?php endif; ?>
</td>
<?php foreach ($diasOrden as $dia): ?> <?php foreach ($diasOrden as $dia): ?>
<?php <?php
$horarioDia = null; $horarioDia = null;
@@ -227,6 +349,9 @@ $domingo->modify('-' . (int)$domingo->format('w') . ' days');
<th class="text-center">Fecha 2</th> <th class="text-center">Fecha 2</th>
<th class="text-center">Fecha 3</th> <th class="text-center">Fecha 3</th>
<th class="text-center">Fecha 4</th> <th class="text-center">Fecha 4</th>
<?php if ($auth->isCoordinador()): ?>
<th class="text-center">Acciones</th>
<?php endif; ?>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -261,6 +386,13 @@ $domingo->modify('-' . (int)$domingo->format('w') . ' days');
<?php endif; ?> <?php endif; ?>
</td> </td>
<?php endfor; ?> <?php endfor; ?>
<?php if ($auth->isCoordinador()): ?>
<td class="text-center">
<a href="/admin/asignaciones_completo.php" class="btn btn-sm btn-outline-primary">
🔄 Gestionar
</a>
</td>
<?php endif; ?>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
@@ -268,6 +400,39 @@ $domingo->modify('-' . (int)$domingo->format('w') . ' days');
</div> </div>
</div> </div>
</div> </div>
<?php if ($auth->isCoordinador()): ?>
<!-- BARRA DE ACCIONES PARA COORDINADORES -->
<div class="card mt-4 shadow-sm border-success">
<div class="card-header bg-success text-white">
<h5 class="mb-0">🎯 Acciones de Coordinador</h5>
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-md-3 mb-3">
<a href="/admin/coordinador.php" class="btn btn-success w-100">
📊 Mi Panel
</a>
</div>
<div class="col-md-3 mb-3">
<a href="/admin/asignaciones.php" class="btn btn-primary w-100">
🔄 Gestionar Turnos
</a>
</div>
<div class="col-md-3 mb-3">
<a href="/admin/usuarios.php" class="btn btn-outline-success w-100">
👥 Agregar Ayudante
</a>
</div>
<div class="col-md-3 mb-3">
<a href="/export-pdf.php" target="_blank" class="btn btn-danger w-100">
📄 Exportar PDF
</a>
</div>
</div>
</div>
</div>
<?php endif; ?>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>

View File

@@ -21,7 +21,7 @@ $db = Database::getInstance()->getConnection();
$horarios = $horariosModel->getActivos(); $horarios = $horariosModel->getActivos();
$userModel = new User(); $userModel = new User();
$ayudantes = $userModel->getAyudantesActivos(); $ayudantes = $asignacionModel->getAyudantesPorOrden();
$semanasFuturas = []; $semanasFuturas = [];
$hoy = new DateTime(); $hoy = new DateTime();
@@ -127,6 +127,44 @@ foreach ($semanasFuturas as $index => $semana) {
$html .= '</tr>'; $html .= '</tr>';
} }
$html .= '</tbody></table>';
$html .= '<h2>Turnos de Ayudantes</h2>';
$html .= '<table>';
$html .= '<thead><tr><th>Ayudante</th><th class="text-center">Fecha 1</th><th class="text-center">Fecha 2</th><th class="text-center">Fecha 3</th><th class="text-center">Fecha 4</th></tr></thead>';
$html .= '<tbody>';
foreach ($ayudantes as $ayudante) {
$stmt = $db->prepare("
SELECT semana_inicio, semana_fin
FROM asignaciones_turnos
WHERE user_id = ? AND semana_inicio >= CURDATE()
ORDER BY semana_inicio
LIMIT 4
");
$stmt->execute([$ayudante['id']]);
$turnos = $stmt->fetchAll();
$html .= '<tr class="' . ($ayudante['id'] == $user['id'] ? 'table-success' : '') . '">';
$html .= '<td>' . htmlspecialchars($ayudante['nombre']);
if ($ayudante['id'] == $user['id']) {
$html .= ' <span class="badge badge-success">Tu</span>';
}
$html .= '</td>';
for ($i = 0; $i < 4; $i++) {
$html .= '<td class="text-center">';
if (isset($turnos[$i])) {
$html .= date('d/m/Y', strtotime($turnos[$i]['semana_inicio'])) . ' - ';
$html .= date('d/m/Y', strtotime($turnos[$i]['semana_fin']));
} else {
$html .= '-';
}
$html .= '</td>';
}
$html .= '</tr>';
}
$html .= '</tbody></table>'; $html .= '</tbody></table>';
$html .= PDFGenerator::getFooter(); $html .= PDFGenerator::getFooter();

View File

@@ -15,35 +15,70 @@ $dbName = getenv('DB_NAME') ?: 'No configurado';
</button> </button>
<div class="collapse navbar-collapse" id="navbarNav"> <div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto"> <ul class="navbar-nav me-auto">
<?php if ($auth->isAdmin()): ?>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link <?= $currentPage === 'dashboard' ? 'active' : '' ?>" href="/admin/index.php">Dashboard</a> <a class="nav-link <?= $currentPage === 'dashboard' ? 'active' : '' ?>" href="/admin/index.php">Dashboard</a>
</li> </li>
<?php else: ?>
<li class="nav-item">
<a class="nav-link <?= $currentPage === 'coordinador' ? 'active' : '' ?>" href="/admin/coordinador.php">🎯 Panel</a>
</li>
<?php endif; ?>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link <?= $currentPage === 'usuarios' ? 'active' : '' ?>" href="/admin/usuarios.php">Usuarios</a> <a class="nav-link <?= $currentPage === 'usuarios' ? 'active' : '' ?>" href="/admin/usuarios.php">Usuarios</a>
</li> </li>
<?php if ($auth->isAdmin()): ?>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link <?= $currentPage === 'horarios' ? 'active' : '' ?>" href="/admin/horarios.php">Horarios</a> <a class="nav-link <?= $currentPage === 'horarios' ? 'active' : '' ?>" href="/admin/horarios.php">Horarios</a>
</li> </li>
<?php endif; ?>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link <?= $currentPage === 'asignaciones' ? 'active' : '' ?>" href="/admin/asignaciones.php">Asignaciones</a> <a class="nav-link <?= $currentPage === 'asignaciones' ? 'active' : '' ?>" href="/admin/asignaciones.php">Asignaciones</a>
</li> </li>
<li class="nav-item">
<a class="nav-link <?= $currentPage === 'vista-ayudante' ? 'active' : '' ?>" href="/ayudante.php">👥 Vista Ayudante</a>
</li>
<?php if ($auth->isAdmin()): ?>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link <?= $currentPage === 'webhook' ? 'active' : '' ?>" href="/admin/webhook.php">🤖 Bot</a> <a class="nav-link <?= $currentPage === 'webhook' ? 'active' : '' ?>" href="/admin/webhook.php">🤖 Bot</a>
</li> </li>
</ul>
<ul class="navbar-nav">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link <?= $currentPage === 'logs' ? 'active' : '' ?>" href="/admin/logs.php">Logs</a> <a class="nav-link <?= $currentPage === 'logs' ? 'active' : '' ?>" href="/admin/logs.php">Logs</a>
</li> </li>
<?php endif; ?>
</ul>
<ul class="navbar-nav">
<?php if ($auth->isAdmin()): ?>
<li class="nav-item">
<a class="nav-link <?= $currentPage === 'logs' ? 'active' : '' ?>" href="/admin/logs.php">Logs</a>
</li>
<?php endif; ?>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"> <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<?= htmlspecialchars($user['nombre'] ?? 'Usuario') ?> <?= htmlspecialchars($user['nombre'] ?? 'Usuario') ?>
</a> </a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><span class="dropdown-item-text d-block"><strong>Usuario:</strong> <?= htmlspecialchars($user['nombre'] ?? 'Usuario') ?></span></li> <li><span class="dropdown-item-text d-block"><strong>Usuario:</strong> <?= htmlspecialchars($user['nombre'] ?? 'Usuario') ?></span></li>
<li><span class="dropdown-item-text d-block"><strong>Rol:</strong> <?= htmlspecialchars(ucfirst($user['rol'] ?? '')) ?></span></li> <li><span class="dropdown-item-text d-block"><strong>Rol:</strong>
<?php
$rol = $user['rol'] ?? '';
if ($rol === 'admin') {
echo 'Administrador';
} elseif ($rol === 'coordinador') {
echo '🎯 Coordinador';
} else {
echo htmlspecialchars(ucfirst($rol));
}
?>
</span></li>
<?php if ($auth->isAdmin()): ?>
<li><span class="dropdown-item-text d-block small text-muted"><strong>DB Host:</strong> <?= htmlspecialchars($dbHost) ?></span></li> <li><span class="dropdown-item-text d-block small text-muted"><strong>DB Host:</strong> <?= htmlspecialchars($dbHost) ?></span></li>
<li><span class="dropdown-item-text d-block small text-muted"><strong>DB Name:</strong> <?= htmlspecialchars($dbName) ?></span></li> <li><span class="dropdown-item-text d-block small text-muted"><strong>DB Name:</strong> <?= htmlspecialchars($dbName) ?></span></li>
<?php endif; ?>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<?php if ($user['rol'] ?? '' === 'ayudante'): ?> <?php if ($user['rol'] ?? '' === 'ayudante'): ?>
<li><a class="dropdown-item" href="/cambiar-password.php">Cambiar Contraseña</a></li> <li><a class="dropdown-item" href="/cambiar-password.php">Cambiar Contraseña</a></li>

View File

@@ -42,7 +42,7 @@ fi
echo "" echo ""
echo -e "${GREEN}Building image...${NC}" echo -e "${GREEN}Building image...${NC}"
docker build -t "$FULL_IMAGE" . docker build --no-cache -t "$FULL_IMAGE" .
echo "" echo ""
read -p "¿Subir imagen al registry? (s/n): " push_confirm read -p "¿Subir imagen al registry? (s/n): " push_confirm

0
scripts/generate-env.php Normal file → Executable file
View File

View File

@@ -9,6 +9,10 @@ class Asignacion {
$this->db = Database::getInstance()->getConnection(); $this->db = Database::getInstance()->getConnection();
} }
public function getDb() {
return $this->db;
}
public function getAsignacionActual() { public function getAsignacionActual() {
$hoy = new DateTime(); $hoy = new DateTime();
$diaSemana = (int)$hoy->format('w'); $diaSemana = (int)$hoy->format('w');
@@ -217,7 +221,7 @@ public function asignar($userId, $semanaInicio) {
SELECT u.*, ro.orden SELECT u.*, ro.orden
FROM users u FROM users u
LEFT JOIN rotacion_orden ro ON u.id = ro.user_id AND ro.activo = 1 LEFT JOIN rotacion_orden ro ON u.id = ro.user_id AND ro.activo = 1
WHERE u.rol = 'ayudante' AND u.activo = 1 WHERE (u.rol = 'ayudante' OR u.rol = 'coordinador') AND u.activo = 1
ORDER BY COALESCE(ro.orden, 999), u.nombre ORDER BY COALESCE(ro.orden, 999), u.nombre
"); ");
return $stmt->fetchAll(); return $stmt->fetchAll();

View File

@@ -10,6 +10,31 @@ class Auth {
public function __construct() { public function __construct() {
$this->userModel = new User(); $this->userModel = new User();
Session::init(); Session::init();
$this->refreshUserSession();
}
/**
* Refresca los datos del usuario en la sesión directamente desde la base de datos
* para que los cambios de rol sean instantáneos.
*/
private function refreshUserSession() {
if ($this->isLoggedIn()) {
$userId = Session::get('user_id');
$user = $this->userModel->getById($userId);
if (!$user || (isset($user['activo']) && !$user['activo'])) {
$this->logout();
return;
}
// Actualizar solo si hay cambios para evitar escrituras innecesarias en la sesión
if (Session::get('user_rol') !== $user['rol']) {
Session::set('user_rol', $user['rol']);
}
if (Session::get('user_name') !== $user['nombre']) {
Session::set('user_name', $user['nombre']);
}
}
} }
public function login($login, $password) { public function login($login, $password) {
@@ -43,10 +68,19 @@ class Auth {
return Session::get('user_rol') === 'ayudante'; return Session::get('user_rol') === 'ayudante';
} }
public function isCoordinador() {
return Session::get('user_rol') === 'coordinador';
}
public function hasRole($role) { public function hasRole($role) {
return Session::get('user_rol') === $role; return Session::get('user_rol') === $role;
} }
public function hasAnyRole($roles) {
$userRole = Session::get('user_rol');
return in_array($userRole, (array)$roles);
}
public function getCurrentUser() { public function getCurrentUser() {
if (!$this->isLoggedIn()) { if (!$this->isLoggedIn()) {
return null; return null;
@@ -73,6 +107,14 @@ class Auth {
} }
} }
public function requireCoordinador($redirectUrl = '/ayudante.php') {
$this->requireAuth($redirectUrl);
if (!$this->isCoordinador() && !$this->isAdmin()) {
header('Location: ' . $redirectUrl);
exit;
}
}
public function requireRole($roles, $redirectUrl = '/') { public function requireRole($roles, $redirectUrl = '/') {
$this->requireAuth($redirectUrl); $this->requireAuth($redirectUrl);
$userRole = Session::get('user_rol'); $userRole = Session::get('user_rol');

View File

@@ -132,8 +132,14 @@ class User {
return $stmt->fetchAll(); return $stmt->fetchAll();
} }
public function getUsuariosGestion() {
// Incluye ayudantes y coordinadores para gestión de turnos
$stmt = $this->db->query("SELECT * FROM users WHERE (rol = 'ayudante' OR rol = 'coordinador') AND activo = 1 ORDER BY nombre");
return $stmt->fetchAll();
}
public function isValidRole($role) { public function isValidRole($role) {
return in_array($role, ['admin', 'ayudante']); return in_array($role, ['admin', 'ayudante', 'coordinador']);
} }
public function hasRole($userId, $role) { public function hasRole($userId, $role) {