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 `
`;
-}
-
-/**
- * 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 : '' %>
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-