/** * ═══════════════════════════════════════════════════════════════════════════ * 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} */ 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é "${fieldName}" qui impacte le calcul du tarif.

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 = ` `; 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);