diff --git a/ecole/.gitignore b/ecole/.gitignore index 30818331..316c585e 100644 --- a/ecole/.gitignore +++ b/ecole/.gitignore @@ -4,6 +4,9 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +# Classique +package-lock.json + # Logs logs/ *.log diff --git a/ecole/public/css/global.css b/ecole/public/css/global.css index 5237957d..ae5e0839 100644 --- a/ecole/public/css/global.css +++ b/ecole/public/css/global.css @@ -102,6 +102,13 @@ hr.form { font-size: smaller; } +.helper-text.error { + font-weight: bold; + text-align: center; + margin-top: 20px; + white-space: pre-line; +} + .mrg { padding: 0 5% !important; } @@ -444,4 +451,64 @@ a.grille-garanties:hover{ .brand-logo img { display: none; } +} + +/* Overlay loader */ +#loader-overlay { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background: linear-gradient( + rgba(10, 20, 60, 0.2), + rgba(0, 0, 0, 0.4) + ); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 9999; + font-family: 'Roboto', sans-serif; + opacity: 0; + backdrop-filter: blur(0px); + pointer-events: none; + transition: opacity 0.5s ease, backdrop-filter 0.5s ease; +} + +#loader-overlay.active { + opacity: 1; + backdrop-filter: blur(3px); + pointer-events: all; +} + +#loader-overlay.hidden { + display: none; +} + +.loader-spin-wrap { + opacity: 0; + transform: translateY(10px); + transition: opacity 0.5s ease, transform 0.5s ease; + transition-delay: 0.5s; +} + +#loader-overlay.active .loader-spin-wrap { + opacity: 1; + transform: translateY(0); +} + +/* Spinner circulaire */ +.loader-spin { + width: 50px; + aspect-ratio: 1; + border-radius: 50%; + mask:1; + background: + radial-gradient(farthest-side,darkblue 94%,transparent) top/8px 8px no-repeat, + conic-gradient(transparent 30%,darkblue); + -webkit-mask: radial-gradient(farthest-side,transparent calc(100% - 8px),#000 0); + animation: l13 1s infinite linear; +} + +@keyframes l13 { + 100% { transform: rotate(1turn); } } \ No newline at end of file diff --git a/ecole/public/css/historiqueParcours.css b/ecole/public/css/historiqueParcours.css index 30e8257e..cab3900f 100644 --- a/ecole/public/css/historiqueParcours.css +++ b/ecole/public/css/historiqueParcours.css @@ -42,99 +42,25 @@ table.dataTable thead th>div { left: 0 !important; } -#historiqueParcours_filter { - margin-bottom: 20px; +#historiqueParcours_filter label { + display: flex; + align-items: center; } -.dataTables_wrapper .dataTables_filter { - position: relative; - text-align: left; - float: left; - padding: 2px; - overflow: visible; -} - -/* Cacher complètement le label "Rechercher" de DataTables */ -.dataTables_wrapper .dataTables_filter label { - position: relative; - display: inline-block; - margin: 0; - font-size: 0 !important; - line-height: 0 !important; - overflow: visible; -} - -.dataTables_wrapper .dataTables_filter label > span:first-child { - display: none !important; -} - -/* fond blanc, bordure grise fine, capsule arrondie */ .dataTables_wrapper .dataTables_filter input[type="search"] { - background-color: #fff; - border: 1px solid #e0e0e0; - border-radius: 42px; + background-color: transparent; + border: none; + border-bottom: 1px solid #26a69a; + border-radius: 0; outline: none; - height: 42px; - width: 220px; - font-size: 15px; - margin: 0; - padding: 0 20px 0 52px; - box-sizing: border-box; - transition: all 0.2s ease; - color: #333; - position: relative; -} - -.dataTables_wrapper .dataTables_filter input[type="search"]:focus { - background-color: #fff; - border-color: #1d9bf0; - box-shadow: 0 0 0 2px #1d9bf0; - color: #333; -} - -.dataTables_wrapper .dataTables_filter input[type="search"]::placeholder { - color: #71767a; -} - -/* Icône de loupe SVG */ -.dataTables_wrapper .dataTables_filter label::before { - content: ""; - position: absolute; - left: 20px; - top: 50%; - transform: translateY(-50%); - width: 18px; - height: 18px; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2371767a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.35-4.35'/%3E%3C/svg%3E"); - background-size: contain; - background-repeat: no-repeat; - background-position: center; - pointer-events: none; - z-index: 1; -} - -/* Texte "Rechercher" à côté de l'icône */ -.dataTables_wrapper .dataTables_filter label::after { - content: "Rechercher"; - position: absolute; - left: 52px; - top: 50%; - transform: translateY(-50%); - font-size: 15px; - color: #71767a; - pointer-events: none; - z-index: 1; - white-space: nowrap; -} - -/* Cacher le texte "Rechercher" quand on tape, focus, ou si l'input a une valeur */ -.dataTables_wrapper .dataTables_filter:focus-within label::after, -.dataTables_wrapper .dataTables_filter label.has-value::after { - opacity: 0; -} - -.dataTables_wrapper .dataTables_filter input[type="search"]:focus::placeholder { - color: transparent; + height: 3rem; + width: 100%; + font-size: 16px; + margin: 0 0 8px 0; + padding: 0; + box-shadow: none; + box-sizing: content-box; + transition: box-shadow .3s, border .3s, -webkit-box-shadow .3s; } #historiqueParcours_length>label { @@ -157,7 +83,7 @@ table.dataTable thead th>div { width: 60px; } -/* Style Input recherche par ligne */ +/* Style Input search by row */ #historiqueParcours>thead>tr:nth-child(2)>th>input { font-size: 13px !important; padding: 6px !important; @@ -179,7 +105,7 @@ table.dataTable thead .sorting_desc:before { content: ""; } -/* boutons de navigation */ +/* boutons de navigationw */ .dataTables_wrapper .dataTables_paginate .paginate_button { background-color: white !important; border: darkblue solid 1.5px !important; @@ -219,31 +145,10 @@ td.nc-value { } #divToggleSearch { + width: 300px; grid-column: 2; grid-row: 3; justify-self: center; - white-space: nowrap; -} - -#toggleSearch { - white-space: nowrap; - width: auto; - min-width: fit-content; - padding: 0 24px; - color: white !important; - background-color: darkblue !important; - border: none !important; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(16, 0, 75, 0.2), 0 4px 8px rgba(16, 0, 75, 0.1); - transition: background-color 0.3s; -} - -#toggleSearch:hover { - background-color: #26a69a !important; -} - -#toggleSearch:active { - background-color: gray !important; } #divExtractAll { @@ -300,15 +205,4 @@ td.nc-value { #checkRegionAdmin label { display: inline-block; -} - -#historiqueParcours tr.shown > td { background: #fffdf5; } - .parcours-details { font-size: 0.95rem; } - - -/* Style pour les boutons d'export désactivés */ -#divBtnFilter button:disabled { - opacity: 0.5 !important; - cursor: not-allowed !important; - pointer-events: none !important; -} +} \ No newline at end of file diff --git a/ecole/public/css/loader.css b/ecole/public/css/loader.css new file mode 100644 index 00000000..d303b759 --- /dev/null +++ b/ecole/public/css/loader.css @@ -0,0 +1,66 @@ +/* Overlay loader (css pris du site https://css-loaders.com/)*/ +#loader-overlay { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background: linear-gradient( + rgba(10, 20, 60, 0.2), + rgba(0, 0, 0, 0.4) + ); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 9999; + font-family: 'Roboto', sans-serif; + opacity: 0; + backdrop-filter: blur(0px); + pointer-events: none; + transition: opacity 0.5s ease, backdrop-filter 0.5s ease; +} +#loader-overlay.active { + opacity: 1; + backdrop-filter: blur(3px); + pointer-events: all; +} +#loader-overlay.hidden { + display: none; +} + +/* Spinner wrapper (fade/slide) */ +.loader-spin-wrap { + opacity: 0; + transform: translateY(10px); + transition: opacity 0.5s ease, transform 0.5s ease; + transition-delay: 0.5s; /* apparaît après 0.5s */ +} +#loader-overlay.active .loader-spin-wrap { + opacity: 1; + transform: translateY(0); +} + +/* Spinner circulaire */ +.loader-spin { + width: 50px; + aspect-ratio: 1; + border-radius: 50%; + mask:1; + background: + radial-gradient(farthest-side,darkblue 94%,transparent) top/8px 8px no-repeat, + conic-gradient(transparent 30%,darkblue); + -webkit-mask: radial-gradient(farthest-side,transparent calc(100% - 8px),#000 0); + animation: l13 1s infinite linear; +} +@keyframes l13 { + 100% { transform: rotate(1turn); } +} + +/* Erreur */ +#error-message { + display: none; + color: red; + font-weight: bold; + text-align: center; + margin-top: 20px; + white-space: pre-line; +} diff --git a/ecole/public/js/global.js b/ecole/public/js/global.js index 3c65822d..0d3b119a 100644 --- a/ecole/public/js/global.js +++ b/ecole/public/js/global.js @@ -160,235 +160,4 @@ async function loadContrat(idContrat) { } catch (error) { console.error("Erreur lors de la récupération des informations contrat :", error); } -} - -// ========== Fonctions utilitaires génériques ========== - -/** - * Formatage des dates (ISO vers format français) - * @param {string} iso - Date au format ISO - * @param {boolean} withTime - Inclure l'heure (défaut: true) - * @returns {string} Date formatée (dd/mm/yyyy ou dd/mm/yyyy hh:mm) - */ -function fmtDate(iso, withTime = true) { - // Si la valeur est null, undefined, ou vide - if (!iso || (typeof iso === 'string' && iso.trim() === "")) return "NC"; - - // Convertir en string si ce n'est pas déjà le cas - const dateStr = String(iso).trim(); - - // Vérifier les valeurs invalides connues - if (dateStr === "00/00/0000" || dateStr === "00/00" || dateStr === "null" || dateStr === "undefined") { - return "NC"; - } - - let d; - - // Si c'est déjà au format jj/mm/aaaa (format français) - if (dateStr.includes("/") && dateStr.split("/").length === 3) { - const parts = dateStr.split("/"); - const day = parseInt(parts[0], 10); - const month = parseInt(parts[1], 10); - const year = parseInt(parts[2], 10); - - // Vérifier si les valeurs sont valides - if (isNaN(day) || isNaN(month) || isNaN(year)) { - return "NC"; - } - - // Si le jour ou le mois est 00, considérer comme invalide - if (day === 0 || month === 0) { - return "NC"; - } - - // Si le mois est invalide - if (month < 1 || month > 12) { - return "NC"; - } - - // Si l'année est 0000, afficher juste jj/mm (sans l'année) - if (year === 0) { - return `${String(day).padStart(2, "0")}/${String(month).padStart(2, "0")}`; - } - - // Si l'année est valide, créer une vraie date et valider - const monthIndex = month - 1; // Les mois commencent à 0 - d = new Date(year, monthIndex, day); - // Vérifier si la date est valide (ex: 31/02/2000 serait invalide) - if (d.getDate() !== day || d.getMonth() !== monthIndex || d.getFullYear() !== year) { - return "NC"; - } - - // Formater la date - const dd = String(d.getDate()).padStart(2, "0"); - const mm = String(d.getMonth() + 1).padStart(2, "0"); - const yyyy = d.getFullYear(); - if (!withTime) return `${dd}/${mm}/${yyyy}`; - const hh = String(d.getHours()).padStart(2, "0"); - const mi = String(d.getMinutes()).padStart(2, "0"); - return `${dd}/${mm}/${yyyy} ${hh}:${mi}`; - } - // Si c'est au format jj/mm (pour date d'échéance) - else if (dateStr.includes("/") && dateStr.split("/").length === 2) { - const parts = dateStr.split("/"); - const day = parseInt(parts[0], 10); - const month = parseInt(parts[1], 10); - - // Pour l'échéance, on retourne juste jj/mm - if (isNaN(day) || isNaN(month) || day === 0 || month === 0 || month > 12) { - return "NC"; - } - return `${String(day).padStart(2, "0")}/${String(month).padStart(2, "0")}`; - } - // Sinon, essayer de parser comme date ISO - else { - d = new Date(dateStr); - if (isNaN(d.getTime())) return "NC"; - - const dd = String(d.getDate()).padStart(2, "0"); - const mm = String(d.getMonth() + 1).padStart(2, "0"); - const yyyy = d.getFullYear(); - if (!withTime) return `${dd}/${mm}/${yyyy}`; - const hh = String(d.getHours()).padStart(2, "0"); - const mi = String(d.getMinutes()).padStart(2, "0"); - return `${dd}/${mm}/${yyyy} ${hh}:${mi}`; - } -} - -/** - * Crée un élément key-value pour l'affichage de détails - * @param {string} label - Libellé - * @param {*} value - Valeur (booléen converti en Oui/Non) - * @returns {string} HTML formaté - */ -function kv(label, value) { - const v = (value === true) ? "Oui" : (value === false) ? "Non" : (value ?? "NC"); - return `
-
${label} :
-
${v}
-
`; -} - -/** - * Crée une grille à 2 colonnes pour l'affichage de détails - * @param {string} innerLeft - Contenu colonne gauche - * @param {string} innerRight - Contenu colonne droite - * @returns {string} HTML formaté - */ -function gridWrap2cols(innerLeft, innerRight) { - return `
${innerLeft}${innerRight}
`; -} - -/** - * Fonction debounce pour limiter la fréquence d'exécution d'une fonction - * Utile pour les recherches en temps réel et éviter les appels API excessifs - * @param {Function} fn - Fonction à débouncer - * @param {number} delay - Délai en millisecondes (défaut: 300ms) - * @returns {Function} Fonction débouncée avec méthode cancel() - */ -function debounce(fn, delay = 300) { - let t; - function wrapped(...args) { - clearTimeout(t); - t = setTimeout(() => fn(...args), delay); - } - wrapped.cancel = () => clearTimeout(t); - return wrapped; -} - -/** - * Décode un token JWT et retourne le payload - * @param {string} token - Token JWT à décoder - * @returns {Object|null} Payload décodé ou null en cas d'erreur - */ -function parseJwt(token) { - try { - const base64Url = token.split(".")[1]; - const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); - const jsonPayload = decodeURIComponent( - atob(base64) - .split("") - .map(function (c) { - return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2); - }) - .join("") - ); - return JSON.parse(jsonPayload); - } catch (error) { - console.error("Erreur lors du décodage du token:", error); - return null; - } -} - -/** - * Affiche un message d'erreur dans l'élément avec l'ID "error" - * @param {string} message - Message d'erreur à afficher - */ -function displayError(message) { - const errorElement = document.getElementById("error"); - if (errorElement) { - errorElement.textContent = message; - errorElement.style.display = "block"; - } -} - -/** - * Crée un message formaté avec différents types (info, warn, error, dev) - * @param {string} type - Type de message : 'info', 'warn', 'error', 'dev' - * @param {string} title - Titre du message - * @param {string} description - Description du message (peut contenir du HTML) - * @returns {string} HTML du message formaté - */ -function createMessageBox(type, title, description) { - // Protection contre les paramètres invalides - if (!type || typeof type !== 'string') type = 'info'; - if (!title || typeof title !== 'string') title = 'Message'; - if (!description || typeof description !== 'string') description = ''; - - const configs = { - info: { - icon: 'fa-info-circle', - bgColor: '#e3f2fd', - borderColor: '#2196f3', - textColor: '#1565c0' - }, - warn: { - icon: 'fa-exclamation-triangle', - bgColor: '#fff3e0', - borderColor: '#ff9800', - textColor: '#e65100' - }, - error: { - icon: 'fa-times-circle', - bgColor: '#ffebee', - borderColor: '#f44336', - textColor: '#c62828' - }, - dev: { - icon: 'fa-tools', - bgColor: '#fff3cd', - borderColor: '#ffc107', - textColor: '#856404' - } - }; - - const config = configs[type] || configs.info; - - // Échappement basique pour éviter les problèmes (description peut contenir du HTML valide) - const safeTitle = String(title).replace(//g, '>'); - // Description peut contenir du HTML (comme
), donc on ne l'échappe pas complètement - // mais on s'assure qu'elle est une string - const safeDescription = String(description); - - return ` -
-

- - ${safeTitle} -

-

- ${safeDescription} -

-
- `; } \ No newline at end of file diff --git a/ecole/public/js/historiqueParcours.js b/ecole/public/js/historiqueParcours.js index 4f64b3c7..964fde60 100644 --- a/ecole/public/js/historiqueParcours.js +++ b/ecole/public/js/historiqueParcours.js @@ -1,562 +1,325 @@ -// public/js/historiqueParcours.js - document.addEventListener("DOMContentLoaded", async function () { - // Récupération du token + // Fetch data from the server + //// parse TOken const token = localStorage.getItem("jwtToken"); if (!token) { throw new Error("Aucun token trouvé dans le localStorage."); } - + const userData = parseJwt(token); if (!userData) { displayError("Erreur lors de l'extraction des données utilisateur à partir du token."); + return; } - // Initialiser DataTables en mode server-side (obligé pour pagination) - const table = initServerSideDataTable(); + const { userAuthGroupe, userMatricule } = userData; + const isAdmin = userAuthGroupe === "ADMIN"; + const matriculeUser = userMatricule; - // Variable pour suivre l'état des exports - let isExporting = false; + let regionUser; + let tableData = []; - // Fonction pour désactiver/activer les boutons d'export - function setExportButtonsState(disabled) { - isExporting = disabled; - const buttons = ["#exportCSV", "#exportCSVFilter", "#exportXlxs", "#exportXlxsFilter"]; - buttons.forEach(selector => { - const $btn = $(selector); - $btn.prop("disabled", disabled); - if (disabled) { - $btn.css("opacity", "0.5"); - $btn.css("cursor", "not-allowed"); - } else { - $btn.css("opacity", "1"); - $btn.css("cursor", "pointer"); - } - }); + const checkAdmin = document.querySelector('#checkRegionAdmin'); + + if (isAdmin) { + checkAdmin.style.display = "flex"; } - // Exports CSV/XLSX - $("#exportCSV").on("click", function () { - if (isExporting) return; - - const dt = $("#historiqueParcours").DataTable(); - if (!dt) { - displayError("Impossible d'accéder à la table de données."); - return; + try { + const userResponse = await fetchUserDetails(matriculeUser); + regionUser = userResponse?.user["@expand"].region?.nom || null; + } catch (error) { + displayError("Erreur lors de la récupération des données utilisateur."); + + return; + } + + const checkboxWrappers = Array.from(document.querySelectorAll('[class^="checkbox-wrapper-"]')); + const checkboxes = checkboxWrappers.map(wrapper => wrapper.querySelector('input[type="checkbox"]')); + const regions = checkboxWrappers.map(wrapper => wrapper.querySelector('.checkboxRegion').textContent); + + // Initialize checkboxes + checkboxes.forEach((checkbox, index) => { + if (regions[index] === regionUser) { + checkbox.checked = true; } + }); + + // Fetch initial data + try { + const response = await fetch(`/historiqueParcours/${regionUser}`); + const dataResponse = await response.json(); - const settings = dt.settings()[0]; - if (!settings || !settings.aoColumns) { - displayError("Structure de données invalide."); - return; + if (dataResponse.valid) { + tableData = dataResponse.data; + populateParcoursTable(tableData); + } else { + displayError("Erreur lors de la récupération des parcours"); } + } catch (error) { + displayError("Failed to fetch data. Please try again later."); + } - setExportButtonsState(true); + // Add event listeners to checkboxes + checkboxes.forEach((checkbox, index) => { + checkbox.addEventListener('change', async (e) => { + const region = regions[index]; - const payload = { - mode: "full", // export total - search: { value: "" }, - columns: settings.aoColumns.map((c, i) => ({ - data: i, - search: { value: "" } - })), - // on garde l'ordre actuel pour la récuperation - order: (dt.order() || []).map(([col, dir]) => ({ column: col || 0, dir: dir || "asc" })) - }; + if (checkbox.checked) { + try { + const response = await fetch(`/historiqueParcours/${region}`); + const dataResponse = await response.json(); - fetch("/historiqueParcours/export/csv", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }) - .then(resp => { - if (!resp.ok) throw new Error("Export CSV (complet) impossible"); - - return resp.blob(); - }) - .then(blob => { - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = "historique_parcours_complet.csv"; - document.body.appendChild(a); - a.click(); - URL.revokeObjectURL(url); - a.remove(); - setExportButtonsState(false); - }) - .catch((err) => { - displayError("Export CSV (complet) impossible"); - setExportButtonsState(false); + if (dataResponse.valid) { + tableData.push(...dataResponse.data); + populateParcoursTable(tableData); + } else { + displayError("Erreur lors de la récupération des parcours"); + } + } catch (error) { + displayError("Failed to fetch data. Please try again later."); + } + } else { + removeRegionFromTableData(region); + populateParcoursTable(tableData); + } }); }); - - $("#exportCSVFilter").on("click", function () { - if (isExporting) return; - - const dt = $("#historiqueParcours").DataTable(); - if (!dt) { - displayError("Impossible d'accéder à la table de données."); - return; - } - - const settings = dt.settings()[0]; - if (!settings || !settings.aoColumns) { - displayError("Structure de données invalide."); - return; - } - - setExportButtonsState(true); - - const payload = { - mode: "filtered", // export avec les filtres/colonnes/tri actuels - search: { value: dt.search() || "" }, // recherche globale - columns: settings.aoColumns.map((c, i) => ({ - data: i, - search: { value: dt.column(i).search() || "" } // filtres par colonne - })), - order: (dt.order() || []).map(([col, dir]) => ({ column: col || 0, dir: dir || "asc" })) - }; - - fetch("/historiqueParcours/export/csv", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }) - .then(resp => { - if (!resp.ok) throw new Error("Export CSV (filtré) impossible"); - return resp.blob(); - }) - .then(blob => { - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = "historique_parcours_filtre.csv"; - document.body.appendChild(a); - a.click(); - URL.revokeObjectURL(url); - a.remove(); - setExportButtonsState(false); - }) - .catch((err) => { - displayError("Export CSV (filtré) impossible"); - setExportButtonsState(false); - }); - }); - - - $("#exportXlxs").on("click", function () { - if (isExporting) return; - - const dt = $("#historiqueParcours").DataTable(); - if (!dt) { - displayError("Impossible d'accéder à la table de données."); - return; - } - - const settings = dt.settings()[0]; - if (!settings || !settings.aoColumns) { - displayError("Structure de données invalide."); - return; - } - - setExportButtonsState(true); - - const payload = { - mode: "full", - search: { value: "" }, - columns: settings.aoColumns.map((c, i) => ({ data: i, search: { value: "" } })), - order: (dt.order() || []).map(([col, dir]) => ({ column: col || 0, dir: dir || "asc" })) - }; - - fetch("/historiqueParcours/export/xls", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }) - .then(resp => { if (!resp.ok) throw new Error(); return resp.blob(); }) - .then(blob => { - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; a.download = "historique_parcours_complet.xls"; - document.body.appendChild(a); a.click(); - URL.revokeObjectURL(url); a.remove(); - setExportButtonsState(false); - }) - .catch((err) => { - displayError("Export XLS (complet) impossible"); - setExportButtonsState(false); - }); - }); - - - $("#exportXlxsFilter").on("click", function () { - if (isExporting) return; - - const dt = $("#historiqueParcours").DataTable(); - if (!dt) { - displayError("Impossible d'accéder à la table de données."); - return; - } - - const settings = dt.settings()[0]; - if (!settings || !settings.aoColumns) { - displayError("Structure de données invalide."); - return; - } - - setExportButtonsState(true); - - const payload = { - mode: "filtered", // export avec les filtres/colonnes/tri actuels - search: { value: dt.search() || "" }, // recherche globale - columns: settings.aoColumns.map((c, i) => ({ - data: i, - search: { value: dt.column(i).search() || "" } // filtres par colonne - })), - order: (dt.order() || []).map(([col, dir]) => ({ column: col || 0, dir: dir || "asc" })) - }; - - fetch("/historiqueParcours/export/xls", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }) - .then(resp => { - if (!resp.ok) throw new Error("Export XLS (filtré) impossible"); - return resp.blob(); - }) - .then(blob => { - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = "historique_parcours_filtre.xls"; - document.body.appendChild(a); - a.click(); - URL.revokeObjectURL(url); - a.remove(); - setExportButtonsState(false); - }) - .catch((err) => { - displayError("Export XLS (filtré) impossible"); - setExportButtonsState(false); - }); - }); - - - // Délégation pour la génération de projet - $("#historiqueParcours").on("click", "button#btnGenerate", function () { - const numParcours = $(this).data("num-parcours"); - const produit = $(this).data("produit"); - generateProject(numParcours, produit); - }); + const removeRegionFromTableData = (region) => { + tableData = tableData.filter(item => item["@expand"]?.dernierUtilisateur?.["@expand"]?.region?.nom !== (region === regionUser ? regionUser : region)); + }; }); -/* ========================= - * Helpers spécifiques server-side - * ========================= */ +const removeRegionFromTableData = (region) => { + if (region === regionUser) { + tableData = tableData.filter(item => item["@expand"]?.dernierUtilisateur?.["@expand"]?.region?.nom !== regionUser); + } else { + tableData = tableData.filter(item => item["@expand"]?.dernierUtilisateur?.["@expand"]?.region?.nom !== region); + } +}; -// Initialisation DataTables en server-side (recherche globale + par colonnes + tri + pagination) -function initServerSideDataTable() { - let inflightController = null; +async function fetchUserDetails(matriculeUser) { + try { + const response = await fetch(`/user/read/matricule/${matriculeUser}`); + const data = await response.json(); + + return data.valid ? data : null; + } catch (error) { + displayError(`Erreur lors de la récupération du contrat avec le matricule ${matriculeUser} :`, error); + + return null; + } +} +function populateParcoursTable(parcoursData) { + //initialise const table = $("#historiqueParcours").DataTable({ - processing: true, - serverSide: true, searching: true, paging: true, orderCellsTop: true, fixedHeader: true, - responsive: { details: false }, - pageLength: 10, + responsive: true, + pageLength: 5, retrieve: true, + columnDefs: [ + { + type: "date-uk", + targets: 10 + }, + { + type: "date-eu", + targets: 4 + }, + ], order: [[0, "desc"]], - searchDelay: 350, language: { search: "Rechercher", lengthMenu: "Afficher _MENU_ entrées par page", info: "Affichage de _START_ à _END_ sur _TOTAL_ entrées", infoEmpty: "Affichage de 0 à 0 sur 0 entrée", infoFiltered: "(filtré de _MAX_ entrées au total)", - paginate: { first: "Début", previous: "Précédent", next: "Suivant", last: "Fin" }, + paginate: { + first: "Début", + previous: "Précédent", + next: "Suivant", + last: "Fin", + }, }, - - ajax: function (data, callback) { - const body = { - draw: data.draw || 1, - start: data.start || 0, - length: data.length || 10, - order: data.order || [], - columns: data.columns || [], - search: data.search || { value: "" }, - }; - - if (inflightController) inflightController.abort(); // action en cours - inflightController = new AbortController(); - const currentController = inflightController; - - fetch("/historiqueParcours/datatable", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - signal: currentController.signal, - }) - .then(res => { - // Vérifier si la requête a été annulée - if (currentController.signal.aborted || inflightController !== currentController) { - return null; - } - if (!res.ok) throw new Error(`HTTP ${res.status}`); - return res.json(); - }) - .then(payload => { - // Vérifier si la requête a été annulée - if (currentController.signal.aborted || inflightController !== currentController) { - return; - } - if (!payload || typeof payload !== 'object') { - throw new Error("Réponse invalide du serveur"); - } - callback({ - draw: payload.draw || 0, - recordsTotal: payload.recordsTotal || 0, - recordsFiltered: payload.recordsFiltered || payload.recordsTotal || 0, - data: Array.isArray(payload.data) ? payload.data : [] - }); - }) - .catch(err => { - // Ignorer silencieusement toutes les erreurs d'abort - if (err && (err.name === "AbortError" || err.name === "DOMException")) { - return; - } - // Vérifier aussi si le signal a été aborted - if (currentController.signal.aborted || inflightController !== currentController) { - return; - } - // Seulement afficher une erreur si ce n'est PAS un abort - displayError("Failed to fetch data. Please try again later."); - callback({ draw: 0, recordsTotal: 0, recordsFiltered: 0, data: [] }); - }); - }, - initComplete: function () { - const api = this.api(); - - // Recherche globale : debounce y compris ENTER (plus de bypass immédiat) - const $globalInput = $('div.dataTables_filter input[type="search"]'); - const $filterLabel = $globalInput.closest('label'); - $globalInput.off('.DT'); // nettoie handlers datatables - const debouncedGlobal = debounce((v) => { - api.search(v); - api.ajax.reload(); - }, 350); - - // Fonction pour gérer l'affichage du texte "Rechercher" - function toggleSearchPlaceholder() { - if ($globalInput.val().trim() !== '') { - $filterLabel.addClass('has-value'); - } else { - $filterLabel.removeClass('has-value'); - } - } - - $globalInput.on('input keyup keydown', function () { - debouncedGlobal(this.value); - toggleSearchPlaceholder(); - }); - - // Vérifier l'état initial - toggleSearchPlaceholder(); - - // Recherche par colonne avec DEBOUNCE (ENTER inclus) - const debouncedColSearch = debounce((i, val) => { - api.column(i).search(val); - api.ajax.reload(); - }, 350); - + const table = this.api(); $("#historiqueParcours thead tr:eq(1) th").each(function (i) { - $("input", this).on("input keyup keydown change", function () { - debouncedColSearch(i, this.value); + $("input", this).on("keyup change", function () { + if (table.column(i).search() !== this.value) { + table.column(i).search(this.value).draw(); + } }); }); - - api.on("responsive-resize", function (e, datatable, columns) { + table.on("responsive-resize", function (e, datatable, columns) { + // Loop over each column to see if it's visible for (let i = 0; i < columns.length; i++) { if (columns[i]) { - $(api.column(i).header()).show(); - $(api.column(i).footer()).show(); + $(table.column(i).header()).show(); + $(table.column(i).footer()).show(); $($("#historiqueParcours thead tr:eq(1) th")[i]).show(); - } - else { - $(api.column(i).header()).hide(); - $(api.column(i).footer()).hide(); + } else { + $(table.column(i).header()).hide(); + $(table.column(i).footer()).hide(); $($("#historiqueParcours thead tr:eq(1) th")[i]).hide(); } } }); - $("#divToggleSearch").on("click", function () { - const $row = $("#historiqueParcours thead tr:eq(1)"); - $row.toggle(); - const $btn = $("#toggleSearch"); - if ($row.is(":visible")) { - $btn.text("ENLEVER LA RECHERCHE PAR COLONNE"); - } else { - $btn.text("ACTIVER LA RECHERCHE PAR COLONNE"); - } + $("#historiqueParcours thead tr:eq(1)").toggle(); }); - // Cacher la 2e ligne au départ $("#historiqueParcours thead tr:eq(1)").hide(); }, - - // --- Ajout bouton détails en 1re colonne - columnDefs: [ - { - targets: 0, - render: function (data, type, row) { - const np = String(data ?? ""); - const numPacours = np.replace(/&/g,"&").replace(//g,">"); - - return '' - + '' - + '' + numPacours + '' - + ''; - - } - }, - { type: "date-eu", targets: 1 }, // Date de Création (colonne 1) - { - // Appliquer la classe nc-value aux cellules contenant "NC" - targets: "_all", - createdCell: function (td, cellData, rowData, row, col) { - // Exclure la première colonne (bouton détails) et les deux dernières (boutons) - if (col !== 0 && col < rowData.length - 2) { - const cellText = String(cellData || "").trim(); - if (cellText === "NC") { - td.classList.add("nc-value"); - } - } - } - }, - { responsivePriority: 1, targets: -1 }, - { responsivePriority: 2, targets: -2 } - ], }); - // --- Toggle des détails - $('#historiqueParcours tbody').on('click', '.btn-row-details', function (e) { - e.preventDefault(); - e.stopPropagation(); - - const api = $('#historiqueParcours').DataTable(); - const $tr = $(this).closest('tr'); - - // si c'est une ligne enfant (responsive), remonter à la parent - const row = api.row($tr.hasClass('child') ? $tr.prev() : $tr); - - if (row.child.isShown()) { - row.child.hide(); - // change icône - this.textContent = "?"; - this.style.background = "#e74c3c"; - return; - } - - // récupérer le numParcours depuis la 1re cellule - const raw = row.data()?.[0] ?? ""; - const tmp = document.createElement('div'); - tmp.innerHTML = String(raw); - const numParcours = tmp.textContent.replace("?", "").trim(); - if (!numParcours) return; - - row.child('
Chargement des détails…
').show(); - this.textContent = "?"; - this.style.background = "#c0392b"; - - fetch(`/historiqueParcours/details/${encodeURIComponent(numParcours)}`) - .then(r => { - if (!r.ok) throw new Error(`HTTP ${r.status}`); - return r.json(); - }) - .then(payload => { - if (!payload || typeof payload !== 'object') { - throw new Error("Réponse invalide"); + //clear existing data + table.clear(); + let row = null; + let tableRow = null; + + //generate Project + //loop on parcours + parcoursData.forEach((parcours) => { + const contratId = parcours["@expand"]?.contrat?.id; + const contrat = contratId ? parcours["@expand"].contrat : null; + const client = contrat ? contrat.client : null; + const lastUser = parcours["@expand"]?.dernierUtilisateur; + const region = lastUser["@expand"]?.region; + const produit = contrat ? (contrat.produit ? contrat.produit : "NC") : "NC" + + row = [ + parcours.numParcours, + new Date(parcours.created).toLocaleDateString("fr-FR", { + day: "numeric", + month: "numeric", + year: "numeric" + }), + parcours["@expand"].dernierUtilisateur?.matricule || "NC", + parcours["@expand"].dernierUtilisateur ? `${parcours["@expand"].dernierUtilisateur.prenom} ${parcours["@expand"].dernierUtilisateur.nom}` : "NC", + region ? region.nom : "NC", + contrat ? (contrat.numSaisine ? contrat.numSaisine : "NC") : "NC", + contrat ? (contrat.numContrat ? contrat.numContrat : "NC") : "NC", + contrat ? (contrat.produit ? contrat.produit : "NC") : "NC", + contrat ? (contrat.type ? contrat.type : "NC") : "NC", + contrat ? contrat["@expand"]?.intermediaire?.numPortefeuille || "NC" : "NC", + contrat ? contrat["@expand"]?.intermediaire?.nom || "NC" : "NC", + client ? client.numClient || "NC" : "NC", + client ? client.nom || "NC" : "NC", + ``, + ``, + ]; + tableRow = table.row.add(row).node(); + + // add class NC to style "Non communiqué" + $(tableRow) + .find("td") + .each(function (colIndex) { + if ($(this).text() === "NC") { + $(this).addClass("nc-value"); } - if (!payload.valid) { - row.child(createMessageBox('error', 'Impossible de charger les détails', - 'Une erreur est survenue lors du chargement des détails du parcours.')); - this.textContent = "?"; - this.style.background = "#e74c3c"; - return; - } - row.child(formatDetailsPanel(payload)); - }) - .catch(() => { - row.child(createMessageBox('error', 'Erreur de chargement', - 'Une erreur réseau est survenue lors du chargement des détails.')); - this.textContent = "?"; - this.style.background = "#e74c3c"; }); }); - return table; + table.draw(); + + // for "afficher" entrées par page + $("#historiqueParcours_length select").val("10").trigger("change"); } -/* ========================= - * Fonctions annexes importantes - * ========================= */ +function downloadExcel(applyFilters) { + const table = $("#historiqueParcours").DataTable(); // Get the DataTable instance + const headers = $("#historiqueParcours th").filter(function () { + return !$(this).hasClass("no-export");}).map(function () { + return $(this).text().trim(); + }).get(); -async function fetchUserDetails(matriculeUser) { - try { - const response = await fetch(`/user/read/matricule/${matriculeUser}`); - const data = await response.json(); + const data = []; + const rowsData = applyFilters ? table.rows({ filter: "applied" }).data() : table.rows().data(); + rowsData.each(function (row) { + const filteredRow = $(row).filter(function (index) { + return !$("#historiqueParcours th").eq(index).hasClass("no-export"); + }); - return data.valid ? data : null; - } catch (error) { - displayError(`Erreur lors de la récupération du contrat avec le matricule ${matriculeUser} :`, error); - return null; + data.push(filteredRow.get()); + }); + + const ws = XLSX.utils.aoa_to_sheet([headers, ...data]); + const wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, "Historique Parcours"); + + const wbout = XLSX.write(wb, { bookType: "xlsx", type: "binary" }); + + function s2ab(s) { + const buf = new ArrayBuffer(s.length); + const view = new Uint8Array(buf); + for (let i = 0; i < s.length; i++) view[i] = s.charCodeAt(i) & 0xff; + + return buf; } + + const now = new Date(); + const pad = (num) => String(num).padStart(2, "0"); + const formattedDate = `${pad(now.getDate())}${pad(now.getMonth() + 1)}${now.getFullYear()}${pad(now.getHours())}${pad(now.getMinutes())}`; + + const blob = new Blob([s2ab(wbout)], { type: "application/octet-stream" }); + const link = document.createElement("a"); + link.href = URL.createObjectURL(blob); + link.download = `historique_parcours_${formattedDate}.xlsx`; + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +} + +function downloadCSV(applyFilters) { + const table = $("#historiqueParcours").DataTable(); + const headers = $("#historiqueParcours th").filter(function () { + return !$(this).hasClass("no-export");}).map(function () { + return $(this).text().trim(); + }).get(); + + let csvContent = "data:text/csv;charset=utf-8," + headers.join(";") + "\n"; + const rowsData = applyFilters ? table.rows({ filter: "applied" }).data() : table.rows().data(); + + rowsData.each(function (row) { + let filteredRow = row.filter((cell, index) => { + return !$(`#historiqueParcours th`).eq(index).hasClass("no-export"); + }); + csvContent += filteredRow.join(";") + "\n"; + }); + + const encodedUri = encodeURI(csvContent); + const link = document.createElement("a"); + link.setAttribute("href", encodedUri); + link.setAttribute("download", "historique_parcours.csv"); + document.body.appendChild(link); + link.click(); } async function generateProject(numParcours, produit) { try { - if (!numParcours || !produit) { - displayError("Paramètres manquants pour la génération du projet."); - return; - } - - const response = await fetch(`/generate/${produit}/projet/${encodeURIComponent(numParcours)}`, { + const response = await fetch(`/generate/${produit}/projet/${numParcours}`, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json"}, }); + if (!response.ok) throw new Error("Erreur réseau ou serveur"); const disposition = response.headers.get("content-disposition"); - let filename = "projet.docx"; - if (disposition) { - const parts = disposition.split(";"); - if (parts.length > 1) { - const filenamePart = parts[1].trim(); - if (filenamePart.startsWith("filename=")) { - filename = filenamePart.split("=")[1]?.replace(/"/g, "") || filename; - } - } - } + const filename = disposition.split(";")[1].trim().split("=")[1]; const blob = await response.blob(); - if (!blob || blob.size === 0) { - throw new Error("Fichier vide reçu"); - } - const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; @@ -566,252 +329,56 @@ async function generateProject(numParcours, produit) { window.URL.revokeObjectURL(url); a.remove(); } catch (error) { - displayError("Erreur lors de la génération du projet : " + (error?.message || "Erreur inconnue")); + console.error("Erreur lors de la génération du projet:", error); } } -// ---------- Helpers rendu ---------- -// Les fonctions kv, fmtDate, gridWrap2cols, debounce, parseJwt et displayError sont dans global.js +// Fonction pour décoder le JWT +function parseJwt(token) { + try { + const base64Url = token.split(".")[1]; + const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); + const jsonPayload = decodeURIComponent(atob(base64).split("").map(function (c) { return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);}).join("")); + return JSON.parse(jsonPayload); + } catch (error) { + console.error("Erreur lors du décodage du token:", error); -//formatDetailsPanel : rend l'HTML pour la récuperation des données par parcours -function formatDetailsPanel(payload) { - const prodKey = (payload.produit || "").toUpperCase(); - const prod = payload.produitRecord || null; - const contrat = payload.contrat || null; - - let body = ""; - if (prodKey === "TPPC") body = sectionTPPC(prod, contrat); - else if (prodKey === "RC") body = sectionRC(prod, contrat); - else if (prodKey === "FAC") body = sectionFAC(prod, contrat); - else body = createMessageBox('warn', 'Produit non renseigné', 'Le type de produit n\'a pas été spécifié pour ce parcours.'); - - return ` -
-
Détails ${prodKey || ""}
- ${body} -
- `; + return null; + } } -// ---------- Sections produit ---------- - -//sectionTTPC : récupere les détails d'un parcours TPPC pour le mettre en forme (2 colonnes) -function sectionTPPC(produit, contrat) { - if (!produit) { - if (contrat) { - return createMessageBox('info', 'Informations non disponibles', - 'Les informations sur ce Parcours TPPC ne sont pas encore disponibles.
La fiche TPPC n\'a pas encore été créée pour ce parcours.'); - } - return createMessageBox('error', 'Fiche TPPC introuvable', - 'Impossible de récupérer les informations de la fiche TPPC pour ce parcours.'); - } - - const tarif = produit?.["@expand"]?.tarif || null; // tppctarif - const projet = produit?.["@expand"]?.projet || null; // tppcprojet - - const garant = Array.isArray(produit.garanties) ? produit.garanties.join(", ") : (produit.garanties || "NC"); - - // Extensions de garanties - const extensions = []; - if (produit.marCiternes) extensions.push("Citernes"); - if (produit.marDenreesSousTemp) extensions.push("Denrées sous température"); - if (produit.marAnimaux) extensions.push("Animaux vivants"); - if (produit.marFranchise) extensions.push("Franchise"); - const extensionsStr = extensions.length > 0 ? extensions.join(", ") : "Aucune"; - - // Type de cotisation et type de révision (couplés) - let typeCotStr = "NC"; - let typeRevStr = "NC"; - if (projet) { - if (projet.typeCot) { - typeCotStr = projet.typeCot === "revisable" ? "Revisable" : (projet.typeCot === "forfaitaire" ? "Forfaitaire" : projet.typeCot); - } - if (projet.typeRev) { - // Mapping des valeurs possibles du type de révision - if (projet.typeRev === "CotCA") { - typeRevStr = "Cotisation CA"; - } else if (projet.typeRev === "CotFlotte" || projet.typeRev === "FlotteOuverte") { - typeRevStr = "Cotisation Flotte"; - } else { - // Si autre valeur, on l'affiche telle quelle - typeRevStr = projet.typeRev; - } - } else if (projet.typeCot === "revisable") { - // Si revisable mais pas de typeRev, on regarde le tarif - if (tarif?.typeContrat === "detaillee") { - typeRevStr = "Cotisation Flotte"; - } - } - } - - const gauche = [ - kv("Activité assurée", produit.actAssuree), - kv("Nombre de véhicules", produit.nbVehic), - kv("Garanties", garant), - kv("Extensions de garanties", extensionsStr), - kv("Type de cotisation", typeCotStr), - kv("Type de révision", typeRevStr), - ].join(""); - - // Formatage des dates du projet - const dateEffet = projet?.dateEffet ? fmtDate(projet.dateEffet, false) : "NC"; - const dateEcheance = projet?.dateEcheance ? fmtDate(projet.dateEcheance, false) : "NC"; - const dateFin = projet?.dateFin ? fmtDate(projet.dateFin, false) : "NC"; - - const droite = [ - kv("Prime HT", produit.primeHT), - kv("Tarif - Référence", tarif?.tarifRef), - kv("Projet - Dates", `Effet: ${dateEffet} / Échéance: ${dateEcheance} / Fin: ${dateFin}`), - ].join(""); - - return gridWrap2cols(gauche, droite); -} - - -//sectionRC : récupere les détails d'un parcours RC pour le mettre en forme (2 colonnes) -function sectionRC(produit, contrat) { - // Si le produit n'existe pas mais qu'on a un contrat RC, c'est que la fiche n'est pas encore créée - if (!produit && contrat) { - return createMessageBox('info', 'Informations non disponibles', - 'Les informations sur ce Parcours RC ne sont pas encore disponibles.
La fiche RC n\'a pas encore été créée pour ce parcours.'); - } +//generagte Project +$("#historiqueParcours").on("click", "button#btnGenerate", function() { + const numParcours = $(this).data("num-parcours"); + const produit = $(this).data("produit"); - // Si le produit n'existe pas et qu'on n'a pas de contrat, cas théorique (ne devrait jamais arriver) - if (!produit) { - return createMessageBox('dev', 'Fonctionnalité en cours de développement', - 'L\'affichage des détails pour les parcours RC n\'est pas encore disponible.
Cette fonctionnalité sera bientôt implémentée.'); - } + generateProject(numParcours, produit); +}); - // Construction de l'activité assurée à partir des activités - const activites = []; - if (produit.actVoiturier) activites.push("Voiturier"); - if (produit.actLoueur) activites.push("Loueur"); - if (produit.actMultimodal) activites.push("Multimodal"); - if (produit.actDouane) activites.push("Douane"); - if (produit.actLevageur) activites.push("Levageur"); - if (produit.actTransitaire) activites.push("Transitaire"); - const activiteAssuree = activites.length > 0 ? activites.join(", ") : "NC"; +//export CSV +$("#exportCSV").on("click", function () { + downloadCSV(false); +}); - // Construction des garanties à partir des marchandises - const garanties = []; - if (produit.marRoulant) garanties.push("Roulant"); - if (produit.marEngins) garanties.push("Engins"); - if (produit.marPerissable) garanties.push("Périssable"); - if (produit.marOrdinaire) garanties.push("Ordinaire"); - if (produit.marAnimaux) garanties.push("Animaux"); - if (produit.marCiterne) garanties.push("Citerne"); - if (produit.marBeton) garanties.push("Béton"); - if (produit.marExceptionnels) garanties.push("Exceptionnels"); - if (produit.marMobilerUsag) garanties.push("Mobilier usagé"); - if (produit.marVrac) garanties.push("Vrac"); - if (produit.marRoulantDem) garanties.push("Roulant déménagement"); - const garantiesStr = garanties.length > 0 ? garanties.join(", ") : "NC"; +//export CSV with filter +$("#exportCSVFilter").on("click", function () { + downloadCSV(true); +}); - // Zones - const zones = []; - if (produit.zone1) zones.push("1"); - if (produit.zone2) zones.push("2"); - if (produit.zone3) zones.push("3"); - if (produit.zone4) zones.push("4"); - if (produit.zone5) zones.push("5"); - if (produit.zone6) zones.push("6"); - const zonesStr = zones.length > 0 ? zones.join(", ") : "NC"; +//export to excel +$("#exportXlxs").on("click", function () { + downloadExcel(false); +}); - // Type de cotisation - let typeCotStr = "NC"; - if (produit.typeCot) { - typeCotStr = produit.typeCot === "revisable" ? "Revisable" : (produit.typeCot === "forfaitaire" ? "Forfaitaire" : produit.typeCot); - } +// export to excel using Filter +$("#exportXlxsFilter").on("click", function () { + downloadExcel(true); +}); - // Formatage des dates - const dateEffet = produit.dateEffet ? fmtDate(produit.dateEffet, false) : "NC"; - const dateEcheance = produit.dateEcheance ? fmtDate(produit.dateEcheance, false) : "NC"; - const dateFin = produit.dateFin ? fmtDate(produit.dateFin, false) : "NC"; - // Vérifier si on a un tarif (pas disponible pour RC actuellement) - const tarif = produit?.["@expand"]?.tarif || null; - const tarifRef = tarif?.tarifRef || "en cours de développement"; +function displayError(message) { + const errorElement = document.getElementById("error"); - const gauche = [ - kv("Activité assurée", activiteAssuree), - kv("Garanties", garantiesStr), - kv("Zones", zonesStr), - kv("Type de cotisation", typeCotStr), - kv("Tempo", produit.tempo || "NC"), - kv("Chiffre d'affaires", produit.ca || "NC"), - ].join(""); - - const droite = [ - kv("Tarif - Référence", tarifRef), - kv("Cot. totale HT / TTC", `${produit.cotTotalHT ?? "NC"} / ${produit.cotTotalTTC ?? "NC"}`), - kv("Dates", `Effet: ${dateEffet} / Échéance: ${dateEcheance} / Fin: ${dateFin}`), - ].join(""); - - return gridWrap2cols(gauche, droite); + errorElement.textContent = message; + errorElement.style.display = "block"; } - -//sectionFAC : récupere les détails d'un parcours FAC pour le mettre en forme (2 colonnes) -function sectionFAC(produit, contrat) { - // Si le produit n'existe pas mais qu'on a un contrat FAC, c'est que la fiche n'est pas encore créée - if (!produit && contrat) { - return createMessageBox('info', 'Informations non disponibles', - 'Les informations sur ce Parcours FAC ne sont pas encore disponibles.
La fiche FAC n\'a pas encore été créée pour ce parcours.'); - } - - // Si le produit n'existe pas et qu'on n'a pas de contrat, cas théorique (ne devrait jamais arriver) - if (!produit) { - return createMessageBox('dev', 'Fonctionnalité en cours de développement', - 'L\'affichage des détails pour les parcours FAC n\'est pas encore disponible.
Cette fonctionnalité sera bientôt implémentée.'); - } - - // Construction des garanties à partir des modes de transport - const garanties = []; - if (produit.terrestre && produit.terrestre !== "NC" && produit.terrestre !== "") garanties.push("Terrestre"); - if (produit.maritime && produit.maritime !== "NC" && produit.maritime !== "") garanties.push("Maritime"); - if (produit.aerien && produit.aerien !== "NC" && produit.aerien !== "") garanties.push("Aérien"); - if (produit.postal && produit.postal !== "NC" && produit.postal !== "") garanties.push("Postal"); - if (produit.fluvial && produit.fluvial !== "NC" && produit.fluvial !== "") garanties.push("Fluvial"); - const garantiesStr = garanties.length > 0 ? garanties.join(", ") : "NC"; - - // Zones - const zones = []; - if (produit.zone1) zones.push("1"); - if (produit.zone2) zones.push("2"); - if (produit.zone3) zones.push("3"); - if (produit.zone4) zones.push("4"); - if (produit.zone5) zones.push("5"); - if (produit.zone6) zones.push("6"); - const zonesStr = zones.length > 0 ? zones.join(", ") : "NC"; - - // Type de contrat - let typeContratStr = "NC"; - if (produit.typeContrat) { - typeContratStr = produit.typeContrat; - } - - // Formatage des dates - const dateEffet = produit.dateEffet ? fmtDate(produit.dateEffet, false) : "NC"; - const dateEcheance = produit.dateEcheance ? fmtDate(produit.dateEcheance, false) : "NC"; - const dateFin = produit.dateFin ? fmtDate(produit.dateFin, false) : "NC"; - - // Vérifier si on a un tarif (pas disponible pour FAC actuellement) - const tarif = produit?.["@expand"]?.tarif || null; - const tarifRef = tarif?.tarifRef || "en cours de développement"; - - const gauche = [ - kv("Activité assurée", produit.actAssure || "NC"), - kv("Garanties", garantiesStr), - kv("Zones", zonesStr), - kv("Type de contrat", typeContratStr), - kv("Tempo", produit.tempo || "NC"), - kv("Chiffre d'affaires", produit.ca || "NC"), - ].join(""); - - const droite = [ - kv("Tarif - Référence", tarifRef), - kv("Cot. annuelle HT / TTC", `${produit.cotAnnuelleHT ?? "NC"} / ${produit.cotAnnuelleTTC ?? "NC"}`), - kv("Dates", `Effet: ${dateEffet} / Échéance: ${dateEcheance} / Fin: ${dateFin}`), - ].join(""); - - return gridWrap2cols(gauche, droite); -} \ No newline at end of file diff --git a/ecole/public/js/loader.js b/ecole/public/js/loader.js new file mode 100644 index 00000000..b7d123ae --- /dev/null +++ b/ecole/public/js/loader.js @@ -0,0 +1,31 @@ +document.addEventListener("DOMContentLoaded", () => { + const loader = document.getElementById("loader-overlay"); // déjà présent dans le layout.ejs + const errorMessage = document.getElementById("error-message"); + + let activateTimeout = null; + + //activer le loader et le montrer a l'écran + window.showLoader = function() { + clearTimeout(activateTimeout); + errorMessage.style.display = "none"; + loader.classList.remove("hidden"); + + activateTimeout = setTimeout(() => { + loader.classList.add("active"); + }, 500); + }; + + //enlever le loader et le faire disparaitre + window.hideLoader = function() { + clearTimeout(activateTimeout); + loader.classList.remove("active"); + setTimeout(() => loader.classList.add("hidden"), 500); + }; + + //cas d'erreur + window.showError = function(msg) { + clearTimeout(activateTimeout); + errorMessage.textContent = msg; + errorMessage.style.display = "block"; + }; +}); diff --git a/ecole/public/js/nav-parcours.js b/ecole/public/js/nav-parcours.js index adf83f24..bb6ed373 100644 --- a/ecole/public/js/nav-parcours.js +++ b/ecole/public/js/nav-parcours.js @@ -79,6 +79,7 @@ document.addEventListener('DOMContentLoaded', function() { document.getElementById('step-' + key).classList.add('line') } } + showLoader(); // Charger le formulaire associé fetch(fetchUrl) @@ -119,6 +120,7 @@ document.addEventListener('DOMContentLoaded', function() { inputChanged = true }) }) + hideLoader(); }) .catch(error => console.error('Error:', error)); @@ -155,6 +157,9 @@ document.addEventListener('DOMContentLoaded', function() { const parcours = JSON.parse(sessionStorage.getItem('parcours')); var produit = parcours["@expand"].contrat.produit + const btn = this // bouton "générer projet" + btn.disabled = true; // le desactiver le temps du téléchargement + var fileName switch (produit.toLowerCase()) { case 'fac': @@ -178,6 +183,9 @@ document.addEventListener('DOMContentLoaded', function() { link.download = fileName; link.click(); }) + .finally(() => { + btn.disabled = false; // réactiver le bouton a la fin du téléchargement + }) .catch(error => console.error('Error downloading file:', error)); }); @@ -188,6 +196,9 @@ document.addEventListener('DOMContentLoaded', function() { const parcours = JSON.parse(sessionStorage.getItem('parcours')); let produit = parcours["@expand"].contrat.produit + const btn = this // bouton "générer projet" + btn.disabled = true; // le desactiver le temps du téléchargement + // Envoi de la requête POST au serveur pour générer le projet fetch(`/generate/${produit}/projet/${numParcours}`, { method: 'POST', @@ -217,6 +228,9 @@ document.addEventListener('DOMContentLoaded', function() { window.URL.revokeObjectURL(url); // Nettoie l'URL objet a.remove(); // Supprime l'élément a du document }) + .finally(() => { + btn.disabled = false; // réactiver le bouton a la fin du téléchargement + }) .catch(error => console.error('Erreur lors de la génération du projet:', error)); }); @@ -227,6 +241,9 @@ document.addEventListener('DOMContentLoaded', function() { const parcours = JSON.parse(sessionStorage.getItem('parcours')); let produit = parcours["@expand"].contrat.produit + const btn = this // bouton "générer déclinaison tarifaire" + btn.disabled = true; // le desactiver le temps du téléchargement + // Envoi de la requête POST au serveur pour générer le projet fetch(`/generate/${produit}/tarif/${numParcours}`, { method: 'POST', @@ -257,6 +274,9 @@ document.addEventListener('DOMContentLoaded', function() { window.URL.revokeObjectURL(url); // Nettoie l'URL objet a.remove(); // Supprime l'élément a du document }) + .finally(() => { + btn.disabled = false; // réactiver le bouton a la fin du téléchargement + }) .catch(error => console.error('Erreur lors de la génération du projet:', error)); }); diff --git a/ecole/public/js/navigation.js b/ecole/public/js/navigation.js new file mode 100644 index 00000000..f2559d91 --- /dev/null +++ b/ecole/public/js/navigation.js @@ -0,0 +1,49 @@ +document.addEventListener("DOMContentLoaded", () => { + const container = document.querySelector(".container"); + + async function loadPage(url, push = true) { + showLoader(); + + try { + + const res = await fetch(url, { headers: { "X-Requested-With": "fetch" }}); + const html = await res.text(); + + const parser = new DOMParser(); + const doc = parser.parseFromString(html, "text/html"); + const newContent = doc.querySelector(".container").innerHTML; + + container.innerHTML = newContent; + + if (push) history.pushState({}, "", url); + + } + + catch (err) { + showError("Impossible de charger la page. Vérifiez votre connexion."); + } + + finally { + hideLoader(); + } + } + + // Intercepter les clics sur les liens internes + document.body.addEventListener("click", (e) => { + const link = e.target.closest("a"); + + if (link && link.getAttribute("href").startsWith("/")) { + + e.preventDefault(); + loadPage(link.href); + + } + }); + + // Gérer le bouton retour + window.addEventListener("popstate", () => { + + loadPage(location.pathname, false); + + }); +}); diff --git a/ecole/public/js/projet-form-tppc.js b/ecole/public/js/projet-form-tppc.js index c173e834..cc12a285 100644 --- a/ecole/public/js/projet-form-tppc.js +++ b/ecole/public/js/projet-form-tppc.js @@ -96,7 +96,7 @@ window.initSubmenuForm = initSubmenuForm;// Module IIFE pour éviter la pollutio const genreValue = document.getElementById('genreVehicule').value || 'Non défini'; const typeValue = document.getElementById('typeVehicule').value || 'Non défini'; const immatValue = document.getElementById('immatVehicule').value || 'Non défini'; - const capitalValue = document.getElementById('capitalVehicule').value || 'Non défini'; + const capitalValue = document.getElementById('capitalVeh').value || 'Non défini'; addRowVehicule(marqueValue, genreValue, typeValue, immatValue, capitalValue); }); @@ -745,7 +745,7 @@ window.initSubmenuForm = initSubmenuForm;// Module IIFE pour éviter la pollutio document.getElementById('genreVehicule').value = ''; document.getElementById('typeVehicule').value = ''; document.getElementById('immatVehicule').value = ''; - document.getElementById('capitalVehicule').value = ''; + document.getElementById('capitalVeh').value = ''; // Ajouter un écouteur d'événements pour supprimer newRow.querySelector('.delete-btn').addEventListener('click', function() { diff --git a/ecole/public/js/tarif-form-tppc.js b/ecole/public/js/tarif-form-tppc.js index 3917487b..b801f805 100644 --- a/ecole/public/js/tarif-form-tppc.js +++ b/ecole/public/js/tarif-form-tppc.js @@ -132,6 +132,10 @@ window.initSubmenuForm = initSubmenuForm;// Module IIFE pour éviter la pollutio element.addEventListener('input', function () { affichagePropositions(); }) + + element.addEventListener('change', function () { + affichagePropositions(); + }) }) document.querySelectorAll('select').forEach((element) => { @@ -417,9 +421,9 @@ window.initSubmenuForm = initSubmenuForm;// Module IIFE pour éviter la pollutio document.getElementById('cotisationDetaillee').checked = true document.getElementById('cotisationEnsemble').checked = false } else { - - document.getElementById('cotisationEnsemble').checked = true - toggleTypeContrat('ensemble') + //Par Defaut + document.getElementById('cotisationDetaillee').checked = true + toggleTypeContrat('detaillee') } if (tarif && tarif.montantSinistre !== undefined && tarif.montantSinistre >= 0) { diff --git a/ecole/src/controllers/historiqueParcoursController.js b/ecole/src/controllers/historiqueParcoursController.js index 70f3fea0..490ea0fd 100644 --- a/ecole/src/controllers/historiqueParcoursController.js +++ b/ecole/src/controllers/historiqueParcoursController.js @@ -1,125 +1,25 @@ -// controllers/historiqueParcoursController.js const express = require("express"); const router = express.Router(); const renderPage = require("../utils/renderHelper"); const logger = require("../utils/logger"); const parcoursService = require("../services/parcoursService"); -const clientService = require("../services/clientService"); -const { fmtDateFR, xmlEsc, cellXml, rowXml } = require("../services/globalService"); -/** - * Construit les filtres et le tri PocketBase à partir des paramètres DataTables - * @param {Object} params - Paramètres de recherche et filtrage - * @param {string[]} params.regions - Liste des régions à filtrer - * @param {Object} params.search - Objet de recherche globale - * @param {Array} params.columns - Colonnes avec leurs filtres individuels - * @param {Array} params.order - Ordre de tri - * @returns {Object} - {filter: string, sort: string} - */ -function buildPocketBaseFilterAndSort({ regions = [], search = { value: "" }, columns = [], order = [] }) { - const parts = []; - - /** - * Recherche globale : recherche dans tous les champs pertinents - */ - const q = (search?.value || "").trim(); - if (q) { - const esc = q.replace(/"/g, '\\"'); - parts.push(`( - numParcours ~ "${esc}" - || contrat.numSaisine ~ "${esc}" - || contrat.numContrat ~ "${esc}" - || contrat.produit ~ "${esc}" - || contrat.type ~ "${esc}" - || contrat.intermediaire.nom ~ "${esc}" - || contrat.intermediaire.numPortefeuille ~ "${esc}" - || contrat.client.nom ~ "${esc}" - || contrat.client.numClient ~ "${esc}" - || dernierUtilisateur.prenom ~ "${esc}" - || dernierUtilisateur.nom ~ "${esc}" - || dernierUtilisateur.matricule ~ "${esc}" - || dernierUtilisateur.region.nom ~ "${esc}" - )`); - } - - /** - * Recherche par colonne : filtre spécifique pour chaque colonne - */ - const colFilter = (idx, fieldPaths) => { - const v = (columns[idx]?.search?.value || "").trim(); - if (!v) return null; - const esc = v.replace(/"/g, '\\"'); - return `(${fieldPaths.map(fp => `${fp} ~ "${esc}"`).join(" || ")})`; - }; - - const pushIf = (v) => { if (v) parts.push(v); }; - - // Filtres par colonne (index correspondant à l'ordre des colonnes DataTables) - pushIf(colFilter(0, ["numParcours"])); - pushIf(colFilter(1, ["created"])); - pushIf(colFilter(2, ["dernierUtilisateur.matricule"])); - pushIf(colFilter(3, ["dernierUtilisateur.prenom", "dernierUtilisateur.nom"])); - pushIf(colFilter(4, ["dernierUtilisateur.region.nom"])); - pushIf(colFilter(5, ["contrat.numSaisine"])); - pushIf(colFilter(6, ["contrat.numContrat"])); - pushIf(colFilter(7, ["contrat.produit"])); - pushIf(colFilter(8, ["contrat.type"])); - pushIf(colFilter(9, ["contrat.intermediaire.numPortefeuille"])); - pushIf(colFilter(10, ["contrat.intermediaire.nom"])); - pushIf(colFilter(11, ["contrat.client.numClient"])); - pushIf(colFilter(12, ["contrat.client.nom"])); - - const filter = parts.length ? parts.join(" && ") : ""; - - /** - * Construction du tri PocketBase - * Mapping des index de colonnes DataTables vers les champs PocketBase - * Le préfixe "-" indique un tri décroissant - */ - const sortMap = { - 0: "numParcours", - 1: "created", - 2: "dernierUtilisateur.matricule", - 4: "dernierUtilisateur.region.nom", - 6: "contrat.numContrat", - 7: "contrat.produit", - 10: "contrat.intermediaire.nom", - 12: "contrat.client.nom" - }; - - let sort = "-created"; // Tri par défaut : date de création décroissante - if (order && order.length > 0) { - const { column, dir } = order[0]; - const field = sortMap[column]; - if (field) { - sort = (dir === "desc" ? "-" : "") + field; - } - } - - return { filter, sort }; -} - -/** - * Route GET / : Affichage de la page Historique des parcours - */ router.get("/", (req, res) => { renderPage("historiqueParcours.ejs", res); }); -/** - * /regionUser : requête sur la region de l'user actuel - */ -router.get("/:regionUser", async (req, res) => { +router.get("/read", async (req, res) => { try { - const { regionUser } = req.params; - const data = await parcoursService.getParcoursByRegionsPage([regionUser], 1, 10, { filter: "", sort: "-created" }); - if (data) { - res.json({ valid: true, data }); + const allParcours = await parcoursService.getAllParcours(); + + if (allParcours) { + res.json({ valid: true, allParcours }); } else { res.json({ valid: false }); } } catch (error) { logger.log("error", error); + res.status(500).json({ valid: false, error: "Erreur lors de la récupération des parcours.", @@ -127,508 +27,25 @@ router.get("/:regionUser", async (req, res) => { } }); -/** - * /datatable : DataTables server-side (gestion de pagination) - */ -router.post("/datatable", async (req, res) => { +//controller to get parcours by region +router.get("/:regionUser", async (req, res) => { try { - const { - draw = 1, - start = 0, - length = 10, - regions = [], - search = { value: "" }, - columns = [], - order = [] - } = req.body || {}; + const { regionUser } = req.params; + const data = await parcoursService.getParcoursByRegion(regionUser); - const page = Math.floor(start / length) + 1; // nb de page - const perPage = Number(length) || 10; //nb d'éléments par page - - const { filter, sort } = buildPocketBaseFilterAndSort({ search, columns, order }); // construction du filtrage côté Back - - const result = await parcoursService.getParcoursByRegionsPage([], page, perPage, { filter, sort }); - - /** - * Construction des lignes de données pour DataTables - * Traitement séquentiel pour garantir la récupération des clients - */ - const rows = []; - for (const parcours of result.items) { - try { - const contrat = parcours["@expand"]?.contrat || null; - - /** - * Récupération du client avec fallback - * L'expand PocketBase ne fonctionne pas toujours pour contrat.client, - * donc on récupère directement via l'ID si nécessaire - */ - let client = null; - if (contrat) { - // Tentative via expand (si disponible) - client = contrat["@expand"]?.client || null; - - // Fallback : récupération directe via l'ID du client - if (!client && contrat.client) { - const clientId = typeof contrat.client === 'string' - ? contrat.client - : (contrat.client?.id || contrat.client); - - if (clientId) { - try { - client = await clientService.getClient(clientId); - } catch (err) { - // Erreur silencieuse : client non trouvé ou erreur de récupération - client = null; - } - } - } - - // Cas où contrat.client est déjà un objet (expand réussi mais pas dans @expand) - if (!client && contrat.client && typeof contrat.client === 'object' && contrat.client.numClient) { - client = contrat.client; - } - } - const lastUser = parcours["@expand"]?.dernierUtilisateur; - const region = lastUser?.["@expand"]?.region; - const produit = contrat ? (contrat.produit || "NC") : "NC"; - - /** - * Construction de la ligne DataTables - * Ordre des colonnes : Numéro Parcours, Date Création, Matricule, Utilisateur, Région, - * Numéro Saisine, Numéro Contrat, Produit, Type, Portefeuille, Intermédiaire, - * Numéro Client, Nom Client, Bouton Reprendre, Bouton Générer - */ - rows.push([ - parcours.numParcours, - fmtDateFR(parcours.created), - lastUser?.matricule || "NC", - lastUser ? `${lastUser.prenom} ${lastUser.nom}`.trim() || "NC" : "NC", - region ? region.nom : "NC", - contrat ? (contrat.numSaisine || "NC") : "NC", - contrat ? (contrat.numContrat || "NC") : "NC", - produit, - contrat ? (contrat.type || "NC") : "NC", - contrat ? (contrat["@expand"]?.intermediaire?.numPortefeuille || "NC") : "NC", - contrat ? (contrat["@expand"]?.intermediaire?.nom || "NC") : "NC", - client ? (client.numClient || "NC") : "NC", - client ? (client.nom || "NC") : "NC", - ``, - `` - ]); - } catch (err) { - logger.log("error", `Erreur traitement parcours ${parcours?.numParcours || 'inconnu'}:`, err); - // Ligne par défaut en cas d'erreur - rows.push(["NC", "NC", "NC", "NC", "NC", "NC", "NC", "NC", "NC", "NC", "NC", "NC", "NC", "", ""]); - } + if (data) { + res.json({ valid: true, data }); + } else { + res.json({ valid: data }); } + } catch (error) { + logger.log("error", error); - res.json({ - draw: Number(draw), - recordsTotal: result.totalItems, - recordsFiltered: result.totalItems, - data: rows + res.status(500).json({ + valid: false, + error: "Erreur lors de la récupération des parcours.", }); - } catch (error) { - logger.log("error", error); - res.status(500).json({ draw: 0, recordsTotal: 0, recordsFiltered: 0, data: [] }); } }); -/** - * EXPORT CSV - * Exporte l'historique des parcours au format CSV - * Supporte l'export complet ou filtré selon les paramètres de la requête - */ -router.post("/export/csv", async (req, res) => { - let aborted = false; - req.on("aborted", () => { - aborted = true; - logger.log("warn", "Client a interrompu la connexion pendant l'export CSV"); - }); - res.on("finish", () => { - logger.log("info", "Export CSV terminé"); - }); - - try { - const { - regions = [], - search = { value: "" }, - columns = [], - order = [], - mode = "filtered", - } = req.body || {}; - - const effective = (mode === "full") - ? { regions: [], search: { value: "" }, columns: [], order } - : { regions, search, columns, order }; - - const { filter, sort } = buildPocketBaseFilterAndSort(effective); - - res.setHeader("Content-Type", "text/csv; charset=utf-8"); - res.setHeader("Content-Disposition", `attachment; filename="historique_parcours.csv"`); - - // BOM UTF-8 pour Excel - res.write("\uFEFF"); - - const headers = [ - "Numéro du Parcours","Date de Création","Matricule","Dernier Utilisateur","Region", - "Numéro Saisine","Numéro Contrat","Produit","Type","Numéro de Portefeuille", - "Nom Intermediaire","Numéro de Client","Nom Client" - ]; - res.write(headers.join(";") + "\n"); - - /** - * OPTIMISATION : getFullList pour récupérer tous les parcours en une requête - * + batch client pour récupérer tous les clients manquants en une requête - */ - - // Construction du filtre régions (identique à getParcoursByRegionsPage) - let regFilter = ""; - if (Array.isArray(effective.regions) && effective.regions.length > 0) { - const ors = effective.regions.map(r => `dernierUtilisateur.region.nom = "${r}"`); - regFilter = `(${ors.join(" || ")})`; - } - const finalFilter = [regFilter, filter].filter(Boolean).join(" && "); - - // Format avec espaces comme dans le code original qui fonctionnait - const expandFields = "contrat, contrat.client, contrat.intermediaire, dernierUtilisateur.region"; - - // Récupération de tous les parcours en une seule requête - let allParcours; - try { - allParcours = await parcoursService.getParcoursFullList({ - filter: finalFilter, - sort: sort || "-created", - expand: expandFields, - batch: 500, - }); - } catch (err) { - logger.log("error", "Erreur récupération parcours pour export CSV:", err); - if (!res.headersSent) { - return res.status(500).send("Erreur lors de la récupération des données"); - } - try { res.end(); } catch {} - return; - } - - // Collecte des IDs clients manquants (l'expand contrat.client ne fonctionne pas en SDK 0.7.x) - const missingClientIds = []; - for (const parcours of allParcours) { - const contrat = parcours["@expand"]?.contrat; - if (contrat && contrat.client && !contrat["@expand"]?.client) { - missingClientIds.push(contrat.client); - } - } - - // Récupération batch de tous les clients manquants en une seule requête - const clientsMap = await clientService.getClientsBatch(missingClientIds); - - // Traitement des parcours - for (const parcours of allParcours) { - if (aborted) break; - - const contrat = parcours["@expand"]?.contrat || null; - const intermediaire = contrat ? (contrat["@expand"]?.intermediaire || null) : null; - - // Client : d'abord depuis l'expand, sinon depuis le batch - let client = contrat ? (contrat["@expand"]?.client || null) : null; - if (!client && contrat && contrat.client) { - client = clientsMap.get(contrat.client) || null; - } - - const lastUser = parcours["@expand"]?.dernierUtilisateur; - const region = lastUser?.["@expand"]?.region; - - const row = [ - parcours.numParcours, - fmtDateFR(parcours.created), - lastUser?.matricule || "NC", - lastUser ? `${lastUser.prenom || ""} ${lastUser.nom || ""}`.trim() || "NC" : "NC", - region ? (region.nom || "NC") : "NC", - contrat ? (contrat.numSaisine || "NC") : "NC", - contrat ? (contrat.numContrat || "NC") : "NC", - contrat ? (contrat.produit || "NC") : "NC", - contrat ? (contrat.type || "NC") : "NC", - intermediaire ? (intermediaire.numPortefeuille || "NC") : "NC", - intermediaire ? (intermediaire.nom || "NC") : "NC", - client ? (client.numClient || "NC") : "NC", - client ? (client.nom || "NC") : "NC", - ]; - - const safe = row.map(v => String(v).replaceAll(";", ",").replace(/\r?\n/g, " ")); - try { - res.write(safe.join(";") + "\n"); - } catch (werr) { - logger.log("error", werr); - aborted = true; - break; - } - } - - if (!aborted) { - res.end(); - } - } catch (error) { - logger.log("error", error); - if (!res.headersSent) { - return res.status(500).send("Erreur export CSV"); - } - try { res.end(); } catch {} - } -}); - - -// ====== UTILITAIRES XML/XLS ====== - -/** - * EXPORT XLS (SpreadsheetML 2003) - * Format XLS utilisé car XLSX est trop complexe à générer manuellement. - * Le format XLS est toujours supporté par Excel sans perte de données. - */ -router.post("/export/xls", async (req, res) => { - let aborted = false; - req.on("aborted", () => { - aborted = true; - logger.log("warn", "Client a interrompu la connexion pendant l'export XLS"); - }); - res.on("finish", () => { - logger.log("info", "Export XLS terminé"); - }); - - try { - const { - regions = [], - search = { value: "" }, - columns = [], - order = [], - mode = "filtered" - } = req.body || {}; - - const effective = (mode === "full") - ? { regions: [], search: { value: "" }, columns, order } - : { regions, search, columns, order }; - - const { filter, sort } = buildPocketBaseFilterAndSort(effective); - - const headers = [ - "Numéro du Parcours","Date de Création","Matricule","Dernier Utilisateur","Region", - "Numéro Saisine","Numéro Contrat","Produit","Type","Numéro de Portefeuille", - "Nom Intermediaire","Numéro de Client","Nom Client" - ]; - - /** - * OPTIMISATION : getFullList pour récupérer tous les parcours en une requête - * + batch client pour récupérer tous les clients manquants en une requête - */ - - // Construction du filtre régions (identique à getParcoursByRegionsPage) - let regFilter = ""; - if (Array.isArray(effective.regions) && effective.regions.length > 0) { - const ors = effective.regions.map(r => `dernierUtilisateur.region.nom = "${r}"`); - regFilter = `(${ors.join(" || ")})`; - } - const finalFilter = [regFilter, filter].filter(Boolean).join(" && "); - - // Format avec espaces comme dans le code original qui fonctionnait - const expandFields = "contrat, contrat.client, contrat.intermediaire, dernierUtilisateur.region"; - - // Récupération de tous les parcours en une seule requête - let allParcours; - try { - allParcours = await parcoursService.getParcoursFullList({ - filter: finalFilter, - sort: sort || "-created", - expand: expandFields, - batch: 500, - }); - } catch (err) { - logger.log("error", "Erreur récupération parcours pour export XLS:", err); - if (!res.headersSent) { - return res.status(500).send("Erreur lors de la récupération des données"); - } - try { res.end(); } catch {} - return; - } - - // Collecte des IDs clients manquants (l'expand contrat.client ne fonctionne pas en SDK 0.7.x) - const missingClientIds = []; - for (const parcours of allParcours) { - const contrat = parcours["@expand"]?.contrat; - if (contrat && contrat.client && !contrat["@expand"]?.client) { - missingClientIds.push(contrat.client); - } - } - - // Récupération batch de tous les clients manquants en une seule requête - const clientsMap = await clientService.getClientsBatch(missingClientIds); - - const fileName = (mode === "full") - ? "historique_parcours_complet.xls" - : "historique_parcours_filtre.xls"; - - res.setHeader("Content-Type", "application/vnd.ms-excel; charset=utf-8"); - res.setHeader("Content-Disposition", `attachment; filename="${fileName}"`); - - // En-tête SpreadsheetML 2003 - res.write( -` - - - - - - - - -` - ); - - res.write( - `` + - headers.map(h => `${xmlEsc(h)}`).join("") + - `\n` - ); - - // Traitement des parcours - for (const parcours of allParcours) { - const contrat = parcours["@expand"]?.contrat || null; - const intermediaire = contrat ? (contrat["@expand"]?.intermediaire || null) : null; - - // Client : d'abord depuis l'expand, sinon depuis le batch - let client = contrat ? (contrat["@expand"]?.client || null) : null; - if (!client && contrat && contrat.client) { - client = clientsMap.get(contrat.client) || null; - } - - const lastUser = parcours["@expand"]?.dernierUtilisateur; - const region = lastUser?.["@expand"]?.region; - - const row = [ - parcours.numParcours, - fmtDateFR(parcours.created), - lastUser?.matricule || "NC", - lastUser ? `${lastUser.prenom || ""} ${lastUser.nom || ""}`.trim() || "NC" : "NC", - region ? (region.nom || "NC") : "NC", - contrat ? (contrat.numSaisine || "NC") : "NC", - contrat ? (contrat.numContrat || "NC") : "NC", - contrat ? (contrat.produit || "NC") : "NC", - contrat ? (contrat.type || "NC") : "NC", - intermediaire ? (intermediaire.numPortefeuille || "NC") : "NC", - intermediaire ? (intermediaire.nom || "NC") : "NC", - client ? (client.numClient || "NC") : "NC", - client ? (client.nom || "NC") : "NC", - ].map(v => String(v).replace(/\r?\n/g, " ")); - - res.write(rowXml(row) + "\n"); - } - - // Fermeture du fichier XML SpreadsheetML - res.write( -`
-
-
` - ); - res.end(); - - } catch (error) { - logger.log("error", error); - if (!res.headersSent) return res.status(500).send("Erreur export XLS"); - try { res.end(); } catch {} - } -}); - -/** - * Route GET /details/:numParcours - * Récupère les détails complets d'un parcours (parcours + contrat + fiche produit) - * Utilisé pour afficher le panneau de détails dans la datatable - */ -router.get("/details/:numParcours", async (req, res) => { - try { - const { numParcours } = req.params; - const parcours = await parcoursService.getDeepDetailsByNumParcours(numParcours); - if (!parcours) return res.json({ valid: false, error: "Parcours introuvable" }); - - /** - * Extraction des données pour faciliter l'accès côté frontend - */ - const contrat = parcours?.["@expand"]?.contrat || null; - const produit = (contrat?.produit || "").toUpperCase(); - - /** - * Récupération de la fiche produit selon le type (TPPC, RC, FAC) - * Si l'expand n'a pas fonctionné, on essaie de récupérer directement via l'ID - */ - let produitRecord = null; - if (produit === "TPPC") { - produitRecord = contrat?.["@expand"]?.tppc || null; - } else if (produit === "RC") { - produitRecord = contrat?.["@expand"]?.rc || null; - // Si l'expand n'a pas fonctionné mais qu'on a l'ID, on récupère directement - if (!produitRecord && contrat?.rc) { - try { - const rcId = typeof contrat.rc === 'string' ? contrat.rc : contrat.rc?.id || contrat.rc; - if (rcId) { - const rcService = require("../services/rcService"); - const rcData = await rcService.getRCbyId(rcId); - // fetchInfoByCriteria retourne directement l'item, pas un objet avec items - produitRecord = rcData || null; - } - } catch (e) { - logger.log("info", `Erreur récupération RC directe pour ${numParcours}: ${e.message}`); - } - } - } else if (produit === "FAC") { - produitRecord = contrat?.["@expand"]?.fac || null; - // Si l'expand n'a pas fonctionné mais qu'on a l'ID, on récupère directement - if (!produitRecord && contrat?.fac) { - try { - const facId = typeof contrat.fac === 'string' ? contrat.fac : contrat.fac?.id || contrat.fac; - if (facId) { - const facService = require("../services/facService"); - const facData = await facService.getFACbyId(facId); - // fetchInfoByCriteria retourne directement l'item, pas un objet avec items - produitRecord = facData || null; - if (!produitRecord) { - logger.log("warn", `FAC non trouvé pour ID ${facId} (parcours ${numParcours})`); - } - } else { - logger.log("warn", `Pas d'ID FAC dans contrat pour parcours ${numParcours}`); - } - } catch (e) { - logger.log("warn", `Erreur récupération FAC directe pour ${numParcours}: ${e.message}`); - } - } else if (!produitRecord && !contrat?.fac) { - logger.log("warn", `Contrat FAC sans relation FAC pour parcours ${numParcours}`); - } - } - - return res.json({ - valid: true, - produit, - parcours, - contrat, - produitRecord, - }); - } catch (e) { - logger.log("error", e); - return res.status(500).json({ valid: false, error: "Erreur recherche détails" }); - } -}); - - -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/ecole/src/db/LICENSE.md b/ecole/src/db/LICENSE.md deleted file mode 100644 index 26265aa2..00000000 --- a/ecole/src/db/LICENSE.md +++ /dev/null @@ -1,17 +0,0 @@ -The MIT License (MIT) -Copyright (c) 2022, Gani Georgiev - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software -and associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, -sublicense, and/or sell copies of the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/ecole/src/db/pb_data/data.db b/ecole/src/db/pb_data/data.db index d7998fd6..c7402141 100644 Binary files a/ecole/src/db/pb_data/data.db and b/ecole/src/db/pb_data/data.db differ diff --git a/ecole/src/db/pb_data/data.db-wal b/ecole/src/db/pb_data/data.db-wal index bdc120b2..e69de29b 100644 Binary files a/ecole/src/db/pb_data/data.db-wal and b/ecole/src/db/pb_data/data.db-wal differ diff --git a/ecole/src/db/pb_data/logs.db b/ecole/src/db/pb_data/logs.db index 0c04c1b4..696e894e 100644 Binary files a/ecole/src/db/pb_data/logs.db and b/ecole/src/db/pb_data/logs.db differ diff --git a/ecole/src/db/pb_data/logs.db-shm b/ecole/src/db/pb_data/logs.db-shm new file mode 100644 index 00000000..99f46fe8 Binary files /dev/null and b/ecole/src/db/pb_data/logs.db-shm differ diff --git a/ecole/src/db/pb_data/logs.db-wal b/ecole/src/db/pb_data/logs.db-wal new file mode 100644 index 00000000..b01f2c5d Binary files /dev/null and b/ecole/src/db/pb_data/logs.db-wal differ diff --git a/ecole/src/db/pocketbase b/ecole/src/db/pocketbase deleted file mode 100755 index c1d7d104..00000000 Binary files a/ecole/src/db/pocketbase and /dev/null differ diff --git a/ecole/src/middlewares/jwtMiddleware.js b/ecole/src/middlewares/jwtMiddleware.js index 8a2f378c..36089bc1 100644 --- a/ecole/src/middlewares/jwtMiddleware.js +++ b/ecole/src/middlewares/jwtMiddleware.js @@ -1,4 +1,5 @@ const jwt = require('jsonwebtoken'); +const logger = require('../utils/logger'); module.exports = function (req, res, next) { const token = req.headers['authorization']?.split(' ')[1]; diff --git a/ecole/src/services/clientService.js b/ecole/src/services/clientService.js index 0b94dcd0..d21355f3 100644 --- a/ecole/src/services/clientService.js +++ b/ecole/src/services/clientService.js @@ -14,55 +14,7 @@ async function getClient(id) { return globalService.fetchInfoByCriteria("client", criteria); } -/** - * Récupère plusieurs clients en plusieurs requêtes batch (optimisation pour exports) - * Découpe en chunks de 50 IDs pour éviter les filtres trop longs - * SDK 0.7.x : getFullList(collection, batchSize, options) - * @param {string[]} clientIds - Tableau d'IDs de clients - * @returns {Map} - Map des clients par ID - */ -async function getClientsBatch(clientIds) { - const clientMap = new Map(); - if (!clientIds || clientIds.length === 0) return clientMap; - - // Filtrer les IDs valides et uniques - const uniqueIds = [...new Set(clientIds.filter(id => id && typeof id === 'string'))]; - if (uniqueIds.length === 0) return clientMap; - - // Découper en chunks de 50 pour éviter les filtres trop longs - const CHUNK_SIZE = 50; - const chunks = []; - for (let i = 0; i < uniqueIds.length; i += CHUNK_SIZE) { - chunks.push(uniqueIds.slice(i, i + CHUNK_SIZE)); - } - - // Traiter chaque chunk - for (const chunk of chunks) { - try { - // Construire le filtre OR pour ce chunk - const filter = chunk.map(id => `id = "${id}"`).join(" || "); - - // SDK 0.7.x : getFullList(collection, batchSize, options) - const clients = await db.records.getFullList("client", 500, { - filter: filter, - }); - - // Ajouter à la map - clients.forEach(client => { - if (client && client.id) { - clientMap.set(client.id, client); - } - }); - } catch (err) { - logger.log("warn", `Erreur récupération clients chunk (${chunk.length} IDs):`, err?.message || String(err)); - } - } - - return clientMap; -} - module.exports = { createClient, getClient, - getClientsBatch, }; \ No newline at end of file diff --git a/ecole/src/services/globalService.js b/ecole/src/services/globalService.js index 2ef716e1..cb539a50 100644 --- a/ecole/src/services/globalService.js +++ b/ecole/src/services/globalService.js @@ -30,15 +30,7 @@ async function fetchInfoByCriteria(collection, criteria) { return resultList.items[0]; } } catch (error) { - /** - * Gestion silencieuse des erreurs d'abort (requêtes interrompues) - * Ces erreurs sont normales lors de requêtes parallèles et ne doivent pas être loggées - */ - if (error?.isAbort || error?.name?.includes("Abort") || error?.status === 0) { - return null; - } - // Autres erreurs loggées en info (pas en error) pour éviter le bruit dans les logs - logger.log("info", `Erreur récupération ${collection}:`, error?.message || error); + logger.log("error", error); } return null; @@ -76,71 +68,10 @@ function cleanDoubleSpaces(inputString) { return inputString.replace(/\s{2,}/g, " "); } -/** - * Formate une date ISO en format français (jj/mm/aaaa) - * @param {string|Date} iso - Date au format ISO - * @param {boolean} withTime - Inclure l'heure (défaut: false) - * @returns {string} Date formatée (dd/mm/yyyy ou dd/mm/yyyy hh:mm) - */ -function fmtDateFR(iso, withTime = false) { - if (!iso) return "NC"; - - const d = new Date(iso); - if (isNaN(d.getTime())) return "NC"; - - const dd = String(d.getDate()).padStart(2, "0"); - const mm = String(d.getMonth() + 1).padStart(2, "0"); - const yyyy = d.getFullYear(); - - if (!withTime) { - return `${dd}/${mm}/${yyyy}`; - } - - const hh = String(d.getHours()).padStart(2, "0"); - const mi = String(d.getMinutes()).padStart(2, "0"); - return `${dd}/${mm}/${yyyy} ${hh}:${mi}`; -} - -/** - * Échappe les caractères spéciaux XML - * @param {string} s - Chaîne à échapper - * @returns {string} Chaîne échappée - */ -function xmlEsc(s) { - return String(s ?? "") - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); -} - -/** - * Génère une cellule XML pour Excel - * @param {string} v - Valeur de la cellule - * @returns {string} Cellule XML formatée - */ -function cellXml(v) { - return `${xmlEsc(v)}`; -} - -/** - * Génère une ligne XML pour Excel - * @param {Array} cells - Tableau de valeurs pour les cellules - * @returns {string} Ligne XML formatée - */ -function rowXml(cells) { - return `${cells.map(cellXml).join("")}`; -} - module.exports = { getRecordIdFromFieldValue, fetchInfoByCriteria, updateRecordFromData, cleanDoubleSpaces, customFormatNumber, - fmtDateFR, - xmlEsc, - cellXml, - rowXml, }; diff --git a/ecole/src/services/parcoursService.js b/ecole/src/services/parcoursService.js index 8a18b309..95dfaa95 100644 --- a/ecole/src/services/parcoursService.js +++ b/ecole/src/services/parcoursService.js @@ -1,259 +1,109 @@ -// services/parcoursService.js const { db } = require("../db/db-connect"); const logger = require("../utils/logger"); const globalService = require("../services/globalService"); -/** - * Récupère un parcours par son numéro (avec expand utiles) - */ async function getParcoursByNumParcours(numParcours) { - const criteria = { - filter: `numParcours='${numParcours}'`, - expand: [ - "dernierUtilisateur.region", - "contrat", - "contrat.client", - "contrat.intermediaire" - ].join(",") - }; + const criteria = {filter: `numParcours='${numParcours}'`, expand: `dernierUtilisateur.region, contrat`}; + return globalService.fetchInfoByCriteria("parcours", criteria); } -/** - * Full list (batch côté PocketBase). | Fetch l'ensemble de la BD via chunk "batch" - * SDK 0.7.x : getFullList(collection, batchSize, options) - */ -async function getParcoursFullList({ filter, sort, expand, fields, batch = 500 }) { - const options = { - sort: sort || "-created", - }; - - // Ajouter expand si défini - if (expand) { - options.expand = expand; - } - - // Ajouter fields si défini - if (fields) { - options.fields = fields; - } - - // Ajouter filter SEULEMENT s'il n'est pas vide (SDK 0.7.x rejette les filtres vides) - if (filter && filter.trim() !== "") { - options.filter = filter; - } - - // SDK 0.7.x : getFullList(collection, batchSize, options) - return db.records.getFullList("parcours", batch, options); -} -/** - * Pagination multi-régions + filtres/tri optionnels (server-side DataTables) - * – Parcours une seule fois db par requête - * @param {string[]} regions - * @param {number} page - * @param {number} perPage - * @param {{filter?: string, sort?: string}} opts - */ -async function getParcoursByRegionsPage(regions = [], page = 1, perPage = 10, opts = {}) { +// get All parcours saved in DB +async function getAllParcours() { try { - let regFilter = ""; - if (Array.isArray(regions) && regions.length > 0) { - const ors = regions.map(r => `dernierUtilisateur.region.nom = "${r}"`); - regFilter = `(${ors.join(" || ")})`; - } + const criteria = {expand: "dernierUtilisateur, contrat, region"}; + const resultList = await db.records.getList("parcours", 1, 200, criteria); - const filter = [regFilter, opts.filter].filter(Boolean).join(" && "); - - /** - * Récupération des parcours avec expands nécessaires - * Note: L'expand de contrat.client ne fonctionne pas toujours, - * d'où la nécessité d'un fallback dans le contrôleur - */ - const list = await db.records.getList("parcours", page, perPage, { - sort: opts.sort || "-created", - filter: filter || "", - expand: [ - "contrat", - "contrat.client", - "contrat.intermediaire", - "dernierUtilisateur.region" - ].join(","), - }); - - return { - page: list.page, - perPage: list.perPage, - totalItems: list.totalItems, - totalPages: list.totalPages, - items: list.items, - }; + return resultList; } catch (error) { - logger.log('error', error); - throw error; + logger.log('error', error); + return null; + } +} + +// get all parcours filtred on region +async function getParcoursByRegion(regionUser) { + try { + // Récupérer les enregistrements de la collection "parcours" + const filter = `dernierUtilisateur.region.nom = "${regionUser}"`; + const parcoursRecords = await db.records.getFullList("parcours", 200, { + sort: "-created", + filter: filter, + expand: "contrat, dernierUtilisateur.region, contrat.intermediaire", + }); + + // Récupérer les relations client pour chaque contrat + for (const record of parcoursRecords) { + const contrat = record["@expand"].contrat; + if (contrat && contrat.client) { + const clientRecord = await db.records.getOne("client", contrat.client); + record["@expand"].contrat.client = clientRecord; + } + } + + return parcoursRecords; + } catch (error) { + logger.log('error', error); + throw error; } } -/** - * Création d'un parcours vide - */ async function createNewEmptyParcours(numParcours) { try { - const data = { ["numParcours"]: numParcours }; - const record = await db.records.create("parcours", data); - if (record) { - return record.id; - } else { - return null; - } + const data = { ["numParcours"]: numParcours }; + const record = await db.records.create("parcours", data); + + if (record) { + return record.id; + } else { + return null; + } } catch (error) { - logger.log("error", error); - return null; + logger.log("error", error); + return null; } } -/** - * MAJ d'un champ d'un parcours - */ async function updateFieldValueParcours(id, field, value) { try { - const data = { [field]: value }; - const record = await db.records.update("parcours", id, data); - if (record) { - return record.id; - } else { - return null; - } + const data = { [field]: value }; + const record = await db.records.update("parcours", id, data); + + if (record) { + return record.id; + } else { + return null; + } } catch (error) { - logger.log("error", error); - return null; + logger.log("error", error); + return null; } } -/** - * Génère le prochain numéro de parcours - */ async function getNewParcoursNumber() { try { - const list = await db.records.getList("parcours", 1, 1, { sort: "-numParcours" }); - const last = list?.items?.[0]; - if (!last?.numParcours) return null; + // fetch a paginated records list en utilisant le filtre pour le parcours + const resultList = await db.records.getFullList("parcours", 99999999, {sort: "-numParcours",}); - const numericValue = parseInt(String(last.numParcours).substring(1), 10); - if (Number.isNaN(numericValue)) return null; + if (resultList.length > 0) { + const lastNumParcours = resultList[0].numParcours; - const next = numericValue + 1; - return "P" + next.toString().padStart(9, "0"); + // Extrait les chiffres du numéro de parcours + const numericPart = lastNumParcours.substring(1); // Supprime le "P" initial + const numericValue = parseInt(numericPart, 10); + + if (!isNaN(numericValue)) { + const newNumericValue = numericValue + 1; + const newNumParcours = "P" + newNumericValue.toString().padStart(9, "0"); + + return newNumParcours; + } + } else { + return null; + } } catch (error) { - logger.log("error", error); - return null; - } -} - -// --- Section détails profonds (contrat + fiche produit) --- // - -/** - * Récupère un parcours (via numParcours) avec les expands utiles pour détails. - */ -async function getParcoursForDetails(numParcours) { - try { - const list = await db.records.getList("parcours", 1, 1, { - filter: `numParcours='${numParcours}'`, - expand: [ - "contrat", - "contrat.client", - "contrat.intermediaire", - "dernierUtilisateur.region" - ].join(","), - }); - return list?.items?.[0] || null; - } catch (e) { - logger.log("error", e); - return null; - } -} - -/** - * Mappe un libellé produit vers la collection PocketBase (à ajuster si changement de parcours). - */ -function mapProduitToCollection(produitRaw = "") { - const p = String(produitRaw || "").trim().toUpperCase(); - const map = { - "TPPC": "tppc", - "RC": "rc", - "FAC": "fac", - }; - return map[p] || null; -} - -/** - * Récupère la fiche produit pour un contrat donné. - * On tente d'abord par relation "contrat = contratId" si elle existe, - * sinon fallback par "numContrat = x" si jamais la fiche stocke le numéro. - */ -async function getProduitRecordForContrat(contrat, opts = {}) { - try { - if (!contrat) return null; - const collection = mapProduitToCollection(contrat.produit); - if (!collection) return null; - - // Tente via une relation directe "contrat" (champ le plus propre) - try { - const record = await db.records.getFirstListItem(collection, `contrat='${contrat.id}'`, { - }); - if (record) return record; - } catch (_) { /* ignore, on tente le fallback */ } - - // Fallback - if (contrat.numContrat) { - try { - const record = await db.records.getFirstListItem(collection, `numContrat='${contrat.numContrat}'`, {}); - if (record) return record; - } catch (_) { /* ignore */ } - } - - return null; - } catch (e) { - logger.log("error", e); - return null; - } -} - -/** - * reformatage texte - a virer - */ -function escPB(s = "") { - return String(s).replace(/"/g, '\\"'); -} - - -/** - * Détails complets: parcours + contrat + fiche produit - */ -async function getDeepDetailsByNumParcours(numParcours) { - try { - const filter = `numParcours = "${escPB(numParcours)}"`; - - const list = await db.records.getList("parcours", 1, 1, { - filter, - expand: [ - "contrat", - "contrat.client", - "contrat.intermediaire", - "dernierUtilisateur.region", - // produit lié - "contrat.tppc", - "contrat.rc", - "contrat.fac", - // sous-relations TPPC - "contrat.tppc.tarif", - "contrat.tppc.projet", - ].join(","), - }); - - return list?.items?.[0] || null; - } catch (e) { - logger.log("error", e); - return null; + logger.log("error", error); + return null; } } @@ -262,10 +112,6 @@ module.exports = { getParcoursByNumParcours, createNewEmptyParcours, updateFieldValueParcours, - getParcoursByRegionsPage, - getParcoursFullList, - getParcoursForDetails, - getProduitRecordForContrat, - getDeepDetailsByNumParcours, - mapProduitToCollection, -}; \ No newline at end of file + getAllParcours, + getParcoursByRegion, +}; diff --git a/ecole/src/services/rcService.js b/ecole/src/services/rcService.js index d5ef3046..736df79a 100644 --- a/ecole/src/services/rcService.js +++ b/ecole/src/services/rcService.js @@ -1,19 +1,10 @@ const { db } = require('../db/db-connect'); const logger = require('../utils/logger'); -const globalService = require("../services/globalService"); async function createRc(data) { return await db.records.create('rc', data); } -async function getRCbyId(id) { - const criteria = { - filter: `id='${id}'` - }; - return globalService.fetchInfoByCriteria("rc", criteria); -} - module.exports = { - createRc, - getRCbyId + createRc }; \ No newline at end of file diff --git a/ecole/src/utils/renderHelper.js b/ecole/src/utils/renderHelper.js index b9c6fdc6..89c9598e 100644 --- a/ecole/src/utils/renderHelper.js +++ b/ecole/src/utils/renderHelper.js @@ -1,13 +1,6 @@ const ejs = require('ejs'); const path = require('path'); -/** - * Rend une page EJS - * @param {string} routePath - Chemin vers le fichier EJS - * @param {Object} res - Objet response Express - * @param {Object} options - Options à passer au template - * @param {boolean} fragment - Si true, envoie uniquement le fragment sans layout - */ function renderPage(routePath, res, options = {}, fragment = false) { ejs.renderFile(path.join(process.cwd(), 'views', routePath), options, (err, str) => { if (err) { diff --git a/ecole/views/historiqueParcours.ejs b/ecole/views/historiqueParcours.ejs index 3db8f398..06f42766 100644 --- a/ecole/views/historiqueParcours.ejs +++ b/ecole/views/historiqueParcours.ejs @@ -31,6 +31,38 @@
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
diff --git a/ecole/views/layout.ejs b/ecole/views/layout.ejs index 270d4d75..5a423aad 100644 --- a/ecole/views/layout.ejs +++ b/ecole/views/layout.ejs @@ -10,44 +10,59 @@ <%- typeof extraHeadContent !=='undefined' ? extraHeadContent : '' %> - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + +
+ + + + <%- include('partials/navbar') %> -
- <%- typeof body !=='undefined' ? body : '' %> -
+ +
+ <%- typeof body !=='undefined' ? body : '' %> +
- - - - - - - - - - + + + + + + + + + + + + + +
- - \ No newline at end of file + diff --git a/ecole/views/projetformtppc.ejs b/ecole/views/projetformtppc.ejs index 87e55aa6..393a2ae5 100644 --- a/ecole/views/projetformtppc.ejs +++ b/ecole/views/projetformtppc.ejs @@ -399,7 +399,7 @@ - + diff --git a/ecole/views/tarifformtppc.ejs b/ecole/views/tarifformtppc.ejs index 64d9a439..be55fffd 100644 --- a/ecole/views/tarifformtppc.ejs +++ b/ecole/views/tarifformtppc.ejs @@ -173,7 +173,7 @@ + placeholder="Non défini" min="0" value="0" />