ScoDoc-Lille/app/static/js/assiduites.js

901 lines
25 KiB
JavaScript

/**
*
* 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) {
console.log("async_post " + path);
let response;
try {
response = await fetch(path, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (response.ok) {
const responseData = await response.json();
success(responseData);
} else {
throw new Error("Network response was not ok.");
}
} catch (error) {
console.error(error);
if (errors) errors(error);
}
return response;
}
/**
* 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,
};
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)
);
});
}
const conflits = readOnly ? [] : recupConflitsAssiduites(etud.assiduites);
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";
}
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.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 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") {
["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;
}
/**
* 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);
[...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 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;
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 "Pas de module";
}
}
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);
}
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);
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);
}
},
(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);
}
);
}
}
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") {
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);
}
}
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) <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));
}
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));
}
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;
}
/**
* 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());
const idDiv = document.createElement("div");
idDiv.className = "assiduite-id";
getModuleImpl(assiduite).then((modImpl) => {
idDiv.textContent = `${modImpl}`;
});
bubble.appendChild(idDiv);
const periodDivDeb = document.createElement("div");
periodDivDeb.className = "assiduite-period";
periodDivDeb.textContent = `${formatDateModal(assiduite.date_debut)}`;
bubble.appendChild(periodDivDeb);
const periodDivFin = document.createElement("div");
periodDivFin.className = "assiduite-period";
periodDivFin.textContent = `${formatDateModal(assiduite.date_fin)}`;
bubble.appendChild(periodDivFin);
const stateDiv = document.createElement("div");
stateDiv.className = "assiduite-state";
stateDiv.textContent = `État: ${assiduite.etat.capitalize()}`;
bubble.appendChild(stateDiv);
const motifDiv = document.createElement("div");
stateDiv.className = "assiduite-why";
const motif = ["", null, undefined].includes(assiduite.desc)
? "Pas de motif"
: assiduite.desc.capitalize();
stateDiv.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);
}