Compare commits

..

11 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
22 changed files with 1415 additions and 546 deletions

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

0
Dockerfile Normal file → Executable file
View File

0
apache2.conf Normal file → Executable file
View File

View File

@@ -20,14 +20,16 @@ class TelegramBot {
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// --- MEJORAS ---
// 1. Forzar el uso de IPv4. Un problema común en entornos Docker
// es un timeout al intentar resolver AAAA (IPv6) antes de usar A (IPv4).
// 1. Forzar el uso de IPv4.
curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
// 2. Añadir timeouts para evitar que el script se cuelgue indefinidamente.
// 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));
@@ -96,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']
@@ -210,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();
@@ -269,7 +271,7 @@ class TelegramBot {
$db = \Database::getInstance()->getConnection();
$horarios = $horariosModel->getActivos();
$ayudantes = $userModel->getAyudantesActivos();
$ayudantes = $asignacionModel->getAyudantesPorOrden();
$semanasFuturas = [];
$hoy = new DateTime();
@@ -398,7 +400,7 @@ class TelegramBot {
$db = \Database::getInstance()->getConnection();
$horarios = $horariosModel->getActivos();
$ayudantes = $userModel->getAyudantesActivos();
$ayudantes = $asignacionModel->getAyudantesPorOrden();
$semanasFuturas = [];
$hoy = new DateTime();

View File

@@ -116,16 +116,14 @@ class TurnoBot {
}
log_timing("handleUpdate: DB query start");
$stmt = $pdo->prepare("SELECT * FROM users WHERE (nombre LIKE ? OR username LIKE ?) AND rol = 'ayudante' AND activo = 1 LIMIT 1");
$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) {
log_timing("handleUpdate: user found, sending PDF");
$this->bot->sendMessage($chatId, "Generando PDF de turnos...");
$this->bot->sendPDF($chatId, $user['id']);
log_timing("handleUpdate: PDF sent");
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));
@@ -165,6 +163,7 @@ class TurnoBot {
case 'mi_pdf':
$this->bot->answerCallback($callbackId, 'Generando PDF...');
$this->bot->deleteMessage($chatId, $messageId);
$this->bot->sendPDFGeneral($chatId);
break;
@@ -196,7 +195,7 @@ class TurnoBot {
$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";

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

631
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();
@@ -24,6 +24,11 @@ if (isset($_GET['success']) && $_GET['success'] === 'orden_actualizado') {
$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';
@@ -31,527 +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';
$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';
}
// 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 === 'rotacion_automatica') {
} elseif ($action === 'rotar_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';
}
header("Location: " . $_SERVER['PHP_SELF'] . "?success=automatica");
exit;
} else {
$message = 'No se pudieron generar asignaciones: ' . implode(', ', $resultado['errors']);
$messageType = 'danger';
}
} 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';
$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)
</p>
<?php if ($asignacionActual): ?>
<div class="alert alert-success">
<strong>Asignado a:</strong> <?= htmlspecialchars($asignacionActual['nombre']) ?>
<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 mb-3">
<h6 class="mb-1">✅ Asignado a:</h6>
<strong class="fs-5"><?= htmlspecialchars($asignacionActual['nombre']) ?></strong>
</div>
<?php else: ?>
<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>
<?php endif; ?>
</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>
<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_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'] ?>"><?= htmlspecialchars($a['nombre']) ?></option>
<option value="<?= $a['id'] ?>"
<?= $asignacionActual && $a['id'] == $asignacionActual['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($a['nombre']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-primary">Asignar</button>
</form>
<?php endif; ?>
<button type="submit" class="btn btn-success btn-accion">
💾 Asignar
</button>
</form>
</div>
</div>
</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>
</div>
<?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']) ?>
<!-- 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 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="rotar_automatica">
<button type="submit" class="btn btn-primary btn-lg btn-accion">
🚀 Generar Automático
</button>
</form>
</div>
</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>
<!-- Sección de Asignación Masiva -->
<div class="card shadow-sm mt-4">
<div class="card-header bg-success text-white">
<h5 class="mb-0">Asignación Masiva</h5>
</div>
<div class="card-body">
<form method="POST" id="asignacionMasivaForm">
<?= 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 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 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>
</div>
<!-- Sección de Rotación Automática -->
<div class="card shadow-sm mt-4">
<div class="card-header bg-warning text-dark">
<h5 class="mb-0">Rotación Automática</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-8">
<h6>Orden de Rotación Actual:</h6>
<div class="d-flex flex-wrap gap-2 mb-3">
<?php
$ayudantesOrdenados = $asignacionModel->getAyudantesPorOrden();
foreach ($ayudantesOrdenados as $index => $ayudante):
?>
<span class="badge bg-primary fs-6">
<?= ($index + 1) ?>. <?= htmlspecialchars($ayudante['nombre']) ?>
</span>
<!-- 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">
<?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>
</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 = $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>
</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 = '';
@@ -50,14 +50,28 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$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,20 +79,39 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} elseif ($action === 'toggle') {
$id = $_POST['id'] ?? 0;
$user = $userModel->getById($id);
if ($user) {
if ($user['activo']) {
$userModel->deactivate($id);
} else {
$userModel->activate($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);
} else {
$userModel->activate($id);
}
$message = 'Estado actualizado';
$messageType = 'success';
}
$message = 'Estado actualizado';
$messageType = 'success';
}
}
}
}
$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,14 +184,19 @@ $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">
<input type="hidden" name="id" value="<?= $u['id'] ?>">
<button type="submit" class="btn btn-sm btn-<?= $u['activo'] ? 'outline-warning' : 'outline-success' ?>">
<?= $u['activo'] ? 'Desactivar' : 'Activar' ?>
</button>
<?= CSRF::getTokenField() ?>
<input type="hidden" name="action" value="toggle">
<input type="hidden" name="id" value="<?= $u['id'] ?>">
<button type="submit" class="btn btn-sm btn-<?= $u['activo'] ? 'outline-warning' : 'outline-success' ?>">
<?= $u['activo'] ? 'Desactivar' : 'Activar' ?>
</button>
</form>
<?php endif; ?>
</td>
@@ -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="admin">Administrador</option>
<option value="coordinador">🎯 Coordinador</option>
<?php if ($auth->isAdmin()): ?>
<option value="admin">Administrador</option>
<?php endif; ?>
</select>
</div>
</div>

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,25 +74,96 @@ $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>
<nav class="navbar navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="/ayudante.php">Contenedor Ibiza</a>
<span class="navbar-text me-3">
Hola, <?= htmlspecialchars($user['nombre']) ?>
</span>
<a href="/cambiar-password.php" class="btn btn-outline-light btn-sm me-3">Cambiar Contraseña</a>
<a href="/logout.php" class="btn btn-outline-light btn-sm">Cerrar Sesión</a>
</div>
</nav>
<?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>
<span class="navbar-text me-3">
Hola, <?= htmlspecialchars($user['nombre']) ?>
</span>
<a href="/cambiar-password.php" class="btn btn-outline-light btn-sm me-3">Cambiar Contraseña</a>
<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">
<h2 class="mb-0">Mis Turnos</h2>
<a href="/export-pdf.php" target="_blank" class="btn btn-danger btn-sm">
📄 Exportar PDF
</a>
<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();
$diaSemana = (int)$hoy->format('w');
@@ -107,25 +180,58 @@ $domingo->modify('-' . (int)$domingo->format('w') . ' days');
Del <?= date('d/m/y', strtotime($asignacionEstaSemana['semana_inicio'])) ?>
al <?= date('d/m/y', strtotime($asignacionEstaSemana['semana_fin'])) ?>
</div>
<?php elseif ($asignacionEstaSemana): ?>
<div class="alert alert-secondary mb-4">
<strong>Turno esta semana:</strong> <?= htmlspecialchars($asignacionEstaSemana['nombre']) ?><br>
<?php if (!empty($misAsignacionesFuturas)): ?>
Tu proximo turno: <?= date('d/m/y', strtotime($misAsignacionesFuturas[0]['semana']['inicio'])) ?>
al <?= date('d/m/y', strtotime($misAsignacionesFuturas[0]['semana']['fin'])) ?>
<?php else: ?>
Tu proximo turno sera en las proximas semanas.
<?php endif; ?>
</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'])) ?>
al <?= date('d/m/y', strtotime($misAsignacionesFuturas[0]['semana']['fin'])) ?>
<?php else: ?>
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">
<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 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">
No hay turnos asignados para las proximas semanas.
<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; ?>
@@ -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">
<li class="nav-item">
<a class="nav-link <?= $currentPage === 'dashboard' ? 'active' : '' ?>" href="/admin/index.php">Dashboard</a>
</li>
<?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>
<li class="nav-item">
<a class="nav-link <?= $currentPage === 'horarios' ? 'active' : '' ?>" href="/admin/horarios.php">Horarios</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 === 'webhook' ? 'active' : '' ?>" href="/admin/webhook.php">🤖 Bot</a>
<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>
<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">
<li class="nav-item">
<a class="nav-link <?= $currentPage === 'logs' ? 'active' : '' ?>" href="/admin/logs.php">Logs</a>
</li>
<?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 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"><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

@@ -221,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) {