diff --git a/ajax_bib_range.php b/ajax_bib_range.php index addb40a..b03c991 100644 --- a/ajax_bib_range.php +++ b/ajax_bib_range.php @@ -723,13 +723,14 @@ if ($action == 'batch_global_free_analysis') { } $tabPlan = fxBibBuildFreeRangePlan($int_eve_id, $tabEprIds, $intStart, $intEnd, $strSort1, $strSort2); - $tabAnalysis = fxBibBuildGlobalBatchFreeAnalysis($tabPlan, $intStart, $intEnd); + $tabAnalysis = fxBibBuildGlobalBatchFreeAnalysis($int_eve_id, $tabPlan, $intStart, $intEnd); echo json_encode([ 'success' => true, 'info' => $tabAnalysis['info'], 'stats' => $tabAnalysis['stats'], 'feedback' => $tabAnalysis['feedback'], + 'duplicates' => $tabAnalysis['duplicates'], ]); exit; } diff --git a/css/style.css b/css/style.css index c7e9002..d2c019b 100644 --- a/css/style.css +++ b/css/style.css @@ -1472,6 +1472,10 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active { .bib-global-free-range__feedback{ margin-top:6px; } +.bib-global-free-range__qty, +.bib-global-free-range__dupes{ + font-size:12.5px; +} .bib-global-free-range__feedback--juste{ color:#155724; font-weight:600; @@ -1484,6 +1488,11 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active { color:#004085; font-weight:600; } +.bib-global-free-range__dupes{ + color:#a94442; + font-weight:600; + margin-top:2px; +} .bib-global-epr--inactive{ opacity:0.55; background:#f1f3f5; diff --git a/js/v2/bib-assignments.js b/js/v2/bib-assignments.js index 4fee8c0..b8354e8 100644 --- a/js/v2/bib-assignments.js +++ b/js/v2/bib-assignments.js @@ -1238,7 +1238,9 @@ start: wrap.querySelector('.bib-global-free-start'), qty: wrap.querySelector('.bib-global-free-qty'), end: wrap.querySelector('.bib-global-free-end'), - feedback: wrap.querySelector('.bib-global-free-range__feedback') + feedback: wrap.querySelector('.bib-global-free-range__feedback'), + qtyEl: wrap.querySelector('.bib-global-free-range__qty'), + dupesEl: wrap.querySelector('.bib-global-free-range__dupes') }; } @@ -1285,32 +1287,56 @@ || (('Quantité manquante : %d').replace('%d', String(Math.abs(diff)))); } + function bibSetGlobalFreeDupesWarn(section, blnShow, strMessage) { + var fields = bibGetGlobalBatchFreeRangeFields(section); + if (!fields || !fields.dupesEl) { + return; + } + + if (!blnShow) { + fields.dupesEl.textContent = ''; + fields.dupesEl.classList.add('bib-ui-hidden'); + return; + } + + fields.dupesEl.textContent = strMessage + || bibJs('jsGlobalFreeDupesWarn') + || 'Attention : vous êtes en train de créer des inscriptions avec des dossards en double.'; + fields.dupesEl.classList.remove('bib-ui-hidden'); + } + function bibUpdateGlobalFreeQtyFeedback(section) { var fields = bibGetGlobalBatchFreeRangeFields(section); - if (!fields || !fields.feedback) { + if (!fields) { return; } var bounds = bibGetGlobalBatchFreeBounds(section); var pending = bibCollectGlobalBatchFreePending(); var msg = bibFormatGlobalFreeQtyFeedback(bounds.qty, pending); - fields.feedback.textContent = msg; - fields.feedback.classList.remove( - 'bib-global-free-range__feedback--juste', - 'bib-global-free-range__feedback--manquante', - 'bib-global-free-range__feedback--supplementaire' - ); + var qtyEl = fields.qtyEl || fields.feedback; - if (!msg) { - return; + if (qtyEl) { + qtyEl.textContent = msg; + qtyEl.classList.remove( + 'bib-global-free-range__feedback--juste', + 'bib-global-free-range__feedback--manquante', + 'bib-global-free-range__feedback--supplementaire' + ); + + if (msg) { + if (bounds.qty === pending) { + qtyEl.classList.add('bib-global-free-range__feedback--juste'); + } else if (bounds.qty < pending) { + qtyEl.classList.add('bib-global-free-range__feedback--manquante'); + } else { + qtyEl.classList.add('bib-global-free-range__feedback--supplementaire'); + } + } } - if (bounds.qty === pending) { - fields.feedback.classList.add('bib-global-free-range__feedback--juste'); - } else if (bounds.qty < pending) { - fields.feedback.classList.add('bib-global-free-range__feedback--manquante'); - } else { - fields.feedback.classList.add('bib-global-free-range__feedback--supplementaire'); + if (!msg) { + bibSetGlobalFreeDupesWarn(section, false); } } @@ -1492,6 +1518,7 @@ bibJs('jsGlobalNoEpr') || bibJs('jsBatchNone'), 'error' ); + bibSetGlobalFreeDupesWarn(section, false); return Promise.resolve(null); } @@ -1501,6 +1528,7 @@ bibJs('jsGlobalFreePlanInvalid') || bibJs('jsFieldsRequired'), 'error' ); + bibSetGlobalFreeDupesWarn(section, false); return Promise.resolve(null); } @@ -1530,11 +1558,21 @@ if (data.feedback && data.feedback.status === 'manquante') { state = 'warning'; } + if (data.duplicates && data.duplicates.has) { + state = 'warning'; + } bibSetGlobalBatchInfo(infoEl, data.info || '', state); + bibUpdateGlobalFreeQtyFeedback(section); + bibSetGlobalFreeDupesWarn( + section, + !!(data.duplicates && data.duplicates.has), + (data.duplicates && data.duplicates.message) || '' + ); return data; }) .catch(function () { bibSetGlobalBatchInfo(infoEl, bibJs('jsErrGeneric'), 'error'); + bibSetGlobalFreeDupesWarn(section, false); return null; }); } diff --git a/php/inc_fx_bib_global_free.php b/php/inc_fx_bib_global_free.php index 591cbbe..4e16998 100644 --- a/php/inc_fx_bib_global_free.php +++ b/php/inc_fx_bib_global_free.php @@ -60,6 +60,73 @@ function fxBibFreeQtyFeedbackMessage(array $tabFeedback) { return ''; } +/** + * MSIN-4445 — Nombre de dossards de la plage déjà utilisés sur une autre épreuve. + * Ces numéros seraient réassignés (doublon inter-épreuve) car le batch ne les saute pas. + */ +function fxBibCountBibsUsedOutsideEpr($int_eve_id, $intStart, $intEnd, $intEprId) { + global $objDatabase; + + $int_eve_id = (int)$int_eve_id; + $intStart = (int)$intStart; + $intEnd = (int)$intEnd; + $intEprId = (int)$intEprId; + + if ($int_eve_id <= 0 || $intEprId <= 0 || $intStart <= 0 || $intEnd < $intStart) { + return 0; + } + + $sqlBib = fxBibSqlNoBibUnsigned('no_bib'); + + return (int)$objDatabase->fxGetVar(" + SELECT COUNT(DISTINCT $sqlBib) + FROM resultats_participants + WHERE eve_id = $int_eve_id + AND epr_id <> $intEprId + AND is_cancelled = 0 + AND no_bib IS NOT NULL + AND TRIM(no_bib) <> '' + AND $sqlBib BETWEEN $intStart AND $intEnd + "); +} + +/** + * MSIN-4445 — Risque de doublons pour le plan libre (plages virtuelles vs dossards déjà pris ailleurs). + * + * @return array{has:bool, count:int, message:string} + */ +function fxBibFreeDuplicateRisk($int_eve_id, $tabPlan) { + $int_eve_id = (int)$int_eve_id; + $intCount = 0; + + if (!is_array($tabPlan)) { + return ['has' => false, 'count' => 0, 'message' => '']; + } + + foreach ($tabPlan as $item) { + if (!empty($item['skip'])) { + continue; + } + + $intCount += fxBibCountBibsUsedOutsideEpr( + $int_eve_id, + (int)($item['start'] ?? 0), + (int)($item['end'] ?? 0), + (int)($item['epr_id'] ?? 0) + ); + } + + if ($intCount <= 0) { + return ['has' => false, 'count' => 0, 'message' => '']; + } + + return [ + 'has' => true, + 'count' => $intCount, + 'message' => fxBibMsg('bib_v4_global_batch_free_dupes_warn'), + ]; +} + /** * Plan de plages virtuelles bornées par Début/Fin (sans écriture BD). * Enchaîne les épreuves comme le bloc généré, mais s'arrête à $intEnd. @@ -181,7 +248,8 @@ function fxBibValidateFreeRangePlan($int_eve_id, $tabPlan, $intStart, $intEnd, $ } /** Stats agrégées + feedback quantité pour l'analyse libre. */ -function fxBibBuildGlobalBatchFreeAnalysis($tabPlan, $intStart, $intEnd) { +function fxBibBuildGlobalBatchFreeAnalysis($int_eve_id, $tabPlan, $intStart, $intEnd) { + $int_eve_id = (int)$int_eve_id; $intStart = (int)$intStart; $intEnd = (int)$intEnd; $intQty = ($intStart > 0 && $intEnd >= $intStart) ? ($intEnd - $intStart + 1) : 0; @@ -194,11 +262,13 @@ function fxBibBuildGlobalBatchFreeAnalysis($tabPlan, $intStart, $intEnd) { } $tabFeedback = fxBibFreeQtyFeedback($intQty, $intPendingAll); + $tabDupes = fxBibFreeDuplicateRisk($int_eve_id, $tabPlan); return [ - 'stats' => $tabStats, - 'feedback' => $tabFeedback, - 'info' => fxBibMsg( + 'stats' => $tabStats, + 'feedback' => $tabFeedback, + 'duplicates' => $tabDupes, + 'info' => fxBibMsg( 'bib_v4_ajax_global_free_analysis', $tabStats['nb_epreuves'], $intStart, @@ -271,7 +341,7 @@ function fxBibSimulateGlobalBatchFree($int_eve_id, $tabPlan, $intStart, $intEnd, . ''; } - $tabAnalysis = fxBibBuildGlobalBatchFreeAnalysis($tabPlan, $intStart, $intEnd); + $tabAnalysis = fxBibBuildGlobalBatchFreeAnalysis($int_eve_id, $tabPlan, $intStart, $intEnd); $info = fxBibMsg( 'bib_v4_ajax_global_free_sim_summary', $tabAnalysis['stats']['nb_epreuves'], @@ -316,12 +386,13 @@ function fxBibPrepareFreeRangePlans($int_eve_id, $tabPlan, $intStart, $intEnd, $ ]; } - $tabAnalysis = fxBibBuildGlobalBatchFreeAnalysis($tabPlan, $intStart, $intEnd); + $tabAnalysis = fxBibBuildGlobalBatchFreeAnalysis($int_eve_id, $tabPlan, $intStart, $intEnd); return [ 'success' => true, 'plans' => $tabPlans, 'stats' => $tabAnalysis['stats'], 'feedback' => $tabAnalysis['feedback'], + 'duplicates' => $tabAnalysis['duplicates'], ]; } diff --git a/php/inc_fx_promoteur.php b/php/inc_fx_promoteur.php index a55fd7d..36454ac 100644 --- a/php/inc_fx_promoteur.php +++ b/php/inc_fx_promoteur.php @@ -3450,6 +3450,7 @@ function fxBibStaticFallback($clef) { 'bib_v4_global_batch_free_qty_juste' => ['fr' => 'Quantité juste', 'en' => 'Exact quantity'], 'bib_v4_global_batch_free_qty_manquante' => ['fr' => 'Quantité manquante : %d', 'en' => 'Missing quantity: %d'], 'bib_v4_global_batch_free_qty_supplementaire' => ['fr' => 'Quantité supplémentaire : %d', 'en' => 'Extra quantity: %d'], + 'bib_v4_global_batch_free_dupes_warn' => ['fr' => 'Attention : vous êtes en train de créer des inscriptions avec des dossards en double.', 'en' => 'Warning: you are about to create registrations with duplicate bib numbers.'], 'bib_v4_global_batch_free_plan_invalid' => ['fr' => 'Sélection invalide : Début, Fin (ou Qté) et au moins une épreuve avec inscriptions à assigner.', 'en' => 'Invalid selection: Start, End (or Qty) and at least one race with registrations to assign.'], 'bib_v4_ajax_global_free_analysis' => ['fr' => '%d épreuve(s) · plage %d → %d · %d à assigner · %s', 'en' => '%d race(s) · range %d → %d · %d to assign · %s'], 'bib_v4_ajax_global_free_sim_summary' => ['fr' => '%d épreuve(s) · %d dossards · %d à assigner · %s', 'en' => '%d race(s) · %d bibs · %d to assign · %s'], @@ -3714,6 +3715,7 @@ function fxBibModuleJsDataAttrs() { 'global-free-qty-juste' => 'bib_v4_global_batch_free_qty_juste', 'global-free-qty-manquante' => 'bib_v4_global_batch_free_qty_manquante', 'global-free-qty-supplementaire' => 'bib_v4_global_batch_free_qty_supplementaire', + 'global-free-dupes-warn' => 'bib_v4_global_batch_free_dupes_warn', 'global-free-plan-invalid' => 'bib_v4_global_batch_free_plan_invalid', 'reset-all-confirm' => 'bib_v4_js_reset_all_confirm', 'reset-all-locked' => 'bib_v4_js_reset_all_locked', @@ -5017,7 +5019,10 @@ function renderBibGlobalBatchPanelFree($int_eve_id, $tabEpreuves, $strLangue) { inputmode="numeric" disabled> -

+
+

+

+
diff --git a/php/inc_settings.php b/php/inc_settings.php index 01dec62..afb2fe7 100644 --- a/php/inc_settings.php +++ b/php/inc_settings.php @@ -7,7 +7,7 @@ * Constantes * * **************/ -define('_VERSION_CODE', '4.72.828'); +define('_VERSION_CODE', '4.72.829'); define('_DATE_CODE', '2026-07-16'); //MSIN-4290 define('QR_SECRET_KEY', 'ms1_qr_2026_cle_secrete_longue_et_fixe'); diff --git a/sql/MSIN-4445-bib-global-free.sql b/sql/MSIN-4445-bib-global-free.sql index 59c9b34..0e851fa 100644 --- a/sql/MSIN-4445-bib-global-free.sql +++ b/sql/MSIN-4445-bib-global-free.sql @@ -65,3 +65,12 @@ FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_ajax_gl INSERT INTO info (info_clef, info_langue, info_texte, info_aide, info_prg, info_description, info_trie, info_actif, info_option1, info_option2, info_option3, info_creation) SELECT 'bib_v4_ajax_global_free_sim_summary', 'en', '%d race(s) · %d bibs · %d to assign · %s', '', 'compte.php', '', 0, 1, '', '', '', NOW() FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_ajax_global_free_sim_summary' AND info_langue = 'en' AND info_prg = 'compte.php'); + +-- MSIN-4445 — Avertissement doublons (plage libre chevauchant des dossards déjà assignés ailleurs) +INSERT INTO info (info_clef, info_langue, info_texte, info_aide, info_prg, info_description, info_trie, info_actif, info_option1, info_option2, info_option3, info_creation) +SELECT 'bib_v4_global_batch_free_dupes_warn', 'fr', 'Attention : vous êtes en train de créer des inscriptions avec des dossards en double.', 'Affiché lorsque la plage libre chevauche des dossards déjà assignés sur une autre épreuve.', 'compte.php', '', 0, 1, '', '', '', NOW() +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_global_batch_free_dupes_warn' AND info_langue = 'fr' AND info_prg = 'compte.php'); + +INSERT INTO info (info_clef, info_langue, info_texte, info_aide, info_prg, info_description, info_trie, info_actif, info_option1, info_option2, info_option3, info_creation) +SELECT 'bib_v4_global_batch_free_dupes_warn', 'en', 'Warning: you are about to create registrations with duplicate bib numbers.', 'Shown when the free range overlaps bibs already assigned on another race.', 'compte.php', '', 0, 1, '', '', '', NOW() +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_global_batch_free_dupes_warn' AND info_langue = 'en' AND info_prg = 'compte.php');