Compare commits
33 Commits
db7b2e9f46
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| a62cca6e45 | |||
| 83dc15ec9f | |||
| 3320a26ada | |||
| f0e0feeeb9 | |||
| 762b31083e | |||
| 358673cada | |||
| eed49c4745 | |||
| 8eb0c644f3 | |||
| 6f7d8c5ef8 | |||
| ed992d2903 | |||
| aee3467918 | |||
| 087ef47e15 | |||
| aabb419e7d | |||
| 3a7c1f4bb7 | |||
| 36eb81d8e8 | |||
| d4ab2e2dc7 | |||
| 8cd023e224 | |||
| 8788f22a81 | |||
| fd7930b139 | |||
| a68f54be80 | |||
| 038bd7bd26 | |||
| 69a97c0044 | |||
| 26a470fe1e | |||
| 80c791c7e1 | |||
| 05ce600e09 | |||
| c6477f7757 | |||
| 8498af24f9 | |||
| 171a48a411 | |||
| d63c92e36a | |||
| 8929236e85 | |||
| 84e65340b6 | |||
| a1b6fa8911 | |||
| 1ca75a4ed4 |
42
.cursor/rules/entreprise/00-cout-benefice.mdc
Normal file
42
.cursor/rules/entreprise/00-cout-benefice.mdc
Normal file
@ -0,0 +1,42 @@
|
||||
---
|
||||
description: Progiweb — toujours proposer l’option au meilleur coût-bénéfice (pas l’essai-erreur)
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Coût-bénéfice — qualité d’approche
|
||||
|
||||
Référentiel agent. L’utilisateur veut **toujours coder au meilleur** : proposer d’abord la façon qui minimise le temps total (humain + agent) pour un résultat commercialement fiable.
|
||||
|
||||
## Règle
|
||||
|
||||
Avant d’implémenter (surtout UI print, layout, mapping, calibrage, « ça a l’air bon ») :
|
||||
|
||||
1. **Identifier** si la tâche invite à l’essai-erreur (deviner des mm/%, relire des screenshots, retoucher au feeling).
|
||||
2. **Proposer d’abord** l’option au meilleur coût-bénéfice — même si elle demande 30–60 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** (1–3) 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 d’impression « 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 d’essais 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 d’attente, volume). Ne pas imposer la coupe comme solution « technique » sans accord produit.
|
||||
|
||||
## Ce que ce n’est pas
|
||||
|
||||
- Pas d’over-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 d’approche
|
||||
|
||||
- Dire clairement : « Je recommande X parce que Y (moins d’aller-retours / une calibration / réutilisable). »
|
||||
- Si l’utilisateur pousse l’essai-erreur : rappeler une fois l’alternative, puis suivre s’il insiste.
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 d’inscription / 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 d’inscription / 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 l’instant** — hub actuel tel quel |
|
||||
| Qui est promoteur | Opérateur MS1 se met **lui-même** promoteur ; droits clients = kits **minimisés via l’existant** |
|
||||
| Mini-hub | **Plus tard**, au fur et à mesure — pas un livrable immédiat |
|
||||
| Waiver | **Hors scope** |
|
||||
| Édition / saisie rapide | Après l’import (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 ; aujourd’hui `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 d’import générique participants aujourd’hui (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 ; l’original 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, ~10–12 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 n’est pas redemandé.
|
||||
- Ne pas inventer de waiver pour « signature de dossier ».
|
||||
- SQL : **dev préprod** seulement, puis Navicat + sync outil.
|
||||
|
||||
@ -32,6 +32,7 @@ Ordre **non négociable** pour faire passer structure + données vers d’autres
|
||||
- **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 qu’une série d’essais PDF.
|
||||
|
||||
## Incohérences repérées
|
||||
|
||||
|
||||
1
assets/dossard_templates/.gitkeep
Normal file
1
assets/dossard_templates/.gitkeep
Normal file
@ -0,0 +1 @@
|
||||
|
||||
BIN
assets/dossard_templates/default.jpg
Normal file
BIN
assets/dossard_templates/default.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 219 KiB |
27
assets/dossard_templates/default.layout.json
Normal file
27
assets/dossard_templates/default.layout.json
Normal 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"
|
||||
}
|
||||
BIN
assets/dossard_templates/mamie-joe-papi-fan.jpg
Normal file
BIN
assets/dossard_templates/mamie-joe-papi-fan.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 219 KiB |
49
dossard_print.php
Normal file
49
dossard_print.php
Normal 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);
|
||||
BIN
images/te_sans_inscription_en_ligne.png
Normal file
BIN
images/te_sans_inscription_en_ligne.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 821 KiB |
662
php/inc_fx_dossard_print.php
Normal file
662
php/inc_fx_dossard_print.php
Normal 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 d’impression (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 0–1 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;
|
||||
}
|
||||
@ -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>
|
||||
|
||||
2737
php/inc_fx_import_resultats.php
Normal file
2737
php/inc_fx_import_resultats.php
Normal file
File diff suppressed because it is too large
Load Diff
@ -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> '
|
||||
. fxInscrGestionEsc(fxInscrGestionT('inscr_gestion_btn_dossard_print'))
|
||||
. '</a>';
|
||||
}
|
||||
echo '</div>';
|
||||
|
||||
$strDetailsType = $blnEquipe && fxIsParEquipe($arrRow['pec_id_original'], $arrRow['epr_id'])
|
||||
|
||||
@ -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)
|
||||
|
||||
108
sql/MSIN-4482-fix-pec-actif-import.sql
Normal file
108
sql/MSIN-4482-fix-pec-actif-import.sql
Normal 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.
|
||||
56
sql/MSIN-4482-fix-questions-fiche-import.sql
Normal file
56
sql/MSIN-4482-fix-questions-fiche-import.sql
Normal 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;
|
||||
63
sql/MSIN-4482-import-resultats-templates.sql
Normal file
63
sql/MSIN-4482-import-resultats-templates.sql
Normal 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;
|
||||
45
sql/MSIN-4482-type-sans-inscription-en-ligne.sql
Normal file
45
sql/MSIN-4482-type-sans-inscription-en-ligne.sql
Normal 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;
|
||||
15
sql/MSIN-4482-urgence-rollback-te-1284.sql
Normal file
15
sql/MSIN-4482-urgence-rollback-te-1284.sql
Normal 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;
|
||||
31
sql/MSIN-4484-perm-grants-all.sql
Normal file
31
sql/MSIN-4484-perm-grants-all.sql
Normal 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 : n’est 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;
|
||||
72
sql/MSIN-4485-dossard-print.sql
Normal file
72
sql/MSIN-4485-dossard-print.sql
Normal 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());
|
||||
36
superadm/ajax_dossard_layout.php
Normal file
36
superadm/ajax_dossard_layout.php
Normal 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;
|
||||
134
superadm/ajax_import_resultats.php
Normal file
134
superadm/ajax_import_resultats.php
Normal 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;
|
||||
@ -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
526
superadm/dossard_layout.php
Normal 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>
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
Reference in New Issue
Block a user