diff --git a/app/static/css/assiduites.css b/app/static/css/assiduites.css index 4fe7ee090b..c935f4f839 100644 --- a/app/static/css/assiduites.css +++ b/app/static/css/assiduites.css @@ -36,10 +36,10 @@ .infos { position: relative; - width: fit-content; display: flex; - justify-content: space-evenly; + justify-content: start; align-content: center; + gap: 10px; } #datestr { @@ -48,7 +48,7 @@ border: 1px #444 solid; border-radius: 5px; padding: 5px; - min-width: 100px; + min-width: 250px; display: inline-block; min-height: 20px; } diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index 0c8edc6102..a58592cc23 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -112,6 +112,7 @@ function validateSelectors(btn) { getAssiduitesFromEtuds(true); document.querySelector(".selectors").disabled = true; + $("#tl_date").datepicker("option", "disabled", true); generateMassAssiduites(); generateAllEtudRow(); btn.remove(); @@ -173,6 +174,7 @@ function uniqueCheckBox(box) { * @param {CallableFunction} errors fonction à effectuer en cas d'échec */ function sync_get(path, success, errors) { + //TODO Optimiser : rendre asynchrone + sans jquery console.log("sync_get " + path); $.ajax({ async: false, @@ -188,16 +190,21 @@ function sync_get(path, success, errors) { * @param {CallableFunction} success fonction à effectuer en cas de succès * @param {CallableFunction} errors fonction à effectuer en cas d'échec */ -function async_get(path, success, errors) { +async function async_get(path, success, errors) { console.log("async_get " + path); - $.ajax({ - async: true, - type: "GET", - url: path, - success: success, - error: errors, - }); + try { + const response = await fetch(path); + if (response.ok) { + const data = await response.json(); + success(data); + } else { + throw new Error("Network response was not ok."); + } + } catch (error) { + errors(error); + } } + /** * Fait une requête POST de façon synchrone * @param {String} path adresse distante @@ -206,6 +213,7 @@ function async_get(path, success, errors) { * @param {CallableFunction} errors fonction à effectuer en cas d'échec */ function sync_post(path, data, success, errors) { + //TODO Optimiser : rendre asynchrone + sans jquery console.log("sync_post " + path); $.ajax({ async: false, @@ -223,17 +231,28 @@ function sync_post(path, data, success, errors) { * @param {CallableFunction} success fonction à effectuer en cas de succès * @param {CallableFunction} errors fonction à effectuer en cas d'échec */ -function async_post(path, data, success, errors) { - console.log("sync_post " + path); - return $.ajax({ - async: true, - type: "POST", - url: path, - data: JSON.stringify(data), - success: success, - error: errors, - }); +async function async_post(path, data, success, errors) { + console.log("async_post " + path); + try { + const response = await fetch(path, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); + + if (response.ok) { + const responseData = await response.json(); + success(responseData); + } else { + throw new Error("Network response was not ok."); + } + } catch (error) { + errors(error); + } } + // <<== Gestion des actions de masse ==>> const massActionQueue = new Map(); @@ -583,10 +602,13 @@ function formatDate(date, styles = { dateStyle: "full" }) { */ function updateDate() { const dateInput = document.querySelector("#tl_date"); + let date = $(dateInput).datepicker("getDate"); + + if (date == null) { + date = new Date(Date.fromFRA(dateInput.value)); + } - const date = dateInput.valueAsDate ?? new Date(); let dateStr = ""; - if (!verifyNonWorkDays(date.getDay(), nonWorkDays)) { dateStr = formatDate(date).capitalize(); } else { @@ -606,7 +628,11 @@ function updateDate() { ) ); openAlertModal("Attention", div, "", "#eec660"); - dateInput.value = lastWorkDay.toISOString().split("T")[0]; + $(dateInput).datepicker( + "setDate", + Date.toFRA(lastWorkDay.toIsoUtcString().split("T")[0]) + ); + dateStr = formatDate(lastWorkDay).capitalize(); } document.querySelector("#datestr").textContent = dateStr; @@ -639,15 +665,15 @@ function setupDate(onchange = null) { const input = document.querySelector("#tl_date"); datestr.addEventListener("click", () => { - if (!input.disabled) { + if (!document.querySelector(".selectors").disabled) { try { - input.showPicker(); + document.querySelector(".infos .ui-datepicker-trigger").click(); } catch {} } }); if (onchange != null) { - input.addEventListener("change", onchange); + $(input).change(onchange); } } @@ -726,7 +752,7 @@ function getTimeLineTimes() { //getPeriodValues() -> retourne la position de la timeline [a,b] avec a et b des number let values = getPeriodValues(); //On récupère la date - const dateiso = document.querySelector("#tl_date").value; + const dateiso = getDate().format("YYYY-MM-DD"); //On génère des objets temps values = values.map((el) => { @@ -757,9 +783,10 @@ function isConflictSameAsPeriod(conflict, period = undefined) { * @returns {Date} la date sélectionnée */ function getDate() { - const date = new Date(document.querySelector("#tl_date").value); - date.setHours(0, 0, 0, 0); - return date; + const date = + $("#tl_date").datepicker("getDate") ?? + new Date(Date.fromFRA(document.querySelector("#tl_date").value)); + return date.startOf("day"); } /** @@ -768,10 +795,7 @@ function getDate() { */ function getNextDate() { const date = getDate(); - const next = new Date(date.valueOf()); - next.setDate(date.getDate() + 1); - next.setHours(0, 0, 0, 0); - return next; + return date.clone().add(1, "days"); } /** * Retourne un objet date représentant le jour précédent @@ -779,10 +803,7 @@ function getNextDate() { */ function getPrevDate() { const date = getDate(); - const next = new Date(date.valueOf()); - next.setDate(date.getDate() - 1); - next.setHours(0, 0, 0, 0); - return next; + return date.clone().add(-1, "days"); } /** @@ -790,31 +811,6 @@ function getPrevDate() { * @param {Date} date * @returns {string} la date iso avec le timezone */ -function toIsoString(date) { - var tzo = -date.getTimezoneOffset(), - dif = tzo >= 0 ? "+" : "-", - pad = function (num) { - return (num < 10 ? "0" : "") + num; - }; - - return ( - date.getFullYear() + - "-" + - pad(date.getMonth() + 1) + - "-" + - pad(date.getDate()) + - "T" + - pad(date.getHours()) + - ":" + - pad(date.getMinutes()) + - ":" + - pad(date.getSeconds()) + - dif + - pad(Math.floor(Math.abs(tzo) / 60)) + - ":" + - pad(Math.abs(tzo) % 60) - ); -} /** * Transforme un temps numérique en une date @@ -823,7 +819,7 @@ function toIsoString(date) { */ function numberTimeToDate(nb) { time = toTime(nb).replace("h", ":"); - date = document.querySelector("#tl_date").value; + date = getDate().format("YYYY-MM-DD"); datetime = `${date}T${time}`; @@ -843,8 +839,8 @@ function numberTimeToDate(nb) { function getAssiduitesFromEtuds(clear, deb, fin) { const etudIds = Object.keys(etuds).join(","); - const date_debut = deb ? deb : toIsoString(getPrevDate()); - const date_fin = fin ? fin : toIsoString(getNextDate()); + const date_debut = deb ? deb : getPrevDate().toIsoUtcString(); + const date_fin = fin ? fin : getNextDate().toIsoUtcString(); if (clear) { assiduites = {}; @@ -1137,8 +1133,8 @@ function getAssiduiteValue(field) { * @param {String | Number} etudid identifiant de l'étudiant */ function actualizeEtudAssiduite(etudid) { - const date_debut = toIsoString(getPrevDate()); - const date_fin = toIsoString(getNextDate()); + const date_debut = getPrevDate().toIsoUtcString(); + const date_fin = getNextDate().toIsoUtcString(); const url_api = getUrl() + diff --git a/app/static/js/date_utils.js b/app/static/js/date_utils.js index 486de19ee6..4a1d21d98b 100644 --- a/app/static/js/date_utils.js +++ b/app/static/js/date_utils.js @@ -1,3 +1,53 @@ +/** + * Transforme une date du format français (DD/MM/YYYY) au format iso (YYYY-MM-DD) + * Exemple d'utilisation : + * new Date(Date.fromFRA("30/06/2024")) -> new Date("2024-06-30") + * @param {string} dateFra + * @returns {string} dateIso + */ +Date.fromFRA = function (dateFra) { + if (dateFra == "") return ""; + // Expression régulière pour valider le format de date ISO (YYYY-MM-DD) + const regexDateFra = /^(0[1-9]|[12][0-9]|3[01])\/(0[1-9]|1[0-2])\/\d{4}$/; + + // Vérification du format de la date ISO + if (!regexDateFra.test(dateFra)) { + throw new Error( + `La date (format français) passée en paramètre [${dateFra}] n'est pas valide.` + ); + } + + // Conversion du format français (DD/MM/YYYY) au format ISO (YYYY-MM-DD) + return `${dateFra.substring(6, 10)}-${dateFra.substring( + 3, + 5 + )}-${dateFra.substring(0, 2)}`; +}; +/** + * Transforme une date du format iso (YYYY-MM-DD) au format français (DD/MM/YYYY) + * Exemple d'utilisation : + * Date.toFRA("2024-06-30") -> "30/06/2024" + * @param {string} dateIso + * @returns {string} dateFra + */ +Date.toFRA = function (dateIso) { + if (dateIso == "") return ""; + // Expression régulière pour valider le format de date ISO (YYYY-MM-DD) + const regexDateIso = /^\d{4}-(0\d|1[0-2])-([0-2]\d|3[01])$/; + + // Vérification du format de la date ISO + if (!regexDateIso.test(dateIso)) { + throw new Error( + `La date ISO passée en paramètre [${dateIso}] n'est pas valide.` + ); + } + + // Conversion du format ISO (YYYY-MM-DD) en format français (DD/MM/YYYY) + return `${dateIso.substring(8, 10)}/${dateIso.substring( + 5, + 7 + )}/${dateIso.substring(0, 4)}`; +}; Object.defineProperty(Date.prototype, "isValid", { value: function () { return !Number.isNaN(this.getTime()); @@ -175,6 +225,7 @@ Object.defineProperty(Date.prototype, "clone", { Object.defineProperty(Date.prototype, "format", { value: function (formatString) { + let iso = this.toIsoUtcString(); switch (formatString) { case "DD/MM/Y HH:mm": return this.toLocaleString("fr-FR", { @@ -196,10 +247,11 @@ Object.defineProperty(Date.prototype, "format", { }); case "YYYY-MM-DDTHH:mm": - let iso = this.toIsoUtcString(); // slice : YYYY-MM-DDTHH // slice + 3 : YYYY-MM-DDTHH:mm return iso.slice(0, iso.indexOf(":") + 3); + case "YYYY-MM-DD": + return iso.slice(0, iso.indexOf("T")); default: return this.toIsoUtcString(); } diff --git a/app/templates/assiduites/pages/bilan_etud.j2 b/app/templates/assiduites/pages/bilan_etud.j2 index 304adf2218..a1c3d0df44 100644 --- a/app/templates/assiduites/pages/bilan_etud.j2 +++ b/app/templates/assiduites/pages/bilan_etud.j2 @@ -12,10 +12,10 @@

Statistiques d'assiduité

- - + +
@@ -87,20 +87,20 @@ function stats() { const dd_val = document.getElementById('stats_date_debut').value; const df_val = document.getElementById('stats_date_fin').value; - - if (dd_val == "" || df_val == "") { + let date_debut = new Date(Date.fromFRA(dd_val)); + let date_fin = new Date(Date.fromFRA(df_val)); + if (dd_val == "" || df_val == "" || !date_debut.isValid() || !date_debut.isValid()) { openAlertModal("Dates invalides", document.createTextNode('Les dates sélectionnées sont invalides')); return; } - const date_debut = new Date(dd_val + "T00:00"); - const date_fin = new Date(df_val + "T23:59"); + date_debut = date_debut.startOf("day") + date_fin = date_fin.endOf("day") - if (date_debut.valueOf() > date_fin.valueOf()) { + if (date_debut.isAfter(date_fin)) { openAlertModal("Dates invalides", document.createTextNode('La date de début se situe après la date de fin.')); return; } - countAssiduites(date_debut.toIsoUtcString(), date_fin.toIsoUtcString()) } diff --git a/app/templates/assiduites/pages/signal_assiduites_etud.j2 b/app/templates/assiduites/pages/signal_assiduites_etud.j2 index 78f45d7890..f611c3cbe8 100644 --- a/app/templates/assiduites/pages/signal_assiduites_etud.j2 +++ b/app/templates/assiduites/pages/signal_assiduites_etud.j2 @@ -9,7 +9,7 @@
Date: - +
{{timeline|safe}} diff --git a/app/templates/assiduites/pages/signal_assiduites_group.j2 b/app/templates/assiduites/pages/signal_assiduites_group.j2 index 5a7c09fecb..9ce8182938 100644 --- a/app/templates/assiduites/pages/signal_assiduites_group.j2 +++ b/app/templates/assiduites/pages/signal_assiduites_group.j2 @@ -20,7 +20,8 @@
Groupes : {{grp|safe}}
Date : - +
diff --git a/app/templates/assiduites/pages/visu_assi.j2 b/app/templates/assiduites/pages/visu_assi.j2 index f947928f33..71585f8d73 100644 --- a/app/templates/assiduites/pages/visu_assi.j2 +++ b/app/templates/assiduites/pages/visu_assi.j2 @@ -1,8 +1,8 @@ {% extends "sco_page.j2" %} {% block scripts %} - {{ super() }} - +{{ super() }} + {% endblock %} {% block app_content %} @@ -10,10 +10,10 @@

Visualisation de l'assiduité {{gr_tit|safe}}

- - + + {{scu.ICON_XLS|safe}} @@ -21,8 +21,8 @@ {{tableau | safe}} -
-Les comptes sont exprimés en {{ assi_metric | lower}}s. +
+ Les comptes sont exprimés en {{ assi_metric | lower}}s.
-{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/app/templates/assiduites/widgets/conflict.j2 b/app/templates/assiduites/widgets/conflict.j2 index 57a74da3db..73709d9b76 100644 --- a/app/templates/assiduites/widgets/conflict.j2 +++ b/app/templates/assiduites/widgets/conflict.j2 @@ -258,7 +258,7 @@ const success = () => { const separatorTime = document.getElementById("promptTime").value; const dateString = - document.querySelector("#tl_date").value + `T${separatorTime}`; + getDate().format("YYYY-MM-DD") + `T${separatorTime}`; const separtorDate = new Date(dateString); const assiduite_debut = new Date(this.selectedAssiduite.date_debut); diff --git a/app/templates/assiduites/widgets/minitimeline.j2 b/app/templates/assiduites/widgets/minitimeline.j2 index 3fd5f5d674..36d03a3d60 100644 --- a/app/templates/assiduites/widgets/minitimeline.j2 +++ b/app/templates/assiduites/widgets/minitimeline.j2 @@ -13,7 +13,8 @@ */ function createMiniTimeline(assiduitesArray, day = null) { const array = [...assiduitesArray]; - const dateiso = day == null ? document.getElementById("tl_date").value : day; + + const dateiso = day == null ? getDate().format("YYYY-MM-DD") : day; const timeline = document.createElement("div"); timeline.className = "mini-timeline"; if (isSingleEtud()) { diff --git a/app/templates/assiduites/widgets/moduleimpl_dynamic_selector.j2 b/app/templates/assiduites/widgets/moduleimpl_dynamic_selector.j2 index 1a5c5a2d55..b82f655fcb 100644 --- a/app/templates/assiduites/widgets/moduleimpl_dynamic_selector.j2 +++ b/app/templates/assiduites/widgets/moduleimpl_dynamic_selector.j2 @@ -93,9 +93,10 @@ function updateSelect(moduleimpl_id, query = "#moduleimpl_select", dateIso = null) { let sem = getEtudFormSemestres() - if (dateIso == null) { - dateIso = document.querySelector("#tl_date").value + if (!dateIso) { + dateIso = getDate().format("YYYY-MM-DD") } + sem = filterFormSemestres(sem, dateIso) const mod = getModulesImplByFormsemestre(sem) populateSelect(mod, moduleimpl_id, query); diff --git a/app/views/assiduites.py b/app/views/assiduites.py index ab2feec05d..65e9735fe3 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -25,6 +25,7 @@ ############################################################################## import datetime +import re from flask import g, request, render_template, flash from flask import abort, url_for, redirect @@ -319,7 +320,7 @@ def signal_assiduites_etud(): render_template( "assiduites/pages/signal_assiduites_etud.j2", sco=ScoData(etud), - date=date, + date=_dateiso_to_datefr(date), morning=morning, lunch=lunch, timeline=_timeline(heures=",".join([f"'{s}'" for s in heures])), @@ -425,8 +426,8 @@ def bilan_etud(): ) # Gestion des dates du bilan (par défaut l'année scolaire) - date_debut: str = f"{scu.annee_scolaire()}-09-01" - date_fin: str = f"{scu.annee_scolaire()+1}-06-30" + date_debut: str = f"01/09/{scu.annee_scolaire()}" + date_fin: str = f"30/06/{scu.annee_scolaire()+1}" # Récupération de la métrique d'assiduité assi_metric = scu.translate_assiduites_metric( @@ -697,7 +698,7 @@ def signal_assiduites_group(): "assiduites/pages/signal_assiduites_group.j2", gr_tit=gr_tit, sem=sem["titre_num"], - date=date, + date=_dateiso_to_datefr(date), formsemestre_id=formsemestre_id, grp=sco_groups_view.menu_groups_choice(groups_infos), moduleimpl_select=_module_selector(formsemestre, moduleimpl_id), @@ -843,7 +844,7 @@ def visu_assiduites_group(): "assiduites/pages/signal_assiduites_group.j2", gr_tit=gr_tit, sem=sem["titre_num"], - date=date, + date=_dateiso_to_datefr(date), formsemestre_id=formsemestre_id, grp=sco_groups_view.menu_groups_choice(groups_infos), moduleimpl_select=_module_selector(formsemestre, moduleimpl_id), @@ -1015,8 +1016,8 @@ def visu_assi_group(): inverse=False, short=False, ), - date_debut=dates["debut"], - date_fin=dates["fin"], + date_debut=_dateiso_to_datefr(dates["debut"]), + date_fin=_dateiso_to_datefr(dates["debut"]), gr_tit=gr_tit, group_ids=request.args.get("group_ids", None), sco=ScoData(formsemestre=groups_infos.get_formsemestre()), @@ -1318,6 +1319,31 @@ def test(): # --- Fonctions internes --- +def _dateiso_to_datefr(date_iso: str) -> str: + """ + _dateiso_to_datefr Transforme une date iso en date format français + + Args: + date_iso (str): date au format iso (YYYY-MM-DD) + + Raises: + ValueError: Si l'argument `date_iso` n'est pas au bon format + + Returns: + str: date au format français (DD/MM/YYYY) + """ + + regex_date_iso: str = r"^\d{4}-([0]\d|1[0-2])-([0-2]\d|3[01])$" + + # Vérification de la date_iso + if not re.match(regex_date_iso, date_iso): + raise ValueError( + f"La dateiso passée en paramètre [{date_iso}] n'est pas valide." + ) + + return f"{date_iso[8:10]}/{date_iso[5:7]}/{date_iso[0:4]}" + + def _get_date_str(deb: datetime.datetime, fin: datetime.datetime) -> str: """ _get_date_str transforme une période en chaîne lisible