diff --git a/ajax_bib_range.php b/ajax_bib_range.php index 78bc912..5997e3f 100644 --- a/ajax_bib_range.php +++ b/ajax_bib_range.php @@ -35,6 +35,7 @@ $arrBibAjaxActions = [ 'reset_preview', 'reset_apply', 'reset_all_apply', + 'save_allow_inter_epr', 'save_team_mode', 'toggle_lock', 'save_auto_config', @@ -257,7 +258,7 @@ if ($action == 'save') { // ===================== // VALIDATION 4 — OVERLAP pas de double epreuves // ===================== - $tabOverlaps = fxBibGetOverlappingRanges($eve_id, $start, $end, $is_new ? 0 : $id); + $tabOverlaps = fxBibGetOverlappingRanges($eve_id, $start, $end, $is_new ? 0 : $id, $epr_id); if (!empty($tabOverlaps)) { echo json_encode([ @@ -356,6 +357,41 @@ if ($action == 'batch_analysis') { exit; } +// ===================== +// MSIN-4424 — Réglage doublons inter-épreuves (événement) +// ===================== +if ($action == 'save_allow_inter_epr') { + + $int_eve_id = (int)($_POST['eve_id'] ?? 0); + $blnAllow = !empty($_POST['allow']) && (string)$_POST['allow'] !== '0'; + + if ($int_eve_id <= 0) { + echo json_encode([ + 'success' => false, + 'message' => fxBibMsg('bib_v4_ajax_epr_invalid'), + ]); + exit; + } + + $result = fxBibSetEventAllowsInterEprDuplicates($int_eve_id, $blnAllow); + + if (empty($result['success'])) { + echo json_encode($result); + exit; + } + + $strLangueAnom = $_SESSION['lang'] ?? 'fr'; + $tabAnomalies = fxBibCollectAnomalies($int_eve_id, $strLangueAnom); + + echo json_encode([ + 'success' => true, + 'allow_inter_epr' => !empty($result['allow_inter_epr']) ? 1 : 0, + 'anomalies_html' => renderBibAnomaliesPanel($int_eve_id, $strLangueAnom, $tabAnomalies), + 'anomaly_count' => fxBibAnomalyTotalCount($tabAnomalies), + ]); + exit; +} + // MSIN-4436 — Panneau assignation globale (chargement différé à l'ouverture). if ($action == 'global_batch_panel') { diff --git a/ajax_inscr_gestion.php b/ajax_inscr_gestion.php index db51a7c..a551c85 100644 --- a/ajax_inscr_gestion.php +++ b/ajax_inscr_gestion.php @@ -147,7 +147,11 @@ switch ($strAction) { . ' WHERE p.par_id = ' . $intParId . ' AND p.pec_id = e.pec_id_original LIMIT 1' )); - $arrDup = fxInscrGestionFindBibDuplicates($intEveId, $strNoBib, $intPecOrig, $blnTeam); + $intCurrentEprId = (int)$objDatabase->fxGetVar( + 'SELECT epr_id FROM resultats_participants WHERE par_id = ' . $intParId . ' LIMIT 1' + ); + + $arrDup = fxInscrGestionFindBibDuplicates($intEveId, $strNoBib, $intPecOrig, $blnTeam, $intCurrentEprId); $arrWho = array(); foreach ($arrDup as $rowDup) { $arrWho[] = array( diff --git a/css/style.css b/css/style.css index b1aaac5..3046aad 100644 --- a/css/style.css +++ b/css/style.css @@ -1064,6 +1064,39 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active { line-height:1.45; } +/* MSIN-4424 — Bandeau réglages dossards (haut du module). */ +.bib-event-settings{ + border:1px solid #c8d6e5; + border-radius:6px; + background:#f8fafc; + padding:10px 14px; + margin-bottom:4px; +} + +.bib-event-settings__label{ + display:flex; + align-items:flex-start; + gap:8px; + margin:0; + font-weight:600; + font-size:14px; + line-height:1.4; + cursor:pointer; +} + +.bib-event-settings__label input[type="checkbox"]{ + margin-top:3px; + flex-shrink:0; +} + +.bib-event-settings__text{ + flex:1 1 auto; +} + +.bib-anomaly-block--inter-epr-dupes .bib-anomaly-block-title{ + color:#8a3b12; +} + /* MSIN-4436 — Assignation globale (épreuves solo) */ .bib-global-batch{ border:1px solid #c8d6e5; diff --git a/js/v2/bib-assignments.js b/js/v2/bib-assignments.js index b5746f7..9b2912f 100644 --- a/js/v2/bib-assignments.js +++ b/js/v2/bib-assignments.js @@ -2711,6 +2711,49 @@ }); }); + // MSIN-4424 — Autoriser mêmes numéros entre épreuves (réglage événement). + let chkAllowInterEpr = moduleBib.querySelector('.bib-event-allow-inter-epr'); + if (chkAllowInterEpr) { + chkAllowInterEpr.addEventListener('change', function () { + 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=save_allow_inter_epr' + + '&eve_id=' + encodeURIComponent(eveId) + + '&allow=' + (chkAllowInterEpr.checked ? '1' : '0') + + bibAjaxLangSuffix() + }) + .then(function (res) { return res.json(); }) + .then(function (data) { + if (!data.success) { + chkAllowInterEpr.checked = !chkAllowInterEpr.checked; + alert(data.message || bibJs('jsErrGeneric')); + return; + } + + moduleBib.dataset.allowInterEpr = data.allow_inter_epr ? '1' : '0'; + + let mount = document.getElementById('bib-anomalies-mount'); + if (mount && data.anomalies_html) { + mount.innerHTML = data.anomalies_html; + initModuleBibTippy(mount); + } + }) + .catch(function () { + chkAllowInterEpr.checked = !chkAllowInterEpr.checked; + alert(bibJs('jsErrGeneric')); + }); + }); + } + // MSIN-4436 — Anomalies : coquille légère au chargement, détail en AJAX. if (document.querySelector('#bib-anomalies-panel[data-bib-anomalies-deferred="1"]')) { refreshBibAnomaliesPanel(); diff --git a/php/inc_fx_inscriptions_gestion.php b/php/inc_fx_inscriptions_gestion.php index 6e19fae..8dd6a97 100644 --- a/php/inc_fx_inscriptions_gestion.php +++ b/php/inc_fx_inscriptions_gestion.php @@ -619,10 +619,12 @@ function fxInscrGestionBibDuplicatePolicy($intEveId) { * @param bool $blnTeam true = dossard d'equipe (e.no_equipe), false = individuel (p.no_bib) * @return array liste de lignes ['par_nom','par_prenom','epr_id'] */ -function fxInscrGestionFindBibDuplicates($intEveId, $strNoBib, $intExceptPec, $blnTeam) { +function fxInscrGestionFindBibDuplicates($intEveId, $strNoBib, $intExceptPec, $blnTeam, $intCurrentEprId = 0) { global $objDatabase; $strNoBib = trim((string)$strNoBib); + $intCurrentEprId = (int)$intCurrentEprId; + if (intval($intEveId) <= 0 || $strNoBib === '') { return array(); } @@ -639,7 +641,25 @@ function fxInscrGestionFindBibDuplicates($intEveId, $strNoBib, $intExceptPec, $b . ' ORDER BY p.par_nom, p.par_prenom'; $tab = $objDatabase->fxGetResults($sql); - return is_array($tab) ? $tab : array(); + if (!is_array($tab) || empty($tab)) { + return array(); + } + + $blnAllowInter = function_exists('fxBibEventAllowsInterEprDuplicates') + && fxBibEventAllowsInterEprDuplicates($intEveId); + + $arrOut = array(); + foreach ($tab as $rowDup) { + $intDupEprId = (int)($rowDup['epr_id'] ?? 0); + + if ($intCurrentEprId > 0 && $intDupEprId !== $intCurrentEprId && $blnAllowInter) { + continue; + } + + $arrOut[] = $rowDup; + } + + return $arrOut; } function fxInscrGestionEpreuveLabel($intEprId, $strLangue) { diff --git a/php/inc_fx_promoteur.php b/php/inc_fx_promoteur.php index 3557c9a..730b808 100644 --- a/php/inc_fx_promoteur.php +++ b/php/inc_fx_promoteur.php @@ -1539,8 +1539,27 @@ function fxShowDupes($int_eve_id) { $strPage = '/account'; } - $sqlNoBib = "SELECT no_bib, epr_id, COUNT(*) c FROM resultats_participants where eve_id = " . intval($int_eve_id) . " AND no_bib <> '' GROUP BY no_bib HAVING c > 1"; - $tabNoBib = $objDatabase->fxGetResults($sqlNoBib); + $sqlNoBib = "SELECT no_bib, epr_id, COUNT(*) c FROM resultats_participants where eve_id = " . intval($int_eve_id) . " AND no_bib <> '' AND is_cancelled = 0 GROUP BY no_bib, epr_id HAVING c > 1"; + $tabIntraDupes = $objDatabase->fxGetResults($sqlNoBib); + + $tabInterDupes = []; + if (!fxBibEventAllowsInterEprDuplicates((int)$int_eve_id)) { + $sqlInter = "SELECT no_bib FROM resultats_participants WHERE eve_id = " . intval($int_eve_id) + . " AND no_bib <> '' AND is_cancelled = 0 GROUP BY no_bib HAVING COUNT(DISTINCT epr_id) > 1"; + $tabInterDupes = $objDatabase->fxGetResults($sqlInter); + } + + $tabBibKeys = []; + if (is_array($tabIntraDupes)) { + foreach ($tabIntraDupes as $row) { + $tabBibKeys[(int)$row['no_bib']] = true; + } + } + if (is_array($tabInterDupes)) { + foreach ($tabInterDupes as $row) { + $tabBibKeys[(int)$row['no_bib']] = true; + } + } ?>
@@ -1559,11 +1578,11 @@ function fxShowDupes($int_eve_id) { fxGetResults($sqlDupes); if ($tabDupes != null) { @@ -1571,7 +1590,7 @@ function fxShowDupes($int_eve_id) { $tabEpreuve = fxGetEpreuve($dupe['epr_id']); ?> - + true]; } +/** MSIN-4424 — Mêmes numéros de dossard autorisés entre épreuves (réglage événement). */ +function fxBibEventAllowsInterEprDuplicates($int_eve_id) { + global $objDatabase; + + static $memCache = []; + + $int_eve_id = (int)$int_eve_id; + + if ($int_eve_id <= 0) { + return false; + } + + if (array_key_exists($int_eve_id, $memCache)) { + return $memCache[$int_eve_id]; + } + + $intVal = (int)$objDatabase->fxGetVar( + 'SELECT eve_bib_allow_inter_epr FROM inscriptions_evenements WHERE eve_id = ' . $int_eve_id . ' LIMIT 1' + ); + + $memCache[$int_eve_id] = ($intVal === 1); + + return $memCache[$int_eve_id]; +} + +/** MSIN-4424 — Persiste le réglage doublons inter-épreuves. */ +function fxBibSetEventAllowsInterEprDuplicates($int_eve_id, $blnAllow) { + global $objDatabase; + + $int_eve_id = (int)$int_eve_id; + $intAllow = $blnAllow ? 1 : 0; + + if ($int_eve_id <= 0) { + return ['success' => false, 'message' => fxBibMsg('bib_v4_ajax_epr_invalid')]; + } + + $objDatabase->fxQuery( + 'UPDATE inscriptions_evenements SET eve_bib_allow_inter_epr = ' . $intAllow + . ' WHERE eve_id = ' . $int_eve_id . ' LIMIT 1' + ); + + return ['success' => true, 'allow_inter_epr' => ($intAllow === 1)]; +} + /** Resout eve_id depuis la requete AJAX bib (eve_id direct ou via epr_id). */ function fxBibResolveEveIdFromRequest() { global $objDatabase; @@ -3034,22 +3097,30 @@ function fxBibEpreuveDisplayName(array $row, $strLangue = 'fr') { } /** - * Séquences de dossards qui chevauchent [start, end] sur le même événement. + * MSIN-4424 — Séquences qui chevauchent [start, end]. + * Par défaut : tout l'événement. Si doublons inter-épreuves autorisés : même epr_id seulement. + * * @return array|null Tableau 1-based fxGetResults, ou null si paramètres invalides. */ -function fxBibGetOverlappingRanges($eve_id, $start, $end, $exclude_bib_id = 0) { +function fxBibGetOverlappingRanges($eve_id, $start, $end, $exclude_bib_id = 0, $epr_id = 0) { global $objDatabase; $eve_id = (int)$eve_id; $start = (int)$start; $end = (int)$end; $exclude_bib_id = (int)$exclude_bib_id; + $epr_id = (int)$epr_id; if ($eve_id <= 0 || $start <= 0 || $end <= 0) { return null; } $sqlExclude = ($exclude_bib_id > 0) ? "AND b.epr_bib_id <> $exclude_bib_id" : ''; + $sqlEprOnly = ''; + + if ($epr_id > 0 && fxBibEventAllowsInterEprDuplicates($eve_id)) { + $sqlEprOnly = "AND b.epr_id = $epr_id"; + } $sql = " SELECT @@ -3070,6 +3141,7 @@ function fxBibGetOverlappingRanges($eve_id, $start, $end, $exclude_bib_id = 0) { AND b.epr_bib_start IS NOT NULL AND b.epr_bib_finish IS NOT NULL $sqlExclude + $sqlEprOnly AND ($start <= b.epr_bib_finish AND $end >= b.epr_bib_start) ORDER BY b.epr_bib_start, b.epr_bib_id "; @@ -4152,7 +4224,9 @@ function fxShowBibTool4($str_code, $int_eve_id, $strLangue){ ?>
-
> +
> + +
@@ -4574,6 +4648,101 @@ function fxBibAnomalyGroupDuplicateRows(array $rows, $strLangue = 'fr') { return array_values($grouped); } +/** + * MSIN-4424 — Dossards en double entre épreuves (même no_bib, epr_id distincts). + * Vide si le réglage événement autorise les doublons inter-épreuves. + * + * @return array> + */ +function fxBibAnomalyInterEprDuplicateBibs($int_eve_id, $strLangue = 'fr') { + global $objDatabase; + + $int_eve_id = (int)$int_eve_id; + + if ($int_eve_id <= 0 || fxBibEventAllowsInterEprDuplicates($int_eve_id)) { + return []; + } + + $sqlBibNumP = fxBibSqlNoBibUnsigned('p.no_bib'); + $sqlBibNumP2 = fxBibSqlNoBibUnsigned('p2.no_bib'); + + $sql = " + SELECT + $sqlBibNumP AS no_bib, + p.epr_id, + p.par_id, + p.par_prenom, + p.par_nom, + e.epr_nom_fr, + e.epr_nom_en, + e.epr_type_fr, + e.epr_type_en, + e.epr_categorie_fr, + e.epr_categorie_en, + e.epr_actif + FROM resultats_participants p + INNER JOIN inscriptions_epreuves e ON e.epr_id = p.epr_id + WHERE e.eve_id = $int_eve_id + AND p.is_cancelled = 0 + AND $sqlBibNumP > 0 + AND $sqlBibNumP IN ( + SELECT $sqlBibNumP2 + FROM resultats_participants p2 + INNER JOIN inscriptions_epreuves e2 ON e2.epr_id = p2.epr_id + WHERE e2.eve_id = $int_eve_id + AND p2.is_cancelled = 0 + AND $sqlBibNumP2 > 0 + GROUP BY $sqlBibNumP2 + HAVING COUNT(DISTINCT p2.epr_id) > 1 + ) + ORDER BY $sqlBibNumP, p.epr_id, p.par_nom, p.par_prenom + "; + + $rows = $objDatabase->fxGetResults($sql); + + if (!is_array($rows) || empty($rows)) { + return []; + } + + $grouped = []; + + foreach ($rows as $row) { + $intBib = (int)$row['no_bib']; + $key = (string)$intBib; + + if (!isset($grouped[$key])) { + $grouped[$key] = [ + 'no_bib' => $intBib, + 'entries' => [], + ]; + } + + $intEprId = (int)$row['epr_id']; + $strEprKey = (string)$intEprId; + + if (!isset($grouped[$key]['entries'][$strEprKey])) { + $grouped[$key]['entries'][$strEprKey] = [ + 'epr_id' => $intEprId, + 'epr_label' => fxBibAnomalyEprLabel($row, $strLangue), + 'participants' => [], + ]; + } + + $grouped[$key]['entries'][$strEprKey]['participants'][] = [ + 'par_id' => (int)$row['par_id'], + 'par_prenom' => trim($row['par_prenom'] ?? ''), + 'par_nom' => trim($row['par_nom'] ?? ''), + ]; + } + + foreach ($grouped as &$item) { + $item['entries'] = array_values($item['entries']); + } + unset($item); + + return array_values($grouped); +} + /** * MSIN-4379 — Dossards en double (même epr_id, même no_bib, >1 unité active). * Couvre les séquences configurées et les épreuves sans séquence (saisie manuelle). @@ -4925,16 +5094,17 @@ function fxBibAnomalyOutOfRangeBibs($int_eve_id, $strLangue = 'fr') { /** MSIN-4379 — Agrège toutes les anomalies dossards pour un événement. */ function fxBibCollectAnomalies($int_eve_id, $strLangue = 'fr') { return [ - 'dupes' => fxBibAnomalyDuplicateBibs($int_eve_id, $strLangue), - 'out_of_range' => fxBibAnomalyOutOfRangeBibs($int_eve_id, $strLangue), - 'orphans' => fxBibAnomalyOrphanBibs($int_eve_id, $strLangue), - 'auto_miss' => fxBibAnomalyAutoMissBibs($int_eve_id, $strLangue), + 'dupes' => fxBibAnomalyDuplicateBibs($int_eve_id, $strLangue), + 'inter_epr_dupes' => fxBibAnomalyInterEprDuplicateBibs($int_eve_id, $strLangue), + 'out_of_range' => fxBibAnomalyOutOfRangeBibs($int_eve_id, $strLangue), + 'orphans' => fxBibAnomalyOrphanBibs($int_eve_id, $strLangue), + 'auto_miss' => fxBibAnomalyAutoMissBibs($int_eve_id, $strLangue), ]; } function fxBibAnomalyTotalCount(array $tabAnomalies) { $intTotal = 0; - foreach (['dupes', 'out_of_range', 'orphans', 'auto_miss'] as $strKey) { + foreach (['dupes', 'inter_epr_dupes', 'out_of_range', 'orphans', 'auto_miss'] as $strKey) { if (!empty($tabAnomalies[$strKey]) && is_array($tabAnomalies[$strKey])) { $intTotal += count($tabAnomalies[$strKey]); } @@ -4942,6 +5112,27 @@ function fxBibAnomalyTotalCount(array $tabAnomalies) { return $intTotal; } +/** MSIN-4424 — Bandeau réglages dossards (haut du module, au-dessus des épreuves). */ +function renderBibEventSettingsBanner($int_eve_id, $strLangue = 'fr') { + $int_eve_id = (int)$int_eve_id; + $blnAllow = fxBibEventAllowsInterEprDuplicates($int_eve_id); + + ob_start(); + ?> +
+ +
+ + +
+

+ + + +

+
    + +
  • + +
      + +
    • + +
        + +
      • + +
      +
    • + +
    +
  • + +
+
+ +

diff --git a/php/inc_settings.php b/php/inc_settings.php index bf6e538..eeb8180 100644 --- a/php/inc_settings.php +++ b/php/inc_settings.php @@ -7,7 +7,7 @@ * Constantes * * **************/ -define('_VERSION_CODE', '4.72.749'); +define('_VERSION_CODE', '4.72.750'); define('_DATE_CODE', '2026-07-08'); //MSIN-4290 define('QR_SECRET_KEY', 'ms1_qr_2026_cle_secrete_longue_et_fixe'); diff --git a/sql/MSIN-4424-bib-allow-inter-epr.sql b/sql/MSIN-4424-bib-allow-inter-epr.sql new file mode 100644 index 0000000..5cc3dab --- /dev/null +++ b/sql/MSIN-4424-bib-allow-inter-epr.sql @@ -0,0 +1,38 @@ +-- MSIN-4424 — Autoriser les mêmes numéros de dossard entre épreuves (réglage événement) +-- Idempotent. Exécuter avant déploiement PHP/JS. + +SET @db := DATABASE(); + +SET @col_exists := ( + SELECT COUNT(*) + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db + AND TABLE_NAME = 'inscriptions_evenements' + AND COLUMN_NAME = 'eve_bib_allow_inter_epr' +); + +SET @sql_col := IF( + @col_exists = 0, + 'ALTER TABLE inscriptions_evenements ADD COLUMN eve_bib_allow_inter_epr TINYINT(1) UNSIGNED NOT NULL DEFAULT 0 COMMENT ''MSIN-4424 — 1 = mêmes no_bib autorisés entre épreuves'' AFTER eve_epreuve_modifiable', + 'SELECT ''eve_bib_allow_inter_epr already exists'' AS note' +); + +PREPARE stmt_col FROM @sql_col; +EXECUTE stmt_col; +DEALLOCATE PREPARE stmt_col; + +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_allow_inter_epr', 'fr', 'Autoriser les mêmes numéros de dossard entre épreuves', 'Permet d''utiliser les mêmes numéros sur plusieurs épreuves (ex. 5 km et 10 km). Les doublons à l''intérieur d''une même épreuve restent interdits.', 'compte.php', '', 0, 1, '', '', '', NOW() +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_allow_inter_epr' 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_allow_inter_epr', 'en', 'Allow the same bib numbers across races', 'Lets multiple races share the same bib numbers (e.g. 5K and 10K). Duplicates within a single race remain forbidden.', 'compte.php', '', 0, 1, '', '', '', NOW() +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_allow_inter_epr' AND info_langue = 'en' 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_anomalies_inter_epr_dupes_title', 'fr', 'Dossards en double entre épreuves', 'Même numéro de dossard utilisé sur plus d''une épreuve alors que l''option n''est pas activée.', 'compte.php', '', 0, 1, '', '', '', NOW() +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_anomalies_inter_epr_dupes_title' 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_anomalies_inter_epr_dupes_title', 'en', 'Duplicate bibs across races', 'Same bib number used on more than one race while the option is disabled.', 'compte.php', '', 0, 1, '', '', '', NOW() +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_anomalies_inter_epr_dupes_title' AND info_langue = 'en' AND info_prg = 'compte.php');