From 5338f682af852ac6fa5fff0be6ffd618a8ccfc27 Mon Sep 17 00:00:00 2001 From: Alexis Burnaz <48258099+alxsbrz@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:24:37 +0200 Subject: [PATCH] RC UI: uniformiser labels flottants et fiabiliser blocages projet/tarif --- ecole/public/css/global.css | 31 ++ ecole/public/js/projet-form-rc.js | 41 +- ecole/public/js/rc-validation-utils.js | 552 +++++++++++++++++++++++++ ecole/public/js/tarif-form-rc.js | 50 +++ ecole/views/layout.ejs | 2 + ecole/views/projetformrc.ejs | 2 +- ecole/views/tarifformrc.ejs | 34 +- 7 files changed, 693 insertions(+), 19 deletions(-) create mode 100644 ecole/public/js/rc-validation-utils.js diff --git a/ecole/public/css/global.css b/ecole/public/css/global.css index 3a502e3a..d4b72e7a 100644 --- a/ecole/public/css/global.css +++ b/ecole/public/css/global.css @@ -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)); diff --git a/ecole/public/js/projet-form-rc.js b/ecole/public/js/projet-form-rc.js index 65488c25..43dcc59e 100644 --- a/ecole/public/js/projet-form-rc.js +++ b/ecole/public/js/projet-form-rc.js @@ -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) { diff --git a/ecole/public/js/rc-validation-utils.js b/ecole/public/js/rc-validation-utils.js new file mode 100644 index 00000000..380d05ff --- /dev/null +++ b/ecole/public/js/rc-validation-utils.js @@ -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 '
  • ' + msg + '
  • '; + }).join(''); + + summary.innerHTML = [ + '
    ' + summaryTitle + '
    ', + '' + ].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 + }; +})(); diff --git a/ecole/public/js/tarif-form-rc.js b/ecole/public/js/tarif-form-rc.js index 8be4e5b9..b739855c 100644 --- a/ecole/public/js/tarif-form-rc.js +++ b/ecole/public/js/tarif-form-rc.js @@ -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); } diff --git a/ecole/views/layout.ejs b/ecole/views/layout.ejs index 0ea299c6..b906368b 100644 --- a/ecole/views/layout.ejs +++ b/ecole/views/layout.ejs @@ -59,6 +59,8 @@ + + diff --git a/ecole/views/projetformrc.ejs b/ecole/views/projetformrc.ejs index f9dd277b..a0d310a4 100644 --- a/ecole/views/projetformrc.ejs +++ b/ecole/views/projetformrc.ejs @@ -1007,7 +1007,7 @@ -
    +
    saveEnregistrer et poursuivre le parcours : diff --git a/ecole/views/tarifformrc.ejs b/ecole/views/tarifformrc.ejs index 202bb1ea..a15ef733 100644 --- a/ecole/views/tarifformrc.ejs +++ b/ecole/views/tarifformrc.ejs @@ -128,8 +128,8 @@ Voiturier / Loueur
    - - + +
    @@ -156,8 +156,8 @@ Commissionnaire de Transport
    - - + +
    @@ -184,8 +184,8 @@ Déménageur
    - - + +
    @@ -212,8 +212,8 @@ Logistique
    - - + +
    @@ -240,8 +240,8 @@ Autocariste
    - - + +
    @@ -268,8 +268,8 @@ Autres activites
    - - + +
    @@ -819,7 +819,7 @@
    @@ -834,7 +834,7 @@
    @@ -849,7 +849,7 @@
    @@ -883,7 +883,7 @@
    -
    +