index.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  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. </a>
  41. <button type="button" data-toggle="collapse" data-target="#navbarResponsive"
  42. aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation"
  43. class="navbar-toggler navbar-toggler-right text-uppercase font-weight-bold bg-primary text-white rounded collapsed">
  44. Menu
  45. <svg class="svg-inline--fa fa-bars fa-w-14" aria-hidden="true" focusable="false"
  46. data-prefix="fas" data-icon="bars" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"
  47. data-fa-i2svg="">
  48. <path fill="currentColor"
  49. 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>
  50. </svg>
  51. </button>
  52. <div id="navbarResponsive" class="navbar-collapse collapse" style="">
  53. <ul class="navbar-nav ml-auto">
  54. <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/">Rangs limites</a></li>
  55. <li class="nav-item mx-0 mx-lg-1">
  56. <a href="https://asclepia.io/utilisateur" class="nav-link py-3 px-0 px-lg-3 rounded js-scroll-trigger">Se connecter</a>
  57. </li>
  58. <li class="nav-item mx-0 mx-lg-1">
  59. <a href="https://asclepia.io/inscription" class="nav-link py-3 px-0 px-lg-3 rounded js-scroll-trigger">Découvrir</a>
  60. </li>
  61. </ul>
  62. </div>
  63. </div>
  64. </nav>
  65. </div>
  66. <div id="rangLimier"><div class="container">
  67. <br>
  68. <h1 style="text-align: left;">RangLimier</h1>
  69. <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 jusqu'aux derniers ECNi).
  70. Il ne réalise pas d'ajustements pour le nombre total d'étudiants au niveau national, qui augmente légèrement chaque année. <br>
  71. Sur un seul tableau, qu'il est possible de restreindre à certains DES et/ou certaines villes, est affiché, dans chaque case, le nombre d'années où le poste aurait été accessible.
  72. </p>
  73. <ul>
  74. <li>En rouge : Jamais accessible à ce classement</li>
  75. <li>En jaune : Accessible sur au moins un an (mais pas toutes les années)</li>
  76. <li>En vert : Accessible toutes les années jusqu'aux derniers ECNi</li>
  77. </ul>
  78. <p>
  79. ⚠ <i> Ces informations sont données à titre indicatif et ne peuvent constituer une quelconque garantie pour les procédures de choix à venir.</i>
  80. <br>
  81. <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
  82. <a href="https://www.cngsante.fr/chiron/celine/">CELINE</a>. Pour toute information officielle, veuillez vous référer à ces derniers.</i>
  83. </p>
  84. <form action="index.php" method="get" class="d-flex flex-column align-items-left">
  85. <div class="form-group row align-items-center">
  86. <label for="classement" class="col-sm-3">Saisissez un rang de classement : </label>
  87. <div class="col-sm-1">
  88. <input type="text" required name="classement" class="form-control rounded-pill" <?= ($isClassementValid === TRUE) ? 'value='.(int) $_GET["classement"] : "" ?>>
  89. </div>
  90. </div>
  91. <div class="d-flex flex-column flex-sm-row">
  92. <div class="form-group align-item-center" style="margin-right: 10px;">
  93. <label for="triSpe">Restreindre aux DES suivants : </label> <br>
  94. <select name="s[]" id="triSpe" multiple size="7" class="form-control rounded">
  95. <?php
  96. if ($isTriSpeValid) { $selectedSpeId = array_flip($_GET['s']); }
  97. foreach ($specialitySourceDataset as $speId => $speName) : ?>
  98. <option
  99. <?= (isset($selectedSpeId[$speId])) ? "selected " : "" ?>
  100. value="<?= $speId ?>">
  101. <?= $specialitySourceDatasetAbrev[$speId]." - ".$speName ?>
  102. </option>
  103. <?php endforeach; ?>
  104. </select>
  105. </div>
  106. <div class="form-group align-item-center">
  107. <label for="triVille">Restreindre aux villes suivantes : </label> <br>
  108. <select name="v[]" id="triVille" multiple size="7" class="form-control rounded">
  109. <?php
  110. if ($isTriVilleValid) { $selectedVilleId = array_flip($_GET['v']); }
  111. foreach ($citySourceDataset as $cityId => $cityName) :
  112. if ($cityName !== "XX") : ?>
  113. <option
  114. <?= (isset($selectedVilleId[$cityId])) ? "selected " : "" ?>
  115. value="<?= $cityId ?>">
  116. <?= $cityName ?>
  117. </option>
  118. <?php endif; endforeach; ?>
  119. </select>
  120. </div>
  121. </div>
  122. <i>Utiliser Ctrl/Command + Clic pour (dé)sélectionner plusieurs options sur PC/Mac</i> <br>
  123. <button type="submit" class="btn btn-info rounded-pill col-sm-2" >Envoyer !</button>
  124. </form>
  125. </div>
  126. <?php if ($isClassementValid) : ?>
  127. <!--
  128. <hr><h1>Tableau de classement</h1>
  129. <h2>Légende</h2>
  130. <table>
  131. <thead>
  132. <th colspan="5" style="text-align: center;">Légende (sur toutes les années)</th>
  133. </thead>
  134. <tbody>
  135. <tr>
  136. <td class="never-available">Jamais proposé</td>
  137. <td class="no-choice">Aucun choix</td>
  138. <td class="half-choices">Plusieurs choix</td>
  139. <td class="all-choices">Tous les choix</td>
  140. </tr>
  141. </tbody>
  142. </table>
  143. <p>
  144. Chaque nombre dans chaque case correspond au nombre d'années où ce choix était dispo au classement rentré <br>
  145. </p> -->
  146. <?php
  147. $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>';
  148. $html .= "<br><div class='row justify-content-center'><div class='col-auto'>";
  149. // On va récupérer tous les rangs limited dispo triés par idChoix
  150. //Mysql
  151. $reqRL = $db->query('SELECT idChoix, annee, rangLimite FROM dataset');
  152. //pgsql
  153. // $reqRL = $db->query('SELECT dataset."idChoix", annee, dataset."rangLimite" FROM dataset');
  154. $rangLimites = $reqRL->fetchAll(PDO::FETCH_GROUP|PDO::FETCH_ASSOC);
  155. $inputClassement = (int) $_GET["classement"];
  156. //////////////////////////////////
  157. // Calcul et affichage des données
  158. //////////////////////////////////
  159. // v2.A Conversion du filtre spé/ville de l'utilisateur
  160. // 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)
  161. // array_intersect_key(array_source, clés_à_garder)
  162. // Si pas de filtre sélectionné, bah on prend toutes les spé/villes disponibles
  163. $cityDataset = ($isTriVilleValid === TRUE) ? array_intersect_key($citySourceDataset, array_flip($_GET["v"])) : $citySourceDataset;
  164. $specialityDatasetAbrev = ($isTriSpeValid === TRUE) ? array_intersect_key($specialitySourceDatasetAbrev, array_flip($_GET["s"])) : $specialitySourceDatasetAbrev;
  165. $specialityDataset = ($isTriSpeValid === TRUE) ? array_intersect_key($specialitySourceDataset, array_flip($_GET['s'])) : $specialitySourceDataset;
  166. // v2. Calcul des rangs potentiels pour chaque année simulée
  167. // Requete sélectionnant le nombre de personnes ayant choisi avant le classement X
  168. $sql = "SELECT idChoix, COUNT(*) as nb_perso_avant
  169. FROM anciens_choix
  170. WHERE classement < :classement AND annee = :annee
  171. GROUP BY idChoix";
  172. // pgSQL
  173. /* $sql = 'SELECT anciens_choix."idChoix", COUNT(*) as nb_perso_avant
  174. FROM anciens_choix
  175. WHERE classement < :classement AND annee = :annee
  176. GROUP BY anciens_choix."idChoix"'; */
  177. $reqAnciensChoixPourClassementX = $db->prepare($sql);
  178. // Requete sélectionnant le nombre total de personnes à chaque choix
  179. $sql = "SELECT idChoix, COUNT(*) as nb_perso_total
  180. FROM anciens_choix
  181. WHERE annee = :annee
  182. GROUP BY idChoix";
  183. // pgSQL
  184. /* $sql = 'SELECT anciens_choix."idChoix", COUNT(*) as nb_perso_total
  185. FROM anciens_choix
  186. WHERE annee = :annee
  187. GROUP BY anciens_choix."idChoix"'; */
  188. $reqAnciensChoixNbPostes = $db->prepare($sql);
  189. $nbAncienChoixParAn = [];
  190. $nbPersChoixAvant = [];
  191. // 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
  192. for ($annee = 2017; $annee <= (int) date("Y") - 1 ; $annee++) {
  193. // On formate les données pour le nombre de postes avant le classement (pour l'intégrer à tous les postes après)
  194. $reqAnciensChoixPourClassementX->execute([
  195. "annee" => $annee,
  196. "classement" => $inputClassement
  197. ]);
  198. $nbChoixAnneeI = $reqAnciensChoixPourClassementX->fetchAll();
  199. foreach ($nbChoixAnneeI as $nbPostesItem) {
  200. $nbPersChoixAvant[$annee][$nbPostesItem['idChoix']] = $nbPostesItem['nb_perso_avant'];
  201. }
  202. // On formate les données pour position dans promo et nombres de postes
  203. $reqAnciensChoixNbPostes->execute(["annee" => $annee]);
  204. $nbPostesRaw = $reqAnciensChoixNbPostes->fetchAll();
  205. foreach ($nbPostesRaw as $choix) {
  206. $nbAncienChoixParAn[$choix["idChoix"]][$annee] = [
  207. "position" => (isset ($nbPersChoixAvant[$annee][$choix['idChoix']])) ? $nbPersChoixAvant[$annee][$choix['idChoix']] + 1 : 1,
  208. "nbPostesTotal" => $choix["nb_perso_total"]
  209. ];
  210. }
  211. }
  212. unset($reqAnciensChoixNbPostes, $reqAnciensChoixPourClassementX, $nbPersChoixAvant);
  213. // Affichage de la 1ère ligne = Titre des colonnes + infobulles (via BootstrapJS)
  214. $html .= "<table><thead><tr><th></th>";
  215. foreach ($specialityDatasetAbrev as $speId => $specialityName) {
  216. // $html .= "<th scope='col'>".$specialityName."</th>";
  217. $html .= "<th scope='col' data-toggle='tooltip' data-placement='bottom' title='".$specialitySourceDataset[$speId]."'>".$specialityName."</th>";
  218. }
  219. $html .= "</tr></thead><tbody>";
  220. // Affichage de la suite du tableau avec le calcul
  221. foreach ($cityDataset as $cityId => $cityName) {
  222. // 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)
  223. // La notation des lignes de séparation est sous forme XX_x car si on laissait XX, la fonction getCsvToArrayKeyValue
  224. // ne retourne la position que pour la 1ère occurence de XX;XX
  225. // 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
  226. // si ils sont égaux à XX, on fait une séparation
  227. if (strpos($cityId, "XX") !== FALSE) {
  228. $html .= "<tr><th></th>";
  229. foreach ($specialityDatasetAbrev as $speId => $specialityName) {
  230. $html .= "<th scope='col' data-toggle='tooltip' data-placement='bottom' title='".$specialitySourceDataset[$speId]."'>".$specialityName."</th>";
  231. }
  232. $html .= "</th>";
  233. continue;
  234. }
  235. $html .= "<tr><th scope='row'>".$cityName."</th>";
  236. foreach ($specialityDatasetAbrev AS $specialityId => $specialityName) {
  237. $idChoice = "0".$cityId.$specialityId;
  238. $tooltip = FALSE;
  239. $tooltipContent = "";
  240. if (!isset($rangLimites[$idChoice])) {
  241. $nbAnneePropose = 0;
  242. $nbPossibleAnneePropose = 0;
  243. } else {
  244. $nbAnneePropose = count($rangLimites[$idChoice]);
  245. $nbPossibleAnneePropose = 0;
  246. $tooltip = TRUE; // Autorise l'insertion des données de la tooltip
  247. foreach ($rangLimites[$idChoice] as $rangLimite) {
  248. if ((int) $rangLimite["rangLimite"] >= $inputClassement) {
  249. $nbPossibleAnneePropose++;
  250. // Gestion du contenu de la tooltip
  251. if(isset($nbAncienChoixParAn[$idChoice][$rangLimite["annee"]])) { // Cas où il reste de la place
  252. $temp = $nbAncienChoixParAn[$idChoice][$rangLimite["annee"]];
  253. $tooltipContent .= $rangLimite["annee"]." : ".$temp["position"]."/".$temp["nbPostesTotal"] ."<br>";
  254. } else { // Cas où le choix n'a pas été pourvu (donc y'a personne dans les anciens choix)
  255. $tooltipContent .= $rangLimite["annee"]." : Non poourvu<br>";
  256. }
  257. } else { // Cas où le choix était impossible cette année
  258. $tooltipContent .= $rangLimite["annee"]." : Impossible. <br>";
  259. }
  260. }
  261. }
  262. // Affichage de la cellule
  263. $html .= "<td ";
  264. $html .= ($tooltip === TRUE) ? "data-toggle='tooltip' data-placement='bottom' data-html='true' " : "";
  265. $html .= " class=\"";
  266. switch (TRUE) {
  267. case ($nbPossibleAnneePropose === 0 && $nbAnneePropose === 0):
  268. $html .= "never-available";
  269. break;
  270. case ($nbPossibleAnneePropose === 0):
  271. $html .= "no-choice";
  272. break;
  273. case ($nbAnneePropose > $nbPossibleAnneePropose && $nbPossibleAnneePropose === 1):
  274. $html .= "last-choice";
  275. break;
  276. case ($nbAnneePropose > $nbPossibleAnneePropose && $nbPossibleAnneePropose !== 1):
  277. $html .= "half-choices";
  278. break;
  279. case ($nbAnneePropose === $nbPossibleAnneePropose):
  280. $html .= "all-choices";
  281. break;
  282. }
  283. $html .= "\" ";
  284. $html .= ($tooltip === TRUE) ? "title='".$tooltipContent."'" : "";
  285. $html .= ">";
  286. if ($nbPossibleAnneePropose > 0) {
  287. $html .= $nbPossibleAnneePropose;
  288. } else if ($nbPossibleAnneePropose === 0 && $nbAnneePropose !== 0) {
  289. $html .= "0";
  290. }
  291. $html .= "</td>";
  292. }
  293. $html .= "</tr>";
  294. }
  295. $html .= "</tbody></table></div></div><br>";
  296. echo $html;
  297. ?>
  298. <table>
  299. </table>
  300. <?php endif;?>
  301. </div>
  302. <div style="text-align: center; font-weight: 700;">
  303. Copyright &copy; 2021 MH2V SAS. Tous droits réservés.
  304. </div>
  305. <!-- Bootstrap core JS-->
  306. <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
  307. <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
  308. <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js"></script>
  309. <script>
  310. $(function () {
  311. $('[data-toggle="tooltip"]').tooltip()
  312. })
  313. </script>
  314. </body>
  315. </html>