diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index a9ded03c84087cbfcc29fe6cc3e3f06e1368ba14..089469eb039242b0834ac05b77cd7d8be383d09d 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -68,7 +68,13 @@ async function async_post(path, data, success, errors) { const responseData = await response.json(); success(responseData); } else { - throw new Error("Network response was not ok."); + if (response.status == 404) { + response.json().then((data) => { + if (errors) errors(data); + }); + } else { + throw new Error("Network response was not ok."); + } } } catch (error) { console.error(error); @@ -615,7 +621,10 @@ function erreurModuleImpl(message) { openAlertModal("Sélection du module", content); } - if (message == "L'étudiant n'est pas inscrit au module") { + if ( + message == "L'étudiant n'est pas inscrit au module" || + message == "param 'moduleimpl_id': etud non inscrit" + ) { const HTML = ` <p>Attention, l'étudiant n'est pas inscrit à ce module.</p> <p>Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.</p> @@ -822,7 +831,7 @@ function dateCouranteEstTravaillee() { const nouvelleDate = retourJourTravail(date); $("#date").datepicker("setDate", nouvelleDate); let msg = "Le jour sélectionné"; - if ((new Date()).format("YYYY-MM-DD") == date.format("YYYY-MM-DD")) { + if (new Date().format("YYYY-MM-DD") == date.format("YYYY-MM-DD")) { msg = "Aujourd'hui"; } const att = document.createTextNode( diff --git a/app/templates/assiduites/pages/signal_assiduites_hebdo.j2 b/app/templates/assiduites/pages/signal_assiduites_hebdo.j2 index 5b2a7f28f32ad6d2b7727a63f097890d6a5df30b..09a0e750b159c37acccaab4f0d82235e9b22928e 100644 --- a/app/templates/assiduites/pages/signal_assiduites_hebdo.j2 +++ b/app/templates/assiduites/pages/signal_assiduites_hebdo.j2 @@ -108,6 +108,11 @@ background-color: var(--color-conflit); } + .conflit_calendar{ + font-size: 1.5em; + cursor: pointer; + } + </style> @@ -185,6 +190,9 @@ <script> + const readonly = "{{readonly | safe}}" == "True"; + const non_present = "{{non_present | safe}}" == "True"; + const etuds = [ {% for etud in etudiants %} { @@ -231,6 +239,8 @@ date_fin: fin.toFakeIso(), } + let cancelEvent = false; + if (assiduite_id != "") { if (same) { // Suppression @@ -243,13 +253,13 @@ td.setAttribute("assiduite_id", ""); } else { console.error(data.errors["0"].message); + cancelEvent = true; erreurModuleImpl(data.errors["0"].message); - } }, (error) => { console.error("Erreur lors de la suppression de l'assiduité", error); - + cancelEvent = true; } ); } else { @@ -262,6 +272,8 @@ }, (error) => { console.error("Erreur lors de la modification de l'assiduité", error); + cancelEvent = true; + erreurModuleImpl(error.message); } ); } @@ -278,7 +290,7 @@ } else { console.error(data.errors["0"].message); erreurModuleImpl(data.errors["0"].message); - + cancelEvent = true; } }, (error) => { @@ -288,6 +300,8 @@ ); } + return cancelEvent; + } async function recupAssiduitesHebdo(callback) { @@ -324,6 +338,14 @@ function updateTable(assiduites) { + const img_conflit = ` + <a + class="conflit_calendar" + title="Des assiduités existent déjà pour cette période. Cliquez ici pour voir le calendrier de l'assiduité de l'étudiant" + data-tooltip + target="_blank" + >📅</a>` + // Suppression existant document.querySelectorAll("td.btns").forEach((el) => { el.remove(); @@ -395,14 +417,17 @@ // 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"> - ` + let boutons = ` + <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"> + ` + + if (!non_present) { + boutons = `<input type="checkbox" name="matin-${etudid}" id="matin-${etudid}" + class="rbtn present" value="present">`+boutons; + } // matin tdMatin.innerHTML = boutons @@ -417,13 +442,15 @@ if (deb.isSame(morningPeriod.deb, "minutes") && fin.isSame(morningPeriod.fin, "minutes")) { let etat = assi.etat.toLowerCase(); - tdMatin.querySelector(`[value="${etat}"]`).checked = true; + const input = tdMatin.querySelector(`[value="${etat}"]`) + if (input) { + input.checked = true; + } tdMatin.setAttribute("assiduite_id", assi.assiduite_id); } else { - tdMatin.innerHTML = "" + tdMatin.innerHTML = img_conflit; + tdMatin.querySelector(".conflit_calendar").href = `calendrier_assi_etud?etudid=${etudid}`; tdMatin.classList.add("conflit"); - tdMatin.title = "Des assiduités existent déjà pour cette période" - tdMatin.setAttribute("data-tooltip", ""); } } @@ -440,13 +467,15 @@ if (deb.isSame(afternoonPeriod.deb, "minutes") && fin.isSame(afternoonPeriod.fin, "minutes")) { let etat = assi.etat.toLowerCase(); - tdApresmidi.querySelector(`[value="${etat}"]`).checked = true; + const input = tdApresmidi.querySelector(`[value="${etat}"]`) + if (input) { + input.checked = true; + } tdApresmidi.setAttribute("assiduite_id", assi.assiduite_id); } else { - tdApresmidi.innerHTML = "" + tdApresmidi.innerHTML = img_conflit; + tdApresmidi.querySelector(".conflit_calendar").href = `calendrier_assi_etud?etudid=${etudid}`; tdApresmidi.classList.add("conflit"); - tdApresmidi.title = "Des assiduités existent déjà pour cette période" - tdApresmidi.setAttribute("data-tooltip", ""); } } @@ -454,16 +483,29 @@ } document.querySelectorAll("td .rbtn").forEach((el) => { - el.addEventListener("click", (e) => { + el.addEventListener("click", async (e) => { + + if (readonly) { + e.preventDefault(); + return; + } + let target = e.target; let parent = target.parentElement; + + let isCancelled = await actionButton(target, !target.checked); + if (isCancelled) { + e.preventDefault(); + target.checked = !target.checked; + return; + } + let inputs = parent.querySelectorAll(".rbtn"); inputs.forEach((input) => { if (input != target) { input.checked = false; } }); - actionButton(target, !target.checked); }); }); @@ -507,10 +549,109 @@ 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(); + let tds = document.querySelectorAll(`td[day="${day}"][time="${time}"]`); + const real_time = time == "am" ? "matin" : "apresmidi"; + const assi = { + etat: "present", + moduleimpl_id: document.getElementById("moduleimpl_select").value, + date_debut: new Date(days[day].date.format('YYYY-MM-DD') + "T" + temps[real_time].debut).toFakeIso(), + date_fin: new Date(days[day].date.format('YYYY-MM-DD') + "T" + temps[real_time].fin).toFakeIso(), + } + + let toCreate = []; // [{etudid:<int>}] + let toEdit = [];// [{etudid:<int>, assiduite_id:<int>}] + + tds.forEach((td) => { + // on ne touche pas aux conflits + if (td.classList.contains("conflit")) { + return; + } + + const tr = td.parentElement; + const etudid = Number(tr.getAttribute("etudid")); + + const assiduite_id = td.getAttribute("assiduite_id"); + if (assiduite_id == "") { + toCreate.push({ etudid: etudid }); + } else { + toEdit.push({ etudid: etudid, assiduite_id: Number(assiduite_id) }); + } + }) + + // Création + toCreate = toCreate.map((el) => { + return { + ...assi, + etudid: el.etudid, + } + }); + + // Modification + toEdit = toEdit.map((el) => { + return { + ...assi, + etudid: el.etudid, + assiduite_id: el.assiduite_id, + } }); + + // Appel API + let counts = { + create: toCreate.length, + edit: toEdit.length + } + const promiseCreate = async_post( + `../../api/assiduites/create`, + toCreate, + async (data) => { + if (data.errors.length > 0) { + console.error(data.errors); + data.errors.forEach((err) => { + let obj = toCreate[err.indice]; + let etu = etuds.find((el) => el.id == obj.etudid); + + const text = document.createTextNode(`Erreur pour ${etu.nom} ${etu.prenom} : ${err.message}`); + const toast = generateToast(text, "var(--color-error)", 10); + pushToast(toast); + }); + } + counts.create = data.success.length; + }, + (error) => { + console.error("Erreur lors de la création de l'assiduité", error); + } + ); + const promiseEdit = async_post( + `../../api/assiduites/edit`, + toEdit, + async (data) => { + if (data.errors.length > 0) { + console.error(data.errors); + data.errors.forEach((err) => { + let obj = toEdit[err.indice]; + let etu = etuds.find((el) => el.id == obj.etudid); + + const text = document.createTextNode(`Erreur pour ${etu.nom} ${etu.prenom} : ${err.message}`); + const toast = generateToast(text, "var(--color-error)"); + pushToast(toast); + }); + } + counts.edit = data.success.length; + }, + (error) => { + console.error("Erreur lors de l'édition de l'assiduité", error); + } + ); + + // Affiche un loader + afficheLoader(); + + Promise.all([promiseCreate, promiseEdit]).then(async () => { + retirerLoader(); + await recupAssiduitesHebdo(updateTable); + envoiToastTous("present", counts.create + counts.edit); + }); + } </script> @@ -654,6 +795,15 @@ document.addEventListener("DOMContentLoaded", ()=>{ 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> +{% if readonly %} +<h4 + title="Vous n'avez pas les permissions nécessaires afin de modifier les assiduités" + data-tooltip +> + Ouvert en mode <span class="rouge">lecture seule</span>. +</h4> + +{% endif %} <table id="table"> <thead> <tr class="premier"> @@ -676,8 +826,9 @@ document.addEventListener("DOMContentLoaded", ()=>{ {% endif %} {% endfor %} </tr> + {% if not readonly and not non_present %} <tr> - {# Ne pas afficher si preference "non presences" #} + {# Ne pas afficher si preference "non presences" / "readonly" #} <th></th> {% for jour in hebdo_jours %} {% if not jour[0] or jour[1][0] not in ['Samedi', 'Dimanche'] %} @@ -689,13 +840,13 @@ document.addEventListener("DOMContentLoaded", ()=>{ </th> {% endif %} {% endfor %} - </tr> + {% endif %} </thead> <tbody> {% for etud in etudiants %} <tr etudid="{{etud.etudid}}" id="row-{{etud.etudid}}"> - <td class="etudinfo" id="etud-{{etud.etudid}}">{{ etud.nomprenom }}</td> + <td class="etudinfo" id="etud-{{etud.etudid}}">{{ etud.nom_prenom() }}</td> {# Sera rempli en JS #} {# Ne pas afficher bouton présent si pref "non présences" #} {# <td> diff --git a/app/views/assiduites.py b/app/views/assiduites.py index f84330b6404a57fb10982370c770d886f9585ed0..99ce6ab6f62cbc1d5c0900e807dff6fd7f3c6467 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -2070,8 +2070,6 @@ def signal_assiduites_hebdo(): grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>" ) - # TODO vérif perm AbsChange -> readonly - # Gestion des jours jours: dict[str, list[str]] = { "lun": [ @@ -2113,12 +2111,19 @@ def signal_assiduites_hebdo(): return render_template( "assiduites/pages/signal_assiduites_hebdo.j2", + title="Assiduité: saisie hebdomadaire", gr=gr_tit, etudiants=etudiants, moduleimpl_select=_module_selector( formsemestre=formsemestre, moduleimpl_id=moduleimpl_id ), hebdo_jours=hebdo_jours, + readonly=not current_user.has_permission(Permission.AbsChange), + non_present=sco_preferences.get_preference( + "non_present", + formsemestre_id=formsemestre_id, + dept_id=g.scodoc_dept_id, + ), )