From d73b92500672b7c93422f1930ab693606f8a0286 Mon Sep 17 00:00:00 2001
From: Iziram <matthias.hartmann@iziram.fr>
Date: Tue, 28 May 2024 20:07:25 +0200
Subject: [PATCH] =?UTF-8?q?Assiduit=C3=A9=20:=20signal=5Fassiduites=5Fhedb?=
 =?UTF-8?q?o=20:=20v1=20OK?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/static/js/date_utils.js                   |  20 +
 .../pages/signal_assiduites_hebdo.j2          | 445 ++++++++++++++++--
 app/views/assiduites.py                       |  40 ++
 3 files changed, 462 insertions(+), 43 deletions(-)

diff --git a/app/static/js/date_utils.js b/app/static/js/date_utils.js
index 0db9d3b0..efdc120e 100644
--- a/app/static/js/date_utils.js
+++ b/app/static/js/date_utils.js
@@ -430,3 +430,23 @@ class Duration {
 function hasTimeConflict(period, interval) {
   return period.deb.isBefore(interval.fin) && period.fin.isAfter(interval.deb);
 }
+
+// Fonction auxiliaire pour obtenir le numéro de semaine ISO d'une date donnée
+function getISOWeek(date) {
+  const target = new Date(date.valueOf());
+  const dayNr = (date.getUTCDay() + 6) % 7;
+  target.setUTCDate(target.getUTCDate() - dayNr + 3);
+  const firstThursday = target.valueOf();
+  target.setUTCMonth(0, 1);
+  if (target.getUTCDay() !== 4) {
+    target.setUTCMonth(0, 1 + ((4 - target.getUTCDay() + 7) % 7));
+  }
+  return 1 + Math.ceil((firstThursday - target) / 604800000);
+}
+
+// Fonction auxiliaire pour obtenir le nombre de semaines ISO dans une année donnée
+function getISOWeeksInYear(year) {
+  const date = new Date(year, 11, 31);
+  const week = getISOWeek(date);
+  return week === 1 ? getISOWeek(new Date(year, 11, 24)) : week;
+}
diff --git a/app/templates/assiduites/pages/signal_assiduites_hebdo.j2 b/app/templates/assiduites/pages/signal_assiduites_hebdo.j2
index 874f463b..5b2a7f28 100644
--- a/app/templates/assiduites/pages/signal_assiduites_hebdo.j2
+++ b/app/templates/assiduites/pages/signal_assiduites_hebdo.j2
@@ -7,8 +7,9 @@
 
 <style>
     .rbtn::before {
-        width: 30px;
-        height: 30px;
+        --size: 1.5em;
+        width: var(--size);
+        height: var(--size);
     }
 
     .ui-timepicker-container,
@@ -62,6 +63,7 @@
         width: 100%;
         max-width: 1600px;
         position: relative;
+        table-layout: fixed;
     }
 
     th,
@@ -99,6 +101,13 @@
         opacity: 0.5;
     }
 
+    .grayed {
+        filter: brightness(0.5);
+    }
+    .conflit {
+        background-color: var(--color-conflit);
+    }
+
 
 </style>
 
@@ -159,6 +168,10 @@
 #confirmButton:hover {
     background-color: var(--color-secondary);
 }
+
+.etudinfo{
+    text-align: left;
+}
 </style>
 {% endblock styles %}
 
@@ -172,37 +185,359 @@
 
 <script>
 
+    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(),
+        }
+
+        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);
+                            erreurModuleImpl(data.errors["0"].message);
+
+                        }
+                    },
+                    (error) => {
+                        console.error("Erreur lors de la suppression de l'assiduité", error);
+
+                    }
+                );
+            } 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);
+                    }
+                );
+            }
+        } 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);
+
+                    }
+                },
+                (error) => {
+                    console.error("Erreur lors de la création de l'assiduité", error);
+
+                }
+            );
+        }
+
+    }
+
+    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) {
+
+        // 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
+                boutons = `
+                    <input type="checkbox" name="matin-${etudid}" id="matin-${etudid}" 
+                    class="rbtn present" value="present">
+                    <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">
+                    `
+
+                // 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();
+                        tdMatin.querySelector(`[value="${etat}"]`).checked = true;
+                        tdMatin.setAttribute("assiduite_id", assi.assiduite_id);
+                    } else {
+                        tdMatin.innerHTML = ""
+                        tdMatin.classList.add("conflit");
+                        tdMatin.title = "Des assiduités existent déjà pour cette période"
+                        tdMatin.setAttribute("data-tooltip", "");
+                    }
+                }
+
+                // 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();
+                        tdApresmidi.querySelector(`[value="${etat}"]`).checked = true;
+                        tdApresmidi.setAttribute("assiduite_id", assi.assiduite_id);
+                    } else {
+                        tdApresmidi.innerHTML = ""
+                        tdApresmidi.classList.add("conflit");
+                        tdApresmidi.title = "Des assiduités existent déjà pour cette période"
+                        tdApresmidi.setAttribute("data-tooltip", "");
+                    }
+                }
+
+            });
+        }
+
+        document.querySelectorAll("td .rbtn").forEach((el) => {
+            el.addEventListener("click", (e) => {
+                let target = e.target;
+                let parent = target.parentElement;
+                let inputs = parent.querySelectorAll(".rbtn");
+                inputs.forEach((input) => {
+                    if (input != target) {
+                        input.checked = false;
+                    }
+                });
+                actionButton(target, !target.checked);
+            });
+        });
+
+        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 inputs = document.querySelectorAll(`td[day="${day}"][time="${time}"] .rbtn[value="present"]`);
+        inputs.forEach((input) => {
+            input.click();
+        });
+    }
+
+</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: "09:00",
-            fin: "12:00"
+            debut: "{{ scu.get_assiduites_time_config("assi_morning_time") }}",
+            fin: "{{ scu.get_assiduites_time_config("assi_lunch_time") }}"
         },
         apresmidi: {
-            debut: "13:00",
-            fin: "17:00"
+            debut: "{{ scu.get_assiduites_time_config("assi_lunch_time") }}",
+            fin: "{{ scu.get_assiduites_time_config("assi_afternoon_time") }}",
         }
     }
 
-    document.querySelectorAll(".rbtn").forEach((el)=>{
-        el.addEventListener("click", (e)=>{
-            let target = e.target;
-            let parent = target.parentElement;
-            let inputs = parent.querySelectorAll(".rbtn");
-
-            inputs.forEach((input)=>{
-                if (input != target){
-                    input.checked = false;
-                }
-            });
-        });
-    });
+    
 
     document.getElementById("text-matin").addEventListener("click", (e)=>{
         e.preventDefault();
@@ -283,10 +618,19 @@ document.addEventListener("DOMContentLoaded", ()=>{
             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 %}
@@ -298,48 +642,63 @@ document.addEventListener("DOMContentLoaded", ()=>{
 <h2>Signalement hebdomadaire de l'assiduité {{ gr | safe }}</h2>
 <br>
 <div id="actions" class="flex">
-    <button>Semaine précédente</button>
+    <button onclick="changeWeek(true)">Semaine précédente</button>
     <label for="moduleimpl_select">
         Module:
         {{moduleimpl_select | safe}}
     </label>
-    <button>Semaine suivante</button>
+    <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>
 
-<table>
+<table id="table">
     <thead>
         <tr class="premier">
             <th rowspan="2">Étudiants</th>
-            <th colspan="2">Lundi</th>
-            <th colspan="2">Mardi</th>
-            <th colspan="2">Mercredi</th>
-            <th colspan="2">Jeudi</th>
-            <th colspan="2">Vendredi</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">
-            <th>Matin</th>
-            <th>Après-midi</th>
-            <th>Matin</th>
-            <th>Après-midi</th>
-            <th>Matin</th>
-            <th>Après-midi</th>
-            <th>Matin</th>
-            <th>Après-midi</th>
-            <th>Matin</th>
-            <th>Après-midi</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 ''}}">Matin</th>
+            <th class="{{'grayed' if jour[0] else ''}}">Après-midi</th>
+            {% endif %}
+            {% endfor %}
+        </tr>
+        <tr>
+            {# Ne pas afficher si preference "non presences" #}
+            <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>
     </thead>
     <tbody>
         {% for etud in etudiants %}
-        <tr>
+        <tr etudid="{{etud.etudid}}" id="row-{{etud.etudid}}">
             <td class="etudinfo" id="etud-{{etud.etudid}}">{{ etud.nomprenom }}</td>
-            {#  à changer par jour travaillés (sco pref) #}
-            {% for day in ['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi'] %}
-            <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">
@@ -348,8 +707,7 @@ document.addEventListener("DOMContentLoaded", ()=>{
                 <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>
-            {% endfor %}
+            </td> #}
         </tr>
         {% endfor %}
     </tbody>
@@ -373,4 +731,5 @@ document.addEventListener("DOMContentLoaded", ()=>{
 </div>
 
 {% include "assiduites/widgets/alert.j2" %}
+{% include "assiduites/widgets/toast.j2" %}
 {% endblock app_content %}
\ No newline at end of file
diff --git a/app/views/assiduites.py b/app/views/assiduites.py
index 2dcfec2e..f84330b6 100644
--- a/app/views/assiduites.py
+++ b/app/views/assiduites.py
@@ -2072,6 +2072,45 @@ def signal_assiduites_hebdo():
 
     # TODO vérif perm AbsChange -> readonly
 
+    # Gestion des jours
+    jours: dict[str, list[str]] = {
+        "lun": [
+            "Lundi",
+            datetime.datetime.strptime(week + "-1", "%G-W%V-%u").strftime("%d/%m/%Y"),
+        ],
+        "mar": [
+            "Mardi",
+            datetime.datetime.strptime(week + "-2", "%G-W%V-%u").strftime("%d/%m/%Y"),
+        ],
+        "mer": [
+            "Mercredi",
+            datetime.datetime.strptime(week + "-3", "%G-W%V-%u").strftime("%d/%m/%Y"),
+        ],
+        "jeu": [
+            "Jeudi",
+            datetime.datetime.strptime(week + "-4", "%G-W%V-%u").strftime("%d/%m/%Y"),
+        ],
+        "ven": [
+            "Vendredi",
+            datetime.datetime.strptime(week + "-5", "%G-W%V-%u").strftime("%d/%m/%Y"),
+        ],
+        "sam": [
+            "Samedi",
+            datetime.datetime.strptime(week + "-6", "%G-W%V-%u").strftime("%d/%m/%Y"),
+        ],
+        "dim": [
+            "Dimanche",
+            datetime.datetime.strptime(week + "-7", "%G-W%V-%u").strftime("%d/%m/%Y"),
+        ],
+    }
+
+    non_travail = sco_preferences.get_preference("non_travail")
+    non_travail = non_travail.replace(" ", "").split(",")
+
+    hebdo_jours: list[tuple[bool, str]] = []
+    for key, val in jours.items():
+        hebdo_jours.append((key in non_travail, val))
+
     return render_template(
         "assiduites/pages/signal_assiduites_hebdo.j2",
         gr=gr_tit,
@@ -2079,6 +2118,7 @@ def signal_assiduites_hebdo():
         moduleimpl_select=_module_selector(
             formsemestre=formsemestre, moduleimpl_id=moduleimpl_id
         ),
+        hebdo_jours=hebdo_jours,
     )
 
 
-- 
GitLab