index.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. <?php
  2. require_once "libs/fnMain.php";
  3. /**************************
  4. * VALIDATION DES DONNEES
  5. **************************/
  6. $isClassementValid = isset($_GET["classement"]) && (int) $_GET["classement"] !== 0;
  7. $isTriVilleValid = isset($_GET["v"]) && is_array($_GET["v"]) && $_GET["v"] !== [];
  8. $isTriSpeValid = isset($_GET["s"]) && is_array($_GET["s"]) && $_GET["s"] !== [];
  9. $specialitySourceDatasetAbrev = getCsvToArrayKeyValue($_SETTINGS["datasetFolder"]."/liste_specialites_abrev.csv");
  10. $specialitySourceDataset = getCsvToArrayKeyValue($_SETTINGS["datasetFolder"]."/liste_specialites.csv");
  11. $citySourceDataset = getCsvToArrayKeyValue($_SETTINGS["datasetFolder"]."/liste_villes.csv");
  12. /**************************
  13. * Affichage du formulaire
  14. **************************/
  15. ?>
  16. <!doctype html>
  17. <html lang="fr">
  18. <head>
  19. <meta charset="UTF-8">
  20. <meta name="viewport"
  21. content="width=device-width, initial-scale=1.0">
  22. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  23. <title>RangLimier</title>
  24. <link rel="icon" type="image/png" href="assets/favicon.png">
  25. <link rel="stylesheet" href="assets/fonts/material-icons.min.css">
  26. <link rel="stylesheet" href="assets/bootstrap.min.css">
  27. <link rel="stylesheet" href="assets/divers.css">
  28. <!-- Font Awesome icons (free version)-->
  29. <script src="https://use.fontawesome.com/releases/v5.15.1/js/all.js" crossorigin="anonymous"></script>
  30. <!-- Google fonts-->
  31. <link href="https://fonts.googleapis.com/css?family=Montserrat:400,700" rel="stylesheet" type="text/css" />
  32. <link href="https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic" rel="stylesheet" type="text/css" />
  33. </head>
  34. <body>
  35. <div id="navbar">
  36. <nav id="mainNav" class="navbar navbar-expand-lg text-uppercase sticky-top">
  37. <div class="container">
  38. <a href="https://asclepia.io/" class="navbar-brand">
  39. <!-- <div class="text-lowercase" style="font-family: no-name;">asclepia</div> -->
  40. <img alt="Logo Asclepia" src="assets/logo-desktop.svg" decoding="async" style="width: 150%;">
  41. </a>
  42. <button type="button" data-toggle="collapse" data-target="#navbarResponsive"
  43. aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation"
  44. class="navbar-toggler navbar-toggler-right text-uppercase font-weight-bold bg-primary text-white rounded collapsed">
  45. Menu
  46. <svg class="svg-inline--fa fa-bars fa-w-14" aria-hidden="true" focusable="false"
  47. data-prefix="fas" data-icon="bars" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"
  48. data-fa-i2svg="">
  49. <path fill="currentColor"
  50. d="M16 132h416c8.837 0 16-7.163 16-16V76c0-8.837-7.163-16-16-16H16C7.163 60 0 67.163 0 76v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16z"></path>
  51. </svg>
  52. </button>
  53. <div id="navbarResponsive" class="navbar-collapse collapse" style="">
  54. <ul class="navbar-nav ml-auto">
  55. <li class="nav-item mx-0 mx-lg-1"><a class="nav-link py-3 px-0 px-lg-3 rounded js-scroll-trigger" href="https://ranglimier.asclepia.io/">Rang Limier</a></li>
  56. <?php // <!-- <li class="nav-item mx-0 mx-lg-1"><a class="nav-link py-3 px-0 px-lg-3 rounded js-scroll-trigger" href="https://dispo.asclepia.io/">Easy Dispo</a></li> -->
  57. ?>
  58. <li class="nav-item mx-0 mx-lg-1">
  59. <a href="https://app.asclepia.io/login" class="nav-link py-3 px-0 px-lg-3 rounded js-scroll-trigger">Se connecter</a>
  60. </li>
  61. <li class="nav-item mx-0 mx-lg-1">
  62. <a href="https://app.asclepia.io/sign-up" class="nav-link py-3 px-0 px-lg-3 rounded js-scroll-trigger">Découvrir</a>
  63. </li>
  64. </ul>
  65. </div>
  66. </div>
  67. </nav>
  68. </div>
  69. <div id="rangLimier"><div class="container">
  70. <br>
  71. <h1 style="text-align: left;">RangLimier</h1>
  72. <p>Cet outil permet d'apprécier les choix de postes accessibles pour un rang de classement donné sur les années post-R3C (à partir des ECNi 2017).
  73. Il ne réalise pas d'ajustements pour le nombre total d'étudiants au niveau national, dont les tendances sont à l'augmentation.<br>
  74. Sur un seul tableau, qu'il est possible de restreindre à certains DES et/ou certaines subdivisions, est affiché, dans chaque case, le nombre d'années où le poste aurait été accessible.
  75. </p>
  76. <ul>
  77. <li>En rouge, les postes n'ayant jamais été accessible.</li>
  78. <li>En jaune, les postes ayant été accessible sur au moins une fois, mais pas tous les ans.</li>
  79. <li>En vert, les postes ayant été accessible tous les ans.</li>
  80. <li>En remplissage bleu marine, les postes n'ayant jamais été proposés.</li>
  81. </ul>
  82. <p>
  83. Un survol d’une cellule fait apparaître une info-bulle avec le classement auquel l’étudiant aurait pu prétendre pour chaque année. Lorsqu’il n’y a pas d’information dans l’info-bulle pour une année, c’est qu’il n’y avait aucun poste proposé la même année.
  84. </p>
  85. <p>
  86. ⚠ <i> Ces informations sont données à titre indicatif et ne peuvent constituer une quelconque garantie pour les procédures de choix à venir.</i>
  87. <br>
  88. <i>Toutes les données utilisées par cet outil proviennent du site <a href="https://www.cng.sante.fr/">cng.sante.fr</a> et de son application
  89. <a href="https://www.cngsante.fr/chiron/celine/">CELINE</a>. Pour toute information officielle, veuillez vous référer à ces derniers.</i>
  90. </p>
  91. <form action="index.php" method="get" class="d-flex flex-column align-items-left">
  92. <div class="form-group row align-items-center">
  93. <label for="classement" class="col-sm-3">Saisissez un rang de classement : </label>
  94. <div class="col-sm-1">
  95. <input type="text" required name="classement" class="form-control rounded-pill" <?= ($isClassementValid === TRUE) ? 'value='.(int) $_GET["classement"] : "" ?>>
  96. </div>
  97. </div>
  98. <div class="d-flex flex-column flex-sm-row">
  99. <div class="form-group align-item-center" style="margin-right: 10px;">
  100. <label for="triSpe">Restreindre aux DES suivants : </label> <br>
  101. <select name="s[]" id="triSpe" multiple size="7" class="form-control rounded">
  102. <?php
  103. if ($isTriSpeValid) { $selectedSpeId = array_flip($_GET['s']); }
  104. foreach ($specialitySourceDataset as $speId => $speName) : ?>
  105. <option
  106. <?= (isset($selectedSpeId[$speId])) ? "selected " : "" ?>
  107. value="<?= $speId ?>">
  108. <?= $specialitySourceDatasetAbrev[$speId]." - ".$speName ?>
  109. </option>
  110. <?php endforeach; ?>
  111. </select>
  112. </div>
  113. <div class="form-group align-item-center">
  114. <label for="triVille">Restreindre aux subdivisions suivantes : </label> <br>
  115. <select name="v[]" id="triVille" multiple size="7" class="form-control rounded">
  116. <?php
  117. if ($isTriVilleValid) { $selectedVilleId = array_flip($_GET['v']); }
  118. foreach ($citySourceDataset as $cityId => $cityName) :
  119. if ($cityName !== "XX") : ?>
  120. <option
  121. <?= (isset($selectedVilleId[$cityId])) ? "selected " : "" ?>
  122. value="<?= $cityId ?>">
  123. <?= $cityName ?>
  124. </option>
  125. <?php endif; endforeach; ?>
  126. </select>
  127. </div>
  128. </div>
  129. <i>Utiliser Ctrl/Command + Clic pour (dé)sélectionner plusieurs options sur PC/Mac</i> <br>
  130. <button type="submit" class="btn btn-info rounded-pill col-sm-2" >Envoyer !</button>
  131. </form>
  132. </div>
  133. <?php if ($isClassementValid) : ?>
  134. <!--
  135. <hr><h1>Tableau de classement</h1>
  136. <h2>Légende</h2>
  137. <table>
  138. <thead>
  139. <th colspan="5" style="text-align: center;">Légende (sur toutes les années)</th>
  140. </thead>
  141. <tbody>
  142. <tr>
  143. <td class="never-available">Jamais proposé</td>
  144. <td class="no-choice">Aucun choix</td>
  145. <td class="half-choices">Plusieurs choix</td>
  146. <td class="all-choices">Tous les choix</td>
  147. </tr>
  148. </tbody>
  149. </table>
  150. <p>
  151. Chaque nombre dans chaque case correspond au nombre d'années où ce choix était dispo au classement rentré <br>
  152. </p> -->
  153. <?php
  154. $html = '<div class="container"><i>Glissez ce lien dans vos favoris pour retrouver cette recherche rapidement : <a href="'.$_SERVER['REQUEST_URI'].'">Recherche RangLimier</i></a></div>';
  155. $html .= "<br><div class='row justify-content-center'><div class='col-auto'>";
  156. // On va récupérer tous les rangs limited dispo triés par idChoix
  157. //Mysql
  158. $reqRL = $db->query('SELECT idChoix, annee, rangLimite FROM dataset');
  159. //pgsql
  160. // $reqRL = $db->query('SELECT dataset."idChoix", annee, dataset."rangLimite" FROM dataset');
  161. $rangLimites = $reqRL->fetchAll(PDO::FETCH_GROUP|PDO::FETCH_ASSOC);
  162. $inputClassement = (int) $_GET["classement"];
  163. //////////////////////////////////
  164. // Calcul et affichage des données
  165. //////////////////////////////////
  166. // v2.A Conversion du filtre spé/ville de l'utilisateur
  167. // On croise l'array source de données avec les id sélectionnés via le GET (ce qui sécurise les données en plus)
  168. // array_intersect_key(array_source, clés_à_garder)
  169. // Si pas de filtre sélectionné, bah on prend toutes les spé/villes disponibles
  170. $cityDataset = ($isTriVilleValid === TRUE) ? array_intersect_key($citySourceDataset, array_flip($_GET["v"])) : $citySourceDataset;
  171. $specialityDatasetAbrev = ($isTriSpeValid === TRUE) ? array_intersect_key($specialitySourceDatasetAbrev, array_flip($_GET["s"])) : $specialitySourceDatasetAbrev;
  172. $specialityDataset = ($isTriSpeValid === TRUE) ? array_intersect_key($specialitySourceDataset, array_flip($_GET['s'])) : $specialitySourceDataset;
  173. // v2. Calcul des rangs potentiels pour chaque année simulée
  174. // Requete sélectionnant le nombre de personnes ayant choisi avant le classement X
  175. $sql = "SELECT idChoix, COUNT(*) as nb_perso_avant
  176. FROM anciens_choix
  177. WHERE classement < :classement AND annee = :annee
  178. GROUP BY idChoix";
  179. // pgSQL
  180. /* $sql = 'SELECT anciens_choix."idChoix", COUNT(*) as nb_perso_avant
  181. FROM anciens_choix
  182. WHERE classement < :classement AND annee = :annee
  183. GROUP BY anciens_choix."idChoix"'; */
  184. $reqAnciensChoixPourClassementX = $db->prepare($sql);
  185. // Requete sélectionnant le nombre total de personnes à chaque choix
  186. $sql = "SELECT idChoix, COUNT(*) as nb_perso_total
  187. FROM anciens_choix
  188. WHERE annee = :annee
  189. GROUP BY idChoix";
  190. // pgSQL
  191. /* $sql = 'SELECT anciens_choix."idChoix", COUNT(*) as nb_perso_total
  192. FROM anciens_choix
  193. WHERE annee = :annee
  194. GROUP BY anciens_choix."idChoix"'; */
  195. $reqAnciensChoixNbPostes = $db->prepare($sql);
  196. $nbAncienChoixParAn = [];
  197. $nbPersChoixAvant = [];
  198. // On détermine les années simulées : 2017 à année actuelle - 1, et on récupère les données pour les gens avant ce classement
  199. for ($annee = 2017; $annee <= (int) date("Y") - 1 ; $annee++) {
  200. // On formate les données pour le nombre de postes avant le classement (pour l'intégrer à tous les postes après)
  201. $reqAnciensChoixPourClassementX->execute([
  202. "annee" => $annee,
  203. "classement" => $inputClassement
  204. ]);
  205. $nbChoixAnneeI = $reqAnciensChoixPourClassementX->fetchAll();
  206. foreach ($nbChoixAnneeI as $nbPostesItem) {
  207. $nbPersChoixAvant[$annee][$nbPostesItem['idChoix']] = $nbPostesItem['nb_perso_avant'];
  208. }
  209. // On formate les données pour position dans promo et nombres de postes
  210. $reqAnciensChoixNbPostes->execute(["annee" => $annee]);
  211. $nbPostesRaw = $reqAnciensChoixNbPostes->fetchAll();
  212. foreach ($nbPostesRaw as $choix) {
  213. $nbAncienChoixParAn[$choix["idChoix"]][$annee] = [
  214. "position" => (isset ($nbPersChoixAvant[$annee][$choix['idChoix']])) ? $nbPersChoixAvant[$annee][$choix['idChoix']] + 1 : 1,
  215. "nbPostesTotal" => $choix["nb_perso_total"]
  216. ];
  217. }
  218. }
  219. unset($reqAnciensChoixNbPostes, $reqAnciensChoixPourClassementX, $nbPersChoixAvant);
  220. // Affichage de la 1ère ligne = Titre des colonnes + infobulles (via BootstrapJS)
  221. $html .= "<table><thead><tr><th></th>";
  222. foreach ($specialityDatasetAbrev as $speId => $specialityName) {
  223. // $html .= "<th scope='col'>".$specialityName."</th>";
  224. $html .= "<th scope='col' data-toggle='tooltip' data-placement='bottom' title='".$specialitySourceDataset[$speId]."'>".$specialityName."</th>";
  225. }
  226. $html .= "</tr></thead><tbody>";
  227. // Affichage de la suite du tableau avec le calcul
  228. foreach ($cityDataset as $cityId => $cityName) {
  229. // On gère la séparation des différentes régions entre elles (représentées dans le csv villes par les lignes XX_:XX)
  230. // La notation des lignes de séparation est sous forme XX_x car si on laissait XX, la fonction getCsvToArrayKeyValue
  231. // ne retourne la position que pour la 1ère occurence de XX;XX
  232. // La manière de fix ça de manière la plus opti et sale, c'est de faire un substr et de tester les 2 premiers charactères
  233. // si ils sont égaux à XX, on fait une séparation
  234. if (strpos($cityId, "XX") !== FALSE) {
  235. $html .= "<tr><th></th>";
  236. foreach ($specialityDatasetAbrev as $speId => $specialityName) {
  237. $html .= "<th scope='col' data-toggle='tooltip' data-placement='bottom' title='".$specialitySourceDataset[$speId]."'>".$specialityName."</th>";
  238. }
  239. $html .= "</th>";
  240. continue;
  241. }
  242. $html .= "<tr><th scope='row'>".$cityName."</th>";
  243. foreach ($specialityDatasetAbrev AS $specialityId => $specialityName) {
  244. $idChoice = "0".$cityId.$specialityId;
  245. $tooltip = FALSE;
  246. $tooltipContent = "";
  247. if (!isset($rangLimites[$idChoice])) {
  248. $nbAnneePropose = 0;
  249. $nbPossibleAnneePropose = 0;
  250. } else {
  251. $nbAnneePropose = count($rangLimites[$idChoice]);
  252. $nbPossibleAnneePropose = 0;
  253. $tooltip = TRUE; // Autorise l'insertion des données de la tooltip
  254. foreach ($rangLimites[$idChoice] as $rangLimite) {
  255. if ((int) $rangLimite["rangLimite"] >= $inputClassement) {
  256. $nbPossibleAnneePropose++;
  257. // Gestion du contenu de la tooltip
  258. if(isset($nbAncienChoixParAn[$idChoice][$rangLimite["annee"]])) { // Cas où il reste de la place
  259. $temp = $nbAncienChoixParAn[$idChoice][$rangLimite["annee"]];
  260. $tooltipContent .= $rangLimite["annee"]." : ".$temp["position"]."/".$temp["nbPostesTotal"] ."<br>";
  261. } else { // Cas où le choix n'a pas été pourvu (donc y'a personne dans les anciens choix)
  262. $tooltipContent .= $rangLimite["annee"]." : Disponible<br>";
  263. }
  264. } else { // Cas où le choix était impossible cette année
  265. $tooltipContent .= $rangLimite["annee"]." : Non disponible<br>";
  266. }
  267. }
  268. }
  269. // Affichage de la cellule
  270. $html .= "<td ";
  271. $html .= ($tooltip === TRUE) ? "data-toggle='tooltip' data-placement='bottom' data-html='true' " : "";
  272. $html .= " class=\"";
  273. switch (TRUE) {
  274. case ($nbPossibleAnneePropose === 0 && $nbAnneePropose === 0):
  275. $html .= "never-available";
  276. break;
  277. case ($nbPossibleAnneePropose === 0):
  278. $html .= "no-choice";
  279. break;
  280. case ($nbAnneePropose > $nbPossibleAnneePropose && $nbPossibleAnneePropose === 1):
  281. $html .= "last-choice";
  282. break;
  283. case ($nbAnneePropose > $nbPossibleAnneePropose && $nbPossibleAnneePropose !== 1):
  284. $html .= "half-choices";
  285. break;
  286. case ($nbAnneePropose === $nbPossibleAnneePropose):
  287. $html .= "all-choices";
  288. break;
  289. }
  290. $html .= "\" ";
  291. $html .= ($tooltip === TRUE) ? "title='".$tooltipContent."'" : "";
  292. $html .= ">";
  293. if ($nbPossibleAnneePropose > 0) {
  294. $html .= $nbPossibleAnneePropose;
  295. } else if ($nbPossibleAnneePropose === 0 && $nbAnneePropose !== 0) {
  296. $html .= "0";
  297. }
  298. $html .= "</td>";
  299. }
  300. $html .= "</tr>";
  301. }
  302. $html .= "</tbody></table></div></div><br>";
  303. echo $html;
  304. ?>
  305. <table>
  306. </table>
  307. <?php endif;?>
  308. </div>
  309. <div style="text-align: center; font-weight: 700;">
  310. Copyright &copy; 2021 MH2V SAS. Tous droits réservés.
  311. </div>
  312. <!-- Bootstrap core JS-->
  313. <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
  314. <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
  315. <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js"></script>
  316. <script>
  317. $(function () {
  318. $('[data-toggle="tooltip"]').tooltip()
  319. })
  320. </script>
  321. </body>
  322. </html>