Add bib anomaly detection for unconfigured sequences

This commit introduces a new function, `fxBibSqlEpreuveHasBibRanges`, to check for the presence of bib ranges in events. It enhances the existing anomaly detection by adding support for cases where no bib ranges are configured, allowing for better reporting of duplicate bibs. Additionally, relevant language strings are added for both French and English to support multilingual reporting. The version code is incremented to reflect these changes.
This commit is contained in:
2026-07-02 12:46:21 -04:00
parent 81e55ea0ec
commit 841fea5103
2 changed files with 124 additions and 47 deletions

View File

@ -2897,6 +2897,7 @@ function fxBibStaticFallback($clef) {
'bib_v4_err_dupes' => ['fr' => 'Dossards en double :', 'en' => 'Duplicate bibs:'],
'bib_v4_anomalies_auto_miss_title' => ['fr' => 'Inscriptions sans dossard (auto)', 'en' => 'Registrations without a bib (auto)'],
'bib_v4_anomalies_out_of_range_title' => ['fr' => 'Dossards hors séquence', 'en' => 'Bibs outside sequences'],
'bib_v4_anomalies_no_range' => ['fr' => 'Aucune séquence configurée', 'en' => 'No sequence configured'],
'bib_v4_anomalies_order_fmt' => ['fr' => 'Commande %s', 'en' => 'Order %s'],
'bib_v4_anomalies_assign_link' => ['fr' => 'Assigner un dossard', 'en' => 'Assign a bib'],
'bib_v4_js_loader_wait' => ['fr' => 'Chargement...', 'en' => 'Loading...'],
@ -4121,8 +4122,52 @@ function fxBibAnomalyEprLabel(array $tabEpreuve, $strLangue = 'fr') {
return $str;
}
/** SQL — l'épreuve a au moins une séquence de dossards configurée. */
function fxBibSqlEpreuveHasBibRanges($strEprIdExpr) {
return "EXISTS (
SELECT 1
FROM inscriptions_epreuves_bib b0
WHERE b0.epr_id = ($strEprIdExpr)
AND b0.epr_bib_start IS NOT NULL
AND b0.epr_bib_finish IS NOT NULL
)";
}
/**
* MSIN-4379 — Dossards en double dans une séquence (même epr_id, même no_bib, >1 participant actif).
* MSIN-4379 — Regroupe les lignes SQL de fxBibAnomalyDuplicateBibs par séquence + no_bib.
*
* @return array<int, array<string, mixed>>
*/
function fxBibAnomalyGroupDuplicateRows(array $rows, $strLangue = 'fr') {
$grouped = [];
foreach ($rows as $row) {
$intRangeId = (int)$row['epr_bib_id'];
$key = ($intRangeId > 0 ? $intRangeId : (int)$row['epr_id']) . '-' . (int)$row['no_bib'];
if (!isset($grouped[$key])) {
$grouped[$key] = [
'epr_id' => (int)$row['epr_id'],
'epr_label' => fxBibAnomalyEprLabel($row, $strLangue),
'range_id' => $intRangeId,
'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 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).
*
* @return array<int, array<string, mixed>>
*/
function fxBibAnomalyDuplicateBibs($int_eve_id, $strLangue = 'fr') {
@ -4134,7 +4179,7 @@ function fxBibAnomalyDuplicateBibs($int_eve_id, $strLangue = 'fr') {
}
$sqlBibNumP = fxBibSqlNoBibUnsigned('p.no_bib');
$sql = "
$sqlInRange = "
SELECT
b.epr_bib_id,
b.epr_id,
@ -4165,33 +4210,48 @@ function fxBibAnomalyDuplicateBibs($int_eve_id, $strLangue = 'fr') {
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)) {
$sqlNoRange = "
SELECT
0 AS epr_bib_id,
p.epr_id,
0 AS epr_bib_start,
0 AS 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 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 NOT " . fxBibSqlEpreuveHasBibRanges('p.epr_id') . "
AND " . fxBibSqlIsBibDuplicateAnomaly('p.epr_id', $sqlBibNumP) . "
ORDER BY p.epr_id, $sqlBibNumP, p.par_nom, p.par_prenom
";
$rows = $objDatabase->fxGetResults($sqlInRange);
if (!is_array($rows)) {
$rows = [];
}
$rowsNoRange = $objDatabase->fxGetResults($sqlNoRange);
if (is_array($rowsNoRange) && !empty($rowsNoRange)) {
$rows = array_merge($rows, $rowsNoRange);
}
if (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);
return fxBibAnomalyGroupDuplicateRows($rows, $strLangue);
}
/**
@ -4380,7 +4440,7 @@ function fxBibAnomalyAutoMissBibs($int_eve_id, $strLangue = 'fr') {
/**
* MSIN-4379 — Dossards assignés hors de toute séquence configurée pour l'épreuve.
* Couvre la saisie manuelle (fiche coureur) ou tout assignement ne passant pas par les plages.
* On ne signale que les épreuves qui ont au moins une séquence (sinon rien à comparer).
* Inclut les épreuves sans aucune séquence (dossards uniques seulement — les doublons vont dans dupes).
*
* @return array<int, array<string, mixed>>
*/
@ -4412,20 +4472,22 @@ function fxBibAnomalyOutOfRangeBibs($int_eve_id, $strLangue = 'fr') {
WHERE e.eve_id = $int_eve_id
AND p.is_cancelled = 0
AND $sqlBibNumP > 0
AND EXISTS (
SELECT 1
FROM inscriptions_epreuves_bib b0
WHERE b0.epr_id = p.epr_id
AND b0.epr_bib_start IS NOT NULL
AND b0.epr_bib_finish IS NOT NULL
)
AND NOT EXISTS (
SELECT 1
FROM inscriptions_epreuves_bib b
WHERE b.epr_id = p.epr_id
AND b.epr_bib_start IS NOT NULL
AND b.epr_bib_finish IS NOT NULL
AND $sqlBibNumP BETWEEN b.epr_bib_start AND b.epr_bib_finish
AND (
(
" . fxBibSqlEpreuveHasBibRanges('p.epr_id') . "
AND NOT EXISTS (
SELECT 1
FROM inscriptions_epreuves_bib b
WHERE b.epr_id = p.epr_id
AND b.epr_bib_start IS NOT NULL
AND b.epr_bib_finish IS NOT NULL
AND $sqlBibNumP BETWEEN b.epr_bib_start AND b.epr_bib_finish
)
)
OR (
NOT " . fxBibSqlEpreuveHasBibRanges('p.epr_id') . "
AND NOT " . fxBibSqlIsBibDuplicateAnomaly('p.epr_id', $sqlBibNumP) . "
)
)
ORDER BY p.epr_id, $sqlBibNumP, p.par_nom, p.par_prenom
";
@ -4512,12 +4574,19 @@ function renderBibAnomaliesPanel($int_eve_id, $strLangue = 'fr', $tabAnomalies =
<?php foreach ($tabAnomalies['dupes'] as $item) { ?>
<li class="bib-anomaly-item">
<strong><?php echo fxBibEsc($item['epr_label']); ?></strong>
<?php echo fxBibMsgTrad(
'bib_v4_anomalies_range_fmt',
(int)$item['range_start'],
(int)$item['range_end']
); ?>
— <?php echo fxBibMsgTrad(
<?php if ((int)$item['range_start'] > 0 || (int)$item['range_end'] > 0) { ?>
<?php echo fxBibMsgTrad(
'bib_v4_anomalies_range_fmt',
(int)$item['range_start'],
(int)$item['range_end']
); ?>
<?php } else { ?>
<?php fxBibTexteTrad('bib_v4_anomalies_no_range', 1); ?>
<?php } ?>
<?php echo fxBibMsgTrad(
'bib_v4_anomalies_bib_fmt',
(int)$item['no_bib']
); ?>

View File

@ -9,3 +9,11 @@ FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_anomali
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_out_of_range_title', 'en', 'Bibs outside sequences', 'Number assigned to an active runner but not falling within any sequence configured for their race (manual entry on profile, import, etc.).', 'compte.php', '', 0, 1, '', '', '', NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_anomalies_out_of_range_title' 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_no_range', 'fr', 'Aucune séquence configurée', 'L''épreuve n''a pas de plage de dossards ; les doublons y sont listés dans la section « Dossards en double ».', 'compte.php', '', 0, 1, '', '', '', NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_anomalies_no_range' 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_no_range', 'en', 'No sequence configured', 'This race has no bib range; duplicates are listed under « Duplicate bib numbers ».', 'compte.php', '', 0, 1, '', '', '', NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_anomalies_no_range' AND info_langue = 'en' AND info_prg = 'compte.php');