From 6d2c3f8dcc22fb1c5d24287d9ac7c3a8e738dbd4 Mon Sep 17 00:00:00 2001 From: iziram <matthias.hartmann@iziram.fr> Date: Thu, 15 Jun 2023 17:50:38 +0200 Subject: [PATCH] Assiduites : Page liste - filtrage des tableaux --- app/static/css/assiduites.css | 12 +- app/static/icons/filter.svg | 1 + app/templates/assiduites/liste_assiduites.j2 | 570 +++++++++++++++++-- 3 files changed, 545 insertions(+), 38 deletions(-) create mode 100644 app/static/icons/filter.svg diff --git a/app/static/css/assiduites.css b/app/static/css/assiduites.css index be4a8842c..56cef447c 100644 --- a/app/static/css/assiduites.css +++ b/app/static/css/assiduites.css @@ -510,16 +510,24 @@ } .order { + background-image: url(../icons/sort.svg); +} + +.filter { + background-image: url(../icons/filter.svg); +} + +.icon { display: block; width: 24px; height: 24px; - background-image: url(../icons/sort.svg); outline: none; border: none; cursor: pointer; + margin: 0 2px; } -.order:focus { +.icon:focus { outline: none; border: none; } \ No newline at end of file diff --git a/app/static/icons/filter.svg b/app/static/icons/filter.svg new file mode 100644 index 000000000..8259c6401 --- /dev/null +++ b/app/static/icons/filter.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M4 3h16a1 1 0 011 1v1.586a1 1 0 01-.293.707l-6.415 6.414a1 1 0 00-.292.707v6.305a1 1 0 01-1.243.97l-2-.5a1 1 0 01-.757-.97v-5.805a1 1 0 00-.293-.707L3.292 6.293A1 1 0 013 5.586V4a1 1 0 011-1z" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg> \ No newline at end of file diff --git a/app/templates/assiduites/liste_assiduites.j2 b/app/templates/assiduites/liste_assiduites.j2 index 685e191e5..2fd4daf6c 100644 --- a/app/templates/assiduites/liste_assiduites.j2 +++ b/app/templates/assiduites/liste_assiduites.j2 @@ -7,37 +7,38 @@ <h2>Liste de l'assiduité et des justificatifs de <span class="rouge">{{sco.etud.nomprenom}}</span></h2> <h3>Assiduités :</h3> + <a class="icon filter" onclick="filter()"></a> <table id="assiduiteTable"> <thead> <tr> <th> <div> <span>Début</span> - <a class="order" onclick="order('date_debut', assiduiteCallBack, this)"></a> + <a class="icon order" onclick="order('date_debut', assiduiteCallBack, this)"></a> </div> </th> <th> <div> <span>Fin</span> - <a class="order" onclick="order('date_fin', assiduiteCallBack, this)"></a> + <a class="icon order" onclick="order('date_fin', assiduiteCallBack, this)"></a> </div> </th> <th> <div> <span>État</span> - <a class="order" onclick="order('etat', assiduiteCallBack, this)"></a> + <a class="icon order" onclick="order('etat', assiduiteCallBack, this)"></a> </div> </th> <th> <div> <span>Module</span> - <a class="order" onclick="order('moduleimpl_id', assiduiteCallBack, this)"></a> + <a class="icon order" onclick="order('moduleimpl_id', assiduiteCallBack, this)"></a> </div> </th> <th> <div> <span>Justifiée</span> - <a class="order" onclick="order('est_just', assiduiteCallBack, this)"></a> + <a class="icon order" onclick="order('est_just', assiduiteCallBack, this)"></a> </div> </th> </tr> @@ -48,31 +49,32 @@ <div id="paginationContainerAssiduites" class="pagination-container"> </div> <h3>Justificatifs :</h3> + <a class="icon filter" onclick="filter(false)"></a> <table id="justificatifTable"> <thead> <tr> <th> <div> <span>Début</span> - <a class="order" onclick="order('date_debut', justificatifCallBack, this, false)"></a> + <a class="icon order" onclick="order('date_debut', justificatifCallBack, this, false)"></a> </div> </th> <th> <div> <span>Fin</span> - <a class="order" onclick="order('date_fin', justificatifCallBack, this, false)"></a> + <a class="icon order" onclick="order('date_fin', justificatifCallBack, this, false)"></a> </div> </th> <th> <div> <span>État</span> - <a class="order" onclick="order('etat', justificatifCallBack, this, false)"></a> + <a class="icon order" onclick="order('etat', justificatifCallBack, this, false)"></a> </div> </th> <th> <div> <span>Raison</span> - <a class="order" onclick="order('raison', justificatifCallBack, this, false)"></a> + <a class="icon order" onclick="order('raison', justificatifCallBack, this, false)"></a> </div> </th> </tr> @@ -94,7 +96,7 @@ <style> .pageContent { width: 100%; - max-width: 800px; + max-width: var(--sco-content-max-width); display: flex; flex-direction: column; flex-wrap: wrap; @@ -141,20 +143,20 @@ background-color: #ddd; } - .present { + .l-present { background-color: #9CF1AF; } - .absent, - .invalid { + .l-absent, + .l-invalid { background-color: #F1A69C; } - .valid { + .l-valid { background-color: #8f7eff; } - .retard { + .l-retard { background-color: #F1D99C; } @@ -190,6 +192,47 @@ justify-content: space-between; align-items: center; } + + .filter-head { + display: flex; + flex-wrap: wrap; + gap: 5px; + } + + .filter-line { + display: flex; + justify-content: start; + align-items: center; + margin: 15px; + } + + .filter-line>* { + margin-right: 5px; + } + + + + .rbtn { + width: 35px; + height: 35px; + margin: 0 5px !important; + } + + .f-label { + margin: 0 5px; + } + + .chk { + margin-left: 2px !important; + } + + label { + display: flex; + justify-content: center; + align-items: center; + padding: 0; + margin: 0; + } </style> <script> @@ -202,6 +245,19 @@ let orderAssiduites = true; let orderJustificatifs = true; + let filterAssiduites = { + columns: [ + "entry_date", "date_debut", "date_fin", "etat", "moduleimpl_id", "est_just" + ], + filters: {} + } + let filterJustificatifs = { + columns: [ + "entry_date", "date_debut", "date_fin", "etat", "raison" + ], + filters: {} + } + const tableBodyAssiduites = document.getElementById("tableBodyAssiduites"); const tableBodyJustificatifs = document.getElementById("tableBodyJustificatifs"); @@ -239,10 +295,53 @@ }); function assiduiteCallBack(assi) { + assi = filterArray(assi, filterAssiduites.filters) renderTableAssiduites(currentPageAssiduites, assi); renderPaginationButtons(assi); } + + function filterArray(array, f) { + return array.filter((el) => { + let t = Object.keys(f).every((k) => { + if (k == "etat") { + return f.etat.includes(el.etat.toLowerCase()) + }; + if (k == "est_just") { + if (f.est_just != "") { + return `${el.est_just}` == f.est_just; + } + } + if (k.indexOf('date') != -1) { + const assi_time = moment.tz(el[k], TIMEZONE); + const filter_time = f[k].time; + switch (f[k].pref) { + + case "0": + return assi_time.isSame(filter_time, 'minute'); + case "-1": + return assi_time.isBefore(filter_time, 'minutes'); + case "1": + return assi_time.isAfter(filter_time, 'minutes'); + } + } + + if (k == "moduleimpl_id") { + const m = el[k] == undefined || el[k] == null ? "null" : el[k]; + if (f.moduleimpl_id != '') { + return m == f.moduleimpl_id; + } + } + + return true; + }) + + return t; + + }) + } + function justificatifCallBack(justi) { + justi = filterArray(justi, filterJustificatifs.filters) renderTableJustificatifs(currentPageJustificatifs, justi); renderPaginationButtons(justi, false); } @@ -265,7 +364,37 @@ } + function generateTableHead(columns, assi = true) { + const table = assi ? "#assiduiteTable" : "#justificatifTable" + const call = assi ? [assiduiteCallBack, true] : [justificatifCallBack, false] + const tr = document.querySelector(`${table} thead tr`); + + tr.innerHTML = "" + + columns.forEach((c) => { + const th = document.createElement('th'); + const div = document.createElement('div'); + + const span = document.createElement('span'); + span.textContent = columnTranslator(c); + + const a = document.createElement('a'); + a.classList.add('icon', "order"); + a.onclick = () => { order(c, call[0], a, call[1]) } + + div.appendChild(span) + div.appendChild(a) + + th.appendChild(div); + + tr.appendChild(th); + }) + } + function renderTableAssiduites(page, assiduités) { + + generateTableHead(filterAssiduites.columns, true) + tableBodyAssiduites.innerHTML = ""; const start = (page - 1) * itemsPerPage; const end = start + itemsPerPage; @@ -276,15 +405,21 @@ row.setAttribute('obj_id', assiduite.assiduite_id); const etat = assiduite.etat.toLowerCase(); - row.classList.add(etat); + row.classList.add(`l-${etat}`); + filterAssiduites.columns.forEach((k) => { + const td = document.createElement('td'); + if (k.indexOf('date') != -1) { + td.textContent = moment.tz(assiduite[k], TIMEZONE).format(`DD/MM/Y HH:mm`) + } else if (k.indexOf("module") != -1) { + td.textContent = getModuleImpl(assiduite.moduleimpl_id); + } else if (k.indexOf('est_just') != -1) { + td.textContent = assiduite[k] ? "Oui" : "Non" + } else { + td.textContent = assiduite[k].capitalize() + } - row.innerHTML = ` - <td>${moment.tz(assiduite.date_debut, TIMEZONE).format(`DD/MM/Y HH:mm`)}</td> - <td>${moment.tz(assiduite.date_fin, TIMEZONE).format(`DD/MM/Y HH:mm`)}</td> - <td>${etat}</td> - <td>${getModuleImpl(assiduite.moduleimpl_id)}</td> <td>${assiduite.est_just ? "Oui" : "Non" - }</td> - `; + row.appendChild(td) + }) row.addEventListener("contextmenu", (e) => { e.preventDefault(); @@ -301,6 +436,8 @@ } function renderTableJustificatifs(page, justificatifs) { + generateTableHead(filterJustificatifs.columns, false) + tableBodyJustificatifs.innerHTML = ""; const start = (page - 1) * itemsPerPage; const end = start + itemsPerPage; @@ -313,17 +450,23 @@ const etat = justificatif.etat.toLowerCase(); if (etat == "valide") { - row.classList.add('valid'); + row.classList.add(`l-valid`); + } else { - row.classList.add('invalid') + row.classList.add(`l-invalid`); + } - row.innerHTML = ` - <td>${new Date(justificatif.date_debut).toLocaleString()}</td> - <td>${new Date(justificatif.date_fin).toLocaleString()}</td> - <td>${etat}</td> - <td>${justificatif.raison}</td> - `; + filterJustificatifs.columns.forEach((k) => { + const td = document.createElement('td'); + if (k.indexOf('date') != -1) { + td.textContent = moment.tz(justificatif[k], TIMEZONE).format(`DD/MM/Y HH:mm`) + } else { + td.textContent = justificatif[k].capitalize() + } + + row.appendChild(td) + }) row.addEventListener("contextmenu", (e) => { e.preventDefault(); @@ -403,7 +546,7 @@ } function order(keyword, callback = () => { }, el, assi = true) { - const call = (array) => { + const call = (array, ordered) => { const sorted = array.sort((a, b) => { let keyValueA = a[keyword]; let keyValueB = b[keyword]; @@ -420,26 +563,381 @@ let orderDertermined = keyValueA > keyValueB; - if (el.classList.contains("desc")) { + if (!ordered) { orderDertermined = keyValueA < keyValueB; } return orderDertermined }); - el.classList.toggle("desc"); + callback(sorted); }; if (assi) { - getAllAssiduitesFromEtud(etudid, call) + orderAssiduites = !orderAssiduites; + getAllAssiduitesFromEtud(etudid, (a) => { call(a, orderAssiduites) }) } else { - getAllJustificatifsFromEtud(etudid, call) + orderJustificatifs = !orderJustificatifs; + getAllJustificatifsFromEtud(etudid, (a) => { call(a, orderJustificatifs) }) } } + function filter(assi = true) { + if (assi) { + let html = ` + <div class="filter-body"> + <h3>Affichage des colonnes:</h3> + <div class="filter-head"> + <label> + Date de saisie + <input class="chk" type="checkbox" name="entry_date" id="entry_date"> + </label> + <label> + Date de Début + <input class="chk" type="checkbox" name="date_debut" id="date_debut" checked> + </label> + <label> + Date de Fin + <input class="chk" type="checkbox" name="date_fin" id="date_fin" checked> + </label> + <label> + Etat + <input class="chk" type="checkbox" name="etat" id="etat" checked> + </label> + <label> + Module + <input class="chk" type="checkbox" name="moduleimpl_id" id="moduleimpl_id" checked> + </label> + <label> + Justifiée + <input class="chk" type="checkbox" name="est_just" id="est_just" checked> + </label> + </div> + <hr> + <h3>Filtrage des colonnes:</h3> + <span class="filter-line"> + <span class="filter-title" for="entry_date">Date de saisie</span> + <select name="entry_date_pref" id="entry_date_pref"> + <option value="-1">Avant</option> + <option value="0">Égal</option> + <option value="1">Après</option> + </select> + <input type="datetime-local" name="entry_date_time" id="entry_date_time"> + </span> + <span class="filter-line"> + <span class="filter-title" for="date_debut">Date de début</span> + <select name="date_debut_pref" id="date_debut_pref"> + <option value="-1">Avant</option> + <option value="0">Égal</option> + <option value="1">Après</option> + </select> + <input type="datetime-local" name="date_debut_time" id="date_debut_time"> + </span> + <span class="filter-line"> + <span class="filter-title" for="date_fin">Date de fin</span> + <select name="date_fin_pref" id="date_fin_pref"> + <option value="-1">Avant</option> + <option value="0">Égal</option> + <option value="1">Après</option> + </select> + <input type="datetime-local" name="date_fin_time" id="date_fin_time"> + </span> + <span class="filter-line"> + <span class="filter-title" for="etat">Etat</span> + <input checked type="checkbox" name="etat_present" id="etat_present" class="rbtn present" value="present"> + <input checked type="checkbox" name="etat_retard" id="etat_retard" class="rbtn retard" value="retard"> + <input checked type="checkbox" name="etat_absent" id="etat_absent" class="rbtn absent" value="absent"> + </span> + <span class="filter-line"> + <span class="filter-title" for="moduleimpl_id">Module</span> + <select id="moduleimpl_id"> + <option value="">Pas de filtre</option> + </select> + </span> + <span class="filter-line"> + <span class="filter-title" for="est_just">Est Justifiée</span> + <select id="est_just"> + <option value="">Pas de filtre</option> + <option value="true">Oui</option> + <option value="false">Non</option> + </select> + </span> + </div> + `; + const span = document.createElement('span'); + span.innerHTML = html + html = span.firstElementChild + + const filterHead = html.querySelector('.filter-head'); + filterHead.innerHTML = "" + let cols = ["entry_date", "date_debut", "date_fin", "etat", "moduleimpl_id", "est_just"]; + + cols.forEach((k) => { + const label = document.createElement('label') + label.classList.add('f-label') + const s = document.createElement('span'); + s.textContent = columnTranslator(k); + + + const input = document.createElement('input'); + input.classList.add('chk') + input.type = "checkbox" + input.name = k + input.id = k; + input.checked = filterAssiduites.columns.includes(k) + + label.appendChild(s) + label.appendChild(input) + filterHead.appendChild(label) + }) + + const sl = html.querySelector('.filter-line #moduleimpl_id'); + let opts = [] + Object.keys(moduleimpls).forEach((k) => { + const opt = document.createElement('option'); + opt.value = k == null ? "null" : k; + opt.textContent = moduleimpls[k]; + opts.push(opt); + }) + + opts = opts.sort((a, b) => { + return a.value < b.value + }) + + sl.append(...opts); + + // Mise à jour des filtres + + Object.keys(filterAssiduites.filters).forEach((key) => { + const l = html.querySelector(`.filter-title[for="${key}"]`).parentElement; + if (key.indexOf('date') != -1) { + l.querySelector(`#${key}_pref`).value = filterAssiduites.filters[key].pref; + l.querySelector(`#${key}_time`).value = filterAssiduites.filters[key].time.format("YYYY-MM-DDTHH:mm"); + + } else if (key.indexOf('etat') != -1) { + l.querySelectorAll('input').forEach((e) => { + e.checked = filterAssiduites.filters[key].includes(e.value) + }) + } else if (key.indexOf("module") != -1) { + l.querySelector('#moduleimpl_id').value = filterAssiduites.filters[key]; + } else if (key.indexOf("est_just") != -1) { + l.querySelector('#est_just').value = filterAssiduites.filters[key]; + } + }) + + openPromptModal("Filtrage des assiduités", html, () => { + + const columns = [...document.querySelectorAll('.chk')] + .map((el) => { if (el.checked) return el.id }) + .filter((el) => el) + + filterAssiduites.columns = columns + filterAssiduites.filters = {} + //reste des filtres + + const lines = [...document.querySelectorAll('.filter-line')]; + + lines.forEach((l) => { + const key = l.querySelector('.filter-title').getAttribute('for'); + + if (key.indexOf('date') != -1) { + const pref = l.querySelector(`#${key}_pref`).value; + const time = l.querySelector(`#${key}_time`).value; + if (l.querySelector(`#${key}_time`).value != "") { + filterAssiduites.filters[key] = { + pref: pref, + time: new moment.tz(time, TIMEZONE) + } + } + } else if (key.indexOf('etat') != -1) { + filterAssiduites.filters[key] = [...l.querySelectorAll("input:checked")].map((e) => e.value); + } else if (key.indexOf("module") != -1) { + filterAssiduites.filters[key] = l.querySelector('#moduleimpl_id').value; + } else if (key.indexOf("est_just") != -1) { + filterAssiduites.filters[key] = l.querySelector('#est_just').value; + } + }) + + + getAllAssiduitesFromEtud(etudid, assiduiteCallBack) + + }, () => { }, "#7059FF"); + } else { + let html = ` + <div class="filter-body"> + <h3>Affichage des colonnes:</h3> + <div class="filter-head"> + <label> + Date de saisie + <input class="chk" type="checkbox" name="entry_date" id="entry_date"> + </label> + <label> + Date de Début + <input class="chk" type="checkbox" name="date_debut" id="date_debut" checked> + </label> + <label> + Date de Fin + <input class="chk" type="checkbox" name="date_fin" id="date_fin" checked> + </label> + <label> + Etat + <input class="chk" type="checkbox" name="etat" id="etat" checked> + </label> + <label> + Raison + <input class="chk" type="checkbox" name="raison" id="raison" checked> + </label> + </div> + <hr> + <h3>Filtrage des colonnes:</h3> + <span class="filter-line"> + <span class="filter-title" for="entry_date">Date de saisie</span> + <select name="entry_date_pref" id="entry_date_pref"> + <option value="-1">Avant</option> + <option value="0">Égal</option> + <option value="1">Après</option> + </select> + <input type="datetime-local" name="entry_date_time" id="entry_date_time"> + </span> + <span class="filter-line"> + <span class="filter-title" for="date_debut">Date de début</span> + <select name="date_debut_pref" id="date_debut_pref"> + <option value="-1">Avant</option> + <option value="0">Égal</option> + <option value="1">Après</option> + </select> + <input type="datetime-local" name="date_debut_time" id="date_debut_time"> + </span> + <span class="filter-line"> + <span class="filter-title" for="date_fin">Date de fin</span> + <select name="date_fin_pref" id="date_fin_pref"> + <option value="-1">Avant</option> + <option value="0">Égal</option> + <option value="1">Après</option> + </select> + <input type="datetime-local" name="date_fin_time" id="date_fin_time"> + </span> + <span class="filter-line"> + <span class="filter-title" for="etat">Etat</span> + <label> + Valide + <input checked type="checkbox" name="etat_valide" id="etat_valide" class="" value="valide"> + </label> + <label> + Non Valide + <input checked type="checkbox" name="etat_valide" id="etat_valide" class="" value="non_valide"> + </label> + <label> + En Attente + <input checked type="checkbox" name="etat_valide" id="etat_valide" class="" value="attente"> + </label> + <label> + Modifié + <input checked type="checkbox" name="etat_valide" id="etat_valide" class="" value="modifie"> + </label> + </span> + </div> + `; + const span = document.createElement('span'); + span.innerHTML = html + html = span.firstElementChild + + const filterHead = html.querySelector('.filter-head'); + filterHead.innerHTML = "" + let cols = ["entry_date", "date_debut", "date_fin", "etat", "raison"]; + + cols.forEach((k) => { + const label = document.createElement('label') + label.classList.add('f-label') + const s = document.createElement('span'); + s.textContent = columnTranslator(k); + + + const input = document.createElement('input'); + input.classList.add('chk') + input.type = "checkbox" + input.name = k + input.id = k; + input.checked = filterJustificatifs.columns.includes(k) + + label.appendChild(s) + label.appendChild(input) + filterHead.appendChild(label) + }) + + // Mise à jour des filtres + + Object.keys(filterJustificatifs.filters).forEach((key) => { + const l = html.querySelector(`.filter-title[for="${key}"]`).parentElement; + if (key.indexOf('date') != -1) { + l.querySelector(`#${key}_pref`).value = filterJustificatifs.filters[key].pref; + l.querySelector(`#${key}_time`).value = filterJustificatifs.filters[key].time.format("YYYY-MM-DDTHH:mm"); + + } else if (key.indexOf('etat') != -1) { + l.querySelectorAll('input').forEach((e) => { + e.checked = filterJustificatifs.filters[key].includes(e.value) + }) + } + }) + + openPromptModal("Filtrage des Justificatifs", html, () => { + + const columns = [...document.querySelectorAll('.chk')] + .map((el) => { if (el.checked) return el.id }) + .filter((el) => el) + + filterJustificatifs.columns = columns + filterJustificatifs.filters = {} + //reste des filtres + + const lines = [...document.querySelectorAll('.filter-line')]; + + lines.forEach((l) => { + const key = l.querySelector('.filter-title').getAttribute('for'); + + if (key.indexOf('date') != -1) { + const pref = l.querySelector(`#${key}_pref`).value; + const time = l.querySelector(`#${key}_time`).value; + if (l.querySelector(`#${key}_time`).value != "") { + filterJustificatifs.filters[key] = { + pref: pref, + time: new moment.tz(time, TIMEZONE) + } + } + } else if (key.indexOf('etat') != -1) { + filterJustificatifs.filters[key] = [...l.querySelectorAll("input:checked")].map((e) => e.value); + } + }) + + + getAllJustificatifsFromEtud(etudid, justificatifCallBack) + + }, () => { }, "#7059FF"); + } + } + + function columnTranslator(colName) { + switch (colName) { + case "date_debut": + return "Début"; + case "entry_date": + return "Saisie le"; + case "date_fin": + return "Fin"; + case "etat": + return "État"; + case "moduleimpl_id": + return "Module"; + case "est_just": + return "Justifiée"; + case "raison": + return "Raison"; + } + } + window.onload = () => { loadAll(); }