Compare commits

..

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

260 changed files with 4546 additions and 33949 deletions

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,443 +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, useLoader = true) {
let loaderShown = false;
if (useLoader && typeof showLoader === 'function') {
showLoader();
loaderShown = true;
}
try {
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');
const data = await response.json();
if (loaderShown && typeof hideLoader === 'function') {
hideLoader();
}
return data;
} catch (error) {
// Toujours cacher le loader en cas d'erreur
if (loaderShown && typeof hideLoader === 'function') {
hideLoader();
}
throw error;
}
}
// 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,129 +0,0 @@
document.addEventListener("DOMContentLoaded", () => {
let activateTimeout = null;
let timeoutWarning = null;
let currentPageLoad = null;
let safetyTimeout = null; // Timeout de sécurité pour éviter que le loader reste bloqué
/**
* Active le loader et le rend visible
* Affiche un message d'annulation après 8 secondes
*/
window.showLoader = function() {
const loader = document.getElementById("loader-overlay");
if (!loader) return; // Protection : si l'élément n'existe pas, on ne fait rien
const timeoutMessage = document.getElementById("timeout-message");
const errorMessage = document.getElementById("error-message");
// Nettoyer les timeouts précédents
clearTimeout(activateTimeout);
clearTimeout(timeoutWarning);
// Réinitialiser l'affichage
if (errorMessage) errorMessage.style.display = "none";
if (timeoutMessage) timeoutMessage.style.display = "none";
loader.classList.remove("hidden");
// Afficher le loader après 500ms (évite le flash sur les chargements rapides)
activateTimeout = setTimeout(() => {
if (loader && !loader.classList.contains("hidden")) {
loader.classList.add("active");
}
}, 500);
// Afficher le message de timeout après 8 secondes
timeoutWarning = setTimeout(() => {
if (loader && loader.classList.contains("active") && !loader.classList.contains("hidden")) {
if (timeoutMessage) {
timeoutMessage.style.display = "block";
}
}
}, 8000);
// Protection : cacher automatiquement le loader après 30 secondes maximum
// Au cas où hideLoader ne serait jamais appelé (page qui ne charge pas, erreur, etc.)
if (safetyTimeout) clearTimeout(safetyTimeout);
safetyTimeout = setTimeout(() => {
if (loader && !loader.classList.contains("hidden")) {
hideLoader();
}
}, 30000); // 30 secondes max
};
/**
* Désactive et cache le loader
*/
window.hideLoader = function() {
const loader = document.getElementById("loader-overlay");
if (!loader) return; // Protection : si l'élément n'existe pas, on ne fait rien
const timeoutMessage = document.getElementById("timeout-message");
clearTimeout(activateTimeout);
clearTimeout(timeoutWarning);
if (safetyTimeout) {
clearTimeout(safetyTimeout);
safetyTimeout = null;
}
loader.classList.remove("active");
if (timeoutMessage) timeoutMessage.style.display = "none";
setTimeout(() => {
if (loader) {
loader.classList.add("hidden");
}
}, 500);
};
/**
* Affiche un message d'erreur dans le loader
* @param {string} msg - Message d'erreur à afficher
*/
window.showError = function(msg) {
const errorMessage = document.getElementById("error-message");
const timeoutMessage = document.getElementById("timeout-message");
if (!errorMessage) return; // Protection : si l'élément n'existe pas, on ne fait rien
clearTimeout(activateTimeout);
clearTimeout(timeoutWarning);
if (timeoutMessage) timeoutMessage.style.display = "none";
errorMessage.textContent = msg;
errorMessage.style.display = "block";
};
/**
* Annule le chargement en cours et cache le loader
*/
const cancelLoadingLink = document.getElementById("cancel-loading-link");
if (cancelLoadingLink) {
cancelLoadingLink.addEventListener("click", () => {
// Annuler le chargement de la page en cours si possible
if (currentPageLoad && currentPageLoad.abort) {
currentPageLoad.abort();
}
hideLoader();
// Afficher une notification discrète
if (typeof M !== 'undefined' && M.toast) {
M.toast({
html: 'Chargement annulé',
classes: 'rounded grey darken-3',
displayLength: 2500
});
}
});
}
/**
* Permet de stocker l'abort controller du chargement en cours
* @param {AbortController} controller - Controller de la requête fetch
*/
window.setCurrentPageLoad = function(controller) {
currentPageLoad = controller;
};
});

View File

@ -1,40 +0,0 @@
/**
* Affiche le loader lors des navigations entre pages (localhost)
* Comme on est en localhost, on peut se permettre d'afficher le loader
* au clic sur un lien, il disparaîtra automatiquement au chargement de la nouvelle page
*/
document.addEventListener("DOMContentLoaded", () => {
// Intercepter les clics sur les liens internes pour afficher le loader
document.body.addEventListener("click", (e) => {
const link = e.target.closest("a");
// Si c'est un lien interne (commence par /) et pas un lien spécial
if (link &&
link.getAttribute("href") &&
link.getAttribute("href").startsWith("/") &&
!link.classList.contains("modal-close") &&
!link.getAttribute("href").startsWith("/#")) {
// Afficher le loader avant la navigation
if (typeof showLoader === 'function') {
showLoader();
}
}
});
// Afficher le loader lors du rechargement (F5, Cmd+R)
window.addEventListener("beforeunload", () => {
if (typeof showLoader === 'function') {
showLoader();
}
});
// Cacher le loader quand la page est complètement chargée
window.addEventListener("load", () => {
if (typeof hideLoader === 'function') {
// Petit délai pour éviter le flash
setTimeout(hideLoader, 100);
}
});
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,647 +0,0 @@
/**
*
* RC DATA MANAGER
*
*
* Ce module gère la collecte, la sauvegarde et le pré-remplissage des données
* RC entre les formulaires Tarif et Projet.
*
* @requires rc-sync-utils.js
* @author AXA Transport Team
* @version 2.0.0
* @since 2026-02-17
*/
(function(window) {
'use strict';
const { toNumber, getValue, setValue, getElementByIdFlexible } = window.RCSync;
// ═══════════════════════════════════════════════════════════════════════
// MAPPING DES CHAMPS TARIF ↔ PROJET
// ═══════════════════════════════════════════════════════════════════════
/**
* Mapping complet des champs entre Tarif et Projet.
* Permet la synchronisation bidirectionnelle.
*
* Structure: { tarifFieldId: projetFieldId }
*/
const FIELD_MAPPING = {
// Informations générales
'CA': 'CA',
'chiffreAffaire': 'CA',
'nbVehicules': 'nombreVehicules',
'nbrVehicule': 'nombreVehicules',
// Type de cotisation
'cotisation': 'typeCot',
// Activités RCC - Voiturier
'checkVoiturier': 'actVoiturier',
'capitalVoiturier': 'valueActVoiturier',
// Activités RCC - Commissionnaire (Multimodal)
'checkCommissionnaire': 'actMultimodal',
'capitalCommissionnaire': 'valueActMultimodal',
// Activités RCC - Déménageur
'checkDemenageur': 'actDemEntr',
'capitalDemenageur': 'valueActDemEntr',
// Activités RCC - Logistique
'checkLogistique': 'actPrestaLog',
'capitalLogistique': 'valueActPrestaLog',
// RCE
'checkRCE': 'autresRC',
// Zones géographiques
'zone1': 'zone1',
'zone2': 'zone2',
'zone3': 'zone3',
'zone4': 'zone4',
'zone5': 'zone5',
'zone6': 'zone6',
// Protection Juridique
'checkPJ': 'pj',
// Garanties additionnelles - Engagements complémentaires
'checkDomImmat': 'extRCCConfie', // Simplifié
'checkContConf': 'extRCCConfie',
'checkTPPC': 'extRCCTPPC',
// Extensions RCC
'checkStationLavage': 'extRCCModifCalArrim',
// Extensions RCE
// (géré séparément car structure différente)
// Sinistralité
'sinistre': 'nbSinistres3ans'
};
// ═══════════════════════════════════════════════════════════════════════
// COLLECTE DES DONNÉES COMPLÈTES
// ═══════════════════════════════════════════════════════════════════════
/**
* Collecte toutes les données du formulaire Tarif RC.
* Cette fonction est exhaustive et capture TOUS les champs nécessaires.
*
* @returns {Object} Objet contenant toutes les données du tarif
*
* @example
* const tarifData = collectAllTarifData();
* console.log(tarifData.ca, tarifData.zones, tarifData.marchandises);
*/
function collectAllTarifData() {
// Références flexibles aux éléments
const getEl = getElementByIdFlexible;
const data = {
// ═══ INFORMATIONS GÉNÉRALES ═══
typeCotisation: document.querySelector('input[name="cotisation"]:checked')?.value || null,
ca: toNumber(getValue('CA') || getValue('chiffreAffaire')),
nombreVehicules: Math.max(0, Math.round(toNumber(getValue('nbVehicules') || getValue('nbrVehicule')))),
// ═══ ACTIVITÉS RCC ═══
activites: {
voiturier: {
checked: getValue('checkVoiturier') || false,
capital: toNumber(getValue('capitalVoiturier')),
pourcentage: toNumber(getValue('pourcent_voiturier') || getValue('pourcentVoiturier/Loueur')),
isSet: Boolean(getValue('pourcent_voiturier')?.trim())
},
commissionnaire: {
checked: getValue('checkCommissionnaire') || false,
capital: toNumber(getValue('capitalCommissionnaire')),
pourcentage: toNumber(getValue('pourcent_commissionnaire')),
isSet: Boolean(getValue('pourcent_commissionnaire')?.trim())
},
demenageur: {
checked: getValue('checkDemenageur') || false,
capital: toNumber(getValue('capitalDemenageur')),
pourcentage: toNumber(getValue('pourcent_demenageur')),
isSet: Boolean(getValue('pourcent_demenageur')?.trim())
},
logistique: {
checked: getValue('checkLogistique') || false,
capital: toNumber(getValue('capitalLogistique')),
pourcentage: toNumber(getValue('pourcent_logistique')),
isSet: Boolean(getValue('pourcent_logistique')?.trim())
},
autocariste: {
checked: getValue('checkAutocariste') || false,
capital: toNumber(getValue('capitalAutocariste')),
pourcentage: toNumber(getValue('pourcent_autocariste')),
isSet: Boolean(getValue('pourcent_autocariste')?.trim())
},
autres: {
checked: getValue('checkAutres') || false,
capital: toNumber(getValue('capitalAutres')),
pourcentage: toNumber(getValue('pourcent_autres')),
isSet: Boolean(getValue('pourcent_autres')?.trim())
}
},
// ═══ RCE ═══
rce: {
checked: getValue('checkRCE') || false
},
// ═══ ACTIVITÉS COMPLÉMENTAIRES (JSON) ═══
activitesComplementaires: {
voiturier: collectActivitesComplJSON('voiturier'),
commissionnaire: collectActivitesComplJSON('commissionnaire'),
demenageur: collectActivitesComplJSON('demenageur'),
logistique: collectActivitesComplJSON('logistique')
},
// ═══ MARCHANDISES (JSON) ═══
marchandises: {
voiturier: collectMarchandisesJSON('voiturier'),
commissionnaire: collectMarchandisesJSON('commissionnaire'),
demenageur: collectMarchandisesJSON('demenageur'),
logistique: collectMarchandisesJSON('logistique'),
autocariste: collectMarchandisesJSON('autocariste'),
autres: collectMarchandisesJSON('autres')
},
// ═══ ZONES GÉOGRAPHIQUES ═══
zones: {
zone1: getValue('zone1') || false,
zone2: getValue('zone2') || false,
zone3: getValue('zone3') || false,
zone4: getValue('zone4') || false,
zone5: getValue('zone5') || false,
zone6: getValue('zone6') || false
},
// ═══ ENGAGEMENTS COMPLÉMENTAIRES ═══
engagementsComplementaires: {
domicileImmatriculation: {
checked: getValue('checkDomImmat') || false,
capital: toNumber(getValue('inputDomImmat'))
},
contenantConfie: {
checked: getValue('checkContConf') || false,
capital: toNumber(getValue('inputContConf'))
},
differenceInventaire: {
checked: getValue('checkDiffInv') || false,
capital: toNumber(getValue('inputDiffInv'))
}
},
// ═══ GARANTIES ADDITIONNELLES ═══
garantiesAdditionnelles: {
stationLavage: getValue('checkStationLavage') || false,
garageInterne: getValue('checkGarageInterne') || false,
cse: getValue('checkCSE') || false,
tppc: {
checked: getValue('checkTPPC') || false,
capital: toNumber(getValue('selTPPCcapital')),
vehicules: Math.max(0, Math.round(toNumber(getValue('selTPPCveh'))))
},
pj: getValue('checkPJ') || false
},
// ═══ SINISTRALITÉ ═══
sinistralite: {
nombre3ans: toNumber(getValue('sinistre')),
montant3ans: 0 // TODO: ajouter si champ existe
},
// ═══ RÉSULTATS DE CALCUL ═══
resultats: {
// Franchise 250
fr250: {
primeRCC: toNumber(getEl('rccFr250')?.textContent),
primeRCE: toNumber(getEl('rceFr250')?.textContent),
primePJ: toNumber(getEl('pjFr250')?.textContent),
primeTotal: toNumber(getEl('priceFr250')?.textContent),
tauxRCC: toNumber(getEl('tauxRccFr250')?.textContent),
tauxRCE: toNumber(getEl('tauxRceFr250')?.textContent),
tauxGlobal: toNumber(getEl('tauxGlobalFr250')?.textContent)
},
// Franchise 400
fr400: {
primeRCC: toNumber(getEl('rccFr400')?.textContent),
primeRCE: toNumber(getEl('rceFr400')?.textContent),
primePJ: toNumber(getEl('pjFr400')?.textContent),
primeTotal: toNumber(getEl('priceFr400')?.textContent),
tauxRCC: toNumber(getEl('tauxRccFr400')?.textContent),
tauxRCE: toNumber(getEl('tauxRceFr400')?.textContent),
tauxGlobal: toNumber(getEl('tauxGlobalFr400')?.textContent)
},
// Franchise 2000
fr2000: {
primeRCC: toNumber(getEl('rccFr2000')?.textContent),
primeRCE: toNumber(getEl('rceFr2000')?.textContent),
primePJ: toNumber(getEl('pjFr2000')?.textContent),
primeTotal: toNumber(getEl('priceFr2000')?.textContent),
tauxRCC: toNumber(getEl('tauxRccFr2000')?.textContent),
tauxRCE: toNumber(getEl('tauxRceFr2000')?.textContent),
tauxGlobal: toNumber(getEl('tauxGlobalFr2000')?.textContent)
},
franchiseChoisie: window.franchiseChoisie || null,
tarifCommercial: toNumber(getValue('tarifCom'))
},
// ═══ COMMENTAIRE ═══
commentaire: getValue('commentaire') || ''
};
console.log('📊 Données Tarif collectées:', data);
return data;
}
/**
* Fonction helper pour collecter les activités complémentaires depuis le formulaire.
*
* @param {string} typeActivite - Type d'activité ('voiturier', 'commissionnaire', etc.)
* @returns {string} JSON array des activités cochées
* @private
*/
function collectActivitesComplJSON(typeActivite) {
let name;
switch(typeActivite.toLowerCase()) {
case 'voiturier':
name = 'actComplVoiturier/Loueur';
break;
case 'commissionnaire':
name = 'actComplCommissionnaire de Transport';
break;
case 'demenageur':
name = 'actComplDéménageur';
break;
case 'logistique':
name = 'actComplLogistique';
break;
default:
return JSON.stringify([]);
}
const checkboxes = document.querySelectorAll(`[name="${name}"] input[type="checkbox"]:checked`);
const activites = [];
checkboxes.forEach(cb => {
const text = cb.nextElementSibling ? cb.nextElementSibling.textContent.trim() : cb.value;
activites.push(text);
});
return JSON.stringify(activites);
}
/**
* Fonction helper pour collecter les marchandises depuis le formulaire.
*
* @param {string} typeActivite - Type d'activité
* @returns {string} JSON array des marchandises cochées
* @private
*/
function collectMarchandisesJSON(typeActivite) {
let name;
switch(typeActivite.toLowerCase()) {
case 'voiturier':
name = 'marVoiturier/Loueur';
break;
case 'commissionnaire':
name = 'marCommissionnaire de Transport';
break;
case 'demenageur':
name = 'marDéménageur';
break;
case 'logistique':
name = 'marLogistique';
break;
case 'autocariste':
name = 'marAutocariste';
break;
case 'autres':
name = 'marAutres activites';
break;
default:
return JSON.stringify([]);
}
const checkboxes = document.querySelectorAll(`[name="${name}"] input[type="checkbox"]:checked`);
const marchandises = [];
checkboxes.forEach(cb => {
const text = cb.nextElementSibling ? cb.nextElementSibling.textContent.trim() : cb.value;
marchandises.push(text);
});
return JSON.stringify(marchandises);
}
// ═══════════════════════════════════════════════════════════════════════
// PRÉ-REMPLISSAGE TARIF → PROJET
// ═══════════════════════════════════════════════════════════════════════
/**
* Pré-remplit le formulaire Projet avec les données du Tarif.
* Cette fonction est appelée quand l'utilisateur passe du Tarif au Projet.
*
* @param {Object} tarifData - Données complètes du tarif (de collectAllTarifData)
*
* @example
* const tarifData = collectAllTarifData();
* prefillProjetFromTarif(tarifData);
*/
function prefillProjetFromTarif(tarifData) {
if (!tarifData) {
console.warn('Pas de données tarif à pré-remplir');
return;
}
console.log('📝 Pré-remplissage Projet depuis Tarif...');
try {
// ═══ INFORMATIONS GÉNÉRALES ═══
// CA
if (tarifData.ca) {
setValue('CA', tarifData.ca);
console.log(' ✓ CA:', tarifData.ca);
}
// Type de cotisation
if (tarifData.typeCotisation) {
const radio = document.querySelector(`input[name="typeCot"][value="${tarifData.typeCotisation}"]`);
if (radio) {
radio.checked = true;
console.log(' ✓ Type cotisation:', tarifData.typeCotisation);
}
}
// Nombre de véhicules
if (tarifData.nombreVehicules) {
setValue('nombreVehicules', tarifData.nombreVehicules);
console.log(' ✓ Véhicules:', tarifData.nombreVehicules);
}
// ═══ ACTIVITÉS ═══
const activitySelector = document.getElementById('activity-selector');
if (activitySelector && tarifData.activites) {
const activitesToAdd = [];
if (tarifData.activites.voiturier?.checked) {
activitesToAdd.push('Voiturier/Loueur');
}
if (tarifData.activites.commissionnaire?.checked) {
activitesToAdd.push('Commissionnaire de Transport');
}
if (tarifData.activites.demenageur?.checked) {
activitesToAdd.push('Déménageur d\'entreprises');
}
if (tarifData.activites.logistique?.checked) {
activitesToAdd.push('Prestataire logistique');
}
if (tarifData.activites.autocariste?.checked) {
activitesToAdd.push('Autocariste');
}
if (tarifData.activites.autres?.checked) {
activitesToAdd.push('Autres activités');
}
// Sélectionner les options dans le select
Array.from(activitySelector.options).forEach(option => {
if (activitesToAdd.includes(option.value)) {
option.selected = true;
}
});
// Trigger change pour créer les chips Materialize
const event = new Event('change', { bubbles: true });
activitySelector.dispatchEvent(event);
console.log(' ✓ Activités:', activitesToAdd.length);
}
// ═══ MARCHANDISES ═══
const marchandiseSelector = document.getElementById('marchandise-selector');
if (marchandiseSelector && tarifData.marchandises) {
const marchandisesToSelect = [];
// Parser les marchandises de chaque type
['voiturier', 'commissionnaire', 'demenageur', 'logistique', 'autocariste', 'autres'].forEach(type => {
const marchArray = tarifData.marchandises[type];
if (Array.isArray(marchArray)) {
marchArray.forEach(m => marchandisesToSelect.push(m));
}
});
// Sélectionner dans le select
Array.from(marchandiseSelector.options).forEach(option => {
if (marchandisesToSelect.includes(option.text) || marchandisesToSelect.includes(option.value)) {
option.selected = true;
}
});
const event = new Event('change', { bubbles: true });
marchandiseSelector.dispatchEvent(event);
console.log(' ✓ Marchandises:', marchandisesToSelect.length);
}
// ═══ ZONES GÉOGRAPHIQUES ═══
if (tarifData.zones) {
let zonesCount = 0;
Object.keys(tarifData.zones).forEach(zoneKey => {
const checkbox = document.getElementById(zoneKey);
if (checkbox && tarifData.zones[zoneKey]) {
checkbox.checked = true;
zonesCount++;
}
});
console.log(' ✓ Zones:', zonesCount);
}
// ═══ PROTECTION JURIDIQUE ═══
if (tarifData.garantiesAdditionnelles?.pj) {
const switchPJ = document.getElementById('switchPJ');
if (switchPJ) {
switchPJ.checked = true;
console.log(' ✓ PJ activée');
// Afficher la section PJ
const pjSection = document.getElementById('pj-section');
if (pjSection) pjSection.style.display = 'block';
}
}
// ═══ RCE ═══
if (tarifData.rce?.checked) {
const choixRCE = document.getElementById('choixRCE');
if (choixRCE) {
choixRCE.checked = true;
console.log(' ✓ RCE activée');
// Afficher la section RCE
const rceSection = document.getElementById('section-rce');
if (rceSection) rceSection.style.display = 'block';
}
}
// ═══ TPPC ═══
if (tarifData.garantiesAdditionnelles?.tppc?.checked) {
const checkTPPC = document.getElementById('checkTPPC');
if (checkTPPC) {
checkTPPC.checked = true;
if (tarifData.garantiesAdditionnelles.tppc.capital) {
setValue('capitalTPPC', tarifData.garantiesAdditionnelles.tppc.capital);
}
if (tarifData.garantiesAdditionnelles.tppc.vehicules) {
setValue('vehiculesTPPC', tarifData.garantiesAdditionnelles.tppc.vehicules);
}
console.log(' ✓ TPPC');
}
}
// ═══ ENGAGEMENTS COMPLÉMENTAIRES ═══
const engagements = tarifData.engagementsComplementaires;
if (engagements) {
if (engagements.domicileImmatriculation?.checked) {
setValue('checkDomImmat', true);
console.log(' ✓ Domicile immatriculation');
}
if (engagements.contenantConfie?.checked) {
setValue('checkContConf', true);
console.log(' ✓ Contenant confié');
}
}
// ═══ SINISTRALITÉ ═══
if (tarifData.sinistralite) {
if (tarifData.sinistralite.nombre3ans) {
setValue('nbSinistres3ans', tarifData.sinistralite.nombre3ans);
}
if (tarifData.sinistralite.montant3ans) {
setValue('montantSinistres3ans', tarifData.sinistralite.montant3ans);
}
console.log(' ✓ Sinistralité');
}
// ═══ RÉSULTATS TARIFAIRES ═══
if (tarifData.resultats) {
const res = tarifData.resultats;
// Taux
if (res.tauxRCCHT) setValue('tauxRCCHT', res.tauxRCCHT);
if (res.tauxRCCTTC) setValue('tauxRCCTTC', res.tauxRCCTTC);
if (res.tauxRCEHT) setValue('tauxRCEHT', res.tauxRCEHT);
if (res.tauxRCETTC) setValue('tauxRCETTC', res.tauxRCETTC);
if (res.tauxTotalHT) setValue('tauxTotalHT', res.tauxTotalHT);
if (res.tauxTotalTTC) setValue('tauxTotalTTC', res.tauxTotalTTC);
// Cotisations
if (res.cotRCCHT) setValue('cotRCCHT', res.cotRCCHT);
if (res.cotRCCTTC) setValue('cotRCCTTC', res.cotRCCTTC);
if (res.cotRCEHT) setValue('cotRCEHT', res.cotRCEHT);
if (res.cotRCETTC) setValue('cotRCETTC', res.cotRCETTC);
if (res.cotPJHT) setValue('cotPJHT', res.cotPJHT);
if (res.cotPJTTC) setValue('cotPJTTC', res.cotPJTTC);
if (res.cotTotalHT) setValue('cotTotalHT', res.cotTotalHT);
if (res.cotTotalTTC) setValue('cotTotalTTC', res.cotTotalTTC);
console.log(' ✓ Résultats tarifaires');
}
// Forcer la mise à jour des éléments Materialize
if (window.M && window.M.FormSelect) {
const selects = document.querySelectorAll('select');
window.M.FormSelect.init(selects);
}
if (window.M && window.M.updateTextFields) {
window.M.updateTextFields();
}
console.log('✅ Pré-remplissage Projet terminé');
} catch (error) {
console.error('❌ Erreur lors du pré-remplissage Projet:', error);
}
}
// ═══════════════════════════════════════════════════════════════════════
// PRÉ-REMPLISSAGE PROJET → TARIF
// ═══════════════════════════════════════════════════════════════════════
/**
* Pré-remplit le formulaire Tarif avec les données du Projet.
* Cette fonction est appelée quand l'utilisateur passe du Projet au Tarif.
*
* @param {Object} projetData - Données complètes du projet
*
* @example
* prefillTarifFromProjet(projetData);
*/
function prefillTarifFromProjet(projetData) {
if (!projetData) {
console.warn('Pas de données projet à pré-remplir');
return;
}
console.log('📝 Pré-remplissage Tarif depuis Projet...');
try {
// CA
if (projetData.ca) {
setValue('CA', projetData.ca);
}
// Type de cotisation
if (projetData.typeCot) {
const radio = document.querySelector(`input[name="cotisation"][value="${projetData.typeCot}"]`);
if (radio) radio.checked = true;
}
// Zones géographiques
['zone1', 'zone2', 'zone3', 'zone4', 'zone5', 'zone6'].forEach(zone => {
if (projetData[zone]) {
setValue(zone, true);
}
});
// PJ
if (projetData.pj) {
setValue('checkPJ', true);
}
// RCE
if (projetData.autresRC) {
setValue('checkRCE', true);
}
console.log('✅ Pré-remplissage Tarif terminé');
} catch (error) {
console.error('❌ Erreur lors du pré-remplissage Tarif:', error);
}
}
// ═══════════════════════════════════════════════════════════════════════
// EXPORT PUBLIC
// ═══════════════════════════════════════════════════════════════════════
window.RCDataManager = {
collectAllTarifData,
prefillProjetFromTarif,
prefillTarifFromProjet,
FIELD_MAPPING
};
console.log('✅ RC Data Manager loaded');
})(window);

View File

@ -1,388 +0,0 @@
/**
*
* RC SYNC ORCHESTRATOR
*
*
* Ce module orchestre la synchronisation entre Tarif RC et Projet RC.
* Il s'intègre avec les formulaires existants sans les modifier.
*
* @requires rc-sync-utils.js
* @requires rc-data-manager.js
* @author AXA Transport Team
* @version 2.0.0
* @since 2026-02-17
*/
(function(window) {
'use strict';
// Attendre que les dépendances soient chargées
if (!window.RCSync || !window.RCDataManager) {
console.error('❌ Dépendances RC Sync manquantes');
return;
}
const { isChangeImpactingTarif, showReturnToTarifModal } = window.RCSync;
const { collectAllTarifData, prefillProjetFromTarif, prefillTarifFromProjet } = window.RCDataManager;
// ═══════════════════════════════════════════════════════════════════════
// CONFIGURATION
// ═══════════════════════════════════════════════════════════════════════
const SESSION_STORAGE_KEYS = {
TARIF_DATA: 'rc_tarif_validated_data',
PROJET_DATA: 'rc_projet_data',
TARIF_ORIGINAL: 'rc_tarif_original_for_comparison'
};
// ═══════════════════════════════════════════════════════════════════════
// DÉTECTION DE LA PAGE ACTIVE
// ═══════════════════════════════════════════════════════════════════════
/**
* Détecte la page active (tarif ou projet) depuis l'URL.
*
* @returns {'tarif'|'projet'|null} Page active ou null
*/
function detectActivePage() {
const params = new URLSearchParams(window.location.search);
const submenu = params.get('submenu');
if (submenu === 'tarif' || submenu === 'tarifrc') {
return 'tarif';
} else if (submenu === 'projet' || submenu === 'projetrc') {
return 'projet';
}
return null;
}
// ═══════════════════════════════════════════════════════════════════════
// GESTION SESSIONSTORAGE
// ═══════════════════════════════════════════════════════════════════════
/**
* Sauvegarde les données du tarif validé dans sessionStorage.
*
* @param {Object} tarifData - Données complètes du tarif
*/
function saveTarifDataToSession(tarifData) {
try {
sessionStorage.setItem(SESSION_STORAGE_KEYS.TARIF_DATA, JSON.stringify(tarifData));
sessionStorage.setItem(SESSION_STORAGE_KEYS.TARIF_ORIGINAL, JSON.stringify(tarifData));
console.log('✅ Données tarif sauvegardées en session');
} catch (error) {
console.error('❌ Erreur sauvegarde session:', error);
}
}
/**
* Récupère les données du tarif depuis sessionStorage.
*
* @returns {Object|null} Données du tarif ou null
*/
function getTarifDataFromSession() {
try {
const data = sessionStorage.getItem(SESSION_STORAGE_KEYS.TARIF_DATA);
return data ? JSON.parse(data) : null;
} catch (error) {
console.error('❌ Erreur lecture session:', error);
return null;
}
}
/**
* Récupère les données originales du tarif pour comparaison.
*
* @returns {Object|null} Données originales du tarif
*/
function getTarifOriginalDataFromSession() {
try {
const data = sessionStorage.getItem(SESSION_STORAGE_KEYS.TARIF_ORIGINAL);
return data ? JSON.parse(data) : null;
} catch (error) {
return null;
}
}
// ═══════════════════════════════════════════════════════════════════════
// HOOK: APRÈS VALIDATION TARIF
// ═══════════════════════════════════════════════════════════════════════
/**
* Hook appelé après la validation du tarif commercial.
* Collecte toutes les données et les sauvegarde en session.
*
* Cette fonction doit être appelée juste avant la redirection vers le projet.
*/
function onTarifValidated() {
console.log('🎯 Hook: Tarif validé, collecte des données...');
try {
// Collecter toutes les données du tarif
const tarifData = collectAllTarifData();
// Sauvegarder en session pour le pré-remplissage projet
saveTarifDataToSession(tarifData);
console.log('✅ Données tarif prêtes pour le projet');
} catch (error) {
console.error('❌ Erreur hook tarif validé:', error);
}
}
// ═══════════════════════════════════════════════════════════════════════
// INITIALISATION PAGE PROJET
// ═══════════════════════════════════════════════════════════════════════
/**
* Initialise le formulaire projet au chargement.
* Configure UNIQUEMENT la détection des changements impactants.
* Le pré-remplissage est géré par prefillFromTarif() existant dans projet-form-RC.js
*/
function initProjetPage() {
console.log('🚀 Initialisation RC Orchestrator pour page Projet...');
// Les données rc/tarif/projet sont DÉJÀ chargées depuis la base
// par le code existant dans projet-form-RC.js
// On configure juste la détection des changements
setTimeout(() => {
setupProjetChangeDetection();
}, 1000); // Attendre que prefillFromTarif() ait fini
}
/**
* Configure la détection des changements impactants dans le projet.
* Affiche un modal si l'utilisateur modifie un champ qui impacte le tarif.
* Utilise les variables globales rc/tarif depuis projet-form-RC.js
*/
function setupProjetChangeDetection() {
// Les données originales sont dans les variables globales window.tarif et window.rc
// définies par projet-form-RC.js
const tarifOriginal = window.tarif;
const rcOriginal = window.rc;
if (!tarifOriginal && !rcOriginal) {
console.log(' Pas de tarif/rc, pas de détection');
return;
}
console.log('👁️ Configuration détection changements...');
console.log('📋 Données originales:', { tarif: tarifOriginal, rc: rcOriginal });
// Liste COMPLÈTE des éléments à surveiller (tous les champs impactants)
const elementsToWatch = [
// CA et infos générales
'CA', 'chiffreAffaire', 'nombreVehicules', 'nbrVehicule',
// Zones géographiques
'zone1', 'zone2', 'zone3', 'zone4', 'zone5', 'zone6',
// Protection Juridique
'switchPJ', 'checkPJ',
// RCE
'choixRCE', 'checkRCE',
// TPPC
'checkTPPC', 'capitalTPPC', 'vehiculesTPPC',
// Engagements complémentaires
'checkDomImmat', 'checkContConf', 'checkDiffInv',
// Garanties additionnelles
'checkStationLavage', 'checkGarageInterne', 'checkCSE',
// Sinistralité
'nbSinistres3ans', 'montantSinistres3ans',
// Autres
'programmeInternationale', 'participationResultat'
];
// Ajouter des listeners sur tous les éléments surveillés
elementsToWatch.forEach(elementId => {
const element = document.getElementById(elementId);
if (!element) return;
const eventType = element.type === 'checkbox' ? 'change' : 'blur';
element.addEventListener(eventType, function(e) {
const fieldName = this.id;
const newValue = this.type === 'checkbox' ? this.checked : this.value;
console.log(`🔍 Changement détecté: ${fieldName} = ${newValue}`);
// Vérifier si c'est un champ impactant
if (isFieldImpactingTarif(fieldName)) {
console.warn(`⚠️ "${fieldName}" impacte le tarif !`);
showReturnToTarifModal(fieldName);
} else {
console.log(` "${fieldName}" n'impacte pas le tarif`);
}
});
});
// Surveiller les radio buttons (type de cotisation)
const radioTypeCot = document.querySelectorAll('input[name="typeCot"]');
radioTypeCot.forEach(radio => {
radio.addEventListener('change', function() {
console.log(`🔍 Changement type cotisation: ${this.value}`);
console.warn(`⚠️ Type de cotisation impacte le tarif !`);
showReturnToTarifModal('Type de cotisation');
});
});
// Surveiller le select activités
const activitySelector = document.getElementById('activity-selector');
if (activitySelector) {
activitySelector.addEventListener('change', function() {
const selectedValues = Array.from(this.selectedOptions).map(opt => opt.value);
console.log(`🔍 Changement activités:`, selectedValues);
console.warn(`⚠️ Activités impactent le tarif !`);
showReturnToTarifModal('Activités');
});
}
// Surveiller le select marchandises
const marchandiseSelector = document.getElementById('marchandise-selector');
if (marchandiseSelector) {
marchandiseSelector.addEventListener('change', function() {
const selectedValues = Array.from(this.selectedOptions).map(opt => opt.value);
console.log(`🔍 Changement marchandises:`, selectedValues);
console.warn(`⚠️ Marchandises impactent le tarif !`);
showReturnToTarifModal('Marchandises');
});
}
// Surveiller les boutons d'action sur les zones (Monde entier / Reset)
['btnMondeEntier', 'btnReset'].forEach(btnId => {
const btn = document.getElementById(btnId);
if (!btn) return;
btn.addEventListener('click', () => {
console.log(`🔍 Changement zones via ${btnId}`);
console.warn('⚠️ Zones géographiques impactent le tarif !');
showReturnToTarifModal('Zones géographiques');
});
});
console.log('✅ Détection changements configurée sur tous les champs impactants');
}
// ═══════════════════════════════════════════════════════════════════════
// INITIALISATION PAGE TARIF
// ═══════════════════════════════════════════════════════════════════════
/**
* Initialise le formulaire tarif au chargement.
* Pré-remplit depuis le projet si l'utilisateur vient du projet.
*/
function initTarifPage() {
console.log('🚀 Initialisation page Tarif...');
// Vérifier si on vient du projet
const projetData = JSON.parse(sessionStorage.getItem(SESSION_STORAGE_KEYS.PROJET_DATA) || 'null');
if (projetData && !getTarifDataFromSession()) {
// On a des données projet mais pas de tarif validé
// = L'utilisateur a commencé par le projet
console.log('📥 Pré-remplissage depuis projet...');
setTimeout(() => {
prefillTarifFromProjet(projetData);
}, 500);
}
}
// ═══════════════════════════════════════════════════════════════════════
// INTERCEPTION DES FONCTIONS EXISTANTES
// ═══════════════════════════════════════════════════════════════════════
/**
* Intercepte la fonction de validation du tarif commercial existante.
* Ajoute notre hook avant la redirection.
*/
function interceptTarifValidation() {
// Attendre que la fonction window.saveTarifRC soit disponible
const checkInterval = setInterval(() => {
if (window.saveTarifRC) {
clearInterval(checkInterval);
// Sauvegarder la fonction originale
const originalSaveTarifRC = window.saveTarifRC;
// Remplacer par notre version wrappée
window.saveTarifRC = async function(...args) {
console.log('🎯 Interception saveTarifRC...');
// Appeler la fonction originale
const result = await originalSaveTarifRC.apply(this, args);
// Si succès, appeler notre hook
if (result && result.valid) {
onTarifValidated();
}
return result;
};
console.log('✅ saveTarifRC intercepté');
}
}, 100);
// Timeout après 5 secondes
setTimeout(() => clearInterval(checkInterval), 5000);
}
// ═══════════════════════════════════════════════════════════════════════
// DÉMARRAGE AUTOMATIQUE
// ═══════════════════════════════════════════════════════════════════════
/**
* Initialise l'orchestrateur au chargement de la page.
*/
function init() {
console.log('🎼 RC Sync Orchestrator: Démarrage...');
const activePage = detectActivePage();
console.log(`📄 Page active détectée: ${activePage || 'aucune'}`);
if (activePage === 'tarif') {
interceptTarifValidation();
// Attendre que le formulaire soit initialisé
setTimeout(() => {
initTarifPage();
}, 1000);
} else if (activePage === 'projet') {
// Attendre que le formulaire soit initialisé
setTimeout(() => {
initProjetPage();
}, 1000);
}
}
// Démarrage au chargement du DOM
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// ═══════════════════════════════════════════════════════════════════════
// EXPORT PUBLIC
// ═══════════════════════════════════════════════════════════════════════
window.RCOrchestrator = {
onTarifValidated,
initProjetPage,
initTarifPage,
saveTarifDataToSession,
getTarifDataFromSession
};
console.log('✅ RC Sync Orchestrator loaded');
})(window);

View File

@ -1,464 +0,0 @@
/**
*
* RC SYNCHRONIZATION UTILITIES
*
*
* Ce module contient toutes les fonctions utilitaires pour la synchronisation
* bidirectionnelle entre les formulaires Tarif RC et Projet RC.
*
* @author AXA Transport Team
* @version 2.0.0
* @since 2026-02-17
*/
(function(window) {
'use strict';
// ═══════════════════════════════════════════════════════════════════════
// CONSTANTES
// ═══════════════════════════════════════════════════════════════════════
/**
* Liste exhaustive des champs qui impactent le calcul du tarif.
* Si l'un de ces champs est modifié dans le projet, un modal
* demandera à l'utilisateur de retourner au tarif.
*
* @constant {Array<string>}
*/
const TARIF_IMPACTING_FIELDS = [
// Chiffre d'affaires et type de contrat
'ca', 'chiffreAffaires', 'CA',
'typeCotisation', 'cotisation',
'nombreVehicules', 'nbVehicules',
// Activités RCC
'checkVoiturier', 'capitalVoiturier', 'actVoiturier',
'checkCommissionnaire', 'capitalCommissionnaire', 'actMultimodal',
'checkDemenageur', 'capitalDemenageur',
'checkLogistique', 'capitalLogistique',
'checkAutocariste', 'capitalAutocariste',
'checkAutres', 'capitalAutres',
// RCE
'checkRCE', 'autresRC',
// Activités complémentaires
'actComplVoiturier', 'actComplCommissionnaire', 'actComplDemenageur', 'actComplLogistique',
'activitesVoiturier', 'activitesCommissionnaire', 'activitesDemenageur', 'activitesLogistique',
// Marchandises
'marchandisesVoiturier', 'marchandisesCommissionnaire', 'marchandisesDemenageur',
'marchandisesLogistique', 'marchandisesAutocariste', 'marchandisesAutres',
'marOrdinaire', 'marRoulant', 'marEngins', 'marRoulantDem', 'marMobilerUsag',
'marPerissable', 'marAnimaux', 'marCiterne', 'marBeton', 'marExceptionnels', 'marVrac',
// Zones géographiques
'zone1', 'zone2', 'zone3', 'zone4', 'zone5', 'zone6',
// Extensions de garantie RCC
'extRCCModifCalArrim', 'extRCCFerroutage', 'extRCCFraisRecons',
'extRCCConfie', 'typeExtConfies', 'extRCCTPPC', 'extRCCRegie', 'extRCCSansMontageDemontage',
'checkDomImmat', 'capitalDomImmat', 'checkContConf', 'capitalContConf',
'checkDiffInv', 'capitalDiffInv', 'checkTPPC', 'capitalTPPC', 'vehiculesTPPC',
// Extensions de garantie RCE
'extRCEBraDebra', 'extRCEMontageDemontage',
// Garanties additionnelles
'checkStationLavage', 'checkGarageInterne', 'checkCSE', 'checkPJ', 'pj',
// Sinistralité
'sinistre', 'nbSinistres3ans', 'montantSinistres3ans'
];
// ═══════════════════════════════════════════════════════════════════════
// HELPERS - MANIPULATION DE VALEURS
// ═══════════════════════════════════════════════════════════════════════
/**
* Convertit une valeur en nombre en gérant les formats français et internationaux.
* Gère les espaces, virgules, points, et valeurs nulles/undefined.
*
* @param {string|number|null|undefined} x - Valeur à convertir
* @returns {number} Nombre converti ou 0 si impossible
*
* @example
* toNumber("1 234,56") // 1234.56
* toNumber("1.234,56") // 1234.56
* toNumber("1,234.56") // 1234.56
* toNumber(null) // 0
*/
function toNumber(x) {
if (x == null) return 0;
let value = String(x).trim();
if (!value) return 0;
value = value
.replace(/\s/g, '')
.replace(/[^\d.,-]/g, '');
if (!value) return 0;
const isNegative = value.startsWith('-');
value = value.replace(/-/g, '');
if (isNegative && value) {
value = '-' + value;
}
const hasComma = value.includes(',');
const hasDot = value.includes('.');
if (hasComma) {
value = value.replace(/\./g, '').replace(/,/g, '.');
} else if (hasDot) {
const dotMatches = value.match(/\./g);
const dotCount = dotMatches ? dotMatches.length : 0;
if (dotCount > 1) {
const parts = value.split('.');
const lastSegment = parts[parts.length - 1];
if (lastSegment.length === 3) {
value = parts.join('');
} else {
value = parts.slice(0, -1).join('') + '.' + lastSegment;
}
}
}
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : 0;
}
/**
* Récupère la valeur d'un élément par son ID de manière flexible.
* Gère les différents types d'éléments (input, select, textarea, etc.)
* et les cas l'ID contient des caractères spéciaux.
*
* @param {string} id - ID de l'élément
* @returns {HTMLElement|null} Élément trouvé ou null
*
* @example
* const element = getElementByIdFlexible("my-element");
*/
function getElementByIdFlexible(id) {
if (!id) return null;
const direct = document.getElementById(id);
if (direct) return direct;
try {
return document.querySelector(`[id="${id.replace(/"/g, '\\"')}"]`);
} catch (err) {
return null;
}
}
/**
* Récupère la valeur d'un champ de formulaire de manière sécurisée.
* Gère les inputs, selects, textareas, checkboxes, et contenus textuels.
*
* @param {string} elementId - ID de l'élément
* @returns {string|number|boolean|null} Valeur du champ
*
* @example
* getValue("ca") // "100000"
* getValue("checkPJ") // true
*/
function getValue(elementId) {
const element = getElementByIdFlexible(elementId);
if (!element) return null;
if (element.type === 'checkbox') {
return element.checked;
} else if (element.type === 'radio') {
const checked = document.querySelector(`input[name="${element.name}"]:checked`);
return checked ? checked.value : null;
} else if (element.tagName === 'SELECT') {
return element.value;
} else if (element.value !== undefined) {
return element.value;
} else {
return element.textContent || element.innerText || null;
}
}
/**
* Définit la valeur d'un champ de formulaire.
* Gère automatiquement le type de champ et met à jour l'interface.
*
* @param {string} elementId - ID de l'élément
* @param {any} value - Valeur à définir
*
* @example
* setValue("ca", 100000);
* setValue("checkPJ", true);
*/
function setValue(elementId, value) {
const element = getElementByIdFlexible(elementId);
if (!element) {
console.warn(`Élément non trouvé: ${elementId}`);
return;
}
if (element.type === 'checkbox') {
element.checked = Boolean(value);
} else if (element.type === 'radio') {
const radio = document.querySelector(`input[name="${element.name}"][value="${value}"]`);
if (radio) radio.checked = true;
} else if (element.tagName === 'SELECT') {
element.value = value;
// Réinitialiser Materialize select si présent
if (window.M && window.M.FormSelect) {
const instance = window.M.FormSelect.getInstance(element);
if (instance) instance.destroy();
window.M.FormSelect.init(element);
}
} else if (element.value !== undefined) {
element.value = value;
} else {
element.textContent = value;
}
}
// ═══════════════════════════════════════════════════════════════════════
// COMPARAISON DE DONNÉES
// ═══════════════════════════════════════════════════════════════════════
/**
* Compare deux tableaux pour vérifier leur égalité.
* Effectue une comparaison profonde élément par élément.
*
* @param {Array} arr1 - Premier tableau
* @param {Array} arr2 - Deuxième tableau
* @returns {boolean} true si les tableaux sont égaux
*
* @example
* arraysEqual([1,2,3], [1,2,3]) // true
* arraysEqual([1,2], [1,2,3]) // false
*/
function arraysEqual(arr1, arr2) {
if (!Array.isArray(arr1) || !Array.isArray(arr2)) return false;
if (arr1.length !== arr2.length) return false;
const sorted1 = [...arr1].sort();
const sorted2 = [...arr2].sort();
return sorted1.every((val, idx) => val === sorted2[idx]);
}
/**
* Compare deux valeurs en tenant compte de leur type.
* Gère les tableaux, objets, null, undefined, et valeurs primitives.
*
* @param {any} value1 - Première valeur
* @param {any} value2 - Deuxième valeur
* @returns {boolean} true si les valeurs sont égales
*
* @example
* valuesEqual([1,2], [2,1]) // true (ordre indépendant)
* valuesEqual(null, undefined) // true
* valuesEqual(100, "100") // true (conversion automatique)
*/
function valuesEqual(value1, value2) {
// Normaliser null et undefined
if (value1 == null && value2 == null) return true;
if (value1 == null || value2 == null) return false;
// Comparer les tableaux
if (Array.isArray(value1) && Array.isArray(value2)) {
return arraysEqual(value1, value2);
}
// Comparer les objets
if (typeof value1 === 'object' && typeof value2 === 'object') {
return JSON.stringify(value1) === JSON.stringify(value2);
}
// Comparer les nombres (avec conversion)
if (!isNaN(value1) && !isNaN(value2)) {
return toNumber(value1) === toNumber(value2);
}
// Comparaison standard
return value1 === value2;
}
// ═══════════════════════════════════════════════════════════════════════
// DÉTECTION DE CHANGEMENTS IMPACTANTS
// ═══════════════════════════════════════════════════════════════════════
/**
* Vérifie si un champ donné impacte le calcul du tarif.
* Se base sur la liste TARIF_IMPACTING_FIELDS.
*
* @param {string} fieldName - Nom du champ
* @returns {boolean} true si le champ impacte le tarif
*
* @example
* isFieldImpactingTarif("ca") // true
* isFieldImpactingTarif("dateEffet") // false
*/
function isFieldImpactingTarif(fieldName) {
return TARIF_IMPACTING_FIELDS.some(field =>
fieldName.includes(field) || field.includes(fieldName)
);
}
/**
* Vérifie si un changement de valeur impacte le tarif.
* Compare la nouvelle valeur avec les données originales du tarif.
*
* @param {string} fieldName - Nom du champ modifié
* @param {any} newValue - Nouvelle valeur
* @param {Object} tarifOriginalData - Données originales du tarif
* @returns {boolean} true si le changement impacte le tarif
*
* @example
* const impacted = isChangeImpactingTarif("ca", 200000, tarifData);
* if (impacted) showReturnToTarifModal();
*/
function isChangeImpactingTarif(fieldName, newValue, tarifOriginalData) {
// Vérifier si le champ est dans la liste des champs impactants
if (!isFieldImpactingTarif(fieldName)) {
return false;
}
// Si pas de données originales, pas d'impact possible
if (!tarifOriginalData) {
return false;
}
// Récupérer la valeur originale
const originalValue = tarifOriginalData[fieldName];
// Comparer les valeurs
return !valuesEqual(newValue, originalValue);
}
// ═══════════════════════════════════════════════════════════════════════
// MODAL DE RETOUR AU TARIF
// ═══════════════════════════════════════════════════════════════════════
/**
* Affiche le modal demandant à l'utilisateur de retourner au tarif.
* Ce modal s'affiche quand une modification dans le projet impacte
* le calcul du tarif.
*
* @param {string} [fieldName] - Nom du champ modifié (optionnel, pour info)
*
* @example
* showReturnToTarifModal("ca");
*/
function showReturnToTarifModal(fieldName) {
const modalId = 'modalRetourTarif';
let modal = document.getElementById(modalId);
// Créer le modal s'il n'existe pas
if (!modal) {
modal = createReturnToTarifModal();
document.body.appendChild(modal);
}
// Mettre à jour le message si un champ est spécifié
if (fieldName) {
const messageEl = modal.querySelector('#modalRetourTarifMessage');
if (messageEl) {
messageEl.innerHTML = `
Vous avez modifié <strong>"${fieldName}"</strong> qui impacte le calcul du tarif.
<br><br>
Vous devez retourner sur le formulaire Tarif pour recalculer et valider le nouveau tarif.
`;
}
}
// Ouvrir le modal
if (window.M && window.M.Modal) {
const instance = window.M.Modal.getInstance(modal) || window.M.Modal.init(modal);
instance.open();
}
}
/**
* Crée l'élément DOM du modal de retour au tarif.
*
* @returns {HTMLElement} Élément modal créé
* @private
*/
function createReturnToTarifModal() {
const modal = document.createElement('div');
modal.id = 'modalRetourTarif';
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-content">
<h5> Modification impactant le tarif</h5>
<p id="modalRetourTarifMessage">
Vous avez modifié une donnée qui impacte le calcul du tarif.
<br><br>
<strong>Vous devez retourner sur le formulaire Tarif pour recalculer et valider le nouveau tarif.</strong>
</p>
</div>
<div class="modal-footer">
<a href="#!" class="modal-close waves-effect waves-red btn-flat">Annuler</a>
<a href="#!" class="waves-effect waves-green btn" onclick="window.RCSync.navigateToTarif()">
Aller au Tarif
</a>
</div>
`;
return modal;
}
/**
* Navigate vers l'onglet Tarif depuis le Projet.
*
* @example
* navigateToTarif();
*/
function navigateToTarif() {
// Fermer le modal
const modal = document.getElementById('modalRetourTarif');
if (modal && window.M) {
const instance = window.M.Modal.getInstance(modal);
if (instance) instance.close();
}
// Naviguer vers le tarif
const numParcours = new URLSearchParams(window.location.search).get('numParcours');
if (numParcours) {
window.location.href = `/navParcours?numParcours=${numParcours}&submenu=tarif`;
}
}
// ═══════════════════════════════════════════════════════════════════════
// EXPORT PUBLIC
// ═══════════════════════════════════════════════════════════════════════
/**
* API publique du module RC Sync.
* Toutes les fonctions exportées ici sont accessibles via window.RCSync.
*/
window.RCSync = {
// Helpers
toNumber,
getValue,
setValue,
getElementByIdFlexible,
// Comparaison
arraysEqual,
valuesEqual,
// Détection changements
isFieldImpactingTarif,
isChangeImpactingTarif,
// Modal
showReturnToTarifModal,
navigateToTarif,
// Constantes
TARIF_IMPACTING_FIELDS
};
console.log('✅ RC Sync Utils loaded');
})(window);

File diff suppressed because it is too large Load Diff

View File

@ -1,625 +0,0 @@
/**
* Utilitaires de synchro RC (collecte + pré-remplissage Tarif/Projet).
*/
(function(window) {
'use strict';
const { toNumber, getValue, setValue, getElementByIdFlexible } = window.RCSync;
// ═══════════════════════════════════════════════════════════════════════
// MAPPING DES CHAMPS TARIF ↔ PROJET
// ═══════════════════════════════════════════════════════════════════════
/**
* Mapping des champs Tarif <-> Projet.
* Format: { idTarif: idProjet }.
*/
const FIELD_MAPPING = {
// Informations générales
'CA': 'CA',
'chiffreAffaire': 'CA',
'nbVehicules': 'nombreVehicules',
'nbrVehicule': 'nombreVehicules',
// Type de cotisation
'cotisation': 'typeCot',
// Activités RCC - Voiturier
'checkVoiturier': 'actVoiturier',
'capitalVoiturier': 'valueActVoiturier',
// Activités RCC - Commissionnaire (Multimodal)
'checkCommissionnaire': 'actMultimodal',
'capitalCommissionnaire': 'valueActMultimodal',
// Activités RCC - Déménageur
'checkDemenageur': 'actDemEntr',
'capitalDemenageur': 'valueActDemEntr',
// Activités RCC - Logistique
'checkLogistique': 'actPrestaLog',
'capitalLogistique': 'valueActPrestaLog',
// RCE
'checkRCE': 'autresRC',
// Zones géographiques
'zone1': 'zone1',
'zone2': 'zone2',
'zone3': 'zone3',
'zone4': 'zone4',
'zone5': 'zone5',
'zone6': 'zone6',
// Protection Juridique
'checkPJ': 'pj',
// Garanties additionnelles - Engagements complémentaires
'checkDomImmat': 'extRCCConfie', // Simplifié
'checkContConf': 'extRCCConfie',
'checkTPPC': 'extRCCTPPC',
// Extensions RCC
'checkStationLavage': 'extRCCModifCalArrim',
// Extensions RCE
// (géré séparément car structure différente)
// Sinistralité
'sinistre': 'nbSinistres3ans'
};
// ═══════════════════════════════════════════════════════════════════════
// COLLECTE DES DONNÉES COMPLÈTES
// ═══════════════════════════════════════════════════════════════════════
/**
* Lit l'ensemble des champs utiles depuis le formulaire tarif.
*/
function collectAllTarifData() {
// Références flexibles aux éléments
const getEl = getElementByIdFlexible;
const data = {
// ═══ INFORMATIONS GÉNÉRALES ═══
typeCotisation: document.querySelector('input[name="cotisation"]:checked')?.value || null,
ca: toNumber(getValue('CA') || getValue('chiffreAffaire')),
nombreVehicules: Math.max(0, Math.round(toNumber(getValue('nbVehicules') || getValue('nbrVehicule')))),
// ═══ ACTIVITÉS RCC ═══
activites: {
voiturier: {
checked: getValue('checkVoiturier') || false,
capital: toNumber(getValue('capitalVoiturier')),
pourcentage: toNumber(getValue('pourcent_voiturier') || getValue('pourcentVoiturier/Loueur')),
isSet: Boolean(getValue('pourcent_voiturier')?.trim())
},
commissionnaire: {
checked: getValue('checkCommissionnaire') || false,
capital: toNumber(getValue('capitalCommissionnaire')),
pourcentage: toNumber(getValue('pourcent_commissionnaire')),
isSet: Boolean(getValue('pourcent_commissionnaire')?.trim())
},
demenageur: {
checked: getValue('checkDemenageur') || false,
capital: toNumber(getValue('capitalDemenageur')),
pourcentage: toNumber(getValue('pourcent_demenageur')),
isSet: Boolean(getValue('pourcent_demenageur')?.trim())
},
logistique: {
checked: getValue('checkLogistique') || false,
capital: toNumber(getValue('capitalLogistique')),
pourcentage: toNumber(getValue('pourcent_logistique')),
isSet: Boolean(getValue('pourcent_logistique')?.trim())
},
autocariste: {
checked: getValue('checkAutocariste') || false,
capital: toNumber(getValue('capitalAutocariste')),
pourcentage: toNumber(getValue('pourcent_autocariste')),
isSet: Boolean(getValue('pourcent_autocariste')?.trim())
},
autres: {
checked: getValue('checkAutres') || false,
capital: toNumber(getValue('capitalAutres')),
pourcentage: toNumber(getValue('pourcent_autres')),
isSet: Boolean(getValue('pourcent_autres')?.trim())
}
},
// ═══ RCE ═══
rce: {
checked: getValue('checkRCE') || false
},
// ═══ ACTIVITÉS COMPLÉMENTAIRES (JSON) ═══
activitesComplementaires: {
voiturier: collectActivitesComplJSON('voiturier'),
commissionnaire: collectActivitesComplJSON('commissionnaire'),
demenageur: collectActivitesComplJSON('demenageur'),
logistique: collectActivitesComplJSON('logistique')
},
// ═══ MARCHANDISES (JSON) ═══
marchandises: {
voiturier: collectMarchandisesJSON('voiturier'),
commissionnaire: collectMarchandisesJSON('commissionnaire'),
demenageur: collectMarchandisesJSON('demenageur'),
logistique: collectMarchandisesJSON('logistique'),
autocariste: collectMarchandisesJSON('autocariste'),
autres: collectMarchandisesJSON('autres')
},
// ═══ ZONES GÉOGRAPHIQUES ═══
zones: {
zone1: getValue('zone1') || false,
zone2: getValue('zone2') || false,
zone3: getValue('zone3') || false,
zone4: getValue('zone4') || false,
zone5: getValue('zone5') || false,
zone6: getValue('zone6') || false
},
// ═══ ENGAGEMENTS COMPLÉMENTAIRES ═══
engagementsComplementaires: {
domicileImmatriculation: {
checked: getValue('checkDomImmat') || false,
capital: toNumber(getValue('inputDomImmat'))
},
contenantConfie: {
checked: getValue('checkContConf') || false,
capital: toNumber(getValue('inputContConf'))
},
differenceInventaire: {
checked: getValue('checkDiffInv') || false,
capital: toNumber(getValue('inputDiffInv'))
}
},
// ═══ GARANTIES ADDITIONNELLES ═══
garantiesAdditionnelles: {
stationLavage: getValue('checkStationLavage') || false,
garageInterne: getValue('checkGarageInterne') || false,
cse: getValue('checkCSE') || false,
tppc: {
checked: getValue('checkTPPC') || false,
capital: toNumber(getValue('selTPPCcapital')),
vehicules: Math.max(0, Math.round(toNumber(getValue('selTPPCveh'))))
},
pj: getValue('checkPJ') || false
},
// ═══ SINISTRALITÉ ═══
sinistralite: {
nombre3ans: toNumber(getValue('sinistre')),
// Pas de champ montant dans le formulaire actuel.
montant3ans: 0
},
// ═══ RÉSULTATS DE CALCUL ═══
resultats: {
// Franchise 250
fr250: {
primeRCC: toNumber(getEl('rccFr250')?.textContent),
primeRCE: toNumber(getEl('rceFr250')?.textContent),
primePJ: toNumber(getEl('pjFr250')?.textContent),
primeTotal: toNumber(getEl('priceFr250')?.textContent),
tauxRCC: toNumber(getEl('tauxRccFr250')?.textContent),
tauxRCE: toNumber(getEl('tauxRceFr250')?.textContent),
tauxGlobal: toNumber(getEl('tauxGlobalFr250')?.textContent)
},
// Franchise 400
fr400: {
primeRCC: toNumber(getEl('rccFr400')?.textContent),
primeRCE: toNumber(getEl('rceFr400')?.textContent),
primePJ: toNumber(getEl('pjFr400')?.textContent),
primeTotal: toNumber(getEl('priceFr400')?.textContent),
tauxRCC: toNumber(getEl('tauxRccFr400')?.textContent),
tauxRCE: toNumber(getEl('tauxRceFr400')?.textContent),
tauxGlobal: toNumber(getEl('tauxGlobalFr400')?.textContent)
},
// Franchise 2000
fr2000: {
primeRCC: toNumber(getEl('rccFr2000')?.textContent),
primeRCE: toNumber(getEl('rceFr2000')?.textContent),
primePJ: toNumber(getEl('pjFr2000')?.textContent),
primeTotal: toNumber(getEl('priceFr2000')?.textContent),
tauxRCC: toNumber(getEl('tauxRccFr2000')?.textContent),
tauxRCE: toNumber(getEl('tauxRceFr2000')?.textContent),
tauxGlobal: toNumber(getEl('tauxGlobalFr2000')?.textContent)
},
franchiseChoisie: window.franchiseChoisie || null,
tarifCommercial: toNumber(getValue('tarifCom'))
},
// ═══ COMMENTAIRE ═══
commentaire: getValue('commentaire') || ''
};
console.log('Donnees tarif collectees:', data);
return data;
}
/**
* Récupère les activités complémentaires cochées pour un type d'activité.
*/
function collectActivitesComplJSON(typeActivite) {
let name;
switch(typeActivite.toLowerCase()) {
case 'voiturier':
name = 'actComplVoiturier/Loueur';
break;
case 'commissionnaire':
name = 'actComplCommissionnaire de Transport';
break;
case 'demenageur':
name = 'actComplDéménageur';
break;
case 'logistique':
name = 'actComplLogistique';
break;
default:
return JSON.stringify([]);
}
const checkboxes = document.querySelectorAll(`[name="${name}"] input[type="checkbox"]:checked`);
const activites = [];
checkboxes.forEach(cb => {
const text = cb.nextElementSibling ? cb.nextElementSibling.textContent.trim() : cb.value;
activites.push(text);
});
return JSON.stringify(activites);
}
/**
* Fonction helper pour collecter les marchandises depuis le formulaire.
*
* @param {string} typeActivite - Type d'activité
* @returns {string} JSON array des marchandises cochées
* @private
*/
function collectMarchandisesJSON(typeActivite) {
let name;
switch(typeActivite.toLowerCase()) {
case 'voiturier':
name = 'marVoiturier/Loueur';
break;
case 'commissionnaire':
name = 'marCommissionnaire de Transport';
break;
case 'demenageur':
name = 'marDéménageur';
break;
case 'logistique':
name = 'marLogistique';
break;
case 'autocariste':
name = 'marAutocariste';
break;
case 'autres':
name = 'marAutres activites';
break;
default:
return JSON.stringify([]);
}
const checkboxes = document.querySelectorAll(`[name="${name}"] input[type="checkbox"]:checked`);
const marchandises = [];
checkboxes.forEach(cb => {
const text = cb.nextElementSibling ? cb.nextElementSibling.textContent.trim() : cb.value;
marchandises.push(text);
});
return JSON.stringify(marchandises);
}
// ═══════════════════════════════════════════════════════════════════════
// PRÉ-REMPLISSAGE TARIF → PROJET
// ═══════════════════════════════════════════════════════════════════════
/**
* Pré-remplit le formulaire Projet avec les données du Tarif.
* Cette fonction est appelée quand l'utilisateur passe du Tarif au Projet.
*
* @param {Object} tarifData - Données complètes du tarif (de collectAllTarifData)
*
* @example
* const tarifData = collectAllTarifData();
* prefillProjetFromTarif(tarifData);
*/
function prefillProjetFromTarif(tarifData) {
if (!tarifData) {
console.warn('Pas de données tarif à pré-remplir');
return;
}
console.log('Pre-remplissage Projet depuis Tarif...');
try {
// ═══ INFORMATIONS GÉNÉRALES ═══
// CA
if (tarifData.ca) {
setValue('CA', tarifData.ca);
console.log(' CA:', tarifData.ca);
}
// Type de cotisation
if (tarifData.typeCotisation) {
const radio = document.querySelector(`input[name="typeCot"][value="${tarifData.typeCotisation}"]`);
if (radio) {
radio.checked = true;
console.log(' Type cotisation:', tarifData.typeCotisation);
}
}
// Nombre de véhicules
if (tarifData.nombreVehicules) {
setValue('nombreVehicules', tarifData.nombreVehicules);
console.log(' Vehicules:', tarifData.nombreVehicules);
}
// ═══ ACTIVITÉS ═══
const activitySelector = document.getElementById('activity-selector');
if (activitySelector && tarifData.activites) {
const activitesToAdd = [];
if (tarifData.activites.voiturier?.checked) {
activitesToAdd.push('Voiturier/Loueur');
}
if (tarifData.activites.commissionnaire?.checked) {
activitesToAdd.push('Commissionnaire de Transport');
}
if (tarifData.activites.demenageur?.checked) {
activitesToAdd.push('Déménageur d\'entreprises');
}
if (tarifData.activites.logistique?.checked) {
activitesToAdd.push('Prestataire logistique');
}
if (tarifData.activites.autocariste?.checked) {
activitesToAdd.push('Autocariste');
}
if (tarifData.activites.autres?.checked) {
activitesToAdd.push('Autres activités');
}
// Sélectionner les options dans le select
Array.from(activitySelector.options).forEach(option => {
if (activitesToAdd.includes(option.value)) {
option.selected = true;
}
});
// Trigger change pour créer les chips Materialize
const event = new Event('change', { bubbles: true });
activitySelector.dispatchEvent(event);
console.log(' Activites:', activitesToAdd.length);
}
// ═══ MARCHANDISES ═══
const marchandiseSelector = document.getElementById('marchandise-selector');
if (marchandiseSelector && tarifData.marchandises) {
const marchandisesToSelect = [];
// Parser les marchandises de chaque type
['voiturier', 'commissionnaire', 'demenageur', 'logistique', 'autocariste', 'autres'].forEach(type => {
const marchArray = tarifData.marchandises[type];
if (Array.isArray(marchArray)) {
marchArray.forEach(m => marchandisesToSelect.push(m));
}
});
// Sélectionner dans le select
Array.from(marchandiseSelector.options).forEach(option => {
if (marchandisesToSelect.includes(option.text) || marchandisesToSelect.includes(option.value)) {
option.selected = true;
}
});
const event = new Event('change', { bubbles: true });
marchandiseSelector.dispatchEvent(event);
console.log(' Marchandises:', marchandisesToSelect.length);
}
// ═══ ZONES GÉOGRAPHIQUES ═══
if (tarifData.zones) {
let zonesCount = 0;
Object.keys(tarifData.zones).forEach(zoneKey => {
const checkbox = document.getElementById(zoneKey);
if (checkbox && tarifData.zones[zoneKey]) {
checkbox.checked = true;
zonesCount++;
}
});
console.log(' Zones:', zonesCount);
}
// ═══ PROTECTION JURIDIQUE ═══
if (tarifData.garantiesAdditionnelles?.pj) {
const switchPJ = document.getElementById('switchPJ');
if (switchPJ) {
switchPJ.checked = true;
console.log(' PJ activee');
// Afficher la section PJ
const pjSection = document.getElementById('pj-section');
if (pjSection) pjSection.style.display = 'block';
}
}
// ═══ RCE ═══
if (tarifData.rce?.checked) {
const choixRCE = document.getElementById('choixRCE');
if (choixRCE) {
choixRCE.checked = true;
console.log(' RCE activee');
// Afficher la section RCE
const rceSection = document.getElementById('section-rce');
if (rceSection) rceSection.style.display = 'block';
}
}
// ═══ TPPC ═══
if (tarifData.garantiesAdditionnelles?.tppc?.checked) {
const checkTPPC = document.getElementById('checkTPPC');
if (checkTPPC) {
checkTPPC.checked = true;
if (tarifData.garantiesAdditionnelles.tppc.capital) {
setValue('capitalTPPC', tarifData.garantiesAdditionnelles.tppc.capital);
}
if (tarifData.garantiesAdditionnelles.tppc.vehicules) {
setValue('vehiculesTPPC', tarifData.garantiesAdditionnelles.tppc.vehicules);
}
console.log(' TPPC');
}
}
// ═══ ENGAGEMENTS COMPLÉMENTAIRES ═══
const engagements = tarifData.engagementsComplementaires;
if (engagements) {
if (engagements.domicileImmatriculation?.checked) {
setValue('checkDomImmat', true);
console.log(' Domicile immatriculation');
}
if (engagements.contenantConfie?.checked) {
setValue('checkContConf', true);
console.log(' Contenant confie');
}
}
// ═══ SINISTRALITÉ ═══
if (tarifData.sinistralite) {
if (tarifData.sinistralite.nombre3ans) {
setValue('nbSinistres3ans', tarifData.sinistralite.nombre3ans);
}
if (tarifData.sinistralite.montant3ans) {
setValue('montantSinistres3ans', tarifData.sinistralite.montant3ans);
}
console.log(' Sinistralite');
}
// ═══ RÉSULTATS TARIFAIRES ═══
if (tarifData.resultats) {
const res = tarifData.resultats;
// Taux
if (res.tauxRCCHT) setValue('tauxRCCHT', res.tauxRCCHT);
if (res.tauxRCCTTC) setValue('tauxRCCTTC', res.tauxRCCTTC);
if (res.tauxRCEHT) setValue('tauxRCEHT', res.tauxRCEHT);
if (res.tauxRCETTC) setValue('tauxRCETTC', res.tauxRCETTC);
if (res.tauxTotalHT) setValue('tauxTotalHT', res.tauxTotalHT);
if (res.tauxTotalTTC) setValue('tauxTotalTTC', res.tauxTotalTTC);
// Cotisations
if (res.cotRCCHT) setValue('cotRCCHT', res.cotRCCHT);
if (res.cotRCCTTC) setValue('cotRCCTTC', res.cotRCCTTC);
if (res.cotRCEHT) setValue('cotRCEHT', res.cotRCEHT);
if (res.cotRCETTC) setValue('cotRCETTC', res.cotRCETTC);
if (res.cotPJHT) setValue('cotPJHT', res.cotPJHT);
if (res.cotPJTTC) setValue('cotPJTTC', res.cotPJTTC);
if (res.cotTotalHT) setValue('cotTotalHT', res.cotTotalHT);
if (res.cotTotalTTC) setValue('cotTotalTTC', res.cotTotalTTC);
console.log(' Resultats tarifaires');
}
// Forcer la mise à jour des éléments Materialize
if (window.M && window.M.FormSelect) {
const selects = document.querySelectorAll('select');
window.M.FormSelect.init(selects);
}
if (window.M && window.M.updateTextFields) {
window.M.updateTextFields();
}
console.log('Pre-remplissage Projet termine');
} catch (error) {
console.error('Erreur lors du pre-remplissage Projet:', error);
}
}
// ═══════════════════════════════════════════════════════════════════════
// PRÉ-REMPLISSAGE PROJET → TARIF
// ═══════════════════════════════════════════════════════════════════════
/**
* Pré-remplit le formulaire Tarif avec les données du Projet.
* Cette fonction est appelée quand l'utilisateur passe du Projet au Tarif.
*
* @param {Object} projetData - Données complètes du projet
*
* @example
* prefillTarifFromProjet(projetData);
*/
function prefillTarifFromProjet(projetData) {
if (!projetData) {
console.warn('Pas de données projet à pré-remplir');
return;
}
console.log('Pre-remplissage Tarif depuis Projet...');
try {
// CA
if (projetData.ca) {
setValue('CA', projetData.ca);
}
// Type de cotisation
if (projetData.typeCot) {
const radio = document.querySelector(`input[name="cotisation"][value="${projetData.typeCot}"]`);
if (radio) radio.checked = true;
}
// Zones géographiques
['zone1', 'zone2', 'zone3', 'zone4', 'zone5', 'zone6'].forEach(zone => {
if (projetData[zone]) {
setValue(zone, true);
}
});
// PJ
if (projetData.pj) {
setValue('checkPJ', true);
}
// RCE
if (projetData.autresRC) {
setValue('checkRCE', true);
}
console.log('Pre-remplissage Tarif termine');
} catch (error) {
console.error('Erreur lors du pre-remplissage Tarif:', error);
}
}
// ═══════════════════════════════════════════════════════════════════════
// EXPORT PUBLIC
// ═══════════════════════════════════════════════════════════════════════
window.RCDataManager = {
collectAllTarifData,
prefillProjetFromTarif,
prefillTarifFromProjet,
FIELD_MAPPING
};
console.log('RC Data Manager loaded');
})(window);

View File

@ -1,377 +0,0 @@
/**
* Orchestrateur de synchro RC entre les écrans Tarif et Projet.
*/
(function(window) {
'use strict';
// Attendre que les dépendances soient chargées
if (!window.RCSync || !window.RCDataManager) {
console.error('Dependances RC Sync manquantes');
return;
}
const { isFieldImpactingTarif, showReturnToTarifModal } = window.RCSync;
const { collectAllTarifData, prefillTarifFromProjet } = window.RCDataManager;
// ═══════════════════════════════════════════════════════════════════════
// CONFIGURATION
// ═══════════════════════════════════════════════════════════════════════
const SESSION_STORAGE_KEYS = {
TARIF_DATA: 'rc_tarif_validated_data',
PROJET_DATA: 'rc_projet_data',
TARIF_ORIGINAL: 'rc_tarif_original_for_comparison'
};
// ═══════════════════════════════════════════════════════════════════════
// DÉTECTION DE LA PAGE ACTIVE
// ═══════════════════════════════════════════════════════════════════════
/**
* Détecte si on est sur la page tarif ou projet.
*/
function detectActivePage() {
const params = new URLSearchParams(window.location.search);
const submenu = params.get('submenu');
if (submenu === 'tarif' || submenu === 'tarifrc') {
return 'tarif';
} else if (submenu === 'projet' || submenu === 'projetrc') {
return 'projet';
}
return null;
}
/**
* Détecte le produit courant à partir de la session.
*
* @returns {string|null} Code produit en minuscule
*/
function detectCurrentProduct() {
try {
const contrat = JSON.parse(sessionStorage.getItem('contrat') || 'null');
const fromContrat = contrat?.produit;
if (fromContrat) return String(fromContrat).toLowerCase();
const parcours = JSON.parse(sessionStorage.getItem('parcours') || 'null');
const fromParcours = parcours?.["@expand"]?.contrat?.produit;
return fromParcours ? String(fromParcours).toLowerCase() : null;
} catch (error) {
console.warn('Impossible de lire le produit courant:', error);
return null;
}
}
// ═══════════════════════════════════════════════════════════════════════
// GESTION SESSIONSTORAGE
// ═══════════════════════════════════════════════════════════════════════
/**
* Sauvegarde le snapshot du tarif validé en sessionStorage.
*/
function saveTarifDataToSession(tarifData) {
try {
sessionStorage.setItem(SESSION_STORAGE_KEYS.TARIF_DATA, JSON.stringify(tarifData));
sessionStorage.setItem(SESSION_STORAGE_KEYS.TARIF_ORIGINAL, JSON.stringify(tarifData));
console.log('Donnees tarif sauvegardees en session');
} catch (error) {
console.error('Erreur sauvegarde session:', error);
}
}
/**
* Lit les données tarif sauvegardées en sessionStorage.
*/
function getTarifDataFromSession() {
try {
const data = sessionStorage.getItem(SESSION_STORAGE_KEYS.TARIF_DATA);
return data ? JSON.parse(data) : null;
} catch (error) {
console.error('Erreur lecture session:', error);
return null;
}
}
// ═══════════════════════════════════════════════════════════════════════
// HOOK: APRÈS VALIDATION TARIF
// ═══════════════════════════════════════════════════════════════════════
/**
* Hook appelé après validation tarif pour garder l'état en session.
*/
function onTarifValidated() {
console.log('Hook tarif valide: collecte des donnees');
try {
// Collecter toutes les données du tarif
const tarifData = collectAllTarifData();
// Sauvegarder en session pour le pré-remplissage projet
saveTarifDataToSession(tarifData);
console.log('Donnees tarif pretes pour le projet');
} catch (error) {
console.error('Erreur hook tarif valide:', error);
}
}
// ═══════════════════════════════════════════════════════════════════════
// INITIALISATION PAGE PROJET
// ═══════════════════════════════════════════════════════════════════════
/**
* Initialisation côté projet.
* Le pré-remplissage reste géré dans projet-form-rc.js.
*/
function initProjetPage() {
console.log('Initialisation RC Orchestrator pour la page Projet');
const currentProduct = detectCurrentProduct();
if (currentProduct === 'rc') {
console.log('Projet RC detecte: la gestion modal est deja faite dans projet-form-rc.js');
return;
}
// Les données rc/tarif/projet sont DÉJÀ chargées depuis la base
// par le code existant dans projet-form-RC.js
// On configure juste la détection des changements
setTimeout(() => {
setupProjetChangeDetection();
}, 1000); // Attendre que prefillFromTarif() ait fini
}
/**
* Fallback générique de détection d'impact sur la page projet.
*/
function setupProjetChangeDetection() {
// Les données originales sont dans les variables globales window.tarif et window.rc
// définies par projet-form-RC.js
const tarifOriginal = window.tarif;
const rcOriginal = window.rc;
if (!tarifOriginal && !rcOriginal) {
console.log('Pas de tarif/rc, pas de detection');
return;
}
console.log('Configuration de la detection des changements');
console.log('Donnees originales:', { tarif: tarifOriginal, rc: rcOriginal });
// Liste COMPLÈTE des éléments à surveiller (tous les champs impactants)
const elementsToWatch = [
// CA et infos générales
'CA', 'chiffreAffaire', 'nombreVehicules', 'nbrVehicule',
// Zones géographiques
'zone1', 'zone2', 'zone3', 'zone4', 'zone5', 'zone6',
// Protection Juridique
'switchPJ', 'checkPJ',
// RCE
'choixRCE', 'checkRCE',
// TPPC
'checkTPPC', 'capitalTPPC', 'vehiculesTPPC',
// Engagements complémentaires
'checkDomImmat', 'checkContConf', 'checkDiffInv',
// Garanties additionnelles
'checkStationLavage', 'checkGarageInterne', 'checkCSE',
// Sinistralité
'nbSinistres3ans', 'montantSinistres3ans',
// Autres
'programmeInternationale', 'participationResultat'
];
// Ajouter des listeners sur tous les éléments surveillés
elementsToWatch.forEach(elementId => {
const element = document.getElementById(elementId);
if (!element) return;
const eventType = element.type === 'checkbox' ? 'change' : 'blur';
element.addEventListener(eventType, function(e) {
const fieldName = this.id;
const newValue = this.type === 'checkbox' ? this.checked : this.value;
console.log(`Changement detecte: ${fieldName} = ${newValue}`);
// Vérifier si c'est un champ impactant
if (isFieldImpactingTarif(fieldName)) {
console.warn(`"${fieldName}" impacte le tarif`);
showReturnToTarifModal(fieldName);
} else {
console.log(`"${fieldName}" n'impacte pas le tarif`);
}
});
});
// Surveiller les radio buttons (type de cotisation)
const radioTypeCot = document.querySelectorAll('input[name="typeCot"]');
radioTypeCot.forEach(radio => {
radio.addEventListener('change', function() {
console.log(`Changement type cotisation: ${this.value}`);
console.warn('Type de cotisation impacte le tarif');
showReturnToTarifModal('Type de cotisation');
});
});
// Surveiller le select activités
const activitySelector = document.getElementById('activity-selector');
if (activitySelector) {
activitySelector.addEventListener('change', function() {
const selectedValues = Array.from(this.selectedOptions).map(opt => opt.value);
console.log('Changement activites:', selectedValues);
console.warn('Activites impactent le tarif');
showReturnToTarifModal('Activités');
});
}
// Surveiller le select marchandises
const marchandiseSelector = document.getElementById('marchandise-selector');
if (marchandiseSelector) {
marchandiseSelector.addEventListener('change', function() {
const selectedValues = Array.from(this.selectedOptions).map(opt => opt.value);
console.log('Changement marchandises:', selectedValues);
console.warn('Marchandises impactent le tarif');
showReturnToTarifModal('Marchandises');
});
}
// Surveiller les boutons d'action sur les zones (Monde entier / Reset)
['btnMondeEntier', 'btnReset'].forEach(btnId => {
const btn = document.getElementById(btnId);
if (!btn) return;
btn.addEventListener('click', () => {
console.log(`Changement zones via ${btnId}`);
console.warn('Zones geographiques impactent le tarif');
showReturnToTarifModal('Zones géographiques');
});
});
console.log('Detection des changements configuree');
}
// ═══════════════════════════════════════════════════════════════════════
// INITIALISATION PAGE TARIF
// ═══════════════════════════════════════════════════════════════════════
/**
* Initialise le formulaire tarif au chargement.
* Pré-remplit depuis le projet si l'utilisateur vient du projet.
*/
function initTarifPage() {
console.log('Initialisation page Tarif');
// Vérifier si on vient du projet
const projetData = JSON.parse(sessionStorage.getItem(SESSION_STORAGE_KEYS.PROJET_DATA) || 'null');
if (projetData && !getTarifDataFromSession()) {
// On a des données projet mais pas de tarif validé
// = L'utilisateur a commencé par le projet
console.log('Pre-remplissage depuis projet');
setTimeout(() => {
prefillTarifFromProjet(projetData);
}, 500);
}
}
// ═══════════════════════════════════════════════════════════════════════
// INTERCEPTION DES FONCTIONS EXISTANTES
// ═══════════════════════════════════════════════════════════════════════
/**
* Intercepte la fonction de validation du tarif commercial existante.
* Ajoute notre hook avant la redirection.
*/
function interceptTarifValidation() {
// Attendre que la fonction window.saveTarifRC soit disponible
const checkInterval = setInterval(() => {
if (window.saveTarifRC) {
clearInterval(checkInterval);
// Sauvegarder la fonction originale
const originalSaveTarifRC = window.saveTarifRC;
// Remplacer par notre version wrappée
window.saveTarifRC = async function(...args) {
console.log('Interception de saveTarifRC');
// Appeler la fonction originale
const result = await originalSaveTarifRC.apply(this, args);
// Si succès, appeler notre hook
if (result && result.valid) {
onTarifValidated();
}
return result;
};
console.log('saveTarifRC intercepte');
}
}, 100);
// Timeout après 5 secondes
setTimeout(() => clearInterval(checkInterval), 5000);
}
// ═══════════════════════════════════════════════════════════════════════
// DÉMARRAGE AUTOMATIQUE
// ═══════════════════════════════════════════════════════════════════════
/**
* Initialise l'orchestrateur au chargement de la page.
*/
function init() {
console.log('RC Sync Orchestrator: demarrage');
const activePage = detectActivePage();
console.log(`Page active detectee: ${activePage || 'aucune'}`);
if (activePage === 'tarif') {
interceptTarifValidation();
// Attendre que le formulaire soit initialisé
setTimeout(() => {
initTarifPage();
}, 1000);
} else if (activePage === 'projet') {
// Attendre que le formulaire soit initialisé
setTimeout(() => {
initProjetPage();
}, 1000);
}
}
// Démarrage au chargement du DOM
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// ═══════════════════════════════════════════════════════════════════════
// EXPORT PUBLIC
// ═══════════════════════════════════════════════════════════════════════
window.RCOrchestrator = {
onTarifValidated,
initProjetPage,
initTarifPage,
saveTarifDataToSession,
getTarifDataFromSession
};
console.log('RC Sync Orchestrator loaded');
})(window);

View File

@ -1,290 +0,0 @@
/**
* Fonctions utilitaires partagées entre les écrans RC Tarif et Projet.
*/
(function(window) {
'use strict';
// ═══════════════════════════════════════════════════════════════════════
// CONSTANTES
// ═══════════════════════════════════════════════════════════════════════
/**
* Champs qui influencent le calcul du tarif.
* On s'en sert pour savoir quand proposer un retour vers l'onglet Tarif.
*/
const TARIF_IMPACTING_FIELDS = [
// Chiffre d'affaires et type de contrat
'ca', 'chiffreAffaires', 'CA',
'typeCotisation', 'cotisation',
'nombreVehicules', 'nbVehicules',
// Activités RCC
'checkVoiturier', 'capitalVoiturier', 'actVoiturier',
'checkCommissionnaire', 'capitalCommissionnaire', 'actMultimodal',
'checkDemenageur', 'capitalDemenageur',
'checkLogistique', 'capitalLogistique',
'checkAutocariste', 'capitalAutocariste',
'checkAutres', 'capitalAutres',
// RCE
'checkRCE', 'autresRC',
// Activités complémentaires
'actComplVoiturier', 'actComplCommissionnaire', 'actComplDemenageur', 'actComplLogistique',
'activitesVoiturier', 'activitesCommissionnaire', 'activitesDemenageur', 'activitesLogistique',
// Marchandises
'marchandisesVoiturier', 'marchandisesCommissionnaire', 'marchandisesDemenageur',
'marchandisesLogistique', 'marchandisesAutocariste', 'marchandisesAutres',
'marOrdinaire', 'marRoulant', 'marEngins', 'marRoulantDem', 'marMobilerUsag',
'marPerissable', 'marAnimaux', 'marCiterne', 'marBeton', 'marExceptionnels', 'marVrac',
// Zones géographiques
'zone1', 'zone2', 'zone3', 'zone4', 'zone5', 'zone6',
// Extensions de garantie RCC
'extRCCModifCalArrim', 'extRCCFerroutage', 'extRCCFraisRecons',
'extRCCConfie', 'typeExtConfies', 'extRCCTPPC', 'extRCCRegie', 'extRCCSansMontageDemontage',
'checkDomImmat', 'capitalDomImmat', 'checkContConf', 'capitalContConf',
'checkDiffInv', 'capitalDiffInv', 'checkTPPC', 'capitalTPPC', 'vehiculesTPPC',
// Extensions de garantie RCE
'extRCEBraDebra', 'extRCEMontageDemontage',
// Garanties additionnelles
'checkStationLavage', 'checkGarageInterne', 'checkCSE', 'checkPJ', 'pj',
// Sinistralité
'sinistre', 'nbSinistres3ans', 'montantSinistres3ans'
];
// ═══════════════════════════════════════════════════════════════════════
// HELPERS - MANIPULATION DE VALEURS
// ═══════════════════════════════════════════════════════════════════════
/**
* Convertit une valeur texte/nombre en nombre JS.
* Accepte les formats FR/EN (espaces, virgules, points).
*/
function toNumber(x) {
if (x == null) return 0;
let value = String(x).trim();
if (!value) return 0;
value = value
.replace(/\s/g, '')
.replace(/[^\d.,-]/g, '');
if (!value) return 0;
const isNegative = value.startsWith('-');
value = value.replace(/-/g, '');
if (isNegative && value) {
value = '-' + value;
}
const hasComma = value.includes(',');
const hasDot = value.includes('.');
if (hasComma) {
value = value.replace(/\./g, '').replace(/,/g, '.');
} else if (hasDot) {
const dotMatches = value.match(/\./g);
const dotCount = dotMatches ? dotMatches.length : 0;
if (dotCount > 1) {
const parts = value.split('.');
const lastSegment = parts[parts.length - 1];
if (lastSegment.length === 3) {
value = parts.join('');
} else {
value = parts.slice(0, -1).join('') + '.' + lastSegment;
}
}
}
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : 0;
}
/**
* Récupère un élément par id, avec fallback querySelector.
*/
function getElementByIdFlexible(id) {
if (!id) return null;
const direct = document.getElementById(id);
if (direct) return direct;
try {
return document.querySelector(`[id="${id.replace(/"/g, '\\"')}"]`);
} catch (err) {
return null;
}
}
/**
* Lit la valeur d'un champ de formulaire selon son type.
*/
function getValue(elementId) {
const element = getElementByIdFlexible(elementId);
if (!element) return null;
if (element.type === 'checkbox') {
return element.checked;
} else if (element.type === 'radio') {
const checked = document.querySelector(`input[name="${element.name}"]:checked`);
return checked ? checked.value : null;
} else if (element.tagName === 'SELECT') {
return element.value;
} else if (element.value !== undefined) {
return element.value;
} else {
return element.textContent || element.innerText || null;
}
}
/**
* Ecrit une valeur dans un champ de formulaire selon son type.
*/
function setValue(elementId, value) {
const element = getElementByIdFlexible(elementId);
if (!element) {
console.warn(`Élément non trouvé: ${elementId}`);
return;
}
if (element.type === 'checkbox') {
element.checked = Boolean(value);
} else if (element.type === 'radio') {
const radio = document.querySelector(`input[name="${element.name}"][value="${value}"]`);
if (radio) radio.checked = true;
} else if (element.tagName === 'SELECT') {
element.value = value;
// Réinitialiser Materialize select si présent
if (window.M && window.M.FormSelect) {
const instance = window.M.FormSelect.getInstance(element);
if (instance) instance.destroy();
window.M.FormSelect.init(element);
}
} else if (element.value !== undefined) {
element.value = value;
} else {
element.textContent = value;
}
}
// ═══════════════════════════════════════════════════════════════════════
// DÉTECTION DE CHANGEMENTS IMPACTANTS
// ═══════════════════════════════════════════════════════════════════════
/**
* Dit si un nom de champ fait partie des champs qui influencent le tarif.
*/
function isFieldImpactingTarif(fieldName) {
return TARIF_IMPACTING_FIELDS.some(field =>
fieldName.includes(field) || field.includes(fieldName)
);
}
// ═══════════════════════════════════════════════════════════════════════
// MODAL DE RETOUR AU TARIF
// ═══════════════════════════════════════════════════════════════════════
/**
* Ouvre le modal "modif tarif" déjà présent dans la vue projet.
*/
function showReturnToTarifModal(fieldName) {
const modal = document.getElementById('modalModif');
if (!modal) {
console.warn('Modal "modalModif" introuvable');
return;
}
if (fieldName) {
console.warn(`Modification impactant le tarif: ${fieldName}`);
}
const okBtn = document.getElementById('modif-OK');
const noBtn = document.getElementById('modif-NO');
if (okBtn) {
okBtn.onclick = (event) => {
event?.preventDefault?.();
navigateToTarif();
};
}
if (noBtn) {
noBtn.onclick = (event) => {
event?.preventDefault?.();
if (!window.M || !window.M.Modal) return;
const instance = window.M.Modal.getInstance(modal);
if (instance) instance.close();
};
}
if (window.M && window.M.Modal) {
const instance = window.M.Modal.getInstance(modal) || window.M.Modal.init(modal);
instance.open();
}
}
/**
* Redirige vers l'onglet Tarif depuis le Projet.
*/
function navigateToTarif() {
// Fermer le modal
const modal = document.getElementById('modalModif');
if (modal && window.M) {
const instance = window.M.Modal.getInstance(modal);
if (instance) instance.close();
}
// Si on est sur le formulaire projet, on garde un snapshot local
// pour pré-remplir le tarif juste après la navigation.
try {
if (typeof window.collectProjetDataForTarifPrefill === 'function') {
const projetData = window.collectProjetDataForTarifPrefill();
if (projetData) {
sessionStorage.setItem('rc_projet_data', JSON.stringify(projetData));
}
}
} catch (error) {
console.error('Impossible de stocker les donnees projet avant redirection:', error);
}
// Naviguer vers le tarif
const numParcours = new URLSearchParams(window.location.search).get('numParcours');
if (numParcours) {
window.location.href = `/navParcours?numParcours=${numParcours}&submenu=tarif`;
}
}
// ═══════════════════════════════════════════════════════════════════════
// EXPORT PUBLIC
// ═══════════════════════════════════════════════════════════════════════
/**
* API publique du module RC Sync.
*/
window.RCSync = {
// Helpers
toNumber,
getValue,
setValue,
getElementByIdFlexible,
// Détection changements
isFieldImpactingTarif,
// Modal
showReturnToTarifModal,
navigateToTarif,
// Constantes
TARIF_IMPACTING_FIELDS
};
console.log('RC Sync Utils loaded');
})(window);

View File

@ -1,645 +0,0 @@
(function () {
/**
* Normalise numeric input.
*/
function normalizeNumericInput(raw) {
if (raw == null) return '';
return String(raw).trim().replace(/\s+/g, '');
}
/**
* Parse loose number.
*/
function parseLooseNumber(raw) {
const normalized = normalizeNumericInput(raw).replace(',', '.');
if (!normalized) return NaN;
const parsed = Number(normalized);
return Number.isFinite(parsed) ? parsed : NaN;
}
/**
* Formate french amount.
*/
function formatFrenchAmount(value, digits) {
const number = Number(value);
if (!Number.isFinite(number)) return '0.00';
return number.toLocaleString('fr-FR', {
minimumFractionDigits: digits,
maximumFractionDigits: digits
});
}
/**
* Synchronise floating labels.
*/
function syncFloatingLabels(root) {
const scope = root && root.querySelectorAll ? root : document;
const labels = scope.querySelectorAll('.rc-field-label[for]');
labels.forEach(function (label) {
const fieldId = label.getAttribute('for');
if (!fieldId) return;
const field = document.getElementById(fieldId);
if (!field) return;
const inputField = label.closest('.input-field');
const container = inputField || label.parentElement;
if (container) {
container.classList.add('rc-has-floating-label');
}
label.classList.add('rc-floating-label');
if (label.dataset.rcFloatBound === 'true') {
// Déjà lié: on force juste un resync visuel.
const hasValue = String(field.value || '').trim() !== '';
label.classList.toggle('active', hasValue || document.activeElement === field);
return;
}
const hasPrefix = Boolean(inputField && inputField.querySelector('.prefix'));
label.style.left = hasPrefix ? '3rem' : '0.75rem';
const syncState = function () {
const hasValue = String(field.value || '').trim() !== '';
label.classList.toggle('active', hasValue || document.activeElement === field);
};
field.addEventListener('focus', syncState);
field.addEventListener('blur', syncState);
field.addEventListener('input', syncState);
field.addEventListener('change', syncState);
label.dataset.rcFloatBound = 'true';
syncState();
});
if (window.M && typeof window.M.updateTextFields === 'function') {
window.M.updateTextFields();
}
}
/**
* Securise error slot.
*/
function ensureErrorSlot(field, customErrorId) {
if (!field) return null;
if (customErrorId) {
const found = document.getElementById(customErrorId);
if (found) return found;
}
if (field.id) {
const byConvention = document.getElementById(field.id + '-error');
if (byConvention) return byConvention;
}
if (field.dataset && field.dataset.errorTarget) {
const byData = document.getElementById(field.dataset.errorTarget);
if (byData) return byData;
}
let sibling = field.parentElement ? field.parentElement.querySelector('.rc-inline-error') : null;
if (sibling) return sibling;
sibling = document.createElement('span');
sibling.className = 'helper-text red-text rc-inline-error';
sibling.style.display = 'none';
field.insertAdjacentElement('afterend', sibling);
return sibling;
}
/**
* Gere pick field label.
*/
function pickFieldLabel(field, fallbackLabel) {
if (fallbackLabel) return fallbackLabel;
if (!field) return 'Champ';
if (field.dataset && field.dataset.rcLabel) return field.dataset.rcLabel;
const directLabel = field.id ? document.querySelector('label[for="' + field.id + '"]') : null;
if (directLabel && directLabel.textContent.trim()) return directLabel.textContent.trim();
const cardLabel = field.closest('.row, .input-field, td, .card-content')?.querySelector('.rc-field-label');
if (cardLabel && cardLabel.textContent.trim()) return cardLabel.textContent.trim();
const th = field.closest('td')?.parentElement?.querySelector('th');
if (th && th.textContent.trim()) return th.textContent.trim();
return field.name || field.id || 'Champ';
}
/**
* Met a jour field error.
*/
function setFieldError(field, errorSlot, message) {
if (!errorSlot) return;
errorSlot.textContent = message || '';
errorSlot.style.display = message ? 'block' : 'none';
if (field) {
if (message) {
field.classList.add('invalid');
} else {
field.classList.remove('invalid');
}
}
}
/**
* Verifie forbidden numeric chars.
*/
function hasForbiddenNumericChars(value) {
return /[A-Za-z€$£¥]/.test(value);
}
/**
* Gere validate numeric.
*/
function validateNumeric(value, options) {
const raw = normalizeNumericInput(value);
const decimals = Number.isInteger(options.decimals) ? options.decimals : 2;
const label = options.label || 'Champ';
const required = Boolean(options.required);
if (!raw) {
if (required) {
return { valid: false, message: label + ' est obligatoire.' };
}
return { valid: true };
}
if (hasForbiddenNumericChars(raw)) {
return { valid: false, message: label + ' doit contenir uniquement des chiffres, avec virgule ou point décimal.' };
}
if (/[^0-9.,-]/.test(raw)) {
return { valid: false, message: label + ' contient des caractères interdits.' };
}
if ((raw.match(/,/g) || []).length > 1 || (raw.match(/\./g) || []).length > 1) {
return { valid: false, message: label + ' a un format invalide.' };
}
if (raw.includes('-') && raw.indexOf('-') !== 0) {
return { valid: false, message: label + ' a un format invalide.' };
}
const decimalRegex = new RegExp('^-?\\d+(?:[.,]\\d{1,' + decimals + '})?$');
if (!decimalRegex.test(raw)) {
return { valid: false, message: label + ' doit être un nombre valide (max ' + decimals + ' décimales).' };
}
const number = parseLooseNumber(raw);
if (!Number.isFinite(number)) {
return { valid: false, message: label + ' doit être un nombre valide.' };
}
if (typeof options.min === 'number' && number < options.min) {
return { valid: false, message: label + ' doit être supérieur ou égal à ' + options.min + '.' };
}
if (typeof options.max === 'number' && number > options.max) {
return { valid: false, message: label + ' doit être inférieur ou égal à ' + options.max + '.' };
}
if (options.positive && number <= 0) {
return { valid: false, message: label + ' doit être strictement supérieur à 0.' };
}
return { valid: true, normalized: raw.replace(',', '.') };
}
/**
* Gere validate integer.
*/
function validateInteger(value, options) {
const raw = normalizeNumericInput(value);
const label = options.label || 'Champ';
const required = Boolean(options.required);
if (!raw) {
if (required) {
return { valid: false, message: label + ' est obligatoire.' };
}
return { valid: true };
}
if (!/^\d+$/.test(raw)) {
return { valid: false, message: label + ' doit contenir uniquement des chiffres entiers.' };
}
const number = Number(raw);
if (typeof options.min === 'number' && number < options.min) {
return { valid: false, message: label + ' doit être supérieur ou égal à ' + options.min + '.' };
}
if (typeof options.max === 'number' && number > options.max) {
return { valid: false, message: label + ' doit être inférieur ou égal à ' + options.max + '.' };
}
return { valid: true };
}
/**
* Gere validate immat.
*/
function validateImmat(value, options) {
const raw = String(value || '').trim().toUpperCase();
const label = options.label || 'Immatriculation';
if (!raw) {
if (options.required) {
return { valid: false, message: label + ' est obligatoire.' };
}
return { valid: true, normalized: '' };
}
if (!/^[A-Z0-9-]+$/.test(raw)) {
return { valid: false, message: label + ' doit contenir uniquement des lettres majuscules, chiffres et tirets.' };
}
return { valid: true, normalized: raw };
}
/**
* Gere validate text safe.
*/
function validateTextSafe(value, options) {
const raw = String(value || '').trim();
const label = options.label || 'Champ';
if (!raw) {
if (options.required) {
return { valid: false, message: label + ' est obligatoire.' };
}
return { valid: true };
}
if (/[<>;&"]/g.test(raw)) {
return { valid: false, message: label + ' contient des caractères interdits (<, >, &, ;, ").' };
}
const pattern = options.pattern || /^[A-Za-zÀ-ÖØ-öø-ÿ0-9\s'.,:/()\-_]+$/;
if (!pattern.test(raw)) {
return { valid: false, message: label + ' contient des caractères non autorisés.' };
}
return { valid: true };
}
/**
* Gere validate number or consult.
*/
function validateNumberOrConsult(raw, options) {
const value = String(raw || '').trim();
const lower = value.toLowerCase();
if (!value) {
if (options.required) {
return { valid: false, message: (options.label || 'Champ') + ' est obligatoire.' };
}
return { valid: true };
}
if (lower === 'nous consulter') {
return { valid: true };
}
return validateNumeric(value, options);
}
/**
* Gere evaluate rule.
*/
function evaluateRule(field, config) {
const profile = config.profile;
const label = pickFieldLabel(field, config.label);
const required = typeof config.requiredWhen === 'function' ? Boolean(config.requiredWhen(field)) : Boolean(config.required);
let result;
switch (profile) {
case 'numeric':
result = validateNumeric(field.value, {
label: label,
required: required,
decimals: config.decimals,
min: config.min,
max: config.max,
positive: config.positive
});
break;
case 'integer':
result = validateInteger(field.value, {
label: label,
required: required,
min: config.min,
max: config.max
});
break;
case 'immat':
result = validateImmat(field.value, {
label: label,
required: required
});
break;
case 'text':
result = validateTextSafe(field.value, {
label: label,
required: required,
pattern: config.pattern
});
break;
case 'number_or_consulter':
result = validateNumberOrConsult(field.value, {
label: label,
required: required,
decimals: config.decimals,
min: config.min,
max: config.max,
positive: config.positive
});
break;
default:
result = { valid: true };
}
if (result.normalized != null && config.normalize !== false && field.value !== result.normalized) {
field.value = result.normalized;
}
return {
valid: result.valid,
message: result.message || '',
label: label
};
}
/**
* Gere create summary element.
*/
function createSummaryElement(summaryId) {
if (!summaryId) return null;
return document.getElementById(summaryId);
}
/**
* Gere create guard.
*/
function createGuard(options) {
const summary = createSummaryElement(options.summaryId);
const summaryTitle = String(options.summaryTitle || 'Impossible de continuer car :');
const state = {
fieldRules: new Map(),
fieldErrors: new Map(),
externalChecks: new Map(),
targetButtons: [],
onChange: typeof options.onChange === 'function' ? options.onChange : null
};
const targetSelectors = Array.isArray(options.blockTargets) ? options.blockTargets : [];
targetSelectors.forEach(function (selector) {
document.querySelectorAll(selector).forEach(function (button) {
if (!button.dataset.rcOriginalDisabled) {
button.dataset.rcOriginalDisabled = button.disabled ? 'true' : 'false';
}
state.targetButtons.push(button);
});
});
/**
* Gere apply summary.
*/
function applySummary(messages) {
if (!summary) return;
if (!messages.length) {
summary.style.display = 'none';
summary.innerHTML = '';
return;
}
const uniqueMessages = Array.from(new Set(messages));
const list = uniqueMessages.map(function (msg) {
return '<li>' + msg + '</li>';
}).join('');
summary.innerHTML = [
'<div class="rc-blocking-title">' + summaryTitle + '</div>',
'<ul class="rc-blocking-list">',
list,
'</ul>'
].join('');
summary.style.display = 'block';
}
/**
* Gere apply blocking.
*/
function applyBlocking(hasErrors) {
state.targetButtons.forEach(function (button) {
if (!button) return;
if (hasErrors) {
button.disabled = true;
button.dataset.rcInvalid = 'true';
return;
}
if (button.dataset.rcInvalid === 'true') {
button.dataset.rcInvalid = 'false';
button.disabled = button.dataset.rcOriginalDisabled === 'true';
}
});
}
/**
* Gere recompute.
*/
function recompute() {
const messages = [];
state.fieldErrors.forEach(function (error) {
if (error && error.message) {
messages.push(error.message);
}
});
state.externalChecks.forEach(function (entry, key) {
try {
const checkResult = typeof entry.fn === 'function' ? entry.fn() : { valid: true };
if (!checkResult || checkResult.valid === false) {
const message = checkResult && checkResult.message ? checkResult.message : entry.message || key;
messages.push(message);
}
} catch (error) {
console.warn('[RC guard] Check externe en erreur:', key, error);
messages.push(entry.message || ('Validation en erreur: ' + key));
}
});
applySummary(messages);
applyBlocking(messages.length > 0);
if (state.onChange) {
state.onChange(messages);
}
return messages;
}
/**
* Gere validate field.
*/
function validateField(field) {
try {
const config = state.fieldRules.get(field);
if (!config) return;
if (!field || !field.isConnected || field.disabled) {
if (config.errorSlot) setFieldError(field, config.errorSlot, '');
state.fieldErrors.delete(config.key);
recompute();
return;
}
if (typeof config.activeWhen === 'function' && !config.activeWhen(field)) {
setFieldError(field, config.errorSlot, '');
state.fieldErrors.delete(config.key);
recompute();
return;
}
// Sécurité défaut: sans activeWhen explicite, on ignore un champ masqué.
if (typeof config.activeWhen !== 'function' && field.type !== 'hidden' && field.offsetParent === null) {
setFieldError(field, config.errorSlot, '');
state.fieldErrors.delete(config.key);
recompute();
return;
}
const result = evaluateRule(field, config);
setFieldError(field, config.errorSlot, result.valid ? '' : result.message);
if (result.valid) {
state.fieldErrors.delete(config.key);
} else {
state.fieldErrors.set(config.key, {
message: result.message
});
}
recompute();
} catch (error) {
const config = state.fieldRules.get(field);
const key = config && config.key ? config.key : 'champ-inconnu';
console.warn('[RC guard] Validation en erreur:', key, error);
if (config && config.errorSlot) {
setFieldError(field, config.errorSlot, 'Erreur de validation, rechargez la page.');
}
state.fieldErrors.set(key, {
message: 'Erreur de validation sur un champ du formulaire.'
});
recompute();
}
}
/**
* Gere attach field.
*/
function attachField(field, config) {
if (!field) return;
const key = config.key || (field.id ? field.id : (field.name ? field.name : ('field_' + state.fieldRules.size)));
if (field.dataset && field.dataset.rcGuardAttached === 'true' && state.fieldRules.has(field)) return;
const fieldConfig = Object.assign({}, config, {
key: key,
errorSlot: ensureErrorSlot(field, config.errorId)
});
state.fieldRules.set(field, fieldConfig);
if (field.dataset) {
field.dataset.rcGuardAttached = 'true';
}
const handler = function () {
validateField(field);
};
field.addEventListener('input', handler);
field.addEventListener('change', handler);
field.addEventListener('blur', handler);
validateField(field);
}
/**
* Enregistre field.
*/
function registerField(selectorOrElement, config) {
if (!selectorOrElement || !config || !config.profile) return;
if (typeof selectorOrElement === 'string') {
document.querySelectorAll(selectorOrElement).forEach(function (element) {
attachField(element, config);
});
return;
}
attachField(selectorOrElement, config);
}
/**
* Gere observe.
*/
function observe(selector, config) {
if (!selector || !config || !config.profile) return;
registerField(selector, config);
const observer = new MutationObserver(function () {
registerField(selector, config);
});
observer.observe(document.body, {
childList: true,
subtree: true
});
return observer;
}
/**
* Enregistre external.
*/
function registerExternal(key, fn, fallbackMessage) {
if (!key || typeof fn !== 'function') return;
state.externalChecks.set(key, {
fn: fn,
message: fallbackMessage || ''
});
recompute();
}
/**
* Gere refresh.
*/
function refresh() {
state.fieldRules.forEach(function (_cfg, field) {
validateField(field);
});
return recompute();
}
return {
registerField: registerField,
observe: observe,
registerExternal: registerExternal,
refresh: refresh,
formatFrenchAmount: formatFrenchAmount,
parseLooseNumber: parseLooseNumber
};
}
window.RCValidationUtils = {
createGuard: createGuard,
parseLooseNumber: parseLooseNumber,
formatFrenchAmount: formatFrenchAmount,
syncFloatingLabels: syncFloatingLabels
};
})();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,198 +0,0 @@
function isValidEmail(email) {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailPattern.test(email);
}
function validateField(fieldId, showErrors = true) {
try {
const inputElement = document.getElementById(fieldId);
if (!inputElement) {
return true;
}
const rawValue = inputElement.value;
const value = typeof rawValue === 'string' ? rawValue.trim() : String(rawValue ?? '').trim();
const hasValidationRules = typeof validationRules !== 'undefined' && validationRules;
const rule = hasValidationRules ? validationRules[fieldId] : null; // Importé de js/json/json-verif-form.js
const errorElement = document.getElementById(`${fieldId}-error`);
if (!rule) {
if (showErrors && errorElement) {
errorElement.textContent = "";
errorElement.style.display = 'none';
}
return true;
}
let isValid = true;
let errorMessage = "";
// Vérifie si le champ est requis et vide
if (rule.required && (!value || value == '')) {
errorMessage = "Ce champ est obligatoire.";
isValid = false;
}
// Vérifie la longueur de la saisie si spécifié et nécessaire
if (isValid && value && rule.length && value.length !== rule.length) {
errorMessage = `Veuillez saisir ${rule.length} caractères, il y en a actuellement ${value.length}.`;
isValid = false;
}
// Vérifie si la saisie est un nombre sans décimale ou avec un maximum de deux chiffres après la virgule
if (isValid && value && rule.numberFormat) {
const isNumberFormatValid = /^(?:\d+|\d+\.\d{1,2})$/.test(value);
if (!isNumberFormatValid) {
errorMessage = "Le champ doit être nombre de deux décimal maximum '00.00' ou entier '00'";
isValid = false;
}
}
// Vérifie si la saisie est un taux sans décimale ou avec un maximum de trois chiffres après la virgule
if (isValid && value && rule.tauxFormat) {
const isTauxFormatValid = /^(?:\d+|\d+\.\d{1,3})$/.test(value);
if (!isTauxFormatValid) {
errorMessage = "Le champ doit être un nombre avec un maximum de trois décimales '00.000' ou entier '00'";
isValid = false;
}
}
// Vérifie si le champ doit être compris dans un certain intervalle
if (isValid && value && rule.range) {
const numberValue = parseFloat(value);
if (isNaN(numberValue) || numberValue < rule.range.min || numberValue > rule.range.max) {
errorMessage = rule.errorMsg;
isValid = false;
}
}
// Valide l'adresse e-mail seulement si le champ n'est pas vide
if (isValid && value && rule.email) {
if (!isValidEmail(value)) {
errorMessage = rule.errorMsg;
isValid = false;
}
}
// Vérifie la présence de caractères potentiellement problématiques
const specialCharsPattern = /[;">&<]/g;
let invalidChars = value.match(specialCharsPattern);
if (isValid && value && invalidChars) {
invalidChars = [...new Set(invalidChars)];
errorMessage = `Caractère(s) invalide(s) détecté(s) [ ${invalidChars.join(" ")} ].`;
isValid = false;
}
// Vérifie si le champ commence par le caractère spécifié
if (isValid && value && rule.startsWith && !value.startsWith(rule.startsWith)) {
errorMessage = `Le champ doit commencer par '${rule.startsWith}' majuscule.`;
isValid = false;
}
// Vérifie si le champ date est correct
if (isValid && value && rule.dateFormat) {
const isDateFormatValid = /^(0[1-9]|[12][0-9]|3[01])\/(0[1-9]|1[012])\/\d{4}$|^00\/00\/0000$/.test(value);
if (!isDateFormatValid) {
errorMessage = "Le champ doit être au format JJ//MM/AAAA ou 00/00/0000.";
isValid = false;
}
}
// Vérifie si la saisie ne contient pas d'espace
if (isValid && value && rule.noSpace) {
const isNoSpaceValid = /^\S*$/.test(value);
if (!isNoSpaceValid) {
errorMessage = "La saisie ne doit pas contenir d'espaces.";
isValid = false;
}
}
// Vérifie si le champ date est correct au format JJ/MM
if (isValid && value && rule.dateFormatShort) {
const isDateFormatShortValid = /^(0[1-9]|[12][0-9]|3[01])\/(0[1-9]|1[012])$|^00\/00$/.test(value);
if (!isDateFormatShortValid) {
errorMessage = "Le champ doit être au format JJ/MM ou 00/00.";
isValid = false;
}
}
// Vérifie la casse des caractères si spécifié
if (isValid && value && rule.case) {
if (rule.case === "upper" && value !== value.toUpperCase()) {
errorMessage = "Le champ doit être en majuscules.";
isValid = false;
} else if (rule.case === "lower" && value !== value.toLowerCase()) {
errorMessage = "Le champ doit être en minuscules.";
isValid = false;
}
}
// Vérifie si le champ doit être numérique
if (isValid && value && rule.digit) {
const isNumeric = /^\d+$/.test(value); // Vérifie si la chaîne est un nombre entier
if (!isNumeric) {
errorMessage = "Le champ doit contenir uniquement des chiffres.";
isValid = false;
}
}
// Affiche ou cache le message d'erreur selon la validité du champ
if (showErrors && errorElement) {
errorElement.textContent = errorMessage;
errorElement.style.display = isValid ? 'none' : 'block';
}
return isValid;
} catch (error) {
console.warn('Validation interrompue pour le champ:', fieldId, error);
return false;
}
}
function updateSubmitButtonState(formId) {
let allFieldsValid = true;
const form = document.querySelector(`#${formId}`);
const hasValidationRules = typeof validationRules !== 'undefined' && validationRules;
if (!form) {
console.error('Formulaire non trouvé:', formId);
return;
}
if (!hasValidationRules) {
return;
}
for (const fieldId in validationRules) {
const inputElement = document.getElementById(fieldId);
if (inputElement && form.contains(inputElement)) {
try {
if (!validateField(fieldId, false)) {
allFieldsValid = false;
}
} catch (error) {
console.warn('Erreur pendant la validation du champ:', fieldId, error);
allFieldsValid = false;
}
}
}
const submitButtons = form.querySelectorAll('button[type="submit"], input[type="submit"]');
if (submitButtons && submitButtons.length > 0) {
submitButtons.forEach((button) => {
button.disabled = !allFieldsValid;
})
}
}

View File

@ -1,908 +0,0 @@
const objModNatureMar = {
"Toute autre marchandise non listée": {cat: "1", taux: 0.0004},
"Produits en bonbonnes": {cat: "2", taux: 0.0005},
"Liquides en bouteilles": {cat: "2", taux: 0.0005},
"Matériel Electrique: accesoires et appareillages électriques": {cat: "2", taux: 0.0005},
"Instruments de mesure": {cat: "2", taux: 0.0005},
"Objets en pierre, cristal, marbre, bois, ivoire, verre, porcelaine, céramique, terre-cuite, plâtre": {cat: "2", taux: 0.0005},
"Verres à vitres, Miroirs, marbre, tuiles, ardoises, carrelages, poterie, céramiques": {cat: "2", taux: 0.0005},
"Lampes et Luminaires": {cat: "2", taux: 0.0005},
"Meubles de consommation, meubles artisanaux massifs": {cat: "2", taux: 0.0005},
"Produits pharmaceutiques ou hospitaliers": {cat: "2", taux: 0.0005},
"Optiques": {cat: "2", taux: 0.0005},
"Matériels médicaux": {cat: "2", taux: 0.0005},
"Petit et gros électroménager": {cat: "2", taux: 0.0005},
"textiles et habillements, accessoires de mode": {cat: "2", taux: 0.0005},
"Matériel et équipement de sports et loisirs (vélos et trotinettes yc électriques) gyropodes": {cat: "2", taux: 0.0005},
"Parfumerie, cosmétique": {cat: "2", taux: 0.0005},
"Maroquinerie": {cat: "2", taux: 0.0005},
"Chaussures et articles chaussants": {cat: "2", taux: 0.0005},
"Téléviseurs, HiFi, vidéo, appareils photos, matériels informatiques y compris accessoires, consommables": {cat: "2", taux: 0.0005},
"Jeux et consoles vidéo": {cat: "2", taux: 0.0005},
"Pneumatiques": {cat: "2", taux: 0.0005},
"Horlogerie": {cat: "2", taux: 0.0005},
"Bijouterie fantaisie (non précieux max 300€ par unité)": {cat: "2", taux: 0.0005},
"Alimentation de luxe et épicerie fine": {cat: "2", taux: 0.0005},
"Instruments de musique, y compris pièces de rechange et accessoires": {cat: "2", taux: 0.0005},
"Outillage": {cat: "2", taux: 0.0005},
"Appareils et équipements sanitaires (robinetterie, droguerie, fonte ou faience)": {cat: "2", taux: 0.0005},
"Equipements Aéronautiques": {cat: "2", taux: 0.0005},
"Machines outils, Matériel agricole, travaux publics (matériaux de construction), câbles, chaudronnerie": {cat: "3", taux: 0.0005},
"Peintures et produits chimiques en fûts": {cat: "3", taux: 0.0005},
"Matériel Electrique": {cat: "3", taux: 0.0005},
"Moteurs, groupes électrogènes, transformateurs": {cat: "3", taux: 0.0005},
"Pièces détachées ou gros outillage industriel": {cat: "3", taux: 0.0005},
"Matériels ferroviaires": {cat: "3", taux: 0.0005},
"Produits frais ; Plantes vivantes": {cat: "4", taux: 0.0007},
"Produits congelés, réfrigérés, surgelés": {cat: "4", taux: 0.0007},
"Produits alimentaires et alimentation animale": {cat: "4", taux: 0.0007},
"Tous véhicules (hors matériel agricole) à moteurs: auto, deux roues, camion": {cat: "5", taux: 0.0012},
"Machines tractées / remorquées (Transportées sur leurs propres essieux)": {cat: "5", taux: 0.0012},
"Bijoux, pierres et métaux précieux": {cat: "6", taux: 0.005},
"Tableaux, dessins, esquisses": {cat: "6", taux: 0.005},
"Articles et vêtements de marque de luxe, fourrure naturelle": {cat: "6", taux: 0.005},
"Supports papiers, magnétiques, électroniques ou optiques de transfert de fonds ou de paiement": {cat: "6", taux: 0.005},
"Effets et bagages personnels dont ordinateurs et téléphones portables": {cat: "6", taux: 0.005},
"Toute marchandise dont la valeur marchande est sans commune mesure avec leur valeur intrinsèque: objets d'art, sculpture peinture, antiquités, objets de curiosité ou de collection, documents, échantillons, prototypes": {cat: "6", taux: 0.005},
"Orfévrerie, monnaies": {cat: "6", taux: 0.005},
"Billets de banque, actions, obligations, coupons, timbres poste, titres et valeurs de toute espèce": {cat: "6", taux: 0.005},
"Véhicule de collection": {cat: "6", taux: 0.005},
"Déménagements privés": {cat: "7", taux: 0.008},
"Déménagements d'entreprises": {cat: "8", taux: 0.5},
"Animaux vivants": {cat: "8", taux: 0.5},
"Métaux bruts": {cat: "8", taux: 0.5},
"Combustibles": {cat: "8", taux: 0.5},
"Matières premières": {cat: "8", taux: 0.5},
"Marchandises en vrac": {cat: "8", taux: 0.5}
}
const objModMontantCA = {
"250000": {
"5000": 0.84, "10000": 0.84, "25000": 0.84, "50000": 0.84,
"100000": 0.893, "310000": 0.945, "500000": 1.05, "750000": 1.155, "1000000": 1.208
},
"500000": {
"5000": 0.8, "10000": 0.8, "25000": 0.8, "50000": 0.8,
"100000": 0.85, "310000": 0.9, "500000": 1, "750000": 1.1, "1000000": 1.15
},
"1000000": {
"5000": 0.76, "10000": 0.76, "25000": 0.76, "50000": 0.76,
"100000": 0.808, "310000": 0.855, "500000": 0.95, "750000": 1.045, "1000000": 1.093
},
"2000000": {
"5000": 0.72, "10000": 0.72, "25000": 0.72, "50000": 0.72,
"100000": 0.765, "310000": 0.81, "500000": 0.9, "750000": 0.99, "1000000": 1.035
},
"3000000": {
"5000": 0.68, "10000": 0.68, "25000": 0.68, "50000": 0.68,
"100000": 0.723, "310000": 0.765, "500000": 0.85, "750000": 0.935, "1000000": 0.978
},
"4000000": {
"5000": 0.64, "10000": 0.64, "25000": 0.64, "50000": 0.64,
"100000": 0.68, "310000": 0.72, "500000": 0.8, "750000": 0.88, "1000000": 0.92
},
"5000000": {
"5000": 0.6, "10000": 0.6, "25000": 0.6, "50000": 0.6,
"100000": 0.638, "310000": 0.675, "500000": 0.75, "750000": 0.825, "1000000": 0.863
},
"7500000": {
"5000": 0.56, "10000": 0.56, "25000": 0.56, "50000": 0.56,
"100000": 0.595, "310000": 0.63, "500000": 0.7, "750000": 0.77, "1000000": 0.805
},
"10000000": {
"5000": 0.52, "10000": 0.52, "25000": 0.52, "50000": 0.52,
"100000": 0.553, "310000": 0.585, "500000": 0.65, "750000": 0.715, "1000000": 0.748
},
"15000000": {
"5000": 0.456, "10000": 0.456, "25000": 0.456, "50000": 0.456,
"100000": 0.485, "310000": 0.513, "500000": 0.57, "750000": 0.627, "1000000": 0.656
},
"20000000": {
"5000": 0.36, "10000": 0.36, "25000": 0.36, "50000": 0.36,
"100000": 0.383, "310000": 0.405, "500000": 0.45, "750000": 0.495, "1000000": 0.518
},
"30000000": {
"5000": 0.296, "10000": 0.296, "25000": 0.296, "50000": 0.296,
"100000": 0.315, "310000": 0.333, "500000": 0.37, "750000": 0.407, "1000000": 0.426
},
"40000000": {
"5000": 0.28, "10000": 0.28, "25000": 0.28, "50000": 0.28,
"100000": 0.298, "310000": 0.315, "500000": 0.35, "750000": 0.385, "1000000": 0.403
},
"50000000": {
"5000": 0.216, "10000": 0.216, "25000": 0.216, "50000": 0.216,
"100000": 0.23, "310000": 0.243, "500000": 0.27, "750000": 0.297, "1000000": 0.311
}
};
// const objModFranchiseTousCas = {
// 149: 1,
// 300: 0.9,
// 750: 0.85,
// 1500: 0.80,
// 3000: 0.75,
// 5000: 0.7
// };
const objModAct = {
"Activite de pre-presse": 1,
"Activites artistiques": 2.5,
"Activites comptables": 1,
"Activites d'architecture": 1,
"Activites de banques de donnees": 1.86,
"Activites de nettoyage": 1,
"Activites des auxiliaires medicaux": 1,
"Activites diverses liees au sport": 1,
"Activites graphiques auxiliaires": 1,
"Activites hospitalieres": 1,
"Activites juridiques": 1,
"Administration d'autres biens immobiliers": 1,
"Administration d'entreprises": 1.86,
"Administration d'immeubles residentiels": 1,
"Administration publique generale": 1,
"Affretement": 1,
"Agencement de lieux de vente": 1,
"Agences de presse": 1,
"Agences de voyage": 1,
"Agences immobilieres": 1,
"Agences, conseil en publicite": 1,
"Aide par le travail, ateliers proteges": 1,
"Analyses, essais et inspections techniques": 1,
"Appret et tannage des cuirs": 1,
"Assemblage de cartes electroniques pour compte de": 1,
"Assurance relevant du code de la mutualite": 1,
"Autre hebergement touristique": 1,
"Autre imprimerie (labeur)": 1,
"Autres activites de courrier": 1,
"Autres activites de realisation de logiciels": 1.86,
"Autres activites d'edition": 1,
"Autres activites manufacturieres n.c.a": 1,
"Autres activites recreatives": 1,
"Autres auxiliaires financiers": 1,
"Autres commerces de detail en magasin non speciali": 1,
"Autres commerces de gros de biens de consommation": 1,
"Autres commerces de gros specialises": 1,
"Autres enseignements": 1,
"Autres formes d'action sociale": 1,
"Autres intermediaires specialises du commerce": 1,
"Autres services personnels": 1,
"Autres transports routiers de voyageurs": 1,
"Autres travaux d'installation": 1,
"Autres travaux specialises de construction": 1.01,
"Auxiliaires d'assurance": 1,
"Bijouterie fantaisie": 1,
"Bijouterie, joaillerie, orfevrerie": 2.5,
"Biscotterie, biscuiterie, patisserie de conservati": 1.86,
"Blanchisserie, teinturerie de detail": 1,
"Boulangerie et boulangerie-patisserie": 1,
"Brasserie": 1,
"Captage, traitement et distribution d'eau": 1,
"Centrales d'achats alimentaires": 1,
"Centrales d'achats non alimentaires": 2.5,
"Centres de collecte et banques d'organes": 1,
"Champagnisation": 1,
"Charcuterie": 1,
"Chaudronnerie nucleaire": 1,
"Chaudronnerie -tuyauterie": 1.99,
"Chocolaterie, confiserie": 1,
"Cidrerie": 1,
"Commerce d'alimentation generale": 1,
"Commerce de detail alimentaire sur eventaires et m": 1,
"Commerce de detail d'appareils electromenagers et": 1.86,
"Commerce de detail d'articles de sport de loisirs": 1,
"Commerce de detail d'articles medicaux et orthoped": 1,
"Commerce de détail de biens d'occasion": 1,
"Commerce de detail de boissons": 1.01,
"Commerce de detail de carburants": 1,
"Commerce de detail de charbons et combustibles": 1,
"Commerce de detail de fleurs": 1.99,
"Commerce de detail de fruits et legumes": 1.01,
"Commerce de detail de la chaussure": 1.86,
"Commerce de detail de livres, journaux et papeteri": 1,
"Commerce de detail de maroquinerie et articles de": 1,
"Commerce de detail de meubles": 1,
"Commerce de detail de parfumerie et de produits de": 1.86,
"Commerce de detail de poissons, crustaces et mollu": 1,
"Commerce de detail de produits pharmaceutiques": 1,
"Commerce de detail de quincaillerie": 1,
"Commerce de detail de viandes et produits a base d": 1,
"Commerce de detail d'equipements automobiles": 1,
"Commerce de detail d'equipements du foyer": 1.86,
"Commerce de detail d'habillement": 1.86,
"Commerce de detail d'horlogerie et de bijouterie": 1,
"Commerce de detail divers en magasin specialise": 1,
"Commerce de detail d'optique, et de photographie": 1,
"Commerce de detail non alimentaire sur eventaires": 1,
"Commerce de gros alimentaire non specialise": 1.01,
"Commerce de gros d'animaux vivants": 1.86,
"Commerce de gros d'appareils electromenagers et de": 1,
"Commerce de gros d'autres machines et equipements": 1.86,
"Commerce de gros d'autres produits intermediaires": 1,
"Commerce de gros de bois et de produits derives": 1,
"Commerce de gros de boissons": 1.86,
"Commerce de gros de cafe, the, cacao et epices": 1,
"Commerce de gros de cereales et aliments pour le b": 1.86,
"Commerce de gros de combustibles": 1,
"Commerce de gros de composants et d'autres equipem": 1,
"Commerce de gros de cuirs et peaux": 1,
"Commerce de gros de dechets et debris": 1,
"Commerce de gros de fournitures et equipements div": 1,
"Commerce de gros de fournitures et equipements ind": 1.99,
"Commerce de gros de fournitures pour plomberie et": 1,
"Commerce de gros de fruits et legumes": 1.01,
"Commerce de gros de jouets": 1,
"Commerce de gros de la chaussure": 1.86,
"Commerce de gros de machines pour l'extraction, la": 1,
"Commerce de gros de machines pour l'industrie text": 1,
"Commerce de gros de machines-outils": 1.86,
"Commerce de gros de materiaux de construction et a": 1,
"Commerce de gros de materiel agricole": 1.99,
"Commerce de gros de materiel electrique": 1.01,
"Commerce de gros de minerais et metaux": 1,
"Commerce de gros de papeterie": 1,
"Commerce de gros de parfumerie et produits de beau": 1.86,
"Commerce de gros de poissons, crustaces et mollusq": 1,
"Commerce de gros de produits chimiques": 1.01,
"Commerce de gros de produits laitiers, œufs, huile": 1,
"Commerce de gros de produits pharmaceutiques": 1,
"Commerce de gros de produits pour entretien et ame": 1,
"Commerce de gros de produits surgeles": 1.86,
"Commerce de gros de quincaillerie": 1.86,
"Commerce de gros de sucre, chocolat et confiserie": 1,
"Commerce de gros de textiles": 1.99,
"Commerce de gros de vaisselle et verrerie de menag": 2.5,
"Commerce de gros de viandes de boucherie": 1,
"Commerce de gros d'equipements automobiles": 1.99,
"Commerce de gros d'habillement": 1.86,
"Commerce de gros d'ordinateurs, d'equipements info": 1.86,
"Commerce de gros non specialise": 1,
"Commerce de vehicules automobiles": 1,
"Commerce et reparation de motocycles": 1,
"Commerces de gros alimentaires specialises divers": 1.86,
"Conditionnement a facon": 1,
"Conseil en systemes informatiques": 1,
"Conseil pour les affaires et la gestion": 1,
"Construction de bateaux de plaisance": 1,
"Construction de batiments divers": 1,
"Construction de cellules d'aeronefs": 1,
"Construction de chaussees routieres et de sols spo": 1,
"Construction de maisons individuelles": 1,
"Construction de navires civiles": 1,
"Construction de vehicules automobiles": 1,
"Creches et garderies d'enfants": 1,
"Culture de cereales ; cultures industrielles": 2.5,
"Culture de legumes ; maraichage": 1,
"Culture et elevage associes": 1.01,
"Culture fruitiere": 1.86,
"Debits de boissons": 1,
"Decolletage": 1,
"Decoupage, emboutissage": 1,
"Demenagement": 2.5,
"Edition de chaines thematiques": 1,
"edition de journaux": 1,
"Edition de logiciels (non personnalises)": 1,
"edition de revues et periodiques": 1,
"edition d'enregistrements sonores": 1,
"Elevage d'autres animaux": 1,
"Elevage de bovins": 1,
"Elevage de porcins": 1,
"Elevage de volailles": 1,
"Enlevement et traitement des ordures menageres": 1,
"Ennoblissement textile": 1,
"Enquetes et securite": 1,
"Enseignement superieur": 1,
"Entreposage frigorifique": 1,
"Entreposage non frigorifique": 1,
"Entretien d'espaces verts": 1,
"Entretien et reparation de vehicules automobiles": 1,
"Entretien, reparation machines de bureau et materi": 1.86,
"epuration des eaux usees": 1,
"etudes de marche et sondage": 1,
"Exploitation de terrains de camping": 1,
"Exploitation forestiere": 1,
"Extraction de pierres pour la construction": 1,
"Fabrication d'aliments adaptes a l'enfant et diete": 1,
"Fabrication d'aliments pour animaux de ferme": 1,
"Fabrication d'appareils de pesage": 1.86,
"Fabrication d'appareils de reception, d'enregistre": 1.86,
"Fabrication d'appareils de telephonie": 1.86,
"Fabrication d'appareils d'eclairage": 1,
"Fabrication d'appareils medicochirurgicaux": 1,
"Fabrication d'articles ceramiques a usage domestiq": 1,
"Fabrication d'articles chaussants a maille": 1,
"Fabrication d'articles de papeterie": 1,
"Fabrication d'articles de robinetterie": 1.01,
"Fabrication d'articles de sport": 1.01,
"Fabrication d'articles de voyage et de maroquineri": 1,
"Fabrication d'articles divers en matieres plastiqu": 1,
"Fabrication d'articles en films metalliques": 1,
"Fabrication d'articles en papier a usage sanitaire": 1,
"Fabrication d'articles metalliques divers": 1,
"Fabrication d'articles metalliques menagers": 1,
"Fabrication d'autres articles confectionnes en tex": 1.01,
"Fabrication d'autres articles en caoutchouc": 1,
"Fabrication d'autres articles en papier ou en cart": 1.86,
"Fabrication d'autres machines d'usage general": 1.86,
"Fabrication d'autres machines-outils": 1,
"Fabrication d'autres produits chimiques inorganiqu": 1.86,
"Fabrication d'autres produits chimiques organiques": 1,
"Fabrication d'autres produits laitiers": 1.01,
"Fabrication d'autres produits pharmaceutiques": 1,
"Fabrication d'autres vetements et accessoires": 1,
"Fabrication de bicyclettes": 1,
"Fabrication de caravanes et vehicules de loisirs": 1,
"Fabrication de cartonnages": 1,
"Fabrication de charpentes et de menuiseries": 1.86,
"Fabrication de chaussures": 1,
"Fabrication de composants electroniques actifs": 1,
"Fabrication de composants passifs et de condensate": 1.86,
"Fabrication de compresseurs": 1,
"Fabrication de condiments et assaisonnements": 1.86,
"Fabrication de constructions metalliques": 1,
"Fabrication de coutellerie": 1,
"Fabrication de fibres artificielles ou synthetique": 1,
"Fabrication de fils a coudre": 1,
"Fabrication de fils et cables isoles": 1,
"Fabrication de fours et brûleurs": 1,
"Fabrication de glaces et sorbets": 1,
"Fabrication de jeux et jouets": 1,
"Fabrication de linge de maison et d'article d'ameu": 1,
"Fabrication de lunettes": 1.01,
"Fabrication de machines d'assemblage automatique": 1,
"Fabrication de machines de bureau": 1,
"Fabrication de machines d'imprimerie": 2.5,
"Fabrication de machines pour les industriels du pa": 1,
"Fabrication de machines pour les industries textil": 1,
"Fabrication de machines pour l'industrie agroalime": 1.01,
"Fabrication de machines pour travail du caoutchouc": 1.86,
"Fabrication de machines-outils a bois": 1,
"Fabrication de machines-outils a metaux": 1,
"Fabrication de machines-outils portative a moteur": 1,
"Fabrication de matelas": 1,
"Fabrication de materiel agricole": 1,
"Fabrication de materiel de sondage": 1,
"Fabrication de materiel d'imagerie medicale et de": 1,
"Fabrication de materiel electromagnetique industri": 1,
"Fabrication de materiel pour les industries chimiq": 2.5,
"Fabrication de materiels de distribution et de com": 1.99,
"Fabrication de materiels de travaux publics": 1,
"Fabrication de materiels electriques n.c.a": 1,
"Fabrication de materiels electriques pour moteurs": 1,
"Fabrication de medicaments": 1,
"Fabrication de menuiseries et fermetures metalliqu": 1.86,
"Fabrication de meubles de bureau et de magasin": 2.5,
"Fabrication de meubles meublants": 1,
"Fabrication de meubles n.c.a": 1.86,
"Fabrication de moteurs, generatrices et transforma": 2.5,
"Fabrication de motocycles": 1.01,
"Fabrication de moules et modeles": 1,
"Fabrication de non tisses": 1,
"Fabrication de panneaux de bois": 1.99,
"Fabrication de papier ou de carton": 1,
"Fabrication de papiers peints": 1.86,
"Fabrication de parfums et de produits pour la toil": 1,
"Fabrication de pates alimentaires": 1,
"Fabrication de peintures et de vernis": 1.01,
"Fabrication de petits articles metalliques": 1,
"Fabrication de petits articles textiles de literie": 1.01,
"Fabrication de pieces techniques en matieres plast": 1.86,
"Fabrication de plaques, feuilles, tubes et profile": 1,
"Fabrication de pompes": 1,
"Fabrication de produits agrochimiques": 1,
"Fabrication de produits chimiques a usage industri": 2.5,
"Fabrication de produits mineraux non metalliques n": 1,
"Fabrication de produits pharmaceutiques de base": 1.99,
"Fabrication de pull-overs et articles similaires": 2.5,
"Fabrication de radiateurs et de chaudieres pour le": 1,
"Fabrication de reservoirs, citernes et conteneurs": 1,
"Fabrication de ressorts": 1.99,
"Fabrication de roulements": 1,
"Fabrication de serrures et ferrures": 1,
"Fabrication de sieges": 1,
"Fabrication de spiritueux": 1,
"Fabrication de tapis et moquettes": 1,
"Fabrication de transmissions hydrauliques et pneum": 1.01,
"Fabrication de tubes en acier": 1,
"Fabrication de verre plat": 1,
"Fabrication de vetements de dessous": 2.5,
"Fabrication de vetements de dessus pour femmes": 1.86,
"Fabrication de vetements de dessus pour hommes": 1,
"Fabrication de vetements de travail": 1,
"Fabrication de vetements en cuir": 1.86,
"Fabrication de vetements sur mesure": 1,
"Fabrication d'elements en beton pour la constructi": 1,
"Fabrication d'elements en matieres plastiques pour": 1,
"Fabrication d'emballages en bois": 1.99,
"Fabrication d'emballages en matieres plastiques": 1.86,
"Fabrication d'emballages en papier": 1,
"Fabrication d'emballages metalliques legers": 1,
"Fabrication d'equipements aerauliques et frigorifi": 1,
"Fabrication d'equipements automobiles": 1,
"Fabrication d'equipements de controle des processu": 1,
"Fabrication d'equipements de levage et de manutent": 1,
"Fabrication d'equipements d'emballage et de condit": 1.01,
"Fabrication d'equipements d'emission et de transmi": 1,
"Fabrication d'etoffes a maille": 1,
"Fabrication d'huiles essentielles": 1,
"Fabrication d'instrumentation scientifique et tech": 1,
"Fabrication d'instruments d'aide a la navigation": 1,
"Fabrication d'instruments de musiques": 1,
"Fabrication d'instruments d'optique et de materiel": 1,
"Fabrication d'isolateurs en verre": 1.86,
"Fabrication d'isolateurs et pieces isolantes en ce": 1,
"Fabrication d'objets divers en bois": 1,
"Fabrication d'objets en liège, vannerie ou sparter": 1,
"Fabrication d'ordinateurs et d'autres equipements": 1,
"Fabrication d'organes mecaniques de transmissions": 2.5,
"Fabrication d'outillage a main": 1,
"Fabrication d'outillage mecanique": 1,
"Fabrication du verre creux": 1,
"Fabrication et faconnage d'articles techniques en": 1,
"Fabrication et transformation du verre plat": 2.5,
"Fabrication industrielle de pain et de patisserie": 1.01,
"Fabrication machines specialisees diverses": 1,
"Ficellerie, corderie, fabrication de filets": 1,
"Fonderie d'acier": 1,
"Fonderie d'autres metaux non ferreux": 1,
"Fonderie de fonte": 1,
"Fonderie de metaux legers": 1,
"Forages et sondages": 1,
"Forge, estampage, matricage": 1,
"Formation des adultes et formation continue": 1,
"Gestion de portefeuilles": 2.5,
"Gestion de salles de spectacle": 1,
"Gestion de supports de publicite": 1,
"Gestion d'installations sportives": 1,
"Gestion du patrimoine culturel": 1,
"Horlogerie": 1.86,
"Horticulture ; pepinieres": 1,
"Hotels avec restaurant": 1,
"Hotels de tourisme sans restaurant": 1,
"Industrie de la brosserie": 1,
"Industrie du carton ondule": 1,
"Industrie du poisson": 1,
"Industries alimentaires n.c.a": 1.86,
"Industries connexes de l'ameublement": 1,
"Industries textiles n.c.a": 1,
"Ingenierie, etudes techniques": 1,
"Installation d'eau et de gaz": 2.5,
"Enlevement et traitement des ordures menageres": 1,
"Ennoblissement textile": 1,
"Enquetes et securite": 1,
"Enseignement superieur": 1,
"Entreposage frigorifique": 1,
"Entreposage non frigorifique": 1,
"Entretien d'espaces verts": 1,
"Entretien et reparation de vehicules automobiles": 1,
"Entretien, reparation machines de bureau et materi": 1.86,
"epuration des eaux usees": 1,
"etudes de marche et sondage": 1,
"Exploitation de terrains de camping": 1,
"Exploitation forestiere": 1,
"Extraction de pierres pour la construction": 1,
"Fabrication d'aliments adaptes a l'enfant et diete": 1,
"Fabrication d'aliments pour animaux de ferme": 1,
"Fabrication d'appareils de pesage": 1.86,
"Fabrication d'appareils de reception, d'enregistre": 1.86,
"Fabrication d'appareils de telephonie": 1.86,
"Fabrication d'appareils d'eclairage": 1,
"Fabrication d'appareils medicochirurgicaux": 1,
"Fabrication d'articles ceramiques a usage domestiq": 1,
"Fabrication d'articles chaussants a maille": 1,
"Fabrication d'articles de papeterie": 1,
"Fabrication d'articles de robinetterie": 1.01,
"Fabrication d'articles de sport": 1.01,
"Fabrication d'articles de voyage et de maroquineri": 1,
"Fabrication d'articles divers en matieres plastiqu": 1,
"Fabrication d'articles en films metalliques": 1,
"Fabrication d'articles en papier a usage sanitaire": 1,
"Fabrication d'articles metalliques divers": 1,
"Fabrication d'articles metalliques menagers": 1,
"Fabrication d'autres articles confectionnes en tex": 1.01,
"Fabrication d'autres articles en caoutchouc": 1,
"Fabrication d'autres articles en papier ou en cart": 1.86,
"Fabrication d'autres machines d'usage general": 1.86,
"Fabrication d'autres machines-outils": 1,
"Fabrication d'autres produits chimiques inorganiqu": 1.86,
"Fabrication d'autres produits chimiques organiques": 1,
"Fabrication d'autres produits laitiers": 1.01,
"Fabrication d'autres produits pharmaceutiques": 1,
"Fabrication d'autres vetements et accessoires": 1,
"Fabrication de bicyclettes": 1,
"Fabrication de caravanes et vehicules de loisirs": 1,
"Fabrication de cartonnages": 1,
"Fabrication de charpentes et de menuiseries": 1.86,
"Fabrication de chaussures": 1,
"Fabrication de composants electroniques actifs": 1,
"Fabrication de composants passifs et de condensate": 1.86,
"Fabrication de compresseurs": 1,
"Fabrication de condiments et assaisonnements": 1.86,
"Fabrication de constructions metalliques": 1,
"Fabrication de coutellerie": 1,
"Fabrication de fibres artificielles ou synthetique": 1,
"Fabrication de fils a coudre": 1,
"Fabrication de fils et cables isoles": 1,
"Fabrication de fours et brûleurs": 1,
"Fabrication de glaces et sorbets": 1,
"Fabrication de jeux et jouets": 1,
"Fabrication de linge de maison et d'article d'ameu": 1,
"Fabrication de lunettes": 1.01,
"Fabrication de machines d'assemblage automatique": 1,
"Fabrication de machines de bureau": 1,
"Fabrication de machines d'imprimerie": 2.5,
"Fabrication de machines pour les industriels du pa": 1,
"Fabrication de machines pour les industries textil": 1,
"Fabrication de machines pour l'industrie agroalime": 1.01,
"Fabrication de machines pour travail du caoutchouc": 1.86,
"Fabrication de machines-outils a bois": 1,
"Fabrication de machines-outils a metaux": 1,
"Fabrication de machines-outils portative a moteur": 1,
"Fabrication de matelas": 1,
"Fabrication de materiel agricole": 1,
"Fabrication de materiel de sondage": 1,
"Fabrication de materiel d'imagerie medicale et de": 1,
"Fabrication de materiel electromagnetique industri": 1,
"Fabrication de materiel pour les industries chimiq": 2.5,
"Fabrication de materiels de distribution et de com": 1.99,
"Fabrication de materiels de travaux publics": 1,
"Fabrication de materiels electriques n.c.a": 1,
"Fabrication de materiels electriques pour moteurs": 1,
"Fabrication de medicaments": 1,
"Fabrication de menuiseries et fermetures metalliqu": 1.86,
"Fabrication de meubles de bureau et de magasin": 2.5,
"Fabrication de meubles meublants": 1,
"Fabrication de meubles n.c.a": 1.86,
"Fabrication de moteurs, generatrices et transforma": 2.5,
"Fabrication de motocycles": 1.01,
"Fabrication de moules et modeles": 1,
"Fabrication de non tisses": 1,
"Fabrication de panneaux de bois": 1.99,
"Fabrication de papier ou de carton": 1,
"Fabrication de papiers peints": 1.86,
"Fabrication de parfums et de produits pour la toil": 1,
"Fabrication de pates alimentaires": 1,
"Fabrication de peintures et de vernis": 1.01,
"Fabrication de petits articles metalliques": 1,
"Fabrication de petits articles textiles de literie": 1.01,
"Fabrication de pieces techniques en matieres plast": 1.86,
"Fabrication de plaques, feuilles, tubes et profile": 1,
"Fabrication de pompes": 1,
"Fabrication de produits agrochimiques": 1,
"Fabrication de produits chimiques a usage industri": 2.5,
"Fabrication de produits mineraux non metalliques n": 1,
"Fabrication de produits pharmaceutiques de base": 1.99,
"Fabrication de pull-overs et articles similaires": 2.5,
"Fabrication de radiateurs et de chaudieres pour le": 1,
"Fabrication de reservoirs, citernes et conteneurs": 1,
"Fabrication de ressorts": 1.99,
"Fabrication de roulements": 1,
"Fabrication de serrures et ferrures": 1,
"Fabrication de sieges": 1,
"Fabrication de spiritueux": 1,
"Fabrication de tapis et moquettes": 1,
"Fabrication de transmissions hydrauliques et pneum": 1.01,
"Fabrication de tubes en acier": 1,
"Fabrication de verre plat": 1,
"Fabrication de vetements de dessous": 2.5,
"Fabrication de vetements de dessus pour femmes": 1.86,
"Fabrication de vetements de dessus pour hommes": 1,
"Fabrication de vetements de travail": 1,
"Fabrication de vetements en cuir": 1.86,
"Fabrication de vetements sur mesure": 1,
"Fabrication d'elements en beton pour la constructi": 1,
"Fabrication d'elements en matieres plastiques pour": 1,
"Fabrication d'emballages en bois": 1.99,
"Fabrication d'emballages en matieres plastiques": 1.86,
"Fabrication d'emballages en papier": 1,
"Fabrication d'emballages metalliques legers": 1,
"Fabrication d'equipements aerauliques et frigorifi": 1,
"Fabrication d'equipements automobiles": 1,
"Fabrication d'equipements de controle des processu": 1,
"Fabrication d'equipements de levage et de manutent": 1,
"Fabrication d'equipements d'emballage et de condit": 1.01,
"Fabrication d'equipements d'emission et de transmi": 1,
"Fabrication d'etoffes a maille": 1,
"Fabrication d'huiles essentielles": 1,
"Fabrication d'instrumentation scientifique et tech": 1,
"Fabrication d'instruments d'aide a la navigation": 1,
"Fabrication d'instruments de musiques": 1,
"Fabrication d'instruments d'optique et de materiel": 1,
"Fabrication d'isolateurs en verre": 1.86,
"Fabrication d'isolateurs et pieces isolantes en ce": 1,
"Fabrication d'objets divers en bois": 1,
"Fabrication d'objets en liège, vannerie ou sparter": 1,
"Fabrication d'ordinateurs et d'autres equipements": 1,
"Fabrication d'organes mecaniques de transmissions": 2.5,
"Fabrication d'outillage a main": 1,
"Fabrication d'outillage mecanique": 1,
"Fabrication du verre creux": 1,
"Fabrication et faconnage d'articles techniques en": 1,
"Fabrication et transformation du verre plat": 2.5,
"Fabrication industrielle de pain et de patisserie": 1.01,
"Fabrication machines specialisees diverses": 1,
"Ficellerie, corderie, fabrication de filets": 1,
"Fonderie d'acier": 1,
"Fonderie d'autres metaux non ferreux": 1,
"Fonderie de fonte": 1,
"Fonderie de metaux legers": 1,
"Forages et sondages": 1,
"Forge, estampage, matricage": 1,
"Formation des adultes et formation continue": 1,
"Gestion de portefeuilles": 2.5,
"Gestion de salles de spectacle": 1,
"Gestion de supports de publicite": 1,
"Gestion d'installations sportives": 1,
"Gestion du patrimoine culturel": 1,
"Horlogerie": 1.86,
"Horticulture ; pepinieres": 1,
"Hotels avec restaurant": 1,
"Hotels de tourisme sans restaurant": 1,
"Industrie de la brosserie": 1,
"Industrie du carton ondule": 1,
"Industrie du poisson": 1,
"Industries alimentaires n.c.a": 1.86,
"Industries connexes de l'ameublement": 1,
"Industries textiles n.c.a": 1,
"Ingenierie, etudes techniques": 1,
"Installation d'eau et de gaz": 2.5,
"Installation d'equipements thermiques et de climat": 2.5,
"Intermediaires du commerce en bois et materiaux de": 1,
"Intermediaires du commerce en combustibles, metaux": 1,
"Intermediaires du commerce en machines, equipement": 1,
"Intermediaires du commerce en matieres premieres a": 1,
"Intermediaires du commerce en meubles, articles de": 1,
"Intermediaires du commerce en produits alimentaire": 1,
"Intermediaires du commerce en textiles, habillemen": 1,
"Intermediaires non specialises du commerce": 1.86,
"Laboratoires d'analyses medicales": 1,
"Levage, montage": 1,
"Location avec operateur de materiel de constructio": 1,
"Location d'autres biens immobiliers": 1.86,
"Location d'autres biens personnels et domestiques": 1,
"Location d'autres materiels de transport terrestre": 1.01,
"Location de camions avec conducteur": 1,
"Location de logements": 1,
"Location de longue duree de vehicules automobiles": 1,
"Location de machines de bureau et de materiel info": 1,
"Location de machines et equipements divers": 1,
"Location de machines et equipements pour la constr": 2.5,
"Location de materiels de transports aerien": 1,
"Location de terrains": 1,
"Maneges forains et parcs d'attractions": 1,
"Manutention non portuaire": 1.86,
"Marchand de biens immobiliers sans execution de tr": 1,
"Mecanique generale": 1,
"Menuiserie bois et matieres plastiques": 1,
"Menuiserie metallique ; serrurerie": 1.01,
"Messagerie, fret express": 1,
"Metallurgie des autres metaux non ferreux": 1,
"Metreurs, geometres": 1,
"Meunerie": 1,
"Moulinage et texturation de soie et textiles artif": 1.86,
"Organisation de foires et salons": 1.01,
"Organisation des transports internationaux": 2.5,
"Organisations associatives n.c.a": 1,
"Organisations patronales et consulaires": 1,
"Organismes de placement en valeurs immobilieres": 1,
"Patisserie": 1,
"Peinture": 1,
"Pisciculture, aquaculture": 1,
"Platrerie": 2.5,
"Pratique medicale": 1.01,
"Premiere transformation de l'aluminium": 1.86,
"Premiere transformation du plomb, du zinc ou de l'": 1,
"Preparation de la laine": 1,
"Preparation et filature du lin": 1,
"Preparation industrielle de produits a base de via": 1,
"Prestations techniques pour le cinema et la televi": 1,
"Production de films institutionnels et publicitair": 1,
"Production de films pour le cinema": 1,
"Production de programmes de television": 1,
"Production de viandes de boucherie": 1,
"Production de viandes de volaille": 1.86,
"Production d'eaux de vie naturelles": 1.86,
"Production d'electricite": 1,
"Production et distribution de chaleur": 1,
"Projection de films cinematographiques": 1,
"Promotion immobiliere de logements": 1,
"Raffinage de petrole": 1,
"Realisation de couvertures par elements": 1,
"Realisation de logiciel": 1,
"Realisation de reseaux": 1,
"Recherche-developpement en sciences humaines et so": 1,
"Recherche-developpement en sciences physiques et n": 1,
"Recuperation de matieres metalliques recyclables": 1,
"Recuperation de matieres non metalliques recyclabl": 1,
"Reliure": 1.99,
"Reparation d'articles personnels et domestiques n.": 1,
"Reparation de materiel electrique": 1,
"Reparation de materiel electronique grand public": 1,
"Reparation de montres, horloges et bijoux": 1,
"Reparation navale": 1,
"Restaurant de type rapide": 1,
"Restaurant de type traditionnel": 2.5,
"Revetement des sols et des murs": 1,
"Routage": 1,
"Sciage et rabotage du bois": 1,
"Secretariat et traduction": 1,
"Services annexes a la production": 1,
"Services annexes a l'elevage": 1,
"Services annexes a l'extraction d'hydrocarbures": 1,
"Services annexes aux spectacles": 1.86,
"Services aux cultures productives": 1,
"Services portuaires, maritimes et fluviaux": 1,
"Siderurgie": 1,
"Soins aux defunts": 1,
"Studios et autres activites photographiques": 1,
"Superettes": 1,
"Supermarches": 1,
"Supports juridiques de gestion de patrimoine": 1,
"Telecommunications (hors transmissions audiovisuel": 1,
"Terrassements divers, demolition": 1,
"Terrassements en grande masse": 1,
"Tissage de l'industrie cotonniere": 1,
"Tissage de l'industrie lainiere-cycle carde": 1,
"Tissage de soieries": 2.5,
"Traitement de donnees": 1,
"Traitement et revetement des metaux": 1,
"Traitements des autres dechets solides": 1,
"Transformation et conservation de fruits": 2.5,
"Transformation et conservation de legumes": 1,
"Transport de voyageurs par taxis": 1,
"Transports aeriens non reguliers": 1,
"Transports maritimes": 1,
"Transports par conduites": 1,
"Transports routiers de marchandises de proximite": 1,
"Transports routiers de marchandises interurbains": 1.01,
"Transports routiers reguliers de voyageurs": 1,
"Transports urbains de voyageurs": 2.5,
"Travail de la pierre": 1.01,
"Travaux de charpente": 1,
"Travaux de finition n.c.a": 1,
"Travaux de maconnerie general": 1,
"Travaux d'installation electrique": 1.99,
"Travaux d'isolation": 1,
"Tutelles des activites economiques": 1,
"Vente a domicile": 2.5,
"Vente par automate": 1,
"Vente par correspondance specialisee": 2.5,
"Vente par correspondance sur catalogue general": 1.01,
"Vinification": 1,
"Visserie et boulonnerie": 1,
"Viticulture": 1,
}
const objModCond = {
usuel: {
nom: "Cartons, Palettes, Conteneurs",
modulo: 1,
fluxDetailles: true,
fluxGlobal: true
},
nu: {
nom: "A nu",
modulo: 1.2,
fluxDetailles: true,
fluxGlobal: false
},
bennes: {
nom: "Vrac en Benne ou Citerne",
modulo: 1.2,
fluxDetailles: true,
fluxGlobal: false
},
sacs: {
nom: "En Sacs",
modulo: 1.2,
fluxDetailles: true,
fluxGlobal: false
},
conteneur: {
nom: "En Conteneur sur Ligne Régulière",
modulo: 1.2,
fluxDetailles: false,
fluxGlobal: true
}
}
const objModZoneTransp = {
"zone1": {
"terrestre": 1,
"maritime": 1.25,
"aerien": 0.5,
"multimodal": 1.25
},
"zone2": {
"terrestre": 1.1,
"maritime": 1.43,
"aerien": 0.5,
"multimodal": 1.43
},
"zone3": {
"terrestre": 1.1,
"maritime": 1.43,
"aerien": 0.5,
"multimodal": 1.43
},
"zone4": {
"terrestre": 1,
"maritime": 1.75,
"aerien": 0.5,
"multimodal": 1.75
},
"zone5": {
"terrestre": 1,
"maritime": 2.06,
"aerien": 0.5,
"multimodal": 2.06
},
"zone6":
{
"terrestre": 1,
"maritime": 2.5,
"aerien": 0.75,
"multimodal": 2.5
}
}
const objModTPPC = {
// 15000: {modulo: 0, franchise: "150"},
// 50000: {modulo: 0.05, franchise: "10% Mini 150€ et Maxi 300€"},
// 100000: {modulo: 0.1, franchise: "10% Mini 150€ et Maxi 750€"},
15000: {modulo: 0, franchise: 150},
50000: {modulo: 0.05, franchise: 300},
100000: {modulo: 0.1, franchise: 750}
}
const objMarEnExpo =
{
"Marchandises Ordinaires": 0.2,
"Marchandises à risques": 0.25,
"Marchandises périssables": 0.35,
"Matériels et Equipements lourds": 0.35,
"Marchandises exclues sauf accord aux CP": 0.35,
"Véhicules roulants": 0.35,
}
const objModRG = {
zone1: {
etendue: 0.0005,
waterborne: 0.00019
},
zone2: {
etendue: 0.001,
waterborne: 0.00032
},
zone3: {
etendue: 0.0016,
waterborne: 0.00048
},
zone4: {
etendue: 0.002,
waterborne: 0.0006
},
zone5: {
etendue: 0.0032,
waterborne: 0.001
},
zone6: {
etendue: 0.008,
waterborne: 0.0025
},
}
module.exports = {
objModNatureMar,
objModMontantCA,
// objModFranchiseTousCas,
objModAct,
objModCond,
objModZoneTransp,
objModTPPC,
objMarEnExpo,
objModRG
};

File diff suppressed because it is too large Load Diff

View File

@ -1,92 +0,0 @@
const express = require('express');
const router = express.Router();
const facService = require('../services/facService');
const constantesJSON = require("../constantes/json-modulateur-fac");
router.post('/create', async (req, res) => {
const data = req.body;
const fac = await facService.createFAC(data);
res.json({valid: Boolean(fac), fac});
});
router.post('/createProjet', async (req, res) => {
const data = req.body;
const fac = await facService.createFacProjet(data);
res.json({valid: Boolean(fac), fac});
});
router.post('/createTarif', async (req, res) => {
const data = req.body;
const fac = await facService.createFacTarif(data);
res.json({valid: Boolean(fac), fac});
});
router.get("/read/id/:id", async (req, res) => {
const id = req.params.id;
try {
const fac = await facService.getFACbyId(id);
res.json({valid: Boolean(fac), fac});
} catch (error) {
res.status(500).json({valid: false, error: "Internal Server Error"});
}
});
router.get("/modulo/:objDemande", async (req, res) => {
const objDemande = req.params.objDemande;
var objRetourne
switch (objDemande) {
case "natureMar":
objRetourne = constantesJSON.objModNatureMar;
break;
case "montantCA":
objRetourne = constantesJSON.objModMontantCA;
break;
case "franchiseTousCas":
objRetourne = constantesJSON.objModFranchiseTousCas;
break;
case "activite":
objRetourne = constantesJSON.objModAct;
break;
case "conditionnement":
objRetourne = constantesJSON.objModCond;
break;
case "zoneTransport":
objRetourne = constantesJSON.objModZoneTransp;
break;
case "tppc":
objRetourne = constantesJSON.objModTPPC;
break;
case "marEnExpo":
objRetourne = constantesJSON.objMarEnExpo;
break;
case "rg":
objRetourne = constantesJSON.objModRG;
break;
}
try {
res.json({valid: Boolean(objRetourne), objRetourne});
} catch (error) {
logger.log("error", `Error finding constant ${objDemande}:`, error);
res.status(500).json({valid: false, error: "Internal Server Error"});
}
});
module.exports = router;

View File

@ -1,521 +0,0 @@
const express = require("express");
const router = express.Router();
const fs = require("fs");
const PizZip = require("pizzip");
const Docxtemplater = require("docxtemplater");
const logger = require("../utils/logger");
const path = require("path");
const moment = require("moment");
const parcoursService = require("../services/parcoursService");
const contratService = require("../services/contratService");
const globalService = require("../services/globalService");
const userService = require("../services/userService");
const constantesFAC = require("../constantes/json-modulateur-fac");
require("moment/locale/fr");
moment.locale("fr");
router.post("/fac/projet/:numParcours", async (req, res) => {
const content = fs.readFileSync(
path.resolve("src/templates/template-projet-fac.docx"),
"binary"
);
const zip = new PizZip(content);
const doc = new Docxtemplater(zip, {paragraphLoop: true, linebreaks: true});
const numParcours = req.params.numParcours.toUpperCase();
const parcours = await parcoursService.getParcoursByNumParcours(numParcours);
const contrat = await contratService.getContratById(parcours.contrat);
const client = contrat?.["@expand"]?.client || {};
const intermediaire = contrat?.["@expand"]?.intermediaire || {};
const fac = contrat?.["@expand"]?.enCours || null;
const projet = fac?.["@expand"]?.projet || null;
const moyenTransportList = ["terrestre", "maritime", "aerien", "postal", "fluvial"]
const selectedTransportList = []
try {
moyenTransportList.forEach((transport) => {
if (projet[transport] !== "") {
selectedTransportList.push(transport)
}
})
} catch (error) {}
var transportListVirguleEt
if (selectedTransportList.length > 1) {
transportListVirguleEt = selectedTransportList
.slice(0, selectedTransportList.length - 1)
.join(', ')
+ ' et ' + selectedTransportList[selectedTransportList.length - 1]
} else {
transportListVirguleEt = selectedTransportList.toString()
}
//au féminin
const transportListOuEt = selectedTransportList.join(" et/ou ")
.replace('fluvial', "fluviale")
.replace('postal', 'postale')
.replace('aerien', 'aerienne')
const listAssAdd = [];
try {
projet.assureAdditionnel.forEach((objet) => {
listAssAdd.push(objet.nom + " - Adresse : " + objet.adresse + " - Siret : " + objet.siret);
});
} catch (error) {}
const risqueTransport = {
"achat": "des contrats d'achat",
"vente": "des contrats de vente",
"sav": "des opérations de SAV",
"demo": "des démonstrations",
"transfert": "des transferts inter-usines"
}
Object.keys(risqueTransport).forEach((risque) => {
if (!projet.risqueTransport.includes(risque)) {
delete risqueTransport[risque]
}
})
const listRisqueTransport = Object.keys(risqueTransport).map((key) => risqueTransport[key])
const mondeEntier = (fac.zones.includes('zone1') &&
fac.zones.includes('zone2') &&
fac.zones.includes('zone3') &&
fac.zones.includes('zone4') &&
fac.zones.includes('zone5') &&
fac.zones.includes('zone6'))
const hasZone456 = (fac.zones.includes('zone4') &&
fac.zones.includes('zone5') &&
fac.zones.includes('zone6'))
const hasGarOptAuto = projet.garOpt.includes('auto')
const hasGarOptEmballage = projet.garOpt.includes('emballage')
const hasGarOptEtiquette = projet.garOpt.includes('etiquette')
const hasGarOptTemperature = projet.garOpt.includes('temperature')
const hasGarOptMarque = projet.garOpt.includes('marque')
const hasTPPC = fac.tppc
const hasMarchandiseExposition = fac.nbVehicExpo > 0
const condition4 = (hasGarOptAuto || hasGarOptEmballage || hasGarOptEtiquette || hasGarOptMarque || hasTPPC || hasMarchandiseExposition)
const hasGarOpt = projet.garOpt.length > 0
const condition2 = hasGarOpt || hasTPPC || hasMarchandiseExposition
const anneeProchaine = moment().add(1, 'years').format('YYYY')
const dateFin = (projet.dateFin == "00/00" || projet.dateFin == "00/00/0000") ? "A PRECISER" : projet.dateFin
const dateEffet = (projet.dateEffet == "00/00" || projet.dateEffet == "00/00/0000") ? "A PRECISER" : projet.dateEffet
const dateEcheance = (projet.dateEcheance == "00/00" || projet.dateEcheance == "00/00/0000") ? "A PRECISER" : projet.dateEcheance + '/' + anneeProchaine
const renderObject = {
nomClient: client.nom,
adrClient: client.adresse,
postalClient: client.codePostal,
villeClient: client.ville,
numClient: client.numClient,
nomInter: intermediaire.nom,
adrInter: intermediaire.adresse,
postalInter: intermediaire.codePostal,
villeInter: intermediaire.ville,
oriasInter: intermediaire.numOrias,
hasOrias: intermediaire.numOrias == "" ? false : true,
numInter: intermediaire.numTelephone,
mailInter: intermediaire.mail,
hasMail: intermediaire.mail ? true : false,
numProjet: contrat.numSaisine ? contrat.numSaisine : contrat.numContrat,
hasCP: (contrat.type == "AN" || contrat.type == "TEMPORAIRE"),
hasRemplacement: (contrat.type == "REMPLACEMENT"),
hasRGAuto: (fac.rg == "auto"),
hasRGDemande: (fac.rg == 'demande'),
hasRG: (fac.rg == 'demande' || fac.rg == 'auto'),
hasEtendue: (fac.typeRG == "etendue"),
hasWaterborne: (fac.typeRG == "waterborne"),
hasProgrammeInternational: projet.programmeInternational,
hasAssuresAdditionnels: listAssAdd.length > 0,
listAssAdd: listAssAdd,
hasTemporaire: contrat.type == "TEMPORAIRE",
actAssure: fac.actAssure,
typeMar: fac.typeMar,
listMoyenTransportEtOu: transportListOuEt,
listMoyenTransportVirguleEt: transportListVirguleEt,
listRisqueTransport: listRisqueTransport,
depart: projet.lieuDepart,
arrivee: projet.lieuArrivee,
dateJour: moment().format("DD MMMM YYYY"),
dateEffet: dateEffet,
dateFin: dateFin,
dateEcheance: dateEcheance,
hasMarchandiseExposition: hasMarchandiseExposition,
hasMondeEntier: mondeEntier,
hasZone1: fac.zones.includes('zone1'),
hasZone2: fac.zones.includes('zone2'),
hasZone3: fac.zones.includes('zone3'),
hasZone4: fac.zones.includes('zone4'),
hasZone5: fac.zones.includes('zone5'),
hasZone6: fac.zones.includes('zone6'),
hasZone456: hasZone456,
hasTPPC: hasTPPC,
hasTPPCTousRisques: projet.typeTPPC.includes('tousRisques'),
hasTPPCFlotteND: projet.typeTPPC.includes('flotteND'),
capitalMax: projet.capitalMax,
franchiseTransport: projet.franchiseTransport,
condition4: condition4,
hasGarOpt: hasGarOpt,
condition2: condition2,
hasGarOptAuto: hasGarOptAuto,
hasGarOptEmballage: hasGarOptEmballage,
hasGarOptEtiquette: hasGarOptEtiquette,
hasGarOptMarque: hasGarOptMarque,
hasGarOptTemperature: hasGarOptTemperature,
capitalTPPC: fac.capitalTPPC,
franchiseExpo: fac.franchiseExpo,
hasCG: projet.valeurAssuree.includes('cg'),
hasDerogation: projet.valeurAssuree.includes('derogation'),
hasVaBasePrix: projet.valeurAssureeBase.includes('prix'),
hasVaBaseAchat: projet.valeurAssureeBase.includes('achat'),
hasVaBaseVente: projet.valeurAssureeBase.includes('vente'),
hasAerienTousRisques: fac.aerien.includes('tousRisques'),
hasAerienEventMaj: fac.aerien.includes('eventMaj'),
hasMaritimeTousRisques: fac.maritime.includes('tousRisques'),
hasMaritimeEventMaj: fac.maritime.includes('eventMaj'),
hasTerrestreTousRisques: fac.terrestre.includes('tousRisques'),
hasTerrestreEventMaj: fac.terrestre.includes('eventMaj'),
hasPostalTousRisques: fac.postal.includes('tousRisques'),
hasPostalEventMaj: fac.postal.includes('eventMaj'),
hasFluvialTousRisques: fac.fluvial.includes('tousRisques'),
hasFluvialEventMaj: fac.fluvial.includes('eventMaj'),
hasFranchiseTransport: projet.franchiseTransport !== "",
cotRO: globalService.customFormatNumber(fac.cotRO),
cotRG: globalService.customFormatNumber(fac.cotRG),
cotProvRO: globalService.customFormatNumber(fac.cotProvRO),
cotProvRG: globalService.customFormatNumber(fac.cotProvRG),
cotComptant: globalService.customFormatNumber(projet.cotComptant),
tauxCotRO: globalService.customFormatNumber(fac.tauxCotRO, true),
tauxCotRG: globalService.customFormatNumber(fac.tauxCotRG, true),
cotAnnuelle: globalService.customFormatNumber(fac.primeHT),
cotIrred: globalService.customFormatNumber(fac.primeMini),
capitalTPPC: globalService.customFormatNumber(fac.capitalTPPC),
capitalExpo: globalService.customFormatNumber(fac.capitalExpo),
franchiseTPPC: globalService.customFormatNumber(fac.franchiseTPPC),
chiffreAffaires: globalService.customFormatNumber(fac.ca),
hasMensuel: (projet.tempo == "mensuel"),
hasChiffreAffaires: (projet.typeContrat == "chiffreAffaires"),
hasAvisAliments: (projet.typeContrat == "avisAliments"),
hasPartResultat: projet.participationResultat,
hasAgentMutualiste: (intermediaire.type == "AGENT MUTUALISTE"),
hasAgent: (intermediaire.type == "AGENT MUTUALISTE" || intermediaire.type == "AGENT NON MUTUALISTE"),
hasCourtier: (intermediaire.type == "COURTIER"),
tempo: projet.tempo,
}
try {
doc.render(renderObject)
} catch (error) {
const e = {
message: error.message,
name: error.name,
stack: error.stack,
properties: error.properties,
};
logger.log('error', JSON.stringify({error: e}));
// Envoyez une réponse d'erreur si le rendu échoue
return res.status(500).send("Erreur lors de la génération du document");
}
const buf = doc.getZip().generate({type: "nodebuffer"});
const formattedDate = moment().format('DD-MM-YYYY-HH-mm-ss')
// Génération du nom de fichier
const sanitizedClientNom = client.nom
.replace(/[^\w\s.-]/gi, "")
.replace(/\s+/g, "-");
const filename = `Projet-${contrat.produit}-${parcours.numParcours}-${sanitizedClientNom}-${formattedDate}`;
// Définit le type de contenu et un nom de fichier par défaut pour le téléchargement
res.setHeader(
"Content-Type",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
);
res.setHeader(
"Content-Disposition",
"attachment; filename=" + filename + ".docx"
);
// Envoie le buffer au client, déclenchant le téléchargement
res.send(buf);
});
//generate declinaison tarifaire FAC
router.post("/fac/tarif/:numParcours", async (req, res) => {
// TODO Attention conditionner en fonction du type de CP
const content = fs.readFileSync(
path.resolve("src/templates/template-declinaison-tarifaire-fac.docx"),
"binary"
);
const zip = new PizZip(content);
const doc = new Docxtemplater(zip, {paragraphLoop: true, linebreaks: true});
const numParcours = req.params.numParcours.toUpperCase();
const parcours = await parcoursService.getParcoursByNumParcours(numParcours);
const contrat = await contratService.getContratById(parcours.contrat);
const client = contrat?.["@expand"]?.client || {};
const fac = contrat?.["@expand"]?.enCours || {};
const tarif = fac?.["@expand"]?.tarif || {};
const user = await userService.getUserById(parcours.dernierUtilisateur);
function getSelectedFranchise(franchiseId) {
switch (franchiseId) {
case "sansFranchise":
return tarif.sansFranchise
case "franchise350":
return tarif.franchise350
case "franchise750":
return tarif.franchise750
}
}
function getSelectedFranchiseTitre(franchiseId) {
switch (franchiseId) {
case "sansFranchise":
return "Sans Franchise"
case "franchise350":
return "Franchise 350 €"
case "franchise750":
return "Franchise 750 €"
}
}
function initialeMaj(str) {
return typeof str == "string" ? str.charAt(0).toUpperCase() + str.slice(1) : ""
}
const selectedFranchiseTitre = getSelectedFranchiseTitre(tarif.selectedFranchise)
const selectedFranchise = getSelectedFranchise(tarif.selectedFranchise)
const transports = []
if (fac.terrestre !== "") {
transports.push("Terrestre")
}
if (fac.maritime !== "") {
transports.push("Maritime")
}
if (fac.aerien !== "") {
transports.push("Aerien")
}
if (fac.postal !== "") {
transports.push("Postal")
}
if (fac.fluvial !== "") {
transports.push("Fluvial")
}
if (fac.multimodal !== "") {
transports.push('Multimodal')
}
const listTransports = transports.join(', ')
const hasMondeEntier = (
fac.zones.includes("zone1") &&
fac.zones.includes("zone2") &&
fac.zones.includes("zone3") &&
fac.zones.includes("zone4") &&
fac.zones.includes("zone5") &&
fac.zones.includes("zone6")
)
const typeRO = tarif.typeRO == "tousRisques" ? "Tous Risques" : "Evenements Majeurs"
var typePolice
switch (tarif.typePolice) {
case "ca":
typePolice = "Police au Chiffre d'Affaires";
break;
case "national":
typePolice = "Police au Voyage National";
break;
case "international":
typePolice = "Police au Voyage International";
break;
}
try {
doc.render({
matricule: user.matricule,
hasContrat: contrat.numContrat || false,
numContrat: contrat.numContrat,
hasSaisine: contrat.numSaisine || false,
numSaisine: contrat.numSaisine,
nomClient: client.nom,
actAssuree: fac.actAssuree,
montantSin: tarif.sinistres,
franchiseSelected: selectedFranchiseTitre,
typeMar: fac.typeMar,
ca: fac.ca,
montantGarantir: tarif.montantGarantir,
conditionnement: constantesFAC.objModCond?.[tarif.conditionnement]?.nom ?? "",
typeMar: fac.typeMar,
transports: listTransports,
hasMondeEntier: hasMondeEntier,
hasZone1: fac.zones.includes("zone1"),
hasZone2: fac.zones.includes("zone2"),
hasZone3: fac.zones.includes("zone3"),
hasZone4: fac.zones.includes("zone4"),
hasZone5: fac.zones.includes("zone5"),
hasZone6: fac.zones.includes("zone6"),
hasZone1Achats: tarif.fluxAchats?.zone == "zone1",
hasZone2Achats: tarif.fluxAchats?.zone == "zone2",
hasZone3Achats: tarif.fluxAchats?.zone == "zone3",
hasZone4Achats: tarif.fluxAchats?.zone == "zone4",
hasZone5Achats: tarif.fluxAchats?.zone == "zone5",
hasZone6Achats: tarif.fluxAchats?.zone == "zone6",
conditionnementAchats: constantesFAC.objModCond[tarif.fluxAchats?.conditionnement]?.nom ?? "",
hasZone1Ventes: tarif.fluxVentes?.zone == "zone1",
hasZone2Ventes: tarif.fluxVentes?.zone == "zone2",
hasZone3Ventes: tarif.fluxVentes?.zone == "zone3",
hasZone4Ventes: tarif.fluxVentes?.zone == "zone4",
hasZone5Ventes: tarif.fluxVentes?.zone == "zone5",
hasZone6Ventes: tarif.fluxVentes?.zone == "zone6",
conditionnementVentes: constantesFAC.objModCond[tarif.fluxVentes?.conditionnement]?.nom ?? "",
hasFluxGlobal: tarif.typeFlux == "global",
hasFluxDetailles: tarif.typeFlux == "detailles",
hasFluxVentes: (tarif.fluxVentes),
hasFluxAchats: (tarif.fluxAchats),
hasFluxIntersites: tarif.fluxIntersites,
fluxAchats: tarif.fluxAchats,
fluxVentes: tarif.fluxVentes,
transportVentes: initialeMaj(tarif.fluxVentes?.transport),
transportAchats: initialeMaj(tarif.fluxAchats?.transport),
typeRO: typeRO,
typePolice: typePolice,
hasTPPC: fac.tppc,
hasMarExpo: (fac.nbVehicExpo > 0),
hasRG: fac.rg == "auto",
hasRGAchats: (tarif.fluxAchats?.typeRG),
hasRGVentes: (tarif.fluxVentes?.typeRG),
tarif350: tarif.franchise350.proposition,
// pourcentAct350: parseFloat(tarif.franchise350.pourcentAct) * 100,
// pourcentCA350: parseFloat(tarif.franchise350.pourcentCA) * 100,
// pourcentMar350: parseFloat(tarif.franchise350.pourcentMar) * 100,
// pourcentFranchise350: parseFloat(tarif.franchise350.pourcentFranchise) * 100,
tauxRO350: tarif.franchise350.tauxRO,
tauxRG350: tarif.franchise350.tauxRG,
franchiseTPPC350: tarif.franchise350.franchiseTPPC ?? "",
franchiseExpo350: tarif.franchise350.franchiseExpo ?? "",
tarif750: tarif.franchise750.proposition,
// pourcentAct750: parseFloat(tarif.franchise750.pourcentAct) * 100,
// pourcentCA750: parseFloat(tarif.franchise750.pourcentCA) * 100,
// pourcentMar750: parseFloat(tarif.franchise750.pourcentMar) * 100,
// pourcentFranchise750: parseFloat(tarif.franchise750.pourcentFranchise) * 100,
tauxRO750: tarif.franchise750.tauxRO,
tauxRG750: tarif.franchise750.tauxRG,
franchiseTPPC750: tarif.franchise750.franchiseTPPC ?? "",
franchiseExpo750: tarif.franchise750.franchiseExpo ?? "",
tarifSansFranchise: tarif.sansFranchise.proposition,
// pourcentActSansFranchise: parseFloat(tarif.sansFranchise.pourcentAct) * 100,
// pourcentCASansFranchise: parseFloat(tarif.sansFranchise.pourcentCA) * 100,
// pourcentMarSansFranchise: parseFloat(tarif.sansFranchise.pourcentMar) * 100,
// pourcentFranchiseSansFranchise: parseFloat(tarif.sansFranchise.pourcentFranchise) * 100,
franchiseTPPCSansFranchise: tarif.sansFranchise.franchiseTPPC ?? "",
franchiseExpoSansFranchise: tarif.sansFranchise.franchiseExpo ?? "",
tauxROSansFranchise: tarif.sansFranchise.tauxRO,
tauxRGSansFranchise: tarif.sansFranchise.tauxRG,
tarifSelected: selectedFranchise.proposition,
tarifCommercial: fac.primeHT,
franchiseTPPCSelected: selectedFranchise.franchiseTPPC ?? "",
franchiseExpoSelected: selectedFranchise.franchiseExpo ?? "",
tauxROSelected: fac.tauxCotRO,
tauxRGSelected: fac.tauxCotRG
});
} catch (error) {
const e = {
message: error.message,
name: error.name,
stack: error.stack,
properties: error.properties,
};
logger.log("error", JSON.stringify({error: e}));
// Envoyez une réponse d'erreur si le rendu échoue
return res.status(500).send("Erreur lors de la génération du document");
}
const buf = doc.getZip().generate({type: "nodebuffer"});
const currentDate = new Date();
// Formatage de la date au format "JJ-MM-AAAA-HH-MM-SS"
const day = String(currentDate.getDate()).padStart(2, "0");
const month = String(currentDate.getMonth() + 1).padStart(2, "0");
const year = currentDate.getFullYear();
const hours = String(currentDate.getHours()).padStart(2, "0");
const minutes = String(currentDate.getMinutes()).padStart(2, "0");
const seconds = String(currentDate.getSeconds()).padStart(2, "0");
const formattedDate = `${day}-${month}-${year}-${hours}-${minutes}-${seconds}`;
// Génération du nom de fichier
const sanitizedClientNom = client.nom
.replace(/[^\w\s.-]/gi, "")
.replace(/\s+/g, "-");
const filename = `Tarif-${contrat.produit}-${parcours.numParcours}-${sanitizedClientNom}-${formattedDate}`;
// Définit le type de contenu et un nom de fichier par défaut pour le téléchargement
res.setHeader(
"Content-Type",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
);
res.setHeader(
"Content-Disposition",
"attachment; filename=" + filename + ".docx"
);
// Envoie le buffer au client, déclenchant le téléchargement
res.send(buf);
});
module.exports = router;

View File

@ -1,651 +0,0 @@
const express = require("express");
const router = express.Router();
const fs = require("fs");
const PizZip = require("pizzip");
const Docxtemplater = require("docxtemplater");
const logger = require("../utils/logger");
const path = require("path");
const moment = require("moment");
const parcoursService = require("../services/parcoursService");
const contratService = require("../services/contratService");
const userService = require("../services/userService");
const globalService = require("../services/globalService");
//const projetformrc = require("../../public/js/projet-form-rc");
//const moduloRC = require("../constantes/json-modulateur-rc"); useless pour le moment ?
require("moment/locale/fr");
moment.locale("fr");
// Fonctions helper pour récupérer les valeurs selon la franchise choisie
function getSelectedTarifReference(tarifRC) {
const franchise = tarifRC.franchiseChoisie;
if (franchise === '250') return tarifRC.primeTotal_250;
if (franchise === '400') return tarifRC.primeTotal_400;
if (franchise === 'mini300') return tarifRC.primeTotal_2000;
return null;
}
function getSelectedPrime(tarifRC, type) {
const franchise = tarifRC.franchiseChoisie;
if (!franchise) return 0;
const suffix = franchise === 'mini300' ? '2000' : franchise;
const fieldName = `prime${type}_${suffix}`;
return tarifRC[fieldName] || 0;
}
function getSelectedTaux(tarifRC, type) {
const franchise = tarifRC.franchiseChoisie;
if (!franchise) return 0;
const suffix = franchise === 'mini300' ? '2000' : franchise;
const fieldName = `taux${type}_${suffix}`;
return tarifRC[fieldName] || 0;
}
router.post("/rc/projet/:numParcours", async (req, res) => {
try {
const content = fs.readFileSync(
path.resolve("src/templates/template-projet-rc.docx"),
"binary"
);
const zip = new PizZip(content);
const doc = new Docxtemplater(zip, { paragraphLoop: true, linebreaks: true });
const numParcours = req.params.numParcours.toUpperCase();
const parcours = await parcoursService.getParcoursByNumParcours(numParcours);
const contrat = await contratService.getContratById(parcours.contrat);
const client = contrat?.["@expand"]?.client || {};
const intermediaire = contrat?.["@expand"]?.intermediaire || {};
// Récupérer la collection rc avec ses relations (APRÈS placerDansEnCours, c'est dans enCours)
const rcFull = contrat?.["@expand"]?.enCours;
if (!rcFull) {
logger.log('error', 'No RC found for contrat:', contrat.id);
return res.status(404).send("Aucune donnée RC trouvée pour ce contrat");
}
// Les données sont déjà expandées par contratService
const rc = rcFull?.["@expand"]?.projetRC || {};
const listAssAdd = [];
// Traiter les assurés additionnels s'ils existent
if (rc.assureAdditionnel && Array.isArray(rc.assureAdditionnel)) {
rc.assureAdditionnel.forEach((objet) => {
listAssAdd.push(
objet.nom +
" - Adresse : " +
objet.adresse +
" - Siret : " +
objet.siret
);
});
}
// Conditions zone
let hasMondeEntier = false;
let hasZone1 = false;
let hasZone2 = false;
let hasZone3 = false;
let hasZone4 = false;
let hasZone5 = false;
let hasZone6 = false;
let hasActiviteButNotMultimodal = false; // Attention trick pour les zones géographiques dans le cas de multimodal
let hasZone456 = false;
// Cas monde entier
if (
rc.zone1 &&
rc.zone2 &&
rc.zone3 &&
rc.zone4 &&
rc.zone5 &&
rc.zone6 &&
(rc.actVoiturier ||
rc.actLoueur ||
rc.actDouane ||
rc.actDemPar ||
rc.actDemParDom ||
rc.actDemParAdv ||
rc.actDemEntr ||
rc.actDemInterne ||
rc.actGardeMeuble ||
rc.actEntDep ||
rc.actPrestaLog ||
rc.actLevageur)
) {
hasMondeEntier = true;
}
// Cas date du jour d'édition
let dateNow = moment().format("DD MMMM YYYY");
// Cas une ou plusieurs zone(s) spécifique(s) + cas zone456
if (hasMondeEntier == false && (rc.actVoiturier || rc.actLoueur || rc.actDouane || rc.actDemPar || rc.actDemParDom || rc.actDemParAct || rc.actDemEntr || rc.actDemInterne || rc.actGardeMeuble || rc.actEntDep || rc.actPrestaLog || rc.actLevageur || rc.actDemParAdv)) {
if (rc.zone1) {
hasZone1 = true;
}
if (rc.zone2) {
hasZone2 = true;
}
if (rc.zone3) {
hasZone3 = true;
}
if (rc.zone4) {
hasZone4 = true;
hasZone456 = true;
}
if (rc.zone5) {
hasZone5 = true;
hasZone456 = true;
}
if (rc.zone6) {
hasZone6 = true;
hasZone456 = true;
}
}
// Cas Activité multimodal + Au minimum une zone
if (rc.actMultimodal) {
if (hasZone1 || hasZone2 || hasZone3 || hasZone456 || hasMondeEntier) {
hasActiviteButNotMultimodal = true;
}
}
let hasMondeEntierOrMultimodal, hasNotMondeEntierOrMultimodal;
if (rc.actMultimodal || hasMondeEntier) {
hasMondeEntierOrMultimodal = true;
hasNotMondeEntierOrMultimodal = false;
} else {
hasMondeEntierOrMultimodal = false;
hasNotMondeEntierOrMultimodal = true;
}
// Conditions Extensions RCC
let extRCC = false;
if ( rc.extRCCModifCalArrim == true || rc.extRCCFerroutage == true || rc.extRCCFraisRecons == true || rc.extRCCConfie == true || rc.extRCCTPPC == true || rc.extRCCRegie == true || rc.extRCCSansMontageDemontage == true ) {
extRCC = true;
}
// Conditions Extensions RCE
let extRCE = false;
if (rc.extRCEMontageDemontage == true || rc.extRCEBraDebra == true) {
extRCE = true;
}
// Conditions Activitées
let hasActiviteDemenageurGardeMeuble = false;
if ( rc.actDemPar == true || rc.actDemEntr == true || rc.actDemParDom == true || rc.actDemParAdv == true || rc.actDemInterne) {
hasActiviteDemenageurGardeMeuble = true;
}
let oneOfActiviteDemenageurParticulier = false;
if ( rc.actDemPar == true || rc.actDemParDom == true || rc.actDemParAdv == true ) {
oneOfActiviteDemenageurParticulier = true;
}
let coorDem = "";
if ( (rc.actDemPar == true || rc.actDemParDom == true || rc.actDemParAdv == true) && (rc.actDemEntr == true || rc.actDemInterne == true) && rc.actGardeMeuble == true) {
coorDem = ",";
} else if ( (rc.actDemPar == true || rc.actDemParDom == true || rc.actDemParAdv == true) && (rc.actDemEntr == true || rc.actDemInterne == true) && rc.actGardeMeuble == false ) {
coorDem = " et";
}
// Variables numériques
const franchiseTarif = "250";
doc.render({
// Client
nomClient: client.nom,
adrClient: client.adresse,
postalClient: client.codePostal,
villeClient: client.ville,
numClient: client.numClient,
// Intermédiaire
nomInter: intermediaire.nom,
adrInter: intermediaire.adresse,
postalInter: intermediaire.codePostal,
villeInter: intermediaire.ville,
oriasInter: intermediaire.numOrias,
hasOrias: intermediaire.numOrias == "" ? false : true,
numInter: intermediaire.numTelephone,
mailInter: intermediaire.mail,
hasMail: intermediaire.mail ? true : false,
// Contrat
numSaisine: contrat.numSaisine,
numContrat: contrat.numContrat,
numProjet: contrat.numSaisine ? contrat.numSaisine : contrat.numContrat,
hasCP: contrat.type == "AN" || contrat.type == "TEMPORAIRE" ? true : false,
hasRemplacement: contrat.type == "REMPLACEMENT" ? true : false,
hasPJ: rc.pj,
hasParticipationResultat: rc.participationResultat,
hasRCE: rc.autresRC,
hasExtRCC: extRCC,
hasExtRCE: extRCE,
hasAssAdd: listAssAdd.length > 0 ? true : false,
listAssAdd: listAssAdd,
listDesiVehicule: rc.designationVehicule,
hasForfaitaire: rc.typeCot == "forfaitaire" ? true : false,
hasRevisable: rc.typeCot == "revisable" ? true : false,
hasCourtier: intermediaire.type == "COURTIER" ? true : false,
hasAgent: intermediaire.type == "COURTIER" ? false : true,
hasAgentMutualiste: intermediaire.type == "AGENT MUTUALISTE" ? true : false,
// Temporalité contrat
dateJour: dateNow,
typeFractionnement: rc.tempo,
hasTypeTemporaire: contrat.type == "TEMPORAIRE" ? true : false,
hasTypeAutreTempo: contrat.type == "AN" || contrat.type == "REMPLACEMENT" ? true : false,
dateDebutEffet: rc.dateEffet == "00/00/0000" ? "A PRECISER" : rc.dateEffet,
dateFinEffet: rc.dateFin == "00/00/0000" ? "A PRECISER" : rc.dateFin,
dateEcheance: rc.dateEcheance == "00/00" ? "A PRECISER" : rc.dateEcheance,
// Activite
hasActiviteVoiturier: rc.actVoiturier,
garActiviteVoiturier: globalService.customFormatNumber(rc.valueActVoiturier, false),
hasActiviteLoueur: rc.actLoueur,
garActiviteLoueur: globalService.customFormatNumber(rc.valueActLoueur, false),
hasActiviteMultimodal: rc.actMultimodal,
garActiviteMultimodal: globalService.customFormatNumber(rc.valueActMultimodal, false),
hasActiviteDouane: rc.actDouane,
garActiviteDouane: globalService.customFormatNumber(rc.valueActDouane, false),
hasOneOfActiviteDemenageurParticulier: oneOfActiviteDemenageurParticulier,
hasActiviteDemenageurParticulier: rc.actDemPar,
garActiviteDemenageurParticulier: globalService.customFormatNumber(rc.valueActDemPar, false),
hasActiviteDPEtendue: rc.actDemParDom,
garActiviteDPEtendue: globalService.customFormatNumber(rc.valueActDemParDom, false),
hasActiviteDemenageurParticulierAdvalorem: rc.actDemParAdv,
garActiviteDemenageurParticulierAdvalorem: globalService.customFormatNumber(rc.valueActDemParAdv, false),
hasActiviteDemenageurEntreprises: rc.actDemEntr,
garActiviteDemenageurEntreprises: globalService.customFormatNumber(rc.valueActDemEntr, false),
hasActiviteDemenageurInterne: rc.actDemInterne,
garActiviteDemenageurInterne: globalService.customFormatNumber(rc.valueActDemInterne, false),
hasActiviteGardeMeubles: rc.actGardeMeuble,
garActiviteGardeMeubles: globalService.customFormatNumber(rc.valueActGardeMeuble, false),
hasActiviteEntrepositaireDepositaire: rc.actEntDep,
garActiviteEntrepositaireDepositaire: globalService.customFormatNumber(rc.valueActEntDep, false),
hasActivitePrestataireLogistique: rc.actPrestaLog,
garActivitePrestataireLogistique: globalService.customFormatNumber(rc.valueActPrestaLog, false),
hasActiviteManutentionnaireLevageur: rc.actLevageur,
garActiviteManutentionnaireLevageur: globalService.customFormatNumber(rc.valueActLevageur, false),
hasActiviteTransitaire: rc.actTransitaire,
garActiviteTransitaire: globalService.customFormatNumber(rc.valueActTransitaire, false),
hasActiviteDemenageurGardeMeuble: hasActiviteDemenageurGardeMeuble,
varCoorDem: coorDem,
// Marchandises
hasMarchandiseOrdinaire: rc.marOrdinaire,
hasMarchandiseVehiculesRoulants: rc.marRoulant,
hasMarchandiseEngins: rc.marEngins,
hasMarchandiseVehiculesRoulantsDemenagement: rc.marRoulantDem,
hasMarchandiseMobiliersUsages: rc.marMobilerUsag,
hasMarchandisePerissables: rc.marPerissable,
hasMarchandiseAnimauxVivants: rc.marAnimaux,
hasMarchandiseCiterne: rc.marCiterne,
hasMarchandiseBeton: rc.marBeton,
hasMarchandiseExceptionnels: rc.marExceptionnels,
hasMarchandiseVrac: rc.marVrac,
// Zone
hasMondeEntier: hasMondeEntier,
hasZone1: hasZone1,
hasZone2: hasZone2,
hasZone3: hasZone3,
hasZone4: hasZone4,
hasZone5: hasZone5,
hasZone6: hasZone6,
hasActiviteButNotMultimodal: hasActiviteButNotMultimodal,
hasMondeEntierOrMultimodal: hasMondeEntierOrMultimodal,
hasNotMondeEntierOrMultimodal: hasNotMondeEntierOrMultimodal,
hasZone456: hasZone456,
// Extensions de garanties RCC
hasExtModifCallageArrimage: rc.extRCCModifCalArrim,
hasExtFerroutage: rc.extRCCFerroutage,
hasExtReconstitution: rc.extRCCFraisRecons,
hasExtConfies: rc.extRCCConfie,
hasCCValeurDecla: rc.typeExtConfies == "VALEUR DECLAREE" || rc.typeExtConfies == "" ? true : false,
hasCCAdValo: rc.typeExtConfies == "ADVALOREM" ? true : false,
hasExtTPPC: rc.extRCCTPPC,
hasExtRegie: rc.extRCCRegie,
hasExtSansMontageDemontage: rc.extRCCSansMontageDemontage,
// Extensions de garanties RCC
hasExtBranchementDebranchement: rc.extRCEBraDebra,
hasExtMontageDemontage: rc.extRCEMontageDemontage,
// Advalo
grMultimodal: rc.grilleMultimodal,
grTerrestre: rc.grilleTerrestre,
grAerien: rc.grilleAerien,
// Variables numérique
franchiseTarif: franchiseTarif,
cotisationRCCHT: globalService.customFormatNumber(rc.cotRCCHT, true),
tauxRCCHT: globalService.customFormatNumber(rc.tauxRCCHT, true, true),
cotisationRCCTTC: globalService.customFormatNumber(rc.cotRCCTTC, true),
tauxRCCTTC: globalService.customFormatNumber(rc.tauxRCCTTC, true, true),
cotisationRCEHT: globalService.customFormatNumber(rc.cotRCEHT, true),
tauxRCEHT: globalService.customFormatNumber(rc.tauxRCEHT, true, true),
cotisationRCETTC: globalService.customFormatNumber(rc.cotRCETTC, true),
tauxRCETTC: globalService.customFormatNumber(rc.tauxRCETTC, true, true),
PJHT: globalService.customFormatNumber(rc.cotPJHT, true),
PJTTC: globalService.customFormatNumber(rc.cotPJTTC, true),
cotisationMini: globalService.customFormatNumber(rc.cotIrreductible, true),
cotisationTotale: globalService.customFormatNumber(rc.cotTotalTTC, true),
fraisRepFraction: globalService.customFormatNumber(rc.cotFraisTTC, true),
ca: globalService.customFormatNumber(rc.ca, true),
});
const buf = doc.getZip().generate({ type: "nodebuffer" });
const currentDate = new Date();
// Formatage de la date au format "JJ-MM-AAAA-HH-MM-SS"
const day = String(currentDate.getDate()).padStart(2, "0");
const month = String(currentDate.getMonth() + 1).padStart(2, "0");
const year = currentDate.getFullYear();
const hours = String(currentDate.getHours()).padStart(2, "0");
const minutes = String(currentDate.getMinutes()).padStart(2, "0");
const seconds = String(currentDate.getSeconds()).padStart(2, "0");
const formattedDate = `${day}-${month}-${year}-${hours}-${minutes}-${seconds}`;
// Génération du nom de fichier
const sanitizedClientNom = client.nom
.replace(/[^\w\s.-]/gi, "")
.replace(/\s+/g, "-");
const filename = `Projet-${contrat.produit}-${parcours.numParcours}-${sanitizedClientNom}-${formattedDate}`;
// Définit le type de contenu et un nom de fichier par défaut pour le téléchargement
res.setHeader(
"Content-Type",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
);
res.setHeader(
"Content-Disposition",
"attachment; filename=" + filename + ".docx"
);
// Envoie le buffer au client, déclenchant le téléchargement
res.send(buf);
} catch (error) {
logger.log('error', 'Error in RC projet generation:', error);
return res.status(500).send("Erreur lors de la génération du projet RC");
}
});
router.post("/rc/tarif/:numParcours", async (req, res) => {
try {
const content = fs.readFileSync(
path.resolve("src/templates/template-declinaison-tarifaire-rc.docx"),
"binary"
);
const zip = new PizZip(content);
const doc = new Docxtemplater(zip, { paragraphLoop: true, linebreaks: true });
const numParcours = req.params.numParcours.toUpperCase();
const parcours = await parcoursService.getParcoursByNumParcours(numParcours);
const contrat = await contratService.getContratById(parcours.contrat);
const client = contrat?.["@expand"]?.client || {};
const user = await userService.getUserById(parcours.dernierUtilisateur);
// Récupérer la collection rc avec ses relations (APRÈS placerDansEnCours, c'est dans enCours)
const rcFull = contrat?.["@expand"]?.enCours;
if (!rcFull) {
logger.log('error', 'No RC found for contrat:', contrat.id);
return res.status(404).send("Aucune donnée RC trouvée pour ce contrat");
}
// Les données sont déjà expandées par contratService
const rc = rcFull?.["@expand"]?.tarifRC || {};
const rcMain = rcFull || {};
// Formatage des taux avec maximum 2 décimales
function formatTaux(taux) {
if (!taux) return 0;
return parseFloat(taux).toFixed(2);
}
// Préparer les données des activités avec leurs marchandises et activités complémentaires
const activites = [];
// Voiturier/Loueur
if (rcMain.checkVoiturier || rcMain.checkLoueur) {
activites.push({
type: 'Voiturier/Loueur',
capital: rcMain.capitalVoiturier || 0,
pourcentage: rc.pourcentageVoiturier || 0,
marchandises: rcMain.marchandisesVoiturier || [],
activitesCompl: rcMain.activitesVoiturier || []
});
}
// Commissionnaire de Transport
if (rcMain.checkCommissionnaire) {
activites.push({
type: 'Commissionnaire de Transport',
capital: rcMain.capitalCommissionnaire || 0,
pourcentage: rc.pourcentageCommissionnaire || 0,
marchandises: rcMain.marchandisesCommissionnaire || [],
activitesCompl: rcMain.activitesCommissionnaire || []
});
}
// Déménageur
if (rcMain.checkDemenageur) {
activites.push({
type: 'Déménageur',
capital: rcMain.capitalDemenageur || 0,
pourcentage: rc.pourcentageDemenageur || 0,
marchandises: rcMain.marchandisesDemenageur || [],
activitesCompl: rcMain.activitesDemenageur || []
});
}
// Logistique
if (rcMain.checkLogistique) {
activites.push({
type: 'Logistique',
capital: rcMain.capitalLogistique || 0,
pourcentage: rc.pourcentageLogistique || 0,
marchandises: rcMain.marchandisesLogistique || [],
activitesCompl: rcMain.activitesLogistique || []
});
}
// Autocariste
if (rcMain.checkAutocariste) {
activites.push({
type: 'Autocariste',
capital: rcMain.capitalAutocariste || 0,
pourcentage: rc.pourcentageAutocariste || 0,
marchandises: rcMain.marchandisesAutocariste || [],
activitesCompl: rcMain.activitesAutocariste || []
});
}
// Autres activités
if (rcMain.checkAutres) {
activites.push({
type: 'Autres activités',
capital: rcMain.capitalAutres || 0,
pourcentage: rc.pourcentageAutres || 0,
marchandises: rcMain.marchandisesAutres || [],
activitesCompl: rcMain.activitesAutres || []
});
}
// Préparer les zones
const zones = [];
if (rcMain.zone1) zones.push("Zone 1");
if (rcMain.zone2) zones.push("Zone 2");
if (rcMain.zone3) zones.push("Zone 3");
if (rcMain.zone4) zones.push("Zone 4");
if (rcMain.zone5) zones.push("Zone 5");
if (rcMain.zone6) zones.push("Zone 6");
// Préparer les garanties additionnelles
const garantiesAdditionnelles = [];
if (rc.checkStationLavage) garantiesAdditionnelles.push("Station de lavage");
if (rc.checkGarageInterne) garantiesAdditionnelles.push("Garage interne");
if (rc.checkCSE) garantiesAdditionnelles.push("CSE");
if (rc.checkTPPC) garantiesAdditionnelles.push(`TPPC (Capital: ${rc.capitalTPPC || 0}€, Véhicules: ${rc.vehiculesTPPC || 0})`);
if (rc.checkPJ) garantiesAdditionnelles.push("Protection Juridique");
// Préparer les engagements complémentaires
const engagements = [];
if (rc.checkDomImmat) engagements.push(`Dommages immatériels (Capital: ${rc.capitalDomImmat || 0}€)`);
if (rc.checkContConf) engagements.push(`Contenu confié (Capital: ${rc.capitalContConf || 0}€)`);
if (rc.checkDiffInv) engagements.push(`Différence d'inventaire (Capital: ${rc.capitalDiffInv || 0}€)`);
let dateNow = moment().format("DD MMMM YYYY");
doc.render({
// Infos générales
matricule: user.matricule,
date: dateNow,
typeCotisation: rcMain.typeCotisation || "Non défini",
hasforfaitaire: !(rcMain.typeCotisation === "forfaitaire" || (!rcMain.chiffreAffaires || rcMain.chiffreAffaires === 0)),
hasRCE: rcMain.checkRCE || false,
hasContrat: !!contrat.numContrat,
numContrat: contrat.numContrat,
hasSaisine: !!contrat.numSaisine,
numSaisine: contrat.numSaisine,
numeroCS: contrat.numSaisine || contrat.numContrat,
nomAssure: client.nom,
ca: rcMain.chiffreAffaires || 0,
hasNbVehicule: (rcMain.nombreVehicules || 0) > 0,
nbVehicule: rcMain.nombreVehicules || 0,
// Activités principales
hasVoiturierLoueur: rcMain.checkVoiturier || rcMain.checkLoueur,
capitalAssureVoiturierLoueur: rcMain.capitalVoiturier || 0,
pourcentageVoiturierLoueur: rc.pourcentageVoiturier || 0,
marchandiseVoiturierLoueur: rcMain.marchandisesVoiturier || [],
activiteVoiturierLoueur: rcMain.actComplVoiturier || [],
hasComTransport: rcMain.checkCommissionnaire || false,
capitalAssureComTransport: rcMain.capitalCommissionnaire || 0,
pourcentageComTransport: rc.pourcentageCommissionnaire || 0,
marchandiseComTransport: rcMain.marchandisesCommissionnaire || [],
activiteComTransport: rcMain.actComplCommissionnaire || [],
hasDemenageur: rcMain.checkDemenageur || false,
capitalAssureDemenageur: rcMain.capitalDemenageur || 0,
pourcentageDemenageur: rc.pourcentageDemenageur || 0,
marchandiseDemenageur: rcMain.marchandisesDemenageur || [],
activiteDemenageur: rcMain.actComplDemenageur || [],
hasLogistique: rcMain.checkLogistique || false,
capitalAssureLogistique: rcMain.capitalLogistique || 0,
pourcentageLogistique: rc.pourcentageLogistique || 0,
marchandiseLogistique: rcMain.marchandisesLogistique || [],
activiteLogistique: rcMain.actComplLogistique || [],
hasAutocariste: rcMain.checkAutocariste || false,
capitalAssureAutocariste: rcMain.capitalAutocariste || 0,
pourcentageAutocariste: rc.pourcentageAutocariste || 0,
marchandiseAutocariste: rcMain.marchandisesAutocariste || [],
activiteAutocariste: rcMain.activitesAutocariste || [],
hasAutre: rcMain.checkAutres || false,
capitalAssureAutre: rcMain.capitalAutres || 0,
pourcentageAutre: rc.pourcentageAutres || 0,
marchandiseAutre: rcMain.marchandisesAutres || [],
//activiteAutre: rcMain.activitesAutres || [], existe pas dans ejs
// Zones géographiques
zones: zones,
// Garanties additionnelles
hasGarantieAdd: garantiesAdditionnelles.length > 0,
garantieAdd: garantiesAdditionnelles,
// Engagements complémentaires
hasEngagement: engagements.length > 0,
engagement: engagements,
// Tarifs sélectionnés
franchise: rc.franchiseChoisie || rc.franchiseSelectionnee,
tarifReference: getSelectedTarifReference(rc),
tarifCommercial: rc.tarifcommercial,
primeRCC: getSelectedPrime(rc, 'RCC'),
tauxRCC: formatTaux(getSelectedTaux(rc, 'RCC')),
hasRCE: rcMain.checkRCE,
primeRCE: getSelectedPrime(rc, 'RCE'),
tauxRCE: formatTaux(getSelectedTaux(rc, 'RCE')),
hasPrimePJ: rc.checkPJ && getSelectedPrime(rc, 'PJ') > 0,
primePJ: getSelectedPrime(rc, 'PJ'),
tauxGlobal: formatTaux(getSelectedTaux(rc, 'Global')),
// Propositions franchise 250
tarifRefP1: rc.primeTotal_250 || 0,
pRCCP1: rc.primeRCC_250 || 0,
tRCCP1: formatTaux(rc.tauxRCC_250),
pRCEP1: rc.primeRCE_250 || 0,
tRCEP1: formatTaux(rc.tauxRCE_250),
primePJP1: rc.primePJ_250 || 0,
tauxGlobalP1: formatTaux(rc.tauxGlobal_250),
// Propositions franchise 400
tarifRefP2: rc.primeTotal_400 || 0,
pRCCP2: rc.primeRCC_400 || 0,
tRCCP2: formatTaux(rc.tauxRCC_400),
pRCEP2: rc.primeRCE_400 || 0,
tRCEP2: formatTaux(rc.tauxRCE_400),
primePJP2: rc.primePJ_400 || 0,
tauxGlobalP2: formatTaux(rc.tauxGlobal_400),
// Propositions franchise 2000
tarifRefP3: rc.primeTotal_2000 || 0,
pRCCP3: rc.primeRCC_2000 || 0,
tRCCP3: formatTaux(rc.tauxRCC_2000),
pRCEP3: rc.primeRCE_2000 || 0,
tRCEP3: formatTaux(rc.tauxRCE_2000),
primePJP3: rc.primePJ_2000 || 0,
tauxGlobalP3: formatTaux(rc.tauxGlobal_2000),
});
const buf = doc.getZip().generate({ type: "nodebuffer" });
const currentDate = new Date();
// Formatage de la date au format "JJ-MM-AAAA-HH-MM-SS"
const day = String(currentDate.getDate()).padStart(2, "0");
const month = String(currentDate.getMonth() + 1).padStart(2, "0");
const year = currentDate.getFullYear();
const hours = String(currentDate.getHours()).padStart(2, "0");
const minutes = String(currentDate.getMinutes()).padStart(2, "0");
const seconds = String(currentDate.getSeconds()).padStart(2, "0");
const formattedDate = `${day}-${month}-${year}-${hours}-${minutes}-${seconds}`;
const sanitizedClientNom = client.nom
.replace(/[^\w\s.-]/gi, "")
.replace(/\s+/g, "-");
const filename = `Tarif-RC-${parcours.numParcours}-${sanitizedClientNom}-${formattedDate}`;
res.setHeader(
"Content-Type",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
);
res.setHeader("Content-Disposition", "attachment; filename=" + filename + ".docx");
res.send(buf);
} catch (error) {
logger.log("error", 'Error in RC tarif generation:', error);
return res.status(500).send("Erreur lors de la génération du tarif RC");
}
});
module.exports = router;

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,152 +0,0 @@
const express = require('express');
const router = express.Router();
const rcService = require('../services/rcService');
const constantesJSON = require("../constantes/json-modulateur-rc");
const logger = require('../utils/logger');
// ===== Routes RC principale =====
router.post('/create', async (req, res) => {
try {
const data = req.body;
const rc = await rcService.createRc(data);
res.json({ valid: Boolean(rc), rc });
} catch (error) {
logger.log("error", "Error creating RC:", error);
res.status(500).json({ valid: false, error: "Internal Server Error" });
}
});
router.get('/:id', async (req, res) => {
try {
const rc = await rcService.getRcById(req.params.id);
res.json({ valid: Boolean(rc), rc });
} catch (error) {
logger.log("error", "Error getting RC:", error);
res.status(500).json({ valid: false, error: "Internal Server Error" });
}
});
router.post('/update/:id', async (req, res) => {
try {
const rc = await rcService.updateRc(req.params.id, req.body);
res.json({ valid: Boolean(rc), rc });
} catch (error) {
logger.log("error", "Error updating RC:", error);
res.status(500).json({ valid: false, error: "Internal Server Error" });
}
});
// ===== Routes TarifRC =====
router.post('/tarif/create', async (req, res) => {
try {
const data = req.body;
const tarifRc = await rcService.createTarifRc(data);
res.json({ valid: Boolean(tarifRc), tarifRc });
} catch (error) {
logger.log("error", "Error creating TarifRC:", error);
res.status(500).json({ valid: false, error: "Internal Server Error" });
}
});
router.get('/tarif/:id', async (req, res) => {
try {
const tarifRc = await rcService.getTarifRcById(req.params.id);
res.json({ valid: Boolean(tarifRc), tarifRc });
} catch (error) {
logger.log("error", "Error getting TarifRC:", error);
res.status(500).json({ valid: false, error: "Internal Server Error" });
}
});
router.post('/tarif/update/:id', async (req, res) => {
try {
const tarifRc = await rcService.updateTarifRc(req.params.id, req.body);
res.json({ valid: Boolean(tarifRc), tarifRc });
} catch (error) {
logger.log("error", "Error updating TarifRC:", error);
res.status(500).json({ valid: false, error: "Internal Server Error" });
}
});
// ===== Routes ProjetRC =====
router.post('/projet/create', async (req, res) => {
try {
const data = req.body;
const projetRc = await rcService.createProjetRc(data);
res.json({ valid: Boolean(projetRc), projetRc });
} catch (error) {
logger.log("error", "Error creating ProjetRC:", error);
res.status(500).json({ valid: false, error: "Internal Server Error" });
}
});
router.get('/projet/:id', async (req, res) => {
try {
const projetRc = await rcService.getProjetRcById(req.params.id);
res.json({ valid: Boolean(projetRc), projetRc });
} catch (error) {
logger.log("error", "Error getting ProjetRC:", error);
res.status(500).json({ valid: false, error: "Internal Server Error" });
}
});
router.post('/projet/update/:id', async (req, res) => {
try {
const projetRc = await rcService.updateProjetRc(req.params.id, req.body);
res.json({ valid: Boolean(projetRc), projetRc });
} catch (error) {
logger.log("error", "Error updating ProjetRC:", error);
res.status(500).json({ valid: false, error: "Internal Server Error" });
}
});
// ===== Routes Modulateurs =====
router.get("/modulo/:objDemande", async (req, res) => {
const objDemande = req.params.objDemande;
var objRetourne
switch (objDemande) {
case "CARC":
objRetourne = constantesJSON.modRCCA;
break;
case "activiteRCC":
objRetourne = constantesJSON.modRCActRCC;
break;
case "activiteRCE":
objRetourne = constantesJSON.modRCActRCE;
break;
case "activiteComplRC":
objRetourne = constantesJSON.modRCActCompl;
break;
case "marchandiseRC":
objRetourne = constantesJSON.modRCMar;
break;
case "zoneRC":
objRetourne = constantesJSON.modRCZone;
break;
case "engagComplRC":
objRetourne = constantesJSON.modRCEngagCompl;
break;
case "garAdditionelRC":
objRetourne = constantesJSON.modRCGarAdd ;
break;
case "sinistreRC":
objRetourne = constantesJSON.modRCSinistre;
break;
case "franchiseRC":
objRetourne = constantesJSON.modRCFranchise;
break;
case "primeMiniRC":
objRetourne = constantesJSON.modRCPrimeMini;
break;
}
try {
res.json({valid: Boolean(objRetourne), objRetourne});
} catch (error) {
logger.log("error", `Error finding constant ${objDemande}:`, error);
res.status(500).json({valid: false, error: "Internal Server Error"});
}
});
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.

Binary file not shown.

Binary file not shown.

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,56 +0,0 @@
const { db } = require('../db/db-connect');
const logger = require('../utils/logger');
const globalService = require("../services/globalService");
// ===== Collection RC principale =====
async function createRc(data) {
return await db.records.create('rc', data);
}
async function getRcById(id) {
return await db.records.getOne('rc', id, {
expand: 'tarifRC,projetRC'
});
}
async function updateRc(id, data) {
return await db.records.update('rc', id, data);
}
// ===== Collection TarifRC =====
async function createTarifRc(data) {
return await db.records.create('tarifRC', data);
}
async function getTarifRcById(id) {
return await db.records.getOne('tarifRC', id);
}
async function updateTarifRc(id, data) {
return await db.records.update('tarifRC', id, data);
}
// ===== Collection ProjetRC =====
async function createProjetRc(data) {
return await db.records.create('projetRC', data);
}
async function getProjetRcById(id) {
return await db.records.getOne('projetRC', id);
}
async function updateProjetRc(id, data) {
return await db.records.update('projetRC', id, data);
}
module.exports = {
createRc,
getRcById,
updateRc,
createTarifRc,
getTarifRcById,
updateTarifRc,
createProjetRc,
getProjetRcById,
updateProjetRc
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

View File

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

View File

@ -1,16 +1,8 @@
body { body {
font-family: 'Roboto', sans-serif; font-family: 'Roboto', sans-serif;
background-color: white;
color: black;
color-scheme: light;
} }
h1, h1, h2, h3, h4, h5, h6 {
h2,
h3,
h4,
h5,
h6 {
color: darkblue !important; color: darkblue !important;
font-weight: bold; font-weight: bold;
} }
@ -121,11 +113,6 @@ hr.form {
padding: 0 5% !important; padding: 0 5% !important;
} }
.center {
display: flex;
justify-content: center
}
.circle { .circle {
cursor: pointer; cursor: pointer;
width: 35px; width: 35px;
@ -196,17 +183,6 @@ hr.form {
padding: 5px 12px !important; padding: 5px 12px !important;
} }
.chip-info{
padding: 5px;
width: 30%;
text-align: center;
background: darkblue;
color: white;
border-radius: 10px;
font-size: 13px;
font-weight: 500;
}
#goodParcoursModal { #goodParcoursModal {
width: 80%; width: 80%;
max-height: 80%; max-height: 80%;
@ -242,10 +218,7 @@ hr.form {
content: none; content: none;
} }
#selectHistory, #selectHistory, #selectHistory ul, #selectHistory li, #selectHistory span {
#selectHistory ul,
#selectHistory li,
#selectHistory span {
font-size: 13px !important; font-size: 13px !important;
} }
@ -301,14 +274,6 @@ hr.form {
padding-right: 10px; padding-right: 10px;
} }
.moyenTransportTarif {
margin-bottom: 30px;
}
.moyenTransportTarif td i {
padding-right: 10px;
}
.select-chip { .select-chip {
display: none; display: none;
color: darkblue color: darkblue
@ -343,13 +308,6 @@ hr.form {
font-weight: bold; font-weight: bold;
} }
.step {
background-color: #f44336 ;
color: white;
font-weight: bold;
margin-top: 10px;
}
.modulo-resume { .modulo-resume {
font-size: 20px; font-size: 20px;
font-weight: normal; font-weight: normal;
@ -369,7 +327,6 @@ hr.form {
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-evenly; justify-content: space-evenly;
margin-bottom: 20px; margin-bottom: 20px;
align-items: flex-start;
} }
@ -410,7 +367,7 @@ hr.form {
outline-offset: 3px; outline-offset: 3px;
} }
.garAdd .card-content { .garAdd .card-content{
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
justify-content: flex-start; justify-content: flex-start;
@ -418,28 +375,29 @@ hr.form {
} }
.garAdd .card-title { .garAdd .card-title {
padding: 10px; padding: 10px;
} }
form h5 { form h5 {
margin: 0px 0px 30px 0px margin : 0px 0px 30px 0px
} }
a.grille-garanties { a.grille-garanties {
width: 40px; width : 40px ;
height: 40px; height : 40px ;
padding: 2px 0 0 0; padding: 2px 0 0 0;
border-radius: 100%; border-radius: 100%;
background-color: #F44336; background-color: #F44336;
} }
a.grille-garanties:hover { a.grille-garanties:hover{
background-color: #be3026; background-color: #be3026;
} }
.modalAlert .modal-content .modalRed { .modalAlert .modal-content .modalRed {
list-style-type: square; list-style-type: square;
color: red color : red;
font-weight: bold;
} }
.modalAlert .modal-content h4 { .modalAlert .modal-content h4 {
@ -447,111 +405,7 @@ a.grille-garanties:hover {
} }
.modalAlert .modal-footer a { .modalAlert .modal-footer a {
color: white color : white
}
#rcProjetBlockingSummary,
#rcTarifBlockingSummary {
margin: 1rem 0 1.5rem 0;
}
.rc-blocking-summary {
display: none;
border-left: 6px solid #c62828;
background-color: #ffebee;
color: #b71c1c;
padding: 12px 16px;
border-radius: 6px;
}
.rc-blocking-title {
font-weight: 700;
margin-bottom: 8px;
}
.rc-blocking-list {
margin: 0;
padding-left: 20px;
}
.rc-blocking-list li {
margin: 3px 0;
}
.rc-field-label {
display: block;
color: #1a237e;
font-weight: 700;
margin-bottom: 6px;
text-align: left;
}
.rc-has-floating-label {
position: relative;
margin-top: 0.5rem;
}
.rc-has-floating-label .rc-field-label.rc-floating-label {
position: static;
top: auto;
left: auto;
margin: 0 0 6px 0;
font-size: 0.95rem;
font-weight: 700;
color: #1a237e;
pointer-events: none;
transition: none;
background: transparent;
padding: 0;
z-index: auto;
}
.rc-has-floating-label .rc-field-label.rc-floating-label.active {
top: auto;
font-size: 0.95rem;
font-weight: 700;
color: #1a237e;
}
.rc-tarifettes-hidden {
display: none !important;
}
.rc-three-col-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 18px;
}
.rc-three-col-grid > [class*="col"] {
width: 100% !important;
margin-left: 0 !important;
padding: 0 !important;
display: flex;
}
.rc-three-col-grid .card {
width: 100%;
display: flex;
flex-direction: column;
}
.rc-three-col-grid .card-content {
flex: 1;
}
.rc-equal-card-row > [class*="col"] {
display: flex;
}
.rc-equal-card-row .card {
width: 100%;
display: flex;
flex-direction: column;
}
.rc-equal-card-row .card-content {
flex: 1;
} }
#modalTarifCom span.material-icons { #modalTarifCom span.material-icons {
@ -567,110 +421,10 @@ a.grille-garanties:hover {
padding: 0 10px; padding: 0 10px;
} }
#rowExtensionsGarantie .dropdown-content.select-dropdown.multiple-select-dropdown { #rowExtensionsGarantie .dropdown-content.select-dropdown.multiple-select-dropdown{
z-index: 20 !important; z-index: 20 !important;
} }
.flux-card {
width: 41rem !important;
padding: 2rem 1.4rem;
border-radius: 20px;
box-shadow: 5px 5px 7px -3px rgba(0, 0, 0, 0.173);
}
.flux-card .flux-card-title {
font-size: 20px;
font-weight: bold;
margin: 0 0 20px 0;
color: darkblue
}
#div-fluxAchats {
background-color: #d5ecd4;
transition: 0.2s;
}
#div-fluxVentes {
background-color: #ddddfb;
transition: 0.2s;
}
#div-fluxAchats:hover {
background-color: #e5f0e5;
}
#div-fluxVentes:hover {
background-color: #f0f0ff;
}
.listNatureMar input:disabled {
color: black !important
}
.flux-card hr {
height: 0px;
}
.flux-card hr.divider {
margin: 3rem 0px;
border-color: #9b9b9b17;
}
.flux-card .select-wrapper {
background-color: white;
}
.listNatureMar {
margin-bottom: 30px;
}
#div-fluxAchats .listNatureMar>div {
background-color: #aad8a7;
}
#div-fluxVentes .listNatureMar>div {
background-color: rgb(193, 189, 236);
}
.listNatureMar>div {
border-radius: 5px;
margin: 5px 0;
padding: 6px 0;
display: flex;
justify-content: space-around;
align-items: center;
}
.listNatureMar .btn-floating {
margin-right: 20px;
}
.flux-card .input-field {
width: 20rem;
}
.checkedCheckbox::before {
background-color: rgba(0, 0, 0, 0.42) !important;
border-color: #949494 !important;
}
.propositions-flex {
display: flex;
justify-content: space-between;
}
.propositions-flex>div {
width: 100%;
margin: 0px 20px
}
#row-sinistresAsIf > div {
display: flex;
justify-content: center;
align-items: center
}
@media only screen and (max-width: 600px) { @media only screen and (max-width: 600px) {
.flex-adaptable { .flex-adaptable {
flex-direction: column; flex-direction: column;
@ -757,50 +511,4 @@ a.grille-garanties:hover {
@keyframes l13 { @keyframes l13 {
100% { transform: rotate(1turn); } 100% { transform: rotate(1turn); }
} }
/* Message d'erreur du loader */
#error-message {
display: none;
color: #ff4444;
font-weight: bold;
text-align: center;
margin-top: 20px;
white-space: pre-line;
font-size: 16px;
}
/* Message de timeout du loader */
#timeout-message {
display: none;
margin-top: 30px;
text-align: center;
color: #d0d0d0;
font-size: 14px;
max-width: 420px;
line-height: 1.6;
padding: 0 20px;
animation: fadeIn 0.5s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Lien cliquable pour annuler le chargement */
#cancel-loading-link {
color: #66B2FF;
text-decoration: underline;
text-underline-offset: 3px;
text-decoration-thickness: 1px;
cursor: pointer;
transition: all 0.2s ease;
font-weight: 500;
}
#cancel-loading-link:hover {
color: #90CAF9;
text-decoration-thickness: 2px;
text-shadow: 0 0 10px rgba(102, 178, 255, 0.6);
}

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