25 Commits

Author SHA1 Message Date
aee3467918 Refactor dossard PDF generation to standardize layout for 1 participant. Update page size to 8.5x5.5" and adjust positioning of text and QR code. Enhance template resolution logic and improve comments for clarity. Increment version code. 2026-07-24 20:09:56 -04:00
087ef47e15 Refactor dossard layout and background placement for PDF generation. Update dimensions to fit 5.5x8.5" bibs on Letter-sized pages, ensuring proper centering and scaling of background images. Adjust QR code and text positioning to align with new layout parameters. Increment version code. 2026-07-24 19:36:09 -04:00
aabb419e7d Add default and custom dossard templates in JPG format. Update image path resolution to include PNG options and enhance image type detection for PDF generation. Increment version code. 2026-07-24 19:33:05 -04:00
3a7c1f4bb7 Replace default PNG templates with JPG versions and add support for image type detection in PDF generation. Update template resolution logic to include new image formats. Increment version code. 2026-07-24 19:29:49 -04:00
36eb81d8e8 Enhance permission sorting in fxEveAccesShowCompteForm to prioritize 'hors kit total' entries and improve display of action permissions. Adjust select element width for better UI presentation. Increment version code. 2026-07-24 19:27:12 -04:00
d4ab2e2dc7 Refactor permission checks in fxEveAccesHasPermission and fxEveAccesHasAnyPermission functions to enhance logic for 'perm_grants_all' propagation. Update comments for clarity regarding key propagation rules. Increment version code. 2026-07-24 19:23:34 -04:00
8cd023e224 Add functionality to print bibs in PDF format for event management. Introduce new translation keys and implement print button logic in the event details view. Increment version code. 2026-07-24 19:17:30 -04:00
8788f22a81 Add functions to handle 'perm_grants_all' column and update permission checks for event kits. Refactor related logic to respect new permission propagation rules. Increment version code. 2026-07-24 19:10:19 -04:00
fd7930b139 Update SQL for 'Sans inscription en ligne' event type to include icon and ensure correct updates. Add logic to set icon if missing and adjust event attachment query for flexibility. Increment version code. 2026-07-24 17:57:08 -04:00
a68f54be80 Refactor fxImportResultatsSaveQuestionAnswer to ensure que_actif is set correctly during question updates and insertions. Simplify SQL logic for handling pec_id and improve error handling for database queries in fxInscrGestionFetchFicheProfile. Increment version code. 2026-07-24 17:53:03 -04:00
038bd7bd26 Implement fxImportResultatsEnsureQuestionsVisibleFiche function to activate questions during import for MSIN-4482. Update SQL queries to manage visibility of questions based on event type and ensure correct filtering of pec_id. Adjust logic in fxInscrGestionFetchFicheProfile to accommodate non-online registration scenarios. Increment version code. 2026-07-24 17:49:14 -04:00
69a97c0044 Enhance import functionality for events without online registration in MSIN-4482. Introduce new SQL queries and functions to manage event types and guard against invalid imports. Update AJAX handling to enforce type restrictions and improve user feedback in the event panel. Increment version code. 2026-07-24 17:45:47 -04:00
26a470fe1e Refactor fxImportResultatsCommit to improve handling of pec_id and pec_id_original during imports for MSIN-4482. Update SQL queries to ensure pec_actif is set correctly and prevent collisions with unique constraints. Enhance logic for updating participant records based on active status and original IDs. Increment version code. 2026-07-24 17:31:48 -04:00
80c791c7e1 Enhance fxImportResultatsCommit and fxImportResultatsReset functions for MSIN-4482 by updating SQL queries to ensure correct handling of pec_id_original and pec_actif flags during imports. Refactor participant deletion logic to improve performance using subqueries. Add diagnostic SQL for verifying import integrity. Increment version code. 2026-07-24 17:20:19 -04:00
05ce600e09 Refactor fxImportResultatsResetPreview to improve SQL query readability and performance by storing the escaped origin in a variable. Add diagnostic SQL query to count visible participants in the list based on active PEC status. Enhance the event panel UI to display database status with refresh functionality. Increment version code. 2026-07-24 17:06:46 -04:00
c6477f7757 Enhance result import functionality for MSIN-4482 by adding reset and preview capabilities. Implement fxImportResultatsResetPreview to count potential deletions and fxImportResultatsReset to remove imported data based on confirmation. Update AJAX handling in ajax_import_resultats.php to support these new actions. Ensure pec_actif is set to 1 for active entries in the import process. Increment version code. 2026-07-24 17:02:55 -04:00
8498af24f9 Implement fxImportResultatsGroupBlockedByType function to categorize and handle blocked import rows in fxImportResultats for MSIN-4482. Enhance error reporting by adding detailed messages for header rows, missing unique identifiers, unmapped races, and database update failures. Update the commit function to utilize the new error handling structure. Increment version code. 2026-07-24 16:26:45 -04:00
171a48a411 Add temporary unique ID generation for pec_id_original and par_id_original in fxImportResultats for MSIN-4482. Implement fxImportResultatsTempOriginalId function to ensure non-zero unique values during insertion. Update SQL queries to utilize generated IDs. Increment version code. 2026-07-24 16:22:28 -04:00
d63c92e36a Implement header row filtering and unique identifier generation in fxImportResultats for MSIN-4482. Add functions to detect header-like rows and create unique panier/commande identifiers. Enhance error handling during database operations. Increment version code. 2026-07-24 16:19:54 -04:00
8929236e85 Enhance participant import functionality in MSIN-4482 by updating the import phases and adding new fields for emergency contact details. Implement commit action in AJAX handling for result mapping validation. Increment version code. 2026-07-24 16:16:00 -04:00
84e65340b6 Enhance fxImportResultatsRenderEventPanel and ajax_import_resultats.php for improved AJAX handling in MSIN-4482. Update AJAX URL construction to include domain, add error handling for JSON parsing, and ensure panel visibility. Implement output buffering to prevent HTML pollution in JSON responses. Increment version code. 2026-07-24 16:09:37 -04:00
a1b6fa8911 Refactor fxImportResultatsComIdForEve to use org_id instead of com_id for event sharing templates in MSIN-4482. Update SQL queries and comments for clarity. Ensure compatibility with legacy data structures. Increment version code. 2026-07-24 15:44:25 -04:00
1ca75a4ed4 Implement UI enhancements for participant import without transaction in MSIN-4482. Add new styles for import dropzone and integrate import functionality into event management panel. Update navigation for improved access to import features. Increment version code. 2026-07-24 15:39:50 -04:00
db7b2e9f46 Add new project rules for MSIN-4482 — Implement import and quick entry for ChronoTrack without online registration. Document decisions, phases, and scope for development clarity. Increment version code. 2026-07-24 15:27:50 -04:00
b67fdaee78 Remove obsolete inbox telephone rules file from project directory, streamlining the codebase and eliminating unnecessary documentation. Increment version code. 2026-07-24 15:27:25 -04:00
24 changed files with 3927 additions and 57 deletions

View File

@ -0,0 +1,102 @@
---
description: Projet MS1 hors transaction — import / saisie rapide / ChronoTrack (cadrage produit)
alwaysApply: true
---
# Projet MSIN-4482 — entrée hors transaction (import + terrain)
JIRA : **MSIN-4482**.
Orthographe produit : **ChronoTrack**.
Doc de cadrage — source de vérité pour le développement de ce chantier.
## Intention
Utiliser la suite MS1 (gestion, ChronoTrack, outils promoteur existants) pour des clients **sans** tunnel dinscription / paiement en ligne MS1.
Les participants arrivent surtout par **import fichier** (puis saisie rapide plus tard) ; ensuite sync ChronoTrack.
Aussi utile plus tard le **jour J** sur une course **avec** inscription en ligne : correction / ajout rapide **sans** transaction financière.
## Décisions actées
| Sujet | Décision |
|---|---|
| Source terrain | Toujours **`resultats_*`** — **pas de panier / `no_panier`** pour ces lignes |
| Type événement | **`inscriptions_types_evenements`** — libellé FR **Sans inscription en ligne** / EN **No online registration** (`sql/MSIN-4482-type-sans-inscription-en-ligne.sql`) ; lévénement pointe via `inscriptions_evenements.te_id` |
| Paiement MS1 | Hors scope ; **interdit** le « panier à 0 $ » |
| Clé unique | ID du fichier client = **`external_id` ChronoTrack** au push ; même fichier = fallback import CT |
| Questions import | **Option C** : matcher ; non matchées → preview + **création confirmée** |
| Hub promoteur | **Ne pas toucher pour linstant** — hub actuel tel quel |
| Qui est promoteur | Opérateur MS1 se met **lui-même** promoteur ; droits clients = kits **minimisés via lexistant** |
| Mini-hub | **Plus tard**, au fur et à mesure — pas un livrable immédiat |
| Waiver | **Hors scope** |
| Édition / saisie rapide | Après limport (phase suivante) |
| Finances | Marqueur d**origine** ; import / saisie rapide exclus des rapports paiement |
## Origines participant (marqueur)
| Code | Canal |
|---|---|
| `inscription_en_ligne` | Flux payant actuel |
| `import_fichier` | Bulk modèle fichier |
| `saisie_rapide` | Création unitaire / jour J (plus tard) |
## Phases (réalignées)
1. **UI import superadm** — dropzone, mapping colonnes + épreuves, templates, bouton **Importer**
2. **Écriture `resultats_*`** — upsert clé unique + questions C (sans panier) — en place
3. **Mapping pays** (valeurs fichier → `pay_id`) — **étape 2**
4. **ChronoTrack** — push avec `par_external_id`
5. **Saisie / édition** — fiche rapide (+ jour J)
6. **Hub minimisé** — seulement si/quand nécessaire
## UI import (décisions)
- Emplacement v1 : **superadm fiche événement** (panneau sous ChronoTrack ; ancre « Import »)
- Colonne fichier pour lépreuve → match vers `epr_id` de lévénement
- UX : dropzone ludique + mapping guidé
- Template mapping : table `inscriptions_import_templates` (empreinte en-têtes + eve/com)
## Exemple fichier client (référence)
Fichier type : export ~Échappée (en-têtes déjà normalisés ; loriginal client peut différer).
| Colonne exemple | Cible MS1 |
|---|---|
| Attendee # | `par_external_id` (clé unique / CT) |
| First Name / Last Name | `par_prenom` / `par_nom` |
| Ticket Type | `epr` (valeurs → épreuves MS1) |
| No dossard | `no_bib` |
| Gender (Male/Female/Other) | `par_sexe` (normaliser à lécriture) |
| Age / Birth Date | `par_age` / `par_naissance` |
| Nationalité, urgence, allergies, condition médicale, langue… | questions (option C) |
**Visibilité liste / CT** : les coquilles `resultats_epreuves_commandees` doivent avoir **`pec_actif = 1`** (sinon absentes de la liste inscriptions et du sync). Les `I{eve}X…` = faux `no_commande` technique, pas un vrai panier.
~1800 lignes, ~1012 valeurs distinctes de Ticket Type (distances / jours).
## Fichiers
| Rôle | Fichier |
|---|---|
| SQL templates | `sql/MSIN-4482-import-resultats-templates.sql` |
| SQL type eve | `sql/MSIN-4482-type-sans-inscription-en-ligne.sql` |
| SQL fix liste | `sql/MSIN-4482-fix-pec-actif-import.sql` |
| Logique + UI | `php/inc_fx_import_resultats.php` |
| AJAX | `superadm/ajax_import_resultats.php` |
| Branche fiche eve | `superadm/php/inc_form.php` |
| CSS | `superadm/css/style_blue.css` |
## Hors scope (maintenant)
- Hub promoteur / mini-hub
- Waiver
- Écriture `resultats_*` (prochaine étape après UI)
- Import inverse CT → MS1
## Règles agent
- Changement minimal ; référencer **MSIN-4482**.
- **Pas de panier** pour ces participants — écrire dans `resultats_*`.
- **Ne pas modifier le hub promoteur** tant que ce nest pas redemandé.
- Ne pas inventer de waiver pour « signature de dossier ».
- SQL : **dev préprod** seulement, puis Navicat + sync outil.

View File

@ -1,24 +0,0 @@
---
description: Inbox Drive — demandes préparées sur le téléphone (.cursor/inbox)
alwaysApply: true
---
# Inbox téléphone → Cursor
## Emplacement
- Chemin projet : `.cursor/inbox/` (junction Windows vers Google Drive).
- Dossier Drive : `I:\Mon disque\inbox-cursor-ms1inscription` (compte syncé sur `I:`).
- Contenu **gitignoré** — notes de travail, pas du code.
## Quand lutilisateur dit « traite linbox », « nouvelles demandes », « ce que jai préparé », etc.
1. Lister les fichiers `.md` dans `.cursor/inbox/` **sauf** `_TEMPLATE.md`, `README.md`, et tout ce qui est sous `done/`.
2. Présenter un résumé court de chaque demande (titre + objectif).
3. Demander laquelle traiter (ou traiter la plus récente si une seule / si lutilisateur le dit).
4. Lire le fichier choisi comme brief de tâche ; demander le **JIRA** sil manque avant de modifier le code.
5. Après traitement réussi : proposer de déplacer le fichier vers `.cursor/inbox/done/`.
## Hors de ces phrases
Ne pas scanner linbox à chaque message. Les fichiers sont visibles dans lexplorateur Cursor.

3
.gitignore vendored
View File

@ -13,9 +13,6 @@ dist/
# IDE — ne jamais committer les connexions SQL (copier settings.json.example)
.vscode/settings.json
# Inbox téléphone (junction → Google Drive) — briefs locaux, pas du code
.cursor/inbox/
# Compiled Java class files
*.class

View File

@ -0,0 +1 @@

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

49
dossard_print.php Normal file
View File

@ -0,0 +1,49 @@
<?php
/**
* MSIN-4485 — Endpoint PDF dossard (1 participant).
* Isolé du tunnel / panier ; droit inscriptions_gestion.dossard_print.
*/
session_start();
$_SERVER['PHP_SELF'] = '/compte.php';
$_SERVER['SCRIPT_NAME'] = '/compte.php';
require_once __DIR__ . '/php/inc_fonctions.php';
require_once __DIR__ . '/php/inc_fx_dossard_print.php';
$strLangue = $_SESSION['lang'] ?? 'fr';
$reqLang = $_REQUEST['lang'] ?? $_REQUEST['lng'] ?? '';
if (!empty($reqLang) && in_array($reqLang, array('fr', 'en'), true)) {
$strLangue = $reqLang;
}
$vPage = 'compte.php';
$vtexte_page = obtenirTextepage($vPage, $strLangue, 2);
$intParId = intval($_GET['par_id'] ?? $_POST['par_id'] ?? 0);
$intEveId = intval($_GET['eve_id'] ?? $_POST['eve_id'] ?? 0);
$intComId = intval($_SESSION['com_id'] ?? 0);
if ($intComId <= 0 || $intParId <= 0 || $intEveId <= 0) {
http_response_code(403);
header('Content-Type: text/plain; charset=utf-8');
echo 'Accès refusé.';
exit;
}
if (!fxDossardPrintCan($intComId, $intEveId)) {
http_response_code(403);
header('Content-Type: text/plain; charset=utf-8');
echo 'Droit insuffisant.';
exit;
}
$arrRow = fxDossardPrintLoadParticipant($intParId, $intEveId);
if ($arrRow === null) {
http_response_code(404);
header('Content-Type: text/plain; charset=utf-8');
echo 'Participant introuvable.';
exit;
}
fxDossardPrintOutputPdf($arrRow);

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 KiB

View File

@ -0,0 +1,302 @@
<?php
/**
* MSIN-4485 — Impression dossard PDF (1 participant).
*
* Format FINAL : demi-feuille Lettre coupée sur le 11"
* → papier 8.5" large × 5.5" haut (paysage)
* → PDF page = exactement cette taille
* → fond pleine page + n° / nom / QR alignés
*
* Impression : papier personnalisé 8.5 × 5.5, échelle 100 %, sans « ajuster à la page ».
*/
define('MSIN_DOSSARD_PRINT_PERM', 'inscriptions_gestion.dossard_print');
/**
* Taille page = feuille coupée (pouces → mm).
* Lettre 8.5×11 coupée en 2 sur le 11" → 8.5 × 5.5.
*
* @return array{0:float,1:float} [largeur, hauteur]
*/
function fxDossardPrintPageSizeMm()
{
return array(8.5 * 25.4, 5.5 * 25.4);
}
/** Dossier des fonds JPG/PNG (même ratio 8.5:5.5 recommandé). */
function fxDossardPrintTemplatesDir()
{
return dirname(__DIR__) . '/assets/dossard_templates';
}
/**
* Fond : eve_{id}.jpg|png sinon default.jpg|png.
*
* @return string|null
*/
function fxDossardPrintResolveTemplatePath($intEveId)
{
$strDir = fxDossardPrintTemplatesDir();
$intEveId = intval($intEveId);
$arrCandidates = array();
if ($intEveId > 0) {
$arrCandidates[] = $strDir . '/eve_' . $intEveId . '.jpg';
$arrCandidates[] = $strDir . '/eve_' . $intEveId . '.jpeg';
$arrCandidates[] = $strDir . '/eve_' . $intEveId . '.png';
}
$arrCandidates[] = $strDir . '/default.jpg';
$arrCandidates[] = $strDir . '/default.jpeg';
$arrCandidates[] = $strDir . '/default.png';
foreach ($arrCandidates as $strPath) {
if (is_file($strPath)) {
return $strPath;
}
}
return null;
}
/**
* Type FPDF d'après le fichier réel.
*
* @return string JPG|PNG|GIF|''
*/
function fxDossardPrintImageType($strPath)
{
$arrInfo = @getimagesize($strPath);
if (!is_array($arrInfo) || empty($arrInfo[2])) {
return '';
}
if ((int)$arrInfo[2] === IMAGETYPE_JPEG) {
return 'JPG';
}
if ((int)$arrInfo[2] === IMAGETYPE_PNG) {
return 'PNG';
}
if ((int)$arrInfo[2] === IMAGETYPE_GIF) {
return 'GIF';
}
return '';
}
/** Texte FPDF (core fonts = Windows-1252). */
function fxDossardPrintPdfText($str)
{
$str = (string)$str;
if (function_exists('iconv')) {
$strConverted = @iconv('UTF-8', 'Windows-1252//TRANSLIT', $str);
if ($strConverted !== false) {
return $strConverted;
}
}
if (function_exists('mb_convert_encoding')) {
return (string)mb_convert_encoding($str, 'Windows-1252', 'UTF-8');
}
return $str;
}
/** Droit impression (hors kit total). */
function fxDossardPrintCan($intComId, $intEveId)
{
if (!function_exists('fxEveAccesHasPermission')) {
require_once __DIR__ . '/inc_fx_eve_acces.php';
}
return fxEveAccesHasPermission(intval($intComId), intval($intEveId), MSIN_DOSSARD_PRINT_PERM);
}
/**
* @return array|null
*/
function fxDossardPrintLoadParticipant($intParId, $intEveId)
{
global $objDatabase;
$intParId = intval($intParId);
$intEveId = intval($intEveId);
if ($intParId <= 0 || $intEveId <= 0) {
return null;
}
$sql = 'SELECT p.par_id, p.par_prenom, p.par_nom, p.no_bib, p.eve_id, p.epr_id, p.pec_id,
e.pec_id_original, e.pec_actif, e.is_cancelled
FROM resultats_participants p
INNER JOIN resultats_epreuves_commandees e
ON e.pec_id_original = p.pec_id AND e.epr_id = p.epr_id
WHERE p.par_id = ' . $intParId . '
AND p.eve_id = ' . $intEveId . '
LIMIT 1';
$row = $objDatabase->fxGetRow($sql);
return ($row === null || $row === false) ? null : $row;
}
function fxDossardPrintQrUrl($intEveId)
{
if (!function_exists('fxKcEveResultatsQrUrl')) {
require_once __DIR__ . '/inc_fx_eve_extra.php';
}
return fxKcEveResultatsQrUrl(intval($intEveId));
}
/**
* @return string|false chemin PNG temp
*/
function fxDossardPrintBuildQrTempFile($intEveId)
{
require_once __DIR__ . '/../libs/phpqrcode-master/qrlib.php';
$strUrl = fxDossardPrintQrUrl($intEveId);
if ($strUrl === '') {
return false;
}
$strTmp = tempnam(sys_get_temp_dir(), 'ms1dossardqr_');
if ($strTmp === false) {
return false;
}
$strPng = $strTmp . '.png';
@unlink($strTmp);
QRcode::png($strUrl, $strPng, QR_ECLEVEL_M, 6, 1);
if (!is_file($strPng)) {
return false;
}
return $strPng;
}
/**
* Layout page 8.5" × 5.5" (mm) + overlays.
* Coordonnées calibrées pour le template comic (case QR bas-droite).
*
* @return array<string,mixed>
*/
function fxDossardPrintLayout()
{
$arrSize = fxDossardPrintPageSizeMm();
$fltW = $arrSize[0]; // 215.9
$fltH = $arrSize[1]; // 139.7
// Case QR du design : bas-droite (~1.1" dans le cadre)
$fltQr = 28.0;
return array(
'page_w' => $fltW,
'page_h' => $fltH,
// N° centré dans la zone blanche centrale
'bib_y' => 52,
'bib_font' => 72,
'bib_h' => 28,
// Nom sous le numéro
'name_y' => 84,
'name_font' => 14,
// QR dans la case « SCANNEZ POUR RÉSULTATS »
'qr_size' => $fltQr,
'qr_x' => $fltW - $fltQr - 11.5,
'qr_y' => $fltH - $fltQr - 14.5,
);
}
/**
* Génère et envoie le PDF (exit).
*
* @param array $arrRow
*/
function fxDossardPrintOutputPdf($arrRow)
{
$intEveId = intval($arrRow['eve_id'] ?? 0);
$strTemplate = fxDossardPrintResolveTemplatePath($intEveId);
if ($strTemplate === null) {
http_response_code(500);
header('Content-Type: text/plain; charset=utf-8');
echo 'Template dossard introuvable (assets/dossard_templates/default.jpg).';
exit;
}
$strFpdf = dirname(__DIR__) . '/fpdf182/fpdf.php';
if (!is_file($strFpdf)) {
$strDocRoot = $_SERVER['DOCUMENT_ROOT'] ?? '';
$strFpdf = rtrim((string)$strDocRoot, '/\\') . '/fpdf182/fpdf.php';
}
if (!is_file($strFpdf)) {
http_response_code(500);
header('Content-Type: text/plain; charset=utf-8');
echo 'FPDF introuvable (fpdf182/fpdf.php).';
exit;
}
require_once $strFpdf;
$arrLayout = fxDossardPrintLayout();
$strBib = trim((string)($arrRow['no_bib'] ?? ''));
if ($strBib === '' || $strBib === '0') {
$strBib = '-';
}
$strPrenom = trim(function_exists('fxUnescape') ? fxUnescape($arrRow['par_prenom'] ?? '') : (string)($arrRow['par_prenom'] ?? ''));
$strNom = trim(function_exists('fxUnescape') ? fxUnescape($arrRow['par_nom'] ?? '') : (string)($arrRow['par_nom'] ?? ''));
$strName = trim($strPrenom . ' ' . $strNom);
$strQrFile = fxDossardPrintBuildQrTempFile($intEveId);
$strTplType = fxDossardPrintImageType($strTemplate);
// Page = feuille coupée 8.5" × 5.5".
// FPDF (_getpagesize) range si w>h ; 'L' remet w=8.5 h=5.5.
$pdf = new FPDF('L', 'mm', array($arrLayout['page_w'], $arrLayout['page_h']));
$pdf->SetAutoPageBreak(false);
$pdf->SetMargins(0, 0, 0);
$pdf->AddPage();
// Fond pleine page — ratio template ≈ 8.5:5.5 → pas de déformation
$pdf->Image(
$strTemplate,
0,
0,
$arrLayout['page_w'],
$arrLayout['page_h'],
$strTplType
);
// Numéro
$pdf->SetTextColor(0, 0, 0);
$pdf->SetFont('Helvetica', 'B', intval($arrLayout['bib_font']));
$pdf->SetXY(20, $arrLayout['bib_y']);
$pdf->Cell(
$arrLayout['page_w'] - 40,
$arrLayout['bib_h'],
fxDossardPrintPdfText($strBib),
0,
0,
'C'
);
// Nom
if ($strName !== '') {
$pdf->SetFont('Helvetica', 'B', intval($arrLayout['name_font']));
$pdf->SetXY(20, $arrLayout['name_y']);
$pdf->Cell($arrLayout['page_w'] - 40, 8, fxDossardPrintPdfText($strName), 0, 0, 'C');
}
// QR dans la case du template
if ($strQrFile !== false) {
$pdf->Image(
$strQrFile,
$arrLayout['qr_x'],
$arrLayout['qr_y'],
$arrLayout['qr_size'],
$arrLayout['qr_size'],
'PNG'
);
@unlink($strQrFile);
}
$strSafeBib = preg_replace('/[^a-zA-Z0-9_-]+/', '-', $strBib);
$strFilename = 'dossard-' . $strSafeBib . '-par' . intval($arrRow['par_id']) . '.pdf';
$pdf->Output('I', $strFilename);
exit;
}

View File

@ -410,6 +410,52 @@ function fxEveAccesCatalogPermIsActive($strPermKey)
return ($intNb != null && intval($intNb) > 0);
}
/** MSIN-4484 — Colonne catalogue perm_grants_all presente ? */
function fxEveAccesHasPermGrantsAllColumn()
{
global $objDatabase;
static $blnHas = null;
if ($blnHas !== null) {
return $blnHas;
}
if (!fxEveAccesIsEnabled()) {
$blnHas = false;
return $blnHas;
}
$tab = $objDatabase->fxGetResults("SHOW COLUMNS FROM inscriptions_eve_permissions LIKE 'perm_grants_all'");
$blnHas = ($tab != null && count($tab) > 0);
return $blnHas;
}
/**
* MSIN-4484 — Cle active et incluse dans un kit role_grants_all ?
* perm_grants_all = 0 → hors propagation (matrice / extra seulement).
*/
function fxEveAccesCatalogPermPropagatesWithGrantsAll($strPermKey)
{
global $objDatabase;
if (!fxEveAccesIsEnabled() || trim((string)$strPermKey) === '') {
return false;
}
if (!fxEveAccesHasPermGrantsAllColumn()) {
return fxEveAccesCatalogPermIsActive($strPermKey);
}
$sql = "SELECT COUNT(*) FROM inscriptions_eve_permissions
WHERE perm_key = '" . $objDatabase->fxEscape($strPermKey) . "'
AND perm_actif = 1
AND perm_grants_all = 1";
$intNb = $objDatabase->fxGetVar($sql);
return ($intNb != null && intval($intNb) > 0);
}
/** Compte avec au moins un kit role_grants_all actif sur cet evenement. */
function fxEveAccesComEventHasGrantsAllKit($intComId, $intEveId)
{
@ -484,8 +530,10 @@ function fxEveAccesHasPermission($intComId, $intEveId, $strPermKey)
return false;
}
if (fxEveAccesComEventHasGrantsAllKit($intComId, $intEveId)) {
return fxEveAccesCatalogPermIsActive($strPermKey);
// MSIN-4484 — kit total = cles propagées ; cles hors kit total → matrice / extra seulement.
if (fxEveAccesComEventHasGrantsAllKit($intComId, $intEveId)
&& fxEveAccesCatalogPermPropagatesWithGrantsAll($strPermKey)) {
return true;
}
$sql = "SELECT COUNT(*) FROM " . fxEveAccesPermSql() . "
@ -505,14 +553,13 @@ function fxEveAccesHasAnyPermission($intComId, $intEveId, $arrPermKeys)
return false;
}
// MSIN-4484 — kit total pour les cles propagées ; sinon (ou cles hors total) → explicite.
if (fxEveAccesComEventHasGrantsAllKit($intComId, $intEveId)) {
foreach ($arrPermKeys as $strKey) {
if (fxEveAccesCatalogPermIsActive($strKey)) {
if (fxEveAccesCatalogPermPropagatesWithGrantsAll($strKey)) {
return true;
}
}
return false;
}
$arrEsc = array();
@ -583,8 +630,14 @@ function fxEveAccesGetCatalogPermissions($strScope = null, $blnActifOnly = true)
return array();
}
$sql = "SELECT perm_key, perm_group, perm_scope, perm_label_fr, perm_label_en, perm_phase, perm_tri
FROM inscriptions_eve_permissions";
// MSIN-4484 — perm_grants_all pour UI matrice (badge hors kit total).
$strGrantsCol = fxEveAccesHasPermGrantsAllColumn()
? ', perm_grants_all'
: ', 1 AS perm_grants_all';
$sql = "SELECT perm_key, perm_group, perm_scope, perm_label_fr, perm_label_en, perm_phase, perm_tri"
. $strGrantsCol
. " FROM inscriptions_eve_permissions";
$arrWhere = array();
if ($blnActifOnly) {
@ -1703,7 +1756,18 @@ function fxEveAccesShowCompteForm($intComId)
$arrAcces = fxEveAccesListByComId($intComId);
$arrExtras = fxEveAccesListExtraByComId($intComId);
$arrRoles = fxEveAccesGetRoles(true);
// MSIN-4485 — extras = actions ; clés hors kit total en tête de liste.
$arrActionPerms = fxEveAccesGetCatalogPermissions('action');
if ($arrActionPerms != null && is_array($arrActionPerms)) {
usort($arrActionPerms, function ($a, $b) {
$intA = isset($a['perm_grants_all']) ? intval($a['perm_grants_all']) : 1;
$intB = isset($b['perm_grants_all']) ? intval($b['perm_grants_all']) : 1;
if ($intA !== $intB) {
return $intA <=> $intB; // 0 (hors total) avant 1
}
return strcmp((string)($a['perm_label_fr'] ?? ''), (string)($b['perm_label_fr'] ?? ''));
});
}
$strT = urlencode($_GET['t'] ?? '');
$intComId = intval($intComId);
@ -1755,8 +1819,13 @@ function fxEveAccesShowCompteForm($intComId)
$strPermOptions = '<option value="">—</option>';
if ($arrActionPerms != null) {
foreach ($arrActionPerms as $rowPerm) {
$blnHorsTotal = isset($rowPerm['perm_grants_all']) && intval($rowPerm['perm_grants_all']) === 0;
$strLabel = (string)($rowPerm['perm_label_fr'] ?? $rowPerm['perm_key']);
if ($blnHorsTotal) {
$strLabel = '★ ' . $strLabel . ' (hors kit total)';
}
$strPermOptions .= '<option value="' . htmlspecialchars($rowPerm['perm_key'], ENT_QUOTES, 'UTF-8') . '">'
. htmlspecialchars($rowPerm['perm_label_fr'], ENT_QUOTES, 'UTF-8') . '</option>';
. htmlspecialchars($strLabel, ENT_QUOTES, 'UTF-8') . '</option>';
}
}
require_once __DIR__ . '/inc_fx_superadm_v2_ui.php';
@ -1827,7 +1896,7 @@ function fxEveAccesShowCompteForm($intComId)
</td>
<td class="align-middle text-right text-nowrap">
<?php if ($blnHasKit) { ?>
<select id="eve-extra-<?= $intEveId ?>" class="custom-select custom-select-sm d-inline-block" style="width:96px;vertical-align:middle;" aria-label="Droit individuel">
<select id="eve-extra-<?= $intEveId ?>" class="custom-select custom-select-sm d-inline-block" style="width:220px;max-width:40vw;vertical-align:middle;" aria-label="Droit individuel">
<?= $strPermOptions ?>
</select>
<button type="button" class="btn btn-outline-warning btn-sm py-0 btn-eve-add-extra" data-eve-id="<?= $intEveId ?>" title="Ajouter un droit individuel" style="border-color:#e67e22;color:#e67e22;">+</button>

File diff suppressed because it is too large Load Diff

View File

@ -71,11 +71,7 @@ function fxInscrGestionCanDo($intComId, $intEveId, $strActionPerm) {
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;
}
// MSIN-4401 / MSIN-4484 — kit total via HasPermission (respecte perm_grants_all = 0).
if (fxEveAccesHasPermission(intval($intComId), intval($intEveId), $strActionPerm)) {
return true;
}
@ -177,12 +173,7 @@ function fxInscrGestionCanScan($intComId, $intEveId) {
$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.
// MSIN-4401 / MSIN-4484 — kit total via HasAnyPermission (respecte perm_grants_all = 0).
if (fxEveAccesHasAnyPermission($intComId, $intEveId, fxInscrGestionPermKeysScan())) {
return true;
}
@ -535,6 +526,11 @@ function fxInscrGestionFallbackTexte($strClef, $strLangue) {
'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',
// MSIN-4485 — impression dossard PDF
'inscr_gestion_btn_dossard_print' => $blnFr ? 'Imprimer le dossard' : 'Print bib',
'inscr_gestion_btn_dossard_print_title' => $blnFr
? 'Générer le PDF du dossard'
: 'Generate bib PDF',
'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',
@ -1254,6 +1250,7 @@ function fxInscrGestionFetchFicheProfile($arrRow, $strLangue) {
$intParId = (int)$arrRow['par_id'];
$intPecId = (int)$arrRow['pec_id_original'];
$intPecPk = isset($arrRow['pec_id']) ? (int)$arrRow['pec_id'] : 0;
$intEveId = (int)$arrRow['eve_id'];
$strLangCol = ($strLangue === 'en') ? 'en' : 'fr';
@ -1290,6 +1287,15 @@ function fxInscrGestionFetchFicheProfile($arrRow, $strLangue) {
}
$intParIdOriginal = (int)$tabProfile['par_id_original'];
// MSIN-4482 — hors tx : accepter pec_id PK ou pec_id_original (réponses import).
// Pas de IFNULL(que_actif) / pas de requête te_nom ici : une SQL en erreur = plantage
// (fxGetResults → mysqli_num_rows(false)).
$strPecClause = 'rq.pec_id = ' . $intPecId;
if ($intPecPk > 0 && $intPecPk !== $intPecId) {
$strPecClause = '(rq.pec_id = ' . $intPecId . ' OR rq.pec_id = ' . $intPecPk . ')';
}
$sqlQuestions = 'SELECT rq.que_choix_' . $strLangCol . ' AS que_choix,'
. ' iq.que_cart_label_' . $strLangCol . ' AS que_label,'
. ' rq.que_question_' . $strLangCol . ' AS que_question,'
@ -1298,7 +1304,7 @@ function fxInscrGestionFetchFicheProfile($arrRow, $strLangue) {
. ' 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 ' . $strPecClause
. ' AND (rq.par_id = ' . $intParId . ' OR rq.par_id = ' . $intParIdOriginal . ')'
. ' AND rq.que_actif = 1'
. " AND TRIM(COALESCE(rq.que_choix_" . $strLangCol . ", '')) <> ''"
@ -2462,6 +2468,21 @@ function fxInscrGestionRenderFiche($intEveId, $strLangue, $strBaseUrl, $arrReq,
}
}
}
// MSIN-4485 — Imprimer le dossard (droit hors kit total)
if (!function_exists('fxDossardPrintCan')) {
require_once __DIR__ . '/inc_fx_dossard_print.php';
}
if (fxDossardPrintCan($intComId, $intRowEveId)) {
$strPrintUrl = rtrim((string)$vDomaine, '/') . '/dossard_print.php'
. '?par_id=' . intval($arrRow['par_id'])
. '&eve_id=' . intval($intRowEveId);
echo ' <a class="btn btn-outline-primary btn-sm inscr-gestion-btn-dossard-print" href="'
. fxInscrGestionEsc($strPrintUrl) . '" target="_blank" rel="noopener noreferrer"'
. ' title="' . fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_btn_dossard_print_title')) . '">'
. '<i class="fa fa-print" aria-hidden="true"></i>&nbsp;'
. fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_btn_dossard_print'))
. '</a>';
}
echo '</div>';
$strDetailsType = $blnEquipe && fxIsParEquipe($arrRow['pec_id_original'], $arrRow['epr_id'])

View File

@ -642,10 +642,10 @@ function fxPromoteurHubGetEventLinks($arrItem, $strLangue)
$blnLegacyPromoteur = fxIsPromoteur($intComId, $intEveId);
// Menus « tout ouvert » legacy seulement si PAS de kit V2 sur cet événement
$blnLegacyMenus = $blnLegacyPromoteur && !$blnHasV2;
$blnGrantsAll = $blnHasV2 && fxEveAccesComEventHasGrantsAllKit($intComId, $intEveId);
$fnCan = function ($arrKeys) use ($intComId, $intEveId, $blnSuper, $blnLegacyMenus, $blnGrantsAll) {
if ($blnSuper || $blnLegacyMenus || $blnGrantsAll) {
// MSIN-4484 — kit total : passer par HasAnyPermission (exclut perm_grants_all = 0).
$fnCan = function ($arrKeys) use ($intComId, $intEveId, $blnSuper, $blnLegacyMenus) {
if ($blnSuper || $blnLegacyMenus) {
return true;
}
return fxEveAccesHasAnyPermission($intComId, $intEveId, $arrKeys);
@ -1587,7 +1587,8 @@ function fxPromoteurHubCanViewDashboard($intComId, $intEveId)
require_once __DIR__ . '/inc_fonctions.php';
}
if (fxEveAccesComEventHasGrantsAllKit($intComId, $intEveId)) {
// MSIN-4484 — kit total respecté via HasPermission (perm_grants_all).
if (fxEveAccesHasPermission($intComId, $intEveId, 'hub.dashboard')) {
return true;
}
@ -1596,7 +1597,7 @@ function fxPromoteurHubCanViewDashboard($intComId, $intEveId)
return true;
}
return fxEveAccesHasPermission($intComId, $intEveId, 'hub.dashboard');
return false;
}
function fxPromoteurHubRenderEventCard($arrItem, $strLangue, $blnArchived = false)

View File

@ -0,0 +1,108 @@
-- MSIN-4482 — Réparer liste inscriptions pour imports hors transaction
-- Contexte : données en BD mais liste / « rapport » trop bas
-- Liste = p.pec_id = e.pec_id_original AND pec_actif = 1
-- Notes : exécution UNIQUEMENT sur dev préprod ; autres env = Navicat structure + sync_static_db
--
-- AVANT : remplacer 1284 par l'eve_id concerné.
-- IMPORTANT : ne PAS forcer pec_id_original = pec_id en masse (UNIQUE pec_id_original → 1062).
SET @eve := 1284;
-- ========== DIAGNOSTIC ==========
SELECT COUNT(*) AS n_participants_import
FROM resultats_participants
WHERE eve_id = @eve AND par_origine = 'import_fichier';
SELECT COUNT(*) AS n_join_ok
FROM resultats_epreuves_commandees e, resultats_participants p
WHERE e.eve_id = @eve
AND p.pec_id = e.pec_id_original
AND p.par_origine = 'import_fichier'
AND p.rol_id NOT IN (3, 4)
AND e.is_cancelled = 0;
SELECT COUNT(*) AS n_visibles_liste
FROM resultats_epreuves_commandees e, resultats_participants p
WHERE e.eve_id = @eve
AND p.pec_id = e.pec_id_original
AND p.par_origine = 'import_fichier'
AND p.rol_id NOT IN (3, 4)
AND e.is_cancelled = 0
AND e.pec_actif = 1;
-- Accroche technique import (p.pec_id = e.pec_id) avant réparation
SELECT COUNT(*) AS n_lie_via_pec_id
FROM resultats_epreuves_commandees e
INNER JOIN resultats_participants p
ON p.eve_id = e.eve_id AND p.pec_id = e.pec_id
WHERE e.eve_id = @eve
AND p.par_origine = 'import_fichier';
-- ========== FIX A — pec_actif = 1 (AVANT de bouger p.pec_id) ==========
UPDATE resultats_epreuves_commandees e
INNER JOIN resultats_participants p
ON p.eve_id = e.eve_id
AND p.pec_id = e.pec_id
AND p.par_origine = 'import_fichier'
SET e.pec_actif = 1
WHERE e.eve_id = @eve
AND e.is_cancelled = 0
AND IFNULL(e.pec_actif, 0) <> 1;
UPDATE resultats_epreuves_commandees e
SET e.pec_actif = 1
WHERE e.eve_id = @eve
AND e.no_panier LIKE CONCAT('I', @eve, 'X%')
AND e.is_cancelled = 0
AND IFNULL(e.pec_actif, 0) <> 1;
-- ========== FIX B — join sans toucher UNIQUE pec_id_original ==========
-- L'import a souvent p.pec_id = e.pec_id alors que la liste veut p.pec_id = e.pec_id_original.
-- On aligne le participant sur pec_id_original (valeur déjà unique).
UPDATE resultats_participants p
INNER JOIN resultats_epreuves_commandees e
ON e.eve_id = p.eve_id
AND e.pec_id = p.pec_id
SET p.pec_id = e.pec_id_original
WHERE p.eve_id = @eve
AND p.par_origine = 'import_fichier'
AND e.pec_id_original > 0
AND p.pec_id <> e.pec_id_original;
-- Optionnel / sûr : pec_id_original := pec_id SEULEMENT s'il n'y a PAS de collision UNIQUE
UPDATE resultats_epreuves_commandees e
INNER JOIN resultats_participants p
ON p.eve_id = e.eve_id
AND (p.pec_id = e.pec_id OR p.pec_id = e.pec_id_original)
AND p.par_origine = 'import_fichier'
LEFT JOIN resultats_epreuves_commandees conflict
ON conflict.pec_id_original = e.pec_id
AND conflict.pec_id <> e.pec_id
SET e.pec_id_original = e.pec_id
WHERE e.eve_id = @eve
AND e.pec_id_original <> e.pec_id
AND conflict.pec_id IS NULL;
-- Si l'optionnel a bougé pec_id_original, ré-aligner p.pec_id
UPDATE resultats_participants p
INNER JOIN resultats_epreuves_commandees e
ON e.eve_id = p.eve_id
AND e.no_panier LIKE CONCAT('I', @eve, 'X%')
AND e.no_panier = p.no_panier
SET p.pec_id = e.pec_id_original
WHERE p.eve_id = @eve
AND p.par_origine = 'import_fichier'
AND e.pec_id_original > 0
AND p.pec_id <> e.pec_id_original;
-- ========== CONTRÔLE ==========
SELECT COUNT(*) AS n_visibles_liste_apres
FROM resultats_epreuves_commandees e, resultats_participants p
WHERE e.eve_id = @eve
AND p.pec_id = e.pec_id_original
AND p.par_origine = 'import_fichier'
AND p.rol_id NOT IN (3, 4)
AND e.is_cancelled = 0
AND e.pec_actif = 1;
-- Attendu : proche de n_participants_import.

View File

@ -0,0 +1,56 @@
-- MSIN-4482 — Réparer questions import pour affichage fiche (eve hors inscription en ligne)
-- Contexte : réponses en BD mais fiche vide (pec_id / que_actif / que_cart_show)
-- Notes : exécution UNIQUEMENT sur dev préprod ; autres env = Navicat structure + sync_static_db
-- Cible : eve_id 1284 + par_origine = import_fichier seulement (ne touche pas au payant)
SET @eve := 1284;
-- Diagnostic : combien de réponses import vs ce que la fiche classique verrait
SELECT COUNT(*) AS n_reponses_via_par
FROM resultats_questions rq
INNER JOIN resultats_participants p
ON (rq.par_id = p.par_id OR rq.par_id = p.par_id_original)
WHERE p.eve_id = @eve AND p.par_origine = 'import_fichier';
-- 1) Aligner rq.pec_id sur e.pec_id_original (clé utilisée par la fiche)
UPDATE resultats_questions rq
INNER JOIN resultats_participants p
ON p.eve_id = @eve
AND p.par_origine = 'import_fichier'
AND (rq.par_id = p.par_id OR rq.par_id = p.par_id_original)
INNER JOIN resultats_epreuves_commandees e
ON e.eve_id = @eve
AND (e.pec_id_original = p.pec_id OR e.pec_id = p.pec_id OR e.pec_id = rq.pec_id)
SET rq.pec_id = e.pec_id_original
WHERE e.pec_id_original > 0
AND rq.pec_id <> e.pec_id_original;
-- 2) Activer les réponses import
UPDATE resultats_questions rq
INNER JOIN resultats_participants p
ON p.eve_id = @eve
AND p.par_origine = 'import_fichier'
AND (rq.par_id = p.par_id OR rq.par_id = p.par_id_original)
SET rq.que_actif = 1
WHERE IFNULL(rq.que_actif, 0) <> 1;
-- 3) Afficher en fiche (que_cart_show) les questions du catalogue liées à cet import
UPDATE inscriptions_questions iq
INNER JOIN resultats_questions rq ON rq.que_id = iq.que_id
INNER JOIN resultats_participants p
ON p.eve_id = @eve
AND p.par_origine = 'import_fichier'
AND (rq.par_id = p.par_id OR rq.par_id = p.par_id_original)
SET iq.que_cart_show = 1,
iq.que_cart_label_fr = IF(
TRIM(IFNULL(iq.que_cart_label_fr, '')) = '',
iq.que_question_fr,
iq.que_cart_label_fr
),
iq.que_cart_label_en = IF(
TRIM(IFNULL(iq.que_cart_label_en, '')) = '',
IF(TRIM(IFNULL(iq.que_question_en, '')) = '', iq.que_question_fr, iq.que_question_en),
iq.que_cart_label_en
)
WHERE iq.eve_id = @eve
AND IFNULL(iq.que_cart_show, 0) <> 1;

View File

@ -0,0 +1,63 @@
-- MSIN-4482 — Templates mapping import participants (hors transaction)
-- Contexte : UI import fiche événement superadm ; upsert sur clé fichier
-- Notes : exécution UNIQUEMENT sur dev préprod ; autres env = Navicat structure + sync_static_db
SET @db := DATABASE();
-- Templates de mapping colonnes (réutilisables par empreinte d'en-têtes)
CREATE TABLE IF NOT EXISTS `inscriptions_import_templates` (
`tpl_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`eve_id` int(10) unsigned NOT NULL DEFAULT 0,
`com_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'MSIN-4482 — org_id evenement (partage templates même org) ; pas com_id compte',
`tpl_name` varchar(120) NOT NULL DEFAULT '',
`tpl_header_sig` char(40) NOT NULL DEFAULT '' COMMENT 'sha1 en-têtes normalisés',
`tpl_headers_json` mediumtext NOT NULL,
`tpl_mapping_json` mediumtext NOT NULL COMMENT 'colonnes + valeurs épreuve + questions',
`tpl_actif` tinyint(1) unsigned NOT NULL DEFAULT 1,
`tpl_usa_id` int(10) unsigned NOT NULL DEFAULT 0,
`tpl_creation` datetime NOT NULL,
`tpl_maj` datetime NOT NULL,
PRIMARY KEY (`tpl_id`),
KEY `idx_import_tpl_eve_sig` (`eve_id`, `tpl_header_sig`),
KEY `idx_import_tpl_com_sig` (`com_id`, `tpl_header_sig`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Clé unique fichier (= external_id ChronoTrack) + origine d'entrée
SET @col_ext := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'resultats_participants' AND COLUMN_NAME = 'par_external_id'
);
SET @sql_ext := IF(
@col_ext = 0,
'ALTER TABLE resultats_participants ADD COLUMN par_external_id VARCHAR(64) NULL DEFAULT NULL COMMENT ''MSIN-4482 — clé unique import / CT external_id'' AFTER par_id_original',
'SELECT ''par_external_id already exists'' AS note'
);
PREPARE stmt_ext FROM @sql_ext;
EXECUTE stmt_ext;
DEALLOCATE PREPARE stmt_ext;
SET @col_ori := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'resultats_participants' AND COLUMN_NAME = 'par_origine'
);
SET @sql_ori := IF(
@col_ori = 0,
'ALTER TABLE resultats_participants ADD COLUMN par_origine VARCHAR(32) NULL DEFAULT NULL COMMENT ''MSIN-4482 — import_fichier|saisie_rapide|inscription_en_ligne'' AFTER par_external_id',
'SELECT ''par_origine already exists'' AS note'
);
PREPARE stmt_ori FROM @sql_ori;
EXECUTE stmt_ori;
DEALLOCATE PREPARE stmt_ori;
SET @idx_ext := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'resultats_participants' AND INDEX_NAME = 'idx_par_external_eve'
);
SET @sql_idx := IF(
@idx_ext = 0,
'ALTER TABLE resultats_participants ADD KEY idx_par_external_eve (eve_id, par_external_id)',
'SELECT ''idx_par_external_eve already exists'' AS note'
);
PREPARE stmt_idx FROM @sql_idx;
EXECUTE stmt_idx;
DEALLOCATE PREPARE stmt_idx;

View File

@ -0,0 +1,45 @@
-- MSIN-4482 — Type d'événement « Sans inscription en ligne »
-- Contexte : marqueur isolé pour eve hors tunnel inscription / paiement MS1 (import resultats_*)
-- Notes : exécution UNIQUEMENT sur dev préprod ; autres env = Navicat structure + sync_static_db
--
-- Table : inscriptions_types_evenements
-- Lien : inscriptions_evenements.te_id
-- Icône : images/te_sans_inscription_en_ligne.png (pas le coureur — écran + interdit)
-- 1) Créer le type (idempotent)
INSERT INTO inscriptions_types_evenements (te_nom_fr, te_nom_en, te_icone, te_actif)
SELECT
'Sans inscription en ligne',
'No online registration',
'te_sans_inscription_en_ligne.png',
1
FROM (SELECT 1 AS _x) AS _seed
WHERE NOT EXISTS (
SELECT 1
FROM inscriptions_types_evenements
WHERE te_nom_fr = 'Sans inscription en ligne'
);
-- 1b) Si le type existe déjà sans icône (ou à corriger) — petit UPDATE, safe
UPDATE inscriptions_types_evenements
SET te_icone = 'te_sans_inscription_en_ligne.png'
WHERE te_nom_fr = 'Sans inscription en ligne'
AND IFNULL(te_icone, '') <> 'te_sans_inscription_en_ligne.png';
-- 2) Vérification
SELECT te_id, te_nom_fr, te_nom_en, te_icone, te_actif
FROM inscriptions_types_evenements
WHERE te_nom_fr = 'Sans inscription en ligne';
-- 3) Rattacher l'événement de test (adapter eve_id si besoin)
UPDATE inscriptions_evenements
SET te_id = (
SELECT te_id FROM inscriptions_types_evenements
WHERE te_nom_fr = 'Sans inscription en ligne'
LIMIT 1
)
WHERE eve_id = 1284;
SELECT eve_id, te_id, eve_nom_fr
FROM inscriptions_evenements
WHERE eve_id = 1284;

View File

@ -0,0 +1,15 @@
-- MSIN-4482 — Urgence : débloquer / rollback type eve 1284 si besoin
-- Notes : exécution UNIQUEMENT sur dev préprod
--
-- 1) Si MySQL est saturé par un gros UPDATE/DELETE import :
-- SHOW FULL PROCESSLIST;
-- puis KILL <id>; sur les requêtes longues sur resultats_* / inscriptions_questions
--
-- 2) Remettre l'événement 1284 en type « Course » (te_id typique = 1 — vérifier d'abord) :
SELECT te_id, te_nom_fr FROM inscriptions_types_evenements WHERE te_actif = 1 ORDER BY te_nom_fr;
-- Exemple si Course = te_id 1 :
-- UPDATE inscriptions_evenements SET te_id = 1 WHERE eve_id = 1284;
SELECT eve_id, te_id, eve_nom_fr FROM inscriptions_evenements WHERE eve_id = 1284;

View File

@ -0,0 +1,31 @@
-- MSIN-4484 — Permission catalogue : hors propagation kit total
-- Prérequis : inscriptions_eve_permissions (accès v2)
-- Notes : exécution UNIQUEMENT sur dev préprod ; autres env = Navicat structure + sync_static_db
-- Idempotent
--
-- perm_grants_all = 1 (défaut) : incluse dans role_grants_all (comportement actuel)
-- perm_grants_all = 0 : nest PAS accordée automatiquement par un kit total ;
-- seulement via matrice / extra explicite (ou gate hors catalogue)
SET NAMES utf8mb4;
SET @col_exists := (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'inscriptions_eve_permissions'
AND COLUMN_NAME = 'perm_grants_all'
);
SET @sql := IF(
@col_exists = 0,
'ALTER TABLE `inscriptions_eve_permissions`
ADD COLUMN `perm_grants_all` tinyint(1) unsigned NOT NULL DEFAULT 1
COMMENT ''MSIN-4484 — 1 = incluse dans kit total (grants_all) ; 0 = non propagée''
AFTER `perm_actif`',
'SELECT ''perm_grants_all already exists'' AS info'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

View File

@ -0,0 +1,72 @@
-- MSIN-4485 — Impression dossard PDF (fiche gestion) + droit hors kit total
-- Prérequis : inscriptions_eve_permissions (accès v2)
-- Inclut la colonne MSIN-4484 (perm_grants_all) si absente — safe si 4484 déjà joué
-- Notes : exécution UNIQUEMENT sur dev préprod ; autres env = Navicat structure + sync_static_db
-- Idempotent
--
-- Droit non propagé (perm_grants_all = 0) : activer via extra / matrice explicite
-- sur le compte voulu — pas via kit owner / total.
SET NAMES utf8mb4;
/* MSIN-4484 — colonne hors propagation kit total (si pas déjà exécuté) */
SET @col_exists := (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'inscriptions_eve_permissions'
AND COLUMN_NAME = 'perm_grants_all'
);
SET @sql := IF(
@col_exists = 0,
'ALTER TABLE `inscriptions_eve_permissions`
ADD COLUMN `perm_grants_all` tinyint(1) unsigned NOT NULL DEFAULT 1
COMMENT ''MSIN-4484 — 1 = incluse dans kit total (grants_all) ; 0 = non propagée''
AFTER `perm_actif`',
'SELECT ''perm_grants_all already exists'' AS info'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
INSERT INTO `inscriptions_eve_permissions`
(`perm_key`, `perm_group`, `perm_scope`, `perm_label_fr`, `perm_label_en`,
`perm_description_fr`, `perm_description_en`, `perm_phase`, `perm_actif`,
`perm_grants_all`, `perm_tri`)
VALUES
('inscriptions_gestion.dossard_print', 'inscriptions', 'action',
'Gestion inscription : imprimer le dossard (PDF)',
'Registration mgmt: print bib (PDF)',
'Bouton Imprimer le dossard sur la fiche participant (PDF demi-lettre)',
'Print bib button on participant sheet (half-letter PDF)',
2, 1, 0, 119)
ON DUPLICATE KEY UPDATE
`perm_group` = VALUES(`perm_group`),
`perm_scope` = VALUES(`perm_scope`),
`perm_label_fr` = VALUES(`perm_label_fr`),
`perm_label_en` = VALUES(`perm_label_en`),
`perm_description_fr` = VALUES(`perm_description_fr`),
`perm_description_en` = VALUES(`perm_description_en`),
`perm_phase` = VALUES(`perm_phase`),
`perm_actif` = 1,
`perm_grants_all` = 0,
`perm_tri` = VALUES(`perm_tri`);
/* Ne pas laisser ce droit dans un kit par accident (hors propagation). */
DELETE FROM `inscriptions_eve_role_permissions`
WHERE `perm_key` = 'inscriptions_gestion.dossard_print';
DELETE FROM info
WHERE info_prg = 'compte.php'
AND info_clef IN (
'inscr_gestion_btn_dossard_print',
'inscr_gestion_btn_dossard_print_title'
);
INSERT INTO info (info_clef, info_langue, info_texte, info_aide, info_prg, info_description, info_trie, info_actif, info_option1, info_option2, info_option3, info_creation) VALUES
('inscr_gestion_btn_dossard_print', 'fr', 'Imprimer le dossard', '', 'compte.php', 'MSIN-4485', 0, 1, '', '', '', NOW()),
('inscr_gestion_btn_dossard_print', 'en', 'Print bib', '', 'compte.php', 'MSIN-4485', 0, 1, '', '', '', NOW()),
('inscr_gestion_btn_dossard_print_title', 'fr', 'Générer le PDF du dossard', '', 'compte.php', 'MSIN-4485', 0, 1, '', '', '', NOW()),
('inscr_gestion_btn_dossard_print_title', 'en', 'Generate bib PDF', '', 'compte.php', 'MSIN-4485', 0, 1, '', '', '', NOW());

View File

@ -0,0 +1,134 @@
<?php
/**
* MSIN-4482 — AJAX import participants (hors transaction) — superadm.
* Buffer strict : toute sortie parasite (erreur SQL, notices Excel) casse dataType:json.
*/
ob_start();
error_reporting(E_ALL & ~E_DEPRECATED & ~E_NOTICE & ~E_WARNING);
ini_set('display_errors', '0');
require_once dirname(__FILE__) . '/php/inc_functions.php';
require_once dirname(__FILE__) . '/../php/inc_fx_import_resultats.php';
$intUsaId = fxSuperadmAjaxAuthOrJsonExit();
if (session_status() === PHP_SESSION_ACTIVE) {
session_write_close();
}
$strAction = trim((string)($_REQUEST['a'] ?? ''));
$intEveId = intval($_REQUEST['eve_id'] ?? 0);
$tabRetour = array('state' => 'error', 'message' => 'action_invalide');
// MSIN-4482 — toutes les actions mutantes exigent le type « Sans inscription en ligne »
$tabActionsGuard = array('upload', 'save_template', 'validate', 'commit', 'reset_preview', 'reset');
if (in_array($strAction, $tabActionsGuard, true)) {
$tabGuard = fxImportResultatsGuardEveSansInscription($intEveId);
if (is_array($tabGuard)) {
$tabRetour = $tabGuard;
while (ob_get_level() > 0) {
ob_end_clean();
}
if (!headers_sent()) {
header('Content-Type: application/json; charset=utf-8');
}
echo json_encode($tabRetour, JSON_UNESCAPED_UNICODE);
exit;
}
}
switch ($strAction) {
case 'upload':
if ($intEveId <= 0) {
$tabRetour = array('state' => 'error', 'message' => 'eve_id manquant');
break;
}
if (empty($_FILES['file'])) {
$tabRetour = array('state' => 'error', 'message' => 'Fichier manquant');
break;
}
$tabRetour = fxImportResultatsSaveUpload($intEveId, $_FILES['file']);
break;
case 'save_template':
if ($intEveId <= 0) {
$tabRetour = array('state' => 'error', 'message' => 'eve_id manquant');
break;
}
$strToken = trim((string)($_REQUEST['token'] ?? ''));
$strName = trim((string)($_REQUEST['tpl_name'] ?? ''));
$arrMapping = $_REQUEST['mapping'] ?? array();
if (is_string($arrMapping)) {
$arrDecoded = json_decode($arrMapping, true);
$arrMapping = is_array($arrDecoded) ? $arrDecoded : array();
}
if (!is_array($arrMapping)) {
$arrMapping = array();
}
$tabRetour = fxImportResultatsSaveTemplate($intEveId, $strToken, $strName, $arrMapping, $intUsaId, true);
break;
case 'validate':
if ($intEveId <= 0) {
$tabRetour = array('state' => 'error', 'message' => 'eve_id manquant');
break;
}
$strToken = trim((string)($_REQUEST['token'] ?? ''));
$arrMapping = $_REQUEST['mapping'] ?? array();
if (is_string($arrMapping)) {
$arrDecoded = json_decode($arrMapping, true);
$arrMapping = is_array($arrDecoded) ? $arrDecoded : array();
}
if (!is_array($arrMapping)) {
$arrMapping = array();
}
$tabRetour = fxImportResultatsValidateMapping($intEveId, $strToken, $arrMapping);
break;
case 'commit':
if ($intEveId <= 0) {
$tabRetour = array('state' => 'error', 'message' => 'eve_id manquant');
break;
}
$strToken = trim((string)($_REQUEST['token'] ?? ''));
$arrMapping = $_REQUEST['mapping'] ?? array();
if (is_string($arrMapping)) {
$arrDecoded = json_decode($arrMapping, true);
$arrMapping = is_array($arrDecoded) ? $arrDecoded : array();
}
if (!is_array($arrMapping)) {
$arrMapping = array();
}
$tabRetour = fxImportResultatsCommit($intEveId, $strToken, $arrMapping);
break;
case 'reset_preview':
if ($intEveId <= 0) {
$tabRetour = array('state' => 'error', 'message' => 'eve_id manquant');
break;
}
$tabRetour = fxImportResultatsResetPreview($intEveId);
break;
case 'reset':
if ($intEveId <= 0) {
$tabRetour = array('state' => 'error', 'message' => 'eve_id manquant');
break;
}
$strConfirm = trim((string)($_REQUEST['confirm'] ?? ''));
$tabRetour = fxImportResultatsReset($intEveId, $strConfirm);
break;
default:
$tabRetour = array('state' => 'error', 'message' => 'action_invalide');
break;
}
// Jeter tout HTML parasite (erreur SQL legacy, notices PHPExcel, etc.)
while (ob_get_level() > 0) {
ob_end_clean();
}
if (!headers_sent()) {
header('Content-Type: application/json; charset=utf-8');
}
echo json_encode($tabRetour, JSON_UNESCAPED_UNICODE);
exit;

View File

@ -396,4 +396,76 @@ table.rapport tr {background-color: #190d7394;}*/
.superadm-v2-panel__body .custom-control-label {
color: #1e293b;
font-weight: 500;
}
/* MSIN-4482 — Import participants (hors transaction) */
.ms1-import__drop {
align-items: center;
background: linear-gradient(160deg, #f0f9fc 0%, #e4f3f8 100%);
border: 2px dashed #045a6e;
border-radius: 12px;
color: #045a6e;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 0.35rem;
justify-content: center;
min-height: 9.5rem;
padding: 1.25rem 1rem;
text-align: center;
transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}
.ms1-import__drop:hover,
.ms1-import__drop:focus {
background: #e8f6fb;
box-shadow: 0 4px 14px rgba(4, 90, 110, 0.12);
outline: none;
}
.ms1-import__drop.is-drag {
background: #dff3f9;
border-color: #0284a8;
border-style: solid;
box-shadow: 0 0 0 3px rgba(2, 132, 168, 0.2);
transform: scale(1.01);
}
.ms1-import__drop.is-busy {
cursor: wait;
opacity: 0.72;
pointer-events: none;
}
.ms1-import__drop.is-ready {
border-style: solid;
min-height: 4.5rem;
padding: 0.65rem;
}
.ms1-import__drop.is-ready .ms1-import__drop-sub {
display: none;
}
.ms1-import__drop-icon {
font-size: 2rem;
line-height: 1;
opacity: 0.9;
}
.ms1-import__drop-title {
font-size: 1.05rem;
font-weight: 700;
}
.ms1-import__drop-sub {
color: #64748b;
font-size: 0.875rem;
}
.ms1-import__h {
color: #045a6e;
font-weight: 700;
margin-top: 0.35rem;
}
.ms1-import__status.alert {
margin-bottom: 0.75rem;
}
.ms1-import__epr,
.ms1-import__que {
background: #f8fafc;
border: 1px solid #cbd5e1;
border-radius: 8px;
padding: 0.75rem 0.85rem;
}

View File

@ -128,6 +128,10 @@ if ($strAction == 'mod') {
));
fxChronotrackApiAdminRenderEventPage($intEveIdForm, $strLangue, true, true);
fxSuperadmV2PanelClose(array('collapse_id' => 'chronotrackApiCollapse'));
// MSIN-4482 — Import participants hors transaction (UI + templates)
require_once(dirname(__FILE__) . '/../../php/inc_fx_import_resultats.php');
fxImportResultatsRenderEventPanel($intEveIdForm);
}
}
@ -153,6 +157,10 @@ if ($strAction == 'mod') {
onclick="if (window.jQuery) { jQuery('#chronotrackApiCollapse').collapse('show'); }">
ChronoTrack
</a>
<a class="btn btn-secondary" href="#import_resultats"
onclick="if (window.jQuery) { jQuery('#importResultatsCollapse').collapse('show'); }">
Import
</a>
<a class="btn btn-secondary" href="#list_eve_extra">
Champs supplémentaires
</a>

View File

@ -978,7 +978,8 @@ function fxEveAccesAdminRenderKitForm($intRoleId, $strError = '')
</label>
</div>
<small class="text-muted">
Si coché : aucune matrice à maintenir. Sinon : choisissez les droits un par un ci-dessous.
Si coché : aucune matrice à maintenir (sauf clés marquées
<em>hors kit total</em>, MSIN-4484). Sinon : choisissez les droits un par un ci-dessous.
</small>
</div>
<?php } ?>
@ -1014,6 +1015,8 @@ function fxEveAccesAdminRenderKitForm($intRoleId, $strError = '')
<strong>Niveau 1</strong> = accès aux menus du hub.
<strong>Niveau 2</strong> = actions dans la page (ex. Inscriptions V2).
La page <code>/mobile</code> est la même gestion inscriptions en affichage mobile — pas une API.
Badge <span class="badge badge-warning">hors kit total</span> = non inclus dans un kit total
(MSIN-4484) ; cocher ici seulement pour une attribution explicite.
</p>
<?php
foreach ($arrOrderedGroups as $strGroup => $arrPerms) {
@ -1033,16 +1036,23 @@ function fxEveAccesAdminRenderKitForm($intRoleId, $strError = '')
$strKey = $rowPerm['perm_key'];
$strId = 'perm_' . preg_replace('/[^a-z0-9_]/', '_', $strKey);
$strScope = (string)($rowPerm['perm_scope'] ?? '');
$blnPropagates = !isset($rowPerm['perm_grants_all'])
|| intval($rowPerm['perm_grants_all']) === 1;
$strBadge = ($strScope === 'page')
? '<span class="badge badge-primary ml-1">page</span>'
: (($strScope === 'action')
? '<span class="badge badge-secondary ml-1">détail</span>'
: '');
if (!$blnPropagates) {
$strBadge .= ' <span class="badge badge-warning ml-1">hors kit total</span>';
}
$strPad = ($strScope === 'action') ? ' pl-3' : '';
$strChkClass = 'custom-control-input perm-checkbox'
. ($blnPropagates ? '' : ' perm-checkbox-no-grants-all');
?>
<div class="col-md-6 col-lg-4 mb-2<?= $strPad ?>">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input perm-checkbox"
<input type="checkbox" class="<?= htmlspecialchars($strChkClass, ENT_QUOTES, 'UTF-8') ?>"
id="<?= htmlspecialchars($strId, ENT_QUOTES, 'UTF-8') ?>"
name="perm_keys[]"
value="<?= htmlspecialchars($strKey, ENT_QUOTES, 'UTF-8') ?>"
@ -1116,7 +1126,8 @@ function fxEveAccesAdminRenderKitForm($intRoleId, $strError = '')
syncGrantsAllUi();
$('#btn-perm-all').on('click', function () {
$('.perm-checkbox').prop('checked', true);
// MSIN-4484 — ne pas cocher les clés hors kit total (évite assignation accidentelle).
$('.perm-checkbox').not('.perm-checkbox-no-grants-all').prop('checked', true);
});
$('#btn-perm-none').on('click', function () {
$('.perm-checkbox').prop('checked', false);
@ -1124,7 +1135,7 @@ function fxEveAccesAdminRenderKitForm($intRoleId, $strError = '')
$('.perm-group-block').each(function () {
var $block = $(this);
$block.find('.btn-group-all').on('click', function () {
$block.find('.perm-checkbox').prop('checked', true);
$block.find('.perm-checkbox').not('.perm-checkbox-no-grants-all').prop('checked', true);
});
$block.find('.btn-group-none').on('click', function () {
$block.find('.perm-checkbox').prop('checked', false);