ScoDoc/app/templates/assiduites/pages/signal_assiduites_diff.j2

647 lines
19 KiB
Django/Jinja

{% 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');
["present", "retard", "absent"].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 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 = `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 }}];
// 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() {
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();
});
}
main();
</script>
{% endblock scripts %}
{% block title %}
{{title}}
{% endblock title %}
{% block app_content %}
<h2>Signalement différé de l'assiduité {{gr |safe}}</h2>
<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>
<option value="present">présents</option>
<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 %}