Assiduité : documentation code JS + cleanup code

This commit is contained in:
Iziram 2024-06-11 14:50:30 +02:00
parent faa6f552d4
commit 70605edad7
13 changed files with 352 additions and 496 deletions

View File

@ -168,6 +168,10 @@ function creerLigneEtudiant(etud, index) {
date_fin: null, date_fin: null,
}; };
/**Retourne une liste d'assiduité en conflit avec la période actuelle
* @param {Array} assiduites - Les assiduités de l'étudiant
* @returns {Array} Les assiduités en conflit
*/
function recupConflitsAssiduites(assiduites) { function recupConflitsAssiduites(assiduites) {
const period = getPeriodAsDate(); const period = getPeriodAsDate();
@ -182,9 +186,12 @@ function creerLigneEtudiant(etud, index) {
); );
}); });
} }
// Pas de conflit en readonly
const conflits = readOnly ? [] : recupConflitsAssiduites(etud.assiduites); const conflits = readOnly ? [] : recupConflitsAssiduites(etud.assiduites);
// Si il y a des conflits, on prend le premier pour l'afficher
// si les dates de début et de fin sont les mêmes, c'est une édition
// sinon c'est un conflit
if (conflits.length > 0) { if (conflits.length > 0) {
currentAssiduite = conflits[0]; currentAssiduite = conflits[0];
@ -200,6 +207,49 @@ function creerLigneEtudiant(etud, index) {
: "conflit"; : "conflit";
} }
// Création de la ligne étudiante en DOM
/* exemple de ligne étudiante
<div class="etud_row" id="etud_row_497">
<div class="index">1</div>
<div class="name_field"><img src="../../api/etudiant/etudid/497/photo?size=small" alt="Baudin Joseph" class="pdp"><a
class="name_set" href="bilan_etud?etudid=497">
<h4 class="nom">Baudin</h4>
<h5 class="prenom">Joseph</h5>
</a></div>
<div class="assiduites_bar">
<div id="prevDateAssi" class="vide"></div>
<div class="mini-timeline"><span class="mini_tick" style="left: 47.5%;">13h</span>
<div class="mini-timeline-block creneau" style="left: 20%; width: 17.5%;"></div>
</div>
</div>
<fieldset class="btns_field single" etudid="497" type="creation" assiduite_id="-1">
<input
type="checkbox"
value="present"
name="btn_assiduites_1"
id="rbtn_present"
class="rbtn present"
title="present"
>
<input
type="checkbox"
value="retard"
name="btn_assiduites_1"
id="rbtn_retard"
class="rbtn retard"
title="retard"
>
<input
type="checkbox"
value="absent"
name="btn_assiduites_1"
id="rbtn_absent"
class="rbtn absent"
title="absent"
>
</fieldset>
</div>
*/
const ligneEtud = document.createElement("div"); const ligneEtud = document.createElement("div");
ligneEtud.classList.add("etud_row"); ligneEtud.classList.add("etud_row");
if (Object.keys(etudsDefDem).includes(etud.id)) { if (Object.keys(etudsDefDem).includes(etud.id)) {
@ -388,6 +438,9 @@ async function creerTousLesEtudiants(etuds) {
etudsDiv.innerHTML = ""; etudsDiv.innerHTML = "";
const moduleImplId = readOnly ? null : $("#moduleimpl_select").val(); const moduleImplId = readOnly ? null : $("#moduleimpl_select").val();
const inscriptions = await getInscriptionModule(moduleImplId); const inscriptions = await getInscriptionModule(moduleImplId);
// on trie les étudiants par ordre alphabétique
// et on garde ceux qui sont inscrits au module
// puis pour chaque étudiant on crée une ligne
[...etuds.values()] [...etuds.values()]
.sort((a, b) => { .sort((a, b) => {
return a.sort_key > b.sort_key ? 1 : -1; return a.sort_key > b.sort_key ? 1 : -1;
@ -496,10 +549,9 @@ async function getInscriptionModule(moduleimpl_id) {
return inscriptionsModules.get(moduleimpl_id); return inscriptionsModules.get(moduleimpl_id);
} }
// Mise à jour de la ligne étudiant
async function MiseAJourLigneEtud(etud) { async function MiseAJourLigneEtud(etud) {
//Récupérer ses assiduités //Récupérer ses assiduités
function RecupAssiduitesEtudiant(etudid) { function RecupAssiduitesEtudiant(etudid) {
const date = $("#date").datepicker("getDate"); const date = $("#date").datepicker("getDate");
const date_debut = date.add(-1, "days").format("YYYY-MM-DDTHH:mm"); const date_debut = date.add(-1, "days").format("YYYY-MM-DDTHH:mm");
@ -527,6 +579,8 @@ async function MiseAJourLigneEtud(etud) {
} }
await RecupAssiduitesEtudiant(etud.id); await RecupAssiduitesEtudiant(etud.id);
// Une fois les assiduités récupérées, on met à jour la ligne étudiant
// on replace l'ancienne ligne par la nouvellement générée
const etudRow = document.getElementById(`etud_row_${etud.id}`); const etudRow = document.getElementById(`etud_row_${etud.id}`);
if (etudRow == null) return; if (etudRow == null) return;
@ -540,12 +594,14 @@ async function MiseAJourLigneEtud(etud) {
etudRow.replaceWith(ligneEtud); etudRow.replaceWith(ligneEtud);
} }
// Action appelée lors d'un clic sur un bouton d'assiduité
// Création, édition ou suppression d'une assiduité
async function actionAssiduite(etud, etat, type, assiduite = null) { async function actionAssiduite(etud, etat, type, assiduite = null) {
const modimpl_id = $("#moduleimpl_select").val(); const modimpl_id = $("#moduleimpl_select").val();
if (assiduite && assiduite.etat.toLowerCase() === etat) type = "suppression"; if (assiduite && assiduite.etat.toLowerCase() === etat) type = "suppression";
const { deb, fin } = getPeriodAsDate(); const { deb, fin } = getPeriodAsDate();
// génération d'un objet assiduité basique qui sera complété
let assiduiteObjet = assiduite ?? { let assiduiteObjet = assiduite ?? {
date_debut: deb, date_debut: deb,
date_fin: fin, date_fin: fin,
@ -554,7 +610,8 @@ async function actionAssiduite(etud, etat, type, assiduite = null) {
assiduiteObjet.etat = etat; assiduiteObjet.etat = etat;
assiduiteObjet.moduleimpl_id = modimpl_id; assiduiteObjet.moduleimpl_id = modimpl_id;
// En fonction du type d'action on appelle la bonne route
// avec les bonnes valeurs
if (type === "creation") { if (type === "creation") {
await async_post( await async_post(
`../../api/assiduite/${etud.id}/create`, `../../api/assiduite/${etud.id}/create`,
@ -606,7 +663,9 @@ async function actionAssiduite(etud, etat, type, assiduite = null) {
); );
} }
} }
// Fonction pour afficher un message d'erreur si le module n'est pas renseigné
// ou si l'étudiant n'est pas inscrit au module.
// On donne le message d'erreur d'une requête api et cela affiche le message correspondant
function erreurModuleImpl(message) { function erreurModuleImpl(message) {
if (message == "Module non renseigné") { if (message == "Module non renseigné") {
const HTML = ` const HTML = `
@ -635,7 +694,9 @@ function erreurModuleImpl(message) {
openAlertModal("Sélection du module", content); openAlertModal("Sélection du module", content);
} }
} }
// Fonction pour ajouter en lot une assiduité à tous les étudiants
// Fonctionne uniquement pour créer ou supprimer des assiduités
// Pas d'édition possible
function mettreToutLeMonde(etat, el = null) { function mettreToutLeMonde(etat, el = null) {
const lignesEtuds = [...document.querySelectorAll("fieldset.btns_field")]; const lignesEtuds = [...document.querySelectorAll("fieldset.btns_field")];
@ -709,7 +770,7 @@ function mettreToutLeMonde(etat, el = null) {
} }
envoiToastTous("remove", assiduites_id.length); envoiToastTous("remove", assiduites_id.length);
if (Object.keys(unDeleted).length == 0) return; if (Object.keys(unDeleted).length == 0) return;
// CAS : des assiduités d'étudiants n'ont pas pu être supprimés
let unDeletedEtuds = ` let unDeletedEtuds = `
<ul> <ul>
${Object.keys(unDeleted) ${Object.keys(unDeleted)
@ -771,6 +832,7 @@ function mettreToutLeMonde(etat, el = null) {
}); });
} }
// Affichage d'un loader (animation jeu pong)
function afficheLoader() { function afficheLoader() {
const loaderDiv = document.createElement("div"); const loaderDiv = document.createElement("div");
loaderDiv.id = "loader"; loaderDiv.id = "loader";
@ -782,11 +844,13 @@ function afficheLoader() {
loaderDiv.appendChild(loader); loaderDiv.appendChild(loader);
document.body.appendChild(loaderDiv); document.body.appendChild(loaderDiv);
} }
// Retrait du loader (animation jeu pong)
function retirerLoader() { function retirerLoader() {
document.getElementById("loader").remove(); document.getElementById("loader").remove();
} }
// Simplification de l'envoie de toast pour un étudiant
// affiche le nom, le prénom et l'état de l'assiduité avec une couleur spécifique
function envoiToastEtudiant(etat, etud) { function envoiToastEtudiant(etat, etud) {
let etatAffiche; let etatAffiche;
@ -810,8 +874,10 @@ function envoiToastEtudiant(etat, etud) {
pushToast(generateToast(span, getToastColorFromEtat(etat.toUpperCase()), 5)); pushToast(generateToast(span, getToastColorFromEtat(etat.toUpperCase()), 5));
} }
// Fonction pour simplifier l'envoie de toast avec le bouton "mettre tout le monde"
// TODO commenter toutes les fonctions js // On donne un etat et un compte et cela affichera le message associé.
// ex : 12 assiduités ont été supprimées
// ex : 15 étudiants ont été mis Absent.
function envoiToastTous(etat, count) { function envoiToastTous(etat, count) {
const span = document.createElement("span"); const span = document.createElement("span");
let etatAffiche = etat; let etatAffiche = etat;
@ -840,7 +906,9 @@ function envoiToastTous(etat, count) {
pushToast(generateToast(span, getToastColorFromEtat(etat.toUpperCase()), 5)); pushToast(generateToast(span, getToastColorFromEtat(etat.toUpperCase()), 5));
} }
// Permet de savoir si un jour est travaillé ou pas
// jour : Date
// nonWorkdays : Array[str] => ["mar", "sam", "dim"]
function estJourTravail(jour, nonWorkdays) { function estJourTravail(jour, nonWorkdays) {
const d = Intl.DateTimeFormat("fr-FR", { const d = Intl.DateTimeFormat("fr-FR", {
timeZone: SCO_TIMEZONE, timeZone: SCO_TIMEZONE,
@ -851,6 +919,9 @@ function estJourTravail(jour, nonWorkdays) {
return !nonWorkdays.includes(d); return !nonWorkdays.includes(d);
} }
// Renvoie le dernier jour travaillé disponible.
// par défaut va en arrière (dans le passé)
// si anti == False => va dans le futur
function retourJourTravail(date, anti = true) { function retourJourTravail(date, anti = true) {
const jourMiliSecondes = 86400000; // 24 * 3600 * 1000 | H * s * ms const jourMiliSecondes = 86400000; // 24 * 3600 * 1000 | H * s * ms
let jour = date; let jour = date;
@ -864,12 +935,18 @@ function retourJourTravail(date, anti = true) {
} }
return jour; return jour;
} }
// Vérifie si la date courante est travaillée
// Si ce n'est pas le cas, on change la date pour le dernier jour travaillé (passé)
// et on affiche une alerte
// (utilise le datepicker #date)
function dateCouranteEstTravaillee() { function dateCouranteEstTravaillee() {
const date = $("#date").datepicker("getDate"); const date = $("#date").datepicker("getDate");
if (!estJourTravail(date, nonWorkDays)) { if (!estJourTravail(date, nonWorkDays)) {
// récupération du jour travaillé le plus proche
const nouvelleDate = retourJourTravail(date); const nouvelleDate = retourJourTravail(date);
$("#date").datepicker("setDate", nouvelleDate); $("#date").datepicker("setDate", nouvelleDate);
// Création du message d'alerte
let msg = "Le jour sélectionné"; let msg = "Le jour sélectionné";
if (new Date().format("YYYY-MM-DD") == date.format("YYYY-MM-DD")) { if (new Date().format("YYYY-MM-DD") == date.format("YYYY-MM-DD")) {
msg = "Aujourd'hui"; msg = "Aujourd'hui";
@ -889,13 +966,15 @@ function dateCouranteEstTravaillee() {
)}.` )}.`
) )
); );
// Affichage de l'alerte
openAlertModal("Attention", div, "", "#eec660"); openAlertModal("Attention", div, "", "#eec660");
return false; return false;
} }
return true; return true;
} }
// Fonction pour passer au jour suivant
// anti : bool => si true, on va dans le passé
function jourSuivant(anti = false) { function jourSuivant(anti = false) {
let date = $("#date").datepicker("getDate"); let date = $("#date").datepicker("getDate");

View File

@ -111,7 +111,13 @@ Bilan assiduité de {{sco.etud.nomprenom}}
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script> <script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
<script> <script>
// Récupération des statistiques d'assiduité et affichage
// Fonction appelée lors du clic sur le bouton "Actualiser"
// Et au chargement de la page
function stats() { function stats() {
// On prend les dates de début et de fin
// (format DD/MM/YYYY) et on les convertit en Date()
const dd_val = document.getElementById('stats_date_debut').value; const dd_val = document.getElementById('stats_date_debut').value;
const df_val = document.getElementById('stats_date_fin').value; const df_val = document.getElementById('stats_date_fin').value;
let date_debut = new Date(Date.fromFRA(dd_val)); let date_debut = new Date(Date.fromFRA(dd_val));
@ -120,7 +126,7 @@ Bilan assiduité de {{sco.etud.nomprenom}}
openAlertModal("Dates invalides", document.createTextNode('Les dates sélectionnées sont invalides')); openAlertModal("Dates invalides", document.createTextNode('Les dates sélectionnées sont invalides'));
return; return;
} }
// On met les dates à 00h et 23h59 pour avoir la journée entière
date_debut = date_debut.startOf("day") date_debut = date_debut.startOf("day")
date_fin = date_fin.endOf("day") date_fin = date_fin.endOf("day")
@ -128,10 +134,15 @@ Bilan assiduité de {{sco.etud.nomprenom}}
openAlertModal("Dates invalides", document.createTextNode('La date de début se situe après la date de fin.')); openAlertModal("Dates invalides", document.createTextNode('La date de début se situe après la date de fin.'));
return; return;
} }
// Appel à l'api et affichage des stats sur la page
countAssiduites(date_debut.toFakeIso(), date_fin.toFakeIso()) countAssiduites(date_debut.toFakeIso(), date_fin.toFakeIso())
} }
// Appel à l'api pour récupérer les statistiques d'assiduité
// Effectue l'action passée en paramètre sur les données récupérées
// dateDeb : date de début au format ISO
// dateFin : date de fin au format ISO
// action : fonction à appeler sur les données récupérées
function getAssiduitesCount(dateDeb, dateFin, action) { function getAssiduitesCount(dateDeb, dateFin, action) {
const url_api = `../../api/assiduites/${etudid}/count/query?date_debut=${dateDeb}&date_fin=${dateFin}&etat=absent,retard,present&split`; const url_api = `../../api/assiduites/${etudid}/count/query?date_debut=${dateDeb}&date_fin=${dateFin}&etat=absent,retard,present&split`;
async_get( async_get(
@ -142,6 +153,9 @@ Bilan assiduité de {{sco.etud.nomprenom}}
} }
function showStats(data){ function showStats(data){
// Initialisation d'un objet contenant les résultats
// Sera mis à jour avec le reste des valeurs dans la suite
// du code
const counter = { const counter = {
"present": { "present": {
"total": data["present"], "total": data["present"],
@ -155,33 +169,39 @@ Bilan assiduité de {{sco.etud.nomprenom}}
"justi": data["absent"]["justifie"], "justi": data["absent"]["justifie"],
} }
} }
// Reset du DOM
const values = document.querySelector('.stats-values'); const values = document.querySelector('.stats-values');
values.innerHTML = ""; values.innerHTML = "";
// Pour chaque état d'assiduité (present, retard, absent)
Object.keys(counter).forEach((key) => { Object.keys(counter).forEach((key) => {
// On créé les éléments HTML qui serviront d'affichage
const item = document.createElement('div'); const item = document.createElement('div');
item.classList.add('stats-values-item'); item.classList.add('stats-values-item');
const div = document.createElement('div'); const div = document.createElement('div');
div.classList.add('stats-values-part'); div.classList.add('stats-values-part');
// Fonction anonyme pour éviter de réécrire tout le temps un test
// Si l'état est "present" alors cela renvoie "" (=> pas de nb justifié)
// Sinon cela renvoie "dont X justifiées"
const withJusti = (key, metric) => { const withJusti = (key, metric) => {
if (key == "present") return ""; if (key == "present") return "";
return ` dont ${counter[key].justi[metric]} justifiées` return ` dont ${counter[key].justi[metric]} justifiées`
} }
// HEURE : aroundie à 2 décimales.
const heure = document.createElement('span'); const heure = document.createElement('span');
heure.textContent = `${counter[key].total.heure.toFixed(2)} heure(s)${withJusti(key, "heure")}`; heure.textContent = `${counter[key].total.heure.toFixed(2)} heure(s)${withJusti(key, "heure")}`;
// DEMI-JOURNEE
const demi = document.createElement('span'); const demi = document.createElement('span');
demi.textContent = `${counter[key].total.demi} demi-journée(s)${withJusti(key, "demi")}`; demi.textContent = `${counter[key].total.demi} demi-journée(s)${withJusti(key, "demi")}`;
// JOURNEE
const jour = document.createElement('span'); const jour = document.createElement('span');
jour.textContent = `${counter[key].total.journee} journée(s)${withJusti(key, "journee")}`; jour.textContent = `${counter[key].total.journee} journée(s)${withJusti(key, "journee")}`;
// On met à jour le DOM avec les valeurs calculées
// On met l'état en Titre pour chaque partie
div.append(jour, demi, heure); div.append(jour, demi, heure);
const title = document.createElement('h5'); const title = document.createElement('h5');
title.textContent = key.capitalize(); title.textContent = key.capitalize();
@ -190,8 +210,10 @@ Bilan assiduité de {{sco.etud.nomprenom}}
values.appendChild(item); values.appendChild(item);
}); });
// On vérifie si l'étudiant a trop d'absences
const nbAbs = data["absent"]["non_justifie"][assi_metric]; const nbAbs = data["absent"]["non_justifie"][assi_metric];
if (nbAbs > assi_seuil) { if (nbAbs > assi_seuil) {
// L'étudiant est au dessus du seuil (défini dans les préférences du département)
document.querySelector('.alerte').classList.remove('invisible'); document.querySelector('.alerte').classList.remove('invisible');
document.querySelector('.alerte p').textContent = `Attention, cet étudiant a trop d'absences ${nbAbs} / ${assi_seuil} (${metriques[assi_metric]})` document.querySelector('.alerte p').textContent = `Attention, cet étudiant a trop d'absences ${nbAbs} / ${assi_seuil} (${metriques[assi_metric]})`
} else { } else {
@ -202,7 +224,8 @@ Bilan assiduité de {{sco.etud.nomprenom}}
function countAssiduites(dateDeb, dateFin) { function countAssiduites(dateDeb, dateFin) {
getAssiduitesCount(dateDeb, dateFin, showStats); getAssiduitesCount(dateDeb, dateFin, showStats);
} }
// Table de conversion des métriques
// Utilisé pour afficher les valeurs en fonction de la métrique
const metriques = { const metriques = {
"heure": "H.", "heure": "H.",
"demi": "1/2 J.", "demi": "1/2 J.",
@ -210,7 +233,8 @@ Bilan assiduité de {{sco.etud.nomprenom}}
} }
// Récupération des données obligatoires pour les fonctions
// Depuis le contexte de la page (Jinja2)
const etudid = {{ sco.etud.id }}; const etudid = {{ sco.etud.id }};
const assi_metric = "{{ assi_metric | safe }}"; const assi_metric = "{{ assi_metric | safe }}";
const assi_seuil = {{ assi_seuil }}; const assi_seuil = {{ assi_seuil }};
@ -218,6 +242,8 @@ Bilan assiduité de {{sco.etud.nomprenom}}
const assi_date_debut = "{{date_debut}}"; const assi_date_debut = "{{date_debut}}";
const assi_date_fin = "{{date_fin}}"; const assi_date_fin = "{{date_fin}}";
// Au chargement de la page, on met les dates par défaut
// Et on appelle la fonction stats() pour afficher les stats
window.addEventListener('load', () => { window.addEventListener('load', () => {
document.getElementById('stats_date_fin').value = assi_date_fin; document.getElementById('stats_date_fin').value = assi_date_fin;
document.getElementById('stats_date_debut').value = assi_date_debut; document.getElementById('stats_date_debut').value = assi_date_debut;

View File

@ -192,6 +192,8 @@ Calendrier de l'assiduité
</style> </style>
<script> <script>
// Retourne un object {show_pres: bool, show_reta: bool, mode_demi: bool}
// Correspondant aux options cochées. (voir les checkbox dans le html)
function getOptions() { function getOptions() {
return { return {
"show_pres": document.getElementById("show_pres").checked, "show_pres": document.getElementById("show_pres").checked,
@ -200,27 +202,33 @@ Calendrier de l'assiduité
} }
} }
// Fonction qui recharge la page en fonction des options cochées
function updatePage() { function updatePage() {
// On génère un Objet URL à partir de l'url actuelle
const url = new URL(location.href); const url = new URL(location.href);
const options = getOptions(); const options = getOptions();
// grace à l'objet URL on peut modifier les paramètres de l'url
// sans devoir parser l'url et la reconstruire
url.searchParams.set("annee", document.getElementById('annee').value); url.searchParams.set("annee", document.getElementById('annee').value);
url.searchParams.set("mode_demi", options.mode_demi); url.searchParams.set("mode_demi", options.mode_demi);
url.searchParams.set("show_pres", options.show_pres); url.searchParams.set("show_pres", options.show_pres);
url.searchParams.set("show_reta", options.show_reta); url.searchParams.set("show_reta", options.show_reta);
// On change l'url de la page si elle est différente de l'url actuelle
if (location.href != url.href) { if (location.href != url.href) {
location.href = url.href location.href = url.href
} }
} }
// L'année scolaire par défaut
const defAnnee = "{{ annee | safe}}" const defAnnee = "{{ annee | safe}}"
// Les années disponibles pour l'étudiant (["2022-2023", "2021-2022", ...])
let annees = {{ annees | safe }} let annees = {{ annees | safe }}
// On retire les doublons
annees = annees.filter((x, i) => annees.indexOf(x) === i) annees = annees.filter((x, i) => annees.indexOf(x) === i)
const etudid = {{ sco.etud.id }}; const etudid = {{ sco.etud.id }};
// Peuplement du sélecteur d'année scolaire avec les années disponibles
const select = document.querySelector('#annee'); const select = document.querySelector('#annee');
annees.forEach((a) => { annees.forEach((a) => {
// <option value="2022">2022-2023</option>
const opt = document.createElement("option"); const opt = document.createElement("option");
let a_1 = a.substring(0, 4) let a_1 = a.substring(0, 4)
opt.value = a_1 + "", opt.value = a_1 + "",
@ -232,13 +240,15 @@ Calendrier de l'assiduité
} }
select.appendChild(opt) select.appendChild(opt)
}) })
// On ajoute un écouteur d'événement pour le rechargement de la page
// donc effectué sur le sélecteur d'année scolaire et les checkbox
document.querySelectorAll('input[type="checkbox"].memo, #annee').forEach(el => { document.querySelectorAll('input[type="checkbox"].memo, #annee').forEach(el => {
el.addEventListener('change', function () { el.addEventListener('change', function () {
updatePage(); updatePage();
}) })
}); });
// On ajoute un click event pour chaque case d'assiduité afin de rediriger
// Sur la page d'édition de l'assiduité
document.querySelectorAll('[assi_id]').forEach((el, i) => { document.querySelectorAll('[assi_id]').forEach((el, i) => {
el.addEventListener('click', () => { el.addEventListener('click', () => {
const assi_id = el.getAttribute('assi_id'); const assi_id = el.getAttribute('assi_id');

View File

@ -44,10 +44,15 @@ label.stats_checkbox {
const date_fin = "{{date_fin}}"; const date_fin = "{{date_fin}}";
const group_ids = "{{group_ids}}"; const group_ids = "{{group_ids}}";
// Changement de la date de début ou de fin des statitiques
// Recharge la page avec les nouvelles dates
function stats() { function stats() {
const deb = Date.fromFRA(document.querySelector('#stats_date_debut').value); const deb = Date.fromFRA(document.querySelector('#stats_date_debut').value);
const fin = Date.fromFRA(document.querySelector('#stats_date_fin').value); const fin = Date.fromFRA(document.querySelector('#stats_date_fin').value);
location.href = `visu_assi_group?group_ids=${group_ids}&date_debut=${deb}&date_fin=${fin}`; let url = new URL(window.location.href);
url.searchParams.set('date_debut', deb);
url.searchParams.set('date_fin', fin);
location.href = url.href;
} }
window.addEventListener('load', () => { window.addEventListener('load', () => {

View File

@ -127,19 +127,29 @@
<script> <script>
const alertmodal = document.getElementById("alertModal"); const alertmodal = document.getElementById("alertModal");
/**
* openAlertModal
* @param {string} titre // titre du modal
* @param {HTMLElement} contenu // contenu du modal
* @param {string} footer // texte du footer
* @param {string} color // valeur CSS de la couleur de fond
*/
function openAlertModal(titre, contenu, footer, color = "var(--color-error)") { function openAlertModal(titre, contenu, footer, color = "var(--color-error)") {
// On affiche le modal
alertmodal.classList.add('is-active'); alertmodal.classList.add('is-active');
// On met à jour les valeurs du modal
alertmodal.querySelector('.alertmodal-title').textContent = titre; alertmodal.querySelector('.alertmodal-title').textContent = titre;
alertmodal.querySelector('.alertmodal-body').innerHTML = "" alertmodal.querySelector('.alertmodal-body').innerHTML = ""
alertmodal.querySelector('.alertmodal-body').appendChild(contenu); alertmodal.querySelector('.alertmodal-body').appendChild(contenu);
alertmodal.querySelector('.alertmodal-footer').textContent = footer; alertmodal.querySelector('.alertmodal-footer').textContent = footer;
// On met à jour les couleurs de chaque partie du modal
const banners = Array.from(alertmodal.querySelectorAll('.alertmodal-footer,.alertmodal-header')) const banners = Array.from(alertmodal.querySelectorAll('.alertmodal-footer,.alertmodal-header'))
banners.forEach((ban) => { banners.forEach((ban) => {
ban.style.backgroundColor = color; ban.style.backgroundColor = color;
}) })
// On ajoute un écouteur d'événement pour fermer le modal
// si on clique en dehors de celui-ci
alertmodal.addEventListener('click', (e) => { alertmodal.addEventListener('click', (e) => {
if (e.target.id == alertmodal.id) { if (e.target.id == alertmodal.id) {
alertmodal.classList.remove('is-active'); alertmodal.classList.remove('is-active');
@ -148,9 +158,11 @@
} }
}) })
} }
// Fonction pour fermer le modal de manière programmatique
function closeAlertModal() { function closeAlertModal() {
alertmodal.classList.remove("is-active") alertmodal.classList.remove("is-active")
} }
// On ajoute un écouteur d'événement pour fermer le modal avec la croix
const alertClose = document.querySelector(".alertmodal-close"); const alertClose = document.querySelector(".alertmodal-close");
alertClose.onclick = function () { alertClose.onclick = function () {
closeAlertModal() closeAlertModal()

View File

@ -32,7 +32,9 @@
}); });
} }
// Pour chaque assiduité (et pour le créneau) on vient créer un block
// le block est positionné en fonction de l'heure de début et de fin
// et prend une largeur proportionnelle à la durée de l'assiduité
array.forEach((assiduité) => { array.forEach((assiduité) => {
if(assiduité.etat == "CRENEAU" && readOnly) return; if(assiduité.etat == "CRENEAU" && readOnly) return;
let startDate = new Date(Date.removeUTC(assiduité.date_debut)); let startDate = new Date(Date.removeUTC(assiduité.date_debut));
@ -57,6 +59,8 @@
block.style.width = `${widthPercentage}%`; block.style.width = `${widthPercentage}%`;
if (assiduité.etat != "CRENEAU") { if (assiduité.etat != "CRENEAU") {
// Si on clique dessus on veut pouvoir
// mettre à jour la timeline principale et modifier le moduleimpl_select
block.addEventListener("click", () => { block.addEventListener("click", () => {
let deb = startDate.getHours() + startDate.getMinutes() / 60; let deb = startDate.getHours() + startDate.getMinutes() / 60;
let fin = endDate.getHours() + endDate.getMinutes() / 60; let fin = endDate.getHours() + endDate.getMinutes() / 60;
@ -90,7 +94,7 @@
return timeline; return timeline;
} }
// Ajout du "13h" sur la mini timeline
function setMiniTick(timelineDate, dayStart, dayDuration) { function setMiniTick(timelineDate, dayStart, dayDuration) {
const endDate = timelineDate.clone().startOf("day"); const endDate = timelineDate.clone().startOf("day");
endDate.setHours(13, 0); endDate.setHours(13, 0);

View File

@ -157,65 +157,92 @@
</style> </style>
<script> <script>
// Récupère l'élément modal par son ID
const promptModal = document.getElementById("promptModal"); const promptModal = document.getElementById("promptModal");
/**
* Ouvre la fenêtre modale avec les paramètres spécifiés.
* @param {string} titre - Le titre de la modale.
* @param {HTMLElement} contenu - Le contenu de la modale.
* @param {Function} success - La fonction à appeler en cas de succès.
* @param {Function} [cancel] - La fonction à appeler en cas d'annulation (optionnelle).
* @param {string} [color="var(--color-error)"] - La couleur de fond des bannières de la modale (optionnelle).
*/
function openPromptModal(titre, contenu, success, cancel = () => { }, color = "var(--color-error)") { function openPromptModal(titre, contenu, success, cancel = () => { }, color = "var(--color-error)") {
// Active la modale en ajoutant une classe
promptModal.classList.add('is-active'); promptModal.classList.add('is-active');
// Met à jour le titre et le contenu de la modale
promptModal.querySelector('.promptModal-title').textContent = titre; promptModal.querySelector('.promptModal-title').textContent = titre;
promptModal.querySelector('.promptModal-body').innerHTML = "" promptModal.querySelector('.promptModal-body').innerHTML = "";
promptModal.querySelector('.promptModal-body').appendChild(contenu); promptModal.querySelector('.promptModal-body').appendChild(contenu);
promptModal.querySelector('.promptModal-footer').innerHTML = "" // Vide le pied de page et ajoute les boutons d'action
promptModal.querySelector('.promptModal-footer').innerHTML = "";
promptModalButtonAction(success, cancel).forEach((btnPrompt) => { promptModalButtonAction(success, cancel).forEach((btnPrompt) => {
promptModal.querySelector('.promptModal-footer').appendChild(btnPrompt) promptModal.querySelector('.promptModal-footer').appendChild(btnPrompt);
}) });
// Change la couleur de fond des bannières de la modale
const banners = Array.from(promptModal.querySelectorAll('.promptModal-footer,.promptModal-header')) const banners = Array.from(promptModal.querySelectorAll('.promptModal-footer, .promptModal-header'));
banners.forEach((ban) => { banners.forEach((ban) => {
ban.style.backgroundColor = color; ban.style.backgroundColor = color;
}) });
// Ajoute un écouteur d'événement pour fermer la modale en cliquant en dehors
promptModal.addEventListener('click', (e) => { promptModal.addEventListener('click', (e) => {
if (e.target.id == promptModal.id) { if (e.target.id == promptModal.id) {
promptModal.classList.remove('is-active'); promptModal.classList.remove('is-active');
promptModal.removeEventListener('click', this) promptModal.removeEventListener('click', this);
} }
}) });
// Désactive le défilement de la page principale
document.body.style.overflow = "hidden"; document.body.style.overflow = "hidden";
} }
/**
* Crée les boutons de validation et d'annulation pour la modale.
* @param {Function} success - La fonction à appeler en cas de succès.
* @param {Function} cancel - La fonction à appeler en cas d'annulation.
* @returns {HTMLElement[]} - Les boutons de validation et d'annulation.
*/
function promptModalButtonAction(success, cancel) { function promptModalButtonAction(success, cancel) {
const succBtn = document.createElement('button') const succBtn = document.createElement('button');
succBtn.classList.add("btnPrompt") succBtn.classList.add("btnPrompt");
succBtn.textContent = "Valider" succBtn.textContent = "Valider";
succBtn.addEventListener('click', () => { succBtn.addEventListener('click', () => {
const retour = success(closePromptModal); const retour = success(closePromptModal);
if (retour == null || retour == false || retour == undefined) { if (retour == null || retour == false || retour == undefined) {
closePromptModal(); closePromptModal();
} }
}) });
const cancelBtn = document.createElement('button')
cancelBtn.classList.add("btnPrompt") const cancelBtn = document.createElement('button');
cancelBtn.textContent = "Annuler" cancelBtn.classList.add("btnPrompt");
cancelBtn.textContent = "Annuler";
cancelBtn.addEventListener('click', () => { cancelBtn.addEventListener('click', () => {
cancel(); cancel();
closePromptModal(); closePromptModal();
}) });
return [succBtn, cancelBtn] return [succBtn, cancelBtn];
} }
/**
* Ferme la fenêtre modale.
*/
function closePromptModal() { function closePromptModal() {
promptModal.classList.remove("is-active") promptModal.classList.remove("is-active");
document.body.style.overflow = "auto"; document.body.style.overflow = "auto";
} }
// Ajoute un écouteur d'événement pour fermer la modale en cliquant sur le bouton de fermeture
const promptClose = document.querySelector(".promptModal-close"); const promptClose = document.querySelector(".promptModal-close");
promptClose.onclick = function () { promptClose.onclick = function () {
closePromptModal() closePromptModal();
} };
</script> </script>
{% endblock promptModal %} {% endblock promptModal %}

View File

@ -154,7 +154,7 @@
<script> <script>
// Fonction pour mettre à jour l'url avec les options du tableau
function updateTableau() { function updateTableau() {
const url = new URL(location.href); const url = new URL(location.href);
const formValues = document.querySelectorAll(".options-tableau *[name]"); const formValues = document.querySelectorAll(".options-tableau *[name]");
@ -173,7 +173,7 @@
} }
const total_pages = {{total_pages}}; const total_pages = {{total_pages}};
// Fonction pour naviguer entre les pages, modifie le champ n_page de l'url
function navigateToPage(pageNumber){ function navigateToPage(pageNumber){
if(pageNumber > total_pages || pageNumber < 1) return; if(pageNumber > total_pages || pageNumber < 1) return;
const url = new URL(location.href); const url = new URL(location.href);
@ -186,7 +186,7 @@
} }
} }
// Préparation des opérations de trai sur les colonnes "external-sort"
window.addEventListener('load', ()=>{ window.addEventListener('load', ()=>{
const table_columns = [...document.querySelectorAll('th.external-sort')]; const table_columns = [...document.querySelectorAll('th.external-sort')];
table_columns.forEach((e)=>e.addEventListener('click', ()=>{ table_columns.forEach((e)=>e.addEventListener('click', ()=>{

View File

@ -1,140 +0,0 @@
<h2>Détails {{type}} concernant <span class="etudinfo"
id="etudid-{{objet.etudid}}">{{etud.html_link_fiche()|safe}}</span></h2>
<style>
.info-row {
margin-top: 12px;
}
.info-label {
font-weight: bold;
}
.info-etat {
font-size: 110%;
font-weight: bold;
background-color: rgb(253, 234, 210);
border: 1px solid grey;
border-radius: 4px;
padding: 4px;
}
.info-saisie {
margin-top: 12px;
margin-bottom: 12px;
font-style: italic;
}
</style>
<div id="informations">
<div class="info-saisie">
<span>Saisie par {{objet.saisie_par}} le {{objet.entry_date}}</span>
</div>
<div class="info-row">
<span class="info-label">Période :</span> du <b>{{objet.date_debut}}</b> au <b>{{objet.date_fin}}</b>
</div>
{% if type == "Assiduité" %}
<div class="info-row">
<span class="info-label">Module :</span> {{objet.module}}
</div>
{% else %}
{% endif %}
<div class="info-row">
{% if type == "Justificatif" %}
<span class="info-label">État du justificatif :</span>
{% else %}
<span class="info-label">État de l'assiduité :</span>
{% endif %}
<span class="info-etat">{{objet.etat}}</span>
</div>
<div class="info-row">
{% if type == "Justificatif" %}
<span class="info-label">Raison:</span>
{% if can_view_justif_detail %}
<span class="text">{{objet.raison or " "}}</span>
{% else %}
<span class="text unauthorized">(cachée)</span>
{% endif %}
{% else %}
<span class="info-label">Description:</span>
{% if objet.description != None %}
<span class="text">{{objet.description}}</span>
{% else %}
<span class="text"></span>
{% endif %}
{% endif %}
</span>
</div>
{# Affichage des justificatifs si assiduité justifiée #}
{% if type == "Assiduité" and objet.etat != "Présence" %}
<div class="info-row">
<span class="info-label">Justifiée: </span>
{% if objet.justification.est_just %}
<span class="text">Oui</span>
<div>
{% for justi in objet.justification.justificatifs %}
<a href="{{url_for('assiduites.tableau_assiduite_actions',
type='justificatif', action='details', obj_id=justi.justif_id, scodoc_dept=g.scodoc_dept)}}"
target="_blank" rel="noopener noreferrer">Justificatif du {{justi.date_debut}} au {{justi.date_fin}}</a>
{% endfor %}
</div>
{% else %}
<span class="text fontred">Non</span>
{% endif %}
</div>
{% endif %}
{# Affichage des assiduités justifiées si justificatif valide #}
{% if type == "Justificatif" and objet.etat == "Valide" %}
<div class="info-row">
<span class="info-label">Assiduités concernées: </span>
{% if objet.justification.assiduites %}
<ul>
{% for assi in objet.justification.assiduites %}
<li><a href="{{url_for('assiduites.tableau_assiduite_actions',
type='assiduite', action='details', obj_id=assi.assiduite_id, scodoc_dept=g.scodoc_dept)
}}" target="_blank">Assiduité {{assi.etat}} du {{assi.date_debut}} au
{{assi.date_fin}}</a>
</li>
{% endfor %}
</ul>
{% else %}
<span class="text">Aucune</span>
{% endif %}
</div>
{% endif %}
{# Affichage des fichiers des justificatifs #}
{% if type == "Justificatif"%}
<div class="info-row">
<span class="info-label">Fichiers enregistrés: </span>
{% if objet.justification.fichiers.total != 0 %}
<div>Total : {{objet.justification.fichiers.total}} </div>
<ul>
{% for filename in objet.justification.fichiers.filenames %}
<li>
<a
href="{{url_for('apiweb.justif_export',justif_id=objet.justif_id,
filename=filename, scodoc_dept=g.scodoc_dept)}}">{{filename}}</a>
</li>
{% endfor %}
{% if not objet.justification.fichiers.filenames %}
<li class="fontred">fichiers non visibles</li>
{% endif %}
</ul>
{% else %}
<span class="text">Aucun</span>
{% endif %}
</div>
{% if current_user.has_permission(sco.Permission.AbsChange) %}
<div><a class="stdlink" href="{{
url_for('assiduites.edit_justificatif_etud', scodoc_dept=g.scodoc_dept, justif_id=obj_id)
}}">modifier ce justificatif</a>
</div>
{% endif %}
{% endif %}
</div>

View File

@ -1,117 +0,0 @@
<h2>Modifier {{objet_name}} de {{ etud.html_link_fiche() | safe }}</h2>
{# XXX cette page ne semble plus utile ! remplacée par edit_justificatif_etud #}
<div>
Actuellement noté{{etud.e}} en <b>{{objet_name|lower()}}</b> du {{objet.date_debut}} au {{objet.date_fin}}
</div>
<form action="" method="post" enctype="multipart/form-data">
<input type="hidden" name="obj_id" value="{{obj_id}}">
<input type="hidden" name="table_url" id="table_url" value="">
{% if type == "Assiduité" %}
<input type="hidden" name="obj_type" value="assiduite">
<legend for="etat">État</legend>
<select name="etat" id="etat">
<option value="absent">Absent</option>
<option value="retard">Retard</option>
<option value="present">Présent</option>
</select>
<legend for="moduleimpl_select">Module</legend>
{{moduleimpl | safe}}
<legend for="description">Description</legend>
<textarea name="description" id="description" cols="50" rows="5">{{objet.description}}</textarea>
{% else %}
<input type="hidden" name="obj_type" value="justificatif">
<legend for="date_debut">Date de début</legend>
<scodoc-datetime name="date_debut" id="date_debut" value="{{objet.real_date_debut}}"></scodoc-datetime>
<legend for="date_fin">Date de fin</legend>
<scodoc-datetime name="date_fin" id="date_fin" value="{{objet.real_date_fin}}"></scodoc-datetime>
<legend for="etat">État</legend>
<select name="etat" id="etat">
<option value="valide">Valide</option>
<option value="non_valide">Non Valide</option>
<option value="attente">En Attente</option>
<option value="modifie">Modifié</option>
</select>
{% if current_user.has_permission(sco.Permission.AbsJustifView) %}
<legend for="raison">Raison</legend>
<textarea name="raison" id="raison" cols="50" rows="5">{{objet.raison}}</textarea>
{% else %}
<div class="unauthorized">(raison non visible ni modifiable)</div>
{% endif %}
<legend>Fichiers</legend>
<div class="info-row">
<label class="info-label">Fichiers enregistrés: </label>
{% if objet.justification.fichiers.total != 0 %}
<div>Total : {{objet.justification.fichiers.total}} </div>
<ul>
{% for filename in objet.justification.fichiers.filenames %}
<li data-id="{{filename}}">
<a data-file="{{filename}}">❌</a>
<a data-link=""
href="{{url_for('apiweb.justif_export',justif_id=objet.justif_id,filename=filename, scodoc_dept=g.scodoc_dept)}}"><span
data-file="{{filename}}">{{filename}}</span></a>
</li>
{% endfor %}
</ul>
{% else %}
<span class="text">Aucun</span>
{% endif %}
</div>
<br>
<label for="justi_fich">Ajouter des fichiers:</label>
<input type="file" name="justi_fich" id="justi_fich" multiple>
{% endif %}
<br>
<br>
<input type="submit" value="Valider">
</form>
<script>
function removeFile(element) {
const link = document.querySelector(`*[data-id="${element.getAttribute('data-file')}"] a[data-link] span`);
link?.toggleAttribute("data-remove")
}
function deleteFiles(justif_id) {
const filenames = Array.from(document.querySelectorAll("*[data-remove]")).map((el) => el.getAttribute("data-file"))
obj = {
"remove": "list",
"filenames": filenames
}
//faire un POST à l'api justificatifs
}
window.addEventListener('load', () => {
document.getElementById('etat').value = "{{objet.real_etat}}";
document.getElementById('table_url').value = document.referrer;
document.querySelectorAll("a[data-file]").forEach((e) => {
e.addEventListener('click', () => {
removeFile(e);
})
})
})
</script>
<style>
[data-remove] {
text-decoration: line-through;
}
[data-file] {
cursor: pointer;
user-select: none;
}
</style>

View File

@ -27,21 +27,25 @@
let handleMoving = false; let handleMoving = false;
// Création des graduations de la timeline
// On créé des grandes graduations pour les heures
// On créé des petites graduations pour les "tick"
function createTicks() { function createTicks() {
let i = t_start; let i = t_start;
while (i <= t_end) { while (i <= t_end) {
// création d'un tick Heure (grand)
const hourTick = document.createElement("div"); const hourTick = document.createElement("div");
hourTick.classList.add("tick", "hour"); hourTick.classList.add("tick", "hour");
hourTick.style.left = `${((i - t_start) / (t_end - t_start)) * 100}%`; hourTick.style.left = `${((i - t_start) / (t_end - t_start)) * 100}%`;
timelineContainer.appendChild(hourTick); timelineContainer.appendChild(hourTick);
// on ajoute un label pour l'heure (ex : 12:00)
const tickLabel = document.createElement("div"); const tickLabel = document.createElement("div");
tickLabel.classList.add("tick-label"); tickLabel.classList.add("tick-label");
tickLabel.style.left = `${((i - t_start) / (t_end - t_start)) * 100}%`; tickLabel.style.left = `${((i - t_start) / (t_end - t_start)) * 100}%`;
tickLabel.textContent = numberToTime(i); tickLabel.textContent = numberToTime(i);
timelineContainer.appendChild(tickLabel); timelineContainer.appendChild(tickLabel);
// Si on est pas à la fin, on ajoute les graduations intermédiaires
if (i < t_end) { if (i < t_end) {
let j = Math.floor(i + 1); let j = Math.floor(i + 1);
@ -49,6 +53,7 @@
i += tick_delay; i += tick_delay;
if (i <= t_end) { if (i <= t_end) {
// création d'un tick (petit)
const quarterTick = document.createElement("div"); const quarterTick = document.createElement("div");
quarterTick.classList.add("tick", "quarter"); quarterTick.classList.add("tick", "quarter");
quarterTick.style.left = `${computePercentage(i, t_start)}%`; quarterTick.style.left = `${computePercentage(i, t_start)}%`;
@ -62,7 +67,8 @@
} }
} }
} }
// Convertit un nombre en heure
// ex : 12.5 => "12:30"
function numberToTime(num) { function numberToTime(num) {
const integer = Math.floor(num); const integer = Math.floor(num);
const decimal = Math.round((num % 1) * 60); const decimal = Math.round((num % 1) * 60);
@ -80,13 +86,12 @@
return int + dec; return int + dec;
} }
// Arrondi un nombre au tick le plus proche
function snapToQuarter(value) { function snapToQuarter(value) {
return Math.round(value * tick_time) / tick_time; return Math.round(value * tick_time) / tick_time;
} }
// Mise à jour des valeurs des timepickers
// En fonction des valeurs de la timeline
function updatePeriodTimeLabel() { function updatePeriodTimeLabel() {
const values = getPeriodValues(); const values = getPeriodValues();
const deb = numberToTime(values[0]) const deb = numberToTime(values[0])
@ -102,96 +107,112 @@
} }
// Gestion des évènements de la timeline
// - Déplacement des poignées
// - Déplacement de la période
function timelineMainEvent(event) { function timelineMainEvent(event) {
// Position de départ de l'événement (souris ou tactile)
const startX = (event.clientX || event.changedTouches[0].clientX); const startX = (event.clientX || event.changedTouches[0].clientX);
// Vérifie si l'événement concerne une poignée de période
if (event.target.classList.contains("period-handle")) { if (event.target.classList.contains("period-handle")) {
// Initialisation des valeurs de départ
const startWidth = parseFloat(periodTimeLine.style.width); const startWidth = parseFloat(periodTimeLine.style.width);
const startLeft = parseFloat(periodTimeLine.style.left); const startLeft = parseFloat(periodTimeLine.style.left);
const isLeftHandle = event.target.classList.contains("left"); const isLeftHandle = event.target.classList.contains("left");
handleMoving = true handleMoving = true;
// Fonction de déplacement de la poignée
const onMouseMove = (moveEvent) => { const onMouseMove = (moveEvent) => {
if (!handleMoving) return; if (!handleMoving) return;
// Calcul du déplacement en pixels
const deltaX = (moveEvent.clientX || moveEvent.changedTouches[0].clientX) - startX; const deltaX = (moveEvent.clientX || moveEvent.changedTouches[0].clientX) - startX;
const containerWidth = timelineContainer.clientWidth; const containerWidth = timelineContainer.clientWidth;
const newWidth = // Calcul de la nouvelle largeur en pourcentage
startWidth + ((isLeftHandle ? -deltaX : deltaX) / containerWidth) * 100; const newWidth = startWidth + ((isLeftHandle ? -deltaX : deltaX) / containerWidth) * 100;
if (isLeftHandle) { if (isLeftHandle) {
// Si la poignée gauche est déplacée, ajuste également la position gauche
const newLeft = startLeft + (deltaX / containerWidth) * 100; const newLeft = startLeft + (deltaX / containerWidth) * 100;
adjustPeriodPosition(newLeft, newWidth); adjustPeriodPosition(newLeft, newWidth);
} else { } else {
adjustPeriodPosition(parseFloat(periodTimeLine.style.left), newWidth); adjustPeriodPosition(parseFloat(periodTimeLine.style.left), newWidth);
} }
// Met à jour l'étiquette de temps de la période
updatePeriodTimeLabel(); updatePeriodTimeLabel();
}; };
// Fonction de relâchement de la souris ou du tactile
// - Alignement des poignées sur les ticks
// - Appel des callbacks
// - Sauvegarde des valeurs dans le local storage
// - Réinitialisation de la variable de déplacement des poignées
const mouseUp = () => { const mouseUp = () => {
snapHandlesToQuarters(); snapHandlesToQuarters();
timelineContainer.removeEventListener("mousemove", onMouseMove); timelineContainer.removeEventListener("mousemove", onMouseMove);
handleMoving = false; handleMoving = false;
func_call(); func_call();
savePeriodInLocalStorage(); savePeriodInLocalStorage();
};
} // Ajoute les écouteurs d'événement pour le déplacement et le relâchement
timelineContainer.addEventListener("mousemove", onMouseMove); timelineContainer.addEventListener("mousemove", onMouseMove);
timelineContainer.addEventListener("touchmove", onMouseMove); timelineContainer.addEventListener("touchmove", onMouseMove);
document.addEventListener( document.addEventListener("mouseup", mouseUp, { once: true });
"mouseup", document.addEventListener("touchend", mouseUp, { once: true });
mouseUp,
{ once: true }
);
document.addEventListener(
"touchend",
mouseUp,
{ once: true }
); // Vérifie si l'événement concerne la période elle-même
} else if (event.target === periodTimeLine) { } else if (event.target === periodTimeLine) {
const startLeft = parseFloat(periodTimeLine.style.left); const startLeft = parseFloat(periodTimeLine.style.left);
// Fonction de déplacement de la période
const onMouseMove = (moveEvent) => { const onMouseMove = (moveEvent) => {
if (handleMoving) return; if (handleMoving) return;
const deltaX = (moveEvent.clientX || moveEvent.changedTouches[0].clientX) - startX; const deltaX = (moveEvent.clientX || moveEvent.changedTouches[0].clientX) - startX;
const containerWidth = timelineContainer.clientWidth; const containerWidth = timelineContainer.clientWidth;
// Calcul de la nouvelle position gauche en pourcentage
const newLeft = startLeft + (deltaX / containerWidth) * 100; const newLeft = startLeft + (deltaX / containerWidth) * 100;
adjustPeriodPosition(newLeft, parseFloat(periodTimeLine.style.width)); adjustPeriodPosition(newLeft, parseFloat(periodTimeLine.style.width));
updatePeriodTimeLabel(); updatePeriodTimeLabel();
}; };
// Fonction de relâchement de la souris ou du tactile
// - Alignement des poignées sur les ticks
// - Appel des callbacks
// - Sauvegarde des valeurs dans le local storage
const mouseUp = () => { const mouseUp = () => {
snapHandlesToQuarters(); snapHandlesToQuarters();
timelineContainer.removeEventListener("mousemove", onMouseMove); timelineContainer.removeEventListener("mousemove", onMouseMove);
func_call(); func_call();
savePeriodInLocalStorage(); savePeriodInLocalStorage();
} };
// Ajoute les écouteurs d'événement pour le déplacement et le relâchement
timelineContainer.addEventListener("mousemove", onMouseMove); timelineContainer.addEventListener("mousemove", onMouseMove);
timelineContainer.addEventListener("touchmove", onMouseMove); timelineContainer.addEventListener("touchmove", onMouseMove);
document.addEventListener( document.addEventListener("mouseup", mouseUp, { once: true });
"mouseup", document.addEventListener("touchend", mouseUp, { once: true });
mouseUp,
{ once: true }
);
document.addEventListener(
"touchend",
mouseUp,
{ once: true }
);
} }
} }
let func_call = () => { }; let func_call = () => { };
// Fonction initialisant la timeline
// La fonction "callback" est appelée à chaque modification de la période
function setupTimeLine(callback) { function setupTimeLine(callback) {
func_call = callback; func_call = callback;
timelineContainer.addEventListener("mousedown", (e) => { timelineMainEvent(e) }); timelineContainer.addEventListener("mousedown", (e) => { timelineMainEvent(e) });
timelineContainer.addEventListener("touchstart", (e) => { timelineMainEvent(e) }); timelineContainer.addEventListener("touchstart", (e) => { timelineMainEvent(e) });
// Initialisation des timepickers (à gauche de la timeline)
// lors d'un changement, cela met à jour la timeline
const updateFromInputs = ()=>{ const updateFromInputs = ()=>{
let deb = $('#deb').val(); let deb = $('#deb').val();
let fin = $('#fin').val(); let fin = $('#fin').val();
@ -209,9 +230,11 @@
$('#deb').data('TimePicker').options.change = updateFromInputs; $('#deb').data('TimePicker').options.change = updateFromInputs;
$('#fin').data('TimePicker').options.change = updateFromInputs; $('#fin').data('TimePicker').options.change = updateFromInputs;
// actualise l'affichage des inputs avec les valeurs de la timeline
updatePeriodTimeLabel(); updatePeriodTimeLabel();
} }
// Ajuste la position de la période en fonction de la nouvelle position et largeur
// Vérifie que la période ne dépasse pas les limites de la timeline
function adjustPeriodPosition(newLeft, newWidth) { function adjustPeriodPosition(newLeft, newWidth) {
const snappedLeft = snapToQuarter(newLeft); const snappedLeft = snapToQuarter(newLeft);
@ -224,30 +247,36 @@
periodTimeLine.style.left = `${clampedLeft}%`; periodTimeLine.style.left = `${clampedLeft}%`;
periodTimeLine.style.width = `${snappedWidth}%`; periodTimeLine.style.width = `${snappedWidth}%`;
} }
// Récupère les valeurs de la période
function getPeriodValues() { function getPeriodValues() {
// On prend les pourcentages
const leftPercentage = parseFloat(periodTimeLine.style.left); const leftPercentage = parseFloat(periodTimeLine.style.left);
const widthPercentage = parseFloat(periodTimeLine.style.width); const widthPercentage = parseFloat(periodTimeLine.style.width);
// On calcule l'inverse des pourcentages pour obtenir les heures
const startHour = (leftPercentage / 100) * (t_end - t_start) + t_start; const startHour = (leftPercentage / 100) * (t_end - t_start) + t_start;
const endHour = ((leftPercentage + widthPercentage) / 100) * (t_end - t_start) + t_start; const endHour = ((leftPercentage + widthPercentage) / 100) * (t_end - t_start) + t_start;
// On les arrondit aux ticks les plus proches
const startValue = snapToQuarter(startHour); const startValue = snapToQuarter(startHour);
const endValue = snapToQuarter(endHour); const endValue = snapToQuarter(endHour);
// on verifie que les valeurs sont bien dans les bornes
const computedValues = [Math.max(startValue, t_start), Math.min(t_end, endValue)]; const computedValues = [Math.max(startValue, t_start), Math.min(t_end, endValue)];
// si les valeurs sont hors des bornes, on les ajuste
if (computedValues[0] > t_end || computedValues[1] < t_start) { if (computedValues[0] > t_end || computedValues[1] < t_start) {
return [t_start, Math.min(t_end, t_start + period_default)]; return [t_start, Math.min(t_end, t_start + period_default)];
} }
// Si la période est trop petite, on l'agrandit artificiellement (il faut au moins 1 tick de largeur)
if (computedValues[1] - computedValues[0] <= tick_delay && computedValues[1] < t_end - tick_delay) { if (computedValues[1] - computedValues[0] <= tick_delay && computedValues[1] < t_end - tick_delay) {
computedValues[1] += tick_delay; computedValues[1] += tick_delay;
} }
return computedValues; return computedValues;
} }
// Met à jour les valeurs de la période
// Met à jour l'affichage de la timeline
// Appelle les callbacks associés
function setPeriodValues(deb, fin) { function setPeriodValues(deb, fin) {
if (fin < deb) { if (fin < deb) {
throw new RangeError(`le paramètre 'deb' doit être inférieur au paramètre 'fin' ([${deb};${fin}])`) throw new RangeError(`le paramètre 'deb' doit être inférieur au paramètre 'fin' ([${deb};${fin}])`)
@ -259,8 +288,8 @@
deb = snapToQuarter(deb); deb = snapToQuarter(deb);
fin = snapToQuarter(fin); fin = snapToQuarter(fin);
let leftPercentage = (deb - t_start) / (t_end - t_start) * 100; let leftPercentage = computePercentage(deb, t_start);
let widthPercentage = (fin - deb) / (t_end - t_start) * 100; let widthPercentage = computePercentage(fin, deb);
periodTimeLine.style.left = `${leftPercentage}%`; periodTimeLine.style.left = `${leftPercentage}%`;
periodTimeLine.style.width = `${widthPercentage}%`; periodTimeLine.style.width = `${widthPercentage}%`;
@ -269,7 +298,9 @@
func_call(); func_call();
savePeriodInLocalStorage(); savePeriodInLocalStorage();
} }
// Aligne les poignées de la période sur les ticks les plus proches
// ex : 12h39 => 12h45 (si les ticks sont à 15min)
// evite aussi les dépassements de la timeline (max et min)
function snapHandlesToQuarters() { function snapHandlesToQuarters() {
const periodValues = getPeriodValues(); const periodValues = getPeriodValues();
let lef = Math.min(computePercentage(Math.abs(periodValues[0]), t_start), computePercentage(Math.abs(t_end), tick_delay)); let lef = Math.min(computePercentage(Math.abs(periodValues[0]), t_start), computePercentage(Math.abs(t_end), tick_delay));
@ -288,15 +319,20 @@
updatePeriodTimeLabel() updatePeriodTimeLabel()
} }
// Retourne le pourcentage d'une valeur par rapport à t_start et t_end
// ex : 12h par rapport à 8h et 20h => 25%
function computePercentage(a, b) { function computePercentage(a, b) {
return ((a - b) / (t_end - t_start)) * 100; return ((a - b) / (t_end - t_start)) * 100;
} }
// Convertit une heure (string) en nombre
// ex : "12:30" => 12.5
function fromTime(time, separator = ":") { function fromTime(time, separator = ":") {
const [hours, minutes] = time.split(separator).map((el) => Number(el)) const [hours, minutes] = time.split(separator).map((el) => Number(el))
return hours + minutes / 60 return hours + minutes / 60
} }
// Renvoie les valeurs de la période sous forme de date
// Les heures sont récupérées depuis la timeline
// la date est récupérée depuis un champ "#date" (datepicker)
function getPeriodAsDate(){ function getPeriodAsDate(){
let [deb, fin] = getPeriodValues(); let [deb, fin] = getPeriodValues();
deb = numberToTime(deb); deb = numberToTime(deb);
@ -305,19 +341,21 @@
const dateStr = $("#date") const dateStr = $("#date")
.datepicker("getDate") .datepicker("getDate")
.format("yyyy-mm-dd") .format("yyyy-mm-dd")
.substring(0, 10); .substring(0, 10); // récupération que de la date, pas des heures
return { return {
deb: new Date(`${dateStr}T${deb}`), deb: new Date(`${dateStr}T${deb}`),
fin: new Date(`${dateStr}T${fin}`) fin: new Date(`${dateStr}T${fin}`)
} }
} }
// Sauvegarde les valeurs de la période dans le local storage
function savePeriodInLocalStorage(){ function savePeriodInLocalStorage(){
const dates = getPeriodValues(); const dates = getPeriodValues();
localStorage.setItem("sco-timeline-values", JSON.stringify(dates)); localStorage.setItem("sco-timeline-values", JSON.stringify(dates));
} }
// Récupère les valeurs de la période depuis le local storage
// Si elles n'existent pas, on les initialise avec les valeurs par défaut
function loadPeriodFromLocalStorage(){ function loadPeriodFromLocalStorage(){
const dates = JSON.parse(localStorage.getItem("sco-timeline-values")); const dates = JSON.parse(localStorage.getItem("sco-timeline-values"));
if(dates){ if(dates){
@ -326,11 +364,13 @@
setPeriodValues(t_start, t_start + period_default); setPeriodValues(t_start, t_start + period_default);
} }
} }
// == Initialisation par défaut de la timeline ==
createTicks(); createTicks(); // création des graduations
loadPeriodFromLocalStorage(); loadPeriodFromLocalStorage(); // chargement des valeurs si disponible
// Si on donne les heures en appelant le template alors on met à jour la timeline
{% if heures %} {% if heures %}
let [heure_deb, heure_fin] = [{{ heures | safe }}] let [heure_deb, heure_fin] = [{{ heures | safe }}]
if (heure_deb != '' && heure_fin != '') { if (heure_deb != '' && heure_fin != '') {

View File

@ -68,32 +68,49 @@
</style> </style>
<script> <script>
/**
* Génère une notification (toast) avec les paramètres spécifiés.
* @param {HTMLElement} content - Le contenu de la notification.
* @param {string} [color="var(--color-present)"] - La couleur de fond de la notification (optionnelle).
* @param {number} [ttl=5] - Le temps de vie de la notification en secondes (optionnelle).
* @returns {HTMLElement} - L'élément toast créé.
*/
function generateToast(content, color = "var(--color-present)", ttl = 5) { function generateToast(content, color = "var(--color-present)", ttl = 5) {
const toast = document.createElement('div') // Crée l'élément de notification et ajoute les classes de style
toast.classList.add('toast', 'fadeIn') const toast = document.createElement('div');
toast.classList.add('toast', 'fadeIn');
const toastContent = document.createElement('div') // Crée le conteneur de contenu de la notification et y ajoute le contenu
toastContent.classList.add('toast-content') const toastContent = document.createElement('div');
toastContent.appendChild(content) toastContent.classList.add('toast-content');
toastContent.appendChild(content);
// Définit la couleur de fond de la notification
toast.style.backgroundColor = color; toast.style.backgroundColor = color;
setTimeout(() => { toast.classList.replace('fadeIn', 'fadeOut') }, Math.max(0, ttl * 1000 - 500)) // Définit les temporisations pour les animations de disparition et la suppression de la notification
setTimeout(() => { toast.remove() }, Math.max(0, ttl * 1000)) setTimeout(() => { toast.classList.replace('fadeIn', 'fadeOut') }, Math.max(0, ttl * 1000 - 500));
toast.appendChild(toastContent) setTimeout(() => { toast.remove() }, Math.max(0, ttl * 1000));
return toast
// Ajoute le contenu à la notification
toast.appendChild(toastContent);
return toast;
} }
/**
* Ajoute une notification (toast) à l'élément conteneur des toasts.
* @param {HTMLElement} toast - L'élément toast à ajouter.
*/
function pushToast(toast) { function pushToast(toast) {
document document.querySelector(".toast-holder").appendChild(toast);
.querySelector(".toast-holder")
.appendChild(
toast
);
} }
/**
* Obtient la couleur de fond de la notification en fonction de l'état spécifié.
* @param {string} etat - L'état de la notification (PRESENT, ABSENT, RETARD).
* @returns {string} - La couleur correspondant à l'état.
*/
function getToastColorFromEtat(etat) { function getToastColorFromEtat(etat) {
let color; let color;
switch (etat.toUpperCase()) { switch (etat.toUpperCase()) {
@ -107,12 +124,11 @@
color = "var(--color-retard)"; color = "var(--color-retard)";
break; break;
default: default:
color = "#AAA"; color = "#AAA"; // Couleur par défaut si l'état est inconnu
break; break;
} }
return color; return color;
} }
</script> </script>

View File

@ -1537,116 +1537,10 @@ def tableau_assiduite_actions():
flash(f"{objet_name} justifiée") flash(f"{objet_name} justifiée")
return redirect(request.referrer) return redirect(request.referrer)
if request.method == "GET": # Si on arrive ici, c'est que l'action n'est pas autorisée
module: str | int = "" # moduleimpl_id ou chaine libre # cette fonction ne sert plus qu'à supprimer ou justifier
flash("Méthode non autorisée", "error")
if obj_type == "assiduite": return redirect(request.referrer)
# Construction du menu module
module = _module_selector_multiple(objet.etudiant, objet.moduleimpl_id)
return render_template(
"assiduites/pages/tableau_assiduite_actions.j2",
action=action,
can_view_justif_detail=current_user.has_permission(Permission.AbsJustifView)
or (obj_type == "justificatif" and current_user.id == objet.user_id),
etud=objet.etudiant,
moduleimpl=module,
obj_id=obj_id,
objet_name=objet_name,
objet=_preparer_objet(obj_type, objet),
sco=ScoData(etud=objet.etudiant),
title=f"Assiduité {objet.etudiant.nom_short}",
# type utilisé dans les actions modifier / détails (modifier.j2, details.j2)
type="Justificatif" if obj_type == "justificatif" else "Assiduité",
)
# ----- Cas POST
if obj_type == "assiduite":
try:
_action_modifier_assiduite(objet)
except ScoValueError as error:
raise ScoValueError(error.args[0], request.referrer) from error
flash("L'assiduité a bien été modifiée.")
else:
try:
_action_modifier_justificatif(objet)
except ScoValueError as error:
raise ScoValueError(error.args[0], request.referrer) from error
flash("Le justificatif a bien été modifié.")
return redirect(request.form["table_url"])
def _action_modifier_assiduite(assi: Assiduite):
form = request.form
# Gestion de l'état
etat = scu.EtatAssiduite.get(form["etat"])
if etat is not None:
assi.etat = etat
if etat == scu.EtatAssiduite.PRESENT:
assi.est_just = False
else:
assi.est_just = len(get_assiduites_justif(assi.assiduite_id, False)) > 0
# Gestion de la description
assi.description = form["description"]
possible_moduleimpl_id: str = form["moduleimpl_select"]
# Raise ScoValueError (si None et force module | Etudiant non inscrit | Module non reconnu)
assi.set_moduleimpl(possible_moduleimpl_id)
db.session.add(assi)
db.session.commit()
scass.simple_invalidate_cache(assi.to_dict(True), assi.etudid)
def _action_modifier_justificatif(justi: Justificatif):
"Modifie le justificatif avec les valeurs dans le form"
form = request.form
# Gestion des Dates
date_debut: datetime = scu.is_iso_formated(form["date_debut"], True)
date_fin: datetime = scu.is_iso_formated(form["date_fin"], True)
if date_debut is None or date_fin is None or date_fin < date_debut:
raise ScoValueError("Dates invalides", request.referrer)
justi.date_debut = date_debut
justi.date_fin = date_fin
# Gestion de l'état
etat = scu.EtatJustificatif.get(form["etat"])
if etat is not None:
justi.etat = etat
else:
raise ScoValueError("État invalide", request.referrer)
# Gestion de la raison
justi.raison = form["raison"]
# Gestion des fichiers
files = request.files.getlist("justi_fich")
if len(files) != 0:
files = request.files.values()
archive_name: str = justi.fichier
# Utilisation de l'archiver de justificatifs
archiver: JustificatifArchiver = JustificatifArchiver()
for fich in files:
archive_name, _ = archiver.save_justificatif(
justi.etudiant,
filename=fich.filename,
data=fich.stream.read(),
archive_name=archive_name,
user_id=current_user.id,
)
justi.fichier = archive_name
justi.dejustifier_assiduites()
db.session.add(justi)
db.session.commit()
justi.justifier_assiduites()
scass.simple_invalidate_cache(justi.to_dict(True), justi.etudid)
def _preparer_objet( def _preparer_objet(