Files
webval/eleve/dashboard.php
2026-01-03 10:01:19 +01:00

857 lines
31 KiB
PHP
Raw 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
/**
* Dashboard élève - VERSION FINALE CORRECTE
* Basé sur la structure BDD réelle analysée le 02/01/2026
*/
require_once __DIR__ . '/../config/config.php';
require_once __DIR__ . '/../config/database.php';
// Vérifier l'authentification
if (!isLoggedIn()) {
header('Location: ../login.php');
exit;
}
$user = currentUser();
// Vérifier que c'est bien un élève
if (!isEleve()) {
header('Location: ../login.php');
exit;
}
$db = Database::getInstance();
// Déterminer le type d'élève
$isEleveLibre = ($user['id_type'] == 3);
$isEleveClasse = ($user['id_type'] == 2);
// Récupérer les évaluations disponibles (non encore passées)
if ($isEleveLibre) {
$queryEvals = "SELECT DISTINCT e.*,
COALESCE(t.statut, 'non_commence') as statut_tentative,
t.note as note_obtenue,
t.note_sur as note_sur_obtenue,
t.date_debut as date_tentative
FROM evaluations e
INNER JOIN acces_evaluations ae ON e.id_evaluation = ae.id_evaluation
INNER JOIN classes c ON ae.id_classe = c.id_classe
LEFT JOIN tentatives_eleves t ON e.id_evaluation = t.id_evaluation
AND t.id_eleve = ?
WHERE c.type_classe = 'soutien'
AND ae.actif = 1
AND NOW() BETWEEN ae.date_debut AND ae.date_fin
AND e.actif = 1
ORDER BY ae.date_fin ASC, e.titre ASC";
$evaluations = $db->fetchAll($queryEvals, [$user['id_utilisateur']]);
} else {
if ($user['id_classe']) {
$queryEvals = "SELECT DISTINCT e.*,
ae.date_debut as debut_acces,
ae.date_fin as fin_acces,
COALESCE(t.statut, 'non_commence') as statut_tentative,
t.note as note_obtenue,
t.note_sur as note_sur_obtenue,
t.date_debut as date_tentative
FROM evaluations e
INNER JOIN acces_evaluations ae ON e.id_evaluation = ae.id_evaluation
LEFT JOIN tentatives_eleves t ON e.id_evaluation = t.id_evaluation
AND t.id_eleve = ?
WHERE ae.id_classe = ?
AND ae.actif = 1
AND NOW() BETWEEN ae.date_debut AND ae.date_fin
AND e.actif = 1
ORDER BY ae.date_fin ASC, e.titre ASC";
$evaluations = $db->fetchAll($queryEvals, [$user['id_utilisateur'], $user['id_classe']]);
} else {
$evaluations = [];
}
}
// Récupérer les 4 dernières évaluations terminées
$query4Dernieres = "SELECT
e.id_evaluation,
e.titre,
e.description,
te.note,
te.note_sur,
te.pourcentage,
te.temps_passe,
te.date_fin,
(SELECT COUNT(*) FROM questions WHERE id_evaluation = e.id_evaluation) as nb_questions
FROM tentatives_eleves te
INNER JOIN evaluations e ON te.id_evaluation = e.id_evaluation
WHERE te.id_eleve = ?
AND te.statut = 'terminee'
ORDER BY te.date_fin DESC
LIMIT 4";
$dernieresEvals = $db->fetchAll($query4Dernieres, [$user['id_utilisateur']]);
// Calculer la moyenne des 4 dernières (arrondie au demi-point supérieur)
$moyenne4Dernieres = 0;
if (!empty($dernieresEvals)) {
$somme_notes_sur_20 = 0;
foreach ($dernieresEvals as $eval) {
$note_sur_20 = $eval['note_sur'] > 0 ? ($eval['note'] / $eval['note_sur']) * 20 : 0;
$somme_notes_sur_20 += $note_sur_20;
}
$moyenne_brute = $somme_notes_sur_20 / count($dernieresEvals);
$moyenne4Dernieres = ceil($moyenne_brute * 2) / 2; // Arrondi demi-point sup
}
// Récupérer TOUTES les évaluations terminées pour le graphique
$queryToutesEvals = "SELECT
e.id_evaluation,
e.titre,
e.description,
te.note,
te.note_sur,
te.pourcentage,
te.date_fin
FROM tentatives_eleves te
INNER JOIN evaluations e ON te.id_evaluation = e.id_evaluation
WHERE te.id_eleve = ?
AND te.statut = 'terminee'
ORDER BY te.date_fin ASC";
$toutesEvals = $db->fetchAll($queryToutesEvals, [$user['id_utilisateur']]);
// Récupérer les statistiques générales
$queryStats = "SELECT
COUNT(DISTINCT t.id_evaluation) as nb_evaluations_passees,
COUNT(CASE WHEN t.statut = 'terminee' THEN 1 END) as nb_evaluations_terminees,
AVG(CASE WHEN t.statut = 'terminee' AND t.note_sur > 0
THEN (t.note / t.note_sur) * 20
ELSE NULL END) as moyenne_generale
FROM tentatives_eleves t
WHERE t.id_eleve = ?";
$stats = $db->fetchOne($queryStats, [$user['id_utilisateur']]) ?? [
'nb_evaluations_passees' => 0,
'nb_evaluations_terminees' => 0,
'moyenne_generale' => null
];
// Arrondir la moyenne générale au demi-point supérieur
if ($stats['moyenne_generale'] !== null) {
$stats['moyenne_generale'] = ceil($stats['moyenne_generale'] * 2) / 2;
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard Élève - <?= htmlspecialchars($user['prenom'] . ' ' . $user['nom']) ?></title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f7fa;
min-height: 100vh;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px 40px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.header-content {
max-width: 1400px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.user-info h1 {
font-size: 24px;
margin-bottom: 5px;
}
.user-info p {
opacity: 0.9;
font-size: 14px;
}
.badge {
background: rgba(255,255,255,0.2);
padding: 5px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.logout-btn {
background: rgba(255,255,255,0.2);
border: 2px solid white;
color: white;
padding: 10px 20px;
border-radius: 8px;
text-decoration: none;
font-weight: 600;
transition: all 0.3s;
}
.logout-btn:hover {
background: white;
color: #667eea;
}
.container {
max-width: 1400px;
margin: 30px auto;
padding: 0 20px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
padding: 25px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
border-left: 4px solid #667eea;
}
.stat-card h3 {
color: #666;
font-size: 14px;
margin-bottom: 10px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-card .value {
font-size: 32px;
font-weight: 700;
color: #333;
}
.stat-card .sub-value {
font-size: 14px;
color: #999;
margin-top: 5px;
}
/* Historique 4 dernières évaluations */
.historique-section {
background: white;
border-radius: 12px;
padding: 25px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 30px;
}
.section-title {
font-size: 20px;
color: #333;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #f0f0f0;
}
.dernieres-evals-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
}
.eval-card-small {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-radius: 10px;
padding: 20px;
transition: all 0.3s;
border: 2px solid transparent;
}
.eval-card-small:hover {
border-color: #667eea;
transform: translateY(-3px);
box-shadow: 0 6px 15px rgba(102, 126, 234, 0.2);
}
.eval-card-small h4 {
color: #333;
font-size: 15px;
margin-bottom: 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.eval-card-small .description {
font-size: 11px;
color: #666;
margin-bottom: 12px;
height: 32px;
overflow: hidden;
}
.note-display-large {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 8px;
}
.note-fraction {
font-size: 13px;
color: #666;
font-weight: 600;
}
.note-sur-20 {
font-size: 28px;
font-weight: 700;
color: #667eea;
}
.eval-card-small .date {
font-size: 11px;
color: #999;
text-align: right;
margin-top: 5px;
}
.moyenne-4 {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 10px;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
}
.moyenne-4 h3 {
font-size: 14px;
margin-bottom: 10px;
opacity: 0.9;
}
.moyenne-4 .value {
font-size: 42px;
font-weight: 700;
}
/* Graphique progression */
.graph-section {
background: white;
border-radius: 12px;
padding: 25px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 30px;
}
.chart-container {
position: relative;
height: 300px;
margin-top: 20px;
}
/* Évaluations disponibles */
.section {
background: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 30px;
}
.eval-grid {
display: grid;
gap: 15px;
}
.eval-card {
border: 2px solid #e0e0e0;
border-radius: 10px;
padding: 20px;
transition: all 0.3s;
display: flex;
justify-content: space-between;
align-items: center;
}
.eval-card:hover {
border-color: #667eea;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
transform: translateY(-2px);
}
.eval-info h3 {
color: #333;
margin-bottom: 8px;
font-size: 18px;
}
.eval-info p {
color: #666;
font-size: 14px;
margin-bottom: 5px;
}
.eval-actions {
display: flex;
gap: 10px;
align-items: center;
}
.btn {
padding: 10px 20px;
border-radius: 8px;
text-decoration: none;
font-weight: 600;
font-size: 14px;
transition: all 0.3s;
border: none;
cursor: pointer;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.btn-secondary {
background: #f0f0f0;
color: #666;
}
.btn-secondary:hover {
background: #e0e0e0;
}
.status-badge {
padding: 5px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.status-non-commence {
background: #e3f2fd;
color: #1976d2;
}
.status-en-cours {
background: #fff3e0;
color: #f57c00;
}
.status-termine {
background: #e8f5e9;
color: #388e3c;
}
.note-display {
font-size: 24px;
font-weight: 700;
color: #667eea;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.alert {
padding: 15px 20px;
border-radius: 8px;
margin-bottom: 20px;
border-left: 4px solid;
}
.alert-info {
background: #e3f2fd;
border-color: #2196f3;
color: #1565c0;
}
</style>
</head>
<body>
<div class="header">
<div class="header-content">
<div class="user-info">
<h1>👋 Bonjour, <?= htmlspecialchars($user['prenom']) ?> !</h1>
<p>
<?php if ($isEleveLibre): ?>
<span class="badge">🎯 Groupe de Soutien</span>
<?php else: ?>
Classe : <?= htmlspecialchars($user['nom_classe'] ?? 'Non assigné') ?>
(<?= htmlspecialchars($user['niveau'] ?? '') ?>)
<?php endif; ?>
</p>
</div>
<a href="../logout.php" class="logout-btn">Déconnexion</a>
</div>
</div>
<div class="container">
<!-- STATISTIQUES GLOBALES -->
<div class="stats-grid">
<div class="stat-card">
<h3>📚 Évaluations passées</h3>
<div class="value"><?= $stats['nb_evaluations_passees'] ?></div>
</div>
<div class="stat-card">
<h3>✅ Évaluations terminées</h3>
<div class="value"><?= $stats['nb_evaluations_terminees'] ?></div>
</div>
<div class="stat-card">
<h3>📊 Moyenne générale</h3>
<div class="value">
<?= $stats['moyenne_generale'] !== null
? number_format($stats['moyenne_generale'], 1) . '/20'
: '-' ?>
</div>
<?php if (!empty($toutesEvals) && count($toutesEvals) > 1): ?>
<div class="sub-value">
Sur <?= count($toutesEvals) ?> évaluation<?= count($toutesEvals) > 1 ? 's' : '' ?>
</div>
<?php endif; ?>
</div>
</div>
<!-- INFO ÉLÈVE LIBRE -->
<?php if ($isEleveLibre): ?>
<div class="alert alert-info">
<strong> Mode Soutien :</strong>
Vous êtes inscrit dans le groupe de soutien. Les évaluations que vous passez ne comptent
pas dans votre bulletin scolaire, elles sont là pour vous entraîner et progresser.
</div>
<?php endif; ?>
<!-- HISTORIQUE 4 DERNIÈRES ÉVALUATIONS -->
<?php if (!empty($dernieresEvals)): ?>
<div class="historique-section">
<h2 class="section-title">📋 Mes 4 dernières évaluations</h2>
<div class="dernieres-evals-grid">
<?php foreach ($dernieresEvals as $eval):
// Convertir la note brute en note sur 20 en utilisant le barème réel
// note_sur = barème total (80, 50, 20, etc.)
$note_sur_20 = $eval['note_sur'] > 0 ? ($eval['note'] / $eval['note_sur']) * 20 : 0;
// Arrondir au demi-point supérieur
$note_arrondie = ceil($note_sur_20 * 2) / 2;
// Le nombre de questions réussies = la note brute elle-même !
$questions_reussies = $eval['note'];
?>
<div class="eval-card-small">
<h4 title="<?= htmlspecialchars($eval['titre']) ?>">
<?= htmlspecialchars($eval['titre']) ?>
</h4>
<div class="description">
<?= htmlspecialchars($eval['description'] ?? '') ?>
</div>
<div class="note-display-large">
<div>
<div class="note-fraction">
<?= number_format($questions_reussies, 1) ?>/<?= number_format($eval['note_sur'], 0) ?> points
</div>
<div class="note-sur-20">
<?= number_format($note_arrondie, 1) ?>/20
</div>
</div>
<div style="text-align: right;">
<div class="note-fraction">
<?= round($eval['pourcentage']) ?>%
</div>
<div style="font-size: 11px; color: #999; margin-top: 3px;">
⏱️ <?= gmdate('i:s', $eval['temps_passe'] ?? 0) ?>
</div>
</div>
</div>
<div class="date">
<?= date('d/m/Y à H:i', strtotime($eval['date_fin'])) ?>
</div>
</div>
<?php endforeach; ?>
<!-- Moyenne des 4 dernières -->
<div class="moyenne-4">
<h3>Moyenne sur ces 4 évaluations</h3>
<div class="value"><?= number_format($moyenne4Dernieres, 1) ?>/20</div>
</div>
</div>
</div>
<?php endif; ?>
<!-- GRAPHIQUE PROGRESSION -->
<?php if (!empty($toutesEvals) && count($toutesEvals) > 1): ?>
<div class="graph-section">
<h2 class="section-title">📈 Ma progression</h2>
<div class="chart-container">
<canvas id="progressionChart"></canvas>
</div>
</div>
<?php endif; ?>
<!-- ÉVALUATIONS DISPONIBLES -->
<div class="section">
<h2 class="section-title">📝 Évaluations disponibles</h2>
<?php if (empty($evaluations)): ?>
<div class="empty-state">
<p style="font-size: 18px; margin-bottom: 10px;">
Aucune évaluation disponible pour le moment
</p>
<p>Les nouvelles évaluations apparaîtront ici lorsque votre enseignant les publiera.</p>
</div>
<?php else: ?>
<div class="eval-grid">
<?php foreach ($evaluations as $eval): ?>
<div class="eval-card">
<div class="eval-info">
<h3><?= htmlspecialchars($eval['titre']) ?></h3>
<?php if (!empty($eval['description'])): ?>
<p><?= htmlspecialchars($eval['description']) ?></p>
<?php endif; ?>
<p>
<strong>Durée :</strong> <?= $eval['duree_minutes'] ?? 'Libre' ?> min
| <strong>Note :</strong> /<?= $eval['note_totale'] ?? 20 ?>
</p>
<?php if (isset($eval['fin_acces'])): ?>
<p style="color: #f57c00;">
⏰ Disponible jusqu'au <?= date('d/m/Y H:i', strtotime($eval['fin_acces'])) ?>
</p>
<?php endif; ?>
</div>
<div class="eval-actions">
<?php
$statut = $eval['statut_tentative'] ?? 'non_commence';
?>
<?php if ($statut === 'terminee'): ?>
<?php
// Utiliser note_sur si disponible, sinon calculer avec nb questions
if (isset($eval['note_sur_obtenue']) && $eval['note_sur_obtenue'] > 0) {
$note_sur_20 = ($eval['note_obtenue'] / $eval['note_sur_obtenue']) * 20;
} else {
// Fallback : calculer avec nb questions
$queryNbQ = "SELECT COUNT(*) as nb FROM questions WHERE id_evaluation = ?";
$resultNbQ = $db->fetchOne($queryNbQ, [$eval['id_evaluation']]);
$nb_q = $resultNbQ['nb'] ?? 1;
$note_sur_20 = ($eval['note_obtenue'] / $nb_q) * 20;
}
$note_affichee = ceil($note_sur_20 * 2) / 2;
?>
<span class="note-display">
<?= number_format($note_affichee, 1) ?>/20
</span>
<span class="status-badge status-termine">✅ Terminé</span>
<a href="../voir_resultat.php?id_evaluation=<?= $eval['id_evaluation'] ?>"
class="btn btn-secondary">
Voir le résultat
</a>
<?php elseif ($statut === 'en_cours'): ?>
<span class="status-badge status-en-cours">⏳ En cours</span>
<a href="../passer_evaluation.php?id_evaluation=<?= $eval['id_evaluation'] ?>"
class="btn btn-primary">
Reprendre
</a>
<?php else: ?>
<span class="status-badge status-non-commence">🆕 Nouveau</span>
<a href="../passer_evaluation.php?id_evaluation=<?= $eval['id_evaluation'] ?>"
class="btn btn-primary">
Commencer
</a>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<?php if (!empty($toutesEvals) && count($toutesEvals) > 1): ?>
<script>
// Données pour le graphique
const toutesEvaluations = <?= json_encode($toutesEvals) ?>;
// Préparer les labels
const labels = toutesEvaluations.map((e, index) => {
const date = new Date(e.date_fin);
return `Eval ${index + 1}\n${date.getDate()}/${date.getMonth() + 1}`;
});
// Récupérer le nombre de questions pour chaque évaluation
// Utiliser note_sur (barème total) pour calculer la note sur 20
const notes = toutesEvaluations.map(e => {
// note / note_sur * 20
const note_sur_20 = (parseFloat(e.note) / parseFloat(e.note_sur)) * 20;
return Math.ceil(note_sur_20 * 2) / 2; // Arrondir demi-point sup
});
// Calculer la moyenne mobile (sur 3 évaluations)
const moyennesMobiles = notes.map((note, index, arr) => {
if (index < 2) return null;
const sum = arr[index - 2] + arr[index - 1] + arr[index];
const avg = sum / 3;
return Math.ceil(avg * 2) / 2;
});
// Créer le graphique
const ctx = document.getElementById('progressionChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'Note (/20)',
data: notes,
borderColor: '#667eea',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
borderWidth: 3,
pointRadius: 6,
pointBackgroundColor: '#667eea',
pointBorderColor: '#fff',
pointBorderWidth: 2,
pointHoverRadius: 8,
tension: 0.3,
fill: true
},
{
label: 'Moyenne mobile (3 évals)',
data: moyennesMobiles,
borderColor: '#28a745',
backgroundColor: 'rgba(40, 167, 69, 0.05)',
borderWidth: 2,
borderDash: [5, 5],
pointRadius: 4,
pointBackgroundColor: '#28a745',
pointBorderColor: '#fff',
pointBorderWidth: 2,
tension: 0.3,
fill: false
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'top',
labels: {
usePointStyle: true,
padding: 15,
font: {
size: 12,
weight: '600'
}
}
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
padding: 12,
titleFont: {
size: 13,
weight: 'bold'
},
bodyFont: {
size: 12
},
callbacks: {
title: function(context) {
const index = context[0].dataIndex;
return toutesEvaluations[index].titre;
},
label: function(context) {
const index = context.dataIndex;
const eval = toutesEvaluations[index];
if (context.datasetIndex === 0) {
return `Note: ${context.parsed.y.toFixed(1)}/20 (${Math.round(eval.pourcentage)}%)`;
} else {
return context.parsed.y ? `Moyenne mobile: ${context.parsed.y.toFixed(1)}/20` : '';
}
},
afterLabel: function(context) {
if (context.datasetIndex === 0) {
const index = context.dataIndex;
const eval = toutesEvaluations[index];
const date = new Date(eval.date_fin);
return `Date: ${date.toLocaleDateString('fr-FR')}`;
}
return '';
}
}
}
},
scales: {
y: {
beginAtZero: true,
max: 20,
ticks: {
stepSize: 2,
callback: function(value) {
return value + '/20';
},
font: {
size: 11
}
},
grid: {
color: 'rgba(0, 0, 0, 0.05)'
}
},
x: {
ticks: {
font: {
size: 10
}
},
grid: {
display: false
}
}
},
interaction: {
intersect: false,
mode: 'index'
}
}
});
</script>
<?php endif; ?>
</body>
</html>