Files
webval/module/resultats_ajax.php

596 lines
19 KiB
PHP

<?php
/**
* API RÉSULTATS ÉVALUATION - Backend JSON
* Retourne statistiques + tableau notes + données graphiques
*
* Fonctionnalités:
* - Dernière tentative par élève (gestion multi-tentatives)
* - Statistiques générales + par classe
* - Analyse questions (% réussite)
* - Données graphiques (histogramme par classe + courbe progression)
*/
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();
// ========================================================================
// 1. RÉCUPÉRER INFORMATIONS ÉVALUATION
// ========================================================================
$stmt = $db->prepare("
SELECT titre, duree_minutes, note_totale, actif
FROM evaluations
WHERE id_evaluation = ?
");
$stmt->execute([$id_evaluation]);
$evaluation = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$evaluation) {
echo json_encode(['success' => false, 'error' => 'Évaluation introuvable']);
exit;
}
// ========================================================================
// 2. RÉCUPÉRER QUESTIONS AVEC POINTS
// ========================================================================
$stmt = $db->prepare("
SELECT
id_question,
ordre,
type_question,
enonce,
points,
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);
// ========================================================================
// 3. RÉCUPÉRER DERNIÈRES TENTATIVES PAR ÉLÈVE
// ========================================================================
$stmt = $db->prepare("
SELECT
te.id_tentative,
te.id_eleve,
te.note,
te.note_sur,
te.pourcentage,
te.temps_passe,
te.statut,
te.date_fin,
te.reponses_json,
u.nom,
u.prenom,
u.login,
c.nom_classe,
c.id_classe
FROM (
SELECT id_eleve, MAX(id_tentative) as max_id
FROM tentatives_eleves
WHERE id_evaluation = ?
GROUP BY id_eleve
) dernieres
JOIN tentatives_eleves te ON te.id_tentative = dernieres.max_id
JOIN utilisateurs u ON te.id_eleve = u.id_utilisateur
LEFT JOIN classes c ON u.id_classe = c.id_classe
ORDER BY te.note DESC, te.date_fin ASC
");
$stmt->execute([$id_evaluation]);
$tentatives = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($tentatives)) {
echo json_encode([
'success' => true,
'evaluation' => $evaluation,
'stats' => [
'total_eleves' => 0,
'termines' => 0,
'en_cours' => 0,
'taux_participation' => 0,
'moyenne' => 0,
'mediane' => 0,
'min' => 0,
'max' => 0,
'ecart_type' => 0,
'taux_reussite' => 0,
'temps_moyen' => '00:00:00'
],
'stats_classes' => [],
'stats_questions' => [],
'tentatives' => [],
'graphiques' => [
'histogramme_classes' => ['labels' => [], 'datasets' => []],
'progression_eleves' => ['labels' => [], 'datasets' => []]
]
]);
exit;
}
// ========================================================================
// 4. CALCUL STATISTIQUES GÉNÉRALES
// ========================================================================
$notes = [];
$temps_total = 0;
$nb_termines = 0;
$nb_en_cours = 0;
foreach ($tentatives as $t) {
if ($t['statut'] === 'terminee') {
$notes[] = (float)$t['note'];
$temps_total += (int)$t['temps_passe'];
$nb_termines++;
} else {
$nb_en_cours++;
}
}
$nb_total = count($tentatives);
// Moyenne
$moyenne = !empty($notes) ? array_sum($notes) / count($notes) : 0;
// Médiane
$mediane = 0;
if (!empty($notes)) {
sort($notes);
$count = count($notes);
$mediane = ($count % 2 === 0)
? ($notes[$count/2 - 1] + $notes[$count/2]) / 2
: $notes[floor($count/2)];
}
// Min / Max
$min = !empty($notes) ? min($notes) : 0;
$max = !empty($notes) ? max($notes) : 0;
// Écart-type
$ecart_type = 0;
if (count($notes) > 1) {
$variance = 0;
foreach ($notes as $note) {
$variance += pow($note - $moyenne, 2);
}
$ecart_type = sqrt($variance / count($notes));
}
// Taux réussite (≥10/20)
$reussites = count(array_filter($notes, fn($n) => $n >= 10));
$taux_reussite = !empty($notes) ? ($reussites / count($notes)) * 100 : 0;
// Temps moyen
$temps_moyen_sec = $nb_termines > 0 ? $temps_total / $nb_termines : 0;
$heures = floor($temps_moyen_sec / 3600);
$minutes = floor(($temps_moyen_sec % 3600) / 60);
$secondes = $temps_moyen_sec % 60;
$temps_moyen = sprintf("%02d:%02d:%02d", $heures, $minutes, $secondes);
$stats = [
'total_eleves' => $nb_total,
'termines' => $nb_termines,
'en_cours' => $nb_en_cours,
'taux_participation' => $nb_total > 0 ? round(($nb_termines / $nb_total) * 100, 1) : 0,
'moyenne' => round($moyenne, 2),
'mediane' => round($mediane, 2),
'min' => round($min, 2),
'max' => round($max, 2),
'ecart_type' => round($ecart_type, 2),
'taux_reussite' => round($taux_reussite, 1),
'temps_moyen' => $temps_moyen
];
// ========================================================================
// 5. STATISTIQUES PAR CLASSE
// ========================================================================
$par_classe = [];
foreach ($tentatives as $t) {
if ($t['statut'] !== 'terminee') continue;
$classe = $t['nom_classe'] ?? 'Sans classe';
if (!isset($par_classe[$classe])) {
$par_classe[$classe] = [
'nom' => $classe,
'id_classe' => $t['id_classe'],
'notes' => [],
'nb_eleves' => 0
];
}
$par_classe[$classe]['notes'][] = (float)$t['note'];
$par_classe[$classe]['nb_eleves']++;
}
// Calcul moyennes par classe
$stats_classes = [];
foreach ($par_classe as $classe => $data) {
$moyenne_classe = array_sum($data['notes']) / count($data['notes']);
$stats_classes[] = [
'nom' => $data['nom'],
'id_classe' => $data['id_classe'],
'nb_eleves' => $data['nb_eleves'],
'moyenne' => round($moyenne_classe, 2)
];
}
// Trier par moyenne décroissante
usort($stats_classes, fn($a, $b) => $b['moyenne'] <=> $a['moyenne']);
// ========================================================================
// 6. ANALYSE QUESTIONS (% réussite)
// ========================================================================
$stats_questions = [];
foreach ($questions as $q) {
$id_q = $q['id_question'];
$nb_reponses = 0;
$nb_correctes = 0;
foreach ($tentatives as $t) {
if ($t['statut'] !== 'terminee') continue;
$reponses = json_decode($t['reponses_json'], true) ?: [];
$reponse_eleve = $reponses[$id_q] ?? null;
if ($reponse_eleve === null || $reponse_eleve === '') continue;
$nb_reponses++;
// Vérifier si correcte (fonction du monitoring)
if (verifierReponseCorrecte($q, $reponse_eleve)) {
$nb_correctes++;
}
}
$taux_reussite_q = $nb_reponses > 0 ? ($nb_correctes / $nb_reponses) * 100 : 0;
$stats_questions[] = [
'ordre' => $q['ordre'],
'enonce' => substr($q['enonce'], 0, 80) . (strlen($q['enonce']) > 80 ? '...' : ''),
'type' => $q['type_question'],
'points' => (float)$q['points'],
'nb_reponses' => $nb_reponses,
'nb_correctes' => $nb_correctes,
'taux_reussite' => round($taux_reussite_q, 1)
];
}
// ========================================================================
// 7. DONNÉES GRAPHIQUE : HISTOGRAMME PAR CLASSE
// ========================================================================
$tranches = [
'0-5' => ['min' => 0, 'max' => 5],
'5-10' => ['min' => 5, 'max' => 10],
'10-15' => ['min' => 10, 'max' => 15],
'15-20' => ['min' => 15, 'max' => 20]
];
$couleurs_classes = [
'rgba(59, 130, 246, 0.6)', // Bleu
'rgba(34, 197, 94, 0.6)', // Vert
'rgba(249, 115, 22, 0.6)', // Orange
'rgba(168, 85, 247, 0.6)', // Violet
'rgba(236, 72, 153, 0.6)', // Rose
'rgba(20, 184, 166, 0.6)', // Teal
'rgba(251, 191, 36, 0.6)', // Jaune
'rgba(239, 68, 68, 0.6)' // Rouge
];
$datasets_histo = [];
$couleur_index = 0;
foreach ($par_classe as $classe => $data) {
$distribution = array_fill(0, count($tranches), 0);
foreach ($data['notes'] as $note) {
$tranche_index = 0;
foreach ($tranches as $t) {
if ($note >= $t['min'] && $note < $t['max']) {
$distribution[$tranche_index]++;
break;
}
// Cas note = 20 exactement
if ($note == 20 && $t['max'] == 20) {
$distribution[$tranche_index]++;
break;
}
$tranche_index++;
}
}
$datasets_histo[] = [
'label' => $classe,
'data' => $distribution,
'backgroundColor' => $couleurs_classes[$couleur_index % count($couleurs_classes)]
];
$couleur_index++;
}
$graphique_histo = [
'labels' => array_keys($tranches),
'datasets' => $datasets_histo
];
// ========================================================================
// 8. DONNÉES GRAPHIQUE : COURBE PROGRESSION PAR ÉLÈVE
// ========================================================================
// Top 5 élèves (notes les plus élevées terminées)
$top_eleves = array_filter($tentatives, fn($t) => $t['statut'] === 'terminee');
usort($top_eleves, fn($a, $b) => $b['note'] <=> $a['note']);
$top_eleves = array_slice($top_eleves, 0, 5);
$couleurs_eleves = [
'rgb(59, 130, 246)', // Bleu
'rgb(34, 197, 94)', // Vert
'rgb(249, 115, 22)', // Orange
'rgb(168, 85, 247)', // Violet
'rgb(236, 72, 153)' // Rose
];
$labels_questions = [];
foreach ($questions as $q) {
$labels_questions[] = 'Q' . $q['ordre'];
}
$datasets_progression = [];
$couleur_index = 0;
foreach ($top_eleves as $eleve) {
$reponses = json_decode($eleve['reponses_json'], true) ?: [];
$points_cumules = [];
$total = 0;
foreach ($questions as $q) {
$id_q = $q['id_question'];
$reponse_eleve = $reponses[$id_q] ?? null;
// Ajouter points si réponse correcte
if ($reponse_eleve !== null && $reponse_eleve !== '' && verifierReponseCorrecte($q, $reponse_eleve)) {
$total += (float)$q['points'];
}
$points_cumules[] = round($total, 2);
}
$datasets_progression[] = [
'label' => $eleve['nom'] . ' ' . substr($eleve['prenom'], 0, 1) . '. (' . round($eleve['note'], 1) . '/' . $eleve['note_sur'] . ')',
'data' => $points_cumules,
'borderColor' => $couleurs_eleves[$couleur_index % count($couleurs_eleves)],
'backgroundColor' => 'rgba(0,0,0,0)',
'tension' => 0.3,
'borderWidth' => 2
];
$couleur_index++;
}
// Ajouter courbe moyenne classe
$moyenne_cumules = [];
$points_moyens = array_fill(0, $nb_questions, 0);
$nb_eleves_valides = 0;
foreach ($tentatives as $t) {
if ($t['statut'] !== 'terminee') continue;
$reponses = json_decode($t['reponses_json'], true) ?: [];
$total = 0;
$q_index = 0;
foreach ($questions as $q) {
$id_q = $q['id_question'];
$reponse_eleve = $reponses[$id_q] ?? null;
if ($reponse_eleve !== null && $reponse_eleve !== '' && verifierReponseCorrecte($q, $reponse_eleve)) {
$total += (float)$q['points'];
}
$points_moyens[$q_index] += $total;
$q_index++;
}
$nb_eleves_valides++;
}
if ($nb_eleves_valides > 0) {
foreach ($points_moyens as $pm) {
$moyenne_cumules[] = round($pm / $nb_eleves_valides, 2);
}
$datasets_progression[] = [
'label' => 'Moyenne classe',
'data' => $moyenne_cumules,
'borderColor' => 'rgb(156, 163, 175)',
'backgroundColor' => 'rgba(0,0,0,0)',
'borderDash' => [5, 5],
'tension' => 0.3,
'borderWidth' => 2
];
}
$graphique_progression = [
'labels' => $labels_questions,
'datasets' => $datasets_progression
];
// ========================================================================
// 9. FORMATER TENTATIVES POUR TABLEAU
// ========================================================================
$tentatives_formatted = [];
$rang = 1;
foreach ($tentatives as $t) {
// Formater temps
$temps_sec = (int)$t['temps_passe'];
$h = floor($temps_sec / 3600);
$m = floor(($temps_sec % 3600) / 60);
$s = $temps_sec % 60;
$temps_fmt = sprintf("%02d:%02d:%02d", $h, $m, $s);
// Formater date
$date_fmt = $t['date_fin'] ? date('d/m/Y H:i', strtotime($t['date_fin'])) : '-';
$tentatives_formatted[] = [
'rang' => $t['statut'] === 'terminee' ? $rang : '-',
'nom' => $t['nom'],
'prenom' => $t['prenom'],
'classe' => $t['nom_classe'] ?? 'Sans classe',
'note' => round($t['note'], 2),
'note_sur' => round($t['note_sur'], 2),
'pourcentage' => round($t['pourcentage'], 1),
'temps' => $temps_fmt,
'statut' => $t['statut'],
'date_fin' => $date_fmt,
'id_tentative' => $t['id_tentative']
];
if ($t['statut'] === 'terminee') {
$rang++;
}
}
// ========================================================================
// 10. RÉPONSE JSON FINALE
// ========================================================================
echo json_encode([
'success' => true,
'evaluation' => [
'titre' => $evaluation['titre'],
'duree_minutes' => (int)$evaluation['duree_minutes'],
'note_totale' => (float)$evaluation['note_totale'],
'nb_questions' => $nb_questions
],
'stats' => $stats,
'stats_classes' => $stats_classes,
'stats_questions' => $stats_questions,
'tentatives' => $tentatives_formatted,
'graphiques' => [
'histogramme_classes' => $graphique_histo,
'progression_eleves' => $graphique_progression
],
'timestamp' => date('Y-m-d H:i:s')
], JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
/**
* Fonction vérification réponse correcte
* (Copie depuis monitoring_ajax.php V3 - logique alignée sur correction)
*/
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
if ($type === 'select') {
$id_reponse_correcte = $reponse_correcte_data['id'] ?? null;
if (!$id_reponse_correcte || !isset($options['options'])) return false;
$texte_correct = null;
foreach ($options['options'] as $opt) {
if (($opt['id'] ?? '') === $id_reponse_correcte) {
$texte_correct = $opt['texte'] ?? null;
break;
}
}
return ($reponse_eleve === $id_reponse_correcte || $reponse_eleve === $texte_correct);
}
// Type QCM
if ($type === 'qcm') {
if (!isset($options['reponses'])) return false;
$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
if ($type === 'checkbox') {
if (!isset($options['reponses'])) return false;
$reponses_correctes = [];
foreach ($options['reponses'] as $rep) {
if (isset($rep['est_correcte']) && $rep['est_correcte'] === true) {
$reponses_correctes[] = $rep['texte'];
}
}
$reponses_eleve_array = is_array($reponse_eleve) ? $reponse_eleve : [$reponse_eleve];
sort($reponses_correctes);
sort($reponses_eleve_array);
return $reponses_correctes === $reponses_eleve_array;
}
return false;
}
?>