personnal/ecole/public/js/rc-sync-utils.js

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