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> |