Enhance participant synchronization and error handling in ChronoTrack API

This commit introduces a new function to classify blocked participants by type, improving the handling of synchronization errors related to missing or invalid data. The changes include updates to the participant classification logic, allowing for better reporting of issues such as duplicate bibs, missing external IDs, and unmapped races. Additionally, the push preview functionality is enhanced to display blocked participants clearly, improving user feedback during the synchronization process. These updates aim to streamline participant management and enhance the overall user experience within the ChronoTrack API.
This commit is contained in:
2026-07-03 09:56:48 -04:00
parent 338734db7a
commit ec1e98d898
2 changed files with 210 additions and 114 deletions

View File

@ -224,7 +224,8 @@ function fxChronotrackApiAdminRenderPushPanel($intEveId, $blnClosed = false) {
echo '<p class="small text-muted mb-2">Lit <code>resultats_participants</code> (comme le rapport chrono). ';
echo 'External ID = <code>par_id_original</code>. Dossard obligatoire ; sexe vide → NOT SPECIFIED côté CT ; ';
echo 'doublon de dossard MS1 = aucun des inscrits concernés n\'est transféré.</p>';
echo '<div id="ct_api_push_summary" class="mb-2">' . fxChronotrackApiAdminRenderWaitHtml('Chargement…', $vDomaine) . '</div>';
echo '<div id="ct_api_push_summary" class="mb-3">' . fxChronotrackApiAdminRenderWaitHtml('Chargement…', $vDomaine) . '</div>';
echo '<div id="ct_api_push_blocked_wrap" class="mb-3 d-none"></div>';
echo '<button type="button" class="btn btn-primary btn-sm" id="ct_api_btn_push" disabled>';
echo '<i class="fa fa-cloud-upload" aria-hidden="true"></i>&nbsp;Pousser vers ChronoTrack</button> ';
echo '<button type="button" class="btn btn-outline-secondary btn-sm" id="ct_api_btn_push_refresh">';
@ -267,6 +268,14 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
}
}
?>
<style>
#ct_api_push_blocked_wrap .ct-api-blocked-table { font-size: 0.8rem; }
#ct_api_push_blocked_wrap .ct-api-blocked-table th { white-space: nowrap; }
#ct_api_push_blocked_wrap .ct-api-blocked-scroll { max-height: 70vh; overflow: auto; }
#ct_api_push_summary .ct-api-stat-row { display: flex; flex-wrap: wrap; gap: 0.5rem 1.25rem; }
#ct_api_push_summary .ct-api-stat { min-width: 140px; }
#ct_api_push_summary .ct-api-stat strong { font-size: 1.25rem; display: block; line-height: 1.2; }
</style>
<?php fxChronotrackApiAdminRenderLoaderAssets($vDomaine); ?>
<script>
(function ($) {
@ -610,74 +619,123 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
window.prompt('Copier le rapport (Ctrl+C) :', strText);
});
function renderAnomaliesList(res, strLimit) {
var tabItems = (res && res.anomalies) ? res.anomalies : [];
if (!tabItems.length) {
return '';
var objPushPreview = null;
function renderBlockedPanel(res) {
var $wrap = $('#ct_api_push_blocked_wrap');
if (!$wrap.length) {
return;
}
var intMax = strLimit || 12;
var html = '<details class="mt-2 mb-0"><summary class="text-danger small"><strong>Anomalies MS1 ('
+ tabItems.length + ')</strong> — corriger avant transfert</summary>';
html += '<ul class="text-danger small mb-0 pl-3 mt-1">';
$.each(tabItems.slice(0, intMax), function (_, a) {
var strLine = escHtml(a.name || ('external_id=' + a.external_id));
if (a.no_bib) {
strLine += ' · dossard #' + escHtml(a.no_bib);
var tabGroups = (res && res.blocked_by_type) ? res.blocked_by_type : [];
var intTotal = (res && res.blocked_count) ? res.blocked_count : 0;
if (intTotal <= 0 || !tabGroups.length) {
$wrap.addClass('d-none').empty();
return;
}
var html = '<div class="card border-danger">';
html += '<div class="card-header py-2 bg-white">';
html += '<a class="d-block text-danger collapsed" data-toggle="collapse" href="#ct_api_blocked_detail" role="button" aria-expanded="false">';
html += '<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> ';
html += '<strong>Exclus du transfert</strong> — <span class="badge badge-danger">' + intTotal + '</span>';
html += ' <span class="text-muted small">(cliquer pour le détail complet)</span>';
html += '</a></div>';
html += '<div class="collapse" id="ct_api_blocked_detail"><div class="card-body py-2">';
$.each(tabGroups, function (_, grp) {
var intCnt = (grp.items && grp.items.length) ? grp.items.length : 0;
if (intCnt === 0) {
return;
}
strLine += ' — ' + escHtml(a.message || a.code || 'anomalie');
html += '<li>' + strLine + '</li>';
html += '<div class="mb-3">';
html += '<div class="d-flex align-items-center mb-1">';
html += '<strong class="text-danger small">' + escHtml(grp.label || grp.code) + '</strong>';
html += ' <span class="badge badge-secondary ml-1">' + intCnt + '</span>';
html += '</div>';
html += '<div class="ct-api-blocked-scroll border rounded">';
html += '<table class="table table-sm table-striped ct-api-blocked-table mb-0">';
html += '<thead class="thead-light"><tr>';
html += '<th>Nom</th><th>external_id</th><th>par_id</th><th>Dossard</th><th>Sexe</th><th>Raison</th>';
html += '</tr></thead><tbody>';
$.each(grp.items, function (__, item) {
html += '<tr>';
html += '<td>' + escHtml(item.name || '—') + '</td>';
html += '<td>' + escHtml(item.external_id || '—') + '</td>';
html += '<td>' + escHtml(String(item.par_id || '')) + '</td>';
html += '<td>' + escHtml(item.no_bib ? ('#' + item.no_bib) : (item.bib ? ('#' + item.bib) : '—')) + '</td>';
html += '<td>' + escHtml(item.par_sexe || '—') + '</td>';
html += '<td class="text-muted">' + escHtml(item.message || '') + '</td>';
html += '</tr>';
});
html += '</tbody></table></div></div>';
});
if (tabItems.length > intMax) {
html += '<li class="text-muted">… et ' + (tabItems.length - intMax) + ' autre(s)</li>';
}
html += '</ul></details>';
return html;
html += '</div></div></div>';
$wrap.removeClass('d-none').html(html);
}
function renderPushPreview(res) {
var $sum = $('#ct_api_push_summary');
var $btn = $('#ct_api_btn_push');
objPushPreview = res;
if (!$sum.length) {
return;
}
if (!res || res.state !== 'ok') {
$sum.html('<div class="text-danger">' + escHtml((res && res.message) ? res.message : 'Erreur') + '</div>');
$('#ct_api_push_blocked_wrap').addClass('d-none').empty();
$btn.prop('disabled', true);
return;
}
var html = '<ul class="mb-0 pl-3">';
html += '<li><strong>' + res.ms1_total + '</strong> participant(s) MS1 actifs</li>';
html += '<li><strong>' + res.transferable + '</strong> transférable(s) (dossard + sexe + épreuve mappée)</li>';
html += '<li><strong>' + res.ct_total + '</strong> déjà dans ChronoTrack</li>';
html += '<li><strong class="text-primary">' + res.to_create + '</strong> nouveau(x) à créer';
if (res.already_in_ct > 0) {
html += ' · <strong>' + res.already_in_ct + '</strong> déjà présent(s) (seront mis à jour au push)';
}
html += '</li>';
if (res.skipped_no_race > 0) {
html += '<li class="text-warning">' + res.skipped_no_race + ' ignoré(s) — épreuve non mappée</li>';
}
if (res.skipped_no_external > 0) {
html += '<li class="text-warning">' + res.skipped_no_external + ' ignoré(s) — par_id_original manquant</li>';
}
if (res.anomaly_no_bib > 0) {
html += '<li class="text-danger">' + res.anomaly_no_bib + ' anomalie(s) — dossard manquant</li>';
}
if (res.anomaly_no_sex > 0) {
html += '<li class="text-danger">' + res.anomaly_no_sex + ' anomalie(s) — sexe MS1 non reconnu (ex. n, a)</li>';
}
if (res.anomaly_duplicate_bib > 0) {
html += '<li class="text-danger">' + res.anomaly_duplicate_bib + ' anomalie(s) — dossard en double MS1';
if (res.duplicate_bib_numbers && res.duplicate_bib_numbers.length) {
var arrBibLabels = $.map(res.duplicate_bib_numbers, function (n) { return '#' + n; });
html += ' (' + escHtml(arrBibLabels.join(', ')) + ')';
var intBlocked = res.blocked_count || 0;
var intTransfer = res.transferable || 0;
var intMs1 = res.ms1_total || 0;
var html = '<p class="small text-muted mb-2 mb-md-1">Avant le push — qui part, qui reste :</p>';
html += '<div class="ct-api-stat-row mb-2">';
html += '<div class="ct-api-stat"><strong>' + intMs1 + '</strong><span class="text-muted small">inscrits MS1 actifs</span></div>';
html += '<div class="ct-api-stat text-primary"><strong>' + intTransfer + '</strong><span class="small">seront envoyés</span></div>';
html += '<div class="ct-api-stat ' + (intBlocked > 0 ? 'text-danger' : 'text-muted') + '">';
html += '<strong>' + intBlocked + '</strong><span class="small">exclus</span></div>';
html += '</div>';
if (intTransfer > 0) {
html += '<p class="small mb-1">Côté ChronoTrack : <strong>' + (res.to_create || 0) + '</strong> nouveau(x)';
if (res.already_in_ct > 0) {
html += ', <strong>' + res.already_in_ct + '</strong> déjà présent(s) (mise à jour au push)';
}
html += '</li>';
html += ' · <span class="text-muted">' + (res.ct_total || 0) + ' entries CT au total</span></p>';
}
html += '</ul>';
html += renderAnomaliesList(res, 15);
if (intBlocked > 0) {
html += '<p class="small text-danger mb-0"><i class="fa fa-info-circle" aria-hidden="true"></i> ';
html += 'Les <strong>' + intBlocked + '</strong> exclus ne seront <em>pas</em> envoyés tant que non corrigés dans MS1. ';
html += 'Détail dans la zone ci-dessous.</p>';
}
$sum.html(html);
$btn.prop('disabled', res.transferable <= 0);
renderBlockedPanel(res);
$btn.prop('disabled', intTransfer <= 0);
}
function renderPushResult(res) {
var $res = $('#ct_api_push_result');
var strClass = 'text-danger';
if (res && (res.state === 'ok' || res.state === 'partial')) {
strClass = res.state === 'ok' ? 'text-success' : 'text-warning';
}
var strHtml = '<div class="' + strClass + '"><strong>Résultat du push</strong> — '
+ escHtml((res && res.message) ? res.message : 'Erreur') + '</div>';
if (res && res.errors && res.errors.length) {
strHtml += '<p class="small text-danger mb-1 mt-2">Erreurs API ChronoTrack :</p>';
strHtml += '<ul class="text-danger small mb-0 pl-3">';
$.each(res.errors, function (_, msg) {
strHtml += '<li>' + escHtml(msg) + '</li>';
});
strHtml += '</ul>';
}
$res.html(strHtml);
}
function loadPushPreview() {
@ -734,20 +792,7 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
$btn.prop('disabled', true);
$res.html(waitHtml('Envoi en cours… (cela peut prendre quelques minutes)'));
$.post(strAjaxUrl, { a: 'sync_push', eve_id: intEveId }, function (res) {
var strClass = 'text-danger';
if (res && (res.state === 'ok' || res.state === 'partial')) {
strClass = res.state === 'ok' ? 'text-success' : 'text-warning';
}
var strHtml = '<div class="' + strClass + '">' + escHtml((res && res.message) ? res.message : 'Erreur') + '</div>';
if (res && res.errors && res.errors.length) {
strHtml += '<ul class="text-danger mb-0 pl-3">';
$.each(res.errors, function (_, msg) {
strHtml += '<li>' + escHtml(msg) + '</li>';
});
strHtml += '</ul>';
}
strHtml += renderAnomaliesList(res, 12);
$res.html(strHtml);
renderPushResult(res);
loadPushPreview();
}, 'json').fail(function () {
$res.html('<div class="text-danger">Erreur réseau lors du push.</div>');