33 Commits

Author SHA1 Message Date
a62cca6e45 Enhance product display functionality by adding 'pro_date_limite' to the product selection query and updating the rendering logic to include the sale end date. Adjust comments to reflect these changes and improve clarity on the product display section. Increment version code. 2026-07-25 16:46:14 -04:00
83dc15ec9f Enhance workflow documentation by adding cost-benefit approach guidelines in both entreprise and projet rules. Update comments in dossard print functions for clarity on PDF formatting and printing specifications. Refine layout instructions in dossard layout file for improved user understanding. Increment version code. 2026-07-25 10:38:03 -04:00
3320a26ada Add layout management functions for dossard PDF generation. Implement features for loading, saving, and normalizing layouts with support for dynamic box configurations. Enhance field catalog for extensibility and improve comments for clarity. Increment version code. 2026-07-25 10:22:43 -04:00
f0e0feeeb9 Update dossard PDF layout to refine positioning of number and name boxes. Adjust percentage values for better alignment and centering, enhancing overall layout clarity. Improve comments for better understanding of layout adjustments. Increment version code. 2026-07-25 10:13:15 -04:00
762b31083e Refactor dossard PDF layout to implement independent boxes for number and name, enhancing text centering and positioning. Update layout calculations for improved alignment and dynamic font sizing based on available space. Enhance comments for clarity. Increment version code. 2026-07-25 10:08:52 -04:00
358673cada Refactor dossard PDF layout to improve background calibration and text positioning. Adjust margins and dimensions for the printable zone, ensuring better alignment of the number and name. Update comments for clarity and implement dynamic font sizing based on ascent height. Increment version code. 2026-07-25 10:02:50 -04:00
eed49c4745 Refactor dossard PDF layout to improve text positioning and sizing. Adjust margins and dimensions for the printable zone, ensuring better alignment of the number and name. Update comments for clarity and implement dynamic font sizing based on glyph height. Increment version code. 2026-07-25 09:59:44 -04:00
8eb0c644f3 Refactor dossard PDF layout to enhance text positioning and font sizing. Update layout calculations for the printable zone and adjust name placement under the number. Improve comments for clarity and ensure dynamic font sizing accommodates both width and height constraints. Increment version code. 2026-07-25 09:57:14 -04:00
6f7d8c5ef8 Refactor dossard PDF layout to improve text alignment and font sizing. Adjust name positioning to align left under the number and implement dynamic font sizing for the number based on available space. Enhance comments for better clarity. Increment version code. 2026-07-25 09:54:07 -04:00
ed992d2903 Update dossard layout to center large number and adjust name positioning. Modify QR code placement and enhance comments for clarity. Increment version code. 2026-07-25 09:51:50 -04:00
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
28 changed files with 4906 additions and 70 deletions

View File

@ -0,0 +1,42 @@
---
description: Progiweb — toujours proposer loption au meilleur coût-bénéfice (pas lessai-erreur)
alwaysApply: true
---
# Coût-bénéfice — qualité dapproche
Référentiel agent. Lutilisateur veut **toujours coder au meilleur** : proposer dabord la façon qui minimise le temps total (humain + agent) pour un résultat commercialement fiable.
## Règle
Avant dimplémenter (surtout UI print, layout, mapping, calibrage, « ça a lair bon ») :
1. **Identifier** si la tâche invite à lessai-erreur (deviner des mm/%, relire des screenshots, retoucher au feeling).
2. **Proposer dabord** loption au meilleur coût-bénéfice — même si elle demande 3060 min de setup.
3. **Ne pas** enchaîner des itérations « change un chiffre → redeploie → regarde » comme stratégie principale.
4. Si plusieurs options : présenter **brièvement** (13) avec coût / bénéfice, **recommander** la meilleure, attendre OK si le setup est plus large que demandé.
## Préférer (exemples)
| Anti-pattern (cher) | Meilleure option (souvent) |
|---|---|
| Deviner positions PDF / CSS au feeling | GUI / outils de calibrage → vérité mesurée (JSON, %) |
| 15 essais de mapping colonnes | Preview + template sauvegardé + règles claires |
| Hardcoder FR/EN partout | Source unique (Info / config) |
| Patch local qui casse au 2ᵉ cas | Modèle extensible minimal (N boîtes, catalogue champs) |
| Changer le format dimpression « pour que ça marche chez moi » | **Demander le scénario volume** (1 test vs 2000 en file) avant de coder |
**Cas vécu MSIN-4485** : calibrateur drag-and-drop des boîtes a coûté **moins** que la série dessais PDF. Ce genre de levier = défaut à proposer.
**Impression / papier** : avant de changer Lettre vs demi-feuille vs coupe, poser la question du **jour J** (file dattente, volume). Ne pas imposer la coupe comme solution « technique » sans accord produit.
## Ce que ce nest pas
- Pas dover-engineering « au cas où » sans besoin.
- Pas de gros refactor hors périmètre.
- Le meilleur coût-bénéfice reste **minimal mais juste** : vérité mesurable + extensible léger > génie fragile.
## À chaque proposition dapproche
- Dire clairement : « Je recommande X parce que Y (moins daller-retours / une calibration / réutilisable). »
- Si lutilisateur pousse lessai-erreur : rappeler une fois lalternative, puis suivre sil insiste.

View File

@ -17,6 +17,7 @@ Référentiel agent — pas une doc humaine. S'appliquer à tous les projets Pro
- **Changement minimal** : uniquement ce qui est demandé.
- Pas de refactor large, renommage massif ou nettoyage opportuniste sans accord explicite.
- Cohérence locale (fichier / module) plutôt que modernisation globale.
- **Approche** : toujours le meilleur **coût-bénéfice** (voir `entreprise/00-cout-benefice.mdc`) — calibrage / vérité mesurable avant essai-erreur.
## Incohérences voisines

View File

@ -11,26 +11,26 @@ Doc de cadrage — source de vérité pour le développement de ce chantier.
## Intention
Utiliser la suite MS1 (gestion, ChronoTrack, hub allégé, signature de dossier, accès par tâche) pour des clients **sans** tunnel dinscription / paiement en ligne MS1.
Les participants arrivent par **import fichier** et/ou **saisie rapide** ; ensuite sync ChronoTrack.
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 le **jour J** sur une course **avec** inscription en ligne : ajout / correction rapide (sexe, âge, etc.) **sans** transaction financière.
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_*`** (`resultats_participants`, `resultats_epreuves_commandees`, `resultats_questions`) — pas le panier |
| Type événement | Nouveau type / mode « sans inscription en ligne » (ex. timing) — à créer |
| Paiement MS1 | Hors scope pour ces lignes ; **interdit** le « panier à 0 $ » |
| Clé unique | ID du fichier client = **`external_id` ChronoTrack** au push (pas seulement `par_id_original`) ; même fichier = fallback import CT |
| Questions import | **Option C** : matcher les questions existantes ; non matchées → preview + **création confirmée** |
| Hub | **Mini-hub / hub allégé** (pas zéro hub) ; grossit avec les options |
| Opérateur | MS1 (Stéphane / équipe) = promoteur opérateur ; délégation = **kits limités par tâche**, pas kit promoteur client « total » |
| Signature de dossier | **Dans le scope hub** — à cartographier (module exact) ; **≠ waiver** |
| Waiver | **Hors scope** — ne pas assimiler « signature de dossier » au waiver |
| Édition fiches | Create/edit sur `resultats_participants` (+ questions) ; **même** chemin jour J sur événement en ligne |
| Finances | Marqueur d**origine** obligatoire ; import / saisie rapide **exclus** des rapports paiement / facture |
| 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)
@ -38,36 +38,65 @@ Aussi utile le **jour J** sur une course **avec** inscription en ligne : ajout /
|---|---|
| `inscription_en_ligne` | Flux payant actuel |
| `import_fichier` | Bulk modèle fichier |
| `saisie_rapide` | Création unitaire / jour J |
| `saisie_rapide` | Création unitaire / jour J (plus tard) |
## Ancrages code existants
## Phases (réalignées)
- Sync CT : `php/chronotrack_api/` — push MS1→CT ; aujourdhui `external_id` ≈ `par_id_original` → **à adapter** si clé import présente
- `ct_entry_id` : ID interne CT post-sync (MSIN-4328) — distinct de la clé fichier
- Questions → CT : `entry_note_*` (MSIN-4461) via `resultats_questions` + `inscriptions_questions`
- Hub / accès : `php/inc_fx_promoteur_hub.php`, `inscriptions_eve_acces*`
- Pas dimport générique participants aujourdhui (seulement FNQ/TQ/PayPal spécialisés)
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
## Hors scope
## UI import (décisions)
- Waiver / `waiver_cleared` comme livrable de ce projet
- Kit promoteur client complet sur ce type dévénement
- Facturation / remboursements pour lignes import / saisie rapide
- Import inverse ChronoTrack → MS1 comme chemin nominal
- 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)
## Phases
## Exemple fichier client (référence)
0. **Cadrage** — contrat fichier + unique ID ; stratégie questions C ; carto « signature de dossier » ; inventaire `no_panier` / finances
1. **Fondations** — type événement ; colonnes clé externe + origine ; coquille `pec` sans commande payante
2. **Import** — UI + upsert clé unique + questions (match / création confirmée) → `resultats_*`
3. **Saisie / édition** — fiche rapide ; ouvert aussi sur courses en ligne (jour J)
4. **CT + hub** — sync avec clé fichier ; mini-hub ; accès par tâche ; signature de dossier
5. **Ops** — tests mélange payant vs hors-transaction ; filet même fichier → CT
Fichier type : export ~Échappée (en-têtes déjà normalisés ; loriginal client peut différer).
## Règles agent (pendant le chantier)
| 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) |
- Changement minimal ; référencer **MSIN-4482** dans SQL / commentaires PHP.
- Ne pas brancher ces flux sur le panier / paiement.
- Ne pas réintroduire le waiver « pour faire la signature de dossier ».
- Préférer étendre gestion / hub existants plutôt que doubler les fiches participant.
- SQL : uniquement **dev préprod**, puis Navicat + sync outil (workflow projet).
**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

@ -32,6 +32,7 @@ Ordre **non négociable** pour faire passer structure + données vers dautres
- **Changement minimal** : corriger uniquement ce qui est demandé.
- Pas de refactor large, renommage massif ou « nettoyage » opportuniste sans accord explicite.
- Pour le code legacy (~10 ans), privilégier la cohérence locale (fichier / module touché) plutôt qu'une modernisation globale.
- Approche : **meilleur coût-bénéfice** (`entreprise/00-cout-benefice.mdc`) — ex. calibrateur GUI plutôt quune série dessais PDF.
## Incohérences repérées

View File

@ -0,0 +1 @@

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

View File

@ -0,0 +1,27 @@
{
"version": 2,
"page": "8.5x5.5",
"boxes": [
{
"id": "bib",
"field": "no_bib",
"align": "C",
"font_max": 220,
"left": 0.2,
"right": 0.78,
"top": 0.36,
"bottom": 0.66
},
{
"id": "prenom",
"field": "par_prenom",
"align": "L",
"font_max": 28,
"left": 0.32,
"right": 0.68,
"top": 0.72,
"bottom": 0.84
}
],
"updated_at": "2026-07-25T00:00:00+00:00"
}

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,662 @@
<?php
/**
* MSIN-4485 — Impression dossard PDF (1 participant).
*
* Zone utile : 8.5" × 5.5" (demi-Lettre coupée sur le 11").
* Format feuille PDF : à trancher selon le mode dimpression (voir discussion produit).
*
* Impression type : échelle 100 %, sans « ajuster à la page » si le papier = taille PDF.
*/
define('MSIN_DOSSARD_PRINT_PERM', 'inscriptions_gestion.dossard_print');
/**
* Zone utile dossard (pouces → mm) : 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;
}
/**
* Fichier layout calibré (GUI drag & drop → %) — vérité pour le PDF.
*
* @param int $intEveId 0 = default
* @return string
*/
function fxDossardPrintLayoutPath($intEveId = 0)
{
$strDir = fxDossardPrintTemplatesDir();
$intEveId = intval($intEveId);
if ($intEveId > 0) {
$strEve = $strDir . '/eve_' . $intEveId . '.layout.json';
if (is_file($strEve)) {
return $strEve;
}
}
return $strDir . '/default.layout.json';
}
/**
* Catalogue des champs exploitables dans une boîte (extensible).
*
* @return array<string,array{label:string,sample:string,font_max:int}>
*/
function fxDossardPrintFieldCatalog()
{
return array(
'no_bib' => array('label' => 'N° dossard', 'sample' => '2347', 'font_max' => 220),
'par_prenom' => array('label' => 'Prénom', 'sample' => 'Cynthia', 'font_max' => 28),
'par_nom' => array('label' => 'Nom', 'sample' => 'Tremblay', 'font_max' => 28),
'par_prenom_nom' => array('label' => 'Prénom + nom', 'sample' => 'Cynthia Tremblay', 'font_max' => 22),
// Réservés pour plus tard (valeur vide tant que non chargé en SQL)
'par_age' => array('label' => 'Âge', 'sample' => '42', 'font_max' => 24),
'par_sexe' => array('label' => 'Sexe', 'sample' => 'F', 'font_max' => 24),
'epr_nom' => array('label' => 'Épreuve', 'sample' => '10K', 'font_max' => 22),
);
}
/**
* Layout défaut : 2 boîtes (n° + prénom) — modèle extensible N boîtes.
*
* @return array{version:int,page:string,boxes:array}
*/
function fxDossardPrintDefaultLayout()
{
return array(
'version' => 2,
'page' => '8.5x5.5',
'boxes' => array(
array(
'id' => 'bib',
'field' => 'no_bib',
'align' => 'C',
'font_max' => 220,
'left' => 0.20,
'right' => 0.78,
'top' => 0.36,
'bottom' => 0.66,
),
array(
'id' => 'prenom',
'field' => 'par_prenom',
'align' => 'L',
'font_max' => 28,
'left' => 0.32,
'right' => 0.68,
'top' => 0.72,
'bottom' => 0.84,
),
),
);
}
/**
* Normalise JSON v1 (bib/name) ou v2 (boxes) → layout v2.
*
* @param array|null $arrData
* @return array
*/
function fxDossardPrintNormalizeLayout($arrData)
{
$arrDefault = fxDossardPrintDefaultLayout();
if (!is_array($arrData)) {
return $arrDefault;
}
// v1 legacy
if (empty($arrData['boxes']) && !empty($arrData['bib']) && !empty($arrData['name'])) {
$arrBoxes = $arrDefault['boxes'];
foreach (array('left', 'right', 'top', 'bottom') as $strEdge) {
if (isset($arrData['bib'][$strEdge])) {
$arrBoxes[0][$strEdge] = floatval($arrData['bib'][$strEdge]);
}
if (isset($arrData['name'][$strEdge])) {
$arrBoxes[1][$strEdge] = floatval($arrData['name'][$strEdge]);
}
}
$arrDefault['boxes'] = $arrBoxes;
return $arrDefault;
}
if (empty($arrData['boxes']) || !is_array($arrData['boxes'])) {
return $arrDefault;
}
$arrCatalog = fxDossardPrintFieldCatalog();
$arrBoxes = array();
foreach ($arrData['boxes'] as $intIdx => $arrBox) {
if (!is_array($arrBox)) {
continue;
}
$strField = isset($arrBox['field']) ? (string)$arrBox['field'] : '';
if ($strField === '' || !isset($arrCatalog[$strField])) {
continue;
}
foreach (array('left', 'right', 'top', 'bottom') as $strEdge) {
if (!isset($arrBox[$strEdge])) {
continue 2;
}
$flt = floatval($arrBox[$strEdge]);
if ($flt < 0 || $flt > 1) {
continue 2;
}
$arrBox[$strEdge] = $flt;
}
if ($arrBox['right'] <= $arrBox['left'] || $arrBox['bottom'] <= $arrBox['top']) {
continue;
}
$strAlign = strtoupper((string)($arrBox['align'] ?? 'C'));
if ($strAlign !== 'L' && $strAlign !== 'R') {
$strAlign = 'C';
}
$intFontMax = isset($arrBox['font_max'])
? max(8, intval($arrBox['font_max']))
: intval($arrCatalog[$strField]['font_max']);
$strId = trim((string)($arrBox['id'] ?? ''));
if ($strId === '') {
$strId = 'box_' . ($intIdx + 1);
}
$arrBoxes[] = array(
'id' => $strId,
'field' => $strField,
'align' => $strAlign,
'font_max' => $intFontMax,
'left' => round($arrBox['left'], 5),
'right' => round($arrBox['right'], 5),
'top' => round($arrBox['top'], 5),
'bottom' => round($arrBox['bottom'], 5),
);
}
if (count($arrBoxes) < 1) {
return $arrDefault;
}
return array(
'version' => 2,
'page' => '8.5x5.5',
'boxes' => $arrBoxes,
'updated_at' => isset($arrData['updated_at']) ? (string)$arrData['updated_at'] : null,
);
}
/**
* Lit le layout calibré (JSON GUI) ou défaut.
*
* @param int $intEveId
* @return array
*/
function fxDossardPrintLoadLayout($intEveId = 0)
{
$strPath = fxDossardPrintLayoutPath($intEveId);
if (!is_file($strPath)) {
return fxDossardPrintDefaultLayout();
}
$strJson = @file_get_contents($strPath);
$arrData = is_string($strJson) ? json_decode($strJson, true) : null;
return fxDossardPrintNormalizeLayout($arrData);
}
/**
* @deprecated alias GUI ancienne — préfère fxDossardPrintLoadLayout
* @return array
*/
function fxDossardPrintLoadLayoutPct($intEveId = 0)
{
return fxDossardPrintLoadLayout($intEveId);
}
/**
* Enregistre le layout calibré (liste de boîtes).
*
* @param array $arrLayout version + boxes[]
* @param int $intEveId 0 = default.layout.json
* @return array{ok:bool,error?:string,path?:string}
*/
function fxDossardPrintSaveLayout($arrLayout, $intEveId = 0)
{
$intEveId = intval($intEveId);
$strDir = fxDossardPrintTemplatesDir();
if (!is_dir($strDir)) {
return array('ok' => false, 'error' => 'Dossier templates absent.');
}
$arrNorm = fxDossardPrintNormalizeLayout($arrLayout);
if (empty($arrNorm['boxes'])) {
return array('ok' => false, 'error' => 'Aucuneune boîte valide.');
}
$strPath = ($intEveId > 0)
? $strDir . '/eve_' . $intEveId . '.layout.json'
: $strDir . '/default.layout.json';
$arrOut = array(
'version' => 2,
'page' => '8.5x5.5',
'boxes' => $arrNorm['boxes'],
'updated_at' => date('c'),
);
$strJson = json_encode($arrOut, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
if ($strJson === false || @file_put_contents($strPath, $strJson) === false) {
return array('ok' => false, 'error' => 'Écriture impossible (droits fichier?).');
}
return array('ok' => true, 'path' => $strPath);
}
/**
* @deprecated — accepte encore {bib,name} ou {boxes}
* @param array $arrPct
* @param int $intEveId
* @return array
*/
function fxDossardPrintSaveLayoutPct($arrPct, $intEveId = 0)
{
if (isset($arrPct['boxes'])) {
return fxDossardPrintSaveLayout($arrPct, $intEveId);
}
return fxDossardPrintSaveLayout($arrPct, $intEveId);
}
/**
* Valeur texte pour un champ boîte.
*
* @param array $arrRow
* @param string $strField
* @return string
*/
function fxDossardPrintResolveFieldValue($arrRow, $strField)
{
$fnUn = function ($str) {
$str = (string)$str;
return function_exists('fxUnescape') ? fxUnescape($str) : $str;
};
switch ((string)$strField) {
case 'no_bib':
$str = trim((string)($arrRow['no_bib'] ?? ''));
return ($str === '' || $str === '0') ? '-' : $str;
case 'par_prenom':
return trim($fnUn($arrRow['par_prenom'] ?? ''));
case 'par_nom':
return trim($fnUn($arrRow['par_nom'] ?? ''));
case 'par_prenom_nom':
return trim($fnUn($arrRow['par_prenom'] ?? '') . ' ' . $fnUn($arrRow['par_nom'] ?? ''));
case 'par_age':
return trim((string)($arrRow['par_age'] ?? $arrRow['age'] ?? ''));
case 'par_sexe':
return trim((string)($arrRow['par_sexe'] ?? ''));
case 'epr_nom':
return trim($fnUn($arrRow['epr_nom'] ?? $arrRow['epr_libelle'] ?? ''));
default:
return '';
}
}
/**
* Layout mm pour le PDF — lit le JSON calibré (N boîtes).
*
* @param int $intEveId
* @return array<string,mixed>
*/
function fxDossardPrintLayout($intEveId = 0)
{
$arrSize = fxDossardPrintPageSizeMm();
$fltW = $arrSize[0];
$fltH = $arrSize[1];
$arrLayout = fxDossardPrintLoadLayout($intEveId);
$arrBoxesMm = array();
foreach ($arrLayout['boxes'] as $arrBox) {
$arrRect = fxDossardPrintBoxFromPct($fltW, $fltH, $arrBox);
$arrBoxesMm[] = array_merge($arrBox, $arrRect);
}
$fltQr = 28.0;
return array(
'page_w' => $fltW,
'page_h' => $fltH,
'boxes' => $arrBoxesMm,
'ascent_ratio' => 0.72,
'qr_size' => $fltQr,
'qr_x' => $fltW - $fltQr - 11.5,
'qr_y' => $fltH - $fltQr - 14.5,
);
}
/**
* Rectangle mm depuis fractions 01 de la page.
*
* @param float $fltPageW
* @param float $fltPageH
* @param array<string,float> $arrPct left|right|top|bottom
* @return array{x:float,y:float,w:float,h:float}
*/
function fxDossardPrintBoxFromPct($fltPageW, $fltPageH, $arrPct)
{
$fltX = $fltPageW * floatval($arrPct['left']);
$fltY = $fltPageH * floatval($arrPct['top']);
$fltW = $fltPageW * (floatval($arrPct['right']) - floatval($arrPct['left']));
$fltH = $fltPageH * (floatval($arrPct['bottom']) - floatval($arrPct['top']));
return array(
'x' => $fltX,
'y' => $fltY,
'w' => $fltW,
'h' => $fltH,
);
}
/**
* Plus grande police Helvetica Bold qui tient dans une boîte (largeur + hauteur).
*
* @param FPDF $pdf
* @param string $strText
* @param float $fltMaxW
* @param float $fltMaxH
* @param int $intFontMax
* @param float $fltAscentRatio
* @return int
*/
function fxDossardPrintFitBibFont($pdf, $strText, $fltMaxW, $fltMaxH, $intFontMax, $fltAscentRatio = 0.72)
{
$strPdf = fxDossardPrintPdfText($strText);
$fltAscentRatio = ($fltAscentRatio > 0) ? $fltAscentRatio : 0.72;
$fltPtPerMm = 72 / 25.4;
$intCapByH = ($fltMaxH > 0)
? (int)floor($fltMaxH * $fltPtPerMm / $fltAscentRatio)
: intval($intFontMax);
$intHi = max(12, min(intval($intFontMax), $intCapByH));
$intLo = 12;
$intBest = $intLo;
while ($intLo <= $intHi) {
$intMid = (int)floor(($intLo + $intHi) / 2);
$pdf->SetFont('Helvetica', 'B', $intMid);
if ($pdf->GetStringWidth($strPdf) <= $fltMaxW) {
$intBest = $intMid;
$intLo = $intMid + 1;
} else {
$intHi = $intMid - 1;
}
}
return $intBest;
}
/**
* Texte dans une boîte (Cell FPDF : align H + centre V de la cellule).
*
* @param FPDF $pdf
* @param array $arrBox x,y,w,h
* @param string $strText
* @param int $intFont
* @param string $strAlign C|L|R
*/
function fxDossardPrintDrawInBox($pdf, $arrBox, $strText, $intFont, $strAlign = 'C')
{
$strAlign = strtoupper((string)$strAlign);
if ($strAlign !== 'L' && $strAlign !== 'R') {
$strAlign = 'C';
}
$pdf->SetFont('Helvetica', 'B', $intFont);
$pdf->SetXY($arrBox['x'], $arrBox['y']);
$pdf->Cell(
$arrBox['w'],
$arrBox['h'],
fxDossardPrintPdfText($strText),
0,
0,
$strAlign
);
}
/** @deprecated alias — préfère fxDossardPrintDrawInBox(..., 'C') */
function fxDossardPrintDrawCenteredInBox($pdf, $arrBox, $strText, $intFont)
{
fxDossardPrintDrawInBox($pdf, $arrBox, $strText, $intFont, 'C');
}
/**
* 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($intEveId);
$strQrFile = fxDossardPrintBuildQrTempFile($intEveId);
$strTplType = fxDossardPrintImageType($strTemplate);
$pdf = new FPDF('L', 'mm', array($arrLayout['page_w'], $arrLayout['page_h']));
$pdf->SetAutoPageBreak(false);
$pdf->SetMargins(0, 0, 0);
$pdf->AddPage();
$pdf->Image(
$strTemplate,
0,
0,
$arrLayout['page_w'],
$arrLayout['page_h'],
$strTplType
);
$pdf->SetTextColor(0, 0, 0);
// MSIN-4485 — N boîtes configurables (champ + align + %)
$strBibForFile = '-';
foreach ($arrLayout['boxes'] as $arrBox) {
$strText = fxDossardPrintResolveFieldValue($arrRow, $arrBox['field']);
if ($arrBox['field'] === 'no_bib') {
$strBibForFile = $strText;
}
if ($strText === '') {
continue;
}
$intFont = fxDossardPrintFitBibFont(
$pdf,
$strText,
$arrBox['w'] * 0.98,
$arrBox['h'] * 0.92,
intval($arrBox['font_max']),
$arrLayout['ascent_ratio']
);
fxDossardPrintDrawInBox($pdf, $arrBox, $strText, $intFont, $arrBox['align']);
}
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_-]+/', '-', $strBibForFile);
$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);
@ -1416,6 +1416,7 @@ function fxPromoteurHubGetProduits($intEveId, $strLangue = 'fr')
$sql = "
SELECT p1.pro_id, p1.pro_nom_fr, p1.pro_nom_en, p1.pro_qte AS dispo,
p1.pro_date_limite,
COALESCE(v.vendus, 0) AS vendus,
COALESCE(pa.panier, 0) AS panier
FROM inscriptions_produits_new p1
@ -1467,6 +1468,14 @@ function fxPromoteurHubGetProduits($intEveId, $strLangue = 'fr')
// pour suivre les ventes sans gerer la limite -> illimite.
$blnIllimite = ($intTotal >= PHUB_SEUIL_ILLIMITE);
// MSIN-4454 — fin de vente (vide / 0000-00-00 = sans limite)
$strDateLimite = trim((string)($row['pro_date_limite'] ?? ''));
if ($strDateLimite === '' || strpos($strDateLimite, '0000-00-00') === 0) {
$strDateLimite = '';
} else {
$strDateLimite = substr($strDateLimite, 0, 10);
}
$arrProduits[] = array(
'pro_id' => intval($row['pro_id']),
'nom' => ($strNom !== '') ? $strNom : ('#' . intval($row['pro_id'])),
@ -1475,6 +1484,7 @@ function fxPromoteurHubGetProduits($intEveId, $strLangue = 'fr')
'panier' => $intPanier,
'restant' => $intRestant,
'illimite' => $blnIllimite,
'date_limite' => $strDateLimite,
);
}
@ -1483,7 +1493,7 @@ function fxPromoteurHubGetProduits($intEveId, $strLangue = 'fr')
/**
* Rendu de la section « Produits en vente » (liste produits + quantites).
* MSIN-4454 — Gros chiffre = vendus / total ; pastille = disponibles.
* MSIN-4454 — Gros chiffre = vendus / total ; pastille = disponibles ; date fin vente.
* N'affiche rien si aucun produit.
*/
function fxPromoteurHubRenderProduits($arrProduits, $strLangue = 'fr')
@ -1502,6 +1512,8 @@ function fxPromoteurHubRenderProduits($arrProduits, $strLangue = 'fr')
$blnIllimite = !empty($arrProd['illimite']);
$blnEpuise = (!$blnIllimite && intval($arrProd['restant']) <= 0);
$strState = $blnIllimite ? 'illimitee' : ($blnEpuise ? 'complete' : 'ok');
$strDateLimite = trim((string)($arrProd['date_limite'] ?? ''));
$strLblLimite = fxPromoteurHubTexte('promoteur_hub_badge_limite', 0);
$html .= '<div class="phub-prod-tile phub-etat-' . $strState . '">';
@ -1529,6 +1541,13 @@ function fxPromoteurHubRenderProduits($arrProduits, $strLangue = 'fr')
. '<i class="fa fa-shopping-cart mr-1" aria-hidden="true"></i>' . intval($arrProd['panier']) . ' '
. fxPromoteurHubEsc(fxPromoteurHubTexte('promoteur_hub_paniers_lbl', 0)) . '</span>';
}
// MSIN-4454 — date fin de vente (pro_date_limite) ; clé Info déjà en place
if ($strDateLimite !== '') {
$html .= '<span class="phub-chip phub-chip-date" title="' . fxPromoteurHubEsc($strLblLimite) . '">'
. '<i class="fa fa-calendar-o mr-1" aria-hidden="true"></i>'
. fxPromoteurHubEsc($strLblLimite) . ' '
. fxShowDate($strDateLimite, $strLangue, 1) . '</span>';
}
$html .= '</div>'; // .phub-epr-chips
$html .= '</div>'; // .phub-prod-tile
@ -1587,7 +1606,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 +1616,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,36 @@
<?php
/**
* MSIN-4485 — Sauvegarde layout dossard (N boîtes / champs).
*/
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_dossard_print.php';
fxSuperadmAjaxAuthOrJsonExit();
if (session_status() === PHP_SESSION_ACTIVE) {
session_write_close();
}
while (ob_get_level() > 0) {
ob_end_clean();
}
header('Content-Type: application/json; charset=utf-8');
$strRaw = file_get_contents('php://input');
$arrIn = is_string($strRaw) ? json_decode($strRaw, true) : null;
if (!is_array($arrIn)) {
http_response_code(400);
echo json_encode(array('ok' => false, 'error' => 'JSON invalide.'));
exit;
}
$intEveId = isset($arrIn['eve_id']) ? intval($arrIn['eve_id']) : 0;
$arrResult = fxDossardPrintSaveLayout($arrIn, $intEveId);
if (empty($arrResult['ok'])) {
http_response_code(400);
}
echo json_encode($arrResult);
exit;

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;
}

526
superadm/dossard_layout.php Normal file
View File

@ -0,0 +1,526 @@
<?php
/**
* MSIN-4485 — Calibrage GUI dossard : N boîtes glissables, champ + alignement.
*/
$strLangue = 'fr';
require_once __DIR__ . '/php/inc_start_time.php';
require_once __DIR__ . '/php/inc_functions.php';
require_once dirname(__DIR__) . '/php/inc_fx_dossard_print.php';
if (empty($_SESSION['usa_id'])) {
header('Location: login.php');
exit;
}
$intEveId = isset($_GET['eve_id']) ? intval($_GET['eve_id']) : 0;
$arrLayout = fxDossardPrintLoadLayout($intEveId);
$arrCatalog = fxDossardPrintFieldCatalog();
$strTplWeb = '../assets/dossard_templates/default.jpg';
if ($intEveId > 0) {
foreach (array('jpg', 'jpeg', 'png') as $strExt) {
$strCand = dirname(__DIR__) . '/assets/dossard_templates/eve_' . $intEveId . '.' . $strExt;
if (is_file($strCand)) {
$strTplWeb = '../assets/dossard_templates/eve_' . $intEveId . '.' . $strExt;
break;
}
}
}
$strLayoutJson = json_encode($arrLayout, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$strCatalogJson = json_encode($arrCatalog, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MSIN-4485 — Calibrage dossard</title>
<style>
:root {
--bg: #1a1d23;
--panel: #252a33;
--text: #e8eaed;
--muted: #9aa0a6;
--ok: #43a047;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: Segoe UI, system-ui, sans-serif;
background: var(--bg);
color: var(--text);
}
header {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: var(--panel);
border-bottom: 1px solid #333;
}
header h1 { margin: 0; font-size: 1.05rem; font-weight: 600; }
header p { margin: 4px 0 0; color: var(--muted); font-size: 0.85rem; }
.actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
button {
appearance: none;
border: 0;
border-radius: 6px;
padding: 8px 14px;
font-size: 0.9rem;
cursor: pointer;
color: #fff;
background: #455a64;
}
button.primary { background: var(--ok); font-weight: 600; }
button.danger { background: #c62828; }
button:disabled { opacity: 0.5; cursor: wait; }
#status { font-size: 0.85rem; color: var(--muted); min-width: 10rem; }
#status.ok { color: #81c784; }
#status.err { color: #ef9a9a; }
.layout {
display: grid;
grid-template-columns: minmax(240px, 300px) 1fr;
gap: 16px;
padding: 16px;
align-items: start;
}
@media (max-width: 900px) {
.layout { grid-template-columns: 1fr; }
}
.sidebar {
background: var(--panel);
border-radius: 8px;
padding: 12px;
}
.sidebar h2 {
margin: 0 0 10px;
font-size: 0.9rem;
color: var(--muted);
font-weight: 600;
}
.box-row {
border: 1px solid #3a414d;
border-radius: 6px;
padding: 8px;
margin-bottom: 8px;
}
.box-row.active { border-color: #90caf9; }
.box-row .row-top {
display: flex;
gap: 6px;
align-items: center;
margin-bottom: 6px;
}
.swatch {
width: 12px;
height: 12px;
border-radius: 2px;
flex: 0 0 auto;
border: 1px solid rgba(255,255,255,.4);
}
.box-row select, .box-row input {
width: 100%;
background: #1a1d23;
color: var(--text);
border: 1px solid #444;
border-radius: 4px;
padding: 4px 6px;
font-size: 0.8rem;
}
.box-row .grid2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
.box-row label {
display: block;
font-size: 0.7rem;
color: var(--muted);
margin-bottom: 2px;
}
.stage-wrap { min-width: 0; }
#stage {
position: relative;
width: 100%;
aspect-ratio: 8.5 / 5.5;
background: #000 center / 100% 100% no-repeat;
border: 1px solid #444;
overflow: hidden;
user-select: none;
touch-action: none;
}
.box {
position: absolute;
border: 2px solid;
background: rgba(255,255,255,.12);
display: flex;
align-items: center;
cursor: move;
min-width: 40px;
min-height: 24px;
}
.box.selected { box-shadow: 0 0 0 2px rgba(255,255,255,.35); }
.box .label {
font-weight: 800;
pointer-events: none;
text-shadow: 0 0 4px #fff, 0 0 2px #fff;
white-space: nowrap;
overflow: hidden;
max-width: 95%;
font-size: clamp(12px, 4vh, 48px);
}
.box.align-C { justify-content: center; }
.box.align-L { justify-content: flex-start; padding-left: 2px; }
.box.align-R { justify-content: flex-end; padding-right: 2px; }
.handle {
position: absolute;
width: 12px;
height: 12px;
background: #fff;
border: 2px solid currentColor;
right: -6px;
bottom: -6px;
cursor: nwse-resize;
}
.pct {
margin-top: 10px;
font-family: ui-monospace, Consolas, monospace;
font-size: 0.7rem;
color: var(--muted);
white-space: pre-wrap;
background: var(--panel);
padding: 10px 12px;
border-radius: 6px;
max-height: 180px;
overflow: auto;
}
</style>
</head>
<body>
<header>
<div>
<h1>Calibrage dossard — boîtes + champs</h1>
<p>Glisse / resize · champ + alignement · ajoute des boîtes (nom, âge…) pour plus tard</p>
</div>
<div class="actions">
<button type="button" id="btnAdd">+ Boîte</button>
<button type="button" id="btnReset">Réinitialiser</button>
<button type="button" class="primary" id="btnSave">Enregistrer</button>
<span id="status"></span>
</div>
</header>
<div class="layout">
<aside class="sidebar">
<h2>Boîtes</h2>
<div id="boxList"></div>
</aside>
<div class="stage-wrap">
<div id="stage" style="background-image:url('<?php echo htmlspecialchars($strTplWeb, ENT_QUOTES, 'UTF-8'); ?>');"></div>
<div class="pct" id="pctOut"></div>
</div>
</div>
<script>
(function () {
var eveId = <?php echo (int)$intEveId; ?>;
var catalog = <?php echo $strCatalogJson; ?>;
var colors = ['#e53935', '#1e88e5', '#43a047', '#fb8c00', '#8e24aa', '#00897b', '#f4511e'];
var initial = <?php echo $strLayoutJson; ?>;
var layout = JSON.parse(JSON.stringify(initial));
var stage = document.getElementById('stage');
var boxList = document.getElementById('boxList');
var statusEl = document.getElementById('status');
var pctOut = document.getElementById('pctOut');
var selectedId = null;
var drag = null;
var uid = 0;
function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
function ensureIds() {
layout.boxes.forEach(function (b, i) {
if (!b.id) b.id = 'box_' + (++uid);
});
}
function colorFor(i) { return colors[i % colors.length]; }
function sampleFor(field) {
return (catalog[field] && catalog[field].sample) ? catalog[field].sample : field;
}
function defaultFont(field) {
return (catalog[field] && catalog[field].font_max) ? catalog[field].font_max : 24;
}
function renderList() {
boxList.innerHTML = '';
layout.boxes.forEach(function (b, i) {
var row = document.createElement('div');
row.className = 'box-row' + (b.id === selectedId ? ' active' : '');
row.dataset.id = b.id;
var top = document.createElement('div');
top.className = 'row-top';
var sw = document.createElement('span');
sw.className = 'swatch';
sw.style.background = colorFor(i);
top.appendChild(sw);
var title = document.createElement('strong');
title.style.fontSize = '0.8rem';
title.style.flex = '1';
title.textContent = (catalog[b.field] && catalog[b.field].label) ? catalog[b.field].label : b.field;
top.appendChild(title);
var btnDel = document.createElement('button');
btnDel.type = 'button';
btnDel.className = 'danger';
btnDel.style.padding = '2px 8px';
btnDel.textContent = '×';
btnDel.title = 'Supprimer';
btnDel.addEventListener('click', function (e) {
e.stopPropagation();
if (layout.boxes.length <= 1) {
statusEl.textContent = 'Garde au moins une boîte.';
statusEl.className = 'err';
return;
}
layout.boxes = layout.boxes.filter(function (x) { return x.id !== b.id; });
if (selectedId === b.id) selectedId = layout.boxes[0].id;
renderAll();
});
top.appendChild(btnDel);
row.appendChild(top);
var g = document.createElement('div');
g.className = 'grid2';
var lf = document.createElement('div');
var labF = document.createElement('label');
labF.textContent = 'Champ';
var selF = document.createElement('select');
Object.keys(catalog).forEach(function (k) {
var o = document.createElement('option');
o.value = k;
o.textContent = catalog[k].label;
if (k === b.field) o.selected = true;
selF.appendChild(o);
});
selF.addEventListener('change', function () {
b.field = selF.value;
b.font_max = defaultFont(b.field);
renderAll();
});
lf.appendChild(labF);
lf.appendChild(selF);
g.appendChild(lf);
var la = document.createElement('div');
var labA = document.createElement('label');
labA.textContent = 'Alignement';
var selA = document.createElement('select');
[['C', 'Centre'], ['L', 'Gauche'], ['R', 'Droite']].forEach(function (pair) {
var o = document.createElement('option');
o.value = pair[0];
o.textContent = pair[1];
if (pair[0] === b.align) o.selected = true;
selA.appendChild(o);
});
selA.addEventListener('change', function () {
b.align = selA.value;
renderAll();
});
la.appendChild(labA);
la.appendChild(selA);
g.appendChild(la);
row.appendChild(g);
row.addEventListener('click', function () {
selectedId = b.id;
renderAll();
});
boxList.appendChild(row);
});
}
function renderStage() {
stage.innerHTML = '';
layout.boxes.forEach(function (b, i) {
var el = document.createElement('div');
el.className = 'box align-' + (b.align || 'C') + (b.id === selectedId ? ' selected' : '');
el.dataset.id = b.id;
el.style.borderColor = colorFor(i);
el.style.color = colorFor(i);
el.style.left = (b.left * 100) + '%';
el.style.top = (b.top * 100) + '%';
el.style.width = ((b.right - b.left) * 100) + '%';
el.style.height = ((b.bottom - b.top) * 100) + '%';
if (b.field === 'no_bib') {
el.querySelector && null;
el.style.fontSize = '';
}
var lab = document.createElement('span');
lab.className = 'label';
lab.textContent = sampleFor(b.field);
if (b.field === 'no_bib') lab.style.fontSize = 'clamp(18px, 8vh, 72px)';
el.appendChild(lab);
var h = document.createElement('div');
h.className = 'handle';
h.setAttribute('data-resize', '1');
el.appendChild(h);
stage.appendChild(el);
});
pctOut.textContent = JSON.stringify(layout, null, 2);
}
function renderAll() {
ensureIds();
renderList();
renderStage();
}
function findBox(id) {
for (var i = 0; i < layout.boxes.length; i++) {
if (layout.boxes[i].id === id) return layout.boxes[i];
}
return null;
}
function readFromDom(id) {
var el = stage.querySelector('.box[data-id="' + id + '"]');
if (!el) return null;
var r = stage.getBoundingClientRect();
var br = el.getBoundingClientRect();
return {
left: clamp((br.left - r.left) / r.width, 0, 1),
top: clamp((br.top - r.top) / r.height, 0, 1),
right: clamp((br.right - r.left) / r.width, 0, 1),
bottom: clamp((br.bottom - r.top) / r.height, 0, 1)
};
}
stage.addEventListener('pointerdown', function (e) {
var el = e.target.closest('.box');
if (!el || !stage.contains(el)) return;
e.preventDefault();
var id = el.getAttribute('data-id');
selectedId = id;
var resize = !!e.target.closest('[data-resize]');
var r = stage.getBoundingClientRect();
var br = el.getBoundingClientRect();
drag = {
id: id,
resize: resize,
startX: e.clientX,
startY: e.clientY,
orig: {
left: (br.left - r.left) / r.width,
top: (br.top - r.top) / r.height,
right: (br.right - r.left) / r.width,
bottom: (br.bottom - r.top) / r.height
}
};
renderAll();
el.setPointerCapture && el.setPointerCapture(e.pointerId);
});
window.addEventListener('pointermove', function (e) {
if (!drag) return;
var b = findBox(drag.id);
if (!b) return;
var r = stage.getBoundingClientRect();
var dx = (e.clientX - drag.startX) / r.width;
var dy = (e.clientY - drag.startY) / r.height;
var o = drag.orig;
if (drag.resize) {
b.left = o.left;
b.top = o.top;
b.right = clamp(o.right + dx, o.left + 0.04, 1);
b.bottom = clamp(o.bottom + dy, o.top + 0.03, 1);
} else {
var w = o.right - o.left;
var h = o.bottom - o.top;
b.left = clamp(o.left + dx, 0, 1 - w);
b.top = clamp(o.top + dy, 0, 1 - h);
b.right = b.left + w;
b.bottom = b.top + h;
}
renderStage();
});
window.addEventListener('pointerup', function () {
if (!drag) return;
var b = findBox(drag.id);
var edges = readFromDom(drag.id);
if (b && edges) {
b.left = edges.left;
b.top = edges.top;
b.right = edges.right;
b.bottom = edges.bottom;
}
drag = null;
renderAll();
});
document.getElementById('btnAdd').addEventListener('click', function () {
uid++;
layout.boxes.push({
id: 'box_' + Date.now(),
field: 'par_nom',
align: 'L',
font_max: defaultFont('par_nom'),
left: 0.35,
right: 0.65,
top: 0.55,
bottom: 0.65
});
selectedId = layout.boxes[layout.boxes.length - 1].id;
renderAll();
});
document.getElementById('btnReset').addEventListener('click', function () {
layout = JSON.parse(JSON.stringify(initial));
selectedId = layout.boxes[0] ? layout.boxes[0].id : null;
renderAll();
statusEl.textContent = 'Réinitialisé (pas encore sauvé).';
statusEl.className = '';
});
document.getElementById('btnSave').addEventListener('click', function () {
var btn = document.getElementById('btnSave');
btn.disabled = true;
statusEl.textContent = 'Enregistrement…';
statusEl.className = '';
fetch('ajax_dossard_layout.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
eve_id: eveId,
version: 2,
boxes: layout.boxes
})
})
.then(function (r) { return r.json(); })
.then(function (data) {
if (data && data.ok) {
initial = JSON.parse(JSON.stringify(layout));
statusEl.textContent = 'Enregistré — PDF = ces boîtes.';
statusEl.className = 'ok';
} else {
statusEl.textContent = (data && data.error) ? data.error : 'Erreur sauvegarde';
statusEl.className = 'err';
}
})
.catch(function () {
statusEl.textContent = 'Erreur réseau';
statusEl.className = 'err';
})
.finally(function () { btn.disabled = false; });
});
selectedId = layout.boxes[0] ? layout.boxes[0].id : null;
renderAll();
})();
</script>
</body>
</html>

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);