Compare commits

..

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

233 changed files with 1975 additions and 3266 deletions

View File

@ -1,15 +0,0 @@
{
"keep": {
"days": true,
"amount": 14
},
"auditLog": "C:\\Users\\B722DA\\Documents\\etv2\\logs\\.08fe637d6ef44c0231313d33bee64c93b9084214-audit.json",
"files": [
{
"date": 1758200946499,
"name": "C:\\Users\\B722DA\\Documents\\etv2\\logs\\easytransport-18092025.log",
"hash": "1706becaf705914860a3975811363123969a9384d470caf37ee1ba3b6762cbcc"
}
],
"hashType": "sha256"
}

View File

@ -1,15 +0,0 @@
{
"keep": {
"days": true,
"amount": 14
},
"auditLog": "C:\\workspace\\etv2\\logs\\.3d67f34307b811ec6c096daeac0152b0eaa334a7-audit.json",
"files": [
{
"date": 1781187673716,
"name": "C:\\workspace\\etv2\\logs\\easytransport-11062026.log",
"hash": "c19430c072d594d17c7e822d673b22a8a9eb97f9955f27a7543fec107e9788c3"
}
],
"hashType": "sha256"
}

View File

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

View File

@ -1,368 +0,0 @@
/* Historique Parcours */
body>main>div>div.section.center-align {
width: 100% !important;
}
body>main>div>div.section.center-align>div.container {
width: 100% !important;
}
#historiqueParcours_wrapper {
width: 100% !important;
overflow-x: auto !important;
margin-top: 30px;
}
table#historiqueParcours {
width: 150%;
border-collapse: collapse !important;
border: 2px solid darkblue !important;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
#historiqueParcours th,
#historiqueParcours td {
color: black;
text-align: left;
font-size: 11px;
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
}
table.dataTable thead th {
padding-right: 18px !important;
position: relative !important;
}
/* Div pour encapsuler le texte dans les cellules d'en-tête */
table.dataTable thead th>div {
position: absolute !important;
top: 0 !important;
left: 0 !important;
}
#historiqueParcours_filter {
margin-bottom: 20px;
}
.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;
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;
}
#historiqueParcours_length>label {
font-size: 14px;
display: flex !important;
align-items: center !important;
}
.dataTables_length select[name="historiqueParcours_length"] {
display: block !important;
font-size: 14px;
color: #555;
padding: 8px;
margin: 0 0.5em;
border: none;
background: none;
padding: 5px;
font-size: 16px;
outline: none;
width: 60px;
}
/* Style Input recherche par ligne */
#historiqueParcours>thead>tr:nth-child(2)>th>input {
font-size: 13px !important;
padding: 6px !important;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
}
/* icone de tri sur les colonnes */
table.dataTable thead .sorting,
table.dataTable thead .sorting_asc,
table.dataTable thead .sorting_desc {
background-position: right center;
background-repeat: no-repeat;
}
/* Ajouter un espacement entre le texte et l'icône */
table.dataTable thead .sorting:before,
table.dataTable thead .sorting_asc:before,
table.dataTable thead .sorting_desc:before {
content: "";
}
/* boutons de navigation */
.dataTables_wrapper .dataTables_paginate .paginate_button {
background-color: white !important;
border: darkblue solid 1.5px !important;
color: black !important;
padding: 6px 12px !important;
margin: 0 2px !important;
cursor: pointer !important;
border-radius: 4px !important;
transition: background-color 0.3s, color 0.3s, border-color 0.3s !important;
}
#historiqueParcours_paginate>span>a.paginate_button.current,
.dataTables_wrapper .dataTables_paginate:hover .paginate_button:hover {
background: none !important;
background-color: darkblue !important;
border-color: white !important;
color: white !important;
}
/* NC value */
td.nc-value {
color: lightgray !important;
}
/* Les bouton pour le filtres et les extraction */
#divBtnFilter {
display: inline-grid;
width: 100%;
grid-template-columns: auto;
column-gap: 3%;
row-gap: 10%;
}
#checkRegionAdmin {
grid-column: 1 / span 3;
justify-self: center;
}
#divToggleSearch {
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 {
grid-column: 1;
grid-row: 1;
justify-self: start;
}
#divExtractFilter {
grid-column: 3;
grid-row: 1;
justify-self: end;
}
#divBtnFilter button {
border-radius: 8px;
box-shadow: 0 2px 4px rgba(16, 0, 75, 0.2),
0 4px 8px rgba(16, 0, 75, 0.1);
}
/* Bouton Reprendre */
#btnReprendre,
#btnGenerate {
border: none;
color: white;
padding: 0px 15px;
font-size: 10px;
cursor: pointer;
border-radius: 8px;
}
#btnReprendre i {
margin: 0;
}
/* checkbox Filter Region Admin a supprimer probablement*/
#checkRegionAdmin {
border: 1px solid #ccc;
padding: 10px;
display: flex;
flex-wrap: wrap;
margin-bottom: 10px;
display: none;
}
[class^="checkbox-wrapper-"] {
margin-right: 20px;
}
#checkRegionAdmin input[type="checkbox"] {
display: none;
visibility: hidden;
}
#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;
}
#historiqueParcours td .col-with-text {
white-space: normal !important;
}
#historiqueParcours_wrapper {
overflow-x: visible !important;
}
table.dataTable {
width: 100% !important;
table-layout: auto !important;
}
.col-with-text {
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
}
.col-with-text .np {
white-space: normal !important;
}
.btn-row-details {
cursor: pointer;
width: 35px;
height: 28px;
background-color: #F44336 !important;
border-radius: 6px;
border: 3px solid darkblue;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 16px;
padding: 0;
line-height: 1;
transition: 0.15s ease-in-out;
}
/* petit effet hover propre pour le bouton détails*/
.btn-row-details:hover {
background-color: #d7372f;
transform: scale(1.05);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

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,422 +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>
`;
}
/**
* Formate une valeur en euros :
* - espace insécable fine entre milliers,
* - virgule pour les décimales,
* - 0 à 2 décimales max,
* - symbole à la fin.
* Si la valeur est invalide => "NC".
*/
function formatEuro(value, options) {
const opts = Object.assign({ minimumFractionDigits: 0, maximumFractionDigits: 2 }, options || {});
if (value === null || value === undefined || value === '' || value === 'NC') return 'NC';
// Accepte string avec virgule ou espaces
const normalized = (typeof value === 'string')
? value.replace(/\s|\u00A0|\u202F|&nbsp;/g, '').replace(',', '.')
: value;
const num = Number(normalized);
if (!isFinite(num)) return 'NC';
const formatted = new Intl.NumberFormat('fr-FR', {
minimumFractionDigits: opts.minimumFractionDigits,
maximumFractionDigits: opts.maximumFractionDigits
}).format(num);
return formatted + ' €';
}

View File

@ -1,858 +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();
// Variable pour suivre l'état des exports
let isExporting = false;
// Fonction pour désactiver/activer les boutons d'export
function setExportButtonsState(disabled) {
isExporting = disabled;
const buttons = ["#exportCSV", "#exportCSVFilter", "#exportXlxs", "#exportXlxsFilter"];
buttons.forEach(selector => {
const $btn = $(selector);
$btn.prop("disabled", disabled);
if (disabled) {
$btn.css("opacity", "0.5");
$btn.css("cursor", "not-allowed");
} else {
$btn.css("opacity", "1");
$btn.css("cursor", "pointer");
}
});
}
// Exports CSV/XLSX
$("#exportCSV").on("click", function () {
if (isExporting) return;
const dt = $("#historiqueParcours").DataTable();
if (!dt) {
displayError("Impossible d'accéder à la table de données.");
return;
}
const settings = dt.settings()[0];
if (!settings || !settings.aoColumns) {
displayError("Structure de données invalide.");
return;
}
setExportButtonsState(true);
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();
setExportButtonsState(false);
})
.catch((err) => {
displayError("Export CSV (complet) impossible");
setExportButtonsState(false);
});
});
$("#exportCSVFilter").on("click", function () {
if (isExporting) return;
const dt = $("#historiqueParcours").DataTable();
if (!dt) {
displayError("Impossible d'accéder à la table de données.");
return;
}
const settings = dt.settings()[0];
if (!settings || !settings.aoColumns) {
displayError("Structure de données invalide.");
return;
}
setExportButtonsState(true);
const payload = {
mode: "filtered", // export avec les filtres/colonnes/tri actuels
search: { value: dt.search() || "" }, // recherche globale
columns: settings.aoColumns.map((c, i) => ({
data: i,
search: { value: dt.column(i).search() || "" } // filtres par colonne
})),
order: (dt.order() || []).map(([col, dir]) => ({ column: col || 0, dir: dir || "asc" }))
};
fetch("/historiqueParcours/export/csv", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
})
.then(resp => {
if (!resp.ok) throw new Error("Export CSV (filtré) impossible");
return resp.blob();
})
.then(blob => {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "historique_parcours_filtre.csv";
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(url);
a.remove();
setExportButtonsState(false);
})
.catch((err) => {
displayError("Export CSV (filtré) impossible");
setExportButtonsState(false);
});
});
$("#exportXlxs").on("click", function () {
if (isExporting) return;
const dt = $("#historiqueParcours").DataTable();
if (!dt) {
displayError("Impossible d'accéder à la table de données.");
return;
}
const settings = dt.settings()[0];
if (!settings || !settings.aoColumns) {
displayError("Structure de données invalide.");
return;
}
setExportButtonsState(true);
const payload = {
mode: "full",
search: { value: "" },
columns: settings.aoColumns.map((c, i) => ({ data: i, search: { value: "" } })),
order: (dt.order() || []).map(([col, dir]) => ({ column: col || 0, dir: dir || "asc" }))
};
fetch("/historiqueParcours/export/xls", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
})
.then(resp => { if (!resp.ok) throw new Error(); return resp.blob(); })
.then(blob => {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url; a.download = "historique_parcours_complet.xls";
document.body.appendChild(a); a.click();
URL.revokeObjectURL(url); a.remove();
setExportButtonsState(false);
})
.catch((err) => {
displayError("Export XLS (complet) impossible");
setExportButtonsState(false);
});
});
$("#exportXlxsFilter").on("click", function () {
if (isExporting) return;
const dt = $("#historiqueParcours").DataTable();
if (!dt) {
displayError("Impossible d'accéder à la table de données.");
return;
}
const settings = dt.settings()[0];
if (!settings || !settings.aoColumns) {
displayError("Structure de données invalide.");
return;
}
setExportButtonsState(true);
const payload = {
mode: "filtered", // export avec les filtres/colonnes/tri actuels
search: { value: dt.search() || "" }, // recherche globale
columns: settings.aoColumns.map((c, i) => ({
data: i,
search: { value: dt.column(i).search() || "" } // filtres par colonne
})),
order: (dt.order() || []).map(([col, dir]) => ({ column: col || 0, dir: dir || "asc" }))
};
fetch("/historiqueParcours/export/xls", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
})
.then(resp => {
if (!resp.ok) throw new Error("Export XLS (filtré) impossible");
return resp.blob();
})
.then(blob => {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "historique_parcours_filtre.xls";
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(url);
a.remove();
setExportButtonsState(false);
})
.catch((err) => {
displayError("Export XLS (filtré) impossible");
setExportButtonsState(false);
});
});
// Délégation pour la génération de projet
$("#historiqueParcours").on("click", "button#btnGenerate", function () {
const numParcours = $(this).data("num-parcours");
const produit = $(this).data("produit");
generateProject(numParcours, produit);
});
});
/* =========================
* 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: false,
autoWidth: false,
scrollX: 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();
const currentController = inflightController;
fetch("/historiqueParcours/datatable", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
signal: currentController.signal,
})
.then(res => {
// Vérifier si la requête a été annulée
if (currentController.signal.aborted || inflightController !== currentController) {
return null;
}
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(payload => {
// Vérifier si la requête a été annulée
if (currentController.signal.aborted || inflightController !== currentController) {
return;
}
if (!payload || typeof payload !== 'object') {
throw new Error("Réponse invalide du serveur");
}
callback({
draw: payload.draw || 0,
recordsTotal: payload.recordsTotal || 0,
recordsFiltered: payload.recordsFiltered || payload.recordsTotal || 0,
data: Array.isArray(payload.data) ? payload.data : []
});
})
.catch(err => {
// Ignorer silencieusement toutes les erreurs d'abort
if (err && (err.name === "AbortError" || err.name === "DOMException")) {
return;
}
// Vérifier aussi si le signal a été aborted
if (currentController.signal.aborted || inflightController !== currentController) {
return;
}
// Seulement afficher une erreur si ce n'est PAS un abort
displayError("Failed to fetch data. Please try again later.");
callback({ draw: 0, recordsTotal: 0, recordsFiltered: 0, data: [] });
});
},
initComplete: function () {
const api = this.api();
// Recherche globale : debounce y compris ENTER (plus de bypass immédiat)
const $globalInput = $('div.dataTables_filter input[type="search"]');
const $filterLabel = $globalInput.closest('label');
$globalInput.off('.DT'); // nettoie handlers datatables
const debouncedGlobal = debounce((v) => {
api.search(v);
api.ajax.reload();
}, 350);
// Fonction pour gérer l'affichage du texte "Rechercher"
function toggleSearchPlaceholder() {
if ($globalInput.val().trim() !== '') {
$filterLabel.addClass('has-value');
} else {
$filterLabel.removeClass('has-value');
}
}
$globalInput.on('input keyup keydown', function () {
debouncedGlobal(this.value);
toggleSearchPlaceholder();
});
// Vérifier l'état initial
toggleSearchPlaceholder();
// Recherche par colonne avec DEBOUNCE (ENTER inclus)
const debouncedColSearch = debounce((i, val) => {
api.column(i).search(val);
api.ajax.reload();
}, 350);
$("#historiqueParcours thead tr:eq(1) th").each(function (i) {
$("input", this).on("input keyup keydown change", function () {
debouncedColSearch(i, this.value);
});
});
$("#toggleSearch").on("click", function () {
const row = $("#historiqueParcours thead tr:eq(1)");
row.toggle();
if (row.is(":visible")) {
$(this).text("ENLEVER LA RECHERCHE PAR COLONNE");
} else {
$(this).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 class="col-with-text">
<button class="btn-row-details" title="Détails" aria-label="Afficher les détails">?</button>
<span class="np">${numPacours}</span>
</span>
`;
}
},
{ type: "date-eu", targets: 1 }, // Date de Création (colonne 1)
{
// Appliquer la classe nc-value aux cellules contenant "NC"
targets: "_all",
createdCell: function (td, cellData, rowData, row, col) {
// Exclure la première colonne (bouton détails) et les deux dernières (boutons)
if (col !== 0 && col < rowData.length - 2) {
const cellText = String(cellData || "").trim();
if (cellText === "NC") {
td.classList.add("nc-value");
}
}
}
},
{ responsivePriority: 1, targets: -1 },
{ responsivePriority: 2, targets: -2 }
],
});
// --- Toggle des détails
$('#historiqueParcours tbody').on('click', '.btn-row-details', function (e) {
e.preventDefault();
e.stopPropagation();
const api = $('#historiqueParcours').DataTable();
const $tr = $(this).closest('tr');
// si c'est une ligne enfant (responsive), remonter à la parent
const row = api.row($tr.hasClass('child') ? $tr.prev() : $tr);
if (row.child.isShown()) {
row.child.hide();
// change icône
this.textContent = "?";
this.style.background = "#e74c3c";
return;
}
// récupérer le numParcours depuis la 1re cellule
const raw = row.data()?.[0] ?? "";
const tmp = document.createElement('div');
tmp.innerHTML = String(raw);
const numParcours = tmp.textContent.replace("?", "").trim();
if (!numParcours) return;
row.child('<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>
`;
}
// Retourne le tarif en fonction du type de tarif
function buildTarifBlock({ tarifRef, ht, ttc }) {
if (tarifRef && String(tarifRef).trim() !== '') {
return kv("Tarif de référence", tarifRef);
}
const htStr = (ht !== undefined && ht !== null) ? formatEuro(ht) : "NC";
const ttcStr = (ttc !== undefined && ttc !== null) ? formatEuro(ttc) : "NC";
return kv("Tarif commercial HT / TTC", `${htStr} / ${ttcStr}`);
}
function safeCA(value) {
return (value === null || value === undefined || value === '' || value === 'NC')
? "NC"
: formatEuro(value);
}
// ---------- Sections produit ----------
//sectionTTPC : récupere les détails d'un parcours TPPC pour le mettre en forme (2 colonnes)
// ---------- TPPC ----------
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
// 1) CA
const caStr = safeCA(produit.ca);
// 2) Type de cotisation
let typeCotStr = "NC";
if (projet?.typeCot) {
typeCotStr = (projet.typeCot === "revisable") ? "Révisable"
: (projet.typeCot === "forfaitaire") ? "Forfaitaire"
: projet.typeCot;
}
// 3) Activité
const activiteAssuree = produit.actAssuree || "NC";
const nbVehicules = (produit.nbVehic !== undefined && produit.nbVehic !== null) ? String(produit.nbVehic) : "NC";
// 4) Marchandises / Garanties
const garanties = Array.isArray(produit.garanties)
? produit.garanties.join(", ")
: (produit.garanties || "NC");
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";
// 5) Zones (supprimer pour TPPC)
const zonesStr = "NC";
// 6) Dates (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 datesStr = `Effet: ${dateEffet} / Échéance: ${dateEcheance} / Fin: ${dateFin}`;
// 7) Tarif (ref ou com HT/TTC)
const blocTarif = buildTarifBlock({
tarifRef: tarif?.tarifRef,
ht: (produit.primeHT ?? produit.cotTotalHT ?? null),
ttc: (produit.primeTTC ?? produit.cotTotalTTC ?? null)
});
// Disposition en 2 colonnes
const gauche = [
kv("Chiffre d'affaires", caStr),
kv("Type de cotisation", typeCotStr),
kv("Activité assurée", activiteAssuree),
kv("Nombre de véhicules", nbVehicules),
kv("Garanties", garanties),
kv("Extensions de garanties", extensionsStr),
].join("");
const droite = [
kv("Zones", zonesStr),
kv("Dates", datesStr),
blocTarif,
].join("");
return gridWrap2cols(gauche, droite);
}
//sectionRC : récupere les détails d'un parcours RC pour le mettre en forme (2 colonnes)
// ---------- RC ----------
function sectionRC(produit, contrat) {
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.'
);
}
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.'
);
}
// 1) CA
const caStr = safeCA(produit.ca);
// 2) Type de cotisation
let typeCotStr = "NC";
if (produit.typeCot) {
typeCotStr = (produit.typeCot === "revisable") ? "Révisable"
: (produit.typeCot === "forfaitaire") ? "Forfaitaire"
: produit.typeCot;
}
// 3) 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";
// 4) 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";
// 5) 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";
// 6) 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";
const datesStr = `Effet: ${dateEffet} / Échéance: ${dateEcheance} / Fin: ${dateFin}`;
// 7) Tarif
const tarif = produit?.["@expand"]?.tarif || null;
const blocTarif = buildTarifBlock({
tarifRef: tarif?.tarifRef,
ht: produit.cotTotalHT ?? null,
ttc: produit.cotTotalTTC ?? null
});
const gauche = [
kv("Chiffre d'affaires", caStr),
kv("Type de cotisation", typeCotStr),
kv("Activités assurées", activiteAssuree),
kv("Marchandises", garantiesStr),
].join("");
const droite = [
kv("Zones", zonesStr),
kv("Dates", datesStr),
blocTarif,
].join("");
return gridWrap2cols(gauche, droite);
}
//sectionFAC : récupere les détails d'un parcours FAC pour le mettre en forme (2 colonnes)
// ---------- FAC ----------
function sectionFAC(produit, contrat) {
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.'
);
}
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.'
);
}
// 1) CA
const caStr = safeCA(produit.ca);
// 2) Type de cotisation
const typeCotStr = "NC";
// 3) Activité
const activiteAssuree = produit.actAssure || "NC";
// 4) Garanties (modes de transport déclarés)
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";
// 5) 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";
// 6) 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";
const datesStr = `Effet: ${dateEffet} / Échéance: ${dateEcheance} / Fin: ${dateFin}`;
// 7) Tarif
const tarif = produit?.["@expand"]?.tarif || null;
const blocTarif = buildTarifBlock({
tarifRef: tarif?.tarifRef,
ht: produit.cotAnnuelleHT ?? null,
ttc: produit.cotAnnuelleTTC ?? null
});
const gauche = [
kv("Chiffre d'affaires", caStr),
kv("Type de cotisation", typeCotStr),
kv("Activité assurée", activiteAssuree),
kv("Garanties", garantiesStr),
].join("");
const droite = [
kv("Zones", zonesStr),
kv("Dates", datesStr),
blocTarif,
].join("");
return gridWrap2cols(gauche, droite);
}

View File

@ -1,647 +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}"
|| 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) => {
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) {
// Ignorer silencieusement les erreurs d'abort (requête annulée côté client)
if (error && (error.name === "AbortError" || error.name === "DOMException" || error.message?.includes("aborted"))) {
return;
}
// Pour toutes les autres erreurs, on les log et on renvoie une réponse d'erreur
logger.log("error", "ça ne marche pas mais, aucune erreur" + error);
if (!res.headersSent) {
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(" && ");
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 pocket)
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(" && ");
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 Pocket)
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})`);
}
} 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,2 +0,0 @@
Pocketbase_0.7.5.exe serve --http="127.0.0.1:8091"
pause

Binary file not shown.

View File

@ -1,68 +0,0 @@
const { db } = require('../db/db-connect');
const logger = require('../utils/logger');
const globalService = require("../services/globalService");
async function createClient() {
return await db.records.create('client', {});
}
async function getClient(id) {
const criteria = {
filter: `id='${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
* avec 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(" || ");
// 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,
};

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,278 +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"
* avec 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 (Pocketbase 0.7 rejette les filtres vides)
if (filter && filter.trim() !== "") {
options.filter = filter;
}
// 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 = {}) {
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>

View File

View File

@ -6,8 +6,7 @@
"scripts": {
"start": "nodemon ./src/server.js",
"build": "pkg ./src/server.js -o EasyTransport",
"test": "jest",
"db" : "cd ./src/db && start cmd /c Lancement_Pocketbase.cmd"
"test": "jest"
},
"pkg": {
"assets": [

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

@ -0,0 +1,208 @@
/* Historique Parcours */
body>main>div>div.section.center-align {
width: 100% !important;
}
body>main>div>div.section.center-align>div.container {
width: 100% !important;
}
#historiqueParcours_wrapper {
width: 100% !important;
overflow-x: auto !important;
margin-top: 30px;
}
table#historiqueParcours {
width: 150%;
border-collapse: collapse !important;
border: 2px solid darkblue !important;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
#historiqueParcours th,
#historiqueParcours td {
color: black;
text-align: left;
font-size: 11px;
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
}
table.dataTable thead th {
padding-right: 18px !important;
position: relative !important;
}
/* Div pour encapsuler le texte dans les cellules d'en-tête */
table.dataTable thead th>div {
position: absolute !important;
top: 0 !important;
left: 0 !important;
}
#historiqueParcours_filter label {
display: flex;
align-items: center;
}
.dataTables_wrapper .dataTables_filter input[type="search"] {
background-color: transparent;
border: none;
border-bottom: 1px solid #26a69a;
border-radius: 0;
outline: none;
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 {
font-size: 14px;
display: flex !important;
align-items: center !important;
}
.dataTables_length select[name="historiqueParcours_length"] {
display: block !important;
font-size: 14px;
color: #555;
padding: 8px;
margin: 0 0.5em;
border: none;
background: none;
padding: 5px;
font-size: 16px;
outline: none;
width: 60px;
}
/* Style Input search by row */
#historiqueParcours>thead>tr:nth-child(2)>th>input {
font-size: 13px !important;
padding: 6px !important;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
}
/* icone de tri sur les colonnes */
table.dataTable thead .sorting,
table.dataTable thead .sorting_asc,
table.dataTable thead .sorting_desc {
background-position: right center;
background-repeat: no-repeat;
}
/* Ajouter un espacement entre le texte et l'icône */
table.dataTable thead .sorting:before,
table.dataTable thead .sorting_asc:before,
table.dataTable thead .sorting_desc:before {
content: "";
}
/* boutons de navigationw */
.dataTables_wrapper .dataTables_paginate .paginate_button {
background-color: white !important;
border: darkblue solid 1.5px !important;
color: black !important;
padding: 6px 12px !important;
margin: 0 2px !important;
cursor: pointer !important;
border-radius: 4px !important;
transition: background-color 0.3s, color 0.3s, border-color 0.3s !important;
}
#historiqueParcours_paginate>span>a.paginate_button.current,
.dataTables_wrapper .dataTables_paginate:hover .paginate_button:hover {
background: none !important;
background-color: darkblue !important;
border-color: white !important;
color: white !important;
}
/* NC value */
td.nc-value {
color: lightgray !important;
}
/* Les bouton pour le filtres et les extraction */
#divBtnFilter {
display: inline-grid;
width: 100%;
grid-template-columns: auto;
column-gap: 3%;
row-gap: 10%;
}
#checkRegionAdmin {
grid-column: 1 / span 3;
justify-self: center;
}
#divToggleSearch {
width: 300px;
grid-column: 2;
grid-row: 3;
justify-self: center;
}
#divExtractAll {
grid-column: 1;
grid-row: 1;
justify-self: start;
}
#divExtractFilter {
grid-column: 3;
grid-row: 1;
justify-self: end;
}
#divBtnFilter button {
border-radius: 8px;
box-shadow: 0 2px 4px rgba(16, 0, 75, 0.2),
0 4px 8px rgba(16, 0, 75, 0.1);
}
/* Bouton Reprendre */
#btnReprendre,
#btnGenerate {
border: none;
color: white;
padding: 0px 15px;
font-size: 10px;
cursor: pointer;
border-radius: 8px;
}
#btnReprendre i {
margin: 0;
}
/* checkbox Filter Region Admin */
#checkRegionAdmin {
border: 1px solid #ccc;
padding: 10px;
display: flex;
flex-wrap: wrap;
margin-bottom: 10px;
display: none;
}
[class^="checkbox-wrapper-"] {
margin-right: 20px;
}
#checkRegionAdmin input[type="checkbox"] {
display: none;
visibility: hidden;
}
#checkRegionAdmin label {
display: inline-block;
}

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