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 = []; 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; } 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); 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($strEprLabel) . '

' . '

' . 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)) ) ) . '
'; } $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, ]; }