forked from ScoDoc/ScoDoc
460 lines
18 KiB
Django/Jinja
460 lines
18 KiB
Django/Jinja
<script>
|
|
/**
|
|
* 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;
|
|
|
|
const percent = (duration / (18 * 60 - 8 * 60)) * 100
|
|
|
|
if (percent > 100) {
|
|
console.log(start, end);
|
|
console.log(startTime, endTime)
|
|
}
|
|
|
|
return percent + "%";
|
|
}
|
|
|
|
class ConflitResolver {
|
|
constructor(assiduitesList, conflictPeriod, interval) {
|
|
this.list = assiduitesList;
|
|
this.conflictPeriod = conflictPeriod;
|
|
this.interval = interval;
|
|
this.selectedAssiduite = null;
|
|
|
|
this.element = undefined;
|
|
|
|
this.callbacks = {
|
|
delete: () => { },
|
|
split: () => { },
|
|
edit: () => { },
|
|
}
|
|
}
|
|
|
|
refresh(assiduitesList, periode) {
|
|
this.list = assiduitesList;
|
|
if (periode) {
|
|
this.conflictPeriod = periode;
|
|
}
|
|
|
|
this.render()
|
|
|
|
}
|
|
|
|
selectAssiduite() {
|
|
|
|
}
|
|
|
|
open() {
|
|
const html = `
|
|
<div id="myModal" class="modal">
|
|
<div class="modal-content">
|
|
<span class="close">×</span>
|
|
<h2>Veuillez régler le conflit pour poursuivre</h2>
|
|
<!-- Ajout de la frise chronologique -->
|
|
<div class="modal-timeline">
|
|
<div class="time-labels"></div>
|
|
<div class="assiduites-container"></div>
|
|
</div>
|
|
|
|
<div class="action-buttons">
|
|
<button id="delete" class="btnPrompt" disabled>Supprimer</button>
|
|
<button id="split" class="btnPrompt" disabled>Séparer</button>
|
|
<button id="edit" class="btnPrompt" disabled>Modifier l'état</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-assiduite-content">
|
|
<h2>Information de l'assiduité sélectionnée</h2>
|
|
<div class="infos">
|
|
<p>Assiduite id : <span id="modal-assiduite-id">A</span></p>
|
|
<p>Etat : <span id="modal-assiduite-etat">B</span></p>
|
|
<p>Date de début : <span id="modal-assiduite-deb">C</span></p>
|
|
<p>Date de fin: <span id="modal-assiduite-fin">D</span></p>
|
|
<p>Module : <span id="modal-assiduite-module">E</span></p>
|
|
<p><span id="modal-assiduite-user">F</span></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.insertAdjacentHTML("afterbegin", html);
|
|
this.element = document.getElementById('myModal');
|
|
this.deleteBtn = document.querySelector('#myModal #delete');
|
|
this.editBtn = document.querySelector('#myModal #edit');
|
|
this.splitBtn = document.querySelector('#myModal #split');
|
|
this.deleteBtn.addEventListener('click', () => { this.deleteAssiduiteModal() });
|
|
this.editBtn.addEventListener('click', () => { this.editAssiduiteModal() });
|
|
this.splitBtn.addEventListener('click', () => { this.splitAssiduiteModal() });
|
|
|
|
document.querySelector('#myModal .close').addEventListener('click', () => { this.close() })
|
|
|
|
this.render()
|
|
}
|
|
|
|
|
|
|
|
close() {
|
|
if (this.element) {
|
|
this.element.remove()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sélection d'une assiduité sur la timeline
|
|
* @param {Assiduité} assiduite l'assiduité sélectionnée
|
|
*/
|
|
selectAssiduite(assiduite) {
|
|
// Désélectionner l'assiduité précédemment sélectionnée
|
|
if (this.selectedAssiduite) {
|
|
const prevSelectedEl = document.querySelector(
|
|
`.assiduite[data-id="${this.selectedAssiduite.assiduite_id}"]`
|
|
);
|
|
if (prevSelectedEl) {
|
|
prevSelectedEl.classList.remove("selected");
|
|
}
|
|
}
|
|
|
|
// Sélectionner la nouvelle assiduité
|
|
this.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 ${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
|
|
this.deleteBtn.disabled = false;
|
|
this.splitBtn.disabled = false;
|
|
this.editBtn.disabled = false;
|
|
}
|
|
/**
|
|
* Suppression de l'assiduité sélectionnée
|
|
*/
|
|
deleteAssiduiteModal() {
|
|
if (!this.selectedAssiduite) return;
|
|
deleteAssiduite(this.selectedAssiduite.assiduite_id);
|
|
|
|
this.callbacks.delete(this.selectedAssiduite)
|
|
|
|
this.refresh(assiduites[this.selectedAssiduite.etudid]);
|
|
|
|
// Désélection de l'assiduité
|
|
this.resetSelection();
|
|
}
|
|
|
|
/**
|
|
* Division d'une assiduité
|
|
*/
|
|
splitAssiduiteModal() {
|
|
//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(this.selectedAssiduite.date_debut, TIMEZONE);
|
|
const assiduite_fin = new moment.tz(this.selectedAssiduite.date_fin, TIMEZONE);
|
|
|
|
if (
|
|
separtorDate.isAfter(assiduite_debut) &&
|
|
separtorDate.isBefore(assiduite_fin)
|
|
) {
|
|
const assiduite_avant = {
|
|
etat: this.selectedAssiduite.etat,
|
|
date_debut: assiduite_debut.format(),
|
|
date_fin: separtorDate.format(),
|
|
};
|
|
|
|
const assiduite_apres = {
|
|
etat: this.selectedAssiduite.etat,
|
|
date_debut: separtorDate.format(),
|
|
date_fin: assiduite_fin.format(),
|
|
};
|
|
|
|
if (this.selectedAssiduite.moduleimpl_id) {
|
|
assiduite_apres["moduleimpl_id"] = this.selectedAssiduite.moduleimpl_id;
|
|
assiduite_avant["moduleimpl_id"] = this.selectedAssiduite.moduleimpl_id;
|
|
}
|
|
|
|
deleteAssiduite(this.selectedAssiduite.assiduite_id);
|
|
|
|
const path = getUrl() + `/api/assiduite/${this.selectedAssiduite.etudid}/create`;
|
|
sync_post(
|
|
path,
|
|
[assiduite_avant, assiduite_apres],
|
|
(data, status) => {
|
|
//success
|
|
},
|
|
(data, status) => {
|
|
//error
|
|
console.error(data, status);
|
|
errorAlert();
|
|
}
|
|
);
|
|
|
|
this.callbacks.split(this.selectedAssiduite)
|
|
this.refresh(assiduites[this.selectedAssiduite.etudid]);
|
|
this.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
|
|
*/
|
|
editAssiduiteModal() {
|
|
if (!this.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(this.selectedAssiduite.assiduite_id, newState);
|
|
this.callbacks.edit(this.selectedAssiduite)
|
|
this.refresh(assiduites[this.selectedAssiduite.etudid]);
|
|
|
|
// Désélection de l'assiduité
|
|
this.resetSelection();
|
|
};
|
|
|
|
//Affichage du prompt
|
|
openPromptModal("Entrée demandée", fieldSet, success, () => { }, "#37f05f");
|
|
}
|
|
|
|
/**
|
|
* Génération du modal
|
|
*/
|
|
render() {
|
|
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(
|
|
this.conflictPeriod.deb,
|
|
this.conflictPeriod.fin
|
|
);
|
|
specialAssiduiteEl.style.left = getLeftPosition(this.conflictPeriod.deb);
|
|
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
|
|
this.list.forEach((assiduite) => {
|
|
const period = {
|
|
deb: new moment.tz(assiduite.date_debut, TIMEZONE),
|
|
fin: new moment.tz(assiduite.date_fin, TIMEZONE),
|
|
};
|
|
if (!hasTimeConflict(period, this.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", () => this.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 ${assiduite.user_id}`;
|
|
infoContainer.appendChild(userIdDiv);
|
|
|
|
el.appendChild(infoContainer);
|
|
assiduitesContainer.appendChild(el);
|
|
});
|
|
}
|
|
/**
|
|
* Remise à zéro de la sélection
|
|
* Désactivation des boutons d'actions de conflit
|
|
*/
|
|
resetSelection() {
|
|
this.selectedAssiduite = null;
|
|
this.deleteBtn.disabled = true;
|
|
this.splitBtn.disabled = true;
|
|
this.editBtn.disabled = true;
|
|
}
|
|
}
|
|
</script> |