Initial commit (code), runtime ignoré, secrets exclus
This commit is contained in:
969
passer_evaluation.php
Normal file
969
passer_evaluation.php
Normal 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>
|
||||
Reference in New Issue
Block a user