2007 lines
92 KiB
PHP
2007 lines
92 KiB
PHP
<?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, l’envoi et le diagnostic s’ouvrent 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> Ouvrir le tableau de bord (nouvelle fenêtre)</a>';
|
||
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 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></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);
|
||
|
||
echo '<div class="ct-api-auto-banner mb-3" id="ct_api_auto_banner">';
|
||
if ($blnAutoOn && !$blnClosed) {
|
||
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 ($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> Fermer / retour super admin</a>';
|
||
} else {
|
||
echo '<a class="btn btn-outline-secondary btn-sm" href="chronotrack_api.php">← Liste</a> ';
|
||
echo '<a class="btn btn-primary btn-sm" href="chronotrack_api.php?eve_id=' . intval($intEveId)
|
||
. '&focus=1" target="_blank" rel="noopener">'
|
||
. '<i class="fa fa-external-link" aria-hidden="true"></i> 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)) . '&a=mod&id=' . intval($intEveId) . '">← 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">';
|
||
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) {
|
||
// 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 s’il 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> 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-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> 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> Voir les logs</button>';
|
||
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 %). Logs à chaque envoi.</p>';
|
||
echo '<div id="ct_api_push_result" class="ct-api-push-result small mb-3"></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">Exclus & détail</span>';
|
||
echo '<span class="text-muted small">ouvrir la liste</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 l’opé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> 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" 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> 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> 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 {
|
||
min-height: 88px;
|
||
padding: 14px 16px;
|
||
border-radius: 10px;
|
||
background: #f4f6f8;
|
||
border: 1px solid #e2e8f0;
|
||
box-sizing: border-box;
|
||
}
|
||
.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: #f8fafc;
|
||
border-color: #cbd5e1;
|
||
}
|
||
.ct-api-push-status__title {
|
||
font-size: 1.2rem;
|
||
font-weight: 700;
|
||
margin: 0 0 4px;
|
||
line-height: 1.3;
|
||
}
|
||
.ct-api-push-status__hint {
|
||
margin: 0;
|
||
font-size: 0.9rem;
|
||
}
|
||
.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, '&').replace(/</g, '<').replace(/>/g, '>');
|
||
}
|
||
|
||
// 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é) ?';
|
||
} 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) {
|
||
// 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);
|
||
});
|
||
|
||
$('#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);
|
||
});
|
||
});
|
||
|
||
// 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;
|
||
if (intTotal <= 0 || !tabGroups.length) {
|
||
$wrap.html('<div class="ct-api-exclus-empty">Aucun exclus pour le moment.</div>');
|
||
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>';
|
||
|
||
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 setStatusLoading(strMsg) {
|
||
var $status = $('#ct_api_push_status');
|
||
if (!$status.length) {
|
||
return;
|
||
}
|
||
$status.removeClass('ct-api-push-status--ok ct-api-push-status--todo')
|
||
.addClass('ct-api-push-status--loading')
|
||
.html(
|
||
'<p class="ct-api-push-status__title mb-1">' + waitHtml(strMsg || 'Vérification…') + '</p>'
|
||
+ '<p class="ct-api-push-status__hint mb-0 text-muted">Les compteurs restent affichés pendant le chargement.</p>'
|
||
+ '<p id="ct_api_status_refresh_cd" class="small text-muted mb-0 mt-1"></p>'
|
||
);
|
||
}
|
||
|
||
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);
|
||
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) n’est 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;
|
||
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) {
|
||
setStatusLoading('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 $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-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();
|
||
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_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 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;
|
||
|
||
blnPushRunning = true;
|
||
$('#ct_api_btn_push, #ct_api_btn_push_full, #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>'
|
||
+ (blnForceFull ? 'Resync complète' : '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);
|
||
var strFinal = (intErrSum === 0)
|
||
? (intOkSum + ' participant(s) synchronisé(s) (POST ' + intPostSum + ' · PUT ' + intPutSum + ').')
|
||
: (intOkSum + ' OK, ' + intErrSum + ' erreur(s).');
|
||
if (blnForceFull && intErrSum === 0) {
|
||
strFinal = 'Resync complète — ' + strFinal;
|
||
}
|
||
var strState = blnFatal ? 'error' : ((intErrSum === 0) ? 'ok' : 'partial');
|
||
renderPushResult({
|
||
state: strState,
|
||
message: strFinal,
|
||
errors: tabErrAll.slice(0, 20)
|
||
});
|
||
loadPushPreview(true);
|
||
loadSyncLogs();
|
||
}
|
||
|
||
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) {
|
||
blnFatal = true;
|
||
tabErrAll.push('Réponse vide du serveur');
|
||
finishPush();
|
||
return;
|
||
}
|
||
if (res.state !== 'ok' && res.state !== 'partial' && res.state !== 'error') {
|
||
blnFatal = true;
|
||
if (res.message) {
|
||
tabErrAll.push(res.message);
|
||
}
|
||
finishPush();
|
||
return;
|
||
}
|
||
if (res.total === 0 && res.state === 'error') {
|
||
renderPushResult(res);
|
||
$('#ct_api_btn_push_refresh, #ct_api_btn_ct_count, #ct_api_btn_logs').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 ' + Math.ceil(intOffset / Math.max(1, intChunk)) + ' terminé'
|
||
);
|
||
if (res.done || intOffset >= intTotal) {
|
||
finishPush();
|
||
return;
|
||
}
|
||
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 — relancer (lots plus petits).';
|
||
} else if (intHttp === 0) {
|
||
strDetail = 'Connexion coupée sur un lot (serveur surchargé / PHP arrêté). 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, 220);
|
||
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,
|
||
force_full: blnForceFull ? 1 : 0
|
||
}
|
||
}).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;
|
||
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 complète' : '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;
|
||
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'])
|
||
. '&focus=1" target="_blank" rel="noopener">Ouvrir</a></td>';
|
||
echo '</tr>';
|
||
}
|
||
echo '</tbody></table>';
|
||
}
|