diff --git a/app/api/assiduites.py b/app/api/assiduites.py index 044931b68d..3c5699c3b6 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -599,7 +599,7 @@ def assiduite_edit(assiduite_id: int): moduleimpl: ModuleImpl = None if moduleimpl_id is not False: - if moduleimpl_id is not None: + if moduleimpl_id is not None and moduleimpl_id != "": moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first() if moduleimpl is None: errors.append("param 'moduleimpl_id': invalide") @@ -611,7 +611,7 @@ def assiduite_edit(assiduite_id: int): else: assiduite_unique.moduleimpl_id = moduleimpl_id else: - assiduite_unique.moduleimpl_id = moduleimpl_id + assiduite_unique.moduleimpl_id = None # Cas 3 : desc desc = data.get("desc", False) diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index e2cfe61ebd..d1b7862d31 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -272,9 +272,6 @@ def justif_edit(justif_id: int): if deb is None: errors.append("param 'date_debut': format invalide") - if justificatif_unique.date_fin >= deb: - errors.append("param 'date_debut': date de début située après date de fin ") - # cas 4 : date_fin date_fin = data.get("date_fin", False) if date_fin is not False: @@ -283,13 +280,14 @@ def justif_edit(justif_id: int): fin = scu.is_iso_formated(date_fin.replace(" ", "+"), convert=True) if fin is None: errors.append("param 'date_fin': format invalide") - if justificatif_unique.date_debut <= fin: - errors.append("param 'date_fin': date de fin située avant date de début ") # Mise à jour des dates deb = deb if deb is not None else justificatif_unique.date_debut fin = fin if fin is not None else justificatif_unique.date_fin + if fin <= deb: + errors.append("param 'dates' : Date de début après date de fin") + justificatif_unique.date_debut = deb justificatif_unique.date_fin = fin diff --git a/app/scodoc/sco_archives_justificatifs.py b/app/scodoc/sco_archives_justificatifs.py index 3cad18e96e..576b39473e 100644 --- a/app/scodoc/sco_archives_justificatifs.py +++ b/app/scodoc/sco_archives_justificatifs.py @@ -54,8 +54,8 @@ class Trace: lines: list[str] = [] for fname, traced in self.content.items(): date_fin: datetime or None = traced[1].isoformat() if traced[1] else "None" - - lines.append(f"{fname},{traced[0].isoformat()},{date_fin}") + if traced[0] is not None: + lines.append(f"{fname},{traced[0].isoformat()},{date_fin}") with open(self.path, "w", encoding="utf-8") as file: file.write("\n".join(lines)) diff --git a/app/static/css/assiduites.css b/app/static/css/assiduites.css index 56cef447c7..45b5bcc204 100644 --- a/app/static/css/assiduites.css +++ b/app/static/css/assiduites.css @@ -517,14 +517,25 @@ background-image: url(../icons/filter.svg); } +[name='destroyFile'] { + -webkit-appearance: none; + appearance: none; + cursor: pointer; + background-image: url(../icons/trash.svg); +} + +[name='destroyFile']:checked { + background-image: url(../icons/remove_circle.svg); +} + .icon { display: block; width: 24px; height: 24px; - outline: none; - border: none; + outline: none !important; + border: none !important; cursor: pointer; - margin: 0 2px; + margin: 0 2px !important; } .icon:focus { diff --git a/app/static/icons/remove_circle.svg b/app/static/icons/remove_circle.svg new file mode 100644 index 0000000000..e0c6e0d7b9 --- /dev/null +++ b/app/static/icons/remove_circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/static/icons/trash.svg b/app/static/icons/trash.svg new file mode 100644 index 0000000000..f8aa78561e --- /dev/null +++ b/app/static/icons/trash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/templates/assiduites/pages/ajout_justificatif.j2 b/app/templates/assiduites/pages/ajout_justificatif.j2 index e10efcaf8b..5c001790f7 100644 --- a/app/templates/assiduites/pages/ajout_justificatif.j2 +++ b/app/templates/assiduites/pages/ajout_justificatif.j2 @@ -159,6 +159,7 @@ $.when( requests ).done(() => { + loadAll(); }) } @@ -171,7 +172,6 @@ let couverture = null; createJustificatif(justificatif, (data) => { - console.log(data); if (Object.keys(data.errors).length > 0) { console.error(data.errors); } @@ -179,7 +179,6 @@ couverture = data.success[0].couverture justif_id = data.success[0].justif_id; importFiles(justif_id); - loadAll(); return; } }) diff --git a/app/templates/assiduites/pages/calendrier.j2 b/app/templates/assiduites/pages/calendrier.j2 index cf1ce4618b..73a3cbf425 100644 --- a/app/templates/assiduites/pages/calendrier.j2 +++ b/app/templates/assiduites/pages/calendrier.j2 @@ -246,7 +246,7 @@ day.textContent = `${dayOfWeek} ${dayOfMonth}`; - if (!nonWorkdays.includes(dayOfWeek.toLowerCase())) { + if (!nonWorkdays.includes(dayOfWeek.toLowerCase()) && dayAssiduities.length > 0) { const cache = document.createElement('div') cache.classList.add('dayline'); cache.appendChild( @@ -304,7 +304,6 @@ let dates = getDaysBetweenDates(bornes.deb, bornes.fin); let datesByMonth = organizeByMonth(dates); const justifs = getEtudJustificatifs(bornes.deb, bornes.fin); - console.log(justifs) let assiduitiesByDay = organizeAssiduitiesByDay(datesByMonth, data, justifs); generateCalendar(assiduitiesByDay, nonwork); }); diff --git a/app/templates/assiduites/widgets/alert.j2 b/app/templates/assiduites/widgets/alert.j2 index 55d82aa87c..d0ce3add8f 100644 --- a/app/templates/assiduites/widgets/alert.j2 +++ b/app/templates/assiduites/widgets/alert.j2 @@ -24,7 +24,7 @@ /* Hidden by default */ position: fixed; /* Stay in place */ - z-index: 750; + z-index: 850; /* Sit on top */ padding-top: 100px; /* Location of the box */ diff --git a/app/templates/assiduites/widgets/prompt.j2 b/app/templates/assiduites/widgets/prompt.j2 index 8ef15c16b7..58c2784ec5 100644 --- a/app/templates/assiduites/widgets/prompt.j2 +++ b/app/templates/assiduites/widgets/prompt.j2 @@ -26,7 +26,7 @@ /* Stay in place */ z-index: 750; /* Sit on top */ - padding-top: 100px; + padding-top: 3vh; /* Location of the box */ left: 0; top: 0; @@ -181,8 +181,10 @@ succBtn.classList.add("btnPrompt") succBtn.textContent = "Valider" succBtn.addEventListener('click', () => { - success(); - closePromptModal(); + const retour = success(); + if (retour == null || retour == false || retour == undefined) { + closePromptModal(); + } }) const cancelBtn = document.createElement('button') cancelBtn.classList.add("btnPrompt") diff --git a/app/templates/assiduites/widgets/tableau_assi.j2 b/app/templates/assiduites/widgets/tableau_assi.j2 index 2f995b2ce5..52114f24d3 100644 --- a/app/templates/assiduites/widgets/tableau_assi.j2 +++ b/app/templates/assiduites/widgets/tableau_assi.j2 @@ -39,6 +39,10 @@
+ + \ No newline at end of file diff --git a/app/templates/assiduites/widgets/tableau_base.j2 b/app/templates/assiduites/widgets/tableau_base.j2 index 6e7520b215..5121cb9acc 100644 --- a/app/templates/assiduites/widgets/tableau_base.j2 +++ b/app/templates/assiduites/widgets/tableau_base.j2 @@ -11,6 +11,7 @@ const itemsPerPage = 10; const contextMenu = document.getElementById("contextMenu"); const editOption = document.getElementById("editOption"); + const detailOption = document.getElementById("detailOption"); const deleteOption = document.getElementById("deleteOption"); let selectedRow; @@ -21,14 +22,32 @@ editOption.addEventListener("click", () => { if (selectedRow) { - // Code pour éditer la ligne sélectionnée - console.debug("Éditer :", selectedRow); + const type = selectedRow.getAttribute('type'); + const obj_id = selectedRow.getAttribute('obj_id'); + + if (type == "assiduite") { + editionAssiduites(obj_id); + } else { + editionJustificatifs(obj_id); + } + } + }); + + detailOption.addEventListener("click", () => { + if (selectedRow) { + const type = selectedRow.getAttribute('type'); + const obj_id = selectedRow.getAttribute('obj_id'); + + if (type == "assiduite") { + detailAssiduites(obj_id); + } else { + detailJustificatifs(obj_id); + } } }); deleteOption.addEventListener("click", () => { if (selectedRow) { - // Code pour supprimer la ligne sélectionnée const type = selectedRow.getAttribute('type'); const obj_id = selectedRow.getAttribute('obj_id'); if (type == "assiduite") { @@ -112,33 +131,70 @@ function renderPaginationButtons(array, assi = true) { const totalPages = Math.ceil(array.length / itemsPerPage); - - if (assi) { - paginationContainerAssiduites.innerHTML = "" - } else { - paginationContainerJustificatifs.innerHTML = "" - } - - if (totalPages == 1) { + if (totalPages <= 1) { return; } - for (let i = 1; i <= totalPages; i++) { - const paginationButton = document.createElement("a"); - paginationButton.textContent = i; - paginationButton.classList.add("pagination-button"); - if (assi) { - paginationButton.addEventListener("click", () => { - currentPageAssiduites = i; + if (assi) { + paginationContainerAssiduites.innerHTML = "" + paginationContainerAssiduites.querySelector('#paginationAssi')?.addEventListener('change', (e) => { + currentPageAssiduites = e.target.value; + renderTableAssiduites(currentPageAssiduites, array); + }) + + paginationContainerAssiduites.querySelector('.pagination_moins').addEventListener('click', () => { + if (currentPageAssiduites > 1) { + currentPageAssiduites--; + paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites renderTableAssiduites(currentPageAssiduites, array); - }); - paginationContainerAssiduites.appendChild(paginationButton); + } + }) + + paginationContainerAssiduites.querySelector('.pagination_plus').addEventListener('click', () => { + if (currentPageAssiduites < totalPages) { + currentPageAssiduites++; + paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites + renderTableAssiduites(currentPageAssiduites, array); + } + }) + } else { + paginationContainerJustificatifs.innerHTML = "" + paginationContainerJustificatifs.querySelector('#paginationJusti')?.addEventListener('change', (e) => { + currentPageJustificatifs = e.target.value; + renderTableJustificatifs(currentPageJustificatifs, array); + }) + + paginationContainerJustificatifs.querySelector('.pagination_moins').addEventListener('click', () => { + if (currentPageJustificatifs > 1) { + currentPageJustificatifs--; + paginationContainerJustificatifs.querySelector('#paginationJusti').value = currentPageAssiduites + renderTableJustificatifs(currentPageJustificatifs, array); + } + }) + + paginationContainerJustificatifs.querySelector('.pagination_plus').addEventListener('click', () => { + if (currentPageJustificatifs < totalPages) { + currentPageJustificatifs++; + paginationContainerJustificatifs.querySelector('#paginationJusti').value = currentPageAssiduites + renderTableJustificatifs(currentPageJustificatifs, array); + } + }) + } + + + + + + + for (let i = 1; i <= totalPages; i++) { + const paginationButton = document.createElement("option"); + paginationButton.textContent = i; + paginationButton.value = i; + + if (assi) { + paginationContainerAssiduites.querySelector('#paginationAssi').appendChild(paginationButton) } else { - paginationButton.addEventListener("click", () => { - currentPageJustificatifs = i; - renderTableAssiduites(currentPageJustificatifs, array); - }); - paginationContainerJustificatifs.appendChild(paginationButton); + paginationContainerJustificatifs.querySelector('#paginationJusti').appendChild(paginationButton) } } updateActivePaginationButton(assi); @@ -571,7 +627,13 @@ } } - + function openContext(e) { + e.preventDefault(); + selectedRow = e.target.parentElement; + contextMenu.style.top = `${e.clientY - contextMenu.offsetHeight}px`; + contextMenu.style.left = `${e.clientX}px`; + contextMenu.style.display = "block"; + } @@ -608,7 +670,7 @@ .context-menu { display: none; - position: absolute; + position: fixed; list-style-type: none; padding: 10px 0; background-color: #f9f9f9; @@ -721,4 +783,34 @@ padding: 0; margin: 0; } + + .obj-title { + text-decoration: underline #bbb; + font-weight: bold; + } + + .obj-part { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 33%; + padding: 5px; + border: 1px solid #bbb; + } + + .obj-dates, + .obj-mod, + .obj-rest { + display: flex; + justify-content: space-evenly; + margin: 2px; + } + + .liste_pagination { + display: flex; + justify-content: space-evenly; + align-items: center; + gap: 5px; + } \ No newline at end of file diff --git a/app/templates/assiduites/widgets/tableau_justi.j2 b/app/templates/assiduites/widgets/tableau_justi.j2 index 27dd12e2a5..c1808640ec 100644 --- a/app/templates/assiduites/widgets/tableau_justi.j2 +++ b/app/templates/assiduites/widgets/tableau_justi.j2 @@ -93,19 +93,360 @@ row.appendChild(td) }) - row.addEventListener("contextmenu", (e) => { - e.preventDefault(); - selectedRow = e.target.parentElement; - contextMenu.style.top = `${e.clientY}px`; - contextMenu.style.left = `${e.clientX}px`; - contextMenu.style.display = "block"; - console.log(selectedRow); - }); - + row.addEventListener("contextmenu", openContext); tableBodyJustificatifs.appendChild(row); }); updateActivePaginationButton(false); } + function detailJustificatifs(justi_id) { + const path = getUrl() + `/api/justificatif/${justi_id}`; + async_get( + path, + (data) => { + const user = getUserFromId(data.user_id); + const date_debut = moment.tz(data.date_debut, TIMEZONE).format("DD/MM/YYYY HH:mm"); + const date_fin = moment.tz(data.date_fin, TIMEZONE).format("DD/MM/YYYY HH:mm"); + const entry_date = moment.tz(data.entry_date, TIMEZONE).format("DD/MM/YYYY HH:mm"); - \ No newline at end of file + const etat = data.etat.capitalize(); + const desc = data.raison == null ? "" : data.raison; + const id = data.justif_id; + const fichier = data.fichier != null ? "Oui" : "Non"; + let filenames = [] + if (fichier) { + sync_get(path + "/list", (data2) => { + filenames = data2; + }) + } + + const html = ` +
+
+
+ Date de début + ${date_debut} +
+
+ Date de fin + ${date_fin} +
+
+ Date de saisie + ${entry_date} +
+
+ +
+
+ Raison + ${desc} +
+
+ Etat + ${etat} +
+
+ Créer par + ${user} +
+
+
+
+ Fichier(s) +
+
+
+ Identifiant du justificatif + ${id} +
+
+
+ + ` + + const el = document.createElement('div'); + el.innerHTML = html; + + const fichContent = el.querySelector('#fich-content'); + + filenames.forEach((name) => { + const a = document.createElement('a'); + a.textContent = name + a.classList.add("fich-file") + + a.onclick = () => { downloadFile(id, name) }; + + fichContent.appendChild(a); + }) + + openAlertModal("Détails", el.firstElementChild, null, "green") + } + ) + } + + function downloadFile(id, name) { + const path = getUrl() + `/api/justificatif/${id}/export/${name}`; + + fetch(path, { + method: "POST" + + }) + // This returns a promise inside of which we are checking for errors from the server. + // The catch promise at the end of the call does not getting called when the server returns an error. + // More information about the error catching can be found here: https://www.tjvantoll.com/2015/09/13/fetch-and-errors/. + .then((result) => { + if (!result.ok) { + throw Error(result.statusText); + } + + // We are reading the *Content-Disposition* header for getting the original filename given from the server + const header = result.headers.get('Content-Disposition'); + const parts = header.split(';'); + filename = parts[1].split('=')[1].replaceAll("\"", ""); + + return result.blob(); + }) + // We use the download property for triggering the download of the file from our browser. + // More information about the following code can be found here: https://stackoverflow.com/questions/32545632/how-can-i-download-a-file-using-window-fetch. + // The filename from the first promise is used as name of the file. + .then((blob) => { + if (blob != null) { + var url = window.URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + a.remove(); + } + }) + // The catch is getting called only for client-side errors. + // For example the throw in the first then-promise, which is the error that came from the server. + .catch((err) => { + console.log(err); + }); + } + + function editionJustificatifs(justif_id) { + const path = getUrl() + `/api/justificatif/${justif_id}`; + async_get( + path, + (data) => { + const html = ` +
+
+
+ Date de début + +
+
+ Date de fin + +
+
+
+
+ Etat du justificatif + +
+
+ +
+
+ Raison + +
+
+
+
+
+
+ Importer un fichier + +
+
+
+ ` + + const desc = data.raison + const fichier = data.fichier != null ? "Oui" : "Non"; + + + const el = document.createElement('div') + el.innerHTML = html; + const assiEdit = el.firstElementChild; + + assiEdit.querySelector('#justi_etat').value = data.etat.toLowerCase(); + assiEdit.querySelector('#justi_raison').value = desc != null ? desc : ""; + + assiEdit.querySelector('#justi_date_debut').value = moment.tz(data.date_debut, TIMEZONE).format("YYYY-MM-DDTHH:MM") + assiEdit.querySelector('#justi_date_fin').value = moment.tz(data.date_fin, TIMEZONE).format("YYYY-MM-DDTHH:MM") + + const fichContent = assiEdit.querySelector('.justi-sect'); + + let filenames = [] + if (data.fichier) { + sync_get(path + "/list", (data2) => { + filenames = data2; + }) + + fichContent.insertAdjacentHTML('beforeend', "Fichier(s)") + } + + + filenames.forEach((name) => { + const a = document.createElement('a'); + a.textContent = name + a.classList.add("fich-file") + + a.onclick = () => { downloadFile(id, name) }; + + const input = document.createElement('input') + input.type = "checkbox" + input.name = "destroyFile"; + input.classList.add('icon') + + const span = document.createElement('span'); + span.classList.add('file-line') + span.appendChild(input) + span.appendChild(a) + + + fichContent.appendChild(span); + }) + + openPromptModal("Modification du justificatif", assiEdit, () => { + const prompt = document.querySelector('.assi-edit'); + + let date_debut = prompt.querySelector('#justi_date_debut').value; + let date_fin = prompt.querySelector('#justi_date_fin').value; + + if (date_debut == "" || date_fin == "") { + openAlertModal("Dates erronées", document.createTextNode('Les dates sont invalides')); + return true + } + date_debut = moment.tz(date_debut, TIMEZONE) + date_fin = moment.tz(date_fin, TIMEZONE) + + if (date_debut >= date_fin) { + openAlertModal("Dates erronées", document.createTextNode('La date de fin doit être après la date de début')); + return true + } + + const edit = { + date_debut: date_debut.format(), + date_fin: date_fin.format(), + raison: prompt.querySelector('#justi_raison').value, + etat: prompt.querySelector('#justi_etat').value, + } + + const toRemoveFiles = [...prompt.querySelectorAll('[name="destroyFile"]:checked')] + + if (toRemoveFiles.length > 0) { + removeFiles(justif_id, toRemoveFiles); + } + + const in_files = prompt.querySelector('#justi_fich'); + + if (in_files.files.length > 0) { + importNewFiles(justif_id, in_files); + } + + fullEditJustificatifs(data.justif_id, edit, () => { + loadAll(); + }) + + + }, () => { }, "green"); + } + ); + } + + function fullEditJustificatifs(justif_id, obj, call = () => { }) { + const path = getUrl() + `/api/justificatif/${justif_id}/edit`; + async_post( + path, + obj, + call, + (data, status) => { + //error + console.error(data, status); + } + ); + } + + function removeFiles(justif_id, files = []) { + const path = getUrl() + `/api/justificatif/${justif_id}/remove`; + files = files.map((el) => { + return el.parentElement.querySelector('a').textContent; + }); + + console.log(justif_id, files); + sync_post( + path, + { + "remove": "list", + "filenames": files, + }, + ); + } + + function importNewFiles(justif_id, in_files) { + const path = getUrl() + `/api/justificatif/${justif_id}/import`; + + const requests = [] + Array.from(in_files.files).forEach((f) => { + const fd = new FormData(); + fd.append('file', f); + requests.push( + $.ajax( + { + url: path, + type: 'POST', + data: fd, + dateType: 'json', + contentType: false, + processData: false, + success: () => { }, + } + ) + ) + + }); + + $.when( + requests + ).done(() => { + }) + + } + + + \ No newline at end of file