forked from ScoDoc/ScoDoc
Merge pull request 'Résolutions des derniers tickets du module Assiduité' (#932) from iziram/ScoDoc:assi_tickets into master
Reviewed-on: ScoDoc/ScoDoc#932
This commit is contained in:
commit
d6eeb5116d
@ -31,6 +31,7 @@
|
|||||||
|
|
||||||
Il suffit d'appeler abs_notify() après chaque ajout d'absence.
|
Il suffit d'appeler abs_notify() après chaque ajout d'absence.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@ -281,6 +282,9 @@ def abs_notification_message(
|
|||||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid, _external=True
|
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid, _external=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Formsemestre concerné (ex: "BUT Informatique semestre 2")
|
||||||
|
values["semestre"] = formsemestre.titre_num()
|
||||||
|
|
||||||
template = prefs["abs_notification_mail_tmpl"]
|
template = prefs["abs_notification_mail_tmpl"]
|
||||||
txt = ""
|
txt = ""
|
||||||
if template:
|
if template:
|
||||||
|
@ -371,20 +371,6 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||||||
>Saisie Absences (Hebdo)</a></span>
|
>Saisie Absences (Hebdo)</a></span>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
H.append(
|
|
||||||
f"""
|
|
||||||
<span class="moduleimpl_abs_link"><a class="stdlink" href="{
|
|
||||||
url_for(
|
|
||||||
"assiduites.signal_assiduites_diff",
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
|
||||||
group_ids=group_id,
|
|
||||||
formsemestre_id=formsemestre.id,
|
|
||||||
moduleimpl_id="" if moduleimpl_id is None else moduleimpl_id
|
|
||||||
)}" title="Page en cours de fusion et sera prochainement supprimée. Veuillez utiliser la page `Saisie Absences`"
|
|
||||||
>(Saisie Absences Différée)</a></span>
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
H.append("</td></tr></table>")
|
H.append("</td></tr></table>")
|
||||||
#
|
#
|
||||||
if not modimpl.check_apc_conformity(nt):
|
if not modimpl.check_apc_conformity(nt):
|
||||||
|
@ -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");
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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,31 +202,37 @@ 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 + "",
|
||||||
opt.textContent = a
|
opt.textContent = a
|
||||||
if (a_1 === defAnnee) {
|
if (a_1 === defAnnee) {
|
||||||
opt.selected = true;
|
opt.selected = true;
|
||||||
document.querySelector('.annee #label-annee').textContent = `Année scolaire ${a}`
|
document.querySelector('.annee #label-annee').textContent = `Année scolaire ${a}`
|
||||||
@ -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');
|
||||||
|
@ -1,702 +0,0 @@
|
|||||||
{% extends "sco_page.j2" %}
|
|
||||||
|
|
||||||
{% block styles %}
|
|
||||||
{{ super() }}
|
|
||||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
|
|
||||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/minitimeline.css">
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.ui-timepicker-container,#ui-datepicker-div{
|
|
||||||
z-index: 5 !important;
|
|
||||||
}
|
|
||||||
#new_periode,
|
|
||||||
#actions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: fit-content;
|
|
||||||
gap: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#actions {
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
#actions label{
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#fix {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 1em;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
#fix>.box {
|
|
||||||
border: 1px solid #444;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
padding: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timepicker {
|
|
||||||
width: 5em;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#moduleimpl_select {
|
|
||||||
width: 10em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#tableau-periode {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow-x: scroll;
|
|
||||||
max-width: var(--sco-content-max-width);
|
|
||||||
}
|
|
||||||
|
|
||||||
#tableau-periode .pdp {
|
|
||||||
width: 5em;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
padding: 10px;
|
|
||||||
text-align: center;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cell, .header {
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
padding: 10px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
.header{
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cell {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
padding: 10px;
|
|
||||||
text-align: center;
|
|
||||||
width: 256px;
|
|
||||||
}
|
|
||||||
.cell p{
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.sticky {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cell .assiduite-bubble {
|
|
||||||
display: block;
|
|
||||||
top: 0;
|
|
||||||
z-index: 0;
|
|
||||||
width: 100% !important;
|
|
||||||
min-width: inherit !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.assi-btns {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pointer{
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ligne{
|
|
||||||
display: flex;
|
|
||||||
gap: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
||||||
{% endblock styles %}
|
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
{{ super() }}
|
|
||||||
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
|
|
||||||
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
|
|
||||||
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
|
|
||||||
{% include "sco_timepicker.j2" %}
|
|
||||||
|
|
||||||
<script>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Permet d'ajouter une nouvelle période au tableau
|
|
||||||
* Par défaut la période est générèe avec les valeurs des inputs
|
|
||||||
* Si une période est passée en paramètre, alors on utilise ses valeurs
|
|
||||||
* @param {Object} period - La période à ajouter
|
|
||||||
*/
|
|
||||||
async function nouvellePeriode(period = null) {
|
|
||||||
// On récupère l'id de la période
|
|
||||||
let periodId;
|
|
||||||
if (period) {
|
|
||||||
periodId = period.periodId;
|
|
||||||
} else {
|
|
||||||
periodId = currentPeriodId++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// On récupère les valeurs des inputs
|
|
||||||
let date = document.getElementById("date").value;
|
|
||||||
let debut = document.getElementById("debut").value;
|
|
||||||
let fin = document.getElementById("fin").value;
|
|
||||||
let moduleimpl_id = document.getElementById("moduleimpl_select").value;
|
|
||||||
const moduleimpl = await getModuleImpl({ moduleimpl_id: moduleimpl_id });
|
|
||||||
|
|
||||||
// Si une période est passée en paramètre, on utilise ses valeurs
|
|
||||||
if (period) {
|
|
||||||
date = period.date_debut.format("DD/MM/YYYY");
|
|
||||||
debut = period.date_debut.format("HH:mm");
|
|
||||||
fin = period.date_fin.format("HH:mm");
|
|
||||||
moduleimpl_id = period.moduleimpl_id;
|
|
||||||
}else{
|
|
||||||
//Sinon on vérifie qu'on a bien des valeurs
|
|
||||||
const text = document.createTextNode("Veuillez remplir tous les champs pour ajouter une plage.")
|
|
||||||
if (date == "" || debut == "" || fin == "" || moduleimpl_id == "") {
|
|
||||||
openAlertModal(
|
|
||||||
"Erreur",
|
|
||||||
text
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérification de la plage horaire
|
|
||||||
// On génère une date de début et de fin de la période
|
|
||||||
const date_debut = new Date(
|
|
||||||
$("#date").datepicker("getDate").format("YYYY-MM-DD") + "T" + debut
|
|
||||||
);
|
|
||||||
const date_fin = new Date(
|
|
||||||
$("#date").datepicker("getDate").format("YYYY-MM-DD") + "T" + fin
|
|
||||||
);
|
|
||||||
date_debut.add(1, "seconds");
|
|
||||||
|
|
||||||
// On vérifie que les dates sont valides
|
|
||||||
if (!date_debut.isValid()){
|
|
||||||
const p = document.createElement("p");
|
|
||||||
p.textContent = "La date de début n'est pas valide.";
|
|
||||||
openAlertModal(
|
|
||||||
"Erreur",
|
|
||||||
p,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!date_fin.isValid()){
|
|
||||||
const p = document.createElement("p");
|
|
||||||
p.textContent = "La date de fin n'est pas valide.";
|
|
||||||
openAlertModal(
|
|
||||||
"Erreur",
|
|
||||||
p,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// On vérifie que l'heure de fin est supérieure à l'heure de début
|
|
||||||
if (date_debut >= date_fin) {
|
|
||||||
const p = document.createElement("p");
|
|
||||||
p.textContent = "La plage horaire n'est pas valide. L'heure de fin doit être "+
|
|
||||||
"supérieure à l'heure de début.";
|
|
||||||
openAlertModal(
|
|
||||||
"Erreur",
|
|
||||||
p,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// On ajoute la nouvelle période au tableau
|
|
||||||
let periodeDiv = document.createElement("div");
|
|
||||||
periodeDiv.classList.add("cell", "header");
|
|
||||||
periodeDiv.id = `periode-${periodId}`;
|
|
||||||
|
|
||||||
const periodP = document.createElement("p");
|
|
||||||
periodP.textContent = `Plage du ${date} de ${debut} à ${fin}`;
|
|
||||||
|
|
||||||
// On ajoute le moduleimpl
|
|
||||||
const modP = document.createElement("p");
|
|
||||||
modP.textContent = moduleimpl;
|
|
||||||
|
|
||||||
// On ajoute le bouton pour supprimer la période
|
|
||||||
const close = document.createElement("button");
|
|
||||||
close.textContent = "❌";
|
|
||||||
close.addEventListener("click", () => {
|
|
||||||
// On supprime toutes les cases du tableau correspondant à cette période
|
|
||||||
document
|
|
||||||
.querySelectorAll(
|
|
||||||
`[data-periodeid="${periodeDiv.getAttribute("data-periodeid")}"]`
|
|
||||||
)
|
|
||||||
.forEach((e) => e.remove());
|
|
||||||
// On supprime la période de la Map periodes
|
|
||||||
periodes.delete(Number(periodeDiv.getAttribute("data-periodeid")));
|
|
||||||
});
|
|
||||||
//On ajoute les éléments au DOM
|
|
||||||
periodeDiv.appendChild(periodP);
|
|
||||||
periodeDiv.appendChild(modP);
|
|
||||||
periodeDiv.appendChild(close);
|
|
||||||
periodeDiv.setAttribute("data-periodeid", periodId);
|
|
||||||
document.getElementById("tete-table").appendChild(periodeDiv);
|
|
||||||
|
|
||||||
// On récupère les étudiants (etudids)
|
|
||||||
let etudids = [
|
|
||||||
...document.querySelectorAll(".ligne[data-etudid]"),
|
|
||||||
].map((e) => e.getAttribute("data-etudid"));
|
|
||||||
|
|
||||||
// Préparation de la requête
|
|
||||||
const url =
|
|
||||||
`../../api/assiduites/group/query?date_debut=${date_debut.toFakeIso()}` +
|
|
||||||
`&date_fin=${date_fin.toFakeIso()}&etudids=${etudids.join(
|
|
||||||
","
|
|
||||||
)}&with_justifs`;
|
|
||||||
|
|
||||||
//Si la période n'existait pas, alors on l'ajoute à la Map
|
|
||||||
if (!period) {
|
|
||||||
periodes.set(periodId, {
|
|
||||||
date_debut: date_debut.clone().add(-1, "seconds"),
|
|
||||||
date_fin: date_fin,
|
|
||||||
moduleimpl_id: moduleimpl_id,
|
|
||||||
periodId: periodId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// On récupère les incriptions au module
|
|
||||||
const inscriptions = await getInscriptionModule(moduleimpl_id);
|
|
||||||
|
|
||||||
// On récupère les assiduités
|
|
||||||
await fetch(url)
|
|
||||||
// On convertit la réponse en JSON
|
|
||||||
.then((res) => {
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error("Network response was not ok");
|
|
||||||
}
|
|
||||||
return res.json();
|
|
||||||
})
|
|
||||||
// On traite les données
|
|
||||||
.then((data) => {
|
|
||||||
for (let etudid of etudids) {
|
|
||||||
// On crée une case pour chaque étudiant
|
|
||||||
let cell = document.createElement("div");
|
|
||||||
cell.classList.add("cell");
|
|
||||||
cell.setAttribute("data-etudid", etudid);
|
|
||||||
cell.setAttribute("data-periodeid", periodId);
|
|
||||||
cell.id = `cell-${etudid}-${periodId}`;
|
|
||||||
document.querySelector(`.ligne[data-etudid="${etudid}"]`).appendChild(cell);
|
|
||||||
|
|
||||||
//Vérification inscription au module
|
|
||||||
// Si l'étudiant n'est pas inscrit, on le notifie et on passe à l'étudiant suivant
|
|
||||||
const inscrit =
|
|
||||||
inscriptions == null ? true : inscriptions.find((e) => e == etudid);
|
|
||||||
if (!inscrit) {
|
|
||||||
cell.textContent = "Non inscrit";
|
|
||||||
cell.classList.add("non-inscrit");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Gestion des assiduités déjà existantes
|
|
||||||
const assiduites = data[etudid];
|
|
||||||
// Si l'étudiant n'a pas d'assiduité, on crée les boutons assiduité
|
|
||||||
if (assiduites.length == 0) {
|
|
||||||
|
|
||||||
const assi_btns = document.createElement('div');
|
|
||||||
assi_btns.classList.add('assi-btns');
|
|
||||||
const etats = ["retard", "absent"];
|
|
||||||
|
|
||||||
if(!window.nonPresent){
|
|
||||||
etats.splice(0,0,"present");
|
|
||||||
}
|
|
||||||
|
|
||||||
etats.forEach((value) => {
|
|
||||||
const cbox = document.createElement("input");
|
|
||||||
cbox.type = "checkbox";
|
|
||||||
cbox.value = value;
|
|
||||||
cbox.name = `rbtn_${etudid}_${periodId}`;
|
|
||||||
cbox.classList.add("rbtn", value);
|
|
||||||
|
|
||||||
// Event pour être sur qu'un seul bouton est coché à la fois
|
|
||||||
cbox.addEventListener("click", (event) => {
|
|
||||||
const parent = event.target.parentElement;
|
|
||||||
parent.querySelectorAll(".rbtn").forEach((ele) => {
|
|
||||||
if (ele.value != value) {
|
|
||||||
ele.checked = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// Si une valeur par défaut est donnée alors on l'applique
|
|
||||||
cbox.checked = etatDef.value == value;
|
|
||||||
|
|
||||||
assi_btns.appendChild(cbox);
|
|
||||||
});
|
|
||||||
cell.appendChild(assi_btns);
|
|
||||||
} else {
|
|
||||||
// Si une (ou plus) assiduité sont trouvée pour la période
|
|
||||||
// alors on affiche les informations de la première assiduité
|
|
||||||
setupAssiduiteBubble(cell, assiduites[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
//Si jamais la requête échoue, on affiche un message d'erreur dans la console
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Error:", error);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("tableau-periode").classList.remove("hidden");
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Permet de récupérer la saisie puis créer les assiduités grâce à l'api
|
|
||||||
*/
|
|
||||||
function sauvegarderAssiduites() {
|
|
||||||
// Initialisation de la liste des assiduités à créer
|
|
||||||
let assiduitesData = [];
|
|
||||||
// Pour chaque période, on récupère les assiduités saisies
|
|
||||||
for (let [periodeId, periode] of periodes.entries()) {
|
|
||||||
// On prend chaque cellule correspondant à la période
|
|
||||||
const cells = document.querySelectorAll(
|
|
||||||
`.cell[data-periodeid="${periodeId}"][data-etudid]`
|
|
||||||
);
|
|
||||||
// Pour chaque cellule, on récupère l'état de l'assiduité
|
|
||||||
cells.forEach((cell) => {
|
|
||||||
const etudid = cell.getAttribute("data-etudid");
|
|
||||||
const etat = cell.querySelector(".rbtn:checked")?.value;
|
|
||||||
// Il est possible que l'état soit null
|
|
||||||
// - Cas où l'étudiant n'est pas inscrit
|
|
||||||
// - Cas où l'étudiant avait déjà une assiduité
|
|
||||||
if (etat) {
|
|
||||||
// On génère un objet "assiduité"
|
|
||||||
/*
|
|
||||||
{
|
|
||||||
etudid: <int>,
|
|
||||||
etat: <string>,
|
|
||||||
date_debut: <string>,
|
|
||||||
date_fin: <string>,
|
|
||||||
moduleimpl_id: <int>,
|
|
||||||
periodId: <int>
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
assiduitesData.push({
|
|
||||||
etudid: etudid,
|
|
||||||
etat: etat,
|
|
||||||
...periode,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Une fois les assiduités générées, on les envoie à l'api
|
|
||||||
async_post(
|
|
||||||
"../../api/assiduites/create",
|
|
||||||
assiduitesData,
|
|
||||||
// Si la requête passe
|
|
||||||
async (data) => {
|
|
||||||
// On supprime toutes les cases du tableau pour le mettre à jour
|
|
||||||
document.querySelectorAll("[data-periodeid]").forEach((e)=>e.remove())
|
|
||||||
|
|
||||||
// On recrée les périodes
|
|
||||||
// (cela permet de redemander les assiduités, donc mettre à jour les cases)
|
|
||||||
for (let periode of periodes.values()) {
|
|
||||||
await nouvellePeriode(periode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si il n'y a pas d'erreur, on affiche un message de succès
|
|
||||||
if (data.errors.length == 0) {
|
|
||||||
const span = document.createElement("span");
|
|
||||||
span.textContent = "Le relevé d'assiduité a été enregistré.";
|
|
||||||
openAlertModal(
|
|
||||||
"Enregistrement de l'assiduité",
|
|
||||||
span,
|
|
||||||
null,
|
|
||||||
"var(--color-present)"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Si il y a des erreurs, on les affiche
|
|
||||||
if (data.errors.length > 0) {
|
|
||||||
// On crée une map pour regrouper les erreurs par période
|
|
||||||
const erreurs = new Map();
|
|
||||||
data.errors.forEach((err) => {
|
|
||||||
// Pour chaque période on créer une liste d'erreurs
|
|
||||||
// format : [message, etudid]
|
|
||||||
const assi = assiduitesData[err.indice];
|
|
||||||
const msg = err.message;
|
|
||||||
const periodErrors = erreurs.get(assi.periodId) || [];
|
|
||||||
|
|
||||||
// Récupération du nom de l'étudiant
|
|
||||||
const etud = document.querySelector(
|
|
||||||
`#head-${assi.etudid} span`
|
|
||||||
).textContent;
|
|
||||||
periodErrors.push([`Erreur pour ${etud} : ${msg}`, assi.etudid]);
|
|
||||||
erreurs.set(assi.periodId, periodErrors);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Création du DOM
|
|
||||||
/*
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
Période du ... de ... à ...
|
|
||||||
<ul>
|
|
||||||
<li>Erreur pour ...</li>
|
|
||||||
<li>Erreur pour ...</li>
|
|
||||||
</ul>
|
|
||||||
/li>
|
|
||||||
</ul>
|
|
||||||
*/
|
|
||||||
|
|
||||||
const ul = document.createElement("ul");
|
|
||||||
//Pour chaque période on créer un titre "periode du ... de ... à ..."
|
|
||||||
for (let [periodeId, periodErrors] of erreurs.entries()) {
|
|
||||||
const period = periodes.get(periodeId);
|
|
||||||
const li = document.createElement("li");
|
|
||||||
// On affiche la période
|
|
||||||
li.textContent = `Plage du ${period.date_debut.format(
|
|
||||||
"DD/MM/YYYY HH:mm"
|
|
||||||
)} à ${period.date_fin.format("HH:mm")}`;
|
|
||||||
|
|
||||||
// Nous emmène à la période lorsqu'on clique dessus
|
|
||||||
li.addEventListener("click", () => {
|
|
||||||
location.href = `#periode-${periodeId}`;
|
|
||||||
});
|
|
||||||
li.classList.add("pointer");
|
|
||||||
|
|
||||||
// Pour chaque erreur, on créer un élément de liste
|
|
||||||
const ul2 = document.createElement("ul");
|
|
||||||
periodErrors.forEach((err) => {
|
|
||||||
const li2 = document.createElement("li");
|
|
||||||
li2.textContent = err[0];
|
|
||||||
li2.classList.add("pointer");
|
|
||||||
|
|
||||||
// Nous emmène à la case de l'étudiant lorsqu'on clique dessus
|
|
||||||
li2.addEventListener("click", () => {
|
|
||||||
location.href = `#cell-${err[1]}-${periodeId}`;
|
|
||||||
});
|
|
||||||
ul2.appendChild(li2);
|
|
||||||
});
|
|
||||||
li.appendChild(ul2);
|
|
||||||
ul.appendChild(li);
|
|
||||||
}
|
|
||||||
|
|
||||||
openAlertModal(
|
|
||||||
"Erreurs lors de la sauvegarde des assiduités",
|
|
||||||
ul,
|
|
||||||
"Les autres assiduités ont bien été sauvegardées."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(e) => {
|
|
||||||
console.error("Erreur lors de la création des assiduités", e);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mis en place des variables globales
|
|
||||||
let currentPeriodId = 0;
|
|
||||||
const periodes = new Map();
|
|
||||||
const moduleimpls = new Map();
|
|
||||||
const inscriptionsModules = new Map();
|
|
||||||
const nonWorkDays = [{{ nonworkdays| safe }}];
|
|
||||||
|
|
||||||
window.nonPresent = {{ 'true' if non_present else 'false' }};
|
|
||||||
|
|
||||||
// Vérification du forçage de module
|
|
||||||
window.forceModule = "{{ forcer_module }}" == "True";
|
|
||||||
if (window.forceModule) {
|
|
||||||
if (moduleimpl_select.value == "") {
|
|
||||||
document.getElementById("forcemodule").style.display = "block";
|
|
||||||
add_periode.disabled = true;
|
|
||||||
}
|
|
||||||
// Désactivation du bouton d'ajout de période si aucun module n'est sélectionné
|
|
||||||
// et affichage du message de forçage de module
|
|
||||||
moduleimpl_select?.addEventListener("change", (e) => {
|
|
||||||
if (e.target.value != "") {
|
|
||||||
document.getElementById("forcemodule").style.display = "none";
|
|
||||||
add_periode.disabled = false;
|
|
||||||
} else {
|
|
||||||
document.getElementById("forcemodule").style.display = "block";
|
|
||||||
add_periode.disabled = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultPlage = {{ nouv_plage | safe}} || [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fonction exécutée au lancement de la page
|
|
||||||
* - On affiche ou non les photos des étudiants
|
|
||||||
* - On vérifie si la date est un jour travaillé
|
|
||||||
*/
|
|
||||||
async function main() {
|
|
||||||
|
|
||||||
// On initialise les sélecteurs avec les valeurs par défaut (si elles existent)
|
|
||||||
if (defaultPlage.every((e) => e)) {
|
|
||||||
$("#date").datepicker("setDate", defaultPlage[0]);
|
|
||||||
$("#debut").val(defaultPlage[1]);
|
|
||||||
$("#fin").val(defaultPlage[2]);
|
|
||||||
|
|
||||||
// On ajoute la période si la date est un jour travaillé
|
|
||||||
if(dateCouranteEstTravaillee()){
|
|
||||||
await nouvellePeriode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const checked = localStorage.getItem("scodoc-etud-pdp") == "true";
|
|
||||||
afficherPDP(checked);
|
|
||||||
$("#date").on("change", async function (d) {
|
|
||||||
// On vérifie si la date est un jour travaillé
|
|
||||||
dateCouranteEstTravaillee();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("load", main);
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{% endblock scripts %}
|
|
||||||
|
|
||||||
{% block title %}
|
|
||||||
{{title}}
|
|
||||||
{% endblock title %}
|
|
||||||
|
|
||||||
{% block app_content %}
|
|
||||||
|
|
||||||
<h2>Signalement différé de l'assiduité {{gr |safe}}</h2>
|
|
||||||
|
|
||||||
<div class="ue_warning warning">
|
|
||||||
Attention, cette page va prochainement être supprimée, car il est plus facile d'utiliser
|
|
||||||
<ul>
|
|
||||||
la page
|
|
||||||
<li><a class="stdlink" href="{{
|
|
||||||
url_for('assiduites.signal_assiduites_group',
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
|
||||||
formsemestre_id=formsemestre_id,
|
|
||||||
group_ids=group_ids)
|
|
||||||
}}">
|
|
||||||
saisie de l'assiduité</a> pour saisir à une seule date quelconque
|
|
||||||
</li>
|
|
||||||
<li>ou <a class="stdlink" href="{{
|
|
||||||
url_for('assiduites.signal_assiduites_hebdo',
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
|
||||||
formsemestre_id=formsemestre_id,
|
|
||||||
group_ids=group_ids,
|
|
||||||
)
|
|
||||||
}}">saisie hebdomadaire</a> pour saisir sur une semaine.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p>Ci-dessous le formulaire vous permettant de saisir plusieurs plages à la fois,
|
|
||||||
qui va bientôt être retiré.
|
|
||||||
</p>
|
|
||||||
<p>N'hésitez pas à commenter sur le <a href="{{scu.SCO_DISCORD_ASSISTANCE}}">salon Discord</a>
|
|
||||||
si vous avez d'autres besoins.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div id="fix">
|
|
||||||
<!-- Nouvelle Plage
|
|
||||||
Permet de créer une nouvelle ligne pour une nouvelle Plage
|
|
||||||
(
|
|
||||||
Jour, -> datepicker
|
|
||||||
Heure de début, -> timepicker
|
|
||||||
Heure de fin -> timepicker
|
|
||||||
ModuleImplId -> select (liste des modules tout semestre confondu)
|
|
||||||
)
|
|
||||||
--->
|
|
||||||
|
|
||||||
<div id="new_periode" class="box">
|
|
||||||
<label for="date">
|
|
||||||
Date :
|
|
||||||
<input type="text" name="date" id="date" class="datepicker">
|
|
||||||
</label>
|
|
||||||
<label for="debut">
|
|
||||||
Heure de début :
|
|
||||||
<input type="text" name="debut" id="debut" class="timepicker">
|
|
||||||
</label>
|
|
||||||
<label for="fin">
|
|
||||||
Heure de fin :
|
|
||||||
<input type="text" name="fin" id="fin" class="timepicker">
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label for="moduleimpl_select">
|
|
||||||
<div id="forcemodule" style="display: none; margin:10px 0px;">
|
|
||||||
Vous devez spécifier le module ! (voir réglage préférence du semestre)
|
|
||||||
</div>
|
|
||||||
Module :
|
|
||||||
{{moduleimpl_select | safe}}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<button id="add_periode" onclick="nouvellePeriode()">Ajouter une plage</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Boutons d'actions
|
|
||||||
- Sauvegarder
|
|
||||||
- Afficher la photo de profil
|
|
||||||
- Assiduité par défaut (aucune, present, retard, absent)
|
|
||||||
--->
|
|
||||||
<br>
|
|
||||||
<div id="actions" class="flex">
|
|
||||||
<button id="save" onclick="sauvegarderAssiduites()">ENREGISTRER</button>
|
|
||||||
<label for="pdp">
|
|
||||||
Photo de profil :
|
|
||||||
<input type="checkbox" name="pdp" id="pdp" checked onclick="afficherPDP(this.checked)">
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label for="etatDef">
|
|
||||||
Intialiser les étudiants comme :
|
|
||||||
<select name="etatDef" id="etatDef">
|
|
||||||
<option value="">-</option>
|
|
||||||
{% if not non_present %}
|
|
||||||
<option value="present">présents</option>
|
|
||||||
{% endif %}
|
|
||||||
<option value="retard">en retard</option>
|
|
||||||
<option value="absent">absents</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tableau à double entrée
|
|
||||||
Colonne : Etudiants (Header = Nom, Prénom, Photo (si actif))
|
|
||||||
Ligne : Période (Header = Jour, Heure de début, Heure de fin, ModuleImplId)
|
|
||||||
Contenu :
|
|
||||||
- bouton assiduité (présent, retard, absent)
|
|
||||||
- Bouton conflit si conflit de période
|
|
||||||
--->
|
|
||||||
|
|
||||||
<div id="tableau-periode" class="grid-table hidden">
|
|
||||||
<!-- Première ligne : Plages -->
|
|
||||||
<div class="ligne" id="tete-table">
|
|
||||||
<div class="cell header sticky">Étudiants</div>
|
|
||||||
{# <div class="cell header" periode-id="X">Plage X</div> #}
|
|
||||||
</div>
|
|
||||||
{# ... #}
|
|
||||||
|
|
||||||
<hr class="hidden" id="separator">
|
|
||||||
|
|
||||||
{% for etud in etudiants %}
|
|
||||||
<div class="ligne" data-etudid="{{etud.etudid}}">
|
|
||||||
<div class="cell etudinfo sticky" id="head-{{etud.etudid}}">
|
|
||||||
<img src="../../api/etudiant/etudid/{{etud.etudid}}/photo?size=small" alt="{{etud.nomprenom}}" class="pdp">
|
|
||||||
<span>{{ etud.nomprenom }}</span>
|
|
||||||
</div>
|
|
||||||
{# <div class="cell" periode-id="X">Assiduité Plage 1</div> #}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
{% include "assiduites/widgets/alert.j2" %}
|
|
||||||
{% endblock app_content %}
|
|
@ -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', () => {
|
||||||
|
@ -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()
|
||||||
|
@ -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);
|
||||||
|
@ -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 %}
|
@ -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', ()=>{
|
||||||
|
@ -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>
|
|
@ -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>
|
|
@ -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 != '') {
|
||||||
|
@ -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>
|
@ -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(
|
||||||
@ -1724,117 +1618,6 @@ def _preparer_objet(
|
|||||||
return objet_prepare
|
return objet_prepare
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/signal_assiduites_diff")
|
|
||||||
@scodoc
|
|
||||||
@permission_required(Permission.AbsChange)
|
|
||||||
def signal_assiduites_diff():
|
|
||||||
"""
|
|
||||||
Utilisé notamment par "Saisie différée" sur tableau de bord semetstre"
|
|
||||||
|
|
||||||
Arguments de la requête:
|
|
||||||
|
|
||||||
- group_ids : liste des groupes
|
|
||||||
example : group_ids=1,2,3
|
|
||||||
- formsemestre_id : id du formsemestre
|
|
||||||
example : formsemestre_id=1
|
|
||||||
- moduleimpl_id : id du moduleimpl
|
|
||||||
example : moduleimpl_id=1
|
|
||||||
|
|
||||||
(Permet de pré-générer une plage. Si non renseigné, la plage sera vide)
|
|
||||||
(Les trois valeurs suivantes doivent être renseignées ensemble)
|
|
||||||
- date
|
|
||||||
example : date=01/01/2021
|
|
||||||
- heure_debut
|
|
||||||
example : heure_debut=08:00
|
|
||||||
- heure_fin
|
|
||||||
example : heure_fin=10:00
|
|
||||||
|
|
||||||
Exemple de requête :
|
|
||||||
signal_assiduites_diff?formsemestre_id=67&group_ids=400&moduleimpl_id=1229&date=15/04/2024&heure_debut=12:34&heure_fin=12:55
|
|
||||||
"""
|
|
||||||
# Récupération des paramètres de la requête
|
|
||||||
group_ids: list[int] = request.args.get("group_ids", None)
|
|
||||||
formsemestre_id: int = request.args.get("formsemestre_id", -1)
|
|
||||||
formsemestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
|
||||||
|
|
||||||
etudiants: list[Identite] = []
|
|
||||||
|
|
||||||
# Vérification des groupes
|
|
||||||
if group_ids is None:
|
|
||||||
group_ids = []
|
|
||||||
else:
|
|
||||||
group_ids = group_ids.split(",")
|
|
||||||
map(str, group_ids)
|
|
||||||
groups_infos = sco_groups_view.DisplayedGroupsInfos(
|
|
||||||
group_ids, formsemestre_id=formsemestre.id, select_all_when_unspecified=True
|
|
||||||
)
|
|
||||||
if not groups_infos.members:
|
|
||||||
return (
|
|
||||||
html_sco_header.sco_header(page_title="Assiduité: saisie différée")
|
|
||||||
+ "<h3>Aucun étudiant ! </h3>"
|
|
||||||
+ html_sco_header.sco_footer()
|
|
||||||
)
|
|
||||||
|
|
||||||
# Récupération des étudiants
|
|
||||||
etudiants.extend(
|
|
||||||
[Identite.get_etud(etudid=m["etudid"]) for m in groups_infos.members]
|
|
||||||
)
|
|
||||||
etudiants = list(sorted(etudiants, key=lambda etud: etud.sort_key))
|
|
||||||
|
|
||||||
if groups_infos.tous_les_etuds_du_sem:
|
|
||||||
gr_tit = "en"
|
|
||||||
else:
|
|
||||||
if len(groups_infos.group_ids) > 1:
|
|
||||||
grp = "des groupes"
|
|
||||||
else:
|
|
||||||
grp = "du groupe"
|
|
||||||
gr_tit = (
|
|
||||||
grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Pré-remplissage des sélecteurs
|
|
||||||
moduleimpl_id = request.args.get("moduleimpl_id", -1)
|
|
||||||
try:
|
|
||||||
moduleimpl_id = int(moduleimpl_id)
|
|
||||||
except ValueError:
|
|
||||||
moduleimpl_id = -1
|
|
||||||
# date fra (dd/mm/yyyy)
|
|
||||||
date = request.args.get("date", "")
|
|
||||||
# heures (hh:mm)
|
|
||||||
heure_deb = request.args.get("heure_debut", "")
|
|
||||||
heure_fin = request.args.get("heure_fin", "")
|
|
||||||
|
|
||||||
# vérifications des sélecteurs
|
|
||||||
date = date if re.match(r"^\d{2}\/\d{2}\/\d{4}$", date) else ""
|
|
||||||
heure_deb = heure_deb if re.match(r"^[0-2]\d:[0-5]\d$", heure_deb) else ""
|
|
||||||
heure_fin = heure_fin if re.match(r"^[0-2]\d:[0-5]\d$", heure_fin) else ""
|
|
||||||
nouv_plage: list[str] = [date, heure_deb, heure_fin]
|
|
||||||
|
|
||||||
return render_template(
|
|
||||||
"assiduites/pages/signal_assiduites_diff.j2",
|
|
||||||
etudiants=etudiants,
|
|
||||||
moduleimpl_select=_module_selector(
|
|
||||||
formsemestre=formsemestre, moduleimpl_id=moduleimpl_id
|
|
||||||
),
|
|
||||||
gr=gr_tit,
|
|
||||||
nonworkdays=_non_work_days(),
|
|
||||||
sco=ScoData(formsemestre=formsemestre),
|
|
||||||
forcer_module=sco_preferences.get_preference(
|
|
||||||
"forcer_module",
|
|
||||||
formsemestre_id=formsemestre_id,
|
|
||||||
dept_id=g.scodoc_dept_id,
|
|
||||||
),
|
|
||||||
non_present=sco_preferences.get_preference(
|
|
||||||
"non_present",
|
|
||||||
formsemestre_id=formsemestre_id,
|
|
||||||
dept_id=g.scodoc_dept_id,
|
|
||||||
),
|
|
||||||
nouv_plage=nouv_plage,
|
|
||||||
formsemestre_id=formsemestre_id,
|
|
||||||
group_ids=group_ids,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/signale_evaluation_abs/<int:evaluation_id>/<int:etudid>")
|
@bp.route("/signale_evaluation_abs/<int:evaluation_id>/<int:etudid>")
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.AbsChange)
|
@permission_required(Permission.AbsChange)
|
||||||
|
Loading…
Reference in New Issue
Block a user