// <=== CONSTANTS and GLOBALS ===> const TIMEZONE = "Europe/Paris"; let url; function getUrl() { if (!url) { url = SCO_URL.substring(0, SCO_URL.lastIndexOf("/")); } return url; } //Les valeurs par défaut de la timeline (8h -> 18h) let currentValues = [8.0, 10.0]; //Objet stockant les étudiants et les assiduités let etuds = {}; let assiduites = {}; let justificatifs = {}; // Variable qui définit si le processus d'action de masse est lancé let currentMassAction = false; let currentMassActionEtat = undefined; /** * Ajout d'une fonction `capitalize` sur tous les strings * alice.capitalize() -> Alice */ Object.defineProperty(String.prototype, "capitalize", { value: function () { return this.charAt(0).toUpperCase() + this.slice(1).toLowerCase(); }, enumerable: false, }); // <<== Outils ==>> Object.defineProperty(Array.prototype, "reversed", { value: function () { return [...this].map(this.pop, this); }, enumerable: false, }); /** * Ajout des évents sur les boutons d'assiduité * @param {Document | HTMLFieldSetElement} parent par défaut le document, un field sinon */ function setupCheckBox(parent = document) { const checkboxes = Array.from(parent.querySelectorAll(".rbtn")); checkboxes.forEach((box) => { box.addEventListener("click", (event) => { if (!uniqueCheckBox(box)) { event.preventDefault(); } if (!box.parentElement.classList.contains("mass")) { assiduiteAction(box); } }); }); } /** * Validation préalable puis désactivation des chammps : * - Groupe * - Module impl * - Date */ function validateSelectors(btn) { const action = () => { const group_ids = getGroupIds(); etuds = {}; group_ids.forEach((group_id) => { sync_get( getUrl() + `/api/group/${group_id}/etudiants`, (data, status) => { if (status === "success") { data.forEach((etud) => { if (!(etud.id in etuds)) { etuds[etud.id] = etud; } }); } } ); }); if (getModuleImplId() == null && window.forceModule) { const HTML = ` <p>Attention, le module doit obligatoirement être renseigné.</p> <p>Cela vient de la configuration du semestre ou plus largement du département.</p> <p>Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.</p> `; const content = document.createElement("div"); content.innerHTML = HTML; openAlertModal("Sélection du module", content); return; } getAssiduitesFromEtuds(true); document.querySelector(".selectors").disabled = true; generateMassAssiduites(); generateAllEtudRow(); btn.remove(); onlyAbs(); }; if (!verifyDateInSemester()) { const HTML = ` <p>Attention, la date sélectionnée n'est pas comprise dans le semestre.</p> <p>Cette page permet l'affichage et la modification des assiduités uniquement pour le semestre sélectionné.</p> <p>Vous n'aurez donc pas accès aux assiduités.</p> <p>Appuyer sur "Valider" uniquement si vous souhaitez poursuivre sans modifier la date.</p> `; const content = document.createElement("div"); content.innerHTML = HTML; openPromptModal("Vérification de la date", content, action); return; } action(); } function onlyAbs() { if (getDate() > moment()) { document .querySelectorAll(".rbtn.present, .rbtn.retard") .forEach((el) => el.remove()); } } /** * Limite le nombre de checkbox marquée * Vérifie aussi si le cliqué est fait sur des assiduités conflictuelles * @param {HTMLInputElement} box la checkbox utilisée * @returns {boolean} Faux si il y a un conflit d'assiduité, Vrai sinon */ function uniqueCheckBox(box) { const type = box.parentElement.getAttribute("type") === "conflit"; if (!type) { const checkboxs = Array.from(box.parentElement.children); checkboxs.forEach((chbox) => { if (chbox.checked && chbox.value !== box.value) { chbox.checked = false; } }); return true; } return false; } /** * Fait une requête GET de façon synchrone * @param {String} path adresse distante * @param {CallableFunction} success fonction à effectuer en cas de succès * @param {CallableFunction} errors fonction à effectuer en cas d'échec */ function sync_get(path, success, errors) { $.ajax({ async: false, type: "GET", url: path, success: success, error: errors, }); } /** * Fait une requête POST de façon synchrone * @param {String} path adresse distante * @param {object} data données à envoyer (objet js) * @param {CallableFunction} success fonction à effectuer en cas de succès * @param {CallableFunction} errors fonction à effectuer en cas d'échec */ function sync_post(path, data, success, errors) { $.ajax({ async: false, type: "POST", url: path, data: JSON.stringify(data), success: success, error: errors, }); } /** * Fait une requête POST de façon asynchrone * @param {String} path adresse distante * @param {object} data données à envoyer (objet js) * @param {CallableFunction} success fonction à effectuer en cas de succès * @param {CallableFunction} errors fonction à effectuer en cas d'échec */ function async_post(path, data, success, errors) { return $.ajax({ async: true, type: "POST", url: path, data: JSON.stringify(data), success: success, error: errors, }); } // <<== Gestion des actions de masse ==>> const massActionQueue = new Map(); /** * Cette fonction remet à zero la gestion des actions de masse */ function resetMassActionQueue() { massActionQueue.set("supprimer", []); massActionQueue.set("editer", []); massActionQueue.set("creer", []); } /** * Fonction pour alimenter la queue des actions de masse * @param {String} type Le type de queue ("creer", "supprimer", "editer") * @param {*} obj L'objet qui sera utilisé par les API */ function addToMassActionQueue(type, obj) { massActionQueue.get(type)?.push(obj); } /** * Fonction pour exécuter les actions de masse */ function executeMassActionQueue() { if (!currentMassAction) return; //Récupération des queues const toCreate = massActionQueue.get("creer"); const toEdit = massActionQueue.get("editer"); const toDelete = massActionQueue.get("supprimer"); //Fonction qui créé les assidutiés de la queue "creer" const create = () => { /** * Création du template de l'assiduité * * { * date_debut: #debut_timeline, * date_fin: #fin_timeline, * moduleimpl_id ?: <> * } */ const tlTimes = getTimeLineTimes(); const assiduite = { date_debut: tlTimes.deb.format(), date_fin: tlTimes.fin.format(), }; const moduleimpl = getModuleImplId(); if (moduleimpl !== null) { assiduite["moduleimpl_id"] = moduleimpl; } const createQueue = []; //liste des assiduités qui seront créées. /** * Pour chaque état de la queue 'creer' on génère une * assiduitée précise depuis le template */ toCreate.forEach((obj) => { const curAssiduite = structuredClone(assiduite); curAssiduite.etudid = obj.etudid; curAssiduite.etat = obj.etat; createQueue.push(curAssiduite); }); /** * On envoie les données à l'API */ const path = getUrl() + `/api/assiduites/create`; sync_post( path, createQueue, (data, status) => { //success }, (data, status) => { //error console.error(data, status); } ); return createQueue.length; }; //Fonction qui modifie les assiduités de la queue 'edition' const edit = () => { //On ajoute le moduleimpl (s'il existe) aux assiduités à modifier const editQueue = toEdit.map((assiduite) => { const moduleimpl = getModuleImplId(); if (moduleimpl !== null) { assiduite["moduleimpl_id"] = moduleimpl; } return assiduite; }); const path = getUrl() + `/api/assiduites/edit`; sync_post( path, editQueue, (data, status) => { //success }, (data, status) => { //error console.error(data, status); } ); return editQueue.length; }; //Fonction qui supprime les assiduités de la queue 'supprimer' const supprimer = () => { const path = getUrl() + `/api/assiduite/delete`; sync_post( path, toDelete, (data, status) => { //success }, (data, status) => { //error console.error(data, status); } ); return toDelete.length; }; //On exécute les fonctions de queue let count = 0; if (currentMassActionEtat == "remove") { count += supprimer(); const span = document.createElement("span"); if (count > 0) { span.innerHTML = `${count} assiduités ont été supprimées.`; } else { span.innerHTML = `Aucune assiduité n'a été supprimée.`; } pushToast( generateToast( span, getToastColorFromEtat(currentMassActionEtat.toUpperCase()), 5 ) ); } else { count += create(); count += edit(); const etat = currentMassActionEtat.toUpperCase() == "RETARD" ? "En retard" : currentMassActionEtat; const span = document.createElement("span"); if (count > 0) { span.innerHTML = `${count} étudiants ont été mis <u><strong>${etat .capitalize() .trim()}</strong></u>`; } else { span.innerHTML = `Aucun étudiant n'a été mis <u><strong>${etat .capitalize() .trim()}</strong></u>`; } pushToast( generateToast( span, getToastColorFromEtat(currentMassActionEtat.toUpperCase()), 5 ) ); } //On récupère les assiduités puis on regénère les lignes d'étudiants getAssiduitesFromEtuds(true); generateAllEtudRow(); } /** * Processus de peuplement des queues * puis d'exécution */ function massAction() { //On récupère tous les boutons d'assiduités const fields = Array.from(document.querySelectorAll(".btns_field.single")); //On récupère l'état de l'action de masse currentMassActionEtat = getAssiduiteValue( document.querySelector(".btns_field.mass") ); //On remet à 0 les queues resetMassActionQueue(); //on met à vrai la variable pour la suite currentMassAction = true; //On affiche le "loader" le temps du processus showLoader(); //On timeout 0 pour le mettre à la fin de l'event queue de JS setTimeout(() => { const conflicts = []; /** * Pour chaque étudiant : * On vérifie s'il y a un conflit -> on place l'étudiant dans l'array conflicts * Sinon -> on fait comme si l'utilisateur cliquait sur le bouton d'assiduité */ fields.forEach((field) => { if (field.getAttribute("type") != "conflit") { if (currentMassActionEtat != "remove") { field.querySelector(`.rbtn.${currentMassActionEtat}`).click(); } else { field.querySelector(".rbtn.absent").click(); } } else { const etudid = field.getAttribute("etudid"); conflicts.push(etuds[parseInt(etudid)]); } }); //on exécute les queues puis on cache le loader executeMassActionQueue(); hideLoader(); //Fin du processus, on remet à false currentMassAction = false; currentMassActionEtat = undefined; //On remet à zero les boutons d'assiduité de masse const boxes = Array.from( document.querySelector(".btns_field.mass").querySelectorAll(".rbtn") ); boxes.forEach((box) => { box.checked = false; }); //Si il y a des conflits d'assiduité, on affiche la liste dans une alert if (conflicts.length > 0) { const div = document.createElement("div"); const sub = document.createElement("p"); sub.textContent = "L'assiduité des étudiants suivant n'a pas pu être modifiée"; div.appendChild(sub); const ul = document.createElement("ul"); conflicts.forEach((etu) => { const li = document.createElement("li"); li.textContent = `${etu.nom} ${etu.prenom.capitalize()}`; ul.appendChild(li); }); div.appendChild(ul); openAlertModal("Conflits d'assiduités", div, ""); } }, 0); } /** * On génère les boutons d'assiduités de masse * puis on ajoute les événements associés */ function generateMassAssiduites() { const content = document.getElementById("content"); const mass = document.createElement("div"); mass.className = "mass-selection"; mass.innerHTML = ` <span>Mettre tout le monde :</span> <fieldset class="btns_field mass"> <input type="checkbox" value="present" name="mass_btn_assiduites" id="mass_rbtn_present" class="rbtn present"> <input type="checkbox" value="retard" name="mass_btn_assiduites" id="mass_rbtn_retard" class="rbtn retard"> <input type="checkbox" value="absent" name="mass_btn_assiduites" id="mass_rbtn_absent" class="rbtn absent"> <input type="checkbox" value="remove" name="mass_btn_assiduites" id="mass_rbtn_aucun" class="rbtn aucun"> </fieldset>`; content.insertBefore(mass, content.querySelector(".etud_holder")); const mass_btn = Array.from(mass.querySelectorAll(".rbtn")); mass_btn.forEach((btn) => { btn.addEventListener("click", () => { massAction(); }); }); if (!verifyDateInSemester()) { content.querySelector(".btns_field.mass").setAttribute("disabled", "true"); } } /** * Affichage du loader */ function showLoader() { document.getElementById("loaderContainer").style.display = "block"; } /** * Dissimulation du loader */ function hideLoader() { document.getElementById("loaderContainer").style.display = "none"; } // <<== Gestion du temps ==>> /** * Transforme un temps numérique en string * 8.75 -> 08h45 * @param {number} time Le temps (float) * @returns {string} le temps (string) */ function toTime(time) { let heure = Math.floor(time); let minutes = Math.round((time - heure) * 60); if (minutes < 10) { minutes = `0${minutes}`; } if (heure < 10) { heure = `0${heure}`; } return `${heure}h${minutes}`; } /** * Transforme une date iso en une date lisible: * new Date('2023-03-03') -> "vendredi 3 mars 2023" * @param {Date} date * @param {object} styles * @returns */ function formatDate(date, styles = { dateStyle: "full" }) { return new Intl.DateTimeFormat("fr-FR", styles).format(date); } /** * Met à jour la date visible sur la page en la formatant */ function updateDate() { const dateInput = document.querySelector("#tl_date"); const date = dateInput.valueAsDate; if (!verifyNonWorkDays(date.getDay(), nonWorkDays)) { $("#datestr").text(formatDate(date).capitalize()); dateInput.setAttribute("value", date.toISOString().split("T")[0]); return true; } else { const att = document.createTextNode( "Le jour sélectionné n'est pas un jour travaillé." ); openAlertModal("Erreur", att, "", "crimson"); dateInput.value = dateInput.getAttribute("value"); return false; } } function verifyDateInSemester() { const date = new moment.tz( document.querySelector("#tl_date").value, TIMEZONE ); const periodSemester = getFormSemestreDates(); return date.isBetween( periodSemester.deb, periodSemester.fin, undefined, "[]" ); } /** * Ajoute la possibilité d'ouvrir le calendrier * lorsqu'on clique sur la date */ function setupDate(onchange = null) { const datestr = document.querySelector("#datestr"); const input = document.querySelector("#tl_date"); datestr.addEventListener("click", () => { if (!input.disabled) { input.showPicker(); } }); if (onchange != null) { input.addEventListener("change", onchange); } } /** * GetAssiduitesOnDateChange * (Utilisé uniquement avec étudiant unique) */ function getAssiduitesOnDateChange() { if (!isSingleEtud()) return; actualizeEtud(etudid); } /** * Transforme une date iso en date intelligible * @param {String} str date iso * @param {String} separator le séparateur de la date intelligible (01/01/2000 {separtor} 10:00) * @returns {String} la date intelligible */ function formatDateModal(str, separator = "·") { return new moment.tz(str, TIMEZONE).format(`DD/MM/Y ${separator} HH:mm`); } /** * Vérifie si la date sélectionnée n'est pas un jour non travaillé * Renvoie Vrai si le jour est non travaillé */ function verifyNonWorkDays(day, nonWorkdays) { let d = ""; switch (day) { case 0: d = "dim"; break; case 1: d = "lun"; break; case 2: d = "mar"; break; case 3: d = "mer"; break; case 4: d = "jeu"; break; case 5: d = "ven"; break; case 6: d = "sam"; break; } return nonWorkdays.indexOf(d) != -1; } /** * Fonction qui vérifie si une période est dans un interval * Objet période / interval * { * deb: moment.tz(<Date>), * fin: moment.tz(<Date>), * } * @param {object} period * @param {object} interval * @returns {boolean} Vrai si la période est dans l'interval */ function hasTimeConflict(period, interval) { return period.deb.isBefore(interval.fin) && period.fin.isAfter(interval.deb); } /** * On récupère la période de la timeline * @returns {deb : moment.tz(), fin: moment.tz()} */ function getTimeLineTimes() { //getPeriodValues() -> retourne la position de la timeline [a,b] avec a et b des number let values = getPeriodValues(); //On récupère la date const dateiso = document.querySelector("#tl_date").value; //On génère des objets temps (moment.tz) values = values.map((el) => { el = toTime(el).replace("h", ":"); el = `${dateiso}T${el}`; return moment.tz(el, TIMEZONE); }); return { deb: values[0], fin: values[1] }; } /** * Vérification de l'égalité entre un conflit et la période de la timeline * @param {object} conflict * @returns {boolean} Renvoie Vrai si la période de la timeline est égal au conflit */ function isConflictSameAsPeriod(conflict, period = undefined) { const tlTimes = period == undefined ? getTimeLineTimes() : period; const clTimes = { deb: moment.tz(conflict.date_debut, TIMEZONE), fin: moment.tz(conflict.date_fin, TIMEZONE), }; return tlTimes.deb.isSame(clTimes.deb) && tlTimes.fin.isSame(clTimes.fin); } /** * Retourne un objet Date de la date sélectionnée * @returns {Date} la date sélectionnée */ function getDate() { const date = new Date( document.querySelector("#tl_date").getAttribute("value") ); date.setHours(0, 0, 0, 0); return date; } /** * Retourne un objet date représentant le jour suivant * @returns {Date} le jour suivant */ function getNextDate() { const date = getDate(); const next = new Date(date.valueOf()); next.setDate(date.getDate() + 1); next.setHours(0, 0, 0, 0); return next; } /** * Retourne un objet date représentant le jour précédent * @returns {Date} le jour précédent */ function getPrevDate() { const date = getDate(); const next = new Date(date.valueOf()); next.setDate(date.getDate() - 1); next.setHours(0, 0, 0, 0); return next; } /** * Transformation d'un objet Date en chaîne ISO * @param {Date} date * @returns {string} la date iso avec le timezone */ function toIsoString(date) { var tzo = -date.getTimezoneOffset(), dif = tzo >= 0 ? "+" : "-", pad = function (num) { return (num < 10 ? "0" : "") + num; }; return ( date.getFullYear() + "-" + pad(date.getMonth() + 1) + "-" + pad(date.getDate()) + "T" + pad(date.getHours()) + ":" + pad(date.getMinutes()) + ":" + pad(date.getSeconds()) + dif + pad(Math.floor(Math.abs(tzo) / 60)) + ":" + pad(Math.abs(tzo) % 60) ); } /** * Transforme un temps numérique en une date moment.tz * @param {number} nb * @returns {moment.tz} Une date formée du temps donné et de la date courante */ function numberTimeToDate(nb) { time = toTime(nb).replace("h", ":"); date = document.querySelector("#tl_date").value; datetime = `${date}T${time}`; return moment.tz(datetime, TIMEZONE); } // <<== Gestion des assiduités ==>> /** * Récupère les assiduités des étudiants * en fonction de : * - du semestre * - de la date courant et du jour précédent. * @param {boolean} clear vidage de l'objet "assiduites" ou non * @returns {object} l'objets Assiduités {<etudid:str> : [<assiduite>,]} */ function getAssiduitesFromEtuds(clear, has_formsemestre = true, deb, fin) { const etudIds = Object.keys(etuds).join(","); const formsemestre_id = has_formsemestre ? `formsemestre_id=${getFormSemestreId()}&` : ""; const date_debut = deb ? deb : toIsoString(getPrevDate()); const date_fin = fin ? fin : toIsoString(getNextDate()); if (clear) { assiduites = {}; } const url_api = getUrl() + `/api/assiduites/group/query?date_debut=${date_debut}&${formsemestre_id}&date_fin=${date_fin}&etudids=${etudIds}`; sync_get(url_api, (data, status) => { if (status === "success") { const dataKeys = Object.keys(data); dataKeys.forEach((key) => { if (clear || !(key in assiduites)) { assiduites[key] = data[key]; } else { assiduites[key] = assiduites[key].concat(data[key]); } let assi_ids = []; assiduites[key] = assiduites[key].reversed().filter((value) => { if (assi_ids.indexOf(value.assiduite_id) == -1) { assi_ids.push(value.assiduite_id); return true; } return false; }); }); } }); return assiduites; } /** * Création d'une assiduité pour un étudiant * @param {String} etat l'état de l'étudiant * @param {Number | String} etudid l'identifiant de l'étudiant * * TODO : Rendre asynchrone */ function createAssiduite(etat, etudid) { const tlTimes = getTimeLineTimes(); const assiduite = { date_debut: tlTimes.deb.format(), date_fin: tlTimes.fin.format(), etat: etat, }; const moduleimpl = getModuleImplId(); if (moduleimpl !== null) { assiduite["moduleimpl_id"] = moduleimpl; } const path = getUrl() + `/api/assiduite/${etudid}/create`; sync_post( path, [assiduite], (data, status) => { //success if (data.success.length > 0) { let obj = data.success["0"].assiduite_id; } }, (data, status) => { //error console.error(data, status); } ); } /** * Suppression d'une assiduité * @param {String | Number} assiduite_id l'identifiant de l'assiduité * TODO : Rendre asynchrone */ function deleteAssiduite(assiduite_id) { const path = getUrl() + `/api/assiduite/delete`; sync_post( path, [assiduite_id], (data, status) => { //success if (data.success.length > 0) { let obj = data.success["0"].assiduite_id; } }, (data, status) => { //error console.error(data, status); } ); } /** * * @param {String | Number} assiduite_id l'identifiant d'une assiduité * @param {String} etat l'état à modifier * @returns {boolean} si l'édition a fonctionné * TODO : Rendre asynchrone */ function editAssiduite(assiduite_id, etat) { const assiduite = { etat: etat, moduleimpl_id: getModuleImplId(), }; const path = getUrl() + `/api/assiduite/${assiduite_id}/edit`; let bool = false; sync_post( path, assiduite, (data, status) => { bool = true; }, (data, status) => { //error console.error(data, status); } ); return bool; } /** * Récupération des assiduités conflictuelles avec la période de la time line * @param {String | Number} etudid identifiant de l'étudiant * @returns {Array[Assiduité]} un tableau d'assiduité */ function getAssiduitesConflict(etudid, periode) { const etudAssiduites = assiduites[etudid]; if (!etudAssiduites) { return []; } if (!periode) { periode = getTimeLineTimes(); } return etudAssiduites.filter((assi) => { const interval = { deb: moment.tz(assi.date_debut, TIMEZONE), fin: moment.tz(assi.date_fin, TIMEZONE), }; return hasTimeConflict(periode, interval); }); } /** * Récupération de la dernière assiduité du jour précédent * @param {String | Number} etudid l'identifiant de l'étudiant * @returns {Assiduité} la dernière assiduité du jour précédent */ function getLastAssiduiteOfPrevDate(etudid) { const etudAssiduites = assiduites[etudid]; if (!etudAssiduites) { return ""; } const period = { deb: moment.tz(getPrevDate(), TIMEZONE), fin: moment.tz(getDate(), TIMEZONE), }; const prevAssiduites = etudAssiduites .filter((assi) => { const interval = { deb: moment.tz(assi.date_debut, TIMEZONE), fin: moment.tz(assi.date_fin, TIMEZONE), }; return hasTimeConflict(period, interval); }) .sort((a, b) => { const a_fin = moment.tz(a.date_fin, TIMEZONE); const b_fin = moment.tz(b.date_fin, TIMEZONE); return b_fin < a_fin; }); if (prevAssiduites.length < 1) { return null; } return prevAssiduites.pop(); } /** * Récupération de l'état appointé * @param {HTMLFieldSetElement} field le conteneur des boutons d'assiduité d'une ligne étudiant * @returns {String} l'état appointé : ('present','absent','retard', 'remove') * * état = 'remove' si le clic désélectionne une assiduité appointée */ function getAssiduiteValue(field) { const checkboxs = Array.from(field.children); let value = "remove"; checkboxs.forEach((chbox) => { if (chbox.checked) { value = chbox.value; } }); return value; } /** * Mise à jour des assiduités d'un étudiant * @param {String | Number} etudid identifiant de l'étudiant */ function actualizeEtudAssiduite(etudid, has_formsemestre = true) { const formsemestre_id = has_formsemestre ? `formsemestre_id=${getFormSemestreId()}&` : ""; const date_debut = toIsoString(getPrevDate()); const date_fin = toIsoString(getNextDate()); const url_api = getUrl() + `/api/assiduites/${etudid}/query?${formsemestre_id}date_debut=${date_debut}&date_fin=${date_fin}`; sync_get(url_api, (data, status) => { if (status === "success") { assiduites[etudid] = data; } }); } function getAllAssiduitesFromEtud(etudid, action) { const url_api = getUrl() + `/api/assiduites/${etudid}`; $.ajax({ async: true, type: "GET", url: url_api, success: (data, status) => { if (status === "success") { assiduites[etudid] = data; action(data); } }, error: () => {}, }); } /** * Déclenchement d'une action après appuie sur un bouton d'assiduité * @param {HTMLInputElement} element Bouton d'assiduité appuyé */ function assiduiteAction(element) { const field = element.parentElement; const type = field.getAttribute("type"); const etudid = parseInt(field.getAttribute("etudid")); const assiduite_id = parseInt(field.getAttribute("assiduite_id")); const etat = getAssiduiteValue(field); // Cas de l'action de masse -> peuplement des queues if (currentMassAction) { if (currentMassActionEtat != "remove") { switch (type) { case "création": addToMassActionQueue("creer", { etat: etat, etudid: etudid }); break; case "édition": if (etat != "remove") { addToMassActionQueue("editer", { etat: etat, assiduite_id: assiduite_id, }); } break; } } else if (type == "édition") { addToMassActionQueue("supprimer", assiduite_id); } } else { // Cas normal -> mise à jour en base switch (type) { case "création": createAssiduite(etat, etudid); break; case "édition": if (etat === "remove") { deleteAssiduite(assiduite_id); } else { editAssiduite(assiduite_id, etat); } break; case "conflit": const conflitResolver = new ConflitResolver( assiduites[etudid], getTimeLineTimes(), { deb: new moment.tz(getDate(), TIMEZONE), fin: new moment.tz(getNextDate(), TIMEZONE), } ); const update = (assi) => { actualizeEtud(assi.etudid); }; conflitResolver.callbacks = { delete: update, edit: update, split: update, }; conflitResolver.open(); return; } if (type != "conflit") { let etatAffiche; switch (etat.toUpperCase()) { case "PRESENT": etatAffiche = "%etud% a été noté(e) <u><strong>présent(e)</strong></u>"; break; case "RETARD": etatAffiche = "%etud% a été noté(e) <u><strong>en retard</strong></u>"; break; case "ABSENT": etatAffiche = "%etud% a été noté(e) <u><strong>absent(e)</strong></u>"; break; case "REMOVE": etatAffiche = "L'assiduité de %etud% a été retirée."; } const nom_prenom = `${etuds[etudid].nom.toUpperCase()} ${etuds[ etudid ].prenom.capitalize()}`; const span = document.createElement("span"); span.innerHTML = etatAffiche.replace("%etud%", nom_prenom); pushToast( generateToast(span, getToastColorFromEtat(etat.toUpperCase()), 5) ); } actualizeEtud(etudid, !isSingleEtud); } } // <<== Gestion de l'affichage des barres étudiant ==>> /** * Génère l'HTML lié à la barre d'un étudiant * @param {Etudiant} etud représentation objet d'un étudiant * @param {Number} index l'index de l'étudiant dans la liste * @param {AssiduitéMod} assiduite Objet représentant l'état de l'étudiant pour la période de la timeline * @returns {String} l'HTML généré */ function generateEtudRow( etud, index, assiduite = { etatAssiduite: "", type: "création", id: -1, date_debut: null, date_fin: null, prevAssiduites: "", } ) { // Génération des boutons du choix de l'assiduité let assi = ""; ["present", "retard", "absent"].forEach((abs) => { if (abs.toLowerCase() === assiduite.etatAssiduite.toLowerCase()) { assi += `<input checked type="checkbox" value="${abs}" name="btn_assiduites_${index}" id="rbtn_${abs}" class="rbtn ${abs}">`; } else { assi += `<input type="checkbox" value="${abs}" name="btn_assiduites_${index}" id="rbtn_${abs}" class="rbtn ${abs}">`; } }); const conflit = assiduite.type == "conflit" ? "conflit" : ""; const pdp_url = `${getUrl()}/api/etudiant/etudid/${etud.id}/photo?size=small`; const HTML = `<div class="etud_row ${conflit}" id="etud_row_${etud.id}"> <div class="index">${index}</div> <div class="name_field"> <img class="pdp" src="${pdp_url}"> <div class="name_set"> <h4 class="nom">${etud.nom}</h4> <h5 class="prenom">${etud.prenom}</h5> </div> </div> <div class="assiduites_bar"> <div id="prevDateAssi" class="${assiduite.prevAssiduites?.etat?.toLowerCase()}"> </div> </div> <fieldset class="btns_field single" etudid="${etud.id}" assiduite_id="${ assiduite.id }" type="${assiduite.type}"> ${assi} </fieldset> </div>`; return HTML; } /** * Insertion de la ligne étudiant * @param {Etudiant} etud l'objet représentant un étudiant * @param {Number} index le n° de l'étudiant dans la liste des étudiants * @param {boolean} output ajout automatique dans la page ou non (default : Non) * @returns {String} HTML si output sinon rien */ function insertEtudRow(etud, index, output = false) { const etudHolder = document.querySelector(".etud_holder"); const conflict = getAssiduitesConflict(etud.id); const prevAssiduite = getLastAssiduiteOfPrevDate(etud.id); let assiduite = { etatAssiduite: "", type: "création", id: -1, date_debut: null, date_fin: null, prevAssiduites: prevAssiduite, }; if (conflict.length > 0) { assiduite.etatAssiduite = conflict[0].etat; assiduite.id = conflict[0].assiduite_id; assiduite.date_debut = conflict[0].date_debut; assiduite.date_fin = conflict[0].date_fin; if (isConflictSameAsPeriod(conflict[0])) { assiduite.type = "édition"; } else { assiduite.type = "conflit"; } } let row = generateEtudRow(etud, index, assiduite); if (output) { return row; } etudHolder.insertAdjacentHTML("beforeend", row); row = document.getElementById(`etud_row_${etud.id}`); const prev = row.querySelector("#prevDateAssi"); setupAssiduiteBuble(prev, prevAssiduite); const bar = row.querySelector(".assiduites_bar"); bar.appendChild(createMiniTimeline(assiduites[etud.id])); if (!verifyDateInSemester()) { row.querySelector(".btns_field.single").setAttribute("disabled", "true"); } } /** * Mise à jour d'une ligne étudiant * @param {String | Number} etudid l'identifiant de l'étudiant */ function actualizeEtud(etudid) { actualizeEtudAssiduite(etudid, !isSingleEtud()); //Actualize row const etudHolder = document.querySelector(".etud_holder"); const ancient_row = document.getElementById(`etud_row_${etudid}`); let new_row = document.createElement("div"); new_row.innerHTML = insertEtudRow( etuds[etudid], ancient_row.querySelector(".index").textContent, true ); setupCheckBox(new_row.firstElementChild); const bar = new_row.firstElementChild.querySelector(".assiduites_bar"); bar.appendChild(createMiniTimeline(assiduites[etudid])); const prev = new_row.firstElementChild.querySelector("#prevDateAssi"); if (isSingleEtud()) { prev.classList.add("single"); } setupAssiduiteBuble(prev, getLastAssiduiteOfPrevDate(etudid)); etudHolder.replaceChild(new_row.firstElementChild, ancient_row); } /** * Génération de toutes les lignes étudiant */ function generateAllEtudRow() { if (isSingleEtud()) { try { actualizeEtud(etudid); } catch (ignored) {} return; } if (!document.querySelector(".selectors")?.disabled) { return; } document.querySelector(".etud_holder").innerHTML = ""; etuds_ids = Object.keys(etuds).sort((a, b) => etuds[a].nom > etuds[b].nom ? 1 : etuds[b].nom > etuds[a].nom ? -1 : 0 ); for (let i = 0; i < etuds_ids.length; i++) { const etud = etuds[etuds_ids[i]]; insertEtudRow(etud, i + 1); } setupCheckBox(); } // <== Gestion du modal de conflit ==> // <<== Gestion de la récupération d'informations ==>> /** * Récupération d'un nom d'utilisateur à partir d'un identifiant * @param {Number} id identifiant de l'utilisateur * @returns {String} le nom de l'utilisateur ou son pseudo ou "Non Renseigné" */ function getUserFromId(id) { if (id == "") { return "Non Renseigné"; } let name = "Non Renseigné"; sync_get(getUrl() + `/api/user/${id}`, (data) => { if (data.nom != "" && data.prenom != "") { name = `${data.nom} ${data.prenom}`; } else { name = data.user_name; } }); return name; } /** * Récupération des ids des groupes * @returns la liste des ids des groupes */ function getGroupIds() { const btns = document.querySelector(".multiselect-container.dropdown-menu"); const groups = Array.from(btns.querySelectorAll(".active")).map((el) => { return el.querySelector("input").value; }); return groups; } /** * Récupération du moduleimpl_id * @returns {String} l'identifiant ou null si inéxistant */ function getModuleImplId() { const val = document.querySelector("#moduleimpl_select")?.value; return ["", undefined, null].includes(val) ? null : val; } /** * Récupération de l'id du formsemestre * @returns {String} l'identifiant du formsemestre */ function getFormSemestreId() { return document.querySelector(".formsemestre_id").textContent; } /** * Récupère la période du semestre * @returns {object} période {deb,fin} */ function getFormSemestreDates() { const dateDeb = document.getElementById( "formsemestre_date_debut" ).textContent; const dateFin = document.getElementById("formsemestre_date_fin").textContent; return { deb: dateDeb, fin: dateFin, }; } /** * Récupère un objet étudiant à partir de son id * @param {Number} etudid */ function getSingleEtud(etudid) { sync_get(getUrl() + `/api/etudiant/etudid/${etudid}`, (data) => { etuds[etudid] = data; }); } function isSingleEtud() { return location.href.includes("SignaleAssiduiteEtud"); } function getCurrentAssiduiteModuleImplId() { const currentAssiduites = getAssiduitesConflict(etudid); if (currentAssiduites.length > 0) { const mod = currentAssiduites[0].moduleimpl_id; return mod == null ? "" : mod; } return ""; } function getCurrentAssiduite(etudid) { const field = document.querySelector( `fieldset.btns_field.single[etudid='${etudid}']` ); if (!field) return null; const assiduite_id = parseInt(field.getAttribute("assiduite_id")); const type = field.getAttribute("type"); if (type == "édition") { let assi = null; assiduites[etudid].forEach((a) => { if (a.assiduite_id === assiduite_id) { assi = a; } }); return assi; } else { return null; } } // <<== Gestion de la justification ==>> function getJustificatifFromPeriod(date, etudid, update) { $.ajax({ async: true, type: "GET", url: getUrl() + `/api/justificatifs/${etudid}/query?date_debut=${date.deb .add(1, "s") .format()}&date_fin=${date.fin.subtract(1, "s").format()}`, success: (data) => { update(data); }, error: () => {}, }); } function updateJustifyBtn() { if (isSingleEtud()) { const assi = getCurrentAssiduite(etudid); const just = assi ? !assi.est_just : false; const btn = document.getElementById("justif-rapide"); if (!just) { btn.setAttribute("disabled", "true"); } else { btn.removeAttribute("disabled"); } } } function fastJustify(assiduite) { const period = { deb: new moment.tz(assiduite.date_debut, TIMEZONE), fin: new moment.tz(assiduite.date_fin, TIMEZONE), }; const action = (justifs) => { if (justifs.length > 0) { justifyAssiduite(assiduite.assiduite_id, !assiduite.est_just); } else { console.debug("WIP"); //créer un nouveau justificatif // Afficher prompt -> demander raison et état const success = () => { const raison = document.getElementById("promptText").value; const etat = document.getElementById("promptSelect").value; //créer justificatif const justif = { date_debut: new moment.tz(assiduite.date_debut, TIMEZONE) .add(1, "s") .format(), date_fin: new moment.tz(assiduite.date_fin, TIMEZONE) .subtract(1, "s") .format(), raison: raison, etat: etat, }; createJustificatif(justif); // justifyAssiduite(assiduite.assiduite_id, true); generateAllEtudRow(); }; const content = document.createElement("fieldset"); const htmlPrompt = `<legend>Entrez l'état du justificatif :</legend> <select name="promptSelect" id="promptSelect" required> <option value="valide">Valide</option> <option value="attente">En Attente de validation</option> <option value="non_valide">Non Valide</option> <option value="modifie">Modifié</option> </select> <legend>Raison:</legend> <textarea type="text" placeholder="Explication du justificatif (non obligatoire)" id="promptText" style="width:100%;"></textarea> `; content.innerHTML = htmlPrompt; openPromptModal( "Nouveau justificatif (Rapide)", content, success, () => {}, "#7059FF" ); } }; if (assiduite.etudid) { getJustificatifFromPeriod(period, assiduite.etudid, action); } } function justifyAssiduite(assiduite_id, justified) { const assiduite = { est_just: justified, }; const path = getUrl() + `/api/assiduite/${assiduite_id}/edit`; let bool = false; sync_post( path, assiduite, (data, status) => { bool = true; }, (data, status) => { //error console.error(data, status); } ); return bool; } function createJustificatif(justif) { const path = getUrl() + `/api/justificatif/${etudid}/create`; sync_post( path, [justif], (data, status) => { //success if (data.success.length > 0) { console.table(data[0]); } }, (data, status) => { //error console.error(data, status); } ); } function getAllJustificatifsFromEtud(etudid, action) { const url_api = getUrl() + `/api/justificatifs/${etudid}`; $.ajax({ async: true, type: "GET", url: url_api, success: (data, status) => { if (status === "success") { action(data); } }, error: () => {}, }); } function deleteJustificatif(justif_id) { const path = getUrl() + `/api/justificatif/delete`; sync_post( path, [justif_id], (data, status) => { //success if (data.success.length > 0) { } }, (data, status) => { //error console.error(data, status); } ); }