From 96765aac84c4c2235fc2fe45811f531936281387 Mon Sep 17 00:00:00 2001 From: stephan Date: Tue, 7 Jul 2026 15:27:33 -0400 Subject: [PATCH] MSIN-4435 Add inc_v2_assets.php integration across multiple files This commit integrates the `inc_v2_assets.php` file into several PHP scripts, including `compte.php`, `inc_footer.php`, `inc_tableau_gestion_epreuves.php`, `inc_tableau_inscriptions_gestion.php`, and `mobile.php`. Additionally, it registers new scripts for improved functionality in the `inscriptions gestion` context. The version code is incremented to reflect these updates, aiming to enhance asset management and script handling across the application. --- .cursor/rules/ms1-v2.mdc | 31 + compte.php | 1 + inc_footer.php | 4 + inc_footer_scripts.php | 2876 +------------------------- inc_tableau_gestion_epreuves.php | 2 + inc_tableau_inscriptions_gestion.php | 2 + js/v2/bib-assignments.js | 1994 ++++++++++++++++++ js/v2/inscr-gestion.js | 895 ++++++++ mobile.php | 2 + php/inc_settings.php | 2 +- php/inc_v2_assets.php | 72 + 11 files changed, 3006 insertions(+), 2875 deletions(-) create mode 100644 .cursor/rules/ms1-v2.mdc create mode 100644 js/v2/bib-assignments.js create mode 100644 js/v2/inscr-gestion.js create mode 100644 php/inc_v2_assets.php diff --git a/.cursor/rules/ms1-v2.mdc b/.cursor/rules/ms1-v2.mdc new file mode 100644 index 0000000..0bf36fd --- /dev/null +++ b/.cursor/rules/ms1-v2.mdc @@ -0,0 +1,31 @@ +--- +description: MS1 modules v2 — bib v4 et gestion inscriptions (hors legacy) +globs: js/v2/**,php/inc_v2_assets.php,inc_tableau_gestion_epreuves.php,inc_tableau_inscriptions_gestion.php,ajax_bib_range.php,ajax_inscr_gestion.php,php/inc_fx_promoteur.php,php/inc_fx_inscriptions_gestion.php +alwaysApply: false +--- + +# Modules v2 MS1 + +## Périmètre v2 (non prod, développement actif) + +- **Bib assignments v4** — `#module-bib`, `fxShowBibTool4`, `ajax_bib_range.php` +- **Gestion inscriptions v2** — `#module-inscr-gestion`, `inc_fx_inscriptions_gestion.php`, `ajax_inscr_gestion.php` + +## Règle JS + +- **Ne jamais** ajouter de code v2 dans `inc_footer_scripts.php`. +- Nouveau JS v2 → `js/v2/` (fichier existant ou nouveau selon le module). +- Enregistrer le script sur la page concernée : `fxV2RegisterScript('bib-assignments')` ou `'inscr-gestion'`. +- Config PHP → JS : `data-*` sur le module (bib) ou `window.MS1_V2_INSCR_GESTION` (inscr). + +## Legacy (ne pas toucher sauf pont minimal) + +- `.btn_bib` jQuery, `fxShowBibTool2/3`, `inc_tableau_dossards.php` + +## Fichiers de référence + +| Rôle | Fichier | +|------|---------| +| Enregistrement / rendu scripts | `php/inc_v2_assets.php` | +| Bib v4 client | `js/v2/bib-assignments.js` | +| Inscr gestion client | `js/v2/inscr-gestion.js` | diff --git a/compte.php b/compte.php index 14a3b25..d34d2a1 100644 --- a/compte.php +++ b/compte.php @@ -22,6 +22,7 @@ require_once('superadm/php/inc_fx_excell.php'); require_once('php/inc_fx_liste_attente.php'); require_once('php/inc_fx_inscriptions_gestion.php'); +require_once('php/inc_v2_assets.php'); require_once('php/inc_fx_eve_acces.php'); require_once('php/inc_fx_promoteur_hub.php'); global $objDatabase, $vPage, $vDomaine; diff --git a/inc_footer.php b/inc_footer.php index fcf288f..371dc8b 100644 --- a/inc_footer.php +++ b/inc_footer.php @@ -369,6 +369,10 @@ if (isset($tabEvenement) && $tabEvenement['general']['eve_commanditaires'] == "1 + diff --git a/inc_footer_scripts.php b/inc_footer_scripts.php index 369daa7..d64e7e2 100644 --- a/inc_footer_scripts.php +++ b/inc_footer_scripts.php @@ -1,4 +1,4 @@ - + - if ($('#frm_inscr_gestion_login').length) { - $('#frm_inscr_gestion_login').validate({ - messages: { - txt_login: "", - txt_password: "" - }, - submitHandler: function (form) { - if (window.Ms1Loader) { - Ms1Loader.show(); - } else { - $preloader.show(); - } - form.submit(); - } - }); - } $('.btn_update_prod').click(function () { var $id = $(this).data('id'); @@ -1299,249 +1283,6 @@ if ($strLangue == 'fr') { } }); - (function () { - var pendingStatutXhr = null; - var lastStatutParId = null; - - $('.inscr-gestion-statut-select').each(function () { - $(this).data('prev', $(this).val()); - }); - - $body.on('change', '.inscr-gestion-statut-select', function () { - var $select = $(this); - var $block = $select.closest('.inscr-gestion-field-block--statut'); - var intParId = $select.data('par_id'); - var strCode = $select.val(); - var strPrev = $select.data('prev'); - - if (!intParId || !strCode) { - return; - } - - if (pendingStatutXhr && lastStatutParId === intParId) { - pendingStatutXhr.abort(); - } - lastStatutParId = intParId; - $block.removeClass('inscr-gestion-statut--saved inscr-gestion-statut--error'); - - $preloader_text.html(''); - $preloader.show(); - - pendingStatutXhr = $.post('/ajax_inscr_gestion.php', { - a: 'par_statut_course', - par_id: intParId, - par_statut_course: strCode, - lang: '' - }, function (result) { - pendingStatutXhr = null; - $preloader.fadeOut('slow'); - if (result && result.state === 'success') { - $select.data('prev', strCode); - $block.addClass('inscr-gestion-statut--saved'); - window.setTimeout(function () { - $block.removeClass('inscr-gestion-statut--saved'); - }, 1200); - } else { - $select.val(strPrev); - $block.addClass('inscr-gestion-statut--error'); - } - }, 'json').fail(function () { - pendingStatutXhr = null; - $preloader.fadeOut('slow'); - $select.val(strPrev); - $block.addClass('inscr-gestion-statut--error'); - }); - }); - })(); - - // ===== Remboursement PayPal (fiche de gestion) ===== - // Bouton bascule : ouvre/ferme le panneau repliable et charge le formulaire en AJAX a la 1ere ouverture. - (function () { - // Texte explicatif affiche apres un remboursement reussi (bouton Annuler inline dans le bandeau). - var refundCancelHint = ; - - // Comportement post-succes : recalcul du restant, re-emission du jeton, mise en evidence du bouton Annuler. - function onRefundSuccess(form, json) { - var $form = $(form); - - // 1) Re-emission du jeton anti double-clic (le serveur l'a consomme) -> autorise un 2e remboursement partiel. - if (json && json.new_token) { - $form.find('input[name="refund_token"]').val(json.new_token); - } - - // 2) Recalcul du montant restant (remboursement partiel) cote client : ancien max - montant rembourse. - var amount = parseFloat($form.find('input[name="refund_amount"]').val()) || 0; - var $max = $('#pp_ref_max'); - var oldMax = parseFloat(String($max.text() || '0').replace(',', '.')) || 0; - var newMax = Math.max(0, oldMax - amount); - $max.text(newMax.toFixed(2)); - - var $amountInput = $('#pp_ref_amount'); - $amountInput.attr('max', newMax.toFixed(2)); - if (newMax <= 0) { - // Plus rien a rembourser : on verrouille le formulaire (le solde affiche reste a 0). - $amountInput.val('0.00').prop('disabled', true); - $form.find('button[type="submit"]').prop('disabled', true); - } else { - $amountInput.val(newMax.toFixed(2)); - } - - // 3) Bandeau explicatif avec bouton Annuler inline (le bouton du haut est masque pour eviter le doublon). - var $cancelBtn = $('#annuler_fiche'); - if ($cancelBtn.length && $cancelBtn.is(':visible')) { - if (!$('#inscr-gestion-refund-cancel-hint').length) { - var $hint = $('
'); - $hint.append($('').text(refundCancelHint)); - $hint.append( - $('') - .text($.trim($cancelBtn.text())) - .on('click', function () { $cancelBtn.trigger('click'); }) - ); - $hint.insertAfter($('#inscr-gestion-refund-panel')); - } - $cancelBtn.addClass('d-none'); - } - } - - $(document).on('click', '.btn_refund_toggle', function () { - var $btn = $(this); - var $panel = $($btn.attr('data-refund-target') || '#inscr-gestion-refund-panel'); - if (!$panel.length) { return; } - - // Si deja ouvert -> on replie. - if (!$panel.prop('hidden')) { - $panel.prop('hidden', true); - $btn.attr('aria-expanded', 'false'); - return; - } - - // Sinon -> on deplie. - $panel.prop('hidden', false); - $btn.attr('aria-expanded', 'true'); - - // Amene le panneau de remboursement dans le viewport et place le focus sur le champ - // montant : utile quand le detail de la transaction est deja affiche au-dessus/dessous. - function focusRefundPanel() { - if ($panel[0] && $panel[0].scrollIntoView) { - $panel[0].scrollIntoView({ behavior: 'smooth', block: 'start' }); - } - var $amount = $panel.find('#pp_ref_amount'); - if ($amount.length && !$amount.prop('disabled')) { - $amount.trigger('focus').trigger('select'); - } - } - - // Deja charge ? on ne recharge pas (le serveur recalcule a chaque (re)chargement reel). - if ($panel.attr('data-loaded') === '1') { focusRefundPanel(); return; } - - $panel.html(ms1LoaderInlineHtml()); - $.post('/ajax_inscr_gestion.php', { - a: 'refund_form', - no_panier: $btn.data('no_panier'), - eve_id: $btn.data('eve_id'), - lang: '' - }, function (result) { - if (result && result.state === 'ok' && result.html) { - $panel.html(result.html); - $panel.attr('data-loaded', '1'); - focusRefundPanel(); - } else { - $panel.html('
\u26a0\ufe0f ' + ((result && result.message) || 'Erreur de chargement.') + '
'); - } - }, 'json').fail(function () { - $panel.html('
\u26a0\ufe0f Erreur reseau.
'); - }); - }); - - // Bascule "Afficher la transaction" (MSIN) : meme phenomene que le remboursement, - // mais panneau independant accessible a tout moment. Charge le resume en AJAX (transaction_resume). - $(document).on('click', '.btn_transaction_toggle', function () { - var $btn = $(this); - var $panel = $($btn.attr('data-transaction-target') || '#inscr-gestion-transaction-panel'); - if (!$panel.length) { return; } - - var strShow = $btn.data('label-show'); - var strHide = $btn.data('label-hide'); - - // Si deja ouvert -> on replie. - if (!$panel.prop('hidden')) { - $panel.prop('hidden', true); - $btn.attr('aria-expanded', 'false'); - if (strShow) { $btn.text(strShow); } - return; - } - - // Sinon -> on deplie. - $panel.prop('hidden', false); - $btn.attr('aria-expanded', 'true'); - if (strHide) { $btn.text(strHide); } - - // Deja charge ? on ne recharge pas. - if ($panel.attr('data-loaded') === '1') { return; } - - $panel.html(ms1LoaderInlineHtml()); - $.post('/ajax_inscr_gestion.php', { - a: 'transaction_resume', - no_panier: $btn.data('no_panier'), - lang: '' - }, function (result) { - if (result && result.state === 'ok' && result.html) { - $panel.html(result.html); - $panel.attr('data-loaded', '1'); - } else { - $panel.html('
\u26a0\ufe0f ' + ((result && result.message) || 'Erreur de chargement.') + '
'); - } - }, 'json').fail(function () { - $panel.html('
\u26a0\ufe0f Erreur reseau.
'); - }); - }); - - // Handler delegue du formulaire injecte (le diff --git a/inc_tableau_gestion_epreuves.php b/inc_tableau_gestion_epreuves.php index 935f2d7..78c4b7f 100644 --- a/inc_tableau_gestion_epreuves.php +++ b/inc_tableau_gestion_epreuves.php @@ -4,6 +4,7 @@ include_once("php/inc_fx_modifierinscriptions.php"); include_once("php/inc_fx_memberships.php"); include_once("php/inc_fx_promoteur.php"); +require_once('php/inc_v2_assets.php'); require_once('php/inc_fx_eve_acces.php'); require_once('superadm/php/inc_manager.php'); @@ -63,6 +64,7 @@ if (isset($_POST['btn_cancel_event_promo'])) { diff --git a/inc_tableau_inscriptions_gestion.php b/inc_tableau_inscriptions_gestion.php index 1ce289f..904795a 100644 --- a/inc_tableau_inscriptions_gestion.php +++ b/inc_tableau_inscriptions_gestion.php @@ -5,6 +5,7 @@ include_once('php/inc_fx_modifierinscriptions.php'); include_once('php/inc_fx_memberships.php'); include_once('php/inc_fx_promoteur.php'); include_once('php/inc_fx_inscriptions_gestion.php'); +require_once('php/inc_v2_assets.php'); require_once('superadm/php/inc_manager.php'); global $objDatabase, $vDomaine, $strLangue; @@ -24,5 +25,6 @@ if ($intEveId === 0) { } echo '
'; +fxV2RegisterScript('inscr-gestion'); fxInscrGestionRun($intEveId, $strLangue); echo '
'; diff --git a/js/v2/bib-assignments.js b/js/v2/bib-assignments.js new file mode 100644 index 0000000..d7b3b9e --- /dev/null +++ b/js/v2/bib-assignments.js @@ -0,0 +1,1994 @@ +/** + * MSIN-4435 — Bib assignments v4 (#module-bib) + * Chargé uniquement sur inc_tableau_gestion_epreuves. + */ +(function () { + 'use strict'; + const moduleBib = document.getElementById('module-bib'); + + + if (moduleBib) { + + function bibAjaxLangSuffix() { + var lang = moduleBib.dataset.lang || 'fr'; + return '&lang=' + encodeURIComponent(lang) + + '&csrf_token=' + encodeURIComponent(bibCsrfToken()); + } + + function bibJs(attr) { + return moduleBib.dataset[attr] || ''; + } + + function bibJsConfirm(attr) { + return bibJs(attr).replace(/\\n/g, '\n'); + } + + function bibJsFmt(attr) { + var tpl = bibJs(attr); + var args = Array.prototype.slice.call(arguments, 1); + var i = 0; + return tpl.replace(/%d/g, function () { + return String(args[i++] ?? 0); + }); + } + + var bibLoaderDepth = 0; + + function bibLoaderShow() { + if (!window.Ms1Loader) { + return; + } + bibLoaderDepth++; + if (bibLoaderDepth === 1) { + var msg = bibJs('jsLoaderWait'); + Ms1Loader.show(msg || undefined); + } + } + + function bibLoaderHide() { + if (!window.Ms1Loader) { + return; + } + bibLoaderDepth = Math.max(0, bibLoaderDepth - 1); + if (bibLoaderDepth === 0) { + Ms1Loader.hide(); + } + } + + /** fetch ajax_bib_range.php — loader plein ecran par defaut (compteur si appels imbriques). */ + function bibFetch(url, options, settings) { + settings = settings || {}; + var useLoader = settings.loader !== false; + if (useLoader) { + bibLoaderShow(); + } + return fetch(url, options).finally(function () { + if (useLoader) { + bibLoaderHide(); + } + }); + } + + var BIB_BATCH_CHUNK_SIZE = 50; + + function bibBatchProgressShow(processed, total) { + var intTotal = total > 0 ? total : 1; + var msg = bibJsFmt('jsBatchProgress', processed, intTotal); + if (window.Ms1Loader && Ms1Loader.showProgress) { + Ms1Loader.showProgress(processed, intTotal, msg); + } else { + bibLoaderShow(); + } + } + + function bibBatchProgressHide() { + if (window.Ms1Loader && Ms1Loader.hide) { + Ms1Loader.hide(); + } else { + bibLoaderHide(); + } + } + + function bibAfterBatchGoSuccess(row, data, isResetMode) { + let existing = document.querySelector('.epr-row-view'); + if (existing) { + existing.remove(); + } + + if (data.html) { + row.insertAdjacentHTML('afterend', data.html); + } + + if (isResetMode) { + row.classList.remove('reset-mode'); + row.dataset.mode = ''; + + let btnReset = row.querySelector('.btn-reset'); + if (btnReset) { + btnReset.textContent = row.dataset.resetLabel || btnReset.textContent; + } + } + row.classList.remove('batch-mode'); + + let container = row.querySelector('.bib-container'); + if (container) { + container.classList.remove('batch-mode'); + } + + let btnBatch = row.querySelector('.btn-assign-batch'); + if (btnBatch) { + btnBatch.textContent = row.dataset.batchLabel || btnBatch.textContent; + } + + row.querySelectorAll('.bib-start, .bib-qty, .bib-end').forEach(input => { + input.removeAttribute('readonly'); + }); + + row.querySelectorAll('.batch-select-range').forEach(cb => { + cb.checked = false; + cb.disabled = false; + }); + + syncBibUiState(row); + + bibFetch('/ajax_bib_range.php', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: + 'action=refresh_ranges' + + '&epr_id=' + encodeURIComponent(row.dataset.eprId) + + '&mode=assign' + + bibAjaxLangSuffix() + }) + .then(res => res.json()) + .then(refresh => { + + if (!refresh.success) return; + + let container = setBibContainerHtml(row, refresh.html); + if (!container) return; + bibFetch('/ajax_bib_range.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: + 'action=batch_analysis' + + '&epr_id=' + encodeURIComponent(row.dataset.eprId) + + bibAjaxLangSuffix() + }) + .then(res => res.json()) + .then(stats => { + + if (!stats.success) return; + + bibSyncEprBibStats(row, stats.info); + syncBibUiState(row); + }); + }); + } + + function bibFetchBatchApplyChunk(row, selected, ba_id, opts) { + opts = opts || {}; + var body = + 'action=batch_apply_chunk' + + '&epr_id=' + encodeURIComponent(row.dataset.eprId) + + '&ranges=' + encodeURIComponent(JSON.stringify(selected)) + + '&ba_id=' + encodeURIComponent(ba_id) + + '&chunk_size=' + BIB_BATCH_CHUNK_SIZE + + '&total_initial=' + (opts.totalInitial || 0) + + '&batch_reset=' + (opts.batchReset ? 1 : 0) + + bibAjaxLangSuffix(); + + return fetch('/ajax_bib_range.php', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: body + }).then(function (res) { return res.json(); }); + } + + function bibRunBatchApplyChunks(row, selected, ba_id) { + var totalInitial = bibRowPendingBibs(row); + var batchReset = true; + + bibBatchProgressShow(0, totalInitial > 0 ? totalInitial : 1); + + function runChunk() { + return bibFetchBatchApplyChunk(row, selected, ba_id, { + batchReset: batchReset, + totalInitial: totalInitial + }).then(function (data) { + if (!data.success) { + throw new Error(data.message || bibJs('jsErrGeneric')); + } + + if (data.total_initial) { + totalInitial = data.total_initial; + } + batchReset = false; + + var processed = data.processed || 0; + bibBatchProgressShow(processed, totalInitial); + + if (data.complete) { + bibAfterBatchGoSuccess(row, data, false); + return; + } + + return runChunk(); + }); + } + + runChunk() + .catch(function (err) { + alert(err.message || bibJs('jsErrGeneric')); + }) + .finally(function () { + bibBatchProgressHide(); + }); + } + + function bibRowPendingBibs(row) { + if (!row) return 0; + return parseInt(row.dataset.bibAAssigner || '0', 10) || 0; + } + + function bibSyncEprBibStats(row, info) { + if (!row || !info) return; + + if (info.total !== undefined) { + let totalEl = row.querySelector('.bib-total'); + if (totalEl) totalEl.textContent = info.total; + } + if (info.avec_bib !== undefined) { + let avecEl = row.querySelector('.bib-avec'); + if (avecEl) avecEl.textContent = info.avec_bib; + } + + let pending = (info.a_assigner !== undefined && info.a_assigner !== null) + ? info.a_assigner + : (info.sans_bib || 0); + row.dataset.bibAAssigner = String(parseInt(pending, 10) || 0); + } + + function bibAlertAutoPendingBibs(row) { + alert(bibJsFmt('jsAutoPendingBibs', bibRowPendingBibs(row)).replace(/\\n/g, '\n')); + } + + /** Bloque l'entrée en mode auto si des inscriptions n'ont pas encore de dossard. */ + function bibBlockAutoActivation(row) { + if (!row || row.dataset.autoActive === '1') { + return false; + } + if (bibRowPendingBibs(row) > 0) { + bibAlertAutoPendingBibs(row); + return true; + } + return false; + } + + // MSIN-4379 — Réduire / développer les blocs gestion et assignation (en-tête seul visible). + moduleBib.addEventListener('click', function (e) { + let btnToggle = e.target.closest('.epr-block-toggle'); + if (!btnToggle || !moduleBib.contains(btnToggle)) { + return; + } + let block = btnToggle.closest('.epr-block'); + if (!block) { + return; + } + let collapsed = block.classList.toggle('is-collapsed'); + btnToggle.setAttribute('aria-expanded', collapsed ? 'false' : 'true'); + let label = collapsed + ? (btnToggle.getAttribute('data-label-expand') || '') + : (btnToggle.getAttribute('data-label-collapse') || ''); + if (label) { + btnToggle.setAttribute('aria-label', label); + btnToggle.setAttribute('title', label); + } + let icon = btnToggle.querySelector('.fa'); + if (icon) { + icon.classList.toggle('fa-chevron-up', !collapsed); + icon.classList.toggle('fa-chevron-down', collapsed); + } + }); + + // ============================================================ + // MSIN-4379 — Tippy.js (module #module-bib uniquement) + // info_texte → data-tippy-content (tip court) + // info_aide → data-tippy-content sur .btn-aide-bib (pop-up long) + // Ré-init après chaque refresh AJAX des séquences. + // ============================================================ + + function getBibContainer(row) { + if (!row) return null; + return row.querySelector('.epr-block-assign .epr-table.bib-container') + || row.querySelector('.epr-table.bib-container') + || row.querySelector('.epr-block-assign .bib-container') + || row.querySelector('.bib-container'); + } + + function bibTippyEscapeHtml(str) { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + } + + function bibTippyAideContent(reference) { + let span = reference.querySelector('.bib-aide-text'); + if (!span) { + return ''; + } + // Mini-formatage : retours à la ligne →
; double saut → paragraphe. + let lines = span.textContent.replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim().split('\n'); + let html = ''; + let para = []; + lines.forEach(function (line) { + if (line.trim() === '') { + if (para.length) { + html += '

' + para.join('
') + '

'; + para = []; + } + } else { + para.push(bibTippyEscapeHtml(line)); + } + }); + if (para.length) { + html += '

' + para.join('
') + '

'; + } + return html; + } + + function initModuleBibTippy(root) { + if (typeof tippy === 'undefined') return; + let scope = root || moduleBib; + scope.querySelectorAll('[data-tippy-content], .bib-aide-trigger').forEach(function (el) { + if (el._tippy) { + el._tippy.destroy(); + } + }); + + tippy(scope.querySelectorAll('[data-tippy-content]'), { + theme: 'ms1-bib', + animation: 'fade', + allowHTML: false, + maxWidth: 280, + interactive: false + }); + + tippy(scope.querySelectorAll('.bib-aide-trigger'), { + theme: 'ms1-bib ms1-bib-aide', + animation: 'fade', + allowHTML: true, + maxWidth: 420, + interactive: true, + content: bibTippyAideContent + }); + } + + /** Visibilité UI v4 — bib-ui-hidden sur le conteneur (bouton + ? ensemble). */ + function bibQuery(row, selector) { + return (row || moduleBib).querySelector(selector); + } + + function bibSetVisible(target, show, row) { + let el = target; + if (typeof target === 'string') { + el = bibQuery(row, target); + } else if (el && el.nodeType === 1 + && !el.classList.contains('epr-action-group') + && !el.classList.contains('batch-config-container') + && !el.classList.contains('auto-config-container') + && !el.classList.contains('epr-add-range-row')) { + el = el.closest('.epr-action-group') || el; + } + if (el) { + el.classList.toggle('bib-ui-hidden', !show); + } + } + + function bibRowModes(row) { + return { + batch: row.classList.contains('batch-mode'), + auto: row.classList.contains('auto-mode'), + reset: row.classList.contains('reset-mode'), + }; + } + + function bibHasSavedSequences(row) { + return Array.from(row.querySelectorAll('.bib-range[data-id]')).some(function (line) { + return parseInt(line.getAttribute('data-id') || '0', 10) > 0; + }); + } + + function bibCountSelectedRanges(row) { + return row.querySelectorAll('.batch-select-range:checked').length; + } + + /** MSIN-4379 — Rafraîchit le dashboard anomalies (chien de garde). */ + function refreshBibAnomaliesPanel() { + let mount = document.getElementById('bib-anomalies-mount'); + if (!mount || !moduleBib) { + return; + } + + let eveId = moduleBib.dataset.eveId || ''; + if (!eveId) { + return; + } + + bibFetch('/ajax_bib_range.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: 'action=anomalies&eve_id=' + encodeURIComponent(eveId) + bibAjaxLangSuffix() + }, { loader: false }) + .then(function (res) { return res.json(); }) + .then(function (data) { + if (!data.success || !data.html) { + return; + } + mount.innerHTML = data.html; + initModuleBibTippy(mount); + }); + } + + function setBibContainerHtml(row, html) { + let container = getBibContainer(row); + if (!container) return null; + // renderBibRanges renvoie un bloc .epr-table.bib-container complet. + if (html.indexOf('bib-container') !== -1) { + container.outerHTML = html; + container = getBibContainer(row); + } else { + container.innerHTML = html; + } + if (container) { + initModuleBibTippy(row); + initBibRangeRows(container); + } + syncBibUiState(row); + refreshBibAnomaliesPanel(); + return container; + } + + // MSIN-4429 — Séquence : Début puis Qté ou Fin (pilote / calculé). + function bibRangeParseInt(val) { + var n = parseInt(String(val || '').trim(), 10); + return isNaN(n) ? 0 : n; + } + + function bibRangeGetFields(row) { + if (!row) return null; + return { + start: row.querySelector('.bib-start'), + qty: row.querySelector('.bib-qty'), + end: row.querySelector('.bib-end') + }; + } + + function bibRangeGetErrorEl(row) { + var el = row && row.nextElementSibling; + if (el && el.classList.contains('bib-error')) { + return el; + } + return null; + } + + function bibRangeClearRangeError(row) { + var errEl = bibRangeGetErrorEl(row); + if (!errEl) return; + + var msg = errEl.querySelector('.bib-range-validate-msg'); + if (msg) { + msg.remove(); + } + + if (!errEl.querySelector('div:not(.bib-range-validate-msg)') && !errEl.textContent.trim()) { + errEl.style.display = 'none'; + } + } + + function bibRangeClearFieldClasses(row) { + var fields = bibRangeGetFields(row); + if (!fields) return; + [fields.start, fields.qty, fields.end].forEach(function (el) { + if (!el) return; + el.classList.remove('bib-field--idle', 'bib-field--pick', 'bib-field--calc', 'bib-field--active'); + }); + } + + function bibRangeClearValidationUi(row) { + var fields = bibRangeGetFields(row); + if (!fields) return; + + [fields.start, fields.qty, fields.end].forEach(function (el) { + if (el) { + el.classList.remove('bib-field--invalid'); + } + }); + + bibRangeClearRangeError(row); + } + + /** MSIN-4429 — Validation différée (blur / OK), pas pendant la frappe. */ + function bibRangeValidate(row) { + var fields = bibRangeGetFields(row); + if (!fields) return null; + + var start = bibRangeParseInt(fields.start && fields.start.value); + if (start <= 0) { + return null; + } + + var pilot = row.dataset.rangePilot || ''; + if (!pilot) { + return null; + } + + if (pilot === 'end') { + var strEnd = String(fields.end && fields.end.value || '').trim(); + if (strEnd === '') { + return null; + } + var end = bibRangeParseInt(strEnd); + if (end <= 0) { + return { key: 'incomplete', field: 'end' }; + } + if (end < start) { + return { key: 'invalidEnd', field: 'end', start: start }; + } + } + + if (pilot === 'qty') { + var strQty = String(fields.qty && fields.qty.value || '').trim(); + if (strQty === '') { + return null; + } + var qty = bibRangeParseInt(strQty); + if (qty <= 0) { + return { key: 'invalidQty', field: 'qty' }; + } + } + + return null; + } + + function bibRangeValidationMessage(validation) { + if (!validation) { + return ''; + } + if (validation.key === 'invalidEnd') { + return bibJsFmt('jsRangeInvalidEnd', validation.start); + } + if (validation.key === 'invalidQty') { + return bibJs('jsRangeInvalidQty'); + } + if (validation.key === 'incomplete') { + return bibJs('jsRangeIncomplete') || bibJs('jsFieldsRequired'); + } + return bibJs('jsRangeInvalid') || bibJs('jsFieldsRequired'); + } + + function bibRangeShowValidation(row, validation) { + var fields = bibRangeGetFields(row); + if (!fields) { + return; + } + + bibRangeClearValidationUi(row); + + if (!validation) { + return; + } + + var target = validation.field === 'qty' ? fields.qty : fields.end; + if (target) { + target.classList.add('bib-field--invalid'); + } + + var errEl = bibRangeGetErrorEl(row); + if (!errEl) { + return; + } + + var msgEl = errEl.querySelector('.bib-range-validate-msg'); + if (!msgEl) { + msgEl = document.createElement('div'); + msgEl.className = 'bib-range-validate-msg'; + errEl.insertBefore(msgEl, errEl.firstChild); + } + msgEl.textContent = bibRangeValidationMessage(validation); + errEl.style.display = 'block'; + } + + function bibRangeValidateOnBlur(row) { + applyBibRangeFieldState(row); + bibRangeShowValidation(row, bibRangeValidate(row)); + } + + function applyBibRangeFieldState(row) { + var fields = bibRangeGetFields(row); + if (!fields || !fields.start || !fields.qty || !fields.end) return; + + if (row.dataset.locked === '1') { + bibRangeClearFieldClasses(row); + return; + } + + var start = bibRangeParseInt(fields.start.value); + var pilot = row.dataset.rangePilot || ''; + + bibRangeClearFieldClasses(row); + + if (start <= 0) { + fields.qty.classList.add('bib-field--idle'); + fields.end.classList.add('bib-field--idle'); + fields.qty.readOnly = true; + fields.end.readOnly = true; + fields.qty.value = ''; + fields.end.value = ''; + row.dataset.rangePilot = ''; + return; + } + + if (!pilot) { + fields.qty.classList.add('bib-field--pick'); + fields.end.classList.add('bib-field--pick'); + fields.qty.readOnly = true; + fields.end.readOnly = true; + return; + } + + if (pilot === 'qty') { + var qty = bibRangeParseInt(fields.qty.value); + fields.qty.classList.add('bib-field--active'); + fields.end.classList.add('bib-field--calc'); + fields.qty.readOnly = false; + fields.end.readOnly = true; + if (qty > 0) { + fields.end.value = String(start + qty - 1); + } else { + fields.end.value = ''; + } + } else if (pilot === 'end') { + var end = bibRangeParseInt(fields.end.value); + fields.end.classList.add('bib-field--active'); + fields.qty.classList.add('bib-field--calc'); + fields.end.readOnly = false; + fields.qty.readOnly = true; + if (end >= start) { + fields.qty.value = String(end - start + 1); + } else { + fields.qty.value = ''; + } + } + } + + function onBibRangeFieldPick(row, pilot) { + if (!row || row.dataset.locked === '1') return; + + var fields = bibRangeGetFields(row); + if (!fields || !fields.start) return; + + if (bibRangeParseInt(fields.start.value) <= 0) { + fields.start.focus(); + return; + } + + row.dataset.rangePilot = pilot; + applyBibRangeFieldState(row); + + var target = pilot === 'qty' ? fields.qty : fields.end; + if (target) { + target.focus(); + target.select(); + } + } + + function onBibRangeStartChange(row) { + var fields = bibRangeGetFields(row); + if (!fields || !fields.start) return; + + var start = bibRangeParseInt(fields.start.value); + var lastStart = bibRangeParseInt(row.dataset.rangeLastStart || '0'); + + if (start !== lastStart && !row.dataset.rangePilot) { + fields.qty.value = ''; + fields.end.value = ''; + row.dataset.hasValues = '0'; + } + + row.dataset.rangeLastStart = String(start); + + if (start <= 0) { + row.dataset.rangePilot = ''; + } + + applyBibRangeFieldState(row); + } + + function bindBibRangeRow(row) { + if (!row || row.dataset.bibRangeBound === '1') return; + row.dataset.bibRangeBound = '1'; + + var fields = bibRangeGetFields(row); + if (!fields || !fields.start || !fields.qty || !fields.end) return; + + row.dataset.rangeLastStart = String(bibRangeParseInt(fields.start.value)); + + fields.start.addEventListener('input', function () { + bibRangeClearValidationUi(row); + onBibRangeStartChange(row); + }); + + fields.qty.addEventListener('input', function () { + bibRangeClearValidationUi(row); + row.dataset.rangePilot = 'qty'; + applyBibRangeFieldState(row); + }); + + fields.end.addEventListener('input', function () { + bibRangeClearValidationUi(row); + row.dataset.rangePilot = 'end'; + applyBibRangeFieldState(row); + }); + + fields.start.addEventListener('blur', function () { + bibRangeValidateOnBlur(row); + }); + + fields.qty.addEventListener('blur', function () { + bibRangeValidateOnBlur(row); + }); + + fields.end.addEventListener('blur', function () { + bibRangeValidateOnBlur(row); + }); + + fields.qty.addEventListener('click', function () { + onBibRangeFieldPick(row, 'qty'); + }); + + fields.end.addEventListener('click', function () { + onBibRangeFieldPick(row, 'end'); + }); + + fields.qty.addEventListener('focus', function () { + onBibRangeFieldPick(row, 'qty'); + }); + + fields.end.addEventListener('focus', function () { + onBibRangeFieldPick(row, 'end'); + }); + + applyBibRangeFieldState(row); + + if (row.dataset.id === '0' && !fields.start.value) { + fields.start.focus(); + } + } + + function initBibRangeRows(root) { + if (!root) return; + root.querySelectorAll('.bib-range').forEach(bindBibRangeRow); + } + + function bibCsrfToken() { + return moduleBib ? (moduleBib.dataset.csrfToken || '') : ''; + } + + initModuleBibTippy(moduleBib); + initBibRangeRows(moduleBib); + + // ============================================================ + // MSIN-4379 — Étape 2 : assignation automatique (interface JS) + // Prochain tour : étape 6 (fxAssignAutoBibForParticipant), étape 7 (fxMajCommande). + // ============================================================ + + /** MSIN-4379 — Compte les séquences cochées et met à jour le texte d'aide. */ + function updateAutoSelectionInfo(row) { + + if (!row) return; + + let count = row.querySelectorAll('.batch-select-range:checked').length; + let info = row.querySelector('.auto-info-text'); + if (!info) return; + + if (count === 0) { + info.textContent = bibJs('jsAutoSelectHint'); + } else { + info.textContent = bibJsFmt('jsAutoSelectCount', count); + } + } + + function syncAutoUiState(row, isActive) { + if (!row) return; + + let btnAuto = row.querySelector('.btn-auto'); + row.dataset.autoActive = isActive ? '1' : '0'; + row.classList.toggle('auto-enabled', isActive); + + if (btnAuto) { + btnAuto.textContent = isActive ? row.dataset.autoLabelConfig : row.dataset.autoLabel; + btnAuto.classList.toggle('btn-bib-auto--active', isActive); + let autoAide = bibQuery(row, '.epr-action-auto-group .epr-btn-aide-segment .bib-aide-trigger'); + if (autoAide) { + autoAide.classList.add('btn-aide-tone-auto'); + autoAide.classList.remove('btn-aide-tone-success', 'btn-aide-tone-secondary'); + } + } + } + + /** MSIN-4379 — Quitte le mode édition auto sans sauver ; recharge les plages (mode assign). */ + function exitAutoMode(row) { + + if (!row) return; + + row.classList.remove('auto-mode'); + + let container = row.querySelector('.bib-container'); + if (container) { + container.classList.remove('batch-mode'); + } + + row.querySelectorAll('.bib-start, .bib-qty, .bib-end').forEach(input => { + input.removeAttribute('readonly'); + }); + + bibFetch('/ajax_bib_range.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: + 'action=refresh_ranges' + + '&epr_id=' + encodeURIComponent(row.dataset.eprId) + + '&mode=assign' + + bibAjaxLangSuffix() + }) + .then(res => res.json()) + .then(data => { + + if (!data.success) return; + + let bibContainer = setBibContainerHtml(row, data.html); + }); + } + + /** MSIN-4379 — Ouvre le mode édition : checkboxes, bloc Accepter/Annuler, refresh mode=auto. */ + function enterAutoMode(row) { + + if (!row) return; + + if (row.classList.contains('batch-mode') || row.classList.contains('reset-mode')) { + return; + } + + if (bibBlockAutoActivation(row)) { + return; + } + + row.classList.add('auto-mode'); + + let container = row.querySelector('.bib-container'); + if (container) { + container.classList.add('batch-mode'); + } + + row.querySelectorAll('.bib-start, .bib-qty, .bib-end').forEach(input => { + input.setAttribute('readonly', true); + }); + + syncBibUiState(row); + + bibFetch('/ajax_bib_range.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: + 'action=refresh_ranges' + + '&epr_id=' + encodeURIComponent(row.dataset.eprId) + + '&mode=auto' + + bibAjaxLangSuffix() + }) + .then(res => res.json()) + .then(data => { + + if (!data.success) return; + + let bibContainer = setBibContainerHtml(row, data.html); + if (!bibContainer) return; + + bibContainer.classList.add('batch-mode'); + + row.querySelectorAll('.bib-start, .bib-qty, .bib-end').forEach(input => { + input.setAttribute('readonly', true); + }); + + updateAutoSelectionInfo(row); + syncBibUiState(row); + }); + } + + function syncBibUiState(row) { + if (!row) return; + + let modes = bibRowModes(row); + let avecBib = parseInt(bibQuery(row, '.bib-avec')?.textContent || '0', 10) || 0; + let hasSequences = bibHasSavedSequences(row); + let hasSelection = bibCountSelectedRanges(row) > 0; + let inBatchFlow = modes.batch || modes.reset; + let inAutoFlow = modes.auto; + let inPickFlow = inBatchFlow || inAutoFlow; + + row.querySelectorAll('.team-mode-box input[type="radio"]').forEach(function (radio) { + if (!radio.closest('.team-mode-option.disabled')) { + radio.disabled = avecBib > 0; + } + }); + + let showReset = hasSequences && (avecBib > 0 || modes.reset) + && !modes.auto + && (!modes.batch || modes.reset); + bibSetVisible('.epr-action-batch-group', + hasSequences && ((!modes.reset && !modes.auto) || modes.batch), row); + bibSetVisible('.epr-action-reset-group', showReset, row); + bibSetVisible('.epr-action-auto-group', + hasSequences && !modes.reset && !modes.auto && !modes.batch, row); + + if (hasSequences) { + syncAutoUiState(row, row.dataset.autoActive === '1'); + } + + bibSetVisible('.epr-add-range-row', !inPickFlow, row); + bibSetVisible('.batch-config-container', inBatchFlow, row); + bibSetVisible('.auto-config-container', inAutoFlow, row); + + let showPick = inPickFlow && !hasSelection; + let showBatchReady = inBatchFlow && hasSelection; + let showAutoReady = inAutoFlow && hasSelection; + bibSetVisible('.bib-seq-pick-prompt', showPick, row); + bibSetVisible('.batch-config-ready', showBatchReady, row); + bibSetVisible('.auto-config-ready', showAutoReady, row); + + let bibContainer = getBibContainer(row); + if (bibContainer) { + bibContainer.classList.toggle('batch-awaiting-selection', showPick); + } + + bibSetVisible('.epr-action-desactiver-group', + hasSequences && row.dataset.autoActive === '1' && inAutoFlow, row); + bibSetVisible('.epr-action-cancel-auto-group', inAutoFlow, row); + bibSetVisible('.epr-action-accept-auto-group', showAutoReady, row); + bibSetVisible('.epr-auto-info-group', showAutoReady, row); + bibSetVisible('.batch-reset-warning', showBatchReady && modes.reset, row); + bibSetVisible('.epr-batch-order-wrap', showBatchReady && !modes.reset, row); + bibSetVisible('.epr-action-sim-group', showBatchReady && !modes.reset, row); + bibSetVisible('.epr-action-go-group', showBatchReady, row); + } + + function updateBatchAnalysis(row) { + + if (!row) return; + + let totalDispo = 0; + let totalReset = 0; + + // ===================== + // TOTAL DISPO + // Source officielle = PHP + // JS ne recalcule PAS les disponibilités. + // Il additionne seulement les valeurs affichées dans .bib-dispo. + // ===================== + row.querySelectorAll('.batch-select-range:checked').forEach(cb => { + + let line = cb.closest('.epr-line'); + if (!line) return; + + let usedDiv = line.querySelector('.bib-used'); + let dispoDiv = line.querySelector('.bib-dispo'); + + let used = parseInt((usedDiv?.textContent || '0'), 10); + let dispo = parseInt((dispoDiv?.textContent || '0'), 10); + + if (isNaN(used)) { + used = 0; + } + + if (isNaN(dispo)) { + dispo = 0; + } + + totalReset += used; + totalDispo += dispo; + }); + + bibFetch('/ajax_bib_range.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: 'action=batch_analysis&epr_id=' + encodeURIComponent(row.dataset.eprId) + bibAjaxLangSuffix() + }, { loader: false }) + .then(res => res.json()) + .then(data => { + + if (!data.success) return; + + bibSyncEprBibStats(row, data.info); + syncBibUiState(row); + let sansBib = bibRowPendingBibs(row); + let isResetMode = row.dataset.mode === 'reset'; + let message = ''; + + if (isResetMode) { + + if (totalReset === 0) { + message = bibJs('jsBatchNone'); + } else { + message = bibJsFmt('jsBatchResetCount', totalReset); + } + + } else { + + if (totalDispo === 0) { + message = bibJs('jsBatchNone'); + } else if (totalDispo < sansBib) { + message = bibJsFmt('jsBatchSummaryShort', totalDispo, sansBib, sansBib - totalDispo); + } else { + message = bibJsFmt('jsBatchSummaryOk', totalDispo, sansBib); + } + + } + + let info = row.querySelector('.epr-actions .batch-info-text'); + if (info && bibCountSelectedRanges(row) > 0) { + info.textContent = message; + } + + syncBibUiState(row); + }); + } + +// ===================== +// BATCH CHECKBOX CHANGE (PROPRE) +// ===================== + moduleBib.addEventListener('change', function (e) { + + // ===================== + // TEAM MODE + // ===================== + let teamRadio = e.target.closest('.team-mode-box input[type="radio"]'); + + if (teamRadio) { + + if (teamRadio.disabled) return; + + let box = teamRadio.closest('.team-mode-box'); + if (!box) return; + + bibFetch('/ajax_bib_range.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: + 'action=save_team_mode' + + '&epr_id=' + encodeURIComponent(box.dataset.eprId) + + '&team_mode=' + encodeURIComponent(teamRadio.value) + + bibAjaxLangSuffix() + }, { loader: false }); + + return; + } + + let cb = e.target.closest('.batch-select-range'); + if (!cb) return; + + let row = cb.closest('.epr-row'); + if (!row) return; + + // MSIN-4379 — Mode auto : même sélection de séquences que le batch, sans GO/simulation. + if (row.classList.contains('auto-mode')) { + updateAutoSelectionInfo(row); + syncBibUiState(row); + return; + } + + if (row.dataset.loading === '1') return; + let existing = document.querySelector('.epr-row-view'); + if (existing) existing.remove(); + row.dataset.loading = '1'; + + // 1. garder sélection + let selected = []; + row.querySelectorAll('.batch-select-range:checked').forEach(el => { + selected.push(el.dataset.rangeId); + }); + + // 2. analyse gauche/droite + updateBatchAnalysis(row); + + // 3. sortir du cycle du change + setTimeout(() => { + + bibFetch('/ajax_bib_range.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: + 'action=refresh_ranges' + + '&epr_id=' + encodeURIComponent(row.dataset.eprId) + + '&mode=' + encodeURIComponent(row.dataset.mode || 'assign') + + bibAjaxLangSuffix() + }, { loader: false }) + .then(res => res.json()) + .then(data => { + + if (!data.success) { + row.dataset.loading = '0'; + return; + } + + setBibContainerHtml(row, data.html); + let container = getBibContainer(row); + if (!container) { + row.dataset.loading = '0'; + return; + } + + // 4. refresh complet serveur + // Re-sync après refresh serveur + + container.classList.add('batch-mode'); + + // 5. remettre readonly + container.querySelectorAll('.bib-start, .bib-qty, .bib-end').forEach(input => { + input.setAttribute('readonly', true); + }); + + // 6. restaurer checkbox + container.querySelectorAll('.batch-select-range').forEach(el => { + if (selected.includes(el.dataset.rangeId)) { + el.checked = true; + } + }); + updateBatchAnalysis(row); + + row.dataset.loading = '0'; + + }) + .catch(() => { + row.dataset.loading = '0'; + }); + + }, 0); + }); + + moduleBib.querySelectorAll('.epr-row').forEach(syncBibUiState); + + moduleBib.addEventListener('click', function (e) { + + let btnGotoAssign = e.target.closest('.bib-anomaly-goto-assign'); + if (btnGotoAssign) { + e.preventDefault(); + + let eprId = btnGotoAssign.dataset.eprId; + if (!eprId) { + return; + } + + let row = moduleBib.querySelector('.epr-row[data-epr-id="' + eprId + '"]'); + if (row) { + row.scrollIntoView({ behavior: 'smooth', block: 'start' }); + let btnView = row.querySelector('.btn-view-range[data-range-id="0"]'); + if (btnView) { + btnView.click(); + } + } + return; + } + + let btnTrad = e.target.closest('.btn-admin-trad, .btn-bib-trad'); + if (btnTrad) { + e.preventDefault(); + e.stopPropagation(); + let url = btnTrad.getAttribute('data-trad-url'); + if (!url) { + return; + } + let clef = btnTrad.getAttribute('data-info-clef') || 'bib_trad'; + let popup = window.open( + url, + 'bib_trad_' + clef, + 'width=960,height=640,scrollbars=yes,resizable=yes' + ); + if (popup) { + let timer = setInterval(function () { + if (popup.closed) { + clearInterval(timer); + window.location.reload(); + } + }, 400); + } + return; + } + + // ===================== + // LOCK / UNLOCK SEQUENCE + // ===================== + let btnLock = e.target.closest('.btn-bib-lock'); + if (btnLock) { + e.preventDefault(); + + let row = btnLock.closest('.epr-row'); + if (!row) return; + + let isLocked = btnLock.dataset.locked === '1'; + let confirmMsg = isLocked + ? bibJsConfirm('jsUnlockConfirm') + : bibJsConfirm('jsLockConfirm'); + + if (!confirm(confirmMsg)) { + return; + } + + bibFetch('/ajax_bib_range.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: + 'action=toggle_lock' + + '&epr_bib_id=' + encodeURIComponent(btnLock.dataset.rangeId) + + '&epr_id=' + encodeURIComponent(btnLock.dataset.eprId || row.dataset.eprId) + + '&locked=' + encodeURIComponent(isLocked ? '0' : '1') + + bibAjaxLangSuffix() + }) + .then(res => res.json()) + .then(data => { + + if (!data.success) { + alert(data.message || bibJs('jsErrGeneric')); + return; + } + + if (typeof data.auto_active !== 'undefined') { + row.dataset.autoActive = data.auto_active ? '1' : '0'; + syncAutoUiState(row, data.auto_active); + syncBibUiState(row); + } + + if (data.html) { + setBibContainerHtml(row, data.html); + } + }); + + return; + } + + // ===================== + // DELETE + // ===================== + let btnDelete = e.target.closest('.btn-delete-range'); + if (btnDelete) { + + + e.preventDefault(); + + let container = btnDelete.closest('.bib-container'); + let errorDiv = container.querySelector('.bib-error-global'); + + // reset erreur + if (errorDiv) { + errorDiv.style.display = 'none'; + errorDiv.innerText = ''; + } + + let id = btnDelete.dataset.id; + if (!id) return; + + bibFetch('/ajax_bib_range.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: 'action=delete&id=' + encodeURIComponent(id) + bibAjaxLangSuffix() + }) + .then(res => res.text()) + .then(raw => { + + let data; + try { + data = JSON.parse(raw); + } catch (err) { + if (errorDiv) { + errorDiv.innerText = bibJs('jsServerError'); + errorDiv.style.display = 'block'; + } + return; + } + + if (!data.success) { + if (errorDiv) { + errorDiv.innerText = data.message || bibJs('jsErrGeneric'); + errorDiv.style.display = 'block'; + } + return; + } + + setBibContainerHtml(btnDelete.closest('.epr-row'), data.html); + }); + + return; + } + + // ===================== + // SAVE + // ===================== + let btnSave = e.target.closest('.btn-save-range'); + if (btnSave) { + e.preventDefault(); + + let row = btnSave.closest('.bib-range'); + let errorDiv = row.nextElementSibling; + let epr_id = btnSave.dataset.eprId; + + // reset + if (errorDiv) { + errorDiv.style.display = 'none'; + errorDiv.innerText = ''; + } + + applyBibRangeFieldState(row); + + let start = bibRangeParseInt(row.querySelector('.bib-start').value); + let end = bibRangeParseInt(row.querySelector('.bib-end').value); + let id = btnSave.dataset.id; + let validation = bibRangeValidate(row); + + // ligne temporaire vide = on la retire + if (id == 0 && start <= 0) { + row.remove(); + if (errorDiv && errorDiv.classList.contains('bib-error')) { + errorDiv.remove(); + } + return; + } + + if (start <= 0 || end <= 0) { + validation = validation || { key: 'incomplete' }; + } + + if (!validation && end < start) { + validation = { key: 'invalidEnd', field: 'end', start: start }; + } + + if (validation) { + bibRangeShowValidation(row, validation); + if (errorDiv) { + var msgEl = errorDiv.querySelector('.bib-range-validate-msg'); + if (!msgEl) { + errorDiv.innerText = bibRangeValidationMessage(validation); + } + errorDiv.style.display = 'block'; + } + var invalidField = validation.field === 'qty' + ? row.querySelector('.bib-qty') + : row.querySelector('.bib-end'); + if (invalidField) { + invalidField.focus(); + } + return; + } + + bibFetch('/ajax_bib_range.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: + 'action=save' + + '&id=' + encodeURIComponent(id) + + '&epr_id=' + encodeURIComponent(epr_id) + + '&start=' + encodeURIComponent(start) + + '&end=' + encodeURIComponent(end) + + bibAjaxLangSuffix() + }) + .then(res => res.text()) + .then(raw => { + + let data; + try { + data = JSON.parse(raw); + } catch (err) { + errorDiv.innerText = bibJs('jsServerError'); + errorDiv.style.display = 'block'; + return; + } + + if (!data.success) { + errorDiv.innerText = data.message || bibJs('jsErrGeneric'); + errorDiv.style.display = 'block'; + return; + } + + setBibContainerHtml(btnSave.closest('.epr-row'), data.html); + }); + + return; + } +// ===================== +// VIEW +// ===================== + let btnView = e.target.closest('.btn-view-range'); + if (btnView) { + e.preventDefault(); + + let epr_id = btnView.dataset.eprId; + let range_id = btnView.dataset.rangeId; + + console.log('epr_id:', epr_id); + console.log('range_id:', range_id); + if (!epr_id || typeof range_id === 'undefined') return; + + let container = btnView.closest('.epr-block'); + + // Vérifier si une vue est déjà ouverte + let existing = document.querySelector('.epr-row-view'); + + // Toggle : si c'est le même œil qui a ouvert la vue, on la ferme et on s'arrête + if (existing + && existing.dataset.eprId === epr_id + && existing.dataset.rangeId === range_id + ) { + existing.remove(); + return; + } + + // Un autre œil a été cliqué : fermer la vue précédente avant d'ouvrir la nouvelle + if (existing) { + existing.remove(); + } + + bibFetch('/ajax_bib_range.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: + 'action=view' + + '&epr_id=' + encodeURIComponent(epr_id) + + '&range_id=' + encodeURIComponent(range_id) + + bibAjaxLangSuffix() + }) + .then(res => res.text()) + .then(raw => { + + let data; + try { + data = JSON.parse(raw); + } catch (err) { + console.log(bibJs('jsServerError')); + return; + } + + if (!data.success) { + console.log(data.message || bibJs('jsErrGeneric')); + return; + } + + let row = btnView.closest('.epr-row'); + + // Injecter la nouvelle vue sous la ligne du bouton cliqué, + // en mémorisant epr_id et range_id pour le toggle suivant + row.insertAdjacentHTML('afterend', data.html); + + // Stocker les identifiants sur la vue injectée pour pouvoir détecter + // le 2e clic sur le même œil + let newView = row.nextElementSibling; + if (newView && newView.classList.contains('epr-row-view')) { + newView.dataset.eprId = epr_id; + newView.dataset.rangeId = range_id; + } + }); + + return; + } + + + // ===================== +// CLOSE VIEW +// ===================== + let btnClose = e.target.closest('.btn-close-view'); + if (btnClose) { + e.preventDefault(); + + let view = btnClose.closest('.epr-row-view'); + if (view) { + view.remove(); + } + + return; + } +// ===================== +// ASSIGN BATCH +// ===================== +// Active/désactive le mode batch pour UNE épreuve. +// - Affiche les checkbox +// - Bloque Début/Fin +// - Affiche le bloc de configuration +// - Recalcule l’analyse initiale +// ===================== + let btnBatch = e.target.closest('.btn-assign-batch'); + if (btnBatch) { + + e.preventDefault(); + + let row = btnBatch.closest('.epr-row'); + if (!row) return; + + // MSIN-4379 — Batch et auto sont mutuellement exclusifs. + if (row.classList.contains('auto-mode')) { + return; + } + + let container = row.querySelector('.bib-container'); + if (!container) return; + + // OFF + if (row.classList.contains('batch-mode')) { + + row.classList.remove('batch-mode'); + row.querySelectorAll('.batch-select-range').forEach(cb => { + cb.disabled = false; + }); + container.classList.remove('batch-mode'); + + row.querySelectorAll('.bib-start, .bib-qty, .bib-end').forEach(input => { + input.removeAttribute('readonly'); + }); + + btnBatch.textContent = row.dataset.batchLabel || btnBatch.textContent; + + let goBtn = row.querySelector('.btn-go-batch'); + if (goBtn) { + goBtn.textContent = row.dataset.goLabel || goBtn.textContent; + } + row.querySelectorAll('.batch-select-range').forEach(cb => { + cb.checked = false; + }); + let info = row.querySelector('.epr-actions .batch-info-text'); + if (info) info.textContent = ''; + syncBibUiState(row); + return; + } + + // ON + row.classList.add('batch-mode'); + container.classList.add('batch-mode'); + + row.querySelectorAll('.bib-start, .bib-qty, .bib-end').forEach(input => { + input.setAttribute('readonly', true); + }); + + btnBatch.textContent = row.dataset.batchLabelCancel || btnBatch.textContent; + let goBtn = row.querySelector('.btn-go-batch'); + if (goBtn) { + goBtn.textContent = row.dataset.goLabel || goBtn.textContent; + } + if (typeof updateBatchAnalysis === 'function') { + updateBatchAnalysis(row); + } + syncBibUiState(row); + + return; + } +// ===================== +// SIMULER BATCH +// ===================== + let btnSim = e.target.closest('.btn-simuler-batch'); + if (btnSim) { + + e.preventDefault(); + + let row = btnSim.closest('.epr-row'); + if (!row) return; + + let selected = []; + + row.querySelectorAll('.batch-select-range:checked').forEach(cb => { + selected.push(cb.dataset.rangeId); + }); + + if (selected.length === 0) { + alert(bibJs('jsNoSeqSelected')); + return; + } + let isResetMode = row.dataset.mode === 'reset'; + let ba_id = row.querySelector('.batch-order').value; + + bibFetch('/ajax_bib_range.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: + (isResetMode ? 'action=reset_preview' : 'action=batch_go') + + '&epr_id=' + encodeURIComponent(row.dataset.eprId) + + '&ranges=' + encodeURIComponent(JSON.stringify(selected)) + + '&ba_id=' + encodeURIComponent(ba_id) + + bibAjaxLangSuffix() + }) + .then(res => res.json()) + .then(data => { + + if (!data.success) { + alert(data.message || bibJs('jsErrGeneric')); + return; + } + + let existing = document.querySelector('.epr-row-view'); + if (existing) { + existing.remove(); + } + + row.insertAdjacentHTML('afterend', data.html); + + + }); + + return; + } + +// ===================== +// RESET MODE +// ===================== + let btnReset = e.target.closest('.btn-reset'); + if (btnReset) { + + e.preventDefault(); + + let row = btnReset.closest('.epr-row'); + if (!row) return; + + // MSIN-4379 — Reset et auto sont mutuellement exclusifs. + if (row.classList.contains('auto-mode')) { + return; + } + + let container = row.querySelector('.bib-container'); + if (!container) return; + + // ===================== + // OFF + // ===================== + if (row.classList.contains('reset-mode')) { + + row.classList.remove('reset-mode'); + btnReset.textContent = row.dataset.resetLabel || btnReset.textContent; + container.classList.remove('batch-mode'); + + row.querySelectorAll('.bib-start, .bib-qty, .bib-end').forEach(input => { + input.removeAttribute('readonly'); + }); + + row.querySelectorAll('.batch-select-range').forEach(cb => { + cb.checked = false; + }); + + let goBtn = row.querySelector('.btn-go-batch'); + if (goBtn) { + goBtn.textContent = row.dataset.goLabel || goBtn.textContent; + } + + let info = row.querySelector('.epr-actions .batch-info-text'); + if (info) { + info.innerHTML = ''; + } + row.dataset.mode = ''; + syncBibUiState(row); + return; + } + + // ===================== + // ON + // ===================== + row.classList.add('reset-mode'); + btnReset.textContent = row.dataset.resetCancelLabel || btnReset.textContent; + row.dataset.mode = 'reset'; + container.classList.add('batch-mode'); + + row.querySelectorAll('.bib-start, .bib-qty, .bib-end').forEach(input => { + input.setAttribute('readonly', true); + }); + + syncBibUiState(row); + + let goBtn = row.querySelector('.btn-go-batch'); + if (goBtn) { + goBtn.textContent = row.dataset.resetLabel || goBtn.textContent; + } + + bibFetch('/ajax_bib_range.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: + 'action=refresh_ranges' + + '&epr_id=' + encodeURIComponent(row.dataset.eprId) + + '&mode=reset' + + bibAjaxLangSuffix() + }) + .then(res => res.json()) + .then(data => { + + if (!data.success) return; + + let container = setBibContainerHtml(row, data.html); + if (!container) return; + + container.classList.add('batch-mode'); + + row.querySelectorAll('.bib-start, .bib-qty, .bib-end').forEach(input => { + input.setAttribute('readonly', true); + }); + + syncBibUiState(row); + }); + + return; + } + + + + +// ===================== +// GO BATCH +// ===================== + let btnGo = e.target.closest('.btn-go-batch'); + if (btnGo) { + + e.preventDefault(); + + let row = btnGo.closest('.epr-row'); + if (!row) return; + + let selected = []; + + row.querySelectorAll('.batch-select-range:checked').forEach(cb => { + selected.push(cb.dataset.rangeId); + }); + + if (selected.length === 0) { + alert(bibJs('jsNoSeqSelected')); + return; + } + let isResetMode = row.dataset.mode === 'reset'; + let ba_id = row.querySelector('.batch-order').value; + + if (isResetMode) { + bibFetch('/ajax_bib_range.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: + 'action=reset_apply' + + '&epr_id=' + encodeURIComponent(row.dataset.eprId) + + '&ranges=' + encodeURIComponent(JSON.stringify(selected)) + + '&ba_id=' + encodeURIComponent(ba_id) + + bibAjaxLangSuffix() + }) + .then(res => res.json()) + .then(data => { + + if (!data.success) { + alert(data.message || bibJs('jsErrGeneric')); + return; + } + + bibAfterBatchGoSuccess(row, data, true); + }); + + return; + } + + bibRunBatchApplyChunks(row, selected, ba_id); + + return; + } + +// ===================== +// MSIN-4379 — Étape 2 : AUTO DOSSARD (clics Accepter / Annuler / Désactiver / Activer) +// Prochain tour : étape 6–7 (assignation PHP à l'inscription). +// ===================== + let btnAccepterAuto = e.target.closest('.btn-accepter-auto'); + if (btnAccepterAuto) { + + e.preventDefault(); + + let row = btnAccepterAuto.closest('.epr-row'); + if (!row) return; + + let selected = []; + + row.querySelectorAll('.batch-select-range:checked').forEach(cb => { + selected.push(cb.dataset.rangeId); + }); + + if (selected.length === 0) { + alert(bibJs('jsNoSeqSelected')); + return; + } + + if (bibRowPendingBibs(row) > 0) { + bibAlertAutoPendingBibs(row); + return; + } + + // MSIN-4379 — Persiste epr_bib_auto=1 sur les séquences choisies (save_auto_config). + bibFetch('/ajax_bib_range.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: + 'action=save_auto_config' + + '&epr_id=' + encodeURIComponent(row.dataset.eprId) + + '&active=1' + + '&ranges=' + encodeURIComponent(JSON.stringify(selected)) + + bibAjaxLangSuffix() + }) + .then(res => res.json()) + .then(data => { + + if (!data.success) { + alert(data.message || bibJs('jsErrGeneric')); + return; + } + + syncAutoUiState(row, true); + exitAutoMode(row); + syncBibUiState(row); + }); + + return; + } + + // MSIN-4379 — Annule l'édition sans appeler save_auto_config. + let btnAnnulerAuto = e.target.closest('.btn-annuler-auto'); + if (btnAnnulerAuto) { + + e.preventDefault(); + + let row = btnAnnulerAuto.closest('.epr-row'); + if (!row) return; + + exitAutoMode(row); + return; + } + + // MSIN-4379 — Désactive l'auto en base (active=0, toutes les séquences à 0). + let btnDesactiverAuto = e.target.closest('.btn-desactiver-auto'); + if (btnDesactiverAuto) { + + e.preventDefault(); + + let row = btnDesactiverAuto.closest('.epr-row'); + if (!row) return; + + if (!confirm(bibJs('jsAutoDisableConfirm'))) { + return; + } + + bibFetch('/ajax_bib_range.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: + 'action=save_auto_config' + + '&epr_id=' + encodeURIComponent(row.dataset.eprId) + + '&active=0' + + '&ranges=' + encodeURIComponent('[]') + + bibAjaxLangSuffix() + }) + .then(res => res.json()) + .then(data => { + + if (!data.success) { + alert(data.message || bibJs('jsErrGeneric')); + return; + } + + syncAutoUiState(row, false); + exitAutoMode(row); + syncBibUiState(row); + }); + + return; + } + + // MSIN-4379 — Entrée dans le mode configuration auto. + let btnAuto = e.target.closest('.btn-auto'); + if (btnAuto) { + + e.preventDefault(); + + let row = btnAuto.closest('.epr-row'); + if (!row) return; + + if (row.classList.contains('batch-mode') || row.classList.contains('reset-mode')) { + return; + } + + enterAutoMode(row); + return; + } + + // ===================== + // CREATE + // ===================== + + let btn = e.target.closest('.btn-add-range'); + if (!btn) return; + + e.preventDefault(); + + let container = btn.closest('.bib-container'); + let errorDiv = container.querySelector('.bib-error-global'); + + // reset + if (errorDiv) { + errorDiv.style.display = 'none'; + errorDiv.innerText = ''; + } + + let epr_id = btn.dataset.eprId; + if (!epr_id) return; + + bibFetch('/ajax_bib_range.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: 'action=create&epr_id=' + encodeURIComponent(epr_id) + bibAjaxLangSuffix() + }) + .then(res => res.text()) + .then(raw => { + + let data; + try { + data = JSON.parse(raw); + } catch (err) { + if (errorDiv) { + errorDiv.innerText = bibJs('jsServerError'); + errorDiv.style.display = 'block'; + } + return; + } + + if (!data.success) { + console.log('CREATE ERROR:', data); + console.log('container:', container); + console.log('errorDiv:', errorDiv); + if (errorDiv) { + errorDiv.innerText = data.message || bibJs('jsErrGeneric'); + errorDiv.style.display = 'block'; + } + return; + } + + setBibContainerHtml(btn.closest('.epr-row'), data.html); + }); + }); + } +})(); diff --git a/js/v2/inscr-gestion.js b/js/v2/inscr-gestion.js new file mode 100644 index 0000000..fdf1642 --- /dev/null +++ b/js/v2/inscr-gestion.js @@ -0,0 +1,895 @@ +/** + * MSIN-4435 — Gestion inscriptions v2 + * Chargé sur inc_tableau_inscriptions_gestion et mobile.php (entrée gestion). + */ +(function () { + 'use strict'; + + var cfg = window.MS1_V2_INSCR_GESTION || {}; + + function ms1LoaderInlineHtml() { + if (window.Ms1Loader) { + return Ms1Loader.inlineHtml(cfg.preloaderWait || undefined); + } + return '
'; + } + + if (typeof jQuery !== 'undefined') { + jQuery(function ($) { + if ($('#frm_inscr_gestion_login').length && cfg.loginMessages) { + $('#frm_inscr_gestion_login').validate({ + messages: { + txt_login: cfg.loginMessages.txt_login || '', + txt_password: cfg.loginMessages.txt_password || '' + }, + submitHandler: function (form) { + if (window.Ms1Loader) { + Ms1Loader.show(cfg.preloaderWait || undefined); + } else { + $('#preloader').show(); + } + form.submit(); + } + }); + } + + var $body = $('body'); + var $preloader = $('#preloader'); + var $preloader_text = $('#preloader_text'); + + (function () { + var pendingStatutXhr = null; + var lastStatutParId = null; + + $('.inscr-gestion-statut-select').each(function () { + $(this).data('prev', $(this).val()); + }); + + $body.on('change', '.inscr-gestion-statut-select', function () { + var $select = $(this); + var $block = $select.closest('.inscr-gestion-field-block--statut'); + var intParId = $select.data('par_id'); + var strCode = $select.val(); + var strPrev = $select.data('prev'); + + if (!intParId || !strCode) { + return; + } + + if (pendingStatutXhr && lastStatutParId === intParId) { + pendingStatutXhr.abort(); + } + lastStatutParId = intParId; + $block.removeClass('inscr-gestion-statut--saved inscr-gestion-statut--error'); + + $preloader_text.html(cfg.preloaderWait || ''); + $preloader.show(); + + pendingStatutXhr = $.post((cfg.domain || '') + '/ajax_inscr_gestion.php', { + a: 'par_statut_course', + par_id: intParId, + par_statut_course: strCode, + lang: cfg.lang || 'fr' + }, function (result) { + pendingStatutXhr = null; + $preloader.fadeOut('slow'); + if (result && result.state === 'success') { + $select.data('prev', strCode); + $block.addClass('inscr-gestion-statut--saved'); + window.setTimeout(function () { + $block.removeClass('inscr-gestion-statut--saved'); + }, 1200); + } else { + $select.val(strPrev); + $block.addClass('inscr-gestion-statut--error'); + } + }, 'json').fail(function () { + pendingStatutXhr = null; + $preloader.fadeOut('slow'); + $select.val(strPrev); + $block.addClass('inscr-gestion-statut--error'); + }); + }); + })(); + + (function () { + var refundCancelHint = cfg.refundCancelHint || ''; + + function onRefundSuccess(form, json) { + var $form = $(form); + + if (json && json.new_token) { + $form.find('input[name="refund_token"]').val(json.new_token); + } + + var amount = parseFloat($form.find('input[name="refund_amount"]').val()) || 0; + var $max = $('#pp_ref_max'); + var oldMax = parseFloat(String($max.text() || '0').replace(',', '.')) || 0; + var newMax = Math.max(0, oldMax - amount); + $max.text(newMax.toFixed(2)); + + var $amountInput = $('#pp_ref_amount'); + $amountInput.attr('max', newMax.toFixed(2)); + if (newMax <= 0) { + $amountInput.val('0.00').prop('disabled', true); + $form.find('button[type="submit"]').prop('disabled', true); + } else { + $amountInput.val(newMax.toFixed(2)); + } + + var $cancelBtn = $('#annuler_fiche'); + if ($cancelBtn.length && $cancelBtn.is(':visible')) { + if (!$('#inscr-gestion-refund-cancel-hint').length) { + var $hint = $('
'); + $hint.append($('').text(refundCancelHint)); + $hint.append( + $('') + .text($.trim($cancelBtn.text())) + .on('click', function () { $cancelBtn.trigger('click'); }) + ); + $hint.insertAfter($('#inscr-gestion-refund-panel')); + } + $cancelBtn.addClass('d-none'); + } + } + + $(document).on('click', '.btn_refund_toggle', function () { + var $btn = $(this); + var $panel = $($btn.attr('data-refund-target') || '#inscr-gestion-refund-panel'); + if (!$panel.length) { return; } + + if (!$panel.prop('hidden')) { + $panel.prop('hidden', true); + $btn.attr('aria-expanded', 'false'); + return; + } + + $panel.prop('hidden', false); + $btn.attr('aria-expanded', 'true'); + + function focusRefundPanel() { + if ($panel[0] && $panel[0].scrollIntoView) { + $panel[0].scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + var $amount = $panel.find('#pp_ref_amount'); + if ($amount.length && !$amount.prop('disabled')) { + $amount.trigger('focus').trigger('select'); + } + } + + if ($panel.attr('data-loaded') === '1') { focusRefundPanel(); return; } + + $panel.html(ms1LoaderInlineHtml()); + $.post((cfg.domain || '') + '/ajax_inscr_gestion.php', { + a: 'refund_form', + no_panier: $btn.data('no_panier'), + eve_id: $btn.data('eve_id'), + lang: cfg.lang || 'fr' + }, function (result) { + if (result && result.state === 'ok' && result.html) { + $panel.html(result.html); + $panel.attr('data-loaded', '1'); + focusRefundPanel(); + } else { + $panel.html('
\u26a0\ufe0f ' + ((result && result.message) || 'Erreur de chargement.') + '
'); + } + }, 'json').fail(function () { + $panel.html('
\u26a0\ufe0f Erreur reseau.
'); + }); + }); + + $(document).on('click', '.btn_transaction_toggle', function () { + var $btn = $(this); + var $panel = $($btn.attr('data-transaction-target') || '#inscr-gestion-transaction-panel'); + if (!$panel.length) { return; } + + var strShow = $btn.data('label-show'); + var strHide = $btn.data('label-hide'); + + if (!$panel.prop('hidden')) { + $panel.prop('hidden', true); + $btn.attr('aria-expanded', 'false'); + if (strShow) { $btn.text(strShow); } + return; + } + + $panel.prop('hidden', false); + $btn.attr('aria-expanded', 'true'); + if (strHide) { $btn.text(strHide); } + + if ($panel.attr('data-loaded') === '1') { return; } + + $panel.html(ms1LoaderInlineHtml()); + $.post((cfg.domain || '') + '/ajax_inscr_gestion.php', { + a: 'transaction_resume', + no_panier: $btn.data('no_panier'), + lang: cfg.lang || 'fr' + }, function (result) { + if (result && result.state === 'ok' && result.html) { + $panel.html(result.html); + $panel.attr('data-loaded', '1'); + } else { + $panel.html('
\u26a0\ufe0f ' + ((result && result.message) || 'Erreur de chargement.') + '
'); + } + }, 'json').fail(function () { + $panel.html('
\u26a0\ufe0f Erreur reseau.
'); + }); + }); + + $(document).on('submit', '#refund-form', function (e) { + e.preventDefault(); + var form = this; + var url = form.getAttribute('data-refund-url'); + var $msg = $('#refund-msg'); + + function showMsg(text, ok) { + $msg.css({ + display: 'block', + background: ok ? '#e9f8ef' : '#feecec', + border: ok ? '1px solid #b8e3c5' : '1px solid #f2b8b5' + }).text(text); + } + + showMsg('\u23f3 Traitement en cours...', false); + if (window.Ms1Loader) { + Ms1Loader.show(cfg.preloaderWait || undefined); + } + + fetch(url, { method: 'POST', body: new FormData(form) }) + .then(function (res) { + if (!res.ok) { throw new Error('network'); } + return res.json(); + }) + .then(function (json) { + if (window.Ms1Loader) { + Ms1Loader.hide(); + } + if (json && json.success) { + showMsg('\u2705 ' + (json.message || 'Remboursement effectue.'), true); + onRefundSuccess(form, json); + } else { + showMsg('\u26a0\ufe0f ' + ((json && json.message) || 'Erreur remboursement.'), false); + } + }) + .catch(function () { + if (window.Ms1Loader) { + Ms1Loader.hide(); + } + showMsg('\u26a0\ufe0f Erreur reseau pendant le remboursement.', false); + }); + }); + })(); + }); + } + (function () { + var qrPanel = document.getElementById('inscr-gestion-qr-panel'); + if (!qrPanel) { + return; + } + + var qrReaderEl = document.getElementById('inscr-gestion-qr-reader'); + var qrFeedback = document.getElementById('inscr-gestion-qr-feedback'); + var qrCamera = null; + var qrStarting = false; + var lastQrRaw = ''; + var qrResolvePending = false; + var qrLibPromise = null; + + function loadHtml5Qrcode() { + if (window.Html5Qrcode) { + return Promise.resolve(); + } + if (qrLibPromise) { + return qrLibPromise; + } + qrLibPromise = new Promise(function (resolve, reject) { + var script = document.createElement('script'); + script.src = 'https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js'; + script.onload = function () { resolve(); }; + script.onerror = function () { + qrLibPromise = null; + reject(new Error('load')); + }; + document.head.appendChild(script); + }); + return qrLibPromise; + } + + function showQrFeedback(type, message) { + if (!qrFeedback) { + return; + } + qrFeedback.className = 'alert mb-2 alert-' + type; + qrFeedback.textContent = message; + qrFeedback.classList.remove('d-none'); + } + + function hideQrFeedback() { + if (!qrFeedback) { + return; + } + qrFeedback.classList.add('d-none'); + qrFeedback.textContent = ''; + } + + function stopQrScanner() { + if (!qrCamera) { + return Promise.resolve(); + } + var instance = qrCamera; + qrCamera = null; + return instance.stop().catch(function () { + return null; + }).then(function () { + try { + instance.clear(); + } catch (e) { + /* ignore */ + } + }); + } + + function startQrScanner() { + if (qrStarting || qrCamera || !qrReaderEl || !qrPanel.open) { + return; + } + qrStarting = true; + hideQrFeedback(); + lastQrRaw = ''; + + loadHtml5Qrcode().then(function () { + if (!qrPanel.open) { + qrStarting = false; + return; + } + var config = window.Html5QrcodeSupportedFormats + ? {formatsToSupport: [Html5QrcodeSupportedFormats.QR_CODE]} + : undefined; + qrCamera = new Html5Qrcode('inscr-gestion-qr-reader', config); + return qrCamera.start( + {facingMode: 'environment'}, + {fps: 8, qrbox: {width: 220, height: 220}}, + onQrDecoded + ); + }).catch(function () { + showQrFeedback('danger', qrPanel.getAttribute('data-qr-msg-camera') || ''); + }).then(function () { + qrStarting = false; + }); + } + + function onQrDecoded(decodedText) { + if (!decodedText || decodedText === lastQrRaw || qrResolvePending) { + return; + } + lastQrRaw = decodedText; + qrResolvePending = true; + + var resolveUrl = qrPanel.getAttribute('data-qr-resolve-url'); + var eveId = qrPanel.getAttribute('data-qr-eve-id'); + var lang = qrPanel.getAttribute('data-qr-lang') || 'fr'; + if (!resolveUrl || !eveId) { + qrResolvePending = false; + return; + } + + var body = new URLSearchParams(); + body.set('qr', decodedText); + body.set('promoteur_eve_id', eveId); + body.set('lang', lang); + + fetch(resolveUrl, { + method: 'POST', + headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}, + body: body.toString(), + credentials: 'same-origin' + }).then(function (response) { + return response.json(); + }).then(function (data) { + qrResolvePending = false; + if (!data || !data.state) { + showQrFeedback('danger', qrPanel.getAttribute('data-qr-msg-invalid') || ''); + window.setTimeout(function () { lastQrRaw = ''; }, 2000); + return; + } + if (data.state === 'success' && data.pec_id) { + stopQrScanner().then(function () { + var url = new URL(window.location.href); + url.searchParams.set('pec_id', String(data.pec_id)); + url.searchParams.set('from_qr', '1'); + window.location.href = url.toString(); + }); + return; + } + if (data.state === 'wrong_event') { + var wrongTpl = qrPanel.getAttribute('data-qr-msg-wrong') || ''; + showQrFeedback('warning', data.message || wrongTpl.replace('%s', data.eve_nom || '')); + window.setTimeout(function () { lastQrRaw = ''; }, 2500); + return; + } + showQrFeedback('danger', data.message || qrPanel.getAttribute('data-qr-msg-invalid') || ''); + window.setTimeout(function () { lastQrRaw = ''; }, 2000); + }).catch(function () { + qrResolvePending = false; + showQrFeedback('danger', qrPanel.getAttribute('data-qr-msg-invalid') || ''); + window.setTimeout(function () { lastQrRaw = ''; }, 2000); + }); + } + + qrPanel.addEventListener('toggle', function () { + if (qrPanel.open) { + startQrScanner(); + } else { + stopQrScanner(); + hideQrFeedback(); + lastQrRaw = ''; + qrResolvePending = false; + } + }); + })(); + + + (function () { + var BIB_MAX_LEN = 6; + var OCR_MAX_WIDTH = 320; + var tesseractPromise = null; + var globalOcrWorker = null; + var globalOcrWorkerPromise = null; + var activeScan = null; + + function loadTesseract() { + if (window.Tesseract) { + return Promise.resolve(); + } + if (tesseractPromise) { + return tesseractPromise; + } + tesseractPromise = new Promise(function (resolve, reject) { + var script = document.createElement('script'); + script.src = 'https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js'; + script.onload = function () { resolve(); }; + script.onerror = function () { + tesseractPromise = null; + reject(new Error('load')); + }; + document.head.appendChild(script); + }); + return tesseractPromise; + } + + function getGlobalOcrWorker() { + if (globalOcrWorker) { + return Promise.resolve(globalOcrWorker); + } + if (globalOcrWorkerPromise) { + return globalOcrWorkerPromise; + } + globalOcrWorkerPromise = loadTesseract().then(function () { + return Tesseract.createWorker('eng'); + }).then(function (worker) { + globalOcrWorker = worker; + var params = {tessedit_char_whitelist: '0123456789'}; + if (window.Tesseract && Tesseract.PSM && Tesseract.PSM.SINGLE_LINE) { + params.tessedit_pageseg_mode = Tesseract.PSM.SINGLE_LINE; + } + return worker.setParameters(params).then(function () { + return globalOcrWorker; + }); + }).catch(function (err) { + globalOcrWorkerPromise = null; + throw err; + }); + return globalOcrWorkerPromise; + } + + function preloadOcrWorker() { + return getGlobalOcrWorker().catch(function () { + /* ignore preload errors */ + }); + } + + function extractBibNumber(raw) { + var matches = String(raw || '').match(/\d+/g); + if (!matches || !matches.length) { + return ''; + } + if (matches.length > 1) { + return ''; + } + var num = matches[0]; + if (num.length < 1 || num.length > BIB_MAX_LEN) { + return ''; + } + return num; + } + + function captureCenterFrame(videoEl) { + if (!videoEl || !videoEl.videoWidth) { + return null; + } + var w = videoEl.videoWidth; + var h = videoEl.videoHeight; + var cropW = Math.round(w * 0.55); + var cropH = Math.round(h * 0.3); + var sx = Math.round((w - cropW) / 2); + var sy = Math.round((h - cropH) / 2); + var canvas = document.createElement('canvas'); + var outW = cropW; + var outH = cropH; + if (cropW > OCR_MAX_WIDTH) { + var scale = OCR_MAX_WIDTH / cropW; + outW = OCR_MAX_WIDTH; + outH = Math.max(1, Math.round(cropH * scale)); + } + canvas.width = outW; + canvas.height = outH; + canvas.getContext('2d').drawImage(videoEl, sx, sy, cropW, cropH, 0, 0, outW, outH); + return canvas; + } + + function BibScanSession(block) { + this.block = block; + this.panel = block.querySelector('.inscr-gestion-bib-scan-panel'); + this.toggleBtn = block.querySelector('.inscr-gestion-bib-scan-toggle'); + this.statusEl = this.panel ? this.panel.querySelector('.inscr-gestion-bib-scan-status') : null; + this.captureEl = this.panel ? this.panel.querySelector('.inscr-gestion-bib-scan-capture') : null; + this.videoWrap = this.captureEl ? this.captureEl.querySelector('.inscr-gestion-bib-scan-video') : null; + this.captureLabel = this.captureEl ? this.captureEl.querySelector('.inscr-gestion-bib-scan-capture-label') : null; + this.inputId = this.panel ? this.panel.getAttribute('data-bib-input-id') : ''; + this.stream = null; + this.video = null; + this.ocrBusy = false; + this.workerReady = false; + this.onCaptureClick = null; + } + + BibScanSession.prototype.getInput = function () { + return this.inputId ? document.getElementById(this.inputId) : null; + }; + + BibScanSession.prototype.setStatus = function (text, active) { + if (!this.statusEl) { + return; + } + this.statusEl.textContent = text || ''; + this.statusEl.classList.toggle('is-active', !!active); + }; + + BibScanSession.prototype.setCaptureReady = function (ready) { + if (this.captureEl) { + this.captureEl.classList.toggle('is-ready', !!ready); + this.captureEl.setAttribute('aria-disabled', ready ? 'false' : 'true'); + } + }; + + // Detecte une clef i18n brute (label absent de la table info) quel que + // soit le prefixe (inscr_gestion_, ancien inscr_mobile_, etc.) : une vraie + // traduction contient des espaces/accents, jamais ce motif "cle_technique". + function isRawI18nKey(str) { + return typeof str === 'string' && /^[a-z][a-z0-9_]*$/.test(str) && str.indexOf('_') !== -1; + } + + // Renvoie la valeur de l'attribut si elle est traduite, sinon le texte + // integre (bilingue) — evite d'afficher une clef brute a l'usager. + function scanMsg(panel, attr, frText, enText) { + var value = panel ? panel.getAttribute(attr) : ''; + if (value && !isRawI18nKey(value)) { + return value; + } + return (document.documentElement.lang === 'en') ? enText : frText; + } + + BibScanSession.prototype.showReady = function () { + this.setStatus(scanMsg(this.panel, 'data-msg-ready', 'Cadrez le numéro, puis touchez l\'image', 'Frame the number, then tap the image'), false); + this.setCaptureReady(true); + if (this.captureLabel) { + this.captureLabel.hidden = false; + } + }; + + BibScanSession.prototype.unbindCapture = function () { + if (this.captureEl && this.onCaptureClick) { + this.captureEl.removeEventListener('click', this.onCaptureClick); + this.captureEl.removeEventListener('keydown', this.onCaptureKey); + } + this.onCaptureClick = null; + this.onCaptureKey = null; + }; + + BibScanSession.prototype.bindCapture = function () { + var self = this; + self.unbindCapture(); + if (!self.captureEl) { + return; + } + self.onCaptureClick = function () { + if (!self.workerReady || self.ocrBusy || activeScan !== self) { + return; + } + self.runOcr(); + }; + self.onCaptureKey = function (e) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + self.onCaptureClick(); + } + }; + self.captureEl.addEventListener('click', self.onCaptureClick); + self.captureEl.addEventListener('keydown', self.onCaptureKey); + }; + + BibScanSession.prototype.stop = function () { + this.unbindCapture(); + if (this.stream) { + this.stream.getTracks().forEach(function (track) { track.stop(); }); + this.stream = null; + } + if (this.video) { + this.video.srcObject = null; + this.video = null; + } + if (this.videoWrap) { + this.videoWrap.innerHTML = ''; + } + this.ocrBusy = false; + this.workerReady = false; + this.setCaptureReady(false); + this.setStatus(''); + if (this.panel) { + this.panel.hidden = true; + } + if (this.toggleBtn) { + this.toggleBtn.setAttribute('aria-expanded', 'false'); + this.toggleBtn.classList.remove('is-active'); + } + if (activeScan === this) { + activeScan = null; + } + }; + + BibScanSession.prototype.isSearchContext = function () { + var form = this.block ? this.block.closest('form.inscr-gestion-form') : null; + return !!(form && !form.closest('.inscr-gestion-fiche')); + }; + + BibScanSession.prototype.submitSearchForm = function (form) { + if (!form) { + return; + } + var submitBtn = form.querySelector('button[type="submit"][name="btn_recherche"], input[type="submit"][name="btn_recherche"]'); + if (submitBtn) { + submitBtn.click(); + return; + } + var hidden = document.createElement('input'); + hidden.type = 'hidden'; + hidden.name = 'btn_recherche'; + hidden.value = '1'; + form.appendChild(hidden); + form.submit(); + }; + + BibScanSession.prototype.applyNumber = function (strNum) { + var input = this.getInput(); + if (!input) { + return; + } + input.value = strNum; + input.classList.add('inscr-gestion-bib--proposed'); + var blnSearch = this.isSearchContext(); + if (blnSearch) { + var tplSearch = scanMsg(this.panel, 'data-msg-found-search', 'Numéro %s détecté — recherche...', 'Number %s detected — searching...'); + this.setStatus(tplSearch.replace('%s', strNum), true); + this.stop(); + var form = this.block.closest('form.inscr-gestion-form'); + window.setTimeout(function () { + BibScanSession.prototype.submitSearchForm(form); + }, 350); + return; + } + input.focus(); + var tpl = scanMsg(this.panel, 'data-msg-found', 'Numéro %s détecté — validez avec OK.', 'Number %s detected — press OK.'); + this.setStatus(tpl.replace('%s', strNum), false); + this.stop(); + }; + + BibScanSession.prototype.runOcr = function () { + var self = this; + if (self.ocrBusy || !self.video || activeScan !== self || !self.workerReady) { + return; + } + var canvas = captureCenterFrame(self.video); + if (!canvas) { + return; + } + self.ocrBusy = true; + self.setCaptureReady(false); + if (self.captureLabel) { + self.captureLabel.hidden = true; + } + self.setStatus(scanMsg(self.panel, 'data-msg-scanning', 'Lecture en cours...', 'Reading...'), true); + getGlobalOcrWorker().then(function (worker) { + return worker.recognize(canvas); + }).then(function (result) { + var rawText = result && result.data ? String(result.data.text || '') : ''; + var strNum = extractBibNumber(rawText); + if (strNum) { + self.applyNumber(strNum); + return; + } + self.setStatus(scanMsg(self.panel, 'data-msg-invalid', 'Numéro illisible. Cadrez un seul numéro.', 'Unreadable number. Frame a single number.'), false); + window.setTimeout(function () { + if (activeScan === self) { + self.showReady(); + } + }, 1800); + }).catch(function () { + self.setStatus(scanMsg(self.panel, 'data-msg-invalid', 'Numéro illisible. Cadrez un seul numéro.', 'Unreadable number. Frame a single number.'), false); + window.setTimeout(function () { + if (activeScan === self) { + self.showReady(); + } + }, 1800); + }).then(function () { + self.ocrBusy = false; + }); + }; + + BibScanSession.prototype.start = function () { + var self = this; + if (!self.panel || !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { + self.setStatus(scanMsg(self.panel, 'data-msg-camera', 'Impossible d\'accéder à la caméra.', 'Unable to access the camera.'), false); + return; + } + if (activeScan && activeScan !== self) { + activeScan.stop(); + } + activeScan = self; + self.panel.hidden = false; + if (self.toggleBtn) { + self.toggleBtn.setAttribute('aria-expanded', 'true'); + self.toggleBtn.classList.add('is-active'); + } + self.setStatus(scanMsg(self.panel, 'data-msg-loading', 'Initialisation (1re fois)...', 'Initializing (first time)...'), true); + + var cameraPromise = navigator.mediaDevices.getUserMedia({ + video: {facingMode: 'environment'}, + audio: false + }); + var workerPromise = getGlobalOcrWorker(); + + Promise.all([cameraPromise, workerPromise]).then(function (results) { + if (activeScan !== self) { + results[0].getTracks().forEach(function (track) { track.stop(); }); + return; + } + self.workerReady = true; + self.stream = results[0]; + self.video = document.createElement('video'); + self.video.setAttribute('playsinline', 'true'); + self.video.setAttribute('autoplay', 'true'); + self.video.muted = true; + self.video.srcObject = self.stream; + self.videoWrap.innerHTML = ''; + self.videoWrap.appendChild(self.video); + return self.video.play(); + }).then(function () { + if (activeScan !== self || !self.workerReady) { + return; + } + self.bindCapture(); + self.showReady(); + }).catch(function () { + self.setStatus(scanMsg(self.panel, 'data-msg-camera', 'Impossible d\'accéder à la caméra.', 'Unable to access the camera.'), false); + self.stop(); + }); + }; + + if (document.querySelector('.inscr-gestion-bib-scan-toggle')) { + preloadOcrWorker(); + } + + document.querySelectorAll('.inscr-gestion-field-block[data-inscr-field="bib"]').forEach(function (block) { + var toggleBtn = block.querySelector('.inscr-gestion-bib-scan-toggle'); + var panel = block.querySelector('.inscr-gestion-bib-scan-panel'); + if (!toggleBtn || !panel) { + return; + } + var session = new BibScanSession(block); + toggleBtn.addEventListener('click', function () { + if (activeScan === session && !panel.hidden) { + session.stop(); + return; + } + session.start(); + }); + var input = session.getInput(); + if (input) { + input.addEventListener('input', function () { + input.classList.remove('inscr-gestion-bib--proposed'); + }); + } + }); + + var ficheFromQr = document.querySelector('.inscr-gestion-fiche[data-from-qr="1"]'); + if (ficheFromQr) { + var editableSection = ficheFromQr.querySelector('.inscr-gestion-fiche-section--editable'); + if (editableSection) { + window.setTimeout(function () { + editableSection.scrollIntoView({behavior: 'smooth', block: 'start'}); + }, 80); + } + } + })(); + + + (function () { + var moduleInscr = document.getElementById('module-inscr-gestion'); + if (!moduleInscr) { + return; + } + + var params = new URLSearchParams(window.location.search); + if (params.get('pec_id')) { + moduleInscr.scrollIntoView({block: 'start'}); + } else if (params.get('pg') && parseInt(params.get('pg'), 10) > 1) { + var listAnchor = document.getElementById('inscr-gestion-list'); + if (listAnchor) { + listAnchor.scrollIntoView({block: 'start'}); + } + } + moduleInscr.addEventListener('click', function (e) { + var btnToggle = e.target.closest('.epr-block-toggle'); + if (!btnToggle || !moduleInscr.contains(btnToggle)) { + return; + } + var block = btnToggle.closest('.epr-block'); + if (!block) { + return; + } + var collapsed = block.classList.toggle('is-collapsed'); + btnToggle.setAttribute('aria-expanded', collapsed ? 'false' : 'true'); + var label = collapsed + ? (btnToggle.getAttribute('data-label-expand') || '') + : (btnToggle.getAttribute('data-label-collapse') || ''); + if (label) { + btnToggle.setAttribute('aria-label', label); + btnToggle.setAttribute('title', label); + } + var icon = btnToggle.querySelector('.fa'); + if (icon) { + icon.classList.toggle('fa-chevron-up', !collapsed); + icon.classList.toggle('fa-chevron-down', collapsed); + } + }); + + var inscrLoaderMsg = cfg.preloaderWait || ''; + + moduleInscr.addEventListener('click', function (e) { + var factureLink = e.target.closest('a[href*="facture.php"]'); + if (!factureLink || !moduleInscr.contains(factureLink)) { + return; + } + if (window.Ms1Loader) { + Ms1Loader.show(inscrLoaderMsg); + window.setTimeout(function () { + Ms1Loader.hide(); + }, 3000); + } + }); + + moduleInscr.addEventListener('submit', function (e) { + var form = e.target; + if (!form || !moduleInscr.contains(form)) { + return; + } + if (!form.classList.contains('inscr-gestion-form')) { + return; + } + if (window.Ms1Loader) { + Ms1Loader.show(inscrLoaderMsg); + } + }); + })(); + +})(); diff --git a/mobile.php b/mobile.php index 23b16ed..9961151 100644 --- a/mobile.php +++ b/mobile.php @@ -8,6 +8,7 @@ require_once('php/inc_fx_messages.php'); require_once('php/inc_fx_compte.php'); require_once('php/inc_fx_promoteur.php'); require_once('php/inc_fx_inscriptions_gestion.php'); +require_once('php/inc_v2_assets.php'); global $objDatabase, $vDomaine, $vPage, $vtexte_page; @@ -18,6 +19,7 @@ if (!empty($_GET['lang'])) { } fxInscrGestionInitTextes($strLangue); +fxV2RegisterScript('inscr-gestion'); $vPage = 'mobile.php'; $strCode = 'mobile'; diff --git a/php/inc_settings.php b/php/inc_settings.php index 34350cd..b34c344 100644 --- a/php/inc_settings.php +++ b/php/inc_settings.php @@ -7,7 +7,7 @@ * Constantes * * **************/ -define('_VERSION_CODE', '4.72.733'); +define('_VERSION_CODE', '4.72.734'); define('_DATE_CODE', '2026-07-07'); //MSIN-4290 define('QR_SECRET_KEY', 'ms1_qr_2026_cle_secrete_longue_et_fixe'); diff --git a/php/inc_v2_assets.php b/php/inc_v2_assets.php new file mode 100644 index 0000000..f534ec5 --- /dev/null +++ b/php/inc_v2_assets.php @@ -0,0 +1,72 @@ + $vDomaine, + 'lang' => $strLangue, + 'preloaderWait' => afficheTexte('preloader_wait', 0, 0, 1), + 'refundCancelHint' => ($strLangue === 'en') + ? 'Your transaction has been refunded. If you also want to cancel it in the registration system:' + : "Votre transaction a été remboursée. Si vous voulez aussi l'annuler dans le système d'inscription :", + 'loginMessages' => [ + 'txt_login' => ($strLangue === 'fr') + ? "Veuillez entrer votre nom d'usager" + : 'Please enter your username', + 'txt_password' => ($strLangue === 'fr') + ? 'Veuillez entrer votre mot de passe' + : 'Please enter your password', + ], + ]; + } + + /** À appeler dans inc_footer.php après inc_footer_scripts.php */ + function fxV2RenderFooterScripts() { + global $MS1_V2_SCRIPTS, $vDomaine, $strLangue; + + if (empty($MS1_V2_SCRIPTS) || !is_array($MS1_V2_SCRIPTS)) { + return; + } + + $mapFiles = [ + 'bib-assignments' => '/js/v2/bib-assignments.js', + 'inscr-gestion' => '/js/v2/inscr-gestion.js', + ]; + + if (in_array('inscr-gestion', $MS1_V2_SCRIPTS, true)) { + $cfg = fxV2InscrGestionConfig($strLangue ?? 'fr'); + echo '' . "\n"; + } + + foreach ($MS1_V2_SCRIPTS as $strName) { + if (!isset($mapFiles[$strName])) { + continue; + } + $strSrc = htmlspecialchars($vDomaine . $mapFiles[$strName], ENT_QUOTES, 'UTF-8'); + echo '' . "\n"; + } + } +}