Compare commits
No commits in common. "advalo-dans-etv" and "main" have entirely different histories.
advalo-dan
...
main
|
|
@ -1,5 +0,0 @@
|
|||
DB_URL=http://127.0.0.1:8091/
|
||||
DB_ADMIN=admin@axa.fr
|
||||
DB_PASSWORD=DTadmin123TT
|
||||
NODE_ENV=developpement
|
||||
PORT=8082
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
DB_URL=http://127.0.0.1:8091/
|
||||
DB_ADMIN=admin@example.local
|
||||
DB_PASSWORD=change-me
|
||||
NODE_ENV=development
|
||||
PORT=8082
|
||||
|
||||
# Optional runtime overrides for Advalorem
|
||||
# ADV_WORKSPACE_ROOT=/absolute/path/to/ecole
|
||||
|
||||
# Optional AXA bridge tuning
|
||||
# AXA_TIMEOUT_MS=65000
|
||||
# AXA_RETRY_COUNT=1
|
||||
# AXA_RETRY_DELAY_MS=1200
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
# Dependencies
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Classique
|
||||
package-lock.json
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# PocketBase runtime data (local only)
|
||||
src/db/pb_data/*.db
|
||||
src/db/pb_data/*.db-shm
|
||||
src/db/pb_data/*.db-wal
|
||||
# Squelette versionné (data.db/logs.db) mais pas les transients
|
||||
src/db/pb_data_backup/*.db-shm
|
||||
src/db/pb_data_backup/*.db-wal
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Documents Advalo générés (données client) — régénérés à l'exécution
|
||||
documents/
|
||||
|
||||
# Rapports de migration/bench (régénérés)
|
||||
reports/
|
||||
scripts/reports/
|
||||
|
||||
# Build du helper AXA C# (recompilé via dotnet publish)
|
||||
src/axa-helper/bin/
|
||||
src/axa-helper/obj/
|
||||
|
||||
# Runtime
|
||||
.runtime/
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
# EasyTransport / Advalorem Runtime Notes
|
||||
|
||||
## Local PocketBase workflow
|
||||
|
||||
1. Bootstrap local DB files from tracked backup:
|
||||
- `npm run db:bootstrap`
|
||||
- `npm run db:bootstrap:reset` (force overwrite)
|
||||
2. Start PocketBase on `DB_URL` host/port:
|
||||
- `npm run db:start`
|
||||
3. Start app:
|
||||
- `npm run start`
|
||||
|
||||
## Advalorem runtime overrides
|
||||
|
||||
- `ADV_WORKSPACE_ROOT`: force workspace root for packaged/runtime environments
|
||||
- Advalorem APIs (`/advalo/*`) run in PocketBase mode by default.
|
||||
- No `sqlite3` CLI dependency is required for historique/cumul/reporting/export.
|
||||
|
||||
## Notes
|
||||
|
||||
- `pb_data/*.db*` are local runtime files and are no longer intended to be versioned.
|
||||
- `src/db/pb_data_backup/` remains the baseline source for local bootstrap.
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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;
|
||||
})();
|
||||
|
|
@ -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;
|
||||
};
|
||||
});
|
||||
|
|
@ -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
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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
|
|
@ -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;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/* eslint-disable no-console */
|
||||
require('dotenv').config();
|
||||
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const BASE_URL = process.env.BENCH_BASE_URL || `http://127.0.0.1:${process.env.PORT || 8082}`;
|
||||
const SAMPLES = Number(process.env.BENCH_SAMPLES || 15);
|
||||
const CONCURRENCY = Number(process.env.BENCH_CONCURRENCY || 3);
|
||||
const MATRICULE = process.env.BENCH_MATRICULE || 'S601153';
|
||||
const REPORT_DIR = process.env.BENCH_REPORT_DIR
|
||||
? path.resolve(process.env.BENCH_REPORT_DIR)
|
||||
: path.resolve(__dirname, 'reports');
|
||||
|
||||
function request(method, path, token, body = null, responseType = 'json') {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(path, BASE_URL);
|
||||
const payload = body ? JSON.stringify(body) : null;
|
||||
const req = http.request({
|
||||
method,
|
||||
hostname: url.hostname,
|
||||
port: url.port,
|
||||
path: `${url.pathname}${url.search}`,
|
||||
headers: {
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...(payload ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) } : {})
|
||||
}
|
||||
}, (res) => {
|
||||
let raw = '';
|
||||
res.on('data', (chunk) => { raw += chunk.toString(); });
|
||||
res.on('end', () => {
|
||||
if (res.statusCode >= 400) {
|
||||
return reject(new Error(`${method} ${path} failed (${res.statusCode}) ${raw.slice(0, 300)}`));
|
||||
}
|
||||
if (responseType === 'text') {
|
||||
return resolve(raw);
|
||||
}
|
||||
if (responseType === 'json') {
|
||||
try {
|
||||
const parsed = raw ? JSON.parse(raw) : {};
|
||||
return resolve(parsed);
|
||||
} catch (error) {
|
||||
return reject(new Error(`Invalid JSON response for ${method} ${path}: ${error.message}`));
|
||||
}
|
||||
}
|
||||
return reject(new Error(`Unsupported responseType '${responseType}' for ${method} ${path}`));
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
if (payload) req.write(payload);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function getToken() {
|
||||
if (process.env.BENCH_TOKEN) return process.env.BENCH_TOKEN;
|
||||
const auth = await request('GET', `/auth/verifyMatricule/${encodeURIComponent(MATRICULE)}`, null);
|
||||
if (!auth.valid || !auth.token) {
|
||||
throw new Error(`Unable to get token for matricule ${MATRICULE}`);
|
||||
}
|
||||
return auth.token;
|
||||
}
|
||||
|
||||
function percentile(values, p) {
|
||||
if (!values.length) return 0;
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
const idx = Math.ceil((p / 100) * sorted.length) - 1;
|
||||
return sorted[Math.max(0, Math.min(sorted.length - 1, idx))];
|
||||
}
|
||||
|
||||
async function benchmarkEndpoint(token, endpoint) {
|
||||
const durations = [];
|
||||
const runOne = async () => {
|
||||
const started = process.hrtime.bigint();
|
||||
await request(
|
||||
endpoint.method,
|
||||
endpoint.path,
|
||||
token,
|
||||
endpoint.body || null,
|
||||
endpoint.responseType || 'json'
|
||||
);
|
||||
const ended = process.hrtime.bigint();
|
||||
durations.push(Number(ended - started) / 1_000_000);
|
||||
};
|
||||
|
||||
await runOne(); // warm-up
|
||||
|
||||
let running = [];
|
||||
for (let i = 0; i < SAMPLES; i += 1) {
|
||||
running.push(runOne());
|
||||
if (running.length >= CONCURRENCY) {
|
||||
await Promise.all(running);
|
||||
running = [];
|
||||
}
|
||||
}
|
||||
if (running.length) await Promise.all(running);
|
||||
|
||||
return {
|
||||
endpoint: endpoint.path,
|
||||
samples: durations.length,
|
||||
p50Ms: Number(percentile(durations, 50).toFixed(2)),
|
||||
p95Ms: Number(percentile(durations, 95).toFixed(2)),
|
||||
avgMs: Number((durations.reduce((acc, x) => acc + x, 0) / durations.length).toFixed(2)),
|
||||
maxMs: Number(Math.max(...durations).toFixed(2))
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const token = await getToken();
|
||||
const endpoints = [
|
||||
{ method: 'GET', path: '/advalo/historique?page=1&pageSize=20' },
|
||||
{ method: 'GET', path: '/advalo/cumul?page=1&pageSize=20' },
|
||||
{ method: 'GET', path: '/advalo/reporting?page=1&pageSize=20' },
|
||||
{ method: 'GET', path: '/advalo/export?page=1&pageSize=100', responseType: 'text' }
|
||||
];
|
||||
|
||||
const results = [];
|
||||
for (const endpoint of endpoints) {
|
||||
console.log(`Benchmarking ${endpoint.path} ...`);
|
||||
const result = await benchmarkEndpoint(token, endpoint);
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
const summary = {
|
||||
baseUrl: BASE_URL,
|
||||
samples: SAMPLES,
|
||||
concurrency: CONCURRENCY,
|
||||
generatedAt: new Date().toISOString(),
|
||||
results,
|
||||
acceptance: {
|
||||
allP95Lt2000: results.every((item) => item.p95Ms < 2000)
|
||||
}
|
||||
};
|
||||
|
||||
fs.mkdirSync(REPORT_DIR, { recursive: true });
|
||||
const reportPath = path.join(REPORT_DIR, `advalo-bench-${Date.now()}.json`);
|
||||
fs.writeFileSync(reportPath, `${JSON.stringify(summary, null, 2)}\n`, 'utf8');
|
||||
|
||||
console.log(JSON.stringify(summary, null, 2));
|
||||
console.log(`Report saved: ${reportPath}`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error.stack || error.message || String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Reconstruit les templates Avenant Advalo pour la parité v1.
|
||||
*
|
||||
* Les .docx d'origine (lettre AXA stylée) n'ont que l'en-tête agent/client. La v1
|
||||
* (golang/bordereau.go) construit par CODE : un tableau de prix (CAPITAUX | TAUX |
|
||||
* COTISATION | COUT D'ACTE | A PERCEVOIR), la phrase "à percevoir", et — pour le
|
||||
* périodique avec liste — un tableau récapitulatif des transports.
|
||||
*
|
||||
* Ce script:
|
||||
* 1. sauvegarde la version pristine de chaque template dans _source/ (au 1er passage);
|
||||
* 2. convertit les tokens bruts (nomAgent, …) en tags docxtemplater {token} (runs isolés);
|
||||
* 3. injecte le tableau de prix + la phrase + (Avenant) la boucle {#avecListe}/{#listeTransports};
|
||||
* 4. réécrit le .docx live. Idempotent : retransforme toujours depuis _source/.
|
||||
*
|
||||
* Usage: node scripts/advalo-build-templates.js
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const PizZip = require('pizzip');
|
||||
|
||||
const TEMPLATE_DIR = path.resolve(__dirname, '..', 'src', 'templates', 'advalo');
|
||||
const SOURCE_DIR = path.join(TEMPLATE_DIR, '_source');
|
||||
|
||||
const COMMON_TOKENS = [
|
||||
'nomAgent', 'adresseAgent', 'postalAgent', 'telAgent', 'faxAgent',
|
||||
'nomClient', 'adresseClient', 'codePostal', 'numContrat', 'numClient',
|
||||
'intervalle', 'numAgent'
|
||||
];
|
||||
|
||||
const BLUE = '254E9B';
|
||||
|
||||
function escAttr(s) {
|
||||
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
// Paragraphe simple. `text` peut contenir des tags docxtemplater {…}.
|
||||
function para(text, { bold = false, color = null, size = null, align = null } = {}) {
|
||||
const rpr = [];
|
||||
if (bold) rpr.push('<w:b/>');
|
||||
if (color) rpr.push(`<w:color w:val="${color}"/>`);
|
||||
if (size) rpr.push(`<w:sz w:val="${size}"/><w:szCs w:val="${size}"/>`);
|
||||
const rPr = rpr.length ? `<w:rPr>${rpr.join('')}</w:rPr>` : '';
|
||||
const pPrParts = [];
|
||||
if (align) pPrParts.push(`<w:jc w:val="${align}"/>`);
|
||||
if (rpr.length) pPrParts.push(`<w:rPr>${rpr.join('')}</w:rPr>`);
|
||||
const pPr = pPrParts.length ? `<w:pPr>${pPrParts.join('')}</w:pPr>` : '';
|
||||
return `<w:p>${pPr}<w:r>${rPr}<w:t xml:space="preserve">${text}</w:t></w:r></w:p>`;
|
||||
}
|
||||
|
||||
// Cellule de tableau. `text` peut contenir des tags docxtemplater.
|
||||
function cell(text, width, { bold = false, color = null, fill = null } = {}) {
|
||||
const rpr = [];
|
||||
if (bold) rpr.push('<w:b/>');
|
||||
if (color) rpr.push(`<w:color w:val="${color}"/>`);
|
||||
rpr.push('<w:sz w:val="18"/><w:szCs w:val="18"/>');
|
||||
const rPr = `<w:rPr>${rpr.join('')}</w:rPr>`;
|
||||
const shd = fill ? `<w:shd w:val="clear" w:color="auto" w:fill="${fill}"/>` : '';
|
||||
return `<w:tc><w:tcPr><w:tcW w:w="${width}" w:type="dxa"/>${shd}<w:vAlign w:val="center"/></w:tcPr>`
|
||||
+ `<w:p><w:pPr><w:jc w:val="center"/></w:pPr><w:r>${rPr}<w:t xml:space="preserve">${text}</w:t></w:r></w:p></w:tc>`;
|
||||
}
|
||||
|
||||
function tableOpen(widths) {
|
||||
const borders = ['top', 'left', 'bottom', 'right', 'insideH', 'insideV']
|
||||
.map((b) => `<w:${b} w:val="single" w:sz="4" w:space="0" w:color="auto"/>`)
|
||||
.join('');
|
||||
const grid = widths.map((w) => `<w:gridCol w:w="${w}"/>`).join('');
|
||||
return `<w:tbl><w:tblPr><w:tblW w:w="0" w:type="auto"/><w:tblBorders>${borders}</w:tblBorders></w:tblPr><w:tblGrid>${grid}</w:tblGrid>`;
|
||||
}
|
||||
|
||||
function row(cells) {
|
||||
return `<w:tr>${cells.join('')}</w:tr>`;
|
||||
}
|
||||
|
||||
// Tableau de prix à une ligne (parité v1: CAPITAUX | TAUX | COTISATION | COUT D'ACTE | A PERCEVOIR).
|
||||
function pricingTable() {
|
||||
const W = [1900, 1500, 1900, 1900, 1900];
|
||||
const headers = ['CAPITAUX', 'TAUX', 'COTISATION', "COUT D'ACTE", 'A PERCEVOIR'];
|
||||
const headerRow = row(headers.map((h, i) => cell(h, W[i], { bold: true, color: 'FFFFFF', fill: BLUE })));
|
||||
const dataRow = row([
|
||||
cell('{capitauxField} €', W[0]),
|
||||
cell('{tauxField} %', W[1]),
|
||||
cell('{cotisationField} €', W[2]),
|
||||
cell('{coutActeField} €', W[3]),
|
||||
cell('{cotisationTTC} €', W[4])
|
||||
]);
|
||||
return tableOpen(W) + headerRow + dataRow + '</w:tbl>';
|
||||
}
|
||||
|
||||
// Tableau récapitulatif des transports (boucle docxtemplater sur {listeTransports}).
|
||||
function recapTable() {
|
||||
const W = [1300, 1300, 1300, 1300, 1300, 1500, 1100];
|
||||
const headers = ['N° Demande', 'Date demande', 'Mode', 'Capital', 'Départ', 'Arrivée', 'Transport', 'Tarif'];
|
||||
const W8 = [1150, 1150, 1100, 1150, 1100, 1100, 1300, 1000];
|
||||
const headerRow = row(headers.map((h, i) => cell(h, W8[i], { bold: true, color: 'FFFFFF', fill: BLUE })));
|
||||
const loopRow = row([
|
||||
cell('{#listeTransports}{numDemande}', W8[0]),
|
||||
cell('{dateDemande}', W8[1]),
|
||||
cell('{mode}', W8[2]),
|
||||
cell('{capital} €', W8[3]),
|
||||
cell('{depart}', W8[4]),
|
||||
cell('{arrivee}', W8[5]),
|
||||
cell('{dateTransport}', W8[6]),
|
||||
cell('{tarif} €{/listeTransports}', W8[7])
|
||||
]);
|
||||
return tableOpen(W8) + headerRow + loopRow + '</w:tbl>';
|
||||
}
|
||||
|
||||
const EMPTY_P = '<w:p/>';
|
||||
|
||||
function buildAvenantBlock() {
|
||||
// Tableau prix + phrase "à percevoir" + récap conditionnel (parité v1 remplissageAvenant).
|
||||
return [
|
||||
EMPTY_P,
|
||||
pricingTable(),
|
||||
EMPTY_P,
|
||||
para('La cotisation à percevoir à la signature du présent avenant est de {cotisationTTC} €.', { bold: true }),
|
||||
EMPTY_P,
|
||||
para('{#avecListe}', {}),
|
||||
para("RECAPITULATIF DES TRANSPORTS SAISIS DANS L'OUTIL AD VALOREM", { bold: true, color: BLUE }),
|
||||
recapTable(),
|
||||
para('{/avecListe}', {}),
|
||||
EMPTY_P
|
||||
].join('');
|
||||
}
|
||||
|
||||
function buildPonctuelBlock() {
|
||||
// Détails transport + tableau prix + phrase "Il est perçu" (parité v1 remplissageAvenantPonctuel).
|
||||
return [
|
||||
EMPTY_P,
|
||||
para('Transport assuré du {dateDebut} au {dateFin}.'),
|
||||
para('Nature de la marchandise : {typeMarchandise}.'),
|
||||
para('Trajet : {depart} → {arrivee}.'),
|
||||
para('Mode(s) de transport : {modes}.'),
|
||||
EMPTY_P,
|
||||
pricingTable(),
|
||||
EMPTY_P,
|
||||
para('Il est perçu la somme de {cotisationTTC} € TTC '
|
||||
+ '(dont cotisation HT {cotisationField} € et coût d’acte {coutActeField} €).', { bold: true }),
|
||||
EMPTY_P
|
||||
].join('');
|
||||
}
|
||||
|
||||
function convertTokens(xml, tokens) {
|
||||
let out = xml;
|
||||
tokens.forEach((tok) => {
|
||||
out = out.replace(new RegExp(`(<w:t[^>]*>)${tok}(</w:t>)`, 'g'), `$1{${tok}}$2`);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function insertBeforeSectPr(xml, block) {
|
||||
const idx = xml.lastIndexOf('<w:sectPr');
|
||||
if (idx === -1) {
|
||||
return xml.replace('</w:body>', `${block}</w:body>`);
|
||||
}
|
||||
return xml.slice(0, idx) + block + xml.slice(idx);
|
||||
}
|
||||
|
||||
function rebuild(name, blockBuilder, { stripCapitauxAnchor = false } = {}) {
|
||||
const livePath = path.join(TEMPLATE_DIR, `${name}.docx`);
|
||||
const sourcePath = path.join(SOURCE_DIR, `${name}.docx`);
|
||||
fs.mkdirSync(SOURCE_DIR, { recursive: true });
|
||||
if (!fs.existsSync(sourcePath)) {
|
||||
fs.copyFileSync(livePath, sourcePath);
|
||||
console.log(`[source] sauvegarde pristine -> ${path.relative(process.cwd(), sourcePath)}`);
|
||||
}
|
||||
|
||||
const zip = new PizZip(fs.readFileSync(sourcePath));
|
||||
let xml = zip.file('word/document.xml').asText();
|
||||
|
||||
xml = convertTokens(xml, COMMON_TOKENS);
|
||||
if (stripCapitauxAnchor) {
|
||||
// L'ancre inline `capitauxField` de la v1 est remplacée par le vrai tableau (ci-dessous).
|
||||
xml = xml.replace(/(<w:t[^>]*>)capitauxField(<\/w:t>)/g, '$1$2');
|
||||
}
|
||||
xml = insertBeforeSectPr(xml, blockBuilder());
|
||||
|
||||
zip.file('word/document.xml', xml);
|
||||
fs.writeFileSync(livePath, zip.generate({ type: 'nodebuffer', compression: 'DEFLATE' }));
|
||||
console.log(`[build] ${name}.docx reconstruit (${xml.length} octets de document.xml)`);
|
||||
}
|
||||
|
||||
rebuild('Avenant', buildAvenantBlock, { stripCapitauxAnchor: true });
|
||||
rebuild('Avenant_Ponctuel', buildPonctuelBlock);
|
||||
console.log('Templates Avenant reconstruits.');
|
||||
|
|
@ -1,703 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/* eslint-disable no-console */
|
||||
require('dotenv').config();
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { parse } = require('csv-parse/sync');
|
||||
const XLSX = require('xlsx');
|
||||
const { connect, db } = require('../src/db/db-connect');
|
||||
|
||||
const VALID_AUTH_GROUPS = new Set(['SOUSCRIPTEUR', 'MANAGER', 'ADMIN', 'REVOQUE']);
|
||||
// Sources embarquées dans le repo (prod) ; repli sur le repo v1 advalorem (dev local).
|
||||
const BUNDLED_SEED_ROOT = path.resolve(__dirname, 'seed-data');
|
||||
const LEGACY_V1_ROOT = path.resolve(__dirname, '..', '..', 'advalorem', 'test');
|
||||
const DEFAULT_V1_ROOT = fs.existsSync(path.join(BUNDLED_SEED_ROOT, 'bdd', 'bordereau.csv'))
|
||||
? BUNDLED_SEED_ROOT
|
||||
: LEGACY_V1_ROOT;
|
||||
const DEFAULT_REPORT_DIR = path.resolve(__dirname, '..', 'reports');
|
||||
const PAGE_SIZE = 500;
|
||||
|
||||
function textField(name, { required = false, unique = false } = {}) {
|
||||
return {
|
||||
name,
|
||||
type: 'text',
|
||||
required,
|
||||
unique,
|
||||
options: { min: null, max: null, pattern: '' }
|
||||
};
|
||||
}
|
||||
|
||||
function numberField(name, { required = false, unique = false } = {}) {
|
||||
return {
|
||||
name,
|
||||
type: 'number',
|
||||
required,
|
||||
unique,
|
||||
options: { min: null, max: null }
|
||||
};
|
||||
}
|
||||
|
||||
function boolField(name, { required = false, unique = false } = {}) {
|
||||
return {
|
||||
name,
|
||||
type: 'bool',
|
||||
required,
|
||||
unique,
|
||||
options: {}
|
||||
};
|
||||
}
|
||||
|
||||
function jsonField(name, { required = false, unique = false } = {}) {
|
||||
return {
|
||||
name,
|
||||
type: 'json',
|
||||
required,
|
||||
unique,
|
||||
options: {}
|
||||
};
|
||||
}
|
||||
|
||||
function selectField(name, values, { required = false, unique = false } = {}) {
|
||||
return {
|
||||
name,
|
||||
type: 'select',
|
||||
required,
|
||||
unique,
|
||||
options: {
|
||||
maxSelect: 1,
|
||||
values
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const REQUIRED_ADVALO_COLLECTIONS = [
|
||||
{
|
||||
name: 'advalo_deleguee',
|
||||
schema: [
|
||||
textField('numDemande', { required: true, unique: true }),
|
||||
textField('numClient'),
|
||||
textField('nomClient'),
|
||||
textField('numContrat'),
|
||||
textField('dateDemande'),
|
||||
textField('marchandise'),
|
||||
textField('mode'),
|
||||
textField('capital'),
|
||||
textField('depart'),
|
||||
textField('arrivee'),
|
||||
textField('dateDebut'),
|
||||
textField('dateFin'),
|
||||
textField('dateDebutIso'),
|
||||
textField('dateFinIso'),
|
||||
textField('nomDevis'),
|
||||
textField('proprietaire'),
|
||||
textField('tarif'),
|
||||
textField('statutCommande'),
|
||||
textField('statutFacturation'),
|
||||
textField('souscripteur'),
|
||||
textField('numPortefeuille')
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'advalo_demande',
|
||||
schema: [
|
||||
textField('sourceType'),
|
||||
textField('numDemande', { required: true, unique: true }),
|
||||
textField('numClient'),
|
||||
textField('nomClient'),
|
||||
textField('numContrat'),
|
||||
textField('dateDemande'),
|
||||
textField('marchandise'),
|
||||
textField('mode'),
|
||||
textField('capital'),
|
||||
textField('depart'),
|
||||
textField('arrivee'),
|
||||
textField('dateDebut'),
|
||||
textField('dateFin'),
|
||||
textField('dateDebutIso'),
|
||||
textField('dateFinIso'),
|
||||
textField('nomDevis'),
|
||||
textField('proprietaire'),
|
||||
textField('tarif'),
|
||||
textField('statutCommande'),
|
||||
textField('statutFacturation'),
|
||||
boolField('isDeleted'),
|
||||
textField('createdBy'),
|
||||
textField('region'),
|
||||
textField('dpt'),
|
||||
textField('souscripteur'),
|
||||
textField('numPortefeuille')
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'advalo_ref_contrat',
|
||||
schema: [
|
||||
textField('numContrat', { required: true, unique: true }),
|
||||
textField('numContratBrut'),
|
||||
textField('type'),
|
||||
textField('nomClient'),
|
||||
textField('matricule'),
|
||||
textField('region'),
|
||||
textField('dpt')
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'advalo_audit',
|
||||
schema: [
|
||||
textField('eventType', { required: true }),
|
||||
textField('createdAt'),
|
||||
jsonField('data')
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'advalo_facturation_batch',
|
||||
schema: [
|
||||
textField('numContrat'),
|
||||
textField('dateDebut'),
|
||||
textField('dateFin'),
|
||||
textField('sourceMode'),
|
||||
jsonField('demandeIds'),
|
||||
numberField('totalCapitaux'),
|
||||
numberField('totalCotisation'),
|
||||
textField('fingerprint', { unique: true }),
|
||||
selectField('status', ['IN_PROGRESS', 'DONE', 'FAILED'], { required: true }),
|
||||
textField('createdBy'),
|
||||
textField('createdAt'),
|
||||
textField('facturedAt'),
|
||||
textField('errorMessage')
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'advalo_document',
|
||||
schema: [
|
||||
textField('type'),
|
||||
textField('path'),
|
||||
textField('sha256'),
|
||||
textField('demandeId'),
|
||||
textField('batchId'),
|
||||
textField('createdAt')
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
function parseArgs(argv) {
|
||||
const out = {
|
||||
reset: false,
|
||||
v1Root: DEFAULT_V1_ROOT,
|
||||
reportDir: DEFAULT_REPORT_DIR
|
||||
};
|
||||
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const arg = argv[i];
|
||||
if (arg === '--reset') {
|
||||
out.reset = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === '--v1-root' && argv[i + 1]) {
|
||||
out.v1Root = path.resolve(argv[++i]);
|
||||
continue;
|
||||
}
|
||||
if (arg === '--report-dir' && argv[i + 1]) {
|
||||
out.reportDir = path.resolve(argv[++i]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseCsvFile(filePath) {
|
||||
const raw = fs.readFileSync(filePath, 'utf8');
|
||||
return parse(raw, {
|
||||
delimiter: ';',
|
||||
columns: true,
|
||||
skip_empty_lines: true,
|
||||
relax_quotes: true,
|
||||
bom: true
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeContract(raw) {
|
||||
const digits = String(raw || '').replace(/\D/g, '');
|
||||
if (!digits) return '';
|
||||
return digits.padStart(16, '0').slice(-16);
|
||||
}
|
||||
|
||||
function parseFrDateToIso(dateFr) {
|
||||
const m = String(dateFr || '').trim().match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
|
||||
if (!m) return '';
|
||||
return `${m[3]}-${m[2]}-${m[1]} 00:00:00.000Z`;
|
||||
}
|
||||
|
||||
function parseFrDate(dateFr) {
|
||||
const m = String(dateFr || '').trim().match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
|
||||
if (!m) return null;
|
||||
const date = new Date(`${m[3]}-${m[2]}-${m[1]}T00:00:00.000Z`);
|
||||
return Number.isNaN(date.getTime()) ? null : date;
|
||||
}
|
||||
|
||||
function uniqueByLast(rows, keySelector) {
|
||||
const map = new Map();
|
||||
rows.forEach((row) => {
|
||||
const key = keySelector(row);
|
||||
if (!key) return;
|
||||
map.set(key, row);
|
||||
});
|
||||
return [...map.values()];
|
||||
}
|
||||
|
||||
async function listAllIds(collectionName) {
|
||||
const ids = [];
|
||||
let page = 1;
|
||||
let totalPages = 1;
|
||||
do {
|
||||
const list = await db.records.getList(collectionName, page, PAGE_SIZE, {
|
||||
fields: 'id'
|
||||
});
|
||||
ids.push(...list.items.map((item) => item.id));
|
||||
totalPages = Number(list.totalPages || 1);
|
||||
page += 1;
|
||||
} while (page <= totalPages);
|
||||
return ids;
|
||||
}
|
||||
|
||||
async function deleteCollectionRecords(collectionName, report) {
|
||||
const ids = await listAllIds(collectionName);
|
||||
report.purged[collectionName] = ids.length;
|
||||
if (!ids.length) return;
|
||||
|
||||
const chunkSize = 20;
|
||||
for (let i = 0; i < ids.length; i += chunkSize) {
|
||||
const chunk = ids.slice(i, i + chunkSize);
|
||||
await Promise.all(chunk.map((id) => db.records.delete(collectionName, id)));
|
||||
if ((i + chunk.length) % 2000 === 0 || i + chunk.length === ids.length) {
|
||||
console.log(`[purge:${collectionName}] ${i + chunk.length}/${ids.length}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function insertRows(collectionName, rows, report) {
|
||||
const chunkSize = 20;
|
||||
let inserted = 0;
|
||||
for (let i = 0; i < rows.length; i += chunkSize) {
|
||||
const chunk = rows.slice(i, i + chunkSize);
|
||||
try {
|
||||
const created = await Promise.all(chunk.map((row) => db.records.create(collectionName, row)));
|
||||
inserted += created.length;
|
||||
} catch (chunkError) {
|
||||
for (const row of chunk) {
|
||||
try {
|
||||
await db.records.create(collectionName, row);
|
||||
inserted += 1;
|
||||
} catch (rowError) {
|
||||
const details = rowError?.data || rowError?.details || {};
|
||||
const diagnostic = {
|
||||
collection: collectionName,
|
||||
index: i,
|
||||
row,
|
||||
details
|
||||
};
|
||||
report.anomalies.push({
|
||||
type: 'insert_error',
|
||||
...diagnostic
|
||||
});
|
||||
const err = new Error(`Insert failed for ${collectionName}: ${JSON.stringify(diagnostic)}`);
|
||||
err.originalError = rowError;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (inserted % 2000 === 0 || inserted === rows.length) {
|
||||
console.log(`[insert:${collectionName}] ${inserted}/${rows.length}`);
|
||||
}
|
||||
}
|
||||
report.inserted[collectionName] = inserted;
|
||||
}
|
||||
|
||||
function splitName(fullName) {
|
||||
const cleaned = String(fullName || '').trim().replace(/\s+/g, ' ');
|
||||
if (!cleaned) return { nom: '', prenom: '' };
|
||||
const parts = cleaned.split(' ');
|
||||
const nom = parts.shift() || '';
|
||||
const prenom = parts.join(' ');
|
||||
return { nom, prenom };
|
||||
}
|
||||
|
||||
function normalizeMail(value) {
|
||||
const mail = String(value || '').trim();
|
||||
if (!mail.includes('@')) return '';
|
||||
return mail;
|
||||
}
|
||||
|
||||
function parseClasse(value) {
|
||||
const parsed = Number(String(value || '').replace(',', '.'));
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
}
|
||||
|
||||
async function loadRegionMap() {
|
||||
const regions = [];
|
||||
let page = 1;
|
||||
let totalPages = 1;
|
||||
do {
|
||||
const list = await db.records.getList('region', page, PAGE_SIZE);
|
||||
regions.push(...list.items);
|
||||
totalPages = Number(list.totalPages || 1);
|
||||
page += 1;
|
||||
} while (page <= totalPages);
|
||||
|
||||
const map = new Map();
|
||||
regions.forEach((region) => {
|
||||
map.set(String(region.nom || '').trim().toUpperCase(), region.id);
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
function normalizeAuthGroup(value) {
|
||||
const out = String(value || '').trim().toUpperCase();
|
||||
if (VALID_AUTH_GROUPS.has(out)) return out;
|
||||
return 'SOUSCRIPTEUR';
|
||||
}
|
||||
|
||||
function normalizeFacturationStatus(value) {
|
||||
const raw = String(value || '').trim().toLowerCase();
|
||||
if (!raw) return 'unknown';
|
||||
if (raw.includes('non') && raw.includes('factur')) return 'non_facture';
|
||||
if (raw.includes('factur')) return 'facture';
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function trackQualityForCommonRow(row, report, kind) {
|
||||
const numDemande = String(row['N° Demande'] || '').trim();
|
||||
const numContrat = normalizeContract(row['N° du contrat']);
|
||||
if (numContrat.length !== 16) {
|
||||
report.quality.invalidContracts += 1;
|
||||
report.anomalies.push({
|
||||
type: `${kind}_invalid_numContrat`,
|
||||
numDemande,
|
||||
numContratRaw: String(row['N° du contrat'] || '')
|
||||
});
|
||||
}
|
||||
|
||||
const dateDebut = parseFrDate(row['Date de début du transport']);
|
||||
const dateFin = parseFrDate(row['Date de fin du transport']);
|
||||
if (dateDebut && dateFin && dateDebut > dateFin) {
|
||||
report.quality.invalidDateRanges += 1;
|
||||
report.anomalies.push({
|
||||
type: `${kind}_invalid_date_range`,
|
||||
numDemande,
|
||||
dateDebut: String(row['Date de début du transport'] || ''),
|
||||
dateFin: String(row['Date de fin du transport'] || '')
|
||||
});
|
||||
}
|
||||
|
||||
const statusClass = normalizeFacturationStatus(row['Statut de la facturation']);
|
||||
if (statusClass === 'unknown') {
|
||||
report.quality.incoherentFacturationStatus += 1;
|
||||
report.anomalies.push({
|
||||
type: `${kind}_incoherent_facturation_status`,
|
||||
numDemande,
|
||||
statutFacturation: String(row['Statut de la facturation'] || '')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function buildUsersRows(usersSheetRows, regionMap, report) {
|
||||
const normalized = usersSheetRows.map((row) => {
|
||||
const matricule = String(row.Matricule || '').trim().toUpperCase();
|
||||
const regionName = String(row.Region || '').trim().toUpperCase();
|
||||
const regionId = regionMap.get(regionName) || '';
|
||||
const { nom, prenom } = splitName(row.Nom_Prenom);
|
||||
|
||||
if (!matricule) {
|
||||
report.anomalies.push({ type: 'user_missing_matricule', row });
|
||||
return null;
|
||||
}
|
||||
if (!regionId) {
|
||||
report.anomalies.push({ type: 'user_region_not_found', matricule, region: regionName });
|
||||
}
|
||||
|
||||
return {
|
||||
matricule,
|
||||
matriculeIT: matricule,
|
||||
nom,
|
||||
prenom,
|
||||
mail: normalizeMail(row.Mail),
|
||||
classe: parseClasse(row.Classe),
|
||||
region: regionId || '',
|
||||
authGroupe: normalizeAuthGroup(row.Auth_Groupe)
|
||||
};
|
||||
}).filter(Boolean);
|
||||
|
||||
const deduped = uniqueByLast(normalized, (row) => row.matricule);
|
||||
if (deduped.length !== normalized.length) {
|
||||
report.anomalies.push({
|
||||
type: 'user_duplicate_matricule',
|
||||
sourceRows: normalized.length,
|
||||
dedupedRows: deduped.length
|
||||
});
|
||||
}
|
||||
return deduped;
|
||||
}
|
||||
|
||||
function buildDelegueeRows(csvRows, report) {
|
||||
const normalized = csvRows.map((row) => {
|
||||
const numDemande = String(row['N° Demande'] || '').trim();
|
||||
if (!numDemande) {
|
||||
report.quality.missingNumDemande += 1;
|
||||
report.anomalies.push({ type: 'deleguee_missing_numDemande', row });
|
||||
return null;
|
||||
}
|
||||
const numContrat = normalizeContract(row['N° du contrat']);
|
||||
trackQualityForCommonRow(row, report, 'deleguee');
|
||||
|
||||
return {
|
||||
numDemande,
|
||||
numClient: String(row['N° Client'] || '').trim(),
|
||||
nomClient: String(row['Nom de client'] || '').trim(),
|
||||
numContrat,
|
||||
dateDemande: String(row['Date de la demande'] || '').trim(),
|
||||
marchandise: String(row['Nature de la marchandise'] || '').trim(),
|
||||
mode: String(row['Mode de transports'] || '').trim(),
|
||||
capital: String(row['Valeur de marchandise'] || '').trim(),
|
||||
depart: String(row['Zone de départ'] || '').trim(),
|
||||
arrivee: String(row["Zone d'arrivée"] || '').trim(),
|
||||
dateDebut: String(row['Date de début du transport'] || '').trim(),
|
||||
dateFin: String(row['Date de fin du transport'] || '').trim(),
|
||||
dateDebutIso: parseFrDateToIso(row['Date de début du transport']),
|
||||
dateFinIso: parseFrDateToIso(row['Date de fin du transport']),
|
||||
nomDevis: String(row['Nom du devis'] || '').trim(),
|
||||
proprietaire: String(row['Propriétaire de la marchandise'] || '').trim(),
|
||||
tarif: String(row.Tarif || '').trim(),
|
||||
statutCommande: String(row['Statut de la commande'] || '').trim(),
|
||||
statutFacturation: String(row['Statut de la facturation'] || '').trim()
|
||||
};
|
||||
}).filter(Boolean);
|
||||
|
||||
return uniqueByLast(normalized, (row) => row.numDemande);
|
||||
}
|
||||
|
||||
function buildDemandeRows(csvRows, report) {
|
||||
const normalized = csvRows.map((row) => {
|
||||
const numDemande = String(row['N° Demande'] || '').trim();
|
||||
if (!numDemande) {
|
||||
report.quality.missingNumDemande += 1;
|
||||
report.anomalies.push({ type: 'demande_missing_numDemande', row });
|
||||
return null;
|
||||
}
|
||||
const numContrat = normalizeContract(row['N° du contrat']);
|
||||
trackQualityForCommonRow(row, report, 'demande');
|
||||
|
||||
return {
|
||||
sourceType: 'hors_grille',
|
||||
numDemande,
|
||||
numClient: String(row['N° Client'] || '').trim(),
|
||||
nomClient: String(row['Nom de client'] || '').trim(),
|
||||
numContrat,
|
||||
dateDemande: String(row['Date de la demande'] || '').trim(),
|
||||
marchandise: String(row['Nature de la marchandise'] || '').trim(),
|
||||
mode: String(row['Mode de transports'] || '').trim(),
|
||||
capital: String(row['Valeur de marchandise'] || '').trim(),
|
||||
depart: String(row['Zone de départ'] || '').trim(),
|
||||
arrivee: String(row["Zone d'arrivée"] || '').trim(),
|
||||
dateDebut: String(row['Date de début du transport'] || '').trim(),
|
||||
dateFin: String(row['Date de fin du transport'] || '').trim(),
|
||||
dateDebutIso: parseFrDateToIso(row['Date de début du transport']),
|
||||
dateFinIso: parseFrDateToIso(row['Date de fin du transport']),
|
||||
nomDevis: String(row['Nom du devis'] || '').trim(),
|
||||
proprietaire: String(row['Propriétaire de la marchandise'] || '').trim(),
|
||||
tarif: String(row.Tarif || '').trim(),
|
||||
statutCommande: String(row['Statut de la commande'] || '').trim(),
|
||||
statutFacturation: String(row['Statut de la facturation'] || '').trim(),
|
||||
isDeleted: String(row.SUPPRIME || '').toUpperCase().includes('SUPPRIME'),
|
||||
createdBy: '',
|
||||
region: ''
|
||||
};
|
||||
}).filter(Boolean);
|
||||
|
||||
return uniqueByLast(normalized, (row) => row.numDemande);
|
||||
}
|
||||
|
||||
function buildRefContratRows(refWorkbookPath, report) {
|
||||
const workbook = XLSX.readFile(refWorkbookPath);
|
||||
const sheet = workbook.Sheets.BaseContratRegionCumulAdvalo;
|
||||
if (!sheet) {
|
||||
throw new Error(`Sheet BaseContratRegionCumulAdvalo missing in ${refWorkbookPath}`);
|
||||
}
|
||||
const rows = XLSX.utils.sheet_to_json(sheet, { defval: '' });
|
||||
const normalized = rows.map((row) => {
|
||||
const numContrat = normalizeContract(row.N_contrat || row.N_contrat_brut || '');
|
||||
if (!numContrat) return null;
|
||||
return {
|
||||
numContrat,
|
||||
numContratBrut: String(row.N_contrat_brut || '').trim(),
|
||||
type: String(row.Type || 'Inconnu').trim() || 'Inconnu',
|
||||
nomClient: String(row.Nom_client || 'Inconnu').trim() || 'Inconnu',
|
||||
matricule: String(row.Matricule || 'Inconnu').trim() || 'Inconnu',
|
||||
region: String(row.Region || 'Inconnu').trim() || 'Inconnu',
|
||||
dpt: String(row.Dpt || 'Inconnu').trim() || 'Inconnu'
|
||||
};
|
||||
}).filter(Boolean);
|
||||
|
||||
const deduped = uniqueByLast(normalized, (row) => row.numContrat);
|
||||
if (deduped.length !== 938) {
|
||||
report.anomalies.push({
|
||||
type: 'ref_contrat_unexpected_count',
|
||||
expected: 938,
|
||||
actual: deduped.length
|
||||
});
|
||||
}
|
||||
return deduped;
|
||||
}
|
||||
|
||||
async function getCollectionCount(collectionName) {
|
||||
const list = await db.records.getList(collectionName, 1, 1);
|
||||
return Number(list.totalItems || 0);
|
||||
}
|
||||
|
||||
async function ensureAdvaloCollections(report) {
|
||||
const existing = await db.collections.getFullList(200, { sort: '+name' });
|
||||
const byName = new Map(existing.map((collection) => [String(collection.name || ''), collection]));
|
||||
const created = [];
|
||||
const alreadyPresent = [];
|
||||
|
||||
for (const definition of REQUIRED_ADVALO_COLLECTIONS) {
|
||||
if (byName.has(definition.name)) {
|
||||
alreadyPresent.push(definition.name);
|
||||
continue;
|
||||
}
|
||||
await db.collections.create({
|
||||
name: definition.name,
|
||||
type: 'base',
|
||||
listRule: null,
|
||||
viewRule: null,
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
schema: definition.schema
|
||||
});
|
||||
created.push(definition.name);
|
||||
}
|
||||
|
||||
report.collectionSetup = {
|
||||
created,
|
||||
alreadyPresent
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const opts = parseArgs(process.argv.slice(2));
|
||||
const report = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
options: opts,
|
||||
sourcePaths: {},
|
||||
purged: {},
|
||||
inserted: {},
|
||||
postCheck: {},
|
||||
quality: {
|
||||
invalidContracts: 0,
|
||||
missingNumDemande: 0,
|
||||
invalidDateRanges: 0,
|
||||
incoherentFacturationStatus: 0
|
||||
},
|
||||
anomalies: [],
|
||||
stats: {}
|
||||
};
|
||||
|
||||
const sourceFiles = {
|
||||
bordereauDeleguee: path.join(opts.v1Root, 'bdd', 'bordereau.csv'),
|
||||
bordereauDemande: path.join(opts.v1Root, 'bdd', 'bordereau_hors_grille.csv'),
|
||||
usersExcel: path.join(opts.v1Root, 'bdd', 'xl_utilisateur.xlsx'),
|
||||
refContratExcel: path.join(opts.v1Root, 'bdd', 'archives', 'xl_ref_contrat_region_cumul_advalo.xlsx')
|
||||
};
|
||||
report.sourcePaths = sourceFiles;
|
||||
|
||||
Object.entries(sourceFiles).forEach(([label, filePath]) => {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`Missing source file (${label}): ${filePath}`);
|
||||
}
|
||||
});
|
||||
|
||||
await connect();
|
||||
await ensureAdvaloCollections(report);
|
||||
|
||||
const regionMap = await loadRegionMap();
|
||||
const delegueeRaw = parseCsvFile(sourceFiles.bordereauDeleguee);
|
||||
const demandeRaw = parseCsvFile(sourceFiles.bordereauDemande);
|
||||
const usersWorkbook = XLSX.readFile(sourceFiles.usersExcel);
|
||||
const usersSheet = usersWorkbook.Sheets.Users;
|
||||
if (!usersSheet) {
|
||||
throw new Error(`Sheet Users missing in ${sourceFiles.usersExcel}`);
|
||||
}
|
||||
const usersRaw = XLSX.utils.sheet_to_json(usersSheet, { defval: '' });
|
||||
|
||||
const delegueeRows = buildDelegueeRows(delegueeRaw, report);
|
||||
const demandeRows = buildDemandeRows(demandeRaw, report);
|
||||
const usersRows = buildUsersRows(usersRaw, regionMap, report);
|
||||
const refContratRows = buildRefContratRows(sourceFiles.refContratExcel, report);
|
||||
|
||||
report.stats = {
|
||||
sourceRows: {
|
||||
utilisateur: usersRaw.length,
|
||||
advalo_deleguee: delegueeRaw.length,
|
||||
advalo_demande: demandeRaw.length
|
||||
},
|
||||
dedupRows: {
|
||||
utilisateur: usersRows.length,
|
||||
advalo_deleguee: delegueeRows.length,
|
||||
advalo_demande: demandeRows.length
|
||||
}
|
||||
};
|
||||
|
||||
report.expected = {
|
||||
utilisateur: usersRows.length,
|
||||
advalo_ref_contrat: refContratRows.length,
|
||||
advalo_deleguee: delegueeRows.length,
|
||||
advalo_demande: demandeRows.length
|
||||
};
|
||||
|
||||
console.log('Expected rows:', report.expected);
|
||||
|
||||
if (opts.reset) {
|
||||
for (const collection of ['advalo_deleguee', 'advalo_demande', 'advalo_ref_contrat', 'utilisateur']) {
|
||||
await deleteCollectionRecords(collection, report);
|
||||
}
|
||||
}
|
||||
|
||||
await insertRows('utilisateur', usersRows, report);
|
||||
await insertRows('advalo_ref_contrat', refContratRows, report);
|
||||
await insertRows('advalo_deleguee', delegueeRows, report);
|
||||
await insertRows('advalo_demande', demandeRows, report);
|
||||
|
||||
report.postCheck = {
|
||||
utilisateur: await getCollectionCount('utilisateur'),
|
||||
advalo_ref_contrat: await getCollectionCount('advalo_ref_contrat'),
|
||||
advalo_deleguee: await getCollectionCount('advalo_deleguee'),
|
||||
advalo_demande: await getCollectionCount('advalo_demande')
|
||||
};
|
||||
|
||||
report.acceptance = {
|
||||
utilisateur_is_188: report.postCheck.utilisateur === 188,
|
||||
advalo_ref_contrat_is_938: report.postCheck.advalo_ref_contrat === 938,
|
||||
advalo_data_loaded: report.postCheck.advalo_deleguee > 0 && report.postCheck.advalo_demande > 0,
|
||||
quality_numContrat_16_digits: report.quality.invalidContracts === 0,
|
||||
quality_numDemande_non_empty: report.quality.missingNumDemande === 0,
|
||||
quality_facturation_status_coherent: report.quality.incoherentFacturationStatus === 0
|
||||
};
|
||||
|
||||
fs.mkdirSync(opts.reportDir, { recursive: true });
|
||||
const reportPath = path.join(opts.reportDir, `advalo-migration-report-${Date.now()}.json`);
|
||||
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
||||
|
||||
console.log(JSON.stringify({
|
||||
reportPath,
|
||||
expected: report.expected,
|
||||
postCheck: report.postCheck,
|
||||
acceptance: report.acceptance,
|
||||
anomalies: report.anomalies.length
|
||||
}, null, 2));
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error.stack || error.message || String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/* eslint-disable no-console */
|
||||
require('dotenv').config();
|
||||
|
||||
const axaBridge = require('../src/services/axaBridgeService');
|
||||
|
||||
function parseArgs(argv) {
|
||||
const out = {
|
||||
matricule: process.env.AXA_SMOKE_MATRICULE || 'SYSTEM',
|
||||
numContrat: process.env.AXA_SMOKE_NUM_CONTRAT || '',
|
||||
runQt550: false,
|
||||
totalCotisation: process.env.AXA_SMOKE_TOTAL_COTISATION || '',
|
||||
dateDebut: process.env.AXA_SMOKE_DATE_DEBUT || '',
|
||||
dateFin: process.env.AXA_SMOKE_DATE_FIN || ''
|
||||
};
|
||||
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const arg = argv[i];
|
||||
if (arg === '--matricule' && argv[i + 1]) {
|
||||
out.matricule = String(argv[++i]).trim();
|
||||
continue;
|
||||
}
|
||||
if (arg === '--num-contrat' && argv[i + 1]) {
|
||||
out.numContrat = String(argv[++i]).trim();
|
||||
continue;
|
||||
}
|
||||
if (arg === '--run-qt550') {
|
||||
out.runQt550 = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === '--total-cotisation' && argv[i + 1]) {
|
||||
out.totalCotisation = String(argv[++i]).trim();
|
||||
continue;
|
||||
}
|
||||
if (arg === '--date-debut' && argv[i + 1]) {
|
||||
out.dateDebut = String(argv[++i]).trim();
|
||||
continue;
|
||||
}
|
||||
if (arg === '--date-fin' && argv[i + 1]) {
|
||||
out.dateFin = String(argv[++i]).trim();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const opts = parseArgs(process.argv.slice(2));
|
||||
|
||||
const health = await axaBridge.getHealth({ matricule: opts.matricule });
|
||||
console.log(JSON.stringify({
|
||||
step: 'health',
|
||||
ok: health.ok,
|
||||
helperReady: health.helperReady,
|
||||
errors: health.errors
|
||||
}, null, 2));
|
||||
|
||||
if (!health.ok) {
|
||||
throw new Error('AXA health check failed.');
|
||||
}
|
||||
|
||||
if (opts.numContrat) {
|
||||
const lookup = await axaBridge.lookupContract({
|
||||
matricule: opts.matricule,
|
||||
numContrat: opts.numContrat
|
||||
});
|
||||
console.log(JSON.stringify({
|
||||
step: 'lookup',
|
||||
numContrat: lookup.numContrat,
|
||||
numClient: lookup.numClient,
|
||||
nomClient: lookup.nomClient,
|
||||
numAgent: lookup.numAgent
|
||||
}, null, 2));
|
||||
}
|
||||
|
||||
if (opts.runQt550) {
|
||||
if (!opts.numContrat || !opts.totalCotisation || !opts.dateDebut || !opts.dateFin) {
|
||||
throw new Error('Missing required options for --run-qt550 (--num-contrat, --total-cotisation, --date-debut, --date-fin).');
|
||||
}
|
||||
await axaBridge.runQt550({
|
||||
matricule: opts.matricule,
|
||||
numContrat: opts.numContrat,
|
||||
totalCotisation: opts.totalCotisation,
|
||||
dateDebut: opts.dateDebut,
|
||||
dateFin: opts.dateFin
|
||||
});
|
||||
console.log(JSON.stringify({
|
||||
step: 'qt550',
|
||||
ok: true
|
||||
}, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error.stack || error.message || String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const rootDir = path.resolve(__dirname, '..');
|
||||
const backupDir = path.join(rootDir, 'src', 'db', 'pb_data_backup');
|
||||
const targetDir = path.join(rootDir, 'src', 'db', 'pb_data');
|
||||
const forceReset = process.argv.includes('--reset');
|
||||
|
||||
const filesToCopy = ['data.db', 'logs.db'];
|
||||
const transientFiles = ['data.db-shm', 'data.db-wal', 'logs.db-shm', 'logs.db-wal'];
|
||||
|
||||
function exists(filePath) {
|
||||
return fs.existsSync(filePath);
|
||||
}
|
||||
|
||||
function ensureDir(dirPath) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
function copyFile(source, target) {
|
||||
fs.copyFileSync(source, target);
|
||||
}
|
||||
|
||||
function removeIfExists(filePath) {
|
||||
if (exists(filePath)) fs.unlinkSync(filePath);
|
||||
}
|
||||
|
||||
function bootstrap() {
|
||||
ensureDir(targetDir);
|
||||
|
||||
if (forceReset) {
|
||||
filesToCopy.concat(transientFiles).forEach((name) => removeIfExists(path.join(targetDir, name)));
|
||||
}
|
||||
|
||||
filesToCopy.forEach((fileName) => {
|
||||
const sourcePath = path.join(backupDir, fileName);
|
||||
const targetPath = path.join(targetDir, fileName);
|
||||
|
||||
if (!exists(sourcePath)) {
|
||||
throw new Error(`Backup manquant: ${sourcePath}`);
|
||||
}
|
||||
|
||||
if (!exists(targetPath) || forceReset) {
|
||||
copyFile(sourcePath, targetPath);
|
||||
process.stdout.write(`copied ${fileName}\n`);
|
||||
} else {
|
||||
process.stdout.write(`kept ${fileName}\n`);
|
||||
}
|
||||
});
|
||||
|
||||
process.stdout.write(`PocketBase bootstrap terminé (${forceReset ? 'reset' : 'safe'})\n`);
|
||||
}
|
||||
|
||||
try {
|
||||
bootstrap();
|
||||
} catch (error) {
|
||||
process.stderr.write(`db-bootstrap-local error: ${error.message}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
const rootDir = path.resolve(__dirname, '..');
|
||||
const dbDir = path.join(rootDir, 'src', 'db');
|
||||
const pbDataDir = path.join(dbDir, 'pb_data');
|
||||
|
||||
const fallbackUrl = 'http://127.0.0.1:8091/';
|
||||
const dbUrl = process.env.DB_URL || fallbackUrl;
|
||||
const url = (() => {
|
||||
try {
|
||||
return new URL(dbUrl);
|
||||
} catch (_error) {
|
||||
return new URL(fallbackUrl);
|
||||
}
|
||||
})();
|
||||
const host = `${url.hostname}:${url.port || '8091'}`;
|
||||
|
||||
const binaryPath = process.platform === 'win32'
|
||||
? path.join(dbDir, 'Pocketbase_0.7.5.exe')
|
||||
: path.join(dbDir, 'pocketbase');
|
||||
|
||||
if (!fs.existsSync(binaryPath)) {
|
||||
process.stderr.write(`PocketBase binaire introuvable: ${binaryPath}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(pbDataDir)) {
|
||||
process.stderr.write(`Répertoire pb_data introuvable: ${pbDataDir}\n`);
|
||||
process.stderr.write('Lance d\'abord: npm run db:bootstrap\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const child = spawn(binaryPath, ['serve', '--http', host, '--dir', pbDataDir], {
|
||||
cwd: dbDir,
|
||||
stdio: 'inherit'
|
||||
});
|
||||
|
||||
child.on('exit', (code) => {
|
||||
process.exit(code || 0);
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
process.stderr.write(`Erreur lancement PocketBase: ${error.message}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
|
@ -1,10 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<Prefer32Bit>true</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
@ -1,549 +0,0 @@
|
|||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
private const uint APPCMD_CLIENTONLY = 0x00000010;
|
||||
private const uint CP_WINANSI = 1004;
|
||||
private const uint XTYP_EXECUTE = 0x4050;
|
||||
private const uint CF_TEXT = 1;
|
||||
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
var options = CliOptions.Parse(args);
|
||||
var runner = new DdeScriptRunner(options.Workdir, options.TimeoutMs);
|
||||
var result = runner.ExecuteScript(options.ScriptPath);
|
||||
Console.WriteLine(JsonSerializer.Serialize(result));
|
||||
return result.Ok ? 0 : 1;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var payload = new ScriptResult
|
||||
{
|
||||
Ok = false,
|
||||
ReturnCode = 0,
|
||||
ErrorCode = "AXA_HELPER_RUNTIME",
|
||||
Message = ex.Message
|
||||
};
|
||||
Console.WriteLine(JsonSerializer.Serialize(payload));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class CliOptions
|
||||
{
|
||||
public string ScriptPath { get; private set; } = string.Empty;
|
||||
public string Workdir { get; private set; } = string.Empty;
|
||||
public int TimeoutMs { get; private set; } = 65000;
|
||||
|
||||
public static CliOptions Parse(string[] args)
|
||||
{
|
||||
var options = new CliOptions();
|
||||
|
||||
for (var i = 0; i < args.Length; i += 1)
|
||||
{
|
||||
var current = args[i];
|
||||
if (current.Equals("--script", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
|
||||
{
|
||||
options.ScriptPath = args[++i];
|
||||
continue;
|
||||
}
|
||||
if (current.Equals("--workdir", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
|
||||
{
|
||||
options.Workdir = args[++i];
|
||||
continue;
|
||||
}
|
||||
if (current.Equals("--timeout-ms", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
|
||||
{
|
||||
if (int.TryParse(args[++i], NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) && parsed > 0)
|
||||
{
|
||||
options.TimeoutMs = parsed;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.ScriptPath))
|
||||
{
|
||||
throw new InvalidOperationException("Missing required argument: --script");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(options.Workdir))
|
||||
{
|
||||
throw new InvalidOperationException("Missing required argument: --workdir");
|
||||
}
|
||||
|
||||
options.ScriptPath = Path.GetFullPath(options.ScriptPath);
|
||||
options.Workdir = Path.GetFullPath(options.Workdir);
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ScriptResult
|
||||
{
|
||||
public bool Ok { get; set; }
|
||||
public int ReturnCode { get; set; }
|
||||
public string ErrorCode { get; set; } = string.Empty;
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
internal sealed class DdeScriptRunner
|
||||
{
|
||||
private readonly string _workdir;
|
||||
private readonly int _timeoutMs;
|
||||
private readonly string _screenReaderExe;
|
||||
|
||||
public DdeScriptRunner(string workdir, int timeoutMs)
|
||||
{
|
||||
_workdir = workdir;
|
||||
_timeoutMs = timeoutMs;
|
||||
_screenReaderExe = Path.Combine(_workdir, "ECFDDEViva.exe");
|
||||
}
|
||||
|
||||
public ScriptResult ExecuteScript(string scriptPath)
|
||||
{
|
||||
if (!File.Exists(scriptPath))
|
||||
{
|
||||
throw new FileNotFoundException($"Script file not found: {scriptPath}");
|
||||
}
|
||||
if (!Directory.Exists(_workdir))
|
||||
{
|
||||
throw new DirectoryNotFoundException($"Workdir not found: {_workdir}");
|
||||
}
|
||||
if (!File.Exists(_screenReaderExe))
|
||||
{
|
||||
throw new FileNotFoundException($"Missing AXA screen reader binary: {_screenReaderExe}");
|
||||
}
|
||||
|
||||
using var session = new DdeSession(_timeoutMs);
|
||||
session.Connect("Viva", "Vivaserveur");
|
||||
|
||||
var returnCode = 0;
|
||||
var lines = File.ReadAllLines(scriptPath, Encoding.UTF8);
|
||||
foreach (var rawLine in lines)
|
||||
{
|
||||
var line = rawLine?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!line.StartsWith("[", StringComparison.Ordinal) || !line.Contains(']'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var command = line.Substring(1, line.IndexOf(']') - 1).Trim().ToUpperInvariant();
|
||||
switch (command)
|
||||
{
|
||||
case "SENDFONC":
|
||||
{
|
||||
var fonc = ExtractRequired(line, "FONC='", "';");
|
||||
var key = MapFunctionKey(fonc);
|
||||
session.SendKey(key, 9999);
|
||||
break;
|
||||
}
|
||||
case "SEND":
|
||||
{
|
||||
var posRaw = ExtractRequiredByRegex(line, @"Pos=(?<value>\d+);");
|
||||
var data = ExtractRequired(line, "Data='", "';");
|
||||
var pos = int.Parse(posRaw, CultureInfo.InvariantCulture);
|
||||
session.SendKey(data, pos);
|
||||
break;
|
||||
}
|
||||
case "WAITBUSY":
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
break;
|
||||
}
|
||||
case "ECRAN":
|
||||
{
|
||||
var posRaw = ExtractRequiredByRegex(line, @"Pos=(?<value>\d+);");
|
||||
var stopRaw = ExtractRequiredByRegex(line, @"Stop=(?<value>\d+);");
|
||||
var targetPathRaw = ExtractRequired(line, "Data='", "';");
|
||||
var targetPath = ResolvePath(targetPathRaw);
|
||||
var pos = int.Parse(posRaw, CultureInfo.InvariantCulture);
|
||||
var stop = int.Parse(stopRaw, CultureInfo.InvariantCulture);
|
||||
var screen = ReadScreen();
|
||||
var fragment = SliceScreen(screen, pos, stop);
|
||||
var dir = Path.GetDirectoryName(targetPath);
|
||||
if (!string.IsNullOrWhiteSpace(dir))
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
File.AppendAllText(targetPath, fragment + Environment.NewLine, Encoding.GetEncoding(1252));
|
||||
break;
|
||||
}
|
||||
case "END":
|
||||
{
|
||||
var parRaw = ExtractRequiredByRegex(line, @"Par=(?<value>-?\d+);");
|
||||
returnCode = int.Parse(parRaw, CultureInfo.InvariantCulture);
|
||||
return new ScriptResult
|
||||
{
|
||||
Ok = true,
|
||||
ReturnCode = returnCode,
|
||||
Message = "Script executed successfully."
|
||||
};
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new ScriptResult
|
||||
{
|
||||
Ok = true,
|
||||
ReturnCode = returnCode,
|
||||
Message = "Script executed without END marker."
|
||||
};
|
||||
}
|
||||
|
||||
private string ResolvePath(string value)
|
||||
{
|
||||
if (Path.IsPathRooted(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
return Path.GetFullPath(Path.Combine(_workdir, value));
|
||||
}
|
||||
|
||||
private string ReadScreen()
|
||||
{
|
||||
var tempFile = Path.Combine(_workdir, $"lu_{Guid.NewGuid():N}.txt");
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = _screenReaderExe,
|
||||
Arguments = $"\"{tempFile}\"",
|
||||
WorkingDirectory = _workdir,
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false
|
||||
};
|
||||
|
||||
using var process = Process.Start(psi) ?? throw new InvalidOperationException("Unable to start ECFDDEViva.exe");
|
||||
if (!process.WaitForExit(Math.Min(_timeoutMs, 10000)))
|
||||
{
|
||||
try
|
||||
{
|
||||
process.Kill(true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
throw new TimeoutException("ECFDDEViva.exe timeout.");
|
||||
}
|
||||
if (!File.Exists(tempFile))
|
||||
{
|
||||
throw new InvalidOperationException($"ECFDDEViva output file missing: {tempFile}");
|
||||
}
|
||||
|
||||
string content;
|
||||
try
|
||||
{
|
||||
content = File.ReadAllText(tempFile, Encoding.GetEncoding(1252));
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
var line = content
|
||||
.Replace("\0", string.Empty)
|
||||
.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None)
|
||||
.FirstOrDefault() ?? string.Empty;
|
||||
return line;
|
||||
}
|
||||
|
||||
private static string SliceScreen(string screen, int pos, int stop)
|
||||
{
|
||||
var start = Math.Max(pos - 1, 0);
|
||||
var length = Math.Max(stop - pos, 0);
|
||||
|
||||
if (start >= screen.Length || length <= 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var safeLength = Math.Min(length, screen.Length - start);
|
||||
return screen.Substring(start, safeLength);
|
||||
}
|
||||
|
||||
private static string MapFunctionKey(string value)
|
||||
{
|
||||
return value.Trim().ToUpperInvariant() switch
|
||||
{
|
||||
"ENTER" => "@E",
|
||||
"CLEAR" => "@C",
|
||||
"PF1" => "@1",
|
||||
"PF2" => "@2",
|
||||
"PF3" => "@3",
|
||||
"PF4" => "@4",
|
||||
"PF5" => "@5",
|
||||
"PF6" => "@6",
|
||||
"PF7" => "@7",
|
||||
"PF8" => "@8",
|
||||
"PF9" => "@9",
|
||||
"PF10" => "@a",
|
||||
"PF11" => "@b",
|
||||
"PF12" => "@c",
|
||||
"PA1" => "@x",
|
||||
"PA2" => "@y",
|
||||
"PA3" => "@z",
|
||||
"PA4" => "@+",
|
||||
"TAB" => "@T",
|
||||
"BTAB" => "@B",
|
||||
"HOME" => "@0",
|
||||
"RESET" => "@R",
|
||||
_ => throw new InvalidOperationException($"Unknown FONC key: {value}")
|
||||
};
|
||||
}
|
||||
|
||||
private static string ExtractRequired(string line, string startToken, string endToken)
|
||||
{
|
||||
var start = line.IndexOf(startToken, StringComparison.OrdinalIgnoreCase);
|
||||
if (start < 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Token not found: {startToken} in '{line}'");
|
||||
}
|
||||
start += startToken.Length;
|
||||
var end = line.IndexOf(endToken, start, StringComparison.OrdinalIgnoreCase);
|
||||
if (end < 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Token not found: {endToken} in '{line}'");
|
||||
}
|
||||
return line.Substring(start, end - start);
|
||||
}
|
||||
|
||||
private static string ExtractRequiredByRegex(string line, string pattern)
|
||||
{
|
||||
var match = Regex.Match(line, pattern, RegexOptions.IgnoreCase);
|
||||
if (!match.Success)
|
||||
{
|
||||
throw new InvalidOperationException($"Pattern not found: {pattern} in '{line}'");
|
||||
}
|
||||
return match.Groups["value"].Value;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class DdeSession : IDisposable
|
||||
{
|
||||
private readonly int _timeoutMs;
|
||||
private uint _instanceId;
|
||||
private IntPtr _conversation = IntPtr.Zero;
|
||||
private IntPtr _serviceHandle = IntPtr.Zero;
|
||||
private IntPtr _topicHandle = IntPtr.Zero;
|
||||
private readonly DdeCallback _callbackDelegate;
|
||||
|
||||
public DdeSession(int timeoutMs)
|
||||
{
|
||||
_timeoutMs = timeoutMs;
|
||||
_callbackDelegate = Callback;
|
||||
var initResult = DdeInitializeA(out _instanceId, _callbackDelegate, APPCMD_CLIENTONLY, 0);
|
||||
if (initResult != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"DDE initialize failed: {initResult}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Connect(string service, string topic)
|
||||
{
|
||||
_serviceHandle = DdeCreateStringHandleA(_instanceId, service, CP_WINANSI);
|
||||
_topicHandle = DdeCreateStringHandleA(_instanceId, topic, CP_WINANSI);
|
||||
|
||||
if (_serviceHandle == IntPtr.Zero || _topicHandle == IntPtr.Zero)
|
||||
{
|
||||
throw new InvalidOperationException("DDE string handle creation failed.");
|
||||
}
|
||||
|
||||
_conversation = DdeConnect(_instanceId, _serviceHandle, _topicHandle, IntPtr.Zero);
|
||||
if (_conversation == IntPtr.Zero)
|
||||
{
|
||||
var errorCode = DdeGetLastError(_instanceId);
|
||||
throw new InvalidOperationException($"DDE connect failed (service={service}, topic={topic}, code={errorCode}).");
|
||||
}
|
||||
}
|
||||
|
||||
public void SendKey(string value, int pos)
|
||||
{
|
||||
if (_conversation == IntPtr.Zero)
|
||||
{
|
||||
throw new InvalidOperationException("DDE conversation is not connected.");
|
||||
}
|
||||
|
||||
if (pos != 9999)
|
||||
{
|
||||
var posRaw = pos.ToString("0000", CultureInfo.InvariantCulture);
|
||||
Execute($"[SetPosition({posRaw})]");
|
||||
}
|
||||
Execute($"[SendKey({value})]");
|
||||
}
|
||||
|
||||
private void Execute(string command)
|
||||
{
|
||||
var payload = Encoding.ASCII.GetBytes(command + '\0');
|
||||
uint result;
|
||||
var dataHandle = DdeClientTransaction(
|
||||
payload,
|
||||
(uint)payload.Length,
|
||||
_conversation,
|
||||
IntPtr.Zero,
|
||||
CF_TEXT,
|
||||
XTYP_EXECUTE,
|
||||
(uint)_timeoutMs,
|
||||
out result
|
||||
);
|
||||
|
||||
if (dataHandle == IntPtr.Zero)
|
||||
{
|
||||
var errorCode = DdeGetLastError(_instanceId);
|
||||
throw new InvalidOperationException($"DDE execute failed (cmd={command}, code={errorCode}).");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_conversation != IntPtr.Zero)
|
||||
{
|
||||
try
|
||||
{
|
||||
DdeDisconnect(_conversation);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
_conversation = IntPtr.Zero;
|
||||
}
|
||||
|
||||
if (_serviceHandle != IntPtr.Zero)
|
||||
{
|
||||
try
|
||||
{
|
||||
DdeFreeStringHandle(_instanceId, _serviceHandle);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
_serviceHandle = IntPtr.Zero;
|
||||
}
|
||||
|
||||
if (_topicHandle != IntPtr.Zero)
|
||||
{
|
||||
try
|
||||
{
|
||||
DdeFreeStringHandle(_instanceId, _topicHandle);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
_topicHandle = IntPtr.Zero;
|
||||
}
|
||||
|
||||
if (_instanceId != 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
DdeUninitialize(_instanceId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
_instanceId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static IntPtr Callback(
|
||||
uint uType,
|
||||
uint uFmt,
|
||||
IntPtr hconv,
|
||||
IntPtr hsz1,
|
||||
IntPtr hsz2,
|
||||
IntPtr hdata,
|
||||
IntPtr dwData1,
|
||||
IntPtr dwData2
|
||||
) => IntPtr.Zero;
|
||||
|
||||
private delegate IntPtr DdeCallback(
|
||||
uint uType,
|
||||
uint uFmt,
|
||||
IntPtr hconv,
|
||||
IntPtr hsz1,
|
||||
IntPtr hsz2,
|
||||
IntPtr hdata,
|
||||
IntPtr dwData1,
|
||||
IntPtr dwData2
|
||||
);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Ansi)]
|
||||
private static extern uint DdeInitializeA(
|
||||
out uint pidInst,
|
||||
DdeCallback pfnCallback,
|
||||
uint afCmd,
|
||||
uint ulRes
|
||||
);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Ansi)]
|
||||
private static extern IntPtr DdeCreateStringHandleA(
|
||||
uint idInst,
|
||||
string psz,
|
||||
uint iCodePage
|
||||
);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Ansi)]
|
||||
private static extern bool DdeFreeStringHandle(
|
||||
uint idInst,
|
||||
IntPtr hsz
|
||||
);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Ansi)]
|
||||
private static extern IntPtr DdeConnect(
|
||||
uint idInst,
|
||||
IntPtr hszService,
|
||||
IntPtr hszTopic,
|
||||
IntPtr pCC
|
||||
);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool DdeDisconnect(
|
||||
IntPtr hConv
|
||||
);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool DdeUninitialize(
|
||||
uint idInst
|
||||
);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Ansi)]
|
||||
private static extern IntPtr DdeClientTransaction(
|
||||
byte[] pData,
|
||||
uint cbData,
|
||||
IntPtr hConv,
|
||||
IntPtr hszItem,
|
||||
uint wFmt,
|
||||
uint wType,
|
||||
uint dwTimeout,
|
||||
out uint pdwResult
|
||||
);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern uint DdeGetLastError(
|
||||
uint idInst
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,122 +0,0 @@
|
|||
const express = require('express');
|
||||
const request = require('supertest');
|
||||
|
||||
jest.mock('jsonwebtoken', () => ({
|
||||
verify: jest.fn()
|
||||
}));
|
||||
|
||||
jest.mock('../../utils/logger', () => ({
|
||||
log: jest.fn()
|
||||
}));
|
||||
|
||||
jest.mock('../../services/advaloService', () => ({
|
||||
lookupContract: jest.fn(),
|
||||
getHistorique: jest.fn(),
|
||||
getHistoriqueDetail: jest.fn(),
|
||||
getCumul: jest.fn(),
|
||||
createPonctuel: jest.fn(),
|
||||
softDeleteDemande: jest.fn(),
|
||||
facturerBatch: jest.fn(),
|
||||
generateDemandeDocument: jest.fn(),
|
||||
generateBatchAvenant: jest.fn(),
|
||||
getReporting: jest.fn(),
|
||||
exportHistorique: jest.fn(),
|
||||
getAxaHealth: jest.fn(),
|
||||
getCacheStatus: jest.fn(),
|
||||
rebuildCache: jest.fn()
|
||||
}));
|
||||
|
||||
const jwt = require('jsonwebtoken');
|
||||
const advaloService = require('../../services/advaloService');
|
||||
const advaloRouter = require('../advaloController');
|
||||
|
||||
function createApp() {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use('/advalo', advaloRouter);
|
||||
return app;
|
||||
}
|
||||
|
||||
describe('advaloController technical endpoints', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jwt.verify.mockReturnValue({
|
||||
userMatricule: 'S601153',
|
||||
userAuthGroupe: 'ADMIN',
|
||||
userLastName: 'TEST',
|
||||
userFirstName: 'Admin'
|
||||
});
|
||||
});
|
||||
|
||||
test('GET /advalo/health/axa returns service payload', async () => {
|
||||
advaloService.getAxaHealth.mockResolvedValue({
|
||||
ok: true,
|
||||
helperReady: true
|
||||
});
|
||||
const app = createApp();
|
||||
|
||||
const res = await request(app)
|
||||
.get('/advalo/health/axa')
|
||||
.set('Authorization', 'Bearer token');
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.valid).toBe(true);
|
||||
expect(res.body.ok).toBe(true);
|
||||
expect(advaloService.getAxaHealth).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('GET /advalo/cache/status returns cache status', async () => {
|
||||
advaloService.getCacheStatus.mockResolvedValue({
|
||||
loaded: true,
|
||||
version: 4,
|
||||
counts: { merged: 42 }
|
||||
});
|
||||
const app = createApp();
|
||||
|
||||
const res = await request(app)
|
||||
.get('/advalo/cache/status')
|
||||
.set('Authorization', 'Bearer token');
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.valid).toBe(true);
|
||||
expect(res.body.loaded).toBe(true);
|
||||
expect(res.body.version).toBe(4);
|
||||
expect(advaloService.getCacheStatus).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('POST /advalo/cache/rebuild returns rebuild status', async () => {
|
||||
advaloService.rebuildCache.mockResolvedValue({
|
||||
loaded: true,
|
||||
version: 5,
|
||||
counts: { merged: 100 }
|
||||
});
|
||||
const app = createApp();
|
||||
|
||||
const res = await request(app)
|
||||
.post('/advalo/cache/rebuild')
|
||||
.set('Authorization', 'Bearer token')
|
||||
.send({});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.valid).toBe(true);
|
||||
expect(res.body.version).toBe(5);
|
||||
expect(advaloService.rebuildCache).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('errors are normalized with status + code', async () => {
|
||||
const err = new Error('forbidden');
|
||||
err.status = 403;
|
||||
err.code = 'FORBIDDEN';
|
||||
advaloService.getCacheStatus.mockRejectedValue(err);
|
||||
const app = createApp();
|
||||
|
||||
const res = await request(app)
|
||||
.get('/advalo/cache/status')
|
||||
.set('Authorization', 'Bearer token');
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.valid).toBe(false);
|
||||
expect(res.body.code).toBe('FORBIDDEN');
|
||||
expect(res.body.message).toBe('forbidden');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,217 +0,0 @@
|
|||
const express = require('express');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const renderPage = require('../utils/renderHelper');
|
||||
const advaloService = require('../services/advaloService');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
function getActor(req) {
|
||||
const authHeader = req.headers.authorization || '';
|
||||
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
|
||||
if (!token) {
|
||||
const err = new Error('Session invalide, reconnectez-vous.');
|
||||
err.status = 401;
|
||||
throw err;
|
||||
}
|
||||
return jwt.verify(token, 'no-mdp');
|
||||
}
|
||||
|
||||
function handleError(res, error) {
|
||||
logger.log('error', `Advalo error: ${error.message}`, {
|
||||
code: error.code || '',
|
||||
status: error.status || 500,
|
||||
stack: error.stack,
|
||||
data: error.data,
|
||||
details: error.details,
|
||||
originalError: error.originalError ? String(error.originalError) : undefined
|
||||
});
|
||||
return res.status(error.status || 500).json({
|
||||
valid: false,
|
||||
code: error.code || 'INTERNAL_ERROR',
|
||||
message: error.message || 'Erreur serveur Advalorem'
|
||||
});
|
||||
}
|
||||
|
||||
router.get('/', (_req, res) => {
|
||||
renderPage('advalo.ejs', res);
|
||||
});
|
||||
|
||||
router.get('/lookup-contract', async (req, res) => {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const info = await advaloService.lookupContract(req.query.numContrat, actor);
|
||||
return res.json({ valid: true, info });
|
||||
} catch (error) {
|
||||
return handleError(res, error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/historique', async (req, res) => {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const data = await advaloService.getHistorique(req.query, actor);
|
||||
return res.json({
|
||||
valid: true,
|
||||
rows: data.rows,
|
||||
meta: {
|
||||
totalRows: data.totalRows,
|
||||
totalPages: data.totalPages,
|
||||
page: data.page,
|
||||
pageSize: data.pageSize
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
return handleError(res, error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/historique/:id', async (req, res) => {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const data = await advaloService.getHistoriqueDetail(req.params.id, actor);
|
||||
return res.json({ valid: true, row: data });
|
||||
} catch (error) {
|
||||
return handleError(res, error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/cumul', async (req, res) => {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const cumul = await advaloService.getCumul(req.query, actor);
|
||||
return res.json({ valid: true, ...cumul });
|
||||
} catch (error) {
|
||||
return handleError(res, error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/ponctuel', async (req, res) => {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const row = await advaloService.createPonctuel(req.body, actor);
|
||||
return res.status(201).json({ valid: true, row });
|
||||
} catch (error) {
|
||||
return handleError(res, error);
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/demande/:id', async (req, res) => {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const row = await advaloService.updateDemande(req.params.id, req.body, actor);
|
||||
return res.json({ valid: true, row });
|
||||
} catch (error) {
|
||||
return handleError(res, error);
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/demande/:id', async (req, res) => {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const row = await advaloService.softDeleteDemande(req.params.id, actor);
|
||||
return res.json({ valid: true, row });
|
||||
} catch (error) {
|
||||
return handleError(res, error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/facturation/batch', async (req, res) => {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const result = await advaloService.facturerBatch(req.body, actor);
|
||||
return res.json({ valid: true, ...result });
|
||||
} catch (error) {
|
||||
return handleError(res, error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/demande/:id/avenant', async (req, res) => {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const doc = await advaloService.generateDemandeDocument(req.params.id, 'avenant', actor);
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${doc.filename}"`);
|
||||
return res.send(doc.buffer);
|
||||
} catch (error) {
|
||||
return handleError(res, error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/demande/:id/attestation', async (req, res) => {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const doc = await advaloService.generateDemandeDocument(req.params.id, 'attestation', actor);
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${doc.filename}"`);
|
||||
return res.send(doc.buffer);
|
||||
} catch (error) {
|
||||
return handleError(res, error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/facturation/batch/:id/avenant', async (req, res) => {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const doc = await advaloService.generateBatchAvenant(req.params.id, actor);
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${doc.filename}"`);
|
||||
return res.send(doc.buffer);
|
||||
} catch (error) {
|
||||
return handleError(res, error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/reporting', async (req, res) => {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const data = await advaloService.getReporting(req.query, actor);
|
||||
return res.json({ valid: true, ...data });
|
||||
} catch (error) {
|
||||
return handleError(res, error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/health/axa', async (req, res) => {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const health = await advaloService.getAxaHealth(actor);
|
||||
return res.json({ valid: true, ...health });
|
||||
} catch (error) {
|
||||
return handleError(res, error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/cache/status', async (req, res) => {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const status = await advaloService.getCacheStatus(actor);
|
||||
return res.json({ valid: true, ...status });
|
||||
} catch (error) {
|
||||
return handleError(res, error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/cache/rebuild', async (req, res) => {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const status = await advaloService.rebuildCache(actor);
|
||||
return res.json({ valid: true, ...status });
|
||||
} catch (error) {
|
||||
return handleError(res, error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/export', async (req, res) => {
|
||||
try {
|
||||
const actor = getActor(req);
|
||||
const csv = await advaloService.exportHistorique(req.query, actor);
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="advalo_export.csv"');
|
||||
return res.send(csv);
|
||||
} catch (error) {
|
||||
return handleError(res, error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
Pocketbase_0.7.5.exe serve --http="127.0.0.1:8091"
|
||||
pause
|
||||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
|||
|
||||
Binary file not shown.
|
|
@ -1,205 +0,0 @@
|
|||
// Mocks DB + pont AXA AVANT le require du service (jest hoiste les jest.mock).
|
||||
const mockRecords = {
|
||||
getList: jest.fn(),
|
||||
getOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn()
|
||||
};
|
||||
const mockRunQt550 = jest.fn().mockResolvedValue({ ok: true });
|
||||
|
||||
jest.mock('../../db/db-connect', () => ({
|
||||
db: { baseUrl: 'http://localhost:8090/', records: mockRecords }
|
||||
}));
|
||||
jest.mock('../axaBridgeService', () => ({
|
||||
runQt550: (...args) => mockRunQt550(...args),
|
||||
lookupContract: jest.fn(),
|
||||
getHealth: jest.fn()
|
||||
}));
|
||||
|
||||
const path = require('path');
|
||||
const PizZip = require('pizzip');
|
||||
const advaloService = require('../advaloService');
|
||||
const { buildCommonDocContext, normalizeLookupInfo, normalizeMergedFallbackRow, renderAdvaloDocx, advaloCache } = advaloService.__private;
|
||||
|
||||
function docxText(buffer) {
|
||||
const xml = new PizZip(buffer).file('word/document.xml').asText();
|
||||
return (xml.match(/<w:t[^>]*>(.*?)<\/w:t>/g) || []).map((t) => t.replace(/<[^>]+>/g, '')).join(' ');
|
||||
}
|
||||
|
||||
const actor = { userMatricule: 'A123BC', userAuthGroupe: 'MANAGER', userFirstName: 'Jean', userLastName: 'Dupont' };
|
||||
|
||||
describe('Advalo — parité v1 du calcul documentaire', () => {
|
||||
test('buildCommonDocContext injecte les prix réellement saisis (pas les valeurs figées)', () => {
|
||||
const row = { numContrat: '0000022126873304', capital: '100000', tarif: '336,00', dateDebut: '01/04/2026', dateFin: '02/04/2026', mode: 'Terrestre', marchandise: 'Moteurs' };
|
||||
const contractInfo = { nomClient: 'DURAND & FILS', adresseAgent: '1 rue de Paris', postalAgent: '75001 PARIS', telAgent: '0102030405', faxAgent: '0102030406', numAgent: '0009', nomAgent: 'AGENCE' };
|
||||
const pricing = { valeurAssuree: '100000', taux: '0.5', primeMinimum: '20', cotisationHT: '500', coutActe: '36', cotisationTTC: '536' };
|
||||
|
||||
const ctx = buildCommonDocContext(row, contractInfo, actor, pricing);
|
||||
|
||||
expect(ctx.tauxField).toBe('0,50');
|
||||
expect(ctx.primeMinimum).toBe('20,00');
|
||||
expect(ctx.cotisationField).toBe('500,00');
|
||||
expect(ctx.coutActeField).toBe('36,00');
|
||||
expect(ctx.cotisationTTC).toBe('536,00');
|
||||
// Coordonnées agent propagées dans l'en-tête (parité v1 Avenant).
|
||||
expect(ctx.adresseAgent).toBe('1 rue de Paris');
|
||||
expect(ctx.telAgent).toBe('0102030405');
|
||||
expect(ctx.faxAgent).toBe('0102030406');
|
||||
// Valeurs brutes (pas de double-échappement XML) — docxtemplater échappera.
|
||||
expect(ctx.nomClient).toBe('DURAND & FILS');
|
||||
});
|
||||
|
||||
test('rendu réel Avenant_Ponctuel.docx (docxtemplater) contient les prix saisis + phrase TTC', () => {
|
||||
const row = { numContrat: '0000022126873304', capital: '100000', tarif: '536,00', dateDebut: '01/04/2026', dateFin: '02/04/2026', mode: 'Terrestre, Aérien', marchandise: 'Moteurs', depart: 'Paris', arrivee: 'Lyon' };
|
||||
const contractInfo = { nomClient: 'DURAND & FILS', adresseAgent: '1 rue de Paris' };
|
||||
const pricing = { valeurAssuree: '100000', taux: '0.5', primeMinimum: '20', cotisationHT: '500', coutActe: '36', cotisationTTC: '536' };
|
||||
const ctx = buildCommonDocContext(row, contractInfo, actor, pricing);
|
||||
const tplPath = path.resolve(__dirname, '..', '..', 'templates', 'advalo', 'Avenant_Ponctuel.docx');
|
||||
const text = docxText(renderAdvaloDocx(tplPath, ctx));
|
||||
|
||||
expect(text).toContain('500,00'); // cotisation HT
|
||||
expect(text).toContain('536,00'); // TTC
|
||||
expect(text).toContain('Il est perçu');
|
||||
expect(text).toContain('Moteurs');
|
||||
expect(text).toContain('DURAND & FILS'); // échappé une seule fois
|
||||
});
|
||||
|
||||
test('buildCommonDocContext: défauts v1 (0,30 / 15 / 36) seulement à défaut de pricing, TTC = HT + acte', () => {
|
||||
const row = { numContrat: '0000022126873304', capital: '3000', tarif: '15,00' };
|
||||
const ctx = buildCommonDocContext(row, {}, actor, {});
|
||||
|
||||
expect(ctx.tauxField).toBe('0,30');
|
||||
expect(ctx.primeMinimum).toBe('15,00');
|
||||
expect(ctx.cotisationField).toBe('15,00'); // HT = tarif (grille déléguée)
|
||||
expect(ctx.coutActeField).toBe('36,00');
|
||||
expect(ctx.cotisationTTC).toBe('51,00'); // 15 + 36
|
||||
});
|
||||
});
|
||||
|
||||
describe('Advalo — lookup contrat (coordonnées intermédiaire)', () => {
|
||||
test('normalizeLookupInfo conserve adresseAgent/postalAgent/telAgent/faxAgent', () => {
|
||||
const raw = {
|
||||
numContrat: '0000022126873304', numClient: '0858406820', nomClient: 'CLIENT',
|
||||
nomAgent: 'AGENCE', numAgent: '0009876543',
|
||||
adresseAgent: '1 rue de Paris', postalAgent: '75001 PARIS', telAgent: '0102030405', faxAgent: '0102030406'
|
||||
};
|
||||
const info = normalizeLookupInfo(raw, '0000022126873304');
|
||||
expect(info.adresseAgent).toBe('1 rue de Paris');
|
||||
expect(info.postalAgent).toBe('75001 PARIS');
|
||||
expect(info.telAgent).toBe('0102030405');
|
||||
expect(info.faxAgent).toBe('0102030406');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Advalo — enrichissement Cumul/Reporting via référentiel contrat', () => {
|
||||
test('normalizeMergedFallbackRow enrichit region/dpt/souscripteur depuis le ref (parité v1 getVarByNumContrat)', () => {
|
||||
advaloCache.refContratById.set('0000022126873304', {
|
||||
nomClient: 'KANGOUROUBOX', region: 'ILE DE FRANCE', dpt: '75', souscripteur: 'Z999ZZ'
|
||||
});
|
||||
const delegueeRow = normalizeMergedFallbackRow(
|
||||
{ id: 'g1', numContrat: '0000022126873304', tarif: '15 €', statutFacturation: 'Non facturé' },
|
||||
'deleguee'
|
||||
);
|
||||
expect(delegueeRow.region).toBe('ILE DE FRANCE');
|
||||
expect(delegueeRow.dpt).toBe('75');
|
||||
expect(delegueeRow.souscripteur).toBe('Z999ZZ');
|
||||
expect(delegueeRow.nomClient).toBe('KANGOUROUBOX');
|
||||
advaloCache.refContratById.delete('0000022126873304');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Advalo — modification demande hors-grille (parité v1 "Modification")', () => {
|
||||
beforeEach(() => {
|
||||
Object.values(mockRecords).forEach((fn) => fn.mockReset());
|
||||
advaloCache.loaded = false;
|
||||
});
|
||||
|
||||
test('updateDemande met à jour les champs et journalise le pricing (event update)', async () => {
|
||||
mockRecords.getOne.mockResolvedValue({
|
||||
id: 'dem9', numContrat: '0000022126873304', numClient: '0858406820',
|
||||
statutFacturation: 'Non facturé', isDeleted: false
|
||||
});
|
||||
mockRecords.update.mockImplementation(async (collection, id, patch) => ({ id, ...patch }));
|
||||
mockRecords.create.mockResolvedValue({ id: 'aud9' });
|
||||
|
||||
const updated = await advaloService.updateDemande('d:dem9', {
|
||||
marchandise: 'Pièces auto', mode: 'Terrestre, Maritime', depart: 'Lyon', arrivee: 'Gênes',
|
||||
dateDebut: '05/04/2026', dateFin: '06/04/2026', capital: '50000',
|
||||
taux: '0.4', primeMinimum: '15', coutActe: '36', cotisationHT: '200', cotisationTTC: '236'
|
||||
}, actor);
|
||||
|
||||
expect(updated.marchandise).toBe('Pièces auto');
|
||||
expect(updated.mode).toBe('Terrestre, Maritime');
|
||||
expect(updated.tarif).toBe('236');
|
||||
const audit = mockRecords.create.mock.calls.find((c) => c[0] === 'advalo_audit');
|
||||
expect(audit[1].eventType).toBe('update');
|
||||
expect(audit[1].data.pricing.cotisationHT).toBe('200');
|
||||
});
|
||||
|
||||
test('updateDemande refuse une demande déjà facturée', async () => {
|
||||
mockRecords.getOne.mockResolvedValue({ id: 'dem8', numContrat: '0000022126873304', statutFacturation: 'Facturé 01/04/2026', isDeleted: false });
|
||||
await expect(advaloService.updateDemande('d:dem8', { marchandise: 'x', mode: 'Terrestre', depart: 'a', arrivee: 'b', dateDebut: '01/04/2026', dateFin: '02/04/2026', capital: '1', taux: '0.3', primeMinimum: '15', cotisationHT: '15', cotisationTTC: '51' }, actor))
|
||||
.rejects.toThrow(/facturée ne peut pas être modifiée/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Advalo — facturation QT550 (parité v1: HT à Pos=329, coût d acte une seule fois)', () => {
|
||||
const realPlatform = process.platform;
|
||||
|
||||
beforeEach(() => {
|
||||
Object.values(mockRecords).forEach((fn) => fn.mockReset());
|
||||
mockRunQt550.mockClear().mockResolvedValue({ ok: true });
|
||||
// Cache vide forcé à chaque test pour isoler.
|
||||
advaloCache.loaded = false;
|
||||
advaloCache.loadingPromise = null;
|
||||
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, 'platform', { value: realPlatform, configurable: true });
|
||||
});
|
||||
|
||||
test('facturerBatch envoie la cotisation HT (audit) et le coût d acte 36 une seule fois — pas de double comptage', async () => {
|
||||
const demande = {
|
||||
id: 'dem1', numContrat: '0000022126873304', numClient: '0858406820', nomClient: 'CLIENT',
|
||||
capital: '100000', tarif: '336,00', // TTC stocké (HT 300 + 36)
|
||||
dateDebut: '01/04/2026', dateFin: '02/04/2026',
|
||||
statutFacturation: 'Non facturé', isDeleted: false
|
||||
};
|
||||
const auditRec = {
|
||||
eventType: 'create', created: '2026-04-01T10:00:00.000Z',
|
||||
data: { demandeId: 'dem1', pricing: { cotisationHT: '300,00', coutActe: '36,00', cotisationTTC: '336,00' } }
|
||||
};
|
||||
|
||||
mockRecords.getOne.mockImplementation(async (collection, id) => {
|
||||
if (collection === 'advalo_demande' && id === 'dem1') return { ...demande };
|
||||
throw new Error(`unexpected getOne ${collection} ${id}`);
|
||||
});
|
||||
mockRecords.getList.mockImplementation(async (collection) => {
|
||||
if (collection === 'advalo_audit') return { items: [auditRec], totalItems: 1, totalPages: 1 };
|
||||
if (collection === 'advalo_facturation_batch') return { items: [], totalItems: 0, totalPages: 1 };
|
||||
return { items: [], totalItems: 0, totalPages: 1 };
|
||||
});
|
||||
mockRecords.create.mockImplementation(async (collection, payload) => ({ id: `${collection}-1`, ...payload }));
|
||||
mockRecords.update.mockImplementation(async (collection, id, patch) => ({ id, ...patch }));
|
||||
|
||||
const result = await advaloService.facturerBatch(
|
||||
{ demandeIds: ['d:dem1'], sourceMode: 'hors_grille', includeTransportDetails: true },
|
||||
actor
|
||||
);
|
||||
|
||||
expect(result.idempotent).toBe(false);
|
||||
|
||||
// QT550 reçoit le HT (300), PAS le TTC (336), et coutActe '36' une seule fois.
|
||||
expect(mockRunQt550).toHaveBeenCalledTimes(1);
|
||||
const qtArgs = mockRunQt550.mock.calls[0][0];
|
||||
expect(qtArgs.totalCotisation).toBe(300);
|
||||
expect(qtArgs.coutActe).toBe('36');
|
||||
|
||||
// Le lot persiste le total HT + l acte séparé.
|
||||
const batchCreate = mockRecords.create.mock.calls.find((c) => c[0] === 'advalo_facturation_batch');
|
||||
expect(batchCreate).toBeDefined();
|
||||
expect(batchCreate[1].totalCotisation).toBe(300);
|
||||
expect(batchCreate[1].totalActe).toBe(advaloService.__private.COUT_ACTE);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
const axaBridgeService = require('../axaBridgeService');
|
||||
|
||||
function withAt(screen, pos, value) {
|
||||
const chars = screen.split('');
|
||||
const start = Math.max(0, pos - 1);
|
||||
for (let i = 0; i < String(value).length; i += 1) {
|
||||
chars[start + i] = String(value)[i];
|
||||
}
|
||||
return chars.join('');
|
||||
}
|
||||
|
||||
function emptyScreen(length = 2200) {
|
||||
return ''.padEnd(length, ' ');
|
||||
}
|
||||
|
||||
describe('axaBridgeService parsing', () => {
|
||||
test('parsePa025Screen extracts expected offsets', () => {
|
||||
let screen = emptyScreen();
|
||||
screen = withAt(screen, 81, 'CLIENT TEST');
|
||||
screen = withAt(screen, 161, 'SA');
|
||||
screen = withAt(screen, 283, '0012345678');
|
||||
screen = withAt(screen, 241, '12 RUE DES TESTS');
|
||||
screen = withAt(screen, 321, 'PARIS');
|
||||
screen = withAt(screen, 401, '75001');
|
||||
screen = withAt(screen, 407, 'PARIS CENTRE');
|
||||
screen = withAt(screen, 726, '31122');
|
||||
screen = withAt(screen, 203, '0009876543');
|
||||
screen = withAt(screen, 443, 'RC123');
|
||||
|
||||
const parsed = axaBridgeService.__private.parsePa025Screen(screen);
|
||||
expect(parsed.nomClient).toBe('CLIENT TEST SA');
|
||||
expect(parsed.numClient).toBe('0012345678');
|
||||
expect(parsed.adresseClient).toBe('12 RUE DES TESTS PARIS');
|
||||
expect(parsed.codePostal).toBe('75001 PARIS CENTRE');
|
||||
expect(parsed.dateFin).toBe('31122');
|
||||
expect(parsed.numAgent).toBe('0009876543');
|
||||
expect(parsed.codeProduit).toBe('RC123');
|
||||
});
|
||||
|
||||
test('parseCl063Screen extracts expected offsets', () => {
|
||||
let screen = emptyScreen();
|
||||
screen = withAt(screen, 149, '0011122233');
|
||||
screen = withAt(screen, 243, 'SARL ');
|
||||
screen = withAt(screen, 249, 'EXEMPLE CLIENT');
|
||||
screen = withAt(screen, 323, 'LIGNE 1');
|
||||
screen = withAt(screen, 403, 'LIGNE 2');
|
||||
screen = withAt(screen, 483, 'VILLE TEST');
|
||||
screen = withAt(screen, 563, '69000');
|
||||
screen = withAt(screen, 569, 'LYON');
|
||||
screen = withAt(screen, 810, '123');
|
||||
screen = withAt(screen, 814, '456');
|
||||
screen = withAt(screen, 818, '789');
|
||||
screen = withAt(screen, 822, '00012');
|
||||
|
||||
const parsed = axaBridgeService.__private.parseCl063Screen(screen);
|
||||
expect(parsed.nomClient).toBe('SARL EXEMPLE CLIENT');
|
||||
expect(parsed.adresseClient).toBe('LIGNE 1 LIGNE 2 VILLE TEST');
|
||||
expect(parsed.codePostal).toBe('69000 LYON');
|
||||
expect(parsed.numAgent).toBe('0011122233');
|
||||
expect(parsed.numAgentPortfolio).toBe('011122233');
|
||||
expect(parsed.siren).toBe('12345678900012');
|
||||
});
|
||||
|
||||
test('parseAc800Screen extracts expected offsets', () => {
|
||||
let screen = emptyScreen();
|
||||
screen = withAt(screen, 824, 'AGENT NOM');
|
||||
screen = withAt(screen, 891, 'ADRESSE A');
|
||||
screen = withAt(screen, 924, '75015');
|
||||
screen = withAt(screen, 931, 'PARIS');
|
||||
screen = withAt(screen, 971, '0102030405');
|
||||
screen = withAt(screen, 1025, '0504030201');
|
||||
|
||||
const parsed = axaBridgeService.__private.parseAc800Screen(screen, true);
|
||||
expect(parsed.nomAgent).toBe('AGENT NOM');
|
||||
expect(parsed.adresseAgent).toBe('ADRESSE A 75015 PARIS');
|
||||
expect(parsed.telAgent).toBe('0102030405');
|
||||
expect(parsed.faxAgent).toBe('0504030201');
|
||||
expect(parsed.postalAgent).toBe('75015 PARIS');
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,514 +0,0 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { spawn } = require('child_process');
|
||||
const runtimePaths = require('../utils/runtimePaths');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const AXA_ROOT = runtimePaths.resolveWorkspacePath('vbs');
|
||||
const HELPER_PROJECT_DIR = runtimePaths.resolveWorkspacePath('src', 'axa-helper');
|
||||
const HELPER_OUTPUT_DIR = runtimePaths.resolveWorkspacePath('src', 'axa-helper', 'bin', 'publish');
|
||||
const HELPER_EXE_PATH = process.env.AXA_DDE_HELPER_EXE
|
||||
? path.resolve(process.env.AXA_DDE_HELPER_EXE)
|
||||
: path.join(HELPER_OUTPUT_DIR, 'AxaDdeBridge.exe');
|
||||
const HELPER_TIMEOUT_MS = Number(process.env.AXA_TIMEOUT_MS || 65000);
|
||||
|
||||
const lockMap = new Map();
|
||||
let helperBuildPromise = null;
|
||||
|
||||
function ensureWindows() {
|
||||
if (process.platform !== 'win32') {
|
||||
const err = new Error('Pont AXA indisponible sur cet environnement (Windows requis).');
|
||||
err.code = 'AXA_WINDOWS_REQUIRED';
|
||||
err.status = 502;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeFilePart(value) {
|
||||
return String(value || '')
|
||||
.replace(/[^a-zA-Z0-9-_]+/g, '_')
|
||||
.replace(/_+/g, '_')
|
||||
.replace(/^_+|_+$/g, '');
|
||||
}
|
||||
|
||||
function padLeft(value, len, char = '0') {
|
||||
const text = String(value || '');
|
||||
if (text.length >= len) return text;
|
||||
return `${char.repeat(len - text.length)}${text}`;
|
||||
}
|
||||
|
||||
function trimLeadingZeros(value) {
|
||||
const digits = String(value || '').replace(/\D/g, '');
|
||||
if (!digits) return '';
|
||||
return digits.replace(/^0+(?=\d)/, '');
|
||||
}
|
||||
|
||||
function screenSlice(screen, start, len) {
|
||||
const src = String(screen || '');
|
||||
const startIndex = Math.max(Number(start || 1) - 1, 0);
|
||||
const size = Math.max(Number(len || 0), 0);
|
||||
if (startIndex >= src.length || size <= 0) return '';
|
||||
return src.slice(startIndex, startIndex + size);
|
||||
}
|
||||
|
||||
function collapseSpaces(value) {
|
||||
return String(value || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function parseAgentPortfolio(value) {
|
||||
return String(value || '').replace(/\D/g, '').slice(-9);
|
||||
}
|
||||
|
||||
function parsePa025Screen(screen) {
|
||||
return {
|
||||
nomClient: collapseSpaces(screenSlice(screen, 81, 30) + screenSlice(screen, 161, 30)),
|
||||
numClient: String(screenSlice(screen, 283, 10) || '').replace(/\D/g, ''),
|
||||
adresseClient: collapseSpaces(screenSlice(screen, 241, 30) + screenSlice(screen, 321, 30)),
|
||||
codePostal: collapseSpaces(screenSlice(screen, 401, 5) + ' ' + screenSlice(screen, 407, 24)),
|
||||
dateFin: collapseSpaces(screenSlice(screen, 726, 5)),
|
||||
numAgent: String(screenSlice(screen, 203, 10) || '').replace(/\D/g, ''),
|
||||
codeProduit: collapseSpaces(screenSlice(screen, 443, 5))
|
||||
};
|
||||
}
|
||||
|
||||
function parseCl063Screen(screen) {
|
||||
const rawNumAgent = String(screenSlice(screen, 149, 10) || '').replace(/\D/g, '');
|
||||
return {
|
||||
nomClient: collapseSpaces(screenSlice(screen, 243, 5) + screenSlice(screen, 249, 30)),
|
||||
adresseClient: collapseSpaces(screenSlice(screen, 323, 30) + screenSlice(screen, 403, 26) + ' ' + screenSlice(screen, 483, 30)),
|
||||
codePostal: collapseSpaces(screenSlice(screen, 563, 5) + ' ' + screenSlice(screen, 569, 30)),
|
||||
numAgent: rawNumAgent,
|
||||
numAgentPortfolio: parseAgentPortfolio(rawNumAgent),
|
||||
siren: String(
|
||||
screenSlice(screen, 810, 3)
|
||||
+ screenSlice(screen, 814, 3)
|
||||
+ screenSlice(screen, 818, 3)
|
||||
+ screenSlice(screen, 822, 5)
|
||||
).replace(/\D/g, '')
|
||||
};
|
||||
}
|
||||
|
||||
function parseAc800Screen(screen, includePostal = false) {
|
||||
const out = {
|
||||
nomAgent: collapseSpaces(screenSlice(screen, 824, 32)),
|
||||
adresseAgent: collapseSpaces(screenSlice(screen, 891, 30) + screenSlice(screen, 924, 30)),
|
||||
telAgent: collapseSpaces(screenSlice(screen, 971, 15)),
|
||||
faxAgent: collapseSpaces(screenSlice(screen, 1025, 15))
|
||||
};
|
||||
if (includePostal) {
|
||||
out.postalAgent = collapseSpaces(screenSlice(screen, 924, 5) + ' ' + screenSlice(screen, 931, 23));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function runProcess(command, args, { cwd, timeoutMs = HELPER_TIMEOUT_MS } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
cwd,
|
||||
windowsHide: true
|
||||
});
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
child.kill('SIGTERM');
|
||||
const err = new Error(`Process timeout: ${command}`);
|
||||
err.code = 'AXA_PROCESS_TIMEOUT';
|
||||
err.status = 504;
|
||||
reject(err);
|
||||
}, timeoutMs);
|
||||
|
||||
child.stdout.on('data', (chunk) => {
|
||||
stdout += chunk.toString();
|
||||
});
|
||||
child.stderr.on('data', (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
child.on('error', (error) => {
|
||||
clearTimeout(timer);
|
||||
reject(error);
|
||||
});
|
||||
child.on('exit', (code) => {
|
||||
clearTimeout(timer);
|
||||
if (code !== 0) {
|
||||
const err = new Error(`Process failed (${command}) code=${code}`);
|
||||
err.code = 'AXA_PROCESS_FAILED';
|
||||
err.status = 502;
|
||||
err.details = { code, stdout, stderr };
|
||||
return reject(err);
|
||||
}
|
||||
return resolve({ stdout, stderr });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureHelperBuilt() {
|
||||
if (fs.existsSync(HELPER_EXE_PATH)) return HELPER_EXE_PATH;
|
||||
if (helperBuildPromise) return helperBuildPromise;
|
||||
|
||||
helperBuildPromise = (async () => {
|
||||
ensureWindows();
|
||||
const projectFile = path.join(HELPER_PROJECT_DIR, 'AxaDdeBridge.csproj');
|
||||
if (!fs.existsSync(projectFile)) {
|
||||
const err = new Error(`Projet helper AXA introuvable: ${projectFile}`);
|
||||
err.code = 'AXA_HELPER_PROJECT_MISSING';
|
||||
err.status = 500;
|
||||
throw err;
|
||||
}
|
||||
|
||||
fs.mkdirSync(HELPER_OUTPUT_DIR, { recursive: true });
|
||||
|
||||
try {
|
||||
await runProcess('dotnet', [
|
||||
'publish',
|
||||
projectFile,
|
||||
'-c',
|
||||
'Release',
|
||||
'-r',
|
||||
'win-x86',
|
||||
'--self-contained',
|
||||
'false',
|
||||
'-o',
|
||||
HELPER_OUTPUT_DIR
|
||||
], {
|
||||
cwd: HELPER_PROJECT_DIR,
|
||||
timeoutMs: 180000
|
||||
});
|
||||
} catch (error) {
|
||||
logger.log('error', 'AXA helper publish failed', {
|
||||
message: error.message,
|
||||
details: error.details
|
||||
});
|
||||
const err = new Error('Impossible de compiler le helper AXA DDE (dotnet publish).');
|
||||
err.code = 'AXA_HELPER_BUILD_FAILED';
|
||||
err.status = 500;
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(HELPER_EXE_PATH)) {
|
||||
const err = new Error(`Binaire helper AXA introuvable après build: ${HELPER_EXE_PATH}`);
|
||||
err.code = 'AXA_HELPER_EXE_MISSING';
|
||||
err.status = 500;
|
||||
throw err;
|
||||
}
|
||||
return HELPER_EXE_PATH;
|
||||
})();
|
||||
|
||||
try {
|
||||
return await helperBuildPromise;
|
||||
} finally {
|
||||
helperBuildPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
function createScriptFile(scriptDir, matricule, lines, prefix) {
|
||||
const safeMatricule = sanitizeFilePart(matricule || 'SYSTEM');
|
||||
const filePath = path.join(
|
||||
scriptDir,
|
||||
'config',
|
||||
`${prefix}_${safeMatricule}_${Date.now()}_${Math.random().toString(16).slice(2)}.txt`
|
||||
);
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, `${lines.join('\r\n')}\r\n`, 'utf8');
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function readCaptureLine(filePath) {
|
||||
if (!fs.existsSync(filePath)) return '';
|
||||
const raw = fs.readFileSync(filePath, 'latin1');
|
||||
const lines = raw
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.replace(/\0/g, '').trimEnd())
|
||||
.filter((line) => line.length > 0);
|
||||
return lines.length ? lines[lines.length - 1] : '';
|
||||
}
|
||||
|
||||
async function executeAxaScript({ scriptDir, matricule, lines, label }) {
|
||||
ensureWindows();
|
||||
const helperExe = await ensureHelperBuilt();
|
||||
const scriptPath = createScriptFile(scriptDir, matricule, lines, `runtime_${label}`);
|
||||
|
||||
try {
|
||||
const { stdout } = await runProcess(helperExe, [
|
||||
'--script',
|
||||
scriptPath,
|
||||
'--workdir',
|
||||
scriptDir,
|
||||
'--timeout-ms',
|
||||
String(HELPER_TIMEOUT_MS)
|
||||
], {
|
||||
cwd: scriptDir,
|
||||
timeoutMs: HELPER_TIMEOUT_MS
|
||||
});
|
||||
|
||||
const parsedLine = stdout
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.reverse()
|
||||
.find((line) => line.startsWith('{') && line.endsWith('}'));
|
||||
|
||||
if (!parsedLine) {
|
||||
const err = new Error(`Réponse helper AXA invalide (${label}).`);
|
||||
err.code = 'AXA_HELPER_INVALID_OUTPUT';
|
||||
err.status = 502;
|
||||
throw err;
|
||||
}
|
||||
|
||||
const result = JSON.parse(parsedLine);
|
||||
if (!result.ok) {
|
||||
const err = new Error(result.message || `Helper AXA en échec (${label}).`);
|
||||
err.code = result.errorCode || 'AXA_HELPER_FAILED';
|
||||
err.status = 502;
|
||||
throw err;
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
try {
|
||||
fs.unlinkSync(scriptPath);
|
||||
} catch (_error) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function withMatriculeLock(matricule, fn) {
|
||||
const key = sanitizeFilePart(matricule || 'SYSTEM') || 'SYSTEM';
|
||||
const previous = lockMap.get(key) || Promise.resolve();
|
||||
let release;
|
||||
const current = new Promise((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
lockMap.set(key, current);
|
||||
|
||||
try {
|
||||
await previous;
|
||||
return await fn();
|
||||
} finally {
|
||||
release();
|
||||
if (lockMap.get(key) === current) {
|
||||
lockMap.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildCapturePath(scriptDir, matricule, prefix) {
|
||||
const safeMatricule = sanitizeFilePart(matricule || 'SYSTEM');
|
||||
return path.join(scriptDir, 'config', `${prefix}_${safeMatricule}.txt`);
|
||||
}
|
||||
|
||||
async function runCl063(matricule, numClient) {
|
||||
const scriptDir = path.join(AXA_ROOT, 'script_cl063');
|
||||
const capturePath = buildCapturePath(scriptDir, matricule, 'capture_runtime_cl063');
|
||||
fs.mkdirSync(path.dirname(capturePath), { recursive: true });
|
||||
fs.writeFileSync(capturePath, '', 'latin1');
|
||||
|
||||
const lines = [
|
||||
"[SENDFONC] FONC='CLEAR';",
|
||||
'[WAITBUSY]',
|
||||
`[SEND] Pos=0162; Data='cl063 /${String(numClient || '').replace(/\D/g, '')}';`,
|
||||
"[SENDFONC] FONC='ENTER';",
|
||||
`[ECRAN] Pos=1; Stop=1900; Data='${capturePath}';`,
|
||||
'[END] Par=100;'
|
||||
];
|
||||
await executeAxaScript({ scriptDir, matricule, lines, label: 'cl063' });
|
||||
const screen = readCaptureLine(capturePath);
|
||||
const info = parseCl063Screen(screen);
|
||||
return {
|
||||
...info,
|
||||
rawScreen: screen
|
||||
};
|
||||
}
|
||||
|
||||
async function runPa025(matricule, numContrat) {
|
||||
const scriptDir = path.join(AXA_ROOT, 'script_pa025');
|
||||
const capturePath = buildCapturePath(scriptDir, matricule, 'capture_runtime_pa025');
|
||||
fs.mkdirSync(path.dirname(capturePath), { recursive: true });
|
||||
fs.writeFileSync(capturePath, '', 'latin1');
|
||||
|
||||
const safeContrat = trimLeadingZeros(numContrat);
|
||||
const lines = [
|
||||
"[SENDFONC] FONC='CLEAR';",
|
||||
'[WAITBUSY]',
|
||||
`[SEND] Pos=0162; Data='pa025 ${safeContrat}';`,
|
||||
"[SENDFONC] FONC='ENTER';",
|
||||
`[ECRAN] Pos=1; Stop=1900; Data='${capturePath}';`,
|
||||
'[END] Par=100;'
|
||||
];
|
||||
await executeAxaScript({ scriptDir, matricule, lines, label: 'pa025' });
|
||||
const screen = readCaptureLine(capturePath);
|
||||
const info = parsePa025Screen(screen);
|
||||
return {
|
||||
...info,
|
||||
rawScreen: screen
|
||||
};
|
||||
}
|
||||
|
||||
async function runAc800(matricule, numPortefeuille, { includePostal = false } = {}) {
|
||||
const scriptDir = path.join(AXA_ROOT, 'script_cl063');
|
||||
const capturePath = buildCapturePath(scriptDir, matricule, 'capture_runtime_ac800');
|
||||
fs.mkdirSync(path.dirname(capturePath), { recursive: true });
|
||||
fs.writeFileSync(capturePath, '', 'latin1');
|
||||
|
||||
const portfolio = parseAgentPortfolio(numPortefeuille);
|
||||
const lines = [
|
||||
"[SENDFONC] FONC='CLEAR';",
|
||||
'[WAITBUSY]',
|
||||
`[SEND] Pos=0162; Data='AC800 ${portfolio}';`,
|
||||
"[SENDFONC] FONC='ENTER';",
|
||||
`[ECRAN] Pos=1; Stop=1900; Data='${capturePath}';`,
|
||||
"[SENDFONC] FONC='CLEAR';",
|
||||
'[WAITBUSY]',
|
||||
'[END] Par=100;'
|
||||
];
|
||||
await executeAxaScript({ scriptDir, matricule, lines, label: 'ac800' });
|
||||
const screen = readCaptureLine(capturePath);
|
||||
return {
|
||||
...parseAc800Screen(screen, includePostal),
|
||||
rawScreen: screen
|
||||
};
|
||||
}
|
||||
|
||||
async function lookupContract({ matricule, numContrat }) {
|
||||
ensureWindows();
|
||||
const safeMatricule = sanitizeFilePart(matricule || 'SYSTEM');
|
||||
return withMatriculeLock(safeMatricule, async () => {
|
||||
const pa025 = await runPa025(safeMatricule, numContrat);
|
||||
const agentDetails = await runAc800(safeMatricule, pa025.numAgent, { includePostal: true });
|
||||
return {
|
||||
numContrat: String(numContrat || '').replace(/\D/g, '').padStart(16, '0').slice(-16),
|
||||
numClient: pa025.numClient || '',
|
||||
nomClient: pa025.nomClient || '',
|
||||
adresseClient: pa025.adresseClient || '',
|
||||
codePostal: pa025.codePostal || '',
|
||||
numAgent: pa025.numAgent || '',
|
||||
codeProduit: pa025.codeProduit || '',
|
||||
nomAgent: agentDetails.nomAgent || '',
|
||||
adresseAgent: agentDetails.adresseAgent || '',
|
||||
telAgent: agentDetails.telAgent || '',
|
||||
faxAgent: agentDetails.faxAgent || '',
|
||||
postalAgent: agentDetails.postalAgent || '',
|
||||
source: 'axa_dde'
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function formatQtDate(value) {
|
||||
const raw = String(value || '').trim();
|
||||
const match = raw.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
|
||||
if (!match) return raw;
|
||||
return `${match[1]}/${match[2]}/${match[3].slice(2)}`;
|
||||
}
|
||||
|
||||
function formatDecimal(value) {
|
||||
const raw = String(value ?? '').trim();
|
||||
if (!raw) return '';
|
||||
return raw.replace(/\./g, ',');
|
||||
}
|
||||
|
||||
async function runQt550({ matricule, numContrat, totalCotisation, coutActe = '36', dateDebut, dateFin }) {
|
||||
ensureWindows();
|
||||
const safeMatricule = sanitizeFilePart(matricule || 'SYSTEM');
|
||||
return withMatriculeLock(safeMatricule, async () => {
|
||||
const scriptDir = path.join(AXA_ROOT, 'script_qt550');
|
||||
const safeContrat = trimLeadingZeros(numContrat);
|
||||
const debut = formatQtDate(dateDebut).split('/');
|
||||
const fin = formatQtDate(dateFin).split('/');
|
||||
const dd1 = padLeft(debut[0] || '', 2);
|
||||
const mm1 = padLeft(debut[1] || '', 2);
|
||||
const yy1 = padLeft((debut[2] || '').slice(-2), 2);
|
||||
const dd2 = padLeft(fin[0] || '', 2);
|
||||
const mm2 = padLeft(fin[1] || '', 2);
|
||||
const yy2 = padLeft((fin[2] || '').slice(-2), 2);
|
||||
|
||||
const lines = [
|
||||
"[SENDFONC] FONC='CLEAR';",
|
||||
'[WAITBUSY]',
|
||||
`[SEND] Pos=0162; Data='pa025 ${safeContrat}';`,
|
||||
"[SENDFONC] FONC='ENTER';",
|
||||
"[SEND] Pos=1809; Data='QT550';",
|
||||
"[SENDFONC] FONC='ENTER';",
|
||||
'[WAITBUSY]',
|
||||
"[SEND] Pos=736; Data='E';",
|
||||
"[SEND] Pos=896; Data='O';",
|
||||
"[SEND] Pos=1296; Data='M';",
|
||||
"[SEND] Pos=1348; Data='P';",
|
||||
"[SEND] Pos=1456; Data='1';",
|
||||
"[SEND] Pos=1616; Data='7';",
|
||||
`[SEND] Pos=1696; Data='${formatDecimal(coutActe)}';`,
|
||||
`[SEND] Pos=766; Data='${dd1}';`,
|
||||
`[SEND] Pos=769; Data='${mm1}';`,
|
||||
`[SEND] Pos=772; Data='${yy1}';`,
|
||||
`[SEND] Pos=791; Data='${dd2}';`,
|
||||
`[SEND] Pos=794; Data='${mm2}';`,
|
||||
`[SEND] Pos=797; Data='${yy2}';`,
|
||||
"[SENDFONC] FONC='ENTER';",
|
||||
'[WAITBUSY]',
|
||||
"[SEND] Pos=325; Data='44';",
|
||||
`[SEND] Pos=329; Data='${formatDecimal(totalCotisation)}';`,
|
||||
"[SENDFONC] FONC='ENTER';",
|
||||
'[WAITBUSY]',
|
||||
'[END] Par=100;'
|
||||
];
|
||||
|
||||
await executeAxaScript({ scriptDir, matricule: safeMatricule, lines, label: 'qt550' });
|
||||
return { ok: true };
|
||||
});
|
||||
}
|
||||
|
||||
async function getHealth({ matricule } = {}) {
|
||||
const health = {
|
||||
ok: true,
|
||||
platform: process.platform,
|
||||
helperPath: HELPER_EXE_PATH,
|
||||
helperReady: false,
|
||||
scriptRoots: {
|
||||
cl063: path.join(AXA_ROOT, 'script_cl063'),
|
||||
pa025: path.join(AXA_ROOT, 'script_pa025'),
|
||||
qt550: path.join(AXA_ROOT, 'script_qt550')
|
||||
},
|
||||
checks: [],
|
||||
errors: []
|
||||
};
|
||||
|
||||
try {
|
||||
ensureWindows();
|
||||
await ensureHelperBuilt();
|
||||
health.helperReady = fs.existsSync(HELPER_EXE_PATH);
|
||||
|
||||
Object.entries(health.scriptRoots).forEach(([name, root]) => {
|
||||
const ok = fs.existsSync(root) && fs.existsSync(path.join(root, 'ECFDDEViva.exe'));
|
||||
health.checks.push({
|
||||
target: name,
|
||||
root,
|
||||
ok
|
||||
});
|
||||
if (!ok) {
|
||||
health.errors.push(`Missing AXA script root or ECFDDEViva.exe for ${name}: ${root}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (matricule) {
|
||||
health.lockKey = sanitizeFilePart(matricule);
|
||||
health.lockBusy = lockMap.has(health.lockKey);
|
||||
}
|
||||
} catch (error) {
|
||||
health.ok = false;
|
||||
health.errors.push(error.message);
|
||||
}
|
||||
|
||||
if (health.errors.length) health.ok = false;
|
||||
return health;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
lookupContract,
|
||||
runQt550,
|
||||
getHealth,
|
||||
__private: {
|
||||
parsePa025Screen,
|
||||
parseCl063Screen,
|
||||
parseAc800Screen,
|
||||
screenSlice
|
||||
}
|
||||
};
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
const { db } = require('../db/db-connect');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
// ===== 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
|
||||
};
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,95 +0,0 @@
|
|||
const path = require('path');
|
||||
const winston = require('winston');
|
||||
require('winston-daily-rotate-file');
|
||||
|
||||
const fileTransport = new winston.transports.DailyRotateFile({
|
||||
filename: path.join(process.cwd(), 'logs/easytransport-%DATE%.log'),
|
||||
datePattern: 'DDMMYYYY',
|
||||
zippedArchive: true,
|
||||
maxSize: '20m',
|
||||
maxFiles: '14d'
|
||||
});
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp({ format: 'DD-MM-YYYY HH:mm:ss' }),
|
||||
winston.format.printf(info => {
|
||||
const message = typeof info.message === 'object' ?
|
||||
JSON.stringify(info.message, null, 2) :
|
||||
info.message;
|
||||
return `[${info.level}][${info.timestamp}] ${message}`;
|
||||
})
|
||||
),
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.printf(info => {
|
||||
const message = typeof info.message === 'object' ?
|
||||
JSON.stringify(info.message, null, 2) :
|
||||
info.message;
|
||||
return `[${info.level}][${info.timestamp}] ${message}`;
|
||||
})
|
||||
)
|
||||
}),
|
||||
fileTransport
|
||||
]
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logger.level = 'debug';
|
||||
}
|
||||
|
||||
function toSerializableError(error) {
|
||||
if (!error || typeof error !== 'object') return null;
|
||||
const out = {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
status: error.status,
|
||||
stack: error.stack,
|
||||
details: error.details,
|
||||
data: error.data
|
||||
};
|
||||
return Object.fromEntries(Object.entries(out).filter(([, value]) => value !== undefined));
|
||||
}
|
||||
|
||||
function toSerializableMeta(meta) {
|
||||
if (meta === null || meta === undefined) return null;
|
||||
if (meta instanceof Error) return toSerializableError(meta);
|
||||
if (typeof meta === 'object') return meta;
|
||||
return { value: String(meta) };
|
||||
}
|
||||
|
||||
function safeStringify(value) {
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch (_error) {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
log: function (level, message, meta = null, matricule = null) {
|
||||
const baseError = message instanceof Error ? toSerializableError(message) : null;
|
||||
const extraMeta = toSerializableMeta(meta);
|
||||
const mergedMeta = baseError || extraMeta ? { ...(baseError || {}), ...(extraMeta || {}) } : null;
|
||||
|
||||
let baseMessage = '';
|
||||
if (typeof message === 'string') baseMessage = message;
|
||||
else if (message instanceof Error) baseMessage = message.message || message.name || 'Erreur';
|
||||
else if (message && typeof message === 'object') baseMessage = 'Log';
|
||||
else if (message !== undefined && message !== null) baseMessage = String(message);
|
||||
else baseMessage = 'Log';
|
||||
|
||||
const formattedMessage = mergedMeta && Object.keys(mergedMeta).length
|
||||
? `${baseMessage} | ${safeStringify(mergedMeta)}`
|
||||
: baseMessage;
|
||||
|
||||
logger.log({
|
||||
level,
|
||||
message: matricule ? `[${matricule}] ${formattedMessage}` : formattedMessage,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function resolveWorkspaceRoot() {
|
||||
if (process.env.ADV_WORKSPACE_ROOT) {
|
||||
return path.resolve(process.env.ADV_WORKSPACE_ROOT);
|
||||
}
|
||||
|
||||
if (process.pkg) {
|
||||
const exeRoot = path.dirname(process.execPath);
|
||||
const nestedEcoleRoot = path.join(exeRoot, 'ecole');
|
||||
|
||||
if (!fs.existsSync(path.join(exeRoot, 'src')) && fs.existsSync(path.join(nestedEcoleRoot, 'src'))) {
|
||||
return nestedEcoleRoot;
|
||||
}
|
||||
|
||||
return exeRoot;
|
||||
}
|
||||
|
||||
return path.resolve(__dirname, '..', '..');
|
||||
}
|
||||
|
||||
const workspaceRoot = resolveWorkspaceRoot();
|
||||
|
||||
function resolveWorkspacePath(...parts) {
|
||||
return path.resolve(workspaceRoot, ...parts);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
workspaceRoot,
|
||||
resolveWorkspacePath
|
||||
};
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,24 +0,0 @@
|
|||
Option Explicit
|
||||
On Error Resume Next
|
||||
ExempleMacroExcel
|
||||
|
||||
Sub ExempleMacroExcel()
|
||||
|
||||
Dim ApplicationExcel
|
||||
Dim ClasseurExcel
|
||||
Set ApplicationExcel = CreateObject("Excel.Application")
|
||||
|
||||
Dim WshShell, strCurDir
|
||||
Set WshShell = CreateObject("WScript.Shell")
|
||||
strCurDir = WshShell.CurrentDirectory
|
||||
|
||||
|
||||
Set ClasseurExcel = ApplicationExcel.Workbooks.Open( strCurDir & "\vbs\script_cl063\Connexion.xlsm")
|
||||
ApplicationExcel.Visible = False
|
||||
ApplicationExcel.Run "CL063_AC800_sub" 'va lancer la macro
|
||||
ApplicationExcel.Quit
|
||||
|
||||
Set ClasseurExcel = Nothing
|
||||
Set ApplicationExcel = Nothing
|
||||
|
||||
End Sub
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,24 +0,0 @@
|
|||
Option Explicit
|
||||
On Error Resume Next
|
||||
ExempleMacroExcel
|
||||
|
||||
Sub ExempleMacroExcel()
|
||||
|
||||
Dim ApplicationExcel
|
||||
Dim ClasseurExcel
|
||||
Set ApplicationExcel = CreateObject("Excel.Application")
|
||||
|
||||
Dim WshShell, strCurDir
|
||||
Set WshShell = CreateObject("WScript.Shell")
|
||||
strCurDir = WshShell.CurrentDirectory
|
||||
|
||||
|
||||
Set ClasseurExcel = ApplicationExcel.Workbooks.Open( strCurDir & "\vbs\script_pa025\attestation.xlsm")
|
||||
ApplicationExcel.Visible = False
|
||||
ApplicationExcel.Run "PA025_AC800_sub" 'va lancer la macro
|
||||
ApplicationExcel.Quit
|
||||
|
||||
Set ClasseurExcel = Nothing
|
||||
Set ApplicationExcel = Nothing
|
||||
|
||||
End Sub
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,24 +0,0 @@
|
|||
Option Explicit
|
||||
On Error Resume Next
|
||||
ExempleMacroExcel
|
||||
|
||||
Sub ExempleMacroExcel()
|
||||
|
||||
Dim ApplicationExcel
|
||||
Dim ClasseurExcel
|
||||
Set ApplicationExcel = CreateObject("Excel.Application")
|
||||
|
||||
Dim WshShell, strCurDir
|
||||
Set WshShell = CreateObject("WScript.Shell")
|
||||
strCurDir = WshShell.CurrentDirectory
|
||||
|
||||
|
||||
Set ClasseurExcel = ApplicationExcel.Workbooks.Open( strCurDir & "\vbs\script_qt550\Bordereau.xlsm")
|
||||
ApplicationExcel.Visible = False
|
||||
ApplicationExcel.Run "QT550_sub" 'va lancer la macro
|
||||
ApplicationExcel.Quit
|
||||
|
||||
Set ClasseurExcel = Nothing
|
||||
Set ApplicationExcel = Nothing
|
||||
|
||||
End Sub
|
||||
|
|
@ -1,369 +0,0 @@
|
|||
<div class="section">
|
||||
<h4 class="center-align">Advalorem</h4>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="section">
|
||||
<nav>
|
||||
<div class="nav-wrapper">
|
||||
<ul class="left" id="advaloNavSelect">
|
||||
<li class="active"><a href="#" data-target="advalo-tab-accueil">Accueil</a></li>
|
||||
<li><a href="#" data-target="advalo-tab-ponctuel">Hors grille</a></li>
|
||||
<li><a href="#" data-target="advalo-tab-facturation">Facturation</a></li>
|
||||
<li><a href="#" data-target="advalo-tab-historique">Historique</a></li>
|
||||
<li><a href="#" data-target="advalo-tab-cumul">Cumul</a></li>
|
||||
<li><a href="#" data-target="advalo-tab-reporting">Reporting</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div id="advalo-tab-accueil" class="section advalo-panel">
|
||||
<div class="card-panel">
|
||||
<h6>Advalorem est intégré à EasyTransport.</h6>
|
||||
<p>Parité V1: Hors grille (ponctuel/périodique), Facturation, Historique, Cumul, Reporting et documents.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="advalo-tab-ponctuel" class="section advalo-panel" style="display:none;">
|
||||
<form id="advalo-ponctuel-form" class="col s12">
|
||||
<div class="chapter"><i class="material-icons">settings</i> Type de facturation</div>
|
||||
<div class="row" style="margin-top:12px;">
|
||||
<div class="col s12 m6">
|
||||
<label><input name="p-typeFacturation" type="radio" value="ponctuel" checked /><span>Transport ponctuel</span></label>
|
||||
</div>
|
||||
<div class="col s12 m6">
|
||||
<label><input name="p-typeFacturation" type="radio" value="periodique" /><span>Bordereau périodique (sans ligne à ligne)</span></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chapter"><i class="material-icons">business</i> Infos client / intermédiaire</div>
|
||||
<div class="row" style="margin-top:12px;">
|
||||
<div class="input-field col s12 m4">
|
||||
<input id="p-numContrat" maxlength="16" required>
|
||||
<label for="p-numContrat">N° Contrat (16 chiffres)</label>
|
||||
</div>
|
||||
<div class="col s12 m2" style="padding-top:16px;">
|
||||
<button id="btn-load-contract" type="button" class="btn waves-effect waves-light">Charger</button>
|
||||
</div>
|
||||
<div class="input-field col s12 m2"><input id="p-numClient"><label for="p-numClient">N° Client</label></div>
|
||||
<div class="input-field col s12 m4"><input id="p-nomClient"><label for="p-nomClient">Nom Client</label></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="input-field col s12 m3"><input id="p-numAgent"><label for="p-numAgent">N° Portefeuille</label></div>
|
||||
<div class="input-field col s12 m3"><input id="p-nomAgent"><label for="p-nomAgent">Intermédiaire</label></div>
|
||||
</div>
|
||||
|
||||
<div class="chapter"><i class="material-icons">local_shipping</i> Infos transport</div>
|
||||
<div class="row" style="margin-top:12px;">
|
||||
<div class="input-field col s12 m4"><input id="p-marchandise" required><label for="p-marchandise">Marchandise *</label></div>
|
||||
<div class="input-field col s12 m4"><input id="p-depart" required><label for="p-depart">Lieu de départ *</label></div>
|
||||
<div class="input-field col s12 m4"><input id="p-arrivee" required><label for="p-arrivee">Lieu d'arrivée *</label></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="input-field col s12 m3"><input id="p-dateDebut" class="advalo-date" autocomplete="off" required><label for="p-dateDebut">Date début</label></div>
|
||||
<div class="input-field col s12 m3"><input id="p-dateFin" class="advalo-date" autocomplete="off" required><label for="p-dateFin">Date fin</label></div>
|
||||
<div class="col s12 m6">
|
||||
<label class="rc-field-label">Mode(s) de transport *</label>
|
||||
<p>
|
||||
<label><input type="checkbox" class="filled-in p-mode-check" value="Terrestre"><span>Terrestre</span></label>
|
||||
<label style="margin-left:16px;"><input type="checkbox" class="filled-in p-mode-check" value="Aérien"><span>Aérien</span></label>
|
||||
<label style="margin-left:16px;"><input type="checkbox" class="filled-in p-mode-check" value="Fluvial"><span>Fluvial</span></label>
|
||||
<label style="margin-left:16px;"><input type="checkbox" class="filled-in p-mode-check" value="Maritime"><span>Maritime</span></label>
|
||||
<label style="margin-left:16px;"><input type="checkbox" class="filled-in p-mode-check" value="Postal"><span>Postal</span></label>
|
||||
</p>
|
||||
<input id="p-mode" type="hidden">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chapter"><i class="material-icons">euro</i> Calcul cotisation</div>
|
||||
<div class="row" style="margin-top:12px;">
|
||||
<div class="input-field col s12 m2"><input id="p-capital" required><label for="p-capital">Valeur assurée *</label></div>
|
||||
<div class="input-field col s12 m2"><input id="p-taux" value="0.3" required><label for="p-taux">Taux (%) *</label></div>
|
||||
<div class="input-field col s12 m2"><input id="p-primeMin" value="15" required><label for="p-primeMin">Prime mini *</label></div>
|
||||
<div class="input-field col s12 m2"><input id="p-coutActe" value="36"><label for="p-coutActe">Coût acte</label></div>
|
||||
<div class="input-field col s12 m2"><input id="p-cotisationHT" required><label for="p-cotisationHT">Cotisation HT *</label></div>
|
||||
<div class="input-field col s12 m2"><input id="p-cotisationTTC" required><label for="p-cotisationTTC">Cotisation TTC *</label></div>
|
||||
<input id="p-tarif" type="hidden">
|
||||
</div>
|
||||
|
||||
<div class="chapter"><i class="material-icons">check_circle</i> Enregistrement / facturation</div>
|
||||
<div class="row" style="margin-top:12px;">
|
||||
<div class="col s12 m4">
|
||||
<label><input type="checkbox" id="p-facturer" /><span>Facturer immédiatement (pont AXA)</span></label>
|
||||
</div>
|
||||
<div class="col s12 m4"><span id="p-form-error" class="helper-text error" style="display:block;"></span></div>
|
||||
<div class="col s12 m4 right-align">
|
||||
<button class="btn waves-effect waves-light" type="submit">Enregistrer</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="advalo-tab-facturation" class="section advalo-panel" style="display:none;">
|
||||
<div class="card-panel">
|
||||
<div class="row">
|
||||
<div class="input-field col s12 m2"><input id="f-numContrat" maxlength="16"><label for="f-numContrat">N° Contrat (16)</label></div>
|
||||
<div class="input-field col s12 m2"><input id="f-dateDebut" class="advalo-date" autocomplete="off"><label for="f-dateDebut">Date début</label></div>
|
||||
<div class="input-field col s12 m2"><input id="f-dateFin" class="advalo-date" autocomplete="off"><label for="f-dateFin">Date fin</label></div>
|
||||
<div class="input-field col s12 m2">
|
||||
<select id="f-sourceMode">
|
||||
<option value="hors_grille" selected>Hors grille</option>
|
||||
<option value="mixte">Mixte</option>
|
||||
</select>
|
||||
<label>Source facturation</label>
|
||||
</div>
|
||||
<div class="col s12 m2" style="padding-top:20px;"><button id="btn-f-load" class="btn waves-effect waves-light">Charger lignes</button></div>
|
||||
<div class="col s12 m2" style="padding-top:20px;"><button id="btn-facturer" class="btn waves-effect waves-light">Facturer</button></div>
|
||||
</div>
|
||||
|
||||
<div class="row" id="facturation-client-agent-row" style="display:none; margin-bottom:0;">
|
||||
<div class="col s12 m6"><b>Client:</b> <span id="f-client-recap">-</span></div>
|
||||
<div class="col s12 m6"><b>Intermédiaire:</b> <span id="f-agent-recap">-</span></div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top:8px; margin-bottom:0;">
|
||||
<div class="col s12 m6">
|
||||
<label><input class="with-gap" name="f-include-details" type="radio" value="true" checked /><span>Inclure le détail des transports dans l’avenant</span></label>
|
||||
</div>
|
||||
<div class="col s12 m6">
|
||||
<label><input class="with-gap" name="f-include-details" type="radio" value="false" /><span>Ne pas inclure le détail des transports</span></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top:10px; margin-bottom:0;">
|
||||
<div class="col s12 m6"><button id="btn-f-remove" class="btn red darken-3 waves-effect waves-light">Retirer de la liste</button></div>
|
||||
<div class="col s12 m6 right-align"><span id="f-selection-info" class="grey-text text-darken-1"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="striped responsive-table">
|
||||
<thead>
|
||||
<tr><th></th><th>Source</th><th>Demande</th><th>Client</th><th>Contrat</th><th>Date début</th><th>Tarif</th><th>Facturation</th></tr>
|
||||
</thead>
|
||||
<tbody id="facturation-body"></tbody>
|
||||
</table>
|
||||
<div id="advalo-loading-facturation" class="center-align advalo-loader-wrap" style="display:none; margin-top:16px;"><div class="advalo-ring-loader"></div></div>
|
||||
</div>
|
||||
|
||||
<div id="advalo-tab-historique" class="section advalo-panel" style="display:none;">
|
||||
<div class="card-panel">
|
||||
<div class="row">
|
||||
<div class="input-field col s12 m2"><input id="h-numClient"><label for="h-numClient">N° Client</label></div>
|
||||
<div class="input-field col s12 m2"><input id="h-numContrat" maxlength="16"><label for="h-numContrat">N° Contrat</label></div>
|
||||
<div class="input-field col s12 m2"><input id="h-dateDebut" class="advalo-date" autocomplete="off"><label for="h-dateDebut">Date début</label></div>
|
||||
<div class="input-field col s12 m2"><input id="h-dateFin" class="advalo-date" autocomplete="off"><label for="h-dateFin">Date fin</label></div>
|
||||
<div class="input-field col s12 m2">
|
||||
<select id="h-sourceType">
|
||||
<option value="all" selected>Toutes</option>
|
||||
<option value="deleguee">Grille déléguée</option>
|
||||
<option value="hors_grille">Hors grille</option>
|
||||
</select>
|
||||
<label>Source</label>
|
||||
</div>
|
||||
<div class="input-field col s12 m2">
|
||||
<select id="h-statut">
|
||||
<option value="all" selected>Tous</option>
|
||||
<option value="facture">Facturé</option>
|
||||
<option value="non_facture">Non facturé</option>
|
||||
</select>
|
||||
<label>Facturé / Non facturé</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="margin-bottom:0;">
|
||||
<div class="col s12 m6">
|
||||
<button id="btn-h-search" class="btn waves-effect waves-light">Rechercher</button>
|
||||
<button id="btn-h-export" class="btn waves-effect waves-light" style="margin-left:8px;">Exporter CSV</button>
|
||||
</div>
|
||||
<div class="col s12 m6 right-align" style="padding-top:8px;">
|
||||
<button id="btn-h-prev" class="btn indigo darken-4 white-text">Précédent</button>
|
||||
<span id="h-page-indicator">Page 1 / 1</span>
|
||||
<button id="btn-h-next" class="btn indigo darken-4 white-text">Suivant</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="striped responsive-table">
|
||||
<thead>
|
||||
<tr><th></th><th>Source</th><th>Demande</th><th>Client</th><th>Contrat</th><th>Date début</th><th>Date fin</th><th>Tarif</th><th>Statut</th><th>Actions</th></tr>
|
||||
</thead>
|
||||
<tbody id="historique-body"></tbody>
|
||||
</table>
|
||||
<div id="advalo-loading-historique" class="center-align advalo-loader-wrap" style="display:none; margin-top:16px;"><div class="advalo-ring-loader"></div></div>
|
||||
</div>
|
||||
|
||||
<div id="advalo-tab-cumul" class="section advalo-panel" style="display:none;">
|
||||
<div class="card-panel">
|
||||
<div class="row">
|
||||
<div class="input-field col s12 m3"><input id="c-numContrat" maxlength="16"><label for="c-numContrat">N° Contrat</label></div>
|
||||
<div class="input-field col s12 m2"><input id="c-numClient"><label for="c-numClient">N° Client</label></div>
|
||||
<div class="input-field col s12 m2"><input id="c-dateDebut" class="advalo-date" autocomplete="off"><label for="c-dateDebut">Date début</label></div>
|
||||
<div class="input-field col s12 m2"><input id="c-dateFin" class="advalo-date" autocomplete="off"><label for="c-dateFin">Date fin</label></div>
|
||||
<div class="col s12 m3 right-align" style="padding-top:20px;"><button id="btn-cumul" class="btn waves-effect waves-light">Calculer</button></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col s12 m3"><div class="card-panel">Total Advalo: <b id="k-total-advalo">0</b></div></div>
|
||||
<div class="col s12 m3"><div class="card-panel">Total Facturé: <b id="k-total-facture">0</b></div></div>
|
||||
<div class="col s12 m3"><div class="card-panel">Total Non facturé: <b id="k-total-nonfacture">0</b></div></div>
|
||||
<div class="col s12 m3"><div class="card-panel">Lignes: <b id="k-total-lignes">0</b></div></div>
|
||||
</div>
|
||||
<div class="row" style="margin-bottom:8px;"><div class="col s12 right-align"><button id="btn-c-prev" class="btn indigo darken-4 white-text">Précédent</button> <span id="c-page-indicator">Page 1 / 1</span> <button id="btn-c-next" class="btn indigo darken-4 white-text">Suivant</button></div></div>
|
||||
<table class="striped responsive-table">
|
||||
<thead>
|
||||
<tr><th>Contrat</th><th>Client</th><th>Région</th><th>DPT</th><th>Souscripteur</th><th>Total Advalo</th><th>Total Facturé</th><th>Total Non facturé</th><th>Actions</th></tr>
|
||||
</thead>
|
||||
<tbody id="cumul-body"></tbody>
|
||||
</table>
|
||||
<div id="advalo-loading-cumul" class="center-align advalo-loader-wrap" style="display:none; margin-top:16px;"><div class="advalo-ring-loader"></div></div>
|
||||
</div>
|
||||
|
||||
<div id="advalo-tab-reporting" class="section advalo-panel" style="display:none;">
|
||||
<div class="card-panel">
|
||||
<div class="row">
|
||||
<div class="input-field col s12 m2"><input id="r-numClient"><label for="r-numClient">N° Client</label></div>
|
||||
<div class="input-field col s12 m2"><input id="r-numContrat" maxlength="16"><label for="r-numContrat">N° Contrat</label></div>
|
||||
<div class="input-field col s12 m2"><input id="r-souscripteur"><label for="r-souscripteur">Souscripteur</label></div>
|
||||
<div class="input-field col s12 m2"><input id="r-region"><label for="r-region">Région</label></div>
|
||||
<div class="input-field col s12 m2"><input id="r-dateDebut" class="advalo-date" autocomplete="off"><label for="r-dateDebut">Date début</label></div>
|
||||
<div class="input-field col s12 m2"><input id="r-dateFin" class="advalo-date" autocomplete="off"><label for="r-dateFin">Date fin</label></div>
|
||||
</div>
|
||||
<div class="row" style="margin-bottom:0;">
|
||||
<div class="input-field col s12 m2"><input id="r-actorMatricule"><label for="r-actorMatricule">Matricule acteur</label></div>
|
||||
<div class="input-field col s12 m2"><input id="r-actorNom"><label for="r-actorNom">Nom acteur</label></div>
|
||||
<div class="input-field col s12 m2">
|
||||
<select id="r-actionType">
|
||||
<option value="" selected>Toutes actions</option>
|
||||
<option value="create">Création</option>
|
||||
<option value="update">Modification</option>
|
||||
<option value="delete_soft">Suppression</option>
|
||||
<option value="facturation_batch">Facturation</option>
|
||||
<option value="doc_avenant">Doc avenant</option>
|
||||
<option value="doc_attestation">Doc attestation</option>
|
||||
<option value="lookup_contrat">Lookup contrat</option>
|
||||
</select>
|
||||
<label>Action</label>
|
||||
</div>
|
||||
<div class="input-field col s12 m2">
|
||||
<select id="r-sourceType">
|
||||
<option value="all" selected>Toutes</option>
|
||||
<option value="deleguee">Grille déléguée</option>
|
||||
<option value="hors_grille">Hors grille</option>
|
||||
</select>
|
||||
<label>Source</label>
|
||||
</div>
|
||||
<div class="input-field col s12 m2">
|
||||
<select id="r-statut">
|
||||
<option value="all" selected>Tous</option>
|
||||
<option value="facture">Facturé</option>
|
||||
<option value="non_facture">Non facturé</option>
|
||||
</select>
|
||||
<label>Facturé / Non facturé</label>
|
||||
</div>
|
||||
<div class="input-field col s12 m2">
|
||||
<select id="r-sort">
|
||||
<option value="totalAdvalo" selected>Total Advalo</option>
|
||||
<option value="totalFacture">Total Facturé</option>
|
||||
<option value="totalNonFacture">Total Non facturé</option>
|
||||
<option value="numContrat">Contrat</option>
|
||||
</select>
|
||||
<label>Trier par</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="margin-bottom:0;">
|
||||
<div class="input-field col s12 m2">
|
||||
<select id="r-order"><option value="desc" selected>Décroissant</option><option value="asc">Croissant</option></select>
|
||||
<label>Ordre</label>
|
||||
</div>
|
||||
<div class="col s12 m10 right-align" style="padding-top:20px;"><button id="btn-reporting" class="btn waves-effect waves-light">Rafraîchir</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col s12 m3"><div class="card-panel">Total Advalo: <b id="r-total-advalo">0</b></div></div>
|
||||
<div class="col s12 m3"><div class="card-panel">Total Facturé: <b id="r-total-facture">0</b></div></div>
|
||||
<div class="col s12 m3"><div class="card-panel">Total Non facturé: <b id="r-total-nonfacture">0</b></div></div>
|
||||
<div class="col s12 m3"><div class="card-panel">Lignes source: <b id="r-total-lignes">0</b></div></div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-bottom:8px;"><div class="col s12 right-align"><button id="btn-r-prev" class="btn indigo darken-4 white-text">Précédent</button> <span id="r-page-indicator">Page 1 / 1</span> <button id="btn-r-next" class="btn indigo darken-4 white-text">Suivant</button></div></div>
|
||||
|
||||
<table class="striped responsive-table">
|
||||
<thead>
|
||||
<tr><th>Contrat</th><th>Client</th><th>Région</th><th>Souscripteur</th><th>Total Advalo</th><th>Total Facturé</th><th>Total Non facturé</th><th>Lignes</th></tr>
|
||||
</thead>
|
||||
<tbody id="reporting-body"></tbody>
|
||||
</table>
|
||||
|
||||
<div class="card-panel" style="margin-top:20px;">
|
||||
<h6>Qui a fait quoi</h6>
|
||||
<table class="striped responsive-table">
|
||||
<thead><tr><th>Matricule</th><th>Acteur</th><th>Action</th><th>Occurrences</th></tr></thead>
|
||||
<tbody id="reporting-actors-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="advalo-loading-reporting" class="center-align advalo-loader-wrap" style="display:none; margin-top:16px;"><div class="advalo-ring-loader"></div></div>
|
||||
</div>
|
||||
|
||||
<script src="/js/advalo-module.js"></script>
|
||||
|
||||
<div id="advalo-confirm-modal" class="modal">
|
||||
<div class="modal-content"><h5>Confirmation ponctuel</h5><p id="advalo-confirm-summary" class="grey-text text-darken-2"></p></div>
|
||||
<div class="modal-footer">
|
||||
<a href="#!" id="advalo-confirm-avenant" class="modal-close waves-effect waves-green btn">Avenant</a>
|
||||
<a href="#!" id="advalo-confirm-attestation" class="modal-close waves-effect waves-light btn">Attestation</a>
|
||||
<a href="#!" id="advalo-confirm-close" class="modal-close waves-effect btn-flat">Fermer</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="advalo-batch-modal" class="modal">
|
||||
<div class="modal-content"><h5>Confirmation facturation</h5><p id="advalo-batch-summary" class="grey-text text-darken-2"></p></div>
|
||||
<div class="modal-footer">
|
||||
<a href="#!" id="advalo-batch-avenant" class="modal-close waves-effect waves-green btn">Avenant périodique</a>
|
||||
<a href="#!" id="advalo-batch-close" class="modal-close waves-effect btn-flat">Fermer</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="advalo-edit-modal" class="modal modal-fixed-footer">
|
||||
<div class="modal-content">
|
||||
<h5>Modifier la demande hors grille</h5>
|
||||
<input type="hidden" id="e-id">
|
||||
<div class="row" style="margin-top:12px;">
|
||||
<div class="input-field col s12 m6"><input id="e-marchandise"><label for="e-marchandise">Marchandise *</label></div>
|
||||
<div class="col s12 m6">
|
||||
<label class="rc-field-label">Mode(s) de transport *</label>
|
||||
<p>
|
||||
<label><input type="checkbox" class="filled-in e-mode-check" value="Terrestre"><span>Terrestre</span></label>
|
||||
<label style="margin-left:16px;"><input type="checkbox" class="filled-in e-mode-check" value="Aérien"><span>Aérien</span></label>
|
||||
<label style="margin-left:16px;"><input type="checkbox" class="filled-in e-mode-check" value="Fluvial"><span>Fluvial</span></label>
|
||||
<label style="margin-left:16px;"><input type="checkbox" class="filled-in e-mode-check" value="Maritime"><span>Maritime</span></label>
|
||||
<label style="margin-left:16px;"><input type="checkbox" class="filled-in e-mode-check" value="Postal"><span>Postal</span></label>
|
||||
</p>
|
||||
<input id="e-mode" type="hidden">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="input-field col s12 m6"><input id="e-depart"><label for="e-depart">Lieu de départ *</label></div>
|
||||
<div class="input-field col s12 m6"><input id="e-arrivee"><label for="e-arrivee">Lieu d'arrivée *</label></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="input-field col s12 m6"><input id="e-dateDebut" class="advalo-date" autocomplete="off"><label for="e-dateDebut">Date début *</label></div>
|
||||
<div class="input-field col s12 m6"><input id="e-dateFin" class="advalo-date" autocomplete="off"><label for="e-dateFin">Date fin *</label></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="input-field col s12 m2"><input id="e-capital" required><label for="e-capital">Valeur assurée *</label></div>
|
||||
<div class="input-field col s12 m2"><input id="e-taux" required><label for="e-taux">Taux (%) *</label></div>
|
||||
<div class="input-field col s12 m2"><input id="e-primeMin" required><label for="e-primeMin">Prime mini *</label></div>
|
||||
<div class="input-field col s12 m2"><input id="e-coutActe"><label for="e-coutActe">Coût acte</label></div>
|
||||
<div class="input-field col s12 m2"><input id="e-cotisationHT" required><label for="e-cotisationHT">Cotisation HT *</label></div>
|
||||
<div class="input-field col s12 m2"><input id="e-cotisationTTC" required><label for="e-cotisationTTC">Cotisation TTC *</label></div>
|
||||
<input id="e-tarif" type="hidden">
|
||||
</div>
|
||||
<span id="e-form-error" class="helper-text error" style="display:block;"></span>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#!" id="advalo-edit-save" class="waves-effect waves-green btn">Valider</a>
|
||||
<a href="#!" class="modal-close waves-effect btn-flat">Annuler</a>
|
||||
</div>
|
||||
</div>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,13 @@
|
|||
# Ignore files type
|
||||
*.log
|
||||
*.env
|
||||
*.exe
|
||||
*.wbk
|
||||
*.cmd
|
||||
*~*
|
||||
|
||||
# Ignore directory
|
||||
**/vbs/
|
||||
**/node_modules/
|
||||
**/logs/
|
||||
**/.vscode/
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
Description
|
||||
EasyTransport est une application en cours de transition technologique, destinée à la plateforme AxA IARD Transport. Le backend est écrit en Node.js avec une base de données embarquée PocketBase, et le frontend utilise EJS et Materialize CSS. L'application propose divers modules tels que la tarification, la génération de contrats à partir de formulaires, l'authentification via JWT, et la génération d'attestations.
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -5,17 +5,13 @@
|
|||
"main": "./src/server.js",
|
||||
"scripts": {
|
||||
"start": "nodemon ./src/server.js",
|
||||
"db:start": "node ./scripts/db-start.js",
|
||||
"db:bootstrap": "node ./scripts/db-bootstrap-local.js",
|
||||
"db:bootstrap:reset": "node ./scripts/db-bootstrap-local.js --reset",
|
||||
"advalo:migrate": "node ./scripts/advalo-migrate-v1-to-v2.js --reset",
|
||||
"advalo:bench": "node ./scripts/advalo-bench.js",
|
||||
"advalo:axa-smoke": "node ./scripts/axa-smoke.js",
|
||||
"build": "pkg ./src/server.js -o EasyTransport",
|
||||
"test": "jest"
|
||||
},
|
||||
"pkg": {
|
||||
"assets": ["public/**"]
|
||||
"assets": [
|
||||
"public/**"
|
||||
]
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "cyril.ducaffy@axa.fr",
|
||||
|
|
@ -23,7 +19,6 @@
|
|||
"dependencies": {
|
||||
"axios": "^1.7.2",
|
||||
"cjs": "^0.0.11",
|
||||
"csv-parse": "^5.5.6",
|
||||
"docxtemplater": "^3.46.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"ejs": "^3.1.9",
|
||||
|
|
@ -36,7 +31,6 @@
|
|||
"numeral": "^2.0.6",
|
||||
"pizzip": "^3.1.6",
|
||||
"pocketbase": "^0.15.3",
|
||||
"xlsx": "^0.18.5",
|
||||
"winston": "^3.13.0",
|
||||
"winston-daily-rotate-file": "^4.7.1"
|
||||
},
|
||||
|
|
@ -1,8 +1,5 @@
|
|||
body {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
background-color: white;
|
||||
color: black;
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
|
|
@ -186,17 +183,6 @@ hr.form {
|
|||
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 {
|
||||
width: 80%;
|
||||
max-height: 80%;
|
||||
|
|
@ -240,63 +226,6 @@ hr.form {
|
|||
background: #26a69a;
|
||||
}
|
||||
|
||||
#advaloNavSelect .active a {
|
||||
background: #26a69a;
|
||||
}
|
||||
|
||||
#advaloNavSelect li a {
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.advalo-panel .input-field > label {
|
||||
color: #1a237e !important;
|
||||
transition: transform .18s ease, color .18s ease, font-size .18s ease;
|
||||
}
|
||||
|
||||
.advalo-panel .input-field > label.active {
|
||||
transform: translateY(-24px) scale(0.82) !important;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.advalo-loader-wrap {
|
||||
min-height: 46px;
|
||||
}
|
||||
|
||||
.advalo-ring-loader {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: 3px solid rgba(0, 0, 139, 0.18);
|
||||
border-top-color: #00008b;
|
||||
border-right-color: #26a69a;
|
||||
border-radius: 50%;
|
||||
margin: 0 auto;
|
||||
animation: advalo-spin 0.9s cubic-bezier(0.5, 0.1, 0.5, 0.9) infinite;
|
||||
}
|
||||
|
||||
.advalo-cumul-hist,
|
||||
.advalo-cumul-fact {
|
||||
margin-right: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.advalo-panel .btn,
|
||||
.advalo-panel .btn-flat {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
@keyframes advalo-spin {
|
||||
0% {
|
||||
transform: rotate(0deg) scale(1);
|
||||
}
|
||||
40% {
|
||||
transform: rotate(180deg) scale(1.04);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.border {
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
|
|
@ -479,110 +408,6 @@ a.grille-garanties:hover{
|
|||
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 {
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
|
@ -687,49 +512,3 @@ a.grille-garanties:hover{
|
|||
@keyframes l13 {
|
||||
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);
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in New Issue