(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 '