From c3cb1da561af8c76a4a54b52de6d60077481fca0 Mon Sep 17 00:00:00 2001 From: Iziram Date: Mon, 11 Mar 2024 11:39:06 +0100 Subject: [PATCH] =?UTF-8?q?Assiduit=C3=A9=20:=20refonte=20signal=5Fassidui?= =?UTF-8?q?tes=5Fgroup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/css/assiduites.css | 284 +- app/static/icons/solveur_conflits.svg | 17 + app/static/js/assiduites.js | 2617 +++++------------ .../pages/signal_assiduites_group.j2 | 129 +- app/templates/assiduites/widgets/conflict.j2 | 744 ++--- .../assiduites/widgets/minitimeline.j2 | 82 +- 6 files changed, 1329 insertions(+), 2544 deletions(-) create mode 100644 app/static/icons/solveur_conflits.svg diff --git a/app/static/css/assiduites.css b/app/static/css/assiduites.css index da872491b..02730449c 100644 --- a/app/static/css/assiduites.css +++ b/app/static/css/assiduites.css @@ -6,7 +6,9 @@ --color-justi: #29b990; --color-justi-clair: #48f6ff; --color-justi-attente: yellow; - --color-justi-attente-stripe: #29b990; /* pink #fa25cb; */ /* #789dbb;*/ + --color-justi-attente-stripe: #29b990; + /* pink #fa25cb; */ + /* #789dbb;*/ --color-justi-modifie: rgb(255, 230, 0); --color-justi-invalide: #a84476; --color-nonwork: #badfff; @@ -28,27 +30,23 @@ --color-defaut-dark: #444; --color-default-text: #1f1f1f; - --motif-justi: repeating-linear-gradient( - 135deg, - transparent, - transparent 4px, - var(--color-justi) 4px, - var(--color-justi) 8px - ); - --motif-justi-invalide: repeating-linear-gradient( - -135deg, - transparent, - transparent 4px, - var(--color-justi-invalide) 4px, - var(--color-justi-invalide) 8px - ); + --motif-justi: repeating-linear-gradient(135deg, + transparent, + transparent 4px, + var(--color-justi) 4px, + var(--color-justi) 8px); + --motif-justi-invalide: repeating-linear-gradient(-135deg, + transparent, + transparent 4px, + var(--color-justi-invalide) 4px, + var(--color-justi-invalide) 8px); } * { box-sizing: border-box; } -.selectors > * { +.selectors>* { margin: 10px 0; } @@ -339,6 +337,11 @@ background-image: url(../icons/retard.svg); } +.rbtn.conflit::before { + background-color: var(--color-absent); + background-image: url(../icons/solveur_conflits.svg); +} + .rbtn:checked:before { outline: 5px solid var(--color-primary); border-radius: 50%; @@ -405,29 +408,11 @@ .assiduite { position: absolute; top: 20px; - cursor: pointer; border-radius: 4px; z-index: 10; height: 100px; padding: 4px; -} - -.assiduite-info { - display: flex; - flex-direction: column; - height: 100%; - justify-content: space-between; -} - -.assiduite-id, -.assiduite-period, -.assiduite-state, -.assiduite-user_id { - font-size: 12px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - text-align: center; + border: 1px solid #444; } .assiduites-container { @@ -438,7 +423,7 @@ margin-bottom: 10px; } -.action-buttons { +.modal-buttons { position: absolute; text-align: center; display: flex; @@ -449,48 +434,35 @@ bottom: 5%; } -/* Ajout de la classe CSS pour la bordure en pointillés */ -.assiduite.selected { - border: 2px dashed black; -} .assiduite-special { height: 120px; position: absolute; z-index: 5; - border: 2px solid #000; - background-color: rgba(36, 36, 36, 0.25); - background-image: repeating-linear-gradient( - 135deg, - transparent, - transparent 5px, - rgba(81, 81, 81, 0.61) 5px, - rgba(81, 81, 81, 0.61) 10px - ); + border: 5px solid var(--color-primary); + /* background-color: rgba(36, 36, 36, 0.25); + background-image: repeating-linear-gradient(135deg, + transparent, + transparent 5px, + rgba(81, 81, 81, 0.61) 5px, + rgba(81, 81, 81, 0.61) 10px); */ border-radius: 5px; } -/*<== Info sur l'assiduité sélectionnée ==>*/ -.modal-assiduite-content { - background-color: #fefefe; - margin: 5% auto; - padding: 20px; - border: 1px solid #888; - width: max-content; - position: relative; - border-radius: 10px; - display: none; +.assiduite .assiduite-bubble { + top: 5px; + left: 50%; + transform: translateX(-50%); } -.modal-assiduite-content.show { - display: block; -} - -.modal-assiduite-content .infos { +.action-buttons { display: flex; - flex-direction: column; - justify-content: space-evenly; - align-items: flex-start; + justify-content: center; + align-items: center; + flex-wrap: wrap; + gap: 2px; + height: 100%; + } /*<=== Mass Action ==>*/ @@ -500,57 +472,16 @@ justify-content: flex-start; align-items: center; width: 100%; - margin: 2% 0; + gap: 4px; } -.mass-selection span { - margin: 0 1%; -} + .mass-selection .rbtn { background-color: transparent; cursor: pointer; } -/*<== Loader ==> */ - -.loader-container { - display: none; - /* Cacher le loader par défaut */ - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.5); - /* Fond semi-transparent pour bloquer les clics */ - z-index: 9999; - /* Placer le loader au-dessus de tout le contenu */ -} - -.loader { - border: 6px solid #f3f3f3; - border-radius: 50%; - border-top: 6px solid var(--color-primary); - width: 60px; - height: 60px; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - animation: spin 2s linear infinite; -} - -@keyframes spin { - 0% { - transform: translate(-50%, -50%) rotate(0deg); - } - - 100% { - transform: translate(-50%, -50%) rotate(360deg); - } -} - .fieldsplit { display: flex; justify-content: flex-start; @@ -569,7 +500,7 @@ flex-direction: column; } -#page-assiduite-content > * { +#page-assiduite-content>* { margin: 1.5% 0; } @@ -649,6 +580,7 @@ margin-right: 24px; padding: 12px; } + #options-tableau label { font-weight: normal; margin-right: 12px; @@ -657,15 +589,20 @@ section.assi-form { margin-bottom: 12px; } + table.liste_assi td.date { width: 140px; } + table.liste_assi.dataTable tbody td.date-debut { padding-left: 12px; } + table.liste_assi td.actions { - white-space: nowrap; /* boutons horizontalement */ + white-space: nowrap; + /* boutons horizontalement */ } + table.liste_assi td.actions a:last-child { padding-right: 12px; } @@ -673,31 +610,154 @@ table.liste_assi td.actions a:last-child { tr.row-assiduite td { border-bottom: 1px solid grey; } + table.liste_assi tbody tr td.assi-type { padding-left: 8px; padding-right: 4px; } + tr.row-assiduite.absent td.assi-type { background-color: var(--color-absent-clair); } + tr.row-assiduite.absent.justifiee td.assi-type { background-color: var(--color-absent-justi); } + tr.row-assiduite.retard td.assi-type { background-color: var(--color-retard); } + tr.row-assiduite.present td.assi-type { background-color: var(--color-present); } + tr.row-justificatif.valide td.assi-type { background-color: var(--color-justi); } + tr.row-justificatif.attente td.assi-type { background-color: var(--color-justi-attente); } + tr.row-justificatif.modifie td.assi-type { background-color: var(--color-justi-modifie); } + tr.row-justificatif.non_valide td.assi-type { background-color: var(--color-justi-invalide); } + +/* + +<== Loader ==> + +*/ +/* HTML:
*/ +.loader { + width: 80px; + height: 70px; + border: 5px solid #000; + padding: 0 8px; + box-sizing: border-box; + background: + linear-gradient(#fff 0 0) 0 0/8px 20px, + linear-gradient(#fff 0 0) 100% 0/8px 20px, + radial-gradient(farthest-side, #fff 90%, #0000) 0 5px/8px 8px content-box, + #000; + background-repeat: no-repeat; + animation: l3 2s infinite linear; +} + +@keyframes l3 { + 25% { + background-position: 0 0, 100% 100%, 100% calc(100% - 5px) + } + + 50% { + background-position: 0 100%, 100% 100%, 0 calc(100% - 5px) + } + + 75% { + background-position: 0 100%, 100% 0, 100% 5px + } +} + +#loader { + width: 100%; + height: 100%; + position: fixed; + top: 50%; + left: 50%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + transform: translate(-50%, -50%); + z-index: 1000; + background-color: rgba(255, 255, 255, 0.8); +} + +/** + * <== Couleurs ==> + */ + +.color.present { + background-color: var(--color-present) !important; +} + +.color.absent { + background-color: var(--color-absent) !important; +} + +.color.absent.est_just { + background-color: var(--color-absent-justi) !important; +} + +.color.retard { + background-color: var(--color-retard) !important; +} + +.color.retard.est_just { + background-color: var(--color-retard-justi) !important; +} + +.color.nonwork { + background-color: var(--color-nonwork) !important; +} + +.color { + background-color: var(--color-defaut) !important; +} + +.color.est_just.sans_etat::before { + content: ""; + position: absolute; + width: 25%; + height: 100%; + background-color: var(--color-justi) !important; + right: 0; +} + +.color.invalide::before { + content: ""; + position: absolute; + width: 25%; + height: 100%; + right: 0; + background-color: var(--color-justi-invalide) !important; +} + +.color.attente::before, +.color.modifie::before { + content: ""; + position: absolute; + width: 25%; + height: 100%; + right: 0; + background: repeating-linear-gradient(to bottom, + var(--color-justi-attente-stripe) 0px, + var(--color-justi-attente-stripe) 4px, + var(--color-justi-attente) 4px, + var(--color-justi-attente) 7px) !important; +} \ No newline at end of file diff --git a/app/static/icons/solveur_conflits.svg b/app/static/icons/solveur_conflits.svg new file mode 100644 index 000000000..1dadba2df --- /dev/null +++ b/app/static/icons/solveur_conflits.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index 8141dec33..91fee8cfe 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -1,30 +1,17 @@ -// TODO : Supprimer les fonctions non utilisées + optimiser les fonctions utilisées -// <=== CONSTANTS and GLOBALS ===> +/** + * + * Ensemble des fonctions liées à la gestion des assiduités + * Créé par : HARTMANN Matthias (Iziram) + * + */ -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; +/** + * <== OUTILS ==> + */ /** * Ajout d'une fonction `capitalize` sur tous les strings - * alice.capitalize() -> Alice + * "alice".capitalize() -> "Alice" */ Object.defineProperty(String.prototype, "capitalize", { value: function () { @@ -33,179 +20,6 @@ Object.defineProperty(String.prototype, "capitalize", { 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); - } - }); - }); -} - -function updateEtudList() { - 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; - } - }); - } - }); - }); - - getAssiduitesFromEtuds(true); - generateAllEtudRow(); -} - -/** - * 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 = ` -

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; - } - - generateMassAssiduites(); - - getAssiduitesFromEtuds(true); - generateAllEtudRow(); - - btn.remove(); - // Auto actualisation - $("#tl_date").on("change", updateEtudList); - $("#group_ids_sel").on("change", updateEtudList); - - onlyAbs(); - }; - - 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(); -} - -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 @@ -213,43 +27,24 @@ function sync_get(path, success, errors) { * @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); - } + const response = fetch(path); + response + .then((response) => { + if (response.ok) { + response.json().then((data) => { + success(data, "success"); + }); + } 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 @@ -283,1679 +78,325 @@ async function async_post(path, data, success, errors) { return response; } -// <<== Gestion des actions de masse ==>> -const massActionQueue = new Map(); - /** - * Cette fonction remet à zero la gestion des actions de masse + * Récupère les étudiants en fonction des groupes sélectionnés + * @param {Array} groupIds - Les identifiants des groupes pour lesquels récupérer les étudiants. + * @returns {Promise} Un objet contenant les étudiants, indexés par leur identifiant. */ -function resetMassActionQueue() { - massActionQueue.set("supprimer", []); - massActionQueue.set("editer", []); - massActionQueue.set("creer", []); -} +async function recupEtuds(groupIds) { + const etuds = new Map(); + if (groupIds == null || groupIds.length == 0) return etuds; -/** - * 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 = ` -

Aucun module n'a été spécifié

- `; - 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 = ` -

Aucun module n'a été spécifié

- `; - 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 ${etat - .capitalize() - .trim()}`; - } else { - span.innerHTML = `Aucun étudiant n'a été mis ${etat - .capitalize() - .trim()}`; - } - 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(); + // Créer un tableau de promesses pour chaque requête GET asynchrone. + let requests = groupIds.map((groupId) => + fetch(`../../api/group/${groupId}/etudiants`) + .then((response) => { + if (!response.ok) { + throw new Error("Network response was not ok"); } - } 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() { - if (readOnly || document.querySelector(".mass-selection") != null) return; - 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() || 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"); - - date = lastWorkDay; - - dateStr = formatDate(lastWorkDay, { - dateStyle: "full", - timeZone: SCO_TIMEZONE, - }).capitalize(); - - $(dateInput).datepicker("setDate", date); - $(dateInput).change(); - } - - 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 {} - } - }); - - //Initialisation du datepicker - // sinon on ne peut pas le mettre à jour - // XXX TODO-assiduite : finir tester + éviter duplication code avec scodoc.js - $(input).datepicker({ - showOn: "button", - buttonImage: "/ScoDoc/static/icons/calendar_img.png", - buttonImageOnly: true, - dateFormat: "dd/mm/yy", - duration: "fast", - firstDay: 1, // Start with Monday - dayNames: [ - "Dimanche", - "Lundi", - "Mardi", - "Mercredi", - "Jeudi", - "Vendredi", - "Samedi", - ], - dayNamesMin: ["Di", "Lu", "Ma", "Me", "Je", "Ve", "Sa"], - dayNamesShort: ["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam"], - monthNames: [ - "Janvier", - "Février", - "Mars", - "Avril", - "May", - "Juin", - "Juillet", - "Août", - "Septembre", - "Octobre", - "Novembre", - "Décembre", - ], - monthNamesShort: [ - "Jan", - "Fév", - "Mar", - "Avr", - "Mai", - "Juin", - "Juil", - "Aoû", - "Sep", - "Oct", - "Nov", - "Déc", - ], - }); - $(input).datepicker( - "option", - $.extend({ showMonthAfterYear: false }, $.datepicker.regional["fr"]) - ); - - 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 { : [,]} - */ -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 response.json(); + }) + .then((data) => { + data.forEach((etud) => { + etuds.set(etud.id, etud); }); + }) + .catch((error) => + console.error( + "There has been a problem with your fetch operation:", + error + ) + ) + ); + + // Attendre que toutes les promesses dans le tableau `requests` soient résolues. + await Promise.all(requests); + + return etuds; +} +/** + * Récupère l'assiduité des étudiants pour une date donnée + * @param {Map} etuds + * @param {Date} date + */ +async function recupAssiduites(etuds, date) { + const etudIds = [...etuds.keys()].join(","); + + const date_debut = date.add(-1, "days").format("YYYY-MM-DDTHH:mm"); + const date_fin = date.add(2, "days").format("YYYY-MM-DDTHH:mm"); + + url = + `../../api/assiduites/group/query?date_debut=${date_debut}` + + `&date_fin=${date_fin}&etudids=${etudIds}&with_justifs`; + + await fetch(url) + .then((res) => { + if (!res.ok) { + throw new Error("Network response was not ok"); + } + return res.json(); + }) + .then((data) => { + Object.keys(data).forEach((etudid) => { + const etud = etuds.get(Number(etudid)); + const assiduites = data[etudid]; + etud.assiduites = assiduites; }); - } - }); - return assiduites; + }) + .catch((error) => + console.error( + "There has been a problem with your fetch operation:", + error + ) + ); } /** - * 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 + * Génération ligne étudiante */ -function createAssiduite(etat, etudid) { - const tlTimes = getTimeLineTimes(); - let assiduite = { - date_debut: tlTimes.deb.toFakeIso(), - date_fin: tlTimes.fin.toFakeIso(), - etat: etat, +function creerLigneEtudiant(etud, index) { + let currentAssiduite = { + etat: "", + type: "creation", + assiduite_id: -1, + date_debut: null, + date_fin: null, }; - assiduite = setModuleImplId(assiduite); + function recupConflitsAssiduites(assiduites) { + const period = getPeriodAsDate(); - if (!hasModuleImpl(assiduite) && window.forceModule) { - const html = ` -

Aucun module n'a été spécifié

- `; - 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 = ` -

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); - } - if ( - data.errors["0"].message == "L'étudiant n'est pas inscrit au module" - ) { - const HTML = ` -

Attention, l'étudiant n'est pas inscrit à ce module.

-

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); - } - 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 = ` -

Aucun module n'a été spécifié

- `; - 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 = ` -

Attention, le module doit obligatoirement être renseigné.

-

Voir configuration du semestre ou du département.

- `; - - 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 = ` -

Attention, l'étudiant n'est pas inscrit à ce module.

- `; - - 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 = ` -

L'assiduité n'a pas pu être enregistrée car un autre évènement - existe sur la période sélectionnée

- `; - - 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 = ` -

Aucune assiduité n'a pû être éditée

- `; - 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 = ` -

Aucun module n'a été spécifié

- `; - 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 = ` -

L'étudiant n'est pas inscrit à ce module

- `; - 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 = ` -

Un module doit être spécifié

- `; - 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) => { + return assiduites.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" : "") - : "" - }`; - async_get( - url_api, - (data) => { - assiduites[etudid] = data; - action(data); - }, - (_) => {} - ); -} - -/** - * 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) présent(e)"; - break; - case "RETARD": - etatAffiche = - "%etud% a été noté(e) en retard"; - break; - case "ABSENT": - etatAffiche = - "%etud% a été noté(e) absent(e)"; - 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) + return ( + period.deb.isBefore(interval.fin) && period.fin.isAfter(interval.deb) ); - } - - 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 += ``; - } - }); - if (readOnly) assi = ""; - const conflit = assiduite.type == "conflit" && !readOnly ? "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 = `
- -
${index}
- -
-
-
-
-
- - ${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 = 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; - // } - const etud_hodler = document.querySelector(".etud_holder"); - if (etud_hodler) etud_hodler.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 conflits = readOnly ? [] : recupConflitsAssiduites(etud.assiduites); - const success = () => { - const raison = document.getElementById("promptText").value; - const etat = document.getElementById("promptSelect").value; + if (conflits.length > 0) { + currentAssiduite = conflits[0]; - //créer justificatif + const conflitsPeriode = { + deb: new Date(Date.removeUTC(currentAssiduite.date_debut)), + fin: new Date(Date.removeUTC(currentAssiduite.date_fin)), + }; + const period = getPeriodAsDate(); + currentAssiduite.type = + period.deb.isSame(conflitsPeriode.deb) && + period.fin.isSame(conflitsPeriode.fin) + ? "edition" + : "conflit"; + } - 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, - }; + const ligneEtud = document.createElement("div"); + ligneEtud.classList.add("etud_row"); + if (Object.keys(etudsDefDem).includes(etud.id)) { + ligneEtud.classList.add(etudsDefDem[etud.id] == "D" ? "dem" : "def"); + } + ligneEtud.id = `etud_row_${etud.id}`; - createJustificatif(justif); + if (currentAssiduite.type === "conflit" && !readOnly) + ligneEtud.classList.add("conflit"); - generateAllEtudRow(); - try { - loadAll(); - } catch {} + // div index avec l'index + const indexDiv = document.createElement("div"); + indexDiv.classList.add("index"); + indexDiv.textContent = index; + + ligneEtud.appendChild(indexDiv); + + // div name_field + + const nameField = document.createElement("div"); + nameField.classList.add("name_field"); + + if ($("#pdp").is(":checked")) { + const pdp = document.createElement("img"); + pdp.src = `../../api/etudiant/etudid/${etud.id}/photo?size=small`; + pdp.alt = `${etud.nom} ${etud.prenom}`; + pdp.classList.add("pdp"); + nameField.appendChild(pdp); + } + + const nameSet = document.createElement("a"); + nameSet.classList.add("name_set"); + nameSet.href = `bilan_etud?etudid=${etud.id}`; + + const nom = document.createElement("h4"); + nom.classList.add("nom"); + nom.textContent = etud.nom; + + const prenom = document.createElement("h5"); + prenom.classList.add("prenom"); + prenom.textContent = etud.prenom; + + nameSet.appendChild(nom); + nameSet.appendChild(prenom); + + nameField.appendChild(nameSet); + ligneEtud.appendChild(nameField); + + // div assiduites_bar + + const assiduitesBar = document.createElement("div"); + assiduitesBar.classList.add("assiduites_bar"); + + const prevDateAssi = document.createElement("div"); + prevDateAssi.id = "prevDateAssi"; + + function recupDerniereAssiduite(assiduites) { + const period = { + deb: $("#date").datepicker("getDate").add(-1, "days"), + fin: $("#date").datepicker("getDate"), }; - const content = document.createElement("fieldset"); + const lastAssiduite = assiduites + .filter((assi) => { + const interval = { + deb: new Date(Date.removeUTC(assi.date_debut)), + fin: new Date(Date.removeUTC(assi.date_fin)), + }; - const htmlPrompt = `Entrez l'état du justificatif : - - Raison: - - `; + return ( + period.deb.isBefore(interval.fin) && period.fin.isAfter(interval.deb) + ); + }) + .sort((a, b) => { + return ( + new Date(Date.removeUTC(b.date_debut)) - + new Date(Date.removeUTC(a.date_debut)) + ); + }) + .pop(); - content.innerHTML = htmlPrompt; - - openPromptModal( - "Nouveau justificatif (Rapide)", - content, - success, - () => {}, - "var(--color-primary)" - ); - }; - if (assiduite.etudid) { - getJustificatifFromPeriod(period, assiduite.etudid, action); + return lastAssiduite ?? null; } + + const lastAssiduite = recupDerniereAssiduite(etud.assiduites); + prevDateAssi.classList.add(lastAssiduite?.etat.toLowerCase() ?? "vide"); + setupAssiduiteBubble(prevDateAssi, lastAssiduite); + assiduitesBar.appendChild(prevDateAssi); + + // div minitimeline + assiduitesBar.appendChild(createMiniTimeline(etud.assiduites)); + ligneEtud.appendChild(assiduitesBar); + + // fieldset btns_field single + const btnsField = document.createElement("fieldset"); + btnsField.classList.add("btns_field", "single"); + btnsField.setAttribute("etudid", etud.id); + btnsField.setAttribute("type", currentAssiduite.type); + btnsField.setAttribute("assiduite_id", currentAssiduite.assiduite_id); + + // Création des boutons d'assiduités + if (readOnly) { + } else if (currentAssiduite.type != "conflit") { + ["present", "retard", "absent"].forEach((abs) => { + const btn = document.createElement("input"); + btn.type = "checkbox"; + btn.value = abs; + btn.name = `btn_assiduites_${index}`; + btn.id = `rbtn_${abs}`; + btn.classList.add("rbtn", abs); + btn.title = abs; + + btn.checked = abs === currentAssiduite?.etat.toLowerCase(); + + // Une seule checkbox à la fois + btn.addEventListener("click", () => { + Array.from(btn.parentElement.children).forEach((chbox) => { + if (chbox.checked && chbox.value !== btn.value) { + chbox.checked = false; + } + }); + }); + + // Action au clic + btn.addEventListener("click", (e) => { + actionAssiduite( + etud, + btn.value, + currentAssiduite.type, + currentAssiduite.type == "edition" ? currentAssiduite : null + ); + e.preventDefault(); + }); + + btnsField.appendChild(btn); + }); + } else { + const btn = document.createElement("input"); + btn.type = "checkbox"; + btn.value = "conflit"; + btn.name = `btn_assiduites_${index}`; + btn.id = `rbtn_conflit`; + btn.classList.add("rbtn", "conflit"); + btn.title = "conflit"; + + // TODO : Ouvrir solveur + + const solveur = new ConflitResolver(etud.assiduites, getPeriodAsDate(), { + deb: $("#date").datepicker("getDate"), + fin: $("#date").datepicker("getDate").add(1, "days"), + }); + + const update = () => { + MiseAJourLigneEtud(etud); + }; + + solveur.callbacks = { + delete: update, + edit: update, + split: update, + }; + + btn.addEventListener("click", () => { + solveur.open(); + btn.checked = false; + }); + + btnsField.appendChild(btn); + } + + ligneEtud.appendChild(btnsField); + + return ligneEtud; } -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(); - } - ); +/** + * Génération de toutes les lignes étudiantes + */ - return bool; +async function creerTousLesEtudiants(etuds) { + const etudsDiv = document.querySelector(".etud_holder"); + etudsDiv.innerHTML = ""; + const moduleImplId = readOnly ? null : $("#moduleimpl_select").val(); + const inscriptions = await getInscriptionModule(moduleImplId); + [...etuds.values()] + .sort((a, b) => { + return a.sort_key > b.sort_key ? 1 : -1; + }) + .filter((etud) => { + return inscriptions == null || inscriptions.includes(etud.id); + }) + .forEach((etud, index) => { + etudsDiv.appendChild(creerLigneEtudiant(etud, index + 1)); + }); } -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" : "") : "" - }`; - - async_get( - url_api, - (data) => { - action(data); - }, - () => {} - ); -} - -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 = ` -

Il peut s'agir d'un problème de droits, ou d'une modification survenue sur le serveur.

-

Si le problème persiste, demandez de l'aide sur le Discord d'assistance de ScoDoc

- - `; - const div = document.createElement("div"); - div.innerHTML = html; - openAlertModal("Une erreur s'est produite", div); -} - -const moduleimpls = {}; - -function getModuleImpl(assiduite) { +/** + * Récupère une version lisible du moduleimpl + * @param {Object} assiduite + * @returns {String} + */ +async function getModuleImpl(assiduite) { if (assiduite == null) return "Pas de module"; const id = assiduite.moduleimpl_id; @@ -1966,7 +407,7 @@ function getModuleImpl(assiduite) { assiduite.external_data.hasOwnProperty("module") ) { return assiduite.external_data.module == "Autre" - ? "Tout module" + ? "Autre module (pas dans la liste)" : assiduite.external_data.module; } else { return "Pas de module"; @@ -1976,51 +417,423 @@ function getModuleImpl(assiduite) { if (id in moduleimpls) { return moduleimpls[id]; } - const url_api = getUrl() + `/api/moduleimpl/${id}`; - sync_get( - url_api, - (data) => { + const url_api = `../../api/moduleimpl/${id}`; + + return await fetch(url_api) + .then((res) => { + if (!res.ok) { + throw new Error("Network response was not ok"); + } + return res.json(); + }) + .then((data) => { moduleimpls[id] = `${data.module.code} ${data.module.abbrev}`; - }, - (data) => { + return moduleimpls[id]; + }) + .catch((_) => { moduleimpls[id] = "Pas de module"; - } - ); - - return moduleimpls[id]; + 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; +/** + * Renvoie le moduleimpl_id de l'assiduité + * ou l'external_data.module si le moduleimpl_id n'est pas défini + * "" si aucun module n'est défini + * @param {Object} assiduite + * @returns {String} + */ +function getModuleImplId(assiduite) { + 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.toLowerCase(); + } else { + return ""; + } + } else { + return id + ""; + } +} +/** + * Récupère les etudid de tous les étudiants inscrits au module + * @param {String} moduleimpl_id + * @returns {Array} + */ +async function getInscriptionModule(moduleimpl_id) { + if ([null, "", "autre"].includes(moduleimpl_id)) return null; + if (!inscriptionsModules.has(moduleimpl_id)) { + const path = `../../api/moduleimpl/${moduleimpl_id}/inscriptions`; + await fetch(path) + .then((res) => { + if (!res.ok) { + throw new Error("Network response was not ok"); + } + return res.json(); + }) + .then((data) => { + inscriptionsModules.set( + moduleimpl_id, + data.map((i) => i.etudid) + ); + }) + .catch((_) => { + inscriptionsModules.set(moduleimpl_id, []); + }); } - return obj.user_nom_complet || obj.user_id; + return inscriptionsModules.get(moduleimpl_id); } -const inscriptionsModule = {}; +async function MiseAJourLigneEtud(etud) { + //Récupérer ses assiduités -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; + function RecupAssiduitesEtudiant(etudid) { + const date = $("#date").datepicker("getDate"); + const date_debut = date.add(-1, "days").format("YYYY-MM-DDTHH:mm"); + const date_fin = date.add(2, "days").format("YYYY-MM-DDTHH:mm"); + url = + `../../api/assiduites/${etudid}/query?date_debut=${date_debut}` + + `&date_fin=${date_fin}&with_justifs`; + + return fetch(url) + .then((res) => { + if (!res.ok) { + throw new Error("Network response was not ok"); + } + return res.json(); + }) + .then((data) => { + etud.assiduites = data; + }) + .catch((error) => { + console.error( + "There has been a problem with your fetch operation:", + error + ); + }); + } + + await RecupAssiduitesEtudiant(etud.id); + + const etudRow = document.getElementById(`etud_row_${etud.id}`); + if (etudRow == null) return; + + const ligneEtud = creerLigneEtudiant( + etud, + document.querySelector(`#etud_row_${etud.id}`).querySelector(".index") + .textContent + ); + + etudRow.replaceWith(ligneEtud); +} + +async function actionAssiduite(etud, etat, type, assiduite = null) { + const modimpl_id = $("#moduleimpl_select").val(); + if ( + assiduite && + assiduite.etat.toLowerCase() === etat && + assiduite.moduleimpl_id == modimpl_id + ) + type = "suppression"; + + const { deb, fin } = getPeriodAsDate(); + + let assiduiteObjet = assiduite ?? { + date_debut: deb, + date_fin: fin, + etudid: etud.id, + }; + + assiduiteObjet.etat = etat; + assiduiteObjet.moduleimpl_id = modimpl_id; + + if (type === "creation") { + await async_post( + `../../api/assiduite/${etud.id}/create`, + [assiduiteObjet], + (data) => { + if (data.success.length > 0) { + MiseAJourLigneEtud(etud); + envoiToastEtudiant(etat, etud); + } else { + console.error(data.errors["0"].message); + erreurModuleImpl(data.errors["0"].message); + } }, - (data, status) => { - //error - console.error(data, status); - errorAlert(); + (error) => { + console.error("Erreur lors de la création de l'assiduité", error); + } + ); + } else if (type === "edition") { + await async_post( + `../../api/assiduite/${assiduite.assiduite_id}/edit`, + { + etat: assiduiteObjet.etat, + moduleimpl_id: assiduiteObjet.moduleimpl_id, + }, + (data) => { + MiseAJourLigneEtud(etud); + envoiToastEtudiant(etat, etud); + }, + (error) => { + console.error("Erreur lors de la modification de l'assiduité", error); + } + ); + } else if (type === "suppression") { + await async_post( + `../../api/assiduite/delete`, + [assiduite.assiduite_id], + (data) => { + if (data.success.length > 0) { + MiseAJourLigneEtud(etud); + envoiToastEtudiant("remove", etud); + } else { + console.error(data.errors["0"].message); + erreurModuleImpl(data.errors["0"].message); + } + }, + (error) => { + console.error("Erreur lors de la suppression de l'assiduité", error); } ); } - const etudsInscrits = inscriptionsModule[moduleimpl_id].map((i) => i.etudid); - return etudsInscrits.indexOf(Number(etudid)) !== -1; +} + +function erreurModuleImpl(message) { + if (message == "Module non renseigné") { + 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); + } + if (message == "L'étudiant n'est pas inscrit au module") { + const HTML = ` +

Attention, l'étudiant n'est pas inscrit à ce module.

+

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); + } +} + +function mettreToutLeMonde(etat, el = null) { + const lignesEtuds = [...document.querySelectorAll("fieldset.btns_field")]; + + const { deb, fin } = getPeriodAsDate(); + const assiduiteObjet = { + date_debut: deb, + date_fin: fin, + etat: etat, + moduleimpl_id: $("#moduleimpl_select").val(), + }; + + if (el != null) el.checked = false; + + // Suppression des assiduités + if (etat == "vide") { + const assiduites_id = lignesEtuds + .filter((e) => e.getAttribute("type") == "edition") + .map((e) => Number(e.getAttribute("assiduite_id"))); + afficheLoader(); + + async_post( + `../../api/assiduite/delete`, + assiduites_id, + async (data) => { + retirerLoader(); + if (data.errors.length == 0) { + await recupAssiduites(etuds, $("#date").datepicker("getDate")); + creerTousLesEtudiants(etuds); + } else { + console.error(data.errors); + } + envoiToastTous("remove", assiduites_id.length); + }, + (error) => { + console.error("Erreur lors de la suppression de l'assiduité", error); + } + ); + + return; + } + + // Création / édition des assiduités + const assiduitesACreer = lignesEtuds + .filter((e) => e.getAttribute("type") == "creation") + .map((e) => Number(e.getAttribute("etudid"))); + const assiduitesAEditer = lignesEtuds + .filter((e) => e.getAttribute("type") == "edition") + .map((e) => Number(e.getAttribute("assiduite_id"))); + + // création + + const promiseCreate = async_post( + `../../api/assiduites/create`, + assiduitesACreer.map((etudid) => { + return { ...assiduiteObjet, etudid }; + }), + async (data) => { + if (data.errors.length > 0) { + console.error(data.errors); + } + }, + (error) => { + console.error("Erreur lors de la création de l'assiduité", error); + } + ); + const promiseEdit = async_post( + `../../api/assiduites/edit`, + assiduitesAEditer.map((assiduite_id) => { + return { ...assiduiteObjet, assiduite_id }; + }), + async (data) => { + if (data.errors.length > 0) { + console.error(data.errors); + } + }, + (error) => { + console.error("Erreur lors de l'édition de l'assiduité", error); + } + ); + + // Affiche un loader + afficheLoader(); + + Promise.all([promiseCreate, promiseEdit]).then(async () => { + retirerLoader(); + await recupAssiduites(etuds, $("#date").datepicker("getDate")); + creerTousLesEtudiants(etuds); + envoiToastTous(etat, assiduitesACreer.length + assiduitesAEditer.length); + }); +} + +function afficheLoader() { + const loaderDiv = document.createElement("div"); + loaderDiv.id = "loader"; + const span = document.createElement("span"); + span.textContent = "Chargement en cours"; + loaderDiv.appendChild(span); + const loader = document.createElement("div"); + loader.classList.add("loader"); + loaderDiv.appendChild(loader); + document.body.appendChild(loaderDiv); +} + +function retirerLoader() { + document.getElementById("loader").remove(); +} + +function envoiToastEtudiant(etat, etud) { + let etatAffiche; + + switch (etat.toUpperCase()) { + case "PRESENT": + etatAffiche = "%etud% a été noté(e) présent(e)"; + break; + case "RETARD": + etatAffiche = "%etud% a été noté(e) en retard"; + break; + case "ABSENT": + etatAffiche = "%etud% a été noté(e) absent(e)"; + break; + case "REMOVE": + etatAffiche = "L'assiduité de %etud% a été retirée."; + } + + const nom_prenom = `${etud.nom.toUpperCase()} ${etud.prenom.capitalize()}`; + const span = document.createElement("span"); + span.innerHTML = etatAffiche.replace("%etud%", nom_prenom); + + pushToast(generateToast(span, getToastColorFromEtat(etat.toUpperCase()), 5)); +} + +function envoiToastTous(etat, count) { + const span = document.createElement("span"); + let etatAffiche = etat; + switch (etat) { + case "remove": + if (count > 0) { + span.innerHTML = `${count} assiduités ont été supprimées.`; + } else { + span.innerHTML = `Aucune assiduité n'a été supprimée.`; + } + break; + case "retard": + etatAffiche = "En retard"; + default: + if (count > 0) { + span.innerHTML = `${count} étudiants ont été mis ${etatAffiche + .capitalize() + .trim()}`; + } else { + span.innerHTML = `Aucun étudiant n'a été mis ${etatAffiche + .capitalize() + .trim()}`; + } + break; + } + + pushToast(generateToast(span, getToastColorFromEtat(etat.toUpperCase()), 5)); +} + +function estJourTravail(jour, nonWorkdays) { + const d = Intl.DateTimeFormat("fr-FR", { + timeZone: SCO_TIMEZONE, + weekday: "short", + }) + .format(jour) + .replace(".", ""); + return !nonWorkdays.includes(d); +} + +function retourJourTravail(date) { + const jourMiliSecondes = 86400000; // 24 * 3600 * 1000 | H * s * ms + let jour = date; + let compte = 0; + + while (!estJourTravail(jour, nonWorkDays) && compte++ < 7) { + jour = new Date(jour - jourMiliSecondes); + } + return jour; +} + +function dateCouranteEstTravaillee() { + const date = $("#date").datepicker("getDate"); + if (!estJourTravail(date, nonWorkDays)) { + const nouvelleDate = retourJourTravail(date); + $("#date").datepicker("setDate", nouvelleDate); + + const att = document.createTextNode( + `Le jour sélectionné (${Date.toFRA( + date.format("YYYY-MM-DD") + )}) 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é : ${Date.toFRA( + nouvelleDate.format("YYYY-MM-DD") + )}.` + ) + ); + openAlertModal("Attention", div, "", "#eec660"); + + return false; + } + return true; } diff --git a/app/templates/assiduites/pages/signal_assiduites_group.j2 b/app/templates/assiduites/pages/signal_assiduites_group.j2 index 4eb21e413..341b624a4 100644 --- a/app/templates/assiduites/pages/signal_assiduites_group.j2 +++ b/app/templates/assiduites/pages/signal_assiduites_group.j2 @@ -13,6 +13,7 @@ +{% include "sco_timepicker.j2" %} {% endblock scripts %} {% block styles %} {{ super() }} - @@ -77,14 +105,8 @@ {% block app_content %} -{% include "assiduites/widgets/toast.j2" %} {{ minitimeline|safe }} -
@@ -104,15 +126,16 @@
Groupes : {{grp|safe}}
-
Date : - +
+
+
+ {{timeline|safe}} +
{% if readonly == "false" %} - {{timeline|safe}}
-{% endblock app_content %} \ No newline at end of file +{% endblock app_content %} diff --git a/app/templates/assiduites/widgets/conflict.j2 b/app/templates/assiduites/widgets/conflict.j2 index 908835898..8a60eae27 100644 --- a/app/templates/assiduites/widgets/conflict.j2 +++ b/app/templates/assiduites/widgets/conflict.j2 @@ -1,118 +1,100 @@