Implement auto-sync configuration for ChronoTrack API. Add functionality to save auto-sync settings, including frequency and end date, enhancing user control over synchronization. Update push panel to reflect new options and improve user feedback during the sync process. This change lays the groundwork for future cron integration.

This commit is contained in:
2026-07-14 12:08:03 -04:00
parent f9a75e8aba
commit 0db8da9810
4 changed files with 253 additions and 4 deletions

View File

@ -103,7 +103,7 @@ function fxChronotrackApiAdminRenderEventPage($intEveId, $strLangue = 'fr', $bln
// Sync dabord (usage courant) ; config repliable ensuite. MSIN-4328
if ($arrConfig !== null && $blnEventLinked && $blnConfigured) {
fxChronotrackApiAdminRenderPushPanel($intEveId, $blnClosed);
fxChronotrackApiAdminRenderPushPanel($intEveId, $blnClosed, $arrConfig);
}
if ($arrConfig !== null && $blnEventLinked) {
@ -244,7 +244,7 @@ function fxChronotrackApiAdminRenderRaceMapping($intEveId, $arrConfig, $strLangu
echo '</div></div></div>';
}
function fxChronotrackApiAdminRenderPushPanel($intEveId, $blnClosed = false) {
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">';
@ -252,7 +252,63 @@ function fxChronotrackApiAdminRenderPushPanel($intEveId, $blnClosed = false) {
echo '<div class="alert alert-warning py-2 small mb-2">Événement fermé — envoi manuel encore possible.</div>';
}
// Zone employé : statut clair + actions dabord. MSIN-4328
// 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 — exécution serveur (cron) à brancher. Arrêt prévu : '
. fxChronotrackApiAdminEsc($strUntilRaw) . '.';
} else {
echo 'Quand cest coché, le serveur (cron) poussera le différentiel à cet intervalle jusquà la date de fin. '
. 'Pas encore actif côté serveur.';
}
echo '</p>';
echo '<div id="ct_api_auto_msg" class="small mt-1"></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 sil y a des changements à envoyer.</div>';
echo '</div>';
@ -263,7 +319,7 @@ function fxChronotrackApiAdminRenderPushPanel($intEveId, $blnClosed = false) {
echo '<button type="button" class="btn btn-primary btn-lg" id="ct_api_btn_push" disabled>';
echo '<i class="fa fa-cloud-upload" aria-hidden="true"></i>&nbsp;Envoyer vers ChronoTrack</button>';
echo '</div>';
echo '<p class="small text-muted mb-2">La sync automatique arrivera bientôt — pour linstant, un clic suffit quand il y a du nouveau.</p>';
echo '<p class="small text-muted mb-2">Envoi manuel : uniquement les nouveaux / modifiés depuis le dernier push OK.</p>';
echo '<div id="ct_api_push_result" class="small mb-3"></div>';
// Détails / debug : sous les boutons, repliés par défaut
@ -355,6 +411,13 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
}
.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; }
</style>
<?php fxChronotrackApiAdminRenderLoaderAssets($vDomaine); ?>
<script>
@ -1080,6 +1143,50 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
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 enregistrée — exécution serveur (cron) à brancher. Arrêt prévu : '
+ res.auto_sync.until + '.');
} else {
$hint.text('Sync auto désactivée. Le cron (phase B) ne traitera pas cet événement.');
}
}
}, '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();

View File

@ -135,6 +135,79 @@ function fxChronotrackApiConfigSetLastPush($intEveId, $intOkCount) {
return (bool)$objDatabase->fxQuery($sql);
}
/**
* MSIN-4328 — fréquences autorisées (minutes), phase A.
*/
function fxChronotrackApiConfigAutoSyncIntervals() {
return array(1, 5, 15, 30, 60);
}
/**
* MSIN-4328 — enregistre la config sync auto (UI). Le cron (phase B) lira ces champs.
*/
function fxChronotrackApiConfigSaveAutoSync($intEveId, $blnEnabled, $intIntervalMin, $strUntil) {
global $objDatabase;
$intEveId = intval($intEveId);
$arrExisting = fxChronotrackApiConfigGet($intEveId);
if ($arrExisting === null) {
return array('state' => 'error', 'message' => 'Événement non lié à ChronoTrack');
}
if (($arrExisting['sync_status'] ?? '') === MSIN_API_CHRONOTRACK_SYNC_FERME) {
return array('state' => 'error', 'message' => 'Événement fermé — sync auto impossible.');
}
$blnEnabled = $blnEnabled ? 1 : 0;
$intIntervalMin = intval($intIntervalMin);
if (!in_array($intIntervalMin, fxChronotrackApiConfigAutoSyncIntervals(), true)) {
$intIntervalMin = 15;
}
$strUntilSql = 'NULL';
$strUntil = trim((string)$strUntil);
if ($strUntil !== '') {
// Accepte "YYYY-MM-DDTHH:MM" (input datetime-local) ou "YYYY-MM-DD HH:MM[:SS]"
$strUntilNorm = str_replace('T', ' ', $strUntil);
if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/', $strUntilNorm)) {
$strUntilNorm .= ':00';
}
$intTs = strtotime($strUntilNorm);
if ($intTs === false) {
return array('state' => 'error', 'message' => 'Date / heure de fin invalide');
}
if ($blnEnabled && $intTs <= time()) {
return array('state' => 'error', 'message' => 'La date/heure de fin doit être dans le futur');
}
$strUntilSql = "'" . $objDatabase->fxEscape(date('Y-m-d H:i:s', $intTs)) . "'";
} elseif ($blnEnabled) {
return array('state' => 'error', 'message' => 'Indiquez une date et une heure de fin pour activer la sync auto');
}
$sql = "UPDATE api_chronotrack_config SET auto_sync_enabled = " . $blnEnabled
. ", auto_sync_interval_min = " . $intIntervalMin
. ", auto_sync_until = " . $strUntilSql
. ", updated_at = '" . fxGetDateTime() . "'"
. " WHERE eve_id = " . $intEveId;
if (!$objDatabase->fxQuery($sql)) {
return array('state' => 'error', 'message' => 'Erreur SQL enregistrement sync auto');
}
$arrFresh = fxChronotrackApiConfigGet($intEveId);
return array(
'state' => 'ok',
'message' => $blnEnabled
? ('Sync auto enregistrée (toutes les ' . $intIntervalMin . ' min jusquau '
. trim((string)($arrFresh['auto_sync_until'] ?? '')) . '). Le cron démarrera en phase B.')
: 'Sync auto désactivée.',
'auto_sync' => array(
'enabled' => intval($arrFresh['auto_sync_enabled'] ?? 0) === 1,
'interval_min' => intval($arrFresh['auto_sync_interval_min'] ?? 15),
'until' => ($arrFresh['auto_sync_until'] ?? null),
'last_run_at' => ($arrFresh['last_auto_run_at'] ?? null),
),
);
}
function fxChronotrackApiRaceMapGetForEvent($intEveId) {
global $objDatabase;

View File

@ -0,0 +1,57 @@
-- MSIN-4328 — Config sync automatique (phase A : stockage + UI, pas encore de cron)
-- Prérequis : sql/MSIN-4328-chronotrack-last-push-at.sql
-- Idempotent.
SET @db := DATABASE();
SET @col_ase := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'api_chronotrack_config' AND COLUMN_NAME = 'auto_sync_enabled'
);
SET @sql_ase := IF(
@col_ase = 0,
'ALTER TABLE api_chronotrack_config ADD COLUMN auto_sync_enabled TINYINT(1) NOT NULL DEFAULT 0 COMMENT ''MSIN-4328 — sync auto ON/OFF'' AFTER last_push_ok_count',
'SELECT ''auto_sync_enabled already exists'' AS note'
);
PREPARE stmt_ase FROM @sql_ase;
EXECUTE stmt_ase;
DEALLOCATE PREPARE stmt_ase;
SET @col_asi := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'api_chronotrack_config' AND COLUMN_NAME = 'auto_sync_interval_min'
);
SET @sql_asi := IF(
@col_asi = 0,
'ALTER TABLE api_chronotrack_config ADD COLUMN auto_sync_interval_min SMALLINT UNSIGNED NOT NULL DEFAULT 15 COMMENT ''MSIN-4328 — fréquence minutes'' AFTER auto_sync_enabled',
'SELECT ''auto_sync_interval_min already exists'' AS note'
);
PREPARE stmt_asi FROM @sql_asi;
EXECUTE stmt_asi;
DEALLOCATE PREPARE stmt_asi;
SET @col_asu := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'api_chronotrack_config' AND COLUMN_NAME = 'auto_sync_until'
);
SET @sql_asu := IF(
@col_asu = 0,
'ALTER TABLE api_chronotrack_config ADD COLUMN auto_sync_until DATETIME NULL DEFAULT NULL COMMENT ''MSIN-4328 — arrêt auto (date+heure)'' AFTER auto_sync_interval_min',
'SELECT ''auto_sync_until already exists'' AS note'
);
PREPARE stmt_asu FROM @sql_asu;
EXECUTE stmt_asu;
DEALLOCATE PREPARE stmt_asu;
SET @col_lar := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'api_chronotrack_config' AND COLUMN_NAME = 'last_auto_run_at'
);
SET @sql_lar := IF(
@col_lar = 0,
'ALTER TABLE api_chronotrack_config ADD COLUMN last_auto_run_at DATETIME NULL DEFAULT NULL COMMENT ''MSIN-4328 — dernier passage cron (phase B)'' AFTER auto_sync_until',
'SELECT ''last_auto_run_at already exists'' AS note'
);
PREPARE stmt_lar FROM @sql_lar;
EXECUTE stmt_lar;
DEALLOCATE PREPARE stmt_lar;

View File

@ -131,6 +131,18 @@ switch ($strAction) {
}
break;
case 'sync_auto_save':
// MSIN-4328 — phase A : config only (cron phase B)
if ($intEveId <= 0) {
$tabRetour = array('state' => 'error', 'message' => 'eve_id manquant');
break;
}
$blnEnabled = (intval($_REQUEST['enabled'] ?? 0) === 1);
$intInterval = intval($_REQUEST['interval_min'] ?? 15);
$strUntil = trim((string)($_REQUEST['until'] ?? ''));
$tabRetour = fxChronotrackApiConfigSaveAutoSync($intEveId, $blnEnabled, $intInterval, $strUntil);
break;
case 'sync_probe_push':
if ($intEveId <= 0) {
$tabRetour = array('state' => 'error', 'message' => 'eve_id manquant');