// <=== CONSTANTS and GLOBALS ===> 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, }); const DatePrecisions = [ "year", "month", "day", "hour", "minute", "second", "millisecond", ]; // <<== 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 && !readOnly) { 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; $("#tl_date").datepicker("option", "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() > Date.now()) { 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) { //TODO Optimiser : rendre asynchrone + sans jquery console.log("sync_get " + path); $.ajax({ async: false, type: "GET", url: path, success: success, error: errors, }); } /** * Fait une requête GET de façon asynchrone * @param {String} path adresse distante * @param {CallableFunction} success fonction à effectuer en cas de succès * @param {CallableFunction} errors fonction à effectuer en cas d'échec */ async function async_get(path, success, errors) { console.log("async_get " + path); let response; try { response = await fetch(path); if (response.ok) { const data = await response.json(); success(data); } else { throw new Error("Network response was not ok."); } } catch (error) { console.error(error); if (errors) errors(error); } return response; } /** * 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) { //TODO Optimiser : rendre asynchrone + sans jquery console.log("sync_post " + path); $.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 */ async function async_post(path, data, success, errors) { console.log("async_post " + path); let response; try { response = await fetch(path, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(data), }); if (response.ok) { const responseData = await response.json(); success(responseData); } else { throw new Error("Network response was not ok."); } } catch (error) { console.error(error); if (errors) errors(error); } return response; } // <<== 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(); let assiduite = { date_debut: tlTimes.deb.toFakeIso(), date_fin: tlTimes.fin.toFakeIso(), }; assiduite = setModuleImplId(assiduite); if (!hasModuleImpl(assiduite) && window.forceModule) { const html = ` <h3>Aucun module n'a été spécifié</h3> `; const div = document.createElement("div"); div.innerHTML = html; openAlertModal("Erreur Module", div); return 0; } 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); errorAlert(); } ); 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) => { assiduite = setModuleImplId(assiduite); return assiduite; }); if (getModuleImplId() == null && window.forceModule) { const html = ` <h3>Aucun module n'a été spécifié</h3> `; const div = document.createElement("div"); div.innerHTML = html; openAlertModal("Erreur Module", div); return 0; } const path = getUrl() + `/api/assiduites/edit`; sync_post( path, editQueue, (data, status) => { //success }, (data, status) => { //error console.error(data, status); errorAlert(); } ); 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); errorAlert(); } ); 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() || readOnly) { 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", { ...{ timeZone: SCO_TIMEZONE }, ...styles, }).format(date); } /** * Met à jour la date visible sur la page en la formatant */ function updateDate() { const dateInput = document.querySelector("#tl_date"); let date = $(dateInput).datepicker("getDate"); if (date == null) { date = new Date(Date.fromFRA(dateInput.value)); } const intlOptions = { dateStyle: "full", timeZone: SCO_TIMEZONE, }; let dateStr = ""; if (!isNonWorkDay(date, nonWorkDays)) { dateStr = formatDate(date, intlOptions).capitalize(); } else { // On se rend au dernier jour travaillé disponible const lastWorkDay = getNearestWorkDay(date); const att = document.createTextNode( `Le jour sélectionné (${formatDate( date, intlOptions )}) n'est pas un jour travaillé.` ); const div = document.createElement("div"); div.appendChild(att); div.appendChild(document.createElement("br")); div.appendChild( document.createTextNode( `Le dernier jour travaillé disponible a été sélectionné : ${formatDate( lastWorkDay, intlOptions )}.` ) ); openAlertModal("Attention", div, "", "#eec660"); /* BUG TODO MATHIAS $(dateInput).datepicker("setDate", date_fra); // XXX ??? non définie dateInput.value = date_fra; */ date = lastWorkDay; dateStr = formatDate(lastWorkDay, { dateStyle: "full", timeZone: SCO_TIMEZONE, }).capitalize(); } document.querySelector("#datestr").textContent = dateStr; return true; } function getNearestWorkDay(date) { const aDay = 86400000; // 24 * 3600 * 1000 | H * s * ms let day = date; let count = 0; while (isNonWorkDay(day, nonWorkDays) && count++ < 7) { day = new Date(day - aDay); } return day; } function verifyDateInSemester() { const date = getDate(); const periodSemester = getFormSemestreDates(); return date.isBetween(periodSemester.deb, periodSemester.fin, "[]"); } /** * 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 (!document.querySelector(".selectors").disabled) { try { document.querySelector(".infos .ui-datepicker-trigger").click(); } catch {} } }); if (onchange != null) { $(input).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 Date(str).format("DD/MM/Y HH:mm").replace(" ", separator); } /** * Vérifie si la date sélectionnée n'est pas un jour non travaillé * Renvoie Vrai si le jour est non travaillé */ function isNonWorkDay(day, nonWorkdays) { const d = Intl.DateTimeFormat("fr-FR", { timeZone: SCO_TIMEZONE, weekday: "short", }) .format(day) .replace(".", ""); return nonWorkdays.indexOf(d) != -1; } /** * Fonction qui vérifie si une période est dans un interval * Objet période / interval * { * deb: Date, * fin: 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 : Date, fin: Date)} */ 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 = getDate().format("YYYY-MM-DD"); //On génère des objets temps values = values.map((el) => { el = toTime(el).replace("h", ":"); el = `${dateiso}T${el}`; return new Date(el); }); 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: new Date(Date.removeUTC(conflict.date_debut)), fin: new Date(Date.removeUTC(conflict.date_fin)), }; 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 = $("#tl_date").datepicker("getDate") ?? new Date(Date.fromFRA(document.querySelector("#tl_date").value)); return date.startOf("day"); } /** * Retourne un objet date représentant le jour suivant * @returns {Date} le jour suivant */ function getNextDate() { const date = getDate(); return date.clone().add(1, "days"); } /** * Retourne un objet date représentant le jour précédent * @returns {Date} le jour précédent */ function getPrevDate() { const date = getDate(); return date.clone().add(-1, "days"); } /** * Transformation d'un objet Date en chaîne ISO * @param {Date} date * @returns {string} la date iso avec le timezone */ /** * Transforme un temps numérique en une date * @param {number} nb * @returns {Date} Une date formée du temps donné et de la date courante */ function numberTimeToDate(nb) { time = toTime(nb).replace("h", ":"); date = getDate().format("YYYY-MM-DD"); datetime = `${date}T${time}`; return new Date(datetime); } // <<== 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'objet Assiduités {<etudid:str> : [<assiduite>,]} */ function getAssiduitesFromEtuds(clear, deb, fin) { const etudIds = Object.keys(etuds).join(","); const date_debut = deb ? deb : getPrevDate().toFakeIso(); const date_fin = fin ? fin : getNextDate().toFakeIso(); if (clear) { assiduites = {}; } const url_api = getUrl() + `/api/assiduites/group/query?date_debut=${date_debut}&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(); let assiduite = { date_debut: tlTimes.deb.toFakeIso(), date_fin: tlTimes.fin.toFakeIso(), etat: etat, }; assiduite = setModuleImplId(assiduite); if (!hasModuleImpl(assiduite) && window.forceModule) { const html = ` <h3>Aucun module n'a été spécifié</h3> `; const div = document.createElement("div"); div.innerHTML = html; openAlertModal("Erreur Module", div); return false; } const path = getUrl() + `/api/assiduite/${etudid}/create`; let with_errors = false; sync_post( path, [assiduite], (data, status) => { //success if (data.success.length > 0) { let obj = data.success["0"].message.assiduite_id; } if (data.errors.length > 0) { console.error(data.errors["0"].message); if (data.errors["0"].message == "Module non renseigné") { 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); } if ( data.errors["0"].message == "L'étudiant n'est pas inscrit au module" ) { 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> `; const content = document.createElement("div"); content.innerHTML = HTML; openAlertModal("Sélection du module", content); } with_errors = true; } }, (data, status) => { //error console.error(data, status); errorAlert(); with_errors = true; } ); return !with_errors; } /** * 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 createAssiduiteComplete(assiduite, etudid) { if (!hasModuleImpl(assiduite) && window.forceModule) { const html = ` <h3>Aucun module n'a été spécifié</h3> `; const div = document.createElement("div"); div.innerHTML = html; openAlertModal("Erreur Module", div); return false; } const path = getUrl() + `/api/assiduite/${etudid}/create`; let with_errors = false; sync_post( path, [assiduite], (data, status) => { //success if (data.success.length > 0) { let obj = data.success["0"].message.assiduite_id; } if (data.errors.length > 0) { console.error(data.errors["0"].message); if (data.errors["0"].message == "Module non renseigné") { const HTML = ` <p>Attention, le module doit obligatoirement être renseigné.</p> <p>Voir configuration du semestre ou du département.</p> `; const content = document.createElement("div"); content.innerHTML = HTML; openAlertModal("Sélection du module", content); } if ( data.errors["0"].message == "L'étudiant n'est pas inscrit au module" ) { const HTML = ` <p>Attention, l'étudiant n'est pas inscrit à ce module.</p> `; const content = document.createElement("div"); content.innerHTML = HTML; openAlertModal("Sélection du module", content); } if ( data.errors["0"].message == "Duplication: la période rentre en conflit avec une plage enregistrée" ) { const HTML = ` <p>L'assiduité n'a pas pu être enregistrée car un autre évènement existe sur la période sélectionnée</p> `; const content = document.createElement("div"); content.innerHTML = HTML; openAlertModal("Période conflictuelle", content); } with_errors = true; } }, (data, status) => { //error console.error(data, status); errorAlert(); with_errors = true; } ); return !with_errors; } /** * 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"].message.assiduite_id; } }, (data, status) => { //error console.error(data, status); errorAlert(); } ); return true; } function hasModuleImpl(assiduite) { if (assiduite.moduleimpl_id != null) return true; return ( assiduite.hasOwnProperty("external_data") && assiduite.external_data != null && assiduite.external_data.hasOwnProperty("module") ); } /** * * @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, assi) { if (assi.length != 1 || !assi[0].hasOwnProperty("assiduite_id")) { const html = ` <h3>Aucune assiduité n'a pû être éditée</h3> `; const div = document.createElement("div"); div.innerHTML = html; openAlertModal("Erreur", div); return; } let assiduite = { etat: etat, external_data: assi ? assi.external_data : null, }; assiduite = setModuleImplId(assiduite); if (!hasModuleImpl(assiduite) && window.forceModule) { const html = ` <h3>Aucun module n'a été spécifié</h3> `; const div = document.createElement("div"); div.innerHTML = html; openAlertModal("Erreur Module", div); return; } 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); try { errorJson = data.responseJSON; if (errorJson.message == "param 'moduleimpl_id': etud non inscrit") { const html = ` <h3>L'étudiant n'est pas inscrit à ce module</h3> `; const div = document.createElement("div"); div.innerHTML = html; openAlertModal("Erreur Module", div); return; } if ( errorJson.message == "param 'moduleimpl_id' : le moduleimpl_id ne peut pas être nul" ) { const html = ` <h3>Un module doit être spécifié</h3> `; const div = document.createElement("div"); div.innerHTML = html; openAlertModal("Erreur Module", div); return; } } catch (e) { console.error(e); //errorAlert(); } } ); return bool; } /** * Récupération des assiduités conflictuelles avec la période de la timeline * @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: new Date(Date.removeUTC(assi.date_debut)), fin: new Date(Date.removeUTC(assi.date_fin)), }; const test = hasTimeConflict(periode, interval); return test; }); } /** * 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: getPrevDate(), fin: getDate(), }; const prevAssiduites = etudAssiduites .filter((assi) => { const interval = { deb: new Date(Date.removeUTC(assi.date_debut)), fin: new Date(Date.removeUTC(assi.date_fin)), }; return hasTimeConflict(period, interval); }) .sort((a, b) => { const a_fin = new Date(Date.removeUTC(a.date_fin)); const b_fin = new Date(Date.removeUTC(b.date_fin)); 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) { const date_debut = getPrevDate().toFakeIso(); const date_fin = getNextDate().toFakeIso(); const url_api = getUrl() + `/api/assiduites/${etudid}/query?date_debut=${date_debut}&date_fin=${date_fin}`; sync_get(url_api, (data, status) => { if (status === "success") { assiduites[etudid] = data; } }); } function getAllAssiduitesFromEtud( etudid, action, order = false, justifs = false, courant = false ) { const url_api = getUrl() + `/api/assiduites/${etudid}${ order ? "/query?order%°" .replace("%", justifs ? "&with_justifs" : "") .replace("°", courant ? "&courant" : "") : "" }`; //TODO Utiliser async_get au lieu de jquery $.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 let done = false; switch (type) { case "création": done = createAssiduite(etat, etudid); break; case "édition": if (etat === "remove") { done = deleteAssiduite(assiduite_id); } else { done = editAssiduite( assiduite_id, etat, assiduites[etudid].filter((a) => a.assiduite_id == assiduite_id) ); } break; case "conflit": const conflitResolver = new ConflitResolver( assiduites[etudid], getTimeLineTimes(), { deb: getDate(), fin: getNextDate(), } ); const update = (assi) => { actualizeEtud(assi.etudid); }; conflitResolver.callbacks = { delete: update, edit: update, split: update, }; conflitResolver.open(); return; } if (type != "conflit" && done) { 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}" title="${abs}">`; } else { assi += `<input type="checkbox" value="${abs}" name="btn_assiduites_${index}" id="rbtn_${abs}" class="rbtn ${abs}" title="${abs}">`; } }); const conflit = assiduite.type == "conflit" ? "conflit" : ""; const pdp_url = `${getUrl()}/api/etudiant/etudid/${etud.id}/photo?size=small`; let defdem = ""; try { if (etud.id in etudsDefDem) { defdem = etudsDefDem[etud.id] == "D" ? "dem" : "def"; } } catch (_) {} const HTML = `<div class="etud_row ${conflit} ${defdem}" id="etud_row_${ etud.id }"> <div class="index">${index}</div> <div class="name_field"> <img class="pdp" src="${pdp_url}"> <a class="name_set" href="bilan_etud?etudid=${etud.id}"> <h4 class="nom">${etud.nom}</h4> <h5 class="prenom">${etud.prenom}</h5> </a> </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 = Date.removeUTC(conflict[0].date_debut); assiduite.date_fin = Date.removeUTC(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() || readOnly) { 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); //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 ); let mod = getModuleImplId(); etuds_ids = etuds_ids.filter((i) => { return checkInscriptionModule(mod, i); }); 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 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; } function setModuleImplId(assiduite, module = null) { const moduleimpl = module == null ? getModuleImplId() : module; if (moduleimpl === "autre") { if ( assiduite.hasOwnProperty("external_data") && assiduite.external_data != null ) { if (assiduite.external_data.hasOwnProperty("module")) { assiduite.external_data.module = "Autre"; } else { assiduite["external_data"] = { module: "Autre" }; } } else { assiduite["external_data"] = { module: "Autre" }; } assiduite.moduleimpl_id = null; } else { assiduite["moduleimpl_id"] = moduleimpl; if ( assiduite.hasOwnProperty("external_data") && assiduite.external_data != null ) { if (assiduite.external_data.hasOwnProperty("module")) { delete assiduite.external_data.module; } } } return assiduite; } /** * 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: new Date(dateDeb), fin: new Date(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("ajout_assiduite_etud"); } function getCurrentAssiduiteModuleImplId() { const currentAssiduites = getAssiduitesConflict(etudid); if (currentAssiduites.length > 0) { let mod = currentAssiduites[0].moduleimpl_id; if ( mod == null && currentAssiduites[0].hasOwnProperty("external_data") && currentAssiduites[0].external_data != null && currentAssiduites[0].external_data.hasOwnProperty("module") ) { mod = currentAssiduites[0].external_data.module; } 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, "seconds") .toFakeIso()}&date_fin=${date.fin.add(-1, "seconds").toFakeIso()}`, 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) { if (assiduite.etat == "PRESENT") { openAlertModal( "Attention", document.createTextNode("Une présence ne peut être justifiée.") ); return; } const period = { deb: new Date(Date.removeUTC(assiduite.date_debut)), fin: new Date(Date.removeUTC(assiduite.date_fin)), }; const action = (justifs) => { //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 Date(Date.removeUTC(assiduite.date_debut)).toFakeIso(), date_fin: new Date(Date.removeUTC(assiduite.date_fin)).toFakeIso(), raison: raison, etat: etat, }; createJustificatif(justif); generateAllEtudRow(); try { loadAll(); } catch {} }; 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, () => {}, "var(--color-primary)" ); }; 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); errorAlert(); } ); return bool; } function createJustificatif(justif, success = () => {}) { const path = getUrl() + `/api/justificatif/${etudid}/create`; sync_post(path, [justif], success, (data, status) => { //error console.error(data, status); errorAlert(); }); } function getAllJustificatifsFromEtud( etudid, action, order = false, courant = false ) { const url_api = getUrl() + `/api/justificatifs/${etudid}${ order ? "/query?order°".replace("°", courant ? "&courant" : "") : "" }`; //TODO Utiliser async_get au lieu de jquery $.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); errorAlert(); } ); } function errorAlert() { const html = ` <h4>Il peut s'agir d'un problème de droits, ou d'une modification survenue sur le serveur.</h4> <p>Si le problème persiste, demandez de l'aide sur le Discord d'assistance de ScoDoc</p> `; const div = document.createElement("div"); div.innerHTML = html; openAlertModal("Une erreur s'est produite", div); } const moduleimpls = {}; function getModuleImpl(assiduite) { if (assiduite == null) return "Pas de module"; const id = assiduite.moduleimpl_id; if (id == null || id == undefined) { if ( assiduite.hasOwnProperty("external_data") && assiduite.external_data != null && assiduite.external_data.hasOwnProperty("module") ) { return assiduite.external_data.module == "Autre" ? "Tout module" : assiduite.external_data.module; } else { return "Pas de module"; } } if (id in moduleimpls) { return moduleimpls[id]; } const url_api = getUrl() + `/api/moduleimpl/${id}`; sync_get( url_api, (data) => { moduleimpls[id] = `${data.module.code} ${data.module.abbrev}`; }, (data) => { moduleimpls[id] = "Pas de module"; } ); return moduleimpls[id]; } // le nom de l'utilisateur à afficher function getUser(obj) { if ( obj.hasOwnProperty("external_data") && obj.external_data != null && obj.external_data.hasOwnProperty("enseignant") ) { return obj.external_data.enseignant; } return obj.user_nom_complet || obj.user_id; } const inscriptionsModule = {}; function checkInscriptionModule(moduleimpl_id, etudid) { if ([null, "", "autre"].indexOf(moduleimpl_id) !== -1) return true; if (!inscriptionsModule.hasOwnProperty(moduleimpl_id)) { const path = getUrl() + `/api/moduleimpl/${moduleimpl_id}/inscriptions`; sync_get( path, (data, status) => { inscriptionsModule[moduleimpl_id] = data; }, (data, status) => { //error console.error(data, status); errorAlert(); } ); } const etudsInscrits = inscriptionsModule[moduleimpl_id].map((i) => i.etudid); return etudsInscrits.indexOf(Number(etudid)) !== -1; }