Initial commit (code), runtime ignoré, secrets exclus

This commit is contained in:
nicoboy
2026-01-02 15:05:25 +01:00
commit 2fdcf50bc1
87 changed files with 11525 additions and 0 deletions

969
passer_evaluation.php Normal file
View File

@ -0,0 +1,969 @@
<?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>
</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>
</body>
</html>