Files
ms1inscription-v5/php/chronotrack_api/fx_chronotrack_admin.php
stephan 29f836e615 Implement orphan management functionality in ChronoTrack API
This commit introduces new features for handling orphan entries within the ChronoTrack API. It adds a new endpoint for syncing orphan entries, enhancing the ability to identify and manage entries that are present in the system but not eligible for transfer. The user interface is updated to include a dedicated section for displaying orphan entries, improving clarity and user interaction. Additionally, the synchronization logic is refined to compute and report orphan entries effectively, aiming to enhance overall data management and user experience in the API.
2026-07-03 20:49:19 -04:00

1038 lines
48 KiB
PHP

<?php
/**
* MSIN API ChronoTrack — rendu admin superadm (Phase 1 : lien + mapping).
*/
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) {
echo '<p class="text-muted mb-0">Lecture seule.</p>';
} else {
echo '<div class="form-row align-items-end mb-3">';
echo '<div class="col-md-5 mb-2"><label for="ct_api_search">Recherche</label>';
echo '<input type="text" class="form-control form-control-sm" id="ct_api_search" placeholder="Nom ou ID ChronoTrack"></div>';
echo '<div class="col-md-4 mb-2"><div class="form-check mt-4">';
echo '<input type="checkbox" class="form-check-input" id="ct_api_show_past" value="1">';
echo '<label class="form-check-label" for="ct_api_show_past">Afficher les événements passés</label></div></div>';
echo '<div class="col-md-3 mb-2"><button type="button" class="btn btn-primary btn-sm btn-block" id="ct_api_btn_load">Charger la liste</button></div>';
echo '</div>';
echo '<div id="ct_api_events_wrap" class="border rounded p-2" style="max-height:320px;overflow:auto;">';
echo '<p class="text-muted small mb-0">Cliquez sur « Charger la liste » pour voir les événements à venir (tri par date).</p>';
echo '</div>';
echo '<button type="button" class="btn btn-success btn-sm mt-3" id="ct_api_btn_save_link" disabled>Enregistrer le lien</button>';
}
echo '</div></div>';
}
function fxChronotrackApiAdminRenderEventPage($intEveId, $strLangue = 'fr', $blnInlineOnEventForm = false, $blnInsideV2Panel = false) {
global $vDomaine;
$arrEvent = fxChronotrackApiMs1EventRow($intEveId, $strLangue);
if ($arrEvent === null) {
echo '<div class="alert alert-danger">Événement MS1 introuvable.</div>';
return;
}
$arrConfig = fxChronotrackApiConfigGet($intEveId);
$blnConfigured = fxChronotrackApiSettingsConfigured();
$strStatus = $arrConfig['sync_status'] ?? '';
$blnClosed = ($strStatus === MSIN_API_CHRONOTRACK_SYNC_FERME);
$strWrapperId = '';
echo '<div class="ct-api-admin mt-2 mb-3"' . $strWrapperId . '>';
if (!$blnInsideV2Panel) {
if ($blnInlineOnEventForm) {
echo '<h2 class="h5">MSIN API ChronoTrack</h2>';
} else {
echo '<h1 class="h3">MSIN API ChronoTrack</h1>';
}
}
echo '<p class="text-muted">Configuration du lien pour l\'événement MS1 <strong>' . fxChronotrackApiAdminEsc($arrEvent['eve_nom']) . '</strong> (eve_id '
. intval($arrEvent['eve_id']) . ').</p>';
if (!$blnConfigured) {
echo '<div class="alert alert-warning">' . fxChronotrackApiAdminEsc(fxChronotrackApiSettingsErrorMessage()) . '</div>';
}
if ($arrConfig !== null) {
echo '<div class="alert alert-info">';
echo 'Statut : <strong>' . fxChronotrackApiAdminEsc(fxChronotrackApiConfigStatusLabel($strStatus)) . '</strong>';
echo ' — ChronoTrack : <strong>' . fxChronotrackApiAdminEsc($arrConfig['ct_event_name']) . '</strong>';
echo ' (ID ' . intval($arrConfig['ct_event_id']) . ')';
echo '</div>';
}
if ($blnClosed) {
echo '<div class="alert alert-secondary">Statut <strong>fermé</strong> : plus de sync auto ni modification du lien/mapping. ';
echo 'Le <strong>push manuel</strong> reste disponible ci-dessous.</div>';
}
$blnEventLinked = ($arrConfig !== null && intval($arrConfig['ct_event_id'] ?? 0) > 0);
if (!$blnEventLinked) {
fxChronotrackApiAdminRenderEventLinkPicker($blnClosed);
}
if ($arrConfig !== null) {
fxChronotrackApiAdminRenderStatusActions($intEveId, $strStatus, $blnEventLinked);
}
if ($arrConfig !== null) {
fxChronotrackApiAdminRenderRaceMapping($intEveId, $arrConfig, $strLangue, $blnClosed, $blnEventLinked);
}
if ($arrConfig !== null && $blnEventLinked && $blnConfigured) {
fxChronotrackApiAdminRenderPushPanel($intEveId, $blnClosed);
}
if ($blnEventLinked && $blnConfigured) {
fxChronotrackApiAdminRenderDiagnosticPanel(intval($arrConfig['ct_event_id']));
}
if (!$blnInlineOnEventForm) {
echo '<p class="mt-3"><a class="btn btn-outline-secondary btn-sm" href="index.php?t='
. urlencode(base64_encode(4)) . '&amp;a=mod&amp;id=' . intval($intEveId) . '">&larr; Retour fiche événement</a></p>';
}
echo '</div>';
fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed, $vDomaine, $blnInlineOnEventForm);
}
function fxChronotrackApiAdminRenderStatusActions($intEveId, $strStatus, $blnEventLinked = true) {
$strStep = $blnEventLinked ? '1' : '2';
echo '<div class="card mb-4"><div class="card-header">' . $strStep . '. Cycle de vie sync</div><div class="card-body">';
echo '<p class="small text-muted">Activer avant la course ; <strong>Terminer</strong> après la course pour bloquer la sync auto et les changements de config.</p>';
echo '<div class="btn-group" role="group">';
if ($strStatus === MSIN_API_CHRONOTRACK_SYNC_LIE) {
echo '<button type="button" class="btn btn-warning btn-sm ct-api-set-status" data-status="' . MSIN_API_CHRONOTRACK_SYNC_ACTIF . '">Activer la sync</button>';
}
if ($strStatus === MSIN_API_CHRONOTRACK_SYNC_ACTIF) {
echo '<span class="badge badge-success align-self-center mr-2">Sync active</span>';
echo '<button type="button" class="btn btn-danger btn-sm ct-api-set-status" data-status="' . MSIN_API_CHRONOTRACK_SYNC_FERME . '">Terminer / Déconnecter</button>';
}
if ($strStatus === MSIN_API_CHRONOTRACK_SYNC_FERME) {
echo '<span class="badge badge-secondary align-self-center mr-2">Fermé</span>';
echo '<button type="button" class="btn btn-outline-primary btn-sm ct-api-set-status" data-status="' . MSIN_API_CHRONOTRACK_SYNC_LIE . '">Rouvrir (Lié)</button>';
}
echo '</div></div></div>';
}
function fxChronotrackApiAdminRenderRaceMapping($intEveId, $arrConfig, $strLangue, $blnClosed, $blnEventLinked = true) {
$arrEpreuves = fxChronotrackApiMs1EpreuvesForEvent($intEveId, $strLangue);
$arrMap = fxChronotrackApiRaceMapGetForEvent($intEveId);
$blnHasSavedMapping = false;
if ($arrEpreuves !== null) {
for ($i = 1; $i <= count($arrEpreuves); $i++) {
$intEprId = intval($arrEpreuves[$i]['epr_id']);
if (intval($arrMap[$intEprId]['ct_race_id'] ?? 0) > 0) {
$blnHasSavedMapping = true;
break;
}
}
}
$strStep = $blnEventLinked ? '2' : '3';
echo '<div class="card mb-4"><div class="card-header">' . $strStep . '. Mapping épreuves MS1 → races ChronoTrack</div><div class="card-body">';
if ($arrEpreuves === null || count($arrEpreuves) === 0) {
echo '<p class="text-muted mb-0">Aucune épreuve MS1 pour cet événement.</p></div></div>';
return;
}
if ($blnHasSavedMapping) {
echo '<div id="ct_api_race_summary_wrap">';
echo '<table class="table table-sm table-bordered mb-2"><thead><tr><th>Épreuve MS1</th><th>Race ChronoTrack</th></tr></thead><tbody>';
for ($i = 1; $i <= count($arrEpreuves); $i++) {
$intEprId = intval($arrEpreuves[$i]['epr_id']);
$strLabel = trim($arrEpreuves[$i]['epr_type'] . ' — ' . $arrEpreuves[$i]['epr_nom']);
$intCtRaceId = intval($arrMap[$intEprId]['ct_race_id'] ?? 0);
$strCtRaceName = trim((string)($arrMap[$intEprId]['ct_race_name'] ?? ''));
echo '<tr><td>' . fxChronotrackApiAdminEsc($strLabel) . ' <span class="text-muted">(epr_id '
. $intEprId . ')</span></td><td>';
if ($intCtRaceId > 0) {
echo fxChronotrackApiAdminEsc($strCtRaceName !== '' ? $strCtRaceName : ('Race ID ' . $intCtRaceId));
echo ' <span class="text-muted">(ID ' . $intCtRaceId . ')</span>';
} else {
echo '<span class="text-muted">— Non mappé —</span>';
}
echo '</td></tr>';
}
echo '</tbody></table>';
if (!$blnClosed) {
echo '<button type="button" class="btn btn-outline-secondary btn-sm" id="ct_api_btn_edit_map">'
. '<i class="fa fa-pencil" aria-hidden="true"></i>&nbsp;Modifier le mapping</button>';
}
echo '</div>';
}
$strEditStyle = $blnHasSavedMapping ? ' style="display:none;"' : '';
echo '<div id="ct_api_race_edit_wrap"' . $strEditStyle . '>';
echo '<p class="small text-muted">ChronoTrack event ID : ' . intval($arrConfig['ct_event_id'])
. ' — courses CT Live (<code>race_name</code> via <code>GET event/{id}/race</code>).</p>';
echo '<div id="ct_api_races_status" class="small mb-2"></div>';
if (!$blnClosed) {
echo '<button type="button" class="btn btn-outline-secondary btn-sm mb-2" id="ct_api_btn_reload_races">'
. '<i class="fa fa-refresh" aria-hidden="true"></i>&nbsp;Charger les courses ChronoTrack</button>';
}
echo '<table class="table table-sm table-bordered"><thead><tr><th>Épreuve MS1</th><th>Race ChronoTrack</th></tr></thead><tbody>';
for ($i = 1; $i <= count($arrEpreuves); $i++) {
$intEprId = intval($arrEpreuves[$i]['epr_id']);
$strLabel = trim($arrEpreuves[$i]['epr_type'] . ' — ' . $arrEpreuves[$i]['epr_nom']);
echo '<tr><td>' . fxChronotrackApiAdminEsc($strLabel) . ' <span class="text-muted">(epr_id '
. $intEprId . ')</span></td><td>';
echo '<select class="form-control form-control-sm ct-api-race-select" data-epr-id="' . $intEprId . '"'
. ($blnClosed ? ' disabled' : '') . '>';
echo '<option value="">— Non mappé —</option>';
echo '</select></td></tr>';
}
echo '</tbody></table>';
if (!$blnClosed) {
echo '<button type="button" class="btn btn-primary btn-sm" id="ct_api_btn_save_map">Enregistrer le mapping</button>';
if ($blnHasSavedMapping) {
echo ' <button type="button" class="btn btn-link btn-sm" id="ct_api_btn_cancel_edit_map">Annuler</button>';
}
}
echo '</div></div></div>';
}
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">';
if ($blnClosed) {
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">Push manuel · External ID = <code>par_id_original</code> · Pas de sync auto.</p>';
echo '<div id="ct_api_push_summary" class="mb-2 text-muted small">Cliquez <strong>Actualiser</strong>.</div>';
echo '<div id="ct_api_push_blocked_wrap" class="mb-2 d-none"></div>';
echo '<div id="ct_api_push_orphans_wrap" class="mb-2 d-none"></div>';
echo '<div class="mb-2">';
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</button> ';
echo '<button type="button" class="btn btn-outline-secondary btn-sm" id="ct_api_btn_push_refresh">';
echo '<i class="fa fa-refresh" aria-hidden="true"></i>&nbsp;Actualiser</button> ';
echo '<button type="button" class="btn btn-outline-warning btn-sm" id="ct_api_btn_orphans" disabled>';
echo '<i class="fa fa-unlink" aria-hidden="true"></i>&nbsp;<span id="ct_api_orphans_btn_label">Orphelines CT</span></button> ';
echo '<button type="button" class="btn btn-outline-secondary btn-sm" id="ct_api_btn_probe_push">';
echo '<i class="fa fa-flask" aria-hidden="true"></i>&nbsp;Sonde</button>';
echo '</div>';
echo '<div id="ct_api_push_result" class="small mt-2"></div>';
echo '</div></div>';
}
function fxChronotrackApiAdminRenderDiagnosticPanel($intCtEventId) {
echo '<div class="card mb-4 border-secondary"><div class="card-header py-2">';
echo '<a class="d-block text-dark collapsed" data-toggle="collapse" href="#ct_api_diagnostic_collapse" role="button" aria-expanded="false">';
echo 'Diagnostic API ChronoTrack <span class="text-muted small">(support — event ID ' . intval($intCtEventId) . ')</span>';
echo '</a></div>';
echo '<div class="collapse" id="ct_api_diagnostic_collapse"><div class="card-body">';
echo '<p class="small text-muted mb-2">Utilise les credentials MS1 déjà configurés (OAuth en cache). ';
echo 'Sonde <code>event/{id}/race</code>, <code>entry</code> (5 premières inscriptions), ';
echo '<code>event</code> et <code>reg_choice</code>. Copie le rapport pour le développement.</p>';
echo '<button type="button" class="btn btn-outline-dark btn-sm mb-2" id="ct_api_btn_diagnostic">';
echo '<i class="fa fa-stethoscope" aria-hidden="true"></i>&nbsp;Lancer le diagnostic API</button> ';
echo '<button type="button" class="btn btn-outline-secondary btn-sm mb-2 d-none" id="ct_api_btn_diagnostic_copy">';
echo 'Copier le rapport</button>';
echo '<div id="ct_api_diagnostic_out" class="small text-muted">Cliquez sur « Lancer le diagnostic API ».</div>';
echo '</div></div></div>';
}
function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed, $vDomaine, $blnInlineOnEventForm = false) {
$intCtEventId = ($arrConfig !== null) ? intval($arrConfig['ct_event_id']) : 0;
$arrMapCurrent = array();
$blnHasSavedMapping = false;
if ($arrConfig !== null) {
$arrMapRows = fxChronotrackApiRaceMapGetForEvent($intEveId);
foreach ($arrMapRows as $intEprId => $arrRow) {
$intCtRaceId = intval($arrRow['ct_race_id']);
$arrMapCurrent[intval($intEprId)] = $intCtRaceId;
if ($intCtRaceId > 0) {
$blnHasSavedMapping = true;
}
}
}
?>
<style>
#ct_api_push_blocked_wrap .ct-api-blocked-table,
#ct_api_push_orphans_wrap .ct-api-orphan-table { font-size: 0.8rem; }
#ct_api_push_blocked_wrap .ct-api-blocked-table th,
#ct_api_push_orphans_wrap .ct-api-orphan-table th { white-space: nowrap; }
#ct_api_push_blocked_wrap .ct-api-blocked-scroll,
#ct_api_push_orphans_wrap .ct-api-orphan-scroll { max-height: 55vh; overflow: auto; }
#ct_api_push_summary .ct-api-dash-table { font-size: 0.85rem; margin-bottom: 0.5rem; }
#ct_api_push_summary .ct-api-dash-table th { background: #f8f9fa; font-weight: 600; white-space: nowrap; }
#ct_api_push_summary .ct-api-dash-table td.num { text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; }
#ct_api_push_summary .ct-api-dash-push { font-size: 0.85rem; margin-bottom: 0; }
</style>
<?php fxChronotrackApiAdminRenderLoaderAssets($vDomaine); ?>
<script>
(function ($) {
var intEveId = <?php echo intval($intEveId); ?>;
var intLinkedCtEventId = <?php echo $intCtEventId; ?>;
var tabMapCurrent = <?php echo json_encode($arrMapCurrent); ?>;
var strAjaxUrl = <?php echo json_encode($vDomaine . '/superadm/ajax_chronotrack_api.php'); ?>;
var strSelectedCtEventId = intLinkedCtEventId > 0 ? String(intLinkedCtEventId) : '';
var blnHasSavedMapping = <?php echo $blnHasSavedMapping ? 'true' : 'false'; ?>;
var blnClosed = <?php echo $blnClosed ? 'true' : 'false'; ?>;
var blnInlineOnEventForm = <?php echo $blnInlineOnEventForm ? 'true' : 'false'; ?>;
var xhrPushPreview = null;
var blnPushPreviewLoaded = 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);
}
function rejectIfSessionLost(res) {
if (!res || typeof res !== 'object') {
return false;
}
if (res.code === 'session' || res.message === 'session') {
if (typeof window.fxSuperadmSessionExpiredRedirect === 'function') {
window.fxSuperadmSessionExpiredRedirect();
} else {
window.location.href = <?php echo json_encode($vDomaine . '/superadm/login.php?expired=1'); ?>;
}
return true;
}
var strMsg = String(res.message || '');
if (strMsg.indexOf('Session expir') === 0) {
if (typeof window.fxSuperadmSessionExpiredRedirect === 'function') {
window.fxSuperadmSessionExpiredRedirect();
} else {
window.location.href = <?php echo json_encode($vDomaine . '/superadm/login.php?expired=1'); ?>;
}
return true;
}
return false;
}
function renderEvents(arrEvents) {
var $wrap = $('#ct_api_events_wrap');
$wrap.empty();
if (!arrEvents || !arrEvents.length) {
$wrap.append('<p class="text-muted small mb-0">Aucun événement trouvé (essayez « événements passés » ou autre recherche).</p>');
return;
}
var $list = $('<div class="list-group list-group-flush"></div>');
$.each(arrEvents, function (_, ev) {
var strId = String(ev.ct_event_id || '');
var $item = $('<label class="list-group-item list-group-item-action py-2 mb-0"></label>');
var $radio = $('<input type="radio" name="ct_api_event_pick" class="mr-2">').val(strId);
if (strSelectedCtEventId === strId) {
$radio.prop('checked', true);
}
$item.append($radio);
$item.append(document.createTextNode((ev.start_label ? ev.start_label + ' — ' : '') + (ev.name || '') + ' (ID ' + strId + ')'));
if (ev.is_past) {
$item.append(' <span class="badge badge-secondary ml-1">passé</span>');
}
$list.append($item);
});
$wrap.append($list);
$('#ct_api_btn_save_link').prop('disabled', false);
}
$('#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,
search: $('#ct_api_search').val(),
show_past: $('#ct_api_show_past').is(':checked') ? 1 : 0
}, function (res) {
if (rejectIfSessionLost(res)) {
return;
}
if (!res || res.state !== 'ok') {
showMsg('error', (res && res.message) ? res.message : 'Erreur chargement');
return;
}
renderEvents(res.events || []);
}, 'json').fail(function () {
showMsg('error', 'Erreur réseau');
});
});
$(document).on('change', 'input[name="ct_api_event_pick"]', function () {
strSelectedCtEventId = $(this).val();
});
$('#ct_api_btn_save_link').on('click', function () {
if (!strSelectedCtEventId) {
showMsg('error', 'Sélectionnez un événement ChronoTrack');
return;
}
var $btn = $(this);
$btn.prop('disabled', true);
$.post(strAjaxUrl, {
a: 'save_link',
eve_id: intEveId,
ct_event_id: strSelectedCtEventId
}, function (res) {
if (rejectIfSessionLost(res)) {
return;
}
if (!res || res.state !== 'ok') {
showMsg('error', (res && res.message) ? res.message : 'Erreur enregistrement');
$btn.prop('disabled', false);
return;
}
window.location.reload();
}, 'json').fail(function () {
showMsg('error', 'Erreur réseau');
$btn.prop('disabled', false);
});
});
$('.ct-api-set-status').on('click', function () {
var strStatus = $(this).data('status');
var strConfirm;
if (strStatus === 'ferme') {
strConfirm = 'Terminer la course ? La sync auto et les changements de config seront bloqués (push manuel toujours possible).';
} else if (strStatus === 'lie') {
strConfirm = 'Rouvrir cet événement ChronoTrack (statut Lié) ?';
} else {
strConfirm = 'Activer la synchronisation automatique pour cet événement ?';
}
if (!window.confirm(strConfirm)) {
return;
}
$.post(strAjaxUrl, { a: 'set_status', eve_id: intEveId, sync_status: strStatus }, function (res) {
if (rejectIfSessionLost(res)) {
return;
}
if (!res || res.state !== 'ok') {
showMsg('error', (res && res.message) ? res.message : 'Erreur statut');
return;
}
window.location.reload();
}, 'json');
});
function loadRacesIntoSelects() {
if (intLinkedCtEventId <= 0) {
return;
}
var $status = $('#ct_api_races_status');
$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 (rejectIfSessionLost(res)) {
return;
}
if (!res || res.state !== 'ok') {
$status.addClass('text-danger').text((res && res.message) ? res.message : 'Erreur chargement des courses');
return;
}
var arrRaces = res.races || [];
var strKind = res.kind || 'reg_choice';
if (!arrRaces.length) {
$status.addClass('text-warning').text((res.message) ? res.message : 'Aucune course ChronoTrack pour cet event.');
} else {
$status.addClass('text-success').text(arrRaces.length + ' course(s) ChronoTrack chargée(s).');
}
$('.ct-api-race-select').each(function () {
var $sel = $(this);
var intEprId = parseInt($sel.data('epr-id'), 10);
var intCurrent = tabMapCurrent[intEprId] || 0;
$sel.find('option:not(:first)').remove();
$.each(arrRaces, function (_, race) {
var strText = race.label || race.name || 'Course';
strText += ' (ID ' + race.ct_race_id + ')';
var $opt = $('<option></option>').val(race.ct_race_id).text(strText);
$sel.append($opt);
});
if (intCurrent > 0) {
$sel.val(String(intCurrent));
}
});
}, 'json').fail(function () {
$status.addClass('text-danger').text('Erreur réseau lors du chargement des races.');
});
}
function showRaceEditMode(blnLoadRaces) {
$('#ct_api_race_summary_wrap').hide();
$('#ct_api_race_edit_wrap').show();
if (blnLoadRaces) {
loadRacesIntoSelects();
}
}
function showRaceSummaryMode() {
$('#ct_api_race_edit_wrap').hide();
$('#ct_api_races_status').empty();
$('#ct_api_race_summary_wrap').show();
}
$('#ct_api_btn_edit_map').on('click', function () {
showRaceEditMode(true);
});
$('#ct_api_btn_cancel_edit_map').on('click', function () {
showRaceSummaryMode();
});
$('#ct_api_btn_reload_races').on('click', function () {
loadRacesIntoSelects();
});
$('#chronotrackApiCollapse').on('shown.bs.collapse', function () {
if (!blnHasSavedMapping && intLinkedCtEventId > 0 && !blnClosed) {
loadRacesIntoSelects();
}
});
<?php if ($arrConfig !== null && !$blnHasSavedMapping) { ?>
if (intLinkedCtEventId > 0 && !blnClosed && !$('#chronotrackApiCollapse').length) {
loadRacesIntoSelects();
}
<?php } ?>
$('#ct_api_btn_save_map').on('click', function () {
var tabMap = {};
$('.ct-api-race-select').each(function () {
var intEprId = parseInt($(this).data('epr-id'), 10);
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 (rejectIfSessionLost(res)) {
return;
}
if (!res || res.state !== 'ok') {
showMsg('error', (res && res.message) ? res.message : 'Erreur mapping');
$btn.prop('disabled', false);
return;
}
window.location.reload();
}, 'json').fail(function () {
showMsg('error', 'Erreur réseau');
$btn.prop('disabled', false);
});
});
var objDiagnosticReport = null;
function renderDiagnosticReport(res) {
objDiagnosticReport = res;
var $out = $('#ct_api_diagnostic_out');
if (!res || res.state !== 'ok') {
$out.html('<div class="text-danger">' + escHtml((res && res.message) ? res.message : 'Erreur diagnostic') + '</div>');
$('#ct_api_btn_diagnostic_copy').addClass('d-none');
return;
}
var html = '<p class="mb-2"><strong>OAuth :</strong> ' + escHtml(res.oauth || '?')
+ ' — <strong>Event CT :</strong> ' + escHtml(String(res.ct_event_id || '')) + '</p>';
html += '<div class="table-responsive"><table class="table table-sm table-bordered mb-2">';
html += '<thead><tr><th>Endpoint</th><th>HTTP</th><th>Résultat</th><th>Entités détectées</th></tr></thead><tbody>';
$.each(res.probes || [], function (_, probe) {
html += '<tr>';
html += '<td><code>' + escHtml(probe.path || '') + '</code></td>';
html += '<td>' + escHtml(String(probe.http_code || '')) + '</td>';
html += '<td>' + escHtml(probe.state === 'ok' ? 'OK' : (probe.message || 'erreur')) + '</td>';
html += '<td>';
if (probe.summary) {
if (probe.summary.event_race && probe.summary.event_race.count > 0) {
html += 'event_race: ' + probe.summary.event_race.count + '<br>';
}
if (probe.summary.entry && probe.summary.entry.count > 0) {
html += 'entry: ' + probe.summary.entry.count + ' (page)<br>';
}
if (probe.summary.reg_choice && probe.summary.reg_choice.count > 0) {
html += 'reg_choice: ' + probe.summary.reg_choice.count + '<br>';
}
if (probe.summary.race && probe.summary.race.count > 0) {
html += 'race: ' + probe.summary.race.count + '<br>';
}
if (probe.summary.field_keys && probe.summary.field_keys.length) {
html += '<span class="text-muted">champs: ' + escHtml(probe.summary.field_keys.slice(0, 12).join(', '));
if (probe.summary.field_keys.length > 12) {
html += '…';
}
html += '</span><br>';
}
var arrSampleSource = probe.summary.entry || probe.summary.event_race || probe.summary.race || probe.summary.reg_choice;
if (arrSampleSource && arrSampleSource.samples && arrSampleSource.samples.length) {
html += '<span class="text-muted">ex. ';
$.each(arrSampleSource.samples, function (i, s) {
if (i > 0) {
html += ' · ';
}
var strLabel = s.name || s.id || '?';
if (s.external_id) {
strLabel += ' [ext:' + s.external_id + ']';
}
if (s.bib) {
strLabel += ' #' + s.bib;
}
if (s.entry_status) {
strLabel += ' (' + s.entry_status + ')';
}
html += escHtml(strLabel);
});
html += '</span>';
}
}
html += '</td></tr>';
});
html += '</tbody></table></div>';
$.each(res.probes || [], function (idx, probe) {
var strProbeLabel = probe.path || ('probe ' + idx);
if (probe.query && typeof probe.query === 'object') {
var arrQ = [];
$.each(probe.query, function (k, v) {
arrQ.push(k + '=' + v);
});
if (arrQ.length) {
strProbeLabel += '?' + arrQ.join('&');
}
}
html += '<details class="mb-2"><summary class="small"><strong>JSON brut — '
+ escHtml(strProbeLabel) + '</strong></summary>';
html += '<pre class="small border rounded p-2 mb-0" style="max-height:280px;overflow:auto;background:#f8f9fa;">'
+ escHtml(probe.raw_json || '') + '</pre></details>';
});
$out.html(html);
$('#ct_api_btn_diagnostic_copy').removeClass('d-none');
}
$('#ct_api_btn_diagnostic').on('click', function () {
var $out = $('#ct_api_diagnostic_out');
$out.html(waitHtml('Diagnostic en cours… (OAuth + 4 endpoints CT, dont entry)'));
$('#ct_api_btn_diagnostic_copy').addClass('d-none');
$.post(strAjaxUrl, {
a: 'diagnostic',
eve_id: intEveId,
ct_event_id: intLinkedCtEventId
}, function (res) {
if (rejectIfSessionLost(res)) {
return;
}
renderDiagnosticReport(res);
}, 'json').fail(function () {
$out.html('<div class="text-danger">Erreur réseau lors du diagnostic.</div>');
});
});
$('#ct_api_btn_diagnostic_copy').on('click', function () {
if (!objDiagnosticReport) {
return;
}
var strText = JSON.stringify(objDiagnosticReport, null, 2);
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(strText).then(function () {
window.alert('Rapport copié dans le presse-papiers.');
});
return;
}
window.prompt('Copier le rapport (Ctrl+C) :', strText);
});
var objPushPreview = null;
function renderBlockedPanel(res) {
var $wrap = $('#ct_api_push_blocked_wrap');
if (!$wrap.length) {
return;
}
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 += '<strong>Exclus MS1</strong> <span class="badge badge-danger">' + intTotal + '</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;
}
var blnTeam = (grp.code === 'team');
var strHeadClass = blnTeam ? 'text-warning' : 'text-danger';
html += '<div class="mb-3">';
html += '<div class="d-flex align-items-center mb-1">';
html += '<strong class="' + strHeadClass + ' small">' + escHtml(grp.label || grp.code) + '</strong>';
html += ' <span class="badge badge-secondary ml-1">' + intCnt + '</span>';
if (blnTeam) {
html += ' <span class="text-muted small ml-1">MVP : capitaine seulement (plus tard)</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>';
if (blnTeam) {
html += '<th>Nom</th><th>external_id</th><th>Équipe</th><th>No équipe</th><th>Rôle</th><th>Dossard</th><th>pec_id</th>';
} else {
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>';
if (blnTeam) {
html += '<td>' + escHtml(item.team_name || '—') + '</td>';
html += '<td>' + escHtml(item.no_equipe ? ('#' + item.no_equipe) : '—') + '</td>';
html += '<td>' + escHtml(item.role_label || '—') + '</td>';
html += '<td>' + escHtml(item.no_bib ? ('#' + item.no_bib) : '—') + '</td>';
html += '<td>' + escHtml(String(item.pec_id || '')) + '</td>';
} else {
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>';
});
html += '</div></div></div>';
$wrap.removeClass('d-none').html(html);
}
function renderOrphansPanel(res) {
var $wrap = $('#ct_api_push_orphans_wrap');
if (!$wrap.length) {
return;
}
if (!res || res.state !== 'ok') {
$wrap.addClass('d-none').html('<div class="text-danger small">' + escHtml((res && res.message) ? res.message : 'Erreur') + '</div>');
return;
}
var tabRows = res.orphans || [];
if (!tabRows.length) {
$wrap.addClass('d-none').empty();
return;
}
var html = '<div class="card border-warning">';
html += '<div class="card-header py-2 bg-white d-flex justify-content-between align-items-center">';
html += '<strong class="text-warning small">Orphelines CT — ' + tabRows.length + '</strong>';
html += '<button type="button" class="btn btn-link btn-sm p-0" id="ct_api_btn_orphans_close">Fermer</button>';
html += '</div><div class="card-body py-2">';
html += '<div class="ct-api-orphan-scroll border rounded">';
html += '<table class="table table-sm table-striped ct-api-orphan-table mb-0">';
html += '<thead class="thead-light"><tr>';
html += '<th>Nom</th><th>external_id</th><th>Dossard</th><th>Statut</th><th>entry_id</th><th>Raison</th>';
html += '</tr></thead><tbody>';
$.each(tabRows, function (_, row) {
html += '<tr>';
html += '<td>' + escHtml(row.name || '—') + '</td>';
html += '<td>' + escHtml(row.external_id || '—') + '</td>';
html += '<td>' + escHtml(row.bib ? ('#' + row.bib) : '—') + '</td>';
html += '<td>' + escHtml(row.entry_status || '—') + '</td>';
html += '<td><code class="small">' + escHtml(row.entry_id || '') + '</code></td>';
html += '<td>' + escHtml(row.reason_label || row.reason || '') + '</td>';
html += '</tr>';
});
html += '</tbody></table></div></div></div>';
$wrap.removeClass('d-none').html(html);
}
function updateOrphansButton(intCount, intCtTotal, intLinked) {
var $btn = $('#ct_api_btn_orphans');
var $lbl = $('#ct_api_orphans_btn_label');
if (!$btn.length || !$lbl.length) {
return;
}
var strLabel = 'Orphelines CT';
if (intCount > 0) {
strLabel += ' (' + intCount + ')';
}
$lbl.text(strLabel);
$btn.prop('disabled', intCount <= 0 && intCtTotal <= intLinked);
}
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();
$('#ct_api_push_orphans_wrap').addClass('d-none').empty();
updateOrphansButton(0, 0, 0);
$btn.prop('disabled', true);
return;
}
var intBlocked = res.blocked_count || 0;
var intEligible = res.transferable || 0;
var intMs1 = res.ms1_total || 0;
var intWillUpdate = res.will_update || res.already_in_ct || 0;
var intWillCreate = res.will_create || res.to_create || 0;
var intCtTotal = res.ct_total || 0;
var intOrphans = res.ct_orphans || 0;
var html = '<table class="table table-sm table-bordered ct-api-dash-table mb-1">';
html += '<thead><tr><th></th><th class="text-right">Total</th><th class="text-right">Sync OK</th><th class="text-right">Hors sync</th></tr></thead>';
html += '<tbody>';
html += '<tr><td>MS1</td>';
html += '<td class="num">' + intMs1 + '</td>';
html += '<td class="num text-primary">' + intEligible + '</td>';
html += '<td class="num text-danger">' + intBlocked + '</td></tr>';
html += '<tr><td>ChronoTrack</td>';
html += '<td class="num">' + intCtTotal + '</td>';
html += '<td class="num text-success">' + intWillUpdate + '</td>';
html += '<td class="num text-warning">' + intOrphans + '</td></tr>';
html += '</tbody></table>';
html += '<p class="ct-api-dash-push"><strong>Push :</strong> ' + intEligible + ' participants · ';
html += intWillCreate + ' créer · ' + intWillUpdate + ' maj</p>';
$sum.html(html);
renderBlockedPanel(res);
updateOrphansButton(intOrphans, intCtTotal, intWillUpdate);
$('#ct_api_push_orphans_wrap').addClass('d-none').empty();
$btn.prop('disabled', intEligible <= 0);
}
function renderPushResult(res, strTitle) {
var $res = $('#ct_api_push_result');
var strHeading = strTitle || 'Résultat du push';
var strClass = 'text-danger';
if (res && res.state === 'warning') {
strClass = 'text-warning';
} else if (res && (res.state === 'ok' || res.state === 'partial')) {
strClass = res.state === 'ok' ? 'text-success' : 'text-warning';
}
var strHtml = '<div class="' + strClass + '"><strong>' + escHtml(strHeading) + '</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(blnForce) {
var $sum = $('#ct_api_push_summary');
if (!$sum.length) {
return;
}
if (xhrPushPreview && xhrPushPreview.readyState !== 4) {
if (!blnForce) {
return;
}
xhrPushPreview.abort();
}
$sum.html(waitHtml('Calcul du diff MS1 ↔ ChronoTrack…'));
xhrPushPreview = $.ajax({
url: strAjaxUrl,
type: 'POST',
dataType: 'json',
timeout: 180000,
data: { a: 'sync_preview', eve_id: intEveId }
}).done(function (res) {
if (rejectIfSessionLost(res)) {
return;
}
blnPushPreviewLoaded = true;
renderPushPreview(res);
}).fail(function (jqXHR, strStatus) {
if (strStatus === 'abort') {
return;
}
var strMsg = (strStatus === 'timeout')
? 'Délai dépassé — ChronoTrack met trop de temps à répondre. Réessayez avec Actualiser.'
: 'Erreur réseau (preview).';
$sum.html('<div class="text-danger">' + escHtml(strMsg) + '</div>');
}).always(function () {
xhrPushPreview = null;
});
}
function maybeLoadPushPreviewOnReveal() {
if (!$('#ct_api_push_card').length) {
return;
}
if (!blnPushPreviewLoaded) {
loadPushPreview(false);
}
}
if ($('#ct_api_push_card').length) {
if (blnInlineOnEventForm && $('#chronotrackApiCollapse').length) {
$('#chronotrackApiCollapse').on('shown.bs.collapse', maybeLoadPushPreviewOnReveal);
} else {
loadPushPreview(false);
}
$('#ct_api_btn_push_refresh').on('click', function () {
loadPushPreview(true);
$('#ct_api_push_result').empty();
$('#ct_api_push_orphans_wrap').addClass('d-none').empty();
});
$('#ct_api_btn_orphans').on('click', function () {
var $btn = $(this);
var $wrap = $('#ct_api_push_orphans_wrap');
$btn.prop('disabled', true);
$wrap.removeClass('d-none').html(waitHtml('Chargement orphelines…'));
$.post(strAjaxUrl, { a: 'sync_orphans', eve_id: intEveId }, function (res) {
if (rejectIfSessionLost(res)) {
return;
}
renderOrphansPanel(res);
if (res && res.state === 'ok') {
updateOrphansButton(res.count || 0, res.ct_total || 0, res.linked_count || 0);
}
}, 'json').fail(function () {
$wrap.html('<div class="text-danger small">Erreur réseau.</div>');
}).always(function () {
if (objPushPreview && objPushPreview.state === 'ok') {
updateOrphansButton(
objPushPreview.ct_orphans || 0,
objPushPreview.ct_total || 0,
objPushPreview.already_in_ct || 0
);
} else {
$btn.prop('disabled', false);
}
});
});
$(document).on('click', '#ct_api_btn_orphans_close', function () {
$('#ct_api_push_orphans_wrap').addClass('d-none').empty();
});
$('#ct_api_btn_probe_push').on('click', function () {
var $btn = $(this);
var $res = $('#ct_api_push_result');
$btn.prop('disabled', true);
$res.html(waitHtml('Sonde POST (1 participant)…'));
$.post(strAjaxUrl, { a: 'sync_probe_push', eve_id: intEveId }, function (res) {
if (rejectIfSessionLost(res)) {
return;
}
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>';
if (res && res.payload) {
strHtml += '<pre class="small bg-light p-2 mt-2 mb-1" style="max-height:200px;overflow:auto;">'
+ escHtml(JSON.stringify(res.payload, null, 2)) + '</pre>';
}
if (res && res.response_body) {
strHtml += '<pre class="small bg-light p-2 mb-0" style="max-height:200px;overflow:auto;">'
+ escHtml(res.response_body) + '</pre>';
}
$res.html(strHtml);
}, 'json').fail(function () {
$res.html('<div class="text-danger">Erreur réseau (sonde).</div>');
}).always(function () {
$btn.prop('disabled', false);
});
});
$('#ct_api_btn_push').on('click', function () {
var intEligible = (objPushPreview && objPushPreview.transferable) ? objPushPreview.transferable : 0;
var strConfirm = 'Pousser ' + intEligible + ' participants vers ChronoTrack ?';
if (!window.confirm(strConfirm)) {
return;
}
var $btn = $(this);
var $res = $('#ct_api_push_result');
$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) {
if (rejectIfSessionLost(res)) {
return;
}
renderPushResult(res);
loadPushPreview(true);
}, 'json').fail(function () {
$res.html('<div class="text-danger">Erreur réseau lors du push.</div>');
}).always(function () {
$btn.prop('disabled', false);
});
});
}
})(jQuery);
</script>
<?php
}
function fxChronotrackApiAdminRenderDashboard($strLangue = 'fr', $blnInsideV2Panel = false) {
$arrRows = fxChronotrackApiListActiveConfigs();
if (!$blnInsideV2Panel) {
echo '<h1 class="h3">MSIN API ChronoTrack — Courses connectées</h1>';
}
echo '<p class="text-muted">Événements liés ou sync active. Le dashboard détaillé (diff, push) viendra en Phase 2.</p>';
if ($arrRows === null || count($arrRows) === 0) {
echo '<div class="alert alert-light">Aucune course connectée.</div>';
return;
}
echo '<table class="table table-sm table-striped"><thead><tr><th>Événement MS1</th><th>ChronoTrack</th><th>Statut</th><th></th></tr></thead><tbody>';
for ($i = 1; $i <= count($arrRows); $i++) {
$arr = $arrRows[$i];
echo '<tr>';
echo '<td>' . fxChronotrackApiAdminEsc($arr['eve_nom']) . ' <span class="text-muted">(' . intval($arr['eve_id']) . ')</span></td>';
echo '<td>' . fxChronotrackApiAdminEsc($arr['ct_event_name']) . ' <span class="text-muted">(' . intval($arr['ct_event_id']) . ')</span></td>';
echo '<td>' . fxChronotrackApiAdminEsc(fxChronotrackApiConfigStatusLabel($arr['sync_status'])) . '</td>';
echo '<td><a class="btn btn-outline-primary btn-sm" href="chronotrack_api.php?eve_id=' . intval($arr['eve_id']) . '">Configurer</a></td>';
echo '</tr>';
}
echo '</tbody></table>';
}