diff --git a/ajax_bib_range.php b/ajax_bib_range.php index 9a390d3..a74ee10 100644 --- a/ajax_bib_range.php +++ b/ajax_bib_range.php @@ -31,6 +31,7 @@ $arrBibAjaxActions = [ 'refresh_ranges_data', 'batch_go', 'batch_apply', + 'batch_apply_chunk', 'reset_preview', 'reset_apply', 'save_team_mode', @@ -515,46 +516,7 @@ if ($action == 'batch_apply') { $exec = fxProcessBatchAssignments($epr_id, $participants, $tabRanges, 'execute'); - $tabRangesInfos = fxInfosBibRange($epr_id); - $ids = array_map('intval', $ranges); - - $tabRangesInfos = array_filter($tabRangesInfos, function($r) use ($ids) { - return in_array((int)$r['epr_bib_id'], $ids); - }); - - $summary = fxBuildBatchSummaryFromRanges($tabRangesInfos, $participants, $epr_id); - - $assigned = count($exec); - $total = count($participants); - $remaining = max(0, $total - $assigned); - $status = 'success'; - - if ($assigned === 0) { - $status = 'error'; - } elseif ($assigned < $total) { - $status = 'partial'; - } - if ($assigned === 0) { - - $info = fxBibMsg('bib_v4_ajax_assign_none'); - - } elseif ($assigned === $total) { - - $info = fxBibMsg('bib_v4_ajax_assign_full', $assigned); - - } else { - - $info = fxBibMsg('bib_v4_ajax_assign_partial', $assigned, $remaining); - } - $html = renderBibView( - $epr_id, - 0, - $strLangue, - 'execute', - $exec, - fxBibTexte('bib_v4_ajax_title_assign'), - $info,$status - ); + $html = fxBibBuildBatchApplyHtml($epr_id, $exec, count($participants), $strLangue); echo json_encode([ 'success' => true, @@ -563,6 +525,98 @@ if ($action == 'batch_apply') { exit; } + +// MSIN-4433 — GO batch par paquets : progression réelle + relecture BD entre paquets. +if ($action == 'batch_apply_chunk') { + + $epr_id = intval($_POST['epr_id'] ?? 0); + $ranges = json_decode($_POST['ranges'] ?? '[]', true); + $ba_id = intval($_POST['ba_id'] ?? 0); + $intChunkSize = (int)($_POST['chunk_size'] ?? 50); + $intTotalInitial = (int)($_POST['total_initial'] ?? 0); + $blnBatchReset = ((int)($_POST['batch_reset'] ?? 0) === 1); + + if ($intChunkSize < 1) { + $intChunkSize = 50; + } + if ($intChunkSize > 100) { + $intChunkSize = 100; + } + + $lockCheck = fxBibAssertRangesUnlocked($epr_id, is_array($ranges) ? $ranges : []); + if (!$lockCheck['success']) { + echo json_encode([ + 'success' => false, + 'message' => $lockCheck['message'] ?? fxBibMsg('bib_v4_ajax_range_locked') + ]); + exit; + } + + $strSessionKey = fxBibBatchExecSessionKey($epr_id); + + if ($blnBatchReset) { + $_SESSION[$strSessionKey] = []; + } elseif (!isset($_SESSION[$strSessionKey]) || !is_array($_SESSION[$strSessionKey])) { + $_SESSION[$strSessionKey] = []; + } + + $participants = fxGetParticipantsForBatchAssign($epr_id, $ba_id); + + if ($intTotalInitial <= 0) { + $intTotalInitial = count($participants) + count($_SESSION[$strSessionKey]); + } + + if (empty($participants)) { + $execAll = $_SESSION[$strSessionKey]; + unset($_SESSION[$strSessionKey]); + + $html = fxBibBuildBatchApplyHtml($epr_id, $execAll, $intTotalInitial, $strLangue); + + echo json_encode([ + 'success' => true, + 'complete' => true, + 'total_initial' => $intTotalInitial, + 'processed' => $intTotalInitial, + 'assigned_total' => count($execAll), + 'assigned_chunk' => 0, + 'html' => $html + ]); + exit; + } + + $tabChunk = array_slice($participants, 0, $intChunkSize); + $tabRanges = fxGetRangesForBatchAssign($epr_id, $ranges); + $execChunk = fxProcessBatchAssignments($epr_id, $tabChunk, $tabRanges, 'execute'); + + $_SESSION[$strSessionKey] = array_merge($_SESSION[$strSessionKey], $execChunk); + + $intRemaining = count(fxGetParticipantsForBatchAssign($epr_id, $ba_id)); + $intProcessed = max(0, $intTotalInitial - $intRemaining); + $blnComplete = ($intRemaining === 0); + + // MSIN-4433 — Dossards épuisés : clôturer si aucune progression sur ce paquet. + if (!$blnComplete && empty($execChunk)) { + $blnComplete = true; + } + + $arrResponse = [ + 'success' => true, + 'complete' => $blnComplete, + 'total_initial' => $intTotalInitial, + 'processed' => $intProcessed, + 'assigned_total' => count($_SESSION[$strSessionKey]), + 'assigned_chunk' => count($execChunk), + ]; + + if ($blnComplete) { + $execAll = $_SESSION[$strSessionKey]; + unset($_SESSION[$strSessionKey]); + $arrResponse['html'] = fxBibBuildBatchApplyHtml($epr_id, $execAll, $intTotalInitial, $strLangue); + } + + echo json_encode($arrResponse); + exit; +} // ===================== // RESET PREVIEW // ===================== diff --git a/css/ms1-loader.css b/css/ms1-loader.css index e00a612..74557af 100644 --- a/css/ms1-loader.css +++ b/css/ms1-loader.css @@ -68,3 +68,33 @@ font-size: 1.1rem; font-weight: 600; } + +.ms1-loader__progress { + width: 100%; + min-width: 220px; + margin-top: 0.25rem; +} + +.ms1-loader__progress-track { + width: 100%; + height: 8px; + background: #e8e8e8; + border-radius: 4px; + overflow: hidden; +} + +.ms1-loader__progress-fill { + height: 100%; + width: 0; + background: linear-gradient(90deg, #2e7d32, #43a047); + border-radius: 4px; + transition: width 0.25s ease; +} + +.ms1-loader__progress-pct { + display: block; + margin-top: 0.35rem; + font-size: 0.85rem; + font-weight: 600; + color: #555; +} diff --git a/inc_footer_scripts.php b/inc_footer_scripts.php index 4226943..369daa7 100644 --- a/inc_footer_scripts.php +++ b/inc_footer_scripts.php @@ -2393,6 +2393,165 @@ if ($strLangue == 'fr') { }); } + var BIB_BATCH_CHUNK_SIZE = 50; + + function bibBatchProgressShow(processed, total) { + var intTotal = total > 0 ? total : 1; + var msg = bibJsFmt('jsBatchProgress', processed, intTotal); + if (window.Ms1Loader && Ms1Loader.showProgress) { + Ms1Loader.showProgress(processed, intTotal, msg); + } else { + bibLoaderShow(); + } + } + + function bibBatchProgressHide() { + if (window.Ms1Loader && Ms1Loader.hide) { + Ms1Loader.hide(); + } else { + bibLoaderHide(); + } + } + + function bibAfterBatchGoSuccess(row, data, isResetMode) { + let existing = document.querySelector('.epr-row-view'); + if (existing) { + existing.remove(); + } + + if (data.html) { + row.insertAdjacentHTML('afterend', data.html); + } + + if (isResetMode) { + row.classList.remove('reset-mode'); + row.dataset.mode = ''; + + let btnReset = row.querySelector('.btn-reset'); + if (btnReset) { + btnReset.textContent = row.dataset.resetLabel || btnReset.textContent; + } + } + row.classList.remove('batch-mode'); + + let container = row.querySelector('.bib-container'); + if (container) { + container.classList.remove('batch-mode'); + } + + let btnBatch = row.querySelector('.btn-assign-batch'); + if (btnBatch) { + btnBatch.textContent = row.dataset.batchLabel || btnBatch.textContent; + } + + row.querySelectorAll('.bib-start, .bib-qty, .bib-end').forEach(input => { + input.removeAttribute('readonly'); + }); + + row.querySelectorAll('.batch-select-range').forEach(cb => { + cb.checked = false; + cb.disabled = false; + }); + + syncBibUiState(row); + + bibFetch('/ajax_bib_range.php', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: + 'action=refresh_ranges' + + '&epr_id=' + encodeURIComponent(row.dataset.eprId) + + '&mode=assign' + + bibAjaxLangSuffix() + }) + .then(res => res.json()) + .then(refresh => { + + if (!refresh.success) return; + + let container = setBibContainerHtml(row, refresh.html); + if (!container) return; + bibFetch('/ajax_bib_range.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: + 'action=batch_analysis' + + '&epr_id=' + encodeURIComponent(row.dataset.eprId) + + bibAjaxLangSuffix() + }) + .then(res => res.json()) + .then(stats => { + + if (!stats.success) return; + + bibSyncEprBibStats(row, stats.info); + syncBibUiState(row); + }); + }); + } + + function bibFetchBatchApplyChunk(row, selected, ba_id, opts) { + opts = opts || {}; + var body = + 'action=batch_apply_chunk' + + '&epr_id=' + encodeURIComponent(row.dataset.eprId) + + '&ranges=' + encodeURIComponent(JSON.stringify(selected)) + + '&ba_id=' + encodeURIComponent(ba_id) + + '&chunk_size=' + BIB_BATCH_CHUNK_SIZE + + '&total_initial=' + (opts.totalInitial || 0) + + '&batch_reset=' + (opts.batchReset ? 1 : 0) + + bibAjaxLangSuffix(); + + return fetch('/ajax_bib_range.php', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: body + }).then(function (res) { return res.json(); }); + } + + function bibRunBatchApplyChunks(row, selected, ba_id) { + var totalInitial = bibRowPendingBibs(row); + var batchReset = true; + + bibBatchProgressShow(0, totalInitial > 0 ? totalInitial : 1); + + function runChunk() { + return bibFetchBatchApplyChunk(row, selected, ba_id, { + batchReset: batchReset, + totalInitial: totalInitial + }).then(function (data) { + if (!data.success) { + throw new Error(data.message || bibJs('jsErrGeneric')); + } + + if (data.total_initial) { + totalInitial = data.total_initial; + } + batchReset = false; + + var processed = data.processed || 0; + bibBatchProgressShow(processed, totalInitial); + + if (data.complete) { + bibAfterBatchGoSuccess(row, data, false); + return; + } + + return runChunk(); + }); + } + + runChunk() + .catch(function (err) { + alert(err.message || bibJs('jsErrGeneric')); + }) + .finally(function () { + bibBatchProgressHide(); + }); + } + function bibRowPendingBibs(row) { if (!row) return 0; return parseInt(row.dataset.bibAAssigner || '0', 10) || 0; @@ -3939,106 +4098,34 @@ if ($strLangue == 'fr') { let isResetMode = row.dataset.mode === 'reset'; let ba_id = row.querySelector('.batch-order').value; - bibFetch('/ajax_bib_range.php', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: - (isResetMode ? 'action=reset_apply' : 'action=batch_apply') - + '&epr_id=' + encodeURIComponent(row.dataset.eprId) - + '&ranges=' + encodeURIComponent(JSON.stringify(selected)) - + '&ba_id=' + encodeURIComponent(ba_id) - + bibAjaxLangSuffix() - }) - .then(res => res.json()) - .then(data => { + if (isResetMode) { + bibFetch('/ajax_bib_range.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: + 'action=reset_apply' + + '&epr_id=' + encodeURIComponent(row.dataset.eprId) + + '&ranges=' + encodeURIComponent(JSON.stringify(selected)) + + '&ba_id=' + encodeURIComponent(ba_id) + + bibAjaxLangSuffix() + }) + .then(res => res.json()) + .then(data => { - if (!data.success) { - alert(data.message || bibJs('jsErrGeneric')); - return; - } - - let existing = document.querySelector('.epr-row-view'); - if (existing) { - existing.remove(); - } - - // afficher résultat - row.insertAdjacentHTML('afterend', data.html); - - // ===================== - // RESET UI BATCH - // ===================== - if (isResetMode) { - row.classList.remove('reset-mode'); - row.dataset.mode = ''; - - let btnReset = row.querySelector('.btn-reset'); - if (btnReset) { - btnReset.textContent = row.dataset.resetLabel || btnReset.textContent; + if (!data.success) { + alert(data.message || bibJs('jsErrGeneric')); + return; } - } - row.classList.remove('batch-mode'); - let container = row.querySelector('.bib-container'); - if (container) { - container.classList.remove('batch-mode'); - } - - let btnBatch = row.querySelector('.btn-assign-batch'); - if (btnBatch) { - btnBatch.textContent = row.dataset.batchLabel || btnBatch.textContent; - } - - row.querySelectorAll('.bib-start, .bib-qty, .bib-end').forEach(input => { - input.removeAttribute('readonly'); + bibAfterBatchGoSuccess(row, data, true); }); - row.querySelectorAll('.batch-select-range').forEach(cb => { - cb.checked = false; - cb.disabled = false; - }); + return; + } - syncBibUiState(row); - - bibFetch('/ajax_bib_range.php', { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: - 'action=refresh_ranges' - + '&epr_id=' + encodeURIComponent(row.dataset.eprId) - + '&mode=assign' - + bibAjaxLangSuffix() - }) - .then(res => res.json()) - .then(refresh => { - - if (!refresh.success) return; - - let container = setBibContainerHtml(row, refresh.html); - if (!container) return; - bibFetch('/ajax_bib_range.php', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: - 'action=batch_analysis' - + '&epr_id=' + encodeURIComponent(row.dataset.eprId) - + bibAjaxLangSuffix() - }) - .then(res => res.json()) - .then(stats => { - - if (!stats.success) return; - - bibSyncEprBibStats(row, stats.info); - syncBibUiState(row); - }); - }); - - }); + bibRunBatchApplyChunks(row, selected, ba_id); return; } diff --git a/js/ms1-loader.js b/js/ms1-loader.js index d804c60..f820b62 100644 --- a/js/ms1-loader.js +++ b/js/ms1-loader.js @@ -42,6 +42,7 @@ if (!$preloader.length) { return; } + clearProgress(); if (message != null && message !== '') { var $text = $('#preloader_text'); if ($text.length) { @@ -51,7 +52,54 @@ $preloader.show(); } + function clearProgress() { + $('#spinner .ms1-loader__progress').remove(); + } + + /** + * MSIN-4433 — Overlay avec coureur + barre de progression (% réel). + * @param {number} current + * @param {number} total + * @param {string} [message] + */ + function showProgress(current, total, message) { + var $preloader = $('#preloader'); + if (!$preloader.length) { + return; + } + + var intCurrent = Math.max(0, parseInt(current, 10) || 0); + var intTotal = Math.max(1, parseInt(total, 10) || 1); + var intPct = Math.min(100, Math.round((intCurrent / intTotal) * 100)); + + var $spinner = $('#spinner'); + var $bar = $spinner.find('.ms1-loader__progress'); + if (!$bar.length) { + $bar = $('
' + + '
' + + '
' + + '
' + + '' + + '
'); + $('#preloader_text').after($bar); + } + + $bar.attr('aria-valuenow', intPct); + $bar.find('.ms1-loader__progress-fill').css('width', intPct + '%'); + $bar.find('.ms1-loader__progress-pct').text(intPct + '%'); + + if (message != null && message !== '') { + var $text = $('#preloader_text'); + if ($text.length) { + $text.html(message); + } + } + + $preloader.show(); + } + function hide() { + clearProgress(); $('#preloader').fadeOut('slow'); } @@ -59,6 +107,7 @@ init: init, inlineHtml: inlineHtml, show: show, + showProgress: showProgress, hide: hide }; }(window, jQuery)); diff --git a/php/inc_fx_promoteur.php b/php/inc_fx_promoteur.php index aa518b9..78644de 100644 --- a/php/inc_fx_promoteur.php +++ b/php/inc_fx_promoteur.php @@ -3106,6 +3106,7 @@ function fxBibModuleJsDataAttrs() { 'range-invalid' => 'bib_v4_js_range_invalid', 'range-invalid-end' => 'bib_v4_js_range_invalid_end', 'range-invalid-qty' => 'bib_v4_js_range_invalid_qty', + 'batch-progress' => 'bib_v4_js_batch_progress', ]; $html = ''; foreach ($map as $attr => $clef) { @@ -5762,47 +5763,20 @@ function fxLoadUsedBibsForBatch($epr_id) { * @param int $teamMode epr_bib_team_mode (1 = individuel, 3 = 1 équipe = 1 dossard) */ function fxFindNextBatchBib($epr_id, $tabRanges, $mode, &$usedBib, $teamMode = 1) { - global $objDatabase; - $epr_id = intval($epr_id); $teamMode = (int)$teamMode; - $sqlBibNum = fxBibSqlNoBibUnsigned('no_bib'); foreach ($tabRanges as $range) { $start = (int)$range['start']; $end = (int)$range['end']; for ($i = $start; $i <= $end; $i++) { - if ($mode === 'simulation') { - // Simulation : cache mémoire (même logique solo/équipe via $teamBibMap en amont) - if (!isset($usedBib[$i])) { + if (!isset($usedBib[$i])) { + // MSIN-4433 — simulation : réserve en mémoire ; execute : réservation après UPDATE réussi. + if ($mode === 'simulation') { $usedBib[$i] = true; - return $i; - } - } elseif ($teamMode === 3) { - // GO — mode « 1 équipe = 1 dossard » - // Le numéro est libre s'il n'est pas déjà pris par une autre équipe. - $sqlCheck = " - SELECT COUNT(DISTINCT pec_id) - FROM resultats_participants - WHERE epr_id = $epr_id - AND $sqlBibNum = $i - AND pec_id > 0 - "; - if ((int)$objDatabase->fxGetVar($sqlCheck) === 0) { - return $i; - } - } else { - // GO — solo ou dossards individuels : un numéro ne peut servir qu'à une personne - $sqlCheck = " - SELECT COUNT(*) - FROM resultats_participants - WHERE epr_id = $epr_id - AND $sqlBibNum = $i - "; - if ((int)$objDatabase->fxGetVar($sqlCheck) === 0) { - return $i; } + return $i; } } } @@ -5810,6 +5784,84 @@ function fxFindNextBatchBib($epr_id, $tabRanges, $mode, &$usedBib, $teamMode = 1 return null; } +/** + * MSIN-4433 — Vérifie en BD qu'un numéro est encore libre (assignation manuelle entre-temps). + */ +function fxBibIsNumberFreeInDb($epr_id, $intBib, $teamMode = 1) { + global $objDatabase; + + $epr_id = (int)$epr_id; + $intBib = (int)$intBib; + $teamMode = (int)$teamMode; + $sqlBibNum = fxBibSqlNoBibUnsigned('no_bib'); + + if ($epr_id <= 0 || $intBib <= 0) { + return false; + } + + if ($teamMode === 3) { + $sqlCheck = " + SELECT COUNT(DISTINCT pec_id) + FROM resultats_participants + WHERE epr_id = $epr_id + AND $sqlBibNum = $intBib + AND pec_id > 0 + "; + return (int)$objDatabase->fxGetVar($sqlCheck) === 0; + } + + $sqlCheck = " + SELECT COUNT(*) + FROM resultats_participants + WHERE epr_id = $epr_id + AND $sqlBibNum = $intBib + "; + + return (int)$objDatabase->fxGetVar($sqlCheck) === 0; +} + +/** + * MSIN-4433 — Prochain dossard libre pour un participant (1 UPDATE si execute). + * Relecture BD avant écriture : cohérence si changement manuel pendant le batch. + * + * @return int|null Numéro attribué, ou null si épuisé / participant déjà servi. + */ +function fxTryAssignNextBibForParticipant($epr_id, $par_id, $tabRanges, $mode, &$usedBib, $teamMode = 1) { + $epr_id = (int)$epr_id; + $par_id = (int)$par_id; + $teamMode = (int)$teamMode; + $intMaxAttempts = 50000; + + for ($intAttempt = 0; $intAttempt < $intMaxAttempts; $intAttempt++) { + $candidate = fxFindNextBatchBib($epr_id, $tabRanges, $mode, $usedBib, $teamMode); + + if ($candidate === null) { + return null; + } + + if ($mode === 'execute') { + if (!fxBibIsNumberFreeInDb($epr_id, $candidate, $teamMode)) { + $usedBib[$candidate] = true; + continue; + } + + $intAffected = fxApplyBatchParticipantBib($epr_id, $par_id, $candidate); + + if ($intAffected <= 0) { + return null; + } + + $usedBib[$candidate] = true; + + return (int)$candidate; + } + + return (int)$candidate; + } + + return null; +} + /** * Écrit le no_bib d'un participant (batch GO). * Note : en mode « 1 équipe = 1 dossard », plusieurs lignes peuvent légitimement @@ -5828,10 +5880,13 @@ function fxApplyBatchParticipantBib($epr_id, $par_id, $foundBib) { "; $objDatabase->fxQuery($sqlUpdate); + $intAffected = (int)$objDatabase->affected_rows; - if (function_exists('fxEvtErreurResoudre')) { + if ($intAffected > 0 && function_exists('fxEvtErreurResoudre')) { fxEvtErreurResoudre('bib_auto_miss', (int)$epr_id, (int)$par_id); } + + return $intAffected; } function fxApplyBatchTeamNumber($epr_id, $teamKey, $teamNumber, &$updatedTeams) { @@ -5868,6 +5923,35 @@ function fxGetBatchTeamKey($p) { return (int)($p['pec_id_original'] ?? $p['pec_id'] ?? 0); } +/** + * MSIN-4433 — Mode équipe 3 : dossard déjà posé sur un coéquipier (batch par paquets). + */ +function fxGetExistingTeamBibInDb($epr_id, $teamKey) { + global $objDatabase; + + $epr_id = (int)$epr_id; + $teamKey = (int)$teamKey; + $sqlBibNum = fxBibSqlNoBibUnsigned('no_bib'); + + if ($epr_id <= 0 || $teamKey <= 0) { + return null; + } + + $sql = " + SELECT $sqlBibNum + FROM resultats_participants + WHERE epr_id = $epr_id + AND pec_id = $teamKey + AND is_cancelled = 0 + AND $sqlBibNum > 0 + LIMIT 1 + "; + + $bib = (int)$objDatabase->fxGetVar($sql); + + return $bib > 0 ? $bib : null; +} + function fxCountBatchAssignUnits($epr_id, $participants = null) { global $objDatabase; @@ -5951,19 +6035,23 @@ function fxCountBatchAssignUnits($epr_id, $participants = null) { function fxProcessBatchAssignmentsSolo($epr_id, $participants, $tabRanges, $mode = 'simulation') { $assignments = []; - $usedBib = ($mode === 'simulation') ? fxLoadUsedBibsForBatch($epr_id) : []; + // MSIN-4433 — état réel rechargé à chaque paquet (execute) ou simulation. + $usedBib = fxLoadUsedBibsForBatch($epr_id); foreach ($participants as $p) { - $foundBib = fxFindNextBatchBib($epr_id, $tabRanges, $mode, $usedBib); + $foundBib = fxTryAssignNextBibForParticipant( + $epr_id, + (int)$p['par_id'], + $tabRanges, + $mode, + $usedBib, + 1 + ); if ($foundBib === null) { break; } - if ($mode === 'execute') { - fxApplyBatchParticipantBib($epr_id, (int)$p['par_id'], $foundBib); - } - $assignments[] = fxBuildBatchAssignmentRow($p, $foundBib, 0); } @@ -5971,16 +6059,12 @@ function fxProcessBatchAssignmentsSolo($epr_id, $participants, $tabRanges, $mode } function fxProcessBatchAssignmentsEquipe($epr_id, $participants, $tabRanges, $mode, $teamMode) { - global $objDatabase; - $assignments = []; $teamMap = []; $updatedTeams = []; - // Mode 3 : mémorise le dossard déjà choisi pour chaque équipe (pec_id) - // → les coéquipiers réutilisent ce numéro sans repasser par fxFindNextBatchBib $teamBibMap = []; $nextTeamNumber = 1; - $usedBib = ($mode === 'simulation') ? fxLoadUsedBibsForBatch($epr_id) : []; + $usedBib = fxLoadUsedBibsForBatch($epr_id); foreach ($participants as $p) { $par_id = (int)$p['par_id']; @@ -5988,37 +6072,60 @@ function fxProcessBatchAssignmentsEquipe($epr_id, $participants, $tabRanges, $mo $teamNumber = 0; $foundBib = null; - if ($teamMode === 3 && $teamKey > 0 && isset($teamBibMap[$teamKey])) { - // Coéquipier suivant : même dossard que le premier membre déjà traité - $foundBib = $teamBibMap[$teamKey]; + if ($teamMode === 3 && $teamKey > 0) { + if (isset($teamBibMap[$teamKey])) { + $foundBib = $teamBibMap[$teamKey]; + } else { + $foundBib = fxGetExistingTeamBibInDb($epr_id, $teamKey); + if ($foundBib !== null) { + $teamBibMap[$teamKey] = $foundBib; + } + } + } + + if ($teamMode === 3 && $teamKey > 0 && $foundBib !== null) { $teamNumber = $foundBib; - } elseif ($teamMode !== 3 && $teamKey > 0) { - // Mode dossards individuels : no_equipe séquentiel (1, 2, 3…) partagé par l'équipe - if (!isset($teamMap[$teamKey])) { - $teamMap[$teamKey] = $nextTeamNumber; - $nextTeamNumber++; + + if ($mode === 'execute') { + $intAffected = fxApplyBatchParticipantBib($epr_id, $par_id, $foundBib); + + if ($intAffected <= 0) { + continue; + } + + fxApplyBatchTeamNumber($epr_id, $teamKey, $teamNumber, $updatedTeams); + } + } else { + if ($teamMode !== 3 && $teamKey > 0) { + if (!isset($teamMap[$teamKey])) { + $teamMap[$teamKey] = $nextTeamNumber; + $nextTeamNumber++; + } + + $teamNumber = $teamMap[$teamKey]; } - $teamNumber = $teamMap[$teamKey]; - } + $foundBib = fxTryAssignNextBibForParticipant( + $epr_id, + $par_id, + $tabRanges, + $mode, + $usedBib, + $teamMode + ); - if ($foundBib === null) { - // Premier membre d'une équipe (mode 3) ou chaque participant (mode 1) - $foundBib = fxFindNextBatchBib($epr_id, $tabRanges, $mode, $usedBib, $teamMode); - } + if ($foundBib === null) { + break; + } - if ($foundBib === null) { - break; - } + if ($teamMode === 3 && $teamKey > 0) { + $teamBibMap[$teamKey] = $foundBib; + $teamNumber = $foundBib; + } - if ($teamMode === 3 && $teamKey > 0) { - $teamBibMap[$teamKey] = $foundBib; - $teamNumber = $foundBib; - } - - if ($mode === 'execute') { - fxApplyBatchParticipantBib($epr_id, $par_id, $foundBib); - fxApplyBatchTeamNumber($epr_id, $teamKey, $teamNumber, $updatedTeams); + if ($mode === 'execute') { + fxApplyBatchTeamNumber($epr_id, $teamKey, $teamNumber, $updatedTeams); + } } $assignments[] = fxBuildBatchAssignmentRow($p, $foundBib, $teamNumber); @@ -6057,6 +6164,46 @@ function fxProcessBatchAssignments($epr_id, $participants, $tabRanges, $mode = ' return fxProcessBatchAssignmentsEquipe($epr_id, $participants, $tabRanges, $mode, $teamMode); } +/** MSIN-4433 — Clé session pour accumuler les lignes assignées entre paquets AJAX. */ +function fxBibBatchExecSessionKey($epr_id) { + return 'bib_batch_exec_' . (int)$epr_id; +} + +/** + * MSIN-4433 — HTML résultat GO batch (batch_apply et dernier paquet). + */ +function fxBibBuildBatchApplyHtml($epr_id, array $exec, $intTotalInitial, $strLangue) { + $assigned = count($exec); + $total = max($assigned, (int)$intTotalInitial); + $remaining = max(0, $total - $assigned); + $status = 'success'; + + if ($assigned === 0) { + $status = 'error'; + } elseif ($assigned < $total) { + $status = 'partial'; + } + + if ($assigned === 0) { + $info = fxBibMsg('bib_v4_ajax_assign_none'); + } elseif ($assigned >= $total) { + $info = fxBibMsg('bib_v4_ajax_assign_full', $assigned); + } else { + $info = fxBibMsg('bib_v4_ajax_assign_partial', $assigned, $remaining); + } + + return renderBibView( + $epr_id, + 0, + $strLangue, + 'execute', + $exec, + fxBibTexte('bib_v4_ajax_title_assign'), + $info, + $status + ); +} + function fxBuildBatchSummaryFromRanges($tabRangesInfos, $participants, $epr_id = 0) { $nbParticipants = fxCountBatchAssignUnits($epr_id, $participants); diff --git a/php/inc_settings.php b/php/inc_settings.php index 11691de..34350cd 100644 --- a/php/inc_settings.php +++ b/php/inc_settings.php @@ -7,7 +7,7 @@ * Constantes * * **************/ -define('_VERSION_CODE', '4.72.732'); +define('_VERSION_CODE', '4.72.733'); define('_DATE_CODE', '2026-07-07'); //MSIN-4290 define('QR_SECRET_KEY', 'ms1_qr_2026_cle_secrete_longue_et_fixe'); diff --git a/sql/MSIN-4433-bib-batch-chunk.sql b/sql/MSIN-4433-bib-batch-chunk.sql new file mode 100644 index 0000000..67a0f4c --- /dev/null +++ b/sql/MSIN-4433-bib-batch-chunk.sql @@ -0,0 +1,10 @@ +-- MSIN-4433 — GO batch par paquets : libellé progression overlay +-- Idempotent. Exécuter après déploiement PHP/JS (ajax batch_apply_chunk). + +INSERT INTO info (info_clef, info_langue, info_texte, info_aide, info_prg, info_description, info_trie, info_actif, info_option1, info_option2, info_option3, info_creation) +SELECT 'bib_v4_js_batch_progress', 'fr', 'Assignation des dossards… %d / %d', '', 'compte.php', '', 0, 1, '', '', '', NOW() +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_js_batch_progress' AND info_langue = 'fr' AND info_prg = 'compte.php'); + +INSERT INTO info (info_clef, info_langue, info_texte, info_aide, info_prg, info_description, info_trie, info_actif, info_option1, info_option2, info_option3, info_creation) +SELECT 'bib_v4_js_batch_progress', 'en', 'Assigning bib numbers… %d / %d', '', 'compte.php', '', 0, 1, '', '', '', NOW() +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_js_batch_progress' AND info_langue = 'en' AND info_prg = 'compte.php');