RC UI: uniformiser labels flottants et fiabiliser blocages projet/tarif
This commit is contained in:
parent
4927d22387
commit
5338f682af
|
|
@ -458,6 +458,37 @@ a.grille-garanties:hover{
|
|||
text-align: left;
|
||||
}
|
||||
|
||||
.rc-has-floating-label {
|
||||
position: relative;
|
||||
margin-top: 1.8rem;
|
||||
}
|
||||
|
||||
.rc-has-floating-label .rc-field-label.rc-floating-label {
|
||||
position: absolute;
|
||||
top: 0.95rem;
|
||||
left: 0.75rem;
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: #7f8c8d;
|
||||
pointer-events: none;
|
||||
transition: top .18s ease, font-size .18s ease, color .18s ease;
|
||||
background: #fff;
|
||||
padding: 0 4px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.rc-has-floating-label .rc-field-label.rc-floating-label.active {
|
||||
top: -0.55rem;
|
||||
font-size: .78rem;
|
||||
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));
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@ window.initSubmenuForm = initSubmenuForm;// Module IIFE pour éviter la pollutio
|
|||
let hasSavedGrilleData = false; // évite d'écraser une grille déjà enregistrée
|
||||
let rcProjetGuard = null;
|
||||
|
||||
function syncRCFloatingLabels() {
|
||||
if (window.RCValidationUtils && typeof window.RCValidationUtils.syncFloatingLabels === 'function') {
|
||||
window.RCValidationUtils.syncFloatingLabels(document);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialisation des tag pour select
|
||||
var tagAnimauxVivants = false;
|
||||
var tagMultimodal = false;
|
||||
|
|
@ -90,11 +96,13 @@ window.initSubmenuForm = initSubmenuForm;// Module IIFE pour éviter la pollutio
|
|||
}
|
||||
updatePrimeReferenceRow();
|
||||
setupRCSafeValidation();
|
||||
syncRCFloatingLabels();
|
||||
updateSubmitButtonState('projetForm');
|
||||
|
||||
setTimeout(() => {
|
||||
updatePrimeReferenceRow();
|
||||
if (rcProjetGuard) rcProjetGuard.refresh();
|
||||
syncRCFloatingLabels();
|
||||
}, 350);
|
||||
}
|
||||
|
||||
|
|
@ -251,7 +259,13 @@ window.initSubmenuForm = initSubmenuForm;// Module IIFE pour éviter la pollutio
|
|||
rcProjetGuard = window.RCValidationUtils.createGuard({
|
||||
summaryId: 'rcProjetBlockingSummary',
|
||||
summaryTitle: 'Impossible d\'enregistrer ou de continuer car :',
|
||||
blockTargets: ['#projetFormBtn', '#generateDeclinaison', '#generateProject']
|
||||
blockTargets: ['#projetFormBtn', '#generateDeclinaison', '#generateProject'],
|
||||
onChange: function (messages) {
|
||||
const actionsRow = document.getElementById('projetActionsRow');
|
||||
if (actionsRow) {
|
||||
actionsRow.classList.toggle('rc-tarifettes-hidden', messages.length > 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
rcProjetGuard.registerField('#CA', {
|
||||
|
|
@ -379,6 +393,21 @@ window.initSubmenuForm = initSubmenuForm;// Module IIFE pour éviter la pollutio
|
|||
required: false
|
||||
});
|
||||
|
||||
rcProjetGuard.registerExternal('marchandises-valides', function () {
|
||||
const marchandiseSelector = document.getElementById('marchandise-selector');
|
||||
if (!marchandiseSelector) {
|
||||
return { valid: true };
|
||||
}
|
||||
const selectedCount = Array.from(marchandiseSelector.selectedOptions || []).length;
|
||||
if (selectedCount > 0) {
|
||||
return { valid: true };
|
||||
}
|
||||
return {
|
||||
valid: false,
|
||||
message: 'Marchandises invalides : sélectionnez au moins une marchandise.'
|
||||
};
|
||||
});
|
||||
|
||||
rcProjetGuard.registerExternal('prime-reference', function () {
|
||||
const state = getPrimeReferenceState();
|
||||
if (state.valid) {
|
||||
|
|
@ -2105,6 +2134,7 @@ window.initSubmenuForm = initSubmenuForm;// Module IIFE pour éviter la pollutio
|
|||
// Réactiver la détection juste après les dispatchs pour conserver le contrôle.
|
||||
setTimeout(() => {
|
||||
isRestoringValue = false;
|
||||
syncRCFloatingLabels();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
|
|
@ -2118,18 +2148,26 @@ window.initSubmenuForm = initSubmenuForm;// Module IIFE pour éviter la pollutio
|
|||
|
||||
document.getElementById('activity-selector').addEventListener('change', function () {
|
||||
handleActivitySelection();
|
||||
if (rcProjetGuard) rcProjetGuard.refresh();
|
||||
syncRCFloatingLabels();
|
||||
});
|
||||
|
||||
document.getElementById('marchandise-selector').addEventListener('change', function () {
|
||||
handleMarchandiseSelection();
|
||||
if (rcProjetGuard) rcProjetGuard.refresh();
|
||||
syncRCFloatingLabels();
|
||||
});
|
||||
|
||||
document.getElementById('garantieRCC-selector').addEventListener('change', function () {
|
||||
handleGarantieRCCSelection();
|
||||
if (rcProjetGuard) rcProjetGuard.refresh();
|
||||
syncRCFloatingLabels();
|
||||
});
|
||||
|
||||
document.getElementById('garantieRCE-selector').addEventListener('change', function () {
|
||||
handleGarantieRCESelection();
|
||||
if (rcProjetGuard) rcProjetGuard.refresh();
|
||||
syncRCFloatingLabels();
|
||||
});
|
||||
|
||||
document.getElementById('choixRCE').addEventListener('change', function () {
|
||||
|
|
@ -3192,6 +3230,7 @@ window.initSubmenuForm = initSubmenuForm;// Module IIFE pour éviter la pollutio
|
|||
|
||||
updatePrimeReferenceRow();
|
||||
if (rcProjetGuard) rcProjetGuard.refresh();
|
||||
syncRCFloatingLabels();
|
||||
}
|
||||
|
||||
function populateGrAdvalo(jsonData, tableID) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,552 @@
|
|||
(function () {
|
||||
function normalizeNumericInput(raw) {
|
||||
if (raw == null) return '';
|
||||
return String(raw).trim().replace(/\s+/g, '');
|
||||
}
|
||||
|
||||
function parseLooseNumber(raw) {
|
||||
const normalized = normalizeNumericInput(raw).replace(',', '.');
|
||||
if (!normalized) return NaN;
|
||||
const parsed = Number(normalized);
|
||||
return Number.isFinite(parsed) ? parsed : NaN;
|
||||
}
|
||||
|
||||
function formatFrenchAmount(value, digits) {
|
||||
const number = Number(value);
|
||||
if (!Number.isFinite(number)) return '0.00';
|
||||
return number.toLocaleString('fr-FR', {
|
||||
minimumFractionDigits: digits,
|
||||
maximumFractionDigits: digits
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hasForbiddenNumericChars(value) {
|
||||
return /[A-Za-z€$£¥]/.test(value);
|
||||
}
|
||||
|
||||
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(',', '.') };
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
function createSummaryElement(summaryId) {
|
||||
if (!summaryId) return null;
|
||||
return document.getElementById(summaryId);
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function recompute() {
|
||||
const messages = [];
|
||||
|
||||
state.fieldErrors.forEach(function (error) {
|
||||
if (error && error.message) {
|
||||
messages.push(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
state.externalChecks.forEach(function (entry, key) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
applySummary(messages);
|
||||
applyBlocking(messages.length > 0);
|
||||
|
||||
if (state.onChange) {
|
||||
state.onChange(messages);
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
function validateField(field) {
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function registerExternal(key, fn, fallbackMessage) {
|
||||
if (!key || typeof fn !== 'function') return;
|
||||
state.externalChecks.set(key, {
|
||||
fn: fn,
|
||||
message: fallbackMessage || ''
|
||||
});
|
||||
recompute();
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
})();
|
||||
|
|
@ -78,6 +78,18 @@ window.initSubmenuForm = initSubmenuForm;// Module IIFE pour éviter la pollutio
|
|||
let rcTarifGuard = null;
|
||||
let rcTarifHasErrors = false;
|
||||
|
||||
function syncRCFloatingLabels() {
|
||||
if (window.RCValidationUtils && typeof window.RCValidationUtils.syncFloatingLabels === 'function') {
|
||||
window.RCValidationUtils.syncFloatingLabels(document);
|
||||
}
|
||||
}
|
||||
|
||||
function updateTarifettesVisibility() {
|
||||
const tarifettesContainer = document.getElementById('tarifettesContainer');
|
||||
if (!tarifettesContainer) return;
|
||||
tarifettesContainer.classList.toggle('rc-tarifettes-hidden', rcTarifHasErrors);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// FONCTIONS HELPERS
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
|
@ -107,6 +119,7 @@ window.initSubmenuForm = initSubmenuForm;// Module IIFE pour éviter la pollutio
|
|||
}
|
||||
button.title = 'Corrigez les erreurs de saisie avant de sélectionner un tarif.';
|
||||
});
|
||||
updateTarifettesVisibility();
|
||||
}
|
||||
|
||||
function setupRCSafeValidation() {
|
||||
|
|
@ -287,6 +300,29 @@ window.initSubmenuForm = initSubmenuForm;// Module IIFE pour éviter la pollutio
|
|||
required: false
|
||||
});
|
||||
|
||||
rcTarifGuard.registerExternal('marchandises-valides', function () {
|
||||
const visibleContainers = Array.from(document.querySelectorAll('[name^="mar"]')).filter((container) => {
|
||||
return container && container.offsetParent !== null;
|
||||
});
|
||||
|
||||
if (!visibleContainers.length) {
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
const hasAtLeastOneChecked = visibleContainers.some((container) => {
|
||||
return Boolean(container.querySelector('input[type="checkbox"]:checked'));
|
||||
});
|
||||
|
||||
if (hasAtLeastOneChecked) {
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'Marchandises invalides : sélectionnez au moins une marchandise.'
|
||||
};
|
||||
});
|
||||
|
||||
rcTarifGuard.refresh();
|
||||
updateTarifChoiceButtonsState();
|
||||
}
|
||||
|
|
@ -418,12 +454,14 @@ window.initSubmenuForm = initSubmenuForm;// Module IIFE pour éviter la pollutio
|
|||
setupTarifetteButtons();
|
||||
populateFormData();
|
||||
setupRCSafeValidation();
|
||||
syncRCFloatingLabels();
|
||||
updatePercentageIndicator(100); // Initialiser à 100%
|
||||
calcGlobal();
|
||||
|
||||
setTimeout(() => {
|
||||
if (rcTarifGuard) rcTarifGuard.refresh();
|
||||
updateTarifChoiceButtonsState();
|
||||
syncRCFloatingLabels();
|
||||
}, 300);
|
||||
})
|
||||
}
|
||||
|
|
@ -456,6 +494,16 @@ window.initSubmenuForm = initSubmenuForm;// Module IIFE pour éviter la pollutio
|
|||
});
|
||||
}
|
||||
|
||||
document.addEventListener('change', function (event) {
|
||||
const target = event.target;
|
||||
if (!target) return;
|
||||
if (!target.matches('[name^="mar"] input[type="checkbox"], [name^="actCompl"] input[type="checkbox"], #checkVoiturier, #checkCommissionnaire, #checkDemenageur, #checkLogistique, #checkAutocariste, #checkAutres, #zone1, #zone2, #zone3, #zone4, #zone5, #zone6')) {
|
||||
return;
|
||||
}
|
||||
if (rcTarifGuard) rcTarifGuard.refresh();
|
||||
updateTarifChoiceButtonsState();
|
||||
});
|
||||
|
||||
document.getElementById('loadHistoriqueBtn').addEventListener('click', function () {
|
||||
handleLoadHistoriqueBtn();
|
||||
});
|
||||
|
|
@ -1820,10 +1868,12 @@ window.initSubmenuForm = initSubmenuForm;// Module IIFE pour éviter la pollutio
|
|||
}
|
||||
|
||||
console.log('Formulaire tarif pre-rempli avec succes');
|
||||
syncRCFloatingLabels();
|
||||
|
||||
// Recalculer après pré-remplissage
|
||||
setTimeout(() => {
|
||||
calcGlobal();
|
||||
syncRCFloatingLabels();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,8 @@
|
|||
<script src="/js/verif-form.js"></script>
|
||||
<!-- Feuille de script pour les donnée static JSON de saisie formulaire -->
|
||||
<script src="/js/json/json-verif-form.js"></script>
|
||||
<!-- Utilitaires de validation et blocage RC -->
|
||||
<script src="/js/rc-validation-utils.js"></script>
|
||||
<!-- Feuille de script pour le bon fonctionnement du loader -->
|
||||
<script src="/js/loader.js"></script>
|
||||
<!-- Script pour la navigation AJAX -->
|
||||
|
|
|
|||
|
|
@ -1007,7 +1007,7 @@
|
|||
<!-- ------------------------------------------- -->
|
||||
<!-- Nouvelle section : Génération et sauvegarde -->
|
||||
<!-- ------------------------------------------- -->
|
||||
<div class="row">
|
||||
<div id="projetActionsRow" class="row">
|
||||
<div class="header">
|
||||
<div class="chapter">
|
||||
<i class="material-icons">save</i><span style="margin-left: 10px;">Enregistrer et poursuivre le parcours :</span>
|
||||
|
|
|
|||
|
|
@ -128,8 +128,8 @@
|
|||
<span><strong style="color: darkblue;">Voiturier / Loueur</strong></span>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<label class="rc-field-label">Capital à assurer</label>
|
||||
<input type="text" name="selectActVoiturier/Loueur" placeholder="Ex: 5000, 10000, 25000" style="text-align: center; font-weight: bold;">
|
||||
<label class="rc-field-label" for="capitalActVoiturierLoueur">Capital à assurer</label>
|
||||
<input type="text" id="capitalActVoiturierLoueur" name="selectActVoiturier/Loueur" placeholder="Ex: 5000, 10000, 25000" style="text-align: center; font-weight: bold;">
|
||||
</div>
|
||||
<div class="row" style="display: flex;align-items: center; justify-content: space-around;">
|
||||
<input id="pourcentVoiturier/Loueur" class="input-pourcent" type="text" placeholder="Ex: 50" title="Appuyez sur Entrée pour valider" style="width: 80px; text-align: center; font-weight: bold; border: 2px solid #3f51b5; border-radius: 4px; padding: 8px;" />
|
||||
|
|
@ -156,8 +156,8 @@
|
|||
<span><strong style="color: darkblue;">Commissionnaire de Transport</strong></span>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<label class="rc-field-label">Capital à assurer</label>
|
||||
<input type="text" name="selectActCommissionnaire de Transport" placeholder="Ex: 100000, 150000, 200000" style="text-align: center; font-weight: bold;">
|
||||
<label class="rc-field-label" for="capitalActCommissionnaire">Capital à assurer</label>
|
||||
<input type="text" id="capitalActCommissionnaire" name="selectActCommissionnaire de Transport" placeholder="Ex: 100000, 150000, 200000" style="text-align: center; font-weight: bold;">
|
||||
</div>
|
||||
<div class="row" style="display: flex;align-items: center; justify-content: space-around;">
|
||||
<input id="pourcentCommissionnaire de Transport" class="input-pourcent" type="text" placeholder="Ex: 50" title="Appuyez sur Entrée pour valider" style="width: 80px; text-align: center; font-weight: bold; border: 2px solid #3f51b5; border-radius: 4px; padding: 8px;" />
|
||||
|
|
@ -184,8 +184,8 @@
|
|||
<span><strong style="color: darkblue;">Déménageur</strong></span>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<label class="rc-field-label">Capital à assurer</label>
|
||||
<input type="text" name="selectActDéménageur" placeholder="Ex: 5000, 10000, 25000" style="text-align: center; font-weight: bold;">
|
||||
<label class="rc-field-label" for="capitalActDemenageur">Capital à assurer</label>
|
||||
<input type="text" id="capitalActDemenageur" name="selectActDéménageur" placeholder="Ex: 5000, 10000, 25000" style="text-align: center; font-weight: bold;">
|
||||
</div>
|
||||
<div class="row" style="display: flex;align-items: center; justify-content: space-around;">
|
||||
<input id="pourcentDéménageur" class="input-pourcent" type="text" placeholder="Ex: 50" title="Appuyez sur Entrée pour valider" style="width: 80px; text-align: center; font-weight: bold; border: 2px solid #3f51b5; border-radius: 4px; padding: 8px;" />
|
||||
|
|
@ -212,8 +212,8 @@
|
|||
<span><strong style="color: darkblue;">Logistique</strong></span>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<label class="rc-field-label">Capital à assurer</label>
|
||||
<input type="text" name="selectActLogistique" placeholder="Ex: 5000, 10000, 25000" style="text-align: center; font-weight: bold;">
|
||||
<label class="rc-field-label" for="capitalActLogistique">Capital à assurer</label>
|
||||
<input type="text" id="capitalActLogistique" name="selectActLogistique" placeholder="Ex: 5000, 10000, 25000" style="text-align: center; font-weight: bold;">
|
||||
</div>
|
||||
<div class="row" style="display: flex;align-items: center; justify-content: space-around;">
|
||||
<input id="pourcentLogistique" class="input-pourcent" type="text" placeholder="Ex: 50" title="Appuyez sur Entrée pour valider" style="width: 80px; text-align: center; font-weight: bold; border: 2px solid #3f51b5; border-radius: 4px; padding: 8px;" />
|
||||
|
|
@ -240,8 +240,8 @@
|
|||
<span><strong style="color: darkblue;">Autocariste</strong></span>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<label class="rc-field-label">Capital à assurer</label>
|
||||
<input type="text" name="selectActAutocariste" placeholder="Ex: 25000, 50000, 75000" style="text-align: center; font-weight: bold;">
|
||||
<label class="rc-field-label" for="capitalActAutocariste">Capital à assurer</label>
|
||||
<input type="text" id="capitalActAutocariste" name="selectActAutocariste" placeholder="Ex: 25000, 50000, 75000" style="text-align: center; font-weight: bold;">
|
||||
</div>
|
||||
<div class="row" style="display: flex;align-items: center; justify-content: space-around;">
|
||||
<input id="pourcentAutocariste" class="input-pourcent" type="text" placeholder="Ex: 50" title="Appuyez sur Entrée pour valider" style="width: 80px; text-align: center; font-weight: bold; border: 2px solid #3f51b5; border-radius: 4px; padding: 8px;" />
|
||||
|
|
@ -268,8 +268,8 @@
|
|||
<span><strong style="color: darkblue;">Autres activites</strong></span>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<label class="rc-field-label">Capital à assurer</label>
|
||||
<input type="text" name="selectActAutres activites" placeholder="Ex: 5000, 10000, 25000" style="text-align: center; font-weight: bold;">
|
||||
<label class="rc-field-label" for="capitalActAutres">Capital à assurer</label>
|
||||
<input type="text" id="capitalActAutres" name="selectActAutres activites" placeholder="Ex: 5000, 10000, 25000" style="text-align: center; font-weight: bold;">
|
||||
</div>
|
||||
<div class="row" style="display: flex;align-items: center; justify-content: space-around;">
|
||||
<input id="pourcentAutres activites" class="input-pourcent" type="text" placeholder="Ex: 50" title="Appuyez sur Entrée pour valider" style="width: 80px; text-align: center; font-weight: bold; border: 2px solid #3f51b5; border-radius: 4px; padding: 8px;" />
|
||||
|
|
@ -819,7 +819,7 @@
|
|||
<label><input id="checkDomImmat" type='checkbox' class='filled-in' /><span>Dommages immatériels</span></label>
|
||||
</div>
|
||||
<div id="selectDomImmat" style="display: none; margin-top: 20px;">
|
||||
<label class="rc-field-label">Capital à assurer</label>
|
||||
<label class="rc-field-label" for="inputDomImmat">Capital à assurer</label>
|
||||
<input type="text" id="inputDomImmat" placeholder="Ex: 15000, 50000, 100000" style="font-weight: bold;">
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -834,7 +834,7 @@
|
|||
<label><input id="checkContConf" type='checkbox' class='filled-in' /><span>Contenants confiés</span></label>
|
||||
</div>
|
||||
<div id="selectContConf" style="display: none; margin-top: 20px;">
|
||||
<label class="rc-field-label">Capital à assurer</label>
|
||||
<label class="rc-field-label" for="inputContConf">Capital à assurer</label>
|
||||
<input type="text" id="inputContConf" placeholder="Ex: 25000, 50000, 100000" style="font-weight: bold;">
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -849,7 +849,7 @@
|
|||
<label><input id="checkDiffInv" type='checkbox' class='filled-in' /><span>Différence inventaire</span></label>
|
||||
</div>
|
||||
<div id="selectDiffInv" style="display: none; margin-top: 20px;">
|
||||
<label class="rc-field-label">Capital à assurer</label>
|
||||
<label class="rc-field-label" for="inputDiffInv">Capital à assurer</label>
|
||||
<input type="text" id="inputDiffInv" placeholder="Ex: 15000, 30000, 50000" style="font-weight: bold;">
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -883,7 +883,7 @@
|
|||
<label><input id="checkTPPC" type='checkbox' class='filled-in' /><span>TPPC</span></label>
|
||||
</div>
|
||||
<div id="selectTPPC" style="display: none; margin-top: 20px;">
|
||||
<label class="rc-field-label">Capital à assurer</label>
|
||||
<label class="rc-field-label" for="selTPPCcapital">Capital à assurer</label>
|
||||
<input type="text" id="selTPPCcapital" placeholder="Ex: 5000, 10000, 15000" style="font-weight: bold; margin-bottom: 10px;">
|
||||
<label class="rc-field-label" for="selTPPCveh">Nombre de véhicules</label>
|
||||
<input type="number" id="selTPPCveh" placeholder="Ex: 1 ou 2" min="1" max="10" style="font-weight: bold;">
|
||||
|
|
@ -973,7 +973,7 @@
|
|||
</div>
|
||||
<div id="rcTarifBlockingSummary" class="rc-blocking-summary"></div>
|
||||
|
||||
<div class="col s12" style="display: flex;">
|
||||
<div id="tarifettesContainer" class="col s12" style="display: flex;">
|
||||
<div class="col s4">
|
||||
<div class="franchise-card card border">
|
||||
<div class="card-tarif-head" style="background-color:#b71c1c">
|
||||
|
|
|
|||
Loading…
Reference in New Issue