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. * MSIN-4445 — utilisé par l'assignation libre. * * @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; } /** MSIN-4445 — Bloc libre : au moins 1 épreuve solo (avec ou sans séquence). */ function fxBibGlobalBatchHasFreeBlock($int_eve_id, $tabEpreuves) { return count(fxBibGetSoloEpreuves($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) || fxBibGlobalBatchHasFreeBlock($int_eve_id, $tabEpreuves); } /** * 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 : taille = a_assigner (sans marge), enchaînement par epr_id. * * @return array[] [{epr_id, start, end, nb_pending, skip}] */ function fxBibBuildGeneratedRangePlan($int_eve_id, $tabEprIds, $intStartBib, $strSort1, $strSort2) { $int_eve_id = (int)$int_eve_id; $intStartBib = (int)$intStartBib; $tabEprIds = fxBibParseGlobalBatchGeneratedEprIds($tabEprIds, $int_eve_id); $intCursor = $intStartBib; $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; } $intStart = $intCursor; $intEnd = $intCursor + $intPending - 1; $tabPlan[] = [ 'epr_id' => $epr_id, 'start' => $intStart, 'end' => $intEnd, 'nb_pending' => $intPending, 'skip' => false, ]; $intCursor = $intEnd + 1; } 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); 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, $strSort1, $strSort2); $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[] = '
' . fxBibEsc($strRangeLabel) . '
' . 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)) ) ) . '