1936 lines
100 KiB
PHP
1936 lines
100 KiB
PHP
<?php
|
|
// Démarrage de la session pour stocker les données de l'utilisateur et du bulletin
|
|
session_start();
|
|
|
|
// Configuration
|
|
$config = [
|
|
'url_login' => 'https://ainur.iut-fbleau.fr/auth/login?service=https%3A%2F%2Fnotes.iut-fbleau.fr%2Fservices%2FdoAuth.php%3Fhref%3Dhttps%3A%2F%2Fnotes.iut-fbleau.fr%2F',
|
|
'url_data_prefix' => 'https://notes.iut-fbleau.fr/services/data.php?q=relev%C3%A9Etudiant&semestre=',
|
|
'semestre_mapping' => [
|
|
'BUT2FI_S1' => '154',
|
|
'BUT2FI_S2' => '248',
|
|
'BUT2FI_S3' => '263',
|
|
'BUT2FI_S4' => '351',
|
|
'BUT2FA_S1' => '154',
|
|
'BUT2FA_S2' => '248',
|
|
'BUT2FA_S3' => '265'
|
|
]
|
|
];
|
|
|
|
// Couleurs pour les UEs
|
|
$ue_colors = [
|
|
'UE.1.1' => '#b80004',
|
|
'UE.1.2' => '#f97b3d',
|
|
'UE.1.3' => '#feb40b',
|
|
'UE.1.4' => '#80cb3f',
|
|
'UE.1.5' => '#05162e',
|
|
'UE.1.6' => '#548687',
|
|
'UE.2.1' => '#b80004',
|
|
'UE.2.2' => '#f97b3d',
|
|
'UE.2.3' => '#feb40b',
|
|
'UE.2.4' => '#80cb3f',
|
|
'UE.2.5' => '#05162e',
|
|
'UE.2.6' => '#548687',
|
|
'UE.3.1' => '#b80004',
|
|
'UE.3.2' => '#f97b3d',
|
|
'UE.3.3' => '#feb40b',
|
|
'UE.3.4' => '#80cb3f',
|
|
'UE.3.5' => '#05162e',
|
|
'UE.3.6' => '#548687'
|
|
];
|
|
|
|
// Fonction pour se connecter et récupérer les données
|
|
function fetchData($username, $password, $formation, $semestre) {
|
|
global $config;
|
|
|
|
// Vérifier si un semestre est sélectionné
|
|
if (!isset($semestre)) {
|
|
return ['error' => 'Semestre non valide'];
|
|
}
|
|
|
|
// Initialiser cURL pour simuler le navigateur
|
|
$ch = curl_init();
|
|
$cookie_file = tempnam(sys_get_temp_dir(), 'cookies');
|
|
|
|
// Configuration initiale de cURL avec un User-Agent de navigateur
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
|
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_file);
|
|
curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie_file);
|
|
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36');
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
|
|
|
// 1. Récupérer le formulaire de login pour obtenir les tokens potentiels
|
|
curl_setopt($ch, CURLOPT_URL, $config['url_login']);
|
|
$login_page = curl_exec($ch);
|
|
|
|
if ($login_page === false) {
|
|
return ['error' => 'Impossible de se connecter au serveur: ' . curl_error($ch)];
|
|
}
|
|
|
|
// Extraire le token CSRF ou lt/execution s'ils existent
|
|
$lt_token = '';
|
|
$execution_token = '';
|
|
|
|
// Chercher lt et execution dans le formulaire CAS
|
|
if (preg_match('/<input type="hidden" name="lt" value="([^"]+)"/', $login_page, $matches)) {
|
|
$lt_token = $matches[1];
|
|
}
|
|
|
|
if (preg_match('/<input type="hidden" name="execution" value="([^"]+)"/', $login_page, $matches)) {
|
|
$execution_token = $matches[1];
|
|
}
|
|
|
|
// 2. Soumettre le formulaire avec les identifiants et tokens
|
|
curl_setopt($ch, CURLOPT_URL, $config['url_login']);
|
|
curl_setopt($ch, CURLOPT_POST, true);
|
|
|
|
$post_data = [
|
|
'username' => $username,
|
|
'password' => $password,
|
|
'submitBtn' => 'Connexion'
|
|
];
|
|
|
|
// Ajouter les tokens s'ils existent
|
|
if (!empty($lt_token)) {
|
|
$post_data['lt'] = $lt_token;
|
|
}
|
|
|
|
if (!empty($execution_token)) {
|
|
$post_data['execution'] = $execution_token;
|
|
$post_data['_eventId'] = 'submit'; // Souvent utilisé avec 'execution'
|
|
}
|
|
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
|
|
|
|
// Configurer les en-têtes pour simuler une requête de formulaire
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
|
'Content-Type: application/x-www-form-urlencoded',
|
|
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
|
'Accept-Language: fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7',
|
|
'Referer: ' . $config['url_login']
|
|
]);
|
|
|
|
$response_post = curl_exec($ch);
|
|
|
|
if ($response_post === false) {
|
|
return ['error' => 'Échec de la connexion: ' . curl_error($ch)];
|
|
}
|
|
|
|
// La redirection devrait nous amener à la page du bulletin
|
|
// Vérifier l'URL actuelle après redirection
|
|
$current_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
|
|
|
|
// Aller directement à la page des notes après l'authentification
|
|
curl_setopt($ch, CURLOPT_URL, 'https://notes.iut-fbleau.fr/');
|
|
curl_setopt($ch, CURLOPT_POST, false);
|
|
$bulletin_page = curl_exec($ch);
|
|
|
|
if ($bulletin_page === false) {
|
|
return ['error' => 'Impossible d\'accéder à la page des notes: ' . curl_error($ch)];
|
|
}
|
|
|
|
// Vérifier si la connexion a réussi
|
|
if (strpos($bulletin_page, "Ce relevé de notes est provisoire") === false &&
|
|
strpos($bulletin_page, "ScoDoc") === false) {
|
|
return ['error' => 'Identifiants incorrects ou problème de connexion'];
|
|
}
|
|
|
|
// 3. Récupérer les données JSON
|
|
$data_url = $config['url_data_prefix'] . $semestre;
|
|
curl_setopt($ch, CURLOPT_URL, $data_url);
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
|
'Accept: application/json',
|
|
'Referer: https://notes.iut-fbleau.fr/'
|
|
]);
|
|
$json_response = curl_exec($ch);
|
|
|
|
curl_close($ch);
|
|
|
|
if ($json_response === false) {
|
|
return ['error' => 'Impossible de récupérer les données'];
|
|
}
|
|
|
|
// Décoder les données JSON
|
|
$data = json_decode($json_response, true);
|
|
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
return ['error' => 'Format de données invalide: ' . json_last_error_msg()];
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
// Simuler les données pour le débogage
|
|
function getSimulatedData() {
|
|
// Détecter si un fichier data.json existe localement
|
|
if (file_exists('data.json')) {
|
|
$json_data = file_get_contents('data.json');
|
|
return json_decode($json_data, true);
|
|
}
|
|
|
|
// Structure simplifiée des données simulées
|
|
return [
|
|
'relevé' => [
|
|
'etudiant' => [
|
|
'nom' => 'DUPONT',
|
|
'prenom' => 'Jean'
|
|
],
|
|
'formation' => [
|
|
'acronyme' => 'BUT INFO'
|
|
],
|
|
'semestre' => [
|
|
'numero' => 2,
|
|
'notes' => [
|
|
'value' => '14.50',
|
|
'min' => '6.75',
|
|
'max' => '16.20',
|
|
'moy' => '12.35'
|
|
],
|
|
'rang' => [
|
|
'value' => '5',
|
|
'total' => '86'
|
|
]
|
|
],
|
|
'ues' => [
|
|
'UE.1.1' => [
|
|
'titre' => 'Développer des applications',
|
|
'moyenne' => [
|
|
'value' => '15.75',
|
|
'min' => '7.50',
|
|
'max' => '18.00',
|
|
'moy' => '13.25',
|
|
'rang' => '4',
|
|
'total' => '86'
|
|
],
|
|
'ressources' => [
|
|
'R1.01' => [
|
|
'coef' => 42,
|
|
'moyenne' => '16.50'
|
|
],
|
|
'R1.02' => [
|
|
'coef' => 18,
|
|
'moyenne' => '15.00'
|
|
]
|
|
],
|
|
'saes' => [
|
|
'S1.01' => [
|
|
'coef' => 40,
|
|
'moyenne' => '16.00'
|
|
]
|
|
]
|
|
],
|
|
'UE.1.2' => [
|
|
'titre' => 'Optimiser des applications',
|
|
'moyenne' => [
|
|
'value' => '14.00',
|
|
'min' => '6.00',
|
|
'max' => '17.50',
|
|
'moy' => '12.00',
|
|
'rang' => '8',
|
|
'total' => '86'
|
|
],
|
|
'ressources' => [
|
|
'R1.03' => [
|
|
'coef' => 24,
|
|
'moyenne' => '14.50'
|
|
],
|
|
'R1.04' => [
|
|
'coef' => 16,
|
|
'moyenne' => '13.50'
|
|
]
|
|
],
|
|
'saes' => [
|
|
'S1.02' => [
|
|
'coef' => 40,
|
|
'moyenne' => '15.00'
|
|
]
|
|
]
|
|
]
|
|
],
|
|
'ressources' => [
|
|
'R1.01' => [
|
|
'titre' => 'Initiation au développement',
|
|
'evaluations' => [
|
|
[
|
|
'id' => 1001,
|
|
'description' => 'TP noté 1',
|
|
'date' => '2023-10-15T10:00:00+02:00',
|
|
'note' => [
|
|
'value' => '17.00',
|
|
'min' => '8.00',
|
|
'max' => '19.00',
|
|
'moy' => '14.50'
|
|
],
|
|
'coef' => '1.00'
|
|
],
|
|
[
|
|
'id' => 1002,
|
|
'description' => 'Examen final',
|
|
'date' => '2023-12-20T14:00:00+01:00',
|
|
'note' => [
|
|
'value' => '16.00',
|
|
'min' => '7.00',
|
|
'max' => '18.50',
|
|
'moy' => '13.75'
|
|
],
|
|
'coef' => '2.00'
|
|
]
|
|
]
|
|
],
|
|
'R1.02' => [
|
|
'titre' => 'Programmation orientée objet',
|
|
'evaluations' => [
|
|
[
|
|
'id' => 1003,
|
|
'description' => 'Projet',
|
|
'date' => '2023-11-10T10:00:00+01:00',
|
|
'note' => [
|
|
'value' => '15.00',
|
|
'min' => '9.00',
|
|
'max' => '18.00',
|
|
'moy' => '14.00'
|
|
],
|
|
'coef' => '1.50'
|
|
]
|
|
]
|
|
],
|
|
'R1.03' => [
|
|
'titre' => 'Architecture des systèmes',
|
|
'evaluations' => [
|
|
[
|
|
'id' => 1004,
|
|
'description' => 'Contrôle',
|
|
'date' => '2023-10-05T08:00:00+02:00',
|
|
'note' => [
|
|
'value' => '14.50',
|
|
'min' => '6.00',
|
|
'max' => '17.00',
|
|
'moy' => '12.00'
|
|
],
|
|
'coef' => '1.00'
|
|
]
|
|
]
|
|
],
|
|
'R1.04' => [
|
|
'titre' => 'Systèmes d\'exploitation',
|
|
'evaluations' => [
|
|
[
|
|
'id' => 1005,
|
|
'description' => 'TP noté',
|
|
'date' => '2023-11-25T14:00:00+01:00',
|
|
'note' => [
|
|
'value' => '13.50',
|
|
'min' => '7.50',
|
|
'max' => '16.50',
|
|
'moy' => '12.50'
|
|
],
|
|
'coef' => '1.00'
|
|
]
|
|
]
|
|
]
|
|
],
|
|
'saes' => [
|
|
'S1.01' => [
|
|
'titre' => 'Implémentation d\'un besoin client',
|
|
'evaluations' => [
|
|
[
|
|
'id' => 2001,
|
|
'description' => 'Livrable',
|
|
'date' => '2023-12-10T10:00:00+01:00',
|
|
'note' => [
|
|
'value' => '16.00',
|
|
'min' => '8.50',
|
|
'max' => '18.00',
|
|
'moy' => '13.75'
|
|
],
|
|
'coef' => '1.00'
|
|
]
|
|
]
|
|
],
|
|
'S1.02' => [
|
|
'titre' => 'Comparaison d\'approches algorithmiques',
|
|
'evaluations' => [
|
|
[
|
|
'id' => 2002,
|
|
'description' => 'Rapport',
|
|
'date' => '2023-11-30T16:00:00+01:00',
|
|
'note' => [
|
|
'value' => '15.00',
|
|
'min' => '7.00',
|
|
'max' => '17.50',
|
|
'moy' => '12.50'
|
|
],
|
|
'coef' => '1.00'
|
|
]
|
|
]
|
|
]
|
|
]
|
|
]
|
|
];
|
|
}
|
|
|
|
// Analyser les statistiques globales
|
|
function calculateOverallStats($data) {
|
|
$allGrades = [];
|
|
|
|
// Récupérer toutes les notes des ressources
|
|
foreach ($data['relevé']['ressources'] as $resource) {
|
|
foreach ($resource['evaluations'] as $eval) {
|
|
if (isset($eval['note']['value'])) {
|
|
$allGrades[] = (float)$eval['note']['value'];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Récupérer toutes les notes des SAEs
|
|
foreach ($data['relevé']['saes'] as $sae) {
|
|
foreach ($sae['evaluations'] as $eval) {
|
|
if (isset($eval['note']['value'])) {
|
|
$allGrades[] = (float)$eval['note']['value'];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculer les statistiques
|
|
$min = !empty($allGrades) ? min($allGrades) : 0;
|
|
$max = !empty($allGrades) ? max($allGrades) : 0;
|
|
$avg = !empty($allGrades) ? array_sum($allGrades) / count($allGrades) : 0;
|
|
$below10Count = count(array_filter($allGrades, function($grade) { return $grade < 10; }));
|
|
$above15Count = count(array_filter($allGrades, function($grade) { return $grade >= 15; }));
|
|
|
|
return [
|
|
'count' => count($allGrades),
|
|
'min' => number_format($min, 2),
|
|
'max' => number_format($max, 2),
|
|
'avg' => number_format($avg, 2),
|
|
'below10Percent' => !empty($allGrades) ? number_format(($below10Count / count($allGrades)) * 100, 1) : 0,
|
|
'above15Percent' => !empty($allGrades) ? number_format(($above15Count / count($allGrades)) * 100, 1) : 0
|
|
];
|
|
}
|
|
|
|
// Traiter la demande de connexion
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['username'], $_POST['password'], $_POST['formation'])) {
|
|
$username = $_POST['username'];
|
|
$password = $_POST['password'];
|
|
$formation = $_POST['formation'];
|
|
|
|
|
|
// Déterminer le semestre à utiliser
|
|
$semestre = isset($config['semestre_mapping'][$formation]) ? $config['semestre_mapping'][$formation] : null;
|
|
|
|
// Vérifier si le mode de simulation est activé
|
|
if ($username === 'demo' && $password === 'demo') {
|
|
$result = getSimulatedData();
|
|
$simulation_mode = true;
|
|
} else {
|
|
$result = fetchData($username, $password, $formation, $semestre);
|
|
$simulation_mode = false;
|
|
}
|
|
|
|
if (isset($result['error'])) {
|
|
$error_message = $result['error'];
|
|
} else {
|
|
// Stocker les données dans la session
|
|
$_SESSION['bulletin_data'] = $result;
|
|
$_SESSION['username'] = $username;
|
|
$_SESSION['formation'] = $formation;
|
|
$_SESSION['simulation_mode'] = $simulation_mode;
|
|
|
|
// Rediriger vers la page du bulletin
|
|
header('Location: ' . $_SERVER['PHP_SELF'] . '?view=dashboard');
|
|
exit;
|
|
}
|
|
}
|
|
|
|
// Déconnexion
|
|
if (isset($_GET['logout'])) {
|
|
session_unset();
|
|
session_destroy();
|
|
header('Location: ' . $_SERVER['PHP_SELF']);
|
|
exit;
|
|
}
|
|
|
|
// Récupérer la vue actuelle (dashboard ou login)
|
|
$current_view = isset($_GET['view']) && $_GET['view'] === 'dashboard' && isset($_SESSION['bulletin_data']) ? 'dashboard' : 'login';
|
|
|
|
// Récupérer l'onglet actif
|
|
$active_tab = isset($_GET['tab']) ? $_GET['tab'] : 'overview';
|
|
|
|
// Si on est sur le dashboard, calculer les statistiques
|
|
if ($current_view === 'dashboard') {
|
|
$data = $_SESSION['bulletin_data'];
|
|
$stats = calculateOverallStats($data);
|
|
}
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Dashboard de Bulletins BUT</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
|
|
</head>
|
|
<body class="bg-gray-50 min-h-screen">
|
|
<?php if ($current_view === 'login'): ?>
|
|
<!-- Page de connexion -->
|
|
<div class="min-h-screen flex items-center justify-center">
|
|
<div class="max-w-md w-full bg-white rounded-lg shadow p-8">
|
|
<h1 class="text-2xl font-bold mb-6 text-center">Connexion à votre bulletin</h1>
|
|
|
|
<?php if (isset($error_message)): ?>
|
|
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
|
<?php echo htmlspecialchars($error_message); ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<form method="post" action="">
|
|
<div class="mb-4">
|
|
<label for="username" class="block text-gray-700 text-sm font-bold mb-2">Nom d'utilisateur</label>
|
|
<input type="text" id="username" name="username" required
|
|
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
|
</div>
|
|
|
|
<div class="mb-6">
|
|
<label for="password" class="block text-gray-700 text-sm font-bold mb-2">Mot de passe</label>
|
|
<input type="password" id="password" name="password" required
|
|
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
|
</div>
|
|
|
|
<div class="mb-6">
|
|
<label for="formation" class="block text-gray-700 text-sm font-bold mb-2">
|
|
Formation et Semestre
|
|
</label>
|
|
<select id="formation" name="formation" required
|
|
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
|
<option value="">Sélectionnez votre formation</option>
|
|
|
|
<optgroup label="BUT1 - Promotion 2023">
|
|
<option value="BUT2FI_S1">BUT1 - Semestre 1</option>
|
|
<option value="BUT2FI_S2">BUT2 - Semestre 2</option>
|
|
</optgroup>
|
|
|
|
<optgroup label="Formation Initiale (FI) - Promotion 2023">
|
|
<option value="BUT2FI_S3">BUT2 FI - Semestre 3</option>
|
|
<option value="BUT2FI_S4">BUT2 FI - Semestre 4</option>
|
|
</optgroup>
|
|
|
|
<optgroup label="Formation en Alternance (FA) - Promotion 2023">
|
|
<option value="BUT2FA_S3">BUT2 FA - Semestre 3</option>
|
|
</optgroup>
|
|
</select>
|
|
</div>
|
|
|
|
|
|
<div class="flex items-center justify-center">
|
|
<button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
|
Se connecter
|
|
</button>
|
|
</div>
|
|
|
|
<div class="mt-4 text-center text-xs text-gray-500">
|
|
<p>Utilisez "demo/demo" pour tester l'application avec des données simulées</p>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<?php else: ?>
|
|
<!-- Dashboard -->
|
|
<header class="bg-white shadow">
|
|
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8 flex justify-between items-center">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-gray-900">
|
|
Dashboard <?php echo htmlspecialchars($data['relevé']['formation']['acronyme']); ?>
|
|
</h1>
|
|
<p class="text-gray-600">
|
|
<?php echo htmlspecialchars($data['relevé']['etudiant']['nom'] . ' ' . $data['relevé']['etudiant']['prenom']); ?>
|
|
- Semestre <?php echo htmlspecialchars($data['relevé']['semestre']['numero']); ?>
|
|
</p>
|
|
<?php if (isset($_SESSION['simulation_mode']) && $_SESSION['simulation_mode']): ?>
|
|
<p class="text-xs text-red-500">Mode simulation - Données non réelles</p>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div class="text-right">
|
|
<div class="text-2xl font-bold"><?php echo htmlspecialchars($data['relevé']['semestre']['notes']['value']); ?>/20</div>
|
|
<div class="text-sm text-gray-600">Moyenne générale</div>
|
|
<div class="text-sm font-medium mt-1">
|
|
Rang : <?php echo htmlspecialchars($data['relevé']['semestre']['rang']['value']); ?>/<?php echo htmlspecialchars($data['relevé']['semestre']['rang']['total']); ?>
|
|
</div>
|
|
<a href="?logout=1" class="text-sm text-red-600 hover:text-red-800">Déconnexion</a>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Navigation Tabs -->
|
|
<div class="border-b border-gray-200 bg-white">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="flex space-x-8">
|
|
<a href="?view=dashboard&tab=overview"
|
|
class="py-4 px-1 border-b-2 font-medium text-sm <?php echo $active_tab === 'overview' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'; ?>">
|
|
Vue d'ensemble
|
|
</a>
|
|
<a href="?view=dashboard&tab=ues"
|
|
class="py-4 px-1 border-b-2 font-medium text-sm <?php echo $active_tab === 'ues' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'; ?>">
|
|
Unités d'Enseignement
|
|
</a>
|
|
<a href="?view=dashboard&tab=resources"
|
|
class="py-4 px-1 border-b-2 font-medium text-sm <?php echo $active_tab === 'resources' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'; ?>">
|
|
Ressources
|
|
</a>
|
|
<a href="?view=dashboard&tab=saes"
|
|
class="py-4 px-1 border-b-2 font-medium text-sm <?php echo $active_tab === 'saes' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'; ?>">
|
|
SAE
|
|
</a>
|
|
<a href="?view=dashboard&tab=evolution"
|
|
class="py-4 px-1 border-b-2 font-medium text-sm <?php echo $active_tab === 'evolution' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'; ?>">
|
|
Évolution
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Content -->
|
|
<main class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
|
<?php if ($active_tab === 'overview'): ?>
|
|
<!-- Overview Tab -->
|
|
<div>
|
|
<div class="flex justify-end mb-2">
|
|
<button id="exportAllBtn" class="bg-green-500 hover:bg-green-700 text-white font-medium py-2 px-4 rounded flex items-center">
|
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
|
</svg>
|
|
Exporter tous les graphiques
|
|
</button>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
<div class="px-4 py-5 sm:p-6">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0 bg-blue-500 rounded-md p-3">
|
|
<svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
</svg>
|
|
</div>
|
|
<div class="ml-5 w-0 flex-1">
|
|
<dt class="text-sm font-medium text-gray-500 truncate">
|
|
Moyenne générale
|
|
</dt>
|
|
<dd class="flex items-baseline">
|
|
<div class="text-2xl font-semibold text-gray-900">
|
|
<?php echo htmlspecialchars($data['relevé']['semestre']['notes']['value']); ?>/20
|
|
</div>
|
|
</dd>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
<div class="px-4 py-5 sm:p-6">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0 bg-indigo-500 rounded-md p-3">
|
|
<svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
|
</svg>
|
|
</div>
|
|
<div class="ml-5 w-0 flex-1">
|
|
<dt class="text-sm font-medium text-gray-500 truncate">
|
|
Meilleure note
|
|
</dt>
|
|
<dd class="flex items-baseline">
|
|
<div class="text-2xl font-semibold text-gray-900">
|
|
<?php echo $stats['max']; ?>/20
|
|
</div>
|
|
</dd>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
<div class="px-4 py-5 sm:p-6">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0 bg-green-500 rounded-md p-3">
|
|
<svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
</div>
|
|
<div class="ml-5 w-0 flex-1">
|
|
<dt class="text-sm font-medium text-gray-500 truncate">
|
|
Notes > 15
|
|
</dt>
|
|
<dd class="flex items-baseline">
|
|
<div class="text-2xl font-semibold text-gray-900">
|
|
<?php echo $stats['above15Percent']; ?>%
|
|
</div>
|
|
</dd>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
<div class="px-4 py-5 sm:p-6">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0 bg-red-500 rounded-md p-3">
|
|
<svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</div>
|
|
<div class="ml-5 w-0 flex-1">
|
|
<dt class="text-sm font-medium text-gray-500 truncate">
|
|
Notes < 10
|
|
</dt>
|
|
<dd class="flex items-baseline">
|
|
<div class="text-2xl font-semibold text-gray-900">
|
|
<?php echo $stats['below10Percent']; ?>%
|
|
</div>
|
|
</dd>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
|
<!-- UE Performance Chart -->
|
|
<div class="bg-white shadow rounded-lg p-6 relative">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h2 class="text-lg font-medium text-gray-900">Performance par UE</h2>
|
|
<button class="export-btn bg-green-500 hover:bg-green-700 text-white text-sm py-1 px-2 rounded" data-chart="chartUE">
|
|
Exporter
|
|
</button>
|
|
</div>
|
|
<canvas id="chartUE" height="300"></canvas>
|
|
</div>
|
|
|
|
<!-- Radar Chart -->
|
|
<div class="bg-white shadow rounded-lg p-6 relative">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h2 class="text-lg font-medium text-gray-900">Profil de compétences</h2>
|
|
<button class="export-btn bg-green-500 hover:bg-green-700 text-white text-sm py-1 px-2 rounded" data-chart="chartRadar">
|
|
Exporter
|
|
</button>
|
|
</div>
|
|
<canvas id="chartRadar" height="300"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
|
<!-- Best Performances -->
|
|
<div class="bg-white shadow rounded-lg p-6">
|
|
<h2 class="text-lg font-medium text-gray-900 mb-4">Meilleures performances</h2>
|
|
<div class="space-y-4">
|
|
<?php
|
|
// Récupérer toutes les évaluations
|
|
$allEvals = [];
|
|
foreach ($data['relevé']['ressources'] as $resourceId => $resource) {
|
|
foreach ($resource['evaluations'] as $eval) {
|
|
if (isset($eval['note']['value'])) {
|
|
$allEvals[] = [
|
|
'resourceId' => $resourceId,
|
|
'resourceTitle' => $resource['titre'],
|
|
'evalId' => $eval['id'],
|
|
'description' => $eval['description'],
|
|
'note' => (float)$eval['note']['value'],
|
|
'coef' => (float)$eval['coef']
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($data['relevé']['saes'] as $saeId => $sae) {
|
|
foreach ($sae['evaluations'] as $eval) {
|
|
if (isset($eval['note']['value'])) {
|
|
$allEvals[] = [
|
|
'resourceId' => $saeId,
|
|
'resourceTitle' => $sae['titre'],
|
|
'evalId' => $eval['id'],
|
|
'description' => $eval['description'],
|
|
'note' => (float)$eval['note']['value'],
|
|
'coef' => (float)$eval['coef']
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Trier par note (décroissant)
|
|
usort($allEvals, function($a, $b) {
|
|
return $b['note'] <=> $a['note'];
|
|
});
|
|
|
|
// Afficher les 5 meilleures
|
|
$bestEvals = array_slice($allEvals, 0, 5);
|
|
|
|
foreach ($bestEvals as $eval):
|
|
?>
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0 h-10 w-10 rounded-full bg-green-100 flex items-center justify-center">
|
|
<span class="text-green-600 font-medium"><?php echo $eval['note']; ?></span>
|
|
</div>
|
|
<div class="ml-4">
|
|
<h3 class="text-sm font-medium text-gray-900"><?php echo htmlspecialchars($eval['resourceId'] . ' - ' . $eval['description']); ?></h3>
|
|
<p class="text-sm text-gray-500"><?php echo htmlspecialchars($eval['resourceTitle'] . ' (Coef: ' . $eval['coef'] . ')'); ?></p>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Worst Performances -->
|
|
<div class="bg-white shadow rounded-lg p-6">
|
|
<h2 class="text-lg font-medium text-gray-900 mb-4">Performances à améliorer</h2>
|
|
<div class="space-y-4">
|
|
<?php
|
|
// Trier par note (croissant)
|
|
usort($allEvals, function($a, $b) {
|
|
return $a['note'] <=> $b['note'];
|
|
});
|
|
|
|
// Afficher les 5 moins bonnes
|
|
$worstEvals = array_slice($allEvals, 0, 5);
|
|
|
|
foreach ($worstEvals as $eval):
|
|
?>
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0 h-10 w-10 rounded-full bg-red-100 flex items-center justify-center">
|
|
<span class="text-red-600 font-medium"><?php echo $eval['note']; ?></span>
|
|
</div>
|
|
<div class="ml-4">
|
|
<h3 class="text-sm font-medium text-gray-900"><?php echo htmlspecialchars($eval['resourceId'] . ' - ' . $eval['description']); ?></h3>
|
|
<p class="text-sm text-gray-500"><?php echo htmlspecialchars($eval['resourceTitle'] . ' (Coef: ' . $eval['coef'] . ')'); ?></p>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Grade Distribution -->
|
|
<div class="bg-white shadow rounded-lg p-6 relative">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h2 class="text-lg font-medium text-gray-900">Distribution des notes</h2>
|
|
<button class="export-btn bg-green-500 hover:bg-green-700 text-white text-sm py-1 px-2 rounded" data-chart="chartDistribution">
|
|
Exporter
|
|
</button>
|
|
</div>
|
|
<canvas id="chartDistribution" height="300"></canvas>
|
|
</div>
|
|
</div>
|
|
<?php elseif ($active_tab === 'ues'): ?>
|
|
<!-- UEs Tab -->
|
|
<div class="bg-white shadow rounded-lg p-6 relative">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h2 class="text-lg font-medium text-gray-900">Performances par UE</h2>
|
|
<button class="export-btn bg-green-500 hover:bg-green-700 text-white text-sm py-1 px-2 rounded" data-chart="chartUEComparison">
|
|
Exporter
|
|
</button>
|
|
</div>
|
|
<canvas id="chartUEComparison" height="400"></canvas>
|
|
|
|
<div class="mt-8">
|
|
<h3 class="text-md font-medium text-gray-900 mb-4">Détails des unités d'enseignement</h3>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
<?php foreach ($data['relevé']['ues'] as $ueId => $ue): ?>
|
|
<div class="border-l-4 bg-white shadow rounded-lg overflow-hidden"
|
|
style="border-color: <?php echo isset($ue_colors[$ueId]) ? $ue_colors[$ueId] : '#8884d8'; ?>">
|
|
<div class="px-4 py-5">
|
|
<div class="flex justify-between items-start">
|
|
<div>
|
|
<h3 class="text-lg font-medium text-gray-900"><?php echo htmlspecialchars($ueId); ?></h3>
|
|
<p class="text-sm text-gray-500"><?php echo htmlspecialchars($ue['titre']); ?></p>
|
|
</div>
|
|
<div class="text-right">
|
|
<div class="text-2xl font-bold"><?php echo htmlspecialchars($ue['moyenne']['value']); ?>/20</div>
|
|
<div class="text-xs text-gray-500">Moy. promo: <?php echo htmlspecialchars($ue['moyenne']['moy']); ?>/20</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4">
|
|
<div class="flex justify-between text-sm">
|
|
<span>Rang: <?php echo htmlspecialchars($ue['moyenne']['rang']); ?>/<?php echo htmlspecialchars($ue['moyenne']['total']); ?></span>
|
|
<span class="font-medium" style="color: <?php echo isset($ue_colors[$ueId]) ? $ue_colors[$ueId] : '#8884d8'; ?>">
|
|
<?php echo number_format((1 - (int)$ue['moyenne']['rang'] / (int)$ue['moyenne']['total']) * 100, 1); ?>%
|
|
</span>
|
|
</div>
|
|
<div class="w-full bg-gray-200 rounded-full h-2.5 mt-1">
|
|
<div class="h-2.5 rounded-full"
|
|
style="width: <?php echo (1 - (int)$ue['moyenne']['rang'] / (int)$ue['moyenne']['total']) * 100; ?>%;
|
|
background-color: <?php echo isset($ue_colors[$ueId]) ? $ue_colors[$ueId] : '#8884d8'; ?>">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Détails de l'UE (ressources et SAEs) -->
|
|
<div class="mt-4 pt-4 border-t border-gray-200">
|
|
<h4 class="text-sm font-medium text-gray-900 mb-2">Ressources</h4>
|
|
<div class="space-y-2">
|
|
<?php if (isset($ue['ressources'])): ?>
|
|
<?php foreach ($ue['ressources'] as $resourceId => $resourceInfo): ?>
|
|
<?php $resource = $data['relevé']['ressources'][$resourceId]; ?>
|
|
<div class="flex justify-between text-sm">
|
|
<span><?php echo htmlspecialchars($resourceId . ' - ' . $resource['titre']); ?></span>
|
|
<span class="font-medium"><?php echo htmlspecialchars($resourceInfo['moyenne']); ?>/20</span>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php else: ?>
|
|
<p class="text-sm text-gray-500">Aucune ressource associée</p>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<h4 class="text-sm font-medium text-gray-900 mt-4 mb-2">SAEs</h4>
|
|
<div class="space-y-2">
|
|
<?php if (isset($ue['saes'])): ?>
|
|
<?php foreach ($ue['saes'] as $saeId => $saeInfo): ?>
|
|
<?php $sae = $data['relevé']['saes'][$saeId]; ?>
|
|
<div class="flex justify-between text-sm">
|
|
<span><?php echo htmlspecialchars($saeId . ' - ' . $sae['titre']); ?></span>
|
|
<span class="font-medium"><?php echo htmlspecialchars($saeInfo['moyenne']); ?>/20</span>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php else: ?>
|
|
<p class="text-sm text-gray-500">Aucune SAE associée</p>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php elseif ($active_tab === 'resources'): ?>
|
|
<!-- Resources Tab -->
|
|
<div class="bg-white shadow rounded-lg p-6">
|
|
<div class="mb-6">
|
|
<input type="text" id="resourceSearch" placeholder="Rechercher une ressource..."
|
|
class="block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm p-2 border">
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" id="resourcesContainer">
|
|
<?php foreach ($data['relevé']['ressources'] as $resourceId => $resource): ?>
|
|
<?php
|
|
// Calculer la moyenne des notes pour cette ressource
|
|
$grades = array_map(function($eval) {
|
|
return (float)$eval['note']['value'];
|
|
}, $resource['evaluations']);
|
|
|
|
$avgGrade = !empty($grades) ? array_sum($grades) / count($grades) : 0;
|
|
|
|
// Trouver les UEs associées
|
|
$linkedUEs = [];
|
|
foreach ($data['relevé']['ues'] as $ueId => $ue) {
|
|
if (isset($ue['ressources']) && array_key_exists($resourceId, $ue['ressources'])) {
|
|
$linkedUEs[] = $ueId;
|
|
}
|
|
}
|
|
?>
|
|
<div class="resource-card bg-white shadow rounded-lg overflow-hidden border hover:shadow-md"
|
|
data-id="<?php echo $resourceId; ?>"
|
|
data-title="<?php echo htmlspecialchars($resource['titre']); ?>">
|
|
<div class="px-4 py-5">
|
|
<div class="flex justify-between items-start">
|
|
<div>
|
|
<h3 class="text-lg font-medium text-gray-900"><?php echo htmlspecialchars($resourceId); ?></h3>
|
|
<p class="text-sm text-gray-500"><?php echo htmlspecialchars($resource['titre']); ?></p>
|
|
<div class="flex flex-wrap mt-2">
|
|
<?php foreach ($linkedUEs as $ueId): ?>
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium mr-2 mb-1 text-white"
|
|
style="background-color: <?php echo isset($ue_colors[$ueId]) ? $ue_colors[$ueId] : '#8884d8'; ?>">
|
|
<?php echo htmlspecialchars($ueId); ?>
|
|
</span>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
<div class="text-right">
|
|
<div class="text-2xl font-bold"><?php echo number_format($avgGrade, 2); ?>/20</div>
|
|
<div class="text-xs text-gray-500"><?php echo count($resource['evaluations']); ?> éval.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4 border-t pt-4 resource-details">
|
|
<h4 class="text-sm font-medium text-gray-900 mb-2">Évaluations</h4>
|
|
<div class="space-y-2">
|
|
<?php foreach ($resource['evaluations'] as $eval): ?>
|
|
<div class="flex justify-between text-sm">
|
|
<span><?php echo htmlspecialchars($eval['description']); ?></span>
|
|
<span class="font-medium"><?php echo htmlspecialchars($eval['note']['value']); ?>/20</span>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
|
|
<?php if (count($resource['evaluations']) > 1): ?>
|
|
<div class="mt-4">
|
|
<div class="w-full h-64">
|
|
<canvas id="resource-<?php echo $resourceId; ?>"></canvas>
|
|
</div>
|
|
<button class="export-chart-btn mt-2 bg-green-500 hover:bg-green-700 text-white text-xs py-1 px-2 rounded"
|
|
data-canvas-id="resource-<?php echo $resourceId; ?>">
|
|
Exporter le graphique
|
|
</button>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
<?php elseif ($active_tab === 'saes'): ?>
|
|
<!-- SAEs Tab -->
|
|
<div class="bg-white shadow rounded-lg p-6">
|
|
<h2 class="text-lg font-medium text-gray-900 mb-4">Situations d'Apprentissage et d'Évaluation</h2>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
<?php foreach ($data['relevé']['saes'] as $saeId => $sae): ?>
|
|
<?php
|
|
// Calculer la moyenne
|
|
$grades = array_map(function($eval) {
|
|
return (float)$eval['note']['value'];
|
|
}, $sae['evaluations']);
|
|
|
|
$avgGrade = !empty($grades) ? array_sum($grades) / count($grades) : 0;
|
|
|
|
// Trouver les UEs associées
|
|
$linkedUEs = [];
|
|
foreach ($data['relevé']['ues'] as $ueId => $ue) {
|
|
if (isset($ue['saes']) && array_key_exists($saeId, $ue['saes'])) {
|
|
$linkedUEs[] = $ueId;
|
|
}
|
|
}
|
|
?>
|
|
<div class="bg-white shadow rounded-lg overflow-hidden border hover:shadow-md">
|
|
<div class="px-4 py-5">
|
|
<div class="flex justify-between items-start">
|
|
<div>
|
|
<h3 class="text-lg font-medium text-gray-900"><?php echo htmlspecialchars($saeId); ?></h3>
|
|
<p class="text-sm text-gray-500"><?php echo htmlspecialchars($sae['titre']); ?></p>
|
|
<div class="flex flex-wrap mt-2">
|
|
<?php foreach ($linkedUEs as $ueId): ?>
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium mr-2 mb-1 text-white"
|
|
style="background-color: <?php echo isset($ue_colors[$ueId]) ? $ue_colors[$ueId] : '#8884d8'; ?>">
|
|
<?php echo htmlspecialchars($ueId); ?>
|
|
</span>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
<div class="text-right">
|
|
<div class="text-2xl font-bold"><?php echo number_format($avgGrade, 2); ?>/20</div>
|
|
<div class="text-xs text-gray-500"><?php echo count($sae['evaluations']); ?> éval.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4 border-t pt-4">
|
|
<h4 class="text-sm font-medium text-gray-900 mb-2">Évaluations</h4>
|
|
<div class="space-y-2">
|
|
<?php foreach ($sae['evaluations'] as $eval): ?>
|
|
<div class="flex justify-between text-sm">
|
|
<span><?php echo htmlspecialchars($eval['description']); ?></span>
|
|
<span class="font-medium"><?php echo htmlspecialchars($eval['note']['value']); ?>/20</span>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
|
|
<?php if (count($sae['evaluations']) > 1): ?>
|
|
<div class="mt-4">
|
|
<div class="w-full h-64">
|
|
<canvas id="sae-<?php echo $saeId; ?>"></canvas>
|
|
</div>
|
|
<button class="export-chart-btn mt-2 bg-green-500 hover:bg-green-700 text-white text-xs py-1 px-2 rounded"
|
|
data-canvas-id="sae-<?php echo $saeId; ?>">
|
|
Exporter le graphique
|
|
</button>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
<?php elseif ($active_tab === 'evolution'): ?>
|
|
<!-- Evolution Tab -->
|
|
<div class="bg-white shadow rounded-lg p-6 relative">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h2 class="text-lg font-medium text-gray-900">Évolution des notes</h2>
|
|
<button class="export-btn bg-green-500 hover:bg-green-700 text-white text-sm py-1 px-2 rounded" data-chart="chartEvolution">
|
|
Exporter
|
|
</button>
|
|
</div>
|
|
<canvas id="chartEvolution" height="400"></canvas>
|
|
|
|
<h3 class="text-md font-medium text-gray-900 mt-8 mb-4">Historique des évaluations</h3>
|
|
<div class="flow-root">
|
|
<ul class="-mb-8">
|
|
<?php
|
|
// Collecter toutes les évaluations avec date
|
|
$gradesOverTime = [];
|
|
|
|
|
|
foreach ($data['relevé']['ressources'] as $resourceId => $resource) {
|
|
foreach ($resource['evaluations'] as $eval) {
|
|
if (isset($eval['date']) && $eval['date']) {
|
|
$gradesOverTime[] = [
|
|
'date' => new DateTime($eval['date']),
|
|
'name' => $resourceId . ' - ' . $eval['description'],
|
|
'grade' => (float)$eval['note']['value'],
|
|
'type' => 'resource'
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($data['relevé']['saes'] as $saeId => $sae) {
|
|
foreach ($sae['evaluations'] as $eval) {
|
|
if (isset($eval['date']) && $eval['date']) {
|
|
$gradesOverTime[] = [
|
|
'date' => new DateTime($eval['date']),
|
|
'name' => $saeId . ' - ' . $eval['description'],
|
|
'grade' => (float)$eval['note']['value'],
|
|
'type' => 'sae'
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Trier par date
|
|
usort($gradesOverTime, function($a, $b) {
|
|
return $a['date'] <=> $b['date'];
|
|
});
|
|
|
|
foreach ($gradesOverTime as $index => $event):
|
|
$colorClass = $event['grade'] >= 16 ? 'bg-green-500' : ($event['grade'] >= 10 ? 'bg-blue-500' : 'bg-red-500');
|
|
?>
|
|
<li>
|
|
<div class="relative pb-8">
|
|
<?php if ($index !== count($gradesOverTime) - 1): ?>
|
|
<span class="absolute top-4 left-4 -ml-px h-full w-0.5 bg-gray-200" aria-hidden="true"></span>
|
|
<?php endif; ?>
|
|
<div class="relative flex space-x-3">
|
|
<div>
|
|
<span class="h-8 w-8 rounded-full flex items-center justify-center ring-8 ring-white <?php echo $colorClass; ?>">
|
|
<span class="text-white text-xs font-medium"><?php echo $event['grade']; ?></span>
|
|
</span>
|
|
</div>
|
|
<div class="min-w-0 flex-1 pt-1.5 flex justify-between space-x-4">
|
|
<div>
|
|
<p class="text-sm text-gray-900">
|
|
<?php echo htmlspecialchars($event['name']); ?>
|
|
</p>
|
|
</div>
|
|
<div class="text-right text-sm whitespace-nowrap text-gray-500">
|
|
<?php echo $event['date']->format('d/m/Y'); ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
<?php endforeach; ?>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<?php elseif ($active_tab === 'comparison'): ?>
|
|
<!-- Comparison Tab -->
|
|
<div class="bg-white shadow rounded-lg p-6">
|
|
<h2 class="text-lg font-medium text-gray-900 mb-4">Comparaison d'éléments</h2>
|
|
|
|
<div class="mb-6">
|
|
<input type="text" id="comparisonSearch" placeholder="Rechercher UEs, ressources, SAEs..."
|
|
class="block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm p-2 border">
|
|
</div>
|
|
|
|
<div id="comparisonResults" class="mt-2 border border-gray-200 rounded-md max-h-60 overflow-y-auto hidden">
|
|
<ul class="divide-y divide-gray-200" id="comparisonResultsList"></ul>
|
|
</div>
|
|
|
|
<div class="mb-6">
|
|
<h3 class="text-md font-medium text-gray-900 mb-2">Éléments sélectionnés</h3>
|
|
<div id="selectedItems" class="flex flex-wrap">
|
|
<p class="text-sm text-gray-500" id="noItemsSelected">Aucun élément sélectionné pour la comparaison</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="comparisonChartContainer" class="hidden relative">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h3 class="text-md font-medium text-gray-900">Graphique de comparaison</h3>
|
|
<button class="export-btn bg-green-500 hover:bg-green-700 text-white text-sm py-1 px-2 rounded" data-chart="chartComparison">
|
|
Exporter
|
|
</button>
|
|
</div>
|
|
<canvas id="chartComparison" height="400"></canvas>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
</main>
|
|
|
|
<!-- JavaScript pour les graphiques et l'interactivité -->
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// ========== DONNÉES ==========
|
|
// Données pour les graphiques
|
|
const bulletinData = <?php echo json_encode($data['relevé']); ?>;
|
|
const ueColors = <?php echo json_encode($ue_colors); ?>;
|
|
|
|
// ========== FONCTION UTILITAIRES ==========
|
|
// Fonction pour obtenir une couleur par défaut
|
|
function getDefaultColor(index) {
|
|
const colors = [
|
|
'#8884d8', '#82ca9d', '#ffc658', '#ff8042', '#a4de6c',
|
|
'#d0ed57', '#83a6ed', '#8dd1e1', '#82ca9d', '#e67c7c'
|
|
];
|
|
return colors[index % colors.length];
|
|
}
|
|
|
|
// Fonction pour exporter un graphique
|
|
function exportChart(chartId, filename) {
|
|
const canvas = document.getElementById(chartId);
|
|
if (!canvas) {
|
|
console.error(`Canvas element with ID ${chartId} not found`);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Créer un lien temporaire et déclencher le téléchargement
|
|
canvas.toBlob(function(blob) {
|
|
if (!blob) return;
|
|
|
|
const defaultFilename = chartId + '.png';
|
|
saveAs(blob, filename || defaultFilename);
|
|
});
|
|
} catch (e) {
|
|
console.error("Erreur lors de l'exportation du graphique:", e);
|
|
}
|
|
}
|
|
|
|
// ========== INITIALISATION DES GRAPHIQUES PRINCIPAUX ==========
|
|
|
|
// Graphique de performance par UE (overview)
|
|
const chartUE = document.getElementById('chartUE');
|
|
if (chartUE) {
|
|
const labels = [];
|
|
const studentValues = [];
|
|
const classAvgValues = [];
|
|
const backgroundColors = [];
|
|
|
|
Object.entries(bulletinData.ues).forEach(([ueId, ue], index) => {
|
|
labels.push(ueId);
|
|
studentValues.push(parseFloat(ue.moyenne.value));
|
|
classAvgValues.push(parseFloat(ue.moyenne.moy));
|
|
backgroundColors.push(ueColors[ueId] || getDefaultColor(index));
|
|
});
|
|
|
|
new Chart(chartUE, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [
|
|
{
|
|
label: 'Ta note',
|
|
data: studentValues,
|
|
backgroundColor: backgroundColors
|
|
},
|
|
{
|
|
label: 'Moyenne promo',
|
|
data: classAvgValues,
|
|
backgroundColor: '#82ca9d'
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
max: 20
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Graphique UE pour l'onglet UEs
|
|
const chartUEComparison = document.getElementById('chartUEComparison');
|
|
if (chartUEComparison) {
|
|
const labels = [];
|
|
const studentValues = [];
|
|
const classAvgValues = [];
|
|
const backgroundColors = [];
|
|
|
|
Object.entries(bulletinData.ues).forEach(([ueId, ue], index) => {
|
|
labels.push(ueId);
|
|
studentValues.push(parseFloat(ue.moyenne.value));
|
|
classAvgValues.push(parseFloat(ue.moyenne.moy));
|
|
backgroundColors.push(ueColors[ueId] || getDefaultColor(index));
|
|
});
|
|
|
|
new Chart(chartUEComparison, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [
|
|
{
|
|
label: 'Ta note',
|
|
data: studentValues,
|
|
backgroundColor: backgroundColors
|
|
},
|
|
{
|
|
label: 'Moyenne promo',
|
|
data: classAvgValues,
|
|
backgroundColor: '#82ca9d'
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
max: 20
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Graphique radar des compétences (overview)
|
|
const chartRadar = document.getElementById('chartRadar');
|
|
if (chartRadar) {
|
|
const radarLabels = [];
|
|
const radarValues = [];
|
|
|
|
Object.entries(bulletinData.ues).forEach(([ueId, ue]) => {
|
|
radarLabels.push(ueId);
|
|
radarValues.push(parseFloat(ue.moyenne.value));
|
|
});
|
|
|
|
new Chart(chartRadar, {
|
|
type: 'radar',
|
|
data: {
|
|
labels: radarLabels,
|
|
datasets: [{
|
|
label: 'Notes',
|
|
data: radarValues,
|
|
fill: true,
|
|
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
|
borderColor: 'rgb(54, 162, 235)',
|
|
pointBackgroundColor: 'rgb(54, 162, 235)',
|
|
pointBorderColor: '#fff',
|
|
pointHoverBackgroundColor: '#fff',
|
|
pointHoverBorderColor: 'rgb(54, 162, 235)'
|
|
}]
|
|
},
|
|
options: {
|
|
scales: {
|
|
r: {
|
|
angleLines: {
|
|
display: true
|
|
},
|
|
suggestedMin: 0,
|
|
suggestedMax: 20
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Graphique de distribution des notes (overview)
|
|
const chartDistribution = document.getElementById('chartDistribution');
|
|
if (chartDistribution) {
|
|
// Collecter toutes les notes
|
|
const allGrades = [];
|
|
|
|
Object.values(bulletinData.ressources).forEach(resource => {
|
|
resource.evaluations.forEach(eval => {
|
|
if (eval.note && eval.note.value) {
|
|
allGrades.push(parseFloat(eval.note.value));
|
|
}
|
|
});
|
|
});
|
|
|
|
Object.values(bulletinData.saes).forEach(sae => {
|
|
sae.evaluations.forEach(eval => {
|
|
if (eval.note && eval.note.value) {
|
|
allGrades.push(parseFloat(eval.note.value));
|
|
}
|
|
});
|
|
});
|
|
|
|
// Définir les tranches
|
|
const ranges = [
|
|
{ name: '0-5', min: 0, max: 5, count: 0 },
|
|
{ name: '5-10', min: 5, max: 10, count: 0 },
|
|
{ name: '10-12', min: 10, max: 12, count: 0 },
|
|
{ name: '12-14', min: 12, max: 14, count: 0 },
|
|
{ name: '14-16', min: 14, max: 16, count: 0 },
|
|
{ name: '16-18', min: 16, max: 18, count: 0 },
|
|
{ name: '18-20', min: 18, max: 21, count: 0 }
|
|
];
|
|
|
|
// Compter les notes dans chaque tranche
|
|
allGrades.forEach(grade => {
|
|
const range = ranges.find(r => grade >= r.min && grade < r.max);
|
|
if (range) range.count++;
|
|
});
|
|
|
|
// Calculer les pourcentages
|
|
ranges.forEach(range => {
|
|
range.percentage = allGrades.length > 0 ? (range.count / allGrades.length) * 100 : 0;
|
|
});
|
|
|
|
new Chart(chartDistribution, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: ranges.map(r => r.name),
|
|
datasets: [{
|
|
label: 'Pourcentage des notes',
|
|
data: ranges.map(r => r.percentage),
|
|
backgroundColor: 'rgba(54, 162, 235, 0.5)'
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
max: 100,
|
|
title: {
|
|
display: true,
|
|
text: 'Pourcentage (%)'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Graphique d'évolution des notes (onglet Evolution)
|
|
const chartEvolution = document.getElementById('chartEvolution');
|
|
if (chartEvolution) {
|
|
// Collecter les évaluations avec date
|
|
const gradesOverTime = [];
|
|
|
|
Object.entries(bulletinData.ressources).forEach(([resourceId, resource]) => {
|
|
resource.evaluations.forEach(eval => {
|
|
if (eval.date) {
|
|
gradesOverTime.push({
|
|
date: new Date(eval.date),
|
|
name: `${resourceId} - ${eval.description}`,
|
|
grade: parseFloat(eval.note.value)
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
Object.entries(bulletinData.saes).forEach(([saeId, sae]) => {
|
|
sae.evaluations.forEach(eval => {
|
|
if (eval.date) {
|
|
gradesOverTime.push({
|
|
date: new Date(eval.date),
|
|
name: `${saeId} - ${eval.description}`,
|
|
grade: parseFloat(eval.note.value)
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// Trier par date
|
|
gradesOverTime.sort((a, b) => a.date - b.date);
|
|
|
|
if (gradesOverTime.length > 0) {
|
|
new Chart(chartEvolution, {
|
|
type: 'line',
|
|
data: {
|
|
labels: gradesOverTime.map(g => g.date.toLocaleDateString('fr-FR')),
|
|
datasets: [{
|
|
label: 'Note',
|
|
data: gradesOverTime.map(g => g.grade),
|
|
borderColor: 'rgb(75, 192, 192)',
|
|
tension: 0.1,
|
|
fill: false,
|
|
pointBackgroundColor: gradesOverTime.map(g =>
|
|
g.grade >= 16 ? 'rgb(34, 197, 94)' : // vert
|
|
g.grade >= 10 ? 'rgb(59, 130, 246)' : // bleu
|
|
'rgb(239, 68, 68)' // rouge
|
|
),
|
|
pointRadius: 6
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
max: 20
|
|
}
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
// Si aucune donnée de date, afficher un message
|
|
chartEvolution.parentElement.innerHTML = '<p class="text-center text-gray-500 py-10">Aucune donnée d\'évolution disponible</p>';
|
|
}
|
|
}
|
|
|
|
// ========== GRAPHIQUES POUR LES RESSOURCES ET SAEs ==========
|
|
|
|
// Créer les graphiques pour les ressources
|
|
Object.entries(bulletinData.ressources).forEach(([resourceId, resource]) => {
|
|
const canvasId = `resource-${resourceId}`;
|
|
const canvas = document.getElementById(canvasId);
|
|
|
|
if (canvas && resource.evaluations.length > 1) {
|
|
const labels = [];
|
|
const data = [];
|
|
const avgData = [];
|
|
const colors = [];
|
|
|
|
resource.evaluations.forEach(eval => {
|
|
labels.push(eval.description);
|
|
data.push(parseFloat(eval.note.value));
|
|
avgData.push(parseFloat(eval.note.moy));
|
|
colors.push(
|
|
parseFloat(eval.note.value) >= 16 ? 'rgb(34, 197, 94)' : // vert
|
|
parseFloat(eval.note.value) >= 10 ? 'rgb(59, 130, 246)' : // bleu
|
|
'rgb(239, 68, 68)' // rouge
|
|
);
|
|
});
|
|
|
|
new Chart(canvas, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [
|
|
{
|
|
label: 'Ta note',
|
|
data: data,
|
|
backgroundColor: colors
|
|
},
|
|
{
|
|
label: 'Moyenne promo',
|
|
data: avgData,
|
|
backgroundColor: 'rgba(153, 102, 255, 0.5)'
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
max: 20
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
position: 'bottom'
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Créer les graphiques pour les SAEs
|
|
Object.entries(bulletinData.saes).forEach(([saeId, sae]) => {
|
|
const canvasId = `sae-${saeId}`;
|
|
const canvas = document.getElementById(canvasId);
|
|
|
|
if (canvas && sae.evaluations.length > 1) {
|
|
const labels = [];
|
|
const data = [];
|
|
const avgData = [];
|
|
const colors = [];
|
|
|
|
sae.evaluations.forEach(eval => {
|
|
labels.push(eval.description);
|
|
data.push(parseFloat(eval.note.value));
|
|
avgData.push(parseFloat(eval.note.moy));
|
|
colors.push(
|
|
parseFloat(eval.note.value) >= 16 ? 'rgb(34, 197, 94)' : // vert
|
|
parseFloat(eval.note.value) >= 10 ? 'rgb(59, 130, 246)' : // bleu
|
|
'rgb(239, 68, 68)' // rouge
|
|
);
|
|
});
|
|
|
|
new Chart(canvas, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [
|
|
{
|
|
label: 'Ta note',
|
|
data: data,
|
|
backgroundColor: colors
|
|
},
|
|
{
|
|
label: 'Moyenne promo',
|
|
data: avgData,
|
|
backgroundColor: 'rgba(153, 102, 255, 0.5)'
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
max: 20
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
position: 'bottom'
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// ========== INTERACTIVITÉ ==========
|
|
|
|
// Boutons d'exportation généraux
|
|
document.querySelectorAll('.export-btn').forEach(button => {
|
|
const chartId = button.getAttribute('data-chart');
|
|
if (document.getElementById(chartId)) {
|
|
button.addEventListener('click', function() {
|
|
exportChart(chartId, chartId + '.png');
|
|
});
|
|
} else {
|
|
// Si le graphique n'existe pas, masquer le bouton
|
|
button.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
// Boutons d'exportation pour les graphiques des ressources et SAEs
|
|
document.querySelectorAll('.export-chart-btn').forEach(button => {
|
|
button.addEventListener('click', function() {
|
|
const canvasId = this.getAttribute('data-canvas-id');
|
|
if (document.getElementById(canvasId)) {
|
|
exportChart(canvasId, canvasId + '.png');
|
|
}
|
|
});
|
|
});
|
|
|
|
// Bouton pour exporter tous les graphiques
|
|
const exportAllBtn = document.getElementById('exportAllBtn');
|
|
if (exportAllBtn) {
|
|
exportAllBtn.addEventListener('click', function() {
|
|
alert('Tous les graphiques vont être exportés. Veuillez patienter pendant le téléchargement de chaque fichier.');
|
|
|
|
// Liste des graphiques principaux
|
|
const mainCharts = ['chartUE', 'chartRadar', 'chartDistribution', 'chartUEComparison', 'chartEvolution', 'chartComparison'];
|
|
|
|
// Exporter les graphiques principaux qui existent
|
|
mainCharts.forEach((chartId, index) => {
|
|
if (document.getElementById(chartId)) {
|
|
setTimeout(() => {
|
|
exportChart(chartId, chartId + '.png');
|
|
}, index * 500);
|
|
}
|
|
});
|
|
|
|
// Collecter et exporter les graphiques des ressources
|
|
const resourceCharts = [];
|
|
document.querySelectorAll('[id^="resource-"]').forEach(canvas => {
|
|
resourceCharts.push(canvas.id);
|
|
});
|
|
|
|
resourceCharts.forEach((chartId, index) => {
|
|
setTimeout(() => {
|
|
exportChart(chartId, chartId + '.png');
|
|
}, (mainCharts.length + index) * 500);
|
|
});
|
|
|
|
// Collecter et exporter les graphiques des SAEs
|
|
const saeCharts = [];
|
|
document.querySelectorAll('[id^="sae-"]').forEach(canvas => {
|
|
saeCharts.push(canvas.id);
|
|
});
|
|
|
|
saeCharts.forEach((chartId, index) => {
|
|
setTimeout(() => {
|
|
exportChart(chartId, chartId + '.png');
|
|
}, (mainCharts.length + resourceCharts.length + index) * 500);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Recherche de ressources
|
|
const resourceSearch = document.getElementById('resourceSearch');
|
|
if (resourceSearch) {
|
|
resourceSearch.addEventListener('input', function() {
|
|
const searchTerm = this.value.toLowerCase();
|
|
const resourceCards = document.querySelectorAll('.resource-card');
|
|
|
|
resourceCards.forEach(card => {
|
|
const id = card.dataset.id.toLowerCase();
|
|
const title = card.dataset.title.toLowerCase();
|
|
const isMatch = id.includes(searchTerm) || title.includes(searchTerm);
|
|
|
|
card.style.display = isMatch ? '' : 'none';
|
|
});
|
|
});
|
|
}
|
|
|
|
// Masquer les détails des ressources au chargement (pour économiser de l'espace)
|
|
const resourceDetails = document.querySelectorAll('.resource-details');
|
|
resourceDetails.forEach(detail => {
|
|
detail.classList.add('hidden');
|
|
});
|
|
|
|
// Afficher/masquer les détails au clic sur la carte
|
|
const resourceCards = document.querySelectorAll('.resource-card');
|
|
resourceCards.forEach(card => {
|
|
card.addEventListener('click', function() {
|
|
const details = this.querySelector('.resource-details');
|
|
if (details) {
|
|
details.classList.toggle('hidden');
|
|
}
|
|
});
|
|
});
|
|
|
|
// ========== SYSTÈME DE COMPARAISON ==========
|
|
|
|
// Variables pour le système de comparaison
|
|
let comparisonChart = null;
|
|
const selectedComparisonItems = [];
|
|
|
|
// Éléments du DOM pour la comparaison
|
|
const comparisonSearch = document.getElementById('comparisonSearch');
|
|
const comparisonResults = document.getElementById('comparisonResults');
|
|
const comparisonResultsList = document.getElementById('comparisonResultsList');
|
|
const selectedItems = document.getElementById('selectedItems');
|
|
const noItemsSelected = document.getElementById('noItemsSelected');
|
|
const comparisonChartContainer = document.getElementById('comparisonChartContainer');
|
|
|
|
// Fonction de recherche d'éléments pour la comparaison
|
|
if (comparisonSearch) {
|
|
comparisonSearch.addEventListener('input', function() {
|
|
const searchTerm = this.value.toLowerCase();
|
|
|
|
if (searchTerm.length === 0) {
|
|
comparisonResults.classList.add('hidden');
|
|
return;
|
|
}
|
|
|
|
comparisonResultsList.innerHTML = '';
|
|
const results = [];
|
|
|
|
// Rechercher dans les UEs
|
|
Object.entries(bulletinData.ues).forEach(([ueId, ue]) => {
|
|
if (ueId.toLowerCase().includes(searchTerm) || ue.titre.toLowerCase().includes(searchTerm)) {
|
|
results.push({
|
|
id: ueId,
|
|
name: ueId,
|
|
fullName: ue.titre,
|
|
type: 'UE',
|
|
grade: parseFloat(ue.moyenne.value),
|
|
classAvg: parseFloat(ue.moyenne.moy)
|
|
});
|
|
}
|
|
});
|
|
|
|
// Rechercher dans les ressources
|
|
Object.entries(bulletinData.ressources).forEach(([resourceId, resource]) => {
|
|
if (resourceId.toLowerCase().includes(searchTerm) || resource.titre.toLowerCase().includes(searchTerm)) {
|
|
// Calculer la moyenne des notes
|
|
const grades = resource.evaluations.map(e => parseFloat(e.note.value));
|
|
const avgGrade = grades.length > 0 ? grades.reduce((sum, grade) => sum + grade, 0) / grades.length : 0;
|
|
|
|
results.push({
|
|
id: resourceId,
|
|
name: resourceId,
|
|
fullName: resource.titre,
|
|
type: 'resource',
|
|
grade: avgGrade
|
|
});
|
|
}
|
|
});
|
|
|
|
// Rechercher dans les SAEs
|
|
Object.entries(bulletinData.saes).forEach(([saeId, sae]) => {
|
|
if (saeId.toLowerCase().includes(searchTerm) || sae.titre.toLowerCase().includes(searchTerm)) {
|
|
// Calculer la moyenne des notes
|
|
const grades = sae.evaluations.map(e => parseFloat(e.note.value));
|
|
const avgGrade = grades.length > 0 ? grades.reduce((sum, grade) => sum + grade, 0) / grades.length : 0;
|
|
|
|
results.push({
|
|
id: saeId,
|
|
name: saeId,
|
|
fullName: sae.titre,
|
|
type: 'sae',
|
|
grade: avgGrade
|
|
});
|
|
}
|
|
});
|
|
|
|
// Afficher les résultats
|
|
if (results.length > 0) {
|
|
results.forEach(item => {
|
|
const li = document.createElement('li');
|
|
li.className = 'px-4 py-3 hover:bg-gray-50';
|
|
li.innerHTML = `
|
|
<div class="flex justify-between">
|
|
<div>
|
|
<p class="text-sm font-medium text-gray-900">${item.name} (${item.type})</p>
|
|
<p class="text-sm text-gray-500">${item.fullName}</p>
|
|
</div>
|
|
<button class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-blue-700 bg-blue-100 hover:bg-blue-200 focus:outline-none">
|
|
Ajouter
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
li.querySelector('button').addEventListener('click', function() {
|
|
addComparisonItem(item);
|
|
});
|
|
|
|
comparisonResultsList.appendChild(li);
|
|
});
|
|
|
|
comparisonResults.classList.remove('hidden');
|
|
} else {
|
|
const li = document.createElement('li');
|
|
li.className = 'px-4 py-3 text-sm text-gray-500';
|
|
li.textContent = 'Aucun résultat trouvé';
|
|
comparisonResultsList.appendChild(li);
|
|
|
|
comparisonResults.classList.remove('hidden');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Fonction pour ajouter un élément à la comparaison
|
|
function addComparisonItem(item) {
|
|
// Vérifier si l'élément est déjà sélectionné
|
|
if (selectedComparisonItems.some(i => i.id === item.id && i.type === item.type)) {
|
|
return;
|
|
}
|
|
|
|
selectedComparisonItems.push(item);
|
|
updateComparisonUI();
|
|
}
|
|
|
|
// Fonction pour supprimer un élément de la comparaison
|
|
function removeComparisonItem(itemId, itemType) {
|
|
const index = selectedComparisonItems.findIndex(i => i.id === itemId && i.type === itemType);
|
|
if (index !== -1) {
|
|
selectedComparisonItems.splice(index, 1);
|
|
updateComparisonUI();
|
|
}
|
|
}
|
|
|
|
// Fonction pour mettre à jour l'interface de comparaison
|
|
function updateComparisonUI() {
|
|
if (!selectedItems || !noItemsSelected || !comparisonChartContainer) {
|
|
return;
|
|
}
|
|
|
|
// Masquer le message "aucun élément sélectionné" si nécessaire
|
|
if (selectedComparisonItems.length > 0) {
|
|
noItemsSelected.classList.add('hidden');
|
|
} else {
|
|
noItemsSelected.classList.remove('hidden');
|
|
comparisonChartContainer.classList.add('hidden');
|
|
}
|
|
|
|
// Mettre à jour la liste des éléments sélectionnés
|
|
const itemsContainer = document.createElement('div');
|
|
itemsContainer.className = 'flex flex-wrap';
|
|
|
|
selectedComparisonItems.forEach(item => {
|
|
const itemElement = document.createElement('div');
|
|
itemElement.className = 'flex items-center bg-gray-100 rounded-full py-1 px-3 mr-2 mb-2';
|
|
itemElement.innerHTML = `
|
|
<span class="text-sm font-medium text-gray-900 mr-1">${item.name}</span>
|
|
<button class="h-4 w-4 flex items-center justify-center rounded-full bg-gray-400 text-white hover:bg-gray-600">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
|
|
</svg>
|
|
</button>
|
|
`;
|
|
|
|
itemElement.querySelector('button').addEventListener('click', function() {
|
|
removeComparisonItem(item.id, item.type);
|
|
});
|
|
|
|
itemsContainer.appendChild(itemElement);
|
|
});
|
|
|
|
// Remplacer le contenu du conteneur
|
|
while (selectedItems.firstChild) {
|
|
if (selectedItems.firstChild === noItemsSelected) {
|
|
break;
|
|
}
|
|
selectedItems.removeChild(selectedItems.firstChild);
|
|
}
|
|
|
|
if (selectedComparisonItems.length > 0) {
|
|
selectedItems.insertBefore(itemsContainer, noItemsSelected);
|
|
}
|
|
|
|
// Mettre à jour le graphique de comparaison
|
|
if (selectedComparisonItems.length > 0) {
|
|
updateComparisonChart();
|
|
comparisonChartContainer.classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
// Fonction pour mettre à jour le graphique de comparaison
|
|
function updateComparisonChart() {
|
|
const chartComparison = document.getElementById('chartComparison');
|
|
if (!chartComparison) {
|
|
return;
|
|
}
|
|
|
|
const labels = selectedComparisonItems.map(item => item.name);
|
|
const values = selectedComparisonItems.map(item => item.grade);
|
|
const classAvgs = selectedComparisonItems.map(item => item.classAvg || null);
|
|
const colors = selectedComparisonItems.map((item, index) =>
|
|
item.type === 'UE' ? (ueColors[item.id] || getDefaultColor(index)) : getDefaultColor(index)
|
|
);
|
|
|
|
// Détruire le graphique existant
|
|
if (comparisonChart) {
|
|
comparisonChart.destroy();
|
|
}
|
|
|
|
// Créer le nouveau graphique
|
|
comparisonChart = new Chart(chartComparison, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [
|
|
{
|
|
label: 'Note',
|
|
data: values,
|
|
backgroundColor: colors
|
|
},
|
|
{
|
|
label: 'Moyenne promo',
|
|
data: classAvgs,
|
|
backgroundColor: '#82ca9d'
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
max: 20
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
<?php endif; ?>
|
|
</body>
|
|
</html>
|