Files
ms1inscription-v5/php/inc_fx_bib_global_free.php

445 lines
13 KiB
PHP

<?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 '';
}
/**
* MSIN-4445 — Nombre de dossards déjà assignés dans [start, end] pour l'événement.
* Jointure via épreuve : plus fiable que p.eve_id seul.
*/
function fxBibCountAssignedBibsInRange($int_eve_id, $intStart, $intEnd) {
global $objDatabase;
$int_eve_id = (int)$int_eve_id;
$intStart = (int)$intStart;
$intEnd = (int)$intEnd;
if ($int_eve_id <= 0 || $intStart <= 0 || $intEnd < $intStart) {
return 0;
}
$sqlBib = fxBibSqlNoBibUnsigned('p.no_bib');
return (int)$objDatabase->fxGetVar("
SELECT COUNT(DISTINCT $sqlBib)
FROM resultats_participants p
INNER JOIN inscriptions_epreuves e ON e.epr_id = p.epr_id
WHERE e.eve_id = $int_eve_id
AND IFNULL(p.is_cancelled, 0) = 0
AND p.no_bib IS NOT NULL
AND TRIM(p.no_bib) <> ''
AND $sqlBib BETWEEN $intStart AND $intEnd
");
}
/**
* MSIN-4445 — Risque de doublons pour une plage libre.
* Avertit si des dossards de la plage sont déjà portés, ou si une séquence
* existante chevauche la plage (même hors épreuves sélectionnées).
*
* @return array{has:bool, count:int, message:string}
*/
function fxBibFreeDuplicateRisk($int_eve_id, $tabPlan, $intStart = 0, $intEnd = 0) {
$int_eve_id = (int)$int_eve_id;
$intStart = (int)$intStart;
$intEnd = (int)$intEnd;
if ($intStart <= 0 || $intEnd < $intStart) {
// Repli : bornes du plan si Début/Fin absents.
foreach ((array)$tabPlan as $item) {
if (!empty($item['skip'])) {
continue;
}
$intItemStart = (int)($item['start'] ?? 0);
$intItemEnd = (int)($item['end'] ?? 0);
if ($intItemStart <= 0 || $intItemEnd < $intItemStart) {
continue;
}
if ($intStart <= 0 || $intItemStart < $intStart) {
$intStart = $intItemStart;
}
if ($intItemEnd > $intEnd) {
$intEnd = $intItemEnd;
}
}
}
if ($int_eve_id <= 0 || $intStart <= 0 || $intEnd < $intStart) {
return ['has' => false, 'count' => 0, 'message' => ''];
}
$intCount = fxBibCountAssignedBibsInRange($int_eve_id, $intStart, $intEnd);
// Séquences déjà configurées qui chevauchent la plage (autre épreuve).
$tabOverlaps = fxBibGetOverlappingRanges($int_eve_id, $intStart, $intEnd, 0, 0);
$intOverlapSeq = is_array($tabOverlaps) ? count($tabOverlaps) : 0;
if ($intCount <= 0 && $intOverlapSeq <= 0) {
return ['has' => false, 'count' => 0, 'message' => ''];
}
return [
'has' => true,
'count' => max($intCount, $intOverlapSeq),
'message' => fxBibMsg('bib_v4_global_batch_free_dupes_warn'),
];
}
/**
* MSIN-4445 — Valide et ordonne les epr_id du bloc libre (solo, avec ou sans séquence).
*
* @return int[]
*/
function fxBibParseGlobalBatchFreeEprIds($eprIds, $int_eve_id) {
if (!is_array($eprIds)) {
return [];
}
$int_eve_id = (int)$int_eve_id;
$tabWanted = [];
foreach ($eprIds as $epr_id) {
$epr_id = (int)$epr_id;
if ($epr_id <= 0) {
continue;
}
$check = fxBibAssertSoloEpreuveForGlobalBatch($epr_id, $int_eve_id);
if (!$check['success']) {
continue;
}
$tabWanted[$epr_id] = true;
}
if (empty($tabWanted)) {
return [];
}
$tabIds = array_keys($tabWanted);
sort($tabIds, SORT_NUMERIC);
return $tabIds;
}
/**
* 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 = fxBibParseGlobalBatchFreeEprIds($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'),
];
}
// MSIN-4445 — OK même si une séquence existe déjà (aucune création de séquence).
$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($int_eve_id, $tabPlan, $intStart, $intEnd) {
$int_eve_id = (int)$int_eve_id;
$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);
$tabDupes = fxBibFreeDuplicateRisk($int_eve_id, $tabPlan, $intStart, $intEnd);
return [
'stats' => $tabStats,
'feedback' => $tabFeedback,
'duplicates' => $tabDupes,
'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($int_eve_id, $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($int_eve_id, $tabPlan, $intStart, $intEnd);
return [
'success' => true,
'plans' => $tabPlans,
'stats' => $tabAnalysis['stats'],
'feedback' => $tabAnalysis['feedback'],
'duplicates' => $tabAnalysis['duplicates'],
];
}