MSIN-4445 — Implement free assignment functionality for solo events without sequence creation. Add new actions for analysis, simulation, and planning of free ranges. Update UI components and JavaScript logic to support the new features, including feedback mechanisms for quantity validation. Increment version code to 4.72.828.

This commit is contained in:
2026-07-16 09:51:34 -04:00
parent b72ea70242
commit 77799add6d
7 changed files with 1393 additions and 7 deletions

View File

@ -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
// =====================

View File

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

View File

@ -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();
}
});

View File

@ -0,0 +1,327 @@
<?php
/**
* MSIN-4445 — Assignation globale solo libre (sans création de séquence).
* Plage Début/Fin (ou Début/Qté) fournie par l'utilisateur ; assignation via plages virtuelles.
*/
/**
* Calcule le feedback quantité vs inscriptions à assigner.
*
* @return array{status:string, amount:int, qty:int, pending:int}
*/
function fxBibFreeQtyFeedback($intQty, $intPending) {
$intQty = (int)$intQty;
$intPending = (int)$intPending;
$intDiff = $intQty - $intPending;
if ($intDiff === 0) {
return [
'status' => '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[] = '<div class="bib-global-sim-section">'
. '<h4 class="bib-global-sim-section__title">' . fxBibEsc($strEprLabel) . '</h4>'
. '<p class="bib-global-sim-section__range text-muted small">' . fxBibEsc($strRangeLabel) . '</p>'
. 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))
)
)
. '</div>';
}
$tabAnalysis = fxBibBuildGlobalBatchFreeAnalysis($tabPlan, $intStart, $intEnd);
$info = fxBibMsg(
'bib_v4_ajax_global_free_sim_summary',
$tabAnalysis['stats']['nb_epreuves'],
$intBibs,
(int)($tabAnalysis['feedback']['pending'] ?? $intParticipants),
fxBibFreeQtyFeedbackMessage($tabAnalysis['feedback'])
);
$html = fxBibWrapGlobalSimView($info, implode('', $htmlParts), 'bib-global-sim-view--free');
return [
'success' => true,
'html' => $html,
'info' => $info,
'feedback' => $tabAnalysis['feedback'],
];
}
/**
* Prépare les plans GO libres (sans INSERT séquence).
*
* @return array{success:bool, plans?:array, stats?:array, feedback?:array, message?:string}
*/
function fxBibPrepareFreeRangePlans($int_eve_id, $tabPlan, $intStart, $intEnd, $strLangue = 'fr') {
$check = fxBibValidateFreeRangePlan($int_eve_id, $tabPlan, $intStart, $intEnd, $strLangue);
if (!$check['success']) {
return $check;
}
$tabPlans = [];
foreach ($tabPlan as $item) {
if (!empty($item['skip'])) {
continue;
}
$tabPlans[] = [
'epr_id' => (int)$item['epr_id'],
'start' => (int)$item['start'],
'end' => (int)$item['end'],
];
}
$tabAnalysis = fxBibBuildGlobalBatchFreeAnalysis($tabPlan, $intStart, $intEnd);
return [
'success' => true,
'plans' => $tabPlans,
'stats' => $tabAnalysis['stats'],
'feedback' => $tabAnalysis['feedback'],
];
}

View File

@ -3444,6 +3444,15 @@ function fxBibStaticFallback($clef) {
'bib_v4_global_batch_go_complete_title' => ['fr' => 'Assignation terminée', 'en' => 'Assignment complete'],
'bib_v4_global_batch_go_complete_summary' => ['fr' => '%d dossard(s) assigné(s) sur %d · %d épreuve(s)', 'en' => '%d bib(s) assigned of %d · %d race(s)'],
'bib_v4_global_batch_go_complete_reload' => ['fr' => 'OK — Recharger la page', 'en' => 'OK — Reload page'],
// MSIN-4445 — Assignation libre.
'bib_v4_global_batch_free_title' => ['fr' => 'Assignation libre', 'en' => 'Free assignment'],
'bib_v4_global_batch_free_hint' => ['fr' => 'Épreuves solo sans séquence : choisissez Début et Fin (ou Qté). Les dossards sont assignés sans créer de séquence. Le Go reste disponible même si la quantité n\'est pas juste.', 'en' => 'Solo races without sequences: choose Start and End (or Qty). Bibs are assigned without creating a sequence. Go stays available even if the quantity is not exact.'],
'bib_v4_global_batch_free_qty_juste' => ['fr' => 'Quantité juste', 'en' => 'Exact quantity'],
'bib_v4_global_batch_free_qty_manquante' => ['fr' => 'Quantité manquante : %d', 'en' => 'Missing quantity: %d'],
'bib_v4_global_batch_free_qty_supplementaire' => ['fr' => 'Quantité supplémentaire : %d', 'en' => 'Extra quantity: %d'],
'bib_v4_global_batch_free_plan_invalid' => ['fr' => 'Sélection invalide : Début, Fin (ou Qté) et au moins une épreuve avec inscriptions à assigner.', 'en' => 'Invalid selection: Start, End (or Qty) and at least one race with registrations to assign.'],
'bib_v4_ajax_global_free_analysis' => ['fr' => '%d épreuve(s) · plage %d → %d · %d à assigner · %s', 'en' => '%d race(s) · range %d → %d · %d to assign · %s'],
'bib_v4_ajax_global_free_sim_summary' => ['fr' => '%d épreuve(s) · %d dossards · %d à assigner · %s', 'en' => '%d race(s) · %d bibs · %d to assign · %s'],
'bib_v4_batch_order_2' => ['fr' => 'Tri secondaire', 'en' => 'Secondary sort'],
'bib_v4_batch_order_none' => ['fr' => '— Aucun —', 'en' => '— None —'],
'bib_v4_batch_sort_group_fixed' => ['fr' => 'Critères fixes', 'en' => 'Fixed criteria'],
@ -3701,6 +3710,11 @@ function fxBibModuleJsDataAttrs() {
'global-batch-complete-title' => 'bib_v4_global_batch_go_complete_title',
'global-batch-complete-summary' => 'bib_v4_global_batch_go_complete_summary',
'global-batch-complete-reload' => 'bib_v4_global_batch_go_complete_reload',
// MSIN-4445
'global-free-qty-juste' => 'bib_v4_global_batch_free_qty_juste',
'global-free-qty-manquante' => 'bib_v4_global_batch_free_qty_manquante',
'global-free-qty-supplementaire' => 'bib_v4_global_batch_free_qty_supplementaire',
'global-free-plan-invalid' => 'bib_v4_global_batch_free_plan_invalid',
'reset-all-confirm' => 'bib_v4_js_reset_all_confirm',
'reset-all-locked' => 'bib_v4_js_reset_all_locked',
'assign-full' => 'bib_v4_ajax_assign_full',
@ -4660,6 +4674,8 @@ function renderBibToolNav($int_eve_id, $tabEpreuves, $strLangue = 'fr') {
// MSIN-4443 — Assignation globale sans séquence (module dédié).
require_once __DIR__ . '/inc_fx_bib_global_generated.php';
// MSIN-4445 — Assignation globale libre (sans création de séquence).
require_once __DIR__ . '/inc_fx_bib_global_free.php';
/**
* MSIN-4436 / MSIN-4443 — Coquille assignation globale (chargement différé à l'ouverture).
@ -4952,14 +4968,132 @@ function renderBibGlobalBatchPanelGenerated($int_eve_id, $tabEpreuves, $strLangu
}
/**
* MSIN-4436 / MSIN-4443 — Panneau assignation globale (deux blocs indépendants).
* MSIN-4445 — Bloc assignation libre (Début/Qté/Fin, sans création de séquence).
*/
function renderBibGlobalBatchPanelFree($int_eve_id, $tabEpreuves, $strLangue) {
$int_eve_id = (int)$int_eve_id;
$tabSolo = fxBibGetSoloEpreuvesWithoutSequence($int_eve_id, $tabEpreuves);
if (empty($tabSolo)) {
return '';
}
$tabGlobalSort = fxBibGetSortOptionsForEpreuve(0, $int_eve_id, $strLangue, false, false);
ob_start();
echo fxBibGlobalBatchSectionOpen('free', 'bib_v4_global_batch_free_title', $strLangue);
?>
<p class="bib-global-batch__hint text-muted small mb-2"><?php fxBibTexteTrad('bib_v4_global_batch_free_hint', 1); ?></p>
<p class="bib-global-batch__team-note text-muted small mb-3"><?php fxBibTexteTrad('bib_v4_global_batch_team_note', 1); ?></p>
<div class="bib-global-free-range mb-3" data-range-pilot="">
<div class="bib-global-free-range__headers">
<div><?php echo fxBibColHeader('bib_v4_col_start'); ?></div>
<div><?php echo fxBibColHeader('bib_v4_col_qty'); ?></div>
<div class="bib-global-free-range__or" aria-hidden="true"><?php fxBibTexte('bib_v4_range_or', 1); ?></div>
<div><?php echo fxBibColHeader('bib_v4_col_end'); ?></div>
</div>
<div class="bib-global-free-range__fields">
<input type="number"
class="form-control form-control-sm bib-global-free-start bib-field--active"
min="1"
step="1"
value=""
inputmode="numeric"
placeholder="">
<input type="number"
class="form-control form-control-sm bib-global-free-qty bib-field--idle"
min="1"
step="1"
value=""
inputmode="numeric"
disabled>
<span class="bib-range-or bib-global-free-range__or" aria-hidden="true"><?php fxBibTexte('bib_v4_range_or', 1); ?></span>
<input type="number"
class="form-control form-control-sm bib-global-free-end bib-field--idle"
min="1"
step="1"
value=""
inputmode="numeric"
disabled>
</div>
<p class="bib-global-free-range__feedback text-muted small mb-0" aria-live="polite"></p>
</div>
<div class="bib-global-batch__epreuves">
<?php foreach ($tabSolo as $epr) {
$eprId = (int)$epr['epr_id'];
$strLabel = fxBibEpreuveDisplayName($epr, $strLangue);
$infoBib = fxInfosBibEpreuve($eprId);
$intPending = (int)($infoBib['a_assigner'] ?? 0);
$blnInactive = ($intPending <= 0);
?>
<div class="bib-global-epr<?php echo $blnInactive ? ' bib-global-epr--inactive' : ''; ?>"
data-epr-id="<?php echo $eprId; ?>"
data-pending="<?php echo $intPending; ?>">
<label class="bib-global-epr__label">
<input type="checkbox"
class="bib-global-free-epr-select"
<?php echo $blnInactive ? 'disabled' : 'checked'; ?>
data-epr-id="<?php echo $eprId; ?>">
<span class="bib-global-epr__name"><?php echo fxBibEsc($strLabel); ?></span>
<span class="bib-global-epr__pending text-muted small">
<?php echo fxBibEsc(fxBibMsg('bib_v4_global_batch_pending', $intPending)); ?>
</span>
</label>
</div>
<?php } ?>
</div>
<div class="bib-global-batch__order mb-2">
<label><?php fxBibTexteAide('bib_v4_batch_order', 1); ?></label>
<select class="form-control form-control-sm bib-global-free-batch-order">
<?php echo fxBibRenderBatchSortOptions($tabGlobalSort, 'ba:2', false, $strLangue); ?>
</select>
</div>
<div class="bib-global-batch__order mb-2">
<label><?php fxBibTexteAide('bib_v4_batch_order_2', 1); ?></label>
<select class="form-control form-control-sm bib-global-free-batch-order-2">
<?php echo fxBibRenderBatchSortOptions($tabGlobalSort, '', true, $strLangue); ?>
</select>
</div>
<p class="bib-global-batch__info bib-global-free-batch__info text-muted small mb-2"></p>
<div class="epr-action-row">
<?php echo fxBibActionGroupOpen('epr-action-global-free-sim-group'); ?>
<?php echo fxBibBtnAideSegmentOpen(); ?>
<button type="button" class="btn btn-sm btn-secondary btn-simuler-global-free-batch">
<?php fxBibTexte('bib_v4_batch_simulate', 1); ?>
</button>
<?php echo fxBibAideButton('bib_v4_batch_simulate', 'secondary', false); ?>
<?php echo fxBibBtnAideSegmentClose('bib_v4_batch_simulate'); ?>
<?php echo fxBibActionGroupClose(); ?>
<?php echo fxBibActionGroupOpen('epr-action-global-free-go-group'); ?>
<?php echo fxBibBtnAideSegmentOpen(); ?>
<button type="button" class="btn btn-sm btn-success btn-go-global-free-batch">
<?php fxBibTexte('bib_v4_batch_go', 1); ?>
</button>
<?php echo fxBibAideButton('bib_v4_batch_go', 'success', false); ?>
<?php echo fxBibBtnAideSegmentClose('bib_v4_batch_go'); ?>
<?php echo fxBibActionGroupClose(); ?>
</div>
<?php
echo fxBibGlobalBatchSectionClose();
return ob_get_clean();
}
/**
* MSIN-4436 / MSIN-4443 / MSIN-4445 — Panneau assignation globale (blocs indépendants).
* Appelé en AJAX à l'ouverture du panneau (pas au chargement page).
*/
function renderBibGlobalBatchPanel($int_eve_id, $tabEpreuves, $tabBibAssignments, $strLangue) {
$strExisting = renderBibGlobalBatchPanelExisting($int_eve_id, $tabEpreuves, $strLangue);
$strGenerated = renderBibGlobalBatchPanelGenerated($int_eve_id, $tabEpreuves, $strLangue);
$strFree = renderBibGlobalBatchPanelFree($int_eve_id, $tabEpreuves, $strLangue);
if ($strExisting === '' && $strGenerated === '') {
if ($strExisting === '' && $strGenerated === '' && $strFree === '') {
return '';
}
@ -4977,6 +5111,9 @@ function renderBibGlobalBatchPanel($int_eve_id, $tabEpreuves, $tabBibAssignments
if ($strGenerated !== '') {
echo $strGenerated;
}
if ($strFree !== '') {
echo $strFree;
}
?>
</div>
</div>

View File

@ -7,8 +7,8 @@
* Constantes *
*
**************/
define('_VERSION_CODE', '4.72.827');
define('_DATE_CODE', '2026-07-15');
define('_VERSION_CODE', '4.72.828');
define('_DATE_CODE', '2026-07-16');
//MSIN-4290
define('QR_SECRET_KEY', 'ms1_qr_2026_cle_secrete_longue_et_fixe');

View File

@ -0,0 +1,67 @@
-- MSIN-4445 — Assignation libre (globale solo, sans création de séquence)
-- Idempotent. Exécuter après déploiement PHP/JS/CSS.
-- Prérequis : MSIN-4443 (assignation globale sans séquence) déjà en place.
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_global_batch_free_title', 'fr', 'Assignation libre', 'Assigne des dossards dans une plage Début/Fin sans créer de séquence.', 'compte.php', '', 0, 1, '', '', '', NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_global_batch_free_title' 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_global_batch_free_title', 'en', 'Free assignment', 'Assigns bibs in a Start/End range without creating a sequence.', 'compte.php', '', 0, 1, '', '', '', NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_global_batch_free_title' AND info_langue = 'en' 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_global_batch_free_hint', 'fr', 'Épreuves solo sans séquence : choisissez Début et Fin (ou Qté). Les dossards sont assignés sans créer de séquence. Le Go reste disponible même si la quantité n''est pas juste.', '', 'compte.php', '', 0, 1, '', '', '', NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_global_batch_free_hint' 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_global_batch_free_hint', 'en', 'Solo races without sequences: choose Start and End (or Qty). Bibs are assigned without creating a sequence. Go stays available even if the quantity is not exact.', '', 'compte.php', '', 0, 1, '', '', '', NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_global_batch_free_hint' AND info_langue = 'en' 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_global_batch_free_qty_juste', 'fr', 'Quantité juste', '', 'compte.php', '', 0, 1, '', '', '', NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_global_batch_free_qty_juste' 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_global_batch_free_qty_juste', 'en', 'Exact quantity', '', 'compte.php', '', 0, 1, '', '', '', NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_global_batch_free_qty_juste' AND info_langue = 'en' 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_global_batch_free_qty_manquante', 'fr', 'Quantité manquante : %d', '', 'compte.php', '', 0, 1, '', '', '', NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_global_batch_free_qty_manquante' 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_global_batch_free_qty_manquante', 'en', 'Missing quantity: %d', '', 'compte.php', '', 0, 1, '', '', '', NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_global_batch_free_qty_manquante' AND info_langue = 'en' 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_global_batch_free_qty_supplementaire', 'fr', 'Quantité supplémentaire : %d', '', 'compte.php', '', 0, 1, '', '', '', NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_global_batch_free_qty_supplementaire' 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_global_batch_free_qty_supplementaire', 'en', 'Extra quantity: %d', '', 'compte.php', '', 0, 1, '', '', '', NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_global_batch_free_qty_supplementaire' AND info_langue = 'en' 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_global_batch_free_plan_invalid', 'fr', 'Sélection invalide : Début, Fin (ou Qté) et au moins une épreuve avec inscriptions à assigner.', '', 'compte.php', '', 0, 1, '', '', '', NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_global_batch_free_plan_invalid' 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_global_batch_free_plan_invalid', 'en', 'Invalid selection: Start, End (or Qty) and at least one race with registrations to assign.', '', 'compte.php', '', 0, 1, '', '', '', NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_global_batch_free_plan_invalid' AND info_langue = 'en' 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_ajax_global_free_analysis', 'fr', '%d épreuve(s) · plage %d → %d · %d à assigner · %s', '', 'compte.php', '', 0, 1, '', '', '', NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_ajax_global_free_analysis' 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_ajax_global_free_analysis', 'en', '%d race(s) · range %d → %d · %d to assign · %s', '', 'compte.php', '', 0, 1, '', '', '', NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_ajax_global_free_analysis' AND info_langue = 'en' 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_ajax_global_free_sim_summary', 'fr', '%d épreuve(s) · %d dossards · %d à assigner · %s', '', 'compte.php', '', 0, 1, '', '', '', NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_ajax_global_free_sim_summary' 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_ajax_global_free_sim_summary', 'en', '%d race(s) · %d bibs · %d to assign · %s', '', 'compte.php', '', 0, 1, '', '', '', NOW()
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM info WHERE info_clef = 'bib_v4_ajax_global_free_sim_summary' AND info_langue = 'en' AND info_prg = 'compte.php');