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

898 lines
29 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>
.rbtn::before {
--size: 1.5em;
width: var(--size);
height: var(--size);
}
.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 {
text-align: center;
}
table {
border-collapse: collapse;
width: 100%;
max-width: 1600px;
position: relative;
table-layout: fixed;
}
th,
td {
border: 1px solid #ddd;
padding: 8px;
text-align: center;
background-color: white;
}
th {
z-index: 1;
}
.premier th {
position: sticky;
top: 0;
background-color: white;
}
.second th {
position: sticky;
top: 38px;
background-color: white;
}
.sticky-col {
position: sticky;
left: 0;
z-index: 1;
}
.rbtn:not(:checked)::before {
opacity: 0.5;
}
.grayed {
filter: brightness(0.5);
}
.conflit {
background-color: var(--color-conflit);
}
.conflit_calendar{
font-size: 1.5em;
cursor: pointer;
}
</style>
<style>
.timePicker-modal {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.5);
}
.timePicker-modal.show {
display: block;
}
.timePicker-modal-content {
background-color: white;
margin: 15% auto;
padding: 20px;
border: 1px solid #888;
width: 300px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
text-align: center;
}
.timePicker-close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
}
.timePicker-close:hover,
.timePicker-close:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
.time-picker-container {
margin: 15px 0;
}
#confirmButton {
padding: 10px 20px;
font-size: 16px;
background-color: var(--color-primary);
color: white;
border: none;
cursor: pointer;
}
#confirmButton:hover {
background-color: var(--color-secondary);
}
.etudinfo{
text-align: left;
}
</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>
const readonly = "{{readonly | safe}}" == "True";
const non_present = "{{non_present | safe}}" == "True";
const etuds = [
{% for etud in etudiants %}
{
id: {{etud.etudid}},
nom: "{{etud.nom}}",
prenom: "{{etud.prenom}}"
},
{% endfor %}
]
let days = [
{% for jour in hebdo_jours %}
{
date : new Date(Date.fromFRA("{{jour[1][1]}}")),
visible : "{{not jour[0]}}" == "True",
nom : "{{jour[1][0]}}",
},
{% endfor %}
] // [0]=Lundi ... [6]=Dimanche -> à 00h00
//Une fonction d'action quand un bouton est cliqué
// 3 possibilités :
// - assiduite_id = null -> créer nv assi avec état du bouton
// - assiduite_id non null et bouton coché == etat assi -> suppression de l'assiduité
// - assiduite_id non null et bouton coché != etat assi -> modification de l'assiduité
async function actionButton(btn, same = false) {
let td = btn.parentElement;
let tr = td.parentElement;
let etudid = tr.getAttribute("etudid");
let etud = etuds.find((etud) => etud.id == etudid);
let etat = btn.value;
let assiduite_id = td.getAttribute("assiduite_id");
let dayInfo = [td.getAttribute("day"), td.getAttribute("time")]// [0]=[0..6] [1]=am/pm
let day = days[dayInfo[0]].date;
dayInfo[1] = dayInfo[1] == "am" ? "matin" : "apresmidi";
let deb = new Date(day.format('YYYY-MM-DD') + "T" + temps[dayInfo[1]].debut);
let fin = new Date(day.format('YYYY-MM-DD') + "T" + temps[dayInfo[1]].fin);
const assi = {
etudid: etudid,
etat: etat,
moduleimpl_id: document.getElementById("moduleimpl_select").value,
date_debut: deb.toFakeIso(),
date_fin: fin.toFakeIso(),
}
let cancelEvent = false;
if (assiduite_id != "") {
if (same) {
// Suppression
await async_post(
`../../api/assiduite/delete`,
[assiduite_id],
(data) => {
if (data.success.length > 0) {
envoiToastEtudiant("remove", etud);
td.setAttribute("assiduite_id", "");
} else {
console.error(data.errors["0"].message);
cancelEvent = true;
erreurModuleImpl(data.errors["0"].message);
}
},
(error) => {
console.error("Erreur lors de la suppression de l'assiduité", error);
cancelEvent = true;
}
);
} else {
// Modification
await async_post(
`../../api/assiduite/${assiduite_id}/edit`,
assi,
(data) => {
envoiToastEtudiant(etat, etud);
},
(error) => {
console.error("Erreur lors de la modification de l'assiduité", error);
cancelEvent = true;
erreurModuleImpl(error.message);
}
);
}
} else {
// Création
await async_post(
`../../api/assiduite/${etud.id}/create`,
[assi],
(data) => {
if (data.success.length > 0) {
envoiToastEtudiant(etat, etud);
//mise à jour de l'assiduité_id dans le td
td.setAttribute("assiduite_id", data.success["0"].message.assiduite_id);
} else {
console.error(data.errors["0"].message);
erreurModuleImpl(data.errors["0"].message);
cancelEvent = true;
}
},
(error) => {
console.error("Erreur lors de la création de l'assiduité", error);
}
);
}
return cancelEvent;
}
async function recupAssiduitesHebdo(callback) {
const etudIds = etuds.map((etud) => etud.id).join(",");
const date_debut = days[0].date.startOf("day").format("YYYY-MM-DDTHH:mm");
const date_fin = days[6].date.endOf("day").format("YYYY-MM-DDTHH:mm");
url =
`../../api/assiduites/group/query?date_debut=${date_debut}` +
`&date_fin=${date_fin}&etudids=${etudIds}&with_justifs`;
await fetch(url)
.then((res) => {
if (!res.ok) {
throw new Error("Network response was not ok");
}
return res.json();
})
.then((data) => {
let assiduites = []
Object.keys(data).forEach((etudid) => {
assiduites.push(...data[etudid]);
});
callback(assiduites);
})
.catch((error) =>
console.error(
"There has been a problem with your fetch operation:",
error
)
);
}
function updateTable(assiduites) {
const img_conflit = `
<a
class="conflit_calendar"
title="Des assiduités existent déjà pour cette période. Cliquez ici pour voir le calendrier de l'assiduité de l'étudiant"
data-tooltip
target="_blank"
>📅</a>`
// Suppression existant
document.querySelectorAll("td.btns").forEach((el) => {
el.remove();
});
for (let i = 0; i < days.length; i++) {
let day = days[i].date;
let morningPeriod = {
deb: new Date(day.format('YYYY-MM-DD') + "T" + temps.matin.debut),
fin: new Date(day.format('YYYY-MM-DD') + "T" + temps.matin.fin),
}
let afternoonPeriod = {
deb: (new Date(day.format('YYYY-MM-DD') + "T" + temps.apresmidi.debut)),
fin: new Date(day.format('YYYY-MM-DD') + "T" + temps.apresmidi.fin),
}
const assiduitesByDay = {
matin: assiduites.filter((assi) => {
const period = {
deb: new Date(assi.date_debut),
fin: new Date(assi.date_fin)
}
return hasTimeConflict(period, morningPeriod);
}),
apresmidi: assiduites.filter((assi) => {
const period = {
deb: new Date(assi.date_debut),
fin: new Date(assi.date_fin)
}
return hasTimeConflict(period, afternoonPeriod);
})
};
// Récupération des tr étudiants
let trs = document.querySelectorAll("tr[etudid]");
trs.forEach((tr) => {
let etudid = tr.getAttribute("etudid");
if (!days[i].visible && i >= 5) {
return;
} else if (!days[i].visible) {
tr.insertAdjacentHTML("beforeend", "<td class='grayed btns' colspan='2'></td>");
return;
}
let etudAssiMorning = assiduitesByDay.matin.filter((a) => {
return a.etudid == etudid;
});
let etudAssiAfternoon = assiduitesByDay.apresmidi.filter((a) => {
return a.etudid == etudid;
});
// Créations des boutons
// matin
let tdMatin = document.createElement("td");
tdMatin.classList.add("btns");
tdMatin.setAttribute("day", i);
tdMatin.setAttribute("time", "am");
tr.appendChild(tdMatin);
// après-midi
let tdApresmidi = document.createElement("td");
tdApresmidi.classList.add("btns");
tdApresmidi.setAttribute("day", i);
tdApresmidi.setAttribute("time", "pm");
tr.appendChild(tdApresmidi);
// Peuplement des boutons en fonction des assiduités
let boutons = `
<input type="checkbox" name="matin-${etudid}" id="matin-${etudid}"
class="rbtn retard" value="retard">
<input type="checkbox" name="matin-${etudid}" id="matin-${etudid}"
class="rbtn absent" value="absent">
`
if (!non_present) {
boutons = `<input type="checkbox" name="matin-${etudid}" id="matin-${etudid}"
class="rbtn present" value="present">`+boutons;
}
// matin
tdMatin.innerHTML = boutons
tdMatin.setAttribute("assiduite_id", "")
if (etudAssiMorning.length != 0) {
let assi = etudAssiMorning[0];
const deb = new Date(assi.date_debut);
const fin = new Date(assi.date_fin);
// si dates == periode -> cocher bouton correspondant
// Sinon supprimer boutons et mettre case "rouge" + tooltip
if (deb.isSame(morningPeriod.deb, "minutes") && fin.isSame(morningPeriod.fin, "minutes")) {
let etat = assi.etat.toLowerCase();
const input = tdMatin.querySelector(`[value="${etat}"]`)
if (input) {
input.checked = true;
}
tdMatin.setAttribute("assiduite_id", assi.assiduite_id);
let saisie = new Date(assi.entry_date).format("DD/MM/Y HH:mm");
saisie = saisie.split(" ").join(" à ");
let text = `noté ${etat} le ${saisie} par ${assi.user_nom_complet}`;
tdMatin.setAttribute("title", text);
tdMatin.setAttribute("data-tooltip", "");
} else {
tdMatin.innerHTML = img_conflit;
tdMatin.querySelector(".conflit_calendar").href = `calendrier_assi_etud?etudid=${etudid}`;
tdMatin.classList.add("conflit");
}
}
// après-midi
tdApresmidi.innerHTML = boutons
tdApresmidi.setAttribute("assiduite_id", "")
if (etudAssiAfternoon.length != 0) {
let assi = etudAssiAfternoon[0];
const deb = new Date(assi.date_debut);
const fin = new Date(assi.date_fin);
// si dates == periode -> cocher bouton correspondant
// Sinon supprimer boutons et mettre case "rouge" + tooltip
if (deb.isSame(afternoonPeriod.deb, "minutes") && fin.isSame(afternoonPeriod.fin, "minutes")) {
let etat = assi.etat.toLowerCase();
const input = tdApresmidi.querySelector(`[value="${etat}"]`)
if (input) {
input.checked = true;
}
tdApresmidi.setAttribute("assiduite_id", assi.assiduite_id);
let saisie = new Date(assi.entry_date).format("DD/MM/Y HH:mm");
saisie = saisie.split(" ").join(" à ");
let text = `noté ${etat} le ${saisie} par ${assi.user_nom_complet}`;
tdApresmidi.setAttribute("title", text);
tdApresmidi.setAttribute("data-tooltip", "");
} else {
tdApresmidi.innerHTML = img_conflit;
tdApresmidi.querySelector(".conflit_calendar").href = `calendrier_assi_etud?etudid=${etudid}`;
tdApresmidi.classList.add("conflit");
}
}
});
}
document.querySelectorAll("td .rbtn").forEach((el) => {
el.addEventListener("click", async (e) => {
if (readonly) {
e.preventDefault();
return;
}
let target = e.target;
let parent = target.parentElement;
let isCancelled = await actionButton(target, !target.checked);
if (isCancelled) {
e.preventDefault();
target.checked = !target.checked;
return;
}
let inputs = parent.querySelectorAll(".rbtn");
inputs.forEach((input) => {
if (input != target) {
input.checked = false;
}
});
});
});
enableTooltips("table");
}
// Une fonction pour changer de semaine (précédente ou suivante)
// fait juste un location.href avec les bons paramètres
function changeWeek(prev = false) {
const currentUrl = new URL(window.location.href); // Récupère l'URL actuelle
const params = new URLSearchParams(currentUrl.search); // Récupère les paramètres de l'URL
let currentWeekParam = params.get('week');
// Extraire l'année et le numéro de semaine du paramètre de la semaine actuelle
const [year, week] = currentWeekParam.split('-W').map(Number);
// Calculer la nouvelle semaine et l'année
let newYear = year;
let newWeek = week + (prev ? -1 : 1);
if (newWeek < 1) {
newYear -= 1; // Passer à l'année précédente
newWeek = getISOWeeksInYear(newYear); // Dernière semaine de l'année précédente
} else if (newWeek > getISOWeeksInYear(newYear)) {
newYear += 1; // Passer à l'année suivante
newWeek = 1; // Première semaine de l'année suivante
}
// Formater le nouveau paramètre de semaine
const newWeekParam = `${newYear}-W${String(newWeek).padStart(2, '0')}`;
params.set('week', newWeekParam); // Mettre à jour le paramètre 'week'
currentUrl.search = params.toString(); // Mettre à jour les paramètres de l'URL
window.location.href = currentUrl.toString(); // Rediriger vers la nouvelle URL
}
// Une fonction pour gérer le bouton "tout le monde présent"
// coche tous les boutons de la colonne
function allPresent(day, time) {
// Version naive : coche tous les boutons de la colonne
// TODO - Optimiser avec une seule requête API
let tds = document.querySelectorAll(`td[day="${day}"][time="${time}"]`);
const real_time = time == "am" ? "matin" : "apresmidi";
const assi = {
etat: "present",
moduleimpl_id: document.getElementById("moduleimpl_select").value,
date_debut: new Date(days[day].date.format('YYYY-MM-DD') + "T" + temps[real_time].debut).toFakeIso(),
date_fin: new Date(days[day].date.format('YYYY-MM-DD') + "T" + temps[real_time].fin).toFakeIso(),
}
let toCreate = []; // [{etudid:<int>}]
let toEdit = [];// [{etudid:<int>, assiduite_id:<int>}]
tds.forEach((td) => {
// on ne touche pas aux conflits
if (td.classList.contains("conflit")) {
return;
}
const tr = td.parentElement;
const etudid = Number(tr.getAttribute("etudid"));
const assiduite_id = td.getAttribute("assiduite_id");
if (assiduite_id == "") {
toCreate.push({ etudid: etudid });
} else {
toEdit.push({ etudid: etudid, assiduite_id: Number(assiduite_id) });
}
})
// Création
toCreate = toCreate.map((el) => {
return {
...assi,
etudid: el.etudid,
}
});
// Modification
toEdit = toEdit.map((el) => {
return {
...assi,
etudid: el.etudid,
assiduite_id: el.assiduite_id,
}
});
// Appel API
let counts = {
create: toCreate.length,
edit: toEdit.length
}
const promiseCreate = async_post(
`../../api/assiduites/create`,
toCreate,
async (data) => {
if (data.errors.length > 0) {
console.error(data.errors);
data.errors.forEach((err) => {
let obj = toCreate[err.indice];
let etu = etuds.find((el) => el.id == obj.etudid);
const text = document.createTextNode(`Erreur pour ${etu.nom} ${etu.prenom} : ${err.message}`);
const toast = generateToast(text, "var(--color-error)", 10);
pushToast(toast);
});
}
counts.create = data.success.length;
},
(error) => {
console.error("Erreur lors de la création de l'assiduité", error);
}
);
const promiseEdit = async_post(
`../../api/assiduites/edit`,
toEdit,
async (data) => {
if (data.errors.length > 0) {
console.error(data.errors);
data.errors.forEach((err) => {
let obj = toEdit[err.indice];
let etu = etuds.find((el) => el.id == obj.etudid);
const text = document.createTextNode(`Erreur pour ${etu.nom} ${etu.prenom} : ${err.message}`);
const toast = generateToast(text, "var(--color-error)");
pushToast(toast);
});
}
counts.edit = data.success.length;
},
(error) => {
console.error("Erreur lors de l'édition de l'assiduité", error);
}
);
// Affiche un loader
afficheLoader();
Promise.all([promiseCreate, promiseEdit]).then(async () => {
retirerLoader();
await recupAssiduitesHebdo(updateTable);
envoiToastTous("present", counts.create + counts.edit);
});
}
</script>
<script>
function updateTemps(temps){
let matin = document.getElementById("text-matin");
let apresmidi = document.getElementById("text-apresmidi");
matin.textContent = `${temps.matin.debut} à ${temps.matin.fin}`;
apresmidi.textContent = `${temps.apresmidi.debut} à ${temps.apresmidi.fin}`;
recupAssiduitesHebdo(updateTable);
}
const temps = {
matin: {
debut: "{{ scu.get_assiduites_time_config("assi_morning_time") }}",
fin: "{{ scu.get_assiduites_time_config("assi_lunch_time") }}"
},
apresmidi: {
debut: "{{ scu.get_assiduites_time_config("assi_lunch_time") }}",
fin: "{{ scu.get_assiduites_time_config("assi_afternoon_time") }}",
}
}
document.getElementById("text-matin").addEventListener("click", (e)=>{
e.preventDefault();
openModal(true);
});
document.getElementById("text-apresmidi").addEventListener("click", (e)=>{
e.preventDefault();
openModal(false);
});
updateTemps(temps);
</script>
<script>
function openModal(morning = true){
let text = morning ? "du matin" : "de l'après-midi";
const modal = document.getElementById("timePickerModal");
modal.querySelector("#timePicker-modal-text").textContent = text;
let time1 = $("#time1");
let time2 = $("#time2");
// Réinitialiser les champs
time1.val(morning ? temps.matin.debut : temps.apresmidi.debut);
time2.val(morning ? temps.matin.fin : temps.apresmidi.fin);
// Définir l'action du bouton de confirmation
document.getElementById("confirmButton").onclick = function(){
let debut = time1.val();
let fin = time2.val();
if (debut == "" || fin == ""){
alert("Veuillez remplir les deux champs");
return;
}
if (debut >= fin){
alert("L'heure de début doit être inférieure à l'heure de fin");
return;
}
if (morning){
if (fin > temps.apresmidi.debut){
alert("L'heure de fin du matin doit être inférieure à l'heure de début de l'après-midi");
return;
}
temps.matin.debut = debut;
temps.matin.fin = fin;
} else {
if (debut < temps.matin.fin){
alert("L'heure de début de l'après-midi doit être supérieure à l'heure de fin du matin");
return;
}
temps.apresmidi.debut = debut;
temps.apresmidi.fin = fin;
}
updateTemps(temps);
modal.classList.remove("show");
}
modal.classList.add("show");
}
document.addEventListener("DOMContentLoaded", ()=>{
const modal = document.getElementById("timePickerModal");
modal.querySelector(".timePicker-close").onclick = function() {
modal.classList.remove("show");
}
document.addEventListener('keyup', function(e) {
if (e.key === "Escape" && modal.classList.contains("show")) {
modal.classList.remove("show");
}
});
document.querySelectorAll("th .rbtn").forEach((el)=>{
el.addEventListener("click", (e)=>{
allPresent(...el.id.split("-"));
e.preventDefault();
})
})
})
</script>
{% endblock scripts %}
{% block title %}
{{ title }}
{% endblock title %}
{% block app_content %}
<h2>Signalement hebdomadaire de l'assiduité {{ gr | safe }}</h2>
<br>
<div id="actions" class="flex">
<button onclick="changeWeek(true)">Semaine précédente</button>
<label for="moduleimpl_select">
Module:
{{moduleimpl_select | safe}}
</label>
<button onclick="changeWeek(false)">Semaine suivante</button>
</div>
<h3 id="tableau-dates">
Le matin <a href="#" id="text-matin" title="Cliquer pour modifier les horaires">9h à 12h</a> et l'après-midi de <a href="#" id="text-apresmidi" title="Cliquer pour modifier les horaires">13h à 17h</a>
</h3>
{% if readonly %}
<h4
title="Vous n'avez pas les permissions nécessaires afin de modifier les assiduités"
data-tooltip
>
Ouvert en mode <span class="rouge">lecture seule</span>.
</h4>
{% endif %}
<table id="table">
<thead>
<tr class="premier">
<th rowspan="2">Étudiants</th>
{% for jour in hebdo_jours %}
{% if not jour[0] or jour[1][0] not in ['Samedi', 'Dimanche'] %}
<th colspan="2" class="{{'grayed' if jour[0] else ''}}" >{{ jour[1][0] }} {{jour[1][1] }}</th>
{% endif %}
{% endfor %}
</tr>
<tr class="second">
{% for jour in hebdo_jours %}
{% if not jour[0] or jour[1][0] not in ['Samedi', 'Dimanche'] %}
<th class="{{'grayed' if jour[0] else ''}}">Matin</th>
<th class="{{'grayed' if jour[0] else ''}}">Après-midi</th>
{% endif %}
{% endfor %}
</tr>
{% if not readonly and not non_present %}
<tr>
{# Ne pas afficher si preference "non presences" / "readonly" #}
<th></th>
{% for jour in hebdo_jours %}
{% if not jour[0] or jour[1][0] not in ['Samedi', 'Dimanche'] %}
<th class="{{'grayed' if jour[0] else ''}}">
<input title="Mettre tout le monde présent" data-tooltip type="checkbox" name="" id="{{loop.index - 1}}-am" class="rbtn present" {{'disabled' if jour[0] else ''}}>
</th>
<th class="{{'grayed' if jour[0] else ''}}">
<input title="Mettre tout le monde présent" data-tooltip type="checkbox" name="" id="{{loop.index - 1}}-pm" class="rbtn present" {{'disabled' if jour[0] else ''}}>
</th>
{% endif %}
{% endfor %}
</tr>
{% endif %}
</thead>
<tbody>
{% for etud in etudiants %}
<tr etudid="{{etud.etudid}}" id="row-{{etud.etudid}}">
<td class="etudinfo" id="etud-{{etud.etudid}}">{{ etud.nom_prenom() }}</td>
{# Sera rempli en JS #}
{# Ne pas afficher bouton présent si pref "non présences" #}
{# <td>
<input type="checkbox" name="" id="" class="rbtn present">
<input type="checkbox" name="" id="" class="rbtn retard">
<input type="checkbox" name="" id="" class="rbtn absent">
</td>
<td>
<input type="checkbox" name="" id="" class="rbtn present">
<input type="checkbox" name="" id="" class="rbtn retard">
<input type="checkbox" name="" id="" class="rbtn absent">
</td> #}
</tr>
{% endfor %}
</tbody>
</table>
<div id="timePickerModal" class="timePicker-modal">
<div class="timePicker-modal-content">
<span class="timePicker-close">&times;</span>
<h2>Choisissez les horaires <span id="timePicker-modal-text"></span></h2>
<div class="time-picker-container">
<label for="time1">Début</label>
<input type="text" id="time1" name="time1" class="timepicker" placeholder="hh:mm">
</div>
<div class="time-picker-container">
<label for="time2">Fin</label>
<input type="text" id="time2" name="time2" class="timepicker" placeholder="hh:mm">
</div>
<span>
<button id="confirmButton">Confirmer</button>
</div>
</div>
{% include "assiduites/widgets/alert.j2" %}
{% include "assiduites/widgets/toast.j2" %}
{% endblock app_content %}