diff --git a/ajax_bib_range.php b/ajax_bib_range.php index 40bd0cd..addb40a 100644 --- a/ajax_bib_range.php +++ b/ajax_bib_range.php @@ -45,6 +45,10 @@ $arrBibAjaxActions = [ 'batch_global_gen_analysis', 'batch_global_gen_simulate', 'batch_global_gen_create', + 'batch_global_free_analysis', + 'batch_global_free_simulate', + 'batch_global_free_plan', + 'batch_apply_free_chunk', 'global_batch_panel', 'dist_print_panel', ]; @@ -700,6 +704,94 @@ if ($action == 'batch_global_gen_create') { exit; } +// MSIN-4445 — Analyse batch global libre (Début/Fin, sans séquence). +if ($action == 'batch_global_free_analysis') { + + $int_eve_id = (int)($_POST['eve_id'] ?? 0); + $intStart = (int)($_POST['start_bib'] ?? 0); + $intEnd = (int)($_POST['end_bib'] ?? 0); + list($strSort1, $strSort2) = fxBibParseBatchSortFromPost(); + $eprIds = json_decode($_POST['epr_ids'] ?? '[]', true); + $tabEprIds = fxBibParseGlobalBatchGeneratedEprIds(is_array($eprIds) ? $eprIds : [], $int_eve_id); + + if ($int_eve_id <= 0 || $intStart <= 0 || $intEnd < $intStart || $strSort1 === '' || empty($tabEprIds)) { + echo json_encode([ + 'success' => false, + 'message' => fxBibMsg('bib_v4_global_batch_free_plan_invalid'), + ]); + exit; + } + + $tabPlan = fxBibBuildFreeRangePlan($int_eve_id, $tabEprIds, $intStart, $intEnd, $strSort1, $strSort2); + $tabAnalysis = fxBibBuildGlobalBatchFreeAnalysis($tabPlan, $intStart, $intEnd); + + echo json_encode([ + 'success' => true, + 'info' => $tabAnalysis['info'], + 'stats' => $tabAnalysis['stats'], + 'feedback' => $tabAnalysis['feedback'], + ]); + exit; +} + +// MSIN-4445 — Simulation batch global libre. +if ($action == 'batch_global_free_simulate') { + + $int_eve_id = (int)($_POST['eve_id'] ?? 0); + $intStart = (int)($_POST['start_bib'] ?? 0); + $intEnd = (int)($_POST['end_bib'] ?? 0); + list($strSort1, $strSort2) = fxBibParseBatchSortFromPost(); + $eprIds = json_decode($_POST['epr_ids'] ?? '[]', true); + $tabEprIds = fxBibParseGlobalBatchGeneratedEprIds(is_array($eprIds) ? $eprIds : [], $int_eve_id); + + if ($int_eve_id <= 0 || $intStart <= 0 || $intEnd < $intStart || $strSort1 === '' || empty($tabEprIds)) { + echo json_encode([ + 'success' => false, + 'message' => fxBibMsg('bib_v4_global_batch_free_plan_invalid'), + ]); + exit; + } + + $tabPlan = fxBibBuildFreeRangePlan($int_eve_id, $tabEprIds, $intStart, $intEnd, $strSort1, $strSort2); + $result = fxBibSimulateGlobalBatchFree( + $int_eve_id, + $tabPlan, + $intStart, + $intEnd, + $strSort1, + $strSort2, + $strLangue + ); + + echo json_encode($result); + exit; +} + +// MSIN-4445 — Prépare les plans GO libres (aucune création de séquence). +if ($action == 'batch_global_free_plan') { + + $int_eve_id = (int)($_POST['eve_id'] ?? 0); + $intStart = (int)($_POST['start_bib'] ?? 0); + $intEnd = (int)($_POST['end_bib'] ?? 0); + list($strSort1, $strSort2) = fxBibParseBatchSortFromPost(); + $eprIds = json_decode($_POST['epr_ids'] ?? '[]', true); + $tabEprIds = fxBibParseGlobalBatchGeneratedEprIds(is_array($eprIds) ? $eprIds : [], $int_eve_id); + + if ($int_eve_id <= 0 || $intStart <= 0 || $intEnd < $intStart || $strSort1 === '' || empty($tabEprIds)) { + echo json_encode([ + 'success' => false, + 'message' => fxBibMsg('bib_v4_global_batch_free_plan_invalid'), + ]); + exit; + } + + $tabPlan = fxBibBuildFreeRangePlan($int_eve_id, $tabEprIds, $intStart, $intEnd, $strSort1, $strSort2); + $prepare = fxBibPrepareFreeRangePlans($int_eve_id, $tabPlan, $intStart, $intEnd, $strLangue); + + echo json_encode($prepare); + exit; +} + if ($action == 'refresh_ranges') { $epr_id = intval($_POST['epr_id'] ?? $_GET['epr_id'] ?? 0); @@ -965,6 +1057,116 @@ if ($action == 'batch_apply_chunk') { echo json_encode($arrResponse); exit; } + +// MSIN-4445 — GO batch par paquets avec plage virtuelle (sans séquence en BD). +if ($action == 'batch_apply_free_chunk') { + + $epr_id = intval($_POST['epr_id'] ?? 0); + $intStart = (int)($_POST['start_bib'] ?? 0); + $intEnd = (int)($_POST['end_bib'] ?? 0); + list($strSort1, $strSort2) = fxBibParseBatchSortFromPost(); + $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; + } + + if ($epr_id <= 0 || $intStart <= 0 || $intEnd < $intStart) { + echo json_encode([ + 'success' => false, + 'message' => fxBibMsg('bib_v4_global_batch_free_plan_invalid'), + ]); + exit; + } + + // Même filtre que le panneau : solo sans séquence. + $intEveId = (int)($_POST['eve_id'] ?? 0); + if ($intEveId > 0) { + $checkEpr = fxBibAssertSoloEpreuveForGlobalBatch($epr_id, $intEveId); + if (!$checkEpr['success']) { + echo json_encode($checkEpr); + exit; + } + if (fxBibEpreuveHasAnySequence($epr_id)) { + echo json_encode([ + 'success' => false, + 'message' => fxBibMsg('bib_v4_global_batch_gen_has_seq'), + ]); + exit; + } + } + + $strSessionKey = fxBibBatchExecSessionKey($epr_id) . '_free'; + + if ($blnBatchReset) { + $_SESSION[$strSessionKey] = []; + fxBibSaveEpreuveBatchSort($epr_id, $strSort1, $strSort2); + } elseif (!isset($_SESSION[$strSessionKey]) || !is_array($_SESSION[$strSessionKey])) { + $_SESSION[$strSessionKey] = []; + } + + $participants = fxGetParticipantsForBatchAssign($epr_id, $strSort1, $strSort2); + + 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 = fxBibBuildVirtualRangesForPlan($intStart, $intEnd); + $execChunk = fxProcessBatchAssignments($epr_id, $tabChunk, $tabRanges, 'execute'); + + $_SESSION[$strSessionKey] = array_merge($_SESSION[$strSessionKey], $execChunk); + + $intRemaining = count(fxGetParticipantsForBatchAssign($epr_id, $strSort1, $strSort2)); + $intProcessed = max(0, $intTotalInitial - $intRemaining); + $blnComplete = ($intRemaining === 0); + + 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/style.css b/css/style.css index 2f3a40e..c7e9002 100644 --- a/css/style.css +++ b/css/style.css @@ -1448,6 +1448,42 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active { margin-bottom:4px; font-weight:600; } +/* MSIN-4445 — Assignation libre : Début / Qté / Fin */ +.bib-global-free-range__headers, +.bib-global-free-range__fields{ + display:grid; + grid-template-columns:minmax(72px, 1fr) minmax(72px, 1fr) auto minmax(72px, 1fr); + gap:6px 8px; + align-items:center; + max-width:420px; +} +.bib-global-free-range__headers{ + margin-bottom:2px; + font-size:11px; + text-transform:uppercase; + letter-spacing:.02em; + color:#6c757d; +} +.bib-global-free-range__or{ + text-align:center; + color:#9aa0a6; + font-size:12px; +} +.bib-global-free-range__feedback{ + margin-top:6px; +} +.bib-global-free-range__feedback--juste{ + color:#155724; + font-weight:600; +} +.bib-global-free-range__feedback--manquante{ + color:#856404; + font-weight:600; +} +.bib-global-free-range__feedback--supplementaire{ + color:#004085; + font-weight:600; +} .bib-global-epr--inactive{ opacity:0.55; background:#f1f3f5; diff --git a/js/v2/bib-assignments.js b/js/v2/bib-assignments.js index 4816fa3..4fee8c0 100644 --- a/js/v2/bib-assignments.js +++ b/js/v2/bib-assignments.js @@ -218,9 +218,11 @@ return { sort1: '', sort2: '' }; } var s1 = panel.querySelector('.bib-global-batch-order') - || panel.querySelector('.bib-global-gen-batch-order'); + || panel.querySelector('.bib-global-gen-batch-order') + || panel.querySelector('.bib-global-free-batch-order'); var s2 = panel.querySelector('.bib-global-batch-order-2') - || panel.querySelector('.bib-global-gen-batch-order-2'); + || panel.querySelector('.bib-global-gen-batch-order-2') + || panel.querySelector('.bib-global-free-batch-order-2'); return { sort1: s1 ? s1.value : '', sort2: s2 ? s2.value : '' @@ -366,6 +368,11 @@ return body ? body.querySelector('.bib-global-batch--generated') : null; } + function bibGetGlobalBatchFreeSection() { + var body = bibGetGlobalBatchBody(); + return body ? body.querySelector('.bib-global-batch--free') : null; + } + function bibInvalidateGlobalBatchPanel() { var mount = document.getElementById('bib-global-batch-mount'); var panel = bibGetGlobalBatchPanel(); @@ -797,6 +804,10 @@ } initModuleBibTippy(mount); bibRemountActiveToolAfterRefresh('global_batch'); + var freeSection = bibGetGlobalBatchFreeSection(); + if (freeSection) { + bibBindGlobalFreeRange(freeSection); + } return panel; }) .finally(function () { @@ -1173,6 +1184,561 @@ }); } + // MSIN-4445 — Assignation libre (Début / Qté / Fin, sans séquence). + function bibCollectGlobalBatchFreeEprIds() { + var section = bibGetGlobalBatchFreeSection(); + if (!section) { + return []; + } + + var ids = []; + section.querySelectorAll('.bib-global-free-epr-select:checked').forEach(function (cb) { + var eprId = parseInt(cb.dataset.eprId || '0', 10); + if (eprId) { + ids.push(eprId); + } + }); + + return ids; + } + + function bibCollectGlobalBatchFreePending() { + var section = bibGetGlobalBatchFreeSection(); + if (!section) { + return 0; + } + + var total = 0; + section.querySelectorAll('.bib-global-free-epr-select:checked').forEach(function (cb) { + var block = cb.closest('.bib-global-epr'); + var pending = block + ? parseInt(block.dataset.pending || '0', 10) + : 0; + if (pending > 0) { + total += pending; + } + }); + + return total; + } + + function bibGetGlobalBatchFreeRangeFields(section) { + section = section || bibGetGlobalBatchFreeSection(); + if (!section) { + return null; + } + + var wrap = section.querySelector('.bib-global-free-range'); + if (!wrap) { + return null; + } + + return { + wrap: wrap, + start: wrap.querySelector('.bib-global-free-start'), + qty: wrap.querySelector('.bib-global-free-qty'), + end: wrap.querySelector('.bib-global-free-end'), + feedback: wrap.querySelector('.bib-global-free-range__feedback') + }; + } + + function bibGetGlobalBatchFreeBounds(section) { + var fields = bibGetGlobalBatchFreeRangeFields(section); + if (!fields) { + return { start: 0, end: 0, qty: 0 }; + } + + var start = bibRangeParseInt(fields.start && fields.start.value); + var qty = bibRangeParseInt(fields.qty && fields.qty.value); + var end = bibRangeParseInt(fields.end && fields.end.value); + var pilot = (fields.wrap && fields.wrap.dataset.rangePilot) || ''; + + if (start > 0 && pilot === 'qty' && qty > 0) { + end = start + qty - 1; + } else if (start > 0 && pilot === 'end' && end >= start) { + qty = end - start + 1; + } else if (start > 0 && end >= start) { + qty = end - start + 1; + } else { + qty = 0; + if (end < start) { + end = 0; + } + } + + return { start: start, end: end, qty: qty }; + } + + function bibFormatGlobalFreeQtyFeedback(qty, pending) { + var diff = qty - pending; + if (qty <= 0) { + return ''; + } + if (diff === 0) { + return bibJs('jsGlobalFreeQtyJuste') || 'Quantité juste'; + } + if (diff > 0) { + return bibJsFmt('jsGlobalFreeQtySupplementaire', diff) + || (('Quantité supplémentaire : %d').replace('%d', String(diff))); + } + return bibJsFmt('jsGlobalFreeQtyManquante', Math.abs(diff)) + || (('Quantité manquante : %d').replace('%d', String(Math.abs(diff)))); + } + + function bibUpdateGlobalFreeQtyFeedback(section) { + var fields = bibGetGlobalBatchFreeRangeFields(section); + if (!fields || !fields.feedback) { + return; + } + + var bounds = bibGetGlobalBatchFreeBounds(section); + var pending = bibCollectGlobalBatchFreePending(); + var msg = bibFormatGlobalFreeQtyFeedback(bounds.qty, pending); + fields.feedback.textContent = msg; + fields.feedback.classList.remove( + 'bib-global-free-range__feedback--juste', + 'bib-global-free-range__feedback--manquante', + 'bib-global-free-range__feedback--supplementaire' + ); + + if (!msg) { + return; + } + + if (bounds.qty === pending) { + fields.feedback.classList.add('bib-global-free-range__feedback--juste'); + } else if (bounds.qty < pending) { + fields.feedback.classList.add('bib-global-free-range__feedback--manquante'); + } else { + fields.feedback.classList.add('bib-global-free-range__feedback--supplementaire'); + } + } + + function bibSyncGlobalFreeRangeDerived(section) { + var fields = bibGetGlobalBatchFreeRangeFields(section); + if (!fields) { + return; + } + + var start = bibRangeParseInt(fields.start && fields.start.value); + var pilot = (fields.wrap && fields.wrap.dataset.rangePilot) || ''; + + if (start <= 0) { + if (fields.qty) { + fields.qty.value = ''; + fields.qty.disabled = true; + fields.qty.classList.remove('bib-field--active', 'bib-field--calc', 'bib-field--pick'); + fields.qty.classList.add('bib-field--idle'); + } + if (fields.end) { + fields.end.value = ''; + fields.end.disabled = true; + fields.end.classList.remove('bib-field--active', 'bib-field--calc', 'bib-field--pick'); + fields.end.classList.add('bib-field--idle'); + } + if (fields.wrap) { + fields.wrap.dataset.rangePilot = ''; + } + bibUpdateGlobalFreeQtyFeedback(section); + return; + } + + if (!pilot) { + if (fields.qty) { + fields.qty.disabled = false; + fields.qty.classList.remove('bib-field--idle', 'bib-field--active', 'bib-field--calc'); + fields.qty.classList.add('bib-field--pick'); + } + if (fields.end) { + fields.end.disabled = false; + fields.end.classList.remove('bib-field--idle', 'bib-field--active', 'bib-field--calc'); + fields.end.classList.add('bib-field--pick'); + } + bibUpdateGlobalFreeQtyFeedback(section); + return; + } + + if (pilot === 'qty') { + var qty = bibRangeParseInt(fields.qty && fields.qty.value); + if (fields.end) { + fields.end.disabled = true; + fields.end.classList.remove('bib-field--active', 'bib-field--pick', 'bib-field--idle'); + fields.end.classList.add('bib-field--calc'); + fields.end.value = qty > 0 ? String(start + qty - 1) : ''; + } + if (fields.qty) { + fields.qty.disabled = false; + fields.qty.classList.remove('bib-field--idle', 'bib-field--calc', 'bib-field--pick'); + fields.qty.classList.add('bib-field--active'); + } + } else if (pilot === 'end') { + var end = bibRangeParseInt(fields.end && fields.end.value); + if (fields.qty) { + fields.qty.disabled = true; + fields.qty.classList.remove('bib-field--active', 'bib-field--pick', 'bib-field--idle'); + fields.qty.classList.add('bib-field--calc'); + fields.qty.value = (end >= start) ? String(end - start + 1) : ''; + } + if (fields.end) { + fields.end.disabled = false; + fields.end.classList.remove('bib-field--idle', 'bib-field--calc', 'bib-field--pick'); + fields.end.classList.add('bib-field--active'); + } + } + + bibUpdateGlobalFreeQtyFeedback(section); + } + + function bibPickGlobalFreePilot(section, pilot) { + var fields = bibGetGlobalBatchFreeRangeFields(section); + if (!fields || !fields.wrap) { + return; + } + + var start = bibRangeParseInt(fields.start && fields.start.value); + if (start <= 0) { + return; + } + + fields.wrap.dataset.rangePilot = pilot; + bibSyncGlobalFreeRangeDerived(section); + + var target = pilot === 'qty' ? fields.qty : fields.end; + if (target && !target.disabled) { + target.focus(); + } + } + + function bibBindGlobalFreeRange(section) { + var fields = bibGetGlobalBatchFreeRangeFields(section); + if (!fields || !fields.wrap || fields.wrap.dataset.bound === '1') { + return; + } + fields.wrap.dataset.bound = '1'; + + if (fields.start) { + fields.start.addEventListener('input', function () { + bibSyncGlobalFreeRangeDerived(section); + updateGlobalBatchFreeAnalysis(); + }); + } + + if (fields.qty) { + fields.qty.addEventListener('focus', function () { + bibPickGlobalFreePilot(section, 'qty'); + }); + fields.qty.addEventListener('click', function () { + bibPickGlobalFreePilot(section, 'qty'); + }); + fields.qty.addEventListener('input', function () { + if (fields.wrap.dataset.rangePilot !== 'qty') { + fields.wrap.dataset.rangePilot = 'qty'; + } + bibSyncGlobalFreeRangeDerived(section); + updateGlobalBatchFreeAnalysis(); + }); + } + + if (fields.end) { + fields.end.addEventListener('focus', function () { + bibPickGlobalFreePilot(section, 'end'); + }); + fields.end.addEventListener('click', function () { + bibPickGlobalFreePilot(section, 'end'); + }); + fields.end.addEventListener('input', function () { + if (fields.wrap.dataset.rangePilot !== 'end') { + fields.wrap.dataset.rangePilot = 'end'; + } + bibSyncGlobalFreeRangeDerived(section); + updateGlobalBatchFreeAnalysis(); + }); + } + + bibSyncGlobalFreeRangeDerived(section); + } + + function bibAppendGlobalBatchFreeParams(body, section) { + var sortKeys = bibGetGlobalBatchSortKeys(section); + var bounds = bibGetGlobalBatchFreeBounds(section); + body = bibAppendBatchSortParams(body, sortKeys); + body += '&start_bib=' + encodeURIComponent(bounds.start); + body += '&end_bib=' + encodeURIComponent(bounds.end); + body += '&epr_ids=' + encodeURIComponent(JSON.stringify(bibCollectGlobalBatchFreeEprIds())); + return body; + } + + function updateGlobalBatchFreeAnalysis() { + var section = bibGetGlobalBatchFreeSection(); + if (!section) { + return Promise.resolve(); + } + + bibBindGlobalFreeRange(section); + + var infoEl = section.querySelector('.bib-global-free-batch__info'); + var eprIds = bibCollectGlobalBatchFreeEprIds(); + var bounds = bibGetGlobalBatchFreeBounds(section); + + bibUpdateGlobalFreeQtyFeedback(section); + + if (!infoEl) { + return Promise.resolve(); + } + + if (eprIds.length === 0) { + bibSetGlobalBatchInfo( + infoEl, + bibJs('jsGlobalNoEpr') || bibJs('jsBatchNone'), + 'error' + ); + return Promise.resolve(null); + } + + if (bounds.start <= 0 || bounds.end < bounds.start) { + bibSetGlobalBatchInfo( + infoEl, + bibJs('jsGlobalFreePlanInvalid') || bibJs('jsFieldsRequired'), + 'error' + ); + return Promise.resolve(null); + } + + bibSetGlobalBatchInfo(infoEl, bibJs('jsLoaderWait') || '…', 'normal'); + + return fetch('/ajax_bib_range.php', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: bibAppendGlobalBatchFreeParams( + 'action=batch_global_free_analysis' + + '&eve_id=' + encodeURIComponent(moduleBib.dataset.eveId || ''), + section + ) + bibAjaxLangSuffix() + }) + .then(function (res) { return res.json(); }) + .then(function (data) { + if (!data.success) { + bibSetGlobalBatchInfo( + infoEl, + data.message || bibJs('jsErrGeneric'), + 'error' + ); + return data; + } + + var state = 'normal'; + if (data.feedback && data.feedback.status === 'manquante') { + state = 'warning'; + } + bibSetGlobalBatchInfo(infoEl, data.info || '', state); + return data; + }) + .catch(function () { + bibSetGlobalBatchInfo(infoEl, bibJs('jsErrGeneric'), 'error'); + return null; + }); + } + + function bibFetchBatchApplyFreeChunk(row, plan, sortKeys, opts) { + opts = opts || {}; + sortKeys = sortKeys || bibGetRowSortKeys(row); + var body = + 'action=batch_apply_free_chunk' + + '&epr_id=' + encodeURIComponent(row.dataset.eprId) + + '&eve_id=' + encodeURIComponent(moduleBib.dataset.eveId || '') + + '&start_bib=' + encodeURIComponent(plan.start || 0) + + '&end_bib=' + encodeURIComponent(plan.end || 0) + + bibAppendBatchSortParams('', sortKeys) + + '&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) { + if (!res.ok) { + throw new Error(bibJs('jsErrGeneric')); + } + return res.json(); + }); + } + + function bibRunBatchApplyFreeChunks(row, plan, sortKeys, options) { + options = options || {}; + var manageLoader = options.manageLoader !== false; + var onProgress = options.onProgress; + var totalInitial = options.totalInitial > 0 + ? options.totalInitial + : bibRowPendingBibs(row); + var batchReset = true; + var chunkGuard = 0; + var lastData = null; + + function runNext() { + chunkGuard += 1; + if (chunkGuard > 500) { + return Promise.reject(new Error(bibJs('jsErrGeneric'))); + } + + return bibFetchBatchApplyFreeChunk(row, plan, sortKeys, { + totalInitial: totalInitial, + batchReset: batchReset + }).then(function (data) { + batchReset = false; + lastData = data; + + if (!data || !data.success) { + throw new Error((data && data.message) || bibJs('jsErrGeneric')); + } + + if (typeof onProgress === 'function') { + onProgress(data.processed || 0); + } + + if (data.complete) { + return data; + } + + return runNext(); + }); + } + + if (manageLoader) { + bibBatchProgressShow(0, totalInitial); + } + + return runNext().then(function (data) { + return data || lastData || { success: true, assigned_total: 0, total_initial: totalInitial }; + }); + } + + function bibRunGlobalBatchFreeGo() { + var section = bibGetGlobalBatchFreeSection(); + if (!section) { + return; + } + + var eprIds = bibCollectGlobalBatchFreeEprIds(); + var bounds = bibGetGlobalBatchFreeBounds(section); + var sortKeys = bibGetGlobalBatchSortKeys(section); + + if (eprIds.length === 0) { + alert(bibJs('jsGlobalNoEpr') || bibJs('jsBatchNone')); + return; + } + + if (bounds.start <= 0 || bounds.end < bounds.start) { + alert(bibJs('jsGlobalFreePlanInvalid') || bibJs('jsFieldsRequired')); + return; + } + + fetch('/ajax_bib_range.php', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: bibAppendGlobalBatchFreeParams( + 'action=batch_global_free_plan' + + '&eve_id=' + encodeURIComponent(moduleBib.dataset.eveId || ''), + section + ) + bibAjaxLangSuffix() + }) + .then(function (res) { + if (!res.ok) { + throw new Error(bibJs('jsErrGeneric') || 'Erreur'); + } + return res.json().catch(function () { + throw new Error(bibJs('jsServerError') || bibJs('jsErrGeneric') || 'Erreur serveur'); + }); + }) + .then(function (planData) { + if (!planData || !planData.success || !Array.isArray(planData.plans) || planData.plans.length === 0) { + throw new Error((planData && planData.message) || bibJs('jsErrGeneric') || 'Erreur'); + } + + var plans = planData.plans; + var globalTotal = (planData.stats && planData.stats.nb_participants) + ? planData.stats.nb_participants + : 1; + var completedOffset = 0; + var runResults = []; + + bibBatchProgressShow(0, globalTotal); + + var chain = Promise.resolve(); + + plans.forEach(function (plan) { + chain = chain.then(function () { + var row = moduleBib.querySelector('.epr-row[data-epr-id="' + plan.epr_id + '"]'); + if (!row) { + throw new Error( + bibJs('jsGlobalGenEprMissing') + || bibJs('jsErrGeneric') + || 'Épreuve introuvable — rechargez la page.' + ); + } + + var pendingBefore = bibRowPendingBibs(row); + var offset = completedOffset; + + return bibRunBatchApplyFreeChunks(row, plan, sortKeys, { + manageLoader: false, + totalInitial: pendingBefore, + onProgress: function (processed) { + bibBatchProgressShow(offset + processed, globalTotal); + } + }).then(function (data) { + runResults.push({ + epr_id: plan.epr_id, + assigned: data.assigned_total || 0, + total: data.total_initial || pendingBefore + }); + completedOffset = offset + pendingBefore; + }); + }); + }); + + return chain.then(function () { + bibBatchProgressShow( + globalTotal, + globalTotal, + bibJs('jsGlobalBatchFinalizing') || bibJs('jsLoaderWait') + ); + + var refreshChain = Promise.resolve(); + runResults.forEach(function (result) { + refreshChain = refreshChain.then(function () { + var row = moduleBib.querySelector('.epr-row[data-epr-id="' + result.epr_id + '"]'); + if (!row) { + return null; + } + return bibAfterBatchGoRefreshOnly(row, false, { + loader: false, + skipAnomalies: true + }); + }); + }); + + return refreshChain.then(function () { + if (typeof refreshBibAnomaliesPanel === 'function') { + return refreshBibAnomaliesPanel(); + } + }).then(function () { + bibInvalidateGlobalBatchPanel(); + bibFinishGlobalBatchGo(runResults); + }); + }); + }) + .catch(function (err) { + alert(err.message || bibJs('jsErrGeneric')); + }) + .finally(function () { + bibBatchProgressForceHide(); + }); + } + function bibRunGlobalBatchGo() { var section = bibGetGlobalBatchExistingSection(); if (!section) { @@ -1408,13 +1974,59 @@ if (e.target.closest('.btn-go-global-gen-batch')) { e.preventDefault(); bibRunGlobalBatchGeneratedGo(); + return; + } + + if (e.target.closest('.btn-simuler-global-free-batch')) { + e.preventDefault(); + var freeSection = bibGetGlobalBatchFreeSection(); + var freeEprIds = bibCollectGlobalBatchFreeEprIds(); + var freeBounds = bibGetGlobalBatchFreeBounds(freeSection); + + if (freeEprIds.length === 0) { + alert(bibJs('jsGlobalNoEpr') || bibJs('jsBatchNone')); + return; + } + + if (freeBounds.start <= 0 || freeBounds.end < freeBounds.start) { + alert(bibJs('jsGlobalFreePlanInvalid') || bibJs('jsFieldsRequired')); + return; + } + + bibFetch('/ajax_bib_range.php', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: bibAppendGlobalBatchFreeParams( + 'action=batch_global_free_simulate' + + '&eve_id=' + encodeURIComponent(moduleBib.dataset.eveId || ''), + freeSection + ) + bibAjaxLangSuffix() + }) + .then(function (res) { return res.json(); }) + .then(function (data) { + if (!data.success) { + alert(data.message || bibJs('jsErrGeneric')); + return; + } + + bibInsertGlobalSimView(data.html); + }); + + return; + } + + if (e.target.closest('.btn-go-global-free-batch')) { + e.preventDefault(); + bibRunGlobalBatchFreeGo(); } }); moduleBib.addEventListener('change', function (e) { if (!e.target.matches( '.bib-global-epr-select, .bib-global-range-select, .bib-global-batch-order, .bib-global-batch-order-2,' - + ' .bib-global-gen-epr-select, .bib-global-gen-batch-order, .bib-global-gen-batch-order-2, .bib-global-gen-start-bib' + + ' .bib-global-gen-epr-select, .bib-global-gen-batch-order, .bib-global-gen-batch-order-2, .bib-global-gen-start-bib,' + + ' .bib-global-free-epr-select, .bib-global-free-batch-order, .bib-global-free-batch-order-2,' + + ' .bib-global-free-start, .bib-global-free-qty, .bib-global-free-end' )) { return; } @@ -1441,6 +2053,11 @@ if (e.target.closest('.bib-global-batch--generated')) { updateGlobalBatchGeneratedAnalysis(); + return; + } + + if (e.target.closest('.bib-global-batch--free')) { + updateGlobalBatchFreeAnalysis(); } }); diff --git a/php/inc_fx_bib_global_free.php b/php/inc_fx_bib_global_free.php new file mode 100644 index 0000000..591cbbe --- /dev/null +++ b/php/inc_fx_bib_global_free.php @@ -0,0 +1,327 @@ + 'juste', + 'amount' => 0, + 'qty' => $intQty, + 'pending' => $intPending, + ]; + } + + if ($intDiff > 0) { + return [ + 'status' => 'supplementaire', + 'amount' => $intDiff, + 'qty' => $intQty, + 'pending' => $intPending, + ]; + } + + return [ + 'status' => 'manquante', + 'amount' => abs($intDiff), + 'qty' => $intQty, + 'pending' => $intPending, + ]; +} + +/** Libellé Info pour le feedback quantité libre. */ +function fxBibFreeQtyFeedbackMessage(array $tabFeedback) { + $strStatus = $tabFeedback['status'] ?? ''; + $intAmount = (int)($tabFeedback['amount'] ?? 0); + + if ($strStatus === 'juste') { + return fxBibMsg('bib_v4_global_batch_free_qty_juste'); + } + + if ($strStatus === 'supplementaire') { + return fxBibMsg('bib_v4_global_batch_free_qty_supplementaire', $intAmount); + } + + if ($strStatus === 'manquante') { + return fxBibMsg('bib_v4_global_batch_free_qty_manquante', $intAmount); + } + + return ''; +} + +/** + * Plan de plages virtuelles bornées par Début/Fin (sans écriture BD). + * Enchaîne les épreuves comme le bloc généré, mais s'arrête à $intEnd. + * + * @return array[] [{epr_id, start, end, nb_pending, skip}] + */ +function fxBibBuildFreeRangePlan($int_eve_id, $tabEprIds, $intStart, $intEnd, $strSort1, $strSort2) { + $int_eve_id = (int)$int_eve_id; + $intStart = (int)$intStart; + $intEnd = (int)$intEnd; + $tabEprIds = fxBibParseGlobalBatchGeneratedEprIds($tabEprIds, $int_eve_id); + $intCursor = $intStart; + $tabPlan = []; + + foreach ($tabEprIds as $epr_id) { + $participants = fxGetParticipantsForBatchAssign($epr_id, $strSort1, $strSort2); + $intPending = count($participants); + + if ($intPending <= 0) { + $tabPlan[] = [ + 'epr_id' => $epr_id, + 'start' => 0, + 'end' => 0, + 'nb_pending' => 0, + 'skip' => true, + ]; + continue; + } + + $intRemaining = $intEnd - $intCursor + 1; + if ($intRemaining <= 0) { + $tabPlan[] = [ + 'epr_id' => $epr_id, + 'start' => 0, + 'end' => 0, + 'nb_pending' => $intPending, + 'skip' => true, + ]; + continue; + } + + $intTake = min($intPending, $intRemaining); + $intRangeStart = $intCursor; + $intRangeEnd = $intCursor + $intTake - 1; + + $tabPlan[] = [ + 'epr_id' => $epr_id, + 'start' => $intRangeStart, + 'end' => $intRangeEnd, + 'nb_pending' => $intPending, + 'skip' => false, + ]; + + $intCursor = $intRangeEnd + 1; + } + + return $tabPlan; +} + +/** + * Valide le plan libre (plage valide + au moins une assignation possible). + * Pas de contrôle de chevauchement ni d'insertion de séquence (MSIN-4445). + */ +function fxBibValidateFreeRangePlan($int_eve_id, $tabPlan, $intStart, $intEnd, $strLangue = 'fr') { + $int_eve_id = (int)$int_eve_id; + $intStart = (int)$intStart; + $intEnd = (int)$intEnd; + + if ($intStart <= 0 || $intEnd < $intStart) { + return [ + 'success' => false, + 'message' => fxBibMsg('bib_v4_global_batch_free_plan_invalid'), + ]; + } + + if (!is_array($tabPlan) || empty($tabPlan)) { + return [ + 'success' => false, + 'message' => fxBibMsg('bib_v4_global_batch_free_plan_invalid'), + ]; + } + + $blnHasAssignable = false; + + foreach ($tabPlan as $item) { + if (!empty($item['skip'])) { + continue; + } + + $epr_id = (int)($item['epr_id'] ?? 0); + $intItemStart = (int)($item['start'] ?? 0); + $intItemEnd = (int)($item['end'] ?? 0); + + if ($epr_id <= 0 || $intItemStart <= 0 || $intItemEnd < $intItemStart) { + return [ + 'success' => false, + 'message' => fxBibMsg('bib_v4_global_batch_free_plan_invalid'), + ]; + } + + if (fxBibEpreuveHasAnySequence($epr_id)) { + return [ + 'success' => false, + 'message' => fxBibMsg('bib_v4_global_batch_gen_has_seq'), + ]; + } + + $blnHasAssignable = true; + } + + if (!$blnHasAssignable) { + return [ + 'success' => false, + 'message' => fxBibMsg('bib_v4_global_batch_gen_no_pending'), + ]; + } + + return ['success' => true]; +} + +/** Stats agrégées + feedback quantité pour l'analyse libre. */ +function fxBibBuildGlobalBatchFreeAnalysis($tabPlan, $intStart, $intEnd) { + $intStart = (int)$intStart; + $intEnd = (int)$intEnd; + $intQty = ($intStart > 0 && $intEnd >= $intStart) ? ($intEnd - $intStart + 1) : 0; + $tabStats = fxBibBuildGlobalBatchGeneratedAnalysisStats($tabPlan); + + // MSIN-4445 — Feedback vs total à assigner (y compris épreuves non couvertes par la plage). + $intPendingAll = 0; + foreach ($tabPlan as $item) { + $intPendingAll += (int)($item['nb_pending'] ?? 0); + } + + $tabFeedback = fxBibFreeQtyFeedback($intQty, $intPendingAll); + + return [ + 'stats' => $tabStats, + 'feedback' => $tabFeedback, + 'info' => fxBibMsg( + 'bib_v4_ajax_global_free_analysis', + $tabStats['nb_epreuves'], + $intStart, + $intEnd, + $intPendingAll, + fxBibFreeQtyFeedbackMessage($tabFeedback) + ), + ]; +} + +/** + * Simulation globale libre (plages virtuelles, aucune écriture BD). + * + * @return array{success:bool, html?:string, info?:string, message?:string} + */ +function fxBibSimulateGlobalBatchFree($int_eve_id, $tabPlan, $intStart, $intEnd, $strSort1, $strSort2, $strLangue) { + $check = fxBibValidateFreeRangePlan($int_eve_id, $tabPlan, $intStart, $intEnd, $strLangue); + + if (!$check['success']) { + return $check; + } + + $htmlParts = []; + $intParticipants = 0; + $intBibs = 0; + + foreach ($tabPlan as $item) { + if (!empty($item['skip'])) { + continue; + } + + $epr_id = (int)$item['epr_id']; + $intItemStart = (int)$item['start']; + $intItemEnd = (int)$item['end']; + $participants = fxGetParticipantsForBatchAssign($epr_id, $strSort1, $strSort2); + $tabRanges = fxBibBuildVirtualRangesForPlan($intItemStart, $intItemEnd); + $simu = fxProcessBatchAssignments($epr_id, $participants, $tabRanges, 'simulation'); + + $intNbPending = count($participants); + $intParticipants += $intNbPending; + $intBibs += max(0, $intItemEnd - $intItemStart + 1); + + $tabEpreuve = fxGetEpreuve($epr_id); + $strEprLabel = $tabEpreuve ? fxBibEpreuveDisplayName($tabEpreuve, $strLangue) : ('#' . $epr_id); + + $strRangeLabel = fxBibMsg( + 'bib_v4_global_batch_gen_range_preview', + $intItemStart, + $intItemEnd, + $intNbPending + ); + + $htmlParts[] = '
' . fxBibEsc($strRangeLabel) . '
' + . renderBibView( + $epr_id, + 0, + $strLangue, + 'simulation', + $simu, + fxBibTexte('bib_v4_ajax_title_simulation'), + fxBibMsg( + 'bib_v4_ajax_sim_summary', + max(0, $intItemEnd - $intItemStart + 1), + $intNbPending, + max(0, $intNbPending - count($simu)) + ) + ) + . '