<?php // Controladores/EvaluacionControlador.php require_once 'Modelos/EvaluacionModelo.php'; require_once 'Modelos/CursoModelo.php'; class EvaluacionControlador { private $modelo; private $db; public function __construct($db) { $this->db = $db; $this->modelo = new EvaluacionModelo($db); if (!esta_autenticado() || !in_array(usuario_actual()['rol'], ['admin', 'instructor'])) { header("Location: index.php"); exit; } } public function index() { // Lógica para CREAR si viene por POST if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'create_quiz') { $curso_id = $_POST['course_id']; $titulo = $_POST['title']; $intentos = $_POST['attempts_allowed']; if ($this->modelo->crearCabecera($curso_id, $titulo, $intentos)) { $_SESSION['mensaje'] = "Evaluación creada. Ahora puedes añadir preguntas."; } else { $_SESSION['error'] = "Error al crear la evaluación."; } header("Location: index.php?r=instructor/quizzes"); exit; } // Lógica para LISTAR $evaluaciones = $this->modelo->listarTodas(); $modeloCurso = new CursoModelo($this->db); $cursos = $modeloCurso->listarTodos(); require_once 'Vistas/plantilla/encabezado.php'; require_once 'Vistas/instructor/evaluaciones.php'; require_once 'Vistas/plantilla/pie.php'; } public function borrar() { $id = $_GET['id'] ?? null; if ($id && $this->modelo->eliminar($id)) { $_SESSION['mensaje'] = "Evaluación eliminada correctamente."; } header("Location: index.php?r=instructor/quizzes"); exit; } // El método de edición usualmente abre una vista aparte para gestionar preguntas public function editar() { $quiz_id = (int)($_GET['id'] ?? 0); // --- LÓGICA PARA GUARDAR PREGUNTA --- if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'add_question') { $qtext = trim($_POST['question_text'] ?? ''); $qtype = $_POST['question_type'] ?? 'mcq'; $points = (int)$_POST['points']; // Guardar la pregunta base $question_id = $this->modelo->agregarPregunta($quiz_id, $qtext, $qtype, $points); // Si es de opción múltiple, guardar las opciones if ($question_id && $qtype === 'mcq' && isset($_POST['options'])) { $correcta = (int)$_POST['correct_option']; foreach ($_POST['options'] as $index => $texto_opcion) { if (!empty(trim($texto_opcion))) { $es_correcta = ($index === $correcta) ? 1 : 0; $this->modelo->agregarOpcion($question_id, $texto_opcion, $es_correcta); } } } header("Location: index.php?r=instructor/quizzes/edit&id=" . $quiz_id); exit; } // --- LÓGICA PARA MOSTRAR LA VISTA --- $quiz = $this->modelo->obtenerPorId($quiz_id); $preguntas = $this->db->query("SELECT * FROM questions WHERE quiz_id = $quiz_id ORDER BY id ASC"); require_once 'Vistas/plantilla/encabezado.php'; require_once 'Vistas/instructor/editEvaluacion.php'; require_once 'Vistas/plantilla/pie.php'; } public function borrarPregunta($id, $quiz_id) { $this->modelo->eliminarPregunta($id); header("Location: index.php?r=instructor/quizzes/edit&id=" . $quiz_id); exit; } /** * Resultados con filtros avanzados y paginación */ public function verResultados() { $quiz_id = $_GET['id'] ?? null; $busqueda = $_GET['s'] ?? ''; // --- NUEVOS FILTROS --- $filtro_eval = $_GET['eval'] ?? ''; $fecha_desde = $_GET['fd'] ?? ''; $fecha_hasta = $_GET['fh'] ?? ''; $estado = $_GET['est'] ?? ''; // --- LÓGICA DE PAGINACIÓN --- $por_pagina = 20; $pagina_actual = isset($_GET['p']) ? (int)$_GET['p'] : 1; if ($pagina_actual < 1) $pagina_actual = 1; $inicio = ($pagina_actual - 1) * $por_pagina; // --- OBTENCIÓN DE DATOS FILTRADOS --- $total_registros = $this->modelo->contarResultados($busqueda, $quiz_id, $filtro_eval, $fecha_desde, $fecha_hasta, $estado); $total_paginas = ceil($total_registros / $por_pagina); $resultados = $this->modelo->obtenerResultadosPaginados($busqueda, $por_pagina, $inicio, $quiz_id, $filtro_eval, $fecha_desde, $fecha_hasta, $estado); // --- LISTA DE EVALUACIONES/CURSOS PARA EL FILTRO DROPDOWN --- $evaluaciones_lista = $this->modelo->obtenerEvaluacionesYCursos(); // --- CARGA DE VISTAS --- require_once 'Vistas/plantilla/encabezado.php'; require_once 'Vistas/instructor/resultados.php'; require_once 'Vistas/plantilla/pie.php'; } /** * Obtener detalle de respuestas de un intento (AJAX/JSON) */ public function verDetalleIntento() { header('Content-Type: application/json; charset=utf-8'); $attempt_id = (int)($_GET['attempt_id'] ?? 0); if (!$attempt_id) { echo json_encode(['error' => 'ID de intento no válido']); exit; } $respuestas = $this->modelo->obtenerRespuestasIntento($attempt_id); // Enriquecer cada respuesta con todas las opciones de la pregunta foreach ($respuestas as &$resp) { $resp['opciones'] = $this->modelo->obtenerOpcionesPregunta($resp['question_id']); } unset($resp); // Calcular resumen $total = count($respuestas); $correctas = count(array_filter($respuestas, fn($r) => $r['is_correct'] == 1)); $incorrectas = $total - $correctas; echo json_encode([ 'attempt_id' => $attempt_id, 'total_preguntas' => $total, 'correctas' => $correctas, 'incorrectas' => $incorrectas, 'respuestas' => $respuestas ], JSON_UNESCAPED_UNICODE); exit; } /** * Ver diploma de un estudiante específico (modo instructor/admin). * Recibe user_id y course_id por GET. */ public function verDiplomaInstructor() { $user_id = (int)($_GET['user_id'] ?? 0); $course_id = (int)($_GET['course_id'] ?? 0); if (!$user_id || !$course_id) { $_SESSION['error'] = 'Parámetros del diploma no válidos.'; header("Location: index.php?r=instructor/resultados"); exit; } // Obtener datos de completación del curso para este usuario $result = $this->modelo->obtenerCompletacionCursoDiploma($user_id, $course_id); if (!$result) { $_SESSION['error'] = 'No se encontró información del curso o del estudiante.'; header("Location: index.php?r=instructor/resultados"); exit; } // Verificar que el estudiante aprobó if ($result['quizzes_passed'] == 0) { $_SESSION['error'] = 'El estudiante aún no ha aprobado la evaluación de este curso.'; header("Location: index.php?r=instructor/resultados"); exit; } // Preparar datos para el diploma (igual que EvaluacionAsigControlador::diploma) $nombre_estudiante = !empty($result['name']) ? $result['name'] : 'Nombre del Estudiante'; $nombre_profesional = !empty($result['namePro']) ? $result['namePro'] : 'Nombre del Instructor'; $cargo_estudiante = !empty($result['cargo']) ? $result['cargo'] : 'OPERARIO'; $cargo_profesional = !empty($result['cargoPro']) ? $result['cargoPro'] : 'INSTRUCTOR'; // Capitalizar nombres $nombre_estudiante = mb_convert_case($nombre_estudiante, MB_CASE_TITLE, "UTF-8"); $nombre_profesional = mb_convert_case($nombre_profesional, MB_CASE_TITLE, "UTF-8"); // Preparar fecha $meses = ['Enero','Febrero','Marzo','Abril','Mayo','Junio','Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre']; $fecha_aprobacion = $result['submitted_at'] ?? date('Y-m-d H:i:s'); $timestamp_aprobacion = strtotime(substr($fecha_aprobacion, 0, 10)); $dia = date('d', $timestamp_aprobacion); $mes = $meses[date('n', $timestamp_aprobacion) - 1]; $anio = date('Y', $timestamp_aprobacion); $fecha_text = $dia . ' del mes de ' . $mes . ' de ' . $anio; // Limpiar buffer while (ob_get_level()) { ob_end_clean(); } require_once 'Vistas/evaluaciones/diploma.php'; exit; } /** * Ver firma digital de un estudiante (modo instructor/admin). * Recibe attempt_id por GET. NO requiere course_assignments. */ public function verFirmaInstructor() { $attempt_id = (int)($_GET['attempt_id'] ?? 0); if (!$attempt_id) { $_SESSION['error'] = 'ID de intento no válido.'; header("Location: index.php?r=instructor/resultados"); exit; } // Consultar directamente el intento con firma (sin verificar course_assignments) $stmt = $this->db->prepare( "SELECT qa.*, u.name as estudiante, u.cedula, q.title as quiz_title, c.title as course_title FROM quiz_attempts qa JOIN users u ON u.id = qa.user_id JOIN quizzes q ON q.id = qa.quiz_id JOIN courses c ON c.id = q.course_id WHERE qa.id = ? AND qa.submitted_at IS NOT NULL" ); $stmt->bind_param('i', $attempt_id); $stmt->execute(); $attempt = $stmt->get_result()->fetch_assoc(); if (!$attempt) { $_SESSION['error'] = 'Intento no encontrado.'; header("Location: index.php?r=instructor/resultados"); exit; } require_once 'Vistas/plantilla/encabezado.php'; require_once 'Vistas/instructor/ver_firma.php'; require_once 'Vistas/plantilla/pie.php'; exit; } /** * Exportar a Excel con filtros avanzados Y detalle de preguntas. * Genera 2 hojas: "Resumen" y "Detalle Preguntas". */ public function excel() { $quiz_id = $_GET['id'] ?? null; $busqueda = $_GET['s'] ?? ''; $filtro_eval = $_GET['eval'] ?? ''; $fecha_desde = $_GET['fd'] ?? ''; $fecha_hasta = $_GET['fh'] ?? ''; $estado = $_GET['est'] ?? ''; // Traer todos los filtrados sin límite de página $resultados = $this->modelo->obtenerResultadosPaginados('', 5000, 0, $quiz_id, $filtro_eval, $fecha_desde, $fecha_hasta, $estado); header('Content-Type: application/vnd.ms-excel; charset=utf-8'); header('Content-Disposition: attachment; filename=Resultados_Evaluaciones_Detalle.xls'); header('Pragma: no-cache'); header('Expires: 0'); echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"; echo "<Workbook xmlns=\"urn:schemas-microsoft-com:office:spreadsheet\"\n"; echo " xmlns:x=\"urn:schemas-microsoft-com:office:excel\"\n"; echo " xmlns:ss=\"urn:schemas-microsoft-com:office:spreadsheet\"\n"; echo " xmlns:html=\"http://www.w3.org/TR/REC-html40\">\n"; // ========== HOJA 1: RESUMEN ========== echo " <Worksheet ss:Name=\"Resumen\">\n"; echo " <Table>\n"; echo " <Row>\n"; foreach (['Estudiante', 'Cedula', 'Evaluacion', 'Curso', 'Puntaje (%)', 'Fecha', 'Estado'] as $header) { echo " <Cell><Data ss:Type=\"String\">" . h($header) . "</Data></Cell>\n"; } echo " </Row>\n"; if ($resultados) { while ($r = $resultados->fetch_assoc()) { echo " <Row>\n"; echo " <Cell><Data ss:Type=\"String\">" . h($r['estudiante']) . "</Data></Cell>\n"; echo " <Cell><Data ss:Type=\"String\">" . h($r['cedula']) . "</Data></Cell>\n"; echo " <Cell><Data ss:Type=\"String\">" . h($r['evaluacion']) . "</Data></Cell>\n"; echo " <Cell><Data ss:Type=\"String\">" . h($r['curso']) . "</Data></Cell>\n"; echo " <Cell><Data ss:Type=\"Number\">" . h($r['score']) . "</Data></Cell>\n"; echo " <Cell><Data ss:Type=\"String\">" . h($r['completed_at']) . "</Data></Cell>\n"; $aprobado = $r['score'] >= 70 ? 'APROBADO' : 'REPROBADO'; echo " <Cell><Data ss:Type=\"String\">" . h($aprobado) . "</Data></Cell>\n"; echo " </Row>\n"; } } echo " </Table>\n"; echo " </Worksheet>\n"; // ========== HOJA 2: DETALLE DE PREGUNTAS ========== echo " <Worksheet ss:Name=\"Detalle Preguntas\">\n"; echo " <Table>\n"; echo " <Row>\n"; foreach (['Estudiante', 'Cedula', 'Evaluacion', 'Curso', 'N° Pregunta', 'Pregunta', 'Tipo', 'Respuesta Seleccionada', 'Respuesta Correcta', 'Resultado', 'Puntos Obtenidos'] as $header) { echo " <Cell><Data ss:Type=\"String\">" . h($header) . "</Data></Cell>\n"; } echo " </Row>\n"; // Re-consultar para recorrer de nuevo (ya se consumió el resultset) $resultados2 = $this->modelo->obtenerResultadosPaginados('', 5000, 0, $quiz_id, $filtro_eval, $fecha_desde, $fecha_hasta, $estado); if ($resultados2) { while ($r = $resultados2->fetch_assoc()) { $attempt_id = (int)$r['id']; $respuestas = $this->modelo->obtenerRespuestasIntento($attempt_id); if (empty($respuestas)) { // Si no hay respuestas, escribir una fila indicándolo echo " <Row>\n"; echo " <Cell><Data ss:Type=\"String\">" . h($r['estudiante']) . "</Data></Cell>\n"; echo " <Cell><Data ss:Type=\"String\">" . h($r['cedula']) . "</Data></Cell>\n"; echo " <Cell><Data ss:Type=\"String\">" . h($r['evaluacion']) . "</Data></Cell>\n"; echo " <Cell><Data ss:Type=\"String\">" . h($r['curso']) . "</Data></Cell>\n"; echo " <Cell><Data ss:Type=\"String\">-</Data></Cell>\n"; echo " <Cell><Data ss:Type=\"String\">Sin respuestas registradas</Data></Cell>\n"; echo " <Cell><Data ss:Type=\"String\">-</Data></Cell>\n"; echo " <Cell><Data ss:Type=\"String\">-</Data></Cell>\n"; echo " <Cell><Data ss:Type=\"String\">-</Data></Cell>\n"; echo " <Cell><Data ss:Type=\"String\">-</Data></Cell>\n"; echo " <Cell><Data ss:Type=\"Number\">0</Data></Cell>\n"; echo " </Row>\n"; continue; } $num_pregunta = 1; foreach ($respuestas as $resp) { $esCorrecta = $resp['is_correct'] == 1; $resultadoTexto = $esCorrecta ? 'Correcta' : 'Incorrecta'; $puntosOtorgados = $esCorrecta ? (float)$resp['awarded_points'] : 0; echo " <Row>\n"; echo " <Cell><Data ss:Type=\"String\">" . h($r['estudiante']) . "</Data></Cell>\n"; echo " <Cell><Data ss:Type=\"String\">" . h($r['cedula']) . "</Data></Cell>\n"; echo " <Cell><Data ss:Type=\"String\">" . h($r['evaluacion']) . "</Data></Cell>\n"; echo " <Cell><Data ss:Type=\"String\">" . h($r['curso']) . "</Data></Cell>\n"; echo " <Cell><Data ss:Type=\"Number\">" . $num_pregunta . "</Data></Cell>\n"; echo " <Cell><Data ss:Type=\"String\">" . h($resp['question_text']) . "</Data></Cell>\n"; echo " <Cell><Data ss:Type=\"String\">" . h($resp['question_type'] === 'mcq' ? 'Opcion Multiple' : 'Abierta') . "</Data></Cell>\n"; // Respuesta seleccionada if ($resp['question_type'] === 'mcq') { $sel = !empty($resp['selected_option_text']) ? $resp['selected_option_text'] : '(Sin seleccion)'; } else { $sel = !empty($resp['answer_text']) ? $resp['answer_text'] : '(Sin respuesta)'; } echo " <Cell><Data ss:Type=\"String\">" . h($sel) . "</Data></Cell>\n"; // Respuesta correcta $correcta_text = !empty($resp['correct_option_text']) ? $resp['correct_option_text'] : '-'; echo " <Cell><Data ss:Type=\"String\">" . h($correcta_text) . "</Data></Cell>\n"; echo " <Cell><Data ss:Type=\"String\">" . h($resultadoTexto) . "</Data></Cell>\n"; echo " <Cell><Data ss:Type=\"Number\">" . $puntosOtorgados . "</Data></Cell>\n"; echo " </Row>\n"; $num_pregunta++; } } } echo " </Table>\n"; echo " </Worksheet>\n"; echo "</Workbook>\n"; exit; } }