Assiduité: signal_assiduites_diff OK

This commit is contained in:
Iziram 2024-03-15 15:56:33 +01:00
parent c617ee321a
commit b74d525c28
6 changed files with 388 additions and 141 deletions

View File

@ -25,8 +25,8 @@
# #
############################################################################## ##############################################################################
"""Tableau de bord module """Tableau de bord module"""
"""
import math import math
import time import time
import datetime import datetime
@ -329,8 +329,6 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
>Saisie Absences journée</a></span> >Saisie Absences journée</a></span>
""" """
) )
year, week, day = datetime.date.today().isocalendar()
semaine: str = f"{year}-W{week}"
H.append( H.append(
f""" f"""
<span class="moduleimpl_abs_link"><a class="stdlink" href="{ <span class="moduleimpl_abs_link"><a class="stdlink" href="{
@ -338,11 +336,10 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
"assiduites.signal_assiduites_diff", "assiduites.signal_assiduites_diff",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
group_ids=group_id, group_ids=group_id,
semaine=semaine,
formsemestre_id=formsemestre.id, formsemestre_id=formsemestre.id,
moduleimpl_id="" if moduleimpl_id is None else moduleimpl_id moduleimpl_id="" if moduleimpl_id is None else moduleimpl_id
)}" )}"
>Saisie Absences hebdo</a></span> >Saisie Absences Différée</a></span>
""" """
) )

View File

@ -440,12 +440,6 @@
position: absolute; position: absolute;
z-index: 5; z-index: 5;
border: 5px solid var(--color-primary); border: 5px solid var(--color-primary);
/* background-color: rgba(36, 36, 36, 0.25);
background-image: repeating-linear-gradient(135deg,
transparent,
transparent 5px,
rgba(81, 81, 81, 0.61) 5px,
rgba(81, 81, 81, 0.61) 10px); */
border-radius: 5px; border-radius: 5px;
} }
@ -455,6 +449,15 @@
transform: translateX(-50%); transform: translateX(-50%);
} }
.assiduite-infos {
position: absolute;
right: 0;
margin: 5px;
top: 0;
font-size: 16px;
cursor: pointer;
}
.action-buttons { .action-buttons {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@ -855,6 +855,16 @@ function setupAssiduiteBubble(el, assiduite) {
bubble.className = "assiduite-bubble"; bubble.className = "assiduite-bubble";
bubble.classList.add(assiduite.etat.toLowerCase()); bubble.classList.add(assiduite.etat.toLowerCase());
// Ajout d'un lien pour plus d'informations
const infos = document.createElement("a");
infos.className = "assiduite-infos";
infos.textContent = ``;
infos.title = "Cliquez pour plus d'informations";
infos.target = "_blank";
infos.href = `tableau_assiduite_actions?type=assiduite&action=details&obj_id=${assiduite.assiduite_id}`;
bubble.appendChild(infos);
const idDiv = document.createElement("div"); const idDiv = document.createElement("div");
idDiv.className = "assiduite-id"; idDiv.className = "assiduite-id";
getModuleImpl(assiduite).then((modImpl) => { getModuleImpl(assiduite).then((modImpl) => {

View File

@ -255,6 +255,13 @@ Object.defineProperty(Date.prototype, "format", {
value: function (formatString) { value: function (formatString) {
let iso = this.toIsoUtcString(); let iso = this.toIsoUtcString();
switch (formatString) { switch (formatString) {
case "DD/MM/YYYY":
return this.toLocaleString("fr-FR", {
day: "2-digit",
month: "2-digit",
year: "numeric",
timeZone: SCO_TIMEZONE,
});
case "DD/MM/Y HH:mm": case "DD/MM/Y HH:mm":
return this.toLocaleString("fr-FR", { return this.toLocaleString("fr-FR", {
day: "2-digit", day: "2-digit",
@ -275,6 +282,8 @@ Object.defineProperty(Date.prototype, "format", {
hour12: false, hour12: false,
timeZone: SCO_TIMEZONE, timeZone: SCO_TIMEZONE,
}); });
case "HH:mm":
return iso.slice(11, 16);
case "YYYY-MM-DDTHH:mm": case "YYYY-MM-DDTHH:mm":
// slice : YYYY-MM-DDTHH // slice : YYYY-MM-DDTHH

View File

@ -97,19 +97,12 @@
z-index: 0; z-index: 0;
} }
.pointer{
cursor: pointer;
}
</style> </style>
{# Temporaire #}
<style>
.wip::before{
content: "WIP 🚧";
font-size: 1.5em;
margin: 2px;
color: red;
}
</style>
{% endblock styles %} {% endblock styles %}
{% block scripts %} {% block scripts %}
@ -121,142 +114,375 @@
<script> <script>
function afficherPDP(checked) { /**
if (checked) { * Permet d'afficher ou non les photos des étudiants
gtrcontent.setAttribute("data-pdp", "true"); * @param {boolean} checked
} else { */
gtrcontent.removeAttribute("data-pdp"); function afficherPDP(checked) {
} if (checked) {
gtrcontent.setAttribute("data-pdp", "true");
} else {
gtrcontent.removeAttribute("data-pdp");
}
}
/**
* 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 période.")
if (date == "" || debut == "" || fin == "" || moduleimpl_id == "") {
openAlertModal(
"Erreur",
text
);
return;
} }
}
async function nouvellePeriode(){ // On ajoute la nouvelle période au tableau
periodeId++; let periodeDiv = document.createElement("div");
periodeDiv.classList.add("cell", "sticky");
periodeDiv.id = `periode-${periodId}`;
const periodP = document.createElement("p");
periodP.textContent = `Période du ${date} de ${debut} à ${fin}`;
// On récupère les valeurs des inputs // On ajoute le moduleimpl
let date = document.getElementById("date").value; const modP = document.createElement("p");
let debut = document.getElementById("debut").value; modP.textContent = moduleimpl;
let fin = document.getElementById("fin").value;
let moduleimpl_id = document.getElementById("moduleimpl_select").value;
const moduleimpl = await getModuleImpl({moduleimpl_id: moduleimpl_id});
// On ajoute la nouvelle période au tableau // On ajoute le bouton pour supprimer la période
let periodeDiv = document.createElement("div"); const close = document.createElement("button");
periodeDiv.classList.add("cell", "sticky"); close.textContent = "❌";
const periodP = document.createElement("p"); close.addEventListener("click", () => {
periodP.textContent = `Période du ${date} de ${debut} à ${fin}`; // On supprime toutes les cases du tableau correspondant à cette période
document
.querySelectorAll(
`.cell[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("tableau-periode").appendChild(periodeDiv);
const modP = document.createElement("p"); // On récupère les étudiants (etudids)
modP.textContent = moduleimpl; let etudids = [
...document.querySelectorAll("#tableau-periode .header[data-etudid]"),
].map((e) => e.getAttribute("data-etudid"));
const close = document.createElement("button"); // On génère une date de début et de fin de la période
close.textContent = "❌"; const date_debut = new Date(
close.addEventListener("click", (event)=>{ $("#date").datepicker("getDate").format("YYYY-MM-DD") + "T" + debut
document.querySelectorAll(`.cell[data-periodeid="${periodeDiv.getAttribute('data-periodeid')}"]`).forEach((e)=>e.remove()); );
}); const date_fin = new Date(
$("#date").datepicker("getDate").format("YYYY-MM-DD") + "T" + fin
);
date_debut.add(1, "seconds");
periodeDiv.appendChild(periodP); // Préparation de la requête
periodeDiv.appendChild(modP); const url =
periodeDiv.appendChild(close); `../../api/assiduites/group/query?date_debut=${date_debut.toFakeIso()}` +
periodeDiv.setAttribute("data-periodeid", periodeId); `&date_fin=${date_fin.toFakeIso()}&etudids=${etudids.join(
document.getElementById("tableau-periode").appendChild(periodeDiv); ","
)}&with_justifs`;
let etudids = [...document
.querySelectorAll("#tableau-periode .header[data-etudid]")]
.map((e)=>e.getAttribute("data-etudid"));
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")
const url =
`../../api/assiduites/group/query?date_debut=${date_debut.toFakeIso()}` +
`&date_fin=${date_fin.toFakeIso()}&etudids=${etudids.join(',')}&with_justifs`;
await fetch(url) //Si la période n'existait pas, alors on l'ajoute à la Map
.then((res) => { if (!period) {
if (!res.ok) { periodes.set(periodId, {
throw new Error("Network response was not ok"); 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.getElementById("tableau-periode").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;
} }
return res.json();
})
.then((data)=>{
Object.entries(data).forEach(([etudid, assiduites]) => {
let cell = document.createElement("div");
cell.classList.add("cell");
cell.setAttribute("data-etudid", etudid);
cell.setAttribute("data-periodeid", periodeId);
if (assiduites.length == 0) { //Gestion des assiduités déjà existantes
["present", "retard", "absent"].forEach((value) => { const assiduites = data[etudid];
const cbox = document.createElement('input'); // Si l'étudiant n'a pas d'assiduité, on crée les boutons assiduité
cbox.type = "checkbox"; if (assiduites.length == 0) {
cbox.value = value; ["present", "retard", "absent"].forEach((value) => {
cbox.name = `rbtn_${etudid}_${periodeId}` const cbox = document.createElement("input");
cbox.classList.add("rbtn", value); cbox.type = "checkbox";
cbox.value = value;
cbox.name = `rbtn_${etudid}_${periodId}`;
cbox.classList.add("rbtn", value);
cbox.addEventListener('click', (event)=>{ // Event pour être sur qu'un seul bouton est coché à la fois
const parent = event.target.parentElement; cbox.addEventListener("click", (event) => {
parent.querySelectorAll(".rbtn").forEach((ele)=>{ const parent = event.target.parentElement;
if(ele.value != value){ parent.querySelectorAll(".rbtn").forEach((ele) => {
ele.checked = false; if (ele.value != value) {
} ele.checked = false;
})
})
cbox.checked = etatDef.value == value
cell.appendChild(cbox);
});
}else{
setupAssiduiteBubble(cell, assiduites[0])
} }
});
document.getElementById("tableau-periode").appendChild(cell);
}); });
}).catch((error)=>{ // Si une valeur par défaut est donnée alors on l'applique
console.error('Error:', error); cbox.checked = etatDef.value == value;
cell.appendChild(cbox);
});
} 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);
});
}
/**
* 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(".cell").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 y n'a pas d'erreur, on affiche un message de succès
if (data.errors.length == 0) {
const span = document.createElement("span");
span.textContent = "Les assiduités ont bien été sauvegardées.";
openAlertModal(
"Sauvegarde des assiduités",
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 = `Période 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");
let periodeId = 0; // Pour chaque erreur, on créer un élément de liste
const moduleimpls = new Map(); const ul2 = document.createElement("ul");
const nonWorkDays = [{{ nonworkdays| safe }}]; periodErrors.forEach((err) => {
const li2 = document.createElement("li");
li2.textContent = err[0];
window.forceModule = "{{ forcer_module }}" == "True" li2.classList.add("pointer");
if (window.forceModule) {
if (moduleimpl_select.value == "") {
document.getElementById('forcemodule').style.display = "block";
add_periode.disabled = true;
// 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);
} }
moduleimpl_select?.addEventListener('change', (e) => { openAlertModal(
if (e.target.value != "") { "Erreurs lors de la sauvegarde des assiduités",
document.getElementById('forcemodule').style.display = "none"; ul,
add_periode.disabled = false; "Les autres assiduités ont bien été sauvegardées."
);
} else { }
document.getElementById('forcemodule').style.display = "block"; },
add_periode.disabled = true; (e) => {
console.error("Erreur lors de la création des assiduités", e);
}
});
} }
);
}
async function main(){ // Mis en place des variables globales
afficherPDP(pdp.checked); let currentPeriodId = 0;
$('#date').on('change', async function(d) { const periodes = new Map();
// On vérifie si la date est un jour travaillé const moduleimpls = new Map();
dateCouranteEstTravaillee(); const inscriptionsModules = new Map();
}); const nonWorkDays = [{{ nonworkdays| safe }}];
// 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;
} }
});
}
/**
* 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() {
afficherPDP(pdp.checked);
$("#date").on("change", async function (d) {
// On vérifie si la date est un jour travaillé
dateCouranteEstTravaillee();
});
}
main();
main();
</script> </script>
{% endblock scripts %} {% endblock scripts %}
@ -311,8 +537,6 @@
- Sauvegarder - Sauvegarder
- Afficher la photo de profil - Afficher la photo de profil
- Assiduité par défaut (aucune, present, retard, absent) - Assiduité par défaut (aucune, present, retard, absent)
? - Import Excel (fournie un fichier excel
avec les étudiants et les périodes préremplis)
---> --->
<div id="actions" class="box"> <div id="actions" class="box">
@ -330,12 +554,8 @@
<option value="absent">Absence</option> <option value="absent">Absence</option>
</select> </select>
</label> </label>
<label for="excel" class="wip"> <button id="save" onclick="sauvegarderAssiduites()">Sauvegarder l'assiduité</button>
Importer Excel :
<input type="file" name="excel" id="excel" accept=".xlsx, .xls, .csv">
</label>
<button class="wip" id="save wip">Sauvegarder l'assiduité</button>
</div> </div>
</div> </div>

View File

@ -1848,10 +1848,18 @@ def signal_assiduites_diff():
grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>" grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
) )
moduleimpl_id = request.args.get("moduleimpl_id", -1)
try:
moduleimpl_id = int(moduleimpl_id)
except ValueError:
moduleimpl_id = -1
return render_template( return render_template(
"assiduites/pages/signal_assiduites_diff.j2", "assiduites/pages/signal_assiduites_diff.j2",
etudiants=etudiants, etudiants=etudiants,
moduleimpl_select=_module_selector(formsemestre=formsemestre), moduleimpl_select=_module_selector(
formsemestre=formsemestre, moduleimpl_id=moduleimpl_id
),
gr=gr_tit, gr=gr_tit,
nonworkdays=_non_work_days(), nonworkdays=_non_work_days(),
sco=ScoData(formsemestre=formsemestre), sco=ScoData(formsemestre=formsemestre),