Lm : a jour
This commit is contained in:
parent
77bf98e6cd
commit
d6f06dfd7b
|
|
@ -4,6 +4,9 @@ npm-debug.log*
|
|||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Classique
|
||||
package-lock.json
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -445,3 +452,63 @@ a.grille-garanties:hover{
|
|||
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); }
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
@ -301,14 +206,3 @@ 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -161,234 +161,3 @@ async function loadContrat(idContrat) {
|
|||
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 `<div style="display:flex; gap:8px; margin:2px 0;">
|
||||
<div style="min-width:220px;"><strong>${label}</strong> :</div>
|
||||
<div>${v}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 `<div style="display:grid;grid-template-columns:repeat(2,minmax(280px,1fr));gap:14px;">${innerLeft}${innerRight}</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, '<').replace(/>/g, '>');
|
||||
// Description peut contenir du HTML (comme <br>), donc on ne l'échappe pas complètement
|
||||
// mais on s'assure qu'elle est une string
|
||||
const safeDescription = String(description);
|
||||
|
||||
return `
|
||||
<div style="padding: 20px; text-align: center; background-color: ${config.bgColor}; border: 1px solid ${config.borderColor}; border-radius: 4px; margin: 10px 0; width: 100%; box-sizing: border-box;">
|
||||
<p style="color: ${config.textColor}; font-weight: 600; margin: 0 0 10px 0;">
|
||||
<i class="fas ${config.icon}" style="margin-right: 8px;"></i>
|
||||
${safeTitle}
|
||||
</p>
|
||||
<p style="color: ${config.textColor}; margin: 0; font-size: 0.9rem;">
|
||||
${safeDescription}
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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";
|
||||
};
|
||||
});
|
||||
|
|
@ -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));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
`<button type="button" id="btnReprendre" class="btn" onclick="window.location.href='${(!contrat || (!contrat.numSaisine && !contrat.numContrat)) ? `/contrat?numParcours=${parcours.numParcours}` : `/navParcours?numParcours=${parcours.numParcours}&submenu=client`}'"><i class="fas fa-arrow-right"></i></button>`,
|
||||
`<button type="button" id="btnGenerate" class="btn" data-produit="${produit}" data-num-parcours="${parcours.numParcours}" ${( !contrat || contrat.produit == "" || (contrat.fac == "" && contrat.rc == "" && contrat.tppc == "") || contrat.client == "" || contrat.intermediaire == "" ) ? 'disabled' : ''}><i class="fa-solid fa-file-arrow-down"></i></button>`
|
||||
]);
|
||||
} 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", "", ""]);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
draw: Number(draw),
|
||||
recordsTotal: result.totalItems,
|
||||
recordsFiltered: result.totalItems,
|
||||
data: rows
|
||||
});
|
||||
} 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(
|
||||
`<?xml version="1.0"?>
|
||||
<?mso-application progid="Excel.Sheet"?>
|
||||
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office"
|
||||
xmlns:x="urn:schemas-microsoft-com:office:excel"
|
||||
xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
|
||||
xmlns:html="http://www.w3.org/TR/REC-html40">
|
||||
<Styles>
|
||||
<Style ss:ID="Default" ss:Name="Normal">
|
||||
<Alignment ss:Vertical="Center"/>
|
||||
<Borders/>
|
||||
<Font ss:FontName="Calibri" ss:Size="11"/>
|
||||
<Interior/>
|
||||
<NumberFormat/>
|
||||
<Protection/>
|
||||
</Style>
|
||||
<Style ss:ID="Header">
|
||||
<Font ss:Bold="1"/>
|
||||
<Alignment ss:Horizontal="Center" ss:Vertical="Center"/>
|
||||
</Style>
|
||||
</Styles>
|
||||
<Worksheet ss:Name="Historique">
|
||||
<Table>
|
||||
`
|
||||
);
|
||||
|
||||
res.write(
|
||||
`<Row ss:StyleID="Header">` +
|
||||
headers.map(h => `<Cell><Data ss:Type="String">${xmlEsc(h)}</Data></Cell>`).join("") +
|
||||
`</Row>\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(
|
||||
` </Table>
|
||||
</Worksheet>
|
||||
</Workbook>`
|
||||
);
|
||||
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})`);
|
||||
}
|
||||
if (data) {
|
||||
res.json({ valid: true, data });
|
||||
} 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}`);
|
||||
}
|
||||
res.json({ valid: data });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log("error", error);
|
||||
|
||||
return res.json({
|
||||
valid: true,
|
||||
produit,
|
||||
parcours,
|
||||
contrat,
|
||||
produitRecord,
|
||||
res.status(500).json({
|
||||
valid: false,
|
||||
error: "Erreur lors de la récupération des parcours.",
|
||||
});
|
||||
} catch (e) {
|
||||
logger.log("error", e);
|
||||
return res.status(500).json({ valid: false, error: "Erreur recherche détails" });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -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.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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<string, Object>} - 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,
|
||||
};
|
||||
|
|
@ -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, """)
|
||||
.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 `<Cell><Data ss:Type="String">${xmlEsc(v)}</Data></Cell>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère une ligne XML pour Excel
|
||||
* @param {Array<string>} cells - Tableau de valeurs pour les cellules
|
||||
* @returns {string} Ligne XML formatée
|
||||
*/
|
||||
function rowXml(cells) {
|
||||
return `<Row>${cells.map(cellXml).join("")}</Row>`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getRecordIdFromFieldValue,
|
||||
fetchInfoByCriteria,
|
||||
updateRecordFromData,
|
||||
cleanDoubleSpaces,
|
||||
customFormatNumber,
|
||||
fmtDateFR,
|
||||
xmlEsc,
|
||||
cellXml,
|
||||
rowXml,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,105 +1,58 @@
|
|||
// 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);
|
||||
|
||||
return resultList;
|
||||
} catch (error) {
|
||||
logger.log('error', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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(","),
|
||||
// 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",
|
||||
});
|
||||
|
||||
return {
|
||||
page: list.page,
|
||||
perPage: list.perPage,
|
||||
totalItems: list.totalItems,
|
||||
totalPages: list.totalPages,
|
||||
items: list.items,
|
||||
};
|
||||
// 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 {
|
||||
|
|
@ -111,13 +64,11 @@ async function createNewEmptyParcours(numParcours) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
|
@ -129,143 +80,38 @@ async function updateFieldValueParcours(id, field, value) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getNewParcoursNumber,
|
||||
getParcoursByNumParcours,
|
||||
createNewEmptyParcours,
|
||||
updateFieldValueParcours,
|
||||
getParcoursByRegionsPage,
|
||||
getParcoursFullList,
|
||||
getParcoursForDetails,
|
||||
getProduitRecordForContrat,
|
||||
getDeepDetailsByNumParcours,
|
||||
mapProduitToCollection,
|
||||
getAllParcours,
|
||||
getParcoursByRegion,
|
||||
};
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,38 @@
|
|||
<div id="divToggleSearch">
|
||||
<button class="btn" id="toggleSearch" type="button">Activer la recherche par colonne</button>
|
||||
</div>
|
||||
<div id="checkRegionAdmin">
|
||||
<div class="checkbox-wrapper-1">
|
||||
<label>
|
||||
<input type="checkbox" id="zone1" name="zone1" class="filled-in" />
|
||||
<span class="checkboxRegion" id="cbx-42">ILE DE FRANCE</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox-wrapper-2">
|
||||
<label>
|
||||
<input type="checkbox" id="zone2" name="zone2" class="filled-in" />
|
||||
<span class="checkboxRegion" id="cbx-43">SUD OUEST</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox-wrapper-3">
|
||||
<label>
|
||||
<input type="checkbox" id="zone3" name="zone3" class="filled-in" />
|
||||
<span class="checkboxRegion" id="cbx-44">SUD EST</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox-wrapper-4">
|
||||
<label>
|
||||
<input type="checkbox" id="zone4" name="zone4" class="filled-in" />
|
||||
<span class="checkboxRegion" id="cbx-45">OUEST</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox-wrapper-5">
|
||||
<label>
|
||||
<input type="checkbox" id="zone5" name="zone5" class="filled-in" />
|
||||
<span class="checkboxRegion" id="cbx-46">NORD EST</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -25,13 +25,25 @@
|
|||
<link rel="stylesheet" href="/css/global.css">
|
||||
<!-- DataTables CSS -->
|
||||
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/2.0.8/css/dataTables.dataTables.css">
|
||||
<!-- Loader CSS : style pour l'affichage du loader -->
|
||||
<link rel="stylesheet" href="/css/loader.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main>
|
||||
|
||||
<!-- Loader -->
|
||||
<div id="loader-overlay" class="hidden">
|
||||
<div class="loader-spin-wrap">
|
||||
<div class="loader-spin"></div>
|
||||
</div>
|
||||
<div id="error-message" class="helper-text error"></div>
|
||||
</div>
|
||||
|
||||
<!-- Navbar -->
|
||||
<%- include('partials/navbar') %>
|
||||
|
||||
<!-- Contenu dynamique -->
|
||||
<div class="container">
|
||||
<%- typeof body !=='undefined' ? body : '' %>
|
||||
</div>
|
||||
|
|
@ -46,8 +58,11 @@
|
|||
<script src="/js/verif-form.js"></script>
|
||||
<!-- Feuille de script pour les donnée static JSON de saisie formulaire -->
|
||||
<script src="/js/json/json-verif-form.js"></script>
|
||||
<!-- Feuille de script pour le bon fonctionnement du loader -->
|
||||
<script src="/js/loader.js"></script>
|
||||
<!-- Script pour la navigation AJAX -->
|
||||
<script src="/js/navigation.js"></script>
|
||||
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -399,7 +399,7 @@
|
|||
<td><input type="text" name="genre" id="genreVehicule" placeholder="VP" /></td>
|
||||
<td><input type="text" name="type" id="typeVehicule" placeholder="Non défini" /></td>
|
||||
<td><input type="text" name="immat" id="immatVehicule" placeholder="AA-999-AA" /></td>
|
||||
<td><input type="text" name="capital" id="capitalVehicule" placeholder="10 000 €" /></td>
|
||||
<td><input type="text" name="capital" id="capitalVeh" placeholder="10 000 €" /></td>
|
||||
<td><button class="btn" type="button" id="btnAddVehicule"><i
|
||||
class="material-icons">add</i></button></td>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@
|
|||
</td>
|
||||
<td>
|
||||
<input type="number" name="nbVehiculesTarif" id="nbVehiculesTarif"
|
||||
placeholder="Non défini" value="0" />
|
||||
placeholder="Non défini" min="0" value="0" />
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" name="primeVehTarif" id="primeVehTarif" min="0" placeholder="Non défini" />
|
||||
|
|
|
|||
Loading…
Reference in New Issue