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 = $('