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}";
}
private function request($method, $data = []) {
private function request($method, $data = [], $httpMethod = 'POST') {
$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_POSTFIELDS, json_encode($data));
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);
// 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);
return json_decode($response, true);
}
@@ -39,13 +66,30 @@ class TelegramBot {
}
public function getMe() {
return $this->request('getMe');
return $this->request('getMe', [], 'GET');
}
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) {
$keyboard = [
'inline_keyboard' => [
@@ -54,7 +98,7 @@ class TelegramBot {
['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']
@@ -168,7 +212,7 @@ class TelegramBot {
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%"]);
$user = $stmt->fetch();
@@ -227,7 +271,7 @@ class TelegramBot {
$db = \Database::getInstance()->getConnection();
$horarios = $horariosModel->getActivos();
$ayudantes = $userModel->getAyudantesActivos();
$ayudantes = $asignacionModel->getAyudantesPorOrden();
$semanasFuturas = [];
$hoy = new DateTime();
@@ -356,7 +400,7 @@ class TelegramBot {
$db = \Database::getInstance()->getConnection();
$horarios = $horariosModel->getActivos();
$ayudantes = $userModel->getAyudantesActivos();
$ayudantes = $asignacionModel->getAyudantesPorOrden();
$semanasFuturas = [];
$hoy = new DateTime();

View File

@@ -1,5 +1,35 @@
<?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
error_reporting(E_ALL);
ini_set('log_errors', 1);
@@ -13,20 +43,25 @@ class TurnoBot {
private $config;
public function __construct() {
log_timing("TurnoBot: __construct start");
$this->config = require __DIR__ . '/../config/config.php';
$this->bot = new TelegramBot();
log_timing("TurnoBot: __construct end");
}
public function handleUpdate($update) {
log_timing("handleUpdate: start");
try {
// Manejar callback de botones inline
if (isset($update['callback_query'])) {
log_timing("handleUpdate: detected callback_query");
$this->handleCallback($update['callback_query']);
return;
}
// Manejar mensajes normales
if (!isset($update['message'])) {
log_timing("handleUpdate: no message found, exiting");
return;
}
@@ -35,64 +70,85 @@ class TurnoBot {
$text = trim($message['text'] ?? '');
if (empty($text)) {
log_timing("handleUpdate: empty text, exiting");
return;
}
$textLower = mb_strtolower($text, 'UTF-8');
log_timing("handleUpdate: processing command '{$textLower}'");
// Comandos
if ($textLower === '/start' || $textLower === '/menu' || $textLower === 'menu') {
$this->sendMenu($chatId);
} elseif ($textLower === '/turnos' || $textLower === 'turnos') {
log_timing("handleUpdate: /turnos command start");
$this->bot->sendMessage($chatId, $this->bot->getTablaTurnos(8));
log_timing("handleUpdate: /turnos command end");
} elseif ($textLower === '/semana' || $textLower === 'semana' || $textLower === 'hoy') {
log_timing("handleUpdate: /semana command start");
$this->bot->sendMessage($chatId, $this->bot->getSemanaActual());
log_timing("handleUpdate: /semana command end");
} elseif ($textLower === '/ayudantes' || $textLower === 'ayudantes') {
log_timing("handleUpdate: /ayudantes command start");
$ayudantes = $this->bot->getListaAyudantesParaBusqueda();
$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') {
log_timing("handleUpdate: /pdf command start");
$this->bot->sendPDFGeneral($chatId);
log_timing("handleUpdate: /pdf command end");
} else {
// Buscar por nombre - verificar si existe el usuario
log_timing("handleUpdate: searching by name '{$text}'");
$config = require __DIR__ . '/../config/config.php';
try {
log_timing("handleUpdate: DB connection start");
$pdo = new PDO(
"mysql:host={$config['db']['host']};port={$config['db']['port']};dbname={$config['db']['database']};charset=utf8mb4",
$config['db']['username'],
$config['db']['password'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
log_timing("handleUpdate: DB connection end");
} catch (Exception $e) {
log_timing("handleUpdate: DB connection FAILED");
$this->bot->sendMessage($chatId, "Error de conexion.");
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%"]);
$user = $stmt->fetch();
log_timing("handleUpdate: DB query end");
if ($user) {
$this->bot->sendMessage($chatId, "Generando PDF de turnos...");
$this->bot->sendPDF($chatId, $user['id']);
log_timing("handleUpdate: user found, sending text turnos");
$this->bot->sendMessage($chatId, $this->bot->getTurnosAyudante($user['nombre']));
} else {
log_timing("handleUpdate: user not found, getting plain text turnos");
$this->bot->sendMessage($chatId, $this->bot->getTurnosAyudante($text));
log_timing("handleUpdate: plain text turnos sent");
}
}
} catch (Exception $e) {
error_log("Error en handleUpdate: " . $e->getMessage());
log_timing("handleUpdate: EXCEPTION: " . $e->getMessage());
if (isset($update['message']['chat']['id'])) {
$this->bot->sendMessage($update['message']['chat']['id'], "Error: " . $e->getMessage());
}
}
log_timing("handleUpdate: end");
}
private function handleCallback($callback) {
log_timing("handleCallback: start");
try {
$callbackId = $callback['id'];
$data = $callback['data'];
$message = $callback['message'];
$chatId = $message['chat']['id'];
$messageId = $message['message_id'];
log_timing("handleCallback: processing data '{$data}'");
switch ($data) {
case 'ver_turnos':
@@ -107,6 +163,7 @@ class TurnoBot {
case 'mi_pdf':
$this->bot->answerCallback($callbackId, 'Generando PDF...');
$this->bot->deleteMessage($chatId, $messageId);
$this->bot->sendPDFGeneral($chatId);
break;
@@ -124,34 +181,44 @@ class TurnoBot {
default:
$this->bot->answerCallback($callbackId, 'Opcion no reconocida');
}
log_timing("handleCallback: data '{$data}' processed");
} catch (Exception $e) {
error_log("Error en handleCallback: " . $e->getMessage());
log_timing("handleCallback: EXCEPTION: " . $e->getMessage());
}
log_timing("handleCallback: end");
}
private function sendMenu($chatId) {
log_timing("sendMenu: start");
$mensaje = "<b>BOT DE TURNOS - CONTENEDOR IBIZA</b>\n\n";
$mensaje .= "Selecciona una opcion del menu:\n\n";
$mensaje .= "Ver Turnos - Tabla completa de asignaciones\n";
$mensaje .= "Semana Actual - Quien tiene turno esta semana\n";
$mensaje .= "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 .= "Mi Turno - Ver tu proximo turno";
$this->bot->sendKeyboard($chatId, $mensaje);
log_timing("sendMenu: end");
}
}
// Recibir actualización
$update = json_decode(file_get_contents('php://input'), true);
log_timing("Webhook invoked");
// 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) {
log_timing("Update received, initializing bot");
$bot = new TurnoBot();
$bot->handleUpdate($update);
log_timing("Script finished");
} else {
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';
$auth = new Auth();
$auth->requireAdmin();
$auth->requireCoordinador();
$userModel = new User();
$horariosModel = new DiasHorarios();
@@ -19,6 +19,16 @@ $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 (isset($_GET['success']) && $_GET['success'] === 'automatica') {
$message = 'Turnos generados automáticamente correctamente.';
$messageType = 'success';
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!CSRF::isValidRequest()) {
$message = 'Error de validación del formulario';
@@ -26,519 +36,247 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} else {
$action = $_POST['action'] ?? '';
if ($action === 'asignar') {
if ($action === 'asignar_actual') {
$userId = $_POST['user_id'] ?? 0;
$semana = $_POST['semana'] ?? '';
if ($userId && $semana) {
$asignacionModel->asignar($userId, $semana);
$message = 'Turno asignado correctamente';
// Encontrar el domingo actual
$hoy = new DateTime();
$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';
header("Location: " . $_SERVER['PHP_SELF'] . "?success=actual");
exit;
}
} 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);
} elseif ($action === 'rotar_automatica') {
$resultado = $asignacionModel->asignarSemanasFuturasAutomaticas(12);
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';
}
header("Location: " . $_SERVER['PHP_SELF'] . "?success=automatica");
exit;
} else {
$message = 'Debes seleccionar al menos un ayudante y una semana';
$message = 'No se pudieron generar turnos: ' . implode(', ', $resultado['errors']);
$messageType = 'danger';
}
}
}
}
$ayudantes = $userModel->getAyudantesActivos();
$horarios = $horariosModel->getActivos();
// Encontrar el domingo actual
// Obtener datos principales
$hoy = new DateTime();
$diaSemana = (int)$hoy->format('w'); // 0 = domingo, 6 = sábado
$diaSemana = (int)$hoy->format('w');
$domingoActual = clone $hoy;
$domingoActual->modify('-' . $diaSemana . ' days'); // Restar días para llegar al domingo
$domingoActual->modify('-' . $diaSemana . ' days');
$currentWeekStart = $domingoActual->format('Y-m-d');
$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)
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++) {
// Obtener próximos turnos (siguientes 3 semanas)
$proximosTurnos = [];
for ($i = 1; $i <= 3; $i++) {
$semanaDomingo = clone $domingoActual;
$semanaDomingo->modify("+{$i} weeks");
$semanaInicio = $semanaDomingo->format('Y-m-d');
$asignacion = $asignacionModel->getAsignacionPorSemana($semanaInicio);
$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'))
$proximosTurnos[] = [
'semana' => $semanaInicio,
'fin' => date('Y-m-d', strtotime('+5 days', strtotime($semanaInicio))),
'asignacion' => $asignacion
];
}
$currentPage = 'asignaciones';
$pageTitle = 'Asignación de Turnos';
$pageTitle = 'Gestió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>
<title>Turnos - 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">
<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>
<body>
<?php include BASE_PATH . '/public/partials/navbar.php'; ?>
<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): ?>
<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; ?>
<!-- TURNO ACTUAL -->
<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 class="col-12">
<div class="card turno-actual shadow-sm">
<div class="card-header bg-success text-white">
<h5 class="mb-0">📍 Turno Actual</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)
<div class="row align-items-center">
<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>
<?php if ($asignacionActual): ?>
<div class="alert alert-success">
<strong>Asignado a:</strong> <?= htmlspecialchars($asignacionActual['nombre']) ?>
<div class="alert alert-success mb-3">
<h6 class="mb-1">✅ Asignado a:</h6>
<strong class="fs-5"><?= htmlspecialchars($asignacionActual['nombre']) ?></strong>
</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 class="alert alert-warning mb-3">
<h6 class="mb-1">⚠️ Sin asignar</h6>
<span class="fs-5">No hay ayudante asignado esta semana</span>
</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">
<div class="col-md-6 text-center">
<form method="POST" class="d-flex justify-content-center 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>
<input type="hidden" name="action" value="asignar_actual">
<select class="form-select" name="user_id" style="max-width: 200px;" required>
<option value="">Seleccionar...</option>
<?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']) ?>
</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>
<button type="submit" class="btn btn-success btn-accion">
💾 Asignar
</button>
</form>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Sección de Asignación Masiva -->
<div class="card shadow-sm mt-4">
<div class="card-header bg-success text-white">
<h5 class="mb-0">Asignación Masiva</h5>
<!-- ACCIONES RÁPIDAS -->
<div class="row mb-4">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">⚡ Acciones Rápidas</h5>
</div>
<div class="card-body">
<form method="POST" id="asignacionMasivaForm">
<div class="card-body text-center">
<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() ?>
<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
<input type="hidden" name="action" value="rotar_automatica">
<button type="submit" class="btn btn-primary btn-lg btn-accion">
🚀 Generar Automático
</button>
<button type="button" class="btn btn-outline-secondary" onclick="selectAllUsers()">
<i class="fas fa-check-square"></i> Seleccionar todos
</button>
<button type="button" class="btn btn-outline-secondary" onclick="deselectAllUsers()">
<i class="fas fa-square"></i> Deseleccionar todos
</button>
</div>
</form>
</div>
</div>
<!-- Sección de Rotación Automática -->
<div class="card shadow-sm mt-4">
<div class="card-header bg-warning text-dark">
<h5 class="mb-0">Rotación Automática</h5>
<div class="col-md-6 mb-3">
<div class="p-3">
<h6 class="mb-3">👥 Gestionar Orden</h6>
<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 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 foreach ($proximosTurnos as $index => $turno): ?>
<div class="col-md-4 mb-3">
<div class="card turno-card h-100">
<div class="card-body text-center">
<h6 class="card-title">
📅 <?= date('d/m', strtotime($turno['semana'])) ?> -
<?= date('d/m', strtotime($turno['fin'])) ?>
</h6>
<?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; ?>
</div>
<?php if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'rotacion_automatica'): ?>
<?php
$resultado = $asignacionModel->asignarSemanasFuturasAutomaticas(12);
?>
<div class="alert alert-<?= !empty($resultado['errors']) ? 'warning' : 'success' ?>">
<strong>Resultado:</strong> Se asignaron <?= $resultado['success'] ?> semanas futuras
<?php if (!empty($resultado['errores'])): ?>
<br><small>Errores: <?= implode(', ', $resultado['errores']) ?></small>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<div class="col-md-4">
<form method="POST" class="h-100 d-flex flex-column justify-content-center">
<?= 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 class="text-center mt-3">
<a href="/admin/asignaciones_completo.php" class="btn btn-outline-info">
📊 Ver todos los turnos
</a>
</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>
<!-- 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>
// Inicializar sortable
$(function() {
$("#sortableList").sortable({
placeholder: "ui-state-highlight",
update: function(event, ui) {
actualizarNumeros();
}
}).disableSelection();
});
function actualizarNumeros() {
$("#sortableList li").each(function(index) {
$(this).find('.badge').text('☰ ' + (index + 1));
});
}
function invertirOrden() {
var items = $("#sortableList li").get().reverse();
$("#sortableList").append(items);
actualizarNumeros();
}
// Seleccionar/deseleccionar todos
document.getElementById('selectAll').addEventListener('change', function() {
const checkboxes = document.querySelectorAll('.user-checkbox');
checkboxes.forEach(checkbox => checkbox.checked = this.checked);
});
function selectAllUsers() {
document.querySelectorAll('.user-checkbox').forEach(checkbox => checkbox.checked = true);
document.getElementById('selectAll').checked = true;
}
function deselectAllUsers() {
document.querySelectorAll('.user-checkbox').forEach(checkbox => checkbox.checked = false);
document.getElementById('selectAll').checked = false;
}
// Validar que al menos un usuario esté seleccionado
document.getElementById('asignacionMasivaForm').addEventListener('submit', function(e) {
const selectedUsers = document.querySelectorAll('.user-checkbox:checked');
if (selectedUsers.length === 0) {
e.preventDefault();
alert('Debes seleccionar al menos un ayudante');
return false;
}
});
// Establecer fecha por defecto al domingo actual
document.addEventListener('DOMContentLoaded', function() {
const hoy = new Date();
const diaSemana = hoy.getDay(); // 0 = domingo, 6 = sábado
const diasParaDomingo = diaSemana === 0 ? 0 : (7 - diaSemana);
const domingoActual = new Date(hoy);
domingoActual.setDate(hoy.getDate() - diasParaDomingo);
const fechaInput = document.querySelector('input[name="semana_inicio"]');
if (fechaInput) {
fechaInput.value = domingoActual.toISOString().split('T')[0];
}
});
</script>
</body>
</html>

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();
$totalUsuarios = count($userModel->getAll());
$totalAyudantes = count($userModel->getAyudantesActivos());
$totalAyudantes = count($userModel->getUsuariosGestion());
$totalHorarios = count($horariosModel->getAll());
$asignacionActual = $asignacionModel->getAsignacionActual();
$ayudantes = $userModel->getAyudantesActivos();
$ayudantes = $asignacionModel->getAyudantesPorOrden();
$horarios = $horariosModel->getAll();
$asignaciones = $asignacionModel->getTodasAsignaciones();
@@ -89,6 +89,40 @@ foreach ($asignacionesLimit as $a) {
$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();
$pdf = new PDFGenerator();

View File

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

View File

@@ -9,7 +9,7 @@ require_once BASE_PATH . '/src/CSRF.php';
require_once BASE_PATH . '/src/Session.php';
$auth = new Auth();
$auth->requireAdmin();
$auth->requireCoordinador();
$userModel = new User();
$message = '';
@@ -51,13 +51,27 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$password = $_POST['password'] ?? '';
$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';
$messageType = 'danger';
} elseif ($userModel->usernameExists($username, $id)) {
}
elseif ($userModel->usernameExists($username, $id)) {
$message = 'El username ya está en uso';
$messageType = 'danger';
} else {
}
else {
$userModel->update($id, compact('nombre', 'email', 'username', 'password', 'rol'));
$message = 'Usuario actualizado exitosamente';
$messageType = 'success';
@@ -65,6 +79,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} elseif ($action === 'toggle') {
$id = $_POST['id'] ?? 0;
$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['activo']) {
$userModel->deactivate($id);
@@ -76,9 +101,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
}
}
}
}
$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';
$pageTitle = 'Gestión de Usuarios';
?>
@@ -126,8 +159,19 @@ $pageTitle = 'Gestión de Usuarios';
<td><?= htmlspecialchars($u['nombre']) ?></td>
<td><?= htmlspecialchars($u['email']) ?></td>
<td>
<span class="badge bg-<?= $u['rol'] === 'admin' ? 'danger' : 'primary' ?>">
<?= ucfirst($u['rol']) ?>
<?php
$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>
</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'] ?>')">
Editar
</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">
<?= CSRF::getTokenField() ?>
<input type="hidden" name="action" value="toggle">
@@ -194,7 +243,10 @@ $pageTitle = 'Gestión de Usuarios';
<label for="rol" class="form-label">Rol</label>
<select class="form-select" id="rol" name="rol">
<option value="ayudante">Ayudante</option>
<option value="coordinador">🎯 Coordinador</option>
<?php if ($auth->isAdmin()): ?>
<option value="admin">Administrador</option>
<?php endif; ?>
</select>
</div>
</div>

View File

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

View File

@@ -1,23 +1,25 @@
<?php
require_once __DIR__ . '/../src/Auth.php';
require_once __DIR__ . '/../src/User.php';
require_once __DIR__ . '/../src/DiasHorarios.php';
require_once __DIR__ . '/../src/Asignacion.php';
require_once __DIR__ . '/../src/Database.php';
if (!defined('BASE_PATH')) {
define('BASE_PATH', dirname(__DIR__));
}
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/Database.php';
$auth = new Auth();
$auth->requireAuth();
if ($auth->isAdmin()) {
header('Location: /admin/index.php');
exit;
}
// Permitir acceso a administradores para que puedan ver la vista de ayudante
$user = $auth->getCurrentUser();
$horariosModel = new DiasHorarios();
$asignacionModel = new Asignacion();
$db = Database::getInstance()->getConnection();
$horarios = $horariosModel->getActivos();
$asignacionActual = $asignacionModel->getAsignacionActual();
@@ -58,7 +60,7 @@ foreach ($semanasFuturas as $semana) {
}
$userModel = new User();
$ayudantes = $userModel->getAyudantesActivos();
$ayudantes = $asignacionModel->getAyudantesPorOrden();
$domingo = new DateTime();
$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">
</head>
<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">
<div class="container">
<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>
</div>
</nav>
<?php } ?>
<div class="container mt-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>
<?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">
📄 Exportar PDF
</a>
</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
$hoy = new DateTime();
@@ -109,6 +182,8 @@ $domingo->modify('-' . (int)$domingo->format('w') . ' days');
</div>
<?php elseif ($asignacionEstaSemana): ?>
<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>
<?php if (!empty($misAsignacionesFuturas)): ?>
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.
<?php endif; ?>
</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)): ?>
<div class="alert alert-info mb-4">
<div class="d-flex justify-content-between align-items-start">
<div>
<strong>Proximo turno:</strong><br>
Del <?= date('d/m/y', strtotime($misAsignacionesFuturas[0]['semana']['inicio'])) ?>
al <?= date('d/m/y', strtotime($misAsignacionesFuturas[0]['semana']['fin'])) ?>
</div>
<?php 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: ?>
<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.
</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; ?>
<div class="card shadow-sm">
@@ -151,6 +257,7 @@ $domingo->modify('-' . (int)$domingo->format('w') . ' days');
<thead class="table-light">
<tr>
<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): ?>
<th class="text-center"><?= $diasNombres[$dia] ?></th>
<?php endforeach; ?>
@@ -172,6 +279,21 @@ $domingo->modify('-' . (int)$domingo->format('w') . ' days');
<span class="badge bg-success ms-1">Tu turno</span>
<?php endif; ?>
</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
$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 3</th>
<th class="text-center">Fecha 4</th>
<?php if ($auth->isCoordinador()): ?>
<th class="text-center">Acciones</th>
<?php endif; ?>
</tr>
</thead>
<tbody>
@@ -261,6 +386,13 @@ $domingo->modify('-' . (int)$domingo->format('w') . ' days');
<?php endif; ?>
</td>
<?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>
<?php endforeach; ?>
</tbody>
@@ -268,6 +400,39 @@ $domingo->modify('-' . (int)$domingo->format('w') . ' days');
</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>
<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();
$userModel = new User();
$ayudantes = $userModel->getAyudantesActivos();
$ayudantes = $asignacionModel->getAyudantesPorOrden();
$semanasFuturas = [];
$hoy = new DateTime();
@@ -127,6 +127,44 @@ foreach ($semanasFuturas as $index => $semana) {
$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 .= PDFGenerator::getFooter();

View File

@@ -15,35 +15,70 @@ $dbName = getenv('DB_NAME') ?: 'No configurado';
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<?php if ($auth->isAdmin()): ?>
<li class="nav-item">
<a class="nav-link <?= $currentPage === 'dashboard' ? 'active' : '' ?>" href="/admin/index.php">Dashboard</a>
</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">
<a class="nav-link <?= $currentPage === 'usuarios' ? 'active' : '' ?>" href="/admin/usuarios.php">Usuarios</a>
</li>
<?php if ($auth->isAdmin()): ?>
<li class="nav-item">
<a class="nav-link <?= $currentPage === 'horarios' ? 'active' : '' ?>" href="/admin/horarios.php">Horarios</a>
</li>
<?php endif; ?>
<li class="nav-item">
<a class="nav-link <?= $currentPage === 'asignaciones' ? 'active' : '' ?>" href="/admin/asignaciones.php">Asignaciones</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $currentPage === 'vista-ayudante' ? 'active' : '' ?>" href="/ayudante.php">👥 Vista Ayudante</a>
</li>
<?php if ($auth->isAdmin()): ?>
<li class="nav-item">
<a class="nav-link <?= $currentPage === 'webhook' ? 'active' : '' ?>" href="/admin/webhook.php">🤖 Bot</a>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link <?= $currentPage === 'logs' ? 'active' : '' ?>" href="/admin/logs.php">Logs</a>
</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">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<?= htmlspecialchars($user['nombre'] ?? 'Usuario') ?>
</a>
<ul class="dropdown-menu">
<li><span class="dropdown-item-text 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 Name:</strong> <?= htmlspecialchars($dbName) ?></span></li>
<?php endif; ?>
<li><hr class="dropdown-divider"></li>
<?php if ($user['rol'] ?? '' === 'ayudante'): ?>
<li><a class="dropdown-item" href="/cambiar-password.php">Cambiar Contraseña</a></li>

View File

@@ -42,7 +42,7 @@ fi
echo ""
echo -e "${GREEN}Building image...${NC}"
docker build -t "$FULL_IMAGE" .
docker build --no-cache -t "$FULL_IMAGE" .
echo ""
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();
}
public function getDb() {
return $this->db;
}
public function getAsignacionActual() {
$hoy = new DateTime();
$diaSemana = (int)$hoy->format('w');
@@ -217,7 +221,7 @@ public function asignar($userId, $semanaInicio) {
SELECT u.*, ro.orden
FROM users u
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
");
return $stmt->fetchAll();

View File

@@ -10,6 +10,31 @@ class Auth {
public function __construct() {
$this->userModel = new User();
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) {
@@ -43,10 +68,19 @@ class Auth {
return Session::get('user_rol') === 'ayudante';
}
public function isCoordinador() {
return Session::get('user_rol') === 'coordinador';
}
public function hasRole($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() {
if (!$this->isLoggedIn()) {
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 = '/') {
$this->requireAuth($redirectUrl);
$userRole = Session::get('user_rol');

View File

@@ -132,8 +132,14 @@ class User {
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) {
return in_array($role, ['admin', 'ayudante']);
return in_array($role, ['admin', 'ayudante', 'coordinador']);
}
public function hasRole($userId, $role) {