Files
webval/enseignant/monitoring_ajax.php

280 lines
9.8 KiB
PHP

<?php
/**
* API MONITORING TEMPS RÉEL - VERSION FINALE CORRIGÉE V3
* Fix: Alignement logique vérification avec soumettre_evaluation.php
*
* Changements:
* 1. Ligne 39: Ajout type_question dans SELECT (fix précédent) ✅
* 2. Ligne 183-271: Fonction verifierReponseCorrecte() alignée sur correction réelle ✅
* 3. Ligne 11-20: Authentification corrigée (session directe) ✅
*/
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../config/session.php';
if (session_status() === PHP_SESSION_NONE) {
SessionManager::startSession();
}
header('Content-Type: application/json');
// Vérifier authentification enseignant
if (!isset($_SESSION['user_id']) || $_SESSION['type_libelle'] !== 'enseignant') {
echo json_encode(['success' => false, 'error' => 'Accès refusé']);
exit;
}
$id_evaluation = isset($_GET['id_evaluation']) ? (int)$_GET['id_evaluation'] : 0;
if ($id_evaluation <= 0) {
echo json_encode(['success' => false, 'error' => 'ID évaluation invalide']);
exit;
}
try {
$db = Database::getInstance()->getConnection();
// Récupérer questions (AVEC type_question - FIX 1)
$stmt = $db->prepare("
SELECT id_question, ordre, type_question, enonce, reponse_correcte_json, options_json
FROM questions
WHERE id_evaluation = ?
ORDER BY ordre ASC
");
$stmt->execute([$id_evaluation]);
$questions = $stmt->fetchAll(PDO::FETCH_ASSOC);
$nb_questions = count($questions);
// Récupérer tentatives avec statut connexion
$stmt = $db->prepare("
SELECT
te.id_tentative,
te.id_eleve,
te.statut,
te.reponses_json,
te.date_debut,
te.date_fin,
te.derniere_sauvegarde,
te.note,
u.nom,
u.prenom,
u.login,
c.nom_classe,
TIMESTAMPDIFF(SECOND, te.date_debut, NOW()) as secondes_ecoulees,
TIMESTAMPDIFF(SECOND,
COALESCE(te.derniere_sauvegarde, te.date_debut),
NOW()
) as secondes_inactif,
CASE
WHEN te.statut = 'terminee' THEN 'termine'
WHEN TIMESTAMPDIFF(SECOND, COALESCE(te.derniere_sauvegarde, te.date_debut), NOW()) > 300 THEN 'hors_ligne'
WHEN TIMESTAMPDIFF(SECOND, COALESCE(te.derniere_sauvegarde, te.date_debut), NOW()) > 180 THEN 'inactif'
WHEN TIMESTAMPDIFF(SECOND, COALESCE(te.derniere_sauvegarde, te.date_debut), NOW()) > 60 THEN 'en_cours'
ELSE 'en_ligne'
END as statut_connexion
FROM tentatives_eleves te
JOIN utilisateurs u ON te.id_eleve = u.id_utilisateur
LEFT JOIN classes c ON u.id_classe = c.id_classe
WHERE te.id_evaluation = ?
ORDER BY
FIELD(statut_connexion, 'en_ligne', 'en_cours', 'inactif', 'hors_ligne', 'termine'),
te.derniere_sauvegarde DESC,
u.nom, u.prenom
");
$stmt->execute([$id_evaluation]);
$tentatives = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Calculer statistiques
$stats = [
'total' => count($tentatives),
'en_ligne' => 0,
'en_cours' => 0,
'inactif' => 0,
'hors_ligne' => 0,
'termine' => 0
];
foreach ($tentatives as $t) {
$stats[$t['statut_connexion']]++;
}
// Formater données pour frontend
$tentatives_formatted = array_map(function($t) use ($questions, $nb_questions) {
$reponses = json_decode($t['reponses_json'] ?? '{}', true) ?: [];
$nb_reponses = count(array_filter($reponses, function($r) {
return $r !== null && $r !== '';
}));
$pourcentage = $nb_questions > 0 ? round(($nb_reponses / $nb_questions) * 100) : 0;
// Calculer temps écoulé
$heures = floor($t['secondes_ecoulees'] / 3600);
$minutes = floor(($t['secondes_ecoulees'] % 3600) / 60);
$temps_ecoule = sprintf("%d:%02d", $heures, $minutes);
// Dernière activité
$derniere_activite = '';
if ($t['statut_connexion'] === 'termine') {
$derniere_activite = '✅ Terminé';
} else if ($t['secondes_inactif'] < 60) {
$derniere_activite = 'À l\'instant';
} else if ($t['secondes_inactif'] < 3600) {
$min_inactif = floor($t['secondes_inactif'] / 60);
$derniere_activite = "Il y a {$min_inactif} min";
} else {
$h_inactif = floor($t['secondes_inactif'] / 3600);
$derniere_activite = "Il y a {$h_inactif}h";
}
// Détails réponses pour chaque question
$reponses_details = [];
foreach ($questions as $q) {
$reponse_eleve = $reponses[$q['id_question']] ?? null;
if ($reponse_eleve === null || $reponse_eleve === '') {
$reponses_details[] = [
'icone' => '⬜',
'tooltip' => 'Pas encore répondu'
];
} else {
// Vérifier si réponse correcte (FIX 2 - utiliser nouvelle fonction alignée)
$est_correcte = verifierReponseCorrecte($q, $reponse_eleve);
$reponses_details[] = [
'icone' => $est_correcte ? '✅' : '❌',
'tooltip' => $est_correcte ? 'Réponse correcte' : 'Réponse incorrecte'
];
}
}
return [
'id_tentative' => $t['id_tentative'],
'nom' => $t['nom'],
'prenom' => $t['prenom'],
'classe' => $t['nom_classe'] ?? 'Sans classe',
'statut_connexion' => $t['statut_connexion'],
'nb_reponses' => $nb_reponses,
'pourcentage_progression' => $pourcentage,
'temps_ecoule' => $temps_ecoule,
'derniere_activite' => $derniere_activite,
'reponses_details' => $reponses_details
];
}, $tentatives);
// Réponse JSON
echo json_encode([
'success' => true,
'stats' => $stats,
'tentatives' => $tentatives_formatted,
'timestamp' => date('Y-m-d H:i:s')
]);
} catch (Exception $e) {
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
/**
* FIX 2: Fonction vérification ALIGNÉE sur soumettre_evaluation.php (ligne 100-188)
*
* Changements principaux:
* - SELECT: Utilise reponse_correcte_json['id'] au lieu de options['est_correcte']
* - Accepte ID ou texte comme correction réelle
* - QCM: Utilise 'reponses' au lieu de 'options'
* - CHECKBOX: Compare textes au lieu de IDs
*/
function verifierReponseCorrecte($question, $reponse_eleve) {
if (empty($reponse_eleve) && $reponse_eleve !== '0' && $reponse_eleve !== 0) {
return false;
}
$type = $question['type_question'];
$options = json_decode($question['options_json'] ?? '{}', true);
$reponse_correcte_data = json_decode($question['reponse_correcte_json'] ?? '{}', true);
// Type NUMBER
if ($type === 'number') {
$reponse_correcte = $reponse_correcte_data['reponse'] ?? null;
if ($reponse_correcte === null) {
return false;
}
return abs((float)$reponse_eleve - (float)$reponse_correcte) < 0.01;
}
// Type TEXT
if ($type === 'text') {
$reponse_correcte = $reponse_correcte_data['reponse'] ?? null;
if ($reponse_correcte === null) {
return false;
}
return strtolower(trim($reponse_eleve)) === strtolower(trim($reponse_correcte));
}
// Type SELECT (FIX MAJEUR)
if ($type === 'select') {
// Réponse correcte dans reponse_correcte_json: {"id": "opt2"}
$id_reponse_correcte = $reponse_correcte_data['id'] ?? null;
if (!$id_reponse_correcte || !isset($options['options'])) {
return false;
}
// Trouver le texte correspondant à l'ID correct
$texte_correct = null;
foreach ($options['options'] as $opt) {
if (($opt['id'] ?? '') === $id_reponse_correcte) {
$texte_correct = $opt['texte'] ?? null;
break;
}
}
// Vérifier si réponse élève correspond (ID OU texte - comme correction réelle)
return ($reponse_eleve === $id_reponse_correcte || $reponse_eleve === $texte_correct);
}
// Type QCM (utilise 'reponses' pas 'options')
if ($type === 'qcm') {
if (!isset($options['reponses'])) {
return false;
}
// Trouver la réponse correcte dans le tableau reponses
$reponse_correcte = null;
foreach ($options['reponses'] as $rep) {
if (isset($rep['est_correcte']) && $rep['est_correcte'] === true) {
$reponse_correcte = $rep['texte'];
break;
}
}
return $reponse_eleve === $reponse_correcte;
}
// Type CHECKBOX (compare textes comme correction réelle)
if ($type === 'checkbox') {
if (!isset($options['reponses'])) {
return false;
}
// Trouver toutes les réponses correctes (TEXTES)
$reponses_correctes = [];
foreach ($options['reponses'] as $rep) {
if (isset($rep['est_correcte']) && $rep['est_correcte'] === true) {
$reponses_correctes[] = $rep['texte'];
}
}
// Normaliser réponses élève
$reponses_eleve_array = is_array($reponse_eleve) ? $reponse_eleve : [$reponse_eleve];
// Comparer tableaux triés
sort($reponses_correctes);
sort($reponses_eleve_array);
return $reponses_correctes === $reponses_eleve_array;
}
return false;
}
?>