From 8258e7ad293a8adcfbc7d15a51377f55b09eff57 Mon Sep 17 00:00:00 2001 From: stephan Date: Thu, 18 Jun 2026 10:42:27 -0400 Subject: [PATCH] Implement dashboard anomalies feature for bib management This commit introduces a new functionality to handle dashboard anomalies related to bibs, including duplicate and orphaned bibs. The PHP code is updated to include a new AJAX action for collecting anomalies, while JavaScript functions are added to refresh the anomalies panel dynamically. Additionally, new CSS styles are introduced to enhance the layout and presentation of the anomalies information. The version code is incremented to 4.72.665 to reflect these changes, improving the overall user experience in managing bib assignments. --- ajax_bib_range.php | 31 +++ css/style.css | 99 ++++++++ inc_footer_scripts.php | 30 +++ php/inc_fx_promoteur.php | 317 +++++++++++++++++++++++- php/inc_settings.php | 2 +- sql/MSIN-4379-bib_v4-anomalies-i18n.sql | 65 +++++ 6 files changed, 540 insertions(+), 4 deletions(-) create mode 100644 sql/MSIN-4379-bib_v4-anomalies-i18n.sql diff --git a/ajax_bib_range.php b/ajax_bib_range.php index e3d86fc..16c97b0 100644 --- a/ajax_bib_range.php +++ b/ajax_bib_range.php @@ -773,6 +773,37 @@ if ($action == 'save_auto_config') { exit; } +// MSIN-4379 — Dashboard anomalies (dossards en double, orphelins, etc.) +if ($action == 'anomalies') { + + if (!fxBibRequirePromoteurSession()) { + echo json_encode([ + 'success' => false, + 'message' => fxBibMsg('bib_v4_ajax_session_invalid') + ]); + exit; + } + + $int_eve_id = (int)($_POST['eve_id'] ?? $_GET['eve_id'] ?? 0); + + if ($int_eve_id <= 0) { + echo json_encode([ + 'success' => false, + 'message' => fxBibMsg('bib_v4_ajax_epr_invalid') + ]); + exit; + } + + $tabAnomalies = fxBibCollectAnomalies($int_eve_id, $strLangue); + + echo json_encode([ + 'success' => true, + 'count' => fxBibAnomalyTotalCount($tabAnomalies), + 'html' => renderBibAnomaliesPanel($int_eve_id, $strLangue, $tabAnomalies), + ]); + exit; +} + // ===================== // ACTION INVALIDE // ===================== diff --git a/css/style.css b/css/style.css index 68018e3..cec62e2 100644 --- a/css/style.css +++ b/css/style.css @@ -2157,3 +2157,102 @@ a.ms1-trad-link.btn-aide-trad{ .team-mode-option input[type="radio"]{ margin-top:2px; } + +/* MSIN-4379 — Dashboard anomalies dossards */ +.bib-tool-card-header{ + display:flex; + align-items:center; + justify-content:space-between; + gap:12px; + flex-wrap:wrap; +} + +.bib-tool-card-title{ + margin:0; +} + +.bib-anomalies-panel{ + margin-bottom:14px; + border:1px solid #ced4da; + border-radius:4px; + overflow:hidden; +} + +.bib-anomalies-panel--alert{ + border-color:#dc3545; +} + +.bib-anomalies-header{ + display:flex; + align-items:center; + justify-content:space-between; + gap:10px; +} + +.bib-anomalies-header-label{ + display:inline-flex; + align-items:center; + gap:8px; + flex-wrap:wrap; +} + +.bib-anomalies-count{ + font-size:12px; + font-weight:700; +} + +.bib-anomalies-content{ + background:#fff; +} + +.bib-anomaly-block{ + padding:10px 12px; + border-top:1px solid #e9ecef; +} + +.bib-anomaly-block:first-child{ + border-top:none; +} + +.bib-anomaly-block-title{ + display:flex; + align-items:center; + flex-wrap:wrap; + gap:8px; + margin:0 0 8px; + font-size:14px; + font-weight:700; + color:#212529; +} + +.bib-anomaly-list{ + margin:0; +} + +.bib-anomaly-item{ + padding:8px 10px; + margin-bottom:6px; + background:#f8f9fa; + border:1px solid #e9ecef; + border-radius:4px; + font-size:13px; +} + +.bib-anomaly-item:last-child{ + margin-bottom:0; +} + +.bib-anomaly-sublist{ + margin-top:4px; + padding-left:14px; + font-size:12px; + color:#495057; +} + +.bib-anomaly-detail{ + margin-top:4px; +} + +.bib-anomalies-empty{ + font-size:13px; +} diff --git a/inc_footer_scripts.php b/inc_footer_scripts.php index b6ecee3..fb04350 100644 --- a/inc_footer_scripts.php +++ b/inc_footer_scripts.php @@ -2167,6 +2167,35 @@ if ($strLangue == 'fr') { 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; + } + + fetch('/ajax_bib_range.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: 'action=anomalies&eve_id=' + encodeURIComponent(eveId) + bibAjaxLangSuffix() + }) + .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; @@ -2181,6 +2210,7 @@ if ($strLangue == 'fr') { initModuleBibTippy(row); } syncBibUiState(row); + refreshBibAnomaliesPanel(); return container; } diff --git a/php/inc_fx_promoteur.php b/php/inc_fx_promoteur.php index 6ae7aac..62ba5ec 100644 --- a/php/inc_fx_promoteur.php +++ b/php/inc_fx_promoteur.php @@ -3543,11 +3543,15 @@ function fxShowBibTool4($str_code, $int_eve_id, $strLangue){ ?> -
-

+
+

-
> +
> + +
+ +
@@ -3863,6 +3867,313 @@ function fxInfosBibRange($epr_id = 0, $epr_bib_id = 0){ return $objDatabase->fxGetResults($sqlBibStats); } +/** MSIN-4379 — Libellé court épreuve pour le dashboard anomalies. */ +function fxBibAnomalyEprLabel(array $tabEpreuve, $strLangue = 'fr') { + $str = fxBibEpreuveDisplayName($tabEpreuve, $strLangue); + $strNom = trim($tabEpreuve['epr_nom_' . $strLangue] ?? ''); + if ($strNom !== '' && stripos($str, $strNom) === false) { + $str .= ' — ' . $strNom; + } + return $str; +} + +/** + * MSIN-4379 — Dossards en double dans une séquence (même epr_id, même no_bib, >1 participant actif). + * @return array> + */ +function fxBibAnomalyDuplicateBibs($int_eve_id, $strLangue = 'fr') { + global $objDatabase; + + $int_eve_id = (int)$int_eve_id; + if ($int_eve_id <= 0) { + return []; + } + + $sqlBibNumP = fxBibSqlNoBibUnsigned('p.no_bib'); + $sql = " + SELECT + b.epr_bib_id, + b.epr_id, + b.epr_bib_start, + b.epr_bib_finish, + 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, + $sqlBibNumP AS no_bib, + p.par_id, + p.par_prenom, + p.par_nom + FROM inscriptions_epreuves_bib b + INNER JOIN inscriptions_epreuves e ON e.epr_id = b.epr_id + INNER JOIN resultats_participants p + ON p.epr_id = b.epr_id + AND p.is_cancelled = 0 + AND $sqlBibNumP BETWEEN b.epr_bib_start AND b.epr_bib_finish + AND $sqlBibNumP > 0 + WHERE e.eve_id = $int_eve_id + AND b.epr_bib_start IS NOT NULL + AND b.epr_bib_finish IS NOT NULL + AND ( + SELECT COUNT(*) + FROM resultats_participants p2 + WHERE p2.epr_id = b.epr_id + AND p2.is_cancelled = 0 + AND " . fxBibSqlNoBibUnsigned('p2.no_bib') . " = $sqlBibNumP + ) > 1 + ORDER BY b.epr_id, b.epr_bib_start, $sqlBibNumP, p.par_nom, p.par_prenom + "; + + $rows = $objDatabase->fxGetResults($sql); + if (!is_array($rows) || empty($rows)) { + return []; + } + + $grouped = []; + foreach ($rows as $row) { + $key = (int)$row['epr_bib_id'] . '-' . (int)$row['no_bib']; + if (!isset($grouped[$key])) { + $grouped[$key] = [ + 'epr_id' => (int)$row['epr_id'], + 'epr_label' => fxBibAnomalyEprLabel($row, $strLangue), + 'range_id' => (int)$row['epr_bib_id'], + 'range_start' => (int)$row['epr_bib_start'], + 'range_end' => (int)$row['epr_bib_finish'], + 'no_bib' => (int)$row['no_bib'], + 'participants' => [], + ]; + } + $grouped[$key]['participants'][] = [ + 'par_id' => (int)$row['par_id'], + 'par_prenom' => trim($row['par_prenom'] ?? ''), + 'par_nom' => trim($row['par_nom'] ?? ''), + ]; + } + + return array_values($grouped); +} + +/** + * MSIN-4379 — Dossards orphelins : numéro dans une séquence mais plus valide pour l'épreuve + * (coureur annulé, ou dossard conservé après changement d'épreuve). + * @return array> + */ +function fxBibAnomalyOrphanBibs($int_eve_id, $strLangue = 'fr') { + global $objDatabase; + + $int_eve_id = (int)$int_eve_id; + if ($int_eve_id <= 0) { + return []; + } + + $sqlBibNumP = fxBibSqlNoBibUnsigned('p.no_bib'); + $sql = " + SELECT + b.epr_bib_id, + b.epr_id, + b.epr_bib_start, + b.epr_bib_finish, + 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, + $sqlBibNumP AS no_bib, + p.par_id, + p.par_prenom, + p.par_nom, + p.epr_id AS participant_epr_id, + p.is_cancelled, + CASE + WHEN p.epr_id <> b.epr_id THEN 'transferred' + WHEN p.is_cancelled = 1 THEN 'cancelled' + ELSE 'other' + END AS orphan_type + FROM inscriptions_epreuves_bib b + INNER JOIN inscriptions_epreuves e ON e.epr_id = b.epr_id + INNER JOIN resultats_participants p + ON p.eve_id = e.eve_id + AND $sqlBibNumP > 0 + AND $sqlBibNumP BETWEEN b.epr_bib_start AND b.epr_bib_finish + WHERE e.eve_id = $int_eve_id + AND b.epr_bib_start IS NOT NULL + AND b.epr_bib_finish IS NOT NULL + AND ( + (p.epr_id <> b.epr_id) + OR (p.epr_id = b.epr_id AND p.is_cancelled = 1) + ) + ORDER BY b.epr_id, b.epr_bib_start, $sqlBibNumP, p.par_nom, p.par_prenom + "; + + $rows = $objDatabase->fxGetResults($sql); + if (!is_array($rows) || empty($rows)) { + return []; + } + + $out = []; + foreach ($rows as $row) { + $participantEprId = (int)$row['participant_epr_id']; + $participantLabel = ''; + if ($participantEprId > 0) { + $tabHolderEpr = $objDatabase->fxGetRow(" + SELECT epr_id, epr_nom_fr, epr_nom_en, epr_type_fr, epr_type_en, + epr_categorie_fr, epr_categorie_en, epr_actif + FROM inscriptions_epreuves + WHERE epr_id = $participantEprId + LIMIT 1 + "); + if (is_array($tabHolderEpr)) { + $participantLabel = fxBibAnomalyEprLabel($tabHolderEpr, $strLangue); + } + } + + $out[] = [ + 'epr_id' => (int)$row['epr_id'], + 'epr_label' => fxBibAnomalyEprLabel($row, $strLangue), + 'range_id' => (int)$row['epr_bib_id'], + 'range_start' => (int)$row['epr_bib_start'], + 'range_end' => (int)$row['epr_bib_finish'], + 'no_bib' => (int)$row['no_bib'], + 'orphan_type' => $row['orphan_type'], + 'participant_epr_id' => $participantEprId, + 'participant_epr_label'=> $participantLabel, + 'par_id' => (int)$row['par_id'], + 'par_prenom' => trim($row['par_prenom'] ?? ''), + 'par_nom' => trim($row['par_nom'] ?? ''), + ]; + } + + return $out; +} + +/** 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), + 'orphans' => fxBibAnomalyOrphanBibs($int_eve_id, $strLangue), + ]; +} + +function fxBibAnomalyTotalCount(array $tabAnomalies) { + $intTotal = 0; + foreach (['dupes', 'orphans'] as $strKey) { + if (!empty($tabAnomalies[$strKey]) && is_array($tabAnomalies[$strKey])) { + $intTotal += count($tabAnomalies[$strKey]); + } + } + return $intTotal; +} + +/** MSIN-4379 — Panneau dashboard anomalies (HTML). */ +function renderBibAnomaliesPanel($int_eve_id, $strLangue = 'fr', $tabAnomalies = null) { + $int_eve_id = (int)$int_eve_id; + if ($tabAnomalies === null) { + $tabAnomalies = fxBibCollectAnomalies($int_eve_id, $strLangue); + } + + $intCount = fxBibAnomalyTotalCount($tabAnomalies); + $blnHasIssues = $intCount > 0; + + ob_start(); + ?> + +