ScoDoc/app/static/js/assiduites.js
2024-10-29 16:42:49 +01:00

1125 lines
33 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
*
* Ensemble des fonctions liées à la gestion des assiduités
* Créé par : HARTMANN Matthias (Iziram)
*
*/
/**
* <== OUTILS ==>
*/
/**
* 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,
});
/**
* Fait une requête GET de façon asynchrone
* @param {String} path adresse distante
* @param {CallableFunction} success fonction à effectuer en cas de succès
* @param {CallableFunction} errors fonction à effectuer en cas d'échec
*/
async function async_get(path, success, errors) {
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 asynchrone
* @param {String} path adresse distante
* @param {object} data données à envoyer (objet js)
* @param {CallableFunction} success fonction à effectuer en cas de succès
* @param {CallableFunction} errors fonction à effectuer en cas d'échec
*/
async function async_post(path, data, success, errors) {
let response;
try {
response = await fetch(path, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (response.ok) {
const responseData = await response.json();
success(responseData);
} else {
if (response.status == 404) {
response.json().then((data) => {
if (errors) errors(data);
});
} else {
throw new Error("Network response was not ok.");
}
}
} catch (error) {
console.error(error);
if (errors) errors(error);
}
return response;
}
/**
* 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<Object>} Un objet contenant les étudiants, indexés par leur identifiant.
*/
async function recupEtuds(groupIds) {
const etuds = new Map();
if (groupIds == null || groupIds.length == 0) return etuds;
// 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");
}
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;
});
})
.catch((error) =>
console.error(
"There has been a problem with your fetch operation:",
error
)
);
}
/**
* Génération ligne étudiante
*/
function creerLigneEtudiant(etud, index) {
let currentAssiduite = {
etat: "",
type: "creation",
assiduite_id: -1,
date_debut: null,
date_fin: null,
};
/**Retourne une liste d'assiduité en conflit avec la période actuelle
* @param {Array} assiduites - Les assiduités de l'étudiant
* @returns {Array} Les assiduités en conflit
*/
function recupConflitsAssiduites(assiduites) {
const period = getPeriodAsDate();
return assiduites.filter((assi) => {
const interval = {
deb: new Date(Date.removeUTC(assi.date_debut)),
fin: new Date(Date.removeUTC(assi.date_fin)),
};
return (
period.deb.isBefore(interval.fin) && period.fin.isAfter(interval.deb)
);
});
}
// Pas de conflit en readonly
const conflits = readOnly ? [] : recupConflitsAssiduites(etud.assiduites);
// Si il y a des conflits, on prend le premier pour l'afficher
// si les dates de début et de fin sont les mêmes, c'est une édition
// sinon c'est un conflit
if (conflits.length > 0) {
currentAssiduite = conflits[0];
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";
}
// Création de la ligne étudiante en DOM
/* exemple de ligne étudiante
<div class="etud_row" id="etud_row_497">
<div class="index">1</div>
<div class="name_field"><img src="../../api/etudiant/etudid/497/photo?size=small" alt="Baudin Joseph" class="pdp"><a
class="name_set" href="bilan_etud?etudid=497">
<h4 class="nom">Baudin</h4>
<h5 class="prenom">Joseph</h5>
</a></div>
<div class="assiduites_bar">
<div id="prevDateAssi" class="vide"></div>
<div class="mini-timeline"><span class="mini_tick" style="left: 47.5%;">13h</span>
<div class="mini-timeline-block creneau" style="left: 20%; width: 17.5%;"></div>
</div>
</div>
<fieldset class="btns_field single" etudid="497" type="creation" assiduite_id="-1">
<input
type="checkbox"
value="present"
name="btn_assiduites_1"
id="rbtn_present"
class="rbtn present"
title="present"
>
<input
type="checkbox"
value="retard"
name="btn_assiduites_1"
id="rbtn_retard"
class="rbtn retard"
title="retard"
>
<input
type="checkbox"
value="absent"
name="btn_assiduites_1"
id="rbtn_absent"
class="rbtn absent"
title="absent"
>
</fieldset>
</div>
*/
const ligneEtud = document.createElement("div");
ligneEtud.classList.add("etud_row");
if (Object.keys(etudsDefDem).includes(etud.id)) {
ligneEtud.classList.add(etudsDefDem[etud.id] == "D" ? "dem" : "def");
}
ligneEtud.id = `etud_row_${etud.id}`;
if (currentAssiduite.type === "conflit" && !readOnly)
ligneEtud.classList.add("conflit");
// div index avec l'index
const indexDiv = document.createElement("div");
indexDiv.classList.add("index");
indexDiv.id = `etudid-${etud.id}`;
indexDiv.textContent = index;
ligneEtud.appendChild(indexDiv);
// div name_field
const nameField = document.createElement("div");
nameField.classList.add("name_field");
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.id = `etudid-${etud.id}`;
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 lastAssiduite = assiduites
.filter((assi) => {
const interval = {
deb: new Date(Date.removeUTC(assi.date_debut)),
fin: new Date(Date.removeUTC(assi.date_fin)),
};
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();
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") {
const etats = ["retard", "absent"];
if (!window.nonPresent) {
etats.splice(0, 0, "present");
}
etats.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);
// Attache les infos de l'étudiant (bulle etud_info)
try {
attach_etud_info(nameSet);
attach_etud_info(indexDiv);
} catch {}
return ligneEtud;
}
/**
* Génération de toutes les lignes étudiantes
*/
async function creerTousLesEtudiants(etuds) {
const etudsDiv = document.querySelector(".etud_holder");
etudsDiv.innerHTML = "";
const moduleImplId = readOnly ? null : $("#moduleimpl_select").val();
const inscriptions = await getInscriptionModule(moduleImplId);
// on trie les étudiants par ordre alphabétique
// et on garde ceux qui sont inscrits au module
// puis pour chaque étudiant on crée une ligne
[...etuds.values()]
.sort((a, b) => {
return a.sort_key > b.sort_key ? 1 : -1;
})
.filter((etud) => {
return inscriptions == null || inscriptions.includes(etud.id);
})
.forEach((etud, index) => {
etudsDiv.appendChild(creerLigneEtudiant(etud, index + 1));
});
// Récupère l'offset timezone serveur pour la date sélectionnée
const date_iso = getSelectedDateIso();
try {
const res = await fetch(`../../api/assiduite/date_time_offset/${date_iso}`);
if (!res.ok) {
throw new Error("Network response was not ok");
}
const text = await res.text();
SERVER_TIMEZONE_OFFSET = text;
} catch (error) {
console.error('Error:', error);
}
}
/**
* Récupère une version lisible du moduleimpl
* @param {Object} assiduite
* @returns {String}
*/
async function getModuleImpl(assiduite) {
if (assiduite == null) return "Module non spécifié";
const id = assiduite.moduleimpl_id;
if (id == null || id == undefined) {
if (
assiduite.hasOwnProperty("external_data") &&
assiduite.external_data != null &&
assiduite.external_data.hasOwnProperty("module")
) {
return assiduite.external_data.module == "Autre"
? "Autre module (pas dans la liste)"
: assiduite.external_data.module;
} else {
return "Module non spécifié";
}
}
if (id in moduleimpls) {
return moduleimpls[id];
}
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 || ""}`;
return moduleimpls[id];
})
.catch((_) => {
moduleimpls[id] = "Pas de module";
return moduleimpls[id];
});
}
/**
* 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 inscriptionsModules.get(moduleimpl_id);
}
// Mise à jour de la ligne étudiant
async function MiseAJourLigneEtud(etud) {
//Récupérer ses assiduités
function RecupAssiduitesEtudiant(etudid) {
const date = $("#date").datepicker("getDate");
const date_debut = date.add(-1, "days").format("YYYY-MM-DDTHH:mm");
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);
// Une fois les assiduités récupérées, on met à jour la ligne étudiant
// on replace l'ancienne ligne par la nouvellement générée
const etudRow = document.getElementById(`etud_row_${etud.id}`);
if (etudRow == null) return;
const ligneEtud = creerLigneEtudiant(
etud,
document.querySelector(`#etud_row_${etud.id}`).querySelector(".index")
.textContent
);
etudRow.replaceWith(ligneEtud);
}
// Action appelée lors d'un clic sur un bouton d'assiduité
// Création, édition ou suppression d'une assiduité
async function actionAssiduite(etud, etat, type, assiduite = null) {
const modimpl_id = $("#moduleimpl_select").val();
if (assiduite && assiduite.etat.toLowerCase() === etat) type = "suppression";
const { deb, fin } = getPeriodAsISO(); // chaines sans timezone pour l'API
// génération d'un objet assiduité basique qui sera complété
let assiduiteObjet = assiduite ?? {
date_debut: deb,
date_fin: fin,
etudid: etud.id,
};
assiduiteObjet.etat = etat;
assiduiteObjet.moduleimpl_id = modimpl_id;
// En fonction du type d'action on appelle la bonne route
// avec les bonnes valeurs
if (type === "creation") {
await async_post(
`../../api/assiduite/${etud.id}/create`,
[assiduiteObjet],
(data) => {
if (data.success.length > 0) {
MiseAJourLigneEtud(etud);
envoiToastEtudiant(etat, etud);
} else {
console.error(data.errors["0"].message);
erreurModuleImpl(data.errors["0"].message);
}
},
(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);
}
);
}
}
// Fonction pour afficher un message d'erreur si le module n'est pas renseigné
// ou si l'étudiant n'est pas inscrit au module.
// On donne le message d'erreur d'une requête api et cela affiche le message correspondant
function erreurModuleImpl(message) {
if (message == "Module non renseigné") {
const HTML = `
<p>Attention, le module doit obligatoirement être renseigné.</p>
<p>Cela vient de la configuration du semestre ou plus largement du département.</p>
<p>Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.</p>
`;
const content = document.createElement("div");
content.innerHTML = HTML;
openAlertModal("Sélection du module", content);
}
if (
message == "L'étudiant n'est pas inscrit au module" ||
message == "param 'moduleimpl_id': etud non inscrit"
) {
const HTML = `
<p>Attention, l'étudiant n'est pas inscrit à ce module.</p>
<p>Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.</p>
`;
const content = document.createElement("div");
content.innerHTML = HTML;
openAlertModal("Sélection du module", content);
}
if (
message == "La date de début n'est pas un jour travaillé"
) {
const HTML = `
<p>Attention, la date de début n'est pas un jour travaillé.</p>
`;
const content = document.createElement("div");
content.innerHTML = HTML;
openAlertModal("Date de début", content);
}
}
// Fonction pour ajouter en lot une assiduité à tous les étudiants
// Fonctionne uniquement pour créer ou supprimer des assiduités
// Pas d'édition possible
function mettreToutLeMonde(etat, el = null) {
const lignesEtuds = [...document.querySelectorAll("fieldset.btns_field")];
const { deb, fin } = getPeriodAsDate(true); // tz server
const period_iso = getPeriodAsISO(); // chaines sans timezone pour l'API
const deb_iso = period_iso.deb;
const fin_iso = period_iso.fin;
const assiduiteObjet = {
date_debut: deb_iso,
date_fin: fin_iso,
etat: etat,
moduleimpl_id: $("#moduleimpl_select").val(),
};
if (el != null) el.checked = false;
// Suppression des assiduités
if (etat == "vide") {
if (!confirm("Effacer tout les évènements correspondant à cette plage ?")) {
return; // annulation
}
// On récupère les lignes avec une seule assiduité
let assiduites_id = lignesEtuds
.filter((e) => e.getAttribute("type") == "edition")
.map((e) => Number(e.getAttribute("assiduite_id")));
// On récupère les assiduités conflictuelles mais qui sont comprises
// dans la plage de suppression
const unDeleted = {};
lignesEtuds
.filter((e) => e.getAttribute("type") == "conflit")
.forEach((e) => {
const etud = etuds.get(Number(e.getAttribute("etudid")));
// On récupère les assiduités couvertes par la plage de suppression
etud.assiduites.forEach((a) => {
const date_debut = new Date(a.date_debut);
const date_fin = new Date(a.date_fin);
// On prend en compte uniquement les assiduités conflictuelles
// (qui intersectent la plage de suppression)
if (
Date.intersect(
{ deb: deb, fin: fin }, // la plage, en Date avec timezone serveur
{ deb: date_debut, fin: date_fin } // dates de l'assiduité avec leur timezone
)
) {
// Si l'assiduité est couverte par la plage de suppression
// On l'ajoute à la liste des assiduités à supprimer.
if (
date_debut.isBetween(deb, fin, "[]") &&
date_fin.isBetween(deb, fin, "[]")
) {
assiduites_id.push(a.assiduite_id);
}
// Sinon on ajoute l'étudiant à la liste des étudiants non gérés
else {
unDeleted[a.etudid] = true;
}
}
});
});
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);
if (Object.keys(unDeleted).length == 0) return;
// CAS : des assiduités d'étudiants n'ont pas pu être supprimés
let unDeletedEtuds = `
<ul>
${Object.keys(unDeleted)
.map((etudid) => {
const etud = etuds.get(Number(etudid));
return `<li>${etud.civilite}. ${etud.nom.toUpperCase()} ${
etud.prenom
}</li>`;
})
.join("")}
</ul>
`;
let html = `
<p>Les assiduités des étudiants suivants n'ont pas été supprimées car elles ne sont pas incluses dans la plage de suppression :</p>
${unDeletedEtuds}
`;
const div = document.createElement("div");
div.innerHTML = html;
openAlertModal("Assiduité non supprimée", div);
},
(error) => {
console.error("Erreur lors de la suppression de l'assiduité", error);
}
);
return;
}
// Création
const assiduitesACreer = lignesEtuds
.filter((e) => e.getAttribute("type") == "creation")
.map((e) => Number(e.getAttribute("etudid")));
// 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);
}
);
// Affiche un loader
afficheLoader();
Promise.all([promiseCreate]).then(async () => {
retirerLoader();
await recupAssiduites(etuds, $("#date").datepicker("getDate"));
creerTousLesEtudiants(etuds);
envoiToastTous(etat, assiduitesACreer.length);
});
}
// Affichage d'un loader (animation jeu pong)
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);
}
// Retrait du loader (animation jeu pong)
function retirerLoader() {
document.getElementById("loader").remove();
}
// Simplification de l'envoie de toast pour un étudiant
// affiche le nom, le prénom et l'état de l'assiduité avec une couleur spécifique
function envoiToastEtudiant(etat, etud) {
let etatAffiche;
switch (etat.toUpperCase()) {
case "PRESENT":
etatAffiche = "%etud% a été noté(e) <u><strong>présent(e)</strong></u>";
break;
case "RETARD":
etatAffiche = "%etud% a été noté(e) <u><strong>en retard</strong></u>";
break;
case "ABSENT":
etatAffiche = "%etud% a été noté(e) <u><strong>absent(e)</strong></u>";
break;
case "REMOVE":
etatAffiche = "L'assiduité de %etud% a été retirée.";
}
const nom_prenom = `${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));
}
// Fonction pour simplifier l'envoie de toast avec le bouton "mettre tout le monde"
// On donne un etat et un compte et cela affichera le message associé.
// ex : 12 assiduités ont été supprimées
// ex : 15 étudiants ont été mis Absent.
function envoiToastTous(etat, count) {
const span = document.createElement("span");
let etatAffiche = etat;
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 <u><strong>${etatAffiche
.capitalize()
.trim()}</strong></u>`;
} else {
span.innerHTML = `Aucun étudiant n'a été mis <u><strong>${etatAffiche
.capitalize()
.trim()}</strong></u>`;
}
break;
}
pushToast(generateToast(span, getToastColorFromEtat(etat.toUpperCase()), 5));
}
// Permet de savoir si un jour est travaillé ou pas
// jour : Date
// nonWorkdays : Array[str] => ["mar", "sam", "dim"]
function estJourTravail(jour, nonWorkdays) {
const d = Intl.DateTimeFormat("fr-FR", {
timeZone: SCO_TIMEZONE,
weekday: "short",
})
.format(jour)
.replace(".", "");
return !nonWorkdays.includes(d);
}
// Renvoie le dernier jour travaillé disponible.
// par défaut va en arrière (dans le passé)
// si anti == False => va dans le futur
function retourJourTravail(date, anti = true) {
const jourMiliSecondes = 86400000; // 24 * 3600 * 1000 | H * s * ms
let jour = date;
let compte = 0;
while (!estJourTravail(jour, nonWorkDays) && compte++ < 7) {
let temps = anti
? jour - jourMiliSecondes
: jour.valueOf() + jourMiliSecondes;
jour = new Date(temps);
}
return jour;
}
// Vérifie si la date courante est travaillée
// Si ce n'est pas le cas, on change la date pour le dernier jour travaillé (passé)
// et on affiche une alerte
// (utilise le datepicker #date)
function dateCouranteEstTravaillee() {
const date = $("#date").datepicker("getDate");
if (!estJourTravail(date, nonWorkDays)) {
// récupération du jour travaillé le plus proche
const nouvelleDate = retourJourTravail(date);
$("#date").datepicker("setDate", nouvelleDate);
// Création du message d'alerte
let msg = "Le jour sélectionné";
if (new Date().format("YYYY-MM-DD") == date.format("YYYY-MM-DD")) {
msg = "Aujourd'hui";
}
const att = document.createTextNode(
`${msg} (${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")
)}.`
)
);
// Affichage de l'alerte
openAlertModal("Attention", div, "", "#eec660");
return false;
}
return true;
}
// Fonction pour passer au jour suivant
// anti : bool => si true, on va dans le passé
function jourSuivant(anti = false) {
let date = $("#date").datepicker("getDate");
date = anti ? date.add(-1, "days") : date.add(1, "days");
const nouvelleDate = retourJourTravail(date, anti);
$("#date").datepicker("setDate", nouvelleDate);
creerTousLesEtudiants(etuds);
}
/**
* 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 setupAssiduiteBubble(el, assiduite) {
function formatDateModal(dateStr) {
const date = new Date(Date.removeUTC(dateStr));
return date.format("DD/MM/Y HH:mm");
}
if (!assiduite) return;
const bubble = document.createElement("div");
bubble.className = "assiduite-bubble";
bubble.classList.add(assiduite.etat.toLowerCase());
// Ajout d'un lien pour plus d'informations
const infos = document.createElement("a");
infos.className = "";
infos.textContent = ``;
infos.title = "Détails / Modifier";
infos.target = "_blank";
infos.href = `edit_assiduite_etud/${assiduite.assiduite_id}`;
const actionsDiv = document.createElement("div");
actionsDiv.className = "assiduite-actions";
actionsDiv.appendChild(infos);
bubble.appendChild(actionsDiv);
const stateDiv = document.createElement("div");
stateDiv.className = "assiduite-state";
stateDiv.textContent = `État: ${assiduite.etat.capitalize()}`;
bubble.appendChild(stateDiv);
const idDiv = document.createElement("div");
idDiv.className = "assiduite-id";
getModuleImpl(assiduite).then((modImpl) => {
idDiv.textContent = `${modImpl}`;
});
bubble.appendChild(idDiv);
// Affichage des dates
// si les jours sont les mêmes, on affiche "jour hh:mm - hh:mm"
// sinon on affiche "jour hh:mm - jour hh:mm"
const periodDiv = document.createElement("div");
periodDiv.className = "assiduite-period";
const dateDeb = new Date(Date.removeUTC(assiduite.date_debut));
const dateFin = new Date(Date.removeUTC(assiduite.date_fin));
if (dateDeb.isSame(dateFin, "day")) {
const jour = dateDeb.format("DD/MM/YYYY");
const deb = dateDeb.format("HH:mm");
const fin = dateFin.format("HH:mm");
periodDiv.textContent = `${jour} de ${deb} à ${fin}`;
} else {
const jourDeb = dateDeb.format("DD/MM/YYYY");
const jourFin = dateFin.format("DD/MM/YYYY");
periodDiv.textContent = `du ${jourDeb} au ${jourFin}`;
}
bubble.appendChild(periodDiv);
const motifDiv = document.createElement("div");
motifDiv.className = "assiduite-why";
const motif = ["", null, undefined].includes(assiduite.desc)
? "Non spécifié"
: assiduite.desc.capitalize();
motifDiv.textContent = `Motif: ${motif}`;
bubble.appendChild(motifDiv);
const userIdDiv = document.createElement("div");
userIdDiv.className = "assiduite-user_id";
userIdDiv.textContent = `saisie le ${formatDateModal(
assiduite.entry_date,
" à "
)}`;
if (assiduite.user_id != null) {
userIdDiv.textContent += `\npar ${assiduite.user_nom_complet}`;
}
bubble.appendChild(userIdDiv);
el.appendChild(bubble);
}
/**
* Permet d'afficher ou non les photos des étudiants
* @param {boolean} checked
*/
function afficherPDP(checked) {
if (checked) {
gtrcontent.setAttribute("data-pdp", "true");
} else {
gtrcontent.removeAttribute("data-pdp");
}
// On sauvegarde le choix dans le localStorage
localStorage.setItem("scodoc-etud-pdp", `${checked}`);
pdp.checked = checked;
}