1013 lines
35 KiB
PHP
1013 lines
35 KiB
PHP
<?php
|
||
/**
|
||
* PASSER UNE ÉVALUATION - Interface élève
|
||
* VERSION CORRIGÉE - 27/10/2025
|
||
* CORRECTION: Utilise id_utilisateur au lieu de id
|
||
* CORRECTION: Compte uniquement les tentatives TERMINÉES
|
||
*/
|
||
|
||
// Chemins
|
||
define('APP_ROOT', __DIR__);
|
||
require_once 'config/config.php';
|
||
require_once 'config/database.php';
|
||
|
||
// Vérification connexion élève avec SessionManager
|
||
if (!isLoggedIn()) {
|
||
header('Location: login.php?error=access_denied');
|
||
exit();
|
||
}
|
||
|
||
$user = currentUser();
|
||
|
||
// ============================================================================
|
||
// GESTION MODE APERÇU ENSEIGNANT
|
||
// ============================================================================
|
||
// Paramètre apercu=1 : Enseignant peut prévisualiser évaluation
|
||
// Paramètre absent : Élève passe évaluation normalement
|
||
// ============================================================================
|
||
$apercu_mode = isset($_GET['apercu']) && $_GET['apercu'] == 1;
|
||
|
||
// Vérification type utilisateur conditionnelle
|
||
if (!$apercu_mode) {
|
||
// MODE NORMAL: Seuls les élèves peuvent passer (id_type 2 ou 3)
|
||
if (!isset($user['id_type']) || !in_array($user['id_type'], [2, 3])) {
|
||
header('Location: login.php?error=not_student');
|
||
exit();
|
||
}
|
||
} else {
|
||
// MODE APERÇU: Seuls les enseignants peuvent prévisualiser (id_type 1)
|
||
if (!isset($user['id_type']) || $user['id_type'] != 1) {
|
||
header('Location: login.php?error=not_teacher');
|
||
exit();
|
||
}
|
||
}
|
||
|
||
$db = Database::getInstance()->getConnection();
|
||
$id_evaluation = isset($_GET['id_evaluation']) ? (int)$_GET['id_evaluation'] : 0;
|
||
|
||
if ($id_evaluation <= 0) {
|
||
die("Évaluation invalide");
|
||
}
|
||
|
||
// === RÉCUPÉRATION ÉVALUATION ===
|
||
$stmt = $db->prepare("
|
||
SELECT e.*,
|
||
COUNT(DISTINCT q.id_question) as nb_questions
|
||
FROM evaluations e
|
||
LEFT JOIN questions q ON e.id_evaluation = q.id_evaluation
|
||
WHERE e.id_evaluation = ?
|
||
GROUP BY e.id_evaluation
|
||
");
|
||
$stmt->execute([$id_evaluation]);
|
||
$evaluation = $stmt->fetch(PDO::FETCH_ASSOC);
|
||
|
||
if (!$evaluation) {
|
||
die("Évaluation introuvable");
|
||
}
|
||
|
||
// === VÉRIFICATIONS ACCÈS ===
|
||
|
||
// 1. Évaluation active ?
|
||
if (!$apercu_mode && !$evaluation['actif']) {
|
||
die("Cette évaluation n'est pas active");
|
||
}
|
||
|
||
// 2. Classe a accès ? (CORRECTION: Gérer élèves libres id_classe NULL)
|
||
if (!$apercu_mode) {
|
||
if ($user['id_classe'] !== null) {
|
||
$stmt = $db->prepare("
|
||
SELECT * FROM acces_evaluations
|
||
WHERE id_evaluation = ?
|
||
AND id_classe = ?
|
||
AND actif = 1
|
||
");
|
||
$stmt->execute([$id_evaluation, $user['id_classe']]);
|
||
$acces = $stmt->fetch(PDO::FETCH_ASSOC);
|
||
|
||
if (!$acces) {
|
||
die("Votre classe n'a pas accès à cette évaluation");
|
||
}
|
||
} else {
|
||
// Élève libre : vérifier si classe soutien existe
|
||
$stmt = $db->prepare("
|
||
SELECT ae.* FROM acces_evaluations ae
|
||
INNER JOIN classes c ON ae.id_classe = c.id_classe
|
||
WHERE ae.id_evaluation = ?
|
||
AND c.type_classe = 'soutien'
|
||
AND ae.actif = 1
|
||
LIMIT 1
|
||
");
|
||
$stmt->execute([$id_evaluation]);
|
||
$acces = $stmt->fetch(PDO::FETCH_ASSOC);
|
||
|
||
if (!$acces) {
|
||
die("Cette évaluation n'est pas disponible pour le groupe soutien");
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3. Dates respectées ?
|
||
if (!$apercu_mode) {
|
||
$now = new DateTime();
|
||
$date_debut = new DateTime($acces['date_debut']);
|
||
$date_fin = new DateTime($acces['date_fin']);
|
||
|
||
if ($now < $date_debut) {
|
||
die("Cette évaluation n'est pas encore disponible. Début : " . $date_debut->format('d/m/Y à H:i'));
|
||
}
|
||
|
||
if ($now > $date_fin) {
|
||
die("Cette évaluation est terminée. Fin : " . $date_fin->format('d/m/Y à H:i'));
|
||
}
|
||
} else {
|
||
// Mode aperçu : pas de vérification dates, $acces fictif
|
||
$acces = ['date_debut' => date('Y-m-d H:i:s'), 'date_fin' => date('Y-m-d H:i:s')];
|
||
}
|
||
|
||
// 4. Vérifier nombre de tentatives TERMINÉES uniquement (CORRECTION PRINCIPALE)
|
||
if (!$apercu_mode) {
|
||
$stmt = $db->prepare("
|
||
SELECT COUNT(*) as nb_tentatives_terminees
|
||
FROM tentatives_eleves
|
||
WHERE id_evaluation = ?
|
||
AND id_eleve = ?
|
||
AND statut = 'terminee'
|
||
");
|
||
$stmt->execute([$id_evaluation, $user['id_utilisateur']]);
|
||
$nb_tentatives_terminees = $stmt->fetchColumn();
|
||
|
||
if ($nb_tentatives_terminees >= $evaluation['tentatives_max']) {
|
||
die("Vous avez déjà effectué le nombre maximum de tentatives pour cette évaluation (" . $evaluation['tentatives_max'] . " tentatives terminées).");
|
||
}
|
||
} else {
|
||
// Mode aperçu : pas de vérification tentatives
|
||
$nb_tentatives_terminees = 0;
|
||
}
|
||
|
||
// === GESTION TENTATIVE ===
|
||
|
||
if (!$apercu_mode) {
|
||
// MODE NORMAL: Gérer tentatives réelles
|
||
|
||
// Chercher tentative en cours (CORRECTION: id_utilisateur)
|
||
$stmt = $db->prepare("
|
||
SELECT * FROM tentatives_eleves
|
||
WHERE id_evaluation = ?
|
||
AND id_eleve = ?
|
||
AND statut = 'en_cours'
|
||
ORDER BY id_tentative DESC
|
||
LIMIT 1
|
||
");
|
||
$stmt->execute([$id_evaluation, $user['id_utilisateur']]);
|
||
$tentative = $stmt->fetch(PDO::FETCH_ASSOC);
|
||
|
||
// Si pas de tentative en cours, en créer une (CORRECTION: utilise nb_tentatives_terminees)
|
||
if (!$tentative) {
|
||
$stmt = $db->prepare("
|
||
INSERT INTO tentatives_eleves (
|
||
id_evaluation, id_eleve, numero_tentative, statut,
|
||
reponses_json, date_debut
|
||
) VALUES (?, ?, ?, 'en_cours', '{}', NOW())
|
||
");
|
||
$stmt->execute([$id_evaluation, $user['id_utilisateur'], $nb_tentatives_terminees + 1]);
|
||
$id_tentative = $db->lastInsertId();
|
||
|
||
// Recharger la tentative
|
||
$stmt = $db->prepare("SELECT * FROM tentatives_eleves WHERE id_tentative = ?");
|
||
$stmt->execute([$id_tentative]);
|
||
$tentative = $stmt->fetch(PDO::FETCH_ASSOC);
|
||
}
|
||
|
||
// Calculer temps restant
|
||
$date_debut_tentative = new DateTime($tentative['date_debut']);
|
||
$duree_secondes = $evaluation['duree_minutes'] * 60;
|
||
$temps_ecoule = $now->getTimestamp() - $date_debut_tentative->getTimestamp();
|
||
$temps_restant = max(0, $duree_secondes - $temps_ecoule);
|
||
|
||
// Si temps écoulé, soumettre automatiquement
|
||
if ($temps_restant <= 0) {
|
||
header('Location: soumettre_evaluation.php?id_evaluation=' . $id_evaluation . '&id_tentative=' . $tentative['id_tentative'] . '&auto=1');
|
||
exit();
|
||
}
|
||
|
||
} else {
|
||
// MODE APERÇU: Variables fictives pour affichage
|
||
$tentative = [
|
||
'id_tentative' => 0,
|
||
'date_debut' => date('Y-m-d H:i:s'),
|
||
'reponses_json' => '{}'
|
||
];
|
||
$temps_restant = $evaluation['duree_minutes'] * 60;
|
||
$reponses_sauvegardees = [];
|
||
}
|
||
|
||
// === RÉCUPÉRATION QUESTIONS AVEC RANDOMISATION ===
|
||
$stmt = $db->prepare("
|
||
SELECT * FROM questions
|
||
WHERE id_evaluation = ?
|
||
");
|
||
$stmt->execute([$id_evaluation]);
|
||
$questions = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||
|
||
// Randomisation par tentative (sauf mode aperçu enseignant)
|
||
if (!$apercu_mode) {
|
||
// Vérifier si ordre déjà sauvegardé pour cette tentative
|
||
if (!empty($tentative['ordre_questions_json'])) {
|
||
// Ordre existant : réappliquer le même ordre
|
||
$ordre_ids = json_decode($tentative['ordre_questions_json'], true);
|
||
$questions_ordonnees = [];
|
||
foreach ($ordre_ids as $id_q) {
|
||
foreach ($questions as $q) {
|
||
if ($q['id_question'] == $id_q) {
|
||
$questions_ordonnees[] = $q;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
$questions = $questions_ordonnees;
|
||
} else {
|
||
// Première fois : randomiser et sauvegarder l'ordre
|
||
shuffle($questions);
|
||
$ordre_ids = array_column($questions, 'id_question');
|
||
$ordre_json = json_encode($ordre_ids);
|
||
|
||
// Sauvegarder l'ordre dans la tentative
|
||
$stmt = $db->prepare("
|
||
UPDATE tentatives_eleves
|
||
SET ordre_questions_json = ?
|
||
WHERE id_tentative = ?
|
||
");
|
||
$stmt->execute([$ordre_json, $id_tentative]);
|
||
}
|
||
} else {
|
||
// Mode aperçu enseignant : ordre normal
|
||
usort($questions, fn($a, $b) => $a['ordre'] <=> $b['ordre']);
|
||
}
|
||
|
||
// Récupérer réponses sauvegardées
|
||
$reponses_sauvegardees = json_decode($tentative['reponses_json'] ?? '{}', true) ?: [];
|
||
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="fr">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||
<meta name="theme-color" content="#667eea">
|
||
<title><?= htmlspecialchars($evaluation['titre']) ?></title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
min-height: 100vh;
|
||
padding: 10px;
|
||
}
|
||
|
||
.container {
|
||
max-width: 900px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
/* Header fixe avec timer */
|
||
.header {
|
||
background: white;
|
||
padding: 15px 20px;
|
||
border-radius: 15px;
|
||
box-shadow: 0 5px 20px rgba(0,0,0,0.2);
|
||
margin-bottom: 20px;
|
||
position: sticky;
|
||
top: 10px;
|
||
z-index: 100;
|
||
}
|
||
|
||
.header-top {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.eval-title {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
color: #333;
|
||
}
|
||
|
||
.timer {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
padding: 8px 16px;
|
||
border-radius: 20px;
|
||
font-weight: 700;
|
||
font-size: 16px;
|
||
min-width: 80px;
|
||
text-align: center;
|
||
}
|
||
|
||
.timer.warning {
|
||
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
|
||
animation: pulse 1s infinite;
|
||
}
|
||
|
||
.timer.danger {
|
||
background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%);
|
||
animation: pulse 0.5s infinite;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% { transform: scale(1); }
|
||
50% { transform: scale(1.05); }
|
||
}
|
||
|
||
.progress-bar {
|
||
height: 6px;
|
||
background: #e0e0e0;
|
||
border-radius: 3px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.progress-fill {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
||
transition: width 0.3s;
|
||
}
|
||
|
||
/* Question card */
|
||
.question-card {
|
||
background: white;
|
||
padding: 25px;
|
||
border-radius: 15px;
|
||
box-shadow: 0 5px 20px rgba(0,0,0,0.2);
|
||
margin-bottom: 20px;
|
||
display: none;
|
||
}
|
||
|
||
.question-card.active {
|
||
display: block;
|
||
}
|
||
|
||
.question-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: start;
|
||
margin-bottom: 20px;
|
||
padding-bottom: 15px;
|
||
border-bottom: 2px solid #f0f0f0;
|
||
}
|
||
|
||
.question-number {
|
||
font-size: 14px;
|
||
color: #667eea;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.question-meta {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
}
|
||
|
||
.badge {
|
||
padding: 4px 12px;
|
||
border-radius: 12px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.badge-points {
|
||
background: #e3f2fd;
|
||
color: #1976d2;
|
||
}
|
||
|
||
.question-enonce {
|
||
font-size: 16px;
|
||
color: #333;
|
||
line-height: 1.6;
|
||
margin-bottom: 25px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* Réponses */
|
||
.reponse-option {
|
||
background: #f8f9fa;
|
||
border: 2px solid #e0e0e0;
|
||
border-radius: 12px;
|
||
padding: 15px;
|
||
margin-bottom: 12px;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.reponse-option:hover {
|
||
border-color: #667eea;
|
||
background: #f0f4ff;
|
||
}
|
||
|
||
.reponse-option input[type="radio"],
|
||
.reponse-option input[type="checkbox"] {
|
||
width: 20px;
|
||
height: 20px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.reponse-option.selected {
|
||
border-color: #667eea;
|
||
background: #e8eeff;
|
||
}
|
||
|
||
.reponse-label {
|
||
flex: 1;
|
||
cursor: pointer;
|
||
font-size: 15px;
|
||
}
|
||
|
||
/* Champs texte */
|
||
.text-input,
|
||
.select-input {
|
||
width: 100%;
|
||
padding: 15px;
|
||
border: 2px solid #e0e0e0;
|
||
border-radius: 12px;
|
||
font-size: 15px;
|
||
font-family: inherit;
|
||
transition: border-color 0.3s;
|
||
}
|
||
|
||
.text-input:focus,
|
||
.select-input:focus {
|
||
outline: none;
|
||
border-color: #667eea;
|
||
}
|
||
|
||
textarea.text-input {
|
||
min-height: 120px;
|
||
resize: vertical;
|
||
}
|
||
|
||
/* Navigation */
|
||
.navigation {
|
||
display: flex;
|
||
gap: 15px;
|
||
justify-content: space-between;
|
||
margin-top: 25px;
|
||
}
|
||
|
||
.btn {
|
||
padding: 15px 30px;
|
||
border: none;
|
||
border-radius: 12px;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
flex: 1;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: #f5f5f5;
|
||
color: #333;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
}
|
||
|
||
.btn-success {
|
||
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
|
||
color: white;
|
||
}
|
||
|
||
.btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.btn:not(:disabled):hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
||
}
|
||
|
||
/* Auto-save feedback */
|
||
.autosave-indicator {
|
||
position: fixed;
|
||
bottom: 20px;
|
||
right: 20px;
|
||
background: white;
|
||
padding: 12px 20px;
|
||
border-radius: 25px;
|
||
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #4CAF50;
|
||
opacity: 0;
|
||
transition: opacity 0.3s;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.autosave-indicator.saving {
|
||
color: #ff9800;
|
||
opacity: 1;
|
||
}
|
||
|
||
.autosave-indicator.saved {
|
||
color: #4CAF50;
|
||
opacity: 1;
|
||
}
|
||
|
||
/* Question map (mini navigation) */
|
||
.question-map {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
padding: 10px 0;
|
||
}
|
||
|
||
.question-dot {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 50%;
|
||
background: #e0e0e0;
|
||
border: 2px solid transparent;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #666;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.question-dot.answered {
|
||
background: #c8e6c9;
|
||
color: #2e7d32;
|
||
}
|
||
|
||
.question-dot.current {
|
||
border-color: #667eea;
|
||
background: #667eea;
|
||
color: white;
|
||
transform: scale(1.2);
|
||
}
|
||
|
||
/* Responsive */
|
||
@media (max-width: 768px) {
|
||
body {
|
||
padding: 5px;
|
||
}
|
||
|
||
.header {
|
||
padding: 12px 15px;
|
||
}
|
||
|
||
.eval-title {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.timer {
|
||
font-size: 14px;
|
||
padding: 6px 12px;
|
||
}
|
||
|
||
.question-card {
|
||
padding: 20px;
|
||
}
|
||
|
||
.btn {
|
||
padding: 12px 20px;
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
</style>
|
||
<!-- Chatbot IA Styles -->
|
||
<link rel="stylesheet" href="assets/css/chatbot.css">
|
||
</head>
|
||
<body>
|
||
<?php if ($apercu_mode): ?>
|
||
<div style="background: #ff9800; color: white; padding: 15px; text-align: center; font-weight: bold; font-size: 18px; position: sticky; top: 0; z-index: 9999; box-shadow: 0 2px 5px rgba(0,0,0,0.2);">
|
||
👁️ MODE APERÇU ENSEIGNANT - Les réponses ne sont pas enregistrées
|
||
</div>
|
||
<div style="background: #fff3cd; border: 2px solid #ffc107; padding: 15px; margin: 15px; border-radius: 5px;">
|
||
<strong>ℹ️ Information:</strong> Vous consultez cette évaluation en mode prévisualisation.
|
||
Ce mode permet de vérifier le contenu avant de le proposer aux élèves.
|
||
</div>
|
||
<?php endif; ?>
|
||
<div class="container">
|
||
<!-- Header avec timer -->
|
||
<div class="header">
|
||
<div class="header-top">
|
||
<div class="eval-title"><?= htmlspecialchars($evaluation['titre']) ?></div>
|
||
<div class="timer" id="timer"><?= gmdate('i:s', $temps_restant) ?></div>
|
||
</div>
|
||
<div class="progress-bar">
|
||
<div class="progress-fill" id="progress" style="width: 0%"></div>
|
||
</div>
|
||
<div class="question-map" id="questionMap"></div>
|
||
</div>
|
||
|
||
<!-- Questions -->
|
||
<form id="evaluationForm" method="post">
|
||
<input type="hidden" name="id_tentative" value="<?= $tentative['id_tentative'] ?>">
|
||
<input type="hidden" name="id_evaluation" value="<?= $id_evaluation ?>">
|
||
|
||
<?php foreach ($questions as $index => $q): ?>
|
||
<div class="question-card" data-question="<?= $index + 1 ?>" data-id="<?= $q['id_question'] ?>">
|
||
<div class="question-header">
|
||
<div class="question-number">Question <?= $index + 1 ?> / <?= count($questions) ?></div>
|
||
<div class="question-meta">
|
||
<span class="badge badge-points"><?= $q['points'] ?> pts</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="question-enonce"><?= nl2br(strip_tags($q['enonce'], '<img><a><br><strong><em><u><b><i><span>')) ?></div>
|
||
|
||
<?php
|
||
$options = json_decode($q['options_json'], true);
|
||
$reponse_eleve = $reponses_sauvegardees[$q['id_question']] ?? null;
|
||
|
||
switch ($q['type_question']):
|
||
case 'qcm':
|
||
$reponses = $options['reponses'] ?? [];
|
||
foreach ($reponses as $rep):
|
||
$checked = ($reponse_eleve == $rep['texte']) ? 'checked' : '';
|
||
?>
|
||
<div class="reponse-option <?= $checked ? 'selected' : '' ?>">
|
||
<input type="radio"
|
||
name="reponse_<?= $q['id_question'] ?>"
|
||
value="<?= htmlspecialchars($rep['texte']) ?>"
|
||
id="q<?= $q['id_question'] ?>_<?= $rep['id'] ?? uniqid() ?>"
|
||
<?= $checked ?>
|
||
onchange="markAnswered(<?= $index + 1 ?>)">
|
||
<label class="reponse-label" for="q<?= $q['id_question'] ?>_<?= $rep['id'] ?? uniqid() ?>">
|
||
<?= htmlspecialchars($rep['texte']) ?>
|
||
</label>
|
||
</div>
|
||
<?php
|
||
endforeach;
|
||
break;
|
||
|
||
case 'checkbox':
|
||
$reponses = $options['reponses'] ?? [];
|
||
$reponses_eleve = is_array($reponse_eleve) ? $reponse_eleve : [];
|
||
foreach ($reponses as $rep):
|
||
$checked = in_array($rep['texte'], $reponses_eleve) ? 'checked' : '';
|
||
?>
|
||
<div class="reponse-option <?= $checked ? 'selected' : '' ?>">
|
||
<input type="checkbox"
|
||
name="reponse_<?= $q['id_question'] ?>[]"
|
||
value="<?= htmlspecialchars($rep['texte']) ?>"
|
||
id="q<?= $q['id_question'] ?>_<?= $rep['id'] ?? uniqid() ?>"
|
||
<?= $checked ?>
|
||
onchange="markAnswered(<?= $index + 1 ?>)">
|
||
<label class="reponse-label" for="q<?= $q['id_question'] ?>_<?= $rep['id'] ?? uniqid() ?>">
|
||
<?= htmlspecialchars($rep['texte']) ?>
|
||
</label>
|
||
</div>
|
||
<?php
|
||
endforeach;
|
||
break;
|
||
|
||
case 'text':
|
||
?>
|
||
<textarea class="text-input"
|
||
name="reponse_<?= $q['id_question'] ?>"
|
||
placeholder="Votre réponse..."
|
||
oninput="markAnswered(<?= $index + 1 ?>)"><?= htmlspecialchars($reponse_eleve ?? '') ?></textarea>
|
||
<?php
|
||
break;
|
||
|
||
case 'number':
|
||
?>
|
||
<input type="text"
|
||
class="text-input number-math-input"
|
||
name="reponse_<?= $q['id_question'] ?>"
|
||
value="<?= htmlspecialchars($reponse_eleve ?? '') ?>"
|
||
placeholder="Formats : entier (7), fraction (2/3), décimal (4,5 ou 4.5)"
|
||
pattern="[0-9,./+\-\s]*"
|
||
title="Formats acceptés : entier, fraction, décimal"
|
||
oninput="markAnswered(<?= $index + 1 ?>)">
|
||
<?php
|
||
break;
|
||
|
||
case 'select':
|
||
$select_options = $options['options'] ?? [];
|
||
?>
|
||
<select class="select-input"
|
||
name="reponse_<?= $q['id_question'] ?>"
|
||
onchange="markAnswered(<?= $index + 1 ?>)">
|
||
<option value="">-- Sélectionner --</option>
|
||
<?php foreach ($select_options as $opt):
|
||
// Gérer le cas où $opt est un tableau ['id' => ..., 'texte' => ...] ou une chaîne
|
||
$opt_id = is_array($opt) ? ($opt['id'] ?? '') : $opt;
|
||
$opt_texte = is_array($opt) ? ($opt['texte'] ?? $opt_id) : $opt;
|
||
$is_selected = ($reponse_eleve == $opt_id || $reponse_eleve == $opt_texte) ? 'selected' : '';
|
||
?>
|
||
<option value="<?= htmlspecialchars($opt_id) ?>" <?= $is_selected ?>>
|
||
<?= htmlspecialchars($opt_texte) ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<?php
|
||
break;
|
||
|
||
case 'text_trous':
|
||
$trous = $options['trous'] ?? [];
|
||
$enonce_trous = $q['enonce'];
|
||
$reponses_trous = is_array($reponse_eleve) ? $reponse_eleve : [];
|
||
|
||
foreach ($trous as $trou):
|
||
$trou_id = $trou['id'];
|
||
$valeur = $reponses_trous[$trou_id] ?? '';
|
||
$input = '<input type="text"
|
||
name="reponse_' . $q['id_question'] . '[' . $trou_id . ']"
|
||
value="' . htmlspecialchars($valeur) . '"
|
||
style="display:inline-block; width:120px; padding:5px; border:2px solid #667eea; border-radius:5px;"
|
||
oninput="markAnswered(' . ($index + 1) . ')">';
|
||
$enonce_trous = str_replace('{{' . $trou_id . '}}', $input, $enonce_trous);
|
||
endforeach;
|
||
?>
|
||
<div style="line-height: 2.5;"><?= $enonce_trous ?></div>
|
||
<?php
|
||
break;
|
||
endswitch;
|
||
?>
|
||
|
||
<div class="navigation">
|
||
<button type="button" class="btn btn-secondary" onclick="previousQuestion()" <?= $index == 0 ? 'disabled' : '' ?>>
|
||
← Précédent
|
||
</button>
|
||
<?php if ($index < count($questions) - 1): ?>
|
||
<button type="button" class="btn btn-primary" onclick="nextQuestion()">
|
||
Suivant →
|
||
</button>
|
||
<?php else: ?>
|
||
<button type="button" class="btn btn-success" onclick="submitEvaluation()">
|
||
✓ Soumettre l'évaluation
|
||
</button>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</form>
|
||
|
||
<!-- Indicateur auto-save -->
|
||
<div class="autosave-indicator" id="autosaveIndicator">
|
||
💾 Sauvegarde...
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Variables globales
|
||
let currentQuestion = 1;
|
||
const totalQuestions = <?= count($questions) ?>;
|
||
let tempsRestant = <?= $temps_restant ?>;
|
||
let autosaveInterval;
|
||
let timerInterval;
|
||
const answeredQuestions = new Set();
|
||
|
||
// Initialisation
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
showQuestion(1);
|
||
initQuestionMap();
|
||
startTimer();
|
||
startAutosave();
|
||
|
||
// Marquer questions déjà répondues
|
||
<?php foreach ($questions as $index => $q): ?>
|
||
<?php if (isset($reponses_sauvegardees[$q['id_question']])): ?>
|
||
answeredQuestions.add(<?= $index + 1 ?>);
|
||
<?php endif; ?>
|
||
<?php endforeach; ?>
|
||
updateQuestionMap();
|
||
});
|
||
|
||
// Navigation
|
||
function showQuestion(n) {
|
||
document.querySelectorAll('.question-card').forEach(card => {
|
||
card.classList.remove('active');
|
||
});
|
||
|
||
const card = document.querySelector(`[data-question="${n}"]`);
|
||
if (card) {
|
||
card.classList.add('active');
|
||
currentQuestion = n;
|
||
updateProgress();
|
||
updateQuestionMap();
|
||
window.scrollTo({top: 0, behavior: 'smooth'});
|
||
}
|
||
}
|
||
|
||
function nextQuestion() {
|
||
if (currentQuestion < totalQuestions) {
|
||
showQuestion(currentQuestion + 1);
|
||
}
|
||
}
|
||
|
||
function previousQuestion() {
|
||
if (currentQuestion > 1) {
|
||
showQuestion(currentQuestion - 1);
|
||
}
|
||
}
|
||
|
||
function goToQuestion(n) {
|
||
showQuestion(n);
|
||
}
|
||
|
||
// Question map
|
||
function initQuestionMap() {
|
||
const map = document.getElementById('questionMap');
|
||
for (let i = 1; i <= totalQuestions; i++) {
|
||
const dot = document.createElement('div');
|
||
dot.className = 'question-dot';
|
||
dot.textContent = i;
|
||
dot.onclick = () => goToQuestion(i);
|
||
map.appendChild(dot);
|
||
}
|
||
}
|
||
|
||
function updateQuestionMap() {
|
||
const dots = document.querySelectorAll('.question-dot');
|
||
dots.forEach((dot, index) => {
|
||
const num = index + 1;
|
||
dot.classList.remove('current', 'answered');
|
||
|
||
if (num === currentQuestion) {
|
||
dot.classList.add('current');
|
||
}
|
||
if (answeredQuestions.has(num)) {
|
||
dot.classList.add('answered');
|
||
}
|
||
});
|
||
}
|
||
|
||
function markAnswered(questionNum) {
|
||
answeredQuestions.add(questionNum);
|
||
updateQuestionMap();
|
||
updateProgress();
|
||
// Auto-save immédiat après réponse
|
||
saveAnswers(true);
|
||
}
|
||
|
||
function updateProgress() {
|
||
const progress = (answeredQuestions.size / totalQuestions) * 100;
|
||
document.getElementById('progress').style.width = progress + '%';
|
||
}
|
||
|
||
// Timer
|
||
function startTimer() {
|
||
const timerEl = document.getElementById('timer');
|
||
|
||
timerInterval = setInterval(() => {
|
||
tempsRestant--;
|
||
|
||
const minutes = Math.floor(tempsRestant / 60);
|
||
const seconds = tempsRestant % 60;
|
||
timerEl.textContent = String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0');
|
||
|
||
// Changement de couleur
|
||
if (tempsRestant <= 60) {
|
||
timerEl.classList.add('danger');
|
||
} else if (tempsRestant <= 300) {
|
||
timerEl.classList.add('warning');
|
||
}
|
||
|
||
// Temps écoulé = soumission auto
|
||
if (tempsRestant <= 0) {
|
||
clearInterval(timerInterval);
|
||
clearInterval(autosaveInterval);
|
||
alert('Temps écoulé ! Votre évaluation va être soumise automatiquement.');
|
||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||
document.getElementById('evaluationForm').action = 'soumettre_evaluation.php?auto=1';
|
||
document.getElementById('evaluationForm').submit();
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
// Auto-save
|
||
function startAutosave() {
|
||
autosaveInterval = setInterval(() => {
|
||
saveAnswers(false);
|
||
}, 30000); // 30 secondes
|
||
}
|
||
|
||
function saveAnswers(showFeedback = true) {
|
||
const formData = new FormData(document.getElementById('evaluationForm'));
|
||
const indicator = document.getElementById('autosaveIndicator');
|
||
|
||
if (showFeedback) {
|
||
indicator.textContent = '💾 Sauvegarde...';
|
||
indicator.classList.add('saving');
|
||
}
|
||
|
||
fetch('sauvegarder_reponses.php', {
|
||
method: 'POST',
|
||
body: formData
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (showFeedback) {
|
||
indicator.classList.remove('saving');
|
||
indicator.classList.add('saved');
|
||
indicator.textContent = '✓ Sauvegardé';
|
||
|
||
setTimeout(() => {
|
||
indicator.classList.remove('saved');
|
||
}, 2000);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Erreur sauvegarde:', error);
|
||
if (showFeedback) {
|
||
indicator.classList.add('saved');
|
||
indicator.textContent = '⚠ Erreur sauvegarde';
|
||
setTimeout(() => {
|
||
indicator.classList.remove('saved');
|
||
}, 2000);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Soumission
|
||
function submitEvaluation() {
|
||
if (!confirm('Êtes-vous sûr de vouloir soumettre votre évaluation ? Vous ne pourrez plus modifier vos réponses.')) {
|
||
return;
|
||
}
|
||
|
||
clearInterval(timerInterval);
|
||
clearInterval(autosaveInterval);
|
||
|
||
// Sauvegarde finale avant soumission
|
||
saveAnswers(false);
|
||
|
||
setTimeout(() => {
|
||
// Retirer l'avertissement avant de soumettre
|
||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||
|
||
document.getElementById('evaluationForm').action = 'soumettre_evaluation.php';
|
||
document.getElementById('evaluationForm').submit();
|
||
}, 500);
|
||
}
|
||
|
||
// Prévenir sortie accidentelle
|
||
function handleBeforeUnload(e) {
|
||
e.preventDefault();
|
||
e.returnValue = 'Vos réponses seront perdues si vous quittez cette page.';
|
||
return e.returnValue;
|
||
}
|
||
|
||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||
</script>
|
||
<!-- Chatbot IA Widget -->
|
||
<div class="chat-bubble">
|
||
<button onclick="chatbot?.ouvrirChat()" class="btn-chat" id="btn-open-chat">
|
||
💬 Besoin d'aide ? <span class="help-badge"><span id="help-remaining">3</span>/3</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div id="modal-chat">
|
||
<div class="chat-container">
|
||
<div class="chat-header">
|
||
<h3>🤖 Assistant Mathématiques</h3>
|
||
<button class="btn-close-chat" id="btn-close-chat">×</button>
|
||
</div>
|
||
<div class="chat-messages" id="chat-messages">
|
||
<div class="message message-assistant">
|
||
Bonjour ! 👋 Je suis là pour t'aider sur cette question.
|
||
<br><br>
|
||
Pose-moi une question et je te guiderai étape par étape, sans te donner la réponse directement. Tu as droit à <strong>3 aides</strong> par question.
|
||
</div>
|
||
</div>
|
||
<div class="chat-input-zone">
|
||
<textarea id="chat-input" placeholder="Pose ta question ici..." rows="3"></textarea>
|
||
<button id="btn-send">📤 Envoyer</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Chatbot IA Scripts -->
|
||
<script>
|
||
// Passer les données PHP à JavaScript
|
||
const tentativeId = <?= $id_tentative ?? 'null' ?>;
|
||
const currentQuestionId = <?= isset($questions) && isset($question_actuelle) ? $questions[$question_actuelle - 1]['id_question'] : 'null' ?>;
|
||
</script>
|
||
<script src="assets/js/chatbot.js"></script>
|
||
<script>
|
||
// Initialiser le chatbot
|
||
if (tentativeId && currentQuestionId) {
|
||
window.chatbot = new ChatbotIA(tentativeId);
|
||
chatbot.currentQuestionId = currentQuestionId;
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|