1187 lines
55 KiB
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)) . '&a=mod&id=' . intval($intEveId) . '">← 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> 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> 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> 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> 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> <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> 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> 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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
|
|
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>';
|
|
}
|