ScoDoc-PE/app/static/js/assiduites.js
2023-04-17 15:53:30 +02:00

1793 lines
49 KiB
JavaScript

// <=== CONSTANTS and GLOBALS ===>
const TIMEZONE = "Europe/Paris";
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 = {};
// Variable qui définit si le processus d'action de masse est lancé
let currentMassAction = false;
/**
* Variable de gestion des conflits
*/
let modal;
let closeBtn;
let timeline;
let deleteBtn;
let splitBtn;
let editBtn;
let selectedAssiduite;
/**
* 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,
});
// <<== Outils ==>>
/**
* 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() {
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;
}
});
}
}
);
});
getAssiduitesFromEtuds(true);
document.querySelector(".selectors").disabled = true;
generateMassAssiduites();
generateAllEtudRow();
};
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();
}
/**
* 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) {
$.ajax({
async: false,
type: "GET",
url: path,
success: success,
error: errors,
});
}
/**
* 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) {
$.ajax({
async: false,
type: "POST",
url: path,
data: JSON.stringify(data),
success: success,
error: errors,
});
}
// <<== 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();
const assiduite = {
date_debut: tlTimes.deb.format(),
date_fin: tlTimes.fin.format(),
};
const moduleimpl = getModuleImplId();
if (moduleimpl !== null) {
assiduite["moduleimpl_id"] = moduleimpl;
}
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);
}
);
};
//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) => {
const moduleimpl = getModuleImplId();
if (moduleimpl !== null) {
assiduite["moduleimpl_id"] = moduleimpl;
}
return assiduite;
});
const path = getUrl() + `/api/assiduites/edit`;
sync_post(
path,
editQueue,
(data, status) => {
//success
},
(data, status) => {
//error
console.error(data, status);
}
);
};
//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);
}
);
};
//On exécute les fonctions de queue
create();
edit();
supprimer();
//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
const action = 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") {
field.querySelector(`.rbtn.${action}`).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;
//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">
</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()) {
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 = (time - heure) * 60;
if (minutes < 1) {
minutes = "00";
}
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");
const date = dateInput.valueAsDate;
$("#datestr").text(formatDate(date).capitalize());
}
function verifyDateInSemester() {
const date = new moment.tz(
document.querySelector("#tl_date").value,
TIMEZONE
);
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 (!input.disabled) {
input.showPicker();
}
});
if (onchange != null) {
input.addEventListener("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 moment.tz(str, TIMEZONE).format(`DD/MM/Y ${separator} HH:mm`);
}
/**
* Fonction qui vérifie si une période est dans un interval
* Objet période / interval
* {
* deb: moment.tz(<Date>),
* fin: moment.tz(<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 : moment.tz(), fin: moment.tz()}
*/
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 = document.querySelector("#tl_date").value;
//On génère des objets temps (moment.tz)
values = values.map((el) => {
el = toTime(el).replace("h", ":");
el = `${dateiso}T${el}`;
return moment.tz(el, TIMEZONE);
});
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 isConflictSameAsTimeLine(conflict) {
const tlTimes = getTimeLineTimes();
const clTimes = {
deb: moment.tz(conflict.date_debut, TIMEZONE),
fin: moment.tz(conflict.date_fin, TIMEZONE),
};
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 = document.querySelector("#tl_date").valueAsDate;
date.setHours(0, 0, 0, 0);
return date;
}
/**
* Retourne un objet date représentant le jour suivant
* @returns {Date} le jour suivant
*/
function getNextDate() {
const date = getDate();
const next = new Date(date.valueOf());
next.setDate(date.getDate() + 1);
next.setHours(0, 0, 0, 0);
return next;
}
/**
* Retourne un objet date représentant le jour précédent
* @returns {Date} le jour précédent
*/
function getPrevDate() {
const date = getDate();
const next = new Date(date.valueOf());
next.setDate(date.getDate() - 1);
next.setHours(0, 0, 0, 0);
return next;
}
/**
* Transformation d'un objet Date en chaîne ISO
* @param {Date} date
* @returns {string} la date iso avec le timezone
*/
function toIsoString(date) {
var tzo = -date.getTimezoneOffset(),
dif = tzo >= 0 ? "+" : "-",
pad = function (num) {
return (num < 10 ? "0" : "") + num;
};
return (
date.getFullYear() +
"-" +
pad(date.getMonth() + 1) +
"-" +
pad(date.getDate()) +
"T" +
pad(date.getHours()) +
":" +
pad(date.getMinutes()) +
":" +
pad(date.getSeconds()) +
dif +
pad(Math.floor(Math.abs(tzo) / 60)) +
":" +
pad(Math.abs(tzo) % 60)
);
}
/**
* Transforme un temps numérique en une date moment.tz
* @param {number} nb
* @returns {moment.tz} Une date formée du temps donné et de la date courante
*/
function numberTimeToDate(nb) {
time = toTime(nb).replace("h", ":");
date = document.querySelector("#tl_date").value;
datetime = `${date}T${time}`;
return moment.tz(datetime, TIMEZONE);
}
// <<== 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'objets Assiduités {<etudid:str> : [<assiduite>,]}
*/
function getAssiduitesFromEtuds(clear, has_formsemestre = true) {
const etudIds = Object.keys(etuds).join(",");
const formsemestre_id = has_formsemestre
? `formsemestre_id=${getFormSemestreId()}&`
: "";
const date_debut = toIsoString(getPrevDate());
const date_fin = toIsoString(getNextDate());
if (clear) {
assiduites = {};
}
const url_api =
getUrl() +
`/api/assiduites/group/query?date_debut=${formsemestre_id}${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) => {
assiduites[key] = data[key];
});
}
});
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();
const assiduite = {
date_debut: tlTimes.deb.format(),
date_fin: tlTimes.fin.format(),
etat: etat,
};
const moduleimpl = getModuleImplId();
if (moduleimpl !== null) {
assiduite["moduleimpl_id"] = moduleimpl;
}
const path = getUrl() + `/api/assiduite/${etudid}/create`;
sync_post(
path,
[assiduite],
(data, status) => {
//success
if (data.success.length > 0) {
let obj = data.success["0"].assiduite_id;
}
},
(data, status) => {
//error
console.error(data, status);
}
);
}
/**
* 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"].assiduite_id;
}
},
(data, status) => {
//error
console.error(data, status);
}
);
}
/**
*
* @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) {
const assiduite = {
etat: etat,
moduleimpl_id: getModuleImplId(),
};
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);
}
);
return bool;
}
/**
* Récupération des assiduités conflictuelles avec la période de la time line
* @param {String | Number} etudid identifiant de l'étudiant
* @returns {Array[Assiduité]} un tableau d'assiduité
*/
function getAssiduitesConflict(etudid) {
const etudAssiduites = assiduites[etudid];
if (!etudAssiduites) {
return [];
}
const period = getTimeLineTimes();
return etudAssiduites.filter((assi) => {
const interval = {
deb: moment.tz(assi.date_debut, TIMEZONE),
fin: moment.tz(assi.date_fin, TIMEZONE),
};
return hasTimeConflict(period, interval);
});
}
/**
* 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: moment.tz(getPrevDate(), TIMEZONE),
fin: moment.tz(getDate(), TIMEZONE),
};
const prevAssiduites = etudAssiduites
.filter((assi) => {
const interval = {
deb: moment.tz(assi.date_debut, TIMEZONE),
fin: moment.tz(assi.date_fin, TIMEZONE),
};
return hasTimeConflict(period, interval);
})
.sort((a, b) => {
const a_fin = moment.tz(a.date_fin, TIMEZONE);
const b_fin = moment.tz(b.date_fin, TIMEZONE);
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, has_formsemestre = true) {
const formsemestre_id = has_formsemestre
? `formsemestre_id=${getFormSemestreId()}&`
: "";
const date_debut = toIsoString(getPrevDate());
const date_fin = toIsoString(getNextDate());
const url_api =
getUrl() +
`/api/assiduites/${etudid}/query?${formsemestre_id}date_debut=${date_debut}&date_fin=${date_fin}`;
sync_get(url_api, (data, status) => {
if (status === "success") {
assiduites[etudid] = data;
}
});
}
/**
* Déclenchement d'une action après appuie sur un bouton d'assiduité
* @param {HTMLInputElement} element Bouton d'assiduité appuyé
*/
function assiduiteAction(element) {
const field = element.parentElement;
const type = field.getAttribute("type");
const etudid = parseInt(field.getAttribute("etudid"));
const assiduite_id = parseInt(field.getAttribute("assiduite_id"));
const etat = getAssiduiteValue(field);
// Cas de l'action de masse -> peuplement des queues
if (currentMassAction) {
switch (type) {
case "création":
addToMassActionQueue("creer", { etat: etat, etudid: etudid });
break;
case "édition":
if (etat === "remove") {
addToMassActionQueue("supprimer", assiduite_id);
} else {
addToMassActionQueue("editer", {
etat: etat,
assiduite_id: assiduite_id,
});
}
break;
}
} else {
// Cas normal -> mise à jour en base
switch (type) {
case "création":
createAssiduite(etat, etudid);
break;
case "édition":
if (etat === "remove") {
deleteAssiduite(assiduite_id);
} else {
editAssiduite(assiduite_id, etat);
}
break;
case "conflit":
openModal(assiduites[etudid]);
break;
}
if (type != "conflit") {
document
.querySelector(".toast-holder")
.appendChild(
generateToast(
document.createTextNode("L'assiduité a bien été enregistrée.")
)
);
}
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}">`;
} else {
assi += `<input type="checkbox" value="${abs}" name="btn_assiduites_${index}" id="rbtn_${abs}" class="rbtn ${abs}">`;
}
});
const conflit = assiduite.type == "conflit" ? "conflit" : "";
const pdp_url = `${getUrl()}/api/etudiant/etudid/${etud.id}/photo?size=small`;
const HTML = `<div class="etud_row ${conflit}" id="etud_row_${etud.id}">
<div class="index">${index}</div>
<div class="name_field">
<img class="pdp" src="${pdp_url}">
<div class="name_set">
<h4 class="nom">${etud.nom}</h4>
<h5 class="prenom">${etud.prenom}</h5>
</div>
</div>
<div class="assiduites_bar">
<div id="prevDateAssi" class="${assiduite.prevAssiduites?.etat?.toLowerCase()}">
<span class="mini_tick">13h</span>
</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 (isConflictSameAsTimeLine(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()) {
row.querySelector(".btns_field.single").setAttribute("disabled", "true");
}
}
/**
* Création de la minitiline d'un étudiant
* @param {Array[Assiduité]} assiduitesArray
* @returns {HTMLElement} l'élément correspondant à la mini timeline
*/
function createMiniTimeline(assiduitesArray) {
const dateiso = document.getElementById("tl_date").value;
const timeline = document.createElement("div");
timeline.className = "mini-timeline";
if (isSingleEtud()) {
timeline.classList.add("single");
}
const timelineDate = moment(dateiso).startOf("day");
const dayStart = timelineDate.clone().add(8, "hours");
const dayEnd = timelineDate.clone().add(18, "hours");
const dayDuration = moment.duration(dayEnd.diff(dayStart)).asMinutes();
assiduitesArray.forEach((assiduité) => {
const startDate = moment(assiduité.date_debut);
const endDate = moment(assiduité.date_fin);
if (startDate.isBefore(dayStart)) {
startDate.startOf("day").add(8, "hours");
}
if (endDate.isAfter(dayEnd)) {
endDate.startOf("day").add(18, "hours");
}
const block = document.createElement("div");
block.className = "mini-timeline-block";
const startOffset = moment.duration(startDate.diff(dayStart)).asMinutes();
const duration = moment.duration(endDate.diff(startDate)).asMinutes();
const leftPercentage = (startOffset / dayDuration) * 100;
const widthPercentage = (duration / dayDuration) * 100;
block.style.left = `${leftPercentage}%`;
block.style.width = `${widthPercentage}%`;
if (isSingleEtud()) {
block.addEventListener("click", () => {
let deb = startDate.hours() + startDate.minutes() / 60;
let fin = endDate.hours() + endDate.minutes() / 60;
deb = Math.max(8, deb);
fin = Math.min(18, fin);
setPeriodValues(deb, fin);
updateSelectedSelect(getCurrentAssiduiteModuleImplId());
});
}
//ajouter affichage assiduites on over
setupAssiduiteBuble(block, assiduité);
switch (assiduité.etat) {
case "PRESENT":
block.classList.add("present");
break;
case "RETARD":
block.classList.add("retard");
break;
case "ABSENT":
block.classList.add("absent");
break;
default:
block.style.backgroundColor = "white";
}
timeline.appendChild(block);
});
return timeline;
}
/**
* 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 setupAssiduiteBuble(el, assiduite) {
if (!assiduite) return;
el.addEventListener("mouseenter", (event) => {
const bubble = document.querySelector(".assiduite-bubble");
bubble.className = "assiduite-bubble";
bubble.classList.add("is-active", assiduite.etat.toLowerCase());
bubble.innerHTML = "";
const idDiv = document.createElement("div");
idDiv.className = "assiduite-id";
idDiv.textContent = `ID: ${assiduite.assiduite_id}`;
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 userIdDiv = document.createElement("div");
userIdDiv.className = "assiduite-user_id";
userIdDiv.textContent = `saisi le ${formatDateModal(
assiduite.entry_date,
"à"
)} \npar ${getUserFromId(assiduite.user_id)}`;
bubble.appendChild(userIdDiv);
bubble.style.left = `${event.clientX - bubble.offsetWidth / 2}px`;
bubble.style.top = `${event.clientY + 20}px`;
});
el.addEventListener("mouseout", () => {
const bubble = document.querySelector(".assiduite-bubble");
bubble.classList.remove("is-active");
});
}
/**
* Mise à jour d'une ligne étudiant
* @param {String | Number} etudid l'identifiant de l'étudiant
*/
function actualizeEtud(etudid) {
actualizeEtudAssiduite(etudid, !isSingleEtud());
//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()) {
actualizeEtud(etudid);
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
);
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 ==>
/**
* Mise à jour du modal de conflit
* @param {Array[Assiduité]} assiduiteList Liste des assiduités de l'étudiant
*/
function refreshModal(assiduiteList) {
const tlTime = getTimeLineTimes();
renderTimeline(assiduiteList, {
date_debut: tlTime.deb,
date_fin: tlTime.fin,
});
}
/**
* Ouverture du modal de conflit
* @param {Array[Assiduité]} assiduiteList Liste des assiduités de l'étudiant
*/
function openModal(assiduiteList) {
modal.style.display = "block";
const tlTime = getTimeLineTimes();
renderTimeline(assiduiteList, {
date_debut: tlTime.deb,
date_fin: tlTime.fin,
});
}
/**
* Fermeture du modal de conflit
*/
function closeModal() {
modal.style.display = "none";
}
/**
* Génération du modal
* @param {Array[Assiduité]} assiduites la liste des assiduités à afficher
* @param {Période} specialAssiduite Une assiduité représentant la période conflictuelle
*/
function renderTimeline(assiduites, specialAssiduite) {
const timeLabels = document.querySelector(".time-labels");
const assiduitesContainer = document.querySelector(".assiduites-container");
timeLabels.innerHTML = "";
assiduitesContainer.innerHTML = '<div class="assiduite-special"></div>';
// Ajout des labels d'heure sur la frise chronologique
// TODO permettre la modification des bornes (8 et 18)
for (let i = 8; i <= 18; i++) {
const timeLabel = document.createElement("div");
timeLabel.className = "time-label";
timeLabel.textContent = i < 10 ? `0${i}:00` : `${i}:00`;
timeLabels.appendChild(timeLabel);
}
//Placement de la période conflictuelle sur la timeline
const specialAssiduiteEl = document.querySelector(".assiduite-special");
specialAssiduiteEl.style.width = getWidth(
specialAssiduite.date_debut,
specialAssiduite.date_fin
);
specialAssiduiteEl.style.left = getLeftPosition(specialAssiduite.date_debut);
specialAssiduiteEl.style.top = "0";
specialAssiduiteEl.style.zIndex = "0"; // Place l'assiduité spéciale en arrière-plan
assiduitesContainer.appendChild(specialAssiduiteEl);
//Placement des assiduités sur la timeline
assiduites.forEach((assiduite) => {
const el = document.createElement("div");
el.className = "assiduite";
el.style.backgroundColor = getColor(assiduite.etat);
el.style.width = getWidth(assiduite.date_debut, assiduite.date_fin);
el.style.left = getLeftPosition(assiduite.date_debut);
el.style.top = "10px";
el.setAttribute("data-id", assiduite.assiduite_id);
el.addEventListener("click", () => selectAssiduite(assiduite));
// Ajout des informations dans la visualisation d'une assiduité
const infoContainer = document.createElement("div");
infoContainer.className = "assiduite-info";
const idDiv = document.createElement("div");
idDiv.className = "assiduite-id";
idDiv.textContent = `ID: ${assiduite.assiduite_id}`;
infoContainer.appendChild(idDiv);
const periodDivDeb = document.createElement("div");
periodDivDeb.className = "assiduite-period";
periodDivDeb.textContent = `${formatDateModal(assiduite.date_debut)}`;
infoContainer.appendChild(periodDivDeb);
const periodDivFin = document.createElement("div");
periodDivFin.className = "assiduite-period";
periodDivFin.textContent = `${formatDateModal(assiduite.date_fin)}`;
infoContainer.appendChild(periodDivFin);
const stateDiv = document.createElement("div");
stateDiv.className = "assiduite-state";
stateDiv.textContent = `État: ${assiduite.etat.capitalize()}`;
infoContainer.appendChild(stateDiv);
const userIdDiv = document.createElement("div");
userIdDiv.className = "assiduite-user_id";
userIdDiv.textContent = `saisi le ${formatDateModal(
assiduite.entry_date,
"à"
)} \npar ${getUserFromId(assiduite.user_id)}`;
infoContainer.appendChild(userIdDiv);
el.appendChild(infoContainer);
assiduitesContainer.appendChild(el);
});
}
/**
* Transformation d'une date de début en position sur la timeline
* @param {String} start
* @returns {String} un déplacement par rapport à la gauche en %
*/
function getLeftPosition(start) {
const startTime = new moment.tz(start, TIMEZONE);
const startMins = (startTime.hours() - 8) * 60 + startTime.minutes();
return (startMins / (18 * 60 - 8 * 60)) * 100 + "%";
}
/**
* Ajustement de l'espacement vertical entre les assiduités superposées
* @param {HTMLElement} container le conteneur des assiduités
* @param {String} start la date début de l'assiduité à placer
* @param {String} end la date de fin de l'assiduité à placer
* @returns {String} La position en px
*/
function getTopPosition(container, start, end) {
const overlaps = (a, b) => {
return a.start < b.end && a.end > b.start;
};
const startTime = new moment.tz(start, TIMEZONE);
const endTime = new moment.tz(end, TIMEZONE);
const assiduiteDuration = { start: startTime, end: endTime };
let position = 0;
let hasOverlap = true;
while (hasOverlap) {
hasOverlap = false;
Array.from(container.children).some((el) => {
const elStart = new moment.tz(el.getAttribute("data-start"));
const elEnd = new moment.tz(el.getAttribute("data-end"));
const elDuration = { start: elStart, end: elEnd };
if (overlaps(assiduiteDuration, elDuration)) {
position += 25; // Pour ajuster l'espacement vertical entre les assiduités superposées
hasOverlap = true;
return true;
}
return false;
});
}
return position + "px";
}
/**
* Transformation d'un état en couleur
* @param {String} state l'état
* @returns {String} la couleur correspondant à l'état
*/
function getColor(state) {
switch (state) {
case "PRESENT":
return "#37f05f";
case "ABSENT":
return "#ec5c49";
case "RETARD":
return "#ecb52a";
default:
return "gray";
}
}
/**
* Calcule de la largeur de l'assiduité sur la timeline
* @param {String} start date iso de début
* @param {String} end date iso de fin
* @returns {String} la taille en %
*/
function getWidth(start, end) {
const startTime = new moment.tz(start, TIMEZONE);
const endTime = new moment.tz(end, TIMEZONE);
const duration = (endTime - startTime) / 1000 / 60;
return (duration / (18 * 60 - 8 * 60)) * 100 + "%";
}
/**
* Sélection d'une assiduité sur la timeline
* @param {Assiduité} assiduite l'assiduité sélectionnée
*/
function selectAssiduite(assiduite) {
// Désélectionner l'assiduité précédemment sélectionnée
if (selectedAssiduite) {
const prevSelectedEl = document.querySelector(
`.assiduite[data-id="${selectedAssiduite.assiduite_id}"]`
);
if (prevSelectedEl) {
prevSelectedEl.classList.remove("selected");
}
}
// Sélectionner la nouvelle assiduité
selectedAssiduite = assiduite;
const selectedEl = document.querySelector(
`.assiduite[data-id="${assiduite.assiduite_id}"]`
);
if (selectedEl) {
selectedEl.classList.add("selected");
}
//Mise à jour de la partie information du modal
const selectedModal = document.querySelector(".modal-assiduite-content");
selectedModal.classList.add("show");
document.getElementById("modal-assiduite-id").textContent =
assiduite.assiduite_id;
document.getElementById(
"modal-assiduite-user"
).textContent = `saisi le ${formatDateModal(
assiduite.entry_date,
"à"
)} \npar ${getUserFromId(assiduite.user_id)}`;
document.getElementById("modal-assiduite-module").textContent =
assiduite.moduleimpl_id;
document.getElementById("modal-assiduite-deb").textContent = formatDateModal(
assiduite.date_debut
);
document.getElementById("modal-assiduite-fin").textContent = formatDateModal(
assiduite.date_fin
);
document.getElementById("modal-assiduite-etat").textContent =
assiduite.etat.capitalize();
//Activation des boutons d'actions de conflit
deleteBtn.disabled = false;
splitBtn.disabled = false;
editBtn.disabled = false;
}
/**
* Suppression de l'assiduité sélectionnée
*/
function deleteAssiduiteModal() {
if (!selectedAssiduite) return;
deleteAssiduite(selectedAssiduite.assiduite_id);
actualizeEtud(selectedAssiduite.etudid);
refreshModal(assiduites[selectedAssiduite.etudid]);
// Désélection de l'assiduité
resetSelection();
}
/**
* Division d'une assiduité
* @param {Assiduité} assiduite l'assiduité sélectionnée
*/
function splitAssiduiteModal(assiduite) {
//Préparation du prompt
const htmlPrompt = `<legend>Entrez l'heure de séparation (HH:mm) :</legend>
<input type="time" id="promptTime" name="appt"
min="08:00" max="18:00" required>`;
const fieldSet = document.createElement("fieldset");
fieldSet.classList.add("fieldsplit");
fieldSet.innerHTML = htmlPrompt;
//Callback de division
const success = () => {
const separatorTime = document.getElementById("promptTime").value;
const dateString =
document.querySelector("#tl_date").value + `T${separatorTime}`;
const separtorDate = new moment.tz(dateString, TIMEZONE);
const assiduite_debut = new moment.tz(assiduite.date_debut, TIMEZONE);
const assiduite_fin = new moment.tz(assiduite.date_fin, TIMEZONE);
if (
separtorDate.isAfter(assiduite_debut) &&
separtorDate.isBefore(assiduite_fin)
) {
const assiduite_avant = {
etat: assiduite.etat,
date_debut: assiduite_debut.format(),
date_fin: separtorDate.format(),
};
const assiduite_apres = {
etat: assiduite.etat,
date_debut: separtorDate.format(),
date_fin: assiduite_fin.format(),
};
if (assiduite.moduleimpl_id) {
assiduite_apres["moduleimpl_id"] = assiduite.moduleimpl_id;
assiduite_avant["moduleimpl_id"] = assiduite.moduleimpl_id;
}
deleteAssiduite(assiduite.assiduite_id);
const path = getUrl() + `/api/assiduite/${assiduite.etudid}/create`;
sync_post(
path,
[assiduite_avant, assiduite_apres],
(data, status) => {
//success
},
(data, status) => {
//error
console.error(data, status);
}
);
actualizeEtud(assiduite.etudid);
refreshModal(assiduites[assiduite.etudid]);
resetSelection();
} else {
const att = document.createTextNode(
"L'heure de séparation doit être compris dans la période de l'assiduité sélectionnée."
);
openAlertModal("Attention", att, "", "#ecb52a");
}
};
openPromptModal("Entrée demandée", fieldSet, success, () => {}, "#37f05f");
}
/**
* Modification d'une assiduité conflictuelle
* @param {Assiduité} selectedAssiduite l'assiduité sélectionnée
*/
function editAssiduiteModal(selectedAssiduite) {
if (!selectedAssiduite) return;
//Préparation du modal d'édition
const htmlPrompt = `<legend>Entrez l'état de l'assiduité :</legend>
<select name="promptSelect" id="promptSelect" required>
<option value="">Choissez l'état</option>
<option value="present">Présent</option>
<option value="retard">En Retard</option>
<option value="absent">Absent</option>
</select>`;
const fieldSet = document.createElement("fieldset");
fieldSet.classList.add("fieldsplit");
fieldSet.innerHTML = htmlPrompt;
//Callback d'action d'édition
const success = () => {
const newState = document.getElementById("promptSelect").value;
if (!["present", "absent", "retard"].includes(newState.toLowerCase())) {
const att = document.createTextNode(
"L'état doit être 'present', 'absent' ou 'retard'."
);
openAlertModal("Attention", att, "", "#ecb52a");
return;
}
// Actualiser l'affichage
editAssiduite(selectedAssiduite.assiduite_id, newState);
actualizeEtud(selectedAssiduite.etudid);
refreshModal(assiduites[selectedAssiduite.etudid]);
// Désélection de l'assiduité
resetSelection();
};
//Affichage du prompt
openPromptModal("Entrée demandée", fieldSet, success, () => {}, "#37f05f");
}
/**
* Remise à zéro de la sélection
* Désactivation des boutons d'actions de conflit
*/
function resetSelection() {
selectedAssiduite = null;
deleteBtn.disabled = true;
splitBtn.disabled = true;
editBtn.disabled = true;
document.querySelector(".modal-assiduite-content").classList.remove("show");
}
/**
* Ajout des évents sur les boutons du modal
*/
window.onload = () => {
modal = document.getElementById("myModal");
closeBtn = document.querySelector(".close");
timeline = document.getElementById("timeline");
deleteBtn = document.getElementById("delete");
splitBtn = document.getElementById("split");
editBtn = document.getElementById("edit");
selectedAssiduite = null;
closeBtn?.addEventListener("click", closeModal);
deleteBtn?.addEventListener("click", deleteAssiduiteModal);
splitBtn?.addEventListener("click", () => {
if (selectedAssiduite) {
splitAssiduiteModal(selectedAssiduite);
}
});
editBtn.addEventListener("click", () => {
if (selectedAssiduite) {
editAssiduiteModal(selectedAssiduite);
}
});
};
// <<== Gestion de la récupération d'informations ==>>
/**
* Récupération d'un nom d'utilisateur à partir d'un identifiant
* @param {Number} id identifiant de l'utilisateur
* @returns {String} le nom de l'utilisateur ou son pseudo ou "Non Renseigné"
*/
function getUserFromId(id) {
if (id == "") {
return "Non Renseigné";
}
let name = "Non Renseigné";
sync_get(`/ScoDoc/api/user/${id}`, (data) => {
if (data.nom != "" && data.prenom != "") {
name = `${data.nom} ${data.prenom}`;
} else {
name = data.user_name;
}
});
return name;
}
/**
* 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;
}
/**
* 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: dateDeb,
fin: 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) {
const mod = currentAssiduites[0].moduleimpl_id;
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 == "edition") {
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) {
let justifs = [];
sync_get(
getUrl() +
`/api/justificatifs/${etudid}/query?date_debut=${date.deb.format()}&date_fin=${date.fin.format()}`,
(data) => {
justifs = data;
}
);
return justifs;
}
function updateJustifieButton(isJustified, isDisabled = true) {
const btn = document.getElementById("justif-rapide");
if (isJustified) {
btn.classList.add("justifie");
} else {
btn.classList.remove("justifie");
}
if (isDisabled) {
btn.setAttribute("disabled", "true");
} else {
btn.removeAttribute("disabled");
}
}
function fastJustify(assiduite) {
const period = {
deb: new moment.tz(assiduite.date_debut, TIMEZONE),
fin: new moment.tz(assiduite.date_fin, TIMEZONE),
};
const justifs = getJustificatifFromPeriod(period);
if (justifs.length > 0) {
//modifier l'assiduité
} else {
//créer un nouveau justificatif
// Afficher prompt -> demander raison et état
}
}