From 2dae90807444c29ce7da09ccf96ed264547c6817 Mon Sep 17 00:00:00 2001 From: stephan Date: Wed, 15 Jul 2026 12:03:42 -0400 Subject: [PATCH] =?UTF-8?q?MSIN-4328=20=E2=80=94=20Add=20custom=20field=20?= =?UTF-8?q?probe=20functionality=20to=20ChronoTrack=20API.=20Implement=20a?= =?UTF-8?q?=20new=20button=20for=20custom=20field=20synchronization=20in?= =?UTF-8?q?=20the=20admin=20interface,=20allowing=20for=20the=20testing=20?= =?UTF-8?q?of=20custom=20fields=20similar=20to=20import=20processes.=20Enh?= =?UTF-8?q?ance=20backend=20logic=20to=20handle=20custom=20field=20creatio?= =?UTF-8?q?n=20and=20validation,=20improving=20the=20overall=20synchroniza?= =?UTF-8?q?tion=20capabilities=20and=20user=20experience.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- php/chronotrack_api/fx_chronotrack_admin.php | 54 ++++++ php/chronotrack_api/fx_chronotrack_sync.php | 164 +++++++++++++++++++ superadm/ajax_chronotrack_api.php | 10 ++ 3 files changed, 228 insertions(+) diff --git a/php/chronotrack_api/fx_chronotrack_admin.php b/php/chronotrack_api/fx_chronotrack_admin.php index 24c6d3b..2618c65 100644 --- a/php/chronotrack_api/fx_chronotrack_admin.php +++ b/php/chronotrack_api/fx_chronotrack_admin.php @@ -372,6 +372,9 @@ function fxChronotrackApiAdminRenderDiagnosticPanel($intCtEventId) { echo ' Lancer le diagnostic API '; echo ' '; + echo ' '; echo ''; echo '
Cliquez sur « Lancer le diagnostic API » ou « Sonde ».
'; @@ -874,6 +877,57 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed }); }); + // MSIN-4328 — sonde custom_Ms1Probe (mimique import Custom) + $('#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 custom field…')); + $('#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 = '
Sonde custom — ' + + escHtml((res && res.message) ? res.message : 'Erreur') + '
'; + if (res) { + strHtml += '
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 || '')) + + '
'; + } + if (res && res.question_endpoints && res.question_endpoints.length) { + strHtml += '
Endpoints question (GET)
'; + } + if (res && res.payload) { + strHtml += '
'
+                    + escHtml(JSON.stringify(res.payload, null, 2)) + '
'; + } + if (res && res.post_response) { + strHtml += '
'
+                    + escHtml(res.post_response) + '
'; + } + $out.html(strHtml); + $('#ct_api_btn_diagnostic_copy').removeClass('d-none'); + }, 'json').fail(function () { + $out.html('
Erreur réseau (sonde custom).
'); + }).always(function () { + $btn.prop('disabled', false); + }); + }); + var objPushPreview = null; function renderBlockedPanel(res) { diff --git a/php/chronotrack_api/fx_chronotrack_sync.php b/php/chronotrack_api/fx_chronotrack_sync.php index 5438e48..9bb98f4 100644 --- a/php/chronotrack_api/fx_chronotrack_sync.php +++ b/php/chronotrack_api/fx_chronotrack_sync.php @@ -915,6 +915,170 @@ function fxChronotrackApiSyncProbePush($intEveId) { ); } +/** + * MSIN-4328 — sonde « custom field » : tente comme un import Custom + * (POST entry avec custom_Ms1Probe*) + lit l’entry, + teste endpoints question. + */ +function fxChronotrackApiSyncProbeCustomField($intEveId) { + $arrConfig = fxChronotrackApiConfigGet($intEveId); + if ($arrConfig === null) { + return array('state' => 'error', 'message' => 'Événement non lié à ChronoTrack'); + } + if (!fxChronotrackApiSettingsConfigured()) { + return array('state' => 'error', 'message' => fxChronotrackApiSettingsErrorMessage()); + } + + $intCtEventId = intval($arrConfig['ct_event_id']); + $arrRaceMap = fxChronotrackApiRaceMapGetForEvent($intEveId); + $tabParticipants = fxChronotrackApiSyncLoadMs1Participants($intEveId); + $arrClass = fxChronotrackApiSyncClassifyParticipants($tabParticipants, $arrRaceMap, $intCtEventId); + + if (count($arrClass['transferable']) === 0) { + return array('state' => 'error', 'message' => 'Aucun participant transférable pour la sonde custom.'); + } + + // Préférer une entry déjà liée CT (maj légère, pas de création complète) + $arrPick = null; + foreach ($arrClass['transferable'] as $arrItem) { + if (intval($arrItem['row']['ct_entry_id'] ?? 0) > 0) { + $arrPick = $arrItem; + break; + } + } + if ($arrPick === null) { + $arrPick = $arrClass['transferable'][0]; + } + + $arrRow = $arrPick['row']; + $strExternalId = (string)($arrPick['payload']['external_id'] ?? ''); + $intCtEntryId = intval($arrRow['ct_entry_id'] ?? 0); + $strFieldKey = 'custom_Ms1Probe'; + $strValue = 'ms1-probe-' . date('YmdHis'); + + $tabQuestionEndpoints = array(); + foreach (array('question', 'custom_question', 'survey_question', 'reg_question') as $strEp) { + $arrG = fxChronotrackApiOAuthApiGet('event/' . $intCtEventId . '/' . $strEp); + $tabQuestionEndpoints[] = array( + 'path' => 'event/' . $intCtEventId . '/' . $strEp, + 'http_code' => intval($arrG['http']['http_code'] ?? 0), + 'state' => $arrG['state'] ?? 'error', + 'message' => isset($arrG['message']) ? substr((string)$arrG['message'], 0, 200) : '', + ); + } + + // Corps minimal : identifier + champ custom (mimique import « Custom ») + $arrProbePayload = array( + 'external_id' => $strExternalId, + $strFieldKey => $strValue, + ); + if ($intCtEntryId > 0) { + // Certaines maj CT acceptent l’id numérique + $arrProbePayload['entry_id'] = (string)$intCtEntryId; + } + + $arrPost = fxChronotrackApiSyncPostEntries($intCtEventId, array($arrProbePayload)); + + $mixReadbackValue = null; + $blnFieldPresent = false; + $arrReadback = null; + $strReadbackHow = ''; + + if ($intCtEntryId > 0) { + $arrGet = fxChronotrackApiOAuthApiGet('entry/' . $intCtEntryId); + $strReadbackHow = 'GET entry/' . $intCtEntryId; + if (($arrGet['state'] ?? '') === 'ok' && is_array($arrGet['json'] ?? null)) { + $arrReadback = $arrGet['json']; + // CT peut wrapper { entry: {...} } ou renvoyer l’objet direct + if (isset($arrReadback['entry']) && is_array($arrReadback['entry'])) { + $arrReadback = $arrReadback['entry']; + } elseif (isset($arrReadback['event_entry'][0]) && is_array($arrReadback['event_entry'][0])) { + $arrReadback = $arrReadback['event_entry'][0]; + } + if (array_key_exists($strFieldKey, $arrReadback)) { + $blnFieldPresent = true; + $mixReadbackValue = $arrReadback[$strFieldKey]; + } + } + } + + if (!$blnFieldPresent) { + // Repli : 1re page entries et recherche par external_id + $arrList = fxChronotrackApiOAuthApiGet( + 'event/' . $intCtEventId . '/entry', + array('page' => 1, 'page_size' => 50) + ); + if ($strReadbackHow === '') { + $strReadbackHow = 'GET event/.../entry page1'; + } else { + $strReadbackHow .= ' + list page1'; + } + $tabEnt = array(); + if (($arrList['state'] ?? '') === 'ok' && is_array($arrList['json'] ?? null)) { + $tabEnt = fxChronotrackApiNormalizeEntityList($arrList['json'], 'entry'); + if (count($tabEnt) === 0 && isset($arrList['json']['event_entry']) && is_array($arrList['json']['event_entry'])) { + $tabEnt = $arrList['json']['event_entry']; + } + } + foreach ($tabEnt as $arrEnt) { + if (!is_array($arrEnt)) { + continue; + } + $strExt = trim((string)( + $arrEnt['entry_external_id'] ?? $arrEnt['external_id'] ?? '' + )); + if ($strExt !== $strExternalId) { + continue; + } + $arrReadback = $arrEnt; + if (array_key_exists($strFieldKey, $arrEnt)) { + $blnFieldPresent = true; + $mixReadbackValue = $arrEnt[$strFieldKey]; + } + break; + } + } + + $strVerdict = 'POST ok mais champ absent au relecture (CT n’a pas créé le custom comme à l’import).'; + $strState = 'error'; + if (($arrPost['state'] ?? '') !== 'ok') { + $strVerdict = 'POST refusé — ' . (string)($arrPost['message'] ?? 'erreur'); + $strState = 'error'; + } elseif ($blnFieldPresent && (string)$mixReadbackValue === $strValue) { + $strVerdict = 'OK — CT a accepté et conservé ' . $strFieldKey . ' (création / slot custom plausible).'; + $strState = 'ok'; + } elseif ($blnFieldPresent) { + $strVerdict = 'POST ok, champ présent mais valeur différente: ' + . json_encode($mixReadbackValue, JSON_UNESCAPED_UNICODE); + $strState = 'partial'; + } + + fxChronotrackApiSyncLog( + $intEveId, + intval($arrRow['par_id'] ?? 0), + 'probe_custom', + ($strState === 'ok') ? 'ok' : 'error', + $strVerdict + ); + + return array( + 'state' => $strState, + 'message' => $strVerdict, + 'ct_event_id' => $intCtEventId, + 'par_id' => intval($arrRow['par_id'] ?? 0), + 'external_id' => $strExternalId, + 'ct_entry_id' => $intCtEntryId, + 'field_key' => $strFieldKey, + 'field_value_sent' => $strValue, + 'field_present' => $blnFieldPresent, + 'field_value_read' => $mixReadbackValue, + 'payload' => $arrProbePayload, + 'post_http_code' => intval($arrPost['http']['http_code'] ?? 0), + 'post_response' => substr(trim((string)($arrPost['http']['body'] ?? '')), 0, 1500), + 'readback_how' => $strReadbackHow, + 'question_endpoints' => $tabQuestionEndpoints, + ); +} + /** * Sonde perf — teste 10 / 50 / 100 / 200 entries (upsert, mêmes gens). MSIN-4328 * Évite un push complet pour calibrer la taille de lot. diff --git a/superadm/ajax_chronotrack_api.php b/superadm/ajax_chronotrack_api.php index 49e3329..1245a90 100644 --- a/superadm/ajax_chronotrack_api.php +++ b/superadm/ajax_chronotrack_api.php @@ -170,6 +170,16 @@ switch ($strAction) { $tabRetour = fxChronotrackApiSyncProbePush($intEveId); break; + case 'sync_probe_custom': + // MSIN-4328 — sonde création champ custom (comme import Custom) + if ($intEveId <= 0) { + $tabRetour = array('state' => 'error', 'message' => 'eve_id manquant'); + break; + } + set_time_limit(120); + $tabRetour = fxChronotrackApiSyncProbeCustomField($intEveId); + break; + case 'sync_push_prepare': if ($intEveId <= 0) { $tabRetour = array('state' => 'error', 'message' => 'eve_id manquant');