From 0f1bd7583863865a10d04e58b65e81873fe4ef8c Mon Sep 17 00:00:00 2001 From: Moncef STITI <stiti@noreply.grond.iut-fbleau.fr> Date: Thu, 27 Mar 2025 09:26:25 +0100 Subject: [PATCH] Ajout d'un simple fichier .php pour le site --- index.php | 1936 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1936 insertions(+) create mode 100644 index.php diff --git a/index.php b/index.php new file mode 100644 index 0000000..f0b55da --- /dev/null +++ b/index.php @@ -0,0 +1,1936 @@ +<?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> \ No newline at end of file