MSIN-4574 — Increment version code to 4.72.981 and enhance ChronoTrack API by improving error messaging for bib conflicts, adding functionality to clear existing bib holders, and refining UI messages for better clarity. This update aims to streamline the handling of bib conflicts and improve user experience during synchronization processes.

This commit is contained in:
2026-08-11 15:53:17 -04:00
parent 5a3078f3de
commit f458ec1dd4
3 changed files with 293 additions and 22 deletions

View File

@ -494,9 +494,11 @@ function fxChronotrackApiAdminRenderPushPanel($intEveId, $blnClosed = false, $ar
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 %). '
. '<strong>Vérifier alignement</strong> = entries CT hors MS1 (annulés, etc.).</p>';
echo '<div id="ct_api_push_result" class="ct-api-push-result small mb-3"></div>';
echo '<div id="ct_api_reconcile_wrap" class="mb-3 d-none"></div>';
. '<strong>Vérifier alignement</strong> = liste orphelins CT (résultat dans le cadre Alignement ci-dessous — '
. '<em>pas</em> le même bloc que le transfert).</p>';
echo '<div id="ct_api_push_result" class="ct-api-push-result small mb-3" '
. 'aria-live="polite" data-ct-result-kind="push"></div>';
echo '<div id="ct_api_reconcile_wrap" class="mb-3 d-none" data-ct-result-kind="alignement"></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">';
@ -1663,25 +1665,43 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
// Section Exclus : reste fermée — compteur dans lentê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 = '<div class="' + strClass + '"><strong>' + escHtml(strHeading) + '</strong> — '
var strHtml = '<div class="border rounded p-2 ' + (strClass === 'text-success' ? 'border-success' : (strClass === 'text-warning' ? 'border-warning' : 'border-danger')) + '">';
strHtml += '<div class="' + strClass + '"><strong>' + escHtml(strHeading) + '</strong> — '
+ escHtml((res && res.message) ? res.message : 'Erreur') + '</div>';
strHtml += '<p class="small text-muted mb-0 mt-1">Ce bloc = résultat du bouton Envoyer / Tout renvoyer. '
+ 'Alignement = cadre séparé plus bas.</p>';
if (res && res.errors && res.errors.length) {
strHtml += '<p class="small text-danger mb-1 mt-2">Erreurs API ChronoTrack :</p>';
strHtml += '<p class="small text-danger mb-1 mt-2">Erreurs pendant ce transfert :</p>';
strHtml += '<ul class="text-danger small mb-0 pl-3">';
$.each(res.errors, function (_, msg) {
strHtml += '<li>' + escHtml(msg) + '</li>';
strHtml += '<li>' + escHtml(humanizePushError(msg)) + '</li>';
});
strHtml += '</ul>';
}
strHtml += '</div>';
$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 = '<div class="border rounded p-2 bg-light">';
var html = '<div class="border rounded p-2 bg-light border-info">';
html += '<div class="d-flex align-items-center justify-content-between mb-2">';
html += '<strong>Alignement ChronoTrack</strong>';
html += '<strong>Alignement ChronoTrack</strong> <span class="badge badge-info">pas le transfert</span>';
html += '<button type="button" class="btn btn-link btn-sm p-0" id="ct_api_btn_reconcile_close">Fermer</button>';
html += '</div>';
html += '<p class="small text-muted mb-2">Entries présentes sur ChronoTrack mais absentes des éligibles MS1 '
html += '<p class="small text-muted mb-2">Lecture seule + retrait optionnel. '
+ 'Ce cadre nest <em>pas</em> 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).</p>';
if (!res || res.state !== 'ok') {
html += '<div class="text-danger small">' + escHtml((res && res.message) ? res.message : 'Erreur') + '</div>';
@ -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(

View File

@ -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 — dabord 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;

View File

@ -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');