Enhance MS1 loader functionality and integrate loading indicators in ChronoTrack API

This commit updates the MS1 loader to include a message parameter for inline HTML usage, ensuring consistent messaging during AJAX operations. Additionally, new functions are introduced to render loading indicators and assets, improving user feedback during data synchronization processes. The integration of these features aims to enhance the overall user experience and clarity within the ChronoTrack API admin interface.
This commit is contained in:
2026-07-03 09:46:32 -04:00
parent 411f2a4d8f
commit 338734db7a
3 changed files with 319 additions and 91 deletions

View File

@ -7,6 +7,20 @@ function fxChronotrackApiAdminEsc($str) {
return htmlspecialchars((string)$str, ENT_QUOTES, 'UTF-8');
}
function fxChronotrackApiAdminRenderWaitHtml($strMessage, $vDomaine) {
$strSrc = fxChronotrackApiAdminEsc($vDomaine . '/images/ms1-runner-loader.gif?v=' . _VERSION_CODE);
$strMsg = fxChronotrackApiAdminEsc($strMessage);
return '<div class="ms1-loader ms1-loader--inline" role="status" aria-live="polite" aria-busy="true">'
. '<img class="ms1-loader__runner" src="' . $strSrc . '" alt="" role="presentation" decoding="async">'
. '<span class="ms1-loader__label">' . $strMsg . '</span>'
. '</div>';
}
function fxChronotrackApiAdminRenderLoaderAssets($vDomaine) {
echo '<link rel="stylesheet" href="' . fxChronotrackApiAdminEsc($vDomaine) . '/css/ms1-loader.css?v=' . _VERSION_CODE . '">';
echo '<script src="' . fxChronotrackApiAdminEsc($vDomaine) . '/js/ms1-loader.js?v=' . _VERSION_CODE . '"></script>';
}
function fxChronotrackApiAdminRenderEventLinkPicker($blnClosed) {
echo '<div class="card mb-4" id="ct_api_event_link_card"><div class="card-header">1. Lier un événement ChronoTrack</div><div class="card-body">';
if ($blnClosed) {
@ -200,6 +214,7 @@ function fxChronotrackApiAdminRenderRaceMapping($intEveId, $arrConfig, $strLangu
}
function fxChronotrackApiAdminRenderPushPanel($intEveId, $blnClosed = false) {
global $vDomaine;
echo '<div class="card mb-4 border-primary" id="ct_api_push_card">';
echo '<div class="card-header">3. Synchronisation manuelle (participants)</div>';
echo '<div class="card-body">';
@ -207,8 +222,9 @@ function fxChronotrackApiAdminRenderPushPanel($intEveId, $blnClosed = false) {
echo '<div class="alert alert-warning py-2 small mb-2">Événement fermé — push manuel autorisé pour tests ou rattrapage.</div>';
}
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>. POST vers ChronoTrack — aucune modification MS1.</p>';
echo '<div id="ct_api_push_summary" class="mb-2"><span class="text-muted">Chargement…</span></div>';
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 '<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">';
@ -251,6 +267,7 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
}
}
?>
<?php fxChronotrackApiAdminRenderLoaderAssets($vDomaine); ?>
<script>
(function ($) {
var intEveId = <?php echo intval($intEveId); ?>;
@ -261,6 +278,23 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
var blnHasSavedMapping = <?php echo $blnHasSavedMapping ? 'true' : 'false'; ?>;
var blnClosed = <?php echo $blnClosed ? 'true' : 'false'; ?>;
function escHtml(str) {
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
if (window.Ms1Loader) {
Ms1Loader.init({
src: <?php echo json_encode($vDomaine . '/images/ms1-runner-loader.gif?v=' . _VERSION_CODE); ?>
});
}
function waitHtml(strMessage) {
if (window.Ms1Loader) {
return Ms1Loader.inlineHtml(strMessage);
}
return '<span class="text-muted">' + escHtml(strMessage || 'Chargement…') + '</span>';
}
function showMsg(strType, strText) {
window.alert(strText);
}
@ -292,6 +326,8 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
}
$('#ct_api_btn_load').on('click', function () {
var $wrap = $('#ct_api_events_wrap');
$wrap.html(waitHtml('Chargement de la liste ChronoTrack…'));
$.post(strAjaxUrl, {
a: 'list_events',
eve_id: intEveId,
@ -317,6 +353,8 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
showMsg('error', 'Sélectionnez un événement ChronoTrack');
return;
}
var $btn = $(this);
$btn.prop('disabled', true);
$.post(strAjaxUrl, {
a: 'save_link',
eve_id: intEveId,
@ -324,10 +362,14 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
}, function (res) {
if (!res || res.state !== 'ok') {
showMsg('error', (res && res.message) ? res.message : 'Erreur enregistrement');
$btn.prop('disabled', false);
return;
}
window.location.reload();
}, 'json');
}, 'json').fail(function () {
showMsg('error', 'Erreur réseau');
$btn.prop('disabled', false);
});
});
$('.ct-api-set-status').on('click', function () {
@ -357,7 +399,7 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
return;
}
var $status = $('#ct_api_races_status');
$status.removeClass('text-danger text-warning text-success').text('Chargement des courses ChronoTrack…');
$status.removeClass('text-danger text-warning text-success').html(waitHtml('Chargement des courses ChronoTrack…'));
$.post(strAjaxUrl, { a: 'list_races', eve_id: intEveId, ct_event_id: intLinkedCtEventId }, function (res) {
if (!res || res.state !== 'ok') {
$status.addClass('text-danger').text((res && res.message) ? res.message : 'Erreur chargement des courses');
@ -435,21 +477,23 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
var strRaceId = $(this).val();
tabMap[intEprId] = { ct_race_id: strRaceId ? parseInt(strRaceId, 10) : 0, ct_race_name: $(this).find('option:selected').text() };
});
var $btn = $(this);
$btn.prop('disabled', true);
$.post(strAjaxUrl, { a: 'save_race_map', eve_id: intEveId, map: tabMap }, function (res) {
if (!res || res.state !== 'ok') {
showMsg('error', (res && res.message) ? res.message : 'Erreur mapping');
$btn.prop('disabled', false);
return;
}
window.location.reload();
}, 'json');
}, 'json').fail(function () {
showMsg('error', 'Erreur réseau');
$btn.prop('disabled', false);
});
});
var objDiagnosticReport = null;
function escHtml(str) {
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function renderDiagnosticReport(res) {
objDiagnosticReport = res;
var $out = $('#ct_api_diagnostic_out');
@ -539,7 +583,7 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
$('#ct_api_btn_diagnostic').on('click', function () {
var $out = $('#ct_api_diagnostic_out');
$out.text('Diagnostic en cours… (OAuth + 4 endpoints CT, dont entry)');
$out.html(waitHtml('Diagnostic en cours… (OAuth + 4 endpoints CT, dont entry)'));
$('#ct_api_btn_diagnostic_copy').addClass('d-none');
$.post(strAjaxUrl, {
a: 'diagnostic',
@ -566,6 +610,30 @@ 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 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);
}
strLine += ' — ' + escHtml(a.message || a.code || 'anomalie');
html += '<li>' + strLine + '</li>';
});
if (tabItems.length > intMax) {
html += '<li class="text-muted">… et ' + (tabItems.length - intMax) + ' autre(s)</li>';
}
html += '</ul></details>';
return html;
}
function renderPushPreview(res) {
var $sum = $('#ct_api_push_summary');
var $btn = $('#ct_api_btn_push');
@ -579,7 +647,7 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
}
var html = '<ul class="mb-0 pl-3">';
html += '<li><strong>' + res.ms1_total + '</strong> participant(s) MS1 actifs</li>';
html += '<li><strong>' + res.eligible + '</strong> éligible(s) (épreuve mappée + external ID)</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) {
@ -592,9 +660,24 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
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(', ')) + ')';
}
html += '</li>';
}
html += '</ul>';
html += renderAnomaliesList(res, 15);
$sum.html(html);
$btn.prop('disabled', res.eligible <= 0);
$btn.prop('disabled', res.transferable <= 0);
}
function loadPushPreview() {
@ -602,7 +685,7 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
if (!$sum.length) {
return;
}
$sum.html('<span class="text-muted">Calcul du diff MS1 ↔ ChronoTrack…</span>');
$sum.html(waitHtml('Calcul du diff MS1 ↔ ChronoTrack…'));
$.post(strAjaxUrl, { a: 'sync_preview', eve_id: intEveId }, function (res) {
renderPushPreview(res);
}, 'json').fail(function () {
@ -622,7 +705,7 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
var $btn = $(this);
var $res = $('#ct_api_push_result');
$btn.prop('disabled', true);
$res.html('<span class="text-muted">Sonde POST (1 participant)…</span>');
$res.html(waitHtml('Sonde POST (1 participant)…'));
$.post(strAjaxUrl, { a: 'sync_probe_push', eve_id: intEveId }, function (res) {
var strClass = (res && res.state === 'ok') ? 'text-success' : 'text-danger';
var strHtml = '<div class="' + strClass + '"><strong>Sonde</strong> — ' + escHtml((res && res.message) ? res.message : 'Erreur') + '</div>';
@ -649,7 +732,7 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
var $btn = $(this);
var $res = $('#ct_api_push_result');
$btn.prop('disabled', true);
$res.html('<span class="text-muted">Envoi en cours…</span>');
$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')) {
@ -663,6 +746,7 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
});
strHtml += '</ul>';
}
strHtml += renderAnomaliesList(res, 12);
$res.html(strHtml);
loadPushPreview();
}, 'json').fail(function () {