Files
ms1inscription-v5/php/inc_fx_inscriptions_gestion.php

1971 lines
84 KiB
PHP

<?php
/**
* Gestion des inscriptions — recherche, filtres, cartes, fiche.
*
* Deux entrees distinctes (meme module, navigation differente) :
* - Promoteur : /compte/inc_tableau_inscriptions_gestion → retour inc_tableau_promoteur
* - Acces v2 seul : /mobile → retour /mobile ou Mon compte
*
* MSIN-4436 — Parametre inscr_return (URL interne encodee) : retour contextuel depuis la fiche
* (ex. panneau anomalies dossards) au lieu de la liste des inscriptions.
*
* Droits : v2 uniquement (fxInscrGestionCanAccess). i18n : toujours info_prg compte.php.
*/
function fxInscrGestionIsEntry() {
return defined('MS1_INSCR_GESTION_ENTRY') && MS1_INSCR_GESTION_ENTRY;
}
/** Menu promoteur legacy (tachometre) visible sur Mon compte ? */
function fxInscrGestionUserHasLegacyPromoteurMenu($intComId)
{
if (!function_exists('fxPromoteurLegacySiteEnabled')) {
require_once __DIR__ . '/inc_fonctions.php';
}
if (!fxPromoteurLegacySiteEnabled()) {
return false;
}
global $objDatabase;
$row = $objDatabase->fxGetRow(
'SELECT com_eve_promoteur FROM inscriptions_comptes WHERE com_id = ' . intval($intComId)
);
return $row !== null && trim((string)$row['com_eve_promoteur']) !== '';
}
/** Utilisateur avec menu promoteur legacy (section Info promoteur). */
function fxInscrGestionUserHasPromoteurMenu($intComId) {
if (fxInscrGestionUserHasLegacyPromoteurMenu($intComId)) {
return true;
}
global $objDatabase;
$row = $objDatabase->fxGetRow(
'SELECT com_rapports FROM inscriptions_comptes WHERE com_id = ' . intval($intComId)
);
return $row !== null && trim((string)$row['com_rapports']) !== '';
}
/** Permissions v2 requises pour la fiche de gestion des inscriptions (web). */
function fxInscrGestionPermKeysView() {
return array('registrations.view', 'inscriptions_gestion.view');
}
function fxInscrGestionPermKeysScan() {
return array('registrations.scan');
}
/** Permission action (niveau fonction dans la fiche de gestion). */
function fxInscrGestionCanDo($intComId, $intEveId, $strActionPerm) {
fxInscrGestionLoadEveAcces();
if (!fxInscrGestionCanAccess($intComId, $intEveId)) {
return false;
}
if (fxEveAccesHasPermission(intval($intComId), intval($intEveId), $strActionPerm)) {
return true;
}
/* Kit inscriptions complet (desktop) inclut les actions de gestion. */
if (fxEveAccesHasPermission(intval($intComId), intval($intEveId), 'inscriptions.edit')) {
return true;
}
/* dossards.manage autorise aussi la modification du no dossard sur la fiche de gestion. */
if ($strActionPerm === 'inscriptions_gestion.bib_edit'
&& fxEveAccesHasPermission(intval($intComId), intval($intEveId), 'dossards.manage')) {
return true;
}
return false;
}
/** Badge cadenas (mode inspecteur super admin) sur la fiche de gestion. */
function fxInscrGestionPermInspectEcho($strPermKey) {
if (!function_exists('fxAdminPermInspectModeActive') || !fxAdminPermInspectModeActive()) {
return;
}
fxInscrGestionLoadEveAcces();
echo ' ' . fxEveAccesPermInspectBadge($strPermKey, 'inscr_gestion');
}
/** Badge + libelle pour un controle masque par les droits (mode inspecteur). */
function fxInscrGestionPermInspectEchoHidden($strPermKey, $strLabel) {
if (!function_exists('fxAdminPermInspectModeActive') || !fxAdminPermInspectModeActive()) {
return;
}
fxInscrGestionLoadEveAcces();
echo '<span class="ms1-perm-inspect-slot">';
echo fxEveAccesPermInspectBadge($strPermKey, 'inscr_gestion');
echo ' <span class="text-muted small">' . fxInscrGestionEsc($strLabel) . '</span>';
echo '</span> ';
}
function fxInscrGestionLoadEveAcces() {
require_once __DIR__ . '/inc_fx_eve_acces.php';
}
/**
* Evenements accessibles via acces v2 uniquement (pas com_eve_promoteur, pas super admin).
*/
function fxInscrGestionGetPromoteurEveIds($intComId) {
fxInscrGestionLoadEveAcces();
return fxEveAccesGetEventIdsWithInscrGestionAccess(intval($intComId));
}
function fxInscrGestionCanAccess($intComId, $intEveId) {
if (!empty($_SESSION['usa_id'])) {
return true;
}
fxInscrGestionLoadEveAcces();
return fxEveAccesHasInscrGestionWebAccess(intval($intComId), intval($intEveId));
}
function fxInscrGestionCanScan($intComId, $intEveId) {
if (!empty($_SESSION['usa_id'])) {
return true;
}
fxInscrGestionLoadEveAcces();
return fxEveAccesHasAnyPermission(intval($intComId), intval($intEveId), fxInscrGestionPermKeysScan());
}
function fxInscrGestionBaseUrl($strLangue, $blnMobileEntry = null) {
global $vDomaine;
if ($blnMobileEntry === null) {
$blnMobileEntry = fxInscrGestionIsEntry();
}
if ($blnMobileEntry) {
return $vDomaine . '/mobile';
}
return $vDomaine . ($strLangue === 'fr' ? '/compte/inc_tableau_inscriptions_gestion' : '/account/inc_tableau_inscriptions_gestion');
}
/**
* MSIN-4436 — Encode une URL interne relative (/compte/... ou /account/...) pour inscr_return.
*/
function fxInscrGestionEncodeReturnUrl($strRelativePath) {
$strRelativePath = trim((string)$strRelativePath);
if ($strRelativePath === '' || $strRelativePath[0] !== '/') {
return '';
}
return rtrim(strtr(base64_encode($strRelativePath), '+/', '-_'), '=');
}
/**
* MSIN-4436 — Décode inscr_return ; retourne le chemin relatif validé ou ''.
*/
function fxInscrGestionDecodeReturnPath($strToken) {
$strToken = trim((string)$strToken);
if ($strToken === '') {
return '';
}
$strB64 = strtr($strToken, '-_', '+/');
$intPad = strlen($strB64) % 4;
if ($intPad > 0) {
$strB64 .= str_repeat('=', 4 - $intPad);
}
$strPath = base64_decode($strB64, true);
if ($strPath === false || $strPath === '' || $strPath[0] !== '/') {
return '';
}
if (preg_match('#^https?://#i', $strPath)) {
return '';
}
if (!preg_match('#^/(compte|account)/#', $strPath)) {
return '';
}
return $strPath;
}
/** MSIN-4436 — URL absolue de retour depuis inscr_return, ou '' si invalide. */
function fxInscrGestionDecodeReturnUrl($strToken) {
global $vDomaine;
$strPath = fxInscrGestionDecodeReturnPath($strToken);
if ($strPath === '') {
return '';
}
return $vDomaine . $strPath;
}
/** MSIN-4436 — Bouton retour fiche : URL externe (inscr_return) ou liste filtrée. */
function fxInscrGestionResolveBackUrl($strBaseUrl, $intEveId, $arrReq) {
if (!empty($arrReq['inscr_return'])) {
$strReturn = fxInscrGestionDecodeReturnUrl($arrReq['inscr_return']);
if ($strReturn !== '') {
return $strReturn;
}
}
return fxInscrGestionBuildUrl($strBaseUrl, $intEveId, $arrReq, array('pec_id' => null, 'pg' => $arrReq['pg']));
}
/** MSIN-4436 — Libellé du bouton retour selon le contexte d'origine. */
function fxInscrGestionResolveBackLabel($arrReq, $strLangue = 'fr') {
if (!empty($arrReq['inscr_return'])) {
$strPath = fxInscrGestionDecodeReturnPath($arrReq['inscr_return']);
if ($strPath !== '' && strpos($strPath, 'inc_tableau_gestion_epreuves') !== false) {
return fxInscrGestionT('inscr_gestion_back_bib');
}
return fxInscrGestionT('inscr_gestion_back_prev');
}
return fxInscrGestionT('inscr_gestion_back_list');
}
function fxInscrGestionEsc($str) {
return htmlspecialchars((string)$str, ENT_QUOTES, 'UTF-8');
}
/** Page info / i18n des libelles de gestion des inscriptions (toujours compte.php). */
function fxInscrGestionTextesPage() {
return 'compte.php';
}
function fxInscrGestionLoadTextes($strLangue) {
static $tabCache = array();
static $strCacheLang = '';
if ($strCacheLang === $strLangue && !empty($tabCache)) {
return $tabCache;
}
$tabCache = obtenirTextepage(fxInscrGestionTextesPage(), $strLangue, 2);
if (!is_array($tabCache)) {
$tabCache = array();
}
$strCacheLang = $strLangue;
return $tabCache;
}
/**
* Recherche un libelle info par clef (toutes pages), priorite compte.php > global > mobile.php.
*/
function fxInscrGestionLookupTexteDb($strClef, $strLangue) {
global $objDatabase;
static $tabMemo = array();
$strMemoKey = $strClef . "\0" . $strLangue;
if (array_key_exists($strMemoKey, $tabMemo)) {
return $tabMemo[$strMemoKey];
}
$sql = "SELECT info_texte FROM info
WHERE info_clef = '" . $objDatabase->fxEscape($strClef) . "'
AND info_langue = '" . $objDatabase->fxEscape($strLangue) . "'
AND info_actif = 1
ORDER BY FIELD(info_prg, 'compte.php', 'global', 'mobile.php', 'mobile')
LIMIT 1";
$strTexte = $objDatabase->fxGetVar($sql);
if ($strTexte === null || trim((string)$strTexte) === '' || trim((string)$strTexte) === $strClef) {
$tabMemo[$strMemoKey] = null;
return null;
}
$tabMemo[$strMemoKey] = (string)$strTexte;
return $tabMemo[$strMemoKey];
}
function fxInscrGestionResolveTexte($strClef, $strLangue) {
global $vtexte_page;
$tab = fxInscrGestionLoadTextes($strLangue);
if (isset($tab[$strClef]['info_texte'])) {
$str = trim((string)$tab[$strClef]['info_texte']);
if ($str !== '' && $str !== $strClef) {
return $str;
}
}
if (is_array($vtexte_page) && isset($vtexte_page[$strClef]['info_texte'])) {
$str = trim((string)$vtexte_page[$strClef]['info_texte']);
if ($str !== '' && $str !== $strClef) {
return $str;
}
}
$strDb = fxInscrGestionLookupTexteDb($strClef, $strLangue);
if ($strDb !== null) {
return $strDb;
}
if (function_exists('fxAdminCanUseTextEditTools') && fxAdminCanUseTextEditTools()) {
global $vPage;
$memPage = $vPage;
$vPage = fxInscrGestionTextesPage();
$str = afficheTexte($strClef, 0);
$vPage = $memPage;
if (strpos($str, '*') === 0 && substr($str, -1) === '*') {
return fxInscrGestionFallbackTexte($strClef, $strLangue);
}
if (trim($str) === '' || trim($str) === $strClef) {
return fxInscrGestionFallbackTexte($strClef, $strLangue);
}
return $str;
}
return fxInscrGestionFallbackTexte($strClef, $strLangue);
}
/** Libelles integres (secours si table info non a jour). */
function fxInscrGestionFallbackTexte($strClef, $strLangue) {
static $tabMemo = array();
if (!isset($tabMemo[$strLangue])) {
$blnFr = ($strLangue !== 'en');
$tabMemo[$strLangue] = array(
'inscr_gestion_qr_title' => $blnFr ? 'Code QR — Lecteur' : 'QR code reader',
'inscr_gestion_qr_hint' => $blnFr ? 'Pointez la caméra vers le code QR du participant.' : 'Point the camera at the participant\'s QR code.',
'inscr_gestion_qr_invalid' => $blnFr ? 'Code QR invalide ou introuvable.' : 'Invalid or unknown QR code.',
'inscr_gestion_qr_not_found' => $blnFr ? 'Participant introuvable pour cet événement.' : 'Participant not found for this event.',
'inscr_gestion_qr_wrong_event' => $blnFr ? 'Ce code QR appartient à un autre événement : %s' : 'This QR code belongs to another event: %s',
'inscr_gestion_qr_camera_error' => $blnFr ? 'Impossible d\'accéder à la caméra.' : 'Unable to access the camera.',
'inscr_gestion_bib_scan_btn' => $blnFr ? 'Scanner le numéro' : 'Scan number',
'inscr_gestion_bib_scan_hint' => $blnFr ? 'Ouvrez la caméra, cadrez le numéro, puis touchez l\'image pour lire.' : 'Open the camera, frame the number, then tap the image to read.',
'inscr_gestion_bib_scan_ready' => $blnFr ? 'Cadrez le numéro, puis touchez l\'image' : 'Frame the number, then tap the image',
'inscr_gestion_bib_scan_tap' => $blnFr
? 'Centrez les numéros que vous voulez considérer comme numéro de dossard avant de cliquer.'
: 'Center the numbers you want to use as the bib number before tapping.',
'inscr_gestion_bib_scan_scanning' => $blnFr ? 'Lecture en cours...' : 'Reading...',
'inscr_gestion_bib_scan_loading' => $blnFr ? 'Initialisation (1re fois)...' : 'Initializing (first time)...',
'inscr_gestion_bib_scan_found' => $blnFr ? 'Numéro %s détecté — validez avec OK.' : 'Number %s detected — press OK.',
'inscr_gestion_bib_scan_invalid' => $blnFr ? 'Numéro illisible. Cadrez un seul numéro.' : 'Unreadable number. Frame a single number.',
// Libelles de la barre de boutons (fiche de gestion) — editables via la table info (page compte.php).
'inscr_gestion_show_invoice' => $blnFr ? 'Afficher la facture' : 'Show invoice',
'inscr_gestion_show_transaction' => $blnFr ? 'Afficher la transaction' : 'Show transaction',
'inscr_gestion_hide_transaction' => $blnFr ? 'Masquer la transaction' : 'Hide transaction',
'inscr_gestion_renvoi' => $blnFr ? 'Retourner confirmation' : 'Resend confirmation',
'inscr_gestion_modifier_participant' => $blnFr ? 'Modifier l\'inscription' : 'Edit registration',
// Invite affichee quand la page arrive vide (aucune recherche lancee).
'inscr_gestion_search_prompt' => $blnFr
? 'Utilisez la recherche ci-dessus pour afficher les inscriptions. Laissez les champs vides et lancez la recherche pour toutes les afficher.'
: 'Use the search above to display registrations. Leave the fields empty and search to show them all.',
// MSIN-4405 — Detail du statut annule (transferee / annulee), derive en lecture.
'inscr_gestion_annulation_titre' => $blnFr ? 'Statut de l\'inscription' : 'Registration status',
'inscr_gestion_statut_annule' => $blnFr ? 'Annulée' : 'Cancelled',
'inscr_gestion_statut_transfert' => $blnFr ? 'Transférée' : 'Transferred',
'inscr_gestion_back_bib' => $blnFr ? 'Retour aux dossards' : 'Back to bibs',
'inscr_gestion_back_prev' => $blnFr ? 'Retour' : 'Back',
// MSIN-4401 — Sortie de /mobile vers hub ou Mon compte (usagers avec plus de droits).
'inscr_gestion_back_full_account' => $blnFr ? 'Retour au tableau de bord' : 'Back to dashboard',
'inscr_gestion_back_mon_compte' => $blnFr ? 'Retour à Mon compte' : 'Back to My account',
'inscr_gestion_chk_cancelled' => $blnFr
? 'Montrer les inscriptions annulées ou transférées'
: 'Show cancelled or transferred registrations',
// MSIN-4401 — Filtre recherche par statut de course (multi-sélection).
'inscr_gestion_rech_statut' => $blnFr ? 'Statut de course' : 'Race status',
'inscr_gestion_rech_statut_all' => $blnFr ? 'Tous les statuts' : 'All statuses',
'inscr_gestion_rech_statut_n' => $blnFr ? '%d sélectionnés' : '%d selected',
);
}
if (isset($tabMemo[$strLangue][$strClef])) {
return $tabMemo[$strLangue][$strClef];
}
return $strClef;
}
/** Charge $vtexte_page pour le module gestion (entry /mobile ou compte). */
function fxInscrGestionInitTextes($strLangue) {
global $vtexte_page, $vPage;
$vPage = fxInscrGestionTextesPage();
$vtexte_page = fxInscrGestionLoadTextes($strLangue);
}
/** Libelle i18n sans echo parasite — independant de la page PHP courante. */
function fxInscrGestionT($strClef) {
global $strLangue;
if (!isset($strLangue) || $strLangue === '') {
$strLangue = 'fr';
}
$strTexte = fxInscrGestionResolveTexte($strClef, $strLangue);
if ($strTexte === $strClef || trim((string)$strTexte) === '') {
$strFallback = fxInscrGestionFallbackTexte($strClef, $strLangue);
if ($strFallback !== $strClef && trim((string)$strFallback) !== '') {
$strTexte = $strFallback;
}
}
if (function_exists('fxAdminTradMarker')) {
$strMarker = fxAdminTradMarker($strClef, 0);
if ($strMarker !== '' && strpos($strTexte, $strMarker) === false) {
$strTexte .= $strMarker;
}
}
return $strTexte;
}
/**
* Libelle "robuste" pour les pastilles du scanner : si la table info renvoie une
* clef i18n brute (donnee corrompue, ex. ancien 'inscr_mobile_bib_scan_tap' jamais
* traduit), on l'ignore et on prend le libelle integre. Evite d'afficher une clef
* technique a l'usager. Pas de marqueur d'edition ici (texte affiche dans la video).
*/
function fxInscrGestionScanLabel($strClef) {
global $strLangue;
$strLang = (isset($strLangue) && $strLangue !== '') ? $strLangue : 'fr';
$strTexte = trim((string)fxInscrGestionResolveTexte($strClef, $strLang));
if ($strTexte === '' || preg_match('/^[a-z][a-z0-9]*(_[a-z0-9]+)+$/', $strTexte)) {
$strTexte = fxInscrGestionFallbackTexte($strClef, $strLang);
}
return $strTexte;
}
function fxInscrGestionLoginActionUrl($strLangue) {
global $vDomaine;
if ($strLangue === 'en') {
return $vDomaine . '/mobile/en';
}
return $vDomaine . '/mobile';
}
function fxInscrGestionRenderLoginPage($strLangue) {
$strActionUrl = fxInscrGestionLoginActionUrl($strLangue);
$strLblUser = ($strLangue === 'fr') ? 'Nom d\'utilisateur' : 'Username';
$strLblPass = ($strLangue === 'fr') ? 'Mot de passe' : 'Password';
$strBtn = ($strLangue === 'fr') ? 'Connexion' : 'Sign in';
$strIntro = ($strLangue === 'fr')
? 'Connectez-vous pour accéder aux inscriptions mobile.'
: 'Sign in to access mobile registrations.';
echo '<div class="inscr-gestion-page inscr-gestion-login-wrap">';
echo '<div id="module-inscr-gestion" class="inscr-gestion-shell inscr-gestion-shell--login">';
fxInscrGestionRenderPageHead(
($strLangue === 'fr') ? 'Inscriptions mobile' : 'Mobile registrations'
);
echo '<p class="inscr-gestion-login-intro text-muted">' . fxInscrGestionEsc($strIntro) . '</p>';
echo '<form action="' . fxInscrGestionEsc($strActionUrl) . '" id="frm_inscr_gestion_login" method="post" class="inscr-gestion-login-form">';
echo '<input type="hidden" name="form_action" value="' . fxInscrGestionEsc(urlencode(base64_encode('login'))) . '">';
echo '<label class="inscr-gestion-label" for="txt_login_mobile">' . fxInscrGestionEsc($strLblUser) . '</label>';
echo '<input class="form-control form-control-sm rounded-0" type="text" id="txt_login_mobile" name="txt_login" required autofocus>';
echo '<label class="inscr-gestion-label" for="txt_password_mobile">' . fxInscrGestionEsc($strLblPass) . '</label>';
echo '<input class="form-control form-control-sm rounded-0" type="password" id="txt_password_mobile" name="txt_password" required>';
echo '<button class="btn btn-primary btn-sm rounded-0 btn-block mt-3" type="submit" id="btn_login_mobile" name="btn_login">' . fxInscrGestionEsc($strBtn) . '</button>';
echo '</form></div></div>';
}
/** Libellé menu promoteur V2 (même texte que l'ancien + suffixe V2). */
function fxInscrGestionPromoteurMenuLabel() {
return afficheTexte('tableau_promoteur_menueinscription', 0) . ' V2';
}
function fxInscrGestionListBackLabel($strLangue) {
if (fxInscrGestionIsEntry()) {
return ($strLangue === 'fr') ? 'Événements' : 'Events';
}
return fxInscrGestionT('promoteur_back');
}
function fxInscrGestionShouldShowListBack($intComId) {
if (!fxInscrGestionIsEntry()) {
return true;
}
return count(fxInscrGestionGetPromoteurEveIds($intComId)) > 1;
}
function fxInscrGestionRenderNavBack($strUrl, $strLabel) {
echo '<a class="btn btn-primary rounded-pill inscr-gestion-nav-back" href="' . fxInscrGestionEsc($strUrl) . '">';
echo '<i class="fa fa-chevron-left mr-2" aria-hidden="true"></i>' . fxInscrGestionEsc($strLabel);
echo '</a>';
}
/**
* MSIN-4401 — Sur /mobile, proposer une sortie vers le hub (ou Mon compte)
* si l'usager a plus que l'outil inscriptions seul.
*/
function fxInscrGestionShouldShowFullAccountLink($intComId) {
if (!fxInscrGestionIsEntry()) {
return false;
}
$intComId = intval($intComId);
if ($intComId <= 0) {
return false;
}
if (!function_exists('fxPromoteurHubIsAvailable')) {
require_once __DIR__ . '/inc_fx_promoteur_hub.php';
}
if (fxPromoteurHubIsAvailable($intComId)) {
return true;
}
return fxInscrGestionUserHasPromoteurMenu($intComId);
}
/** MSIN-4401 — URL hub si dispo, sinon Mon compte. */
function fxInscrGestionFullAccountUrl($strLangue) {
global $vDomaine;
$strLangue = ($strLangue === 'en') ? 'en' : 'fr';
$intComId = intval($_SESSION['com_id'] ?? 0);
if (!function_exists('fxPromoteurHubIsAvailable')) {
require_once __DIR__ . '/inc_fx_promoteur_hub.php';
}
if ($intComId > 0 && fxPromoteurHubIsAvailable($intComId)) {
return fxPromoteurHubUrl($strLangue);
}
return $vDomaine . ($strLangue === 'en' ? '/account' : '/compte');
}
/** MSIN-4401 — Bouton « Retour au tableau de bord / Mon compte » (entrée /mobile). */
function fxInscrGestionRenderFullAccountLink($strLangue) {
$intComId = intval($_SESSION['com_id'] ?? 0);
if (!fxInscrGestionShouldShowFullAccountLink($intComId)) {
return;
}
$strLangue = ($strLangue === 'en') ? 'en' : 'fr';
$strUrl = fxInscrGestionFullAccountUrl($strLangue);
if (!function_exists('fxPromoteurHubIsAvailable')) {
require_once __DIR__ . '/inc_fx_promoteur_hub.php';
}
$blnHub = ($intComId > 0 && fxPromoteurHubIsAvailable($intComId));
$strLabel = $blnHub
? fxInscrGestionT('inscr_gestion_back_full_account')
: fxInscrGestionT('inscr_gestion_back_mon_compte');
echo '<div class="inscr-gestion-full-account">';
echo '<a class="btn btn-outline-secondary btn-sm rounded-0" href="' . fxInscrGestionEsc($strUrl) . '">';
echo '<i class="fa fa-th-large mr-2" aria-hidden="true"></i>' . fxInscrGestionEsc($strLabel);
echo '</a>';
echo '</div>';
}
function fxInscrGestionRenderPageHead($strTitle, $strSub = '') {
echo '<div class="inscr-gestion-page-head">';
echo '<h1 class="inscr-gestion-page-title">' . fxInscrGestionEsc($strTitle) . '</h1>';
if (trim((string)$strSub) !== '') {
echo '<p class="inscr-gestion-page-sub text-muted mb-0">' . fxInscrGestionEsc($strSub) . '</p>';
}
echo '</div>';
}
function fxInscrGestionRenderSectionTitle($strLabel, $strIconClass) {
echo '<h2 class="inscr-gestion-fiche-section__title">';
echo '<i class="fa ' . fxInscrGestionEsc($strIconClass) . ' inscr-gestion-fiche-section__icon" aria-hidden="true"></i>';
echo '<span>' . fxInscrGestionEsc($strLabel) . '</span>';
echo '</h2>';
}
function fxInscrGestionRenderFicheSectionStart($strModifier, $strLabel, $strIconClass, $blnOpen = true) {
echo '<details class="inscr-gestion-fiche-section inscr-gestion-fiche-section--' . fxInscrGestionEsc($strModifier) . '"';
if ($blnOpen) {
echo ' open';
}
echo '>';
echo '<summary class="inscr-gestion-fiche-section__title">';
echo '<i class="fa ' . fxInscrGestionEsc($strIconClass) . ' inscr-gestion-fiche-section__icon" aria-hidden="true"></i>';
echo '<span>' . fxInscrGestionEsc($strLabel) . '</span>';
echo '</summary>';
echo '<div class="inscr-gestion-fiche-section__body">';
}
function fxInscrGestionRenderFicheSectionEnd() {
echo '</div></details>';
}
function fxInscrGestionRenderPanelSummary($strLabel, $strIconClass) {
echo '<i class="fa ' . fxInscrGestionEsc($strIconClass) . ' inscr-gestion-panel__icon" aria-hidden="true"></i>';
echo '<span>' . fxInscrGestionEsc($strLabel) . '</span>';
}
function fxInscrGestionParseEveId($strRaw) {
if ($strRaw === null || $strRaw === '') {
return 0;
}
$strDecoded = base64_decode(urldecode($strRaw), true);
if ($strDecoded === false || $strDecoded === '') {
return 0;
}
return (int)$strDecoded;
}
/**
* MSIN-4401 — Codes statut de course soumis (GET sel_rech_statut[]), whitelistes.
* Tableau vide = aucun filtre (tous les statuts).
*/
function fxInscrGestionParseStatutFilter($mixRaw) {
if (!is_array($mixRaw)) {
return array();
}
$tabOut = array();
foreach ($mixRaw as $mixCode) {
$strCode = strtoupper(trim((string)$mixCode));
$strCode = preg_replace('/[^A-Z0-9_]/', '', $strCode);
if ($strCode !== '') {
$tabOut[$strCode] = $strCode;
}
}
return array_values($tabOut);
}
function fxInscrGestionParseRequest() {
$arr = array(
'rech_prenom' => isset($_GET['rech_prenom']) ? trim($_GET['rech_prenom']) : '',
'rech_nom' => isset($_GET['rech_nom']) ? trim($_GET['rech_nom']) : '',
'rech_no_commande' => isset($_GET['rech_no_commande']) ? trim($_GET['rech_no_commande']) : '',
'rech_nom_equipe' => isset($_GET['rech_nom_equipe']) ? trim($_GET['rech_nom_equipe']) : '',
'chk_bib' => !empty($_GET['chk_bib']) ? 1 : 0,
'chk_cancelled' => !empty($_GET['chk_cancelled']) ? 1 : 0,
'sel_rech_epreuve' => 0,
// MSIN-4401 — Filtre multi statut de course (vide = tous).
'sel_rech_statut' => fxInscrGestionParseStatutFilter(
isset($_GET['sel_rech_statut']) ? $_GET['sel_rech_statut'] : array()
),
'pg' => isset($_GET['pg']) ? max(1, intval($_GET['pg'])) : 1,
'pec_id' => isset($_GET['pec_id']) ? intval($_GET['pec_id']) : 0,
// MSIN-4436 — Page d'origine (ex. gestion dossards) pour le bouton retour de la fiche.
'inscr_return' => isset($_GET['inscr_return']) ? trim((string)$_GET['inscr_return']) : '',
// q = 1 lorsque l'utilisateur a lance une recherche/filtre. La liste reste vide tant que q = 0.
'q' => 0,
);
if (!empty($_GET['sel_rech_epreuve'])) {
$decoded = base64_decode(urldecode($_GET['sel_rech_epreuve']), true);
if ($decoded !== false && $decoded !== '') {
$arr['sel_rech_epreuve'] = intval($decoded);
} else {
$arr['sel_rech_epreuve'] = intval($_GET['sel_rech_epreuve']);
}
}
// Recherche "active" si : bouton Rechercher/Filtres soumis, marqueur q transporte par les liens,
// ou au moins un critere present (utile pour le retour depuis une fiche / la pagination).
$blnSubmit = !empty($_GET['btn_recherche']) || !empty($_GET['apply_filters']) || !empty($_GET['q']);
$blnCriteria = ($arr['rech_prenom'] !== '' || $arr['rech_nom'] !== '' || $arr['rech_no_commande'] !== ''
|| $arr['rech_nom_equipe'] !== '' || $arr['sel_rech_epreuve'] > 0 || $arr['chk_bib'] || $arr['chk_cancelled']
|| count($arr['sel_rech_statut']) > 0);
$arr['q'] = ($blnSubmit || $blnCriteria) ? 1 : 0;
return $arr;
}
function fxInscrGestionFilterActiveCount($arrReq) {
$int = 0;
if ($arrReq['sel_rech_epreuve'] > 0) {
$int++;
}
if ($arrReq['chk_bib']) {
$int++;
}
if ($arrReq['chk_cancelled']) {
$int++;
}
if (!empty($arrReq['sel_rech_statut']) && is_array($arrReq['sel_rech_statut']) && count($arrReq['sel_rech_statut']) > 0) {
$int++;
}
return $int;
}
function fxInscrGestionEmptyListRequest() {
return array(
'rech_prenom' => '',
'rech_nom' => '',
'rech_no_commande' => '',
'rech_nom_equipe' => '',
'chk_bib' => 0,
'chk_cancelled' => 0,
'sel_rech_epreuve' => 0,
'sel_rech_statut' => array(),
'pg' => 1,
'pec_id' => 0,
'inscr_return' => '',
'q' => 0,
);
}
function fxInscrGestionSearchResetUrl($strBaseUrl, $intEveId) {
return fxInscrGestionBuildUrl($strBaseUrl, $intEveId, fxInscrGestionEmptyListRequest(), array('pg' => 1));
}
function fxInscrGestionBuildWhere($intEveId, $arrReq) {
global $objDatabase;
$strWhere = ' e.eve_id = ' . intval($intEveId);
if ($arrReq['rech_nom'] !== '') {
$strWhere .= ' AND p.par_nom LIKE "%' . $objDatabase->fxEscape($arrReq['rech_nom']) . '%"';
}
if ($arrReq['rech_prenom'] !== '') {
$strWhere .= ' AND p.par_prenom LIKE "%' . $objDatabase->fxEscape($arrReq['rech_prenom']) . '%"';
}
if ($arrReq['rech_no_commande'] !== '') {
$strEscCmd = $objDatabase->fxEscape($arrReq['rech_no_commande']);
$strWhere .= ' AND (e.no_commande LIKE "%' . $strEscCmd . '%" OR p.no_commande LIKE "%' . $strEscCmd . '%")';
}
if ($arrReq['rech_nom_equipe'] !== '') {
$strEscBibEquipe = $objDatabase->fxEscape($arrReq['rech_nom_equipe']);
$strWhere .= ' AND ('
. ' TRIM(p.no_bib) LIKE "%' . $strEscBibEquipe . '%"'
. ' OR TRIM(e.no_equipe) LIKE "%' . $strEscBibEquipe . '%"'
. ' OR e.pec_nom_equipe LIKE "%' . $strEscBibEquipe . '%"'
. ' OR p.par_nom_equipe LIKE "%' . $strEscBibEquipe . '%"'
. ')';
}
if ($arrReq['sel_rech_epreuve'] > 0) {
$strWhere .= ' AND p.epr_id = ' . intval($arrReq['sel_rech_epreuve']) . ' AND p.epr_id = e.epr_id';
}
if ($arrReq['chk_bib']) {
$strWhere .= " AND TRIM(p.no_bib) = ''";
}
if ($arrReq['chk_cancelled']) {
$strWhere .= ' AND e.is_cancelled = 1';
} else {
$strWhere .= ' AND e.is_cancelled = 0 AND e.pec_actif = 1';
}
// MSIN-4401 — Filtre statut de course (vide = tous). CONF inclut aussi les valeurs vides (défaut).
if (!empty($arrReq['sel_rech_statut']) && is_array($arrReq['sel_rech_statut'])) {
$tabCodes = array();
foreach ($arrReq['sel_rech_statut'] as $strCode) {
$strCode = strtoupper(trim((string)$strCode));
$strCode = preg_replace('/[^A-Z0-9_]/', '', $strCode);
if ($strCode !== '') {
$tabCodes[$strCode] = "'" . $objDatabase->fxEscape($strCode) . "'";
}
}
if (count($tabCodes) > 0) {
$strIn = implode(',', $tabCodes);
if (isset($tabCodes['CONF'])) {
$strWhere .= ' AND (p.par_statut_course IN (' . $strIn . ')'
. " OR TRIM(IFNULL(p.par_statut_course, '')) = '')";
} else {
$strWhere .= ' AND p.par_statut_course IN (' . $strIn . ')';
}
}
}
return $strWhere;
}
function fxInscrGestionFetchRows($intEveId, $arrReq) {
global $objDatabase;
$strWhere = fxInscrGestionBuildWhere($intEveId, $arrReq);
$sql = 'SELECT p.no_bib, p.no_bib_remis, p.no_bib_remis_date, p.no_bib_remis_par, p.par_statut_course, p.par_id, p.par_equipe,'
. ' p.par_prenom, p.par_nom, e.pec_id_original, e.epr_id, e.eve_id, e.pec_id, e.is_cancelled, e.pec_actif, p.rol_id,'
. ' e.no_commande, e.no_panier, e.no_equipe, e.pec_equipe'
. ' FROM resultats_epreuves_commandees e, resultats_participants p'
. ' WHERE ' . $strWhere . ' AND p.pec_id = e.pec_id_original'
. ' GROUP BY e.pec_id_original'
. ' ORDER BY p.par_nom, p.par_prenom';
return $objDatabase->fxGetResults($sql);
}
function fxInscrGestionFetchRowByPec($intPecId, $intEveId) {
global $objDatabase;
$sql = 'SELECT p.no_bib, p.no_bib_remis, p.no_bib_remis_date, p.no_bib_remis_par, p.par_statut_course, p.par_id, p.par_equipe,'
. ' p.par_prenom, p.par_nom, e.pec_id_original, e.epr_id, e.eve_id, e.pec_id, e.is_cancelled, e.pec_actif, p.rol_id,'
. ' e.no_commande, e.no_panier, e.no_equipe, e.pec_equipe'
. ' FROM resultats_epreuves_commandees e, resultats_participants p'
. ' WHERE e.eve_id = ' . intval($intEveId)
. ' AND e.pec_id_original = ' . intval($intPecId)
. ' AND p.pec_id = e.pec_id_original'
. ' LIMIT 1';
return $objDatabase->fxGetRow($sql);
}
/**
* Politique de gestion des dossards en double pour un evenement.
* Pour l'instant : toujours 'warn' (avertissement non bloquant).
*
* Hook prevu pour le futur : lire un reglage par evenement afin de renvoyer
* 'block' (interdiction totale) ou 'allow' (doublons silencieux). Exemple :
* $rec = fxGetEvenementsId($intEveId);
* if (!empty($rec['eve_bib_doublon_interdit'])) return 'block';
* if (!empty($rec['eve_bib_doublon_autorise'])) return 'allow';
*
* @return string 'warn' | 'block' | 'allow'
*/
function fxInscrGestionBibDuplicatePolicy($intEveId) {
return 'warn';
}
/**
* Participants d'un evenement deja porteurs du dossard donne (hors la commande
* courante). Alimente le garde-fou "dossard en double".
*
* @param bool $blnTeam true = dossard d'equipe (e.no_equipe), false = individuel (p.no_bib)
* @return array liste de lignes ['par_nom','par_prenom','epr_id']
*/
function fxInscrGestionFindBibDuplicates($intEveId, $strNoBib, $intExceptPec, $blnTeam, $intCurrentEprId = 0) {
global $objDatabase;
$strNoBib = trim((string)$strNoBib);
$intCurrentEprId = (int)$intCurrentEprId;
if (intval($intEveId) <= 0 || $strNoBib === '') {
return array();
}
$strBibCol = $blnTeam ? 'e.no_equipe' : 'p.no_bib';
$sql = 'SELECT p.par_nom, p.par_prenom, e.epr_id'
. ' FROM resultats_epreuves_commandees e, resultats_participants p'
. ' WHERE e.eve_id = ' . intval($intEveId)
. ' AND p.pec_id = e.pec_id_original'
. ' AND e.is_cancelled = 0 AND e.pec_actif = 1'
. " AND TRIM(" . $strBibCol . ") = '" . $objDatabase->fxEscape($strNoBib) . "'"
. ' AND e.pec_id_original <> ' . intval($intExceptPec)
. ' GROUP BY e.pec_id_original'
. ' ORDER BY p.par_nom, p.par_prenom';
$tab = $objDatabase->fxGetResults($sql);
if (!is_array($tab) || empty($tab)) {
return array();
}
$blnAllowInter = function_exists('fxBibEventAllowsInterEprDuplicates')
&& fxBibEventAllowsInterEprDuplicates($intEveId);
$arrOut = array();
foreach ($tab as $rowDup) {
$intDupEprId = (int)($rowDup['epr_id'] ?? 0);
if ($intCurrentEprId > 0 && $intDupEprId !== $intCurrentEprId && $blnAllowInter) {
continue;
}
$arrOut[] = $rowDup;
}
return $arrOut;
}
function fxInscrGestionEpreuveLabel($intEprId, $strLangue) {
$tab = fxGetEpreuve($intEprId);
if ($tab === null) {
return '';
}
$str = '';
if (trim($tab['epr_categorie_' . $strLangue]) !== '') {
$str .= $tab['epr_categorie_' . $strLangue] . ' — ';
}
$str .= $tab['epr_type_' . $strLangue];
if (trim($tab['epr_nom_' . $strLangue]) !== '') {
$str .= ' — ' . $tab['epr_nom_' . $strLangue];
}
return fxUnescape($str);
}
function fxInscrGestionBibDisplay($arrRow) {
if (intval($arrRow['pec_equipe']) === 1) {
return fxShowBibNumber($arrRow['no_equipe'], $arrRow['epr_id'], 'equipe');
}
return fxShowBibNumber($arrRow['no_bib'], $arrRow['epr_id'], 'participant');
}
/** Mode rendu fiche condensee (grille 2 colonnes dans l'onglet Details). */
function fxInscrGestionKvFicheCardMode($blnSet = null) {
static $blnMode = false;
if ($blnSet !== null) {
$blnMode = (bool)$blnSet;
}
return $blnMode;
}
/** Valeur longue ou courriel : occupe toute la largeur de la fiche. */
function fxInscrGestionKvValueWantsFullWidth($strValue, $blnHtml = false) {
if ($blnHtml) {
$strPlain = trim(strip_tags((string)$strValue));
} else {
$strPlain = trim((string)$strValue);
}
if ($strPlain === '') {
return false;
}
$intLen = function_exists('mb_strlen') ? mb_strlen($strPlain) : strlen($strPlain);
return $intLen > 48;
}
function fxInscrGestionRenderKvListOpen($blnFicheCard = false) {
fxInscrGestionKvFicheCardMode($blnFicheCard);
$strClass = 'inscr-gestion-kv-list';
if ($blnFicheCard) {
$strClass .= ' inscr-gestion-fiche-card';
}
echo '<div class="' . fxInscrGestionEsc($strClass) . '">';
}
function fxInscrGestionRenderKvListClose() {
fxInscrGestionKvFicheCardMode(false);
echo '</div>';
}
function fxInscrGestionRenderKvRow($strLabel, $strValue, $blnHtml = false, $strExtraClass = '') {
if (!$blnHtml && trim((string)$strValue) === '') {
return;
}
$strClass = 'inscr-gestion-kv-row';
if ($strExtraClass !== '') {
$strClass .= ' ' . $strExtraClass;
}
if (fxInscrGestionKvFicheCardMode() && fxInscrGestionKvValueWantsFullWidth($strValue, $blnHtml)) {
$strClass .= ' inscr-gestion-kv-row--full';
}
echo '<div class="' . fxInscrGestionEsc($strClass) . '">';
echo '<div class="inscr-gestion-kv-row__label">' . fxInscrGestionEsc($strLabel) . '</div>';
echo '<div class="inscr-gestion-kv-row__value">';
if ($blnHtml) {
echo $strValue;
} else {
echo fxInscrGestionEsc($strValue);
}
echo '</div></div>';
}
function fxInscrGestionFetchFicheProfile($arrRow, $strLangue) {
global $objDatabase;
$intParId = (int)$arrRow['par_id'];
$intPecId = (int)$arrRow['pec_id_original'];
$intEveId = (int)$arrRow['eve_id'];
$strLangCol = ($strLangue === 'en') ? 'en' : 'fr';
// Le titre "Capitaine" n'a de sens qu'en epreuve d'equipe : meme condition que le champ "Type"
// de la fiche (cf. fxInscrGestionRenderFiche). En individuel, on ne l'affiche pas, meme si rol_id=1.
$blnEquipe = (intval($arrRow['pec_equipe']) === 1)
&& fxIsParEquipe((int)$arrRow['pec_id_original'], (int)$arrRow['epr_id']);
$blnCapitaine = ((int)$arrRow['rol_id'] === 1) && $blnEquipe;
$tabProfile = $objDatabase->fxGetRow(
'SELECT p.par_courriel, p.par_naissance, p.par_sexe, p.par_id_original'
. ' FROM resultats_participants p'
. ' WHERE p.par_id = ' . $intParId
. ' LIMIT 1'
);
if ($tabProfile === null) {
return array(
'bln_capitaine' => $blnCapitaine,
'email' => '',
'birth_gender' => '',
'questions' => array(),
);
}
$strEmail = trim((string)$tabProfile['par_courriel']);
$strBirthGender = '';
$strNaissance = trim((string)$tabProfile['par_naissance']);
if ($strNaissance !== '' && $strNaissance !== '0000-00-00') {
$strBirthGender = fxShowDate($strNaissance, $strLangue)
. ' - '
. fxGetGender($tabProfile['par_sexe'], $strLangue);
}
$intParIdOriginal = (int)$tabProfile['par_id_original'];
$sqlQuestions = 'SELECT rq.que_choix_' . $strLangCol . ' AS que_choix,'
. ' iq.que_cart_label_' . $strLangCol . ' AS que_label,'
. ' rq.que_question_' . $strLangCol . ' AS que_question,'
. ' iq.que_tri, iq.que_id'
. ' FROM resultats_questions rq'
. ' INNER JOIN inscriptions_questions iq ON iq.que_id = rq.que_id'
. ' WHERE iq.que_cart_show = 1'
. ' AND iq.eve_id = ' . $intEveId
. ' AND rq.pec_id = ' . $intPecId
. ' AND (rq.par_id = ' . $intParId . ' OR rq.par_id = ' . $intParIdOriginal . ')'
. ' AND rq.que_actif = 1'
. " AND TRIM(COALESCE(rq.que_choix_" . $strLangCol . ", '')) <> ''"
. ' ORDER BY iq.que_tri, iq.que_id';
$tabQuestionsRaw = $objDatabase->fxGetResults($sqlQuestions);
$tabQuestions = array();
if ($tabQuestionsRaw !== null) {
for ($i = 1; $i <= count($tabQuestionsRaw); $i++) {
$tabQ = $tabQuestionsRaw[$i];
$strLabel = trim(fxUnescape($tabQ['que_label']));
if ($strLabel === '') {
$strLabel = trim(fxUnescape($tabQ['que_question']));
}
$strValue = trim(fxUnescape($tabQ['que_choix']));
if ($strLabel === '' || $strValue === '') {
continue;
}
$tabQuestions[] = array(
'label' => $strLabel,
'value' => $strValue,
);
}
}
return array(
'bln_capitaine' => $blnCapitaine,
'email' => $strEmail,
'birth_gender' => $strBirthGender,
'questions' => $tabQuestions,
);
}
function fxInscrGestionRenderKvRowFlag($strText, $strExtraClass = '') {
$strClass = 'inscr-gestion-kv-row inscr-gestion-kv-row--flag';
if ($strExtraClass !== '') {
$strClass .= ' ' . $strExtraClass;
}
if (fxInscrGestionKvFicheCardMode()) {
$strClass .= ' inscr-gestion-kv-row--full';
}
echo '<div class="' . fxInscrGestionEsc($strClass) . '">';
echo '<span class="inscr-gestion-kv-flag">' . fxInscrGestionEsc($strText) . '</span>';
echo '</div>';
}
function fxInscrGestionRenderFicheProfileKvRows($arrRow, $strLangue) {
$tabProfile = fxInscrGestionFetchFicheProfile($arrRow, $strLangue);
$blnFirstProfile = true;
$strProfileSep = 'inscr-gestion-kv-row--profile';
if ($tabProfile['bln_capitaine']) {
$strCaptainLabel = trim(fxInscrGestionT('participant-titre-capitaine'));
if ($strCaptainLabel === '') {
$strCaptainLabel = ($strLangue === 'fr') ? 'Capitaine' : 'Captain';
}
fxInscrGestionRenderKvRowFlag(
$strCaptainLabel,
$blnFirstProfile ? $strProfileSep : ''
);
$blnFirstProfile = false;
}
if ($tabProfile['email'] !== '') {
fxInscrGestionRenderKvRow(
fxInscrGestionT('inscr_gestion_email'),
$tabProfile['email'],
false,
$blnFirstProfile ? 'inscr-gestion-kv-row--profile' : ''
);
$blnFirstProfile = false;
}
if ($tabProfile['birth_gender'] !== '') {
fxInscrGestionRenderKvRow(
fxInscrGestionT('inscr_gestion_birthdate'),
$tabProfile['birth_gender'],
false,
$blnFirstProfile ? 'inscr-gestion-kv-row--profile' : ''
);
$blnFirstProfile = false;
}
foreach ($tabProfile['questions'] as $tabQuestion) {
fxInscrGestionRenderKvRow(
$tabQuestion['label'],
$tabQuestion['value'],
false,
$blnFirstProfile ? 'inscr-gestion-kv-row--profile' : ''
);
$blnFirstProfile = false;
}
}
/** Code statut course par défaut (Confirmé). */
function fxInscrGestionParStatutCourseDefault() {
return 'CONF';
}
function fxInscrGestionResolveParStatutCourse($strCurrent) {
$strCurrent = trim((string)$strCurrent);
return $strCurrent !== '' ? $strCurrent : fxInscrGestionParStatutCourseDefault();
}
/** Options statut course depuis info (info_description = opt). */
function fxInscrGestionGetStatutCourseOptions($strLangue) {
global $objDatabase;
static $tabCache = array();
if (isset($tabCache[$strLangue])) {
return $tabCache[$strLangue];
}
$sql = "SELECT info_texte, info_option2 FROM info"
. " WHERE info_actif = 1 AND info_clef = 'inscr_statut_course'"
. " AND info_langue = '" . $objDatabase->fxEscape($strLangue) . "'"
. " AND info_description = 'opt'"
. " ORDER BY info_trie";
$tabRows = $objDatabase->fxGetResults($sql);
$tabCache[$strLangue] = ($tabRows !== null) ? $tabRows : array();
return $tabCache[$strLangue];
}
function fxInscrGestionRenderStatutField($intParId, $strCurrent, $intEveId, $strLangue) {
$tabOptions = fxInscrGestionGetStatutCourseOptions($strLangue);
if (count($tabOptions) === 0) {
echo '<div class="inscr-gestion-muted">' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_statut_label')) . '</div>';
return;
}
$strCurrent = fxInscrGestionResolveParStatutCourse($strCurrent);
$strSelectId = 'inscr_statut_' . (int)$intParId;
echo '<select class="form-control rounded-0 inscr-gestion-statut-select" id="' . fxInscrGestionEsc($strSelectId) . '"'
. ' data-par_id="' . (int)$intParId . '"'
. ' data-eve_id="' . rawurlencode(base64_encode((string)$intEveId)) . '">';
foreach ($tabOptions as $arrOpt) {
$strCode = trim((string)$arrOpt['info_option2']);
$strLabel = trim((string)$arrOpt['info_texte']);
if ($strCode === '') {
continue;
}
echo '<option value="' . fxInscrGestionEsc($strCode) . '"'
. ($strCurrent === $strCode ? ' selected' : '') . '>'
. fxInscrGestionEsc($strLabel) . '</option>';
}
echo '</select>';
}
/**
* MSIN-4401 — Multi-sélection statut de course (mobile-friendly : panneau + cases à cocher).
* Aucune case cochée = tous les statuts.
*/
function fxInscrGestionRenderStatutFilter($arrReq, $strLangue) {
$tabOptions = fxInscrGestionGetStatutCourseOptions($strLangue);
if (count($tabOptions) === 0) {
return;
}
$tabSelected = (!empty($arrReq['sel_rech_statut']) && is_array($arrReq['sel_rech_statut']))
? $arrReq['sel_rech_statut']
: array();
$tabSelectedMap = array();
foreach ($tabSelected as $strCode) {
$tabSelectedMap[$strCode] = true;
}
$intNb = count($tabSelected);
$strSummary = ($intNb === 0)
? fxInscrGestionT('inscr_gestion_rech_statut_all')
: sprintf(fxInscrGestionT('inscr_gestion_rech_statut_n'), $intNb);
echo '<label class="inscr-gestion-label" for="inscr-gestion-statut-multi-summary">'
. fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_rech_statut')) . '</label>';
echo '<details class="inscr-gestion-statut-multi"'
. ' data-label-all="' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_rech_statut_all')) . '"'
. ' data-label-n="' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_rech_statut_n')) . '">';
echo '<summary class="inscr-gestion-statut-multi__summary" id="inscr-gestion-statut-multi-summary">'
. '<span class="inscr-gestion-statut-multi__summary-text">' . fxInscrGestionEsc($strSummary) . '</span>'
. '</summary>';
echo '<div class="inscr-gestion-statut-multi__panel" role="group"'
. ' aria-label="' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_rech_statut')) . '">';
foreach ($tabOptions as $arrOpt) {
$strCode = trim((string)$arrOpt['info_option2']);
$strLabel = trim((string)$arrOpt['info_texte']);
if ($strCode === '') {
continue;
}
$strId = 'sel_rech_statut_' . preg_replace('/[^A-Za-z0-9_]/', '_', $strCode);
$blnChecked = isset($tabSelectedMap[$strCode]);
echo '<label class="inscr-gestion-statut-multi__opt" for="' . fxInscrGestionEsc($strId) . '">';
echo '<input type="checkbox" id="' . fxInscrGestionEsc($strId) . '"'
. ' name="sel_rech_statut[]" value="' . fxInscrGestionEsc($strCode) . '"'
. ($blnChecked ? ' checked' : '') . '>';
echo '<span>' . fxInscrGestionEsc($strLabel) . '</span>';
echo '</label>';
}
echo '</div></details>';
}
/** Paramètres GET pour liens (sans double-encodage). */
function fxInscrGestionQueryParams($intEveId, $arrReq, $arrExtra = array()) {
$tab = array(
'promoteur_eve_id' => base64_encode((string)$intEveId),
);
if ($arrReq['rech_prenom'] !== '') {
$tab['rech_prenom'] = $arrReq['rech_prenom'];
}
if ($arrReq['rech_nom'] !== '') {
$tab['rech_nom'] = $arrReq['rech_nom'];
}
if ($arrReq['rech_no_commande'] !== '') {
$tab['rech_no_commande'] = $arrReq['rech_no_commande'];
}
if ($arrReq['rech_nom_equipe'] !== '') {
$tab['rech_nom_equipe'] = $arrReq['rech_nom_equipe'];
}
if ($arrReq['chk_bib']) {
$tab['chk_bib'] = 1;
}
if ($arrReq['chk_cancelled']) {
$tab['chk_cancelled'] = 1;
}
if ($arrReq['sel_rech_epreuve'] > 0) {
$tab['sel_rech_epreuve'] = base64_encode((string)$arrReq['sel_rech_epreuve']);
}
// MSIN-4401 — Conserver le filtre multi-statut dans pagination / fiche.
if (!empty($arrReq['sel_rech_statut']) && is_array($arrReq['sel_rech_statut'])) {
$tab['sel_rech_statut'] = array_values($arrReq['sel_rech_statut']);
}
if (!empty($_GET['lng'])) {
$tab['lng'] = $_GET['lng'];
}
if (!empty($arrReq['q'])) {
// Transporte l'etat "recherche lancee" pour que la pagination et le retour de fiche gardent la liste.
$tab['q'] = 1;
}
if (!empty($arrReq['pg']) && (int)$arrReq['pg'] > 1) {
$tab['pg'] = (int)$arrReq['pg'];
}
if (!empty($arrReq['inscr_return'])) {
$tab['inscr_return'] = $arrReq['inscr_return'];
}
foreach ($arrExtra as $strKey => $mixVal) {
if ($mixVal === null || $mixVal === '' || ($strKey === 'pec_id' && (int)$mixVal === 0)) {
unset($tab[$strKey]);
} else {
$tab[$strKey] = $mixVal;
}
}
return $tab;
}
function fxInscrGestionBuildUrl($strBaseUrl, $intEveId, $arrReq, $arrExtra = array()) {
$tab = fxInscrGestionQueryParams($intEveId, $arrReq, $arrExtra);
$strQs = http_build_query($tab, '', '&');
return $strBaseUrl . ($strQs !== '' ? '?' . $strQs : '');
}
function fxInscrGestionPaginationUrl($strBaseUrl, $intEveId, $arrReq) {
$tab = fxInscrGestionQueryParams($intEveId, $arrReq, array('pec_id' => null));
unset($tab['pg'], $tab['pec_id']);
$strQs = http_build_query($tab, '', '&amp;');
return $strBaseUrl . ($strQs !== '' ? '?' . $strQs : '?promoteur_eve_id=' . rawurlencode(base64_encode((string)$intEveId)));
}
function fxInscrGestionRenderPagination($strBaseUrl, $intEveId, $arrReq, $intTotal, $intPerPage, $intPage) {
if ($intTotal <= $intPerPage) {
return '';
}
return '<div class="inscr-gestion-pager">'
. fxGetPagination(
fxInscrGestionPaginationUrl($strBaseUrl, $intEveId, $arrReq),
$intTotal,
$intPerPage,
$intPage
)
. '</div>';
}
function fxInscrGestionExtractQrToken($strRaw) {
$strRaw = trim((string)$strRaw);
if ($strRaw === '') {
return '';
}
if (preg_match('/[?&]t=([^&\s#]+)/', $strRaw, $tabMatch)) {
return urldecode($tabMatch[1]);
}
if (preg_match('/^https?:\/\//i', $strRaw)) {
$tabParts = parse_url($strRaw);
if (!empty($tabParts['query'])) {
parse_str($tabParts['query'], $tabQuery);
if (!empty($tabQuery['t'])) {
return trim((string)$tabQuery['t']);
}
}
}
if (preg_match('/^[a-zA-Z0-9_-]{6,64}$/', $strRaw)) {
return $strRaw;
}
return '';
}
function fxInscrGestionResolveQrScan($strRaw, $intEveId, $strLangue) {
global $objDatabase;
$strToken = fxInscrGestionExtractQrToken($strRaw);
if ($strToken === '') {
return array('state' => 'error', 'message' => fxInscrGestionT('inscr_gestion_qr_invalid'));
}
$tabToken = $objDatabase->fxGetRow(
'SELECT qr_public_id, pec_id_original, eve_id'
. ' FROM qr_tokens'
. " WHERE qr_public_id = '" . $objDatabase->fxEscape($strToken) . "'"
. ' LIMIT 1'
);
if ($tabToken === null) {
return array('state' => 'error', 'message' => fxInscrGestionT('inscr_gestion_qr_invalid'));
}
$intPecId = (int)$tabToken['pec_id_original'];
if ($intPecId <= 0) {
return array('state' => 'error', 'message' => fxInscrGestionT('inscr_gestion_qr_invalid'));
}
$tabPec = $objDatabase->fxGetRow(
'SELECT ec.pec_id_original'
. ' FROM resultats_epreuves_commandees ec'
. ' WHERE ec.pec_id_original = ' . $intPecId
. ' AND ec.eve_id = ' . intval($intEveId)
. ' AND ec.pec_actif = 1'
. ' LIMIT 1'
);
if ($tabPec !== null) {
$arrRow = fxInscrGestionFetchRowByPec($intPecId, $intEveId);
if ($arrRow === null) {
return array('state' => 'error', 'message' => fxInscrGestionT('inscr_gestion_qr_not_found'));
}
return array(
'state' => 'success',
'pec_id' => $intPecId,
);
}
$intQrEveId = (int)$tabToken['eve_id'];
if ($intQrEveId <= 0) {
$tabOther = $objDatabase->fxGetRow(
'SELECT ec.eve_id FROM resultats_epreuves_commandees ec'
. ' WHERE ec.pec_id_original = ' . $intPecId
. ' AND ec.pec_actif = 1'
. ' ORDER BY ec.pec_id_original DESC LIMIT 1'
);
if ($tabOther !== null) {
$intQrEveId = (int)$tabOther['eve_id'];
}
}
$strEveNom = '';
if ($intQrEveId > 0) {
$arrEv = fxGetEvenementsId($intQrEveId);
if (!empty($arrEv)) {
$strEveNom = fxUnescape($arrEv['eve_nom_' . $strLangue]);
}
}
if ($strEveNom === '') {
$strEveNom = '#' . $intQrEveId;
}
return array(
'state' => 'wrong_event',
'message' => str_replace('%s', $strEveNom, fxInscrGestionT('inscr_gestion_qr_wrong_event')),
'eve_nom' => $strEveNom,
);
}
function fxInscrGestionRenderEventPicker($tabEveIds, $strLangue, $strBaseUrl) {
global $objDatabase;
$strTitle = ($strLangue === 'fr') ? 'Choisir un événement' : 'Choose an event';
echo '<div id="module-inscr-gestion" class="inscr-gestion-shell">';
fxInscrGestionRenderPageHead($strTitle);
echo '<div class="inscr-gestion-event-list">';
foreach ($tabEveIds as $intEveId) {
$qry = 'SELECT e.* FROM inscriptions_evenements e WHERE e.eve_actif = 1 AND e.eve_id = ' . intval($intEveId);
$tabEv = $objDatabase->fxGetRow($qry);
if ($tabEv === null) {
continue;
}
$strUrl = fxInscrGestionBuildUrl($strBaseUrl, $intEveId, fxInscrGestionParseRequest(), array());
echo '<a class="inscr-gestion-event-item" href="' . fxInscrGestionEsc($strUrl) . '">';
echo '<span class="inscr-gestion-event-item__name">' . fxInscrGestionEsc(fxUnescape($tabEv['eve_nom_' . $strLangue])) . '</span>';
echo '<i class="fa fa-chevron-right" aria-hidden="true"></i>';
echo '</a>';
}
echo '</div></div>';
}
function fxInscrGestionRenderQrPanel($intEveId, $strLangue) {
global $vDomaine;
$intComId = intval($_SESSION['com_id'] ?? 0);
if (!fxInscrGestionCanScan($intComId, $intEveId)) {
return;
}
$strQrAjaxUrl = $vDomaine . '/ajax_inscr_gestion_qr.php';
echo '<details class="inscr-gestion-panel inscr-gestion-panel--qr" id="inscr-gestion-qr-panel"';
echo ' data-qr-resolve-url="' . fxInscrGestionEsc($strQrAjaxUrl) . '"';
echo ' data-qr-eve-id="' . fxInscrGestionEsc(base64_encode((string)$intEveId)) . '"';
echo ' data-qr-lang="' . fxInscrGestionEsc($strLangue) . '"';
echo ' data-qr-msg-invalid="' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_qr_invalid')) . '"';
echo ' data-qr-msg-wrong="' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_qr_wrong_event')) . '"';
echo ' data-qr-msg-camera="' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_qr_camera_error')) . '">';
echo '<summary class="inscr-gestion-panel__head">';
fxInscrGestionRenderPanelSummary(fxInscrGestionT('inscr_gestion_qr_title'), 'fa-qrcode');
echo '</summary>';
echo '<div class="inscr-gestion-panel__body">';
echo '<p class="inscr-gestion-qr-hint text-muted small mb-2">' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_qr_hint')) . '</p>';
echo '<div id="inscr-gestion-qr-feedback" class="alert d-none mb-2" role="alert"></div>';
echo '<div id="inscr-gestion-qr-reader-wrap" class="inscr-gestion-qr-reader-wrap">';
echo '<div id="inscr-gestion-qr-reader"></div>';
echo '</div></div></details>';
}
function fxInscrGestionRenderBibScanBlock($strBibInputId, $strLangue) {
echo '<div class="inscr-gestion-bib-scan-panel" hidden';
echo ' data-bib-input-id="' . fxInscrGestionEsc($strBibInputId) . '"';
echo ' data-msg-scanning="' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_bib_scan_scanning')) . '"';
echo ' data-msg-loading="' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_bib_scan_loading')) . '"';
echo ' data-msg-ready="' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_bib_scan_ready')) . '"';
echo ' data-msg-found="' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_bib_scan_found')) . '"';
echo ' data-msg-invalid="' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_bib_scan_invalid')) . '"';
echo ' data-msg-camera="' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_qr_camera_error')) . '">';
echo '<p class="inscr-gestion-bib-scan-hint text-muted small mb-1">' . fxInscrGestionEsc(fxInscrGestionScanLabel('inscr_gestion_bib_scan_hint')) . '</p>';
echo '<div class="inscr-gestion-bib-scan-status small mb-1" aria-live="polite"></div>';
echo '<div class="inscr-gestion-bib-scan-capture" role="button" tabindex="0">';
echo '<div class="inscr-gestion-bib-scan-video"></div>';
// Zone-cible centrale (eclaircie) : aligne le repere visuel sur la region
// reellement lue par l'OCR (captureCenterFrame : ~55% largeur x 30% hauteur).
echo '<div class="inscr-gestion-bib-scan-target" aria-hidden="true"></div>';
echo '<span class="inscr-gestion-bib-scan-capture-label">' . fxInscrGestionEsc(fxInscrGestionScanLabel('inscr_gestion_bib_scan_tap')) . '</span>';
echo '</div>';
echo '</div>';
}
function fxInscrGestionRenderList($intEveId, $strLangue, $strBaseUrl, $arrReq) {
global $vDomaine;
$arrEvenement = fxGetEvenementsId($intEveId);
$strEveNom = fxUnescape($arrEvenement['eve_nom_' . $strLangue]);
$blnSearchActive = !empty($arrReq['q']);
$blnFreshSearch = !empty($_GET['btn_recherche']) || !empty($_GET['apply_filters']);
$tabRows = null;
// Un seul resultat apres soumission : redirection JS (header() impossible ici, la page est deja entamee).
if ($blnSearchActive && $blnFreshSearch) {
$tabRows = fxInscrGestionFetchRows($intEveId, $arrReq);
if ($tabRows !== null && count($tabRows) === 1) {
$strFicheUrl = fxInscrGestionBuildUrl($strBaseUrl, $intEveId, $arrReq, array(
'pec_id' => (int)$tabRows[1]['pec_id_original'],
'pg' => 1,
));
echo '<div id="module-inscr-gestion" class="inscr-gestion-shell" data-inscr-gestion="1">';
echo '<script>window.location.replace(' . json_encode($strFicheUrl, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE) . ');</script>';
echo '<noscript><meta http-equiv="refresh" content="0;url=' . fxInscrGestionEsc($strFicheUrl) . '"></noscript>';
echo '</div>';
return;
}
}
$strBackUrl = fxInscrGestionIsEntry()
? $strBaseUrl
: ($vDomaine . ($strLangue === 'fr' ? '/compte/inc_tableau_promoteur' : '/account/inc_tableau_promoteur')
. '?promoteur_eve_id=' . rawurlencode(base64_encode((string)$intEveId)));
echo '<div id="module-inscr-gestion" class="inscr-gestion-shell" data-inscr-gestion="1">';
if (fxInscrGestionShouldShowListBack((int)$_SESSION['com_id'])) {
fxInscrGestionRenderNavBack($strBackUrl, fxInscrGestionListBackLabel($strLangue));
}
fxInscrGestionRenderPageHead(fxInscrGestionT('inscr_gestion_title'), $strEveNom);
fxInscrGestionRenderQrPanel($intEveId, $strLangue);
// Recherche + filtres dans un seul panneau (repliable — ouvert par défaut tant qu'aucune recherche n'a été lancée).
global $objDatabase;
$tabEpreuves = $objDatabase->fxGetResults(
'SELECT * FROM inscriptions_epreuves e WHERE e.eve_id = ' . intval($intEveId) . ' AND epr_actif = 1 ORDER BY epr_id'
);
echo '<details class="inscr-gestion-panel"' . ($blnSearchActive ? '' : ' open') . '>';
echo '<summary class="inscr-gestion-panel__head">';
fxInscrGestionRenderPanelSummary(fxInscrGestionT('txt_recherche'), 'fa-search');
echo '</summary>';
echo '<div class="inscr-gestion-panel__body">';
echo '<form action="' . fxInscrGestionEsc($strBaseUrl) . '" method="get" class="inscr-gestion-form">';
echo '<input type="hidden" name="promoteur_eve_id" value="' . fxInscrGestionEsc(base64_encode((string)$intEveId)) . '">';
$arrFields = array(
'rech_prenom' => 'rech_prenom',
'rech_nom' => 'rech_nom',
'rech_no_commande' => 'rech_no_commande',
'rech_nom_equipe' => 'inscr_gestion_rech_bib_equipe',
);
foreach ($arrFields as $strKey => $strLbl) {
echo '<label class="inscr-gestion-label" for="' . $strKey . '">' . fxInscrGestionEsc(fxInscrGestionT($strLbl)) . '</label>';
if ($strKey === 'rech_nom_equipe') {
echo '<div class="inscr-gestion-field-block inscr-gestion-field-block--search-bib" data-inscr-field="bib">';
fxInscrGestionRenderBibScanBlock($strKey, $strLangue);
echo '<div class="inscr-gestion-bib-row">';
echo '<input class="form-control form-control-sm rounded-0" type="text" id="' . $strKey . '" name="' . $strKey . '" value="' . fxInscrGestionEsc($arrReq[$strKey]) . '">';
echo '<button class="btn btn-secondary btn-sm rounded-0 inscr-gestion-bib-scan-toggle" type="button" aria-expanded="false"'
. ' title="' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_bib_scan_btn')) . '"'
. ' aria-label="' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_bib_scan_btn')) . '">';
echo '<i class="fa fa-camera" aria-hidden="true"></i>';
echo '</button>';
echo '</div></div>';
continue;
}
echo '<input class="form-control form-control-sm rounded-0" type="text" id="' . $strKey . '" name="' . $strKey . '" value="' . fxInscrGestionEsc($arrReq[$strKey]) . '">';
}
echo '<label class="inscr-gestion-label" for="sel_rech_epreuve">' . fxInscrGestionEsc(fxInscrGestionT('sel_rech_epreuve')) . '</label>';
echo '<select class="form-control form-control-sm rounded-0" id="sel_rech_epreuve" name="sel_rech_epreuve">';
echo '<option value="">—</option>';
for ($i = 1; $i <= count($tabEpreuves); $i++) {
$strOpt = fxInscrGestionEpreuveLabel($tabEpreuves[$i]['epr_id'], $strLangue);
$strSel = ($arrReq['sel_rech_epreuve'] == $tabEpreuves[$i]['epr_id']) ? ' selected' : '';
$strVal = base64_encode((string)$tabEpreuves[$i]['epr_id']);
echo '<option value="' . fxInscrGestionEsc($strVal) . '"' . $strSel . '>' . fxInscrGestionEsc($strOpt) . '</option>';
}
echo '</select>';
// MSIN-4401 — Filtre multi statut de course (tous si aucune sélection).
fxInscrGestionRenderStatutFilter($arrReq, $strLangue);
echo '<label class="inscr-gestion-check mt-2"><input type="checkbox" name="chk_bib" value="1"' . ($arrReq['chk_bib'] ? ' checked' : '') . '> ' . fxInscrGestionEsc(fxInscrGestionT('chk_bib')) . '</label>';
echo '<label class="inscr-gestion-check"><input type="checkbox" name="chk_cancelled" value="1"' . ($arrReq['chk_cancelled'] ? ' checked' : '') . '> ' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_chk_cancelled')) . '</label>';
echo '<div class="inscr-gestion-form-actions mt-2">';
echo '<a class="btn btn-secondary btn-sm rounded-0" href="' . fxInscrGestionEsc(fxInscrGestionSearchResetUrl($strBaseUrl, $intEveId)) . '">'
. fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_reset_search')) . '</a>';
echo '<button class="btn btn-primary btn-sm rounded-0" type="submit" name="btn_recherche" value="1">' . fxInscrGestionEsc(fxInscrGestionT('txt_recherche')) . '</button>';
echo '</div></form></div></details>';
echo '<div id="inscr-gestion-list" class="inscr-gestion-list-anchor">';
// Tant qu'aucune recherche n'a été lancée, on n'affiche aucune inscription : seulement une invite.
if (!$blnSearchActive) {
echo '<div class="alert alert-light border inscr-gestion-empty">'
. fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_search_prompt')) . '</div>';
echo '</div>';
return;
}
// Résultats (la recherche a été lancée — une recherche vide affiche toutes les inscriptions).
if ($tabRows === null) {
$tabRows = fxInscrGestionFetchRows($intEveId, $arrReq);
}
$intNbItemsParPage = 50;
$intNbTotal = ($tabRows !== null) ? count($tabRows) : 0;
$intPage = 1;
$intStart = 1;
$intEnd = $intNbItemsParPage;
if (!empty($_GET['pg'])) {
$intPage = max(1, intval($_GET['pg']));
}
if (intval($intPage) > 1) {
$intStart = ($intPage - 1) * $intNbItemsParPage + 1;
$intEnd = $intStart + $intNbItemsParPage - 1;
}
if ($intEnd > $intNbTotal) {
$intEnd = $intNbTotal;
}
echo '<div class="inscr-gestion-list-head">';
if ($strLangue === 'fr') {
echo '<span>' . (int)$intNbTotal . ' inscription' . ($intNbTotal > 1 ? 's' : '') . '</span>';
} else {
echo '<span>' . (int)$intNbTotal . ' registration' . ($intNbTotal !== 1 ? 's' : '') . '</span>';
}
if ($intNbTotal > $intNbItemsParPage) {
$intNbPages = (int)ceil($intNbTotal / $intNbItemsParPage);
echo '<span class="inscr-gestion-list-head__pg">' . ($strLangue === 'fr' ? 'Page ' : 'Page ') . $intPage . ' / ' . $intNbPages . '</span>';
}
echo '</div>';
if ($intNbTotal > 0) {
echo '<div class="inscr-gestion-list">';
// En-tete de colonnes (memes colonnes que les lignes ; masque en vue cellulaire,
// ou la liste passe en cartes). aria-hidden : purement visuel, redondant pour les
// lecteurs d'ecran car chaque valeur de ligne porte deja son sens.
echo '<div class="inscr-gestion-list-cols" aria-hidden="true">';
if ($strLangue === 'fr') {
echo '<span>Participant</span>';
echo '<span>Épreuve</span>';
echo '<span>Commande</span>';
echo '<span class="inscr-gestion-list-cols__bib">Dossard</span>';
echo '<span class="inscr-gestion-list-cols__check">Remis</span>';
} else {
echo '<span>Participant</span>';
echo '<span>Race</span>';
echo '<span>Order</span>';
echo '<span class="inscr-gestion-list-cols__bib">Bib</span>';
echo '<span class="inscr-gestion-list-cols__check">Issued</span>';
}
echo '<span></span>';
echo '</div>';
for ($j = $intStart; $j <= $intEnd; $j++) {
$row = $tabRows[$j];
$strFicheUrl = fxInscrGestionBuildUrl($strBaseUrl, $intEveId, $arrReq, array(
'pec_id' => (int)$row['pec_id_original'],
'pg' => $intPage,
));
$strBib = fxInscrGestionBibDisplay($row);
$strName = fxUnescape($row['par_nom']) . ', ' . fxUnescape($row['par_prenom']);
$strRace = fxInscrGestionEpreuveLabel($row['epr_id'], $strLangue);
$strCardClass = 'inscr-gestion-list-item';
$strCancelBadge = '';
if ($row['is_cancelled'] == '1') {
// MSIN-4405 — Cause derivee en lecture seule (aucune ecriture en base).
$strCause = fxGetAnnulationCause($row['pec_id_original'], $row['pec_actif']);
$strCardClass .= ' inscr-gestion-list-item--cancelled inscr-gestion-list-item--' . $strCause;
$strCancelBadge = ' <span class="inscr-gestion-cancel-badge inscr-gestion-cancel-badge--' . $strCause . '">'
. fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_statut_' . $strCause)) . '</span>';
}
echo '<a class="' . $strCardClass . '" href="' . fxInscrGestionEsc($strFicheUrl) . '">';
echo '<span class="inscr-gestion-list-item__name">' . fxInscrGestionEsc($strName) . $strCancelBadge . '</span>';
echo '<span class="inscr-gestion-list-item__race">' . fxInscrGestionEsc($strRace) . '</span>';
echo '<span class="inscr-gestion-list-item__order">' . fxInscrGestionEsc($row['no_commande']) . '</span>';
echo '<span class="inscr-gestion-list-item__bib">';
echo trim($strBib) !== '' ? fxInscrGestionEsc($strBib) : '—';
echo '</span>';
echo '<span class="inscr-gestion-list-item__check">';
if ($row['no_bib_remis'] == 1) {
echo '<span class="inscr-gestion-badge inscr-gestion-badge--ok">' . fxInscrGestionEsc(fxInscrGestionT('promoteur_bib_enregistre')) . '</span>';
}
echo '</span>';
echo '<i class="fa fa-chevron-right inscr-gestion-list-item__arrow" aria-hidden="true"></i>';
echo '</a>';
}
echo '</div>';
echo fxInscrGestionRenderPagination($strBaseUrl, $intEveId, $arrReq, $intNbTotal, $intNbItemsParPage, $intPage);
} else {
echo '<div class="alert alert-light border inscr-gestion-empty">' . fxInscrGestionEsc(fxInscrGestionT('txt_recherche_no_result')) . '</div>';
}
echo '</div>';
}
function fxInscrGestionRenderFiche($intEveId, $strLangue, $strBaseUrl, $arrReq, $arrRow) {
global $vDomaine;
$intComId = intval($_SESSION['com_id'] ?? 0);
$intRowEveId = intval($arrRow['eve_id']);
$blnCanStatut = fxInscrGestionCanDo($intComId, $intRowEveId, 'inscriptions_gestion.statut_edit');
$blnCanBib = fxInscrGestionCanDo($intComId, $intRowEveId, 'inscriptions_gestion.bib_edit');
$blnCanRemis = fxInscrGestionCanDo($intComId, $intRowEveId, 'inscriptions_gestion.bib_remis');
$blnCanEdit = fxInscrGestionCanDo($intComId, $intRowEveId, 'inscriptions_gestion.edit');
$blnCanCancel = fxInscrGestionCanDo($intComId, $intRowEveId, 'inscriptions_gestion.cancel');
$blnCanRestore = fxInscrGestionCanDo($intComId, $intRowEveId, 'inscriptions_gestion.restore');
// Droit de remboursement PayPal (etape remboursement) : controle l'affichage du bouton + panneau.
$blnCanRefund = fxInscrGestionCanDo($intComId, $intRowEveId, 'inscriptions_gestion.refund');
$blnHasEditable = ($blnCanStatut || $blnCanBib || $blnCanRemis);
$blnHasGestion = ($blnCanEdit || $blnCanCancel || $blnCanRestore || $blnCanRefund);
$blnInspect = function_exists('fxAdminPermInspectModeActive') && fxAdminPermInspectModeActive();
$strBackUrl = fxInscrGestionResolveBackUrl($strBaseUrl, $intEveId, $arrReq);
$strEveCode = fxGetEvenementsUrl($arrRow['eve_id']);
$strBib = fxInscrGestionBibDisplay($arrRow);
$intParId = $arrRow['par_id'];
$blnCancelled = ($arrRow['is_cancelled'] == '1');
$blnEquipe = (intval($arrRow['pec_equipe']) === 1);
$strBibAction = $blnEquipe ? 'no_equipe' : 'no_bib';
$strBibInputId = $blnEquipe ? 'txt_no_bib_e' : 'txt_no_bib_p';
if ($arrRow['no_bib_remis'] == 1) {
$strRemisClass = 'btn-success';
$strRemisIcon = '<i class="fa fa-check" aria-hidden="true"></i>';
$intRemisVal = 0;
} else {
$strRemisClass = 'btn-primary';
$strRemisIcon = '<i class="fa fa-sharp fa-regular fa-square fa-2xs" aria-hidden="true"></i>';
$intRemisVal = 1;
}
echo '<div id="module-inscr-gestion" class="inscr-gestion-shell inscr-gestion-fiche" data-inscr-gestion="1"';
$blnFromQr = !empty($_GET['from_qr']);
// MSIN-4401 — Kit « sections ouvertes » : force tous les onglets de la fiche.
$blnForceSectionsOpen = function_exists('fxEveAccesComEventHasFicheSectionsOpenKit')
&& fxEveAccesComEventHasFicheSectionsOpenKit($intComId, $intRowEveId);
if ($blnFromQr) {
echo ' data-from-qr="1"';
}
echo '>';
fxInscrGestionRenderNavBack($strBackUrl, fxInscrGestionResolveBackLabel($arrReq, $strLangue));
echo '<div class="inscr-gestion-fiche-head">';
echo '<h1 class="inscr-gestion-page-title">' . fxInscrGestionEsc(fxUnescape($arrRow['par_nom']) . ', ' . fxUnescape($arrRow['par_prenom'])) . '</h1>';
echo '</div>';
$strDetailsType = $blnEquipe && fxIsParEquipe($arrRow['pec_id_original'], $arrRow['epr_id'])
? fxInscrGestionT('promoteur_bib_show_teamates')
: fxInscrGestionT('epreuve_individuelle');
fxInscrGestionRenderFicheSectionStart(
'details',
fxInscrGestionT('inscr_gestion_details'),
'fa-info-circle',
($blnForceSectionsOpen || !$blnFromQr)
);
fxInscrGestionRenderKvListOpen(true);
fxInscrGestionRenderKvRow(fxInscrGestionT('inscr_gestion_epreuve'), fxInscrGestionEpreuveLabel($arrRow['epr_id'], $strLangue));
fxInscrGestionRenderKvRow(fxInscrGestionT('inscr_gestion_type'), $strDetailsType);
if ($blnCancelled) {
// MSIN-4405 — Detail de l'annulation (transferee / annulee) derive en lecture seule.
$strCause = fxGetAnnulationCause($arrRow['pec_id_original'], $arrRow['pec_actif']);
fxInscrGestionRenderKvRow(
fxInscrGestionT('inscr_gestion_annulation_titre'),
'<span class="inscr-gestion-cancel-badge inscr-gestion-cancel-badge--' . $strCause . '">'
. fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_statut_' . $strCause)) . '</span>',
true
);
}
fxInscrGestionRenderKvRow(fxInscrGestionT('inscr_gestion_modified'), fxCheckMAJ($arrRow['pec_id_original'], $intParId, $strLangue), true);
fxInscrGestionRenderFicheProfileKvRows($arrRow, $strLangue);
fxInscrGestionRenderKvListClose();
fxInscrGestionRenderFicheSectionEnd();
$strStyleEdit = $strStyleCancel = '';
$strStyleRetablir = ' d-none';
if ($blnCancelled) {
$strStyleEdit = $strStyleCancel = ' d-none';
$strStyleRetablir = '';
}
// Libelle du bouton de renvoi (facture/invitation) : passe par la table info pour etre editable.
$strRenvoi = fxInscrGestionT('inscr_gestion_renvoi');
if ($blnHasGestion || $blnInspect) {
// MSIN — a l'ouverture de la fiche, seul l'onglet Détails reste deploye (sauf kit sections ouvertes).
fxInscrGestionRenderFicheSectionStart(
'gestion',
fxInscrGestionT('inscr_gestion_section_management'),
'fa-cog',
$blnForceSectionsOpen
);
echo '<div class="inscr-gestion-gestion-actions">';
// Libelle devant le bouton facture (numero de commande) — invite a afficher la facture.
echo '<span class="inscr-gestion-facture-label">' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_show_invoice')) . '</span>';
echo '<a class="btn btn-primary btn-sm rounded-pill" href="' . $vDomaine . '/facture.php?no_panier=' . rawurlencode($arrRow['no_panier']) . '&amp;lang=' . $strLangue . '" target="_blank">' . fxInscrGestionEsc($arrRow['no_commande']) . '</a>';
// Bascule "Afficher la transaction" (MSIN) : place entre le numero de transaction et Modifier.
// Charge le resume de la commande en AJAX (action transaction_resume) dans le panneau ci-dessous.
echo '<button class="btn btn-info btn-sm rounded-0 btn_transaction_toggle" type="button"'
. ' data-no_panier="' . fxInscrGestionEsc($arrRow['no_panier']) . '"'
. ' data-transaction-target="#inscr-gestion-transaction-panel"'
. ' data-label-show="' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_show_transaction')) . '"'
. ' data-label-hide="' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_hide_transaction')) . '"'
. ' aria-expanded="false">' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_show_transaction')) . '</button>';
if ($blnCanEdit) {
echo '<button class="btn btn-primary btn-sm rounded-0 btn_edit_event_promo' . $strStyleEdit . '" type="button"'
. ' id="modifier_fiche" data-eve_code="' . fxInscrGestionEsc($strEveCode) . '"'
. ' data-epr_id="' . (int)$arrRow['epr_id'] . '" data-eve_id="' . (int)$arrRow['eve_id'] . '"'
. ' value="' . (int)$arrRow['pec_id_original'] . '">' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_modifier_participant')) . '</button>';
fxInscrGestionPermInspectEcho('inscriptions_gestion.edit');
} elseif ($blnInspect) {
fxInscrGestionPermInspectEchoHidden('inscriptions_gestion.edit', fxInscrGestionT('inscr_gestion_modifier_participant'));
}
if ($blnCanRefund) {
// Bouton bascule : ouvre/ferme le panneau de remboursement et declenche le chargement AJAX du formulaire.
echo '<button class="btn btn-warning btn-sm rounded-0 btn_refund_toggle" type="button"'
. ' data-inscr-field="remboursement"'
. ' data-no_panier="' . fxInscrGestionEsc($arrRow['no_panier']) . '"'
. ' data-eve_id="' . (int)$intRowEveId . '"'
. ' data-refund-target="#inscr-gestion-refund-panel"'
. ' aria-expanded="false">' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_remboursement')) . '</button>';
fxInscrGestionPermInspectEcho('inscriptions_gestion.refund');
} elseif ($blnInspect) {
fxInscrGestionPermInspectEchoHidden('inscriptions_gestion.refund', fxInscrGestionT('inscr_gestion_remboursement'));
}
if ($blnCanCancel) {
echo '<button class="btn btn-secondary btn-sm rounded-0 btn_cancel_event_promo' . $strStyleCancel . '" type="button"'
. ' id="annuler_fiche" data-eve_code="' . fxInscrGestionEsc($strEveCode) . '"'
. ' data-epr_id="' . (int)$arrRow['epr_id'] . '" data-eve_id="' . (int)$arrRow['eve_id'] . '"'
. ' value="' . (int)$arrRow['pec_id_original'] . '">' . fxInscrGestionEsc(fxInscrGestionT('btn_annulerinscriptions')) . '</button>';
fxInscrGestionPermInspectEcho('inscriptions_gestion.cancel');
} elseif ($blnInspect) {
fxInscrGestionPermInspectEchoHidden('inscriptions_gestion.cancel', fxInscrGestionT('btn_annulerinscriptions'));
}
if ($blnCanRestore) {
echo '<button class="btn btn-secondary btn-sm rounded-0 btn_retablir_event_promo' . $strStyleRetablir . '" type="button"'
. ' id="retablir_fiche" data-eve_code="' . fxInscrGestionEsc($strEveCode) . '"'
. ' data-epr_id="' . (int)$arrRow['epr_id'] . '" data-eve_id="' . (int)$arrRow['eve_id'] . '"'
. ' value="' . (int)$arrRow['pec_id_original'] . '">' . fxInscrGestionEsc(fxInscrGestionT('btn_retablirinscriptions')) . '</button>';
fxInscrGestionPermInspectEcho('inscriptions_gestion.restore');
} elseif ($blnInspect) {
fxInscrGestionPermInspectEchoHidden('inscriptions_gestion.restore', fxInscrGestionT('btn_retablirinscriptions'));
}
// RENVOI place en dernier (MSIN-4403) : retourne la confirmation au participant.
echo '<button class="btn btn-primary btn-sm rounded-0 link_confirmation" type="button"'
. ' data-no_panier="' . fxInscrGestionEsc($arrRow['no_panier']) . '"'
. ' data-no_commande="' . fxInscrGestionEsc($arrRow['no_commande']) . '"'
. ' data-lng="' . $strLangue . '">' . fxInscrGestionEsc($strRenvoi) . '</button>';
echo '</div>';
if ($blnCanRefund) {
// Panneau de remboursement : vide au depart, rempli en AJAX a la 1ere ouverture (action=refund_form).
// data-loaded evite de recharger inutilement ; data-no_panier identifie la commande cote serveur.
// Place AVANT le panneau transaction pour qu'a l'ouverture le formulaire apparaisse au-dessus du detail.
echo '<div class="inscr-gestion-refund-panel" id="inscr-gestion-refund-panel" style="margin-top:10px;"'
. ' data-no_panier="' . fxInscrGestionEsc($arrRow['no_panier']) . '"'
. ' data-loaded="0" hidden></div>';
}
// Panneau de transaction : vide au depart, rempli en AJAX a la 1ere ouverture (action=transaction_resume).
// Sous les boutons, repliable, accessible a tout moment (independant du remboursement).
echo '<div class="inscr-gestion-transaction-panel" id="inscr-gestion-transaction-panel" style="margin-top:10px;"'
. ' data-no_panier="' . fxInscrGestionEsc($arrRow['no_panier']) . '"'
. ' data-loaded="0" hidden></div>';
fxInscrGestionRenderFicheSectionEnd();
}
if ($blnHasEditable || $blnInspect) {
// MSIN — champs modifiables fermes par defaut ; ouverts si scan QR ou kit « sections ouvertes ».
fxInscrGestionRenderFicheSectionStart(
'editable',
fxInscrGestionT('inscr_gestion_section_editable'),
'fa-pencil-square-o',
($blnForceSectionsOpen || $blnFromQr)
);
if ($blnCanBib) {
$strFullBibInputId = $strBibInputId . $intParId;
// Scanner OCR toujours offert sur la fiche de gestion : la saisie se fait
// souvent au cellulaire / a une table d'inscription. La lecture est 100%
// cote client (Tesseract.js) et ne touche aucun endpoint serveur protege.
$blnCanBibScan = true;
echo '<div class="inscr-gestion-field-block" data-inscr-field="bib">';
echo '<div class="inscr-gestion-field-block__label">' . fxInscrGestionEsc(fxInscrGestionT('promoteur_bib_number')) . '</div>';
if ($blnCanBibScan) {
fxInscrGestionRenderBibScanBlock($strFullBibInputId, $strLangue);
}
echo '<div class="inscr-gestion-bib-row">';
echo '<input class="form-control rounded-0" type="text" id="' . $strFullBibInputId . '" value="' . fxInscrGestionEsc($strBib) . '">';
if ($blnCanBibScan) {
echo '<button class="btn btn-secondary rounded-0 inscr-gestion-bib-scan-toggle" type="button" aria-expanded="false"'
. ' title="' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_bib_scan_btn')) . '"'
. ' aria-label="' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_bib_scan_btn')) . '">';
echo '<i class="fa fa-camera" aria-hidden="true"></i>';
echo '</button>';
}
echo '<button class="btn btn-primary rounded-0 btn_bib" type="button" data-action="' . $strBibAction . '"'
. ' data-eve_id="' . rawurlencode(base64_encode((string)$arrRow['eve_id'])) . '"'
. ' data-epr_id="' . (int)$arrRow['epr_id'] . '"'
. ' data-par_id="' . (int)$intParId . '"'
. ' data-pec_id="' . (int)$arrRow['pec_id_original'] . '">OK</button>';
fxInscrGestionPermInspectEcho('inscriptions_gestion.bib_edit');
echo '</div>';
echo '</div>';
} elseif ($blnInspect) {
fxInscrGestionPermInspectEchoHidden('inscriptions_gestion.bib_edit', fxInscrGestionT('promoteur_bib_number'));
}
if ($blnCanStatut) {
echo '<div class="inscr-gestion-field-block inscr-gestion-field-block--statut" data-inscr-field="statut">';
echo '<div class="inscr-gestion-field-block__label">' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_statut_label')) . '</div>';
fxInscrGestionRenderStatutField($intParId, $arrRow['par_statut_course'] ?? '', $arrRow['eve_id'], $strLangue);
fxInscrGestionPermInspectEcho('inscriptions_gestion.statut_edit');
echo '</div>';
} elseif ($blnInspect) {
fxInscrGestionPermInspectEchoHidden('inscriptions_gestion.statut_edit', fxInscrGestionT('inscr_gestion_statut_label'));
}
$strRemisDate = trim((string)$arrRow['no_bib_remis_date']);
if ($strRemisDate === '0000-00-00 00:00:00') {
$strRemisDate = '';
}
$strRemisPar = trim((string)$arrRow['no_bib_remis_par']);
if ($blnCanRemis) {
echo '<div class="inscr-gestion-field-block" data-inscr-field="remis">';
echo '<div class="inscr-gestion-remis-row">';
echo '<div class="inscr-gestion-remis-row__left">';
echo '<div class="inscr-gestion-field-block__label inscr-gestion-field-block__label--inline">' . fxInscrGestionEsc(fxInscrGestionT('promoteur_bib_enregistre')) . '</div>';
echo '<span id="' . (int)$intParId . '">';
echo '<a class="btn btn-sm rounded-0 btn_remis ' . $strRemisClass . '" href="#" data-action="remis"'
. ' data-table="' . rawurlencode(base64_encode('resultats_participants')) . '"'
. ' data-field="no_bib_remis" data-key="par_id" data-valeur="' . $intRemisVal . '"'
. ' data-id="' . (int)$intParId . '">' . $strRemisIcon . '</a>';
echo '</span>';
echo '</div>';
echo '<div class="inscr-gestion-remis-row__meta inscr-gestion-muted">';
echo '<span id="date_' . (int)$intParId . '">' . fxInscrGestionEsc($strRemisDate) . '</span>';
if ($strRemisDate !== '' && $strRemisPar !== '') {
echo '<span class="inscr-gestion-remis-row__sep"> — </span>';
}
echo '<span id="par_' . (int)$intParId . '">' . fxInscrGestionEsc($strRemisPar) . '</span>';
echo '</div></div></div>';
fxInscrGestionPermInspectEcho('inscriptions_gestion.bib_remis');
} elseif ($blnInspect) {
fxInscrGestionPermInspectEchoHidden('inscriptions_gestion.bib_remis', fxInscrGestionT('promoteur_bib_enregistre'));
}
fxInscrGestionRenderFicheSectionEnd();
}
echo '</div>';
}
function fxInscrGestionRun($intEveId, $strLangue) {
$strBaseUrl = fxInscrGestionBaseUrl($strLangue);
$arrReq = fxInscrGestionParseRequest();
if ($arrReq['pec_id'] > 0) {
$arrRow = fxInscrGestionFetchRowByPec($arrReq['pec_id'], $intEveId);
if ($arrRow === null) {
echo '<div class="alert alert-danger">' . fxInscrGestionEsc(fxInscrGestionT('txt_recherche_no_result')) . '</div>';
echo '<a class="btn btn-primary rounded-0" href="' . fxInscrGestionEsc(fxInscrGestionResolveBackUrl($strBaseUrl, $intEveId, $arrReq)) . '">';
echo fxInscrGestionEsc(fxInscrGestionResolveBackLabel($arrReq, $strLangue)) . '</a>';
return;
}
fxInscrGestionRenderFiche($intEveId, $strLangue, $strBaseUrl, $arrReq, $arrRow);
return;
}
fxInscrGestionRenderList($intEveId, $strLangue, $strBaseUrl, $arrReq);
}