Files
ms1inscription-v5/php/inc_fx_inscriptions_gestion.php

2784 lines
125 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* Gestion des inscriptions — recherche, filtres, cartes, fiche.
*
* Deux entrees distinctes (meme module, navigation differente) :
* - Promoteur : /compte/inc_tableau_inscriptions_gestion → retour hub (ou inc_tableau_promoteur legacy)
* - 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() {
// MSIN-4401 — page V2 seulement (plus d'ancienne clé API registrations.view)
return array('inscriptions_gestion.view');
}
function fxInscrGestionPermKeysScan() {
// MSIN-4401 — lecteur QR = détail Inscriptions V2 (même droit sur /compte et /mobile)
return array('inscriptions_gestion.qr_scan');
}
/** Permission action (niveau fonction dans la fiche de gestion). */
function fxInscrGestionCanDo($intComId, $intEveId, $strActionPerm) {
fxInscrGestionLoadEveAcces();
if (!fxInscrGestionCanAccess($intComId, $intEveId)) {
return false;
}
// MSIN-4401 — kit propriétaire / total (grants_all) = toutes les actions, sans cocher chaque droit.
if (fxEveAccesComEventHasGrantsAllKit(intval($intComId), intval($intEveId))) {
return true;
}
if (fxEveAccesHasPermission(intval($intComId), intval($intEveId), $strActionPerm)) {
return true;
}
// MSIN-4401 — Héritage kit desktop « inscriptions.edit » : actions de gestion courantes,
// PAS les actions sensibles (remboursement, etc.) — celles-ci exigent le droit explicite.
$tabNoInheritFromInscrEdit = array(
'inscriptions_gestion.refund' => true,
);
if (empty($tabNoInheritFromInscrEdit[$strActionPerm])
&& 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>';
}
/** MSIN-4401 — Enveloppe action + cadenas (toujours a droite du controle). */
/**
* MSIN-4464 — Bouton « ! » historique (seulement s'il y a au moins 1 log pour le champ).
*
* @param array<string,true> $arrFieldsWithLogs
*/
function fxInscrGestionRenderAuditBang($intParId, $strFieldKey, $blnCanAudit, $arrFieldsWithLogs)
{
$intParId = intval($intParId);
$strFieldKey = preg_replace('/[^a-z0-9_]/i', '', (string)$strFieldKey);
if (!$blnCanAudit || $intParId <= 0 || $strFieldKey === '' || empty($arrFieldsWithLogs[$strFieldKey])) {
return;
}
$strTitle = fxInscrGestionT('inscr_gestion_audit_bang_title');
$strAria = fxInscrGestionT('inscr_gestion_audit_bang_aria');
echo ' <button type="button" class="inscr-gestion-audit-bang"'
. ' data-par_id="' . $intParId . '"'
. ' data-field="' . fxInscrGestionEsc($strFieldKey) . '"'
. ' title="' . fxInscrGestionEsc($strTitle) . '"'
. ' aria-label="' . fxInscrGestionEsc($strAria) . '">!</button>';
}
function fxInscrGestionActionItemOpen($strExtraClass = '') {
$strExtra = trim((string)$strExtraClass);
echo '<span class="inscr-gestion-action-item'
. ($strExtra !== '' ? ' ' . $strExtra : '')
. '">';
}
function fxInscrGestionActionItemClose() {
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) {
// MSIN-4401 — Super-admin (usa_id) ne bypass plus : kits / extras seulement.
fxInscrGestionLoadEveAcces();
return fxEveAccesHasInscrGestionWebAccess(intval($intComId), intval($intEveId));
}
function fxInscrGestionCanScan($intComId, $intEveId) {
fxInscrGestionLoadEveAcces();
$intComId = intval($intComId);
$intEveId = intval($intEveId);
// MSIN-4401 — kit propriétaire / total (grants_all) = tout, sans cocher chaque droit.
if (fxEveAccesComEventHasGrantsAllKit($intComId, $intEveId)) {
return true;
}
// Même règle hub / compte /mobile : droit explicite scanner QR.
if (fxEveAccesHasAnyPermission($intComId, $intEveId, fxInscrGestionPermKeysScan())) {
return true;
}
// Transition : clé absente en BD → fallback page Inscriptions V2.
if (!fxEveAccesCatalogPermIsActive('inscriptions_gestion.qr_scan')
&& fxEveAccesHasPermission($intComId, $intEveId, 'inscriptions_gestion.view')) {
return true;
}
return false;
}
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
|| fxInscrGestionIsRawI18nKey($strTexte)) {
$tabMemo[$strMemoKey] = null;
return null;
}
$tabMemo[$strMemoKey] = (string)$strTexte;
return $tabMemo[$strMemoKey];
}
/**
* True si $str ressemble a une clef info technique (ex. inscr_gestion_bib_scan_tap),
* pas a un vrai libelle (espaces/accents/points). Ignore marqueurs ZWSP trad.
*/
function fxInscrGestionIsRawI18nKey($str) {
$str = preg_replace('/[\x{200B}\x{200C}]/u', '', (string)$str);
// Payload base64 eventuel (mode edition textes) entre ZWSP deja retire.
$str = trim($str);
if ($str === '') {
return false;
}
return (bool)preg_match('/^[a-z][a-z0-9]*(_[a-z0-9]+)+$/i', $str);
}
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 && !fxInscrGestionIsRawI18nKey($str)) {
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 && !fxInscrGestionIsRawI18nKey($str)) {
return $str;
}
}
$strDb = fxInscrGestionLookupTexteDb($strClef, $strLangue);
if ($strDb !== null && !fxInscrGestionIsRawI18nKey($strDb)) {
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);
}
$strPlain = trim(preg_replace('/[\x{200B}\x{200C}]/u', '', (string)$str));
if ($strPlain === '' || $strPlain === $strClef || fxInscrGestionIsRawI18nKey($strPlain)) {
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
? 'Cadrez le numéro dans le cadre, puis touchez l\'image.'
: 'Frame the number in the box, then tap the image.',
'inscr_gestion_bib_scan_ready' => $blnFr ? 'Touchez l\'image pour lire' : 'Tap the image to read',
'inscr_gestion_bib_scan_tap' => $blnFr
? 'Touchez pour lire'
: 'Tap to read',
'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',
'inscr_gestion_mobile_title' => $blnFr ? 'Inscriptions mobile' : 'Mobile registrations',
'inscr_gestion_login_user' => $blnFr ? 'Nom d\'utilisateur' : 'Username',
'inscr_gestion_login_pass' => $blnFr ? 'Mot de passe' : 'Password',
'inscr_gestion_login_btn' => $blnFr ? 'Connexion' : 'Sign in',
'inscr_gestion_login_intro' => $blnFr
? 'Connectez-vous pour accéder aux inscriptions mobile.'
: 'Sign in to access mobile registrations.',
'inscr_gestion_login_err_user' => $blnFr
? 'Veuillez entrer votre nom d\'usager'
: 'Please enter your username',
'inscr_gestion_login_err_pass' => $blnFr
? 'Veuillez entrer votre mot de passe'
: 'Please enter your password',
'inscr_gestion_choose_event' => $blnFr ? 'Choisir un événement' : 'Choose an event',
'inscr_gestion_back_events' => $blnFr ? 'Événements' : 'Events',
'tableau_promoteur_menueinscription_v2' => $blnFr ? 'Inscriptions V2' : 'Registrations V2',
'mobile_no_acces_v2' => $blnFr
? 'Aucun accès inscriptions mobile (v2) n\'est actif pour ce compte.'
: 'No active mobile registration access (v2) for this account.',
'mobile_invite_conflict_signed_as' => $blnFr
? 'Vous êtes actuellement connecté en tant que %name%.'
: 'You are currently signed in as %name%.',
'mobile_invite_conflict_for' => $blnFr
? 'Cette invitation concerne le compte %name%%mail_part%. Déconnectez-vous d\'abord, puis reconnectez-vous avec ce compte.'
: 'This invitation is for %name%%mail_part%. Sign out first, then sign in with that account.',
'mobile_invite_conflict_btn_logout' => $blnFr
? 'Se déconnecter et continuer'
: 'Sign out and continue',
'mobile_invite_conflict_btn_stay' => $blnFr
? 'Rester sur mon compte actuel'
: 'Stay on my current account',
'inscr_gestion_bib_scan_found_search' => $blnFr
? 'Numéro %s détecté — recherche...'
: 'Number %s detected — searching...',
'inscr_gestion_bib_ocr_not_configured' => $blnFr
? 'OCR non configuré (clé Vision absente sur le serveur).'
: 'OCR not configured (Vision API key missing on server).',
'inscr_gestion_bib_ocr_vision_prefix' => $blnFr ? 'Erreur Vision : ' : 'Vision error: ',
'inscr_gestion_bib_ocr_image_invalid' => $blnFr ? 'Image invalide.' : 'Invalid image capture.',
'inscr_gestion_bib_ocr_network' => $blnFr ? 'Erreur réseau ou capture.' : 'Network or capture error.',
'inscr_gestion_err_load' => $blnFr ? 'Erreur de chargement.' : 'Loading error.',
'inscr_gestion_err_network' => $blnFr ? 'Erreur réseau.' : 'Network error.',
'inscr_gestion_err_refund' => $blnFr ? 'Erreur remboursement.' : 'Refund error.',
'inscr_gestion_refund_ok' => $blnFr ? 'Remboursement effectué.' : 'Refund completed.',
'inscr_gestion_refund_cancel_hint' => $blnFr
? 'Votre transaction a été remboursée. Si vous voulez aussi l\'annuler dans le système d\'inscription :'
: 'Your transaction has been refunded. If you also want to cancel it in the registration system:',
'inscr_gestion_col_participant' => $blnFr ? 'Participant' : 'Participant',
'inscr_gestion_col_commande' => $blnFr ? 'Commande' : 'Order',
'inscr_gestion_col_dossard' => $blnFr ? 'Dossard' : 'Bib',
// MSIN-4399 — Colonne statut de course (entre dossard et remis).
'inscr_gestion_col_statut' => $blnFr ? 'Statut' : 'Status',
'inscr_gestion_col_remis' => $blnFr ? 'Remis' : 'Issued',
// MSIN-4448 — Edition inline liste (OK dossard + confirmation remis).
'inscr_gestion_bib_ok' => 'OK',
'inscr_gestion_remis_confirm_title' => $blnFr ? 'Confirmation' : 'Confirmation',
'inscr_gestion_remis_confirm_msg' => $blnFr
? 'Êtes-vous certain de vouloir retirer la mention « dossard récupéré » pour ce participant ?'
: 'Are you sure you want to remove the "bib retrieved" status for this participant?',
'inscr_gestion_remis_confirm_ok' => $blnFr ? 'Retirer' : 'Remove',
'inscr_gestion_remis_confirm_cancel' => $blnFr ? 'Annuler' : 'Cancel',
// MSIN-4464 — historique fiche ( ! )
'inscr_gestion_audit_bang_title' => $blnFr ? 'Voir les derniers changements' : 'View recent changes',
'inscr_gestion_audit_bang_aria' => $blnFr ? 'Historique des modifications' : 'Change history',
'inscr_gestion_audit_popup_title' => $blnFr ? 'Derniers changements' : 'Recent changes',
'inscr_gestion_audit_popup_empty' => $blnFr ? 'Aucun historique pour ce champ.' : 'No history for this field.',
'inscr_gestion_audit_popup_close' => $blnFr ? 'Fermer' : 'Close',
'inscr_gestion_audit_col_when' => $blnFr ? 'Quand' : 'When',
'inscr_gestion_audit_col_who' => $blnFr ? 'Par' : 'By',
'inscr_gestion_audit_col_from' => $blnFr ? 'Avant' : 'From',
'inscr_gestion_audit_col_to' => $blnFr ? 'Après' : 'To',
'inscr_gestion_audit_field_par_statut_course' => $blnFr ? 'Statut de course' : 'Race status',
'inscr_gestion_audit_field_no_bib' => $blnFr ? 'Numéro de dossard' : 'Bib number',
'inscr_gestion_audit_field_no_bib_remis' => $blnFr ? 'Dossard remis' : 'Bib handed out',
'inscr_gestion_audit_field_is_cancelled' => $blnFr ? 'Annulation' : 'Cancellation',
// MSIN-4328 — bouton superadmin vers fiche ChronoTrack
'inscr_gestion_btn_chronotrack' => $blnFr ? 'Voir dans ChronoTrack' : 'View in ChronoTrack',
'inscr_gestion_list_count_one' => $blnFr ? '%d inscription' : '%d registration',
'inscr_gestion_list_count_many' => $blnFr ? '%d inscriptions' : '%d registrations',
'inscr_gestion_list_page' => $blnFr ? 'Page %d / %d' : 'Page %d / %d',
'inscr_gestion_list_filter_kv' => $blnFr ? '%s : %s' : '%s: %s',
'inscr_gestion_rech_bib_equipe' => $blnFr ? 'Dossard ou équipe' : 'Bib or team',
'inscr_gestion_bib_match_exact' => $blnFr ? 'Exact' : 'Exact',
'inscr_gestion_bib_match_partial' => $blnFr ? 'Partiel' : 'Partial',
'inscr_gestion_bib_match_aria' => $blnFr
? 'Mode de recherche du dossard'
: 'Bib search mode',
'chk_bib' => $blnFr ? 'Sans numéro de dossard' : 'Without bib number',
'inscr_gestion_reset_search' => $blnFr ? 'Réinitialiser la recherche' : 'Reset search',
// MSIN-4459 — Nom d'équipe + coéquipiers sous le capitaine en liste.
'inscr_gestion_list_team_name' => $blnFr ? 'Nom d\'équipe : %s' : 'Team name: %s',
'inscr_gestion_list_teammate' => $blnFr ? 'Coéquipier' : 'Teammate',
);
}
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);
$strPlain = trim(preg_replace('/[\x{200B}\x{200C}]/u', '', (string)$strTexte));
if ($strPlain === '' || $strPlain === $strClef || fxInscrGestionIsRawI18nKey($strPlain)) {
$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 le scanner : jamais de clef technique ni marqueur trad
* (affiche dans la video / data-msg lus par le JS).
*/
function fxInscrGestionScanLabel($strClef) {
global $strLangue;
$strLang = (isset($strLangue) && $strLangue !== '') ? $strLangue : 'fr';
$strTexte = trim(preg_replace('/[\x{200B}\x{200C}]/u', '', (string)fxInscrGestionResolveTexte($strClef, $strLang)));
if ($strTexte === '' || $strTexte === $strClef || fxInscrGestionIsRawI18nKey($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);
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(fxInscrGestionT('inscr_gestion_mobile_title'));
echo '<p class="inscr-gestion-login-intro text-muted">'
. fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_login_intro')) . '</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(fxInscrGestionT('inscr_gestion_login_user')) . '</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(fxInscrGestionT('inscr_gestion_login_pass')) . '</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(fxInscrGestionT('inscr_gestion_login_btn')) . '</button>';
echo '</form></div></div>';
}
/** Libellé menu promoteur V2 (Info). */
function fxInscrGestionPromoteurMenuLabel() {
return fxInscrGestionT('tableau_promoteur_menueinscription_v2');
}
function fxInscrGestionListBackLabel($strLangue) {
if (fxInscrGestionIsEntry()) {
return fxInscrGestionT('inscr_gestion_back_events');
}
return fxInscrGestionT('promoteur_back');
}
/**
* MSIN-4399 — URL retour depuis la liste inscriptions V2 :
* hub promoteur si dispo (avec focus sur l'événement), sinon ancien tableau promoteur.
*/
function fxInscrGestionListBackUrl($intEveId, $strLangue) {
$strLangue = ($strLangue === 'en') ? 'en' : 'fr';
if (!function_exists('fxPromoteurHubOrLegacyBackUrl')) {
require_once __DIR__ . '/inc_fx_promoteur_hub.php';
}
return fxPromoteurHubOrLegacyBackUrl($intEveId, $strLangue);
}
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 » (rendu dans inc_header sur /mobile). */
function fxInscrGestionRenderFullAccountLink($strLangue) {
// Conservé pour appels éventuels ; l'UI principale est dans inc_header (zone Mon compte / Déconnexion).
$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']) : '',
// MSIN-4399 — Match dossard : exact (défaut) | partial.
'bib_match' => (isset($_GET['bib_match']) && $_GET['bib_match'] === 'partial') ? 'partial' : 'exact',
'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' => '',
'bib_match' => 'exact',
'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) {
// MSIN-4399 — Réinit = champs vides + toutes les inscriptions (q=1).
// Distinct de la 1re visite (sans q) qui n'affiche que l'invite.
return fxInscrGestionBuildUrl($strBaseUrl, $intEveId, fxInscrGestionEmptyListRequest(), array('pg' => 1, 'q' => 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']);
// MSIN-4399 — Exact = égalité TRIM (défaut) ; Partiel = LIKE %…%.
if (empty($arrReq['bib_match']) || $arrReq['bib_match'] !== 'partial') {
$strWhere .= ' AND ('
. " TRIM(IFNULL(p.no_bib, '')) = '" . $strEscBibEquipe . "'"
. " OR TRIM(IFNULL(e.no_equipe, '')) = '" . $strEscBibEquipe . "'"
. " OR TRIM(IFNULL(e.pec_nom_equipe, '')) = '" . $strEscBibEquipe . "'"
. " OR TRIM(IFNULL(p.par_nom_equipe, '')) = '" . $strEscBibEquipe . "'"
. ')';
} else {
$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';
}
// MSIN-4401 — « Sans dossard » = le numéro affiché en liste (solo: p.no_bib, équipe: e.no_equipe).
if ($arrReq['chk_bib']) {
$strWhere .= ' AND ('
. " (IFNULL(e.pec_equipe, 0) = 0 AND TRIM(IFNULL(p.no_bib, '')) = '')"
. " OR (IFNULL(e.pec_equipe, 0) = 1 AND TRIM(IFNULL(e.no_equipe, '')) = '')"
. ')';
}
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);
// MSIN-4459 — pec_nom_equipe pour affichage du nom d'équipe en liste (1 ligne = 1 commande).
$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, e.pec_nom_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);
}
/**
* MSIN-4459 — Membres d'équipe pour enrichir la liste (sous le capitaine).
* Une requête batch par page ; exclut rol_id 3/4 (comme promoteur).
*
* @param array $arrPecIds pec_id_original
* @return array map pec_id => liste de participants (capitaine en premier si present)
*/
function fxInscrGestionFetchTeamMembersByPecs($arrPecIds) {
global $objDatabase;
$arrIds = array();
if (is_array($arrPecIds)) {
foreach ($arrPecIds as $intPec) {
$intPec = intval($intPec);
if ($intPec > 0) {
$arrIds[$intPec] = $intPec;
}
}
}
if (count($arrIds) === 0) {
return array();
}
$sql = 'SELECT pec_id, par_id, par_nom, par_prenom, rol_id, no_bib_remis, par_statut_course'
. ' FROM resultats_participants'
. ' WHERE pec_id IN (' . implode(',', $arrIds) . ')'
. ' AND rol_id NOT IN (3, 4)'
. ' ORDER BY pec_id, CASE WHEN rol_id = 1 THEN 0 ELSE 1 END, par_nom, par_prenom';
$tab = $objDatabase->fxGetResults($sql);
$map = array();
if (!is_array($tab) || count($tab) === 0) {
return $map;
}
$intNb = count($tab);
for ($i = 1; $i <= $intNb; $i++) {
$intPec = intval($tab[$i]['pec_id']);
if (!isset($map[$intPec])) {
$map[$intPec] = array();
}
$map[$intPec][] = $tab[$i];
}
return $map;
}
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, p.par_id_original, p.ct_entry_id, 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();
}
/**
* MSIN-4399 — Libellé affichable du statut de course (Confirmé, DNS, …).
* Valeur vide → Confirmé (même défaut que la fiche).
*/
function fxInscrGestionStatutCourseLabel($strCode, $strLangue) {
$strCode = fxInscrGestionResolveParStatutCourse($strCode);
$tabOptions = fxInscrGestionGetStatutCourseOptions($strLangue);
foreach ($tabOptions as $arrOpt) {
if (trim((string)$arrOpt['info_option2']) === $strCode) {
$strLabel = trim((string)$arrOpt['info_texte']);
return $strLabel !== '' ? $strLabel : $strCode;
}
}
return $strCode;
}
/** 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'];
}
// MSIN-4399 — Conserver le mode partiel dans pagination / fiche (exact = défaut, pas dans lURL).
if (!empty($arrReq['bib_match']) && $arrReq['bib_match'] === 'partial') {
$tab['bib_match'] = 'partial';
}
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 = fxInscrGestionT('inscr_gestion_choose_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>';
}
/**
* MSIN-4401 — OCR dossard via Google Cloud Vision (serveur).
* Solution produit : ~95 % sur photos reelles vs ~50 % Tesseract.js mobile.
*/
function fxInscrGestionBibOcrCloudEnabled() {
return defined('MSIN_BIB_OCR_VISION_API_KEY')
&& trim((string)MSIN_BIB_OCR_VISION_API_KEY) !== '';
}
/** Extraire un numero de dossard depuis du texte OCR (plus long groupe 16 chiffres). */
function fxInscrGestionExtractBibNumberFromOcr($strRaw) {
if (!preg_match_all('/\d+/', (string)$strRaw, $arrM) || empty($arrM[0])) {
return '';
}
$arrMatches = $arrM[0];
$blnAllSingles = true;
foreach ($arrMatches as $strM) {
if (strlen($strM) !== 1) {
$blnAllSingles = false;
break;
}
}
if ($blnAllSingles) {
$strJoined = implode('', $arrMatches);
return (strlen($strJoined) >= 1 && strlen($strJoined) <= 6) ? $strJoined : '';
}
$strBest = '';
foreach ($arrMatches as $strM) {
$intLen = strlen($strM);
if ($intLen <= 6 && $intLen > strlen($strBest)) {
$strBest = $strM;
}
}
return $strBest;
}
/**
* Appel REST Vision TEXT_DETECTION (sans SDK Composer).
* $strImageBase64 = contenu image (jpeg/png), sans prefixe data:.
* @return array{ok:bool, text?:string, number?:string, message?:string}
*/
function fxInscrGestionVisionReadBibNumber($strImageBase64) {
$strKey = defined('MSIN_BIB_OCR_VISION_API_KEY')
? trim((string)MSIN_BIB_OCR_VISION_API_KEY)
: '';
if ($strKey === '') {
return array('ok' => false, 'message' => 'ocr_not_configured');
}
$strImageBase64 = preg_replace('#^data:image/[^;]+;base64,#', '', (string)$strImageBase64);
$strImageBase64 = preg_replace('/\s+/', '', $strImageBase64);
if ($strImageBase64 === '' || strlen($strImageBase64) > 5 * 1024 * 1024) {
return array('ok' => false, 'message' => 'image_invalid');
}
$strUrl = 'https://vision.googleapis.com/v1/images:annotate?key=' . rawurlencode($strKey);
$arrPayload = array(
'requests' => array(
array(
'image' => array('content' => $strImageBase64),
'features' => array(
array('type' => 'TEXT_DETECTION', 'maxResults' => 10),
),
),
),
);
$strBody = json_encode($arrPayload);
$strResponse = '';
$intHttp = 0;
if (function_exists('curl_init')) {
$ch = curl_init($strUrl);
curl_setopt_array($ch, array(
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => array('Content-Type: application/json'),
CURLOPT_POSTFIELDS => $strBody,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 25,
CURLOPT_CONNECTTIMEOUT => 10,
));
$mixResponse = curl_exec($ch);
$intHttp = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
$strCurlErr = (string)curl_error($ch);
curl_close($ch);
if ($mixResponse === false) {
error_log('[bib_ocr] Vision curl fail: ' . $strCurlErr);
return array('ok' => false, 'message' => 'vision_curl', 'detail' => $strCurlErr);
}
$strResponse = (string)$mixResponse;
} else {
$ctx = stream_context_create(array(
'http' => array(
'method' => 'POST',
'header' => "Content-Type: application/json\r\n",
'content' => $strBody,
'timeout' => 25,
'ignore_errors' => true,
),
));
$strResponse = (string)@file_get_contents($strUrl, false, $ctx);
if (isset($http_response_header[0]) && preg_match('/\s(\d{3})\s/', $http_response_header[0], $m)) {
$intHttp = (int)$m[1];
}
}
if ($strResponse === '' || ($intHttp > 0 && $intHttp >= 400)) {
$arrErr = json_decode($strResponse, true);
$strDetail = '';
if (is_array($arrErr) && !empty($arrErr['error']['message'])) {
$strDetail = (string)$arrErr['error']['message'];
}
error_log('[bib_ocr] Vision HTTP ' . $intHttp . ' body=' . substr($strResponse, 0, 400));
return array('ok' => false, 'message' => 'vision_http', 'detail' => $strDetail !== '' ? $strDetail : ('HTTP ' . $intHttp));
}
$arrJson = json_decode($strResponse, true);
if (!is_array($arrJson)) {
return array('ok' => false, 'message' => 'vision_parse');
}
if (!empty($arrJson['error']['message'])) {
error_log('[bib_ocr] Vision error: ' . $arrJson['error']['message']);
return array('ok' => false, 'message' => 'vision_api', 'detail' => (string)$arrJson['error']['message']);
}
$strText = '';
if (!empty($arrJson['responses'][0]['textAnnotations'][0]['description'])) {
$strText = (string)$arrJson['responses'][0]['textAnnotations'][0]['description'];
} elseif (!empty($arrJson['responses'][0]['fullTextAnnotation']['text'])) {
$strText = (string)$arrJson['responses'][0]['fullTextAnnotation']['text'];
}
$strNum = fxInscrGestionExtractBibNumberFromOcr($strText);
if ($strNum === '') {
return array('ok' => false, 'message' => 'no_number', 'text' => $strText);
}
return array('ok' => true, 'number' => $strNum, 'text' => $strText);
}
function fxInscrGestionRenderBibScanBlock($strBibInputId, $strLangue) {
echo '<div class="inscr-gestion-bib-scan-panel" hidden';
echo ' data-bib-input-id="' . fxInscrGestionEsc($strBibInputId) . '"';
// MSIN-4401 — ScanLabel : jamais de clef brute ni marqueur trad dans data-* / pastille.
echo ' data-msg-scanning="' . fxInscrGestionEsc(fxInscrGestionScanLabel('inscr_gestion_bib_scan_scanning')) . '"';
echo ' data-msg-loading="' . fxInscrGestionEsc(fxInscrGestionScanLabel('inscr_gestion_bib_scan_loading')) . '"';
echo ' data-msg-ready="' . fxInscrGestionEsc(fxInscrGestionScanLabel('inscr_gestion_bib_scan_ready')) . '"';
echo ' data-msg-found="' . fxInscrGestionEsc(fxInscrGestionScanLabel('inscr_gestion_bib_scan_found')) . '"';
echo ' data-msg-found-search="' . fxInscrGestionEsc(fxInscrGestionScanLabel('inscr_gestion_bib_scan_found_search')) . '"';
echo ' data-msg-invalid="' . fxInscrGestionEsc(fxInscrGestionScanLabel('inscr_gestion_bib_scan_invalid')) . '"';
echo ' data-msg-camera="' . fxInscrGestionEsc(fxInscrGestionScanLabel('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>';
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>';
}
/**
* MSIN-4401 — Pastilles des filtres actifs (non vides) pour la barre de resultats.
* @return array liste de libelles deja pret a afficher (non escapes).
*/
function fxInscrGestionActiveFilterLabels($arrReq, $strLangue) {
$tab = array();
$strKv = fxInscrGestionT('inscr_gestion_list_filter_kv');
if ($strKv === 'inscr_gestion_list_filter_kv' || fxInscrGestionIsRawI18nKey($strKv)) {
$strKv = ($strLangue === 'en') ? '%s: %s' : '%s : %s';
}
if ($arrReq['rech_prenom'] !== '') {
$tab[] = sprintf($strKv, fxInscrGestionT('rech_prenom'), $arrReq['rech_prenom']);
}
if ($arrReq['rech_nom'] !== '') {
$tab[] = sprintf($strKv, fxInscrGestionT('rech_nom'), $arrReq['rech_nom']);
}
if ($arrReq['rech_no_commande'] !== '') {
$tab[] = sprintf($strKv, fxInscrGestionT('rech_no_commande'), $arrReq['rech_no_commande']);
}
if ($arrReq['rech_nom_equipe'] !== '') {
$tab[] = sprintf(
$strKv,
fxInscrGestionT('inscr_gestion_rech_bib_equipe'),
$arrReq['rech_nom_equipe']
);
// MSIN-4399 — Chip seulement si hors défaut (partiel).
if (!empty($arrReq['bib_match']) && $arrReq['bib_match'] === 'partial') {
$tab[] = fxInscrGestionT('inscr_gestion_bib_match_partial');
}
}
if ($arrReq['sel_rech_epreuve'] > 0) {
$tab[] = sprintf(
$strKv,
fxInscrGestionT('sel_rech_epreuve'),
fxInscrGestionEpreuveLabel($arrReq['sel_rech_epreuve'], $strLangue)
);
}
if (!empty($arrReq['sel_rech_statut']) && is_array($arrReq['sel_rech_statut'])) {
$tabOptions = fxInscrGestionGetStatutCourseOptions($strLangue);
$tabMap = array();
foreach ($tabOptions as $arrOpt) {
$strCode = trim((string)$arrOpt['info_option2']);
if ($strCode !== '') {
$tabMap[$strCode] = trim((string)$arrOpt['info_texte']);
}
}
$tabStatLabels = array();
foreach ($arrReq['sel_rech_statut'] as $strCode) {
$tabStatLabels[] = isset($tabMap[$strCode]) && $tabMap[$strCode] !== ''
? $tabMap[$strCode]
: $strCode;
}
if (count($tabStatLabels) > 0) {
$tab[] = sprintf(
$strKv,
fxInscrGestionT('inscr_gestion_rech_statut'),
implode(', ', $tabStatLabels)
);
}
}
if ($arrReq['chk_bib']) {
$tab[] = fxInscrGestionT('chk_bib');
}
if ($arrReq['chk_cancelled']) {
$tab[] = fxInscrGestionT('inscr_gestion_chk_cancelled');
}
return $tab;
}
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
: fxInscrGestionListBackUrl($intEveId, $strLangue);
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 inscr-gestion-panel--search"' . ($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)) . '">';
// MSIN-4401 — Grille responsive (1 col mobile, 23 cols bureau) comme la fiche Détails.
echo '<div class="inscr-gestion-form-grid">';
echo '<div class="inscr-gestion-form-field">';
echo '<label class="inscr-gestion-label" for="rech_prenom">' . fxInscrGestionEsc(fxInscrGestionT('rech_prenom')) . '</label>';
echo '<input class="form-control form-control-sm rounded-0" type="text" id="rech_prenom" name="rech_prenom" value="'
. fxInscrGestionEsc($arrReq['rech_prenom']) . '">';
echo '</div>';
echo '<div class="inscr-gestion-form-field">';
echo '<label class="inscr-gestion-label" for="rech_nom">' . fxInscrGestionEsc(fxInscrGestionT('rech_nom')) . '</label>';
echo '<input class="form-control form-control-sm rounded-0" type="text" id="rech_nom" name="rech_nom" value="'
. fxInscrGestionEsc($arrReq['rech_nom']) . '">';
echo '</div>';
echo '<div class="inscr-gestion-form-field">';
echo '<label class="inscr-gestion-label" for="rech_no_commande">' . fxInscrGestionEsc(fxInscrGestionT('rech_no_commande')) . '</label>';
echo '<input class="form-control form-control-sm rounded-0" type="text" id="rech_no_commande" name="rech_no_commande" value="'
. fxInscrGestionEsc($arrReq['rech_no_commande']) . '">';
echo '</div>';
echo '<div class="inscr-gestion-form-field inscr-gestion-form-field--bib">';
echo '<label class="inscr-gestion-label" for="rech_nom_equipe">'
. fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_rech_bib_equipe')) . '</label>';
echo '<div class="inscr-gestion-field-block inscr-gestion-field-block--search-bib" data-inscr-field="bib">';
fxInscrGestionRenderBibScanBlock('rech_nom_equipe', $strLangue);
echo '<div class="inscr-gestion-bib-row">';
echo '<input class="form-control form-control-sm rounded-0" type="text" id="rech_nom_equipe" name="rech_nom_equipe" value="'
. fxInscrGestionEsc($arrReq['rech_nom_equipe']) . '">';
// MSIN-4401 — Exact vs Partiel (entre champ et caméra).
// MSIN-4399 — Exact sélectionné par défaut.
$blnBibExact = (empty($arrReq['bib_match']) || $arrReq['bib_match'] !== 'partial');
echo '<div class="inscr-gestion-bib-match" role="group"'
. ' aria-label="' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_bib_match_aria')) . '">';
echo '<label class="inscr-gestion-bib-match__opt">';
echo '<input type="radio" name="bib_match" value="exact"' . ($blnBibExact ? ' checked' : '') . '>';
echo '<span>' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_bib_match_exact')) . '</span>';
echo '</label>';
echo '<label class="inscr-gestion-bib-match__opt">';
echo '<input type="radio" name="bib_match" value="partial"' . ($blnBibExact ? '' : ' checked') . '>';
echo '<span>' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_bib_match_partial')) . '</span>';
echo '</label>';
echo '</div>';
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></div>';
echo '<div class="inscr-gestion-form-field">';
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></div>';
echo '<div class="inscr-gestion-form-field inscr-gestion-form-field--statut">';
// MSIN-4401 — Filtre multi statut de course (tous si aucune sélection).
fxInscrGestionRenderStatutFilter($arrReq, $strLangue);
echo '</div>';
echo '<div class="inscr-gestion-form-field inscr-gestion-form-field--checks">';
echo '<label class="inscr-gestion-check"><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>';
echo '<div class="inscr-gestion-form-field inscr-gestion-form-field--actions">';
echo '<div class="inscr-gestion-form-actions">';
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></div>';
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">';
echo '<div class="inscr-gestion-list-head__main">';
$strCountKey = ($intNbTotal === 1) ? 'inscr_gestion_list_count_one' : 'inscr_gestion_list_count_many';
echo '<span class="inscr-gestion-list-head__count">'
. fxInscrGestionEsc(sprintf(fxInscrGestionT($strCountKey), $intNbTotal)) . '</span>';
// MSIN-4401 — Rappel des filtres / criteres non vides a cote du compteur.
$tabFilters = fxInscrGestionActiveFilterLabels($arrReq, $strLangue);
if (count($tabFilters) > 0) {
echo '<span class="inscr-gestion-list-head__filters">';
foreach ($tabFilters as $strFilt) {
echo '<span class="inscr-gestion-list-head__chip">' . fxInscrGestionEsc($strFilt) . '</span>';
}
echo '</span>';
}
echo '</div>';
if ($intNbTotal > $intNbItemsParPage) {
$intNbPages = (int)ceil($intNbTotal / $intNbItemsParPage);
echo '<span class="inscr-gestion-list-head__pg">'
. fxInscrGestionEsc(sprintf(fxInscrGestionT('inscr_gestion_list_page'), $intPage, $intNbPages))
. '</span>';
}
echo '</div>';
if ($intNbTotal > 0) {
// MSIN-4448 — Edition inline dossard / remis selon droits granulaires (sans camera).
$intComId = intval($_SESSION['com_id'] ?? 0);
$blnCanBib = fxInscrGestionCanDo($intComId, $intEveId, 'inscriptions_gestion.bib_edit');
$blnCanRemis = fxInscrGestionCanDo($intComId, $intEveId, 'inscriptions_gestion.bib_remis');
$strListClass = 'inscr-gestion-list';
if ($blnCanRemis) {
$strListClass .= ' inscr-gestion-list--can-edit-remis';
}
echo '<div class="' . $strListClass . '">';
// En-tete de colonnes (memes colonnes que les lignes ; masque en vue cellulaire).
// aria-hidden : purement visuel — libellés Info (pas de FR/EN en dur).
echo '<div class="inscr-gestion-list-cols" aria-hidden="true">';
echo '<span class="inscr-gestion-list-cols__name">'
. fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_col_participant')) . '</span>';
echo '<span class="inscr-gestion-list-cols__race">'
. fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_epreuve')) . '</span>';
echo '<span class="inscr-gestion-list-cols__order">'
. fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_col_commande')) . '</span>';
echo '<span class="inscr-gestion-list-cols__bib">'
. fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_col_dossard')) . '</span>';
// MSIN-4399 — Statut de course entre dossard et remis.
echo '<span class="inscr-gestion-list-cols__statut">'
. fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_col_statut')) . '</span>';
echo '<span class="inscr-gestion-list-cols__check">'
. fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_col_remis')) . '</span>';
echo '<span class="inscr-gestion-list-cols__arrow"></span>';
echo '</div>';
$strRemisLabel = fxInscrGestionT('promoteur_bib_enregistre');
$strBibOkLabel = fxInscrGestionT('inscr_gestion_bib_ok');
$strTeammateLabel = fxInscrGestionT('inscr_gestion_list_teammate');
// MSIN-4459 — Membres d'équipe de la page courante (1 requête batch).
$arrTeamPecs = array();
for ($jTeam = $intStart; $jTeam <= $intEnd; $jTeam++) {
if (!empty($tabRows[$jTeam]) && intval($tabRows[$jTeam]['pec_equipe']) === 1) {
$arrTeamPecs[] = (int)$tabRows[$jTeam]['pec_id_original'];
}
}
$tabTeamMembersByPec = fxInscrGestionFetchTeamMembersByPecs($arrTeamPecs);
for ($j = $intStart; $j <= $intEnd; $j++) {
$row = $tabRows[$j];
$strFicheUrl = fxInscrGestionBuildUrl($strBaseUrl, $intEveId, $arrReq, array(
'pec_id' => (int)$row['pec_id_original'],
'pg' => $intPage,
));
$strFicheHref = fxInscrGestionEsc($strFicheUrl);
$blnEquipe = (intval($row['pec_equipe']) === 1);
$intPecId = (int)$row['pec_id_original'];
$tabMates = array();
$strTeamName = '';
// MSIN-4459 — Ligne parente = capitaine ; coéquipiers en sous-liste.
if ($blnEquipe) {
$strTeamName = trim(fxUnescape(isset($row['pec_nom_equipe']) ? $row['pec_nom_equipe'] : ''));
$tabMembers = isset($tabTeamMembersByPec[$intPecId]) ? $tabTeamMembersByPec[$intPecId] : array();
$arrCaptain = null;
foreach ($tabMembers as $arrMem) {
if ((int)$arrMem['rol_id'] === 1 && $arrCaptain === null) {
$arrCaptain = $arrMem;
} else {
$tabMates[] = $arrMem;
}
}
if ($arrCaptain !== null) {
$row['par_nom'] = $arrCaptain['par_nom'];
$row['par_prenom'] = $arrCaptain['par_prenom'];
$row['par_id'] = $arrCaptain['par_id'];
$row['no_bib_remis'] = $arrCaptain['no_bib_remis'];
$row['par_statut_course'] = $arrCaptain['par_statut_course'];
$row['rol_id'] = 1;
} else {
// Pas de capitaine : garder la ligne GROUP BY ; exclure ce par_id des mates.
$intMainPar = (int)$row['par_id'];
$tabMates = array();
foreach ($tabMembers as $arrMem) {
if ((int)$arrMem['par_id'] !== $intMainPar) {
$tabMates[] = $arrMem;
}
}
}
}
$strBib = fxInscrGestionBibDisplay($row);
$strName = fxUnescape($row['par_nom']) . ', ' . fxUnescape($row['par_prenom']);
$strRace = fxInscrGestionEpreuveLabel($row['epr_id'], $strLangue);
$strStatutLabel = fxInscrGestionStatutCourseLabel(
isset($row['par_statut_course']) ? $row['par_statut_course'] : '',
$strLangue
);
$strStatutCode = fxInscrGestionResolveParStatutCourse(
isset($row['par_statut_course']) ? $row['par_statut_course'] : ''
);
$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--cancellation inscr-gestion-list-item--' . $strCause;
$strCancelBadge = ' <span class="inscr-gestion-cancel-badge inscr-gestion-cancel-badge--' . $strCause . '">'
. fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_statut_' . $strCause)) . '</span>';
}
$blnRemis = (intval($row['no_bib_remis']) === 1);
$intParId = (int)$row['par_id'];
// MSIN-4448 — Ligne en div : cellules nav (lien) vs dossard/remis editables.
echo '<div class="' . $strCardClass . '">';
// MSIN-4459 — Bloc nom : capitaine + nom d'équipe + coéquipiers.
echo '<div class="inscr-gestion-list-item__who">';
echo '<a class="inscr-gestion-list-item__name" href="' . $strFicheHref . '">'
. fxInscrGestionEsc($strName) . $strCancelBadge . '</a>';
if ($blnEquipe && $strTeamName !== '') {
echo '<span class="inscr-gestion-list-item__team">'
. fxInscrGestionEsc(sprintf(fxInscrGestionT('inscr_gestion_list_team_name'), $strTeamName))
. '</span>';
}
if ($blnEquipe && count($tabMates) > 0) {
echo '<ul class="inscr-gestion-list-mates">';
foreach ($tabMates as $arrMate) {
$strMateName = fxUnescape($arrMate['par_nom']) . ', ' . fxUnescape($arrMate['par_prenom']);
echo '<li class="inscr-gestion-list-mates__item">';
echo '<a class="inscr-gestion-list-mates__link" href="' . $strFicheHref . '">';
echo '<span class="inscr-gestion-list-mates__role">'
. fxInscrGestionEsc($strTeammateLabel) . '</span> ';
echo fxInscrGestionEsc($strMateName);
echo '</a></li>';
}
echo '</ul>';
}
echo '</div>';
echo '<a class="inscr-gestion-list-item__race" href="' . $strFicheHref . '" title="'
. fxInscrGestionEsc($strRace) . '">' . fxInscrGestionEsc($strRace) . '</a>';
echo '<a class="inscr-gestion-list-item__order" href="' . $strFicheHref . '">'
. fxInscrGestionEsc($row['no_commande']) . '</a>';
echo '<span class="inscr-gestion-list-item__bib">';
if ($blnCanBib) {
// MSIN-4448 — Pastille par defaut ; clic → champ + OK (sans camera).
$strBibAction = $blnEquipe ? 'no_equipe' : 'no_bib';
$strBibInputId = ($blnEquipe ? 'txt_no_bib_e' : 'txt_no_bib_p') . $intParId;
$blnBibEmpty = (trim($strBib) === '');
$strPillClass = 'inscr-gestion-bib-pill inscr-gestion-list-bib-pill';
if ($blnBibEmpty) {
$strPillClass .= ' inscr-gestion-bib-pill--empty';
}
$strPillText = $blnBibEmpty ? '—' : $strBib;
echo '<div class="inscr-gestion-list-bib-wrap">';
echo '<button type="button" class="' . $strPillClass . '"'
. ' title="' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_col_dossard')) . '"'
. ' aria-label="' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_col_dossard')) . '">'
. fxInscrGestionEsc($strPillText) . '</button>';
echo '<div class="inscr-gestion-list-bib-edit" hidden>';
echo '<input class="form-control form-control-sm rounded-0 inscr-gestion-list-bib-input" type="text"'
. ' id="' . fxInscrGestionEsc($strBibInputId) . '"'
. ' value="' . fxInscrGestionEsc($strBib) . '"'
. ' maxlength="6" autocomplete="off"'
. ' aria-label="' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_col_dossard')) . '">';
echo '<button class="btn btn-primary btn-sm rounded-0 btn_bib" type="button"'
. ' data-action="' . fxInscrGestionEsc($strBibAction) . '"'
. ' data-eve_id="' . rawurlencode(base64_encode((string)$row['eve_id'])) . '"'
. ' data-epr_id="' . (int)$row['epr_id'] . '"'
. ' data-par_id="' . $intParId . '"'
. ' data-pec_id="' . (int)$row['pec_id_original'] . '"'
. ' title="' . fxInscrGestionEsc($strBibOkLabel) . '"'
. ' aria-label="' . fxInscrGestionEsc($strBibOkLabel) . '">OK</button>';
echo '</div></div>';
} elseif (trim($strBib) !== '') {
echo '<span class="inscr-gestion-bib-pill">' . fxInscrGestionEsc($strBib) . '</span>';
} else {
echo '<span class="inscr-gestion-bib-pill inscr-gestion-bib-pill--empty">—</span>';
}
echo '</span>';
// MSIN-4399 — Statut de course en lecture seule dans la liste (edition sur la fiche).
$strStatutMod = preg_replace('/[^A-Za-z0-9_-]/', '', strtolower($strStatutCode));
echo '<a class="inscr-gestion-list-item__statut" href="' . $strFicheHref . '">';
echo '<span class="inscr-gestion-statut-pill inscr-gestion-statut-pill--' . fxInscrGestionEsc($strStatutMod) . '"'
. ' title="' . fxInscrGestionEsc($strStatutLabel) . '">'
. fxInscrGestionEsc($strStatutLabel) . '</span>';
echo '</a>';
echo '<span class="inscr-gestion-list-item__check">';
if ($blnCanRemis) {
$intRemisVal = $blnRemis ? 0 : 1;
$strRemisMod = $blnRemis ? 'yes' : 'no';
echo '<button type="button" class="inscr-gestion-remis inscr-gestion-remis--'
. $strRemisMod . ' inscr-gestion-list-remis"'
. ' data-id="' . $intParId . '"'
. ' data-valeur="' . $intRemisVal . '"'
. ' title="' . fxInscrGestionEsc($strRemisLabel) . '"'
. ' aria-label="' . fxInscrGestionEsc($strRemisLabel) . '"'
. ' aria-pressed="' . ($blnRemis ? 'true' : 'false') . '">';
if ($blnRemis) {
echo '<i class="fa fa-check-circle" aria-hidden="true"></i>';
}
echo '</button>';
} elseif ($blnRemis) {
echo '<span class="inscr-gestion-remis inscr-gestion-remis--yes" title="'
. fxInscrGestionEsc($strRemisLabel) . '">';
echo '<i class="fa fa-check-circle" aria-hidden="true"></i>';
echo '<span class="sr-only">' . fxInscrGestionEsc($strRemisLabel) . '</span>';
echo '</span>';
} else {
echo '<span class="inscr-gestion-remis inscr-gestion-remis--no" aria-hidden="true"></span>';
}
echo '</span>';
echo '<a class="inscr-gestion-list-item__arrow" href="' . $strFicheHref . '"'
. ' aria-label="' . fxInscrGestionEsc($strName) . '">'
. '<i class="fa fa-chevron-right" aria-hidden="true"></i></a>';
echo '</div>';
}
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');
// MSIN-4401 — granularite facture / transaction / renvoi (plus d'acces implicite sans droit).
$blnCanInvoice = fxInscrGestionCanDo($intComId, $intRowEveId, 'inscriptions_gestion.invoice');
$blnCanTransaction = fxInscrGestionCanDo($intComId, $intRowEveId, 'inscriptions_gestion.transaction');
$blnCanRenvoi = fxInscrGestionCanDo($intComId, $intRowEveId, 'inscriptions_gestion.renvoi');
// MSIN-4464 — historique ( ! ) sur champs whitelist avec au moins 1 log.
$blnCanAudit = fxInscrGestionCanDo($intComId, $intRowEveId, 'inscriptions_gestion.audit_view');
$arrAuditFieldsWithLogs = array();
if ($blnCanAudit) {
if (!function_exists('fxFicheAuditFieldsWithLogsForPar')) {
require_once dirname(__FILE__) . '/inc_fx_fiche_audit.php';
}
$arrAuditFieldsWithLogs = fxFicheAuditFieldsWithLogsForPar(intval($arrRow['par_id']));
}
$blnHasAuditEditable = $blnCanAudit && (
!empty($arrAuditFieldsWithLogs['no_bib'])
|| !empty($arrAuditFieldsWithLogs['par_statut_course'])
|| !empty($arrAuditFieldsWithLogs['no_bib_remis'])
);
$blnHasEditable = ($blnCanStatut || $blnCanBib || $blnCanRemis || $blnHasAuditEditable);
$blnHasGestion = ($blnCanEdit || $blnCanCancel || $blnCanRestore || $blnCanRefund
|| $blnCanInvoice || $blnCanTransaction || $blnCanRenvoi
|| ($blnCanAudit && !empty($arrAuditFieldsWithLogs['is_cancelled'])));
$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>';
// MSIN-4328 — lien direct ChronoTrack (superadmin + entry_id connu)
if (!empty($_SESSION['usa_id'])) {
$strCtEntryId = trim((string)($arrRow['ct_entry_id'] ?? ''));
if ($strCtEntryId !== '') {
if (!defined('MSIN_API_CHRONOTRACK_LOADED')) {
require_once __DIR__ . '/chronotrack_api/inc_bootstrap.php';
}
$arrCtCfg = fxChronotrackApiConfigGet(intval($arrRow['eve_id'] ?? $intEveId));
$intCtEventIdLink = intval($arrCtCfg['ct_event_id'] ?? 0);
$strCtUrl = fxChronotrackApiAdminEntryUrl($intCtEventIdLink, $strCtEntryId);
if ($strCtUrl !== '') {
echo ' <a class="btn btn-outline-secondary btn-sm inscr-gestion-btn-chronotrack" href="'
. fxInscrGestionEsc($strCtUrl) . '" target="_blank" rel="noopener noreferrer">'
. '<i class="fa fa-external-link" aria-hidden="true"></i>&nbsp;'
. fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_btn_chronotrack'))
. '</a>';
}
}
}
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']);
$strCancelVal = '<span class="inscr-gestion-cancel-badge inscr-gestion-cancel-badge--' . $strCause . '">'
. fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_statut_' . $strCause)) . '</span>';
if ($blnCanAudit && !empty($arrAuditFieldsWithLogs['is_cancelled'])) {
ob_start();
fxInscrGestionRenderAuditBang($intParId, 'is_cancelled', $blnCanAudit, $arrAuditFieldsWithLogs);
$strCancelVal .= ob_get_clean();
}
fxInscrGestionRenderKvRow(
fxInscrGestionT('inscr_gestion_annulation_titre'),
$strCancelVal,
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">';
// MSIN-4401 — Chaque action = wrap [controle][cadenas a droite] (evite badge orphelin a gauche).
if ($blnCanInvoice) {
fxInscrGestionActionItemOpen();
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>';
fxInscrGestionPermInspectEcho('inscriptions_gestion.invoice');
fxInscrGestionActionItemClose();
} elseif ($blnInspect) {
fxInscrGestionActionItemOpen('inscr-gestion-action-item--inspect-only');
fxInscrGestionPermInspectEchoHidden('inscriptions_gestion.invoice', fxInscrGestionT('inscr_gestion_show_invoice'));
fxInscrGestionActionItemClose();
}
if ($blnCanTransaction) {
fxInscrGestionActionItemOpen();
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>';
fxInscrGestionPermInspectEcho('inscriptions_gestion.transaction');
fxInscrGestionActionItemClose();
} elseif ($blnInspect) {
fxInscrGestionActionItemOpen('inscr-gestion-action-item--inspect-only');
fxInscrGestionPermInspectEchoHidden('inscriptions_gestion.transaction', fxInscrGestionT('inscr_gestion_show_transaction'));
fxInscrGestionActionItemClose();
}
if ($blnCanEdit) {
// d-none sur le wrap entier (pas seulement le bouton) pour ne pas laisser le cadenas orphelin.
fxInscrGestionActionItemOpen(trim($strStyleEdit));
echo '<button class="btn btn-primary btn-sm rounded-0 btn_edit_event_promo" 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');
fxInscrGestionActionItemClose();
} elseif ($blnInspect) {
fxInscrGestionActionItemOpen('inscr-gestion-action-item--inspect-only');
fxInscrGestionPermInspectEchoHidden('inscriptions_gestion.edit', fxInscrGestionT('inscr_gestion_modifier_participant'));
fxInscrGestionActionItemClose();
}
if ($blnCanRefund) {
fxInscrGestionActionItemOpen();
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');
fxInscrGestionActionItemClose();
} elseif ($blnInspect) {
fxInscrGestionActionItemOpen('inscr-gestion-action-item--inspect-only');
fxInscrGestionPermInspectEchoHidden('inscriptions_gestion.refund', fxInscrGestionT('inscr_gestion_remboursement'));
fxInscrGestionActionItemClose();
}
if ($blnCanCancel) {
fxInscrGestionActionItemOpen(trim($strStyleCancel));
echo '<button class="btn btn-secondary btn-sm rounded-0 btn_cancel_event_promo" 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');
fxInscrGestionActionItemClose();
} elseif ($blnInspect) {
fxInscrGestionActionItemOpen('inscr-gestion-action-item--inspect-only');
fxInscrGestionPermInspectEchoHidden('inscriptions_gestion.cancel', fxInscrGestionT('btn_annulerinscriptions'));
fxInscrGestionActionItemClose();
}
if ($blnCanRestore) {
fxInscrGestionActionItemOpen(trim($strStyleRetablir));
echo '<button class="btn btn-secondary btn-sm rounded-0 btn_retablir_event_promo" 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');
fxInscrGestionActionItemClose();
} elseif ($blnInspect) {
fxInscrGestionActionItemOpen('inscr-gestion-action-item--inspect-only');
fxInscrGestionPermInspectEchoHidden('inscriptions_gestion.restore', fxInscrGestionT('btn_retablirinscriptions'));
fxInscrGestionActionItemClose();
}
// MSIN-4464 — historique annulation si pas deja affiche a cote du badge (fiche active).
if ($blnCanAudit && !empty($arrAuditFieldsWithLogs['is_cancelled']) && !$blnCancelled) {
fxInscrGestionActionItemOpen();
echo '<span class="inscr-gestion-audit-inline">'
. fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_audit_field_is_cancelled'));
fxInscrGestionRenderAuditBang($intParId, 'is_cancelled', $blnCanAudit, $arrAuditFieldsWithLogs);
echo '</span>';
fxInscrGestionActionItemClose();
}
if ($blnInspect) {
fxInscrGestionPermInspectEcho('inscriptions_gestion.audit_view');
}
if ($blnCanRenvoi) {
fxInscrGestionActionItemOpen();
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>';
fxInscrGestionPermInspectEcho('inscriptions_gestion.renvoi');
fxInscrGestionActionItemClose();
} elseif ($blnInspect) {
fxInscrGestionActionItemOpen('inscr-gestion-action-item--inspect-only');
fxInscrGestionPermInspectEchoHidden('inscriptions_gestion.renvoi', $strRenvoi);
fxInscrGestionActionItemClose();
}
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).
if ($blnCanTransaction) {
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'));
fxInscrGestionRenderAuditBang($intParId, 'no_bib', $blnCanAudit, $arrAuditFieldsWithLogs);
echo '</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) . '">';
// MSIN-4430 — OK avant camera (action principale a gauche du scan).
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>';
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>';
}
fxInscrGestionPermInspectEcho('inscriptions_gestion.bib_edit');
echo '</div>';
echo '</div>';
} elseif ($blnCanAudit && !empty($arrAuditFieldsWithLogs['no_bib'])) {
echo '<div class="inscr-gestion-field-block" data-inscr-field="bib">';
echo '<div class="inscr-gestion-field-block__label">' . fxInscrGestionEsc(fxInscrGestionT('promoteur_bib_number'));
fxInscrGestionRenderAuditBang($intParId, 'no_bib', $blnCanAudit, $arrAuditFieldsWithLogs);
echo '</div>';
echo '<div class="inscr-gestion-muted">' . fxInscrGestionEsc($strBib) . '</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'));
fxInscrGestionRenderAuditBang($intParId, 'par_statut_course', $blnCanAudit, $arrAuditFieldsWithLogs);
echo '</div>';
fxInscrGestionRenderStatutField($intParId, $arrRow['par_statut_course'] ?? '', $arrRow['eve_id'], $strLangue);
fxInscrGestionPermInspectEcho('inscriptions_gestion.statut_edit');
echo '</div>';
} elseif ($blnCanAudit && !empty($arrAuditFieldsWithLogs['par_statut_course'])) {
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'));
fxInscrGestionRenderAuditBang($intParId, 'par_statut_course', $blnCanAudit, $arrAuditFieldsWithLogs);
echo '</div>';
$strStatutDisp = trim((string)($arrRow['par_statut_course'] ?? ''));
echo '<div class="inscr-gestion-muted">' . fxInscrGestionEsc($strStatutDisp !== '' ? $strStatutDisp : '—') . '</div>';
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'));
fxInscrGestionRenderAuditBang($intParId, 'no_bib_remis', $blnCanAudit, $arrAuditFieldsWithLogs);
echo '</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 ($blnCanAudit && !empty($arrAuditFieldsWithLogs['no_bib_remis'])) {
echo '<div class="inscr-gestion-field-block" data-inscr-field="remis">';
echo '<div class="inscr-gestion-field-block__label">'
. fxInscrGestionEsc(fxInscrGestionT('promoteur_bib_enregistre'));
fxInscrGestionRenderAuditBang($intParId, 'no_bib_remis', $blnCanAudit, $arrAuditFieldsWithLogs);
echo '</div>';
echo '</div>';
} 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);
}