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(); + ?> + +