// <=== 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; /** * Variable de gestion des conflits */ let modal; let closeBtn; let timeline; let deleteBtn; let splitBtn; let editBtn; let selectedAssiduite; /** * 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 ==>> /** * 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() { 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 && forceModule) { const HTML = `

Attention, le module doit obligatoirement être renseigné.

Cela vient de la configuration du semestre ou plus largement du département.

Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.

`; const content = document.createElement("div"); content.innerHTML = HTML; openAlertModal("Sélection du module", content); return; } getAssiduitesFromEtuds(true); document.querySelector(".selectors").disabled = true; generateMassAssiduites(); generateAllEtudRow(); }; if (!verifyDateInSemester()) { const HTML = `

Attention, la date sélectionnée n'est pas comprise dans le semestre.

Cette page permet l'affichage et la modification des assiduités uniquement pour le semestre sélectionné.

Vous n'aurez donc pas accès aux assiduités.

Appuyer sur "Valider" uniquement si vous souhaitez poursuivre sans modifier la date.

`; const content = document.createElement("div"); content.innerHTML = HTML; openPromptModal("Vérification de la date", content, action); return; } action(); } /** * 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, }); } // <<== 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); } ); }; //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); } ); }; //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); } ); }; //On exécute les fonctions de queue create(); edit(); supprimer(); //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 const action = 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") { field.querySelector(`.rbtn.${action}`).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; //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 = ` Mettre tout le monde :
`; 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 = (time - heure) * 60; if (minutes < 1) { minutes = "00"; } 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; $("#datestr").text(formatDate(date).capitalize()); } function verifyDateInSemester() { const date = new moment.tz( document.querySelector("#tl_date").value, TIMEZONE ); 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 (!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`); } /** * Fonction qui vérifie si une période est dans un interval * Objet période / interval * { * deb: moment.tz(), * fin: moment.tz(), * } * @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 isConflictSameAsTimeLine(conflict) { const tlTimes = getTimeLineTimes(); 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 = document.querySelector("#tl_date").valueAsDate; 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 { : [,]} */ function getAssiduitesFromEtuds(clear, has_formsemestre = true) { const etudIds = Object.keys(etuds).join(","); const formsemestre_id = has_formsemestre ? `formsemestre_id=${getFormSemestreId()}&` : ""; const date_debut = toIsoString(getPrevDate()); const date_fin = toIsoString(getNextDate()); if (clear) { assiduites = {}; } const url_api = getUrl() + `/api/assiduites/group/query?date_debut=${formsemestre_id}${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) => { assiduites[key] = data[key]; }); } }); 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) { const etudAssiduites = assiduites[etudid]; if (!etudAssiduites) { return []; } const period = getTimeLineTimes(); return etudAssiduites.filter((assi) => { const interval = { deb: moment.tz(assi.date_debut, TIMEZONE), fin: moment.tz(assi.date_fin, TIMEZONE), }; return hasTimeConflict(period, 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) { switch (type) { case "création": addToMassActionQueue("creer", { etat: etat, etudid: etudid }); break; case "édition": if (etat === "remove") { addToMassActionQueue("supprimer", assiduite_id); } else { addToMassActionQueue("editer", { etat: etat, assiduite_id: assiduite_id, }); } break; } } 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": openModal(assiduites[etudid]); break; } if (type != "conflit") { document .querySelector(".toast-holder") .appendChild( generateToast( document.createTextNode("L'assiduité a bien été enregistrée.") ) ); } 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 += ``; } else { assi += ``; } }); const conflit = assiduite.type == "conflit" ? "conflit" : ""; const pdp_url = `${getUrl()}/api/etudiant/etudid/${etud.id}/photo?size=small`; const HTML = `
${index}

${etud.nom}

${etud.prenom}
13h
${assi}
`; 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 (isConflictSameAsTimeLine(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"); } } /** * Création de la minitiline d'un étudiant * @param {Array[Assiduité]} assiduitesArray * @returns {HTMLElement} l'élément correspondant à la mini timeline */ function createMiniTimeline(assiduitesArray) { const array = [...assiduitesArray]; const dateiso = document.getElementById("tl_date").value; const timeline = document.createElement("div"); timeline.className = "mini-timeline"; if (isSingleEtud()) { timeline.classList.add("single"); } const timelineDate = moment(dateiso).startOf("day"); const dayStart = timelineDate.clone().add(8, "hours"); const dayEnd = timelineDate.clone().add(18, "hours"); const dayDuration = moment.duration(dayEnd.diff(dayStart)).asMinutes(); const tlTimes = getTimeLineTimes(); const period_assi = { date_debut: tlTimes.deb.format(), date_fin: tlTimes.fin.format(), etat: "CRENEAU", }; array.push(period_assi); array.forEach((assiduité) => { const startDate = moment(assiduité.date_debut); const endDate = moment(assiduité.date_fin); if (startDate.isBefore(dayStart)) { startDate.startOf("day").add(8, "hours"); } if (endDate.isAfter(dayEnd)) { endDate.startOf("day").add(18, "hours"); } const block = document.createElement("div"); block.className = "mini-timeline-block"; const startOffset = moment.duration(startDate.diff(dayStart)).asMinutes(); const duration = moment.duration(endDate.diff(startDate)).asMinutes(); const leftPercentage = (startOffset / dayDuration) * 100; const widthPercentage = (duration / dayDuration) * 100; block.style.left = `${leftPercentage}%`; block.style.width = `${widthPercentage}%`; if (assiduité.etat != "CRENEAU") { if (isSingleEtud()) { block.addEventListener("click", () => { let deb = startDate.hours() + startDate.minutes() / 60; let fin = endDate.hours() + endDate.minutes() / 60; deb = Math.max(8, deb); fin = Math.min(18, fin); setPeriodValues(deb, fin); updateSelectedSelect(getCurrentAssiduiteModuleImplId()); updateJustifyBtn(); }); } //ajouter affichage assiduites on over setupAssiduiteBuble(block, assiduité); } const action = (justificatifs) => { if (justificatifs.length > 0) { let j = "invalid_justified"; justificatifs.forEach((ju) => { if (ju.etat == "VALIDE") { j = "justified"; } }); block.classList.add(j); } }; if (assiduité.etudid) { getJustificatifFromPeriod( { deb: new moment.tz(assiduité.date_debut, TIMEZONE), fin: new moment.tz(assiduité.date_fin, TIMEZONE), }, assiduité.etudid, action ); } switch (assiduité.etat) { case "PRESENT": block.classList.add("present"); break; case "RETARD": block.classList.add("retard"); break; case "ABSENT": block.classList.add("absent"); break; case "CRENEAU": block.classList.add("creneau"); break; default: block.style.backgroundColor = "white"; } timeline.appendChild(block); }); return timeline; } /** * Ajout de la visualisation des assiduités de la mini timeline * @param {HTMLElement} el l'élément survollé * @param {Assiduité} assiduite l'assiduité représentée par l'élément */ function setupAssiduiteBuble(el, assiduite) { if (!assiduite) return; el.addEventListener("mouseenter", (event) => { const bubble = document.querySelector(".assiduite-bubble"); bubble.className = "assiduite-bubble"; bubble.classList.add("is-active", assiduite.etat.toLowerCase()); bubble.innerHTML = ""; const idDiv = document.createElement("div"); idDiv.className = "assiduite-id"; idDiv.textContent = `ID: ${assiduite.assiduite_id}`; bubble.appendChild(idDiv); const periodDivDeb = document.createElement("div"); periodDivDeb.className = "assiduite-period"; periodDivDeb.textContent = `${formatDateModal(assiduite.date_debut)}`; bubble.appendChild(periodDivDeb); const periodDivFin = document.createElement("div"); periodDivFin.className = "assiduite-period"; periodDivFin.textContent = `${formatDateModal(assiduite.date_fin)}`; bubble.appendChild(periodDivFin); const stateDiv = document.createElement("div"); stateDiv.className = "assiduite-state"; stateDiv.textContent = `État: ${assiduite.etat.capitalize()}`; bubble.appendChild(stateDiv); const userIdDiv = document.createElement("div"); userIdDiv.className = "assiduite-user_id"; userIdDiv.textContent = `saisi le ${formatDateModal( assiduite.entry_date, "à" )} \npar ${getUserFromId(assiduite.user_id)}`; bubble.appendChild(userIdDiv); bubble.style.left = `${event.clientX - bubble.offsetWidth / 2}px`; bubble.style.top = `${event.clientY + 20}px`; }); el.addEventListener("mouseout", () => { const bubble = document.querySelector(".assiduite-bubble"); bubble.classList.remove("is-active"); }); } /** * 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 ==> /** * Mise à jour du modal de conflit * @param {Array[Assiduité]} assiduiteList Liste des assiduités de l'étudiant */ function refreshModal(assiduiteList) { const tlTime = getTimeLineTimes(); renderTimeline(assiduiteList, { date_debut: tlTime.deb, date_fin: tlTime.fin, }); } /** * Ouverture du modal de conflit * @param {Array[Assiduité]} assiduiteList Liste des assiduités de l'étudiant */ function openModal(assiduiteList) { modal.style.display = "block"; const tlTime = getTimeLineTimes(); renderTimeline(assiduiteList, { date_debut: tlTime.deb, date_fin: tlTime.fin, }); } /** * Fermeture du modal de conflit */ function closeModal() { modal.style.display = "none"; } /** * Génération du modal * @param {Array[Assiduité]} assiduites la liste des assiduités à afficher * @param {Période} specialAssiduite Une assiduité représentant la période conflictuelle */ function renderTimeline(assiduites, specialAssiduite) { const timeLabels = document.querySelector(".time-labels"); const assiduitesContainer = document.querySelector(".assiduites-container"); timeLabels.innerHTML = ""; assiduitesContainer.innerHTML = '
'; // Ajout des labels d'heure sur la frise chronologique // TODO permettre la modification des bornes (8 et 18) for (let i = 8; i <= 18; i++) { const timeLabel = document.createElement("div"); timeLabel.className = "time-label"; timeLabel.textContent = i < 10 ? `0${i}:00` : `${i}:00`; timeLabels.appendChild(timeLabel); } //Placement de la période conflictuelle sur la timeline const specialAssiduiteEl = document.querySelector(".assiduite-special"); specialAssiduiteEl.style.width = getWidth( specialAssiduite.date_debut, specialAssiduite.date_fin ); specialAssiduiteEl.style.left = getLeftPosition(specialAssiduite.date_debut); specialAssiduiteEl.style.top = "0"; specialAssiduiteEl.style.zIndex = "0"; // Place l'assiduité spéciale en arrière-plan assiduitesContainer.appendChild(specialAssiduiteEl); const interval = { deb: new moment.tz(getDate(), TIMEZONE), fin: new moment.tz(getNextDate(), TIMEZONE), }; //Placement des assiduités sur la timeline assiduites.forEach((assiduite) => { const period = { deb: new moment.tz(assiduite.date_debut, TIMEZONE), fin: new moment.tz(assiduite.date_fin, TIMEZONE), }; if (!hasTimeConflict(period, interval)) { return; } const el = document.createElement("div"); el.className = "assiduite"; el.style.backgroundColor = getColor(assiduite.etat); el.style.width = getWidth(assiduite.date_debut, assiduite.date_fin); el.style.left = getLeftPosition(assiduite.date_debut); el.style.top = "10px"; el.setAttribute("data-id", assiduite.assiduite_id); el.addEventListener("click", () => selectAssiduite(assiduite)); // Ajout des informations dans la visualisation d'une assiduité const infoContainer = document.createElement("div"); infoContainer.className = "assiduite-info"; const idDiv = document.createElement("div"); idDiv.className = "assiduite-id"; idDiv.textContent = `ID: ${assiduite.assiduite_id}`; infoContainer.appendChild(idDiv); const periodDivDeb = document.createElement("div"); periodDivDeb.className = "assiduite-period"; periodDivDeb.textContent = `${formatDateModal(assiduite.date_debut)}`; infoContainer.appendChild(periodDivDeb); const periodDivFin = document.createElement("div"); periodDivFin.className = "assiduite-period"; periodDivFin.textContent = `${formatDateModal(assiduite.date_fin)}`; infoContainer.appendChild(periodDivFin); const stateDiv = document.createElement("div"); stateDiv.className = "assiduite-state"; stateDiv.textContent = `État: ${assiduite.etat.capitalize()}`; infoContainer.appendChild(stateDiv); const userIdDiv = document.createElement("div"); userIdDiv.className = "assiduite-user_id"; userIdDiv.textContent = `saisi le ${formatDateModal( assiduite.entry_date, "à" )} \npar ${getUserFromId(assiduite.user_id)}`; infoContainer.appendChild(userIdDiv); el.appendChild(infoContainer); assiduitesContainer.appendChild(el); }); } /** * Transformation d'une date de début en position sur la timeline * @param {String} start * @returns {String} un déplacement par rapport à la gauche en % */ function getLeftPosition(start) { const startTime = new moment.tz(start, TIMEZONE); const startMins = (startTime.hours() - 8) * 60 + startTime.minutes(); return (startMins / (18 * 60 - 8 * 60)) * 100 + "%"; } /** * Ajustement de l'espacement vertical entre les assiduités superposées * @param {HTMLElement} container le conteneur des assiduités * @param {String} start la date début de l'assiduité à placer * @param {String} end la date de fin de l'assiduité à placer * @returns {String} La position en px */ function getTopPosition(container, start, end) { const overlaps = (a, b) => { return a.start < b.end && a.end > b.start; }; const startTime = new moment.tz(start, TIMEZONE); const endTime = new moment.tz(end, TIMEZONE); const assiduiteDuration = { start: startTime, end: endTime }; let position = 0; let hasOverlap = true; while (hasOverlap) { hasOverlap = false; Array.from(container.children).some((el) => { const elStart = new moment.tz(el.getAttribute("data-start")); const elEnd = new moment.tz(el.getAttribute("data-end")); const elDuration = { start: elStart, end: elEnd }; if (overlaps(assiduiteDuration, elDuration)) { position += 25; // Pour ajuster l'espacement vertical entre les assiduités superposées hasOverlap = true; return true; } return false; }); } return position + "px"; } /** * Transformation d'un état en couleur * @param {String} state l'état * @returns {String} la couleur correspondant à l'état */ function getColor(state) { switch (state) { case "PRESENT": return "#9CF1AF"; case "ABSENT": return "#F1A69C"; case "RETARD": return "#F1D99C"; default: return "gray"; } } /** * Calcule de la largeur de l'assiduité sur la timeline * @param {String} start date iso de début * @param {String} end date iso de fin * @returns {String} la taille en % */ function getWidth(start, end) { const startTime = new moment.tz(start, TIMEZONE); const endTime = new moment.tz(end, TIMEZONE); const duration = (endTime - startTime) / 1000 / 60; return (duration / (18 * 60 - 8 * 60)) * 100 + "%"; } /** * Sélection d'une assiduité sur la timeline * @param {Assiduité} assiduite l'assiduité sélectionnée */ function selectAssiduite(assiduite) { // Désélectionner l'assiduité précédemment sélectionnée if (selectedAssiduite) { const prevSelectedEl = document.querySelector( `.assiduite[data-id="${selectedAssiduite.assiduite_id}"]` ); if (prevSelectedEl) { prevSelectedEl.classList.remove("selected"); } } // Sélectionner la nouvelle assiduité selectedAssiduite = assiduite; const selectedEl = document.querySelector( `.assiduite[data-id="${assiduite.assiduite_id}"]` ); if (selectedEl) { selectedEl.classList.add("selected"); } //Mise à jour de la partie information du modal const selectedModal = document.querySelector(".modal-assiduite-content"); selectedModal.classList.add("show"); document.getElementById("modal-assiduite-id").textContent = assiduite.assiduite_id; document.getElementById( "modal-assiduite-user" ).textContent = `saisi le ${formatDateModal( assiduite.entry_date, "à" )} \npar ${getUserFromId(assiduite.user_id)}`; document.getElementById("modal-assiduite-module").textContent = assiduite.moduleimpl_id; document.getElementById("modal-assiduite-deb").textContent = formatDateModal( assiduite.date_debut ); document.getElementById("modal-assiduite-fin").textContent = formatDateModal( assiduite.date_fin ); document.getElementById("modal-assiduite-etat").textContent = assiduite.etat.capitalize(); //Activation des boutons d'actions de conflit deleteBtn.disabled = false; splitBtn.disabled = false; editBtn.disabled = false; } /** * Suppression de l'assiduité sélectionnée */ function deleteAssiduiteModal() { if (!selectedAssiduite) return; deleteAssiduite(selectedAssiduite.assiduite_id); actualizeEtud(selectedAssiduite.etudid); refreshModal(assiduites[selectedAssiduite.etudid]); // Désélection de l'assiduité resetSelection(); } /** * Division d'une assiduité * @param {Assiduité} assiduite l'assiduité sélectionnée */ function splitAssiduiteModal(assiduite) { //Préparation du prompt const htmlPrompt = `Entrez l'heure de séparation (HH:mm) : `; const fieldSet = document.createElement("fieldset"); fieldSet.classList.add("fieldsplit"); fieldSet.innerHTML = htmlPrompt; //Callback de division const success = () => { const separatorTime = document.getElementById("promptTime").value; const dateString = document.querySelector("#tl_date").value + `T${separatorTime}`; const separtorDate = new moment.tz(dateString, TIMEZONE); const assiduite_debut = new moment.tz(assiduite.date_debut, TIMEZONE); const assiduite_fin = new moment.tz(assiduite.date_fin, TIMEZONE); if ( separtorDate.isAfter(assiduite_debut) && separtorDate.isBefore(assiduite_fin) ) { const assiduite_avant = { etat: assiduite.etat, date_debut: assiduite_debut.format(), date_fin: separtorDate.format(), }; const assiduite_apres = { etat: assiduite.etat, date_debut: separtorDate.format(), date_fin: assiduite_fin.format(), }; if (assiduite.moduleimpl_id) { assiduite_apres["moduleimpl_id"] = assiduite.moduleimpl_id; assiduite_avant["moduleimpl_id"] = assiduite.moduleimpl_id; } deleteAssiduite(assiduite.assiduite_id); const path = getUrl() + `/api/assiduite/${assiduite.etudid}/create`; sync_post( path, [assiduite_avant, assiduite_apres], (data, status) => { //success }, (data, status) => { //error console.error(data, status); } ); actualizeEtud(assiduite.etudid); refreshModal(assiduites[assiduite.etudid]); resetSelection(); } else { const att = document.createTextNode( "L'heure de séparation doit être compris dans la période de l'assiduité sélectionnée." ); openAlertModal("Attention", att, "", "#ecb52a"); } }; openPromptModal("Entrée demandée", fieldSet, success, () => {}, "#37f05f"); } /** * Modification d'une assiduité conflictuelle * @param {Assiduité} selectedAssiduite l'assiduité sélectionnée */ function editAssiduiteModal(selectedAssiduite) { if (!selectedAssiduite) return; //Préparation du modal d'édition const htmlPrompt = `Entrez l'état de l'assiduité : `; const fieldSet = document.createElement("fieldset"); fieldSet.classList.add("fieldsplit"); fieldSet.innerHTML = htmlPrompt; //Callback d'action d'édition const success = () => { const newState = document.getElementById("promptSelect").value; if (!["present", "absent", "retard"].includes(newState.toLowerCase())) { const att = document.createTextNode( "L'état doit être 'present', 'absent' ou 'retard'." ); openAlertModal("Attention", att, "", "#ecb52a"); return; } // Actualiser l'affichage editAssiduite(selectedAssiduite.assiduite_id, newState); actualizeEtud(selectedAssiduite.etudid); refreshModal(assiduites[selectedAssiduite.etudid]); // Désélection de l'assiduité resetSelection(); }; //Affichage du prompt openPromptModal("Entrée demandée", fieldSet, success, () => {}, "#37f05f"); } /** * Remise à zéro de la sélection * Désactivation des boutons d'actions de conflit */ function resetSelection() { selectedAssiduite = null; deleteBtn.disabled = true; splitBtn.disabled = true; editBtn.disabled = true; document.querySelector(".modal-assiduite-content").classList.remove("show"); } /** * Ajout des évents sur les boutons du modal */ window.onload = () => { modal = document.getElementById("myModal"); closeBtn = document.querySelector(".close"); timeline = document.getElementById("timeline"); deleteBtn = document.getElementById("delete"); splitBtn = document.getElementById("split"); editBtn = document.getElementById("edit"); selectedAssiduite = null; closeBtn?.addEventListener("click", closeModal); deleteBtn?.addEventListener("click", deleteAssiduiteModal); splitBtn?.addEventListener("click", () => { if (selectedAssiduite) { splitAssiduiteModal(selectedAssiduite); } }); editBtn?.addEventListener("click", () => { if (selectedAssiduite) { editAssiduiteModal(selectedAssiduite); } }); }; // <<== 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(`/ScoDoc/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 = `Entrez l'état du justificatif : Raison: `; 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]); } console.warn(data); }, (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); } ); }