Files
ms1inscription-v5/php/chronotrack_api/fx_chronotrack_admin.php

1187 lines
55 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) {
$strApiBase = fxChronotrackApiGetBaseUrl();
$blnIsTestApi = (strpos($strApiBase, 'api-test') !== false);
echo '<p class="small text-muted mb-2">Endpoint API : <code>' . fxChronotrackApiAdminEsc($strApiBase) . '</code>';
if ($blnIsTestApi) {
echo ' <span class="badge badge-warning">QA</span>';
} else {
echo ' <span class="badge badge-secondary">prod</span>';
}
echo '</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');
var intChunk = 50;
var intOffset = 0;
var intTotal = intEligible;
var intOkSum = 0;
var intErrSum = 0;
var intPostSum = 0;
var intPutSum = 0;
var tabErrAll = [];
var blnFatal = false;
$btn.prop('disabled', true);
$('#ct_api_btn_push_refresh, #ct_api_btn_orphans, #ct_api_btn_probe_push').prop('disabled', true);
function renderProgress(intDone, intTot, strExtra) {
var intPct = (intTot > 0) ? Math.min(100, Math.round((intDone * 100) / intTot)) : 0;
var strHtml = '<div class="mb-1"><strong>Envoi</strong> — '
+ intDone + ' / ' + intTot
+ (strExtra ? (' · ' + escHtml(strExtra)) : '')
+ '</div>';
strHtml += '<div class="progress" style="height:1.25rem;">';
strHtml += '<div class="progress-bar progress-bar-striped progress-bar-animated bg-primary" '
+ 'role="progressbar" style="width:' + intPct + '%;" aria-valuenow="' + intPct
+ '" aria-valuemin="0" aria-valuemax="100">' + intPct + '%</div>';
strHtml += '</div>';
if (intOkSum || intErrSum) {
strHtml += '<p class="small text-muted mb-0 mt-1">OK ' + intOkSum
+ ' · erreurs ' + intErrSum
+ ' · POST ' + intPostSum + ' · PUT ' + intPutSum + '</p>';
}
$res.html(strHtml);
}
function finishPush() {
$('#ct_api_btn_push_refresh, #ct_api_btn_orphans, #ct_api_btn_probe_push').prop('disabled', false);
$btn.prop('disabled', false);
var strFinal = (intErrSum === 0)
? (intOkSum + ' participant(s) synchronisé(s) (POST ' + intPostSum + ' · PUT ' + intPutSum + ').')
: (intOkSum + ' OK, ' + intErrSum + ' erreur(s).');
var strState = blnFatal ? 'error' : ((intErrSum === 0) ? 'ok' : 'partial');
renderPushResult({
state: strState,
message: strFinal,
errors: tabErrAll.slice(0, 20)
});
loadPushPreview(true);
}
function pushChunk() {
renderProgress(intOffset, intTotal, 'lot en cours…');
$.ajax({
url: strAjaxUrl,
type: 'POST',
dataType: 'json',
timeout: 300000,
data: {
a: 'sync_push',
eve_id: intEveId,
offset: intOffset,
limit: intChunk,
refresh_preview: 0
}
}).done(function (res) {
if (rejectIfSessionLost(res)) {
return;
}
if (!res || (res.state !== 'ok' && res.state !== 'partial')) {
if (res && res.total === 0) {
renderPushResult(res);
$('#ct_api_btn_push_refresh, #ct_api_btn_orphans, #ct_api_btn_probe_push').prop('disabled', false);
$btn.prop('disabled', false);
return;
}
blnFatal = true;
if (res && res.message) {
tabErrAll.push(res.message);
}
finishPush();
return;
}
if (res.total) {
intTotal = parseInt(res.total, 10) || intTotal;
}
intOkSum += parseInt(res.pushed_ok || 0, 10);
intErrSum += parseInt(res.pushed_error || 0, 10);
intPostSum += parseInt(res.pushed_post || 0, 10);
intPutSum += parseInt(res.pushed_put || 0, 10);
if (res.errors && res.errors.length) {
$.each(res.errors, function (_, msg) {
if (tabErrAll.length < 20) {
tabErrAll.push(msg);
}
});
}
intOffset = parseInt(res.next_offset != null ? res.next_offset : (intOffset + (res.chunk_done || intChunk)), 10);
renderProgress(Math.min(intOffset, intTotal), intTotal, '');
if (res.done || intOffset >= intTotal) {
finishPush();
return;
}
pushChunk();
}).fail(function (jqXHR, strStatus) {
blnFatal = true;
var strDetail = strStatus === 'timeout'
? 'Délai dépassé sur un lot — relancer le push (upsert CT).'
: ('Erreur réseau sur un lot (' + strStatus
+ (jqXHR && jqXHR.status ? ', HTTP ' + jqXHR.status : '') + ').');
if (jqXHR && jqXHR.responseText) {
var strBody = String(jqXHR.responseText).replace(/\s+/g, ' ').substr(0, 180);
if (strBody) {
strDetail += ' — ' + strBody;
}
}
tabErrAll.push(strDetail);
finishPush();
});
}
renderProgress(0, intTotal, 'préparation…');
$.ajax({
url: strAjaxUrl,
type: 'POST',
dataType: 'json',
timeout: 180000,
data: { a: 'sync_push_prepare', eve_id: intEveId }
}).done(function (prep) {
if (rejectIfSessionLost(prep)) {
return;
}
if (!prep || prep.state !== 'ok') {
renderPushResult(prep || { state: 'error', message: 'Préparation échouée' });
$('#ct_api_btn_push_refresh, #ct_api_btn_orphans, #ct_api_btn_probe_push').prop('disabled', false);
$btn.prop('disabled', false);
return;
}
intTotal = parseInt(prep.total, 10) || intTotal;
if (prep.chunk_size) {
intChunk = parseInt(prep.chunk_size, 10) || intChunk;
}
renderProgress(0, intTotal, 'démarrage');
pushChunk();
}).fail(function (jqXHR, strStatus) {
var strMsg = 'Erreur préparation push (' + strStatus
+ (jqXHR && jqXHR.status ? ', HTTP ' + jqXHR.status : '') + ')';
if (jqXHR && jqXHR.responseText) {
strMsg += ' — ' + String(jqXHR.responseText).replace(/\s+/g, ' ').substr(0, 180);
}
strMsg += '. Vérifiez que le code MSIN-4328 est déployé (action sync_push_prepare).';
renderPushResult({ state: 'error', message: strMsg });
$('#ct_api_btn_push_refresh, #ct_api_btn_orphans, #ct_api_btn_probe_push').prop('disabled', false);
$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>';
}