Files
webval/passer_evaluation.php
2026-01-04 10:08:27 +01:00

1056 lines
37 KiB
PHP
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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)
// APRÈS (CORRIGÉ)
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);
} else {
// Reprendre la tentative existante
$id_tentative = $tentative['id_tentative'];
}
// 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">
<!-- KaTeX pour le rendu mathématique -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.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'] ?>" data-question-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 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>
<!-- KaTeX JS -->
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
<!-- Chatbot IA Scripts -->
<script src="assets/js/chatbot.js"></script>
<script>
// Variables globales pour le chatbot
const tentativeId = <?= $id_tentative ?? 'null' ?>;
const questionsIds = <?= json_encode(array_column($questions ?? [], 'id_question')) ?>;
console.log('Init chatbot - tentativeId:', tentativeId);
console.log('Questions disponibles:', questionsIds);
// Initialiser le chatbot
document.addEventListener('DOMContentLoaded', () => {
if (tentativeId && questionsIds.length > 0) {
try {
window.chatbot = new ChatbotIA(tentativeId);
// Fonction pour obtenir la question actuelle
window.chatbot.getCurrentQuestionId = function() {
// Chercher l'input radio/checkbox sélectionné ou le dernier input
const inputs = document.querySelectorAll('input[name^="reponse_"]');
if (inputs.length > 0) {
// Prendre la première question visible
for (let input of inputs) {
const match = input.name.match(/reponse_(\d+)/);
if (match) {
const qId = parseInt(match[1]);
console.log('Question actuelle détectée:', qId);
return qId;
}
}
}
// Fallback : première question
console.log('Fallback: première question');
return questionsIds[0];
};
console.log('✅ Chatbot initialisé avec succès');
} catch (error) {
console.error('❌ Erreur initialisation chatbot:', error);
}
} else {
console.warn('⚠️ Chatbot non initialisé:', {tentativeId, questionsCount: questionsIds.length});
}
});
</script>
</body>
</html>