Files
ms1inscription-v5/php/inc_fx_bib_global_generated.php

658 lines
19 KiB
PHP

<?php
/**
* MSIN-4443 / MSIN-4445 — Assignation globale solo sans séquence existante.
* Création automatique de plages puis assignation.
* Début obligatoire ; Fin/Qté optionnels (sinon taille = à assigner).
*/
/** True si l'épreuve a au moins une séquence enregistrée. */
function fxBibEpreuveHasAnySequence($epr_id) {
global $objDatabase;
$epr_id = (int)$epr_id;
if ($epr_id <= 0) {
return false;
}
$intCount = (int)$objDatabase->fxGetVar("
SELECT COUNT(*)
FROM inscriptions_epreuves_bib
WHERE epr_id = $epr_id
AND epr_bib_start IS NOT NULL
AND epr_bib_finish IS NOT NULL
");
return $intCount > 0;
}
/**
* Épreuves solo actives (avec ou sans séquence), triées par epr_id.
*
* @return array[] lignes inscriptions_epreuves
*/
function fxBibGetSoloEpreuves($int_eve_id, $tabEpreuves = null) {
global $objDatabase;
$int_eve_id = (int)$int_eve_id;
if ($int_eve_id <= 0) {
return [];
}
if (!is_array($tabEpreuves)) {
$tabEpreuves = $objDatabase->fxGetResults(
"SELECT * FROM inscriptions_epreuves e"
. " WHERE e.eve_id = $int_eve_id AND epr_actif = 1"
. " ORDER BY epr_id"
);
}
$out = [];
foreach ($tabEpreuves as $epr) {
if ((int)($epr['epr_equipe'] ?? 0) === 1) {
continue;
}
$out[] = $epr;
}
return $out;
}
/**
* Épreuves solo sans aucune séquence, triées par epr_id (ordre page).
*
* @return array[] lignes inscriptions_epreuves
*/
function fxBibGetSoloEpreuvesWithoutSequence($int_eve_id, $tabEpreuves = null) {
$out = [];
foreach (fxBibGetSoloEpreuves($int_eve_id, $tabEpreuves) as $epr) {
if (fxBibEpreuveHasAnySequence((int)$epr['epr_id'])) {
continue;
}
$out[] = $epr;
}
return $out;
}
/** Bloc 1 disponible : au moins 2 épreuves solo avec séquences sélectionnables. */
function fxBibGlobalBatchHasExistingBlock($int_eve_id, $tabEpreuves) {
$int_eve_id = (int)$int_eve_id;
$intCount = 0;
foreach ($tabEpreuves as $epr) {
if ((int)($epr['epr_equipe'] ?? 0) === 1) {
continue;
}
if (empty(fxBibGetSelectableRangesForGlobalBatch((int)$epr['epr_id']))) {
continue;
}
$intCount++;
if ($intCount >= 2) {
return true;
}
}
return false;
}
/** Bloc 2 disponible : au moins 1 épreuve solo sans séquence. */
function fxBibGlobalBatchHasGeneratedBlock($int_eve_id, $tabEpreuves) {
return count(fxBibGetSoloEpreuvesWithoutSequence($int_eve_id, $tabEpreuves)) >= 1;
}
/** Tuile / coquille assignation globale visible si l'un des blocs est utilisable. */
function fxBibGlobalBatchToolIsAvailable($int_eve_id, $tabEpreuves) {
return fxBibGlobalBatchHasExistingBlock($int_eve_id, $tabEpreuves)
|| fxBibGlobalBatchHasGeneratedBlock($int_eve_id, $tabEpreuves);
}
/**
* MSIN-4445 — Feedback quantité vs inscriptions à assigner (Fin/Qté optionnels).
*
* @return array{status:string, amount:int, qty:int, pending:int}
*/
function fxBibGeneratedQtyFeedback($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é (réutilise les clés MSIN-4445). */
function fxBibGeneratedQtyFeedbackMessage(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.
*/
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
");
}
/**
* Avertit si des dossards de la plage sont déjà portés, ou si une séquence chevauche.
*
* @return array{has:bool, count:int, message:string}
*/
function fxBibGeneratedDuplicateRisk($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) {
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);
$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'),
];
}
/**
* Valide et ordonne les epr_id du bloc généré (solo, sans séquence, ordre epr_id).
*
* @return int[]
*/
function fxBibParseGlobalBatchGeneratedEprIds($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;
}
if (fxBibEpreuveHasAnySequence($epr_id)) {
continue;
}
$tabWanted[$epr_id] = true;
}
if (empty($tabWanted)) {
return [];
}
$tabIds = array_keys($tabWanted);
sort($tabIds, SORT_NUMERIC);
return $tabIds;
}
/**
* Plan de plages générées, enchaînement par epr_id.
* - Sans Fin ($intEndBib <= 0) : taille = à assigner (comportement historique).
* - Avec Fin/Qté : plage bornée ; le surplus va sur la dernière épreuve (impression d'avance).
*
* @return array[] [{epr_id, start, end, nb_pending, skip}]
*/
function fxBibBuildGeneratedRangePlan($int_eve_id, $tabEprIds, $intStartBib, $strSort1, $strSort2 = '', $intEndBib = 0) {
$int_eve_id = (int)$int_eve_id;
$intStartBib = (int)$intStartBib;
$intEndBib = (int)$intEndBib;
$tabEprIds = fxBibParseGlobalBatchGeneratedEprIds($tabEprIds, $int_eve_id);
$blnBounded = ($intEndBib > 0 && $intEndBib >= $intStartBib);
$intCursor = $intStartBib;
$tabPlan = [];
$tabSorts = is_array($strSort1)
? fxBibNormalizeSortKeysList($strSort1)
: fxBibNormalizeSortKeysList([$strSort1, $strSort2]);
foreach ($tabEprIds as $epr_id) {
$participants = fxGetParticipantsForBatchAssign($epr_id, $tabSorts);
$intPending = count($participants);
if ($intPending <= 0) {
$tabPlan[] = [
'epr_id' => $epr_id,
'start' => 0,
'end' => 0,
'nb_pending' => 0,
'skip' => true,
];
continue;
}
if ($blnBounded) {
$intRemaining = $intEndBib - $intCursor + 1;
if ($intRemaining <= 0) {
$tabPlan[] = [
'epr_id' => $epr_id,
'start' => 0,
'end' => 0,
'nb_pending' => $intPending,
'skip' => true,
];
continue;
}
$intTake = min($intPending, $intRemaining);
$intStart = $intCursor;
$intEnd = $intCursor + $intTake - 1;
} else {
$intStart = $intCursor;
$intEnd = $intCursor + $intPending - 1;
}
$tabPlan[] = [
'epr_id' => $epr_id,
'start' => $intStart,
'end' => $intEnd,
'nb_pending' => $intPending,
'skip' => false,
];
$intCursor = $intEnd + 1;
}
// MSIN-4445 — Marge Fin/Qté : étendre la dernière séquence jusqu'à Fin (blancs imprimables).
if ($blnBounded) {
$intLastIdx = -1;
foreach ($tabPlan as $i => $item) {
if (empty($item['skip'])) {
$intLastIdx = $i;
}
}
if ($intLastIdx >= 0 && (int)$tabPlan[$intLastIdx]['end'] < $intEndBib) {
$tabPlan[$intLastIdx]['end'] = $intEndBib;
}
}
return $tabPlan;
}
/** Plages virtuelles pour simulation (même format que fxGetRangesForBatchAssign). */
function fxBibBuildVirtualRangesForPlan($intStart, $intEnd) {
$intStart = (int)$intStart;
$intEnd = (int)$intEnd;
if ($intStart <= 0 || $intEnd < $intStart) {
return [];
}
return [[
'id' => 0,
'start' => $intStart,
'end' => $intEnd,
]];
}
/**
* Valide le plan généré (chevauchements, épreuves sans séquence, au moins un à assigner).
*/
function fxBibValidateGeneratedRangePlan($int_eve_id, $tabPlan, $strLangue = 'fr') {
$int_eve_id = (int)$int_eve_id;
if (!is_array($tabPlan) || empty($tabPlan)) {
return [
'success' => false,
'message' => fxBibMsg('bib_v4_global_batch_gen_plan_invalid'),
];
}
$blnHasAssignable = false;
foreach ($tabPlan as $item) {
if (!empty($item['skip'])) {
continue;
}
$blnHasAssignable = true;
$epr_id = (int)($item['epr_id'] ?? 0);
$intStart = (int)($item['start'] ?? 0);
$intEnd = (int)($item['end'] ?? 0);
if ($epr_id <= 0 || $intStart <= 0 || $intEnd < $intStart) {
return [
'success' => false,
'message' => fxBibMsg('bib_v4_global_batch_gen_plan_invalid'),
];
}
if (fxBibEpreuveHasAnySequence($epr_id)) {
return [
'success' => false,
'message' => fxBibMsg('bib_v4_global_batch_gen_has_seq'),
];
}
$tabOverlaps = fxBibGetOverlappingRanges($int_eve_id, $intStart, $intEnd, 0, $epr_id);
if (!empty($tabOverlaps)) {
return [
'success' => false,
'message' => fxBibFormatOverlapConflictMessage($tabOverlaps, $strLangue),
];
}
}
if (!$blnHasAssignable) {
return [
'success' => false,
'message' => fxBibMsg('bib_v4_global_batch_gen_no_pending'),
];
}
return ['success' => true];
}
/**
* Crée les séquences en BD pour le plan validé.
*
* @return array{success:bool, message?:string, plans?:array}
*/
function fxBibCreateGeneratedRanges($int_eve_id, $tabPlan, $strLangue = 'fr') {
global $objDatabase;
$int_eve_id = (int)$int_eve_id;
$tabCreated = [];
if (fxBibProductionEventIsLocked($int_eve_id)) {
return [
'success' => false,
'message' => fxBibMsg('bib_v5_production_locked_error'),
];
}
foreach ($tabPlan as $item) {
if (!empty($item['skip'])) {
continue;
}
$epr_id = (int)($item['epr_id'] ?? 0);
$intStart = (int)($item['start'] ?? 0);
$intEnd = (int)($item['end'] ?? 0);
if ($epr_id <= 0 || $intStart <= 0 || $intEnd < $intStart) {
return [
'success' => false,
'message' => fxBibMsg('bib_v4_global_batch_gen_plan_invalid'),
];
}
if (fxBibEpreuveHasAnySequence($epr_id)) {
return [
'success' => false,
'message' => fxBibMsg('bib_v4_global_batch_gen_has_seq'),
];
}
$tabOverlaps = fxBibGetOverlappingRanges($int_eve_id, $intStart, $intEnd, 0, $epr_id);
if (!empty($tabOverlaps)) {
return [
'success' => false,
'message' => fxBibFormatOverlapConflictMessage($tabOverlaps, $strLangue),
];
}
// MSIN-4443 — Création séquence : vérifier INSERT + last_id (évite le message générique « Erreur »).
$sqlInsert = "
INSERT INTO inscriptions_epreuves_bib
(epr_id, epr_bib_start, epr_bib_finish, update_date, update_qui, eve_id)
VALUES
($epr_id, $intStart, $intEnd, NOW(), 'admin', $int_eve_id)
";
$qryInsert = $objDatabase->fxQuery($sqlInsert);
if ($qryInsert === false) {
return [
'success' => false,
'message' => fxBibMsg('bib_v4_global_batch_gen_insert_failed'),
];
}
$intBibId = 0;
if (method_exists($objDatabase, 'fxGetLastId')) {
$intBibId = (int)$objDatabase->fxGetLastId();
} elseif (isset($objDatabase->last_id)) {
$intBibId = (int)$objDatabase->last_id;
}
// Repli : si insert_id absent, retrouver la ligne fraîchement créée.
if ($intBibId <= 0) {
$intBibId = (int)$objDatabase->fxGetVar("
SELECT epr_bib_id
FROM inscriptions_epreuves_bib
WHERE epr_id = $epr_id
AND epr_bib_start = $intStart
AND epr_bib_finish = $intEnd
AND eve_id = $int_eve_id
ORDER BY epr_bib_id DESC
LIMIT 1
");
}
if ($intBibId <= 0) {
return [
'success' => false,
'message' => fxBibMsg('bib_v4_global_batch_gen_insert_failed'),
];
}
$tabCreated[] = [
'epr_id' => $epr_id,
'ranges' => [$intBibId],
'start' => $intStart,
'end' => $intEnd,
];
}
return [
'success' => true,
'plans' => $tabCreated,
];
}
/** Statistiques agrégées pour l'analyse du bloc généré. */
function fxBibBuildGlobalBatchGeneratedAnalysisStats($tabPlan) {
$intEpreuves = 0;
$intPending = 0;
$intBibs = 0;
foreach ($tabPlan as $item) {
if (!empty($item['skip'])) {
continue;
}
$intEpreuves++;
$intPending += (int)($item['nb_pending'] ?? 0);
$intStart = (int)($item['start'] ?? 0);
$intEnd = (int)($item['end'] ?? 0);
if ($intEnd >= $intStart && $intStart > 0) {
$intBibs += ($intEnd - $intStart + 1);
}
}
return [
'nb_epreuves' => $intEpreuves,
'nb_participants' => $intPending,
'nb_bibs' => $intBibs,
];
}
/**
* Simulation globale bloc généré (plages virtuelles, aucune écriture BD).
*
* @return array{success:bool, html?:string, info?:string, message?:string}
*/
function fxBibSimulateGlobalBatchGenerated($int_eve_id, $tabPlan, $strSort1, $strSort2, $strLangue) {
$int_eve_id = (int)$int_eve_id;
$check = fxBibValidateGeneratedRangePlan($int_eve_id, $tabPlan, $strLangue);
$tabSorts = is_array($strSort1)
? fxBibNormalizeSortKeysList($strSort1)
: fxBibNormalizeSortKeysList([$strSort1, $strSort2]);
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'];
$intStart = (int)$item['start'];
$intEnd = (int)$item['end'];
$participants = fxGetParticipantsForBatchAssign($epr_id, $tabSorts);
$tabRanges = fxBibBuildVirtualRangesForPlan($intStart, $intEnd);
$simu = fxProcessBatchAssignments($epr_id, $participants, $tabRanges, 'simulation');
$intNbPending = count($participants);
$intParticipants += $intNbPending;
$intBibs += max(0, $intEnd - $intStart + 1);
$tabEpreuve = fxGetEpreuve($epr_id);
$strEprLabel = $tabEpreuve ? fxBibEpreuveDisplayName($tabEpreuve, $strLangue) : ('#' . $epr_id);
$strRangeLabel = fxBibMsg(
'bib_v4_global_batch_gen_range_preview',
$intStart,
$intEnd,
$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, $intEnd - $intStart + 1),
$intNbPending,
max(0, $intNbPending - count($simu))
)
)
. '</div>';
}
$info = fxBibMsg(
'bib_v4_ajax_global_gen_sim_summary',
fxBibBuildGlobalBatchGeneratedAnalysisStats($tabPlan)['nb_epreuves'],
$intBibs,
$intParticipants
);
$html = fxBibWrapGlobalSimView($info, implode('', $htmlParts), 'bib-global-sim-view--generated');
return [
'success' => true,
'html' => $html,
'info' => $info,
];
}