diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index 0d12635cfdc10b224a79b31a90256bf1c52453e3..4bcb8396072f605fbbac277c8788ff6c36bb44e9 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -168,6 +168,10 @@ function creerLigneEtudiant(etud, index) { date_fin: null, }; + /**Retourne une liste d'assiduité en conflit avec la période actuelle + * @param {Array} assiduites - Les assiduités de l'étudiant + * @returns {Array} Les assiduités en conflit + */ function recupConflitsAssiduites(assiduites) { const period = getPeriodAsDate(); @@ -182,9 +186,12 @@ function creerLigneEtudiant(etud, index) { ); }); } - + // Pas de conflit en readonly const conflits = readOnly ? [] : recupConflitsAssiduites(etud.assiduites); + // Si il y a des conflits, on prend le premier pour l'afficher + // si les dates de début et de fin sont les mêmes, c'est une édition + // sinon c'est un conflit if (conflits.length > 0) { currentAssiduite = conflits[0]; @@ -200,6 +207,49 @@ function creerLigneEtudiant(etud, index) { : "conflit"; } + // Création de la ligne étudiante en DOM + /* exemple de ligne étudiante + <div class="etud_row" id="etud_row_497"> + <div class="index">1</div> + <div class="name_field"><img src="../../api/etudiant/etudid/497/photo?size=small" alt="Baudin Joseph" class="pdp"><a + class="name_set" href="bilan_etud?etudid=497"> + <h4 class="nom">Baudin</h4> + <h5 class="prenom">Joseph</h5> + </a></div> + <div class="assiduites_bar"> + <div id="prevDateAssi" class="vide"></div> + <div class="mini-timeline"><span class="mini_tick" style="left: 47.5%;">13h</span> + <div class="mini-timeline-block creneau" style="left: 20%; width: 17.5%;"></div> + </div> + </div> + <fieldset class="btns_field single" etudid="497" type="creation" assiduite_id="-1"> + <input + type="checkbox" + value="present" + name="btn_assiduites_1" + id="rbtn_present" + class="rbtn present" + title="present" + > + <input + type="checkbox" + value="retard" + name="btn_assiduites_1" + id="rbtn_retard" + class="rbtn retard" + title="retard" + > + <input + type="checkbox" + value="absent" + name="btn_assiduites_1" + id="rbtn_absent" + class="rbtn absent" + title="absent" + > + </fieldset> + </div> + */ const ligneEtud = document.createElement("div"); ligneEtud.classList.add("etud_row"); if (Object.keys(etudsDefDem).includes(etud.id)) { @@ -388,6 +438,9 @@ async function creerTousLesEtudiants(etuds) { etudsDiv.innerHTML = ""; const moduleImplId = readOnly ? null : $("#moduleimpl_select").val(); const inscriptions = await getInscriptionModule(moduleImplId); + // on trie les étudiants par ordre alphabétique + // et on garde ceux qui sont inscrits au module + // puis pour chaque étudiant on crée une ligne [...etuds.values()] .sort((a, b) => { return a.sort_key > b.sort_key ? 1 : -1; @@ -496,10 +549,9 @@ async function getInscriptionModule(moduleimpl_id) { return inscriptionsModules.get(moduleimpl_id); } - +// Mise à jour de la ligne étudiant async function MiseAJourLigneEtud(etud) { //Récupérer ses assiduités - function RecupAssiduitesEtudiant(etudid) { const date = $("#date").datepicker("getDate"); const date_debut = date.add(-1, "days").format("YYYY-MM-DDTHH:mm"); @@ -527,6 +579,8 @@ async function MiseAJourLigneEtud(etud) { } await RecupAssiduitesEtudiant(etud.id); + // Une fois les assiduités récupérées, on met à jour la ligne étudiant + // on replace l'ancienne ligne par la nouvellement générée const etudRow = document.getElementById(`etud_row_${etud.id}`); if (etudRow == null) return; @@ -540,12 +594,14 @@ async function MiseAJourLigneEtud(etud) { etudRow.replaceWith(ligneEtud); } +// Action appelée lors d'un clic sur un bouton d'assiduité +// Création, édition ou suppression d'une assiduité async function actionAssiduite(etud, etat, type, assiduite = null) { const modimpl_id = $("#moduleimpl_select").val(); if (assiduite && assiduite.etat.toLowerCase() === etat) type = "suppression"; const { deb, fin } = getPeriodAsDate(); - + // génération d'un objet assiduité basique qui sera complété let assiduiteObjet = assiduite ?? { date_debut: deb, date_fin: fin, @@ -554,7 +610,8 @@ async function actionAssiduite(etud, etat, type, assiduite = null) { assiduiteObjet.etat = etat; assiduiteObjet.moduleimpl_id = modimpl_id; - + // En fonction du type d'action on appelle la bonne route + // avec les bonnes valeurs if (type === "creation") { await async_post( `../../api/assiduite/${etud.id}/create`, @@ -606,7 +663,9 @@ async function actionAssiduite(etud, etat, type, assiduite = null) { ); } } - +// Fonction pour afficher un message d'erreur si le module n'est pas renseigné +// ou si l'étudiant n'est pas inscrit au module. +// On donne le message d'erreur d'une requête api et cela affiche le message correspondant function erreurModuleImpl(message) { if (message == "Module non renseigné") { const HTML = ` @@ -635,7 +694,9 @@ function erreurModuleImpl(message) { openAlertModal("Sélection du module", content); } } - +// Fonction pour ajouter en lot une assiduité à tous les étudiants +// Fonctionne uniquement pour créer ou supprimer des assiduités +// Pas d'édition possible function mettreToutLeMonde(etat, el = null) { const lignesEtuds = [...document.querySelectorAll("fieldset.btns_field")]; @@ -709,7 +770,7 @@ function mettreToutLeMonde(etat, el = null) { } envoiToastTous("remove", assiduites_id.length); if (Object.keys(unDeleted).length == 0) return; - + // CAS : des assiduités d'étudiants n'ont pas pu être supprimés let unDeletedEtuds = ` <ul> ${Object.keys(unDeleted) @@ -771,6 +832,7 @@ function mettreToutLeMonde(etat, el = null) { }); } +// Affichage d'un loader (animation jeu pong) function afficheLoader() { const loaderDiv = document.createElement("div"); loaderDiv.id = "loader"; @@ -782,11 +844,13 @@ function afficheLoader() { loaderDiv.appendChild(loader); document.body.appendChild(loaderDiv); } - +// Retrait du loader (animation jeu pong) function retirerLoader() { document.getElementById("loader").remove(); } +// Simplification de l'envoie de toast pour un étudiant +// affiche le nom, le prénom et l'état de l'assiduité avec une couleur spécifique function envoiToastEtudiant(etat, etud) { let etatAffiche; @@ -810,8 +874,10 @@ function envoiToastEtudiant(etat, etud) { pushToast(generateToast(span, getToastColorFromEtat(etat.toUpperCase()), 5)); } - -// TODO commenter toutes les fonctions js +// Fonction pour simplifier l'envoie de toast avec le bouton "mettre tout le monde" +// On donne un etat et un compte et cela affichera le message associé. +// ex : 12 assiduités ont été supprimées +// ex : 15 étudiants ont été mis Absent. function envoiToastTous(etat, count) { const span = document.createElement("span"); let etatAffiche = etat; @@ -840,7 +906,9 @@ function envoiToastTous(etat, count) { pushToast(generateToast(span, getToastColorFromEtat(etat.toUpperCase()), 5)); } - +// Permet de savoir si un jour est travaillé ou pas +// jour : Date +// nonWorkdays : Array[str] => ["mar", "sam", "dim"] function estJourTravail(jour, nonWorkdays) { const d = Intl.DateTimeFormat("fr-FR", { timeZone: SCO_TIMEZONE, @@ -851,6 +919,9 @@ function estJourTravail(jour, nonWorkdays) { return !nonWorkdays.includes(d); } +// Renvoie le dernier jour travaillé disponible. +// par défaut va en arrière (dans le passé) +// si anti == False => va dans le futur function retourJourTravail(date, anti = true) { const jourMiliSecondes = 86400000; // 24 * 3600 * 1000 | H * s * ms let jour = date; @@ -864,12 +935,18 @@ function retourJourTravail(date, anti = true) { } return jour; } - +// Vérifie si la date courante est travaillée +// Si ce n'est pas le cas, on change la date pour le dernier jour travaillé (passé) +// et on affiche une alerte +// (utilise le datepicker #date) function dateCouranteEstTravaillee() { const date = $("#date").datepicker("getDate"); + if (!estJourTravail(date, nonWorkDays)) { + // récupération du jour travaillé le plus proche const nouvelleDate = retourJourTravail(date); $("#date").datepicker("setDate", nouvelleDate); + // Création du message d'alerte let msg = "Le jour sélectionné"; if (new Date().format("YYYY-MM-DD") == date.format("YYYY-MM-DD")) { msg = "Aujourd'hui"; @@ -889,13 +966,15 @@ function dateCouranteEstTravaillee() { )}.` ) ); + // Affichage de l'alerte openAlertModal("Attention", div, "", "#eec660"); return false; } return true; } - +// Fonction pour passer au jour suivant +// anti : bool => si true, on va dans le passé function jourSuivant(anti = false) { let date = $("#date").datepicker("getDate"); diff --git a/app/templates/assiduites/pages/bilan_etud.j2 b/app/templates/assiduites/pages/bilan_etud.j2 index 6bb97053c199b897a2f6d97b20668b84e5d04314..74586c5ff946f00cc083cf238241d8461ee3a4da 100644 --- a/app/templates/assiduites/pages/bilan_etud.j2 +++ b/app/templates/assiduites/pages/bilan_etud.j2 @@ -111,7 +111,13 @@ Bilan assiduité de {{sco.etud.nomprenom}} <script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script> <script> + + // Récupération des statistiques d'assiduité et affichage + // Fonction appelée lors du clic sur le bouton "Actualiser" + // Et au chargement de la page function stats() { + // On prend les dates de début et de fin + // (format DD/MM/YYYY) et on les convertit en Date() const dd_val = document.getElementById('stats_date_debut').value; const df_val = document.getElementById('stats_date_fin').value; let date_debut = new Date(Date.fromFRA(dd_val)); @@ -120,7 +126,7 @@ Bilan assiduité de {{sco.etud.nomprenom}} openAlertModal("Dates invalides", document.createTextNode('Les dates sélectionnées sont invalides')); return; } - + // On met les dates à 00h et 23h59 pour avoir la journée entière date_debut = date_debut.startOf("day") date_fin = date_fin.endOf("day") @@ -128,10 +134,15 @@ Bilan assiduité de {{sco.etud.nomprenom}} openAlertModal("Dates invalides", document.createTextNode('La date de début se situe après la date de fin.')); return; } + // Appel à l'api et affichage des stats sur la page countAssiduites(date_debut.toFakeIso(), date_fin.toFakeIso()) } - + // Appel à l'api pour récupérer les statistiques d'assiduité + // Effectue l'action passée en paramètre sur les données récupérées + // dateDeb : date de début au format ISO + // dateFin : date de fin au format ISO + // action : fonction à appeler sur les données récupérées function getAssiduitesCount(dateDeb, dateFin, action) { const url_api = `../../api/assiduites/${etudid}/count/query?date_debut=${dateDeb}&date_fin=${dateFin}&etat=absent,retard,present&split`; async_get( @@ -142,6 +153,9 @@ Bilan assiduité de {{sco.etud.nomprenom}} } function showStats(data){ + // Initialisation d'un objet contenant les résultats + // Sera mis à jour avec le reste des valeurs dans la suite + // du code const counter = { "present": { "total": data["present"], @@ -155,33 +169,39 @@ Bilan assiduité de {{sco.etud.nomprenom}} "justi": data["absent"]["justifie"], } } - + // Reset du DOM const values = document.querySelector('.stats-values'); values.innerHTML = ""; - + // Pour chaque état d'assiduité (present, retard, absent) Object.keys(counter).forEach((key) => { + + // On créé les éléments HTML qui serviront d'affichage const item = document.createElement('div'); item.classList.add('stats-values-item'); const div = document.createElement('div'); div.classList.add('stats-values-part'); + // Fonction anonyme pour éviter de réécrire tout le temps un test + // Si l'état est "present" alors cela renvoie "" (=> pas de nb justifié) + // Sinon cela renvoie "dont X justifiées" const withJusti = (key, metric) => { if (key == "present") return ""; return ` dont ${counter[key].justi[metric]} justifiées` } - + // HEURE : aroundie à 2 décimales. const heure = document.createElement('span'); heure.textContent = `${counter[key].total.heure.toFixed(2)} heure(s)${withJusti(key, "heure")}`; - + // DEMI-JOURNEE const demi = document.createElement('span'); demi.textContent = `${counter[key].total.demi} demi-journée(s)${withJusti(key, "demi")}`; - + // JOURNEE const jour = document.createElement('span'); jour.textContent = `${counter[key].total.journee} journée(s)${withJusti(key, "journee")}`; - + + // On met à jour le DOM avec les valeurs calculées + // On met l'état en Titre pour chaque partie div.append(jour, demi, heure); - const title = document.createElement('h5'); title.textContent = key.capitalize(); @@ -190,8 +210,10 @@ Bilan assiduité de {{sco.etud.nomprenom}} values.appendChild(item); }); + // On vérifie si l'étudiant a trop d'absences const nbAbs = data["absent"]["non_justifie"][assi_metric]; if (nbAbs > assi_seuil) { + // L'étudiant est au dessus du seuil (défini dans les préférences du département) document.querySelector('.alerte').classList.remove('invisible'); document.querySelector('.alerte p').textContent = `Attention, cet étudiant a trop d'absences ${nbAbs} / ${assi_seuil} (${metriques[assi_metric]})` } else { @@ -202,7 +224,8 @@ Bilan assiduité de {{sco.etud.nomprenom}} function countAssiduites(dateDeb, dateFin) { getAssiduitesCount(dateDeb, dateFin, showStats); } - + // Table de conversion des métriques + // Utilisé pour afficher les valeurs en fonction de la métrique const metriques = { "heure": "H.", "demi": "1/2 J.", @@ -210,7 +233,8 @@ Bilan assiduité de {{sco.etud.nomprenom}} } - + // Récupération des données obligatoires pour les fonctions + // Depuis le contexte de la page (Jinja2) const etudid = {{ sco.etud.id }}; const assi_metric = "{{ assi_metric | safe }}"; const assi_seuil = {{ assi_seuil }}; @@ -218,6 +242,8 @@ Bilan assiduité de {{sco.etud.nomprenom}} const assi_date_debut = "{{date_debut}}"; const assi_date_fin = "{{date_fin}}"; + // Au chargement de la page, on met les dates par défaut + // Et on appelle la fonction stats() pour afficher les stats window.addEventListener('load', () => { document.getElementById('stats_date_fin').value = assi_date_fin; document.getElementById('stats_date_debut').value = assi_date_debut; diff --git a/app/templates/assiduites/pages/calendrier_assi_etud.j2 b/app/templates/assiduites/pages/calendrier_assi_etud.j2 index b4fa38547c7f024f1ae6ca07fb6e750847f7f6ce..33275ba16d1233dd9d0bef4370f627e4a42c1f11 100644 --- a/app/templates/assiduites/pages/calendrier_assi_etud.j2 +++ b/app/templates/assiduites/pages/calendrier_assi_etud.j2 @@ -192,6 +192,8 @@ Calendrier de l'assiduité </style> <script> + // Retourne un object {show_pres: bool, show_reta: bool, mode_demi: bool} + // Correspondant aux options cochées. (voir les checkbox dans le html) function getOptions() { return { "show_pres": document.getElementById("show_pres").checked, @@ -200,31 +202,37 @@ Calendrier de l'assiduité } } - + // Fonction qui recharge la page en fonction des options cochées function updatePage() { + // On génère un Objet URL à partir de l'url actuelle const url = new URL(location.href); const options = getOptions(); + // grace à l'objet URL on peut modifier les paramètres de l'url + // sans devoir parser l'url et la reconstruire url.searchParams.set("annee", document.getElementById('annee').value); url.searchParams.set("mode_demi", options.mode_demi); url.searchParams.set("show_pres", options.show_pres); url.searchParams.set("show_reta", options.show_reta); - + // On change l'url de la page si elle est différente de l'url actuelle if (location.href != url.href) { location.href = url.href } } - + // L'année scolaire par défaut const defAnnee = "{{ annee | safe}}" + // Les années disponibles pour l'étudiant (["2022-2023", "2021-2022", ...]) let annees = {{ annees | safe }} + // On retire les doublons annees = annees.filter((x, i) => annees.indexOf(x) === i) const etudid = {{ sco.etud.id }}; - + // Peuplement du sélecteur d'année scolaire avec les années disponibles const select = document.querySelector('#annee'); annees.forEach((a) => { + // <option value="2022">2022-2023</option> const opt = document.createElement("option"); let a_1 = a.substring(0, 4) opt.value = a_1 + "", - opt.textContent = a + opt.textContent = a if (a_1 === defAnnee) { opt.selected = true; document.querySelector('.annee #label-annee').textContent = `Année scolaire ${a}` @@ -232,13 +240,15 @@ Calendrier de l'assiduité } select.appendChild(opt) }) - + // On ajoute un écouteur d'événement pour le rechargement de la page + // donc effectué sur le sélecteur d'année scolaire et les checkbox document.querySelectorAll('input[type="checkbox"].memo, #annee').forEach(el => { el.addEventListener('change', function () { updatePage(); }) }); - + // On ajoute un click event pour chaque case d'assiduité afin de rediriger + // Sur la page d'édition de l'assiduité document.querySelectorAll('[assi_id]').forEach((el, i) => { el.addEventListener('click', () => { const assi_id = el.getAttribute('assi_id'); diff --git a/app/templates/assiduites/pages/visu_assi_group.j2 b/app/templates/assiduites/pages/visu_assi_group.j2 index 2b99c8a1f156f370132b65943a14ef5fd60a45c7..3dc48d26504d0323edf3b86602629d99c62a813d 100644 --- a/app/templates/assiduites/pages/visu_assi_group.j2 +++ b/app/templates/assiduites/pages/visu_assi_group.j2 @@ -44,10 +44,15 @@ label.stats_checkbox { const date_fin = "{{date_fin}}"; const group_ids = "{{group_ids}}"; + // Changement de la date de début ou de fin des statitiques + // Recharge la page avec les nouvelles dates function stats() { const deb = Date.fromFRA(document.querySelector('#stats_date_debut').value); const fin = Date.fromFRA(document.querySelector('#stats_date_fin').value); - location.href = `visu_assi_group?group_ids=${group_ids}&date_debut=${deb}&date_fin=${fin}`; + let url = new URL(window.location.href); + url.searchParams.set('date_debut', deb); + url.searchParams.set('date_fin', fin); + location.href = url.href; } window.addEventListener('load', () => { diff --git a/app/templates/assiduites/widgets/alert.j2 b/app/templates/assiduites/widgets/alert.j2 index bf1f0bfec6545758ec29e2bc36be3a4a1244ba5a..7aa527ba621f35b7204f877d080f3330d27ab829 100644 --- a/app/templates/assiduites/widgets/alert.j2 +++ b/app/templates/assiduites/widgets/alert.j2 @@ -127,19 +127,29 @@ <script> const alertmodal = document.getElementById("alertModal"); + /** + * openAlertModal + * @param {string} titre // titre du modal + * @param {HTMLElement} contenu // contenu du modal + * @param {string} footer // texte du footer + * @param {string} color // valeur CSS de la couleur de fond + */ function openAlertModal(titre, contenu, footer, color = "var(--color-error)") { + // On affiche le modal alertmodal.classList.add('is-active'); - + // On met à jour les valeurs du modal alertmodal.querySelector('.alertmodal-title').textContent = titre; alertmodal.querySelector('.alertmodal-body').innerHTML = "" alertmodal.querySelector('.alertmodal-body').appendChild(contenu); alertmodal.querySelector('.alertmodal-footer').textContent = footer; + // On met à jour les couleurs de chaque partie du modal const banners = Array.from(alertmodal.querySelectorAll('.alertmodal-footer,.alertmodal-header')) banners.forEach((ban) => { ban.style.backgroundColor = color; }) - + // On ajoute un écouteur d'événement pour fermer le modal + // si on clique en dehors de celui-ci alertmodal.addEventListener('click', (e) => { if (e.target.id == alertmodal.id) { alertmodal.classList.remove('is-active'); @@ -148,9 +158,11 @@ } }) } + // Fonction pour fermer le modal de manière programmatique function closeAlertModal() { alertmodal.classList.remove("is-active") } + // On ajoute un écouteur d'événement pour fermer le modal avec la croix const alertClose = document.querySelector(".alertmodal-close"); alertClose.onclick = function () { closeAlertModal() diff --git a/app/templates/assiduites/widgets/minitimeline.j2 b/app/templates/assiduites/widgets/minitimeline.j2 index 1e2efdeef599e72e0ea36589a9a4538ace72e8b7..feab40fd7ac42a1b5c1363b36356fdf13996cb22 100644 --- a/app/templates/assiduites/widgets/minitimeline.j2 +++ b/app/templates/assiduites/widgets/minitimeline.j2 @@ -32,7 +32,9 @@ }); } - + // Pour chaque assiduité (et pour le créneau) on vient créer un block + // le block est positionné en fonction de l'heure de début et de fin + // et prend une largeur proportionnelle à la durée de l'assiduité array.forEach((assiduité) => { if(assiduité.etat == "CRENEAU" && readOnly) return; let startDate = new Date(Date.removeUTC(assiduité.date_debut)); @@ -57,6 +59,8 @@ block.style.width = `${widthPercentage}%`; if (assiduité.etat != "CRENEAU") { + // Si on clique dessus on veut pouvoir + // mettre à jour la timeline principale et modifier le moduleimpl_select block.addEventListener("click", () => { let deb = startDate.getHours() + startDate.getMinutes() / 60; let fin = endDate.getHours() + endDate.getMinutes() / 60; @@ -90,7 +94,7 @@ return timeline; } - + // Ajout du "13h" sur la mini timeline function setMiniTick(timelineDate, dayStart, dayDuration) { const endDate = timelineDate.clone().startOf("day"); endDate.setHours(13, 0); diff --git a/app/templates/assiduites/widgets/prompt.j2 b/app/templates/assiduites/widgets/prompt.j2 index 008e02901e6664bfddb996dd39d0cd514f00ec12..e483d11e1bd10ade4626d3c535469dd73d4fce83 100644 --- a/app/templates/assiduites/widgets/prompt.j2 +++ b/app/templates/assiduites/widgets/prompt.j2 @@ -157,65 +157,92 @@ </style> <script> + // Récupère l'élément modal par son ID const promptModal = document.getElementById("promptModal"); + + /** + * Ouvre la fenêtre modale avec les paramètres spécifiés. + * @param {string} titre - Le titre de la modale. + * @param {HTMLElement} contenu - Le contenu de la modale. + * @param {Function} success - La fonction à appeler en cas de succès. + * @param {Function} [cancel] - La fonction à appeler en cas d'annulation (optionnelle). + * @param {string} [color="var(--color-error)"] - La couleur de fond des bannières de la modale (optionnelle). + */ function openPromptModal(titre, contenu, success, cancel = () => { }, color = "var(--color-error)") { + // Active la modale en ajoutant une classe promptModal.classList.add('is-active'); + // Met à jour le titre et le contenu de la modale promptModal.querySelector('.promptModal-title').textContent = titre; - promptModal.querySelector('.promptModal-body').innerHTML = "" + promptModal.querySelector('.promptModal-body').innerHTML = ""; promptModal.querySelector('.promptModal-body').appendChild(contenu); - promptModal.querySelector('.promptModal-footer').innerHTML = "" + // Vide le pied de page et ajoute les boutons d'action + promptModal.querySelector('.promptModal-footer').innerHTML = ""; promptModalButtonAction(success, cancel).forEach((btnPrompt) => { - promptModal.querySelector('.promptModal-footer').appendChild(btnPrompt) - }) - + promptModal.querySelector('.promptModal-footer').appendChild(btnPrompt); + }); - const banners = Array.from(promptModal.querySelectorAll('.promptModal-footer,.promptModal-header')) + // Change la couleur de fond des bannières de la modale + const banners = Array.from(promptModal.querySelectorAll('.promptModal-footer, .promptModal-header')); banners.forEach((ban) => { ban.style.backgroundColor = color; - }) + }); + // Ajoute un écouteur d'événement pour fermer la modale en cliquant en dehors promptModal.addEventListener('click', (e) => { if (e.target.id == promptModal.id) { promptModal.classList.remove('is-active'); - promptModal.removeEventListener('click', this) + promptModal.removeEventListener('click', this); } - }) + }); + // Désactive le défilement de la page principale document.body.style.overflow = "hidden"; - } + /** + * Crée les boutons de validation et d'annulation pour la modale. + * @param {Function} success - La fonction à appeler en cas de succès. + * @param {Function} cancel - La fonction à appeler en cas d'annulation. + * @returns {HTMLElement[]} - Les boutons de validation et d'annulation. + */ function promptModalButtonAction(success, cancel) { - const succBtn = document.createElement('button') - succBtn.classList.add("btnPrompt") - succBtn.textContent = "Valider" + const succBtn = document.createElement('button'); + succBtn.classList.add("btnPrompt"); + succBtn.textContent = "Valider"; succBtn.addEventListener('click', () => { const retour = success(closePromptModal); if (retour == null || retour == false || retour == undefined) { closePromptModal(); } - }) - const cancelBtn = document.createElement('button') - cancelBtn.classList.add("btnPrompt") - cancelBtn.textContent = "Annuler" + }); + + const cancelBtn = document.createElement('button'); + cancelBtn.classList.add("btnPrompt"); + cancelBtn.textContent = "Annuler"; cancelBtn.addEventListener('click', () => { cancel(); closePromptModal(); - }) + }); - return [succBtn, cancelBtn] + return [succBtn, cancelBtn]; } + /** + * Ferme la fenêtre modale. + */ function closePromptModal() { - promptModal.classList.remove("is-active") + promptModal.classList.remove("is-active"); document.body.style.overflow = "auto"; } + + // Ajoute un écouteur d'événement pour fermer la modale en cliquant sur le bouton de fermeture const promptClose = document.querySelector(".promptModal-close"); promptClose.onclick = function () { - closePromptModal() - } + closePromptModal(); + }; + </script> {% endblock promptModal %} \ No newline at end of file diff --git a/app/templates/assiduites/widgets/tableau.j2 b/app/templates/assiduites/widgets/tableau.j2 index 1517ebcb9b916b060406e026fcc0a55a53308f3e..a392b7f875e23b5f3d8671ceff83bab66323d795 100644 --- a/app/templates/assiduites/widgets/tableau.j2 +++ b/app/templates/assiduites/widgets/tableau.j2 @@ -154,7 +154,7 @@ <script> - + // Fonction pour mettre à jour l'url avec les options du tableau function updateTableau() { const url = new URL(location.href); const formValues = document.querySelectorAll(".options-tableau *[name]"); @@ -173,7 +173,7 @@ } const total_pages = {{total_pages}}; - + // Fonction pour naviguer entre les pages, modifie le champ n_page de l'url function navigateToPage(pageNumber){ if(pageNumber > total_pages || pageNumber < 1) return; const url = new URL(location.href); @@ -186,7 +186,7 @@ } } - + // Préparation des opérations de trai sur les colonnes "external-sort" window.addEventListener('load', ()=>{ const table_columns = [...document.querySelectorAll('th.external-sort')]; table_columns.forEach((e)=>e.addEventListener('click', ()=>{ diff --git a/app/templates/assiduites/widgets/tableau_actions/details.j2 b/app/templates/assiduites/widgets/tableau_actions/details.j2 deleted file mode 100644 index 7665ee03c069315c50b3de55f80fe8d43850a3bf..0000000000000000000000000000000000000000 --- a/app/templates/assiduites/widgets/tableau_actions/details.j2 +++ /dev/null @@ -1,140 +0,0 @@ -<h2>Détails {{type}} concernant <span class="etudinfo" - id="etudid-{{objet.etudid}}">{{etud.html_link_fiche()|safe}}</span></h2> - -<style> -.info-row { - margin-top: 12px; -} -.info-label { - font-weight: bold; -} -.info-etat { - font-size: 110%; - font-weight: bold; - background-color: rgb(253, 234, 210); - border: 1px solid grey; - border-radius: 4px; - padding: 4px; -} -.info-saisie { - margin-top: 12px; - margin-bottom: 12px; - font-style: italic; -} -</style> - -<div id="informations"> - - <div class="info-saisie"> - <span>Saisie par {{objet.saisie_par}} le {{objet.entry_date}}</span> - </div> - - <div class="info-row"> - <span class="info-label">Période :</span> du <b>{{objet.date_debut}}</b> au <b>{{objet.date_fin}}</b> - </div> - - {% if type == "Assiduité" %} - <div class="info-row"> - <span class="info-label">Module :</span> {{objet.module}} - </div> - {% else %} - {% endif %} - - <div class="info-row"> - {% if type == "Justificatif" %} - <span class="info-label">État du justificatif :</span> - {% else %} - <span class="info-label">État de l'assiduité :</span> - {% endif %} - <span class="info-etat">{{objet.etat}}</span> - - </div> - - <div class="info-row"> - {% if type == "Justificatif" %} - <span class="info-label">Raison:</span> - {% if can_view_justif_detail %} - <span class="text">{{objet.raison or " "}}</span> - {% else %} - <span class="text unauthorized">(cachée)</span> - {% endif %} - {% else %} - <span class="info-label">Description:</span> - {% if objet.description != None %} - <span class="text">{{objet.description}}</span> - {% else %} - <span class="text"></span> - {% endif %} - {% endif %} - </span> - </div> - - {# Affichage des justificatifs si assiduité justifiée #} - {% if type == "Assiduité" and objet.etat != "Présence" %} - <div class="info-row"> - <span class="info-label">Justifiée: </span> - {% if objet.justification.est_just %} - <span class="text">Oui</span> - <div> - {% for justi in objet.justification.justificatifs %} - <a href="{{url_for('assiduites.tableau_assiduite_actions', - type='justificatif', action='details', obj_id=justi.justif_id, scodoc_dept=g.scodoc_dept)}}" - target="_blank" rel="noopener noreferrer">Justificatif du {{justi.date_debut}} au {{justi.date_fin}}</a> - {% endfor %} - </div> - {% else %} - <span class="text fontred">Non</span> - {% endif %} - </div> - {% endif %} - - {# Affichage des assiduités justifiées si justificatif valide #} - {% if type == "Justificatif" and objet.etat == "Valide" %} - <div class="info-row"> - <span class="info-label">Assiduités concernées: </span> - {% if objet.justification.assiduites %} - <ul> - {% for assi in objet.justification.assiduites %} - <li><a href="{{url_for('assiduites.tableau_assiduite_actions', - type='assiduite', action='details', obj_id=assi.assiduite_id, scodoc_dept=g.scodoc_dept) - }}" target="_blank">Assiduité {{assi.etat}} du {{assi.date_debut}} au - {{assi.date_fin}}</a> - </li> - {% endfor %} - </ul> - {% else %} - <span class="text">Aucune</span> - {% endif %} - </div> - {% endif %} - - {# Affichage des fichiers des justificatifs #} - {% if type == "Justificatif"%} - <div class="info-row"> - <span class="info-label">Fichiers enregistrés: </span> - {% if objet.justification.fichiers.total != 0 %} - <div>Total : {{objet.justification.fichiers.total}} </div> - <ul> - {% for filename in objet.justification.fichiers.filenames %} - <li> - <a - href="{{url_for('apiweb.justif_export',justif_id=objet.justif_id, - filename=filename, scodoc_dept=g.scodoc_dept)}}">{{filename}}</a> - </li> - {% endfor %} - {% if not objet.justification.fichiers.filenames %} - <li class="fontred">fichiers non visibles</li> - {% endif %} - </ul> - {% else %} - <span class="text">Aucun</span> - {% endif %} - </div> - {% if current_user.has_permission(sco.Permission.AbsChange) %} - <div><a class="stdlink" href="{{ - url_for('assiduites.edit_justificatif_etud', scodoc_dept=g.scodoc_dept, justif_id=obj_id) - }}">modifier ce justificatif</a> - </div> - {% endif %} - {% endif %} -</div> diff --git a/app/templates/assiduites/widgets/tableau_actions/modifier.j2 b/app/templates/assiduites/widgets/tableau_actions/modifier.j2 deleted file mode 100644 index 4c877fded6ad45f736239dbf305950c8c1ee16cc..0000000000000000000000000000000000000000 --- a/app/templates/assiduites/widgets/tableau_actions/modifier.j2 +++ /dev/null @@ -1,117 +0,0 @@ -<h2>Modifier {{objet_name}} de {{ etud.html_link_fiche() | safe }}</h2> - -{# XXX cette page ne semble plus utile ! remplacée par edit_justificatif_etud #} - -<div> -Actuellement noté{{etud.e}} en <b>{{objet_name|lower()}}</b> du {{objet.date_debut}} au {{objet.date_fin}} -</div> - -<form action="" method="post" enctype="multipart/form-data"> - <input type="hidden" name="obj_id" value="{{obj_id}}"> - <input type="hidden" name="table_url" id="table_url" value=""> - - {% if type == "Assiduité" %} - <input type="hidden" name="obj_type" value="assiduite"> - <legend for="etat">État</legend> - <select name="etat" id="etat"> - <option value="absent">Absent</option> - <option value="retard">Retard</option> - <option value="present">Présent</option> - </select> - - <legend for="moduleimpl_select">Module</legend> - {{moduleimpl | safe}} - - <legend for="description">Description</legend> - <textarea name="description" id="description" cols="50" rows="5">{{objet.description}}</textarea> - - {% else %} - <input type="hidden" name="obj_type" value="justificatif"> - - <legend for="date_debut">Date de début</legend> - <scodoc-datetime name="date_debut" id="date_debut" value="{{objet.real_date_debut}}"></scodoc-datetime> - <legend for="date_fin">Date de fin</legend> - <scodoc-datetime name="date_fin" id="date_fin" value="{{objet.real_date_fin}}"></scodoc-datetime> - - <legend for="etat">État</legend> - <select name="etat" id="etat"> - <option value="valide">Valide</option> - <option value="non_valide">Non Valide</option> - <option value="attente">En Attente</option> - <option value="modifie">Modifié</option> - </select> - - {% if current_user.has_permission(sco.Permission.AbsJustifView) %} - <legend for="raison">Raison</legend> - <textarea name="raison" id="raison" cols="50" rows="5">{{objet.raison}}</textarea> - {% else %} - <div class="unauthorized">(raison non visible ni modifiable)</div> - {% endif %} - - <legend>Fichiers</legend> - - <div class="info-row"> - <label class="info-label">Fichiers enregistrés: </label> - {% if objet.justification.fichiers.total != 0 %} - <div>Total : {{objet.justification.fichiers.total}} </div> - <ul> - {% for filename in objet.justification.fichiers.filenames %} - <li data-id="{{filename}}"> - <a data-file="{{filename}}">❌</a> - <a data-link="" - href="{{url_for('apiweb.justif_export',justif_id=objet.justif_id,filename=filename, scodoc_dept=g.scodoc_dept)}}"><span - data-file="{{filename}}">{{filename}}</span></a> - </li> - {% endfor %} - </ul> - {% else %} - <span class="text">Aucun</span> - {% endif %} - </div> - <br> - <label for="justi_fich">Ajouter des fichiers:</label> - <input type="file" name="justi_fich" id="justi_fich" multiple> - - - {% endif %} - <br> - <br> - <input type="submit" value="Valider"> -</form> - -<script> - function removeFile(element) { - const link = document.querySelector(`*[data-id="${element.getAttribute('data-file')}"] a[data-link] span`); - link?.toggleAttribute("data-remove") - } - - function deleteFiles(justif_id) { - - const filenames = Array.from(document.querySelectorAll("*[data-remove]")).map((el) => el.getAttribute("data-file")) - obj = { - "remove": "list", - "filenames": filenames - } - //faire un POST à l'api justificatifs - } - - window.addEventListener('load', () => { - document.getElementById('etat').value = "{{objet.real_etat}}"; - document.getElementById('table_url').value = document.referrer; - document.querySelectorAll("a[data-file]").forEach((e) => { - e.addEventListener('click', () => { - removeFile(e); - }) - }) - }) -</script> -<style> - [data-remove] { - text-decoration: line-through; - } - - [data-file] { - cursor: pointer; - user-select: none; - } -</style> diff --git a/app/templates/assiduites/widgets/timeline.j2 b/app/templates/assiduites/widgets/timeline.j2 index d7acdbe85bf158976421fb7054f9e0085857f307..eaac2b84591dd162f5ddd1fa0ee726e668128c1a 100644 --- a/app/templates/assiduites/widgets/timeline.j2 +++ b/app/templates/assiduites/widgets/timeline.j2 @@ -27,21 +27,25 @@ let handleMoving = false; + // Création des graduations de la timeline + // On créé des grandes graduations pour les heures + // On créé des petites graduations pour les "tick" function createTicks() { let i = t_start; while (i <= t_end) { + // création d'un tick Heure (grand) const hourTick = document.createElement("div"); hourTick.classList.add("tick", "hour"); hourTick.style.left = `${((i - t_start) / (t_end - t_start)) * 100}%`; timelineContainer.appendChild(hourTick); - + // on ajoute un label pour l'heure (ex : 12:00) const tickLabel = document.createElement("div"); tickLabel.classList.add("tick-label"); tickLabel.style.left = `${((i - t_start) / (t_end - t_start)) * 100}%`; tickLabel.textContent = numberToTime(i); timelineContainer.appendChild(tickLabel); - + // Si on est pas à la fin, on ajoute les graduations intermédiaires if (i < t_end) { let j = Math.floor(i + 1); @@ -49,6 +53,7 @@ i += tick_delay; if (i <= t_end) { + // création d'un tick (petit) const quarterTick = document.createElement("div"); quarterTick.classList.add("tick", "quarter"); quarterTick.style.left = `${computePercentage(i, t_start)}%`; @@ -62,7 +67,8 @@ } } } - + // Convertit un nombre en heure + // ex : 12.5 => "12:30" function numberToTime(num) { const integer = Math.floor(num); const decimal = Math.round((num % 1) * 60); @@ -80,13 +86,12 @@ return int + dec; } - + // Arrondi un nombre au tick le plus proche function snapToQuarter(value) { - - return Math.round(value * tick_time) / tick_time; } - + // Mise à jour des valeurs des timepickers + // En fonction des valeurs de la timeline function updatePeriodTimeLabel() { const values = getPeriodValues(); const deb = numberToTime(values[0]) @@ -102,96 +107,112 @@ } + // Gestion des évènements de la timeline + // - Déplacement des poignées + // - Déplacement de la période function timelineMainEvent(event) { + // Position de départ de l'événement (souris ou tactile) const startX = (event.clientX || event.changedTouches[0].clientX); + // Vérifie si l'événement concerne une poignée de période if (event.target.classList.contains("period-handle")) { + // Initialisation des valeurs de départ const startWidth = parseFloat(periodTimeLine.style.width); const startLeft = parseFloat(periodTimeLine.style.left); const isLeftHandle = event.target.classList.contains("left"); - handleMoving = true + handleMoving = true; + + // Fonction de déplacement de la poignée const onMouseMove = (moveEvent) => { if (!handleMoving) return; + // Calcul du déplacement en pixels const deltaX = (moveEvent.clientX || moveEvent.changedTouches[0].clientX) - startX; const containerWidth = timelineContainer.clientWidth; - const newWidth = - startWidth + ((isLeftHandle ? -deltaX : deltaX) / containerWidth) * 100; + // Calcul de la nouvelle largeur en pourcentage + const newWidth = startWidth + ((isLeftHandle ? -deltaX : deltaX) / containerWidth) * 100; if (isLeftHandle) { + // Si la poignée gauche est déplacée, ajuste également la position gauche const newLeft = startLeft + (deltaX / containerWidth) * 100; adjustPeriodPosition(newLeft, newWidth); } else { adjustPeriodPosition(parseFloat(periodTimeLine.style.left), newWidth); } + // Met à jour l'étiquette de temps de la période updatePeriodTimeLabel(); }; + + // Fonction de relâchement de la souris ou du tactile + // - Alignement des poignées sur les ticks + // - Appel des callbacks + // - Sauvegarde des valeurs dans le local storage + // - Réinitialisation de la variable de déplacement des poignées const mouseUp = () => { snapHandlesToQuarters(); timelineContainer.removeEventListener("mousemove", onMouseMove); handleMoving = false; func_call(); savePeriodInLocalStorage(); + }; - } + // Ajoute les écouteurs d'événement pour le déplacement et le relâchement timelineContainer.addEventListener("mousemove", onMouseMove); timelineContainer.addEventListener("touchmove", onMouseMove); - document.addEventListener( - "mouseup", - mouseUp, - { once: true } - ); - document.addEventListener( - "touchend", - mouseUp, - { once: true } - - ); + document.addEventListener("mouseup", mouseUp, { once: true }); + document.addEventListener("touchend", mouseUp, { once: true }); + + // Vérifie si l'événement concerne la période elle-même } else if (event.target === periodTimeLine) { const startLeft = parseFloat(periodTimeLine.style.left); + // Fonction de déplacement de la période const onMouseMove = (moveEvent) => { if (handleMoving) return; const deltaX = (moveEvent.clientX || moveEvent.changedTouches[0].clientX) - startX; const containerWidth = timelineContainer.clientWidth; + // Calcul de la nouvelle position gauche en pourcentage const newLeft = startLeft + (deltaX / containerWidth) * 100; adjustPeriodPosition(newLeft, parseFloat(periodTimeLine.style.width)); - updatePeriodTimeLabel(); }; + + // Fonction de relâchement de la souris ou du tactile + // - Alignement des poignées sur les ticks + // - Appel des callbacks + // - Sauvegarde des valeurs dans le local storage const mouseUp = () => { snapHandlesToQuarters(); timelineContainer.removeEventListener("mousemove", onMouseMove); func_call(); savePeriodInLocalStorage(); - } + }; + + // Ajoute les écouteurs d'événement pour le déplacement et le relâchement timelineContainer.addEventListener("mousemove", onMouseMove); timelineContainer.addEventListener("touchmove", onMouseMove); - document.addEventListener( - "mouseup", - mouseUp, - { once: true } - ); - document.addEventListener( - "touchend", - mouseUp, - { once: true } - ); + document.addEventListener("mouseup", mouseUp, { once: true }); + document.addEventListener("touchend", mouseUp, { once: true }); } } + let func_call = () => { }; + // Fonction initialisant la timeline + // La fonction "callback" est appelée à chaque modification de la période function setupTimeLine(callback) { func_call = callback; timelineContainer.addEventListener("mousedown", (e) => { timelineMainEvent(e) }); timelineContainer.addEventListener("touchstart", (e) => { timelineMainEvent(e) }); + // Initialisation des timepickers (à gauche de la timeline) + // lors d'un changement, cela met à jour la timeline const updateFromInputs = ()=>{ let deb = $('#deb').val(); let fin = $('#fin').val(); @@ -209,9 +230,11 @@ $('#deb').data('TimePicker').options.change = updateFromInputs; $('#fin').data('TimePicker').options.change = updateFromInputs; + // actualise l'affichage des inputs avec les valeurs de la timeline updatePeriodTimeLabel(); } - + // Ajuste la position de la période en fonction de la nouvelle position et largeur + // Vérifie que la période ne dépasse pas les limites de la timeline function adjustPeriodPosition(newLeft, newWidth) { const snappedLeft = snapToQuarter(newLeft); @@ -224,30 +247,36 @@ periodTimeLine.style.left = `${clampedLeft}%`; periodTimeLine.style.width = `${snappedWidth}%`; } - + // Récupère les valeurs de la période function getPeriodValues() { + // On prend les pourcentages const leftPercentage = parseFloat(periodTimeLine.style.left); const widthPercentage = parseFloat(periodTimeLine.style.width); + // On calcule l'inverse des pourcentages pour obtenir les heures const startHour = (leftPercentage / 100) * (t_end - t_start) + t_start; const endHour = ((leftPercentage + widthPercentage) / 100) * (t_end - t_start) + t_start; - + // On les arrondit aux ticks les plus proches const startValue = snapToQuarter(startHour); const endValue = snapToQuarter(endHour); - + + // on verifie que les valeurs sont bien dans les bornes const computedValues = [Math.max(startValue, t_start), Math.min(t_end, endValue)]; - + + // si les valeurs sont hors des bornes, on les ajuste if (computedValues[0] > t_end || computedValues[1] < t_start) { return [t_start, Math.min(t_end, t_start + period_default)]; } - + // Si la période est trop petite, on l'agrandit artificiellement (il faut au moins 1 tick de largeur) if (computedValues[1] - computedValues[0] <= tick_delay && computedValues[1] < t_end - tick_delay) { computedValues[1] += tick_delay; } return computedValues; } - + // Met à jour les valeurs de la période + // Met à jour l'affichage de la timeline + // Appelle les callbacks associés function setPeriodValues(deb, fin) { if (fin < deb) { throw new RangeError(`le paramètre 'deb' doit être inférieur au paramètre 'fin' ([${deb};${fin}])`) @@ -259,8 +288,8 @@ deb = snapToQuarter(deb); fin = snapToQuarter(fin); - let leftPercentage = (deb - t_start) / (t_end - t_start) * 100; - let widthPercentage = (fin - deb) / (t_end - t_start) * 100; + let leftPercentage = computePercentage(deb, t_start); + let widthPercentage = computePercentage(fin, deb); periodTimeLine.style.left = `${leftPercentage}%`; periodTimeLine.style.width = `${widthPercentage}%`; @@ -269,7 +298,9 @@ func_call(); savePeriodInLocalStorage(); } - + // Aligne les poignées de la période sur les ticks les plus proches + // ex : 12h39 => 12h45 (si les ticks sont à 15min) + // evite aussi les dépassements de la timeline (max et min) function snapHandlesToQuarters() { const periodValues = getPeriodValues(); let lef = Math.min(computePercentage(Math.abs(periodValues[0]), t_start), computePercentage(Math.abs(t_end), tick_delay)); @@ -288,15 +319,20 @@ updatePeriodTimeLabel() } - + // Retourne le pourcentage d'une valeur par rapport à t_start et t_end + // ex : 12h par rapport à 8h et 20h => 25% function computePercentage(a, b) { return ((a - b) / (t_end - t_start)) * 100; } + // Convertit une heure (string) en nombre + // ex : "12:30" => 12.5 function fromTime(time, separator = ":") { const [hours, minutes] = time.split(separator).map((el) => Number(el)) return hours + minutes / 60 } - + // Renvoie les valeurs de la période sous forme de date + // Les heures sont récupérées depuis la timeline + // la date est récupérée depuis un champ "#date" (datepicker) function getPeriodAsDate(){ let [deb, fin] = getPeriodValues(); deb = numberToTime(deb); @@ -305,19 +341,21 @@ const dateStr = $("#date") .datepicker("getDate") .format("yyyy-mm-dd") - .substring(0, 10); + .substring(0, 10); // récupération que de la date, pas des heures return { deb: new Date(`${dateStr}T${deb}`), fin: new Date(`${dateStr}T${fin}`) } } - + // Sauvegarde les valeurs de la période dans le local storage function savePeriodInLocalStorage(){ const dates = getPeriodValues(); localStorage.setItem("sco-timeline-values", JSON.stringify(dates)); } + // Récupère les valeurs de la période depuis le local storage + // Si elles n'existent pas, on les initialise avec les valeurs par défaut function loadPeriodFromLocalStorage(){ const dates = JSON.parse(localStorage.getItem("sco-timeline-values")); if(dates){ @@ -326,11 +364,13 @@ setPeriodValues(t_start, t_start + period_default); } } + // == Initialisation par défaut de la timeline == - createTicks(); + createTicks(); // création des graduations - loadPeriodFromLocalStorage(); + loadPeriodFromLocalStorage(); // chargement des valeurs si disponible + // Si on donne les heures en appelant le template alors on met à jour la timeline {% if heures %} let [heure_deb, heure_fin] = [{{ heures | safe }}] if (heure_deb != '' && heure_fin != '') { diff --git a/app/templates/assiduites/widgets/toast.j2 b/app/templates/assiduites/widgets/toast.j2 index 6081d227f4418ef2e8ca3749c96a359a8d1da5a4..b75cc1e481415573310e875a768b9fb5a8dc7f83 100644 --- a/app/templates/assiduites/widgets/toast.j2 +++ b/app/templates/assiduites/widgets/toast.j2 @@ -68,32 +68,49 @@ </style> <script> - + /** + * Génère une notification (toast) avec les paramètres spécifiés. + * @param {HTMLElement} content - Le contenu de la notification. + * @param {string} [color="var(--color-present)"] - La couleur de fond de la notification (optionnelle). + * @param {number} [ttl=5] - Le temps de vie de la notification en secondes (optionnelle). + * @returns {HTMLElement} - L'élément toast créé. + */ function generateToast(content, color = "var(--color-present)", ttl = 5) { - const toast = document.createElement('div') - toast.classList.add('toast', 'fadeIn') + // Crée l'élément de notification et ajoute les classes de style + const toast = document.createElement('div'); + toast.classList.add('toast', 'fadeIn'); - const toastContent = document.createElement('div') - toastContent.classList.add('toast-content') - toastContent.appendChild(content) + // Crée le conteneur de contenu de la notification et y ajoute le contenu + const toastContent = document.createElement('div'); + toastContent.classList.add('toast-content'); + toastContent.appendChild(content); + // Définit la couleur de fond de la notification toast.style.backgroundColor = color; - setTimeout(() => { toast.classList.replace('fadeIn', 'fadeOut') }, Math.max(0, ttl * 1000 - 500)) - setTimeout(() => { toast.remove() }, Math.max(0, ttl * 1000)) - toast.appendChild(toastContent) - return toast + // Définit les temporisations pour les animations de disparition et la suppression de la notification + setTimeout(() => { toast.classList.replace('fadeIn', 'fadeOut') }, Math.max(0, ttl * 1000 - 500)); + setTimeout(() => { toast.remove() }, Math.max(0, ttl * 1000)); + + // Ajoute le contenu à la notification + toast.appendChild(toastContent); + + return toast; } + /** + * Ajoute une notification (toast) à l'élément conteneur des toasts. + * @param {HTMLElement} toast - L'élément toast à ajouter. + */ function pushToast(toast) { - document - .querySelector(".toast-holder") - .appendChild( - toast - ); + document.querySelector(".toast-holder").appendChild(toast); } - + /** + * Obtient la couleur de fond de la notification en fonction de l'état spécifié. + * @param {string} etat - L'état de la notification (PRESENT, ABSENT, RETARD). + * @returns {string} - La couleur correspondant à l'état. + */ function getToastColorFromEtat(etat) { let color; switch (etat.toUpperCase()) { @@ -107,12 +124,11 @@ color = "var(--color-retard)"; break; default: - color = "#AAA"; + color = "#AAA"; // Couleur par défaut si l'état est inconnu break; } return color; } - </script> \ No newline at end of file diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 105222b919bb42e7e0c19e789e5b46d10c91abd5..00c4d3171ea3f4c2c3eceec275ed156a8326ed89 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -1537,116 +1537,10 @@ def tableau_assiduite_actions(): flash(f"{objet_name} justifiée") return redirect(request.referrer) - if request.method == "GET": - module: str | int = "" # moduleimpl_id ou chaine libre - - if obj_type == "assiduite": - # Construction du menu module - module = _module_selector_multiple(objet.etudiant, objet.moduleimpl_id) - - return render_template( - "assiduites/pages/tableau_assiduite_actions.j2", - action=action, - can_view_justif_detail=current_user.has_permission(Permission.AbsJustifView) - or (obj_type == "justificatif" and current_user.id == objet.user_id), - etud=objet.etudiant, - moduleimpl=module, - obj_id=obj_id, - objet_name=objet_name, - objet=_preparer_objet(obj_type, objet), - sco=ScoData(etud=objet.etudiant), - title=f"Assiduité {objet.etudiant.nom_short}", - # type utilisé dans les actions modifier / détails (modifier.j2, details.j2) - type="Justificatif" if obj_type == "justificatif" else "Assiduité", - ) - # ----- Cas POST - if obj_type == "assiduite": - try: - _action_modifier_assiduite(objet) - except ScoValueError as error: - raise ScoValueError(error.args[0], request.referrer) from error - flash("L'assiduité a bien été modifiée.") - else: - try: - _action_modifier_justificatif(objet) - except ScoValueError as error: - raise ScoValueError(error.args[0], request.referrer) from error - flash("Le justificatif a bien été modifié.") - return redirect(request.form["table_url"]) - - -def _action_modifier_assiduite(assi: Assiduite): - form = request.form - - # Gestion de l'état - etat = scu.EtatAssiduite.get(form["etat"]) - if etat is not None: - assi.etat = etat - if etat == scu.EtatAssiduite.PRESENT: - assi.est_just = False - else: - assi.est_just = len(get_assiduites_justif(assi.assiduite_id, False)) > 0 - - # Gestion de la description - assi.description = form["description"] - - possible_moduleimpl_id: str = form["moduleimpl_select"] - - # Raise ScoValueError (si None et force module | Etudiant non inscrit | Module non reconnu) - assi.set_moduleimpl(possible_moduleimpl_id) - - db.session.add(assi) - db.session.commit() - scass.simple_invalidate_cache(assi.to_dict(True), assi.etudid) - - -def _action_modifier_justificatif(justi: Justificatif): - "Modifie le justificatif avec les valeurs dans le form" - form = request.form - - # Gestion des Dates - date_debut: datetime = scu.is_iso_formated(form["date_debut"], True) - date_fin: datetime = scu.is_iso_formated(form["date_fin"], True) - if date_debut is None or date_fin is None or date_fin < date_debut: - raise ScoValueError("Dates invalides", request.referrer) - justi.date_debut = date_debut - justi.date_fin = date_fin - - # Gestion de l'état - etat = scu.EtatJustificatif.get(form["etat"]) - if etat is not None: - justi.etat = etat - else: - raise ScoValueError("État invalide", request.referrer) - - # Gestion de la raison - justi.raison = form["raison"] - - # Gestion des fichiers - files = request.files.getlist("justi_fich") - if len(files) != 0: - files = request.files.values() - - archive_name: str = justi.fichier - # Utilisation de l'archiver de justificatifs - archiver: JustificatifArchiver = JustificatifArchiver() - - for fich in files: - archive_name, _ = archiver.save_justificatif( - justi.etudiant, - filename=fich.filename, - data=fich.stream.read(), - archive_name=archive_name, - user_id=current_user.id, - ) - - justi.fichier = archive_name - - justi.dejustifier_assiduites() - db.session.add(justi) - db.session.commit() - justi.justifier_assiduites() - scass.simple_invalidate_cache(justi.to_dict(True), justi.etudid) + # Si on arrive ici, c'est que l'action n'est pas autorisée + # cette fonction ne sert plus qu'à supprimer ou justifier + flash("Méthode non autorisée", "error") + return redirect(request.referrer) def _preparer_objet(