forked from ScoDoc/ScoDoc
1125 lines
33 KiB
JavaScript
1125 lines
33 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) {
|
||
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;
|
||
}
|