From d73b92500672b7c93422f1930ab693606f8a0286 Mon Sep 17 00:00:00 2001 From: Iziram <matthias.hartmann@iziram.fr> Date: Tue, 28 May 2024 20:07:25 +0200 Subject: [PATCH] =?UTF-8?q?Assiduit=C3=A9=20:=20signal=5Fassiduites=5Fhedb?= =?UTF-8?q?o=20:=20v1=20OK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/js/date_utils.js | 20 + .../pages/signal_assiduites_hebdo.j2 | 445 ++++++++++++++++-- app/views/assiduites.py | 40 ++ 3 files changed, 462 insertions(+), 43 deletions(-) diff --git a/app/static/js/date_utils.js b/app/static/js/date_utils.js index 0db9d3b0..efdc120e 100644 --- a/app/static/js/date_utils.js +++ b/app/static/js/date_utils.js @@ -430,3 +430,23 @@ class Duration { function hasTimeConflict(period, interval) { return period.deb.isBefore(interval.fin) && period.fin.isAfter(interval.deb); } + +// Fonction auxiliaire pour obtenir le numéro de semaine ISO d'une date donnée +function getISOWeek(date) { + const target = new Date(date.valueOf()); + const dayNr = (date.getUTCDay() + 6) % 7; + target.setUTCDate(target.getUTCDate() - dayNr + 3); + const firstThursday = target.valueOf(); + target.setUTCMonth(0, 1); + if (target.getUTCDay() !== 4) { + target.setUTCMonth(0, 1 + ((4 - target.getUTCDay() + 7) % 7)); + } + return 1 + Math.ceil((firstThursday - target) / 604800000); +} + +// Fonction auxiliaire pour obtenir le nombre de semaines ISO dans une année donnée +function getISOWeeksInYear(year) { + const date = new Date(year, 11, 31); + const week = getISOWeek(date); + return week === 1 ? getISOWeek(new Date(year, 11, 24)) : week; +} diff --git a/app/templates/assiduites/pages/signal_assiduites_hebdo.j2 b/app/templates/assiduites/pages/signal_assiduites_hebdo.j2 index 874f463b..5b2a7f28 100644 --- a/app/templates/assiduites/pages/signal_assiduites_hebdo.j2 +++ b/app/templates/assiduites/pages/signal_assiduites_hebdo.j2 @@ -7,8 +7,9 @@ <style> .rbtn::before { - width: 30px; - height: 30px; + --size: 1.5em; + width: var(--size); + height: var(--size); } .ui-timepicker-container, @@ -62,6 +63,7 @@ width: 100%; max-width: 1600px; position: relative; + table-layout: fixed; } th, @@ -99,6 +101,13 @@ opacity: 0.5; } + .grayed { + filter: brightness(0.5); + } + .conflit { + background-color: var(--color-conflit); + } + </style> @@ -159,6 +168,10 @@ #confirmButton:hover { background-color: var(--color-secondary); } + +.etudinfo{ + text-align: left; +} </style> {% endblock styles %} @@ -172,37 +185,359 @@ <script> + const etuds = [ + {% for etud in etudiants %} + { + id: {{etud.etudid}}, + nom: "{{etud.nom}}", + prenom: "{{etud.prenom}}" + }, + {% endfor %} + ] + + let days = [ + {% for jour in hebdo_jours %} + { + date : new Date(Date.fromFRA("{{jour[1][1]}}")), + visible : "{{not jour[0]}}" == "True", + nom : "{{jour[1][0]}}", + }, + {% endfor %} + ] // [0]=Lundi ... [6]=Dimanche -> à 00h00 + + //Une fonction d'action quand un bouton est cliqué + // 3 possibilités : + // - assiduite_id = null -> créer nv assi avec état du bouton + // - assiduite_id non null et bouton coché == etat assi -> suppression de l'assiduité + // - assiduite_id non null et bouton coché != etat assi -> modification de l'assiduité + async function actionButton(btn, same = false) { + let td = btn.parentElement; + let tr = td.parentElement; + let etudid = tr.getAttribute("etudid"); + let etud = etuds.find((etud) => etud.id == etudid); + let etat = btn.value; + let assiduite_id = td.getAttribute("assiduite_id"); + let dayInfo = [td.getAttribute("day"), td.getAttribute("time")]// [0]=[0..6] [1]=am/pm + let day = days[dayInfo[0]].date; + dayInfo[1] = dayInfo[1] == "am" ? "matin" : "apresmidi"; + let deb = new Date(day.format('YYYY-MM-DD') + "T" + temps[dayInfo[1]].debut); + let fin = new Date(day.format('YYYY-MM-DD') + "T" + temps[dayInfo[1]].fin); + + const assi = { + etudid: etudid, + etat: etat, + moduleimpl_id: document.getElementById("moduleimpl_select").value, + date_debut: deb.toFakeIso(), + date_fin: fin.toFakeIso(), + } + + if (assiduite_id != "") { + if (same) { + // Suppression + await async_post( + `../../api/assiduite/delete`, + [assiduite_id], + (data) => { + if (data.success.length > 0) { + envoiToastEtudiant("remove", etud); + td.setAttribute("assiduite_id", ""); + } else { + console.error(data.errors["0"].message); + erreurModuleImpl(data.errors["0"].message); + + } + }, + (error) => { + console.error("Erreur lors de la suppression de l'assiduité", error); + + } + ); + } else { + // Modification + await async_post( + `../../api/assiduite/${assiduite_id}/edit`, + assi, + (data) => { + envoiToastEtudiant(etat, etud); + }, + (error) => { + console.error("Erreur lors de la modification de l'assiduité", error); + } + ); + } + } else { + // Création + await async_post( + `../../api/assiduite/${etud.id}/create`, + [assi], + (data) => { + if (data.success.length > 0) { + envoiToastEtudiant(etat, etud); + //mise à jour de l'assiduité_id dans le td + td.setAttribute("assiduite_id", data.success["0"].message.assiduite_id); + } else { + console.error(data.errors["0"].message); + erreurModuleImpl(data.errors["0"].message); + + } + }, + (error) => { + console.error("Erreur lors de la création de l'assiduité", error); + + } + ); + } + + } + + async function recupAssiduitesHebdo(callback) { + const etudIds = etuds.map((etud) => etud.id).join(","); + const date_debut = days[0].date.startOf("day").format("YYYY-MM-DDTHH:mm"); + const date_fin = days[6].date.endOf("day").format("YYYY-MM-DDTHH:mm"); + + url = + `../../api/assiduites/group/query?date_debut=${date_debut}` + + `&date_fin=${date_fin}&etudids=${etudIds}&with_justifs`; + + await fetch(url) + .then((res) => { + if (!res.ok) { + throw new Error("Network response was not ok"); + } + return res.json(); + }) + .then((data) => { + let assiduites = [] + Object.keys(data).forEach((etudid) => { + assiduites.push(...data[etudid]); + }); + callback(assiduites); + }) + .catch((error) => + console.error( + "There has been a problem with your fetch operation:", + error + ) + ); + } + + + function updateTable(assiduites) { + + // Suppression existant + document.querySelectorAll("td.btns").forEach((el) => { + el.remove(); + }); + + for (let i = 0; i < days.length; i++) { + let day = days[i].date; + + let morningPeriod = { + deb: new Date(day.format('YYYY-MM-DD') + "T" + temps.matin.debut), + fin: new Date(day.format('YYYY-MM-DD') + "T" + temps.matin.fin), + } + let afternoonPeriod = { + deb: (new Date(day.format('YYYY-MM-DD') + "T" + temps.apresmidi.debut)), + fin: new Date(day.format('YYYY-MM-DD') + "T" + temps.apresmidi.fin), + } + const assiduitesByDay = { + matin: assiduites.filter((assi) => { + const period = { + deb: new Date(assi.date_debut), + fin: new Date(assi.date_fin) + } + return hasTimeConflict(period, morningPeriod); + }), + apresmidi: assiduites.filter((assi) => { + const period = { + deb: new Date(assi.date_debut), + fin: new Date(assi.date_fin) + } + return hasTimeConflict(period, afternoonPeriod); + }) + }; + + // Récupération des tr étudiants + let trs = document.querySelectorAll("tr[etudid]"); + + trs.forEach((tr) => { + let etudid = tr.getAttribute("etudid"); + + if (!days[i].visible && i >= 5) { + return; + } else if (!days[i].visible) { + tr.insertAdjacentHTML("beforeend", "<td class='grayed btns' colspan='2'></td>"); + return; + } + + let etudAssiMorning = assiduitesByDay.matin.filter((a) => { + return a.etudid == etudid; + }); + let etudAssiAfternoon = assiduitesByDay.apresmidi.filter((a) => { + return a.etudid == etudid; + }); + + // Créations des boutons + // matin + let tdMatin = document.createElement("td"); + tdMatin.classList.add("btns"); + tdMatin.setAttribute("day", i); + tdMatin.setAttribute("time", "am"); + + tr.appendChild(tdMatin); + + // après-midi + let tdApresmidi = document.createElement("td"); + tdApresmidi.classList.add("btns"); + tdApresmidi.setAttribute("day", i); + tdApresmidi.setAttribute("time", "pm"); + tr.appendChild(tdApresmidi); + + + // Peuplement des boutons en fonction des assiduités + boutons = ` + <input type="checkbox" name="matin-${etudid}" id="matin-${etudid}" + class="rbtn present" value="present"> + <input type="checkbox" name="matin-${etudid}" id="matin-${etudid}" + class="rbtn retard" value="retard"> + <input type="checkbox" name="matin-${etudid}" id="matin-${etudid}" + class="rbtn absent" value="absent"> + ` + + // matin + tdMatin.innerHTML = boutons + tdMatin.setAttribute("assiduite_id", "") + if (etudAssiMorning.length != 0) { + let assi = etudAssiMorning[0]; + const deb = new Date(assi.date_debut); + const fin = new Date(assi.date_fin); + + // si dates == periode -> cocher bouton correspondant + // Sinon supprimer boutons et mettre case "rouge" + tooltip + + if (deb.isSame(morningPeriod.deb, "minutes") && fin.isSame(morningPeriod.fin, "minutes")) { + let etat = assi.etat.toLowerCase(); + tdMatin.querySelector(`[value="${etat}"]`).checked = true; + tdMatin.setAttribute("assiduite_id", assi.assiduite_id); + } else { + tdMatin.innerHTML = "" + tdMatin.classList.add("conflit"); + tdMatin.title = "Des assiduités existent déjà pour cette période" + tdMatin.setAttribute("data-tooltip", ""); + } + } + + // après-midi + tdApresmidi.innerHTML = boutons + tdApresmidi.setAttribute("assiduite_id", "") + if (etudAssiAfternoon.length != 0) { + let assi = etudAssiAfternoon[0]; + const deb = new Date(assi.date_debut); + const fin = new Date(assi.date_fin); + + // si dates == periode -> cocher bouton correspondant + // Sinon supprimer boutons et mettre case "rouge" + tooltip + + if (deb.isSame(afternoonPeriod.deb, "minutes") && fin.isSame(afternoonPeriod.fin, "minutes")) { + let etat = assi.etat.toLowerCase(); + tdApresmidi.querySelector(`[value="${etat}"]`).checked = true; + tdApresmidi.setAttribute("assiduite_id", assi.assiduite_id); + } else { + tdApresmidi.innerHTML = "" + tdApresmidi.classList.add("conflit"); + tdApresmidi.title = "Des assiduités existent déjà pour cette période" + tdApresmidi.setAttribute("data-tooltip", ""); + } + } + + }); + } + + document.querySelectorAll("td .rbtn").forEach((el) => { + el.addEventListener("click", (e) => { + let target = e.target; + let parent = target.parentElement; + let inputs = parent.querySelectorAll(".rbtn"); + inputs.forEach((input) => { + if (input != target) { + input.checked = false; + } + }); + actionButton(target, !target.checked); + }); + }); + + enableTooltips("table"); + + } + + // Une fonction pour changer de semaine (précédente ou suivante) + // fait juste un location.href avec les bons paramètres + function changeWeek(prev = false) { + const currentUrl = new URL(window.location.href); // Récupère l'URL actuelle + const params = new URLSearchParams(currentUrl.search); // Récupère les paramètres de l'URL + let currentWeekParam = params.get('week'); + + // Extraire l'année et le numéro de semaine du paramètre de la semaine actuelle + const [year, week] = currentWeekParam.split('-W').map(Number); + + // Calculer la nouvelle semaine et l'année + let newYear = year; + let newWeek = week + (prev ? -1 : 1); + + if (newWeek < 1) { + newYear -= 1; // Passer à l'année précédente + newWeek = getISOWeeksInYear(newYear); // Dernière semaine de l'année précédente + } else if (newWeek > getISOWeeksInYear(newYear)) { + newYear += 1; // Passer à l'année suivante + newWeek = 1; // Première semaine de l'année suivante + } + + // Formater le nouveau paramètre de semaine + const newWeekParam = `${newYear}-W${String(newWeek).padStart(2, '0')}`; + params.set('week', newWeekParam); // Mettre à jour le paramètre 'week' + currentUrl.search = params.toString(); // Mettre à jour les paramètres de l'URL + window.location.href = currentUrl.toString(); // Rediriger vers la nouvelle URL + } + + + + // Une fonction pour gérer le bouton "tout le monde présent" + // coche tous les boutons de la colonne + function allPresent(day, time) { + // Version naive : coche tous les boutons de la colonne + // TODO - Optimiser avec une seule requête API + let inputs = document.querySelectorAll(`td[day="${day}"][time="${time}"] .rbtn[value="present"]`); + inputs.forEach((input) => { + input.click(); + }); + } + +</script> + + +<script> function updateTemps(temps){ let matin = document.getElementById("text-matin"); let apresmidi = document.getElementById("text-apresmidi"); matin.textContent = `${temps.matin.debut} à ${temps.matin.fin}`; apresmidi.textContent = `${temps.apresmidi.debut} à ${temps.apresmidi.fin}`; + + recupAssiduitesHebdo(updateTable); } const temps = { matin: { - debut: "09:00", - fin: "12:00" + debut: "{{ scu.get_assiduites_time_config("assi_morning_time") }}", + fin: "{{ scu.get_assiduites_time_config("assi_lunch_time") }}" }, apresmidi: { - debut: "13:00", - fin: "17:00" + debut: "{{ scu.get_assiduites_time_config("assi_lunch_time") }}", + fin: "{{ scu.get_assiduites_time_config("assi_afternoon_time") }}", } } - document.querySelectorAll(".rbtn").forEach((el)=>{ - el.addEventListener("click", (e)=>{ - let target = e.target; - let parent = target.parentElement; - let inputs = parent.querySelectorAll(".rbtn"); - - inputs.forEach((input)=>{ - if (input != target){ - input.checked = false; - } - }); - }); - }); + document.getElementById("text-matin").addEventListener("click", (e)=>{ e.preventDefault(); @@ -283,10 +618,19 @@ document.addEventListener("DOMContentLoaded", ()=>{ modal.classList.remove("show"); } }); + + + document.querySelectorAll("th .rbtn").forEach((el)=>{ + el.addEventListener("click", (e)=>{ + allPresent(...el.id.split("-")); + e.preventDefault(); + }) + }) }) </script> + {% endblock scripts %} {% block title %} @@ -298,48 +642,63 @@ document.addEventListener("DOMContentLoaded", ()=>{ <h2>Signalement hebdomadaire de l'assiduité {{ gr | safe }}</h2> <br> <div id="actions" class="flex"> - <button>Semaine précédente</button> + <button onclick="changeWeek(true)">Semaine précédente</button> <label for="moduleimpl_select"> Module: {{moduleimpl_select | safe}} </label> - <button>Semaine suivante</button> + <button onclick="changeWeek(false)">Semaine suivante</button> </div> <h3 id="tableau-dates"> Le matin <a href="#" id="text-matin" title="Cliquer pour modifier les horaires">9h à 12h</a> et l'après-midi de <a href="#" id="text-apresmidi" title="Cliquer pour modifier les horaires">13h à 17h</a> </h3> -<table> +<table id="table"> <thead> <tr class="premier"> <th rowspan="2">Étudiants</th> - <th colspan="2">Lundi</th> - <th colspan="2">Mardi</th> - <th colspan="2">Mercredi</th> - <th colspan="2">Jeudi</th> - <th colspan="2">Vendredi</th> + + {% for jour in hebdo_jours %} + + {% if not jour[0] or jour[1][0] not in ['Samedi', 'Dimanche'] %} + <th colspan="2" class="{{'grayed' if jour[0] else ''}}" >{{ jour[1][0] }} {{jour[1][1] }}</th> + {% endif %} + + {% endfor %} </tr> <tr class="second"> - <th>Matin</th> - <th>Après-midi</th> - <th>Matin</th> - <th>Après-midi</th> - <th>Matin</th> - <th>Après-midi</th> - <th>Matin</th> - <th>Après-midi</th> - <th>Matin</th> - <th>Après-midi</th> + {% for jour in hebdo_jours %} + + {% if not jour[0] or jour[1][0] not in ['Samedi', 'Dimanche'] %} + <th class="{{'grayed' if jour[0] else ''}}">Matin</th> + <th class="{{'grayed' if jour[0] else ''}}">Après-midi</th> + {% endif %} + {% endfor %} + </tr> + <tr> + {# Ne pas afficher si preference "non presences" #} + <th></th> + {% for jour in hebdo_jours %} + {% if not jour[0] or jour[1][0] not in ['Samedi', 'Dimanche'] %} + <th class="{{'grayed' if jour[0] else ''}}"> + <input title="Mettre tout le monde présent" data-tooltip type="checkbox" name="" id="{{loop.index - 1}}-am" class="rbtn present" {{'disabled' if jour[0] else ''}}> + </th> + <th class="{{'grayed' if jour[0] else ''}}"> + <input title="Mettre tout le monde présent" data-tooltip type="checkbox" name="" id="{{loop.index - 1}}-pm" class="rbtn present" {{'disabled' if jour[0] else ''}}> + </th> + {% endif %} + {% endfor %} + </tr> </thead> <tbody> {% for etud in etudiants %} - <tr> + <tr etudid="{{etud.etudid}}" id="row-{{etud.etudid}}"> <td class="etudinfo" id="etud-{{etud.etudid}}">{{ etud.nomprenom }}</td> - {# à changer par jour travaillés (sco pref) #} - {% for day in ['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi'] %} - <td> + {# Sera rempli en JS #} + {# Ne pas afficher bouton présent si pref "non présences" #} + {# <td> <input type="checkbox" name="" id="" class="rbtn present"> <input type="checkbox" name="" id="" class="rbtn retard"> <input type="checkbox" name="" id="" class="rbtn absent"> @@ -348,8 +707,7 @@ document.addEventListener("DOMContentLoaded", ()=>{ <input type="checkbox" name="" id="" class="rbtn present"> <input type="checkbox" name="" id="" class="rbtn retard"> <input type="checkbox" name="" id="" class="rbtn absent"> - </td> - {% endfor %} + </td> #} </tr> {% endfor %} </tbody> @@ -373,4 +731,5 @@ document.addEventListener("DOMContentLoaded", ()=>{ </div> {% include "assiduites/widgets/alert.j2" %} +{% include "assiduites/widgets/toast.j2" %} {% endblock app_content %} \ No newline at end of file diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 2dcfec2e..f84330b6 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -2072,6 +2072,45 @@ def signal_assiduites_hebdo(): # TODO vérif perm AbsChange -> readonly + # Gestion des jours + jours: dict[str, list[str]] = { + "lun": [ + "Lundi", + datetime.datetime.strptime(week + "-1", "%G-W%V-%u").strftime("%d/%m/%Y"), + ], + "mar": [ + "Mardi", + datetime.datetime.strptime(week + "-2", "%G-W%V-%u").strftime("%d/%m/%Y"), + ], + "mer": [ + "Mercredi", + datetime.datetime.strptime(week + "-3", "%G-W%V-%u").strftime("%d/%m/%Y"), + ], + "jeu": [ + "Jeudi", + datetime.datetime.strptime(week + "-4", "%G-W%V-%u").strftime("%d/%m/%Y"), + ], + "ven": [ + "Vendredi", + datetime.datetime.strptime(week + "-5", "%G-W%V-%u").strftime("%d/%m/%Y"), + ], + "sam": [ + "Samedi", + datetime.datetime.strptime(week + "-6", "%G-W%V-%u").strftime("%d/%m/%Y"), + ], + "dim": [ + "Dimanche", + datetime.datetime.strptime(week + "-7", "%G-W%V-%u").strftime("%d/%m/%Y"), + ], + } + + non_travail = sco_preferences.get_preference("non_travail") + non_travail = non_travail.replace(" ", "").split(",") + + hebdo_jours: list[tuple[bool, str]] = [] + for key, val in jours.items(): + hebdo_jours.append((key in non_travail, val)) + return render_template( "assiduites/pages/signal_assiduites_hebdo.j2", gr=gr_tit, @@ -2079,6 +2118,7 @@ def signal_assiduites_hebdo(): moduleimpl_select=_module_selector( formsemestre=formsemestre, moduleimpl_id=moduleimpl_id ), + hebdo_jours=hebdo_jours, ) -- GitLab