Compare commits

..

No commits in common. "Hp" and "main" have entirely different histories.
Hp ... main

225 changed files with 3323 additions and 4884 deletions

View File

@ -1,5 +0,0 @@
DB_URL=http://127.0.0.1:8091/
DB_ADMIN=admin@axa.fr
DB_PASSWORD=DTadmin123TT
NODE_ENV=developpement
PORT=8082

30
ecole/.gitignore vendored
View File

@ -1,30 +0,0 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Logs
logs/
*.log
# Environment variables
.env
.env.local
.env.*.local
# OS files
.DS_Store
Thumbs.db
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Build outputs
dist/
build/

View File

@ -1,279 +0,0 @@
// Module IIFE pour éviter la pollution de l'espace global
(function () {
// Variables globales du module
let parcours, contrat, client, matricule;
// Initialisation du formulaire et des données
function init() {
const token = localStorage.getItem('jwtToken');
if (token) {
const decoded = jwt_decode(token);
matricule = decoded.userMatricule;
}
// Accéder aux informations stockées du parcours
parcours = JSON.parse(sessionStorage.getItem('parcours'));
contrat = JSON.parse(sessionStorage.getItem('contrat'));
client = contrat?.["@expand"]?.client || null;
console.log("Matricule user actuel:", matricule);
console.log("Initialisation pour formulaire client :", contrat);
// Materialize init Modal
var modals = document.querySelectorAll('.modal');
M.Modal.init(modals);
// Appel des différentes fonctions d'initialisation
setupEventListeners();
populateFormData();
updateSubmitButtonState('clientForm');
}
// Configuration des écouteurs d'événements
function setupEventListeners() {
document.getElementById('clientFormBtn').addEventListener('click', handleSubmitForm);
document.getElementById('creditsafe').addEventListener('click', (event) => handleOpenLink(event, 'creditsafeURL'));
document.getElementById('easyQP').addEventListener('click', (event) => handleOpenLink(event, 'easyQPURL'));
document.getElementById('refNoteFi').addEventListener('click', (event) => handleOpenLink(event, 'refNoteFiURL'));
document.getElementById('cl063-client').addEventListener('click', (event) => handleExtractClient(event));
document.getElementById('modalExtraireClient').addEventListener('click', (event) => handleModalExtract(event));
// Controle de saisie et format sur les champs du formulaire
document.getElementById('noteFinanciereClient').addEventListener('input', function () {
validateField('noteFinanciereClient', true);
updateSubmitButtonState('clientForm');
});
document.getElementById('numClient').addEventListener('input', function () {
validateField('numClient', true);
updateSubmitButtonState('clientForm');
});
document.getElementById('nomClient').addEventListener('input', function () {
validateField('nomClient', true);
updateSubmitButtonState('clientForm');
});
document.getElementById('emailClient').addEventListener('input', function () {
validateField('emailClient', true);
updateSubmitButtonState('clientForm');
});
document.getElementById('adresseClient').addEventListener('input', function () {
validateField('adresseClient', true);
updateSubmitButtonState('clientForm');
});
document.getElementById('codePostalClient').addEventListener('input', function () {
validateField('codePostalClient', true);
updateSubmitButtonState('clientForm');
});
document.getElementById('villeClient').addEventListener('input', function () {
validateField('villeClient', true);
updateSubmitButtonState('clientForm');
});
document.getElementById('modalNumClient').addEventListener('input', function () {
validateField('modalNumClient', true);
updateSubmitButtonState('modalExtraireClient');
});
}
// Peupler le formulaire avec les données
function populateFormData() {
const clientStorage = JSON.parse(sessionStorage.getItem('tmp'));
if (client) {
document.getElementById('nomClient').value = client.nom || '';
document.getElementById('numClient').value = client.numClient || '';
document.getElementById('adresseClient').value = client.adresse || '';
document.getElementById('emailClient').value = client.mail || '';
document.getElementById('codePostalClient').value = client.codePostal || '';
document.getElementById('villeClient').value = client.ville || '';
document.getElementById('noteFinanciereClient').value = client.noteFinanciere || '';
}
if (clientStorage) {
document.getElementById('nomClient').value = clientStorage.nomClient || '';
document.getElementById('numClient').value = clientStorage.numClient || '';
document.getElementById('adresseClient').value = clientStorage.adresseClient || '';
document.getElementById('codePostalClient').value = clientStorage.postalClient || '';
document.getElementById('villeClient').value = clientStorage.villeClient || '';
}
}
// Gérer l'ouverture de liens externes
function handleOpenLink(event, urlType) {
event.preventDefault();
let url = '';
switch (urlType) {
case 'creditsafeURL':
url = 'https://www.creditsafe.fr/csfr?UserName=735265dorothee&Password=UH04EuLocXZMxIRqY19w6A%3d%3d&BackOfficeCountry=FR&origincountry=FR&linkages=Y';
break;
case 'easyQPURL':
if (document.getElementById('numClient').value != "") {
url = `https://qualite-portefeuille-iard-ent.axa-fr.intraxa/client/${document.getElementById('numClient').value}`;
} else {
url = `https://qualite-portefeuille-iard-ent.axa-fr.intraxa/`;
}
break;
case 'refNoteFiURL':
url = 'https://app.powerbi.com/groups/me/apps/7b48f9a2-bd97-4178-bc1a-a5f7ef6e985f/reports/d29a9f83-cafe-4a0f-bc38-0929921e8cd3/ReportSection?ctid=396b38cc-aa65-492b-bb0e-3d94ed25a97b';
break;
}
window.open(url, '_blank');
}
// Gérer l'extraction axaPAC
async function handleExtractClient(event) {
event.preventDefault();
// Affiche le modal
const elem = document.getElementById('modalExtractAxAPAC');
const instance = M.Modal.getInstance(elem);
instance.open();
}
async function handleModalExtract() {
document.getElementById('modalExtraireClient').disabled = true;
const response = await fetch(`/client/extract`, {
method: 'POST',
body: JSON.stringify({
"matricule": matricule,
"numClient": document.getElementById("modalNumClient").value
}),
headers: {
'Content-Type': 'application/json',
},
});
const res = await response.json();
if (res.valid) {
document.getElementById("error-extract").style.display = "none";
document.getElementById('modalExtraireClient').disabled = false;
console.log(res);
// Save sessionStorage for Intermediaire
sessionStorage.setItem('tmp', JSON.stringify(res.data));
// Populate data
document.getElementById("numClient").value = (res.data.numClient) ? res.data.numClient : null;
document.getElementById("nomClient").value = (res.data.nomClient) ? res.data.nomClient : null;
document.getElementById("adresseClient").value = (res.data.adresseClient) ? res.data.adresseClient : null;
document.getElementById("codePostalClient").value = (res.data.postalClient) ? res.data.postalClient : null;
document.getElementById("villeClient").value = (res.data.villeClient) ? res.data.villeClient : null;
validateField('numClient', true);
validateField('nomClient', true);
validateField('adresseClient', true);
validateField('codePostalClient', true);
validateField('villeClient', true);
updateSubmitButtonState('clientForm');
// close le modal
const elem = document.getElementById('modalExtractAxAPAC');
const instance = M.Modal.getInstance(elem);
instance.close();
} else {
document.getElementById("error-extract").style.display = "block";
document.getElementById('modalExtraireClient').disabled = false;
console.log("Problème rencontré lors de l'extraction cl063 AxA PAC :", res.error);
}
}
// Gérer la soumission du formulaire
async function handleSubmitForm(event) {
event.preventDefault();
let idClient = client?.id;
const numClient = document.getElementById('numClient').value;
let responseClient;
if (numClient) {
responseClient = await fetch(`/client/read/${numClient}`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
}
const dataClient = await responseClient.json();
if (dataClient.valid) {
idClient = dataClient.idClient;
} else {
// Créez le client ici si non trouvé
const responseCreateClient = await fetch(`/client/create`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
});
const dataCreateClient = await responseCreateClient.json();
if (dataCreateClient.valid) {
idClient = dataCreateClient.client.id;
}
}
// Mettre à jour le contrat avec l'ID du client
if (idClient) {
const response = await fetch(`/client/update/${idClient}`, {
method: 'POST',
body: JSON.stringify({
"nom": document.getElementById('nomClient').value.toUpperCase(),
"numClient": document.getElementById('numClient').value,
"adresse": document.getElementById('adresseClient').value.toUpperCase(),
"mail": document.getElementById('emailClient').value,
"codePostal": document.getElementById('codePostalClient').value,
"ville": document.getElementById('villeClient').value.toUpperCase(),
"noteFinanciere": document.getElementById('noteFinanciereClient').value
}),
headers: {
'Content-Type': 'application/json',
},
});
const data = await response.json();
if (data.valid) {
const idContrat = contrat?.id;
fetch(`/contrat/update/client/${idContrat}/${idClient}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})
.then(response => response.json())
.then(data => {
if (data.valid) {
// Update Session storage
const clientStorage = JSON.parse(sessionStorage.getItem('tmp'));
if (clientStorage) {
clientStorage.nomClient = document.getElementById('nomClient').value.toUpperCase();
clientStorage.numClient = document.getElementById('numClient').value;
clientStorage.adresseClient = document.getElementById('adresseClient').value.toUpperCase();
clientStorage.postalClient = document.getElementById('codePostalClient').value;
clientStorage.villeClient = document.getElementById('villeClient').value.toUpperCase();
sessionStorage.setItem('tmp', JSON.stringify(clientStorage))
}
// Redirect vers intermédiaire
window.location.href = `/navParcours?numParcours=${getNumParcoursFromURL()}&submenu=intermediaire`;
} else {
console.log('Echec lors de la mise à jour de la relation id contrat - id client :', data);
}
});
}
}
}
// Exposer init globalement pour y accéder depuis l'extérieur
window.initSubmenuForm = init;
})();

View File

@ -1,394 +0,0 @@
// Événement de vérification d'authentification
document.addEventListener('DOMContentLoaded', function () {
// Vérification de la page sur laquelle nous sommes
if (window.location.pathname !== '/auth') {
// Récupération du token JWT du localStorage
const token = localStorage.getItem('jwtToken');
// Si pas de token, redirige vers la page d'authentification
if (!token) {
window.location.replace('/auth');
} else {
// Si un token existe, vérification de sa validité
fetch('/auth/verifyToken', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token
}
})
.then(res => res.json())
.then(data => {
// Si le token n'est pas valide, redirige vers la page d'authentification
if (!data.valid) {
localStorage.removeItem('jwtToken');
window.location.replace('/auth');
}
})
.catch(error => {
console.error('Error:', error);
});
}
}
});
// Événement du bouton déconnexion
document.addEventListener('DOMContentLoaded', function (event) {
// Empêche le comportement par défaut du formulaire
event.preventDefault();
const logoutBtn = document.getElementById('logoutBtn');
if (window.location.pathname === '/auth') {
logoutBtn.style.display = 'none';
} else {
logoutBtn.addEventListener('click', function () {
// Suppression du token JWT du localStorage et redirection
localStorage.removeItem('jwtToken');
window.location.replace('/auth');
});
}
});
// Événement d'initialisation des Materialize content
document.addEventListener('DOMContentLoaded', function () {
// Init dropdown
var sidenav = document.querySelectorAll('.sidenav');
M.Sidenav.init(sidenav);
// Init Modal
var modals = document.querySelectorAll('.modal');
M.Modal.init(modals);
});
// Événement condition de groupe pour les menus
document.addEventListener('DOMContentLoaded', function () {
const token = localStorage.getItem('jwtToken');
if (token) {
const decoded = jwt_decode(token);
const userAuthGroupe = decoded.userAuthGroupe;
const reportingItem = document.getElementById("reportingSidenavSelect");
const adminItem = document.getElementById("adminSidenavSelect");
// Cache par défaut
reportingItem.style.display = 'none';
adminItem.style.display = 'none';
if (userAuthGroupe === 'MANAGER' || userAuthGroupe === 'ADMIN') {
reportingItem.style.display = 'block';
}
if (userAuthGroupe === 'ADMIN') {
adminItem.style.display = 'block';
}
}
});
// Fonction pour extraire le numéro de parcours de l'URL
function getNumParcoursFromURL() {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('numParcours');
}
// Fonction pour extraire le sous-menu de l'URL
function getSubmenuFromURL() {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('submenu');
}
// Fonction pour vérifier si une valeur est nulle ou non définie
function isNullOrUndefined(value) {
return value === null || value === undefined;
}
// Fonction pour éviter la duplication du code fetch
async function fetchWithJson(url, method, body = null) {
const options = {
method: method,
headers: { 'Content-Type': 'application/json' },
};
if (body) options.body = JSON.stringify(body);
const response = await fetch(url, options);
if (!response.ok) throw new Error('Réseau ou erreur serveur');
return await response.json();
}
// Fonction de load du parcours en session storage
async function loadParcours(numParcours) {
try {
const data = await fetchWithJson(`/parcours/read/${numParcours}`, 'GET');
if (data.valid) {
sessionStorage.setItem('parcours', JSON.stringify(data.parcours));
} else {
sessionStorage.setItem('parcours', null);
sessionStorage.setItem('admins', JSON.stringify(data.admins));
console.log("Parcours introuvable");
}
} catch (error) {
console.error("Erreur lors de la récupération des informations parcours :", error);
}
}
// Fonction générique de toggle
function toggler(btn, div) {
if (document.getElementById(btn).checked) {
document.getElementById(div).style.display = 'block';
} else {
document.getElementById(div).style.display = 'none';
}
}
// Fonction de load du parcours en session storage
async function loadContrat(idContrat) {
try {
const data = await fetchWithJson(`/contrat/read/id/${idContrat}`, 'GET');
if (data.valid) {
sessionStorage.setItem('contrat', JSON.stringify(data.contrat));
} else {
sessionStorage.setItem('contrat', null);
console.log("Contrat introuvable");
}
} 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 `<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, '&lt;').replace(/>/g, '&gt;');
// 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>
`;
}

View File

@ -1,721 +0,0 @@
// public/js/historiqueParcours.js
document.addEventListener("DOMContentLoaded", async function () {
// Récupération du 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();
// Exports CSV/XLSX
$("#exportCSV").on("click", function () {
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;
}
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" }))
};
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();
})
.catch(() => displayError("Export CSV (complet) impossible"));
});
$("#exportCSVFilter").on("click", function () {
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;
}
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();
})
.catch(() => displayError("Export CSV (filtré) impossible"));
});
$("#exportXlxs").on("click", function () {
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;
}
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();
})
.catch(() => displayError("Export XLS (complet) impossible"));
});
$("#exportXlxsFilter").on("click", function () {
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;
}
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();
})
.catch(() => displayError("Export XLS (filtré) impossible"));
});
// 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);
});
});
/* =========================
* Helpers spécifiques server-side
* ========================= */
// Initialisation DataTables en server-side (recherche globale + par colonnes + tri + pagination)
function initServerSideDataTable() {
let inflightController = null;
const table = $("#historiqueParcours").DataTable({
processing: true,
serverSide: true,
searching: true,
paging: true,
orderCellsTop: true,
fixedHeader: true,
responsive: { details: false },
pageLength: 10,
retrieve: true,
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" },
},
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();
fetch("/historiqueParcours/datatable", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
signal: inflightController.signal,
})
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(payload => {
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 => {
if (err && err.name === "AbortError") return;
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"]');
$globalInput.off('.DT'); // nettoie handlers datatables
const debouncedGlobal = debounce((v) => {
api.search(v);
api.ajax.reload();
}, 350);
$globalInput.on('input keyup keydown', function () {
debouncedGlobal(this.value);
});
// Recherche par colonne avec DEBOUNCE (ENTER inclus)
const debouncedColSearch = debounce((i, val) => {
api.column(i).search(val);
api.ajax.reload();
}, 350);
$("#historiqueParcours thead tr:eq(1) th").each(function (i) {
$("input", this).on("input keyup keydown change", function () {
debouncedColSearch(i, this.value);
});
});
api.on("responsive-resize", function (e, datatable, columns) {
for (let i = 0; i < columns.length; i++) {
if (columns[i]) {
$(api.column(i).header()).show();
$(api.column(i).footer()).show();
$($("#historiqueParcours thead tr:eq(1) th")[i]).show();
}
else {
$(api.column(i).header()).hide();
$(api.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");
}
});
// 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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
return '<span style="display:inline-flex; align-items:center; gap:8px;">'
+ '<button class="btn-row-details" title="Détails" aria-label="Afficher les détails"'
+ ' style="display:inline-block; width:22px; height:22px; border:none; border-radius:4px;'
+ ' background:#e74c3c; color:#fff; font-weight:700; cursor:pointer; line-height:22px;'
+ ' text-align:center; font-size:14px;">?</button>'
+ '<span class="np">' + numPacours + '</span>'
+ '</span>';
}
},
{ type: "date-eu", targets: 1 }, // Date de Création (colonne 1)
{ 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('<div style="padding:12px 16px; font-style:italic;">Chargement des détails…</div>').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");
}
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;
}
/* =========================
* Fonctions annexes importantes
* ========================= */
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;
}
}
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)}`, {
method: "POST",
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 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;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
a.remove();
} catch (error) {
displayError("Erreur lors de la génération du projet : " + (error?.message || "Erreur inconnue"));
}
}
// ---------- Helpers rendu ----------
// Les fonctions kv, fmtDate, gridWrap2cols, debounce, parseJwt et displayError sont dans global.js
//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 `
<div class="parcours-details" style="padding:14px 16px; background:#fafafa; border-top:1px solid #e0e0e0;">
<div style="font-weight:600; margin:0 0 8px;">Détails ${prodKey || ""}</div>
${body}
</div>
`;
}
// ---------- 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.<br>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.<br>La fiche RC 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 RC n\'est pas encore disponible.<br>Cette fonctionnalité sera bientôt implémentée.');
}
// 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";
// 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";
// 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 cotisation
let typeCotStr = "NC";
if (produit.typeCot) {
typeCotStr = produit.typeCot === "revisable" ? "Revisable" : (produit.typeCot === "forfaitaire" ? "Forfaitaire" : produit.typeCot);
}
// 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";
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);
}
//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.<br>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.<br>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);
}

View File

@ -1,628 +0,0 @@
// 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}"
|| dernierUtilisateur.prenom ~ "${esc}"
|| dernierUtilisateur.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) => {
try {
const { regionUser } = req.params;
const data = await parcoursService.getParcoursByRegionsPage([regionUser], 1, 10, { filter: "", sort: "-created" });
if (data) {
res.json({ valid: true, data });
} 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.",
});
}
});
/**
* /datatable : DataTables server-side (gestion de pagination)
*/
router.post("/datatable", async (req, res) => {
try {
const {
draw = 1,
start = 0,
length = 10,
regions = [],
search = { value: "" },
columns = [],
order = []
} = req.body || {};
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");
/**
* Export par pages de 1000 parcours pour éviter la surcharge mémoire
*/
const perPage = 1000;
let page = 1;
while (true) {
if (aborted) break;
let result;
try {
result = await parcoursService.getParcoursByRegionsPage(
effective.regions,
page,
perPage,
{ filter, sort }
);
} catch (err) {
logger.log("error", err);
break;
}
/**
* Traitement séquentiel de chaque parcours avec récupération des clients
*/
for (const parcours of result.items) {
if (aborted) break;
const contrat = parcours["@expand"]?.contrat || null;
const intermediaire = contrat ? (contrat["@expand"]?.intermediaire || null) : null;
/**
* Récupération du client avec fallback
* L'expand PocketBase ne fonctionne pas toujours, donc récupération directe si nécessaire
*/
let client = contrat ? (contrat["@expand"]?.client || null) : null;
if (!client && contrat && contrat.client) {
try {
const clientData = await clientService.getClient(contrat.client);
if (clientData) {
client = clientData;
}
} catch (err) {
// Erreur silencieuse : client non trouvé ou erreur de récupération
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",
];
/**
* Échappement des caractères spéciaux pour le format CSV
* Remplacement des ";" par "," et suppression des retours à la ligne
*/
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;
}
}
// Passage à la page suivante si nécessaire
if (aborted || page >= result.totalPages) break;
page++;
}
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"
];
/**
* Export par pages de 1000 parcours pour éviter la surcharge mémoire
*/
const perPage = 1000;
let page = 1;
const first = await parcoursService.getParcoursByRegionsPage(
effective.regions, page, perPage, { filter, sort }
);
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
* Nécessaire pour que le fichier soit reconnu comme XLS par Excel
*/
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`
);
/**
* Écrit une page de résultats dans le fichier XLS
* Récupère les clients avec fallback si l'expand ne fonctionne pas
*/
const writePage = async (result) => {
for (const parcours of result.items) {
const contrat = parcours["@expand"]?.contrat || null;
const intermediaire = contrat ? (contrat["@expand"]?.intermediaire || null) : null;
/**
* Récupération du client avec fallback
* L'expand PocketBase ne fonctionne pas toujours, donc récupération directe si nécessaire
*/
let client = contrat ? (contrat["@expand"]?.client || null) : null;
if (!client && contrat && contrat.client) {
try {
client = await clientService.getClient(contrat.client);
} catch (err) {
// Erreur silencieuse : client non trouvé ou erreur de récupération
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");
}
};
await writePage(first);
/**
* Traitement des pages suivantes
* Export par pages de 1000 parcours pour éviter la surcharge mémoire
*/
const totalPages = first.totalPages || 1;
while (page < totalPages) {
page++;
try {
const next = await parcoursService.getParcoursByRegionsPage(
effective.regions, page, perPage, { filter, sort }
);
await writePage(next);
} catch (e) {
logger.log("error", e);
break;
}
}
/**
* 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})`);
}
} 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;

View File

@ -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.

View File

@ -1,2 +0,0 @@
Pocketbase_0.7.5.exe serve --http="127.0.0.1:8091"
pause

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,146 +0,0 @@
const { db } = require("../db/db-connect");
const logger = require("../utils/logger");
const numeral = require("numeral");
require("numeral/locales/fr");
numeral.locale("fr");
async function getRecordIdFromFieldValue(collection, field, value) {
try {
const resultList = await db.records.getList(collection, 1, 1, {
filter: `${field} = '${value}'`,
});
if (resultList.totalItems > 0) {
return resultList.items[0].id;
} else {
return null;
}
} catch (error) {
logger.log("error", error);
return null;
}
}
async function fetchInfoByCriteria(collection, criteria) {
try {
const resultList = await db.records.getList(collection, 1, 1, criteria);
if (resultList.totalItems > 0) {
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);
}
return null;
}
async function updateRecordFromData(collection, recordID, data) {
try {
const record = await db.records.update(collection, recordID, data);
return record;
} catch (error) {
logger.log("error", error);
}
return null;
}
function customFormatNumber(number, decimal, taux=false) {
if (Number.isInteger(number)) {
if (decimal == true) {
return numeral(number).format("0,0.00");
} else {
return numeral(number).format("0,0");
}
} else {
if (taux == true) {
return numeral(number).format("0,0.000");
} else {
return numeral(number).format("0,0.00");
}
}
}
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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
}
/**
* 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,
};

View File

@ -1,257 +0,0 @@
// 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(",")
};
return globalService.fetchInfoByCriteria("parcours", criteria);
}
/**
* Full list (batch côté PocketBase). | Fetch l'ensemble de la BD via chunk "batch"
*/
async function getParcoursFullList({ filter, sort, expand, fields, batch = 500, skipTotal = true }) {
return db.records.getFullList("parcours", {
filter: filter || "",
sort: sort || "-created",
expand,
fields,
batch, // taille interne des sous-requêtes
skipTotal, // évite le COUNT total (évite calcul inutile + gain de temps)
});
}
/**
* 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 = {}) {
try {
let regFilter = "";
if (Array.isArray(regions) && regions.length > 0) {
const ors = regions.map(r => `dernierUtilisateur.region.nom = "${r}"`);
regFilter = `(${ors.join(" || ")})`;
}
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,
};
} 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;
}
} catch (error) {
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;
}
} catch (error) {
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;
const numericValue = parseInt(String(last.numParcours).substring(1), 10);
if (Number.isNaN(numericValue)) return null;
const next = numericValue + 1;
return "P" + next.toString().padStart(9, "0");
} 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,
};

View File

@ -1,19 +0,0 @@
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
};

View File

@ -1,53 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Outil souscripteur Transport">
<title>EasyTransport</title>
<!-- Variable pour complément spécifique HEAD -->
<%- typeof extraHeadContent !=='undefined' ? extraHeadContent : '' %>
<!-- Init CSS -->
<!-- Favicon: icône affichée dans l'onglet du navigateur -->
<link rel="icon" href="/images/favicon-axa.ico">
<!-- Import de la police Roboto avec différents poids pour une utilisation dans le site -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap">
<!-- Icônes Material Design : Bibliothèque d'icônes pour utilisation dans le site -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!-- Normalisation du CSS : élimine les différences de style par défaut entre les navigateurs -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css">
<!-- Materialize CSS : Bibliothèque CSS pour des éléments de conception matérialisée -->
<link rel="stylesheet" href="/materialize/css/materialize.min.css">
<!-- Feuille de style globale : Styles CSS personnalisés pour ce projet -->
<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">
</head>
<body>
<main>
<%- include('partials/navbar') %>
<div class="container">
<%- typeof body !=='undefined' ? body : '' %>
</div>
<!-- Script de Materialize -->
<script src="/materialize/js/materialize.min.js"></script>
<!-- Script de decoding de token -->
<script src="/js/jwt-decode.js"></script>
<!-- Feuille de script globale : JS personnalisés pour ce projet -->
<script src="/js/global.js"></script>
<!-- Feuille de script pour les contrôle de saisie formulaire : JS personnalisés pour ce projet -->
<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>
</main>
</body>
</html>

13
etv2/.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
# Ignore files type
*.log
*.env
*.exe
*.wbk
*.cmd
*~*
# Ignore directory
**/vbs/
**/node_modules/
**/logs/
**/.vscode/

2
etv2/README.md Normal file
View File

@ -0,0 +1,2 @@
Description
EasyTransport est une application en cours de transition technologique, destinée à la plateforme AxA IARD Transport. Le backend est écrit en Node.js avec une base de données embarquée PocketBase, et le frontend utilise EJS et Materialize CSS. L'application propose divers modules tels que la tarification, la génération de contrats à partir de formulaires, l'authentification via JWT, et la génération d'attestations.

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,9 @@
"test": "jest"
},
"pkg": {
"assets": ["public/**"]
"assets": [
"public/**"
]
},
"keywords": [],
"author": "cyril.ducaffy@axa.fr",

View File

@ -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); }
}

View File

@ -145,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 {
@ -226,7 +205,4 @@ td.nc-value {
#checkRegionAdmin label {
display: inline-block;
}
#historiqueParcours tr.shown > td { background: #fffdf5; }
.parcours-details { font-size: 0.95rem; }
}

View File

@ -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;
}

Some files were not shown because too many files have changed in this diff Show More