596 lines
19 KiB
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;
|
|
}
|
|
?>
|