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

2663 lines
127 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* MSIN API ChronoTrack — rendu admin superadm (lien fiche + hub Opération/Config/Diagnostic).
* MSIN-4328
*/
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">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>';
}
/**
* MSIN-4328 — résumé compact + CTA hub (fiche événement superadm uniquement).
*/
function fxChronotrackApiAdminRenderEventFormLinkedSummary($intEveId, $arrConfig, $blnClosed) {
$strHubUrl = 'chronotrack_api.php?eve_id=' . intval($intEveId) . '&focus=1';
$strStatus = $arrConfig['sync_status'] ?? '';
echo '<div class="card mb-0 border-success" id="ct_api_event_linked_card">';
echo '<div class="card-body">';
echo '<p class="mb-2">Événement <strong>lié</strong> à ChronoTrack.';
echo ' La configuration, lenvoi et le diagnostic souvrent en <strong>fenêtre opération</strong> (plein écran).</p>';
echo '<p class="small text-muted mb-3">';
echo 'Statut : <strong>' . fxChronotrackApiAdminEsc(fxChronotrackApiConfigStatusLabel($strStatus)) . '</strong>';
echo ' — CT : <strong>' . fxChronotrackApiAdminEsc($arrConfig['ct_event_name'] ?? '') . '</strong>';
echo ' (ID ' . intval($arrConfig['ct_event_id'] ?? 0) . ')';
if ($blnClosed) {
echo ' — <span class="badge badge-secondary">fermé</span>';
}
echo '</p>';
echo '<a class="btn btn-primary" href="' . fxChronotrackApiAdminEsc($strHubUrl) . '" target="_blank" rel="noopener">'
. '<i class="fa fa-external-link" aria-hidden="true"></i>&nbsp;Ouvrir le tableau de bord (nouvelle fenêtre)</a>';
// MSIN-4568 — délier pour choisir un autre event CT
echo ' <button type="button" class="btn btn-outline-danger ml-2" id="ct_api_btn_unlink">'
. 'Déconnecter de ChronoTrack</button>';
echo '</div></div>';
}
/**
* MSIN-4328 — formulaire sync auto (zone Configuration).
*/
function fxChronotrackApiAdminRenderAutoSyncForm($blnClosed, $arrConfig) {
$blnAutoOn = (intval($arrConfig['auto_sync_enabled'] ?? 0) === 1);
$intAutoInterval = intval($arrConfig['auto_sync_interval_min'] ?? 15);
if (!in_array($intAutoInterval, array(1, 5, 15, 30, 60), true)) {
$intAutoInterval = 15;
}
$strUntilRaw = trim((string)($arrConfig['auto_sync_until'] ?? ''));
$strUntilLocal = '';
if ($strUntilRaw !== '' && $strUntilRaw !== '0000-00-00 00:00:00') {
$intUntilTs = strtotime($strUntilRaw);
if ($intUntilTs !== false) {
$strUntilLocal = date('Y-m-d\TH:i', $intUntilTs);
}
}
echo '<div class="card mb-3 border"><div class="card-header py-2">Mise à jour automatique</div><div class="card-body py-3">';
echo '<div class="ct-api-auto-wrap mb-0' . ($blnClosed ? ' ct-api-auto-wrap--disabled' : '') . '" id="ct_api_auto_wrap">';
echo '<div class="form-check mb-2">';
echo '<input class="form-check-input" type="checkbox" id="ct_api_auto_enabled" value="1"'
. ($blnAutoOn ? ' checked' : '') . ($blnClosed ? ' disabled' : '') . '>';
echo '<label class="form-check-label font-weight-bold" for="ct_api_auto_enabled">Activer la sync automatique</label>';
echo '</div>';
echo '<div class="form-row align-items-end" id="ct_api_auto_options">';
echo '<div class="form-group col-sm-4 mb-2 mb-sm-0">';
echo '<label class="small text-muted mb-1" for="ct_api_auto_interval">Fréquence</label>';
echo '<select class="form-control form-control-sm" id="ct_api_auto_interval"'
. ($blnClosed ? ' disabled' : '') . '>';
foreach (array(1 => 'Toutes les minutes', 5 => 'Toutes les 5 minutes', 15 => 'Toutes les 15 minutes',
30 => 'Toutes les 30 minutes', 60 => 'Toutes les heures') as $intMin => $strLabel) {
echo '<option value="' . $intMin . '"' . ($intAutoInterval === $intMin ? ' selected' : '') . '>'
. fxChronotrackApiAdminEsc($strLabel) . '</option>';
}
echo '</select></div>';
echo '<div class="form-group col-sm-5 mb-2 mb-sm-0">';
echo '<label class="small text-muted mb-1" for="ct_api_auto_until">Date et heure de fin</label>';
echo '<input type="datetime-local" class="form-control form-control-sm" id="ct_api_auto_until" value="'
. fxChronotrackApiAdminEsc($strUntilLocal) . '"' . ($blnClosed ? ' disabled' : '') . '>';
echo '</div>';
if (!$blnClosed) {
echo '<div class="form-group col-sm-3 mb-0">';
echo '<button type="button" class="btn btn-outline-primary btn-sm btn-block" id="ct_api_btn_auto_save">'
. 'Enregistrer</button>';
echo '</div>';
}
echo '</div>';
echo '<p class="small text-muted mb-0 mt-2" id="ct_api_auto_hint">';
if ($blnAutoOn && $strUntilRaw !== '') {
echo 'Config enregistrée — cron <code>auto_chronotrack_sync.php</code>. Arrêt : '
. fxChronotrackApiAdminEsc($strUntilRaw) . '.';
} else {
echo 'Cochez, choisissez fréquence + date/heure de fin, puis Enregistrer.';
}
echo '</p>';
$strLastAuto = trim((string)($arrConfig['last_auto_run_at'] ?? ''));
echo '<p class="small mb-0 mt-1" id="ct_api_auto_last">';
echo '<strong>Dernier passage auto :</strong> ';
if ($strLastAuto !== '' && $strLastAuto !== '0000-00-00 00:00:00') {
echo fxChronotrackApiAdminEsc($strLastAuto);
} else {
echo '<span class="text-warning">jamais (le cron na pas encore poussé cet event)</span>';
}
echo '</p>';
echo '<div id="ct_api_auto_msg" class="small mt-1"></div>';
echo '<div id="ct_api_auto_diag" class="small mt-2 text-muted"></div>';
echo '</div></div></div>';
}
/**
* MSIN-4328 — bandeau compact sync auto (zone Opération).
*/
function fxChronotrackApiAdminRenderAutoSyncBanner($blnClosed, $arrConfig) {
$blnAutoOn = (intval($arrConfig['auto_sync_enabled'] ?? 0) === 1);
$strUntilRaw = trim((string)($arrConfig['auto_sync_until'] ?? ''));
$strLastAuto = trim((string)($arrConfig['last_auto_run_at'] ?? ''));
$intInterval = intval($arrConfig['auto_sync_interval_min'] ?? 15);
$strSyncStatus = trim((string)($arrConfig['sync_status'] ?? ''));
$blnActif = ($strSyncStatus === MSIN_API_CHRONOTRACK_SYNC_ACTIF);
echo '<div class="ct-api-auto-banner mb-3" id="ct_api_auto_banner">';
// MSIN-4574 — sync auto ON mais Lié = nenvoie pas (cron exige Actif)
if ($blnAutoOn && !$blnClosed && $blnActif) {
echo '<span class="badge badge-success mr-2">Sync auto ON</span>';
echo '<span class="small">toutes les ' . intval($intInterval) . ' min';
if ($strUntilRaw !== '' && $strUntilRaw !== '0000-00-00 00:00:00') {
echo ' · fin ' . fxChronotrackApiAdminEsc($strUntilRaw);
}
echo '</span>';
} elseif ($blnAutoOn && !$blnClosed && !$blnActif) {
echo '<span class="badge badge-warning mr-2">Sync auto en pause</span>';
echo '<span class="small text-muted">Statut '
. fxChronotrackApiAdminEsc(fxChronotrackApiConfigStatusLabel($strSyncStatus))
. ' — passez en <strong>Actif</strong> pour que le cron envoie</span>';
} elseif ($blnClosed) {
echo '<span class="badge badge-secondary mr-2">Sync auto off</span>';
echo '<span class="small text-muted">Événement fermé</span>';
} else {
echo '<span class="badge badge-light border mr-2">Sync auto OFF</span>';
echo '<span class="small text-muted">Pas de push automatique</span>';
}
if ($strLastAuto !== '' && $strLastAuto !== '0000-00-00 00:00:00') {
echo ' <span class="small text-muted ml-2">Dernier auto : '
. fxChronotrackApiAdminEsc($strLastAuto) . '</span>';
}
echo ' <a class="small ml-2" data-toggle="collapse" href="#ct_api_config_collapse" role="button">'
. 'Régler</a>';
echo '</div>';
}
function fxChronotrackApiAdminHasSavedRaceMapping($intEveId) {
$arrMapRows = fxChronotrackApiRaceMapGetForEvent($intEveId);
foreach ($arrMapRows as $arrMapRow) {
if (intval($arrMapRow['ct_race_id'] ?? 0) > 0) {
return true;
}
}
return false;
}
function fxChronotrackApiAdminRenderEventPage($intEveId, $strLangue = 'fr', $blnInlineOnEventForm = false, $blnInsideV2Panel = false, $blnFocus = 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);
$blnEventLinked = ($arrConfig !== null && intval($arrConfig['ct_event_id'] ?? 0) > 0);
echo '<div class="ct-api-admin mt-2 mb-3' . ($blnFocus ? ' ct-api-admin--focus' : '') . '">';
if (!$blnInsideV2Panel) {
if ($blnInlineOnEventForm) {
echo '<h2 class="h5">MSIN API ChronoTrack</h2>';
} else {
echo '<h1 class="h3">MSIN API ChronoTrack</h1>';
}
}
// --- Fiche événement : lien seulement + CTA hub (MSIN-4328) ---
if ($blnInlineOnEventForm) {
echo '<p class="text-muted small mb-2">Branchement MS1 ↔ ChronoTrack pour <strong>'
. fxChronotrackApiAdminEsc($arrEvent['eve_nom']) . '</strong> (eve_id '
. intval($arrEvent['eve_id']) . '). Opération et config : Courses connectées.</p>';
if (!$blnConfigured) {
echo '<div class="alert alert-warning">' . fxChronotrackApiAdminEsc(fxChronotrackApiSettingsErrorMessage()) . '</div>';
}
if (!$blnEventLinked) {
fxChronotrackApiAdminRenderEventLinkPicker($blnClosed);
} else {
fxChronotrackApiAdminRenderEventFormLinkedSummary($intEveId, $arrConfig, $blnClosed);
}
echo '</div>';
fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed, $vDomaine, true);
return;
}
// --- Hub Courses connectées : Opération / Configuration / Diagnostic ---
echo '<div class="ct-api-course-head d-flex flex-wrap align-items-center justify-content-between mb-3">';
echo '<div>';
echo '<div class="ct-api-course-head__title">' . fxChronotrackApiAdminEsc($arrEvent['eve_nom']) . '</div>';
echo '<div class="ct-api-course-head__meta text-muted small">eve_id ' . intval($arrEvent['eve_id']);
if ($arrConfig !== null) {
echo ' · CT ' . fxChronotrackApiAdminEsc($arrConfig['ct_event_name'] ?? '')
. ' (#' . intval($arrConfig['ct_event_id'] ?? 0) . ')'
. ' · ' . fxChronotrackApiAdminEsc(fxChronotrackApiConfigStatusLabel($strStatus));
}
echo '</div></div>';
echo '<div class="mt-2 mt-md-0">';
if ($blnFocus) {
echo '<a class="btn btn-outline-secondary btn-sm" href="index.php">'
. '<i class="fa fa-times" aria-hidden="true"></i>&nbsp;Fermer / retour super admin</a>';
} else {
echo '<a class="btn btn-outline-secondary btn-sm" href="chronotrack_api.php">&larr; Liste</a> ';
echo '<a class="btn btn-primary btn-sm" href="chronotrack_api.php?eve_id=' . intval($intEveId)
. '&amp;focus=1" target="_blank" rel="noopener">'
. '<i class="fa fa-external-link" aria-hidden="true"></i>&nbsp;Ouvrir plein écran</a>';
}
echo '</div></div>';
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 ($blnClosed) {
echo '<div class="alert alert-secondary py-2">Statut <strong>fermé</strong> : plus de sync auto ni modification du mapping. '
. 'Le <strong>push manuel</strong> reste disponible dans Opération.</div>';
}
if (!$blnEventLinked) {
fxChronotrackApiAdminRenderEventLinkPicker($blnClosed);
}
// Bloc A — Opération (toujours ouvert)
if ($arrConfig !== null && $blnEventLinked && $blnConfigured) {
fxChronotrackApiAdminRenderPushPanel($intEveId, $blnClosed, $arrConfig);
}
// Bloc B — Configuration (repliée si mapping prêt)
if ($arrConfig !== null && $blnEventLinked) {
$blnHasSavedMapping = fxChronotrackApiAdminHasSavedRaceMapping($intEveId);
$strConfigExpanded = $blnHasSavedMapping ? '' : ' show';
echo '<div class="card mb-4" id="ct_api_config_card">';
echo '<div class="card-header py-2">';
echo '<a class="d-block text-dark' . ($blnHasSavedMapping ? ' collapsed' : '') . '" data-toggle="collapse" '
. 'href="#ct_api_config_collapse" role="button" aria-expanded="'
. ($blnHasSavedMapping ? 'false' : 'true') . '">';
echo 'Configuration';
if ($blnHasSavedMapping) {
echo ' <span class="badge badge-success ml-1">Prêt</span>';
} else {
echo ' <span class="badge badge-warning ml-1">À compléter</span>';
}
echo ' <span class="text-muted small">(cycle de vie, mapping, sync auto)</span>';
echo '</a></div>';
echo '<div class="collapse' . $strConfigExpanded . '" id="ct_api_config_collapse"><div class="card-body pt-3">';
fxChronotrackApiAdminRenderStatusActions($intEveId, $strStatus, $blnEventLinked);
fxChronotrackApiAdminRenderRaceMapping($intEveId, $arrConfig, $strLangue, $blnClosed, $blnEventLinked);
if ($blnConfigured) {
fxChronotrackApiAdminRenderAutoSyncForm($blnClosed, $arrConfig);
}
echo '</div></div></div>';
} elseif ($arrConfig !== null) {
fxChronotrackApiAdminRenderStatusActions($intEveId, $strStatus, $blnEventLinked);
fxChronotrackApiAdminRenderRaceMapping($intEveId, $arrConfig, $strLangue, $blnClosed, $blnEventLinked);
}
// Bloc C — Diagnostic (toujours fermé par défaut)
if ($blnEventLinked && $blnConfigured) {
fxChronotrackApiAdminRenderDiagnosticPanel(intval($arrConfig['ct_event_id']));
}
if (!$blnFocus) {
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, false);
}
function fxChronotrackApiAdminRenderStatusActions($intEveId, $strStatus, $blnEventLinked = true) {
echo '<div class="card mb-3 border"><div class="card-header py-2">Cycle de vie sync</div><div class="card-body py-3">';
// MSIN-4574 — Lié ≠ Actif : seul Actif autorise la sync auto / cron
echo '<p class="small text-muted mb-2"><strong>Lié</strong> = câblé (mapping, envoi manuel de test). '
. '<strong>Actif</strong> = course / sync auto. '
. '<strong>Fermé</strong> = terminé (bloque sync auto + config).</p>';
echo '<div class="btn-group" role="group">';
if ($strStatus === MSIN_API_CHRONOTRACK_SYNC_LIE) {
echo '<span class="badge badge-info align-self-center mr-2">Lié</span>';
echo '<button type="button" class="btn btn-warning btn-sm ct-api-set-status" data-status="' . MSIN_API_CHRONOTRACK_SYNC_ACTIF . '">Activer la sync (Actif)</button>';
}
if ($strStatus === MSIN_API_CHRONOTRACK_SYNC_ACTIF) {
echo '<span class="badge badge-success align-self-center mr-2">Actif</span>';
echo '<button type="button" class="btn btn-danger btn-sm ct-api-set-status" data-status="' . MSIN_API_CHRONOTRACK_SYNC_FERME . '">Terminer la course</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>';
// MSIN-4568 — vrai délier (≠ Terminer) pour changer devent CT
if ($blnEventLinked) {
echo '<hr class="my-3">';
echo '<p class="small text-muted mb-2">Pour lier un <strong>autre</strong> événement ChronoTrack : déconnecter '
. '(remet le choix CT ; les participants déjà envoyés restent dans lancien CT).</p>';
echo '<button type="button" class="btn btn-outline-danger btn-sm" id="ct_api_btn_unlink">'
. 'Déconnecter de ChronoTrack</button>';
}
echo '</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;
}
}
}
echo '<div class="card mb-0 border"><div class="card-header py-2">Mapping épreuves MS1 → races ChronoTrack</div><div class="card-body py-3">';
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, $arrConfig = null) {
// MSIN-4328 — tableau de bord Opération (layout stable, suivi course)
echo '<div class="card mb-4 border-0 shadow-sm ct-api-ops-card" id="ct_api_push_card">';
echo '<div class="card-header ct-api-ops-card__header border-0">Opération</div>';
echo '<div class="card-body">';
if ($blnClosed) {
echo '<div class="alert alert-warning py-2 small mb-2">Événement fermé — envoi manuel encore possible.</div>';
}
if (is_array($arrConfig)) {
fxChronotrackApiAdminRenderAutoSyncBanner($blnClosed, $arrConfig);
}
echo '<div id="ct_api_push_status" class="ct-api-push-status mb-3" aria-live="polite">';
echo '<p class="ct-api-push-status__title text-muted mb-1">En attente</p>';
echo '<p class="ct-api-push-status__hint mb-0 text-muted">Cliquez <strong>Actualiser</strong> pour vérifier sil y a des changements.</p>';
echo '<p id="ct_api_status_refresh_cd" class="small text-muted mb-0 mt-1"></p>';
echo '</div>';
// KPI fixes (mis à jour en place — pas de reflow « Excel »)
echo '<div class="ct-api-kpi-row mb-3" id="ct_api_push_summary">';
echo '<div class="ct-api-kpi" data-kpi="total"><div class="ct-api-kpi__label">Total MS1</div>'
. '<div class="ct-api-kpi__value" id="ct_api_kpi_total">—</div></div>';
echo '<div class="ct-api-kpi ct-api-kpi--ok" data-kpi="eligible"><div class="ct-api-kpi__label">Éligibles</div>'
. '<div class="ct-api-kpi__value" id="ct_api_kpi_eligible">—</div></div>';
echo '<div class="ct-api-kpi ct-api-kpi--danger" data-kpi="blocked"><div class="ct-api-kpi__label">Exclus</div>'
. '<div class="ct-api-kpi__value" id="ct_api_kpi_blocked">—</div></div>';
echo '<div class="ct-api-kpi ct-api-kpi--warn" data-kpi="pending"><div class="ct-api-kpi__label">À envoyer</div>'
. '<div class="ct-api-kpi__value" id="ct_api_kpi_pending">—</div></div>';
echo '</div>';
echo '<p class="ct-api-dash-push small text-muted mb-3" id="ct_api_kpi_live_hint">ChronoTrack Live : non chargé '
. '(outil dans Diagnostic).</p>';
echo '<div class="ct-api-push-actions mb-2">';
echo '<button type="button" class="btn btn-outline-secondary" 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-primary btn-lg" id="ct_api_btn_push" disabled>';
echo '<i class="fa fa-cloud-upload" aria-hidden="true"></i>&nbsp;Envoyer vers ChronoTrack</button> ';
echo '<button type="button" class="btn btn-outline-warning" id="ct_api_btn_push_full" disabled'
. ' title="Renvoie tous les éligibles, sans tenir compte de la date du dernier envoi">';
echo '<i class="fa fa-refresh" aria-hidden="true"></i>&nbsp;Tout renvoyer</button> ';
echo '<button type="button" class="btn btn-outline-dark btn-sm" id="ct_api_btn_logs">';
echo '<i class="fa fa-list" aria-hidden="true"></i>&nbsp;Voir les logs</button> ';
// MSIN-4574 — alignement MS1 ↔ CT (orphelins / annulés restés sur CT)
echo '<button type="button" class="btn btn-outline-info btn-sm" id="ct_api_btn_reconcile"'
. ' title="Liste les inscriptions ChronoTrack absentes des éligibles MS1 (annulés, transferts, fantômes)">';
echo '<i class="fa fa-balance-scale" aria-hidden="true"></i>&nbsp;Vérifier alignement</button>';
echo '</div>';
echo '<div class="form-check mb-2">';
echo '<input class="form-check-input" type="checkbox" id="ct_api_verbose_logs" value="1">';
echo '<label class="form-check-label small" for="ct_api_verbose_logs">'
. 'Logs détaillés (1 ligne par participant) — sinon résumé par lot seulement</label>';
echo '</div>';
echo '<p class="small text-muted mb-2"><strong>Envoyer</strong> = nouveaux / modifiés. '
. '<strong>Tout renvoyer</strong> = tous les éligibles (même barre %). '
. '<strong>Vérifier alignement</strong> = entries CT hors MS1 (annulés, etc.).</p>';
echo '<div id="ct_api_push_result" class="ct-api-push-result small mb-3"></div>';
echo '<div id="ct_api_reconcile_wrap" class="mb-3 d-none"></div>';
echo '<div id="ct_api_logs_wrap" class="mb-3 d-none"></div>';
echo '<div class="ct-api-exclus-shell" id="ct_api_push_details_card">';
echo '<div class="ct-api-exclus-shell__head">';
echo '<a class="d-flex align-items-center justify-content-between text-decoration-none collapsed" '
. 'data-toggle="collapse" href="#ct_api_push_details_collapse" role="button" aria-expanded="false">';
echo '<span class="ct-api-exclus-shell__title-wrap">';
echo '<span class="ct-api-exclus-shell__title">Exclus</span>';
echo ' <span class="badge badge-danger ml-2"><span id="ct_api_hdr_exclus">0</span> exclus</span>';
echo '</span>';
echo '<span class="text-muted small">ouvrir</span>';
echo '</a></div>';
echo '<div class="collapse" id="ct_api_push_details_collapse">';
echo '<div class="ct-api-exclus-shell__body">';
echo '<div id="ct_api_push_blocked_wrap" class="mb-0"></div>';
echo '<p class="small text-muted mt-3 mb-0">external ID = <code>par_id_original</code>. '
. 'Comptage Live / sondes → Diagnostic.</p>';
echo '</div></div></div>';
echo '</div></div>';
}
function fxChronotrackApiAdminRenderDiagnosticPanel($intCtEventId) {
// MSIN-4328 — zone Diagnostic / debug (fermée par défaut)
echo '<div class="card mb-4 border-secondary" id="ct_api_diagnostic_card"><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 / debug <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">Outils de support et tests. Pas nécessaires pour lopération courante.</p>';
echo '<div class="mb-3">';
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" id="ct_api_btn_probe_push">';
echo '<i class="fa fa-flask" aria-hidden="true"></i>&nbsp;Sonde (1 participant POST)</button> ';
// MSIN-4574 — bouton dédié (avant : pays caché dans le résultat de la sonde POST)
echo '<button type="button" class="btn btn-outline-info btn-sm mb-2" id="ct_api_btn_probe_pays"'
. ' title="ÉCRITURE : PUT 1 participant + relecture (peut écraser un réglage manuel CT)">'
. '<i class="fa fa-globe" aria-hidden="true"></i>&nbsp;Sonde pays (écriture)</button> ';
// MSIN-4574 — lecture seule (calibrage UI Canada/Quebec sans écraser)
echo '<button type="button" class="btn btn-outline-success btn-sm mb-2" id="ct_api_btn_probe_pays_read"'
. ' title="GET entry seulement — ne modifie rien sur ChronoTrack">'
. '<i class="fa fa-eye" aria-hidden="true"></i>&nbsp;Sonde lecture pays</button> ';
echo '<button type="button" class="btn btn-outline-secondary btn-sm mb-2" id="ct_api_btn_probe_custom" '
. 'title="PUT/POST entry avec entry_note_test + relecture (MSIN-4461 — notes custom CT)">';
echo '<i class="fa fa-flask" aria-hidden="true"></i>&nbsp;Sonde entry_note_test</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>';
echo '<div class="mb-3">';
echo '<button type="button" class="btn btn-outline-secondary btn-sm mb-2" id="ct_api_btn_auto_diag">'
. 'État sync auto</button> ';
echo '<button type="button" class="btn btn-outline-secondary btn-sm mb-2" id="ct_api_btn_cron_ping" title="Appelle auto_chronotrack_sync.php sur ce même domaine">'
. 'Lancer le script cron</button> ';
echo '<button type="button" class="btn btn-outline-info btn-sm mb-2" id="ct_api_btn_ct_count">';
echo '<i class="fa fa-calculator" aria-hidden="true"></i>&nbsp;Nb ChronoTrack Live</button> ';
echo '<button type="button" class="btn btn-outline-secondary btn-sm mb-2" id="ct_api_btn_mark_current" title="Si le différentiel est faux (ex. après comptage Live)">';
echo 'Remettre à jour (sans envoyer)</button>';
echo '</div>';
echo '<div id="ct_api_diagnostic_out" class="small text-muted">Cliquez sur un outil ci-dessus.</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 { font-size: 0.8rem; }
#ct_api_push_blocked_wrap .ct-api-blocked-table th { white-space: nowrap; }
#ct_api_push_blocked_wrap .ct-api-blocked-scroll { max-height: 55vh; overflow: auto; }
#ct_api_config_collapse .card { box-shadow: none; }
.ct-api-course-head__title { font-size: 1.35rem; font-weight: 700; line-height: 1.25; }
.ct-api-ops-card { background: #fff; }
.ct-api-ops-card__header {
background: linear-gradient(135deg, #1e3a5f 0%, #2c5282 100%);
color: #fff;
font-weight: 600;
letter-spacing: 0.02em;
}
.ct-api-push-status {
height: 96px;
padding: 14px 16px;
border-radius: 10px;
background: #f4f6f8;
border: 1px solid #e2e8f0;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden;
}
.ct-api-push-status--ok {
background: #ecfdf3;
border-color: #abefc6;
}
.ct-api-push-status--todo {
background: #fff7ed;
border-color: #fdba74;
}
.ct-api-push-status--loading {
background: #ecfdf3;
border-color: #abefc6;
}
.ct-api-push-status__title {
font-size: 1.2rem;
font-weight: 700;
margin: 0 0 4px;
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ct-api-push-status__hint {
margin: 0;
font-size: 0.9rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ct-api-push-status__busy {
display: flex;
align-items: center;
gap: 12px;
min-height: 0;
width: 100%;
}
.ct-api-push-status__busy .ms1-loader__runner {
width: 36px;
height: 36px;
flex-shrink: 0;
}
.ct-api-push-status__busy-text {
min-width: 0;
flex: 1;
}
.ct-api-kpi-row {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
@media (max-width: 768px) {
.ct-api-kpi-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.ct-api-kpi {
border-radius: 12px;
padding: 14px 16px;
background: #f8fafc;
border: 1px solid #e2e8f0;
min-height: 84px;
}
.ct-api-kpi--ok { background: #eff6ff; border-color: #bfdbfe; }
.ct-api-kpi--danger { background: #fef2f2; border-color: #fecaca; }
.ct-api-kpi--warn { background: #fffbeb; border-color: #fde68a; }
.ct-api-kpi--warn.is-active { background: #fff7ed; border-color: #fb923c; box-shadow: 0 0 0 1px rgba(251,146,60,.25); }
.ct-api-kpi__label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: #64748b;
font-weight: 600;
margin-bottom: 6px;
}
.ct-api-kpi__value {
font-size: 1.75rem;
font-weight: 750;
font-variant-numeric: tabular-nums;
line-height: 1.1;
color: #0f172a;
min-height: 1.1em;
}
.ct-api-kpi--ok .ct-api-kpi__value { color: #1d4ed8; }
.ct-api-kpi--danger .ct-api-kpi__value { color: #b91c1c; }
.ct-api-kpi--warn .ct-api-kpi__value { color: #b45309; }
.ct-api-push-actions .btn-lg { font-size: 1.05rem; padding: 0.55rem 1.1rem; }
.ct-api-push-result { min-height: 1.5em; }
.ct-api-exclus-shell {
border: 1px solid #e2e8f0;
border-radius: 12px;
overflow: hidden;
background: #fff;
}
.ct-api-exclus-shell__head {
padding: 10px 14px;
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
}
.ct-api-exclus-shell__title {
font-weight: 650;
color: #334155;
}
.ct-api-exclus-shell__body { padding: 12px 14px; }
.ct-api-exclus-empty {
color: #64748b;
font-size: 0.9rem;
padding: 8px 0;
}
.ct-api-exclus-chip {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 14px;
border-radius: 10px;
border: 1px solid #fecaca;
background: linear-gradient(180deg, #fff 0%, #fff5f5 100%);
margin-bottom: 10px;
}
.ct-api-exclus-chip__count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2.25rem;
height: 2.25rem;
padding: 0 8px;
border-radius: 999px;
background: #dc2626;
color: #fff;
font-weight: 700;
}
.ct-api-auto-wrap {
padding: 12px 14px;
border-radius: 8px;
border: 1px dashed #adb5bd;
background: #fafbfc;
}
.ct-api-auto-wrap--disabled { opacity: 0.7; }
.ct-api-auto-banner {
display: flex;
flex-wrap: wrap;
align-items: center;
padding: 8px 12px;
border-radius: 8px;
background: #f8f9fa;
border: 1px solid #e9ecef;
font-size: 0.9rem;
}
#ct_api_logs_wrap .ct-api-logs-scroll { max-height: 280px; overflow: auto; }
.ct-api-focus-page { max-width: 1400px; margin: 0 auto; }
</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;
var intCtLiveCount = null;
var blnPushRunning = false;
var intStatusRefreshTimer = null;
var intStatusCountdownTimer = null;
var intStatusRefreshSec = 60;
var intStatusCountdownLeft = intStatusRefreshSec;
function escHtml(str) {
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// MSIN-4328 — compte à rebours du prochain refresh écran (JS only, aucun envoi CT)
function paintStatusRefreshCountdown() {
var $el = $('#ct_api_status_refresh_cd');
if (!$el.length || !$('#ct_api_push_card').is(':visible')) {
return;
}
$el.text('Prochain refresh écran dans ' + intStatusCountdownLeft + ' s');
}
function resetStatusRefreshCountdown() {
intStatusCountdownLeft = intStatusRefreshSec;
paintStatusRefreshCountdown();
}
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é) ?\n\nLa sync auto ne tournera plus tant que vous ne repassez pas en Actif.';
} else {
strConfirm = 'Passer en Actif ?\n\nLa sync automatique (cron) pourra alors envoyer. Lenvoi manuel reste possible aussi en Lié.';
}
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');
});
// MSIN-4568 — délier (confirm) puis recharger le sélecteur CT
$(document).on('click', '#ct_api_btn_unlink', function () {
var strConfirm = 'Êtes-vous sûr de vouloir déconnecter cet événement de ChronoTrack ?\n\n'
+ 'Vous pourrez ensuite en choisir un autre.\n'
+ 'Les participants déjà envoyés restent dans lancien événement ChronoTrack.\n'
+ 'Le mapping et les liens techniques MS1 seront réinitialisés.';
if (!window.confirm(strConfirm)) {
return;
}
var $btn = $(this);
$btn.prop('disabled', true);
$.post(strAjaxUrl, { a: 'unlink', eve_id: intEveId }, function (res) {
if (rejectIfSessionLost(res)) {
return;
}
if (!res || res.state !== 'ok') {
showMsg('error', (res && res.message) ? res.message : 'Erreur déconnexion');
$btn.prop('disabled', false);
return;
}
window.location.reload();
}, 'json').fail(function () {
showMsg('error', 'Erreur réseau');
$btn.prop('disabled', false);
});
});
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) {
// MSIN-4328 — colonne CT : race_name (reg_choice) (ID race_id)
var strName = race.name || 'Course';
var strRc = race.reg_choice_label || '';
var strText = strName;
if (strRc) {
strText += ' (' + strRc + ')';
} else if (race.label && race.label !== strName) {
strText = race.label;
}
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);
});
// MSIN-4574 — rendu commun sonde POST / sonde pays
function renderProbePushResult(res, blnPaysFocus) {
var strState = (res && res.state) ? res.state : 'error';
var strClass = (strState === 'ok') ? 'text-success'
: ((strState === 'partial') ? 'text-warning' : 'text-danger');
var strTitle = blnPaysFocus ? 'Sonde pays' : 'Sonde';
var strHtml = '<div class="' + strClass + '"><strong>' + escHtml(strTitle) + '</strong> — '
+ escHtml((res && res.message) ? res.message : 'Erreur') + '</div>';
if (res) {
var strSent = res.sent_location_country || '';
var strRb = (res.ct_country_readback && res.ct_country_readback.location_country)
? res.ct_country_readback.location_country : '';
var blnPaysOk = !!res.pays_match || (strSent && strRb
&& String(strSent).toLowerCase() === String(strRb).toLowerCase());
strHtml += '<div class="small mt-2 p-2 border rounded '
+ (blnPaysOk ? 'border-success' : 'border-warning bg-light') + '">';
strHtml += '<strong>Pays</strong> — MS1 pay_id=' + escHtml(String(res.ms1_pay_id != null ? res.ms1_pay_id : '—'))
+ ' iso2=' + escHtml(res.ms1_country_iso2 || '—')
+ ' iso3=' + escHtml(res.ms1_country_iso3 || res.ms1_country_resolved || '—')
+ ' label=' + escHtml(res.ms1_country_label || '—')
+ '<br>→ envoyé location_country=<code>' + escHtml(strSent || '—') + '</code>'
+ ' country_code=<code>' + escHtml(res.sent_country_code || '—') + '</code>';
if (res.ct_country_readback) {
strHtml += '<br>Relecture CT (' + escHtml(res.ct_country_readback.how || '') + ') : '
+ 'location_country=<code>' + escHtml(res.ct_country_readback.location_country || '—') + '</code>'
+ ' location_region=<code>' + escHtml(res.ct_country_readback.location_region || '—') + '</code>'
+ ' country_name=<code>' + escHtml(res.ct_country_readback.country_name || '—') + '</code>';
}
strHtml += '<br>method=' + escHtml(res.method || '—')
+ ' http=' + escHtml(String(res.http_code != null ? res.http_code : '—'));
if (res.transferable_total != null) {
strHtml += '<br>Transférables avec pays résolu : '
+ escHtml(String(res.transferable_with_country || 0))
+ ' / ' + escHtml(String(res.transferable_total))
+ ' (sans pay_id : ' + escHtml(String(res.transferable_no_pay_id || 0)) + ')';
}
if (blnPaysFocus && !strSent) {
strHtml += '<br><span class="text-warning">Aucun location_country dans le payload — '
+ 'vérifier pay_id / pay_iso / pay_iso3 sur ce participant.</span>';
}
if (blnPaysFocus && strRb && strRb.length === 2) {
strHtml += '<br><span class="text-danger">CT a une province 2 lettres dans location_country (« '
+ escHtml(strRb) + ' ») — pollution ; on envoie maintenant le nom pays.</span>';
}
strHtml += '</div>';
}
if (res && res.payload && !blnPaysFocus) {
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>';
} else if (res && res.payload && blnPaysFocus) {
strHtml += '<pre class="small bg-light p-2 mt-2 mb-1" style="max-height:140px;overflow:auto;">'
+ escHtml(JSON.stringify({
external_id: res.payload.external_id,
location_country: res.payload.location_country,
country_code: res.payload.country_code,
country_name: res.payload.country_name,
location_region: res.payload.location_region,
first_name: res.payload.first_name,
last_name: res.payload.last_name
}, null, 2)) + '</pre>';
}
if (res && res.response_body && (blnPaysFocus || strState !== 'ok')) {
strHtml += '<pre class="small bg-light p-2 mb-0" style="max-height:160px;overflow:auto;">'
+ escHtml(res.response_body) + '</pre>';
}
return strHtml;
}
function runProbePush($btn, blnPaysFocus) {
var $out = $('#ct_api_diagnostic_out');
var $btns = $('#ct_api_btn_probe_push, #ct_api_btn_probe_pays');
$btns.prop('disabled', true);
$out.html(waitHtml(blnPaysFocus
? 'Sonde pays (POST 1 + relecture location_country)…'
: 'Sonde POST (1 participant)…'));
$('#ct_api_btn_diagnostic_copy').addClass('d-none');
$.post(strAjaxUrl, { a: 'sync_probe_push', eve_id: intEveId }, function (res) {
if (rejectIfSessionLost(res)) {
return;
}
$out.html(renderProbePushResult(res, !!blnPaysFocus));
}, 'json').fail(function () {
$out.html('<div class="text-danger">Erreur réseau (sonde'
+ (blnPaysFocus ? ' pays' : '') + ').</div>');
}).always(function () {
$btns.prop('disabled', false);
});
}
$('#ct_api_btn_probe_push').on('click', function () {
runProbePush($(this), false);
});
$('#ct_api_btn_probe_pays').on('click', function () {
runProbePush($(this), true);
});
// MSIN-4574 — sonde lecture seule (aucun PUT)
$('#ct_api_btn_probe_pays_read').on('click', function () {
var $btn = $(this);
var $out = $('#ct_api_diagnostic_out');
$btn.prop('disabled', true);
$out.html(waitHtml('Sonde lecture pays (GET seul — aucune écriture)…'));
$('#ct_api_btn_diagnostic_copy').addClass('d-none');
$.post(strAjaxUrl, { a: 'sync_probe_pays_read', eve_id: intEveId }, function (res) {
if (rejectIfSessionLost(res)) {
return;
}
if (!res || res.state !== 'ok') {
$out.html('<div class="text-danger"><strong>Sonde lecture pays</strong> — '
+ escHtml((res && res.message) ? res.message : 'Erreur') + '</div>');
return;
}
var ms1 = res.ms1 || {};
var ct = res.ct_location || {};
var html = '<div class="text-success"><strong>Sonde lecture pays</strong> — '
+ escHtml(res.message || 'OK') + '</div>';
html += '<div class="small mt-2 p-2 border rounded border-success bg-light">';
html += '<div><strong>Participant</strong> '
+ escHtml(res.name || '—')
+ ' · ext=<code>' + escHtml(String(res.external_id || '')) + '</code>'
+ ' · entry_id=<code>' + escHtml(String(res.ct_entry_id || '')) + '</code>'
+ ' · ' + escHtml(res.how || '') + '</div>';
html += '<div class="mt-2"><strong>MS1 (BD)</strong> pay_id=' + escHtml(String(ms1.pay_id != null ? ms1.pay_id : '—'))
+ ' pro_id=' + escHtml(String(ms1.pro_id != null ? ms1.pro_id : '—'))
+ ' → iso2=' + escHtml(ms1.country_iso2 || '—')
+ ' iso3=' + escHtml(ms1.country_code || ms1.country_iso3 || '—')
+ ' label=' + escHtml(ms1.country_label || '—')
+ ' state_iso=' + escHtml(ms1.state_iso || '—') + '</div>';
html += '<div class="mt-2"><strong>CT (lu)</strong>'
+ '<br>location_country=<code>' + escHtml(ct.location_country || '—') + '</code>'
+ ' location_region=<code>' + escHtml(ct.location_region || '—') + '</code>'
+ '<br>country_code=<code>' + escHtml(ct.country_code || '—') + '</code>'
+ ' country_name=<code>' + escHtml(ct.country_name || '—') + '</code>'
+ '<br>state_code=<code>' + escHtml(ct.state_code || '—') + '</code>'
+ ' state_name=<code>' + escHtml(ct.state_name || '—') + '</code>'
+ '<br>city=<code>' + escHtml(ct.location_city || ct.city || '—') + '</code>'
+ ' postal=<code>' + escHtml(ct.location_postal_code || ct.postal_code || '—') + '</code>'
+ '</div>';
html += '<p class="mb-0 mt-2 text-muted">Aucun PUT/POST — safe pour calibrer après réglage manuel dans Athlete Info.</p>';
html += '</div>';
html += '<pre class="small bg-light p-2 mt-2 mb-0" style="max-height:220px;overflow:auto;">'
+ escHtml(JSON.stringify(ct, null, 2)) + '</pre>';
$out.html(html);
}, 'json').fail(function () {
$out.html('<div class="text-danger">Erreur réseau (sonde lecture pays).</div>');
}).always(function () {
$btn.prop('disabled', false);
});
});
// MSIN-4461 — sonde entry_note_test (notes custom CT)
$('#ct_api_btn_probe_custom').on('click', function () {
var $btn = $(this);
var $out = $('#ct_api_diagnostic_out');
$btn.prop('disabled', true);
$out.html(waitHtml('Sonde entry_note_test…'));
$('#ct_api_btn_diagnostic_copy').addClass('d-none');
$.post(strAjaxUrl, { a: 'sync_probe_custom', eve_id: intEveId }, function (res) {
if (rejectIfSessionLost(res)) {
return;
}
var strClass = (res && res.state === 'ok')
? 'text-success'
: ((res && res.state === 'partial') ? 'text-warning' : 'text-danger');
var strHtml = '<div class="' + strClass + '"><strong>Sonde entry_note</strong> — '
+ escHtml((res && res.message) ? res.message : 'Erreur') + '</div>';
if (res) {
strHtml += '<div class="small text-muted mt-1">method='
+ escHtml(res.method || '')
+ ' · field=' + escHtml(res.field_key || '')
+ ' sent=' + escHtml(String(res.field_value_sent || ''))
+ ' present=' + (res.field_present ? 'oui' : 'non')
+ ' read=' + escHtml(JSON.stringify(res.field_value_read))
+ ' · ext=' + escHtml(String(res.external_id || ''))
+ ' · entry_id=' + escHtml(String(res.ct_entry_id || ''))
+ '</div>';
}
if (res && res.payload) {
strHtml += '<pre class="small bg-light p-2 mt-2 mb-1" style="max-height:180px;overflow:auto;">'
+ escHtml(JSON.stringify(res.payload, null, 2)) + '</pre>';
}
if (res && res.readback_sample) {
strHtml += '<div class="small mt-1"><strong>Relecture (extrait)</strong></div>';
strHtml += '<pre class="small bg-light p-2 mb-1" style="max-height:180px;overflow:auto;">'
+ escHtml(JSON.stringify(res.readback_sample, null, 2)) + '</pre>';
}
if (res && res.post_response) {
strHtml += '<pre class="small bg-light p-2 mb-0" style="max-height:180px;overflow:auto;">'
+ escHtml(res.post_response) + '</pre>';
}
$out.html(strHtml);
$('#ct_api_btn_diagnostic_copy').removeClass('d-none');
}, 'json').fail(function () {
$out.html('<div class="text-danger">Erreur réseau (sonde entry_note).</div>');
}).always(function () {
$btn.prop('disabled', false);
});
});
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;
var cov = (res && res.country_coverage) ? res.country_coverage : null;
var intSansPays = (cov && cov.without_country) ? parseInt(cov.without_country, 10) : 0;
$('#ct_api_hdr_exclus').text(String(intTotal));
if (intTotal <= 0 || !tabGroups.length) {
var htmlEmpty = '<div class="ct-api-exclus-empty">Aucun exclus pour le moment.</div>';
if (intSansPays > 0) {
htmlEmpty += '<div class="ct-api-exclus-chip mt-2" style="border-color:#f0ad4e;background:#fff8e6;">';
htmlEmpty += '<div><strong class="text-warning">Sans pays MS1</strong>';
htmlEmpty += '<div class="small text-muted">Envoyés quand même — CT restera sans pays. Corriger <code>pay_id</code>.</div></div>';
htmlEmpty += '<span class="ct-api-exclus-chip__count" style="background:#f0ad4e;">'
+ escHtml(String(intSansPays)) + '</span></div>';
htmlEmpty += '<div class="small text-muted">avec pays: '
+ escHtml(String(cov.with_country || 0))
+ ' / ' + escHtml(String(cov.total || 0))
+ ' · sans pay_id: ' + escHtml(String(cov.no_pay_id || 0))
+ ' — voir logs push_prepare / push_done</div>';
}
$wrap.html(htmlEmpty);
return;
}
var html = '<div class="ct-api-exclus-chip">';
html += '<div><strong class="text-danger">Exclus MS1</strong>';
html += '<div class="small text-muted">Participants non envoyés (à corriger ou volontairement exclus)</div></div>';
html += '<span class="ct-api-exclus-chip__count">' + intTotal + '</span></div>';
// MSIN-4574 — alerte pays (envoyé quand même, mais CT sans location_country)
if (intSansPays > 0) {
html += '<div class="ct-api-exclus-chip mt-2" style="border-color:#f0ad4e;background:#fff8e6;">';
html += '<div><strong class="text-warning">Sans pays MS1</strong>';
html += '<div class="small text-muted">Envoyés quand même — CT restera sans pays. Corriger <code>pay_id</code> / mapping pays.</div></div>';
html += '<span class="ct-api-exclus-chip__count" style="background:#f0ad4e;">'
+ escHtml(String(intSansPays)) + '</span></div>';
html += '<div class="small text-muted mb-2">avec pays: '
+ escHtml(String(cov.with_country || 0))
+ ' / ' + escHtml(String(cov.total || 0))
+ ' · sans pay_id: ' + escHtml(String(cov.no_pay_id || 0))
+ ' — détails aussi dans <em>Voir les logs</em> (push_prepare / push_done)</div>';
}
html += '<div id="ct_api_blocked_detail">';
$.each(tabGroups, function (_, grp) {
var intCnt = (grp.items && grp.items.length) ? grp.items.length : 0;
if (intCnt === 0) {
return;
}
var blnTeam = (grp.code === 'team' || grp.code === 'team_covered');
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 (grp.code === 'team_covered') {
html += ' <span class="text-muted small ml-1">1 entrée CT = EQ. + nom d\'équipe (capitaine)</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>';
$wrap.html(html);
}
function setKpi(strId, intVal) {
var $el = $('#' + strId);
if ($el.length) {
$el.text(String(intVal));
}
}
function statusRunnerImg() {
var strSrc = <?php echo json_encode($vDomaine . '/images/ms1-runner-loader.gif?v=' . _VERSION_CODE); ?>;
return '<img class="ms1-loader__runner" src="' + strSrc + '" alt="" role="presentation" decoding="async">';
}
function setStatusLoading(strMsg) {
var $status = $('#ct_api_push_status');
if (!$status.length) {
return;
}
// Même hauteur / fond vert que « Tout est à jour » — petit coureur inline, pas le loader 120px
$status.removeClass('ct-api-push-status--ok ct-api-push-status--todo')
.addClass('ct-api-push-status--loading')
.html(
'<div class="ct-api-push-status__busy">'
+ statusRunnerImg()
+ '<div class="ct-api-push-status__busy-text">'
+ '<p class="ct-api-push-status__title mb-1">' + escHtml(strMsg || 'Vérification…') + '</p>'
+ '<p class="ct-api-push-status__hint mb-0 text-muted">Les compteurs restent affichés.</p>'
+ '<p id="ct_api_status_refresh_cd" class="small text-muted mb-0 mt-1"></p>'
+ '</div></div>'
);
}
function renderPushPreview(res) {
var $sum = $('#ct_api_push_summary');
var $status = $('#ct_api_push_status');
var $btn = $('#ct_api_btn_push');
var $btnFull = $('#ct_api_btn_push_full');
objPushPreview = res;
if (!$sum.length && !$status.length) {
return;
}
if (!res || res.state !== 'ok') {
if ($status.length) {
$status.removeClass('ct-api-push-status--ok ct-api-push-status--todo ct-api-push-status--loading')
.html(
'<p class="ct-api-push-status__title text-danger mb-1">'
+ escHtml((res && res.message) ? res.message : 'Erreur') + '</p>'
+ '<p id="ct_api_status_refresh_cd" class="small text-muted mb-0 mt-1"></p>'
);
}
$btn.prop('disabled', true);
$btnFull.prop('disabled', true);
$('#ct_api_hdr_exclus').text('0');
return;
}
var intBlocked = res.blocked_count || 0;
var intEligible = res.transferable || 0;
var intPending = (typeof res.pending_push !== 'undefined')
? res.pending_push
: (res.to_push || 0);
var intMs1 = res.ms1_total || 0;
var blnFirst = !!res.is_first_push;
var strLastPush = res.last_push_at || null;
setKpi('ct_api_kpi_total', intMs1);
setKpi('ct_api_kpi_eligible', intEligible);
setKpi('ct_api_kpi_blocked', intBlocked);
setKpi('ct_api_kpi_pending', intPending);
$('.ct-api-kpi[data-kpi="pending"]').toggleClass('is-active', intPending > 0);
if ($status.length) {
$status.removeClass('ct-api-push-status--ok ct-api-push-status--todo ct-api-push-status--loading');
if (intPending > 0) {
$status.addClass('ct-api-push-status--todo');
var strTodoTitle = blnFirst
? (intPending + ' à envoyer (premier envoi)')
: (intPending + ' changement' + (intPending > 1 ? 's' : '') + ' à envoyer');
$status.html(
'<p class="ct-api-push-status__title">' + escHtml(strTodoTitle) + '</p>'
+ '<p class="ct-api-push-status__hint mb-0">Cliquez <strong>Envoyer vers ChronoTrack</strong> — seuls les nouveaux / modifiés partent.</p>'
+ '<p id="ct_api_status_refresh_cd" class="small text-muted mb-0 mt-1"></p>'
);
} else {
$status.addClass('ct-api-push-status--ok');
var strOk = 'Tout est à jour';
if (strLastPush) {
strOk += ' <span class="text-muted font-weight-normal" style="font-size:0.85rem;">· dernier envoi '
+ escHtml(strLastPush) + '</span>';
}
$status.html(
'<p class="ct-api-push-status__title text-success">' + strOk + '</p>'
+ '<p class="ct-api-push-status__hint mb-0 text-muted">'
+ intEligible + ' éligibles · rien à envoyer pour le moment.</p>'
+ '<p id="ct_api_status_refresh_cd" class="small text-muted mb-0 mt-1"></p>'
);
}
paintStatusRefreshCountdown();
}
var $live = $('#ct_api_kpi_live_hint');
if ($live.length) {
if (intCtLiveCount !== null) {
$live.html('ChronoTrack Live : <strong>' + intCtLiveCount + '</strong> inscription(s) '
+ '<button type="button" class="btn btn-link btn-sm p-0 align-baseline" id="ct_api_btn_ct_count_inline">recompter</button>');
} else {
$live.text('ChronoTrack Live : non chargé (outil dans Diagnostic).');
}
}
renderBlockedPanel(res);
$btn.prop('disabled', intPending <= 0 || blnClosed || blnPushRunning);
$btnFull.prop('disabled', intEligible <= 0 || blnClosed || blnPushRunning);
// MSIN-4328 — auto-réparation au load : last_push déjà posé mais 100 % des
// éligibles « à pousser » (= faux différentiel, ex. par_maj bumpé par backfill entry_id).
// Un vrai premier envoi (is_first_push) nest pas touché.
if (!blnFirst && intPending > 0 && intEligible > 0 && intPending === intEligible
&& !window._ctApiAutoMarkDone) {
window._ctApiAutoMarkDone = true;
setStatusLoading('Recalage du différentiel…');
$.post(strAjaxUrl, {
a: 'sync_mark_current',
eve_id: intEveId,
ok_count: intEligible
}, function (markRes) {
if (rejectIfSessionLost(markRes)) {
return;
}
loadPushPreview(true);
}, 'json').fail(function () {
window._ctApiAutoMarkDone = false;
});
return;
}
// Section Exclus : reste fermée — compteur dans lentête
}
function renderPushResult(res, strTitle) {
var $res = $('#ct_api_push_result');
var strHeading = strTitle || 'Résultat';
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');
var $status = $('#ct_api_push_status');
if (!$sum.length && !$status.length) {
return;
}
if (xhrPushPreview && xhrPushPreview.readyState !== 4) {
if (!blnForce) {
return;
}
xhrPushPreview.abort();
}
if ($status.length) {
setStatusLoading('Vérification des changements…');
}
xhrPushPreview = $.ajax({
url: strAjaxUrl,
type: 'POST',
dataType: 'json',
timeout: 120000,
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;
if (strStatus === 'timeout') {
strMsg = 'Délai dépassé (preview) — réessayez Actualiser.';
} else if (strStatus === 'parsererror') {
strMsg = 'Réponse invalide (preview) — souvent un envoi encore en cours ou erreur PHP.';
} else if (jqXHR && jqXHR.status) {
strMsg = 'Erreur preview HTTP ' + jqXHR.status + ' — réessayez Actualiser.';
} else {
strMsg = 'Erreur réseau (preview). Si un envoi tourne, attendez la fin puis Actualiser.';
}
if ($status.length) {
$status.removeClass('ct-api-push-status--ok ct-api-push-status--todo ct-api-push-status--loading')
.html(
'<p class="ct-api-push-status__title text-danger mb-1">' + escHtml(strMsg) + '</p>'
+ '<p class="ct-api-push-status__hint mb-0 text-muted">Les derniers compteurs affichés sont conservés.</p>'
+ '<p id="ct_api_status_refresh_cd" class="small text-muted mb-0 mt-1"></p>'
);
}
}).always(function () {
xhrPushPreview = null;
});
}
function loadCtLiveCount() {
var $btn = $('#ct_api_btn_ct_count');
var $live = $('#ct_api_kpi_live_hint');
$btn.prop('disabled', true);
if ($live.length) {
$live.html(waitHtml('Comptage ChronoTrack Live…'));
}
$.ajax({
url: strAjaxUrl,
type: 'POST',
dataType: 'json',
timeout: 180000,
data: { a: 'sync_ct_count', eve_id: intEveId }
}).done(function (res) {
if (rejectIfSessionLost(res)) {
return;
}
if (!res || res.state !== 'ok') {
window.alert((res && res.message) ? res.message : 'Impossible de compter sur ChronoTrack Live');
return;
}
intCtLiveCount = parseInt(res.ct_total, 10);
if (isNaN(intCtLiveCount)) {
intCtLiveCount = null;
}
if (objPushPreview && objPushPreview.state === 'ok') {
renderPushPreview(objPushPreview);
}
}).fail(function (jqXHR, strStatus) {
window.alert(strStatus === 'timeout'
? 'Délai dépassé pour le comptage ChronoTrack Live.'
: 'Erreur réseau (comptage CT).');
}).always(function () {
$btn.prop('disabled', false);
});
}
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);
}
function syncAutoOptionsState() {
var blnOn = $('#ct_api_auto_enabled').is(':checked');
$('#ct_api_auto_interval, #ct_api_auto_until').prop('disabled', blnClosed || !blnOn);
}
syncAutoOptionsState();
$('#ct_api_auto_enabled').on('change', syncAutoOptionsState);
$('#ct_api_btn_auto_save').on('click', function () {
var $btn = $(this);
var $msg = $('#ct_api_auto_msg');
var $hint = $('#ct_api_auto_hint');
var blnOn = $('#ct_api_auto_enabled').is(':checked');
$btn.prop('disabled', true);
$msg.removeClass('text-success text-danger').text('Enregistrement…');
$.post(strAjaxUrl, {
a: 'sync_auto_save',
eve_id: intEveId,
enabled: blnOn ? 1 : 0,
interval_min: $('#ct_api_auto_interval').val(),
until: $('#ct_api_auto_until').val()
}, function (res) {
if (rejectIfSessionLost(res)) {
return;
}
if (!res || res.state !== 'ok') {
$msg.addClass('text-danger').text((res && res.message) ? res.message : 'Erreur');
return;
}
$msg.addClass('text-success').text(res.message || 'Enregistré');
if (res.auto_sync) {
if (res.auto_sync.enabled && res.auto_sync.until) {
$hint.text('Config OK — cron toutes les minutes lit cette config (auto_chronotrack_sync.php). Arrêt : '
+ res.auto_sync.until + '.');
} else {
$hint.text('Sync auto désactivée.');
}
}
}, 'json').fail(function () {
$msg.addClass('text-danger').text('Erreur réseau');
}).always(function () {
$btn.prop('disabled', false);
});
});
$('#ct_api_btn_push_refresh').on('click', function () {
loadPushPreview(true);
$('#ct_api_push_result').empty();
resetStatusRefreshCountdown();
});
// MSIN-4328 — refresh léger 60 s (preview MS1 seulement) + compte à rebours écran
function startStatusAutoRefresh() {
if (intStatusRefreshTimer) {
clearInterval(intStatusRefreshTimer);
}
if (intStatusCountdownTimer) {
clearInterval(intStatusCountdownTimer);
}
resetStatusRefreshCountdown();
intStatusCountdownTimer = setInterval(function () {
if (!$('#ct_api_push_card').is(':visible') || document.hidden) {
return;
}
if (intStatusCountdownLeft > 0) {
intStatusCountdownLeft--;
}
paintStatusRefreshCountdown();
}, 1000);
intStatusRefreshTimer = setInterval(function () {
if (blnPushRunning) {
return;
}
if (!$('#ct_api_push_card').is(':visible')) {
return;
}
if (document.hidden) {
return;
}
loadPushPreview(true);
resetStatusRefreshCountdown();
}, intStatusRefreshSec * 1000);
}
startStatusAutoRefresh();
function renderSyncLogs(res) {
var $wrap = $('#ct_api_logs_wrap');
if (!$wrap.length) {
return;
}
if (!res || res.state !== 'ok') {
$wrap.removeClass('d-none').html('<div class="text-danger small">'
+ escHtml((res && res.message) ? res.message : 'Erreur logs') + '</div>');
return;
}
var tabLogs = res.logs || [];
var html = '<div class="card border-secondary"><div class="card-header py-2 d-flex justify-content-between align-items-center">';
html += '<strong class="small">Logs sync (200 derniers)</strong>';
html += '<button type="button" class="btn btn-link btn-sm p-0" id="ct_api_btn_logs_close">Fermer</button>';
html += '</div><div class="card-body py-2">';
if (!tabLogs.length) {
html += '<p class="small text-muted mb-0">Aucun log pour cet événement.</p>';
} else {
html += '<p class="small text-muted mb-2">Ligne <code>push_entry</code> : '
+ '<code>dossard=</code> n° MS1 (<code>no_bib</code>), '
+ '<code>envoi_bib=</code> valeur envoyée, '
+ '<code>conflit_bib</code> = dossard différé (2e passe / swap via PUT clear), '
+ '<code>RETRY_BIB</code> = 2e tentative. '
+ 'Si la 2e passe échoue → erreur visible.</p>';
html += '<div class="ct-api-logs-scroll border rounded">';
html += '<table class="table table-sm table-striped mb-0" style="font-size:0.78rem;">';
html += '<thead class="thead-light"><tr><th>Quand</th><th>Action</th><th>Statut</th><th>par_id</th><th>Message</th></tr></thead><tbody>';
$.each(tabLogs, function (_, row) {
var strCls = (row.status === 'ok') ? 'text-success' : 'text-danger';
html += '<tr>';
html += '<td class="text-nowrap">' + escHtml(row.created_at || '') + '</td>';
html += '<td>' + escHtml(row.action || '') + '</td>';
html += '<td class="' + strCls + '">' + escHtml(row.status || '') + '</td>';
html += '<td>' + escHtml(row.par_id ? String(row.par_id) : '—') + '</td>';
html += '<td>' + escHtml(row.message || '') + '</td>';
html += '</tr>';
});
html += '</tbody></table></div>';
}
html += '</div></div>';
$wrap.removeClass('d-none').html(html);
}
function loadSyncLogs() {
var $wrap = $('#ct_api_logs_wrap');
$wrap.removeClass('d-none').html(waitHtml('Chargement des logs…'));
$.post(strAjaxUrl, { a: 'sync_logs', eve_id: intEveId, limit: 200 }, function (res) {
if (rejectIfSessionLost(res)) {
return;
}
renderSyncLogs(res);
}, 'json').fail(function () {
$wrap.html('<div class="text-danger small">Erreur réseau (logs).</div>');
});
}
$('#ct_api_btn_logs').on('click', function () {
var $wrap = $('#ct_api_logs_wrap');
if ($wrap.is(':visible') && !$wrap.hasClass('d-none') && $wrap.children().length) {
$wrap.addClass('d-none').empty();
return;
}
loadSyncLogs();
});
$(document).on('click', '#ct_api_btn_logs_close', function () {
$('#ct_api_logs_wrap').addClass('d-none').empty();
});
// MSIN-4574 — vérifier alignement MS1 ↔ CT (orphelins)
function setReconcileStatus(strTitle, strHint, strKind) {
var $status = $('#ct_api_push_status');
if (!$status.length) {
return;
}
strKind = strKind || 'loading';
var strColor = (strKind === 'ok') ? 'text-success'
: ((strKind === 'error') ? 'text-danger'
: ((strKind === 'todo') ? 'text-warning' : 'text-primary'));
$status.removeClass('ct-api-push-status--ok ct-api-push-status--todo ct-api-push-status--loading');
if (strKind === 'loading') {
$status.addClass('ct-api-push-status--loading');
} else if (strKind === 'ok') {
$status.addClass('ct-api-push-status--ok');
} else if (strKind === 'todo') {
$status.addClass('ct-api-push-status--todo');
}
$status.html(
'<p class="ct-api-push-status__title ' + strColor + ' mb-1">'
+ (strKind === 'loading'
? '<i class="fa fa-balance-scale" aria-hidden="true"></i> '
: '')
+ escHtml(strTitle) + '</p>'
+ '<p class="ct-api-push-status__hint mb-0">' + escHtml(strHint || '') + '</p>'
);
try {
if ($status[0] && $status[0].scrollIntoView) {
$status[0].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
} catch (eScroll) { /* ignore */ }
}
function renderReconcilePanel(res) {
var $wrap = $('#ct_api_reconcile_wrap');
var intCount = (res && typeof res.count !== 'undefined')
? parseInt(res.count, 10)
: ((res && res.orphans) ? res.orphans.length : 0);
var tabRows = (res && res.orphans) ? res.orphans : [];
var intShowMax = 80;
var html = '<div class="border rounded p-2 bg-light">';
html += '<div class="d-flex align-items-center justify-content-between mb-2">';
html += '<strong>Alignement ChronoTrack</strong>';
html += '<button type="button" class="btn btn-link btn-sm p-0" id="ct_api_btn_reconcile_close">Fermer</button>';
html += '</div>';
html += '<p class="small text-muted mb-2">Entries présentes sur ChronoTrack mais absentes des éligibles MS1 '
+ '(annulés, transferts, fantômes après changement déquipe / coureur).</p>';
if (!res || res.state !== 'ok') {
html += '<div class="text-danger small">' + escHtml((res && res.message) ? res.message : 'Erreur') + '</div>';
html += '</div>';
$wrap.removeClass('d-none').html(html);
setReconcileStatus(
'Alignement — erreur',
(res && res.message) ? res.message : 'Erreur',
'error'
);
return;
}
html += '<p class="mb-2"><span class="badge badge-info">' + intCount + '</span> orphelin(s)';
if (typeof res.ct_total !== 'undefined' && res.ct_total !== null) {
html += ' · CT total ' + escHtml(String(res.ct_total));
}
if (typeof res.eligible_count !== 'undefined') {
html += ' · éligibles MS1 ' + escHtml(String(res.eligible_count));
}
if (res.elapsed_ms) {
html += ' · ' + escHtml(String(Math.round(res.elapsed_ms / 100) / 10)) + ' s';
}
html += '</p>';
if (intCount === 0) {
html += '<div class="text-success small mb-0">Aucun écart détecté — MS1 et ChronoTrack sont alignés côté entries.</div>';
html += '</div>';
$wrap.removeClass('d-none').html(html);
setReconcileStatus('Alignement OK', 'Aucun orphelin CT détecté.', 'ok');
return;
}
html += '<div class="table-responsive mb-2" style="max-height:280px;overflow:auto;">';
html += '<table class="table table-sm table-striped mb-0"><thead><tr>';
html += '<th>Nom CT</th><th>ext</th><th>bib</th><th>statut</th><th>raison</th></tr></thead><tbody>';
$.each(tabRows.slice(0, intShowMax), function (_, row) {
html += '<tr>';
html += '<td>' + escHtml(row.name || '—') + '</td>';
html += '<td><code>' + escHtml(row.external_id || '—') + '</code></td>';
html += '<td>' + escHtml(row.bib || '—') + '</td>';
html += '<td>' + escHtml(row.entry_status || '—') + '</td>';
html += '<td>' + escHtml(row.reason_label || row.reason || '—') + '</td>';
html += '</tr>';
});
html += '</tbody></table></div>';
if (tabRows.length > intShowMax) {
html += '<p class="small text-muted">Affichage des ' + intShowMax
+ ' premiers — total ' + intCount + '. Le retrait traite toute la liste.</p>';
}
html += '<button type="button" class="btn btn-warning btn-sm" id="ct_api_btn_reconcile_withdraw">';
html += '<i class="fa fa-user-times" aria-hidden="true"></i>&nbsp;Retirer orphelins (WITHDRAWN + clear bib)</button>';
html += '<p class="small text-muted mt-2 mb-0">Action : statut WITHDRAWN + dossard vidé sur ChronoTrack '
+ '(hors_eligible / doublons). Les entries sans external ID sont listées mais non retirées automatiquement.</p>';
html += '</div>';
$wrap.removeClass('d-none').html(html);
setReconcileStatus(
'Alignement — ' + intCount + ' orphelin(s)',
(res.message || '') + ' Détail sous les boutons.',
'todo'
);
}
function loadReconcile() {
var $btn = $('#ct_api_btn_reconcile');
var $wrap = $('#ct_api_reconcile_wrap');
$btn.prop('disabled', true);
$wrap.removeClass('d-none').html(
'<span class="text-muted"><i class="fa fa-spinner fa-spin" aria-hidden="true"></i> '
+ 'Analyse alignement ChronoTrack (lecture entries CT)…</span>'
);
setReconcileStatus(
'Alignement en cours…',
'Lecture des entries ChronoTrack — ne fermez pas la page.',
'loading'
);
$.ajax({
url: strAjaxUrl,
type: 'POST',
dataType: 'json',
timeout: 240000,
data: { a: 'sync_orphans', eve_id: intEveId }
}).done(function (res) {
if (rejectIfSessionLost(res)) {
return;
}
renderReconcilePanel(res);
}).fail(function (jqXHR, strStatus) {
var intHttp = (jqXHR && jqXHR.status) ? parseInt(jqXHR.status, 10) : 0;
var strMsg;
if (strStatus === 'timeout') {
strMsg = 'Délai dépassé (lecture entries CT trop longue). Réessayez.';
} else if (intHttp === 503 || intHttp === 502) {
strMsg = 'HTTP ' + intHttp + ' serveur MS1 pendant lalignement — réessayez.';
} else {
strMsg = 'Erreur réseau (alignement' + (intHttp ? ', HTTP ' + intHttp : '') + ').';
}
renderReconcilePanel({ state: 'error', message: strMsg });
}).always(function () {
$btn.prop('disabled', false);
});
}
$('#ct_api_btn_reconcile').on('click', function () {
var $wrap = $('#ct_api_reconcile_wrap');
if ($wrap.is(':visible') && !$wrap.hasClass('d-none') && $wrap.find('table').length) {
$wrap.addClass('d-none').empty();
return;
}
loadReconcile();
});
$(document).on('click', '#ct_api_btn_reconcile_close', function () {
$('#ct_api_reconcile_wrap').addClass('d-none').empty();
});
$(document).on('click', '#ct_api_btn_reconcile_withdraw', function () {
if (!window.confirm(
'Retirer les orphelins ChronoTrack ?\n\n'
+ 'Statut WITHDRAWN + dossard vidé pour les entries hors éligibles MS1 / doublons.\n'
+ 'Les dossards MS1 des annulés sont déjà libérés à lannulation.'
)) {
return;
}
var $btn = $(this);
$btn.prop('disabled', true).text('Retrait en cours…');
setReconcileStatus('Retrait orphelins…', 'WITHDRAWN + clear bib sur ChronoTrack.', 'loading');
$.ajax({
url: strAjaxUrl,
type: 'POST',
dataType: 'json',
timeout: 300000,
data: { a: 'sync_reconcile_withdraw', eve_id: intEveId }
}).done(function (res) {
if (rejectIfSessionLost(res)) {
return;
}
window.alert((res && res.message) ? res.message : 'Terminé');
loadReconcile();
loadPushPreview(true);
}).fail(function () {
window.alert('Erreur réseau (retrait orphelins).');
setReconcileStatus('Retrait interrompu', 'Erreur réseau.', 'error');
}).always(function () {
$btn.prop('disabled', false);
});
});
// MSIN-4328 — appelle auto_chronotrack_sync.php (même URL / même base que le CrowdJob)
$('#ct_api_btn_cron_ping').on('click', function () {
var $btn = $(this);
var $res = $('#ct_api_diagnostic_out');
if (!$res.length) {
$res = $('#ct_api_push_result');
}
$btn.prop('disabled', true);
$res.html('<span class="text-muted">Appel auto_chronotrack_sync.php…</span>');
$.ajax({
url: '/auto_chronotrack_sync.php',
method: 'GET',
dataType: 'text',
timeout: 120000
}).done(function (txt) {
$res.html('<div class="alert alert-info small mb-0"><strong>Réponse script :</strong><br><pre class="mb-0" style="white-space:pre-wrap;">'
+ $('<div/>').text(txt || '(vide)').html() + '</pre></div>');
loadAutoDiag();
}).fail(function (xhr) {
$res.html('<div class="alert alert-danger small mb-0">Script injoignable sur ce domaine (HTTP '
+ (xhr.status || '?') + '). Fichier pas déployé ici ?</div>');
}).always(function () {
$btn.prop('disabled', false);
});
});
function loadAutoDiag() {
var $box = $('#ct_api_diagnostic_out');
var $boxCfg = $('#ct_api_auto_diag');
if (!$box.length) {
$box = $boxCfg;
}
if (!$box.length) {
return;
}
$box.html(waitHtml('Lecture état sync auto…'));
if ($boxCfg.length && !$boxCfg.is($box)) {
$boxCfg.html(waitHtml('Lecture état sync auto…'));
}
$.post(strAjaxUrl, { a: 'sync_auto_diag', eve_id: intEveId }, function (res) {
if (rejectIfSessionLost(res)) {
return;
}
if (!res || res.state !== 'ok' || !res.diag) {
var strErr = '<span class="text-danger">'
+ escHtml((res && res.message) ? res.message : 'Diag impossible') + '</span>';
$box.html(strErr);
if ($boxCfg.length && !$boxCfg.is($box)) {
$boxCfg.html(strErr);
}
return;
}
var d = res.diag;
var html = '<div class="border rounded p-2 bg-light">';
html += '<div><strong>Verdict :</strong> ' + escHtml(d.would || '') + '</div>';
html += '<div>auto=' + (d.auto_enabled ? 'ON' : 'OFF')
+ ' · freq=' + escHtml(String(d.interval_min)) + ' min'
+ ' · fin=' + escHtml(d.until || '—')
+ ' · dernier passage=' + escHtml(d.last_auto_run_at || 'jamais') + '</div>';
html += '<div>logs en base pour cet event : <strong>' + escHtml(String(d.log_count)) + '</strong>';
if (d.last_log) {
html += ' · dernier : [' + escHtml(d.last_log.created_at || '') + '] '
+ escHtml(d.last_log.action || '') + ' — ' + escHtml(d.last_log.message || '');
}
html += '</div>';
if (d.cols_note) {
html += '<div class="text-danger">' + escHtml(d.cols_note) + '</div>';
}
html += '<div class="text-muted">host=' + escHtml(d.host || '') + '</div>';
html += '</div>';
$box.html(html);
if ($boxCfg.length && !$boxCfg.is($box)) {
$boxCfg.html(html);
}
if (d.last_auto_run_at) {
$('#ct_api_auto_last').html('<strong>Dernier passage auto :</strong> '
+ escHtml(d.last_auto_run_at));
}
}, 'json').fail(function () {
var strNet = '<span class="text-danger">Erreur réseau (diag).</span>';
$box.html(strNet);
if ($boxCfg.length && !$boxCfg.is($box)) {
$boxCfg.html(strNet);
}
});
}
$('#ct_api_btn_auto_diag').on('click', loadAutoDiag);
$('#ct_api_btn_ct_count').on('click', function () {
loadCtLiveCount();
});
$(document).on('click', '#ct_api_btn_ct_count_inline', function (e) {
e.preventDefault();
loadCtLiveCount();
});
$('#ct_api_btn_mark_current').on('click', function () {
var intEligible = (objPushPreview && objPushPreview.transferable)
? parseInt(objPushPreview.transferable, 10) : 0;
var $btn = $(this);
$btn.prop('disabled', true);
$.post(strAjaxUrl, {
a: 'sync_mark_current',
eve_id: intEveId,
ok_count: intEligible || (intCtLiveCount || 0)
}, function (res) {
if (rejectIfSessionLost(res)) {
return;
}
if (!res || res.state !== 'ok') {
window.alert((res && res.message) ? res.message : 'Échec');
return;
}
loadPushPreview(true);
}, 'json').fail(function () {
window.alert('Erreur réseau');
}).always(function () {
$btn.prop('disabled', false);
});
});
// MSIN-4328 — envoi manuel (différentiel) ou resync complète (force_full)
function runManualPush(blnForceFull) {
var intPending = 0;
if (objPushPreview) {
if (blnForceFull) {
intPending = objPushPreview.transferable || 0;
} else if (typeof objPushPreview.pending_push !== 'undefined') {
intPending = objPushPreview.pending_push;
} else {
intPending = objPushPreview.to_push || objPushPreview.transferable || 0;
}
}
if (blnForceFull) {
if (!window.confirm('Tout renvoyer vers ChronoTrack ?\n\nTous les participants éligibles seront renvoyés, sans tenir compte des dates du dernier envoi.')) {
return;
}
}
var $res = $('#ct_api_push_result');
var $status = $('#ct_api_push_status');
var intChunk = 100;
var intOffset = 0;
var intTotal = intPending;
var intOkSum = 0;
var intErrSum = 0;
var intPostSum = 0;
var intPutSum = 0;
var tabErrAll = [];
var blnFatal = false;
var intPushStartedAt = Date.now();
var intElapsedTimer = null;
var blnScrolledOnce = false;
blnPushRunning = true;
if (xhrPushPreview && xhrPushPreview.readyState !== 4) {
xhrPushPreview.abort();
}
$('#ct_api_btn_push, #ct_api_btn_push_full, #ct_api_btn_push_refresh, #ct_api_btn_ct_count, #ct_api_btn_logs, #ct_api_btn_reconcile')
.prop('disabled', true);
function formatElapsed() {
var intSec = Math.max(0, Math.floor((Date.now() - intPushStartedAt) / 1000));
var intMin = Math.floor(intSec / 60);
var intRem = intSec % 60;
return (intMin > 0 ? (intMin + ' min ') : '') + intRem + ' s';
}
function stopElapsedTimer() {
if (intElapsedTimer) {
window.clearInterval(intElapsedTimer);
intElapsedTimer = null;
}
}
// MSIN-4574 — feedback visible en haut (#ct_api_push_status), pas seulement sous les boutons
function renderProgress(intDone, intTot, strExtra) {
var intPct = (intTot > 0) ? Math.min(100, Math.round((intDone * 100) / intTot)) : 0;
var strMode = blnForceFull ? 'Tout renvoyer' : 'Envoi';
var strTitle = 'Transfert en cours — ' + strMode;
var strHint = intDone + ' / ' + intTot
+ (strExtra ? (' · ' + strExtra) : '')
+ ' · écoulé ' + formatElapsed();
if (intOkSum || intErrSum) {
strHint += ' · OK ' + intOkSum + ' · err ' + intErrSum
+ ' · POST ' + intPostSum + ' · PUT ' + intPutSum;
}
var strBusy = '<div class="ct-api-push-status__busy">'
+ '<div class="ct-api-push-status__busy-text">'
+ '<p class="ct-api-push-status__title mb-1 text-primary">'
+ '<i class="fa fa-cloud-upload" aria-hidden="true"></i> '
+ escHtml(strTitle) + '</p>'
+ '<p class="ct-api-push-status__hint mb-2">' + escHtml(strHint) + '</p>'
+ '<div class="progress mb-0" style="height:1.25rem;">'
+ '<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>'
+ '</div>'
+ '</div></div>';
if ($status.length) {
$status.removeClass('ct-api-push-status--ok ct-api-push-status--todo')
.addClass('ct-api-push-status--loading')
.html(strBusy);
}
$res.html(
'<div class="alert alert-info py-2 mb-0">'
+ '<strong>' + escHtml(strTitle) + '</strong><br>'
+ escHtml(strHint)
+ '</div>'
);
if (!blnScrolledOnce) {
blnScrolledOnce = true;
try {
if ($status.length && $status[0].scrollIntoView) {
$status[0].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
} catch (eScroll) { /* ignore */ }
}
}
function finishPush(strModeFinish) {
stopElapsedTimer();
blnPushRunning = false;
$('#ct_api_btn_push, #ct_api_btn_push_full, #ct_api_btn_push_refresh, #ct_api_btn_ct_count, #ct_api_btn_logs, #ct_api_btn_reconcile')
.prop('disabled', false);
var strMode = strModeFinish || (blnFatal ? 'interrupted' : ((intErrSum === 0) ? 'ok' : 'partial'));
var strFinal;
if (strMode === 'interrupted') {
strFinal = 'Interrompu après ' + intOkSum + ' OK'
+ (intErrSum ? (', ' + intErrSum + ' erreur(s)') : '')
+ ' (POST ' + intPostSum + '). Relancer Tout renvoyer pour reprendre le reste.';
} else if (intErrSum === 0) {
strFinal = intOkSum + ' participant(s) synchronisé(s) (POST ' + intPostSum
+ ' · dossards ' + intPutSum + ').';
if (blnForceFull) {
strFinal = 'Resync complète — ' + strFinal;
}
} else {
strFinal = intOkSum + ' OK, ' + intErrSum + ' erreur(s).';
}
strFinal += ' · durée ' + formatElapsed();
var strState = (strMode === 'interrupted') ? 'error'
: ((intErrSum === 0) ? 'ok' : 'partial');
renderPushResult({
state: strState,
message: strFinal,
errors: tabErrAll.slice(0, 20)
});
if ($status.length) {
var strCls = strState === 'ok' ? 'ct-api-push-status--ok'
: (strState === 'partial' ? 'ct-api-push-status--todo' : '');
var strColor = strState === 'ok' ? 'text-success'
: (strState === 'partial' ? 'text-warning' : 'text-danger');
var strHead = strState === 'ok' ? 'Transfert terminé'
: (strState === 'partial' ? 'Transfert terminé avec erreurs' : 'Transfert interrompu');
$status.removeClass('ct-api-push-status--ok ct-api-push-status--todo ct-api-push-status--loading')
.addClass(strCls)
.html(
'<p class="ct-api-push-status__title ' + strColor + ' mb-1">' + strHead + '</p>'
+ '<p class="ct-api-push-status__hint mb-0">' + escHtml(strFinal) + '</p>'
+ '<p id="ct_api_status_refresh_cd" class="small text-muted mb-0 mt-1"></p>'
);
}
loadPushPreview(true);
}
// MSIN-4574 — passe dossards découpée (après identité)
function pushBibChunk(intBibPendingHint, intBibChunk) {
intBibChunk = intBibChunk || 20;
var intLeft = (intBibPendingHint != null) ? parseInt(intBibPendingHint, 10) : 0;
renderProgress(
Math.min(intOffset, intTotal),
intTotal,
'passe dossards — reste ~' + intLeft + '…'
);
$.ajax({
url: strAjaxUrl,
type: 'POST',
dataType: 'json',
timeout: 180000,
data: {
a: 'sync_push_bibs',
eve_id: intEveId,
limit: intBibChunk
}
}).done(function (res) {
if (rejectIfSessionLost(res)) {
stopElapsedTimer();
return;
}
if (!res) {
blnFatal = true;
tabErrAll.push('Réponse vide (passe dossards)');
finishPush('interrupted');
return;
}
intOkSum += parseInt(res.pushed_ok || 0, 10);
intErrSum += parseInt(res.pushed_error || 0, 10);
intPutSum += parseInt(res.pushed_ok || 0, 10);
if (res.errors && res.errors.length) {
$.each(res.errors, function (_, msg) {
if (tabErrAll.length < 20) {
tabErrAll.push(msg);
}
});
}
var intPend = parseInt(res.bib_pending != null ? res.bib_pending : 0, 10);
if (res.done || intPend <= 0) {
finishPush(intErrSum === 0 ? 'ok' : 'partial');
return;
}
window.setTimeout(function () {
pushBibChunk(intPend, intBibChunk);
}, 50);
}).fail(function (jqXHR, strStatus) {
blnFatal = true;
var intHttp = (jqXHR && jqXHR.status) ? parseInt(jqXHR.status, 10) : 0;
var strDetail = (strStatus === 'timeout')
? 'Délai dépassé sur passe dossards — relancer Tout renvoyer.'
: ('Interruption passe dossards (' + strStatus + ', HTTP ' + intHttp + '). Relancer.');
tabErrAll.push(strDetail);
finishPush('interrupted');
});
}
function pushChunk() {
var intLot = Math.floor(intOffset / Math.max(1, intChunk)) + 1;
var intLots = Math.max(1, Math.ceil(intTotal / Math.max(1, intChunk)));
renderProgress(
intOffset,
intTotal,
'identité lot ' + intLot + '/' + intLots
+ ' (POST ' + Math.min(intChunk, Math.max(0, intTotal - intOffset)) + ')…'
);
$.ajax({
url: strAjaxUrl,
type: 'POST',
dataType: 'json',
timeout: 180000,
data: {
a: 'sync_push',
eve_id: intEveId,
offset: intOffset,
limit: intChunk,
refresh_preview: 0
}
}).done(function (res) {
if (rejectIfSessionLost(res)) {
stopElapsedTimer();
return;
}
if (!res) {
blnFatal = true;
tabErrAll.push('Réponse vide du serveur');
finishPush('interrupted');
return;
}
if (res.state !== 'ok' && res.state !== 'partial' && res.state !== 'error') {
blnFatal = true;
if (res.message) {
tabErrAll.push(res.message);
}
finishPush('interrupted');
return;
}
if (res.total === 0 && res.state === 'error') {
stopElapsedTimer();
renderPushResult(res);
$('#ct_api_btn_push, #ct_api_btn_push_full, #ct_api_btn_push_refresh, #ct_api_btn_ct_count, #ct_api_btn_logs, #ct_api_btn_reconcile')
.prop('disabled', false);
blnPushRunning = false;
loadPushPreview(true);
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.message && res.state === 'error' && tabErrAll.length < 20) {
tabErrAll.push(res.message);
}
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,
'lot identité terminé'
);
// Identité finie → passe dossards séparée (évite 503 hébergeur)
if (res.identity_done || res.phase === 'bibs') {
var intBibPend = parseInt(res.bib_pending != null ? res.bib_pending : 0, 10);
var intBibChunk = parseInt(res.bib_chunk_size != null ? res.bib_chunk_size : 20, 10);
if (intBibPend > 0) {
window.setTimeout(function () {
pushBibChunk(intBibPend, intBibChunk);
}, 50);
return;
}
finishPush(intErrSum === 0 ? 'ok' : 'partial');
return;
}
if (res.done || intOffset >= intTotal) {
finishPush(intErrSum === 0 ? 'ok' : 'partial');
return;
}
// Enchaîner tout de suite (pas de pause artificielle — CT + retry 503 suffisent)
pushChunk();
}).fail(function (jqXHR, strStatus) {
blnFatal = true;
var intHttp = (jqXHR && jqXHR.status) ? parseInt(jqXHR.status, 10) : 0;
var strDetail;
if (strStatus === 'timeout') {
strDetail = 'Délai dépassé sur un lot identité — relancer Tout renvoyer.';
} else if (intHttp === 503 || intHttp === 502) {
strDetail = 'HTTP ' + intHttp
+ ' sur le serveur MS1 (timeout / surcharge) pendant un lot — '
+ 'pas un refus métier CT. Relancer Tout renvoyer ; le modèle découpe maintenant identité + dossards.';
} else if (intHttp === 0) {
strDetail = 'Connexion coupée sur un lot. Relancer Tout renvoyer.';
} else {
strDetail = 'Erreur réseau sur un lot (' + strStatus + ', HTTP ' + intHttp + ').';
}
if (jqXHR && jqXHR.responseText) {
var strBody = String(jqXHR.responseText).replace(/\s+/g, ' ').substr(0, 180);
if (strBody && strBody.indexOf('{') !== 0) {
strDetail += ' — ' + strBody;
}
}
tabErrAll.push(strDetail);
finishPush('interrupted');
});
}
// Afficher immédiatement « commencé » (avant le prepare long)
renderProgress(0, intTotal > 0 ? intTotal : 1, 'démarré — préparation MS1 (liste + job)…');
intElapsedTimer = window.setInterval(function () {
if (!blnPushRunning) {
stopElapsedTimer();
return;
}
renderProgress(
intOffset,
intTotal > 0 ? intTotal : 1,
intOffset === 0
? 'préparation MS1 encore en cours…'
: 'lot en cours…'
);
}, 1000);
$.ajax({
url: strAjaxUrl,
type: 'POST',
dataType: 'json',
timeout: 180000,
data: {
a: 'sync_push_prepare',
eve_id: intEveId,
force_full: blnForceFull ? 1 : 0,
verbose_logs: $('#ct_api_verbose_logs').is(':checked') ? 1 : 0
}
}).done(function (prep) {
if (rejectIfSessionLost(prep)) {
stopElapsedTimer();
return;
}
if (!prep || prep.state !== 'ok') {
stopElapsedTimer();
renderPushResult(prep || { state: 'error', message: 'Préparation échouée' });
if ($status.length) {
$status.removeClass('ct-api-push-status--ok ct-api-push-status--todo ct-api-push-status--loading')
.html(
'<p class="ct-api-push-status__title text-danger mb-1">Préparation échouée</p>'
+ '<p class="ct-api-push-status__hint mb-0">'
+ escHtml((prep && prep.message) ? prep.message : 'Erreur')
+ '</p>'
);
}
$('#ct_api_btn_push_refresh, #ct_api_btn_ct_count, #ct_api_btn_logs, #ct_api_btn_reconcile')
.prop('disabled', false);
blnPushRunning = false;
loadPushPreview(true);
return;
}
intTotal = parseInt(prep.total, 10) || intTotal;
if (prep.chunk_size) {
intChunk = parseInt(prep.chunk_size, 10) || intChunk;
}
renderProgress(0, intTotal, blnForceFull ? 'resync — 1er lot…' : 'démarrage 1er lot…');
pushChunk();
}).fail(function (jqXHR, strStatus) {
stopElapsedTimer();
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 });
if ($status.length) {
$status.removeClass('ct-api-push-status--ok ct-api-push-status--todo ct-api-push-status--loading')
.html(
'<p class="ct-api-push-status__title text-danger mb-1">Préparation échouée</p>'
+ '<p class="ct-api-push-status__hint mb-0">' + escHtml(strMsg) + '</p>'
);
}
$('#ct_api_btn_push_refresh, #ct_api_btn_ct_count, #ct_api_btn_logs, #ct_api_btn_reconcile')
.prop('disabled', false);
blnPushRunning = false;
loadPushPreview(true);
});
}
$('#ct_api_btn_push').on('click', function () {
runManualPush(false);
});
$('#ct_api_btn_push_full').on('click', function () {
runManualPush(true);
});
}
})(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 MS1 liés à ChronoTrack. « Ouvrir » lance le tableau de bord en nouvelle fenêtre (plein écran, sans menu).</p>';
if ($arrRows === null || count($arrRows) === 0) {
echo '<div class="alert alert-light">Aucune course connectée. Liez un événement depuis sa fiche (panneau ChronoTrack).</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-primary btn-sm" href="chronotrack_api.php?eve_id=' . intval($arr['eve_id'])
. '&amp;focus=1" target="_blank" rel="noopener">Ouvrir</a></td>';
echo '</tr>';
}
echo '</tbody></table>';
}