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

1873 lines
49 KiB
JavaScript

// <=== CONSTANTS and GLOBALS ===>
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;
/**
* 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,
});
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);
}
});
});
}
/**
* 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 = `
<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);
return;
}
getAssiduitesFromEtuds(true);
document.querySelector(".selectors").disabled = true;
$("#tl_date").datepicker("option", "disabled", true);
generateMassAssiduites();
generateAllEtudRow();
btn.remove();
onlyAbs();
};
if (!verifyDateInSemester()) {
const HTML = `
<p>Attention, la date sélectionnée n'est pas comprise dans le semestre.</p>
<p>Cette page permet l'affichage et la modification des assiduités uniquement pour le semestre sélectionné.</p>
<p>Vous n'aurez donc pas accès aux assiduités.</p>
<p>Appuyer sur "Valider" uniquement si vous souhaitez poursuivre sans modifier la date.</p>
`;
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
* @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) {
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);
}
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
* @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;
}
// <<== Gestion des actions de masse ==>>
const massActionQueue = new Map();
/**
* Cette fonction remet à zero la gestion des actions de masse
*/
function resetMassActionQueue() {
massActionQueue.set("supprimer", []);
massActionQueue.set("editer", []);
massActionQueue.set("creer", []);
}
/**
* Fonction pour alimenter la queue des actions de masse
* @param {String} type Le type de queue ("creer", "supprimer", "editer")
* @param {*} obj L'objet qui sera utilisé par les API
*/
function addToMassActionQueue(type, obj) {
massActionQueue.get(type)?.push(obj);
}
/**
* Fonction pour exécuter les actions de masse
*/
function executeMassActionQueue() {
if (!currentMassAction) return;
//Récupération des queues
const toCreate = massActionQueue.get("creer");
const toEdit = massActionQueue.get("editer");
const toDelete = massActionQueue.get("supprimer");
//Fonction qui créé les assidutiés de la queue "creer"
const create = () => {
/**
* Création du template de l'assiduité
*
* {
* date_debut: #debut_timeline,
* date_fin: #fin_timeline,
* moduleimpl_id ?: <>
* }
*/
const tlTimes = getTimeLineTimes();
let assiduite = {
date_debut: tlTimes.deb.toIsoUtcString(),
date_fin: tlTimes.fin.toIsoUtcString(),
};
assiduite = setModuleImplId(assiduite);
if (!hasModuleImpl(assiduite) && window.forceModule) {
const html = `
<h3>Aucun module n'a été spécifié</h3>
`;
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 = `
<h3>Aucun module n'a été spécifié</h3>
`;
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 <u><strong>${etat
.capitalize()
.trim()}</strong></u>`;
} else {
span.innerHTML = `Aucun étudiant n'a été mis <u><strong>${etat
.capitalize()
.trim()}</strong></u>`;
}
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();
}
} 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() {
const content = document.getElementById("content");
const mass = document.createElement("div");
mass.className = "mass-selection";
mass.innerHTML = `
<span>Mettre tout le monde :</span>
<fieldset class="btns_field mass">
<input type="checkbox" value="present" name="mass_btn_assiduites" id="mass_rbtn_present"
class="rbtn present">
<input type="checkbox" value="retard" name="mass_btn_assiduites" id="mass_rbtn_retard" class="rbtn retard">
<input type="checkbox" value="absent" name="mass_btn_assiduites" id="mass_rbtn_absent" class="rbtn absent">
<input type="checkbox" value="remove" name="mass_btn_assiduites" id="mass_rbtn_aucun" class="rbtn aucun">
</fieldset>`;
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", 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));
}
let dateStr = "";
if (!isNonWorkDay(date.getDay(), nonWorkDays)) {
dateStr = formatDate(date).capitalize();
} else {
// On se rend au dernier jour travaillé disponible
const lastWorkDay = getNearestWorkDay(date);
const att = document.createTextNode(
`Le jour sélectionné (${formatDate(date)}) 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
)}.`
)
);
openAlertModal("Attention", div, "", "#eec660");
const date_fra = Date.toFRA(lastWorkDay.toIsoUtcString().split("T")[0]);
$(dateInput).datepicker("setDate", date_fra);
dateInput.value = date_fra;
date = lastWorkDay;
dateStr = formatDate(lastWorkDay).capitalize();
}
console.warn(dateStr, date, date.toIsoUtcString());
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.getDay(), 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 {}
}
});
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) {
let d = "";
switch (day) {
case 0:
d = "dim";
break;
case 1:
d = "lun";
break;
case 2:
d = "mar";
break;
case 3:
d = "mer";
break;
case 4:
d = "jeu";
break;
case 5:
d = "ven";
break;
case 6:
d = "sam";
break;
}
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(conflict.date_debut),
fin: new Date(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 {<etudid:str> : [<assiduite>,]}
*/
function getAssiduitesFromEtuds(clear, deb, fin) {
const etudIds = Object.keys(etuds).join(",");
const date_debut = deb ? deb : getPrevDate().toIsoUtcString();
const date_fin = fin ? fin : getNextDate().toIsoUtcString();
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 assiduites;
}
/**
* Création d'une assiduité pour un étudiant
* @param {String} etat l'état de l'étudiant
* @param {Number | String} etudid l'identifiant de l'étudiant
*
* TODO : Rendre asynchrone
*/
function createAssiduite(etat, etudid) {
const tlTimes = getTimeLineTimes();
let assiduite = {
date_debut: tlTimes.deb.toIsoUtcString(),
date_fin: tlTimes.fin.toIsoUtcString(),
etat: etat,
};
assiduite = setModuleImplId(assiduite);
if (!hasModuleImpl(assiduite) && window.forceModule) {
const html = `
<h3>Aucun module n'a été spécifié</h3>
`;
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 = `
<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);
}
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 = `
<h3>Aucune assiduité n'a pû être éditée</h3>
`;
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 = `
<h3>Aucun module n'a été spécifié</h3>
`;
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 = `
<h3>L'étudiant n'est pas inscrit à ce module</h3>
`;
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 = `
<h3>Un module doit être spécifié</h3>
`;
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(assi.date_debut),
fin: new Date(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) => {
const interval = {
deb: new Date(assi.date_debut),
fin: new Date(assi.date_fin),
};
return hasTimeConflict(period, interval);
})
.sort((a, b) => {
const a_fin = new Date(a.date_fin);
const b_fin = new Date(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().toIsoUtcString();
const date_fin = getNextDate().toIsoUtcString();
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" : "")
: ""
}`;
//TODO Utiliser async_get au lieu de jquery
$.ajax({
async: true,
type: "GET",
url: url_api,
success: (data, status) => {
if (status === "success") {
assiduites[etudid] = data;
action(data);
}
},
error: () => {},
});
}
/**
* Déclenchement d'une action après appuie sur un bouton d'assiduité
* @param {HTMLInputElement} element Bouton d'assiduité appuyé
*/
function assiduiteAction(element) {
const field = element.parentElement;
const type = field.getAttribute("type");
const etudid = parseInt(field.getAttribute("etudid"));
const assiduite_id = parseInt(field.getAttribute("assiduite_id"));
const etat = getAssiduiteValue(field);
// Cas de l'action de masse -> peuplement des queues
if (currentMassAction) {
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) <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 = `${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)
);
}
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 += `<input checked type="checkbox" value="${abs}" name="btn_assiduites_${index}" id="rbtn_${abs}" class="rbtn ${abs}" title="${abs}">`;
} else {
assi += `<input type="checkbox" value="${abs}" name="btn_assiduites_${index}" id="rbtn_${abs}" class="rbtn ${abs}" title="${abs}">`;
}
});
const conflit = assiduite.type == "conflit" ? "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 = `<div class="etud_row ${conflit} ${defdem}" id="etud_row_${
etud.id
}">
<div class="index">${index}</div>
<div class="name_field">
<img class="pdp" src="${pdp_url}">
<a class="name_set" href="BilanEtud?etudid=${etud.id}">
<h4 class="nom">${etud.nom}</h4>
<h5 class="prenom">${etud.prenom}</h5>
</a>
</div>
<div class="assiduites_bar">
<div id="prevDateAssi" class="${assiduite.prevAssiduites?.etat?.toLowerCase()}">
</div>
</div>
<fieldset class="btns_field single" etudid="${etud.id}" assiduite_id="${
assiduite.id
}" type="${assiduite.type}">
${assi}
</fieldset>
</div>`;
return HTML;
}
/**
* Insertion de la ligne étudiant
* @param {Etudiant} etud l'objet représentant un étudiant
* @param {Number} index le n° de l'étudiant dans la liste des étudiants
* @param {boolean} output ajout automatique dans la page ou non (default : Non)
* @returns {String} HTML si output sinon rien
*/
function insertEtudRow(etud, index, output = false) {
const etudHolder = document.querySelector(".etud_holder");
const conflict = getAssiduitesConflict(etud.id);
const prevAssiduite = getLastAssiduiteOfPrevDate(etud.id);
let assiduite = {
etatAssiduite: "",
type: "création",
id: -1,
date_debut: null,
date_fin: null,
prevAssiduites: prevAssiduite,
};
if (conflict.length > 0) {
assiduite.etatAssiduite = conflict[0].etat;
assiduite.id = conflict[0].assiduite_id;
assiduite.date_debut = conflict[0].date_debut;
assiduite.date_fin = conflict[0].date_fin;
if (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;
}
document.querySelector(".etud_holder").innerHTML = "";
etuds_ids = Object.keys(etuds).sort((a, b) =>
etuds[a].nom > etuds[b].nom ? 1 : etuds[b].nom > etuds[a].nom ? -1 : 0
);
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("SignaleAssiduiteEtud");
}
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")
.toIsoUtcString()}&date_fin=${date.fin
.add(-1, "seconds")
.toIsoUtcString()}`,
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(assiduite.date_debut),
fin: new Date(assiduite.date_fin),
};
const action = (justifs) => {
//créer un nouveau justificatif
// Afficher prompt -> demander raison et état
const success = () => {
const raison = document.getElementById("promptText").value;
const etat = document.getElementById("promptSelect").value;
//créer justificatif
const justif = {
date_debut: new Date(assiduite.date_debut).toIsoUtcString(),
date_fin: new Date(assiduite.date_fin).toIsoUtcString(),
raison: raison,
etat: etat,
};
createJustificatif(justif);
generateAllEtudRow();
try {
loadAll();
} catch {}
};
const content = document.createElement("fieldset");
const htmlPrompt = `<legend>Entrez l'état du justificatif :</legend>
<select name="promptSelect" id="promptSelect" required>
<option value="valide">Valide</option>
<option value="attente">En Attente de validation</option>
<option value="non_valide">Non Valide</option>
<option value="modifie">Modifié</option>
</select>
<legend>Raison:</legend>
<textarea type="text" placeholder="Explication du justificatif (non obligatoire)" id="promptText" style="width:100%;"></textarea>
`;
content.innerHTML = htmlPrompt;
openPromptModal(
"Nouveau justificatif (Rapide)",
content,
success,
() => {},
"var(--color-primary)"
);
};
if (assiduite.etudid) {
getJustificatifFromPeriod(period, assiduite.etudid, action);
}
}
function justifyAssiduite(assiduite_id, justified) {
const assiduite = {
est_just: justified,
};
const path = getUrl() + `/api/assiduite/${assiduite_id}/edit`;
let bool = false;
sync_post(
path,
assiduite,
(data, status) => {
bool = true;
},
(data, status) => {
//error
console.error(data, status);
errorAlert();
}
);
return bool;
}
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" : "") : ""
}`;
//TODO Utiliser async_get au lieu de jquery
$.ajax({
async: true,
type: "GET",
url: url_api,
success: (data, status) => {
if (status === "success") {
action(data);
}
},
error: () => {},
});
}
function deleteJustificatif(justif_id) {
const path = getUrl() + `/api/justificatif/delete`;
sync_post(
path,
[justif_id],
(data, status) => {
//success
if (data.success.length > 0) {
}
},
(data, status) => {
//error
console.error(data, status);
errorAlert();
}
);
}
function errorAlert() {
const html = `
<h4>Il peut s'agir d'un problème de droits, ou d'une modification survenue sur le serveur.</h4>
<p>Si le problème persiste, demandez de l'aide sur le Discord d'assistance de ScoDoc</p>
`;
const div = document.createElement("div");
div.innerHTML = html;
openAlertModal("Une erreur s'est produite", div);
}
const moduleimpls = {};
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"
? "Tout module"
: assiduite.external_data.module;
} else {
return "Pas de module";
}
}
if (id in moduleimpls) {
return moduleimpls[id];
}
const url_api = getUrl() + `/api/moduleimpl/${id}`;
sync_get(
url_api,
(data) => {
moduleimpls[id] = `${data.module.code} ${data.module.abbrev}`;
},
(data) => {
moduleimpls[id] = "Pas de module";
}
);
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;
}
return obj.user_nom_complet || obj.user_id;
}
const inscriptionsModule = {};
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;
},
(data, status) => {
//error
console.error(data, status);
errorAlert();
}
);
}
const etudsInscrits = inscriptionsModule[moduleimpl_id].map((i) => i.etudid);
return etudsInscrits.indexOf(Number(etudid)) !== -1;
}