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