1598 lines
74 KiB
PHP
1598 lines
74 KiB
PHP
<?php
|
||
/**
|
||
* MSIN API ChronoTrack — rendu admin superadm (Phase 1 : lien + mapping).
|
||
*/
|
||
|
||
function fxChronotrackApiAdminEsc($str) {
|
||
return htmlspecialchars((string)$str, ENT_QUOTES, 'UTF-8');
|
||
}
|
||
|
||
function fxChronotrackApiAdminRenderWaitHtml($strMessage, $vDomaine) {
|
||
$strSrc = fxChronotrackApiAdminEsc($vDomaine . '/images/ms1-runner-loader.gif?v=' . _VERSION_CODE);
|
||
$strMsg = fxChronotrackApiAdminEsc($strMessage);
|
||
return '<div class="ms1-loader ms1-loader--inline" role="status" aria-live="polite" aria-busy="true">'
|
||
. '<img class="ms1-loader__runner" src="' . $strSrc . '" alt="" role="presentation" decoding="async">'
|
||
. '<span class="ms1-loader__label">' . $strMsg . '</span>'
|
||
. '</div>';
|
||
}
|
||
|
||
function fxChronotrackApiAdminRenderLoaderAssets($vDomaine) {
|
||
echo '<link rel="stylesheet" href="' . fxChronotrackApiAdminEsc($vDomaine) . '/css/ms1-loader.css?v=' . _VERSION_CODE . '">';
|
||
echo '<script src="' . fxChronotrackApiAdminEsc($vDomaine) . '/js/ms1-loader.js?v=' . _VERSION_CODE . '"></script>';
|
||
}
|
||
|
||
function fxChronotrackApiAdminRenderEventLinkPicker($blnClosed) {
|
||
echo '<div class="card mb-4" id="ct_api_event_link_card"><div class="card-header">1. Lier un événement ChronoTrack</div><div class="card-body">';
|
||
if ($blnClosed) {
|
||
echo '<p class="text-muted mb-0">Lecture seule.</p>';
|
||
} else {
|
||
echo '<div class="form-row align-items-end mb-3">';
|
||
echo '<div class="col-md-5 mb-2"><label for="ct_api_search">Recherche</label>';
|
||
echo '<input type="text" class="form-control form-control-sm" id="ct_api_search" placeholder="Nom ou ID ChronoTrack"></div>';
|
||
echo '<div class="col-md-4 mb-2"><div class="form-check mt-4">';
|
||
echo '<input type="checkbox" class="form-check-input" id="ct_api_show_past" value="1">';
|
||
echo '<label class="form-check-label" for="ct_api_show_past">Afficher les événements passés</label></div></div>';
|
||
echo '<div class="col-md-3 mb-2"><button type="button" class="btn btn-primary btn-sm btn-block" id="ct_api_btn_load">Charger la liste</button></div>';
|
||
echo '</div>';
|
||
echo '<div id="ct_api_events_wrap" class="border rounded p-2" style="max-height:320px;overflow:auto;">';
|
||
echo '<p class="text-muted small mb-0">Cliquez sur « Charger la liste » pour voir les événements à venir (tri par date).</p>';
|
||
echo '</div>';
|
||
echo '<button type="button" class="btn btn-success btn-sm mt-3" id="ct_api_btn_save_link" disabled>Enregistrer le lien</button>';
|
||
}
|
||
echo '</div></div>';
|
||
}
|
||
|
||
function fxChronotrackApiAdminRenderEventPage($intEveId, $strLangue = 'fr', $blnInlineOnEventForm = false, $blnInsideV2Panel = false) {
|
||
global $vDomaine;
|
||
|
||
$arrEvent = fxChronotrackApiMs1EventRow($intEveId, $strLangue);
|
||
if ($arrEvent === null) {
|
||
echo '<div class="alert alert-danger">Événement MS1 introuvable.</div>';
|
||
return;
|
||
}
|
||
|
||
$arrConfig = fxChronotrackApiConfigGet($intEveId);
|
||
$blnConfigured = fxChronotrackApiSettingsConfigured();
|
||
$strStatus = $arrConfig['sync_status'] ?? '';
|
||
$blnClosed = ($strStatus === MSIN_API_CHRONOTRACK_SYNC_FERME);
|
||
|
||
$strWrapperId = '';
|
||
echo '<div class="ct-api-admin mt-2 mb-3"' . $strWrapperId . '>';
|
||
if (!$blnInsideV2Panel) {
|
||
if ($blnInlineOnEventForm) {
|
||
echo '<h2 class="h5">MSIN API ChronoTrack</h2>';
|
||
} else {
|
||
echo '<h1 class="h3">MSIN API ChronoTrack</h1>';
|
||
}
|
||
}
|
||
echo '<p class="text-muted">Configuration du lien pour l\'événement MS1 <strong>' . fxChronotrackApiAdminEsc($arrEvent['eve_nom']) . '</strong> (eve_id '
|
||
. intval($arrEvent['eve_id']) . ').</p>';
|
||
if ($blnConfigured) {
|
||
$strApiBase = fxChronotrackApiGetBaseUrl();
|
||
$blnIsTestApi = (strpos($strApiBase, 'api-test') !== false);
|
||
echo '<p class="small text-muted mb-2">Endpoint API : <code>' . fxChronotrackApiAdminEsc($strApiBase) . '</code>';
|
||
if ($blnIsTestApi) {
|
||
echo ' <span class="badge badge-warning">QA</span>';
|
||
} else {
|
||
echo ' <span class="badge badge-secondary">prod</span>';
|
||
}
|
||
echo '</p>';
|
||
}
|
||
|
||
if (!$blnConfigured) {
|
||
echo '<div class="alert alert-warning">' . fxChronotrackApiAdminEsc(fxChronotrackApiSettingsErrorMessage()) . '</div>';
|
||
}
|
||
|
||
if ($arrConfig !== null) {
|
||
echo '<div class="alert alert-info">';
|
||
echo 'Statut : <strong>' . fxChronotrackApiAdminEsc(fxChronotrackApiConfigStatusLabel($strStatus)) . '</strong>';
|
||
echo ' — ChronoTrack : <strong>' . fxChronotrackApiAdminEsc($arrConfig['ct_event_name']) . '</strong>';
|
||
echo ' (ID ' . intval($arrConfig['ct_event_id']) . ')';
|
||
echo '</div>';
|
||
}
|
||
|
||
if ($blnClosed) {
|
||
echo '<div class="alert alert-secondary">Statut <strong>fermé</strong> : plus de sync auto ni modification du lien/mapping. ';
|
||
echo 'Le <strong>push manuel</strong> reste disponible ci-dessous.</div>';
|
||
}
|
||
|
||
$blnEventLinked = ($arrConfig !== null && intval($arrConfig['ct_event_id'] ?? 0) > 0);
|
||
if (!$blnEventLinked) {
|
||
fxChronotrackApiAdminRenderEventLinkPicker($blnClosed);
|
||
}
|
||
|
||
// Sync d’abord (usage courant) ; config repliable ensuite. MSIN-4328
|
||
if ($arrConfig !== null && $blnEventLinked && $blnConfigured) {
|
||
fxChronotrackApiAdminRenderPushPanel($intEveId, $blnClosed, $arrConfig);
|
||
}
|
||
|
||
if ($arrConfig !== null && $blnEventLinked) {
|
||
$blnHasSavedMapping = false;
|
||
$arrMapRows = fxChronotrackApiRaceMapGetForEvent($intEveId);
|
||
foreach ($arrMapRows as $arrMapRow) {
|
||
if (intval($arrMapRow['ct_race_id'] ?? 0) > 0) {
|
||
$blnHasSavedMapping = true;
|
||
break;
|
||
}
|
||
}
|
||
$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 <span class="text-muted small">(cycle de vie, mapping épreuves)</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);
|
||
echo '</div></div></div>';
|
||
} elseif ($arrConfig !== null) {
|
||
fxChronotrackApiAdminRenderStatusActions($intEveId, $strStatus, $blnEventLinked);
|
||
fxChronotrackApiAdminRenderRaceMapping($intEveId, $arrConfig, $strLangue, $blnClosed, $blnEventLinked);
|
||
}
|
||
|
||
if ($blnEventLinked && $blnConfigured) {
|
||
fxChronotrackApiAdminRenderDiagnosticPanel(intval($arrConfig['ct_event_id']));
|
||
}
|
||
|
||
if (!$blnInlineOnEventForm) {
|
||
echo '<p class="mt-3"><a class="btn btn-outline-secondary btn-sm" href="index.php?t='
|
||
. urlencode(base64_encode(4)) . '&a=mod&id=' . intval($intEveId) . '">← Retour fiche événement</a></p>';
|
||
}
|
||
echo '</div>';
|
||
|
||
fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed, $vDomaine, $blnInlineOnEventForm);
|
||
}
|
||
|
||
function fxChronotrackApiAdminRenderStatusActions($intEveId, $strStatus, $blnEventLinked = true) {
|
||
echo '<div class="card mb-3 border"><div class="card-header py-2">Cycle de vie sync</div><div class="card-body py-3">';
|
||
echo '<p class="small text-muted mb-2">Activer avant la course ; <strong>Terminer</strong> après pour bloquer la sync auto '
|
||
. 'et les changements de config. (La sync auto différentielle viendra ensuite.)</p>';
|
||
echo '<div class="btn-group" role="group">';
|
||
if ($strStatus === MSIN_API_CHRONOTRACK_SYNC_LIE) {
|
||
echo '<button type="button" class="btn btn-warning btn-sm ct-api-set-status" data-status="' . MSIN_API_CHRONOTRACK_SYNC_ACTIF . '">Activer la sync</button>';
|
||
}
|
||
if ($strStatus === MSIN_API_CHRONOTRACK_SYNC_ACTIF) {
|
||
echo '<span class="badge badge-success align-self-center mr-2">Sync active</span>';
|
||
echo '<button type="button" class="btn btn-danger btn-sm ct-api-set-status" data-status="' . MSIN_API_CHRONOTRACK_SYNC_FERME . '">Terminer / Déconnecter</button>';
|
||
}
|
||
if ($strStatus === MSIN_API_CHRONOTRACK_SYNC_FERME) {
|
||
echo '<span class="badge badge-secondary align-self-center mr-2">Fermé</span>';
|
||
echo '<button type="button" class="btn btn-outline-primary btn-sm ct-api-set-status" data-status="' . MSIN_API_CHRONOTRACK_SYNC_LIE . '">Rouvrir (Lié)</button>';
|
||
}
|
||
echo '</div></div></div>';
|
||
}
|
||
|
||
function fxChronotrackApiAdminRenderRaceMapping($intEveId, $arrConfig, $strLangue, $blnClosed, $blnEventLinked = true) {
|
||
$arrEpreuves = fxChronotrackApiMs1EpreuvesForEvent($intEveId, $strLangue);
|
||
$arrMap = fxChronotrackApiRaceMapGetForEvent($intEveId);
|
||
|
||
$blnHasSavedMapping = false;
|
||
if ($arrEpreuves !== null) {
|
||
for ($i = 1; $i <= count($arrEpreuves); $i++) {
|
||
$intEprId = intval($arrEpreuves[$i]['epr_id']);
|
||
if (intval($arrMap[$intEprId]['ct_race_id'] ?? 0) > 0) {
|
||
$blnHasSavedMapping = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
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> Modifier le mapping</button>';
|
||
}
|
||
echo '</div>';
|
||
}
|
||
|
||
$strEditStyle = $blnHasSavedMapping ? ' style="display:none;"' : '';
|
||
echo '<div id="ct_api_race_edit_wrap"' . $strEditStyle . '>';
|
||
echo '<p class="small text-muted">ChronoTrack event ID : ' . intval($arrConfig['ct_event_id'])
|
||
. ' — courses CT Live (<code>race_name</code> via <code>GET event/{id}/race</code>).</p>';
|
||
echo '<div id="ct_api_races_status" class="small mb-2"></div>';
|
||
if (!$blnClosed) {
|
||
echo '<button type="button" class="btn btn-outline-secondary btn-sm mb-2" id="ct_api_btn_reload_races">'
|
||
. '<i class="fa fa-refresh" aria-hidden="true"></i> Charger les courses ChronoTrack</button>';
|
||
}
|
||
echo '<table class="table table-sm table-bordered"><thead><tr><th>Épreuve MS1</th><th>Race ChronoTrack</th></tr></thead><tbody>';
|
||
|
||
for ($i = 1; $i <= count($arrEpreuves); $i++) {
|
||
$intEprId = intval($arrEpreuves[$i]['epr_id']);
|
||
$strLabel = trim($arrEpreuves[$i]['epr_type'] . ' — ' . $arrEpreuves[$i]['epr_nom']);
|
||
|
||
echo '<tr><td>' . fxChronotrackApiAdminEsc($strLabel) . ' <span class="text-muted">(epr_id '
|
||
. $intEprId . ')</span></td><td>';
|
||
echo '<select class="form-control form-control-sm ct-api-race-select" data-epr-id="' . $intEprId . '"'
|
||
. ($blnClosed ? ' disabled' : '') . '>';
|
||
echo '<option value="">— Non mappé —</option>';
|
||
echo '</select></td></tr>';
|
||
}
|
||
|
||
echo '</tbody></table>';
|
||
if (!$blnClosed) {
|
||
echo '<button type="button" class="btn btn-primary btn-sm" id="ct_api_btn_save_map">Enregistrer le mapping</button>';
|
||
if ($blnHasSavedMapping) {
|
||
echo ' <button type="button" class="btn btn-link btn-sm" id="ct_api_btn_cancel_edit_map">Annuler</button>';
|
||
}
|
||
}
|
||
echo '</div></div></div>';
|
||
}
|
||
|
||
function fxChronotrackApiAdminRenderPushPanel($intEveId, $blnClosed = false, $arrConfig = null) {
|
||
echo '<div class="card mb-4 border-primary" id="ct_api_push_card">';
|
||
echo '<div class="card-header">Synchronisation ChronoTrack</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>';
|
||
}
|
||
|
||
// MSIN-4328 — Phase A : config sync auto (exécutée plus tard par cron)
|
||
$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="ct-api-auto-wrap mb-3' . ($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">Mise à jour 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> (comme liste d’attente). 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 n’a 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>';
|
||
|
||
// Zone employé : statut clair + actions
|
||
echo '<div id="ct_api_push_status" class="ct-api-push-status mb-3">';
|
||
echo '<div class="text-muted small">Cliquez <strong>Actualiser</strong> pour vérifier s’il y a des changements à envoyer.</div>';
|
||
echo '</div>';
|
||
|
||
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> 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> Envoyer vers ChronoTrack</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> Voir les logs</button> ';
|
||
echo '<button type="button" class="btn btn-outline-secondary btn-sm" id="ct_api_btn_auto_diag">'
|
||
. 'État sync auto</button> ';
|
||
echo '<button type="button" class="btn btn-outline-secondary btn-sm" id="ct_api_btn_cron_ping" title="Appelle auto_chronotrack_sync.php sur ce même domaine">'
|
||
. 'Lancer le script cron</button>';
|
||
echo '</div>';
|
||
echo '<p class="small text-muted mb-2">Envoi manuel : nouveaux / modifiés depuis le dernier push. '
|
||
. 'Page : refresh statut MS1 chaque minute.</p>';
|
||
echo '<div id="ct_api_push_result" class="small mb-3"></div>';
|
||
echo '<div id="ct_api_logs_wrap" class="mb-3 d-none"></div>';
|
||
|
||
// Détails / debug : sous les boutons, repliés par défaut
|
||
echo '<div class="card border-light mb-0" id="ct_api_push_details_card">';
|
||
echo '<div class="card-header py-2 bg-white">';
|
||
echo '<a class="d-block text-muted small collapsed" data-toggle="collapse" href="#ct_api_push_details_collapse" '
|
||
. 'role="button" aria-expanded="false">';
|
||
echo 'Détails & exclus <span class="text-muted">(compteurs, liste à corriger)</span>';
|
||
echo '</a></div>';
|
||
echo '<div class="collapse" id="ct_api_push_details_collapse"><div class="card-body py-2">';
|
||
echo '<div id="ct_api_push_summary" class="mb-2 text-muted small"></div>';
|
||
echo '<div id="ct_api_push_blocked_wrap" class="mb-2 d-none"></div>';
|
||
echo '<button type="button" class="btn btn-outline-info btn-sm" id="ct_api_btn_ct_count">';
|
||
echo '<i class="fa fa-calculator" aria-hidden="true"></i> Nb ChronoTrack Live</button> ';
|
||
echo '<button type="button" class="btn btn-outline-secondary btn-sm" 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 '<p class="small text-muted mt-2 mb-0">Info technique : external ID = <code>par_id_original</code>. '
|
||
. 'Le comptage Live sert aussi à lier les entry_id pour le bouton fiche.</p>';
|
||
echo '</div></div></div>';
|
||
|
||
echo '</div></div>';
|
||
}
|
||
|
||
function fxChronotrackApiAdminRenderDiagnosticPanel($intCtEventId) {
|
||
echo '<div class="card mb-4 border-secondary"><div class="card-header py-2">';
|
||
echo '<a class="d-block text-dark collapsed" data-toggle="collapse" href="#ct_api_diagnostic_collapse" role="button" aria-expanded="false">';
|
||
echo 'Diagnostic API ChronoTrack <span class="text-muted small">(support — event ID ' . intval($intCtEventId) . ')</span>';
|
||
echo '</a></div>';
|
||
echo '<div class="collapse" id="ct_api_diagnostic_collapse"><div class="card-body">';
|
||
echo '<p class="small text-muted mb-2">Outils de support (credentials MS1 / OAuth en cache). '
|
||
. 'Pas nécessaire pour une course en production.</p>';
|
||
echo '<button type="button" class="btn btn-outline-dark btn-sm mb-2" id="ct_api_btn_diagnostic">';
|
||
echo '<i class="fa fa-stethoscope" aria-hidden="true"></i> Lancer le diagnostic API</button> ';
|
||
echo '<button type="button" class="btn btn-outline-secondary btn-sm mb-2" id="ct_api_btn_probe_push">';
|
||
echo '<i class="fa fa-flask" aria-hidden="true"></i> Sonde (1 participant POST)</button> ';
|
||
echo '<button type="button" class="btn btn-outline-secondary btn-sm mb-2 d-none" id="ct_api_btn_diagnostic_copy">';
|
||
echo 'Copier le rapport</button>';
|
||
echo '<div id="ct_api_diagnostic_out" class="small text-muted">Cliquez sur « Lancer le diagnostic API » ou « Sonde ».</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_push_summary .ct-api-dash-table { font-size: 0.85rem; margin-bottom: 0.5rem; }
|
||
#ct_api_push_summary .ct-api-dash-table th { background: #f8f9fa; font-weight: 600; white-space: nowrap; }
|
||
#ct_api_push_summary .ct-api-dash-table td.num { text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; }
|
||
#ct_api_push_summary .ct-api-dash-push { font-size: 0.85rem; margin-bottom: 0; }
|
||
#ct_api_config_collapse .card { box-shadow: none; }
|
||
.ct-api-push-status {
|
||
padding: 12px 14px;
|
||
border-radius: 8px;
|
||
background: #f8f9fa;
|
||
border: 1px solid #e9ecef;
|
||
}
|
||
.ct-api-push-status--ok {
|
||
background: #e8f7ee;
|
||
border-color: #b7e4c7;
|
||
}
|
||
.ct-api-push-status--todo {
|
||
background: #fff8e6;
|
||
border-color: #ffe1a6;
|
||
}
|
||
.ct-api-push-status__title {
|
||
font-size: 1.15rem;
|
||
font-weight: 700;
|
||
margin: 0 0 4px;
|
||
line-height: 1.3;
|
||
}
|
||
.ct-api-push-status__hint {
|
||
margin: 0;
|
||
font-size: 0.9rem;
|
||
}
|
||
.ct-api-push-actions .btn-lg { font-size: 1.05rem; padding: 0.55rem 1.1rem; }
|
||
#ct_api_push_details_card .card-header a { text-decoration: none; }
|
||
.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_logs_wrap .ct-api-logs-scroll { max-height: 280px; overflow: 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;
|
||
|
||
function escHtml(str) {
|
||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||
}
|
||
|
||
if (window.Ms1Loader) {
|
||
Ms1Loader.init({
|
||
src: <?php echo json_encode($vDomaine . '/images/ms1-runner-loader.gif?v=' . _VERSION_CODE); ?>
|
||
});
|
||
}
|
||
|
||
function waitHtml(strMessage) {
|
||
if (window.Ms1Loader) {
|
||
return Ms1Loader.inlineHtml(strMessage);
|
||
}
|
||
return '<span class="text-muted">' + escHtml(strMessage || 'Chargement…') + '</span>';
|
||
}
|
||
|
||
function showMsg(strType, strText) {
|
||
window.alert(strText);
|
||
}
|
||
|
||
function rejectIfSessionLost(res) {
|
||
if (!res || typeof res !== 'object') {
|
||
return false;
|
||
}
|
||
if (res.code === 'session' || res.message === 'session') {
|
||
if (typeof window.fxSuperadmSessionExpiredRedirect === 'function') {
|
||
window.fxSuperadmSessionExpiredRedirect();
|
||
} else {
|
||
window.location.href = <?php echo json_encode($vDomaine . '/superadm/login.php?expired=1'); ?>;
|
||
}
|
||
return true;
|
||
}
|
||
var strMsg = String(res.message || '');
|
||
if (strMsg.indexOf('Session expir') === 0) {
|
||
if (typeof window.fxSuperadmSessionExpiredRedirect === 'function') {
|
||
window.fxSuperadmSessionExpiredRedirect();
|
||
} else {
|
||
window.location.href = <?php echo json_encode($vDomaine . '/superadm/login.php?expired=1'); ?>;
|
||
}
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function renderEvents(arrEvents) {
|
||
var $wrap = $('#ct_api_events_wrap');
|
||
$wrap.empty();
|
||
if (!arrEvents || !arrEvents.length) {
|
||
$wrap.append('<p class="text-muted small mb-0">Aucun événement trouvé (essayez « événements passés » ou autre recherche).</p>');
|
||
return;
|
||
}
|
||
var $list = $('<div class="list-group list-group-flush"></div>');
|
||
$.each(arrEvents, function (_, ev) {
|
||
var strId = String(ev.ct_event_id || '');
|
||
var $item = $('<label class="list-group-item list-group-item-action py-2 mb-0"></label>');
|
||
var $radio = $('<input type="radio" name="ct_api_event_pick" class="mr-2">').val(strId);
|
||
if (strSelectedCtEventId === strId) {
|
||
$radio.prop('checked', true);
|
||
}
|
||
$item.append($radio);
|
||
$item.append(document.createTextNode((ev.start_label ? ev.start_label + ' — ' : '') + (ev.name || '') + ' (ID ' + strId + ')'));
|
||
if (ev.is_past) {
|
||
$item.append(' <span class="badge badge-secondary ml-1">passé</span>');
|
||
}
|
||
$list.append($item);
|
||
});
|
||
$wrap.append($list);
|
||
$('#ct_api_btn_save_link').prop('disabled', false);
|
||
}
|
||
|
||
$('#ct_api_btn_load').on('click', function () {
|
||
var $wrap = $('#ct_api_events_wrap');
|
||
$wrap.html(waitHtml('Chargement de la liste ChronoTrack…'));
|
||
$.post(strAjaxUrl, {
|
||
a: 'list_events',
|
||
eve_id: intEveId,
|
||
search: $('#ct_api_search').val(),
|
||
show_past: $('#ct_api_show_past').is(':checked') ? 1 : 0
|
||
}, function (res) {
|
||
if (rejectIfSessionLost(res)) {
|
||
return;
|
||
}
|
||
if (!res || res.state !== 'ok') {
|
||
showMsg('error', (res && res.message) ? res.message : 'Erreur chargement');
|
||
return;
|
||
}
|
||
renderEvents(res.events || []);
|
||
}, 'json').fail(function () {
|
||
showMsg('error', 'Erreur réseau');
|
||
});
|
||
});
|
||
|
||
$(document).on('change', 'input[name="ct_api_event_pick"]', function () {
|
||
strSelectedCtEventId = $(this).val();
|
||
});
|
||
|
||
$('#ct_api_btn_save_link').on('click', function () {
|
||
if (!strSelectedCtEventId) {
|
||
showMsg('error', 'Sélectionnez un événement ChronoTrack');
|
||
return;
|
||
}
|
||
var $btn = $(this);
|
||
$btn.prop('disabled', true);
|
||
$.post(strAjaxUrl, {
|
||
a: 'save_link',
|
||
eve_id: intEveId,
|
||
ct_event_id: strSelectedCtEventId
|
||
}, function (res) {
|
||
if (rejectIfSessionLost(res)) {
|
||
return;
|
||
}
|
||
if (!res || res.state !== 'ok') {
|
||
showMsg('error', (res && res.message) ? res.message : 'Erreur enregistrement');
|
||
$btn.prop('disabled', false);
|
||
return;
|
||
}
|
||
window.location.reload();
|
||
}, 'json').fail(function () {
|
||
showMsg('error', 'Erreur réseau');
|
||
$btn.prop('disabled', false);
|
||
});
|
||
});
|
||
|
||
$('.ct-api-set-status').on('click', function () {
|
||
var strStatus = $(this).data('status');
|
||
var strConfirm;
|
||
if (strStatus === 'ferme') {
|
||
strConfirm = 'Terminer la course ? La sync auto et les changements de config seront bloqués (push manuel toujours possible).';
|
||
} else if (strStatus === 'lie') {
|
||
strConfirm = 'Rouvrir cet événement ChronoTrack (statut Lié) ?';
|
||
} else {
|
||
strConfirm = 'Activer la synchronisation automatique pour cet événement ?';
|
||
}
|
||
if (!window.confirm(strConfirm)) {
|
||
return;
|
||
}
|
||
$.post(strAjaxUrl, { a: 'set_status', eve_id: intEveId, sync_status: strStatus }, function (res) {
|
||
if (rejectIfSessionLost(res)) {
|
||
return;
|
||
}
|
||
if (!res || res.state !== 'ok') {
|
||
showMsg('error', (res && res.message) ? res.message : 'Erreur statut');
|
||
return;
|
||
}
|
||
window.location.reload();
|
||
}, 'json');
|
||
});
|
||
|
||
function loadRacesIntoSelects() {
|
||
if (intLinkedCtEventId <= 0) {
|
||
return;
|
||
}
|
||
var $status = $('#ct_api_races_status');
|
||
$status.removeClass('text-danger text-warning text-success').html(waitHtml('Chargement des courses ChronoTrack…'));
|
||
$.post(strAjaxUrl, { a: 'list_races', eve_id: intEveId, ct_event_id: intLinkedCtEventId }, function (res) {
|
||
if (rejectIfSessionLost(res)) {
|
||
return;
|
||
}
|
||
if (!res || res.state !== 'ok') {
|
||
$status.addClass('text-danger').text((res && res.message) ? res.message : 'Erreur chargement des courses');
|
||
return;
|
||
}
|
||
var arrRaces = res.races || [];
|
||
var strKind = res.kind || 'reg_choice';
|
||
if (!arrRaces.length) {
|
||
$status.addClass('text-warning').text((res.message) ? res.message : 'Aucune course ChronoTrack pour cet event.');
|
||
} else {
|
||
$status.addClass('text-success').text(arrRaces.length + ' course(s) ChronoTrack chargée(s).');
|
||
}
|
||
$('.ct-api-race-select').each(function () {
|
||
var $sel = $(this);
|
||
var intEprId = parseInt($sel.data('epr-id'), 10);
|
||
var intCurrent = tabMapCurrent[intEprId] || 0;
|
||
$sel.find('option:not(:first)').remove();
|
||
$.each(arrRaces, function (_, race) {
|
||
var strText = race.label || race.name || 'Course';
|
||
strText += ' (ID ' + race.ct_race_id + ')';
|
||
var $opt = $('<option></option>').val(race.ct_race_id).text(strText);
|
||
$sel.append($opt);
|
||
});
|
||
if (intCurrent > 0) {
|
||
$sel.val(String(intCurrent));
|
||
}
|
||
});
|
||
}, 'json').fail(function () {
|
||
$status.addClass('text-danger').text('Erreur réseau lors du chargement des races.');
|
||
});
|
||
}
|
||
|
||
function showRaceEditMode(blnLoadRaces) {
|
||
$('#ct_api_race_summary_wrap').hide();
|
||
$('#ct_api_race_edit_wrap').show();
|
||
if (blnLoadRaces) {
|
||
loadRacesIntoSelects();
|
||
}
|
||
}
|
||
|
||
function showRaceSummaryMode() {
|
||
$('#ct_api_race_edit_wrap').hide();
|
||
$('#ct_api_races_status').empty();
|
||
$('#ct_api_race_summary_wrap').show();
|
||
}
|
||
|
||
$('#ct_api_btn_edit_map').on('click', function () {
|
||
showRaceEditMode(true);
|
||
});
|
||
|
||
$('#ct_api_btn_cancel_edit_map').on('click', function () {
|
||
showRaceSummaryMode();
|
||
});
|
||
|
||
$('#ct_api_btn_reload_races').on('click', function () {
|
||
loadRacesIntoSelects();
|
||
});
|
||
|
||
$('#chronotrackApiCollapse').on('shown.bs.collapse', function () {
|
||
if (!blnHasSavedMapping && intLinkedCtEventId > 0 && !blnClosed) {
|
||
loadRacesIntoSelects();
|
||
}
|
||
});
|
||
|
||
<?php if ($arrConfig !== null && !$blnHasSavedMapping) { ?>
|
||
if (intLinkedCtEventId > 0 && !blnClosed && !$('#chronotrackApiCollapse').length) {
|
||
loadRacesIntoSelects();
|
||
}
|
||
<?php } ?>
|
||
|
||
$('#ct_api_btn_save_map').on('click', function () {
|
||
var tabMap = {};
|
||
$('.ct-api-race-select').each(function () {
|
||
var intEprId = parseInt($(this).data('epr-id'), 10);
|
||
var strRaceId = $(this).val();
|
||
tabMap[intEprId] = { ct_race_id: strRaceId ? parseInt(strRaceId, 10) : 0, ct_race_name: $(this).find('option:selected').text() };
|
||
});
|
||
var $btn = $(this);
|
||
$btn.prop('disabled', true);
|
||
$.post(strAjaxUrl, { a: 'save_race_map', eve_id: intEveId, map: tabMap }, function (res) {
|
||
if (rejectIfSessionLost(res)) {
|
||
return;
|
||
}
|
||
if (!res || res.state !== 'ok') {
|
||
showMsg('error', (res && res.message) ? res.message : 'Erreur mapping');
|
||
$btn.prop('disabled', false);
|
||
return;
|
||
}
|
||
window.location.reload();
|
||
}, 'json').fail(function () {
|
||
showMsg('error', 'Erreur réseau');
|
||
$btn.prop('disabled', false);
|
||
});
|
||
});
|
||
|
||
var objDiagnosticReport = null;
|
||
|
||
function renderDiagnosticReport(res) {
|
||
objDiagnosticReport = res;
|
||
var $out = $('#ct_api_diagnostic_out');
|
||
if (!res || res.state !== 'ok') {
|
||
$out.html('<div class="text-danger">' + escHtml((res && res.message) ? res.message : 'Erreur diagnostic') + '</div>');
|
||
$('#ct_api_btn_diagnostic_copy').addClass('d-none');
|
||
return;
|
||
}
|
||
|
||
var html = '<p class="mb-2"><strong>OAuth :</strong> ' + escHtml(res.oauth || '?')
|
||
+ ' — <strong>Event CT :</strong> ' + escHtml(String(res.ct_event_id || '')) + '</p>';
|
||
html += '<div class="table-responsive"><table class="table table-sm table-bordered mb-2">';
|
||
html += '<thead><tr><th>Endpoint</th><th>HTTP</th><th>Résultat</th><th>Entités détectées</th></tr></thead><tbody>';
|
||
|
||
$.each(res.probes || [], function (_, probe) {
|
||
html += '<tr>';
|
||
html += '<td><code>' + escHtml(probe.path || '') + '</code></td>';
|
||
html += '<td>' + escHtml(String(probe.http_code || '')) + '</td>';
|
||
html += '<td>' + escHtml(probe.state === 'ok' ? 'OK' : (probe.message || 'erreur')) + '</td>';
|
||
html += '<td>';
|
||
if (probe.summary) {
|
||
if (probe.summary.event_race && probe.summary.event_race.count > 0) {
|
||
html += 'event_race: ' + probe.summary.event_race.count + '<br>';
|
||
}
|
||
if (probe.summary.entry && probe.summary.entry.count > 0) {
|
||
html += 'entry: ' + probe.summary.entry.count + ' (page)<br>';
|
||
}
|
||
if (probe.summary.reg_choice && probe.summary.reg_choice.count > 0) {
|
||
html += 'reg_choice: ' + probe.summary.reg_choice.count + '<br>';
|
||
}
|
||
if (probe.summary.race && probe.summary.race.count > 0) {
|
||
html += 'race: ' + probe.summary.race.count + '<br>';
|
||
}
|
||
if (probe.summary.field_keys && probe.summary.field_keys.length) {
|
||
html += '<span class="text-muted">champs: ' + escHtml(probe.summary.field_keys.slice(0, 12).join(', '));
|
||
if (probe.summary.field_keys.length > 12) {
|
||
html += '…';
|
||
}
|
||
html += '</span><br>';
|
||
}
|
||
var arrSampleSource = probe.summary.entry || probe.summary.event_race || probe.summary.race || probe.summary.reg_choice;
|
||
if (arrSampleSource && arrSampleSource.samples && arrSampleSource.samples.length) {
|
||
html += '<span class="text-muted">ex. ';
|
||
$.each(arrSampleSource.samples, function (i, s) {
|
||
if (i > 0) {
|
||
html += ' · ';
|
||
}
|
||
var strLabel = s.name || s.id || '?';
|
||
if (s.external_id) {
|
||
strLabel += ' [ext:' + s.external_id + ']';
|
||
}
|
||
if (s.bib) {
|
||
strLabel += ' #' + s.bib;
|
||
}
|
||
if (s.entry_status) {
|
||
strLabel += ' (' + s.entry_status + ')';
|
||
}
|
||
html += escHtml(strLabel);
|
||
});
|
||
html += '</span>';
|
||
}
|
||
}
|
||
html += '</td></tr>';
|
||
});
|
||
html += '</tbody></table></div>';
|
||
|
||
$.each(res.probes || [], function (idx, probe) {
|
||
var strProbeLabel = probe.path || ('probe ' + idx);
|
||
if (probe.query && typeof probe.query === 'object') {
|
||
var arrQ = [];
|
||
$.each(probe.query, function (k, v) {
|
||
arrQ.push(k + '=' + v);
|
||
});
|
||
if (arrQ.length) {
|
||
strProbeLabel += '?' + arrQ.join('&');
|
||
}
|
||
}
|
||
html += '<details class="mb-2"><summary class="small"><strong>JSON brut — '
|
||
+ escHtml(strProbeLabel) + '</strong></summary>';
|
||
html += '<pre class="small border rounded p-2 mb-0" style="max-height:280px;overflow:auto;background:#f8f9fa;">'
|
||
+ escHtml(probe.raw_json || '') + '</pre></details>';
|
||
});
|
||
|
||
$out.html(html);
|
||
$('#ct_api_btn_diagnostic_copy').removeClass('d-none');
|
||
}
|
||
|
||
$('#ct_api_btn_diagnostic').on('click', function () {
|
||
var $out = $('#ct_api_diagnostic_out');
|
||
$out.html(waitHtml('Diagnostic en cours… (OAuth + 4 endpoints CT, dont entry)'));
|
||
$('#ct_api_btn_diagnostic_copy').addClass('d-none');
|
||
$.post(strAjaxUrl, {
|
||
a: 'diagnostic',
|
||
eve_id: intEveId,
|
||
ct_event_id: intLinkedCtEventId
|
||
}, function (res) {
|
||
if (rejectIfSessionLost(res)) {
|
||
return;
|
||
}
|
||
renderDiagnosticReport(res);
|
||
}, 'json').fail(function () {
|
||
$out.html('<div class="text-danger">Erreur réseau lors du diagnostic.</div>');
|
||
});
|
||
});
|
||
|
||
$('#ct_api_btn_diagnostic_copy').on('click', function () {
|
||
if (!objDiagnosticReport) {
|
||
return;
|
||
}
|
||
var strText = JSON.stringify(objDiagnosticReport, null, 2);
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(strText).then(function () {
|
||
window.alert('Rapport copié dans le presse-papiers.');
|
||
});
|
||
return;
|
||
}
|
||
window.prompt('Copier le rapport (Ctrl+C) :', strText);
|
||
});
|
||
|
||
$('#ct_api_btn_probe_push').on('click', function () {
|
||
var $btn = $(this);
|
||
var $out = $('#ct_api_diagnostic_out');
|
||
$btn.prop('disabled', true);
|
||
$out.html(waitHtml('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;
|
||
}
|
||
var strClass = (res && res.state === 'ok') ? 'text-success' : 'text-danger';
|
||
var strHtml = '<div class="' + strClass + '"><strong>Sonde</strong> — '
|
||
+ escHtml((res && res.message) ? res.message : 'Erreur') + '</div>';
|
||
if (res && res.payload) {
|
||
strHtml += '<pre class="small bg-light p-2 mt-2 mb-1" style="max-height:200px;overflow:auto;">'
|
||
+ escHtml(JSON.stringify(res.payload, null, 2)) + '</pre>';
|
||
}
|
||
if (res && res.response_body) {
|
||
strHtml += '<pre class="small bg-light p-2 mb-0" style="max-height:200px;overflow:auto;">'
|
||
+ escHtml(res.response_body) + '</pre>';
|
||
}
|
||
$out.html(strHtml);
|
||
}, 'json').fail(function () {
|
||
$out.html('<div class="text-danger">Erreur réseau (sonde).</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;
|
||
if (intTotal <= 0 || !tabGroups.length) {
|
||
$wrap.addClass('d-none').empty();
|
||
return;
|
||
}
|
||
|
||
var html = '<div class="card border-danger">';
|
||
html += '<div class="card-header py-2 bg-white">';
|
||
html += '<a class="d-block text-danger collapsed" data-toggle="collapse" href="#ct_api_blocked_detail" role="button" aria-expanded="false">';
|
||
html += '<strong>Exclus MS1</strong> <span class="badge badge-danger">' + intTotal + '</span>';
|
||
html += '</a></div>';
|
||
html += '<div class="collapse" id="ct_api_blocked_detail"><div class="card-body py-2">';
|
||
|
||
$.each(tabGroups, function (_, grp) {
|
||
var intCnt = (grp.items && grp.items.length) ? grp.items.length : 0;
|
||
if (intCnt === 0) {
|
||
return;
|
||
}
|
||
var blnTeam = (grp.code === 'team');
|
||
var strHeadClass = blnTeam ? 'text-warning' : 'text-danger';
|
||
html += '<div class="mb-3">';
|
||
html += '<div class="d-flex align-items-center mb-1">';
|
||
html += '<strong class="' + strHeadClass + ' small">' + escHtml(grp.label || grp.code) + '</strong>';
|
||
html += ' <span class="badge badge-secondary ml-1">' + intCnt + '</span>';
|
||
if (blnTeam) {
|
||
html += ' <span class="text-muted small ml-1">MVP : capitaine seulement (plus tard)</span>';
|
||
}
|
||
html += '</div>';
|
||
html += '<div class="ct-api-blocked-scroll border rounded">';
|
||
html += '<table class="table table-sm table-striped ct-api-blocked-table mb-0">';
|
||
html += '<thead class="thead-light"><tr>';
|
||
if (blnTeam) {
|
||
html += '<th>Nom</th><th>external_id</th><th>Équipe</th><th>No équipe</th><th>Rôle</th><th>Dossard</th><th>pec_id</th>';
|
||
} else {
|
||
html += '<th>Nom</th><th>external_id</th><th>par_id</th><th>Dossard</th><th>Sexe</th><th>Raison</th>';
|
||
}
|
||
html += '</tr></thead><tbody>';
|
||
$.each(grp.items, function (__, item) {
|
||
html += '<tr>';
|
||
html += '<td>' + escHtml(item.name || '—') + '</td>';
|
||
html += '<td>' + escHtml(item.external_id || '—') + '</td>';
|
||
if (blnTeam) {
|
||
html += '<td>' + escHtml(item.team_name || '—') + '</td>';
|
||
html += '<td>' + escHtml(item.no_equipe ? ('#' + item.no_equipe) : '—') + '</td>';
|
||
html += '<td>' + escHtml(item.role_label || '—') + '</td>';
|
||
html += '<td>' + escHtml(item.no_bib ? ('#' + item.no_bib) : '—') + '</td>';
|
||
html += '<td>' + escHtml(String(item.pec_id || '')) + '</td>';
|
||
} else {
|
||
html += '<td>' + escHtml(String(item.par_id || '')) + '</td>';
|
||
html += '<td>' + escHtml(item.no_bib ? ('#' + item.no_bib) : (item.bib ? ('#' + item.bib) : '—')) + '</td>';
|
||
html += '<td>' + escHtml(item.par_sexe || '—') + '</td>';
|
||
html += '<td class="text-muted">' + escHtml(item.message || '') + '</td>';
|
||
}
|
||
html += '</tr>';
|
||
});
|
||
html += '</tbody></table></div></div>';
|
||
});
|
||
|
||
html += '</div></div></div>';
|
||
$wrap.removeClass('d-none').html(html);
|
||
}
|
||
|
||
function renderPushPreview(res) {
|
||
var $sum = $('#ct_api_push_summary');
|
||
var $status = $('#ct_api_push_status');
|
||
var $btn = $('#ct_api_btn_push');
|
||
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')
|
||
.html('<div class="text-danger">' + escHtml((res && res.message) ? res.message : 'Erreur') + '</div>');
|
||
}
|
||
if ($sum.length) {
|
||
$sum.html('');
|
||
}
|
||
$('#ct_api_push_blocked_wrap').addClass('d-none').empty();
|
||
$btn.prop('disabled', true);
|
||
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;
|
||
|
||
// Bannière employé (ludique / claire)
|
||
if ($status.length) {
|
||
$status.removeClass('ct-api-push-status--ok ct-api-push-status--todo');
|
||
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>'
|
||
);
|
||
} 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>'
|
||
);
|
||
}
|
||
}
|
||
|
||
// Détails (sous les boutons, zone repliable)
|
||
var html = '<table class="table table-sm table-bordered ct-api-dash-table mb-1">';
|
||
html += '<thead><tr><th>MS1</th><th class="text-right">Total</th><th class="text-right">Éligibles</th>';
|
||
html += '<th class="text-right">Exclus</th><th class="text-right">Différentiel</th></tr></thead><tbody>';
|
||
html += '<tr><td>Participants</td>';
|
||
html += '<td class="num">' + intMs1 + '</td>';
|
||
html += '<td class="num text-primary">' + intEligible + '</td>';
|
||
html += '<td class="num text-danger">' + intBlocked + '</td>';
|
||
html += '<td class="num ' + (intPending > 0 ? 'text-warning' : 'text-success') + '">' + intPending + '</td></tr>';
|
||
html += '</tbody></table>';
|
||
|
||
if (intCtLiveCount !== null) {
|
||
html += '<p class="ct-api-dash-push mb-1 text-muted">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></p>';
|
||
} else {
|
||
html += '<p class="ct-api-dash-push mb-1 text-muted">ChronoTrack Live : total non chargé (bouton ci-dessous).</p>';
|
||
}
|
||
|
||
$sum.html(html);
|
||
renderBlockedPanel(res);
|
||
$btn.prop('disabled', intPending <= 0);
|
||
|
||
// 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) n’est pas touché.
|
||
if (!blnFirst && intPending > 0 && intEligible > 0 && intPending === intEligible
|
||
&& !window._ctApiAutoMarkDone) {
|
||
window._ctApiAutoMarkDone = true;
|
||
if ($status.length) {
|
||
$status.removeClass('ct-api-push-status--ok ct-api-push-status--todo')
|
||
.html(waitHtml('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;
|
||
if (intBlocked > 0) {
|
||
$('#ct_api_push_details_collapse').collapse('show');
|
||
}
|
||
});
|
||
return;
|
||
}
|
||
|
||
// Ouvre les détails seulement s’il y a des exclus à corriger
|
||
if (intBlocked > 0) {
|
||
$('#ct_api_push_details_collapse').collapse('show');
|
||
}
|
||
}
|
||
|
||
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) {
|
||
$status.removeClass('ct-api-push-status--ok ct-api-push-status--todo')
|
||
.html(waitHtml('Vérification des changements…'));
|
||
}
|
||
xhrPushPreview = $.ajax({
|
||
url: strAjaxUrl,
|
||
type: 'POST',
|
||
dataType: 'json',
|
||
timeout: 60000,
|
||
data: { a: 'sync_preview', eve_id: intEveId }
|
||
}).done(function (res) {
|
||
if (rejectIfSessionLost(res)) {
|
||
return;
|
||
}
|
||
blnPushPreviewLoaded = true;
|
||
renderPushPreview(res);
|
||
}).fail(function (jqXHR, strStatus) {
|
||
if (strStatus === 'abort') {
|
||
return;
|
||
}
|
||
var strMsg = (strStatus === 'timeout')
|
||
? 'Délai dépassé — réessayez Actualiser.'
|
||
: 'Erreur réseau (preview).';
|
||
if ($status.length) {
|
||
$status.removeClass('ct-api-push-status--ok ct-api-push-status--todo')
|
||
.html('<div class="text-danger">' + escHtml(strMsg) + '</div>');
|
||
}
|
||
}).always(function () {
|
||
xhrPushPreview = null;
|
||
});
|
||
}
|
||
|
||
function loadCtLiveCount() {
|
||
var $btn = $('#ct_api_btn_ct_count');
|
||
var $sum = $('#ct_api_push_summary');
|
||
$btn.prop('disabled', true);
|
||
if ($sum.length && objPushPreview && objPushPreview.state === 'ok') {
|
||
// conserve le tableau MS1, indique le chargement CT
|
||
$sum.find('.ct-api-dash-push').first().replaceWith(
|
||
'<p class="ct-api-dash-push mb-1 text-muted">' + waitHtml('Comptage ChronoTrack Live…') + '</p>'
|
||
);
|
||
}
|
||
$.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();
|
||
});
|
||
|
||
// MSIN-4328 — refresh léger 60 s (preview MS1 seulement, pas CT Live / pas de push)
|
||
function startStatusAutoRefresh() {
|
||
if (intStatusRefreshTimer) {
|
||
clearInterval(intStatusRefreshTimer);
|
||
}
|
||
intStatusRefreshTimer = setInterval(function () {
|
||
if (blnPushRunning) {
|
||
return;
|
||
}
|
||
if (!$('#ct_api_push_card').is(':visible')) {
|
||
return;
|
||
}
|
||
if (document.hidden) {
|
||
return;
|
||
}
|
||
loadPushPreview(true);
|
||
}, 60000);
|
||
}
|
||
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 (50 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 += '<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: 50 }, 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-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_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();
|
||
loadSyncLogs();
|
||
}).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_auto_diag');
|
||
if (!$box.length) {
|
||
return;
|
||
}
|
||
$box.text('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) {
|
||
$box.html('<span class="text-danger">'
|
||
+ escHtml((res && res.message) ? res.message : 'Diag impossible') + '</span>');
|
||
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 (d.last_auto_run_at) {
|
||
$('#ct_api_auto_last').html('<strong>Dernier passage auto :</strong> '
|
||
+ escHtml(d.last_auto_run_at));
|
||
}
|
||
}, 'json').fail(function () {
|
||
$box.html('<span class="text-danger">Erreur réseau (diag).</span>');
|
||
});
|
||
}
|
||
$('#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);
|
||
});
|
||
});
|
||
|
||
$('#ct_api_btn_push').on('click', function () {
|
||
var intPending = 0;
|
||
if (objPushPreview) {
|
||
if (typeof objPushPreview.pending_push !== 'undefined') {
|
||
intPending = objPushPreview.pending_push;
|
||
} else {
|
||
intPending = objPushPreview.to_push || objPushPreview.transferable || 0;
|
||
}
|
||
}
|
||
// MSIN-4328 — push immédiat, sans confirm
|
||
var $btn = $(this);
|
||
var $res = $('#ct_api_push_result');
|
||
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;
|
||
|
||
$btn.prop('disabled', true);
|
||
blnPushRunning = true;
|
||
$('#ct_api_btn_push_refresh, #ct_api_btn_ct_count, #ct_api_btn_logs').prop('disabled', true);
|
||
|
||
function renderProgress(intDone, intTot, strExtra) {
|
||
var intPct = (intTot > 0) ? Math.min(100, Math.round((intDone * 100) / intTot)) : 0;
|
||
var strHtml = '<div class="mb-1"><strong>Envoi</strong> — '
|
||
+ intDone + ' / ' + intTot
|
||
+ (strExtra ? (' · ' + escHtml(strExtra)) : '')
|
||
+ '</div>';
|
||
strHtml += '<div class="progress" style="height:1.25rem;">';
|
||
strHtml += '<div class="progress-bar progress-bar-striped progress-bar-animated bg-primary" '
|
||
+ 'role="progressbar" style="width:' + intPct + '%;" aria-valuenow="' + intPct
|
||
+ '" aria-valuemin="0" aria-valuemax="100">' + intPct + '%</div>';
|
||
strHtml += '</div>';
|
||
if (intOkSum || intErrSum) {
|
||
strHtml += '<p class="small text-muted mb-0 mt-1">OK ' + intOkSum
|
||
+ ' · erreurs ' + intErrSum
|
||
+ ' · POST ' + intPostSum + ' · PUT ' + intPutSum + '</p>';
|
||
}
|
||
$res.html(strHtml);
|
||
}
|
||
|
||
function finishPush() {
|
||
blnPushRunning = false;
|
||
$('#ct_api_btn_push_refresh, #ct_api_btn_ct_count, #ct_api_btn_logs').prop('disabled', false);
|
||
$btn.prop('disabled', false);
|
||
var strFinal = (intErrSum === 0)
|
||
? (intOkSum + ' participant(s) synchronisé(s) (POST ' + intPostSum + ' · PUT ' + intPutSum + ').')
|
||
: (intOkSum + ' OK, ' + intErrSum + ' erreur(s).');
|
||
var strState = blnFatal ? 'error' : ((intErrSum === 0) ? 'ok' : 'partial');
|
||
renderPushResult({
|
||
state: strState,
|
||
message: strFinal,
|
||
errors: tabErrAll.slice(0, 20)
|
||
});
|
||
loadPushPreview(true);
|
||
}
|
||
|
||
function pushChunk() {
|
||
renderProgress(intOffset, intTotal, 'lot en cours…');
|
||
$.ajax({
|
||
url: strAjaxUrl,
|
||
type: 'POST',
|
||
dataType: 'json',
|
||
timeout: 300000,
|
||
data: {
|
||
a: 'sync_push',
|
||
eve_id: intEveId,
|
||
offset: intOffset,
|
||
limit: intChunk,
|
||
refresh_preview: 0
|
||
}
|
||
}).done(function (res) {
|
||
if (rejectIfSessionLost(res)) {
|
||
return;
|
||
}
|
||
if (!res || (res.state !== 'ok' && res.state !== 'partial')) {
|
||
if (res && res.total === 0) {
|
||
renderPushResult(res);
|
||
$('#ct_api_btn_push_refresh, #ct_api_btn_ct_count, #ct_api_btn_logs').prop('disabled', false);
|
||
blnPushRunning = false;
|
||
$btn.prop('disabled', false);
|
||
return;
|
||
}
|
||
blnFatal = true;
|
||
if (res && res.message) {
|
||
tabErrAll.push(res.message);
|
||
}
|
||
finishPush();
|
||
return;
|
||
}
|
||
if (res.total) {
|
||
intTotal = parseInt(res.total, 10) || intTotal;
|
||
}
|
||
intOkSum += parseInt(res.pushed_ok || 0, 10);
|
||
intErrSum += parseInt(res.pushed_error || 0, 10);
|
||
intPostSum += parseInt(res.pushed_post || 0, 10);
|
||
intPutSum += parseInt(res.pushed_put || 0, 10);
|
||
if (res.errors && res.errors.length) {
|
||
$.each(res.errors, function (_, msg) {
|
||
if (tabErrAll.length < 20) {
|
||
tabErrAll.push(msg);
|
||
}
|
||
});
|
||
}
|
||
intOffset = parseInt(res.next_offset != null ? res.next_offset : (intOffset + (res.chunk_done || intChunk)), 10);
|
||
renderProgress(Math.min(intOffset, intTotal), intTotal, '');
|
||
if (res.done || intOffset >= intTotal) {
|
||
finishPush();
|
||
return;
|
||
}
|
||
pushChunk();
|
||
}).fail(function (jqXHR, strStatus) {
|
||
blnFatal = true;
|
||
var strDetail = strStatus === 'timeout'
|
||
? 'Délai dépassé sur un lot — relancer le push (upsert CT).'
|
||
: ('Erreur réseau sur un lot (' + strStatus
|
||
+ (jqXHR && jqXHR.status ? ', HTTP ' + jqXHR.status : '') + ').');
|
||
if (jqXHR && jqXHR.responseText) {
|
||
var strBody = String(jqXHR.responseText).replace(/\s+/g, ' ').substr(0, 180);
|
||
if (strBody) {
|
||
strDetail += ' — ' + strBody;
|
||
}
|
||
}
|
||
tabErrAll.push(strDetail);
|
||
finishPush();
|
||
});
|
||
}
|
||
|
||
renderProgress(0, intTotal, 'préparation…');
|
||
$.ajax({
|
||
url: strAjaxUrl,
|
||
type: 'POST',
|
||
dataType: 'json',
|
||
timeout: 180000,
|
||
data: { a: 'sync_push_prepare', eve_id: intEveId }
|
||
}).done(function (prep) {
|
||
if (rejectIfSessionLost(prep)) {
|
||
return;
|
||
}
|
||
if (!prep || prep.state !== 'ok') {
|
||
renderPushResult(prep || { state: 'error', message: 'Préparation échouée' });
|
||
$('#ct_api_btn_push_refresh, #ct_api_btn_ct_count, #ct_api_btn_logs').prop('disabled', false);
|
||
blnPushRunning = false;
|
||
$btn.prop('disabled', false);
|
||
return;
|
||
}
|
||
intTotal = parseInt(prep.total, 10) || intTotal;
|
||
if (prep.chunk_size) {
|
||
intChunk = parseInt(prep.chunk_size, 10) || intChunk;
|
||
}
|
||
renderProgress(0, intTotal, 'démarrage');
|
||
pushChunk();
|
||
}).fail(function (jqXHR, strStatus) {
|
||
var strMsg = 'Erreur préparation push (' + strStatus
|
||
+ (jqXHR && jqXHR.status ? ', HTTP ' + jqXHR.status : '') + ')';
|
||
if (jqXHR && jqXHR.responseText) {
|
||
strMsg += ' — ' + String(jqXHR.responseText).replace(/\s+/g, ' ').substr(0, 180);
|
||
}
|
||
strMsg += '. Vérifiez que le code MSIN-4328 est déployé (action sync_push_prepare).';
|
||
renderPushResult({ state: 'error', message: strMsg });
|
||
$('#ct_api_btn_push_refresh, #ct_api_btn_ct_count, #ct_api_btn_logs').prop('disabled', false);
|
||
blnPushRunning = false;
|
||
$btn.prop('disabled', false);
|
||
});
|
||
});
|
||
}
|
||
|
||
})(jQuery);
|
||
</script>
|
||
<?php
|
||
}
|
||
|
||
function fxChronotrackApiAdminRenderDashboard($strLangue = 'fr', $blnInsideV2Panel = false) {
|
||
$arrRows = fxChronotrackApiListActiveConfigs();
|
||
if (!$blnInsideV2Panel) {
|
||
echo '<h1 class="h3">MSIN API ChronoTrack — Courses connectées</h1>';
|
||
}
|
||
echo '<p class="text-muted">Événements liés ou sync active. Le dashboard détaillé (diff, push) viendra en Phase 2.</p>';
|
||
|
||
if ($arrRows === null || count($arrRows) === 0) {
|
||
echo '<div class="alert alert-light">Aucune course connectée.</div>';
|
||
return;
|
||
}
|
||
|
||
echo '<table class="table table-sm table-striped"><thead><tr><th>Événement MS1</th><th>ChronoTrack</th><th>Statut</th><th></th></tr></thead><tbody>';
|
||
for ($i = 1; $i <= count($arrRows); $i++) {
|
||
$arr = $arrRows[$i];
|
||
echo '<tr>';
|
||
echo '<td>' . fxChronotrackApiAdminEsc($arr['eve_nom']) . ' <span class="text-muted">(' . intval($arr['eve_id']) . ')</span></td>';
|
||
echo '<td>' . fxChronotrackApiAdminEsc($arr['ct_event_name']) . ' <span class="text-muted">(' . intval($arr['ct_event_id']) . ')</span></td>';
|
||
echo '<td>' . fxChronotrackApiAdminEsc(fxChronotrackApiConfigStatusLabel($arr['sync_status'])) . '</td>';
|
||
echo '<td><a class="btn btn-outline-primary btn-sm" href="chronotrack_api.php?eve_id=' . intval($arr['eve_id']) . '">Configurer</a></td>';
|
||
echo '</tr>';
|
||
}
|
||
echo '</tbody></table>';
|
||
}
|