Files
webval/enseignant/monitoring.php
2026-01-02 16:24:12 +01:00

658 lines
21 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

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
/**
* MATRICE MONITORING TEMPS RÉEL - VERSION AMÉLIORÉE
* Affichage matriciel des questions pour éviter débordement horizontal
*/
require_once '../config/config.php';
require_once '../config/database.php';
require_once '../config/session.php';
if (session_status() === PHP_SESSION_NONE) {
SessionManager::startSession();
}
// Vérification enseignant
if (!isset($_SESSION['user_id']) || $_SESSION['type_libelle'] !== 'enseignant') {
header('Location: ../auth/login.php');
exit;
}
$id_evaluation = $_GET['id_evaluation'] ?? null;
if (!$id_evaluation) {
die('❌ ID évaluation manquant');
}
try {
$db = Database::getInstance()->getConnection();
// Récupérer évaluation
$stmt = $db->prepare("SELECT * FROM evaluations WHERE id_evaluation = ?");
$stmt->execute([$id_evaluation]);
$evaluation = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$evaluation) {
die('❌ Évaluation non trouvée');
}
// Récupérer questions
$stmt = $db->prepare("SELECT id_question, ordre, enonce FROM questions WHERE id_evaluation = ? ORDER BY ordre ASC");
$stmt->execute([$id_evaluation]);
$questions = $stmt->fetchAll(PDO::FETCH_ASSOC);
$nb_questions = count($questions);
// Calculer dimensions matrice optimales (proche du carré)
$cols = ceil(sqrt($nb_questions));
$rows = ceil($nb_questions / $cols);
} catch (Exception $e) {
die('Erreur: ' . $e->getMessage());
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Monitoring - <?= htmlspecialchars($evaluation['titre']) ?></title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 10px;
}
.container {
max-width: 1600px;
margin: 0 auto;
}
.header {
background: white;
padding: 12px 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 12px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
color: #333;
font-size: 18px;
display: flex;
align-items: center;
gap: 8px;
}
.refresh-indicator {
padding: 5px 10px;
background: #f0f0f0;
border-radius: 4px;
font-size: 10px;
color: #666;
display: flex;
align-items: center;
gap: 5px;
}
.refresh-indicator.active {
background: #d4edda;
color: #155724;
}
.stats-bar {
background: white;
padding: 10px 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 12px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
}
.stat-item {
text-align: center;
}
.stat-value {
font-size: 22px;
font-weight: bold;
color: #667eea;
}
.stat-label {
font-size: 10px;
color: #666;
margin-top: 2px;
}
/* Cards élèves - VERSION COMPACTE */
.eleves-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 12px;
}
.eleve-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 12px;
transition: transform 0.2s, box-shadow 0.2s;
}
.eleve-card:hover {
transform: translateY(-3px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.eleve-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.eleve-info {
flex: 1;
min-width: 0; /* Pour ellipsis */
}
.eleve-name {
font-size: 13px;
font-weight: 600;
color: #333;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.eleve-classe {
font-size: 10px;
color: #666;
}
.status-badge {
padding: 3px 8px;
border-radius: 12px;
font-size: 9px;
font-weight: 600;
white-space: nowrap;
}
.status-en-ligne { background: #d4edda; color: #155724; }
.status-en-cours { background: #fff3cd; color: #856404; }
.status-inactif { background: #fff3cd; color: #856404; }
.status-hors-ligne { background: #f8d7da; color: #721c24; }
.status-termine { background: #e2e3e5; color: #383d41; }
.progress-section {
margin-bottom: 8px;
}
.progress-bar-container {
background: #e9ecef;
border-radius: 6px;
height: 16px;
overflow: hidden;
position: relative;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #28a745 0%, #20c997 100%);
transition: width 0.5s ease;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 9px;
font-weight: bold;
}
.progress-text {
font-size: 9px;
color: #666;
margin-top: 3px;
text-align: center;
}
.time-info {
display: flex;
justify-content: space-between;
font-size: 9px;
color: #666;
margin-bottom: 8px;
gap: 4px;
}
.time-info span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Matrice questions - VERSION ULTRA COMPACTE */
.questions-matrix {
display: grid;
grid-template-columns: repeat(<?= $cols ?>, 1fr);
gap: 2px;
margin-top: 6px;
}
.question-cell {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
border-radius: 2px;
cursor: pointer;
transition: transform 0.15s, box-shadow 0.15s;
background: #f8f9fa;
position: relative;
min-height: 18px;
}
.question-cell:hover {
transform: scale(1.3);
box-shadow: 0 2px 6px rgba(0,0,0,0.25);
z-index: 10;
}
.question-cell.repondu {
background: #cce5ff;
}
.question-cell.correct {
background: #d4edda;
}
.question-cell.incorrect {
background: #f8d7da;
}
.question-number {
position: absolute;
top: 1px;
left: 2px;
font-size: 6px;
font-weight: 600;
color: #666;
line-height: 1;
}
.matrix-legend {
display: flex;
justify-content: center;
gap: 15px;
margin-top: 10px;
font-size: 11px;
color: #666;
}
.matrix-legend-item {
display: flex;
align-items: center;
gap: 5px;
}
.btn-retour {
background: white;
color: #667eea;
border: 2px solid #667eea;
padding: 6px 12px;
border-radius: 6px;
text-decoration: none;
font-weight: 600;
font-size: 11px;
transition: all 0.3s;
}
.btn-retour:hover {
background: #667eea;
color: white;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
animation: spin 1s linear infinite;
}
.no-data {
text-align: center;
padding: 60px 20px;
color: #666;
font-size: 16px;
background: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
/* Légende globale */
.legende {
background: white;
padding: 10px 15px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-top: 12px;
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
font-size: 10px;
}
.legende-title {
font-weight: 600;
color: #333;
font-size: 11px;
}
.legende-item {
display: flex;
align-items: center;
gap: 5px;
}
.tooltip {
position: absolute;
background: rgba(0,0,0,0.9);
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
white-space: nowrap;
pointer-events: none;
z-index: 1000;
display: none;
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<div class="header">
<h1>
📊 Monitoring
<span style="font-size: 14px; font-weight: normal; color: #666;">
- <?= htmlspecialchars($evaluation['titre']) ?>
</span>
<span style="font-size: 11px; font-weight: normal; color: #999;">
(<?= $nb_questions ?>Q - <?= $rows ?>×<?= $cols ?>)
</span>
</h1>
<div style="display: flex; gap: 15px; align-items: center;">
<div class="refresh-indicator" id="refreshIndicator">
<span id="refreshIcon">🔄</span>
<span id="refreshText">Chargement...</span>
</div>
<a href="dashboard.php" class="btn-retour">← Retour</a>
</div>
</div>
<!-- Statistiques -->
<div class="stats-bar" id="statsBar">
<div class="stat-item">
<div class="stat-value" id="statTotal">-</div>
<div class="stat-label">Total élèves</div>
</div>
<div class="stat-item">
<div class="stat-value" style="color: #28a745;" id="statActifs">-</div>
<div class="stat-label">🟢 En ligne</div>
</div>
<div class="stat-item">
<div class="stat-value" style="color: #ffc107;" id="statEnCours">-</div>
<div class="stat-label">🟡 En cours</div>
</div>
<div class="stat-item">
<div class="stat-value" style="color: #dc3545;" id="statInactifs">-</div>
<div class="stat-label">🔴 Inactifs</div>
</div>
<div class="stat-item">
<div class="stat-value" style="color: #6c757d;" id="statTermines">-</div>
<div class="stat-label">⚫ Terminés</div>
</div>
</div>
<!-- Grille élèves -->
<div class="eleves-grid" id="elevesGrid">
<div class="no-data">
🔄 Chargement des données...
</div>
</div>
<!-- Légende -->
<div class="legende">
<div class="legende-title">Légende :</div>
<div class="legende-item">
<span style="display: inline-block; width: 14px; height: 14px; background: #f8f9fa; border-radius: 2px;"></span>
<span>Non répondu</span>
</div>
<div class="legende-item">
<span style="display: inline-block; width: 14px; height: 14px; background: #cce5ff; border-radius: 2px;"></span>
<span>✅ Répondu</span>
</div>
<div class="legende-item">
<span style="display: inline-block; width: 14px; height: 14px; background: #d4edda; border-radius: 2px;"></span>
<span>✔️ Correct</span>
</div>
<div class="legende-item">
<span style="display: inline-block; width: 14px; height: 14px; background: #f8d7da; border-radius: 2px;"></span>
<span>❌ Incorrect</span>
</div>
<div class="legende-item" style="margin-left: 15px;">
<span>🟢 En ligne (&lt;1min)</span>
</div>
<div class="legende-item">
<span>🟡 En cours (&lt;3min)</span>
</div>
<div class="legende-item">
<span>🔴 Inactif (&gt;3min)</span>
</div>
</div>
</div>
<!-- Tooltip -->
<div class="tooltip" id="tooltip"></div>
<script>
const ID_EVALUATION = <?= $id_evaluation ?>;
const NB_QUESTIONS = <?= $nb_questions ?>;
const MATRIX_COLS = <?= $cols ?>;
let refreshInterval;
// Charger données initiales
loadMonitoringData();
// Auto-refresh toutes les 10 secondes
refreshInterval = setInterval(loadMonitoringData, 10000);
function loadMonitoringData() {
const refreshIcon = document.getElementById('refreshIcon');
const refreshText = document.getElementById('refreshText');
const refreshIndicator = document.getElementById('refreshIndicator');
// Animation chargement
refreshIcon.classList.add('spinner');
refreshText.textContent = 'Actualisation...';
refreshIndicator.classList.add('active');
fetch(`monitoring_ajax.php?id_evaluation=${ID_EVALUATION}`)
.then(response => response.json())
.then(data => {
if (data.success) {
updateStats(data.stats);
updateElevesGrid(data.tentatives);
// Animation succès
refreshIcon.textContent = '✅';
refreshText.textContent = 'Mis à jour ' + new Date().toLocaleTimeString('fr-FR');
setTimeout(() => {
refreshIcon.textContent = '🔄';
refreshIndicator.classList.remove('active');
}, 2000);
} else {
throw new Error(data.error || 'Erreur inconnue');
}
})
.catch(error => {
console.error('Erreur:', error);
refreshIcon.textContent = '❌';
refreshText.textContent = 'Erreur de chargement';
refreshIndicator.style.background = '#f8d7da';
refreshIndicator.style.color = '#721c24';
})
.finally(() => {
refreshIcon.classList.remove('spinner');
});
}
function updateStats(stats) {
document.getElementById('statTotal').textContent = stats.total;
document.getElementById('statActifs').textContent = stats.en_ligne;
document.getElementById('statEnCours').textContent = stats.en_cours;
document.getElementById('statInactifs').textContent = stats.inactif + stats.hors_ligne;
document.getElementById('statTermines').textContent = stats.termine;
}
function updateElevesGrid(tentatives) {
const grid = document.getElementById('elevesGrid');
if (tentatives.length === 0) {
grid.innerHTML = `
<div class="no-data">
📭 Aucun élève n'a encore commencé cette évaluation
</div>
`;
return;
}
grid.innerHTML = tentatives.map(t => createEleveCard(t)).join('');
// Ajouter tooltips sur cellules questions
addQuestionTooltips();
}
function createEleveCard(tentative) {
const statusLabels = {
'en-ligne': '🟢 En ligne',
'en-cours': '🟡 En cours',
'inactif': '🟠 Inactif',
'hors-ligne': '🔴 Hors ligne',
'termine': '⚫ Terminé'
};
return `
<div class="eleve-card">
<!-- Header -->
<div class="eleve-header">
<div class="eleve-info">
<div class="eleve-name">${escapeHtml(tentative.nom)} ${escapeHtml(tentative.prenom)}</div>
<div class="eleve-classe">${escapeHtml(tentative.classe || 'Libre')}</div>
</div>
<div class="status-badge status-${tentative.statut_connexion}">
${statusLabels[tentative.statut_connexion] || tentative.statut_connexion}
</div>
</div>
<!-- Progression -->
<div class="progress-section">
<div class="progress-bar-container">
<div class="progress-bar" style="width: ${tentative.pourcentage_progression}%">
${tentative.pourcentage_progression}%
</div>
</div>
<div class="progress-text">${tentative.nb_reponses}/${NB_QUESTIONS} questions répondues</div>
</div>
<!-- Temps -->
<div class="time-info">
<span>⏱️ Temps écoulé: ${tentative.temps_ecoule}</span>
<span>🕐 Activité: ${tentative.derniere_activite}</span>
</div>
<!-- Matrice questions -->
<div class="questions-matrix">
${createQuestionsMatrix(tentative.reponses_details)}
</div>
</div>
`;
}
function createQuestionsMatrix(reponses) {
let html = '';
for (let i = 0; i < NB_QUESTIONS; i++) {
const reponse = reponses[i] || {};
const classe = reponse.classe || '';
const icone = reponse.icone || '⬜';
const tooltip = reponse.tooltip || `Question ${i + 1}`;
html += `
<div class="question-cell ${classe}"
data-question="${i + 1}"
data-tooltip="${escapeHtml(tooltip)}">
<span class="question-number">${i + 1}</span>
<span>${icone}</span>
</div>
`;
}
return html;
}
function addQuestionTooltips() {
const tooltip = document.getElementById('tooltip');
const cells = document.querySelectorAll('.question-cell');
cells.forEach(cell => {
cell.addEventListener('mouseenter', (e) => {
const tooltipText = cell.dataset.tooltip;
tooltip.textContent = tooltipText;
tooltip.style.display = 'block';
const rect = cell.getBoundingClientRect();
tooltip.style.left = rect.left + (rect.width / 2) - (tooltip.offsetWidth / 2) + 'px';
tooltip.style.top = rect.top - tooltip.offsetHeight - 5 + 'px';
});
cell.addEventListener('mouseleave', () => {
tooltip.style.display = 'none';
});
});
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Nettoyer interval au déchargement
window.addEventListener('beforeunload', () => {
if (refreshInterval) {
clearInterval(refreshInterval);
}
});
</script>
</body>
</html>