From f458ec1dd4509f501ec9145520e2ffe7d81cebc1 Mon Sep 17 00:00:00 2001 From: stephan Date: Tue, 11 Aug 2026 15:53:17 -0400 Subject: [PATCH] =?UTF-8?q?MSIN-4574=20=E2=80=94=20Increment=20version=20c?= =?UTF-8?q?ode=20to=204.72.981=20and=20enhance=20ChronoTrack=20API=20by=20?= =?UTF-8?q?improving=20error=20messaging=20for=20bib=20conflicts,=20adding?= =?UTF-8?q?=20functionality=20to=20clear=20existing=20bib=20holders,=20and?= =?UTF-8?q?=20refining=20UI=20messages=20for=20better=20clarity.=20This=20?= =?UTF-8?q?update=20aims=20to=20streamline=20the=20handling=20of=20bib=20c?= =?UTF-8?q?onflicts=20and=20improve=20user=20experience=20during=20synchro?= =?UTF-8?q?nization=20processes.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- php/chronotrack_api/fx_chronotrack_admin.php | 56 +++- php/chronotrack_api/fx_chronotrack_sync.php | 257 ++++++++++++++++++- php/inc_settings.php | 2 +- 3 files changed, 293 insertions(+), 22 deletions(-) diff --git a/php/chronotrack_api/fx_chronotrack_admin.php b/php/chronotrack_api/fx_chronotrack_admin.php index f100504a..d039d7d5 100644 --- a/php/chronotrack_api/fx_chronotrack_admin.php +++ b/php/chronotrack_api/fx_chronotrack_admin.php @@ -494,9 +494,11 @@ function fxChronotrackApiAdminRenderPushPanel($intEveId, $blnClosed = false, $ar echo ''; echo '

Envoyer = nouveaux / modifiés. ' . 'Tout renvoyer = tous les éligibles (même barre %). ' - . 'Vérifier alignement = entries CT hors MS1 (annulés, etc.).

'; - echo '
'; - echo '
'; + . 'Vérifier alignement = liste orphelins CT (résultat dans le cadre Alignement ci-dessous — ' + . 'pas le même bloc que le transfert).

'; + echo '
'; + echo '
'; echo '
'; echo '
'; @@ -1663,25 +1665,43 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed // Section Exclus : reste fermée — compteur dans l’entête } + function humanizePushError(strMsg) { + strMsg = String(strMsg || ''); + var mSwap = strMsg.match(/RETRY_BIB bloqué \(swap max\)\s*external_id=(\S+)\s*dossard=(\S*)/i); + if (mSwap) { + return 'Dossard ' + (mSwap[2] || '?') + + ' encore pris après plusieurs essais auto (ext ' + mSwap[1] + + '). Le système libère le détenteur sur ChronoTrack — relancer Tout renvoyer.'; + } + if (/RETRY_BIB/i.test(strMsg)) { + return strMsg.replace(/^RETRY_BIB\s*/i, 'Conflit dossard (auto) — '); + } + return strMsg; + } + function renderPushResult(res, strTitle) { var $res = $('#ct_api_push_result'); - var strHeading = strTitle || 'Résultat'; + var strHeading = strTitle || 'Résultat — Transfert MS1 → ChronoTrack'; 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 = '
' + escHtml(strHeading) + ' — ' + var strHtml = '
'; + strHtml += '
' + escHtml(strHeading) + ' — ' + escHtml((res && res.message) ? res.message : 'Erreur') + '
'; + strHtml += '

Ce bloc = résultat du bouton Envoyer / Tout renvoyer. ' + + 'Alignement = cadre séparé plus bas.

'; if (res && res.errors && res.errors.length) { - strHtml += '

Erreurs API ChronoTrack :

'; + strHtml += '

Erreurs pendant ce transfert :

'; strHtml += '
    '; $.each(res.errors, function (_, msg) { - strHtml += '
  • ' + escHtml(msg) + '
  • '; + strHtml += '
  • ' + escHtml(humanizePushError(msg)) + '
  • '; }); strHtml += '
'; } + strHtml += '
'; $res.html(strHtml); } @@ -1984,12 +2004,14 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed : ((res && res.orphans) ? res.orphans.length : 0); var tabRows = (res && res.orphans) ? res.orphans : []; var intShowMax = 80; - var html = '
'; + var html = '
'; html += '
'; - html += 'Alignement ChronoTrack'; + html += 'Alignement ChronoTrack pas le transfert'; html += ''; html += '
'; - html += '

Entries présentes sur ChronoTrack mais absentes des éligibles MS1 ' + html += '

Lecture seule + retrait optionnel. ' + + 'Ce cadre n’est pas le résultat de Tout renvoyer. ' + + 'Entries présentes sur ChronoTrack mais absentes des éligibles MS1 ' + '(annulés, transferts, fantômes après changement d’équipe / coureur).

'; if (!res || res.state !== 'ok') { html += '
' + escHtml((res && res.message) ? res.message : 'Erreur') + '
'; @@ -2383,7 +2405,7 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed state: strState, message: strFinal, errors: tabErrAll.slice(0, 20) - }); + }, blnForceFull ? 'Résultat — Tout renvoyer' : 'Résultat — Envoyer'); if ($status.length) { var strCls = strState === 'ok' ? 'ct-api-push-status--ok' : (strState === 'partial' ? 'ct-api-push-status--todo' : ''); @@ -2503,7 +2525,7 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed } if (res.total === 0 && res.state === 'error') { stopElapsedTimer(); - renderPushResult(res); + renderPushResult(res, blnForceFull ? 'Résultat — Tout renvoyer' : 'Résultat — Envoyer'); $('#ct_api_btn_push, #ct_api_btn_push_full, #ct_api_btn_push_refresh, #ct_api_btn_ct_count, #ct_api_btn_logs, #ct_api_btn_reconcile') .prop('disabled', false); blnPushRunning = false; @@ -2614,7 +2636,10 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed } if (!prep || prep.state !== 'ok') { stopElapsedTimer(); - renderPushResult(prep || { state: 'error', message: 'Préparation échouée' }); + renderPushResult( + prep || { state: 'error', message: 'Préparation échouée' }, + blnForceFull ? 'Résultat — Tout renvoyer' : 'Résultat — Envoyer' + ); if ($status.length) { $status.removeClass('ct-api-push-status--ok ct-api-push-status--todo ct-api-push-status--loading') .html( @@ -2644,7 +2669,10 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed 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 }); + renderPushResult( + { state: 'error', message: strMsg }, + blnForceFull ? 'Résultat — Tout renvoyer' : 'Résultat — Envoyer' + ); if ($status.length) { $status.removeClass('ct-api-push-status--ok ct-api-push-status--todo ct-api-push-status--loading') .html( diff --git a/php/chronotrack_api/fx_chronotrack_sync.php b/php/chronotrack_api/fx_chronotrack_sync.php index 65d793f4..202c6e1a 100644 --- a/php/chronotrack_api/fx_chronotrack_sync.php +++ b/php/chronotrack_api/fx_chronotrack_sync.php @@ -579,6 +579,163 @@ function fxChronotrackApiSyncPayloadClearBib(array $arrPayload) { return $arrOut; } +/** + * MSIN-4574 — comparer dossards CT/MS1 (2342 == 02342). + */ +function fxChronotrackApiSyncBibNorm($strBib) { + $str = trim((string)$strBib); + if ($str === '') { + return ''; + } + if (ctype_digit($str)) { + return (string)intval($str); + } + return strtolower($str); +} + +/** + * MSIN-4574 — conflit bib : trouver qui détient déjà ce dossard sur CT et le vider. + * Sans ça, on ne libère que « nous » → boucle swap max. Automatique, pas manuel. + * + * @return array{cleared:int,holders:array,message:string} + */ +function fxChronotrackApiSyncClearOtherBibHolderOnCt( + $intCtEventId, + $strBib, + $strExceptExternalId = '', + $strExceptEntryId = '', + $intEveId = 0, + $blnForceRefresh = false +) { + $intCtEventId = intval($intCtEventId); + $intEveId = intval($intEveId); + $strBibWanted = fxChronotrackApiSyncBibNorm($strBib); + $strExceptExternalId = trim((string)$strExceptExternalId); + $strExceptEntryId = trim((string)$strExceptEntryId); + $arrOut = array('cleared' => 0, 'holders' => array(), 'message' => ''); + + if ($intCtEventId <= 0 || $strBibWanted === '') { + $arrOut['message'] = 'paramètres manquants'; + return $arrOut; + } + + static $tabEntitiesByEvent = array(); + $strCacheKey = (string)$intCtEventId; + if ($blnForceRefresh || !isset($tabEntitiesByEvent[$strCacheKey])) { + $arrFetch = fxChronotrackApiSyncFetchAllCtEntryEntities($intCtEventId); + if (($arrFetch['state'] ?? '') !== 'ok') { + $arrOut['message'] = (string)($arrFetch['message'] ?? 'lecture CT échouée'); + return $arrOut; + } + $tabEntitiesByEvent[$strCacheKey] = is_array($arrFetch['entities'] ?? null) + ? $arrFetch['entities'] + : array(); + } + + $tabEntities = &$tabEntitiesByEvent[$strCacheKey]; + foreach ($tabEntities as $intIdx => $arrEntity) { + if (!is_array($arrEntity)) { + continue; + } + $strTheirBib = fxChronotrackApiSyncBibNorm( + fxChronotrackApiRaceField($arrEntity, array('bib', 'bib_number', 'entry_bib')) + ); + if ($strTheirBib === '' || $strTheirBib !== $strBibWanted) { + continue; + } + $strTheirEntryId = fxChronotrackApiEntityId($arrEntity); + $strTheirExt = fxChronotrackApiEntityExternalId($arrEntity); + if ($strExceptEntryId !== '' && $strTheirEntryId !== '' && $strTheirEntryId === $strExceptEntryId) { + continue; + } + if ($strExceptExternalId !== '' && $strTheirExt !== '' && $strTheirExt === $strExceptExternalId) { + continue; + } + if ($strTheirEntryId === '') { + continue; + } + + $arrClearPayload = array( + 'entry_id' => $strTheirEntryId, + 'external_id' => $strTheirExt !== '' ? $strTheirExt : '0', + 'event_id' => $intCtEventId, + 'bib' => '', + ); + $intRaceId = intval(fxChronotrackApiRaceField($arrEntity, array('race_id', 'ct_race_id'))); + if ($intRaceId > 0) { + $arrClearPayload['race_id'] = $intRaceId; + } + $strStatus = trim((string)fxChronotrackApiRaceField( + $arrEntity, + array('entry_status', 'status', 'reg_status') + )); + if ($strStatus !== '') { + $arrClearPayload['entry_status'] = $strStatus; + $arrClearPayload['status'] = $strStatus; + } + + $arrWrite = fxChronotrackApiSyncPutEntry($strTheirEntryId, $arrClearPayload); + $arrOut['holders'][] = array( + 'entry_id' => $strTheirEntryId, + 'external_id' => $strTheirExt, + 'name' => fxChronotrackApiEntityEntryLabel($arrEntity), + 'ok' => (($arrWrite['state'] ?? '') === 'ok'), + 'message' => (string)($arrWrite['message'] ?? ''), + ); + if (($arrWrite['state'] ?? '') === 'ok') { + $arrOut['cleared']++; + // Cache : dossard libéré pour les prochains conflits du même job + $tabEntities[$intIdx]['bib'] = ''; + if ($intEveId > 0) { + fxChronotrackApiSyncLog( + $intEveId, + 0, + 'bib_holder_clear', + 'ok', + 'dossard=' . $strBibWanted + . ' retiré de entry_id=' . $strTheirEntryId + . ' external_id=' . $strTheirExt + . ' nom=' . fxChronotrackApiEntityEntryLabel($arrEntity) + ); + } + } elseif ($intEveId > 0) { + fxChronotrackApiSyncLog( + $intEveId, + 0, + 'bib_holder_clear', + 'error', + 'dossard=' . $strBibWanted + . ' entry_id=' . $strTheirEntryId + . ' — ' . ($arrWrite['message'] ?? 'échec') + ); + } + } + + $arrOut['message'] = ($arrOut['cleared'] > 0) + ? ('détenteur(s) libéré(s)=' . $arrOut['cleared']) + : 'aucun autre détenteur trouvé'; + return $arrOut; +} + +/** + * MSIN-4574 — message d’échec bib lisible (UI / logs), pas du jargon RETRY_BIB. + */ +function fxChronotrackApiSyncHumanBibBlockedMessage(array $arrMeta, $strBib) { + $strName = trim((string)($arrMeta['name'] ?? '')); + $strExt = trim((string)($arrMeta['external_id'] ?? '')); + $strBib = trim((string)$strBib); + $str = 'Dossard ' . ($strBib !== '' ? $strBib : '?') + . ' encore pris après libération auto du détenteur ChronoTrack'; + if ($strName !== '') { + $str .= ' — ' . $strName; + } + if ($strExt !== '') { + $str .= ' (ext ' . $strExt . ')'; + } + $str .= '. Relancer Tout renvoyer — le système réessaie automatiquement.'; + return $str; +} + function fxChronotrackApiSyncParticipantLabel(array $arrRow) { return trim(trim((string)($arrRow['par_prenom'] ?? '')) . ' ' . trim((string)($arrRow['par_nom'] ?? ''))); } @@ -1688,8 +1845,41 @@ function fxChronotrackApiSyncPushOneEntryWithBibConflictHandling( } $strErr = (string)($arrFirst['message'] ?? 'Erreur entry'); - // Conflit dossard + on voulait en envoyer un → libérer puis 2e passe + // Conflit dossard + on voulait en envoyer un → libérer le détenteur CT, réessayer, sinon clear + 2e passe if ($strBibWanted !== '' && fxChronotrackApiSyncIsBibConflictMessage($strErr)) { + // MSIN-4574 — auto : vider le dossard chez QUI le détient déjà sur CT (pas seulement chez nous) + fxChronotrackApiSyncClearOtherBibHolderOnCt( + $intCtEventId, + $strBibWanted, + (string)($arrMeta['external_id'] ?? ''), + $strEntryId, + $intEveId, + false + ); + // Réessai immédiat avec le dossard voulu (souvent OK si le détenteur était un orphelin / autre) + if ($strEntryId !== '') { + $arrAfterHolder = fxChronotrackApiSyncPutEntry($strEntryId, $arrPayload); + } else { + $arrAfterHolder = fxChronotrackApiSyncPostEntries( + $intCtEventId, + array(fxChronotrackApiSyncPayloadForPost($arrPayload)) + ); + } + if (($arrAfterHolder['state'] ?? '') === 'ok') { + if ($intEveId > 0) { + if ($strMethod === 'POST' && $strEntryId === '') { + fxChronotrackApiSyncApplyEntryIdsFromApiJson($arrAfterHolder['json'] ?? null, array($arrMeta)); + } + fxChronotrackApiSyncLogPushEntryOk( + $intEveId, + $arrMeta, + $arrPayload, + $strMethod . '+holder_clear' + ); + } + return array('ok' => 1, 'err' => 0, 'bib_retry' => array(), 'method' => $strMethod); + } + $arrCleared = fxChronotrackApiSyncPayloadClearBib($arrPayload); if ($strEntryId !== '') { $arrClearRes = fxChronotrackApiSyncPutEntry($strEntryId, $arrCleared); @@ -1837,8 +2027,32 @@ function fxChronotrackApiSyncPostEntriesAdaptive($intCtEventId, array $tabEntrie $strBibWanted = isset($arrPayload['bib']) ? trim((string)$arrPayload['bib']) : ''; if ($strBibWanted !== '' && fxChronotrackApiSyncIsBibConflictMessage($strOneErr)) { - // MSIN-4328 — conflit sans entry_id : tenter clear bib='' puis sans champ bib + // MSIN-4574 — d’abord libérer le détenteur CT (auto) $strEntryIdOne = trim((string)($arrPayload['entry_id'] ?? '')); + fxChronotrackApiSyncClearOtherBibHolderOnCt( + $intCtEventId, + $strBibWanted, + (string)($arrM['external_id'] ?? ''), + $strEntryIdOne, + $intEveId, + false + ); + $arrAfterHolder = ($strEntryIdOne !== '') + ? fxChronotrackApiSyncPutEntry($strEntryIdOne, $arrPayload) + : fxChronotrackApiSyncPostEntries( + $intCtEventId, + array(fxChronotrackApiSyncPayloadForPost($arrPayload)) + ); + if (($arrAfterHolder['state'] ?? '') === 'ok') { + if ($intEveId > 0) { + if ($strEntryIdOne === '') { + fxChronotrackApiSyncApplyEntryIdsFromApiJson($arrAfterHolder['json'] ?? null, array($arrM)); + } + fxChronotrackApiSyncLogPushEntryOk($intEveId, $arrM, $arrPayload, 'POST+holder_clear'); + } + return array('ok' => 1, 'err' => 0, 'bib_retry' => array()); + } + // MSIN-4328 — conflit sans entry_id : tenter clear bib='' puis sans champ bib if ($strEntryIdOne !== '') { $arrClearRes = fxChronotrackApiSyncPutEntry( $strEntryIdOne, @@ -3608,16 +3822,45 @@ function fxChronotrackApiSyncPushBibsPhase($intEveId, $arrOptions = array()) { $intBibRound++; $arrJob['bib_round'] = $intBibRound; } - // Après trop de tours sans drain : abandonner le reste comme erreurs + // Après trop de tours sans drain : 1 dernière passe (refresh CT + clear détenteur), sinon erreur claire if ($intBibRound >= 5 && count($tabRequeue) > 0 && count($tabRest) === 0) { + $tabStillBlocked = array(); foreach ($tabRequeue as $arrLeft) { - $intErr++; + if (!is_array($arrLeft) || !is_array($arrLeft['payload'] ?? null)) { + continue; + } + $arrPayloadL = $arrLeft['payload']; $arrMetaL = is_array($arrLeft['meta'] ?? null) ? $arrLeft['meta'] : array(); - $strBibL = isset($arrLeft['payload']['bib']) ? trim((string)$arrLeft['payload']['bib']) : ''; - $tabErrors[] = 'RETRY_BIB bloqué (swap max) external_id=' - . ($arrMetaL['external_id'] ?? '') . ' dossard=' . $strBibL; + $strBibL = isset($arrPayloadL['bib']) ? trim((string)$arrPayloadL['bib']) : ''; + $strEntryL = trim((string)($arrPayloadL['entry_id'] ?? '')); + if ($strBibL !== '') { + fxChronotrackApiSyncClearOtherBibHolderOnCt( + $intCtEventId, + $strBibL, + (string)($arrMetaL['external_id'] ?? ''), + $strEntryL, + $intEveId, + true + ); + $arrLast = fxChronotrackApiSyncPushOneEntryWithBibConflictHandling( + $intCtEventId, + $arrPayloadL, + $arrMetaL, + $intEveId + ); + if (intval($arrLast['ok'] ?? 0) > 0 && empty($arrLast['bib_retry'])) { + $intOk++; + continue; + } + } + $intErr++; + $tabStillBlocked[] = $arrLeft; + if (count($tabErrors) < 12) { + $tabErrors[] = fxChronotrackApiSyncHumanBibBlockedMessage($arrMetaL, $strBibL); + } } $tabNewPending = array(); + unset($tabStillBlocked); } $arrJob['bib_retry'] = $tabNewPending; diff --git a/php/inc_settings.php b/php/inc_settings.php index dd0578d2..65d142bd 100644 --- a/php/inc_settings.php +++ b/php/inc_settings.php @@ -7,7 +7,7 @@ * Constantes * * **************/ -define('_VERSION_CODE', '4.72.980'); // MSIN-4574 — pays CT = ISO-2 (CA) + province QC (calibrage export) +define('_VERSION_CODE', '4.72.981'); // MSIN-4574 — conflit bib: clear détenteur CT auto + messages UI clairs define('_DATE_CODE', '2026-08-11'); //MSIN-4290 define('QR_SECRET_KEY', 'ms1_qr_2026_cle_secrete_longue_et_fixe');