forked from ScoDoc/ScoDoc
f26ecb1f8a
Assiduites : ajout style justifié (minitimeline)
1937 lines
53 KiB
JavaScript
1937 lines
53 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 = {};
|
|
let justificatifs = {};
|
|
|
|
// 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 array = [...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();
|
|
|
|
const tlTimes = getTimeLineTimes();
|
|
|
|
const period_assi = {
|
|
date_debut: tlTimes.deb.format(),
|
|
date_fin: tlTimes.fin.format(),
|
|
etat: "CRENEAU",
|
|
};
|
|
|
|
array.push(period_assi);
|
|
|
|
array.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 (assiduité.etat != "CRENEAU") {
|
|
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());
|
|
updateJustifyBtn();
|
|
});
|
|
}
|
|
//ajouter affichage assiduites on over
|
|
setupAssiduiteBuble(block, assiduité);
|
|
|
|
if (assiduité.est_just) {
|
|
block.classList.add("justified");
|
|
}
|
|
}
|
|
|
|
const action = (justificatifs) => {
|
|
if (justificatifs.length > 0) {
|
|
let j = "invalid_justified";
|
|
|
|
justificatifs.forEach((ju) => {
|
|
if (ju.etat == "VALIDE") {
|
|
j = "justified";
|
|
}
|
|
});
|
|
|
|
block.classList.add(j);
|
|
}
|
|
};
|
|
|
|
getJustificatifFromPeriod(
|
|
{
|
|
deb: new moment.tz(assiduité.date_debut, TIMEZONE),
|
|
fin: new moment.tz(assiduité.date_fin, TIMEZONE),
|
|
},
|
|
assiduité.etudid,
|
|
action
|
|
);
|
|
|
|
switch (assiduité.etat) {
|
|
case "PRESENT":
|
|
block.classList.add("present");
|
|
break;
|
|
case "RETARD":
|
|
block.classList.add("retard");
|
|
break;
|
|
case "ABSENT":
|
|
block.classList.add("absent");
|
|
break;
|
|
case "CRENEAU":
|
|
block.classList.add("creneau");
|
|
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);
|
|
const interval = {
|
|
deb: new moment.tz(getDate(), TIMEZONE),
|
|
fin: new moment.tz(getNextDate(), TIMEZONE),
|
|
};
|
|
//Placement des assiduités sur la timeline
|
|
assiduites.forEach((assiduite) => {
|
|
const period = {
|
|
deb: new moment.tz(assiduite.date_debut, TIMEZONE),
|
|
fin: new moment.tz(assiduite.date_fin, TIMEZONE),
|
|
};
|
|
if (!hasTimeConflict(period, interval)) {
|
|
return;
|
|
}
|
|
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 "#9CF1AF";
|
|
case "ABSENT":
|
|
return "#F1A69C";
|
|
case "RETARD":
|
|
return "#F1D99C";
|
|
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 == "é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, "s")
|
|
.format()}&date_fin=${date.fin.subtract(1, "s").format()}`,
|
|
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) {
|
|
const period = {
|
|
deb: new moment.tz(assiduite.date_debut, TIMEZONE),
|
|
fin: new moment.tz(assiduite.date_fin, TIMEZONE),
|
|
};
|
|
const action = (justifs) => {
|
|
if (justifs.length > 0) {
|
|
justifyAssiduite(assiduite.assiduite_id, !assiduite.est_just);
|
|
} else {
|
|
console.debug("WIP");
|
|
//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 moment.tz(assiduite.date_debut, TIMEZONE)
|
|
.add(1, "s")
|
|
.format(),
|
|
date_fin: new moment.tz(assiduite.date_fin, TIMEZONE)
|
|
.subtract(1, "s")
|
|
.format(),
|
|
raison: raison,
|
|
etat: etat,
|
|
};
|
|
|
|
createJustificatif(justif);
|
|
|
|
// justifyAssiduite(assiduite.assiduite_id, true);
|
|
generateAllEtudRow();
|
|
};
|
|
|
|
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,
|
|
() => {},
|
|
"#7059FF"
|
|
);
|
|
}
|
|
};
|
|
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);
|
|
}
|
|
);
|
|
|
|
return bool;
|
|
}
|
|
|
|
function createJustificatif(justif) {
|
|
const path = getUrl() + `/api/justificatif/${etudid}/create`;
|
|
sync_post(
|
|
path,
|
|
[justif],
|
|
(data, status) => {
|
|
//success
|
|
if (data.success.length > 0) {
|
|
console.table(data[0]);
|
|
}
|
|
console.warn(data);
|
|
},
|
|
(data, status) => {
|
|
//error
|
|
console.error(data, status);
|
|
}
|
|
);
|
|
}
|