Compare commits
102 Commits
feature/MS
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| aee3467918 | |||
| 087ef47e15 | |||
| aabb419e7d | |||
| 3a7c1f4bb7 | |||
| 36eb81d8e8 | |||
| d4ab2e2dc7 | |||
| 8cd023e224 | |||
| 8788f22a81 | |||
| fd7930b139 | |||
| a68f54be80 | |||
| 038bd7bd26 | |||
| 69a97c0044 | |||
| 26a470fe1e | |||
| 80c791c7e1 | |||
| 05ce600e09 | |||
| c6477f7757 | |||
| 8498af24f9 | |||
| 171a48a411 | |||
| d63c92e36a | |||
| 8929236e85 | |||
| 84e65340b6 | |||
| a1b6fa8911 | |||
| 1ca75a4ed4 | |||
| db7b2e9f46 | |||
| b67fdaee78 | |||
| fd250996b2 | |||
| 797223e976 | |||
| 5961b40349 | |||
| 447dc5a7dc | |||
| 5406e7e3db | |||
| 685a1fd4f3 | |||
| f93f6d14ff | |||
| d15a68285c | |||
| f3233eb19b | |||
| a763a7b230 | |||
| f76fb95fcd | |||
| 5b6faf93f9 | |||
| 88a8010a18 | |||
| 5ceeec7fdd | |||
| 9697b37263 | |||
| 18b667d24b | |||
| c154196090 | |||
| 1306770f2e | |||
| ab063aeb23 | |||
| 36631dc201 | |||
| 4851a50279 | |||
| 03cb227b5d | |||
| 1612d91ac6 | |||
| e27e2b5bc0 | |||
| b18407f64b | |||
| 46e34d4dcc | |||
| 839d865d7c | |||
| 10e6a09640 | |||
| e747b180aa | |||
| 57e281b46f | |||
| 847f2f57a7 | |||
| 23def197c8 | |||
| b9e4395297 | |||
| 0a19621949 | |||
| 162419069b | |||
| b74d5ac34a | |||
| 26b658acf8 | |||
| 25c626be45 | |||
| b038537d1c | |||
| 4b6e461b32 | |||
| dfab716014 | |||
| aa61dfef15 | |||
| 7858823205 | |||
| 4bb4d189e6 | |||
| 9143f7a967 | |||
| ad57a54407 | |||
| fd6ec9b876 | |||
| db5b1b9da3 | |||
| a8c588f41f | |||
| b97fa8a337 | |||
| 2d706fb628 | |||
| 9233c65351 | |||
| 7120453ec8 | |||
| 215d3777db | |||
| 22f45f98ff | |||
| 99aa63f0e1 | |||
| e701fd5543 | |||
| e85f25c21d | |||
| f74fbc3092 | |||
| f29ed77969 | |||
| 31a7c9d012 | |||
| afdf784215 | |||
| 0a35370a81 | |||
| 2cb9417bbf | |||
| 35f1a15fc7 | |||
| 39c062eafc | |||
| 8027155bde | |||
| c6be4c38a6 | |||
| eedb5d45d4 | |||
| 6d6e3c68f5 | |||
| 14727ce6b9 | |||
| 52d354bc9f | |||
| 9732763fc8 | |||
| 4bc61c370c | |||
| 62037febe2 | |||
| fd25dacaee | |||
| 397d82a458 |
@ -42,11 +42,12 @@ En-tête obligatoire en tête de fichier :
|
||||
```sql
|
||||
-- MSIN-xxxx — Titre court
|
||||
-- Contexte / prérequis (scripts à exécuter avant)
|
||||
-- Notes prod (idempotent, safe pilote, etc.)
|
||||
-- Notes : exécution UNIQUEMENT sur dev préprod ; autres env = Navicat structure + sync_static_db
|
||||
```
|
||||
|
||||
- Indiquer l'ordre d'exécution si plusieurs scripts forment une migration.
|
||||
- Préférer les scripts **idempotents** quand c'est possible (pattern déjà utilisé dans `sql/`).
|
||||
- **Ne jamais** documenter / conseiller d’exécuter ces scripts hors **dev préprod** (voir `projet/10-workflow.mdc` — Promotion BD).
|
||||
|
||||
## Résumé des changements (agent)
|
||||
|
||||
@ -54,6 +55,6 @@ En-tête obligatoire en tête de fichier :
|
||||
|
||||
1. **Quoi** — comportement ajouté ou corrigé.
|
||||
2. **Où** — fichiers principaux touchés.
|
||||
3. **Déploiement** — SQL, `_VERSION_CODE`, config, étapes manuelles.
|
||||
3. **Déploiement** — SQL **dev préprod seulement**, puis Navicat structure + outil sync pour les autres env ; `_VERSION_CODE` / config / étapes manuelles.
|
||||
|
||||
Ne pas créer de fichiers README ou docs hors demande explicite.
|
||||
|
||||
102
.cursor/rules/projet/10-hors-transaction-import.mdc
Normal file
102
.cursor/rules/projet/10-hors-transaction-import.mdc
Normal file
@ -0,0 +1,102 @@
|
||||
---
|
||||
description: Projet MS1 hors transaction — import / saisie rapide / ChronoTrack (cadrage produit)
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Projet MSIN-4482 — entrée hors transaction (import + terrain)
|
||||
|
||||
JIRA : **MSIN-4482**.
|
||||
Orthographe produit : **ChronoTrack**.
|
||||
Doc de cadrage — source de vérité pour le développement de ce chantier.
|
||||
|
||||
## Intention
|
||||
|
||||
Utiliser la suite MS1 (gestion, ChronoTrack, outils promoteur existants) pour des clients **sans** tunnel d’inscription / paiement en ligne MS1.
|
||||
Les participants arrivent surtout par **import fichier** (puis saisie rapide plus tard) ; ensuite sync ChronoTrack.
|
||||
|
||||
Aussi utile plus tard le **jour J** sur une course **avec** inscription en ligne : correction / ajout rapide **sans** transaction financière.
|
||||
|
||||
## Décisions actées
|
||||
|
||||
| Sujet | Décision |
|
||||
|---|---|
|
||||
| Source terrain | Toujours **`resultats_*`** — **pas de panier / `no_panier`** pour ces lignes |
|
||||
| Type événement | **`inscriptions_types_evenements`** — libellé FR **Sans inscription en ligne** / EN **No online registration** (`sql/MSIN-4482-type-sans-inscription-en-ligne.sql`) ; l’événement pointe via `inscriptions_evenements.te_id` |
|
||||
| Paiement MS1 | Hors scope ; **interdit** le « panier à 0 $ » |
|
||||
| Clé unique | ID du fichier client = **`external_id` ChronoTrack** au push ; même fichier = fallback import CT |
|
||||
| Questions import | **Option C** : matcher ; non matchées → preview + **création confirmée** |
|
||||
| Hub promoteur | **Ne pas toucher pour 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)
|
||||
|
||||
| Code | Canal |
|
||||
|---|---|
|
||||
| `inscription_en_ligne` | Flux payant actuel |
|
||||
| `import_fichier` | Bulk modèle fichier |
|
||||
| `saisie_rapide` | Création unitaire / jour J (plus tard) |
|
||||
|
||||
## Phases (réalignées)
|
||||
|
||||
1. **UI import superadm** — dropzone, mapping colonnes + épreuves, templates, bouton **Importer**
|
||||
2. **Écriture `resultats_*`** — upsert clé unique + questions C (sans panier) — en place
|
||||
3. **Mapping pays** (valeurs fichier → `pay_id`) — **étape 2**
|
||||
4. **ChronoTrack** — push avec `par_external_id`
|
||||
5. **Saisie / édition** — fiche rapide (+ jour J)
|
||||
6. **Hub minimisé** — seulement si/quand nécessaire
|
||||
|
||||
## UI import (décisions)
|
||||
|
||||
- Emplacement v1 : **superadm fiche événement** (panneau sous ChronoTrack ; ancre « Import »)
|
||||
- Colonne fichier pour l’épreuve → match vers `epr_id` de l’événement
|
||||
- UX : dropzone ludique + mapping guidé
|
||||
- Template mapping : table `inscriptions_import_templates` (empreinte en-têtes + eve/com)
|
||||
|
||||
## Exemple fichier client (référence)
|
||||
|
||||
Fichier type : export ~Échappée (en-têtes déjà normalisés ; l’original client peut différer).
|
||||
|
||||
| Colonne exemple | Cible MS1 |
|
||||
|---|---|
|
||||
| Attendee # | `par_external_id` (clé unique / CT) |
|
||||
| First Name / Last Name | `par_prenom` / `par_nom` |
|
||||
| Ticket Type | `epr` (valeurs → épreuves MS1) |
|
||||
| No dossard | `no_bib` |
|
||||
| Gender (Male/Female/Other) | `par_sexe` (normaliser à l’écriture) |
|
||||
| Age / Birth Date | `par_age` / `par_naissance` |
|
||||
| Nationalité, urgence, allergies, condition médicale, langue… | questions (option C) |
|
||||
|
||||
**Visibilité liste / CT** : les coquilles `resultats_epreuves_commandees` doivent avoir **`pec_actif = 1`** (sinon absentes de la liste inscriptions et du sync). Les `I{eve}X…` = faux `no_commande` technique, pas un vrai panier.
|
||||
|
||||
~1800 lignes, ~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.
|
||||
@ -1,5 +1,5 @@
|
||||
---
|
||||
description: Workflow MS1 — JIRA, validation avant gros changements, code legacy
|
||||
description: Workflow MS1 — JIRA, validation avant gros changements, code legacy, promotion BD
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
@ -8,10 +8,25 @@ alwaysApply: true
|
||||
## Numéro JIRA (MSIN-xxxx)
|
||||
|
||||
- Au début d'une tâche de développement (nouvelle feature, correctif, refactor), **demander le numéro JIRA** si l'utilisateur ne l'a pas fourni.
|
||||
- Préfixe courant features : `MSIN-xxxx`. Continuité / ops BD peut aussi être `CON-xxxx`.
|
||||
- Ne pas bloquer une simple question ou une lecture du code — seulement quand on s'apprête à modifier des fichiers.
|
||||
- Référencer le ticket dans le code et les scripts SQL : `// MSIN-4290` ou `-- MSIN-4379 — description`.
|
||||
- Nommer les nouveaux scripts SQL : `sql/MSIN-xxxx-description-courte.sql`.
|
||||
|
||||
## Promotion BD (règle absolue)
|
||||
|
||||
Ordre **non négociable** pour faire passer structure + données vers d’autres environnements :
|
||||
|
||||
1. **Scripts SQL** (`sql/MSIN-*.sql`) → exécutés **uniquement** sur **dev préprod**.
|
||||
2. **Navicat** → copie de **structure** (préprod → autres env : dev prod, client préprod, client prod).
|
||||
3. **Outil soft sync** (`php/sync_static_db.php`, CON-333) → peupler / aligner les **données** (manquants + corrections).
|
||||
|
||||
**Interdit** : demander ou suggérer de « rejouer le SQL » sur prod, préprod client, ou tout env autre que **dev préprod**.
|
||||
|
||||
- Table absente / Erreur lecture ailleurs = d’abord **structure Navicat** depuis préprod, **puis** sync outil.
|
||||
- `info` : jamais de DELETE (auto-création `afficheTexte`). Soft only.
|
||||
- Catalogues (droits, pages, doc…) : pas d’auto-création ; l’alignement données passe par l’outil, pas par un second import SQL.
|
||||
|
||||
## Portée des changements
|
||||
|
||||
- **Changement minimal** : corriger uniquement ce qui est demandé.
|
||||
@ -28,5 +43,6 @@ Quand du code voisin contredit le pattern du fichier ou du module :
|
||||
|
||||
## Fin de tâche
|
||||
|
||||
- Résumer ce qui a changé (fichiers, comportement, SQL à exécuter).
|
||||
- Mentionner les étapes manuelles restantes (déploiement, import SQL, cache).
|
||||
- Résumer ce qui a changé (fichiers, comportement).
|
||||
- Pour le SQL : rappeler qu’il s’exécute **seulement en dev préprod**, puis structure Navicat + sync outil pour le reste — **pas** « exécuter ce script en prod ».
|
||||
- Mentionner les étapes manuelles restantes (déploiement code, Navicat, sync soft, cache).
|
||||
|
||||
@ -57,6 +57,26 @@ if ($action !== '' && in_array($action, $arrBibAjaxActions, true)) {
|
||||
fxBibRequireAjaxAccess();
|
||||
}
|
||||
|
||||
// MSIN-4469 — Le fichier officiel verrouille toute modification des séquences.
|
||||
$arrBibProductionLockedActions = [
|
||||
'create',
|
||||
'delete',
|
||||
'save',
|
||||
'toggle_lock',
|
||||
'save_auto_config',
|
||||
'batch_global_gen_create',
|
||||
];
|
||||
if (in_array($action, $arrBibProductionLockedActions, true)) {
|
||||
$intProductionEveId = fxBibResolveEveIdFromRequest();
|
||||
if ($intProductionEveId > 0 && fxBibProductionEventIsLocked($intProductionEveId)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => fxBibMsg('bib_v5_production_locked_error'),
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// =====================
|
||||
// CREATE
|
||||
// =====================
|
||||
@ -715,7 +735,7 @@ if ($action == 'batch_global_free_analysis') {
|
||||
$intEnd = (int)($_POST['end_bib'] ?? 0);
|
||||
list($strSort1, $strSort2) = fxBibParseBatchSortFromPost();
|
||||
$eprIds = json_decode($_POST['epr_ids'] ?? '[]', true);
|
||||
$tabEprIds = fxBibParseGlobalBatchGeneratedEprIds(is_array($eprIds) ? $eprIds : [], $int_eve_id);
|
||||
$tabEprIds = fxBibParseGlobalBatchFreeEprIds(is_array($eprIds) ? $eprIds : [], $int_eve_id);
|
||||
|
||||
if ($int_eve_id <= 0 || $intStart <= 0 || $intEnd < $intStart) {
|
||||
echo json_encode([
|
||||
@ -765,7 +785,7 @@ if ($action == 'batch_global_free_simulate') {
|
||||
$intEnd = (int)($_POST['end_bib'] ?? 0);
|
||||
list($strSort1, $strSort2) = fxBibParseBatchSortFromPost();
|
||||
$eprIds = json_decode($_POST['epr_ids'] ?? '[]', true);
|
||||
$tabEprIds = fxBibParseGlobalBatchGeneratedEprIds(is_array($eprIds) ? $eprIds : [], $int_eve_id);
|
||||
$tabEprIds = fxBibParseGlobalBatchFreeEprIds(is_array($eprIds) ? $eprIds : [], $int_eve_id);
|
||||
|
||||
if ($int_eve_id <= 0 || $intStart <= 0 || $intEnd < $intStart || $strSort1 === '' || empty($tabEprIds)) {
|
||||
echo json_encode([
|
||||
@ -798,7 +818,7 @@ if ($action == 'batch_global_free_plan') {
|
||||
$intEnd = (int)($_POST['end_bib'] ?? 0);
|
||||
list($strSort1, $strSort2) = fxBibParseBatchSortFromPost();
|
||||
$eprIds = json_decode($_POST['epr_ids'] ?? '[]', true);
|
||||
$tabEprIds = fxBibParseGlobalBatchGeneratedEprIds(is_array($eprIds) ? $eprIds : [], $int_eve_id);
|
||||
$tabEprIds = fxBibParseGlobalBatchFreeEprIds(is_array($eprIds) ? $eprIds : [], $int_eve_id);
|
||||
|
||||
if ($int_eve_id <= 0 || $intStart <= 0 || $intEnd < $intStart || $strSort1 === '' || empty($tabEprIds)) {
|
||||
echo json_encode([
|
||||
@ -1108,7 +1128,7 @@ if ($action == 'batch_apply_free_chunk') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Même filtre que le panneau : solo sans séquence.
|
||||
// MSIN-4445 — Solo seulement ; une séquence existante n'empêche pas l'assignation libre.
|
||||
$intEveId = (int)($_POST['eve_id'] ?? 0);
|
||||
if ($intEveId > 0) {
|
||||
$checkEpr = fxBibAssertSoloEpreuveForGlobalBatch($epr_id, $intEveId);
|
||||
@ -1116,13 +1136,6 @@ if ($action == 'batch_apply_free_chunk') {
|
||||
echo json_encode($checkEpr);
|
||||
exit;
|
||||
}
|
||||
if (fxBibEpreuveHasAnySequence($epr_id)) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => fxBibMsg('bib_v4_global_batch_gen_has_seq'),
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$strSessionKey = fxBibBatchExecSessionKey($epr_id) . '_free';
|
||||
@ -1261,7 +1274,9 @@ if ($action == 'reset_apply') {
|
||||
|
||||
$sql = "
|
||||
UPDATE resultats_participants
|
||||
SET no_bib = NULL
|
||||
SET no_bib = NULL,
|
||||
par_date_bib = NULL,
|
||||
par_maj = '" . fxGetDateTime() . "'
|
||||
WHERE par_id = $par_id
|
||||
AND is_cancelled = 0
|
||||
";
|
||||
@ -1330,7 +1345,9 @@ if ($action == 'reset_all_apply') {
|
||||
|
||||
$sql = "
|
||||
UPDATE resultats_participants
|
||||
SET no_bib = NULL
|
||||
SET no_bib = NULL,
|
||||
par_date_bib = NULL,
|
||||
par_maj = '" . fxGetDateTime() . "'
|
||||
WHERE par_id = $par_id
|
||||
AND is_cancelled = 0
|
||||
";
|
||||
|
||||
@ -1,13 +1,22 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
// MSIN-4468 — Les libellés AJAX appartiennent toujours au contexte compte.php.
|
||||
define('MS1_PROMOTEUR_AJAX', true);
|
||||
$_SERVER['PHP_SELF'] = '/compte.php';
|
||||
$_SERVER['SCRIPT_NAME'] = '/compte.php';
|
||||
$vPage = 'compte.php';
|
||||
|
||||
require_once "php/inc_fonctions.php";
|
||||
include_once("php/inc_fx_modifierinscriptions.php");
|
||||
require_once "php/inc_fx_promoteur.php";
|
||||
require_once "php/inc_fx_messages.php";
|
||||
require_once "php/inc_fx_eve_acces.php";
|
||||
|
||||
global $vDomaine;
|
||||
$strAction = $intEvenement = $intNewNoBib = $intEpreuveCommandee = $intEpreuve = $intPartipcipant = $intQuantite = $strLangue = $strStatutCourse = '';
|
||||
$strAction = $intEvenement = $intNewNoBib = $intEpreuveCommandee = $intEpreuve = $intPartipcipant = $intQuantite = $strStatutCourse = '';
|
||||
$strLangue = $_SESSION['lang'] ?? 'fr';
|
||||
$strQuantiteTotale = '';
|
||||
|
||||
if (isset($_REQUEST['a'])) {
|
||||
$strAction = $_REQUEST['a'];
|
||||
@ -30,22 +39,65 @@ if (isset($_REQUEST['par_id'])) {
|
||||
if (isset($_REQUEST['qte'])) {
|
||||
$intQuantite = $_REQUEST['qte'];
|
||||
}
|
||||
if (isset($_REQUEST['lang'])) {
|
||||
if (isset($_REQUEST['total'])) {
|
||||
$strQuantiteTotale = trim((string)$_REQUEST['total']);
|
||||
}
|
||||
if (isset($_REQUEST['lang']) && in_array($_REQUEST['lang'], array('fr', 'en'), true)) {
|
||||
$strLangue = $_REQUEST['lang'];
|
||||
}
|
||||
if (isset($_REQUEST['par_statut_course'])) {
|
||||
$strStatutCourse = $_REQUEST['par_statut_course'];
|
||||
}
|
||||
|
||||
$vPage = 'compte.php';
|
||||
$vtexte_page = obtenirTextepage($vPage, $strLangue, 2);
|
||||
|
||||
$tabRetour = array();
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
switch($strAction) {
|
||||
case 'qte':
|
||||
$tabRetour = fxUpdateQuantites($intEpreuve, $intQuantite, $strLangue);
|
||||
break;
|
||||
case 'qte_total':
|
||||
// MSIN-4468 — Nouvelle convention V4; l'action legacy qte demeure inchangée.
|
||||
if (!fxBibRequirePromoteurSession()) {
|
||||
$tabRetour = array('state' => 'error', 'message' => fxBibMsg('bib_v4_ajax_session_invalid'));
|
||||
break;
|
||||
}
|
||||
if (!fxBibValidateCsrf($_POST['csrf_token'] ?? '')) {
|
||||
$tabRetour = array('state' => 'error', 'message' => fxBibMsg('bib_v4_ajax_unauthorized'));
|
||||
break;
|
||||
}
|
||||
|
||||
$intEveId = fxBibResolveEveIdFromRequest();
|
||||
$blnCanEditQte = !empty($_SESSION['usa_id'])
|
||||
|| (!empty($_SESSION['com_id'])
|
||||
&& $intEveId > 0
|
||||
&& fxEveAccesHasPermission($_SESSION['com_id'], $intEveId, 'epreuves.edit_qte'));
|
||||
if (!$blnCanEditQte) {
|
||||
$tabRetour = array('state' => 'error', 'message' => fxBibMsg('bib_v4_ajax_unauthorized'));
|
||||
break;
|
||||
}
|
||||
|
||||
if ($strQuantiteTotale === ''
|
||||
|| strlen($strQuantiteTotale) > 7
|
||||
|| !ctype_digit($strQuantiteTotale)) {
|
||||
$tabRetour = array('state' => 'error', 'message' => afficheTexte('promoteur_bad_qte_update', 0));
|
||||
break;
|
||||
}
|
||||
|
||||
$tabRetour = fxUpdateQuantiteTotale($intEpreuve, intval($strQuantiteTotale));
|
||||
if (($tabRetour['state'] ?? 'error') !== 'success') {
|
||||
if (($tabRetour['error_code'] ?? '') === 'total_below_registrations') {
|
||||
$tabRetour['message'] = fxBibMsg(
|
||||
'bib_v5_qte_total_min',
|
||||
intval($tabRetour['qte_inscrits'] ?? 0)
|
||||
);
|
||||
} else {
|
||||
$tabRetour['message'] = afficheTexte('promoteur_bad_qte_update', 0);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'no_equipe':
|
||||
case 'no_bib':
|
||||
$tabRetour = fxSetBibManuel($strAction, $intEvenement, $intNewNoBib, $intEpreuveCommandee, $intEpreuve, $intPartipcipant, $strLangue);
|
||||
|
||||
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 |
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 |
83
bib_production_excel.php
Normal file
83
bib_production_excel.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
// MSIN-4469 — Génération et retéléchargement du fichier officiel de production.
|
||||
|
||||
session_start();
|
||||
|
||||
$_SERVER['PHP_SELF'] = '/compte.php';
|
||||
$_SERVER['SCRIPT_NAME'] = '/compte.php';
|
||||
|
||||
require_once('php/inc_fonctions.php');
|
||||
require_once('php/inc_fx_promoteur.php');
|
||||
require_once('php/inc_fx_eve_acces.php');
|
||||
|
||||
$strLangue = $_SESSION['lang'] ?? 'fr';
|
||||
$strReqLang = $_REQUEST['lang'] ?? '';
|
||||
if (in_array($strReqLang, ['fr', 'en'], true)) {
|
||||
$strLangue = $strReqLang;
|
||||
}
|
||||
$vPage = 'compte.php';
|
||||
$vtexte_page = obtenirTextepage($vPage, $strLangue, 2);
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
exit;
|
||||
}
|
||||
|
||||
$intEveId = (int)($_POST['eve_id'] ?? 0);
|
||||
$strAction = (string)($_POST['action'] ?? '');
|
||||
|
||||
fxBibRequireDistPrintAccess();
|
||||
if (!fxBibProductionUserCanManageEvent($intEveId)) {
|
||||
http_response_code(403);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo fxBibMsg('bib_v4_ajax_unauthorized');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($strAction === 'generate') {
|
||||
$tabResult = fxBibProductionCreateOfficialOrder($intEveId, $strLangue);
|
||||
if (empty($tabResult['success']) || empty($tabResult['order'])) {
|
||||
http_response_code(400);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo $tabResult['message'] ?? fxBibMsg('bib_v5_production_generation_error');
|
||||
exit;
|
||||
}
|
||||
$tabOrder = $tabResult['order'];
|
||||
} elseif ($strAction === 'download') {
|
||||
$intOrderId = (int)($_POST['order_id'] ?? 0);
|
||||
$tabOrder = fxBibProductionGetOrder($intOrderId, $intEveId);
|
||||
if (!$tabOrder || (int)($tabOrder['bpo_active_key'] ?? 0) !== 1) {
|
||||
http_response_code(404);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo fxBibMsg('bib_v5_production_file_missing');
|
||||
exit;
|
||||
}
|
||||
} elseif ($strAction === 'development_unlock') {
|
||||
$tabResult = fxBibProductionDevelopmentUnlock($intEveId);
|
||||
if (empty($tabResult['success'])) {
|
||||
http_response_code(403);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo $tabResult['message'] ?? fxBibMsg('bib_v4_ajax_unauthorized');
|
||||
exit;
|
||||
}
|
||||
|
||||
$strAccountPage = $strLangue === 'en' ? '/account' : '/compte';
|
||||
header(
|
||||
'Location: ' . $strAccountPage
|
||||
. '/inc_tableau_gestion_epreuves?promoteur_eve_id='
|
||||
. urlencode(base64_encode($intEveId))
|
||||
);
|
||||
exit;
|
||||
} else {
|
||||
http_response_code(400);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!fxBibProductionStreamOrder($tabOrder)) {
|
||||
http_response_code(404);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo fxBibMsg('bib_v5_production_file_missing');
|
||||
exit;
|
||||
}
|
||||
|
||||
exit;
|
||||
392
css/style.css
392
css/style.css
@ -1078,7 +1078,7 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
|
||||
margin-top:10px;
|
||||
}
|
||||
|
||||
#module-bib .bib-workspace-nav{
|
||||
#module-bib .bib-workspace-bar{
|
||||
display:flex;
|
||||
align-items:stretch;
|
||||
gap:4px;
|
||||
@ -1089,6 +1089,12 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
|
||||
overflow-x:auto;
|
||||
}
|
||||
|
||||
#module-bib .bib-workspace-nav{
|
||||
display:flex;
|
||||
align-items:stretch;
|
||||
gap:4px;
|
||||
}
|
||||
|
||||
#module-bib .bib-workspace-tab{
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
@ -1107,6 +1113,41 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
/* MSIN-4470 — L'impression PDF reste accessible à droite des espaces principaux. */
|
||||
#module-bib .bib-workspace-action{
|
||||
display:flex;
|
||||
flex:0 0 auto;
|
||||
margin-left:auto;
|
||||
}
|
||||
|
||||
#module-bib .bib-workspace-action .bib-tool-btn{
|
||||
width:auto;
|
||||
min-height:42px;
|
||||
padding:8px 16px;
|
||||
border-width:0;
|
||||
background:transparent;
|
||||
color:#495057;
|
||||
font-size:13px;
|
||||
font-weight:600;
|
||||
line-height:1.25;
|
||||
}
|
||||
|
||||
#module-bib .bib-workspace-action .bib-tool-btn:not(.is-active):hover,
|
||||
#module-bib .bib-workspace-action .bib-tool-btn:not(.is-active):focus{
|
||||
background:#e8f0f8;
|
||||
color:#1e4a9e;
|
||||
box-shadow:none;
|
||||
}
|
||||
|
||||
#module-bib .bib-workspace-action .bib-tool-btn.is-active,
|
||||
#module-bib .bib-workspace-action .bib-tool-btn.is-active:hover,
|
||||
#module-bib .bib-workspace-action .bib-tool-btn.is-active:focus{
|
||||
background:#2f5fd0;
|
||||
color:#fff;
|
||||
border-color:transparent;
|
||||
box-shadow:none;
|
||||
}
|
||||
|
||||
#module-bib .bib-workspace-tab:hover,
|
||||
#module-bib .bib-workspace-tab:focus{
|
||||
background:#e8f0f8;
|
||||
@ -1145,9 +1186,37 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
|
||||
margin-bottom:16px;
|
||||
}
|
||||
|
||||
#module-bib .bib-preparation-groups{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(2, minmax(0, 1fr));
|
||||
gap:12px;
|
||||
}
|
||||
|
||||
#module-bib .bib-preparation-group{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:10px;
|
||||
min-width:0;
|
||||
padding:12px;
|
||||
border:1px solid #c8d6e5;
|
||||
border-radius:8px;
|
||||
background:#f8fbfe;
|
||||
}
|
||||
|
||||
#module-bib .bib-preparation-group--quantities{
|
||||
background:#fafbfc;
|
||||
}
|
||||
|
||||
#module-bib .bib-preparation-group h3{
|
||||
margin:0;
|
||||
color:#1e3a6e;
|
||||
font-size:14px;
|
||||
font-weight:700;
|
||||
}
|
||||
|
||||
#module-bib .bib-preparation-stats{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(4, minmax(0, 1fr));
|
||||
grid-template-columns:repeat(2, minmax(0, 1fr));
|
||||
gap:1px;
|
||||
margin:0;
|
||||
overflow:hidden;
|
||||
@ -1201,9 +1270,8 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
|
||||
gap:8px;
|
||||
}
|
||||
|
||||
#module-bib .bib-tool-nav--preparation,
|
||||
#module-bib .bib-tool-nav--operations{
|
||||
grid-template-columns:repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns:repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
#module-bib .bib-tool-nav--settings{
|
||||
@ -1211,6 +1279,11 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
|
||||
max-width:520px;
|
||||
}
|
||||
|
||||
#module-bib .bib-tool-nav--group{
|
||||
grid-template-columns:minmax(0, 1fr);
|
||||
margin-top:auto;
|
||||
}
|
||||
|
||||
#module-bib .bib-tool-btn:nth-child(4){
|
||||
grid-column:1;
|
||||
}
|
||||
@ -1623,6 +1696,7 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
|
||||
}
|
||||
|
||||
@media (max-width:768px){
|
||||
#module-bib .bib-workspace-bar,
|
||||
#module-bib .bib-workspace-nav{
|
||||
align-items:stretch;
|
||||
}
|
||||
@ -1631,6 +1705,10 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
|
||||
flex:1 0 auto;
|
||||
}
|
||||
|
||||
#module-bib .bib-preparation-groups{
|
||||
grid-template-columns:1fr;
|
||||
}
|
||||
|
||||
#module-bib .bib-preparation-stats{
|
||||
grid-template-columns:repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
@ -1947,10 +2025,8 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
|
||||
}
|
||||
|
||||
.epr-row-detail{
|
||||
display:grid;
|
||||
grid-template-columns:minmax(148px, 178px) minmax(0, 1fr);
|
||||
gap:12px;
|
||||
align-items:start;
|
||||
/* MSIN-4467 — Le détail utilise toute la largeur, sans colonne d'identité. */
|
||||
display:block;
|
||||
padding:12px;
|
||||
border-top:1px solid #dbe4f3;
|
||||
}
|
||||
@ -1961,7 +2037,7 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
|
||||
|
||||
.bib-race-summary{
|
||||
display:grid;
|
||||
grid-template-columns:minmax(220px, 1.4fr) minmax(300px, 1.4fr) auto 32px;
|
||||
grid-template-columns:minmax(220px, 1.2fr) minmax(180px, .9fr) minmax(270px, 1.3fr) 32px;
|
||||
gap:14px;
|
||||
align-items:center;
|
||||
min-height:64px;
|
||||
@ -1983,17 +2059,60 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
|
||||
.bib-race-summary__identity{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:2px;
|
||||
gap:4px;
|
||||
min-width:0;
|
||||
}
|
||||
|
||||
.bib-race-summary__identity strong{
|
||||
.bib-race-summary__identity-head{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:7px;
|
||||
min-width:0;
|
||||
}
|
||||
|
||||
.bib-race-summary__identity-head strong{
|
||||
min-width:0;
|
||||
overflow:hidden;
|
||||
color:#1e3a6e;
|
||||
font-size:14px;
|
||||
line-height:1.3;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
|
||||
.bib-race-summary__identity span{
|
||||
.bib-race-summary__identity-head .bib-view-range-cell{
|
||||
flex:0 0 auto;
|
||||
margin-left:auto;
|
||||
}
|
||||
|
||||
.bib-race-summary-view{
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
min-width:26px;
|
||||
height:24px;
|
||||
padding:2px 6px;
|
||||
border:1px solid #b8c9e8;
|
||||
background:#fff;
|
||||
color:#2f5fd0;
|
||||
}
|
||||
|
||||
.bib-race-summary-view:hover,
|
||||
.bib-race-summary-view:focus{
|
||||
border-color:#8fa8d8;
|
||||
background:#e8f0fc;
|
||||
color:#1e4fbf;
|
||||
}
|
||||
|
||||
.bib-race-summary__metadata{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
flex-wrap:wrap;
|
||||
gap:3px 6px;
|
||||
min-width:0;
|
||||
}
|
||||
|
||||
.bib-race-summary__metadata span{
|
||||
overflow:hidden;
|
||||
color:#6c757d;
|
||||
font-size:11px;
|
||||
@ -2001,6 +2120,43 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
|
||||
white-space:nowrap;
|
||||
}
|
||||
|
||||
.bib-race-summary__metadata span + span::before{
|
||||
content:"·";
|
||||
margin-right:6px;
|
||||
color:#9aa4b2;
|
||||
}
|
||||
|
||||
.bib-race-summary__group{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
justify-content:center;
|
||||
align-self:stretch;
|
||||
min-width:0;
|
||||
padding:5px 10px;
|
||||
border:1px solid #cbd3dc;
|
||||
border-radius:6px;
|
||||
background:#f1f3f5;
|
||||
}
|
||||
|
||||
.bib-race-summary__group--bibs{
|
||||
border-color:#b8c9e8;
|
||||
background:#e8f0fc;
|
||||
}
|
||||
|
||||
.bib-race-summary__group h4{
|
||||
margin:0 0 5px;
|
||||
color:#1e3a6e;
|
||||
font-size:11px;
|
||||
font-weight:800;
|
||||
letter-spacing:.06em;
|
||||
text-transform:uppercase;
|
||||
}
|
||||
|
||||
/* MSIN-4470 — Accentue légèrement les deux repères principaux de chaque épreuve. */
|
||||
.bib-race-summary__group--bibs h4{
|
||||
color:#1e4fbf;
|
||||
}
|
||||
|
||||
.bib-race-summary__stats{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(3, minmax(0, 1fr));
|
||||
@ -2008,6 +2164,10 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
|
||||
margin:0;
|
||||
}
|
||||
|
||||
.bib-race-summary__group--quantities .bib-race-summary__stats{
|
||||
grid-template-columns:repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.bib-race-summary__stats > div{
|
||||
min-width:0;
|
||||
}
|
||||
@ -2213,11 +2373,86 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
|
||||
overflow:hidden;
|
||||
}
|
||||
|
||||
.bib-team-mode-step{
|
||||
padding:12px 14px;
|
||||
border-bottom:1px solid #b8c9e8;
|
||||
background:#f8fafc;
|
||||
}
|
||||
|
||||
.bib-team-mode-step .team-mode-box{
|
||||
margin:0;
|
||||
padding:0;
|
||||
border:0;
|
||||
background:transparent;
|
||||
}
|
||||
|
||||
.bib-team-mode-step .team-mode-title{
|
||||
margin:0 0 8px;
|
||||
color:#1e3a6e;
|
||||
font-size:13px;
|
||||
font-weight:700;
|
||||
}
|
||||
|
||||
.bib-team-mode-options{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
flex-wrap:wrap;
|
||||
gap:8px 24px;
|
||||
}
|
||||
|
||||
.bib-team-mode-options .team-mode-option{
|
||||
align-items:center;
|
||||
margin:0;
|
||||
}
|
||||
|
||||
.bib-team-mode-options .team-mode-option input[type="radio"]{
|
||||
margin-top:0;
|
||||
}
|
||||
|
||||
.epr-col-right .epr-block{
|
||||
border:none;
|
||||
border-radius:0;
|
||||
}
|
||||
|
||||
/* MSIN-4467 — En-têtes secondaires plus compacts et moins dominants visuellement. */
|
||||
.epr-col-right > .epr-block > .epr-header{
|
||||
padding:8px 10px;
|
||||
border-bottom:1px solid #b8c9e2;
|
||||
background:#d9e4f4;
|
||||
color:#1e3a6e;
|
||||
font-size:14px;
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
.epr-col-right > .epr-block:not(.is-collapsed) > .epr-header{
|
||||
background:#c7d7ef;
|
||||
}
|
||||
|
||||
.epr-col-right > .epr-block > .epr-header:hover{
|
||||
background:#cfddf1;
|
||||
}
|
||||
|
||||
.epr-col-right > .epr-block > .epr-header .epr-block-toggle{
|
||||
width:26px;
|
||||
height:26px;
|
||||
border-color:#9fb2cf;
|
||||
background:#fff;
|
||||
color:#2f5fd0;
|
||||
}
|
||||
|
||||
.epr-col-right > .epr-block > .epr-header .epr-block-toggle:hover,
|
||||
.epr-col-right > .epr-block > .epr-header .epr-block-toggle:focus{
|
||||
border-color:#7f9bc5;
|
||||
background:#e3ebf6;
|
||||
color:#1e4fbf;
|
||||
}
|
||||
|
||||
.epr-col-right > .epr-block > .epr-header .epr-header-summary--seq-empty{
|
||||
border-color:#c3cede;
|
||||
background:#fff;
|
||||
color:#65758b;
|
||||
}
|
||||
|
||||
.epr-col-right .epr-block + .epr-block{
|
||||
border-top:1px solid #b8c9e8;
|
||||
}
|
||||
@ -2242,7 +2477,7 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
|
||||
|
||||
.epr-gestion-stats{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(3, minmax(0, 1fr));
|
||||
grid-template-columns:repeat(4, minmax(0, 1fr));
|
||||
gap:12px 16px;
|
||||
margin:0;
|
||||
}
|
||||
@ -2370,14 +2605,11 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
|
||||
.epr-block-assign .bib-container{
|
||||
width:100%;
|
||||
min-width:0;
|
||||
/* MSIN-4475 — filet mobile étroit seulement ; plus de min-width 1100px (slider inutile). */
|
||||
overflow-x:auto;
|
||||
-webkit-overflow-scrolling:touch;
|
||||
}
|
||||
|
||||
.epr-block-assign .bib-container .epr-line{
|
||||
min-width:720px;
|
||||
}
|
||||
|
||||
.epr-line-header{
|
||||
background:#fff3cd;
|
||||
font-weight:700;
|
||||
@ -2633,11 +2865,16 @@ ul.ms1-menu-compte li a:hover, ul.ms1-menu-compte li a:active {
|
||||
font-weight:600;
|
||||
line-height:1.2;
|
||||
padding:0.35em 0.5em;
|
||||
white-space:nowrap;
|
||||
/* MSIN-4475 — peut passer sur 2 lignes si la colonne restante est étroite. */
|
||||
white-space:normal;
|
||||
}
|
||||
|
||||
.bib-auto-badge--row{
|
||||
display:inline-block;
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
flex-wrap:wrap;
|
||||
gap:2px;
|
||||
max-width:100%;
|
||||
}
|
||||
|
||||
.bib-container.batch-mode .bib-auto-badge--row{
|
||||
@ -2977,21 +3214,23 @@ a.ms1-trad-link.btn-aide-trad{
|
||||
width:100%;
|
||||
}
|
||||
|
||||
/* MSIN-4379 — Cadenas + view/OK/stats ; badge auto à droite. */
|
||||
/* MSIN-4379 / MSIN-4469 — cadenas utilisateur + impression + view/OK/stats. */
|
||||
/* MSIN-4429 — colonnes Début | Qté | ou | Fin (alignées sur l'en-tête). */
|
||||
/* MSIN-4475 — grille compacte (~640px min) pour tenir dans le wrapper page sans slider. */
|
||||
.epr-line{
|
||||
display:grid;
|
||||
grid-template-columns:
|
||||
42px
|
||||
minmax(70px, 78px)
|
||||
36px
|
||||
36px
|
||||
minmax(54px, 64px)
|
||||
minmax(54px, 64px)
|
||||
minmax(60px, 72px)
|
||||
minmax(60px, 72px)
|
||||
20px
|
||||
minmax(54px, 64px)
|
||||
minmax(72px, 80px)
|
||||
40px
|
||||
minmax(152px, 168px)
|
||||
minmax(28px, 40px);
|
||||
minmax(60px, 72px)
|
||||
minmax(48px, 56px)
|
||||
36px
|
||||
minmax(140px, 152px)
|
||||
minmax(0, 1fr);
|
||||
gap:6px;
|
||||
align-items:center;
|
||||
width:100%;
|
||||
@ -3000,15 +3239,16 @@ a.ms1-trad-link.btn-aide-trad{
|
||||
|
||||
.bib-container.batch-mode .epr-line{
|
||||
grid-template-columns:
|
||||
42px
|
||||
minmax(70px, 78px)
|
||||
36px
|
||||
36px
|
||||
minmax(54px, 64px)
|
||||
minmax(54px, 64px)
|
||||
minmax(60px, 72px)
|
||||
minmax(60px, 72px)
|
||||
20px
|
||||
minmax(54px, 64px)
|
||||
minmax(72px, 80px)
|
||||
40px
|
||||
minmax(152px, 168px)
|
||||
minmax(60px, 72px)
|
||||
minmax(48px, 56px)
|
||||
36px
|
||||
minmax(140px, 152px)
|
||||
28px;
|
||||
}
|
||||
|
||||
@ -3019,8 +3259,37 @@ a.ms1-trad-link.btn-aide-trad{
|
||||
min-width:0;
|
||||
}
|
||||
|
||||
.epr-col-production-lock{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
min-width:0;
|
||||
}
|
||||
|
||||
.btn-bib-production-lock{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
width:38px;
|
||||
min-height:31px;
|
||||
padding:4px 0;
|
||||
border:1px solid #2f4ba0;
|
||||
background-color:#3f63c9;
|
||||
color:#fff;
|
||||
font-size:15px;
|
||||
line-height:1;
|
||||
cursor:help;
|
||||
}
|
||||
|
||||
.btn-bib-production-lock:hover,
|
||||
.btn-bib-production-lock:focus{
|
||||
border-color:#263d83;
|
||||
background-color:#3656ad;
|
||||
color:#fff;
|
||||
}
|
||||
|
||||
.btn-bib-lock{
|
||||
width:100%;
|
||||
width:38px;
|
||||
min-height:31px;
|
||||
padding:4px 0;
|
||||
border-width:1px;
|
||||
@ -3146,7 +3415,7 @@ a.ms1-trad-link.btn-aide-trad{
|
||||
|
||||
.epr-col-stats{
|
||||
display:grid;
|
||||
grid-template-columns:56px 48px 48px;
|
||||
grid-template-columns:minmax(44px, 52px) minmax(40px, 48px) minmax(40px, 48px);
|
||||
gap:4px;
|
||||
align-items:center;
|
||||
justify-content:start;
|
||||
@ -3173,11 +3442,12 @@ a.ms1-trad-link.btn-aide-trad{
|
||||
}
|
||||
|
||||
.epr-col-batch{
|
||||
/* MSIN-4475 — badge auto dans la colonne fluide restante (pas de min-width forcé). */
|
||||
overflow:hidden;
|
||||
min-width:0;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:flex-end;
|
||||
justify-content:flex-start;
|
||||
gap:4px;
|
||||
}
|
||||
|
||||
@ -3212,6 +3482,26 @@ a.ms1-trad-link.btn-aide-trad{
|
||||
gap:10px;
|
||||
}
|
||||
|
||||
.bib-assignment-step__heading{
|
||||
flex:1 1 auto;
|
||||
min-width:0;
|
||||
}
|
||||
|
||||
.bib-assignment-step__actions{
|
||||
flex:0 0 auto;
|
||||
margin-left:auto;
|
||||
}
|
||||
|
||||
.bib-assignment-step__actions .epr-identity-reset-all{
|
||||
padding:0;
|
||||
border:0;
|
||||
background:transparent;
|
||||
}
|
||||
|
||||
.bib-assignment-step__actions .epr-identity-reset-all .btn-reset-all-epr{
|
||||
width:auto;
|
||||
}
|
||||
|
||||
.bib-assignment-step__number{
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
@ -3319,22 +3609,33 @@ a.ms1-trad-link.btn-aide-trad{
|
||||
/* MOBILE */
|
||||
@media (max-width:768px){
|
||||
.epr-row-detail{
|
||||
grid-template-columns: 1fr;
|
||||
padding:8px;
|
||||
}
|
||||
|
||||
.bib-race-summary{
|
||||
grid-template-columns:minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.bib-race-summary__stats{
|
||||
.bib-race-summary__group{
|
||||
grid-column:1 / -1;
|
||||
padding:8px 10px;
|
||||
}
|
||||
|
||||
.bib-race-summary__group--quantities{
|
||||
grid-row:2;
|
||||
}
|
||||
|
||||
.bib-race-summary__status{
|
||||
grid-column:1;
|
||||
.bib-assignment-step__header{
|
||||
flex-wrap:wrap;
|
||||
}
|
||||
|
||||
.bib-assignment-step__actions{
|
||||
flex:1 0 100%;
|
||||
margin-left:36px;
|
||||
}
|
||||
|
||||
.bib-race-summary__group--bibs{
|
||||
grid-row:3;
|
||||
justify-self:start;
|
||||
}
|
||||
|
||||
.bib-race-toggle{
|
||||
@ -5714,6 +6015,11 @@ button.inscr-gestion-list-remis:hover{
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
/* MSIN-4473 — liens GESTION / RAPPORTS : mêmes coins que #module-bib .bib-tool-btn */
|
||||
.promoteur-hub-link {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* ===== Hub promoteur : resume flash (haut) + tuiles par epreuve (collapse) ===== */
|
||||
.promoteur-hub-card-row {
|
||||
align-items: stretch;
|
||||
|
||||
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);
|
||||
0
images/doc/.gitkeep
Normal file
0
images/doc/.gitkeep
Normal file
BIN
images/doc/dossardposition.png
Normal file
BIN
images/doc/dossardposition.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
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 |
@ -450,7 +450,10 @@ if ($strLangue == 'fr') {
|
||||
strpath = '/book/';
|
||||
}
|
||||
|
||||
document.location.href = '<?php echo $vDomaine; ?>' + strpath + code + '/' + intEprId + '?promoteur=1&action=mod&pec_id=' + intPecId;
|
||||
// MSIN-4474 — ramener à la liste d'origine après Enregistrer / Annuler
|
||||
document.location.href = '<?php echo $vDomaine; ?>' + strpath + code + '/' + intEprId
|
||||
+ '?promoteur=1&action=mod&pec_id=' + intPecId
|
||||
+ '&return_url=' + encodeURIComponent(window.location.href);
|
||||
});
|
||||
<?php
|
||||
}
|
||||
@ -1326,7 +1329,10 @@ if ($strLangue == 'fr') {
|
||||
<?php
|
||||
break;
|
||||
case 'reserver.php':
|
||||
if (($tabEvenement['general']['eve_epreuve_modifiable'] == 1 || $tabEvenement['general']['eve_epreuve_modifiable_client'] == 1) && $strAction == 'mod' && !isset($_SESSION['no_panier']) && fxCheckAccesEpreuve('mod', $intEpreuve, $tabEvenement['general']['eve_id'], $_GET['pec_id'], $_GET['promoteur'], '')) { // si le promoteur ou le participant peut changer l'épreuve
|
||||
// MSIN-4474 — même périmètre que le HTML (inc_fx_reserver) : accès legacy, v2 ou admin
|
||||
$intPecIdSwitch = isset($_GET['pec_id']) ? $_GET['pec_id'] : 0;
|
||||
$intPromoteurSwitch = isset($_GET['promoteur']) ? $_GET['promoteur'] : 0;
|
||||
if (($tabEvenement['general']['eve_epreuve_modifiable'] == 1 || $tabEvenement['general']['eve_epreuve_modifiable_client'] == 1) && $strAction == 'mod' && !isset($_SESSION['no_panier']) && fxCheckAccesEpreuve('mod', $intEpreuve, $tabEvenement['general']['eve_id'], $intPecIdSwitch, $intPromoteurSwitch, '')) { // si le promoteur ou le participant peut changer l'épreuve
|
||||
?>
|
||||
$("#sel_epreuve").change(function () {
|
||||
$("#btn_changer_epreuve").show();
|
||||
@ -1350,9 +1356,13 @@ if ($strLangue == 'fr') {
|
||||
} else {
|
||||
echo '';
|
||||
} ?>';
|
||||
//sl
|
||||
// alert('<?php echo $vDomaine?>/' + strPage + '/' + strUrl + '/' + $("#sel_epreuve").val() + '?action=mod&switch=true' + strPromoteur + '&pec_id=<?php echo $_GET['pec_id']; ?>');
|
||||
document.location.href = '<?php echo $vDomaine?>/' + strPage + '/' + strUrl + '/' + $("#sel_epreuve").val() + '?action=mod&switch=true' + strPromoteur + '&pec_id=<?php echo $_GET['pec_id']; ?>';
|
||||
// MSIN-4474 — préserver return_url au switch d'épreuve
|
||||
var strReturn = '';
|
||||
var $returnUrl = $("#return_url");
|
||||
if ($returnUrl.length && $returnUrl.val()) {
|
||||
strReturn = '&return_url=' + encodeURIComponent($returnUrl.val());
|
||||
}
|
||||
document.location.href = '<?php echo $vDomaine?>/' + strPage + '/' + strUrl + '/' + $("#sel_epreuve").val() + '?action=mod&switch=true' + strPromoteur + '&pec_id=<?php echo isset($_GET['pec_id']) ? $_GET['pec_id'] : ''; ?>' + strReturn;
|
||||
});
|
||||
$("#frm_participant").valid();
|
||||
<?php
|
||||
|
||||
12
index.php
12
index.php
@ -58,7 +58,7 @@ require_once("inc_header.php");
|
||||
}
|
||||
}
|
||||
|
||||
echo str_replace('%liste_evenements%', $strListeEvenements, fxUnescape($recPage['pag_texte_' . $strLangue], 1));
|
||||
echo str_replace('%liste_evenements%', $strListeEvenements, fxNormalizeCmsPageLinks(fxUnescape($recPage['pag_texte_' . $strLangue], 1)));
|
||||
} else {
|
||||
?>
|
||||
<h1><?php echo fxUnescape($recPage['pag_titre_' . $strLangue], 1); ?></h1>
|
||||
@ -66,12 +66,12 @@ require_once("inc_header.php");
|
||||
switch ($strCode) {
|
||||
case 'cookie':
|
||||
$strCookieContent = trim((string) $recPage['pag_texte_' . $strLangue]);
|
||||
echo $strCookieContent !== '' ? fxUnescape($recPage['pag_texte_' . $strLangue], 1) : fxCookiePolicyPageContent($strLangue);
|
||||
echo $strCookieContent !== '' ? fxNormalizeCmsPageLinks(fxUnescape($recPage['pag_texte_' . $strLangue], 1)) : fxCookiePolicyPageContent($strLangue);
|
||||
break;
|
||||
|
||||
case 'confidentialite':
|
||||
$strPrivacyContent = trim((string) $recPage['pag_texte_' . $strLangue]);
|
||||
echo $strPrivacyContent !== '' ? fxUnescape($recPage['pag_texte_' . $strLangue], 1) : fxPrivacyPolicyPageContent($strLangue);
|
||||
echo $strPrivacyContent !== '' ? fxNormalizeCmsPageLinks(fxUnescape($recPage['pag_texte_' . $strLangue], 1)) : fxPrivacyPolicyPageContent($strLangue);
|
||||
break;
|
||||
|
||||
case 'evenements':
|
||||
@ -85,7 +85,7 @@ require_once("inc_header.php");
|
||||
}
|
||||
}
|
||||
|
||||
echo str_replace('%liste_evenements%', $strListeEvenements, fxUnescape($recPage['pag_texte_' . $strLangue], 1));
|
||||
echo str_replace('%liste_evenements%', $strListeEvenements, fxNormalizeCmsPageLinks(fxUnescape($recPage['pag_texte_' . $strLangue], 1)));
|
||||
break;
|
||||
case 'contact_send':
|
||||
print_r($_POST);
|
||||
@ -93,7 +93,7 @@ require_once("inc_header.php");
|
||||
break;
|
||||
case 'contact':
|
||||
$arrEvenements = fxGetEvenements('', $strLangue, 1, true);
|
||||
$strText = fxUnescape($recPage['pag_texte_' . $strLangue], 1);
|
||||
$strText = fxNormalizeCmsPageLinks(fxUnescape($recPage['pag_texte_' . $strLangue], 1));
|
||||
$contact_promoteurs = fxShowListeEvenementsContact($arrEvenements, $strLangue);
|
||||
//$strText = str_replace('%evenements%', $strListeEvenements, $strText);
|
||||
|
||||
@ -152,7 +152,7 @@ require_once("inc_header.php");
|
||||
break;
|
||||
case 'contactsend':
|
||||
$arrEvenements = fxGetEvenements('', $strLangue, 1, true);
|
||||
$strText = fxUnescape($recPage['pag_texte_' . $strLangue], 1);
|
||||
$strText = fxNormalizeCmsPageLinks(fxUnescape($recPage['pag_texte_' . $strLangue], 1));
|
||||
$strListeEvenements = fxShowListeEvenementsContact($arrEvenements, $strLangue);
|
||||
$strText = str_replace('%evenements%', $strListeEvenements, $strText);
|
||||
echo $strText;
|
||||
|
||||
@ -534,15 +534,28 @@
|
||||
if (races) {
|
||||
races.classList.toggle('bib-ui-hidden', workspaceId !== 'preparation');
|
||||
}
|
||||
|
||||
// MSIN-4475 — Modes d'assignation : afficher les choix tout de suite (sans clic tuile).
|
||||
if (workspaceId === 'operations' && options.keepTool !== true) {
|
||||
var autoTool = panel.getAttribute('data-bib-auto-tool');
|
||||
if (autoTool) {
|
||||
bibOpenTool(autoTool);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function bibWorkspaceForTool(toolId) {
|
||||
if (toolId === 'event_config') {
|
||||
return 'settings';
|
||||
}
|
||||
if (toolId === 'anomalies' || toolId === 'dist_print') {
|
||||
if (toolId === 'global_batch') {
|
||||
return 'operations';
|
||||
}
|
||||
// MSIN-4470 — L'action PDF est globale et s'ouvre depuis la barre supérieure
|
||||
// sans déplacer l'utilisateur vers un autre espace de travail.
|
||||
if (toolId === 'dist_print') {
|
||||
return bibActiveWorkspace;
|
||||
}
|
||||
return 'preparation';
|
||||
}
|
||||
|
||||
@ -625,16 +638,46 @@
|
||||
}
|
||||
|
||||
var btn = moduleBib.querySelector('.bib-tool-btn[data-bib-tool="' + toolId + '"]');
|
||||
titleEl.textContent = btn
|
||||
? (btn.getAttribute('data-bib-tool-label') || '')
|
||||
if (btn) {
|
||||
titleEl.textContent = btn.getAttribute('data-bib-tool-label') || '';
|
||||
return;
|
||||
}
|
||||
|
||||
// MSIN-4475 — Titre depuis le panneau (outil ouvert auto, sans tuile).
|
||||
var autoPanel = moduleBib.querySelector(
|
||||
'[data-bib-workspace-panel][data-bib-auto-tool="' + toolId + '"]'
|
||||
);
|
||||
titleEl.textContent = autoPanel
|
||||
? (autoPanel.getAttribute('data-bib-auto-tool-label') || '')
|
||||
: '';
|
||||
}
|
||||
|
||||
function bibCloseTool() {
|
||||
// MSIN-4470 — Le panneau PDF s'ouvre immédiatement sous la barre supérieure.
|
||||
function bibPositionToolDetail(toolId) {
|
||||
var detail = document.getElementById('bib-tool-detail');
|
||||
var defaultSlot = document.getElementById('bib-tool-detail-default-slot');
|
||||
if (!detail || !defaultSlot || !defaultSlot.parentNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (toolId === 'dist_print') {
|
||||
var firstWorkspacePanel = moduleBib.querySelector('[data-bib-workspace-panel]');
|
||||
if (firstWorkspacePanel && firstWorkspacePanel.parentNode) {
|
||||
firstWorkspacePanel.parentNode.insertBefore(detail, firstWorkspacePanel);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
defaultSlot.parentNode.insertBefore(detail, defaultSlot.nextSibling);
|
||||
}
|
||||
|
||||
function bibCloseTool(options) {
|
||||
options = options || {};
|
||||
if (!bibActiveTool) {
|
||||
return;
|
||||
}
|
||||
|
||||
var closedTool = bibActiveTool;
|
||||
bibRestoreToolContent(bibActiveTool);
|
||||
bibInvalidateToolSlot(bibActiveTool);
|
||||
bibActiveTool = null;
|
||||
@ -650,6 +693,14 @@
|
||||
if (detail) {
|
||||
detail.classList.add('bib-ui-hidden');
|
||||
}
|
||||
bibPositionToolDetail(null);
|
||||
|
||||
// MSIN-4475 — Fermer les modes (plus de tuile) ramène à la préparation.
|
||||
if (options.returnToPreparation === true
|
||||
&& closedTool === 'global_batch'
|
||||
&& bibActiveWorkspace === 'operations') {
|
||||
bibSetWorkspace('preparation', { keepTool: true });
|
||||
}
|
||||
}
|
||||
|
||||
function bibSyncToolNavBadges(toolId) {
|
||||
@ -790,6 +841,7 @@
|
||||
bibActiveTool = toolId;
|
||||
bibSetActiveToolButton(toolId);
|
||||
bibSetToolDetailTitle(toolId);
|
||||
bibPositionToolDetail(toolId);
|
||||
|
||||
var detail = document.getElementById('bib-tool-detail');
|
||||
if (detail) {
|
||||
@ -1983,6 +2035,15 @@
|
||||
bibOpenTool(toolBtn.getAttribute('data-bib-tool'));
|
||||
});
|
||||
|
||||
// MSIN-4469 — Le téléchargement part dans un nouvel onglet;
|
||||
// l’écran principal revient aussitôt à Quantités et assignation.
|
||||
moduleBib.addEventListener('submit', function (e) {
|
||||
var productionForm = e.target.closest('.bib-production-return-preparation-form');
|
||||
if (productionForm && moduleBib.contains(productionForm)) {
|
||||
bibSetWorkspace('preparation');
|
||||
}
|
||||
});
|
||||
|
||||
moduleBib.addEventListener('click', function (e) {
|
||||
var workspaceTab = e.target.closest('.bib-workspace-tab');
|
||||
if (workspaceTab && moduleBib.contains(workspaceTab)) {
|
||||
@ -2002,7 +2063,7 @@
|
||||
|
||||
if (e.target.closest('.bib-tool-detail__close')) {
|
||||
e.preventDefault();
|
||||
bibCloseTool();
|
||||
bibCloseTool({ returnToPreparation: true });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -2219,17 +2280,6 @@
|
||||
|
||||
function bibSyncRaceSummaryStatus(row) {
|
||||
if (!row) return;
|
||||
var pendingEl = row.querySelector('.bib-race-summary-pending');
|
||||
var rangesEl = row.querySelector('.bib-race-summary-ranges');
|
||||
var statusEl = row.querySelector('.bib-race-summary__status');
|
||||
var pending = parseInt(pendingEl ? pendingEl.textContent : '0', 10) || 0;
|
||||
var ranges = parseInt(rangesEl ? rangesEl.textContent : '0', 10) || 0;
|
||||
var ready = pending === 0 && ranges > 0;
|
||||
|
||||
if (statusEl) {
|
||||
statusEl.classList.toggle('is-ready', ready);
|
||||
statusEl.textContent = statusEl.getAttribute(ready ? 'data-label-ready' : 'data-label-incomplete') || '';
|
||||
}
|
||||
bibRefreshPreparationOverview();
|
||||
}
|
||||
|
||||
@ -2274,7 +2324,7 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
// MSIN-4379 — Mise à jour affichage quantités (panneau gestion + pastille en-tête).
|
||||
// MSIN-4468 — Met à jour la capacité totale éditable et la disponibilité calculée.
|
||||
function bibApplyQteData(data) {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return;
|
||||
@ -2290,18 +2340,14 @@
|
||||
return;
|
||||
}
|
||||
|
||||
let elOrigine = document.getElementById('qte_origine_' + eprId);
|
||||
let elAjustement = document.getElementById('qte_ajustement_' + eprId);
|
||||
let elTotale = document.getElementById('qte_totale_' + eprId);
|
||||
let elCourante = document.getElementById('qte_courante_' + eprId);
|
||||
let elInscrits = document.getElementById('qte_inscrits_' + eprId);
|
||||
let elCheckin = document.getElementById('qte_checkin_' + eprId);
|
||||
let elRestante = document.getElementById('qte_restante_' + eprId);
|
||||
|
||||
if (elOrigine) {
|
||||
elOrigine.textContent = row.qte_origine;
|
||||
}
|
||||
if (elAjustement) {
|
||||
elAjustement.textContent = row.qte_ajustement;
|
||||
if (elTotale) {
|
||||
elTotale.value = row.qte_courante;
|
||||
}
|
||||
if (elCourante) {
|
||||
elCourante.textContent = row.qte_courante;
|
||||
@ -2309,11 +2355,23 @@
|
||||
if (elInscrits) {
|
||||
elInscrits.textContent = row.qte_inscrits;
|
||||
}
|
||||
let gestionPanel = document.querySelector(
|
||||
'.epr-gestion-panel[data-epr-id="' + eprId + '"]'
|
||||
);
|
||||
if (gestionPanel && row.qte_inscrits !== undefined) {
|
||||
gestionPanel.dataset.qteInscrits = row.qte_inscrits;
|
||||
}
|
||||
if (elCheckin) {
|
||||
elCheckin.textContent = row.qte_checkin;
|
||||
}
|
||||
if (elRestante) {
|
||||
elRestante.value = row.qte_restante;
|
||||
elRestante.textContent = row.qte_restante;
|
||||
}
|
||||
let summaryAvailable = document.querySelector(
|
||||
'.epr-row[data-epr-id="' + eprId + '"] .bib-race-summary-available'
|
||||
);
|
||||
if (summaryAvailable && row.qte_restante !== undefined) {
|
||||
summaryAvailable.textContent = row.qte_restante;
|
||||
}
|
||||
|
||||
let dispoBadge = document.querySelector(
|
||||
@ -2325,29 +2383,42 @@
|
||||
dispoBadge.textContent = label + ' : ' + row.qte_restante;
|
||||
}
|
||||
|
||||
let btnQte = document.querySelector(
|
||||
'.epr-gestion-panel[data-epr-id="' + eprId + '"] .btn_qte'
|
||||
let btnQteTotal = document.querySelector(
|
||||
'.epr-gestion-panel[data-epr-id="' + eprId + '"] .btn_qte_total'
|
||||
);
|
||||
if (btnQte) {
|
||||
btnQte.setAttribute('data-qte_initiale', row.qte_restante);
|
||||
if (btnQteTotal) {
|
||||
btnQteTotal.setAttribute('data-qte_initiale', row.qte_courante);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function bibSaveQte(btn) {
|
||||
function bibSaveQteTotal(btn) {
|
||||
let eprId = btn.getAttribute('data-epr_id');
|
||||
if (!eprId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let input = document.getElementById('qte_restante_' + eprId);
|
||||
let input = document.getElementById('qte_totale_' + eprId);
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
let intQte = parseInt(input.value, 10);
|
||||
if (isNaN(intQte) || intQte < 0) {
|
||||
alert(bibJs('err-generic'));
|
||||
let strQteTotale = String(input.value || '').trim();
|
||||
let intQteTotale = Number(strQteTotale);
|
||||
if (!/^\d+$/.test(strQteTotale)
|
||||
|| !Number.isSafeInteger(intQteTotale)
|
||||
|| intQteTotale > 9999999) {
|
||||
// MSIN-4479 — data-js-err-generic → dataset.jsErrGeneric
|
||||
alert(bibJs('jsErrGeneric') || 'Erreur');
|
||||
return;
|
||||
}
|
||||
|
||||
let gestionPanel = btn.closest('.epr-gestion-panel');
|
||||
let intNbInscriptions = gestionPanel
|
||||
? parseInt(gestionPanel.dataset.qteInscrits, 10) || 0
|
||||
: 0;
|
||||
if (intQteTotale < intNbInscriptions) {
|
||||
alert(bibJsFmt('jsQteTotalMin', intNbInscriptions));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -2359,10 +2430,11 @@
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body:
|
||||
'a=qte'
|
||||
'a=qte_total'
|
||||
+ '&epr_id=' + encodeURIComponent(eprId)
|
||||
+ '&qte=' + encodeURIComponent(intQte)
|
||||
+ '&total=' + encodeURIComponent(intQteTotale)
|
||||
+ '&lang=' + encodeURIComponent(moduleBib.dataset.lang || 'fr')
|
||||
+ '&csrf_token=' + encodeURIComponent(moduleBib.dataset.csrfToken || '')
|
||||
})
|
||||
.then(function (res) {
|
||||
return res.json();
|
||||
@ -2372,10 +2444,10 @@
|
||||
bibApplyQteData(result.data);
|
||||
return;
|
||||
}
|
||||
alert(bibJs('err-generic'));
|
||||
alert((result && result.message) || bibJs('jsErrGeneric') || 'Erreur');
|
||||
})
|
||||
.catch(function () {
|
||||
alert(bibJs('server-error'));
|
||||
alert(bibJs('jsServerError') || bibJs('jsErrGeneric') || 'Erreur serveur');
|
||||
})
|
||||
.finally(function () {
|
||||
bibLoaderHide();
|
||||
@ -2447,6 +2519,41 @@
|
||||
}
|
||||
}
|
||||
|
||||
/** MSIN-4467 — Accordéon du détail : au plus un panneau ouvert, mais les deux peuvent être fermés. */
|
||||
function bibActivateRaceDetailBlock(block) {
|
||||
if (!block) {
|
||||
return;
|
||||
}
|
||||
|
||||
let column = block.closest('.epr-col-right');
|
||||
if (!column) {
|
||||
return;
|
||||
}
|
||||
|
||||
let btnToggle = block.querySelector('.epr-block-toggle');
|
||||
if (!block.classList.contains('is-collapsed')) {
|
||||
if (btnToggle) {
|
||||
bibToggleCollapsibleBlock(block, btnToggle);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
column.querySelectorAll('.epr-block-gestion, .epr-block-assign').forEach(function (otherBlock) {
|
||||
if (otherBlock === block || otherBlock.classList.contains('is-collapsed')) {
|
||||
return;
|
||||
}
|
||||
|
||||
let otherToggle = otherBlock.querySelector('.epr-block-toggle');
|
||||
if (otherToggle) {
|
||||
bibToggleCollapsibleBlock(otherBlock, otherToggle);
|
||||
}
|
||||
});
|
||||
|
||||
if (btnToggle) {
|
||||
bibToggleCollapsibleBlock(block, btnToggle);
|
||||
}
|
||||
}
|
||||
|
||||
function bibCollapseGlobalBatchSection(section) {
|
||||
if (!section || section.classList.contains('is-collapsed')) {
|
||||
return;
|
||||
@ -2506,6 +2613,15 @@
|
||||
return;
|
||||
}
|
||||
|
||||
let raceBlockHeader = e.target.closest(
|
||||
'.epr-block-gestion > .epr-header--collapsible, .epr-block-assign > .epr-header--collapsible'
|
||||
);
|
||||
if (raceBlockHeader && moduleBib.contains(raceBlockHeader)) {
|
||||
e.preventDefault();
|
||||
bibActivateRaceDetailBlock(raceBlockHeader.closest('.epr-block'));
|
||||
return;
|
||||
}
|
||||
|
||||
let anomalyHdr = e.target.closest('.bib-anomaly-block-header');
|
||||
if (anomalyHdr && moduleBib.contains(anomalyHdr) && !e.target.closest('.epr-block-toggle')) {
|
||||
e.preventDefault();
|
||||
@ -3691,7 +3807,22 @@
|
||||
+ '&epr_id=' + encodeURIComponent(box.dataset.eprId)
|
||||
+ '&team_mode=' + encodeURIComponent(teamRadio.value)
|
||||
+ bibAjaxLangSuffix()
|
||||
}, { loader: false });
|
||||
}, { loader: false })
|
||||
.then(function (res) { return res.json(); })
|
||||
.then(function (data) {
|
||||
if (!data.success) {
|
||||
alert(data.message || bibJs('jsErrGeneric'));
|
||||
return;
|
||||
}
|
||||
|
||||
let row = teamRadio.closest('.epr-row');
|
||||
let summary = row ? row.querySelector('.bib-race-summary-team-mode') : null;
|
||||
let optionLabel = teamRadio.closest('.team-mode-option');
|
||||
let optionText = optionLabel ? optionLabel.querySelector('span') : null;
|
||||
if (summary && optionText) {
|
||||
summary.textContent = optionText.textContent.trim();
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
@ -3784,10 +3915,10 @@
|
||||
|
||||
moduleBib.addEventListener('click', function (e) {
|
||||
|
||||
let btnQte = e.target.closest('.btn_qte');
|
||||
if (btnQte && moduleBib.contains(btnQte)) {
|
||||
let btnQteTotal = e.target.closest('.btn_qte_total');
|
||||
if (btnQteTotal && moduleBib.contains(btnQteTotal)) {
|
||||
e.preventDefault();
|
||||
bibSaveQte(btnQte);
|
||||
bibSaveQteTotal(btnQteTotal);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -438,14 +438,19 @@
|
||||
var $cancelBtn = $('#annuler_fiche');
|
||||
if ($cancelBtn.length && $cancelBtn.is(':visible')) {
|
||||
if (!$('#inscr-gestion-refund-cancel-hint').length) {
|
||||
// MSIN-4419 — texte Info + marqueur crayon (rescan car injection post-AJAX).
|
||||
var $hint = $('<div id="inscr-gestion-refund-cancel-hint" style="margin-top:8px;padding:10px;border:1px solid #ffe1a6;background:#fff8e6;border-radius:8px;font-size:13px;display:flex;align-items:center;flex-wrap:wrap;gap:10px;"></div>');
|
||||
$hint.append($('<span></span>').text(refundCancelHint));
|
||||
var $hintText = $('<span></span>').text(refundCancelHint);
|
||||
$hint.append($hintText);
|
||||
$hint.append(
|
||||
$('<button type="button" class="btn btn-danger btn-sm rounded-0"></button>')
|
||||
.text($.trim($cancelBtn.text()))
|
||||
.on('click', function () { $cancelBtn.trigger('click'); })
|
||||
);
|
||||
$hint.insertAfter($('#inscr-gestion-refund-panel'));
|
||||
if (typeof window.MS1_ADMIN_TRAD_SCAN === 'function') {
|
||||
window.MS1_ADMIN_TRAD_SCAN($hintText[0]);
|
||||
}
|
||||
}
|
||||
$cancelBtn.addClass('d-none');
|
||||
}
|
||||
|
||||
87
kc.php
87
kc.php
@ -1,39 +1,72 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* MSIN-3487 — Point d'entrée QR court.
|
||||
*
|
||||
* Modes :
|
||||
* 1) Résultats événement :
|
||||
* /kc.php?t=eve&id={eve_id}
|
||||
* → inscriptions_evenements_extra.eve_resultats_url
|
||||
* → sinon fallback https://ms1.ca
|
||||
*
|
||||
* 2) Key chain (porte-clés / véhicules) — inchangé :
|
||||
* /kc.php?param1={pk_key_chain}
|
||||
* → key_chain.key_chain_http
|
||||
* → sinon insert + fallback https://ms1.ca
|
||||
*/
|
||||
session_start();
|
||||
global $objDatabase, $vDomaine, $strLangue;
|
||||
require_once "php/inc_fonctions.php";
|
||||
require_once 'php/inc_fonctions.php';
|
||||
|
||||
if (!empty($_GET['param1']))
|
||||
$key_chain_id = $_GET['param1'];
|
||||
else
|
||||
$key_chain_id = 0;
|
||||
$defaut_landingpage="https://ms1.ca";
|
||||
$strType = isset($_GET['t']) ? strtolower(trim((string) $_GET['t'])) : '';
|
||||
|
||||
$sqlkey_chain = "SELECT * FROM key_chain WHERE pk_key_chain = " . $key_chain_id ;
|
||||
$infoCompte = $objDatabase->fxGetRow($sqlkey_chain);
|
||||
if (!$infoCompte==null){
|
||||
|
||||
$mem_count=$infoCompte['key_chain_count']+1;
|
||||
$update_plus="";
|
||||
if ($infoCompte['key_chain_com_id']==0 && isset($_SESSION['com_id'])){
|
||||
$update_plus="key_chain_com_id=".$_SESSION['com_id'].",";}
|
||||
|
||||
$sqlUpdate = "UPDATE key_chain SET ".$update_plus."key_chain_adress_ip='". $_SERVER['REMOTE_ADDR'] ."',key_chain_count = ".$mem_count." WHERE pk_key_chain = " . $key_chain_id ;
|
||||
$qryUpdate = $objDatabase->fxQuery($sqlUpdate);
|
||||
|
||||
header("Location: ".$infoCompte['key_chain_http']);
|
||||
}else{
|
||||
if (isset($_SESSION['com_id'])){
|
||||
$mem_com_id=$_SESSION['com_id'];
|
||||
}else{
|
||||
$mem_com_id=0;
|
||||
// --- MSIN-3487 : redirect résultats par eve_id ---
|
||||
if ($strType === 'eve') {
|
||||
$intEveId = 0;
|
||||
if (isset($_GET['id'])) {
|
||||
$intEveId = intval($_GET['id']);
|
||||
} elseif (isset($_GET['eve_id'])) {
|
||||
$intEveId = intval($_GET['eve_id']);
|
||||
}
|
||||
|
||||
header('Location: ' . fxEveExtraResolveResultatsUrl($intEveId));
|
||||
exit;
|
||||
}
|
||||
|
||||
$sqlUpdate = "insert key_chain SET key_chain_com_id=".$mem_com_id.",key_chain_timestamp=now(),key_chain_http='".$defaut_landingpage."',key_chain_adress_ip='". $_SERVER['REMOTE_ADDR'] ."',key_chain_count = 1,pk_key_chain=" . $key_chain_id;
|
||||
// --- Key chain historique ---
|
||||
$key_chain_id = !empty($_GET['param1']) ? intval($_GET['param1']) : 0;
|
||||
$defaut_landingpage = fxEveExtraResultatsFallbackUrl();
|
||||
|
||||
$sqlkey_chain = 'SELECT * FROM key_chain WHERE pk_key_chain = ' . $key_chain_id;
|
||||
$infoCompte = $objDatabase->fxGetRow($sqlkey_chain);
|
||||
if (!$infoCompte == null) {
|
||||
|
||||
$mem_count = $infoCompte['key_chain_count'] + 1;
|
||||
$update_plus = '';
|
||||
if ($infoCompte['key_chain_com_id'] == 0 && isset($_SESSION['com_id'])) {
|
||||
$update_plus = 'key_chain_com_id=' . intval($_SESSION['com_id']) . ',';
|
||||
}
|
||||
|
||||
$sqlUpdate = 'UPDATE key_chain SET ' . $update_plus
|
||||
. "key_chain_adress_ip='" . $objDatabase->fxEscape($_SERVER['REMOTE_ADDR']) . "',"
|
||||
. 'key_chain_count = ' . intval($mem_count)
|
||||
. ' WHERE pk_key_chain = ' . $key_chain_id;
|
||||
$qryUpdate = $objDatabase->fxQuery($sqlUpdate);
|
||||
|
||||
header("Location: $defaut_landingpage" );
|
||||
header('Location: ' . $infoCompte['key_chain_http']);
|
||||
} else {
|
||||
if (isset($_SESSION['com_id'])) {
|
||||
$mem_com_id = intval($_SESSION['com_id']);
|
||||
} else {
|
||||
$mem_com_id = 0;
|
||||
}
|
||||
|
||||
$sqlUpdate = 'INSERT key_chain SET key_chain_com_id=' . $mem_com_id
|
||||
. ',key_chain_timestamp=now(),'
|
||||
. "key_chain_http='" . $objDatabase->fxEscape($defaut_landingpage) . "',"
|
||||
. "key_chain_adress_ip='" . $objDatabase->fxEscape($_SERVER['REMOTE_ADDR']) . "',"
|
||||
. 'key_chain_count = 1,pk_key_chain=' . $key_chain_id;
|
||||
$qryUpdate = $objDatabase->fxQuery($sqlUpdate);
|
||||
|
||||
header('Location: ' . $defaut_landingpage);
|
||||
}
|
||||
exit;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -48,6 +48,22 @@ function fxChronotrackApiNormalizeEntityList($mixPayload, $strEntityKey) {
|
||||
}
|
||||
}
|
||||
|
||||
// MSIN-4328 — clé CT pour reg_choice (souvent event_reg_choice)
|
||||
if ($strEntityKey === 'reg_choice') {
|
||||
foreach (array('event_reg_choice', 'registration_choice') as $strAltKey) {
|
||||
if (!isset($mixPayload[$strAltKey]) || !is_array($mixPayload[$strAltKey])) {
|
||||
continue;
|
||||
}
|
||||
$mixList = $mixPayload[$strAltKey];
|
||||
if (fxChronotrackApiIsAssocArray($mixList)) {
|
||||
$arrOut[] = $mixList;
|
||||
} else {
|
||||
$arrOut = $mixList;
|
||||
}
|
||||
return $arrOut;
|
||||
}
|
||||
}
|
||||
|
||||
// Clé CT Live pour GET event/{id}/entry (ex. event_entry, pas entry)
|
||||
if ($strEntityKey === 'entry') {
|
||||
foreach (array('event_entry', 'entries') as $strAltKey) {
|
||||
@ -235,6 +251,7 @@ function fxChronotrackApiFormatRaceRegChoicesLabel($arrEntity) {
|
||||
}
|
||||
|
||||
function fxChronotrackApiFormatRaceLabel($arrEntity) {
|
||||
// MSIN-4328 — nom de race seul ; le reg_choice est ajouté au mapping via ApplyRegChoiceLabels
|
||||
$strName = fxChronotrackApiRaceField($arrEntity, array('race_name', 'display_name', 'name', 'title'));
|
||||
if ($strName !== '') {
|
||||
return $strName;
|
||||
@ -244,24 +261,15 @@ function fxChronotrackApiFormatRaceLabel($arrEntity) {
|
||||
$strType = fxChronotrackApiRaceField($arrEntity, array('race_type', 'sport', 'race_course_type'));
|
||||
$strDist = fxChronotrackApiFormatRaceDistanceLabel($arrEntity);
|
||||
$strTeam = fxChronotrackApiFormatRaceTeamLabel($arrEntity);
|
||||
$strRegChoice = fxChronotrackApiRaceField($arrEntity, array(
|
||||
'reg_choice_name', 'registration_choice_name', 'choice_name', 'reg_choice_label',
|
||||
));
|
||||
if ($strRegChoice === '') {
|
||||
$strRegChoice = fxChronotrackApiFormatRaceRegChoicesLabel($arrEntity);
|
||||
}
|
||||
|
||||
$arrExtras = array();
|
||||
if ($strRegChoice !== '' && $strRegChoice !== $strName) {
|
||||
$arrExtras[] = $strRegChoice;
|
||||
}
|
||||
if ($strTag !== '' && $strTag !== $strName && $strTag !== $strRegChoice) {
|
||||
if ($strTag !== '') {
|
||||
$arrExtras[] = 'tag ' . $strTag;
|
||||
}
|
||||
if ($strType !== '') {
|
||||
$arrExtras[] = $strType;
|
||||
}
|
||||
if ($strDist !== '' && $strDist !== $strName) {
|
||||
if ($strDist !== '') {
|
||||
$arrExtras[] = $strDist;
|
||||
}
|
||||
if ($strTeam !== '') {
|
||||
@ -269,10 +277,7 @@ function fxChronotrackApiFormatRaceLabel($arrEntity) {
|
||||
}
|
||||
|
||||
$arrExtras = array_values(array_unique($arrExtras));
|
||||
|
||||
if ($strName === '') {
|
||||
$strName = $strDist !== '' ? $strDist : ('Race ' . fxChronotrackApiEntityId($arrEntity));
|
||||
}
|
||||
$strName = $strDist !== '' ? $strDist : ('Race ' . fxChronotrackApiEntityId($arrEntity));
|
||||
|
||||
if (count($arrExtras) === 0) {
|
||||
return $strName;
|
||||
@ -281,6 +286,154 @@ function fxChronotrackApiFormatRaceLabel($arrEntity) {
|
||||
return $strName . ' — ' . implode(' · ', $arrExtras);
|
||||
}
|
||||
|
||||
/**
|
||||
* MSIN-4328 — Fusionne deux index race_id → noms reg_choice.
|
||||
*
|
||||
* @param array<int, string[]> $arrA
|
||||
* @param array<int, string[]> $arrB
|
||||
* @return array<int, string[]>
|
||||
*/
|
||||
function fxChronotrackApiMergeRegChoiceNamesByRaceId($arrA, $arrB) {
|
||||
if (!is_array($arrA)) {
|
||||
$arrA = array();
|
||||
}
|
||||
if (!is_array($arrB) || count($arrB) === 0) {
|
||||
return $arrA;
|
||||
}
|
||||
foreach ($arrB as $intRaceId => $arrNames) {
|
||||
$intRaceId = intval($intRaceId);
|
||||
if ($intRaceId <= 0 || !is_array($arrNames)) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($arrA[$intRaceId])) {
|
||||
$arrA[$intRaceId] = array();
|
||||
}
|
||||
foreach ($arrNames as $strName) {
|
||||
$strName = trim((string)$strName);
|
||||
if ($strName === '') {
|
||||
continue;
|
||||
}
|
||||
$arrA[$intRaceId][] = $strName;
|
||||
}
|
||||
$arrA[$intRaceId] = array_values(array_unique($arrA[$intRaceId]));
|
||||
}
|
||||
return $arrA;
|
||||
}
|
||||
|
||||
/**
|
||||
* MSIN-4328 — Indexe les noms de reg_choice par race_id parent (payload event/race).
|
||||
*
|
||||
* @return array<int, string[]>
|
||||
*/
|
||||
function fxChronotrackApiRegChoiceNamesByRaceId($mixPayload) {
|
||||
$arrByRace = array();
|
||||
foreach (fxChronotrackApiCollectRegChoiceEntities($mixPayload) as $arrChoice) {
|
||||
if (!is_array($arrChoice)) {
|
||||
continue;
|
||||
}
|
||||
$intParent = intval(fxChronotrackApiRaceField($arrChoice, array('race_id', 'event_race_id')));
|
||||
if ($intParent <= 0 && isset($arrChoice['race']) && is_array($arrChoice['race'])) {
|
||||
$intParent = intval(fxChronotrackApiEntityId($arrChoice['race']));
|
||||
}
|
||||
if ($intParent <= 0) {
|
||||
continue;
|
||||
}
|
||||
$strName = fxChronotrackApiRaceField($arrChoice, array(
|
||||
'reg_choice_name', 'registration_choice_name', 'choice_name', 'display_name', 'name', 'title',
|
||||
));
|
||||
if ($strName === '' && isset($arrChoice['reg_choice_id']) && $arrChoice['reg_choice_id'] !== '') {
|
||||
$strName = 'reg_choice ' . $arrChoice['reg_choice_id'];
|
||||
}
|
||||
if ($strName === '') {
|
||||
continue;
|
||||
}
|
||||
if (!isset($arrByRace[$intParent])) {
|
||||
$arrByRace[$intParent] = array();
|
||||
}
|
||||
$arrByRace[$intParent][] = $strName;
|
||||
}
|
||||
foreach ($arrByRace as $intRaceId => $arrNames) {
|
||||
$arrByRace[$intRaceId] = array_values(array_unique($arrNames));
|
||||
}
|
||||
return $arrByRace;
|
||||
}
|
||||
|
||||
/**
|
||||
* MSIN-4328 — Filet : déduit race_id → reg_choice_name depuis les entries CT.
|
||||
*
|
||||
* @return array<int, string[]>
|
||||
*/
|
||||
function fxChronotrackApiRegChoiceNamesFromEntriesPayload($mixPayload) {
|
||||
$arrByRace = array();
|
||||
$arrEntries = fxChronotrackApiNormalizeEntityList($mixPayload, 'entry');
|
||||
if (count($arrEntries) === 0 && is_array($mixPayload) && isset($mixPayload['event']) && is_array($mixPayload['event'])) {
|
||||
$arrEntries = fxChronotrackApiNormalizeEntityList($mixPayload['event'], 'entry');
|
||||
}
|
||||
foreach ($arrEntries as $arrEntry) {
|
||||
if (!is_array($arrEntry)) {
|
||||
continue;
|
||||
}
|
||||
$intRaceId = intval(fxChronotrackApiRaceField($arrEntry, array('race_id', 'event_race_id')));
|
||||
if ($intRaceId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$strName = fxChronotrackApiRaceField($arrEntry, array(
|
||||
'reg_choice_name', 'registration_choice_name', 'choice_name',
|
||||
));
|
||||
if ($strName === '') {
|
||||
continue;
|
||||
}
|
||||
if (!isset($arrByRace[$intRaceId])) {
|
||||
$arrByRace[$intRaceId] = array();
|
||||
}
|
||||
$arrByRace[$intRaceId][] = $strName;
|
||||
}
|
||||
foreach ($arrByRace as $intRaceId => $arrNames) {
|
||||
$arrByRace[$intRaceId] = array_values(array_unique($arrNames));
|
||||
}
|
||||
return $arrByRace;
|
||||
}
|
||||
|
||||
/**
|
||||
* MSIN-4328 — Ajoute « (reg_choice) » au label de chaque course du mapping.
|
||||
*/
|
||||
function fxChronotrackApiApplyRegChoiceLabelsToRaceRows(&$arrRaceRows, $arrNamesByRaceId) {
|
||||
if (!is_array($arrRaceRows) || !is_array($arrNamesByRaceId)) {
|
||||
return;
|
||||
}
|
||||
foreach ($arrRaceRows as &$arrRow) {
|
||||
if (!is_array($arrRow)) {
|
||||
continue;
|
||||
}
|
||||
$intId = intval($arrRow['ct_race_id'] ?? 0);
|
||||
$strRc = '';
|
||||
if ($intId > 0 && !empty($arrNamesByRaceId[$intId]) && is_array($arrNamesByRaceId[$intId])) {
|
||||
$strRc = implode(', ', $arrNamesByRaceId[$intId]);
|
||||
}
|
||||
if ($strRc === '' && isset($arrRow['raw']) && is_array($arrRow['raw'])) {
|
||||
$strRc = fxChronotrackApiFormatRaceRegChoicesLabel($arrRow['raw']);
|
||||
if ($strRc === '') {
|
||||
$strRc = fxChronotrackApiRaceField($arrRow['raw'], array(
|
||||
'reg_choice_name', 'registration_choice_name', 'choice_name', 'reg_choice_label',
|
||||
));
|
||||
}
|
||||
}
|
||||
if ($strRc === '') {
|
||||
continue;
|
||||
}
|
||||
$arrRow['reg_choice_label'] = $strRc;
|
||||
$strBase = isset($arrRow['name']) && $arrRow['name'] !== ''
|
||||
? $arrRow['name']
|
||||
: (isset($arrRow['label']) ? $arrRow['label'] : 'Course');
|
||||
// Évite double parenthèses si FormatRaceLabel a déjà collé le même texte
|
||||
$strExpected = $strBase . ' (' . $strRc . ')';
|
||||
if (!isset($arrRow['label']) || $arrRow['label'] !== $strExpected) {
|
||||
$arrRow['label'] = $strExpected;
|
||||
}
|
||||
}
|
||||
unset($arrRow);
|
||||
}
|
||||
|
||||
function fxChronotrackApiEntityStartEpoch($arrEntity) {
|
||||
foreach (array(
|
||||
'race_planned_start_time', 'race_start_time', 'event_start_time',
|
||||
@ -625,6 +778,29 @@ function fxChronotrackApiFetchRacesForEvent($intCtEventId) {
|
||||
}
|
||||
|
||||
$arrRaceRows = fxChronotrackApiParseRaceRowsFromPayload($arrApi['json'], $intCtEventId);
|
||||
|
||||
// MSIN-4328 — enrichir libellés dropdown CT : race_name (reg_choice)
|
||||
$arrNamesByRace = fxChronotrackApiRegChoiceNamesByRaceId($arrApi['json']);
|
||||
if (count($arrRaceRows) > 0) {
|
||||
$arrEventApi = fxChronotrackApiOAuthApiGet('event/' . $intCtEventId);
|
||||
if (($arrEventApi['state'] ?? '') === 'ok') {
|
||||
$arrNamesByRace = fxChronotrackApiMergeRegChoiceNamesByRaceId(
|
||||
$arrNamesByRace,
|
||||
fxChronotrackApiRegChoiceNamesByRaceId($arrEventApi['json'] ?? null)
|
||||
);
|
||||
}
|
||||
// Filet : noms visibles CT souvent présents sur les entries
|
||||
if (count($arrNamesByRace) === 0) {
|
||||
$arrEntryApi = fxChronotrackApiOAuthApiGet('event/' . $intCtEventId . '/entry');
|
||||
if (($arrEntryApi['state'] ?? '') === 'ok') {
|
||||
$arrNamesByRace = fxChronotrackApiRegChoiceNamesFromEntriesPayload(
|
||||
$arrEntryApi['json'] ?? null
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
fxChronotrackApiApplyRegChoiceLabelsToRaceRows($arrRaceRows, $arrNamesByRace);
|
||||
|
||||
if (count($arrRaceRows) > 0) {
|
||||
return array(
|
||||
'state' => 'ok',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -12,6 +12,7 @@ include_once APPLICATION_PATH . 'php/inc_mysql.php';
|
||||
include_once APPLICATION_PATH . 'php/inc_dates.php';
|
||||
include_once APPLICATION_PATH . 'php/inc_mail.php';
|
||||
include_once APPLICATION_PATH . 'php/inc_fx_memberships.php';
|
||||
include_once APPLICATION_PATH . 'php/inc_fx_eve_extra.php'; // MSIN-3487 — extra événement (lien résultats, etc.)
|
||||
include_once APPLICATION_PATH . 'php/inc_fx_liste_attente.php';// inclure les fonctions des abonemennts
|
||||
include_once APPLICATION_PATH . 'php/recaptcha.class.php';
|
||||
|
||||
@ -368,6 +369,44 @@ function fxGetPage($code)
|
||||
return $recPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Corrige les liens internes CMS issus de TinyMCE (relative_urls).
|
||||
* Ex. href="page/cookie/fr" ou href="page/confidentialite/page/cookie/fr."
|
||||
* → /page/cookie/fr. Ne touche pas http(s), mailto, #, ni les /page/ déjà corrects.
|
||||
*/
|
||||
function fxNormalizeCmsPageLinks($strHtml)
|
||||
{
|
||||
if ($strHtml === null || $strHtml === '') {
|
||||
return $strHtml;
|
||||
}
|
||||
|
||||
return preg_replace_callback(
|
||||
'#\bhref=(["\'])([^"\']*)\1#i',
|
||||
function ($arrMatch) {
|
||||
$strQuote = $arrMatch[1];
|
||||
$strUrl = rtrim($arrMatch[2], '.');
|
||||
|
||||
if ($strUrl === ''
|
||||
|| $strUrl[0] === '#'
|
||||
|| stripos($strUrl, 'mailto:') === 0
|
||||
|| preg_match('#^(?:https?:)?//#i', $strUrl)
|
||||
) {
|
||||
return $arrMatch[0];
|
||||
}
|
||||
|
||||
// Dernier segment page/{code}/{fr|en} (y compris chemins imbriqués TinyMCE)
|
||||
if (preg_match('#(?:^|/)page/([\w\-]+)/(fr|en)/?$#i', $strUrl, $arrPage)
|
||||
&& preg_match('#(^|/)page/#i', $strUrl)
|
||||
) {
|
||||
return 'href=' . $strQuote . '/page/' . $arrPage[1] . '/' . strtolower($arrPage[2]) . $strQuote;
|
||||
}
|
||||
|
||||
return $arrMatch[0];
|
||||
},
|
||||
$strHtml
|
||||
);
|
||||
}
|
||||
|
||||
// fonction qui détermine le url de retour (JSP)
|
||||
// créé : 2012-07-02
|
||||
function fxGetReferer()
|
||||
@ -386,6 +425,57 @@ function fxGetReferer()
|
||||
return $url_retour;
|
||||
}
|
||||
|
||||
/**
|
||||
* MSIN-4474 — URL de retour après modif inscription (même hôte uniquement).
|
||||
* Préserve la liste d'origine (ex. inscriptions_gestion) via GET/POST return_url.
|
||||
*/
|
||||
function fxSafeReturnUrl($strUrl, $strFallback = '')
|
||||
{
|
||||
global $vDomaine;
|
||||
|
||||
$strUrl = trim((string)$strUrl);
|
||||
$strFallback = trim((string)$strFallback);
|
||||
|
||||
if ($strUrl === '') {
|
||||
return $strFallback;
|
||||
}
|
||||
|
||||
$arrDom = parse_url($vDomaine);
|
||||
$arrUrl = parse_url($strUrl);
|
||||
|
||||
if ($arrUrl === false || empty($arrUrl['host']) || empty($arrDom['host'])) {
|
||||
return $strFallback;
|
||||
}
|
||||
|
||||
if (stripos($arrUrl['host'], $arrDom['host']) === false) {
|
||||
return $strFallback;
|
||||
}
|
||||
|
||||
return $strUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* MSIN-4474 — Résout l'URL de retour pour le formulaire modifier (promoteur/client).
|
||||
* Ignore le referer s'il pointe déjà vers reserver/book (après Suivant / switch).
|
||||
*/
|
||||
function fxModInscriptionReturnUrl($strFallback = '')
|
||||
{
|
||||
$strCandidate = '';
|
||||
|
||||
if (!empty($_POST['return_url'])) {
|
||||
$strCandidate = $_POST['return_url'];
|
||||
} elseif (!empty($_GET['return_url'])) {
|
||||
$strCandidate = $_GET['return_url'];
|
||||
} elseif (!empty($_SERVER['HTTP_REFERER'])) {
|
||||
$strRef = (string)$_SERVER['HTTP_REFERER'];
|
||||
if (!preg_match('#/(reserver|book)(/|\\?|$)#i', $strRef)) {
|
||||
$strCandidate = $strRef;
|
||||
}
|
||||
}
|
||||
|
||||
return fxSafeReturnUrl($strCandidate, $strFallback);
|
||||
}
|
||||
|
||||
// fonction pour un message via la page contact (JSP)
|
||||
// param : données post, langue
|
||||
// créé : 2012-07-03
|
||||
|
||||
@ -463,11 +463,13 @@ function fxAdminTradMarkerScriptHtml() {
|
||||
textNode.parentNode.replaceChild(frag, textNode);
|
||||
}
|
||||
|
||||
function scan() {
|
||||
if (!document.body) {
|
||||
// root optionnel : rescan après HTML dynamique (ex. hint remboursement AJAX).
|
||||
function scan(root) {
|
||||
var start = root && root.nodeType ? root : document.body;
|
||||
if (!start) {
|
||||
return;
|
||||
}
|
||||
var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null);
|
||||
var walker = document.createTreeWalker(start, NodeFilter.SHOW_TEXT, null);
|
||||
var nodes = [];
|
||||
while (walker.nextNode()) {
|
||||
nodes.push(walker.currentNode);
|
||||
@ -475,8 +477,10 @@ function fxAdminTradMarkerScriptHtml() {
|
||||
nodes.forEach(replaceMarkers);
|
||||
}
|
||||
|
||||
window.MS1_ADMIN_TRAD_SCAN = scan;
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", scan);
|
||||
document.addEventListener("DOMContentLoaded", function () { scan(); });
|
||||
} else {
|
||||
scan();
|
||||
}
|
||||
|
||||
@ -142,6 +142,43 @@ function fxBibFreeDuplicateRisk($int_eve_id, $tabPlan, $intStart = 0, $intEnd =
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* MSIN-4445 — Valide et ordonne les epr_id du bloc libre (solo, avec ou sans séquence).
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
function fxBibParseGlobalBatchFreeEprIds($eprIds, $int_eve_id) {
|
||||
if (!is_array($eprIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$int_eve_id = (int)$int_eve_id;
|
||||
$tabWanted = [];
|
||||
|
||||
foreach ($eprIds as $epr_id) {
|
||||
$epr_id = (int)$epr_id;
|
||||
if ($epr_id <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$check = fxBibAssertSoloEpreuveForGlobalBatch($epr_id, $int_eve_id);
|
||||
if (!$check['success']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tabWanted[$epr_id] = true;
|
||||
}
|
||||
|
||||
if (empty($tabWanted)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$tabIds = array_keys($tabWanted);
|
||||
sort($tabIds, SORT_NUMERIC);
|
||||
|
||||
return $tabIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plan de plages virtuelles bornées par Début/Fin (sans écriture BD).
|
||||
* Enchaîne les épreuves comme le bloc généré, mais s'arrête à $intEnd.
|
||||
@ -152,7 +189,7 @@ function fxBibBuildFreeRangePlan($int_eve_id, $tabEprIds, $intStart, $intEnd, $s
|
||||
$int_eve_id = (int)$int_eve_id;
|
||||
$intStart = (int)$intStart;
|
||||
$intEnd = (int)$intEnd;
|
||||
$tabEprIds = fxBibParseGlobalBatchGeneratedEprIds($tabEprIds, $int_eve_id);
|
||||
$tabEprIds = fxBibParseGlobalBatchFreeEprIds($tabEprIds, $int_eve_id);
|
||||
$intCursor = $intStart;
|
||||
$tabPlan = [];
|
||||
|
||||
@ -242,13 +279,7 @@ function fxBibValidateFreeRangePlan($int_eve_id, $tabPlan, $intStart, $intEnd, $
|
||||
];
|
||||
}
|
||||
|
||||
if (fxBibEpreuveHasAnySequence($epr_id)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => fxBibMsg('bib_v4_global_batch_gen_has_seq'),
|
||||
];
|
||||
}
|
||||
|
||||
// MSIN-4445 — OK même si une séquence existe déjà (aucune création de séquence).
|
||||
$blnHasAssignable = true;
|
||||
}
|
||||
|
||||
|
||||
@ -25,11 +25,12 @@ function fxBibEpreuveHasAnySequence($epr_id) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Épreuves solo sans aucune séquence, triées par epr_id (ordre page).
|
||||
* Épreuves solo actives (avec ou sans séquence), triées par epr_id.
|
||||
* MSIN-4445 — utilisé par l'assignation libre.
|
||||
*
|
||||
* @return array[] lignes inscriptions_epreuves
|
||||
*/
|
||||
function fxBibGetSoloEpreuvesWithoutSequence($int_eve_id, $tabEpreuves = null) {
|
||||
function fxBibGetSoloEpreuves($int_eve_id, $tabEpreuves = null) {
|
||||
global $objDatabase;
|
||||
|
||||
$int_eve_id = (int)$int_eve_id;
|
||||
@ -52,6 +53,21 @@ function fxBibGetSoloEpreuvesWithoutSequence($int_eve_id, $tabEpreuves = null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$out[] = $epr;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Épreuves solo sans aucune séquence, triées par epr_id (ordre page).
|
||||
*
|
||||
* @return array[] lignes inscriptions_epreuves
|
||||
*/
|
||||
function fxBibGetSoloEpreuvesWithoutSequence($int_eve_id, $tabEpreuves = null) {
|
||||
$out = [];
|
||||
|
||||
foreach (fxBibGetSoloEpreuves($int_eve_id, $tabEpreuves) as $epr) {
|
||||
if (fxBibEpreuveHasAnySequence((int)$epr['epr_id'])) {
|
||||
continue;
|
||||
}
|
||||
@ -90,10 +106,16 @@ function fxBibGlobalBatchHasGeneratedBlock($int_eve_id, $tabEpreuves) {
|
||||
return count(fxBibGetSoloEpreuvesWithoutSequence($int_eve_id, $tabEpreuves)) >= 1;
|
||||
}
|
||||
|
||||
/** Tuile / coquille assignation globale visible si l'un des deux blocs est utilisable. */
|
||||
/** MSIN-4445 — Bloc libre : au moins 1 épreuve solo (avec ou sans séquence). */
|
||||
function fxBibGlobalBatchHasFreeBlock($int_eve_id, $tabEpreuves) {
|
||||
return count(fxBibGetSoloEpreuves($int_eve_id, $tabEpreuves)) >= 1;
|
||||
}
|
||||
|
||||
/** Tuile / coquille assignation globale visible si l'un des blocs est utilisable. */
|
||||
function fxBibGlobalBatchToolIsAvailable($int_eve_id, $tabEpreuves) {
|
||||
return fxBibGlobalBatchHasExistingBlock($int_eve_id, $tabEpreuves)
|
||||
|| fxBibGlobalBatchHasGeneratedBlock($int_eve_id, $tabEpreuves);
|
||||
|| fxBibGlobalBatchHasGeneratedBlock($int_eve_id, $tabEpreuves)
|
||||
|| fxBibGlobalBatchHasFreeBlock($int_eve_id, $tabEpreuves);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -266,6 +288,13 @@ function fxBibCreateGeneratedRanges($int_eve_id, $tabPlan, $strLangue = 'fr') {
|
||||
$int_eve_id = (int)$int_eve_id;
|
||||
$tabCreated = [];
|
||||
|
||||
if (fxBibProductionEventIsLocked($int_eve_id)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => fxBibMsg('bib_v5_production_locked_error'),
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($tabPlan as $item) {
|
||||
if (!empty($item['skip'])) {
|
||||
continue;
|
||||
|
||||
926
php/inc_fx_bib_production.php
Normal file
926
php/inc_fx_bib_production.php
Normal file
@ -0,0 +1,926 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* MSIN-4469 — Production Excel des dossards.
|
||||
*
|
||||
* Une commande active représente un instantané immuable des séquences. Le fichier
|
||||
* peut être téléchargé de nouveau, mais une seconde génération est refusée tant
|
||||
* que la commande active n'a pas été remplacée par un futur garde-fou.
|
||||
*/
|
||||
|
||||
function fxBibProductionTemplatePath() {
|
||||
return __DIR__ . DIRECTORY_SEPARATOR . 'templates'
|
||||
. DIRECTORY_SEPARATOR . 'MSIN-4469-commande-dossards.xlsx';
|
||||
}
|
||||
|
||||
function fxBibProductionStorageRoot() {
|
||||
return dirname(__DIR__) . DIRECTORY_SEPARATOR . 'data'
|
||||
. DIRECTORY_SEPARATOR . 'bib-production';
|
||||
}
|
||||
|
||||
function fxBibProductionGetActiveOrder($intEveId) {
|
||||
global $objDatabase;
|
||||
|
||||
$intEveId = (int)$intEveId;
|
||||
if ($intEveId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $objDatabase->fxGetRow(
|
||||
"SELECT *
|
||||
FROM inscriptions_bib_production_orders
|
||||
WHERE eve_id = $intEveId
|
||||
AND bpo_active_key = 1
|
||||
LIMIT 1"
|
||||
);
|
||||
}
|
||||
|
||||
function fxBibProductionGetOrder($intOrderId, $intEveId = 0) {
|
||||
global $objDatabase;
|
||||
|
||||
$intOrderId = (int)$intOrderId;
|
||||
$intEveId = (int)$intEveId;
|
||||
if ($intOrderId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$strWhereEve = $intEveId > 0 ? " AND eve_id = $intEveId" : '';
|
||||
return $objDatabase->fxGetRow(
|
||||
"SELECT *
|
||||
FROM inscriptions_bib_production_orders
|
||||
WHERE bpo_id = $intOrderId
|
||||
$strWhereEve
|
||||
LIMIT 1"
|
||||
);
|
||||
}
|
||||
|
||||
function fxBibProductionEventIsLocked($intEveId) {
|
||||
return fxBibProductionGetActiveOrder($intEveId) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* MSIN-4469 — Déverrouillage temporaire (dev) : même droit que la génération
|
||||
* (promoteur avec dossards.manage, ou superadmin).
|
||||
*/
|
||||
function fxBibProductionDevelopmentUnlock($intEveId) {
|
||||
global $objDatabase;
|
||||
|
||||
$intEveId = (int)$intEveId;
|
||||
if ($intEveId <= 0 || !fxBibProductionUserCanManageEvent($intEveId)) {
|
||||
return ['success' => false, 'message' => fxBibMsg('bib_v4_ajax_unauthorized')];
|
||||
}
|
||||
|
||||
$tabOrder = fxBibProductionGetActiveOrder($intEveId);
|
||||
if (!$tabOrder) {
|
||||
return ['success' => true];
|
||||
}
|
||||
|
||||
$strBy = fxBibGetCurrentPromoteurDisplayName();
|
||||
$objDatabase->fxQuery(
|
||||
"UPDATE inscriptions_bib_production_orders
|
||||
SET bpo_status = 'development_unlocked',
|
||||
bpo_active_key = NULL,
|
||||
bpo_superseded_at = NOW(),
|
||||
bpo_superseded_by = '" . $objDatabase->fxEscape($strBy) . "',
|
||||
bpo_superseded_reason = 'MSIN-4469 development unlock'
|
||||
WHERE bpo_id = " . (int)$tabOrder['bpo_id'] . "
|
||||
AND bpo_active_key = 1"
|
||||
);
|
||||
|
||||
return ['success' => true];
|
||||
}
|
||||
|
||||
function fxBibProductionEpreuveIsLocked($intEprId) {
|
||||
global $objDatabase;
|
||||
|
||||
$intEprId = (int)$intEprId;
|
||||
if ($intEprId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$intEveId = (int)$objDatabase->fxGetVar(
|
||||
"SELECT eve_id FROM inscriptions_epreuves WHERE epr_id = $intEprId LIMIT 1"
|
||||
);
|
||||
return fxBibProductionEventIsLocked($intEveId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie l'accès réel à l'événement, en plus de la session et du jeton CSRF.
|
||||
*/
|
||||
function fxBibProductionUserCanManageEvent($intEveId) {
|
||||
$intEveId = (int)$intEveId;
|
||||
if ($intEveId <= 0 || !fxBibRequirePromoteurSession()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!empty($_SESSION['usa_id'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$intComId = (int)($_SESSION['com_id'] ?? 0);
|
||||
return $intComId > 0
|
||||
&& function_exists('fxEveAccesHasPermission')
|
||||
&& fxEveAccesHasPermission($intComId, $intEveId, 'dossards.manage');
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge les épreuves actives et toutes leurs séquences, dans l'ordre d'affichage.
|
||||
*/
|
||||
function fxBibProductionCollectSnapshot($intEveId, $strLangue = 'fr') {
|
||||
global $objDatabase;
|
||||
|
||||
$intEveId = (int)$intEveId;
|
||||
$strLangue = in_array($strLangue, ['fr', 'en'], true) ? $strLangue : 'fr';
|
||||
|
||||
$tabEvent = $objDatabase->fxGetRow(
|
||||
"SELECT eve_id, eve_nom_fr, eve_nom_en
|
||||
FROM inscriptions_evenements
|
||||
WHERE eve_id = $intEveId
|
||||
LIMIT 1"
|
||||
);
|
||||
if (!$tabEvent) {
|
||||
return ['success' => false, 'message' => fxBibMsg('bib_v5_production_event_invalid')];
|
||||
}
|
||||
|
||||
$tabEpreuves = $objDatabase->fxGetResults(
|
||||
"SELECT *
|
||||
FROM inscriptions_epreuves
|
||||
WHERE eve_id = $intEveId
|
||||
AND epr_actif = 1
|
||||
ORDER BY epr_tri, epr_id"
|
||||
);
|
||||
if (!is_array($tabEpreuves)) {
|
||||
$tabEpreuves = [];
|
||||
}
|
||||
|
||||
$tabRaces = [];
|
||||
$intBibCount = 0;
|
||||
$intRangeCount = 0;
|
||||
|
||||
foreach ($tabEpreuves as $tabEpreuve) {
|
||||
$intEprId = (int)($tabEpreuve['epr_id'] ?? 0);
|
||||
if ($intEprId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// MSIN-4469 — La commande porte sur toute la séquence à imprimer,
|
||||
// même lorsqu’aucun coureur ni aucun dossard assigné ne s’y trouve encore.
|
||||
$tabRangesRaw = $objDatabase->fxGetResults(
|
||||
"SELECT epr_bib_id, epr_bib_start, epr_bib_finish
|
||||
FROM inscriptions_epreuves_bib
|
||||
WHERE epr_id = $intEprId
|
||||
ORDER BY epr_bib_start, epr_bib_id"
|
||||
);
|
||||
if (!is_array($tabRangesRaw)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tabRanges = [];
|
||||
foreach ($tabRangesRaw as $tabRange) {
|
||||
$intStart = (int)($tabRange['epr_bib_start'] ?? 0);
|
||||
$intFinish = (int)($tabRange['epr_bib_finish'] ?? 0);
|
||||
if ($intStart <= 0 || $intFinish < $intStart) {
|
||||
return ['success' => false, 'message' => fxBibMsg('bib_v5_production_range_invalid')];
|
||||
}
|
||||
|
||||
$intQuantity = $intFinish - $intStart + 1;
|
||||
$intBibCount += $intQuantity;
|
||||
$intRangeCount++;
|
||||
$tabRanges[] = [
|
||||
'epr_bib_id' => (int)$tabRange['epr_bib_id'],
|
||||
'start' => $intStart,
|
||||
'finish' => $intFinish,
|
||||
'quantity' => $intQuantity,
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($tabRanges)) {
|
||||
$tabRaces[] = [
|
||||
'epr_id' => $intEprId,
|
||||
// Le libellé type/catégorie est le nom court retenu pour la V1.
|
||||
'label' => fxBibEpreuveDisplayName($tabEpreuve, $strLangue),
|
||||
'ranges' => $tabRanges,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($intRangeCount === 0) {
|
||||
return ['success' => false, 'message' => fxBibMsg('bib_v5_production_no_ranges')];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'snapshot' => [
|
||||
'event' => [
|
||||
'eve_id' => $intEveId,
|
||||
'name' => trim((string)($tabEvent['eve_nom_' . $strLangue] ?? '')),
|
||||
],
|
||||
'language' => $strLangue,
|
||||
'bib_count' => $intBibCount,
|
||||
'range_count' => $intRangeCount,
|
||||
'races' => $tabRaces,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
function fxBibProductionSafeFilePart($strValue) {
|
||||
$strValue = trim((string)$strValue);
|
||||
if (function_exists('iconv')) {
|
||||
$strAscii = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $strValue);
|
||||
if ($strAscii !== false) {
|
||||
$strValue = $strAscii;
|
||||
}
|
||||
}
|
||||
$strValue = strtolower($strValue);
|
||||
$strValue = preg_replace('/[^a-z0-9]+/', '-', $strValue);
|
||||
$strValue = trim((string)$strValue, '-');
|
||||
return $strValue !== '' ? $strValue : 'evenement';
|
||||
}
|
||||
|
||||
function fxBibProductionEnsureDirectory($strDirectory) {
|
||||
if (is_dir($strDirectory)) {
|
||||
return true;
|
||||
}
|
||||
return @mkdir($strDirectory, 0770, true) || is_dir($strDirectory);
|
||||
}
|
||||
|
||||
function fxBibProductionXmlLoad($strXml) {
|
||||
$objDom = new DOMDocument('1.0', 'UTF-8');
|
||||
$objDom->preserveWhiteSpace = false;
|
||||
$objDom->formatOutput = false;
|
||||
if (!@$objDom->loadXML($strXml)) {
|
||||
throw new Exception('MSIN-4469 invalid OpenXML part');
|
||||
}
|
||||
return $objDom;
|
||||
}
|
||||
|
||||
/** @return DOMElement|null */
|
||||
function fxBibProductionXmlFindRow(DOMXPath $objXpath, $intRow) {
|
||||
$objRow = $objXpath->query('//m:sheetData/m:row[@r="' . (int)$intRow . '"]')->item(0);
|
||||
return $objRow instanceof DOMElement ? $objRow : null;
|
||||
}
|
||||
|
||||
function fxBibProductionXmlClearCell(DOMElement $objCell) {
|
||||
while ($objCell->firstChild) {
|
||||
$objCell->removeChild($objCell->firstChild);
|
||||
}
|
||||
$objCell->removeAttribute('t');
|
||||
}
|
||||
|
||||
function fxBibProductionXmlFindCell(DOMElement $objRow, $strColumn) {
|
||||
foreach ($objRow->childNodes as $objChild) {
|
||||
if ($objChild instanceof DOMElement && $objChild->localName === 'c') {
|
||||
$strRef = $objChild->getAttribute('r');
|
||||
if (preg_replace('/[0-9]+/', '', $strRef) === $strColumn) {
|
||||
return $objChild;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function fxBibProductionXmlEnsureCell(
|
||||
DOMDocument $objDom,
|
||||
DOMElement $objRow,
|
||||
$strColumn,
|
||||
$intRow,
|
||||
$strStyle = ''
|
||||
) {
|
||||
$objCell = fxBibProductionXmlFindCell($objRow, $strColumn);
|
||||
if (!$objCell) {
|
||||
$objCell = $objDom->createElementNS(
|
||||
'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
||||
'c'
|
||||
);
|
||||
$objRow->appendChild($objCell);
|
||||
}
|
||||
$objCell->setAttribute('r', $strColumn . (int)$intRow);
|
||||
if ($strStyle !== '') {
|
||||
$objCell->setAttribute('s', $strStyle);
|
||||
}
|
||||
return $objCell;
|
||||
}
|
||||
|
||||
function fxBibProductionXmlSetInlineString(
|
||||
DOMDocument $objDom,
|
||||
DOMElement $objRow,
|
||||
$strColumn,
|
||||
$intRow,
|
||||
$strValue,
|
||||
$strStyle = ''
|
||||
) {
|
||||
$strNs = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
|
||||
$objCell = fxBibProductionXmlEnsureCell($objDom, $objRow, $strColumn, $intRow, $strStyle);
|
||||
fxBibProductionXmlClearCell($objCell);
|
||||
$objCell->setAttribute('t', 'inlineStr');
|
||||
$objInline = $objDom->createElementNS($strNs, 'is');
|
||||
$objText = $objDom->createElementNS($strNs, 't');
|
||||
$objText->appendChild($objDom->createTextNode((string)$strValue));
|
||||
$objInline->appendChild($objText);
|
||||
$objCell->appendChild($objInline);
|
||||
}
|
||||
|
||||
function fxBibProductionXmlSetNumber(
|
||||
DOMDocument $objDom,
|
||||
DOMElement $objRow,
|
||||
$strColumn,
|
||||
$intRow,
|
||||
$mixValue,
|
||||
$strStyle = ''
|
||||
) {
|
||||
$strNs = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
|
||||
$objCell = fxBibProductionXmlEnsureCell($objDom, $objRow, $strColumn, $intRow, $strStyle);
|
||||
fxBibProductionXmlClearCell($objCell);
|
||||
$objValue = $objDom->createElementNS($strNs, 'v');
|
||||
$objValue->appendChild($objDom->createTextNode((string)$mixValue));
|
||||
$objCell->appendChild($objValue);
|
||||
}
|
||||
|
||||
function fxBibProductionXmlSetFormula(
|
||||
DOMDocument $objDom,
|
||||
DOMElement $objRow,
|
||||
$strColumn,
|
||||
$intRow,
|
||||
$strFormula,
|
||||
$strStyle = '',
|
||||
$mixCachedValue = null
|
||||
) {
|
||||
$strNs = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
|
||||
$objCell = fxBibProductionXmlEnsureCell($objDom, $objRow, $strColumn, $intRow, $strStyle);
|
||||
fxBibProductionXmlClearCell($objCell);
|
||||
$objFormula = $objDom->createElementNS($strNs, 'f');
|
||||
$objFormula->appendChild($objDom->createTextNode($strFormula));
|
||||
$objCell->appendChild($objFormula);
|
||||
// MSIN-4469 — Valeur calculée en cache : sinon Excel/LibreOffice peut
|
||||
// afficher le TOTAL vide tant que le recalcul n'a pas tourné.
|
||||
if ($mixCachedValue !== null && $mixCachedValue !== '') {
|
||||
$objValue = $objDom->createElementNS($strNs, 'v');
|
||||
$objValue->appendChild($objDom->createTextNode((string)$mixCachedValue));
|
||||
$objCell->appendChild($objValue);
|
||||
}
|
||||
}
|
||||
|
||||
function fxBibProductionXmlCloneRow(DOMElement $objTemplateRow, $intTargetRow) {
|
||||
$objRow = $objTemplateRow->cloneNode(true);
|
||||
$objRow->setAttribute('r', (string)$intTargetRow);
|
||||
foreach ($objRow->childNodes as $objChild) {
|
||||
if ($objChild instanceof DOMElement && $objChild->localName === 'c') {
|
||||
$strColumn = preg_replace('/[0-9]+/', '', $objChild->getAttribute('r'));
|
||||
$objChild->setAttribute('r', $strColumn . $intTargetRow);
|
||||
fxBibProductionXmlClearCell($objChild);
|
||||
}
|
||||
}
|
||||
return $objRow;
|
||||
}
|
||||
|
||||
function fxBibProductionXmlShiftReference($strReference, $intFromRow, $intOffset) {
|
||||
return preg_replace_callback(
|
||||
'/([A-Z]+)([0-9]+)/',
|
||||
function ($tabMatch) use ($intFromRow, $intOffset) {
|
||||
$intRow = (int)$tabMatch[2];
|
||||
if ($intRow >= $intFromRow) {
|
||||
$intRow += $intOffset;
|
||||
}
|
||||
return $tabMatch[1] . $intRow;
|
||||
},
|
||||
$strReference
|
||||
);
|
||||
}
|
||||
|
||||
function fxBibProductionBuildSupplierXml($strXml, array $tabSnapshot, array $tabLabels) {
|
||||
$objDom = fxBibProductionXmlLoad($strXml);
|
||||
$objXpath = new DOMXPath($objDom);
|
||||
$objXpath->registerNamespace('m', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
|
||||
|
||||
$objSheetData = $objXpath->query('//m:sheetData')->item(0);
|
||||
$objHeaderTemplate = fxBibProductionXmlFindRow($objXpath, 1);
|
||||
$objDataTemplate = fxBibProductionXmlFindRow($objXpath, 2);
|
||||
if (!$objSheetData || !$objHeaderTemplate || !$objDataTemplate) {
|
||||
throw new Exception('MSIN-4469 invalid supplier template');
|
||||
}
|
||||
|
||||
while ($objSheetData->firstChild) {
|
||||
$objSheetData->removeChild($objSheetData->firstChild);
|
||||
}
|
||||
|
||||
// MSIN-4469 — Convention imprimeur : cellules bleues = imprimées sur le dossard.
|
||||
// Colonne A (numéro) = bleu ; colonne B (épreuve) = neutre (info, non imprimée).
|
||||
$objHeader = fxBibProductionXmlCloneRow($objHeaderTemplate, 1);
|
||||
$strHeaderStyleA = (string)(fxBibProductionXmlFindCell($objHeader, 'A')->getAttribute('s'));
|
||||
$strHeaderStyleB = (string)(fxBibProductionXmlFindCell($objHeader, 'B')->getAttribute('s'));
|
||||
fxBibProductionXmlSetInlineString($objDom, $objHeader, 'A', 1, $tabLabels['bib'], $strHeaderStyleA);
|
||||
fxBibProductionXmlSetInlineString($objDom, $objHeader, 'B', 1, $tabLabels['race'], $strHeaderStyleB);
|
||||
$objSheetData->appendChild($objHeader);
|
||||
|
||||
$strDataStyleA = (string)(fxBibProductionXmlFindCell($objDataTemplate, 'A')->getAttribute('s'));
|
||||
$strDataStyleB = (string)(fxBibProductionXmlFindCell($objDataTemplate, 'B')->getAttribute('s'));
|
||||
$intRow = 2;
|
||||
foreach ($tabSnapshot['races'] as $intRaceIndex => $tabRace) {
|
||||
if ($intRaceIndex > 0) {
|
||||
$intRow++;
|
||||
}
|
||||
foreach ($tabRace['ranges'] as $tabRange) {
|
||||
for ($intBib = (int)$tabRange['start']; $intBib <= (int)$tabRange['finish']; $intBib++) {
|
||||
$objRow = fxBibProductionXmlCloneRow($objDataTemplate, $intRow);
|
||||
fxBibProductionXmlSetNumber($objDom, $objRow, 'A', $intRow, $intBib, $strDataStyleA);
|
||||
fxBibProductionXmlSetInlineString(
|
||||
$objDom,
|
||||
$objRow,
|
||||
'B',
|
||||
$intRow,
|
||||
$tabRace['label'],
|
||||
$strDataStyleB
|
||||
);
|
||||
$objSheetData->appendChild($objRow);
|
||||
$intRow++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$objDimension = $objXpath->query('//m:dimension')->item(0);
|
||||
if ($objDimension instanceof DOMElement) {
|
||||
$objDimension->setAttribute('ref', 'A1:B' . max(1, $intRow - 1));
|
||||
}
|
||||
return $objDom->saveXML();
|
||||
}
|
||||
|
||||
function fxBibProductionBuildOrderXml($strXml, array $tabSnapshot, array $tabLabels) {
|
||||
$objDom = fxBibProductionXmlLoad($strXml);
|
||||
$objXpath = new DOMXPath($objDom);
|
||||
$objXpath->registerNamespace('m', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
|
||||
|
||||
$objSheetData = $objXpath->query('//m:sheetData')->item(0);
|
||||
$objBaseEvent = fxBibProductionXmlFindRow($objXpath, 4);
|
||||
$objBaseSecond = fxBibProductionXmlFindRow($objXpath, 5);
|
||||
$objBaseBlank = fxBibProductionXmlFindRow($objXpath, 6);
|
||||
$objTotal = fxBibProductionXmlFindRow($objXpath, 31);
|
||||
if (!$objSheetData || !$objBaseEvent || !$objBaseSecond || !$objTotal) {
|
||||
throw new Exception('MSIN-4469 invalid order template');
|
||||
}
|
||||
if (!$objBaseBlank) {
|
||||
$objBaseBlank = $objBaseSecond;
|
||||
}
|
||||
|
||||
$tabOrderRows = [];
|
||||
foreach ($tabSnapshot['races'] as $tabRace) {
|
||||
foreach ($tabRace['ranges'] as $intRangeIndex => $tabRange) {
|
||||
$tabOrderRows[] = [
|
||||
// MSIN-4470 — Une ligne reste réservée à chaque séquence,
|
||||
// mais le nom de l'épreuve n'est répété que sur la première.
|
||||
'label' => $intRangeIndex === 0 ? $tabRace['label'] : '',
|
||||
'start' => (int)$tabRange['start'],
|
||||
'finish' => (int)$tabRange['finish'],
|
||||
'quantity' => (int)$tabRange['quantity'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// MSIN-4469 — Le gabarit a 9 emplacements ; on n'en garde que le nombre
|
||||
// réel de séquences (sinon lignes jaunes vides avant le TOTAL).
|
||||
$intTemplateSlots = 9;
|
||||
$intSlotCount = count($tabOrderRows);
|
||||
$intRowDelta = ($intSlotCount - $intTemplateSlots) * 3;
|
||||
$intTotalRow = 31 + $intRowDelta;
|
||||
|
||||
$tabRemove = [];
|
||||
foreach ($objXpath->query('//m:sheetData/m:row') as $objRow) {
|
||||
if (!($objRow instanceof DOMElement)) {
|
||||
continue;
|
||||
}
|
||||
$intCurrentRow = (int)$objRow->getAttribute('r');
|
||||
if ($intCurrentRow >= 4 && $intCurrentRow <= 30) {
|
||||
$tabRemove[] = $objRow;
|
||||
}
|
||||
}
|
||||
foreach ($tabRemove as $objRow) {
|
||||
$objSheetData->removeChild($objRow);
|
||||
}
|
||||
|
||||
if ($intRowDelta !== 0) {
|
||||
foreach ($objXpath->query('//m:sheetData/m:row') as $objRow) {
|
||||
if (!($objRow instanceof DOMElement)) {
|
||||
continue;
|
||||
}
|
||||
$intCurrentRow = (int)$objRow->getAttribute('r');
|
||||
if ($intCurrentRow < 31) {
|
||||
continue;
|
||||
}
|
||||
$intShiftedRow = $intCurrentRow + $intRowDelta;
|
||||
$objRow->setAttribute('r', (string)$intShiftedRow);
|
||||
foreach ($objRow->childNodes as $objCell) {
|
||||
if ($objCell instanceof DOMElement && $objCell->localName === 'c') {
|
||||
$strColumn = preg_replace('/[0-9]+/', '', $objCell->getAttribute('r'));
|
||||
$objCell->setAttribute('r', $strColumn . $intShiftedRow);
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($objXpath->query('//m:mergeCell') as $objMerge) {
|
||||
if (!($objMerge instanceof DOMElement)) {
|
||||
continue;
|
||||
}
|
||||
$objMerge->setAttribute(
|
||||
'ref',
|
||||
fxBibProductionXmlShiftReference($objMerge->getAttribute('ref'), 31, $intRowDelta)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$objTotal = fxBibProductionXmlFindRow($objXpath, $intTotalRow);
|
||||
if (!$objTotal) {
|
||||
throw new Exception('MSIN-4469 total row missing');
|
||||
}
|
||||
|
||||
for ($intSlot = 0; $intSlot < $intSlotCount; $intSlot++) {
|
||||
$intRow = 4 + ($intSlot * 3);
|
||||
$objEventRow = fxBibProductionXmlCloneRow($objBaseEvent, $intRow);
|
||||
$objSecondRow = fxBibProductionXmlCloneRow($objBaseSecond, $intRow + 1);
|
||||
$objBlankRow = fxBibProductionXmlCloneRow($objBaseBlank, $intRow + 2);
|
||||
|
||||
$tabRow = $tabOrderRows[$intSlot];
|
||||
$objCellF = fxBibProductionXmlFindCell($objEventRow, 'F');
|
||||
$strStyleK = $objCellF ? (string)$objCellF->getAttribute('s') : '';
|
||||
fxBibProductionXmlSetInlineString($objDom, $objEventRow, 'A', $intRow, $tabRow['label']);
|
||||
fxBibProductionXmlSetNumber($objDom, $objEventRow, 'F', $intRow, $tabRow['quantity']);
|
||||
fxBibProductionXmlSetNumber($objDom, $objEventRow, 'G', $intRow, $tabRow['start']);
|
||||
fxBibProductionXmlSetNumber($objDom, $objEventRow, 'H', $intRow, $tabRow['finish']);
|
||||
fxBibProductionXmlSetFormula($objDom, $objEventRow, 'I', $intRow, 'H' . $intRow . '-G' . $intRow . '+1');
|
||||
fxBibProductionXmlSetFormula($objDom, $objEventRow, 'J', $intRow, 'I' . $intRow . '=F' . $intRow);
|
||||
// MSIN-4469 — À côté de la quantité : lignes Imprimeur pour cette séquence.
|
||||
fxBibProductionXmlSetNumber($objDom, $objEventRow, 'K', $intRow, $tabRow['quantity'], $strStyleK);
|
||||
// MSIN-4469 — V1 : une puce par dossard, avec le même numéro.
|
||||
fxBibProductionXmlSetNumber($objDom, $objEventRow, 'L', $intRow, $tabRow['quantity']);
|
||||
fxBibProductionXmlSetNumber($objDom, $objEventRow, 'M', $intRow, $tabRow['start']);
|
||||
fxBibProductionXmlSetNumber($objDom, $objEventRow, 'N', $intRow, $tabRow['finish']);
|
||||
fxBibProductionXmlSetFormula($objDom, $objEventRow, 'O', $intRow, 'G' . $intRow . '=M' . $intRow);
|
||||
fxBibProductionXmlSetFormula($objDom, $objEventRow, 'P', $intRow, 'H' . $intRow . '=N' . $intRow);
|
||||
|
||||
$objSheetData->insertBefore($objEventRow, $objTotal);
|
||||
$objSheetData->insertBefore($objSecondRow, $objTotal);
|
||||
$objSheetData->insertBefore($objBlankRow, $objTotal);
|
||||
}
|
||||
|
||||
$objTitleRow = fxBibProductionXmlFindRow($objXpath, 1);
|
||||
if ($objTitleRow) {
|
||||
fxBibProductionXmlSetInlineString(
|
||||
$objDom,
|
||||
$objTitleRow,
|
||||
'A',
|
||||
1,
|
||||
$tabSnapshot['event']['name']
|
||||
);
|
||||
}
|
||||
|
||||
// MSIN-4469 — Colonne K : validation quantité vs lignes onglet Imprimeur (hors vides).
|
||||
$objHeaderRow = fxBibProductionXmlFindRow($objXpath, 3);
|
||||
if ($objHeaderRow) {
|
||||
$objHeaderCellF = fxBibProductionXmlFindCell($objHeaderRow, 'F');
|
||||
$strHeaderStyleK = $objHeaderCellF
|
||||
? (string)$objHeaderCellF->getAttribute('s')
|
||||
: '';
|
||||
fxBibProductionXmlSetInlineString(
|
||||
$objDom,
|
||||
$objHeaderRow,
|
||||
'K',
|
||||
3,
|
||||
isset($tabLabels['printer_lines']) ? $tabLabels['printer_lines'] : 'Lignes Imprimeur',
|
||||
$strHeaderStyleK
|
||||
);
|
||||
}
|
||||
|
||||
$intBibTotal = 0;
|
||||
foreach ($tabOrderRows as $tabRow) {
|
||||
$intBibTotal += (int)$tabRow['quantity'];
|
||||
}
|
||||
|
||||
// Reprendre la ligne TOTAL après les insertions (référence DOM à jour).
|
||||
$objTotal = fxBibProductionXmlFindRow($objXpath, $intTotalRow);
|
||||
if (!$objTotal) {
|
||||
throw new Exception('MSIN-4469 total row missing');
|
||||
}
|
||||
|
||||
fxBibProductionXmlSetInlineString($objDom, $objTotal, 'A', $intTotalRow, $tabLabels['total']);
|
||||
fxBibProductionXmlSetFormula(
|
||||
$objDom,
|
||||
$objTotal,
|
||||
'F',
|
||||
$intTotalRow,
|
||||
'SUM(F4:F' . ($intTotalRow - 1) . ')',
|
||||
'',
|
||||
$intBibTotal
|
||||
);
|
||||
fxBibProductionXmlSetFormula(
|
||||
$objDom,
|
||||
$objTotal,
|
||||
'I',
|
||||
$intTotalRow,
|
||||
'SUM(I4:I' . ($intTotalRow - 1) . ')',
|
||||
'',
|
||||
$intBibTotal
|
||||
);
|
||||
fxBibProductionXmlSetFormula(
|
||||
$objDom,
|
||||
$objTotal,
|
||||
'L',
|
||||
$intTotalRow,
|
||||
'SUM(L4:L' . ($intTotalRow - 1) . ')',
|
||||
'',
|
||||
$intBibTotal
|
||||
);
|
||||
// COUNT ignore l'en-tête texte et les lignes blanches entre épreuves.
|
||||
fxBibProductionXmlSetFormula(
|
||||
$objDom,
|
||||
$objTotal,
|
||||
'K',
|
||||
$intTotalRow,
|
||||
'COUNT(Imprimeur!A:A)',
|
||||
'',
|
||||
$intBibTotal
|
||||
);
|
||||
fxBibProductionXmlSetFormula(
|
||||
$objDom,
|
||||
$objTotal,
|
||||
'J',
|
||||
$intTotalRow,
|
||||
'F' . $intTotalRow . '=K' . $intTotalRow,
|
||||
'',
|
||||
'1'
|
||||
);
|
||||
|
||||
$objDimension = $objXpath->query('//m:dimension')->item(0);
|
||||
if ($objDimension instanceof DOMElement) {
|
||||
$objDimension->setAttribute('ref', 'A1:Q' . (48 + $intRowDelta));
|
||||
}
|
||||
return $objDom->saveXML();
|
||||
}
|
||||
|
||||
function fxBibProductionBuildWorkbook(array $tabSnapshot, $strDestination, array $tabLabels) {
|
||||
$strTemplate = fxBibProductionTemplatePath();
|
||||
if (!is_file($strTemplate)) {
|
||||
throw new Exception('MSIN-4469 template missing');
|
||||
}
|
||||
if (!class_exists('ZipArchive') || !class_exists('DOMDocument')) {
|
||||
throw new Exception('MSIN-4469 ZIP/XML extensions missing');
|
||||
}
|
||||
if (!copy($strTemplate, $strDestination)) {
|
||||
throw new Exception('MSIN-4469 template copy failed');
|
||||
}
|
||||
|
||||
$objZip = new ZipArchive();
|
||||
if ($objZip->open($strDestination) !== true) {
|
||||
throw new Exception('MSIN-4469 workbook open failed');
|
||||
}
|
||||
|
||||
try {
|
||||
$strSupplierXml = $objZip->getFromName('xl/worksheets/sheet1.xml');
|
||||
$strOrderXml = $objZip->getFromName('xl/worksheets/sheet2.xml');
|
||||
if ($strSupplierXml === false || $strOrderXml === false) {
|
||||
throw new Exception('MSIN-4469 worksheet missing');
|
||||
}
|
||||
|
||||
$strSupplierXml = fxBibProductionBuildSupplierXml($strSupplierXml, $tabSnapshot, $tabLabels);
|
||||
$strOrderXml = fxBibProductionBuildOrderXml($strOrderXml, $tabSnapshot, $tabLabels);
|
||||
|
||||
$objZip->deleteName('xl/worksheets/sheet1.xml');
|
||||
$objZip->addFromString('xl/worksheets/sheet1.xml', $strSupplierXml);
|
||||
$objZip->deleteName('xl/worksheets/sheet2.xml');
|
||||
$objZip->addFromString('xl/worksheets/sheet2.xml', $strOrderXml);
|
||||
|
||||
$strWorkbookXml = $objZip->getFromName('xl/workbook.xml');
|
||||
if ($strWorkbookXml !== false) {
|
||||
$objWorkbookDom = fxBibProductionXmlLoad($strWorkbookXml);
|
||||
$objWorkbookXpath = new DOMXPath($objWorkbookDom);
|
||||
$objWorkbookXpath->registerNamespace('m', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
|
||||
$objCalcPr = $objWorkbookXpath->query('//m:calcPr')->item(0);
|
||||
if ($objCalcPr instanceof DOMElement) {
|
||||
$objCalcPr->setAttribute('fullCalcOnLoad', '1');
|
||||
$objCalcPr->setAttribute('forceFullCalc', '1');
|
||||
}
|
||||
$objZip->deleteName('xl/workbook.xml');
|
||||
$objZip->addFromString('xl/workbook.xml', $objWorkbookDom->saveXML());
|
||||
}
|
||||
} catch (Exception $objException) {
|
||||
$objZip->close();
|
||||
@unlink($strDestination);
|
||||
throw $objException;
|
||||
}
|
||||
|
||||
$objZip->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée la commande officielle et verrouille immédiatement les séquences via
|
||||
* bpo_active_key. Une requête concurrente retrouve la commande déjà active.
|
||||
*/
|
||||
function fxBibProductionCreateOfficialOrder($intEveId, $strLangue = 'fr') {
|
||||
global $objDatabase;
|
||||
|
||||
$intEveId = (int)$intEveId;
|
||||
$tabExisting = fxBibProductionGetActiveOrder($intEveId);
|
||||
if ($tabExisting) {
|
||||
return ['success' => true, 'order' => $tabExisting, 'existing' => true];
|
||||
}
|
||||
|
||||
$tabCollected = fxBibProductionCollectSnapshot($intEveId, $strLangue);
|
||||
if (empty($tabCollected['success'])) {
|
||||
return $tabCollected;
|
||||
}
|
||||
$tabSnapshot = $tabCollected['snapshot'];
|
||||
$strSnapshot = json_encode($tabSnapshot, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if ($strSnapshot === false) {
|
||||
return ['success' => false, 'message' => fxBibMsg('bib_v5_production_generation_error')];
|
||||
}
|
||||
|
||||
$strCreatedBy = fxBibGetCurrentPromoteurDisplayName();
|
||||
$intComId = (int)($_SESSION['com_id'] ?? 0);
|
||||
$intUsaId = (int)($_SESSION['usa_id'] ?? 0);
|
||||
$strEventPart = fxBibProductionSafeFilePart($tabSnapshot['event']['name']);
|
||||
|
||||
$objDatabase->fxQuery('START TRANSACTION');
|
||||
$objDatabase->fxGetVar(
|
||||
"SELECT eve_id FROM inscriptions_evenements WHERE eve_id = $intEveId FOR UPDATE"
|
||||
);
|
||||
|
||||
$tabExisting = fxBibProductionGetActiveOrder($intEveId);
|
||||
if ($tabExisting) {
|
||||
$objDatabase->fxQuery('COMMIT');
|
||||
return ['success' => true, 'order' => $tabExisting, 'existing' => true];
|
||||
}
|
||||
|
||||
$intVersion = (int)$objDatabase->fxGetVar(
|
||||
"SELECT COALESCE(MAX(bpo_version), 0) + 1
|
||||
FROM inscriptions_bib_production_orders
|
||||
WHERE eve_id = $intEveId"
|
||||
);
|
||||
if ($intVersion <= 0) {
|
||||
$intVersion = 1;
|
||||
}
|
||||
|
||||
$strFileName = 'commande-dossards-' . $strEventPart . '-v' . $intVersion . '.xlsx';
|
||||
$strInsert = "
|
||||
INSERT INTO inscriptions_bib_production_orders (
|
||||
eve_id,
|
||||
bpo_com_id,
|
||||
bpo_usa_id,
|
||||
bpo_status,
|
||||
bpo_active_key,
|
||||
bpo_version,
|
||||
bpo_file_name,
|
||||
bpo_file_path,
|
||||
bpo_sha256,
|
||||
bpo_snapshot,
|
||||
bpo_created_at,
|
||||
bpo_created_by
|
||||
) VALUES (
|
||||
$intEveId,
|
||||
$intComId,
|
||||
$intUsaId,
|
||||
'generating',
|
||||
1,
|
||||
$intVersion,
|
||||
'" . $objDatabase->fxEscape($strFileName) . "',
|
||||
'',
|
||||
'',
|
||||
'" . $objDatabase->fxEscape($strSnapshot) . "',
|
||||
NOW(),
|
||||
'" . $objDatabase->fxEscape($strCreatedBy) . "'
|
||||
)
|
||||
";
|
||||
$qryInsert = $objDatabase->fxQuery($strInsert);
|
||||
$intOrderId = $qryInsert ? (int)$objDatabase->fxGetLastId() : 0;
|
||||
if ($intOrderId <= 0) {
|
||||
$objDatabase->fxQuery('ROLLBACK');
|
||||
$tabExisting = fxBibProductionGetActiveOrder($intEveId);
|
||||
if ($tabExisting) {
|
||||
return ['success' => true, 'order' => $tabExisting, 'existing' => true];
|
||||
}
|
||||
return ['success' => false, 'message' => fxBibMsg('bib_v5_production_generation_error')];
|
||||
}
|
||||
|
||||
foreach ($tabSnapshot['races'] as $tabRace) {
|
||||
foreach ($tabRace['ranges'] as $tabRange) {
|
||||
$qryRange = $objDatabase->fxQuery(
|
||||
"INSERT INTO inscriptions_bib_production_ranges (
|
||||
bpo_id,
|
||||
eve_id,
|
||||
epr_id,
|
||||
epr_bib_id,
|
||||
bpr_epr_label,
|
||||
bpr_start,
|
||||
bpr_finish,
|
||||
bpr_quantity
|
||||
) VALUES (
|
||||
$intOrderId,
|
||||
$intEveId,
|
||||
" . (int)$tabRace['epr_id'] . ",
|
||||
" . (int)$tabRange['epr_bib_id'] . ",
|
||||
'" . $objDatabase->fxEscape($tabRace['label']) . "',
|
||||
" . (int)$tabRange['start'] . ",
|
||||
" . (int)$tabRange['finish'] . ",
|
||||
" . (int)$tabRange['quantity'] . "
|
||||
)"
|
||||
);
|
||||
if ($qryRange === false) {
|
||||
$objDatabase->fxQuery('ROLLBACK');
|
||||
return ['success' => false, 'message' => fxBibMsg('bib_v5_production_generation_error')];
|
||||
}
|
||||
}
|
||||
}
|
||||
$objDatabase->fxQuery('COMMIT');
|
||||
|
||||
$strRelativeDir = 'eve-' . $intEveId . DIRECTORY_SEPARATOR . 'order-' . $intOrderId;
|
||||
$strAbsoluteDir = fxBibProductionStorageRoot() . DIRECTORY_SEPARATOR . $strRelativeDir;
|
||||
$strAbsoluteFile = $strAbsoluteDir . DIRECTORY_SEPARATOR . $strFileName;
|
||||
|
||||
try {
|
||||
if (!fxBibProductionEnsureDirectory($strAbsoluteDir)) {
|
||||
throw new Exception('MSIN-4469 storage unavailable');
|
||||
}
|
||||
|
||||
$strStorageRoot = fxBibProductionStorageRoot();
|
||||
if (fxBibProductionEnsureDirectory($strStorageRoot)) {
|
||||
$strDenyFile = $strStorageRoot . DIRECTORY_SEPARATOR . '.htaccess';
|
||||
if (!is_file($strDenyFile)) {
|
||||
@file_put_contents($strDenyFile, "Deny from all\n");
|
||||
}
|
||||
}
|
||||
|
||||
fxBibProductionBuildWorkbook(
|
||||
$tabSnapshot,
|
||||
$strAbsoluteFile,
|
||||
[
|
||||
'bib' => fxBibTexte('bib_v5_production_col_bib', 0),
|
||||
'race' => fxBibTexte('bib_v5_production_col_race', 0),
|
||||
'total' => fxBibTexte('bib_v5_production_total', 0),
|
||||
'title' => fxBibTexte('bib_v5_production_excel_title', 0),
|
||||
'printer_lines' => fxBibTexte('bib_v5_production_col_printer_lines', 0),
|
||||
]
|
||||
);
|
||||
|
||||
if (!is_file($strAbsoluteFile)) {
|
||||
throw new Exception('MSIN-4469 workbook not written');
|
||||
}
|
||||
|
||||
$strRelativeFile = str_replace('\\', '/', $strRelativeDir . DIRECTORY_SEPARATOR . $strFileName);
|
||||
$strSha256 = hash_file('sha256', $strAbsoluteFile);
|
||||
$objDatabase->fxQuery(
|
||||
"UPDATE inscriptions_bib_production_orders
|
||||
SET bpo_status = 'ready',
|
||||
bpo_file_path = '" . $objDatabase->fxEscape($strRelativeFile) . "',
|
||||
bpo_sha256 = '" . $objDatabase->fxEscape($strSha256) . "'
|
||||
WHERE bpo_id = $intOrderId
|
||||
AND bpo_active_key = 1"
|
||||
);
|
||||
} catch (Exception $objException) {
|
||||
if (is_file($strAbsoluteFile)) {
|
||||
@unlink($strAbsoluteFile);
|
||||
}
|
||||
$objDatabase->fxQuery(
|
||||
"UPDATE inscriptions_bib_production_orders
|
||||
SET bpo_status = 'failed',
|
||||
bpo_active_key = NULL,
|
||||
bpo_error = '" . $objDatabase->fxEscape($objException->getMessage()) . "'
|
||||
WHERE bpo_id = $intOrderId"
|
||||
);
|
||||
return ['success' => false, 'message' => fxBibMsg('bib_v5_production_generation_error')];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'order' => fxBibProductionGetOrder($intOrderId, $intEveId),
|
||||
'existing' => false,
|
||||
];
|
||||
}
|
||||
|
||||
function fxBibProductionOrderAbsolutePath(array $tabOrder) {
|
||||
$strRelative = str_replace('\\', '/', (string)($tabOrder['bpo_file_path'] ?? ''));
|
||||
if ($strRelative === '' || strpos($strRelative, '..') !== false || strpos($strRelative, "\0") !== false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return fxBibProductionStorageRoot() . DIRECTORY_SEPARATOR
|
||||
. str_replace('/', DIRECTORY_SEPARATOR, $strRelative);
|
||||
}
|
||||
|
||||
function fxBibProductionStreamOrder(array $tabOrder) {
|
||||
if (($tabOrder['bpo_status'] ?? '') !== 'ready') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$strFile = fxBibProductionOrderAbsolutePath($tabOrder);
|
||||
if ($strFile === '' || !is_file($strFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
while (ob_get_level() > 0) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
$strDownloadName = basename((string)$tabOrder['bpo_file_name']);
|
||||
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
header('Content-Disposition: attachment; filename="' . str_replace('"', '', $strDownloadName) . '"');
|
||||
header('Content-Length: ' . filesize($strFile));
|
||||
header('Cache-Control: private, no-store, max-age=0');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
readfile($strFile);
|
||||
return true;
|
||||
}
|
||||
302
php/inc_fx_dossard_print.php
Normal file
302
php/inc_fx_dossard_print.php
Normal file
@ -0,0 +1,302 @@
|
||||
<?php
|
||||
/**
|
||||
* MSIN-4485 — Impression dossard PDF (1 participant).
|
||||
*
|
||||
* Format FINAL : demi-feuille Lettre coupée sur le 11"
|
||||
* → papier 8.5" large × 5.5" haut (paysage)
|
||||
* → PDF page = exactement cette taille
|
||||
* → fond pleine page + n° / nom / QR alignés
|
||||
*
|
||||
* Impression : papier personnalisé 8.5 × 5.5, échelle 100 %, sans « ajuster à la page ».
|
||||
*/
|
||||
|
||||
define('MSIN_DOSSARD_PRINT_PERM', 'inscriptions_gestion.dossard_print');
|
||||
|
||||
/**
|
||||
* Taille page = feuille coupée (pouces → mm).
|
||||
* Lettre 8.5×11 coupée en 2 sur le 11" → 8.5 × 5.5.
|
||||
*
|
||||
* @return array{0:float,1:float} [largeur, hauteur]
|
||||
*/
|
||||
function fxDossardPrintPageSizeMm()
|
||||
{
|
||||
return array(8.5 * 25.4, 5.5 * 25.4);
|
||||
}
|
||||
|
||||
/** Dossier des fonds JPG/PNG (même ratio 8.5:5.5 recommandé). */
|
||||
function fxDossardPrintTemplatesDir()
|
||||
{
|
||||
return dirname(__DIR__) . '/assets/dossard_templates';
|
||||
}
|
||||
|
||||
/**
|
||||
* Fond : eve_{id}.jpg|png sinon default.jpg|png.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
function fxDossardPrintResolveTemplatePath($intEveId)
|
||||
{
|
||||
$strDir = fxDossardPrintTemplatesDir();
|
||||
$intEveId = intval($intEveId);
|
||||
$arrCandidates = array();
|
||||
if ($intEveId > 0) {
|
||||
$arrCandidates[] = $strDir . '/eve_' . $intEveId . '.jpg';
|
||||
$arrCandidates[] = $strDir . '/eve_' . $intEveId . '.jpeg';
|
||||
$arrCandidates[] = $strDir . '/eve_' . $intEveId . '.png';
|
||||
}
|
||||
$arrCandidates[] = $strDir . '/default.jpg';
|
||||
$arrCandidates[] = $strDir . '/default.jpeg';
|
||||
$arrCandidates[] = $strDir . '/default.png';
|
||||
|
||||
foreach ($arrCandidates as $strPath) {
|
||||
if (is_file($strPath)) {
|
||||
return $strPath;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type FPDF d'après le fichier réel.
|
||||
*
|
||||
* @return string JPG|PNG|GIF|''
|
||||
*/
|
||||
function fxDossardPrintImageType($strPath)
|
||||
{
|
||||
$arrInfo = @getimagesize($strPath);
|
||||
if (!is_array($arrInfo) || empty($arrInfo[2])) {
|
||||
return '';
|
||||
}
|
||||
if ((int)$arrInfo[2] === IMAGETYPE_JPEG) {
|
||||
return 'JPG';
|
||||
}
|
||||
if ((int)$arrInfo[2] === IMAGETYPE_PNG) {
|
||||
return 'PNG';
|
||||
}
|
||||
if ((int)$arrInfo[2] === IMAGETYPE_GIF) {
|
||||
return 'GIF';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/** Texte FPDF (core fonts = Windows-1252). */
|
||||
function fxDossardPrintPdfText($str)
|
||||
{
|
||||
$str = (string)$str;
|
||||
if (function_exists('iconv')) {
|
||||
$strConverted = @iconv('UTF-8', 'Windows-1252//TRANSLIT', $str);
|
||||
if ($strConverted !== false) {
|
||||
return $strConverted;
|
||||
}
|
||||
}
|
||||
if (function_exists('mb_convert_encoding')) {
|
||||
return (string)mb_convert_encoding($str, 'Windows-1252', 'UTF-8');
|
||||
}
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
/** Droit impression (hors kit total). */
|
||||
function fxDossardPrintCan($intComId, $intEveId)
|
||||
{
|
||||
if (!function_exists('fxEveAccesHasPermission')) {
|
||||
require_once __DIR__ . '/inc_fx_eve_acces.php';
|
||||
}
|
||||
|
||||
return fxEveAccesHasPermission(intval($intComId), intval($intEveId), MSIN_DOSSARD_PRINT_PERM);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|null
|
||||
*/
|
||||
function fxDossardPrintLoadParticipant($intParId, $intEveId)
|
||||
{
|
||||
global $objDatabase;
|
||||
|
||||
$intParId = intval($intParId);
|
||||
$intEveId = intval($intEveId);
|
||||
if ($intParId <= 0 || $intEveId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$sql = 'SELECT p.par_id, p.par_prenom, p.par_nom, p.no_bib, p.eve_id, p.epr_id, p.pec_id,
|
||||
e.pec_id_original, e.pec_actif, e.is_cancelled
|
||||
FROM resultats_participants p
|
||||
INNER JOIN resultats_epreuves_commandees e
|
||||
ON e.pec_id_original = p.pec_id AND e.epr_id = p.epr_id
|
||||
WHERE p.par_id = ' . $intParId . '
|
||||
AND p.eve_id = ' . $intEveId . '
|
||||
LIMIT 1';
|
||||
|
||||
$row = $objDatabase->fxGetRow($sql);
|
||||
|
||||
return ($row === null || $row === false) ? null : $row;
|
||||
}
|
||||
|
||||
function fxDossardPrintQrUrl($intEveId)
|
||||
{
|
||||
if (!function_exists('fxKcEveResultatsQrUrl')) {
|
||||
require_once __DIR__ . '/inc_fx_eve_extra.php';
|
||||
}
|
||||
|
||||
return fxKcEveResultatsQrUrl(intval($intEveId));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|false chemin PNG temp
|
||||
*/
|
||||
function fxDossardPrintBuildQrTempFile($intEveId)
|
||||
{
|
||||
require_once __DIR__ . '/../libs/phpqrcode-master/qrlib.php';
|
||||
|
||||
$strUrl = fxDossardPrintQrUrl($intEveId);
|
||||
if ($strUrl === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$strTmp = tempnam(sys_get_temp_dir(), 'ms1dossardqr_');
|
||||
if ($strTmp === false) {
|
||||
return false;
|
||||
}
|
||||
$strPng = $strTmp . '.png';
|
||||
@unlink($strTmp);
|
||||
|
||||
QRcode::png($strUrl, $strPng, QR_ECLEVEL_M, 6, 1);
|
||||
if (!is_file($strPng)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $strPng;
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout page 8.5" × 5.5" (mm) + overlays.
|
||||
* Coordonnées calibrées pour le template comic (case QR bas-droite).
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
function fxDossardPrintLayout()
|
||||
{
|
||||
$arrSize = fxDossardPrintPageSizeMm();
|
||||
$fltW = $arrSize[0]; // 215.9
|
||||
$fltH = $arrSize[1]; // 139.7
|
||||
|
||||
// Case QR du design : bas-droite (~1.1" dans le cadre)
|
||||
$fltQr = 28.0;
|
||||
|
||||
return array(
|
||||
'page_w' => $fltW,
|
||||
'page_h' => $fltH,
|
||||
// N° centré dans la zone blanche centrale
|
||||
'bib_y' => 52,
|
||||
'bib_font' => 72,
|
||||
'bib_h' => 28,
|
||||
// Nom sous le numéro
|
||||
'name_y' => 84,
|
||||
'name_font' => 14,
|
||||
// QR dans la case « SCANNEZ POUR RÉSULTATS »
|
||||
'qr_size' => $fltQr,
|
||||
'qr_x' => $fltW - $fltQr - 11.5,
|
||||
'qr_y' => $fltH - $fltQr - 14.5,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère et envoie le PDF (exit).
|
||||
*
|
||||
* @param array $arrRow
|
||||
*/
|
||||
function fxDossardPrintOutputPdf($arrRow)
|
||||
{
|
||||
$intEveId = intval($arrRow['eve_id'] ?? 0);
|
||||
$strTemplate = fxDossardPrintResolveTemplatePath($intEveId);
|
||||
if ($strTemplate === null) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo 'Template dossard introuvable (assets/dossard_templates/default.jpg).';
|
||||
exit;
|
||||
}
|
||||
|
||||
$strFpdf = dirname(__DIR__) . '/fpdf182/fpdf.php';
|
||||
if (!is_file($strFpdf)) {
|
||||
$strDocRoot = $_SERVER['DOCUMENT_ROOT'] ?? '';
|
||||
$strFpdf = rtrim((string)$strDocRoot, '/\\') . '/fpdf182/fpdf.php';
|
||||
}
|
||||
if (!is_file($strFpdf)) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo 'FPDF introuvable (fpdf182/fpdf.php).';
|
||||
exit;
|
||||
}
|
||||
require_once $strFpdf;
|
||||
|
||||
$arrLayout = fxDossardPrintLayout();
|
||||
$strBib = trim((string)($arrRow['no_bib'] ?? ''));
|
||||
if ($strBib === '' || $strBib === '0') {
|
||||
$strBib = '-';
|
||||
}
|
||||
$strPrenom = trim(function_exists('fxUnescape') ? fxUnescape($arrRow['par_prenom'] ?? '') : (string)($arrRow['par_prenom'] ?? ''));
|
||||
$strNom = trim(function_exists('fxUnescape') ? fxUnescape($arrRow['par_nom'] ?? '') : (string)($arrRow['par_nom'] ?? ''));
|
||||
$strName = trim($strPrenom . ' ' . $strNom);
|
||||
|
||||
$strQrFile = fxDossardPrintBuildQrTempFile($intEveId);
|
||||
$strTplType = fxDossardPrintImageType($strTemplate);
|
||||
|
||||
// Page = feuille coupée 8.5" × 5.5".
|
||||
// FPDF (_getpagesize) range si w>h ; 'L' remet w=8.5 h=5.5.
|
||||
$pdf = new FPDF('L', 'mm', array($arrLayout['page_w'], $arrLayout['page_h']));
|
||||
$pdf->SetAutoPageBreak(false);
|
||||
$pdf->SetMargins(0, 0, 0);
|
||||
$pdf->AddPage();
|
||||
|
||||
// Fond pleine page — ratio template ≈ 8.5:5.5 → pas de déformation
|
||||
$pdf->Image(
|
||||
$strTemplate,
|
||||
0,
|
||||
0,
|
||||
$arrLayout['page_w'],
|
||||
$arrLayout['page_h'],
|
||||
$strTplType
|
||||
);
|
||||
|
||||
// Numéro
|
||||
$pdf->SetTextColor(0, 0, 0);
|
||||
$pdf->SetFont('Helvetica', 'B', intval($arrLayout['bib_font']));
|
||||
$pdf->SetXY(20, $arrLayout['bib_y']);
|
||||
$pdf->Cell(
|
||||
$arrLayout['page_w'] - 40,
|
||||
$arrLayout['bib_h'],
|
||||
fxDossardPrintPdfText($strBib),
|
||||
0,
|
||||
0,
|
||||
'C'
|
||||
);
|
||||
|
||||
// Nom
|
||||
if ($strName !== '') {
|
||||
$pdf->SetFont('Helvetica', 'B', intval($arrLayout['name_font']));
|
||||
$pdf->SetXY(20, $arrLayout['name_y']);
|
||||
$pdf->Cell($arrLayout['page_w'] - 40, 8, fxDossardPrintPdfText($strName), 0, 0, 'C');
|
||||
}
|
||||
|
||||
// QR dans la case du template
|
||||
if ($strQrFile !== false) {
|
||||
$pdf->Image(
|
||||
$strQrFile,
|
||||
$arrLayout['qr_x'],
|
||||
$arrLayout['qr_y'],
|
||||
$arrLayout['qr_size'],
|
||||
$arrLayout['qr_size'],
|
||||
'PNG'
|
||||
);
|
||||
@unlink($strQrFile);
|
||||
}
|
||||
|
||||
$strSafeBib = preg_replace('/[^a-zA-Z0-9_-]+/', '-', $strBib);
|
||||
$strFilename = 'dossard-' . $strSafeBib . '-par' . intval($arrRow['par_id']) . '.pdf';
|
||||
|
||||
$pdf->Output('I', $strFilename);
|
||||
exit;
|
||||
}
|
||||
@ -97,7 +97,7 @@ function fxEveAccesGetRoles($blnActifOnly = true)
|
||||
return $objDatabase->fxGetResults($sql);
|
||||
}
|
||||
|
||||
/** Kits proposables par un promoteur (role_delegable = 1, hors migration_legacy). */
|
||||
/** Kits proposables par un promoteur (role_delegable = 1, hors migration_legacy / kit total). */
|
||||
function fxEveAccesGetDelegableRoles()
|
||||
{
|
||||
global $objDatabase;
|
||||
@ -116,11 +116,18 @@ function fxEveAccesGetDelegableRoles()
|
||||
return array();
|
||||
}
|
||||
|
||||
// MSIN-4478 — jamais offrir un kit total (rapports / argent / tout) en invitation promoteur.
|
||||
$strNoGrantsAll = '';
|
||||
if (fxEveAccesHasGrantsAllColumn()) {
|
||||
$strNoGrantsAll = ' AND role_grants_all = 0';
|
||||
}
|
||||
|
||||
$sql = "SELECT role_id, role_code, role_label_fr, role_label_en, role_description_fr
|
||||
FROM inscriptions_eve_roles
|
||||
WHERE role_actif = 1
|
||||
AND role_delegable = 1
|
||||
AND role_code <> 'migration_legacy'
|
||||
$strNoGrantsAll
|
||||
ORDER BY role_tri ASC, role_label_fr ASC";
|
||||
$tab = $objDatabase->fxGetResults($sql);
|
||||
|
||||
@ -142,8 +149,9 @@ function fxEveAccesRoleIsDelegable($intRoleId)
|
||||
return false;
|
||||
}
|
||||
|
||||
$strGrantsCol = fxEveAccesHasGrantsAllColumn() ? ', role_grants_all' : ', 0 AS role_grants_all';
|
||||
$row = $objDatabase->fxGetRow(
|
||||
"SELECT role_id, role_code, role_delegable, role_actif
|
||||
"SELECT role_id, role_code, role_delegable, role_actif $strGrantsCol
|
||||
FROM inscriptions_eve_roles WHERE role_id = $intRoleId LIMIT 1"
|
||||
);
|
||||
if ($row === null || intval($row['role_actif']) !== 1) {
|
||||
@ -152,6 +160,10 @@ function fxEveAccesRoleIsDelegable($intRoleId)
|
||||
if (($row['role_code'] ?? '') === 'migration_legacy') {
|
||||
return false;
|
||||
}
|
||||
// MSIN-4478 — kit total = non délégable (même si la case est cochée en BD).
|
||||
if (intval($row['role_grants_all'] ?? 0) === 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return intval($row['role_delegable'] ?? 0) === 1;
|
||||
}
|
||||
@ -398,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)
|
||||
{
|
||||
@ -472,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() . "
|
||||
@ -493,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();
|
||||
@ -571,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) {
|
||||
@ -1691,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);
|
||||
|
||||
@ -1743,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';
|
||||
@ -1815,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>
|
||||
|
||||
@ -71,6 +71,11 @@ function fxEveAccesPromoteurT($strClef, $strLangue = 'fr')
|
||||
'fr' => 'Compte créé et accès assigné. Un courriel avec le lien de mot de passe a été envoyé.',
|
||||
'en' => 'Account created and access granted. An email with the password link was sent.',
|
||||
),
|
||||
// MSIN-4478 — réinvite d'un stub jamais activé
|
||||
'eve_acces_promoteur_ok_pending' => array(
|
||||
'fr' => 'Invitation mise à jour. Un courriel avec le lien de mot de passe a été envoyé.',
|
||||
'en' => 'Invitation updated. An email with the password link was sent.',
|
||||
),
|
||||
'eve_acces_promoteur_ok_revoked' => array('fr' => 'Accès retiré.', 'en' => 'Access revoked.'),
|
||||
'eve_acces_promoteur_ok_resent' => array(
|
||||
'fr' => 'Courriel d\'invitation renvoyé.',
|
||||
@ -176,6 +181,145 @@ function fxEveAccesPromoteurHasInviteTeamColumn()
|
||||
return $blnHas;
|
||||
}
|
||||
|
||||
/**
|
||||
* MSIN-4478 — compte stub d'invitation jamais vraiment utilisé (pas de 1re visite).
|
||||
*/
|
||||
function fxEveAccesPromoteurCompteIsPendingInvite($arrCompte)
|
||||
{
|
||||
if (!is_array($arrCompte) || empty($arrCompte['com_id'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$strLastVisit = trim((string)($arrCompte['com_lastvisit'] ?? ''));
|
||||
$blnNeverVisited = ($strLastVisit === '' || strpos($strLastVisit, '0000-00-00') === 0);
|
||||
if (!$blnNeverVisited) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!fxEveAccesPromoteurHasInviteTeamColumn()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return intval($arrCompte['com_invite_team'] ?? 0) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* MSIN-4478 — nouveau jeton reset pour renvoyer le mail « créez votre mot de passe ».
|
||||
*
|
||||
* @return string token ('' si colonnes absentes)
|
||||
*/
|
||||
function fxEveAccesPromoteurRefreshResetToken($intComId)
|
||||
{
|
||||
global $objDatabase;
|
||||
|
||||
$intComId = intval($intComId);
|
||||
if ($intComId <= 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$strToken = uniqid('inv', true);
|
||||
$strNow = function_exists('fxGetDateTime') ? fxGetDateTime() : date('Y-m-d H:i:s');
|
||||
$arrSet = array();
|
||||
|
||||
$tabResetTok = $objDatabase->fxGetResults("SHOW COLUMNS FROM inscriptions_comptes LIKE 'com_reset_token'");
|
||||
if ($tabResetTok != null && count($tabResetTok) > 0) {
|
||||
$arrSet[] = "com_reset_token = '" . $objDatabase->fxEscape($strToken) . "'";
|
||||
}
|
||||
$tabResetDate = $objDatabase->fxGetResults("SHOW COLUMNS FROM inscriptions_comptes LIKE 'com_reset_date'");
|
||||
if ($tabResetDate != null && count($tabResetDate) > 0) {
|
||||
$arrSet[] = "com_reset_date = '" . $objDatabase->fxEscape($strNow) . "'";
|
||||
}
|
||||
|
||||
if (count($arrSet) === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$objDatabase->fxQuery(
|
||||
'UPDATE inscriptions_comptes SET ' . implode(', ', $arrSet)
|
||||
. ' WHERE com_id = ' . $intComId
|
||||
);
|
||||
|
||||
return $strToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* MSIN-4478 — un seul kit actif/invite par personne+événement : révoque les autres rôles.
|
||||
*/
|
||||
function fxEveAccesPromoteurRevokeOtherKits($intComId, $intEveId, $intKeepRoleId, $intActorComId)
|
||||
{
|
||||
global $objDatabase;
|
||||
|
||||
$intComId = intval($intComId);
|
||||
$intEveId = intval($intEveId);
|
||||
$intKeepRoleId = intval($intKeepRoleId);
|
||||
$intActorComId = intval($intActorComId);
|
||||
|
||||
if ($intComId <= 0 || $intEveId <= 0 || $intKeepRoleId <= 0 || !fxEveAccesIsEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tab = $objDatabase->fxGetResults(
|
||||
"SELECT ea_id FROM inscriptions_eve_acces
|
||||
WHERE com_id = $intComId
|
||||
AND eve_id = $intEveId
|
||||
AND role_id <> $intKeepRoleId
|
||||
AND ea_statut IN ('actif', 'invite')"
|
||||
);
|
||||
if ($tab == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($tab as $row) {
|
||||
fxEveAccesRevoke(intval($row['ea_id']), $intActorComId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MSIN-4478 — après revoke : si stub jamais activé et plus aucun accès, désactive le compte (libère le courriel).
|
||||
*/
|
||||
function fxEveAccesPromoteurDeactivateOrphanInviteStub($intComId)
|
||||
{
|
||||
global $objDatabase;
|
||||
|
||||
$intComId = intval($intComId);
|
||||
if ($intComId <= 0 || !fxEveAccesIsEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$arrCompte = $objDatabase->fxGetRow(
|
||||
'SELECT com_id, com_actif, com_invite_team, com_lastvisit
|
||||
FROM inscriptions_comptes WHERE com_id = ' . $intComId . ' LIMIT 1'
|
||||
);
|
||||
if (empty($arrCompte) || !fxEveAccesPromoteurCompteIsPendingInvite($arrCompte)) {
|
||||
return false;
|
||||
}
|
||||
if (intval($arrCompte['com_actif'] ?? 0) !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$intLeft = intval($objDatabase->fxGetVar(
|
||||
"SELECT COUNT(*) FROM inscriptions_eve_acces
|
||||
WHERE com_id = $intComId
|
||||
AND ea_statut IN ('actif', 'invite')"
|
||||
));
|
||||
if ($intLeft > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$arrSet = array('com_actif = 0');
|
||||
$tabResetTok = $objDatabase->fxGetResults("SHOW COLUMNS FROM inscriptions_comptes LIKE 'com_reset_token'");
|
||||
if ($tabResetTok != null && count($tabResetTok) > 0) {
|
||||
$arrSet[] = "com_reset_token = ''";
|
||||
}
|
||||
|
||||
$objDatabase->fxQuery(
|
||||
'UPDATE inscriptions_comptes SET ' . implode(', ', $arrSet)
|
||||
. ' WHERE com_id = ' . $intComId
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* MSIN-4401 — crée un compte MS1 minimal pour invitation promoteur.
|
||||
*
|
||||
@ -371,7 +515,7 @@ function fxEveAccesPromoteurSendInviteMail($arrCompte, $arrEve, $arrRole, $strLa
|
||||
}
|
||||
|
||||
/**
|
||||
* MSIN-4401 — invitation / assignation par promoteur.
|
||||
* MSIN-4401 / MSIN-4478 — invitation / assignation par promoteur.
|
||||
*
|
||||
* @return array{state:string,message:string,created?:bool}
|
||||
*/
|
||||
@ -417,8 +561,13 @@ function fxEveAccesPromoteurInvite($intEveId, $intGrantedBy, $arrPost, $strLangu
|
||||
return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_generic', $strLangue));
|
||||
}
|
||||
|
||||
$strCompteCols = 'com_id, com_prenom, com_nom, com_courriel, com_login, com_actif, com_lastvisit';
|
||||
if (fxEveAccesPromoteurHasInviteTeamColumn()) {
|
||||
$strCompteCols .= ', com_invite_team';
|
||||
}
|
||||
|
||||
$rowExisting = $objDatabase->fxGetRow(
|
||||
"SELECT com_id, com_prenom, com_nom, com_courriel, com_login, com_actif
|
||||
"SELECT $strCompteCols
|
||||
FROM inscriptions_comptes
|
||||
WHERE com_courriel = '" . $objDatabase->fxEscape($strCourriel) . "'
|
||||
ORDER BY com_actif DESC, com_id DESC
|
||||
@ -426,13 +575,24 @@ function fxEveAccesPromoteurInvite($intEveId, $intGrantedBy, $arrPost, $strLangu
|
||||
);
|
||||
|
||||
$blnCreated = false;
|
||||
$blnPending = false;
|
||||
$strResetToken = '';
|
||||
$arrCompte = null;
|
||||
|
||||
if (!empty($rowExisting) && is_array($rowExisting)) {
|
||||
$blnPending = fxEveAccesPromoteurCompteIsPendingInvite($rowExisting);
|
||||
|
||||
// MSIN-4478 — stub désactivé après revoke : on le réactive pour réinviter.
|
||||
if (intval($rowExisting['com_actif']) !== 1) {
|
||||
return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_inactive', $strLangue));
|
||||
if (!$blnPending) {
|
||||
return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_inactive', $strLangue));
|
||||
}
|
||||
$objDatabase->fxQuery(
|
||||
'UPDATE inscriptions_comptes SET com_actif = 1 WHERE com_id = ' . intval($rowExisting['com_id'])
|
||||
);
|
||||
$rowExisting['com_actif'] = 1;
|
||||
}
|
||||
|
||||
$arrCompte = $rowExisting;
|
||||
if ($strPrenom !== '' && trim((string)$arrCompte['com_prenom']) === '') {
|
||||
$objDatabase->fxQuery(
|
||||
@ -448,6 +608,10 @@ function fxEveAccesPromoteurInvite($intEveId, $intGrantedBy, $arrPost, $strLangu
|
||||
);
|
||||
$arrCompte['com_nom'] = $strNom;
|
||||
}
|
||||
|
||||
if ($blnPending) {
|
||||
$strResetToken = fxEveAccesPromoteurRefreshResetToken(intval($arrCompte['com_id']));
|
||||
}
|
||||
} else {
|
||||
if ($strPrenom === '' || $strNom === '') {
|
||||
return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_nom', $strLangue));
|
||||
@ -463,6 +627,7 @@ function fxEveAccesPromoteurInvite($intEveId, $intGrantedBy, $arrPost, $strLangu
|
||||
return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_generic', $strLangue));
|
||||
}
|
||||
$blnCreated = true;
|
||||
$blnPending = true;
|
||||
$strResetToken = (string)($arrCreate['reset_token'] ?? '');
|
||||
$arrCompte = array(
|
||||
'com_id' => intval($arrCreate['com_id']),
|
||||
@ -470,17 +635,29 @@ function fxEveAccesPromoteurInvite($intEveId, $intGrantedBy, $arrPost, $strLangu
|
||||
'com_nom' => $strNom,
|
||||
'com_courriel' => $strCourriel,
|
||||
'com_login' => $strCourriel,
|
||||
'com_invite_team' => 1,
|
||||
'com_lastvisit' => '',
|
||||
);
|
||||
}
|
||||
|
||||
// MSIN-4478 — changer de kit = remplacer, pas empiler.
|
||||
fxEveAccesPromoteurRevokeOtherKits(
|
||||
intval($arrCompte['com_id']),
|
||||
$intEveId,
|
||||
$intRoleId,
|
||||
$intGrantedBy
|
||||
);
|
||||
|
||||
// Stub jamais activé → statut invite + mail mot de passe (même au 2e envoi).
|
||||
$strGrantStatut = $blnPending ? 'invite' : 'actif';
|
||||
$arrGrant = fxEveAccesGrant(
|
||||
intval($arrCompte['com_id']),
|
||||
$intEveId,
|
||||
$intRoleId,
|
||||
$intJours,
|
||||
$intGrantedBy,
|
||||
'MSIN-4401 invitation promoteur',
|
||||
$blnCreated ? 'invite' : 'actif'
|
||||
'MSIN-4478 invitation promoteur',
|
||||
$strGrantStatut
|
||||
);
|
||||
|
||||
if (($arrGrant['state'] ?? '') !== 'success') {
|
||||
@ -492,22 +669,35 @@ function fxEveAccesPromoteurInvite($intEveId, $intGrantedBy, $arrPost, $strLangu
|
||||
|
||||
// L'accès est déjà enregistré : un échec mail ne doit pas faire planter la page.
|
||||
try {
|
||||
fxEveAccesPromoteurSendInviteMail($arrCompte, $arrEve, $arrRole, $strLangue, $blnCreated, $strResetToken);
|
||||
fxEveAccesPromoteurSendInviteMail(
|
||||
$arrCompte,
|
||||
$arrEve,
|
||||
$arrRole,
|
||||
$strLangue,
|
||||
$blnPending,
|
||||
$strResetToken
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
if ($blnCreated) {
|
||||
$strMsg = fxEveAccesPromoteurT('eve_acces_promoteur_ok_created', $strLangue);
|
||||
} elseif ($blnPending) {
|
||||
$strMsg = fxEveAccesPromoteurT('eve_acces_promoteur_ok_pending', $strLangue);
|
||||
} else {
|
||||
$strMsg = fxEveAccesPromoteurT('eve_acces_promoteur_ok_existing', $strLangue);
|
||||
}
|
||||
|
||||
return array(
|
||||
'state' => 'success',
|
||||
'created' => $blnCreated,
|
||||
'message' => $blnCreated
|
||||
? fxEveAccesPromoteurT('eve_acces_promoteur_ok_created', $strLangue)
|
||||
: fxEveAccesPromoteurT('eve_acces_promoteur_ok_existing', $strLangue),
|
||||
'message' => $strMsg,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* MSIN-4401 — révocation par le promoteur (team.manage ou auteur du grant / super admin).
|
||||
* MSIN-4401 / MSIN-4478 — révocation par le promoteur (team.manage ou auteur du grant / super admin).
|
||||
*/
|
||||
function fxEveAccesPromoteurRevoke($intEaId, $intActorComId, $intEveId)
|
||||
{
|
||||
@ -531,7 +721,12 @@ function fxEveAccesPromoteurRevoke($intEaId, $intActorComId, $intEveId)
|
||||
return array('state' => 'error');
|
||||
}
|
||||
|
||||
return fxEveAccesRevoke($intEaId, $intActorComId);
|
||||
$arrResult = fxEveAccesRevoke($intEaId, $intActorComId);
|
||||
if (($arrResult['state'] ?? '') === 'success') {
|
||||
fxEveAccesPromoteurDeactivateOrphanInviteStub(intval($tab['com_id'] ?? 0));
|
||||
}
|
||||
|
||||
return $arrResult;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -592,27 +787,12 @@ function fxEveAccesPromoteurResendMail($intEaId, $intActorComId, $intEveId, $str
|
||||
$strLastVisit = trim((string)($arrCompte['com_lastvisit'] ?? ''));
|
||||
$blnInviteTeam = intval($arrCompte['com_invite_team'] ?? 0) === 1;
|
||||
$blnPending = ($strStatut === 'invite')
|
||||
|| fxEveAccesPromoteurCompteIsPendingInvite($arrCompte)
|
||||
|| ($blnInviteTeam && ($strLastVisit === '' || strpos($strLastVisit, '0000-00-00') === 0));
|
||||
|
||||
$strResetToken = '';
|
||||
if ($blnPending) {
|
||||
$strResetToken = uniqid('inv', true);
|
||||
$strNow = function_exists('fxGetDateTime') ? fxGetDateTime() : date('Y-m-d H:i:s');
|
||||
$arrSet = array();
|
||||
$tabResetTok = $objDatabase->fxGetResults("SHOW COLUMNS FROM inscriptions_comptes LIKE 'com_reset_token'");
|
||||
if ($tabResetTok != null && count($tabResetTok) > 0) {
|
||||
$arrSet[] = "com_reset_token = '" . $objDatabase->fxEscape($strResetToken) . "'";
|
||||
}
|
||||
$tabResetDate = $objDatabase->fxGetResults("SHOW COLUMNS FROM inscriptions_comptes LIKE 'com_reset_date'");
|
||||
if ($tabResetDate != null && count($tabResetDate) > 0) {
|
||||
$arrSet[] = "com_reset_date = '" . $objDatabase->fxEscape($strNow) . "'";
|
||||
}
|
||||
if (count($arrSet) > 0) {
|
||||
$objDatabase->fxQuery(
|
||||
'UPDATE inscriptions_comptes SET ' . implode(', ', $arrSet)
|
||||
. ' WHERE com_id = ' . $intComId
|
||||
);
|
||||
}
|
||||
$strResetToken = fxEveAccesPromoteurRefreshResetToken($intComId);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
149
php/inc_fx_eve_extra.php
Normal file
149
php/inc_fx_eve_extra.php
Normal file
@ -0,0 +1,149 @@
|
||||
<?php
|
||||
/**
|
||||
* MSIN-3487 — Métadonnées supplémentaires d'événement
|
||||
* (table inscriptions_evenements_extra).
|
||||
*
|
||||
* Fonctions indépendantes : utilisables depuis kc.php, impression dossards,
|
||||
* hub promoteur, etc. Ne pas mélanger avec key_chain (QR physiques).
|
||||
*/
|
||||
|
||||
/**
|
||||
* URL de repli si aucun lien résultats n'est configuré pour l'événement.
|
||||
* Aligné sur le défaut historique de kc.php / key_chain.
|
||||
*/
|
||||
function fxEveExtraResultatsFallbackUrl()
|
||||
{
|
||||
return 'https://ms1.ca';
|
||||
}
|
||||
|
||||
/**
|
||||
* Lit la ligne extra d'un événement (ou null).
|
||||
*
|
||||
* @param int $intEveId
|
||||
* @return array|null
|
||||
*/
|
||||
function fxEveExtraGetRow($intEveId)
|
||||
{
|
||||
global $objDatabase;
|
||||
|
||||
$intEveId = intval($intEveId);
|
||||
if ($intEveId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$sql = 'SELECT * FROM inscriptions_evenements_extra WHERE eve_id = ' . $intEveId . ' LIMIT 1';
|
||||
$tabRow = $objDatabase->fxGetRow($sql);
|
||||
|
||||
return ($tabRow === null || $tabRow === false) ? null : $tabRow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le lien résultats brut, ou null s'il est absent / vide.
|
||||
*
|
||||
* @param int $intEveId
|
||||
* @return string|null
|
||||
*/
|
||||
function fxEveExtraGetResultatsUrl($intEveId)
|
||||
{
|
||||
$tabRow = fxEveExtraGetRow($intEveId);
|
||||
if ($tabRow === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$strUrl = trim((string) ($tabRow['eve_resultats_url'] ?? ''));
|
||||
|
||||
return ($strUrl !== '') ? $strUrl : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lien résultats à utiliser pour une redirection :
|
||||
* URL configurée, sinon fallback (ms1.ca).
|
||||
*
|
||||
* @param int $intEveId
|
||||
* @return string
|
||||
*/
|
||||
function fxEveExtraResolveResultatsUrl($intEveId)
|
||||
{
|
||||
$strUrl = fxEveExtraGetResultatsUrl($intEveId);
|
||||
|
||||
return ($strUrl !== null) ? $strUrl : fxEveExtraResultatsFallbackUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée ou met à jour le lien résultats pour un événement.
|
||||
*
|
||||
* @param int $intEveId
|
||||
* @param string $strUrl Chaîne vide = effacer le lien (NULL en BD)
|
||||
* @return bool
|
||||
*/
|
||||
function fxEveExtraSetResultatsUrl($intEveId, $strUrl)
|
||||
{
|
||||
global $objDatabase;
|
||||
|
||||
$intEveId = intval($intEveId);
|
||||
if ($intEveId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$strUrl = trim((string) $strUrl);
|
||||
if ($strUrl === '') {
|
||||
$strSqlUrl = 'NULL';
|
||||
} else {
|
||||
$strSqlUrl = "'" . $objDatabase->fxEscape($strUrl) . "'";
|
||||
}
|
||||
|
||||
// MSIN-3487 — upsert : une ligne max par eve_id
|
||||
$sql = 'INSERT INTO inscriptions_evenements_extra (eve_id, eve_resultats_url, eve_extra_maj)'
|
||||
. ' VALUES (' . $intEveId . ', ' . $strSqlUrl . ', NOW())'
|
||||
. ' ON DUPLICATE KEY UPDATE'
|
||||
. ' eve_resultats_url = VALUES(eve_resultats_url),'
|
||||
. ' eve_extra_maj = NOW()';
|
||||
|
||||
return (bool) $objDatabase->fxQuery($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* URL courte à encoder dans un QR (kc.php redirige vers les résultats).
|
||||
* Exemple : https://ms1inscription.com/kc.php?t=eve&id=760
|
||||
*
|
||||
* @param int $intEveId
|
||||
* @param string|null $strBaseUrl Défaut = $vDomaine
|
||||
* @return string
|
||||
*/
|
||||
function fxKcEveResultatsQrUrl($intEveId, $strBaseUrl = null)
|
||||
{
|
||||
global $vDomaine;
|
||||
|
||||
$intEveId = intval($intEveId);
|
||||
if ($strBaseUrl === null || trim((string) $strBaseUrl) === '') {
|
||||
$strBaseUrl = !empty($vDomaine) ? $vDomaine : 'https://ms1inscription.com';
|
||||
}
|
||||
|
||||
return rtrim($strBaseUrl, '/') . '/kc.php?t=eve&id=' . $intEveId;
|
||||
}
|
||||
|
||||
/**
|
||||
* PNG binaire du QR résultats événement (avec logo MS1).
|
||||
* MSIN-3487
|
||||
*
|
||||
* @param int $intEveId
|
||||
* @param string|null $strBaseUrl
|
||||
* @param int $intModuleSize
|
||||
* @param bool $blnWithLogo
|
||||
* @return string|false
|
||||
*/
|
||||
function fxKcEveResultatsQrPngBinary($intEveId, $strBaseUrl = null, $intModuleSize = 10, $blnWithLogo = true)
|
||||
{
|
||||
require_once __DIR__ . '/inc_fx_kc_qr.php';
|
||||
|
||||
$intEveId = intval($intEveId);
|
||||
if ($intEveId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return fxKcQrGeneratePngBinary(
|
||||
fxKcEveResultatsQrUrl($intEveId, $strBaseUrl),
|
||||
$intModuleSize,
|
||||
$blnWithLogo
|
||||
);
|
||||
}
|
||||
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;
|
||||
}
|
||||
@ -502,8 +493,9 @@ function fxInscrGestionFallbackTexte($strClef, $strLangue) {
|
||||
'inscr_gestion_err_refund' => $blnFr ? 'Erreur remboursement.' : 'Refund error.',
|
||||
'inscr_gestion_refund_ok' => $blnFr ? 'Remboursement effectué.' : 'Refund completed.',
|
||||
'inscr_gestion_refund_cancel_hint' => $blnFr
|
||||
? 'Votre transaction a été remboursée. Si vous voulez aussi l\'annuler dans le système d\'inscription :'
|
||||
: 'Your transaction has been refunded. If you also want to cancel it in the registration system:',
|
||||
// MSIN-4419 — hint post-remboursement + bouton annuler.
|
||||
? 'Votre transaction a été remboursée. Si vous voulez aussi annuler le participant cliquez sur annuler'
|
||||
: 'Your transaction has been refunded. If you also want to cancel the participant, click cancel',
|
||||
'inscr_gestion_col_participant' => $blnFr ? 'Participant' : 'Participant',
|
||||
'inscr_gestion_col_commande' => $blnFr ? 'Commande' : 'Order',
|
||||
'inscr_gestion_col_dossard' => $blnFr ? 'Dossard' : 'Bib',
|
||||
@ -534,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',
|
||||
@ -1253,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';
|
||||
|
||||
@ -1289,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,'
|
||||
@ -1297,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 . ", '')) <> ''"
|
||||
@ -2461,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'])
|
||||
|
||||
@ -123,11 +123,17 @@ function fxKcQrOverlayLogo($strQrPath, $fltLogoRatio = 0.22)
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un QR key_chain prêt pour l'impression.
|
||||
* Génère un PNG QR pour une URL quelconque (niveau H, logo optionnel).
|
||||
* MSIN-3487 — réutilisé key_chain + QR résultats événement.
|
||||
*
|
||||
* @param string $strUrl
|
||||
* @param string $strOutputFile
|
||||
* @param int $intModuleSize
|
||||
* @param bool $blnWithLogo
|
||||
* @return string Chemin du fichier généré
|
||||
*/
|
||||
function fxKcQrGeneratePng($intParam1, $strBaseUrl, $strOutputFile, $intModuleSize = 18, $blnWithLogo = true)
|
||||
function fxKcQrGeneratePngFromUrl($strUrl, $strOutputFile, $intModuleSize = 18, $blnWithLogo = true)
|
||||
{
|
||||
$strUrl = rtrim($strBaseUrl, '/') . '/kc.php?param1=' . intval($intParam1);
|
||||
$strDir = dirname($strOutputFile);
|
||||
if (!is_dir($strDir)) {
|
||||
mkdir($strDir, 0755, true);
|
||||
@ -137,7 +143,7 @@ function fxKcQrGeneratePng($intParam1, $strBaseUrl, $strOutputFile, $intModuleSi
|
||||
$strUrl,
|
||||
$strOutputFile,
|
||||
QR_ECLEVEL_H,
|
||||
$intModuleSize,
|
||||
max(4, (int) $intModuleSize),
|
||||
2
|
||||
);
|
||||
|
||||
@ -145,6 +151,45 @@ function fxKcQrGeneratePng($intParam1, $strBaseUrl, $strOutputFile, $intModuleSi
|
||||
fxKcQrOverlayLogo($strOutputFile);
|
||||
}
|
||||
|
||||
return $strOutputFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un PNG QR en mémoire (binaire) pour une URL.
|
||||
*
|
||||
* @param string $strUrl
|
||||
* @param int $intModuleSize
|
||||
* @param bool $blnWithLogo
|
||||
* @return string|false
|
||||
*/
|
||||
function fxKcQrGeneratePngBinary($strUrl, $intModuleSize = 10, $blnWithLogo = true)
|
||||
{
|
||||
$strTmpBase = tempnam(sys_get_temp_dir(), 'kcqr_');
|
||||
if ($strTmpBase === false) {
|
||||
return false;
|
||||
}
|
||||
@unlink($strTmpBase);
|
||||
$strTmpPng = $strTmpBase . '.png';
|
||||
|
||||
fxKcQrGeneratePngFromUrl($strUrl, $strTmpPng, $intModuleSize, $blnWithLogo);
|
||||
if (!is_file($strTmpPng)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$binPng = file_get_contents($strTmpPng);
|
||||
@unlink($strTmpPng);
|
||||
|
||||
return ($binPng === false || $binPng === '') ? false : $binPng;
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un QR key_chain prêt pour l'impression.
|
||||
*/
|
||||
function fxKcQrGeneratePng($intParam1, $strBaseUrl, $strOutputFile, $intModuleSize = 18, $blnWithLogo = true)
|
||||
{
|
||||
$strUrl = rtrim($strBaseUrl, '/') . '/kc.php?param1=' . intval($intParam1);
|
||||
fxKcQrGeneratePngFromUrl($strUrl, $strOutputFile, $intModuleSize, $blnWithLogo);
|
||||
|
||||
return [
|
||||
'param1' => intval($intParam1),
|
||||
'url' => $strUrl,
|
||||
|
||||
@ -927,8 +927,11 @@ LEFT JOIN inscriptions_panier_epreuves ee on e.epr_id=ee.epr_id
|
||||
$intAge = fxGetAge($strNaissance, $tabEpreuve['epr_date_age']);
|
||||
|
||||
// if ($strPrenom != '' && $strNomFamille != '' && $strSexe != '' && $strNaissance != '') {
|
||||
// MSIN-4474 — ne pas écraser l'identité si prénom/nom absents du POST (champs disabled)
|
||||
if ((isset($_SESSION['no_panier']) && $tabData['promoteur'] == 0) || (!isset($_SESSION['no_panier']) && $tabData['promoteur'] == 1 && $tabEvenement['general']['te_id'] != 13) || (!isset($_SESSION['no_panier']) && $tabData['promoteur'] == 0 && $i > 1)) {
|
||||
$strFields = ", par_prenom = '" . $objDatabase->fxEscape($strPrenom) . "', par_nom = '" . $objDatabase->fxEscape($strNomFamille) . "', par_naissance = '" . $objDatabase->fxEscape($strNaissance) . "', par_sexe = '" . $objDatabase->fxEscape($strSexe) . "'";
|
||||
if (isset($tabData['par_prenom_' . $i]) && isset($tabData['par_nom_' . $i])) {
|
||||
$strFields = ", par_prenom = '" . $objDatabase->fxEscape($strPrenom) . "', par_nom = '" . $objDatabase->fxEscape($strNomFamille) . "', par_naissance = '" . $objDatabase->fxEscape($strNaissance) . "', par_sexe = '" . $objDatabase->fxEscape($strSexe) . "'";
|
||||
}
|
||||
}
|
||||
|
||||
$sqlUpdatePart = "UPDATE " . $strTableSqlPart . " SET " . $strModMarker . "epr_id = " . intval($intEprId) . ", par_age = " . intval($intAge) . ", pro_id = " . intval($intProvince) . ", par_codepostal = '" . $objDatabase->fxEscape($strCodepostal) . "', pay_id = " . intval($intPays) . ", par_adresse = '" . $objDatabase->fxEscape($strAdresse) . "', par_adresse2 = '" . $objDatabase->fxEscape($strAdresse2) . "', par_ville = '" . $objDatabase->fxEscape($strVille) . "', par_maj = '" . fxGetDateTime() . "', par_telephone1 = '" . $objDatabase->fxEscape($strTelephone1) . "', par_telephone2 = '" . $objDatabase->fxEscape($strTelephone2) . "', par_nom_equipe = '" . $objDatabase->fxEscape($strParNomEquipe) . "', par_courriel = '" . $objDatabase->fxEscape($strCourriel) . "', par_equipe = " . intval($tabEpreuve['epr_equipe']) . $strCancel . $strFields . ", par_contact_urgence_nom = '" . $objDatabase->fxEscape($strUrgenceNom) . "', par_contact_urgence_telephone = '" . $objDatabase->fxEscape($strUrgenceTel) . "' WHERE " . $strWhereSql . " = " . intval($intParId);
|
||||
@ -1270,9 +1273,14 @@ LEFT JOIN inscriptions_panier_epreuves ee on e.epr_id=ee.epr_id
|
||||
else
|
||||
$strPage = 'account';
|
||||
|
||||
if ($_POST['promoteur'] == 1)
|
||||
header("Location: " . $vDomaine . '/' . $strPage . '/inc_tableau_promoteur?promoteur_eve_id=' . urlencode(base64_encode($tabEvenement['general']['eve_id'])));
|
||||
else
|
||||
if ($_POST['promoteur'] == 1) {
|
||||
// MSIN-4474 — retour à la page d'origine (gestion / promoteur), sinon tableau promoteur legacy
|
||||
$strReturnAfterMod = fxModInscriptionReturnUrl('');
|
||||
if ($strReturnAfterMod === '') {
|
||||
$strReturnAfterMod = $vDomaine . '/' . $strPage . '/inc_tableau_promoteur?promoteur_eve_id=' . urlencode(base64_encode($tabEvenement['general']['eve_id']));
|
||||
}
|
||||
header("Location: " . $strReturnAfterMod);
|
||||
} else
|
||||
header("Location: " . $vDomaine . '/' . $strPage);
|
||||
} else {
|
||||
header("Location: " . $tabData['url']);
|
||||
|
||||
@ -181,7 +181,8 @@ function fxShowInfosParticipants(&$tabRules, &$tabMessages, &$tabCalendriers, $t
|
||||
$strFieldDisabled = 'disabled_';
|
||||
}
|
||||
} elseif ($strAction == 'mod') {
|
||||
if ((!isset($_SESSION ['no_panier']) && !fxIsPromoteur($_SESSION['com_info']['com_id'], $tabEpreuve['eve_id'])) || intval($tabEpreuve['tep_id']) == 4) {
|
||||
// MSIN-4474 — promoteur=1 (URL) : garder prénom/nom éditables (évite POST vide → nom effacé)
|
||||
if ((!isset($_SESSION ['no_panier']) && intval($intPromoteur) != 1 && !fxIsPromoteur($_SESSION['com_info']['com_id'], $tabEpreuve['eve_id'])) || intval($tabEpreuve['tep_id']) == 4) {
|
||||
$strDisabled = "disabled";
|
||||
$strFieldDisabled = 'disabled_';
|
||||
}
|
||||
@ -284,7 +285,9 @@ function fxShowInfosParticipants(&$tabRules, &$tabMessages, &$tabCalendriers, $t
|
||||
<?php
|
||||
} else {
|
||||
if ($tabInfos[$intCtrInfos]['cha_nom'] == 'par_nom' || $tabInfos[$intCtrInfos]['cha_nom'] == 'par_prenom') {
|
||||
if ($strAction == 'mod' && !isset($_SESSION ['no_panier']) && $intPromoteur == 0 && $intCtr == 1) {
|
||||
// MSIN-4474 — hidden de secours si le champ visible est disabled (client ou promoteur)
|
||||
if ($strAction == 'mod' && !isset($_SESSION ['no_panier']) && $intCtr == 1
|
||||
&& (intval($intPromoteur) == 0 || trim($strDisabled) != '')) {
|
||||
?>
|
||||
<input type="hidden" name="<?php echo $strField; ?>"
|
||||
value="<?php echo (isset($tabParticipant[$intCtr][$tabInfos[$intCtrInfos]['cha_nom']])) ? ($tabParticipant[$intCtr][$tabInfos[$intCtrInfos]['cha_nom']]) : (""); ?>">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
@ -812,8 +812,9 @@ function fxPromoteurHubRenderLinkList($arrLinks, $strDefaultIcon)
|
||||
$strIcon = !empty($arrLink['icon']) ? $arrLink['icon'] : $strDefaultIcon;
|
||||
$strTarget = !empty($arrLink['target']) ? ' target="' . fxPromoteurHubEsc($arrLink['target']) . '"' : '';
|
||||
|
||||
// MSIN-4473 — coins alignés sur les tuiles outils dossards (border-radius 8px)
|
||||
$html .= '<li class="mb-2">';
|
||||
$html .= '<a class="btn btn-outline-primary btn-block text-left rounded-0" href="' . fxPromoteurHubEsc($arrLink['url']) . '"' . $strTarget . '>';
|
||||
$html .= '<a class="btn btn-outline-primary btn-block text-left promoteur-hub-link" href="' . fxPromoteurHubEsc($arrLink['url']) . '"' . $strTarget . '>';
|
||||
$html .= '<i class="fa ' . fxPromoteurHubEsc($strIcon) . ' mr-2" aria-hidden="true"></i>' . fxPromoteurHubEsc($arrLink['label']);
|
||||
$html .= '</a></li>';
|
||||
}
|
||||
@ -1586,7 +1587,8 @@ function fxPromoteurHubCanViewDashboard($intComId, $intEveId)
|
||||
require_once __DIR__ . '/inc_fonctions.php';
|
||||
}
|
||||
|
||||
if (fxEveAccesComEventHasGrantsAllKit($intComId, $intEveId)) {
|
||||
// MSIN-4484 — kit total respecté via HasPermission (perm_grants_all).
|
||||
if (fxEveAccesHasPermission($intComId, $intEveId, 'hub.dashboard')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -1595,7 +1597,7 @@ function fxPromoteurHubCanViewDashboard($intComId, $intEveId)
|
||||
return true;
|
||||
}
|
||||
|
||||
return fxEveAccesHasPermission($intComId, $intEveId, 'hub.dashboard');
|
||||
return false;
|
||||
}
|
||||
|
||||
function fxPromoteurHubRenderEventCard($arrItem, $strLangue, $blnArchived = false)
|
||||
|
||||
@ -433,13 +433,16 @@ function fxShowFormParticipant($tabEvenement, $strAction, $strLangue, $intEpreuv
|
||||
</button>
|
||||
<?php
|
||||
} elseif ($strAction == 'mod') {
|
||||
// MSIN-4474 — conserver la page d'origine (liste promoteur / gestion inscriptions)
|
||||
$strReturnUrl = fxModInscriptionReturnUrl(fxGetReferer());
|
||||
?>
|
||||
<input type="hidden" id="promoteur_eve_id" name="promoteur_eve_id" value="<?php echo $tabEvenement['general']['eve_id']; ?>">
|
||||
<input type="hidden" id="return_url" name="return_url" value="<?php echo htmlspecialchars($strReturnUrl, ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<button class="btn btn-primary rounded-0" id="btn_enregistrer_modif" name="btn_enregistrer_modif" type="submit">
|
||||
<i class="fa fa-check mr-2" aria-hidden="true"></i>
|
||||
<?php echo afficheTexte('btn_enregistrer', 0, 1, 1) ?>
|
||||
</button>
|
||||
<button class="btn btn-secondary rounded-0 btn_page_prec" data-code="<?php echo fxGetReferer(); ?>" type="button">
|
||||
<button class="btn btn-secondary rounded-0 btn_page_prec" data-code="<?php echo htmlspecialchars($strReturnUrl, ENT_QUOTES, 'UTF-8'); ?>" type="button">
|
||||
<i class="fa fa-cart-plus mr-2" aria-hidden="true"></i>
|
||||
<?php afficheTexte('btn_annulerinscriptions'); ?>
|
||||
</button>
|
||||
@ -946,11 +949,32 @@ function fxCheckAccesEpreuve($strAction, $intEpreuve, $intEvenement, $intpec_id,
|
||||
$blnReturn = true;
|
||||
}
|
||||
} elseif (intval($intPromoteur) == 1 && isset($_SESSION['com_info']) && intval($intpec_id) != 0) { // si modif par promoteur
|
||||
$arrEvenements = explode(',', $_SESSION['com_info']['com_eve_promoteur']);
|
||||
// MSIN-4474 — legacy CSV + super-admin + accès v2 (sinon Suivant changement d'épreuve absent)
|
||||
$strCsvPromoteur = isset($_SESSION['com_info']['com_eve_promoteur'])
|
||||
? (string)$_SESSION['com_info']['com_eve_promoteur']
|
||||
: '';
|
||||
$arrEvenements = array();
|
||||
foreach (array_map('trim', explode(',', $strCsvPromoteur)) as $strEveId) {
|
||||
if ($strEveId !== '') {
|
||||
$arrEvenements[] = intval($strEveId);
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array($intEvenement, $arrEvenements)) {
|
||||
if (in_array(intval($intEvenement), $arrEvenements, true)) {
|
||||
$blnReturn = true;
|
||||
}
|
||||
|
||||
if (!$blnReturn) {
|
||||
if (!function_exists('fxEveAccesIsSuperAdminSession')) {
|
||||
require_once dirname(__FILE__) . '/inc_fx_eve_acces.php';
|
||||
}
|
||||
if (fxEveAccesIsSuperAdminSession()) {
|
||||
$blnReturn = true;
|
||||
} elseif (!empty($_SESSION['com_info']['com_id'])
|
||||
&& fxEveAccesHasInscrGestionWebAccess(intval($_SESSION['com_info']['com_id']), intval($intEvenement))) {
|
||||
$blnReturn = true;
|
||||
}
|
||||
}
|
||||
} elseif (intval($intPromoteur) == 0 && isset($_SESSION['com_info']) && !isset($_SESSION['no_panier']) && intval($intpec_id) != 0) { // si modif d'une épreuve déjà commandée
|
||||
$sqlEpreuve = "SELECT COUNT(*) FROM resultats_epreuves_commandees WHERE pec_id_original = " . intval($intpec_id) . " AND com_id = " . intval($_SESSION['com_info']['com_id']) . $strWhere;
|
||||
$nbEpreuve = $objDatabase->fxGetVar($sqlEpreuve);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -43,18 +43,24 @@ function fxStaticSyncTableDefinitions() {
|
||||
'label' => 'Accès v2 — permissions',
|
||||
'keys' => array('perm_key'),
|
||||
'ignore_cols' => array(),
|
||||
// CON-333 — catalogue : les en trop peuvent être retirés pour coller à la préprod.
|
||||
'allow_remove_extras' => true,
|
||||
),
|
||||
array(
|
||||
'table' => 'inscriptions_eve_roles',
|
||||
'label' => 'Accès v2 — rôles',
|
||||
'keys' => array('role_code'),
|
||||
'ignore_cols' => array('role_id', 'role_created_at', 'role_updated_at'),
|
||||
'allow_remove_extras' => true,
|
||||
),
|
||||
array(
|
||||
'table' => 'inscriptions_eve_role_permissions',
|
||||
'label' => 'Accès v2 — matrice rôles',
|
||||
'keys' => array('role_id', 'perm_key'),
|
||||
'ignore_cols' => array(),
|
||||
// CON-333 — role_id change entre BD ; comparer / sync via role_code.
|
||||
'keys' => array('role_code', 'perm_key'),
|
||||
'ignore_cols' => array('role_id'),
|
||||
'join_role_code' => true,
|
||||
'allow_remove_extras' => true,
|
||||
),
|
||||
array(
|
||||
'table' => 'inscriptions_pages',
|
||||
@ -128,6 +134,18 @@ function fxStaticSyncTableDefinitions() {
|
||||
'keys' => array('reg_id'),
|
||||
'ignore_cols' => array(),
|
||||
),
|
||||
|
||||
// CON-333 — seeds traçabilité fiche (MSIN-4464) ; log runtime exclu.
|
||||
array(
|
||||
'table' => 'inscriptions_fiche_audit_settings',
|
||||
'label' => 'Audit fiche — réglages',
|
||||
'keys' => array('setting_id'),
|
||||
'ignore_cols' => array('setting_updated_at'),
|
||||
),
|
||||
array(
|
||||
'table' => 'inscriptions_fiche_audit_fields',
|
||||
'label' => 'Audit fiche — champs',
|
||||
'keys' => array('field_key'),
|
||||
'ignore_cols' => array('field_updated_at'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -7,8 +7,8 @@
|
||||
* Constantes *
|
||||
*
|
||||
**************/
|
||||
define('_VERSION_CODE', '4.72.843');
|
||||
define('_DATE_CODE', '2026-07-21');
|
||||
define('_VERSION_CODE', '4.72.869'); // MSIN-4479 — cache-bust bib-assignments (alert qte)
|
||||
define('_DATE_CODE', '2026-07-24');
|
||||
//MSIN-4290
|
||||
define('QR_SECRET_KEY', 'ms1_qr_2026_cle_secrete_longue_et_fixe');
|
||||
|
||||
@ -30,7 +30,7 @@ define('KEYCHAIN_HTTP_EDIT_ALLOWED_COM_IDS', '');
|
||||
// MSIN-4464 — Même liste pour la config traçabilité fiche (whitelist + rétention).
|
||||
// Valeurs = com_courriel ou com_login (session superadm usa_info).
|
||||
$arrEveAccesKitEditors = array(
|
||||
'stephan@progiweb.ca',
|
||||
'stephan@progiweb.ca','desjardins.lp@ms1timing.com',
|
||||
);
|
||||
|
||||
/**************************************
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Tableau de bord sync tables statiques — préprod dev, superadm seulement.
|
||||
* Source : devinscription_preprod
|
||||
* CON-333 — Tableau de bord sync tables statiques (soft).
|
||||
* Source : devinscription_preprod. Aucun DELETE — insert manquants + réparation poubelles info.
|
||||
*/
|
||||
|
||||
require_once('inc_start_time.php');
|
||||
@ -26,6 +26,24 @@ function ssEsc($str) {
|
||||
return htmlspecialchars((string)$str, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function ssSoftActionLabel($strType) {
|
||||
if ($strType === 'extra') {
|
||||
return 'en trop (cible seulement — ne pas effacer auto)';
|
||||
}
|
||||
if ($strType === 'missing') {
|
||||
return 'manque en cible';
|
||||
}
|
||||
if ($strType === 'poubelle') {
|
||||
return 'poubelle réparable';
|
||||
}
|
||||
|
||||
if ($strType === 'diff') {
|
||||
return 'contenu différent — Appliquer corrections';
|
||||
}
|
||||
|
||||
return 'contenu différent';
|
||||
}
|
||||
|
||||
$strCsrf = fxStaticSyncEnsureCsrfToken();
|
||||
$strFlash = '';
|
||||
$strFlashType = 'info';
|
||||
@ -36,16 +54,24 @@ if (is_array($tabPopFlash)) {
|
||||
$strFlashType = $tabPopFlash['type'];
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'sync') {
|
||||
// CON-333 — actions soft : apply_missing | apply_content (préprod = vérité textes).
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||
$strAction = trim($_POST['action'] ?? '');
|
||||
|
||||
if (!fxStaticSyncValidateCsrf($_POST['csrf'] ?? '')) {
|
||||
fxStaticSyncSetFlash('Jeton CSRF invalide.', 'bad');
|
||||
} elseif (!in_array($strAction, array('apply_missing', 'apply_content', 'apply_all', 'remove_extras', 'remove_info_poubelle_extras'), true)) {
|
||||
fxStaticSyncSetFlash(
|
||||
'Action refusée. Sync destructif désactivé (CON-333). '
|
||||
. 'Utiliser Appliquer manquants / corrections / Retirer en trop / Retirer poubelles info.',
|
||||
'bad'
|
||||
);
|
||||
} else {
|
||||
$strTable = trim($_POST['table'] ?? '');
|
||||
$strEnvId = trim($_POST['env'] ?? '');
|
||||
$tabDef = fxStaticSyncFindTableDef($strTable);
|
||||
$tabEnv = fxStaticSyncFindEnvDef($strEnvId);
|
||||
|
||||
if (!$tabDef || !$tabEnv || !empty($tabEnv['is_source'])) {
|
||||
if (!$tabEnv || !empty($tabEnv['is_source'])) {
|
||||
fxStaticSyncSetFlash('Paramètres invalides.', 'bad');
|
||||
} else {
|
||||
$tabConnections = fxStaticSyncOpenConnections();
|
||||
@ -57,24 +83,54 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
|
||||
'Connexion BD impossible — ' . fxStaticSyncFormatConnectionError($tabEnv),
|
||||
'bad'
|
||||
);
|
||||
} elseif ($strAction === 'apply_all') {
|
||||
$tabResult = fxStaticSyncApplyAllSoft($objSource, $objTarget, $strEnvId);
|
||||
$strMsg = $tabEnv['label'] . ' — ' . $tabResult['message'];
|
||||
if (!empty($tabResult['lines'])) {
|
||||
$strMsg .= "\n" . implode("\n", $tabResult['lines']);
|
||||
}
|
||||
fxStaticSyncSetFlash($strMsg, !empty($tabResult['ok']) ? 'ok' : 'bad');
|
||||
} else {
|
||||
$tabResult = fxStaticSyncCopyTable($objSource, $objTarget, $strTable, $tabDef);
|
||||
if ($tabResult['ok']) {
|
||||
$tabVerify = fxStaticSyncCompareEnvPair($objSource, $objTarget, $tabDef);
|
||||
$strMsg = $tabDef['label'] . ' → ' . $tabEnv['label'] . ' : ' . $tabResult['message'];
|
||||
if ($tabVerify['status'] === 'ok') {
|
||||
$strMsg .= ' Vérification : aligné.';
|
||||
fxStaticSyncSetFlash($strMsg, 'ok');
|
||||
} else {
|
||||
$strMsg .= ' ATTENTION — après sync : '
|
||||
. fxStaticSyncStatusLabel($tabVerify['status'], $tabVerify) . '.';
|
||||
fxStaticSyncSetFlash($strMsg, 'bad');
|
||||
}
|
||||
$tabDef = fxStaticSyncFindTableDef($strTable);
|
||||
if (!$tabDef) {
|
||||
fxStaticSyncSetFlash('Paramètres invalides.', 'bad');
|
||||
} else {
|
||||
fxStaticSyncSetFlash(
|
||||
$tabDef['label'] . ' → ' . $tabEnv['label'] . ' — échec : ' . $tabResult['message'],
|
||||
'bad'
|
||||
);
|
||||
if ($strAction === 'apply_missing') {
|
||||
$tabResult = fxStaticSyncApplyMissingRows($objSource, $objTarget, $strTable, $tabDef);
|
||||
} elseif ($strAction === 'remove_extras') {
|
||||
$tabResult = fxStaticSyncRemoveExtras($objSource, $objTarget, $strTable, $tabDef);
|
||||
} elseif ($strAction === 'remove_info_poubelle_extras') {
|
||||
if ($tabDef['table'] !== 'info') {
|
||||
$tabResult = array('ok' => false, 'message' => 'Réservé à info.');
|
||||
} else {
|
||||
$tabResult = fxStaticSyncRemoveInfoExtraPoubelle($objSource, $objTarget, $tabDef);
|
||||
}
|
||||
} else {
|
||||
$tabResult = fxStaticSyncApplyContentUpdates($objSource, $objTarget, $strTable, $tabDef);
|
||||
}
|
||||
|
||||
if ($tabResult['ok']) {
|
||||
$tabVerify = fxStaticSyncCompareEnvPair($objSource, $objTarget, $tabDef);
|
||||
$strMsg = $tabDef['label'] . ' → ' . $tabEnv['label'] . ' : ' . $tabResult['message'];
|
||||
$intExtra = count($tabVerify['extra_keys']);
|
||||
$intMiss = count($tabVerify['missing_keys']);
|
||||
$intDiff = count($tabVerify['diff_keys']);
|
||||
$strMsg .= ' Après : manque ' . $intMiss
|
||||
. ', contenu diff. ' . $intDiff
|
||||
. ', en trop (conservés) ' . $intExtra . '.';
|
||||
if ($intMiss === 0 && $intDiff === 0 && $tabVerify['status'] === 'ok') {
|
||||
fxStaticSyncSetFlash($strMsg, 'ok');
|
||||
} elseif ($intMiss === 0 && $intDiff === 0) {
|
||||
fxStaticSyncSetFlash($strMsg . ' Écarts restants = revue manuelle (pas de DELETE).', 'ok');
|
||||
} else {
|
||||
fxStaticSyncSetFlash($strMsg, 'ok');
|
||||
}
|
||||
} else {
|
||||
fxStaticSyncSetFlash(
|
||||
$tabDef['label'] . ' → ' . $tabEnv['label'] . ' — échec : ' . $tabResult['message'],
|
||||
'bad'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -100,7 +156,7 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Sync tables statiques</title>
|
||||
<title>Sync tables statiques (soft) — CON-333</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; background: #f4f6f8; color: #222; }
|
||||
h1 { font-size: 1.4rem; margin: 0 0 8px; }
|
||||
@ -109,8 +165,11 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
|
||||
.flash-ok { background: #d4edda; border: 1px solid #b7dfc3; }
|
||||
.flash-bad { background: #f8d7da; border: 1px solid #f1b8be; }
|
||||
.flash-info { background: #e8f0fe; border: 1px solid #c5d7fb; }
|
||||
.warn-box { background: #fff3cd; border: 1px solid #ffecb5; padding: 12px 14px; border-radius: 6px; margin-bottom: 16px; font-size: 0.9rem; }
|
||||
.warn-box strong { color: #856404; }
|
||||
.warn-box { background: #e8f5e9; border: 1px solid #a5d6a7; padding: 12px 14px; border-radius: 6px; margin-bottom: 16px; font-size: 0.9rem; }
|
||||
.warn-box strong { color: #1b5e20; }
|
||||
.proc { background: #fff; border: 1px solid #ccc; padding: 12px 14px; border-radius: 6px; margin-bottom: 16px; font-size: 0.9rem; }
|
||||
.proc ol { margin: 8px 0 0; padding-left: 22px; }
|
||||
.proc li { margin-bottom: 4px; }
|
||||
table { width: 100%; border-collapse: collapse; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
|
||||
th, td { border: 1px solid #ddd; padding: 8px 10px; vertical-align: top; font-size: 0.9rem; }
|
||||
th { background: #eef2f7; text-align: left; }
|
||||
@ -122,9 +181,12 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
|
||||
.badge { display: inline-block; padding: 3px 8px; border-radius: 4px; font-size: 0.82rem; font-weight: 600; max-width: 100%; line-height: 1.35; }
|
||||
.count { font-size: 0.82rem; color: #444; margin-top: 4px; }
|
||||
.count-hint { font-size: 0.78rem; color: #666; }
|
||||
.btn { display: inline-block; margin-top: 6px; padding: 4px 10px; font-size: 0.82rem; border: 1px solid #888; background: #fff; border-radius: 4px; cursor: pointer; text-decoration: none; color: #222; }
|
||||
.btn { display: inline-block; margin-top: 6px; margin-right: 4px; padding: 4px 10px; font-size: 0.82rem; border: 1px solid #888; background: #fff; border-radius: 4px; cursor: pointer; text-decoration: none; color: #222; }
|
||||
.btn:hover { background: #f0f0f0; }
|
||||
.btn-sync { border-color: #2e6da4; color: #1a4f7a; }
|
||||
.btn-soft { border-color: #2e7d32; color: #1b5e20; }
|
||||
.btn-content { border-color: #1565c0; color: #0d47a1; }
|
||||
.btn-remove { border-color: #c62828; color: #b71c1c; }
|
||||
.btn-repair { border-color: #ef6c00; color: #e65100; }
|
||||
.detail-box { margin-top: 24px; background: #fff; padding: 16px; border: 1px solid #ccc; border-radius: 6px; }
|
||||
.detail-box h2 { margin-top: 0; font-size: 1.1rem; }
|
||||
.detail-list { margin: 0; padding-left: 18px; }
|
||||
@ -136,11 +198,31 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Sync tables statiques</h1>
|
||||
<h1>Sync tables statiques — mode soft (CON-333)</h1>
|
||||
<div class="warn-box">
|
||||
<strong>Attention — opération destructive.</strong>
|
||||
Sync = recopie complète depuis dev préprod (efface la cible). Un clic sur Sync.
|
||||
<strong>Aucun DELETE.</strong>
|
||||
On n’efface jamais un <strong>vrai</strong> libellé <code>info</code> seulement en cible.
|
||||
Les <strong>poubelles</strong> info en trop (texte = clé) : bouton dédié pour les retirer.
|
||||
Catalogues accès v2 : Retirer en trop (sauf rôle encore utilisé).
|
||||
</div>
|
||||
|
||||
<div class="proc">
|
||||
<strong>Test complet (un environnement à la fois)</strong>
|
||||
<ol>
|
||||
<li>Rafraîchir cette page (nouveau code déployé).</li>
|
||||
<li>Cliquer <em>TOUT APPLIQUER</em> sous la colonne voulue (commencer par <strong>Dev prod</strong>).</li>
|
||||
<li>Lire le message vert/rouge en haut : succès ou échec + détail table par table.</li>
|
||||
<li>Recharger : Manque=0 et Diff=0. Sur accès v2, En trop aussi → 0 (retirés auto). Sur <code>info</code>, En trop = à revoir via Détails (jamais DELETE auto).</li>
|
||||
<li>Lignes SAUTÉES (structure / table absente) = à corriger en Navicat, puis re-cliquer.</li>
|
||||
</ol>
|
||||
<form method="post" style="margin-top:12px;" onsubmit="return confirm('CON-333 — TOUT APPLIQUER sur Dev prod ?\nToutes les tables : manquants + corrections.\nAucun DELETE.');">
|
||||
<input type="hidden" name="action" value="apply_all">
|
||||
<input type="hidden" name="csrf" value="<?php echo ssEsc($strCsrf); ?>">
|
||||
<input type="hidden" name="env" value="dev_prod">
|
||||
<button type="submit" class="btn btn-content" style="font-weight:700;padding:8px 14px;">TOUT APPLIQUER → Dev prod</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p class="sub">
|
||||
Source de vérité : <strong>dev préprod</strong> (<code><?php echo ssEsc($arrConDatabasePreProd['db'] ?? ''); ?></code>).
|
||||
Comparaison vers dev prod, client préprod et client prod.
|
||||
@ -197,12 +279,30 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
|
||||
}
|
||||
$strClass = fxStaticSyncStatusClass($tabCmp['status']);
|
||||
$strLabel = fxStaticSyncStatusLabel($tabCmp['status'], $tabCmp);
|
||||
$intMissing = isset($tabCmp['missing_keys']) ? count($tabCmp['missing_keys']) : 0;
|
||||
$intExtra = isset($tabCmp['extra_keys']) ? count($tabCmp['extra_keys']) : 0;
|
||||
$intDiff = isset($tabCmp['diff_keys']) ? count($tabCmp['diff_keys']) : 0;
|
||||
$intPoubExtra = isset($tabCmp['poubelle_extra_keys']) ? count($tabCmp['poubelle_extra_keys']) : 0;
|
||||
$intRealExtra = isset($tabCmp['real_extra_keys']) ? count($tabCmp['real_extra_keys']) : 0;
|
||||
$blnCanRemoveExtras = !empty($tabDef['allow_remove_extras']);
|
||||
$blnCanWrite = ($tabCmp['status'] !== 'no_connection' && $tabCmp['status'] !== 'schema_mismatch'
|
||||
&& $tabCmp['status'] !== 'missing_table' && $tabCmp['status'] !== 'error');
|
||||
?>
|
||||
<td class="<?php echo ssEsc($strClass); ?>">
|
||||
<span class="badge"><?php echo ssEsc($strLabel); ?></span>
|
||||
<div class="count">
|
||||
Cible <?php echo (int)$tabCmp['target_count']; ?> · Source <?php echo (int)$tabCmp['source_count']; ?>
|
||||
</div>
|
||||
<?php if ($blnCanWrite) { ?>
|
||||
<div class="count-hint">
|
||||
Manque <?php echo (int)$intMissing; ?>
|
||||
· Diff <?php echo (int)$intDiff; ?>
|
||||
· En trop <?php echo (int)$intExtra; ?>
|
||||
<?php if ($tabDef['table'] === 'info' && $intExtra > 0) { ?>
|
||||
<br>dont poubelles <?php echo (int)$intPoubExtra; ?> · vrais textes <?php echo (int)$intRealExtra; ?>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<?php if ($tabCmp['status'] === 'schema_mismatch') {
|
||||
$tabSchemaDiff = array(
|
||||
'missing_in_target' => isset($tabCmp['schema_missing_cols']) ? $tabCmp['schema_missing_cols'] : array(),
|
||||
@ -220,13 +320,40 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
|
||||
if ($strConnErr !== '') { ?>
|
||||
<div class="count-hint"><?php echo ssEsc($strConnErr); ?></div>
|
||||
<?php } } ?>
|
||||
<?php if ($tabCmp['status'] !== 'no_connection' && $tabCmp['status'] !== 'schema_mismatch') { ?>
|
||||
<form method="post" style="display:inline;">
|
||||
<input type="hidden" name="action" value="sync">
|
||||
<?php if ($blnCanWrite && $intMissing > 0) { ?>
|
||||
<form method="post" style="display:inline;" onsubmit="return confirm('CON-333 — Ajouter <?php echo (int)$intMissing; ?> ligne(s) manquante(s) dans <?php echo ssEsc($tabEnv['label']); ?> ?\nAucun DELETE.');">
|
||||
<input type="hidden" name="action" value="apply_missing">
|
||||
<input type="hidden" name="csrf" value="<?php echo ssEsc($strCsrf); ?>">
|
||||
<input type="hidden" name="table" value="<?php echo ssEsc($tabDef['table']); ?>">
|
||||
<input type="hidden" name="env" value="<?php echo ssEsc($strEnvId); ?>">
|
||||
<button type="submit" class="btn btn-sync">Sync →</button>
|
||||
<button type="submit" class="btn btn-soft">Appliquer manquants (<?php echo (int)$intMissing; ?>)</button>
|
||||
</form>
|
||||
<?php } ?>
|
||||
<?php if ($blnCanWrite && $intDiff > 0) { ?>
|
||||
<form method="post" style="display:inline;" onsubmit="return confirm('CON-333 — Écraser <?php echo (int)$intDiff; ?> ligne(s) en cible avec le contenu préprod ?\nLes clés en trop en cible ne sont pas touchées. Aucun DELETE.');">
|
||||
<input type="hidden" name="action" value="apply_content">
|
||||
<input type="hidden" name="csrf" value="<?php echo ssEsc($strCsrf); ?>">
|
||||
<input type="hidden" name="table" value="<?php echo ssEsc($tabDef['table']); ?>">
|
||||
<input type="hidden" name="env" value="<?php echo ssEsc($strEnvId); ?>">
|
||||
<button type="submit" class="btn btn-content">Appliquer corrections (<?php echo (int)$intDiff; ?>)</button>
|
||||
</form>
|
||||
<?php } ?>
|
||||
<?php if ($blnCanWrite && $blnCanRemoveExtras && $intExtra > 0) { ?>
|
||||
<form method="post" style="display:inline;" onsubmit="return confirm('CON-333 — Retirer <?php echo (int)$intExtra; ?> en trop dans <?php echo ssEsc($tabEnv['label']); ?> ?\nCatalogue accès v2 seulement.\nLes rôles encore utilisés (inscriptions_eve_acces) ne seront PAS effacés.');">
|
||||
<input type="hidden" name="action" value="remove_extras">
|
||||
<input type="hidden" name="csrf" value="<?php echo ssEsc($strCsrf); ?>">
|
||||
<input type="hidden" name="table" value="<?php echo ssEsc($tabDef['table']); ?>">
|
||||
<input type="hidden" name="env" value="<?php echo ssEsc($strEnvId); ?>">
|
||||
<button type="submit" class="btn btn-remove">Retirer en trop (<?php echo (int)$intExtra; ?>)</button>
|
||||
</form>
|
||||
<?php } ?>
|
||||
<?php if ($blnCanWrite && $tabDef['table'] === 'info' && $intPoubExtra > 0) { ?>
|
||||
<form method="post" style="display:inline;" onsubmit="return confirm('CON-333 — Retirer <?php echo (int)$intPoubExtra; ?> poubelle(s) info en trop ?\nSeules les lignes texte=clé absentes de la préprod.\nLes <?php echo (int)$intRealExtra; ?> vrais libellés seulement en cible restent.');">
|
||||
<input type="hidden" name="action" value="remove_info_poubelle_extras">
|
||||
<input type="hidden" name="csrf" value="<?php echo ssEsc($strCsrf); ?>">
|
||||
<input type="hidden" name="table" value="info">
|
||||
<input type="hidden" name="env" value="<?php echo ssEsc($strEnvId); ?>">
|
||||
<button type="submit" class="btn btn-remove">Retirer poubelles en trop (<?php echo (int)$intPoubExtra; ?>)</button>
|
||||
</form>
|
||||
<?php } ?>
|
||||
</td>
|
||||
@ -240,51 +367,75 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
|
||||
$tabCmp = $tabDetail['compare'];
|
||||
$tabDef = fxStaticSyncFindTableDef($strDetailTable);
|
||||
$tabEnv = fxStaticSyncFindEnvDef($strDetailEnv);
|
||||
$intMissing = count($tabCmp['missing_keys']);
|
||||
$intExtra = count($tabCmp['extra_keys']);
|
||||
$intDiff = count($tabCmp['diff_keys']);
|
||||
$tabMissingSamples = isset($tabDetail['samples_missing']) ? $tabDetail['samples_missing'] : array();
|
||||
$tabDiffSamples = isset($tabDetail['samples_diff']) ? $tabDetail['samples_diff'] : array();
|
||||
$tabExtraSamples = isset($tabDetail['samples_extra']) ? $tabDetail['samples_extra'] : array();
|
||||
?>
|
||||
<div class="detail-box">
|
||||
<h2>Détails — <?php echo ssEsc($tabDef['label'] ?? $strDetailTable); ?> → <?php echo ssEsc($tabEnv['label'] ?? $strDetailEnv); ?></h2>
|
||||
<p>
|
||||
<?php
|
||||
$intDelta = (int)$tabCmp['target_count'] - (int)$tabCmp['source_count'];
|
||||
if ($intDelta < 0) {
|
||||
echo 'Il en manque ' . abs($intDelta) . ' (cible ' . (int)$tabCmp['target_count']
|
||||
. ' · source ' . (int)$tabCmp['source_count'] . ')';
|
||||
} elseif ($intDelta > 0) {
|
||||
echo 'Il y en a ' . $intDelta . ' en trop (cible ' . (int)$tabCmp['target_count']
|
||||
. ' · source ' . (int)$tabCmp['source_count'] . ')';
|
||||
} else {
|
||||
echo 'Même nombre de lignes, contenu différent (cible ' . (int)$tabCmp['target_count']
|
||||
. ' · source ' . (int)$tabCmp['source_count'] . ')';
|
||||
}
|
||||
?>
|
||||
<?php if ($tabCmp['status'] === 'schema_mismatch') {
|
||||
$tabSchemaDiff = array(
|
||||
'missing_in_target' => isset($tabCmp['schema_missing_cols']) ? $tabCmp['schema_missing_cols'] : array(),
|
||||
'extra_in_target' => isset($tabCmp['schema_extra_cols']) ? $tabCmp['schema_extra_cols'] : array(),
|
||||
);
|
||||
$strSchemaDiff = fxStaticSyncFormatSchemaDiff($tabSchemaDiff);
|
||||
if ($strSchemaDiff !== '') { ?>
|
||||
<br><?php echo ssEsc($strSchemaDiff); ?>
|
||||
<?php } } ?>
|
||||
Manque <strong><?php echo (int)$intMissing; ?></strong>
|
||||
· Contenu différent <strong><?php echo (int)$intDiff; ?></strong>
|
||||
· En trop en cible <strong><?php echo (int)$intExtra; ?></strong>
|
||||
<a class="btn" href="sync_static_db.php" style="margin-left:12px;">Fermer</a>
|
||||
</p>
|
||||
<?php if (!empty($tabDetail['samples'])) { ?>
|
||||
<?php if (!empty($tabDetail['note'])) { ?>
|
||||
<p class="count-hint"><?php echo ssEsc($tabDetail['note']); ?></p>
|
||||
<?php } ?>
|
||||
<?php if (!empty($tabDetail['extra_context'])) { ?>
|
||||
<p class="warn-box" style="margin-top:10px;"><?php echo ssEsc($tabDetail['extra_context']); ?></p>
|
||||
<?php } ?>
|
||||
|
||||
<?php if (count($tabExtraSamples) > 0) { ?>
|
||||
<h3 style="margin-top:18px;font-size:1rem;">En trop en cible (<?php echo (int)$intExtra; ?>) — explications</h3>
|
||||
<ul class="detail-list">
|
||||
<?php foreach ($tabDetail['samples'] as $tabSample) { ?>
|
||||
<?php foreach ($tabExtraSamples as $tabSample) { ?>
|
||||
<li>
|
||||
<strong><?php echo ssEsc(fxStaticSyncFormatKeyPreview($tabSample['key'])); ?></strong>
|
||||
(<?php
|
||||
echo ssEsc($tabSample['type'] === 'extra' ? 'en trop'
|
||||
: ($tabSample['type'] === 'missing' ? 'manque' : 'contenu diff.'));
|
||||
?>)
|
||||
<?php if ($tabDef['table'] === 'info' && $tabSample['type'] === 'diff') { ?>
|
||||
<br>Source : <em><?php echo ssEsc($tabSample['source_texte']); ?></em>
|
||||
<br>Cible : <em><?php echo ssEsc($tabSample['target_texte']); ?></em>
|
||||
<br><?php echo ssEsc(isset($tabSample['explain']) ? $tabSample['explain'] : ssSoftActionLabel('extra')); ?>
|
||||
<?php if ($tabDef['table'] === 'info' && !empty($tabSample['target_texte'])) { ?>
|
||||
<br>Texte cible : <em><?php echo ssEsc($tabSample['target_texte']); ?></em>
|
||||
<?php } ?>
|
||||
</li>
|
||||
<?php } ?>
|
||||
</ul>
|
||||
<?php } else { ?>
|
||||
<?php if ($intExtra > count($tabExtraSamples)) { ?>
|
||||
<p class="count-hint">Affichage limité à <?php echo count($tabExtraSamples); ?> / <?php echo (int)$intExtra; ?>.</p>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
|
||||
<?php if (count($tabMissingSamples) > 0) { ?>
|
||||
<h3 style="margin-top:18px;font-size:1rem;">Manquants (<?php echo (int)$intMissing; ?>)</h3>
|
||||
<ul class="detail-list">
|
||||
<?php foreach ($tabMissingSamples as $tabSample) { ?>
|
||||
<li>
|
||||
<strong><?php echo ssEsc(fxStaticSyncFormatKeyPreview($tabSample['key'])); ?></strong>
|
||||
— <?php echo ssEsc(isset($tabSample['explain']) ? $tabSample['explain'] : 'manque'); ?>
|
||||
</li>
|
||||
<?php } ?>
|
||||
</ul>
|
||||
<?php } ?>
|
||||
|
||||
<?php if (count($tabDiffSamples) > 0 && $intDiff > 0) { ?>
|
||||
<h3 style="margin-top:18px;font-size:1rem;">Contenu différent (<?php echo (int)$intDiff; ?>)</h3>
|
||||
<ul class="detail-list">
|
||||
<?php foreach ($tabDiffSamples as $tabSample) { ?>
|
||||
<li>
|
||||
<strong><?php echo ssEsc(fxStaticSyncFormatKeyPreview($tabSample['key'])); ?></strong>
|
||||
— <?php echo ssEsc(isset($tabSample['explain']) ? $tabSample['explain'] : 'diff'); ?>
|
||||
<?php if ($tabDef['table'] === 'info') { ?>
|
||||
<br>Source : <em><?php echo ssEsc($tabSample['source_texte'] ?? ''); ?></em>
|
||||
<br>Cible : <em><?php echo ssEsc($tabSample['target_texte'] ?? ''); ?></em>
|
||||
<?php } ?>
|
||||
</li>
|
||||
<?php } ?>
|
||||
</ul>
|
||||
<?php } ?>
|
||||
|
||||
<?php if ($intMissing === 0 && $intDiff === 0 && $intExtra === 0) { ?>
|
||||
<p>Aucun écart à afficher.</p>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
BIN
php/templates/MSIN-4469-commande-dossards.xlsx
Normal file
BIN
php/templates/MSIN-4469-commande-dossards.xlsx
Normal file
Binary file not shown.
24
sql/MSIN-3487-inscriptions-evenements-extra.sql
Normal file
24
sql/MSIN-3487-inscriptions-evenements-extra.sql
Normal file
@ -0,0 +1,24 @@
|
||||
-- MSIN-3487 — Table supplémentaire par événement (lien résultats, etc.)
|
||||
-- Prérequis : aucun
|
||||
-- Notes prod : idempotent (CREATE IF NOT EXISTS) ; pas de FK (pattern projet)
|
||||
--
|
||||
-- IMPORTANT — sélectionner la base AVANT d'exécuter (sinon erreur 1046).
|
||||
-- Décommenter UNE ligne USE ci-dessous selon l'environnement :
|
||||
--
|
||||
-- USE devinscription_preprod;
|
||||
-- USE devinscription_prod;
|
||||
-- USE ms1inscription_preprod;
|
||||
-- USE ms1inscription_prod;
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `inscriptions_evenements_extra` (
|
||||
`eve_id` int(10) unsigned NOT NULL
|
||||
COMMENT 'MSIN-3487 — PK = inscriptions_evenements.eve_id',
|
||||
`eve_resultats_url` varchar(500) DEFAULT NULL
|
||||
COMMENT 'MSIN-3487 — URL publique des résultats de course',
|
||||
`eve_extra_maj` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP
|
||||
COMMENT 'MSIN-3487 — dernière mise à jour de la ligne',
|
||||
PRIMARY KEY (`eve_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='MSIN-3487 — métadonnées supplémentaires d''événement (hors table principale)';
|
||||
@ -200,8 +200,8 @@ INSERT INTO info (info_clef, info_langue, info_texte, info_aide, info_prg, info_
|
||||
('inscr_gestion_err_refund', 'en', 'Refund error.', '', 'compte.php', '', 0, 1, '', '', '', NOW()),
|
||||
('inscr_gestion_refund_ok', 'fr', 'Remboursement effectué.', '', 'compte.php', '', 0, 1, '', '', '', NOW()),
|
||||
('inscr_gestion_refund_ok', 'en', 'Refund completed.', '', 'compte.php', '', 0, 1, '', '', '', NOW()),
|
||||
('inscr_gestion_refund_cancel_hint', 'fr', 'Votre transaction a été remboursée. Si vous voulez aussi l''annuler dans le système d''inscription :', '', 'compte.php', '', 0, 1, '', '', '', NOW()),
|
||||
('inscr_gestion_refund_cancel_hint', 'en', 'Your transaction has been refunded. If you also want to cancel it in the registration system:', '', 'compte.php', '', 0, 1, '', '', '', NOW()),
|
||||
('inscr_gestion_refund_cancel_hint', 'fr', 'Votre transaction a été remboursée. Si vous voulez aussi annuler le participant cliquez sur annuler', '', 'compte.php', '', 0, 1, '', '', '', NOW()),
|
||||
('inscr_gestion_refund_cancel_hint', 'en', 'Your transaction has been refunded. If you also want to cancel the participant, click cancel', '', 'compte.php', '', 0, 1, '', '', '', NOW()),
|
||||
('promoteur_hub_load_menu', 'fr', 'Chargement du menu…', '', 'compte.php', '', 0, 1, '', '', '', NOW()),
|
||||
('promoteur_hub_load_menu', 'en', 'Loading menu…', '', 'compte.php', '', 0, 1, '', '', '', NOW()),
|
||||
('promoteur_hub_load_dashboard', 'fr', 'Chargement du tableau de bord…', '', 'compte.php', '', 0, 1, '', '', '', NOW()),
|
||||
|
||||
15
sql/MSIN-4419-refund-cancel-hint.sql
Normal file
15
sql/MSIN-4419-refund-cancel-hint.sql
Normal file
@ -0,0 +1,15 @@
|
||||
-- MSIN-4419 — Hint post-remboursement (annuler le participant)
|
||||
-- Prérequis : clés déjà créées (MSIN-4401-v2-info-complet.sql)
|
||||
-- Idempotent : UPDATE ciblé FR/EN sur compte.php
|
||||
|
||||
UPDATE info
|
||||
SET info_texte = 'Votre transaction a été remboursée. Si vous voulez aussi annuler le participant cliquez sur annuler'
|
||||
WHERE info_clef = 'inscr_gestion_refund_cancel_hint'
|
||||
AND info_langue = 'fr'
|
||||
AND info_prg = 'compte.php';
|
||||
|
||||
UPDATE info
|
||||
SET info_texte = 'Your transaction has been refunded. If you also want to cancel the participant, click cancel'
|
||||
WHERE info_clef = 'inscr_gestion_refund_cancel_hint'
|
||||
AND info_langue = 'en'
|
||||
AND info_prg = 'compte.php';
|
||||
15
sql/MSIN-4445-bib-global-free-all-solo.sql
Normal file
15
sql/MSIN-4445-bib-global-free-all-solo.sql
Normal file
@ -0,0 +1,15 @@
|
||||
-- MSIN-4445 — Assignation libre : toutes les épreuves solo (avec ou sans séquence)
|
||||
-- Idempotent. Met à jour le hint Info du bloc libre.
|
||||
-- Prérequis : sql/MSIN-4445-bib-global-free.sql déjà exécuté.
|
||||
|
||||
UPDATE info
|
||||
SET info_texte = 'Toutes les épreuves solo : choisissez Début et Fin (ou Qté). Les dossards sont assignés sans créer de séquence, même si une séquence existe déjà. Le Go reste disponible même si la quantité n''est pas juste.'
|
||||
WHERE info_clef = 'bib_v4_global_batch_free_hint'
|
||||
AND info_langue = 'fr'
|
||||
AND info_prg = 'compte.php';
|
||||
|
||||
UPDATE info
|
||||
SET info_texte = 'All solo races: choose Start and End (or Qty). Bibs are assigned without creating a sequence, even if a sequence already exists. Go stays available even if the quantity is not exact.'
|
||||
WHERE info_clef = 'bib_v4_global_batch_free_hint'
|
||||
AND info_langue = 'en'
|
||||
AND info_prg = 'compte.php';
|
||||
@ -11,12 +11,17 @@ WHERE info_prg = 'compte.php'
|
||||
'bib_v5_workspace_operations',
|
||||
'bib_v5_workspace_settings',
|
||||
'bib_v5_preparation_intro',
|
||||
'bib_v5_group_quantities',
|
||||
'bib_v5_group_bibs',
|
||||
'bib_v5_modes_intro',
|
||||
'bib_v5_stat_races',
|
||||
'bib_v5_stat_registered',
|
||||
'bib_v5_stat_assigned',
|
||||
'bib_v5_stat_pending',
|
||||
'bib_v5_race_ready',
|
||||
'bib_v5_race_incomplete',
|
||||
'bib_v5_quantities_title',
|
||||
'bib_v5_bib_assignment_title',
|
||||
'bib_v5_ranges_title',
|
||||
'bib_v5_ranges_hint',
|
||||
'bib_v5_method_title',
|
||||
|
||||
@ -42,6 +42,12 @@ FROM (
|
||||
UNION ALL SELECT 'bib_v5_workspace_settings', 'en', 'Settings', 'Rules that apply to the entire event.'
|
||||
UNION ALL SELECT 'bib_v5_preparation_intro', 'fr', 'Préparez les épreuves et attribuez les dossards aux inscriptions.', ''
|
||||
UNION ALL SELECT 'bib_v5_preparation_intro', 'en', 'Prepare races and assign bibs to registrations.', ''
|
||||
UNION ALL SELECT 'bib_v5_group_quantities', 'fr', 'Quantités', ''
|
||||
UNION ALL SELECT 'bib_v5_group_quantities', 'en', 'Quantities', ''
|
||||
UNION ALL SELECT 'bib_v5_group_bibs', 'fr', 'Dossards', ''
|
||||
UNION ALL SELECT 'bib_v5_group_bibs', 'en', 'Bibs', ''
|
||||
UNION ALL SELECT 'bib_v5_modes_intro', 'fr', 'Utilisez les modes d’assignation qui traitent plusieurs épreuves ou des besoins particuliers.', ''
|
||||
UNION ALL SELECT 'bib_v5_modes_intro', 'en', 'Use assignment modes that process multiple races or special requirements.', ''
|
||||
UNION ALL SELECT 'bib_v5_stat_races', 'fr', 'Épreuves actives', ''
|
||||
UNION ALL SELECT 'bib_v5_stat_races', 'en', 'Active races', ''
|
||||
UNION ALL SELECT 'bib_v5_stat_registered', 'fr', 'Inscriptions', ''
|
||||
@ -54,6 +60,10 @@ FROM (
|
||||
UNION ALL SELECT 'bib_v5_race_ready', 'en', 'Ready', ''
|
||||
UNION ALL SELECT 'bib_v5_race_incomplete', 'fr', 'À compléter', ''
|
||||
UNION ALL SELECT 'bib_v5_race_incomplete', 'en', 'To complete', ''
|
||||
UNION ALL SELECT 'bib_v5_quantities_title', 'fr', 'Quantités d’inscription', ''
|
||||
UNION ALL SELECT 'bib_v5_quantities_title', 'en', 'Registration quantities', ''
|
||||
UNION ALL SELECT 'bib_v5_bib_assignment_title', 'fr', 'Attribution des dossards', ''
|
||||
UNION ALL SELECT 'bib_v5_bib_assignment_title', 'en', 'Bib assignment', ''
|
||||
UNION ALL SELECT 'bib_v5_ranges_title', 'fr', 'Plages de numéros', ''
|
||||
UNION ALL SELECT 'bib_v5_ranges_title', 'en', 'Number ranges', ''
|
||||
UNION ALL SELECT 'bib_v5_ranges_hint', 'fr', 'Définissez les numéros disponibles pour cette épreuve.', ''
|
||||
|
||||
49
sql/MSIN-4468-message-quantite-totale.sql
Normal file
49
sql/MSIN-4468-message-quantite-totale.sql
Normal file
@ -0,0 +1,49 @@
|
||||
-- MSIN-4468 — Message de validation de la quantité totale
|
||||
-- À exécuter avec le déploiement de l'édition de la quantité totale dans la gestion des dossards.
|
||||
-- Idempotent; ajoute uniquement les libellés FR/EN absents pour compte.php.
|
||||
|
||||
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
|
||||
)
|
||||
SELECT
|
||||
src.info_clef,
|
||||
src.info_langue,
|
||||
src.info_texte,
|
||||
'',
|
||||
'compte.php',
|
||||
'MSIN-4468',
|
||||
0,
|
||||
1,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
NOW()
|
||||
FROM (
|
||||
SELECT
|
||||
'bib_v5_qte_total_min' AS info_clef,
|
||||
'fr' AS info_langue,
|
||||
'Quantité refusée : le total doit être d''au moins %d, soit le nombre d''inscriptions déjà vendues.' AS info_texte
|
||||
UNION ALL
|
||||
SELECT
|
||||
'bib_v5_qte_total_min',
|
||||
'en',
|
||||
'Quantity rejected: the total must be at least %d, which is the number of registrations already sold.'
|
||||
) src
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM info current_info
|
||||
WHERE current_info.info_clef = src.info_clef
|
||||
AND current_info.info_langue = src.info_langue
|
||||
AND current_info.info_prg = 'compte.php'
|
||||
);
|
||||
46
sql/MSIN-4469-col-lignes-imprimeur.sql
Normal file
46
sql/MSIN-4469-col-lignes-imprimeur.sql
Normal file
@ -0,0 +1,46 @@
|
||||
-- MSIN-4469 — Colonne validation « Lignes Imprimeur » (Commande MS1)
|
||||
-- Prérequis : MSIN-4469-production-excel-dossards.sql
|
||||
-- Notes : exécution UNIQUEMENT sur dev préprod ; autres env = Navicat structure + sync_static_db
|
||||
|
||||
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
|
||||
)
|
||||
SELECT
|
||||
src.info_clef,
|
||||
src.info_langue,
|
||||
src.info_texte,
|
||||
src.info_aide,
|
||||
'compte.php',
|
||||
'MSIN-4469',
|
||||
0,
|
||||
1,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
NOW()
|
||||
FROM (
|
||||
SELECT 'bib_v5_production_col_printer_lines' info_clef, 'fr' info_langue,
|
||||
'Lignes Imprimeur' info_texte,
|
||||
'Nombre de lignes de l’onglet Imprimeur (hors lignes vides). Doit égaler la quantité de dossards.' info_aide
|
||||
UNION ALL SELECT 'bib_v5_production_col_printer_lines', 'en',
|
||||
'Printer lines',
|
||||
'Number of rows on the Printer sheet (excluding blank rows). Must match the bib quantity.'
|
||||
) src
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM info current_info
|
||||
WHERE current_info.info_clef = src.info_clef
|
||||
AND current_info.info_langue = src.info_langue
|
||||
AND current_info.info_prg = 'compte.php'
|
||||
);
|
||||
124
sql/MSIN-4469-production-excel-dossards.sql
Normal file
124
sql/MSIN-4469-production-excel-dossards.sql
Normal file
@ -0,0 +1,124 @@
|
||||
-- MSIN-4469 — Production Excel des dossards
|
||||
-- Prérequis : MSIN-4467-refonte-visuelle-dossards.sql.
|
||||
-- Idempotent. Exécuter avant le déploiement PHP et avant le premier test.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `inscriptions_bib_production_orders` (
|
||||
`bpo_id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`eve_id` INT NOT NULL,
|
||||
`bpo_com_id` INT NOT NULL DEFAULT 0,
|
||||
`bpo_usa_id` INT NOT NULL DEFAULT 0,
|
||||
`bpo_status` VARCHAR(24) NOT NULL DEFAULT 'generating',
|
||||
`bpo_active_key` TINYINT UNSIGNED NULL DEFAULT NULL
|
||||
COMMENT '1 = commande active; NULL = historique (unicité MySQL par événement)',
|
||||
`bpo_version` INT UNSIGNED NOT NULL DEFAULT 1,
|
||||
`bpo_file_name` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`bpo_file_path` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
`bpo_sha256` CHAR(64) NOT NULL DEFAULT '',
|
||||
`bpo_snapshot` LONGTEXT NULL,
|
||||
`bpo_created_at` DATETIME NOT NULL,
|
||||
`bpo_created_by` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`bpo_superseded_at` DATETIME NULL DEFAULT NULL,
|
||||
`bpo_superseded_by` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`bpo_superseded_reason` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
`bpo_error` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (`bpo_id`),
|
||||
UNIQUE KEY `uq_bpo_active_event` (`eve_id`, `bpo_active_key`),
|
||||
KEY `idx_bpo_event_history` (`eve_id`, `bpo_version`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8
|
||||
COMMENT='MSIN-4469 — fichiers officiels de production des dossards';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `inscriptions_bib_production_ranges` (
|
||||
`bpr_id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`bpo_id` INT UNSIGNED NOT NULL,
|
||||
`eve_id` INT NOT NULL,
|
||||
`epr_id` INT NOT NULL,
|
||||
`epr_bib_id` INT NOT NULL,
|
||||
`bpr_epr_label` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`bpr_start` INT UNSIGNED NOT NULL,
|
||||
`bpr_finish` INT UNSIGNED NOT NULL,
|
||||
`bpr_quantity` INT UNSIGNED NOT NULL,
|
||||
PRIMARY KEY (`bpr_id`),
|
||||
UNIQUE KEY `uq_bpr_order_range` (`bpo_id`, `epr_bib_id`),
|
||||
KEY `idx_bpr_event` (`eve_id`),
|
||||
KEY `idx_bpr_epr` (`epr_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8
|
||||
COMMENT='MSIN-4469 — instantané des séquences incluses dans une commande';
|
||||
|
||||
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
|
||||
)
|
||||
SELECT
|
||||
src.info_clef,
|
||||
src.info_langue,
|
||||
src.info_texte,
|
||||
src.info_aide,
|
||||
'compte.php',
|
||||
'MSIN-4469',
|
||||
0,
|
||||
1,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
NOW()
|
||||
FROM (
|
||||
SELECT 'bib_v5_production_excel_title' info_clef, 'fr' info_langue, 'Fichier Excel pour l’imprimeur' info_texte, 'Crée le fichier officiel associé à l’événement et verrouille les séquences utilisées.' info_aide
|
||||
UNION ALL SELECT 'bib_v5_production_excel_title', 'en', 'Printer Excel file', 'Creates the official event file and locks the ranges used.'
|
||||
UNION ALL SELECT 'bib_v5_production_excel_intro', 'fr', 'Le fichier contient un onglet Imprimeur et un onglet Commande MS1.', ''
|
||||
UNION ALL SELECT 'bib_v5_production_excel_intro', 'en', 'The file contains a Printer sheet and an MS1 Order sheet.', ''
|
||||
UNION ALL SELECT 'bib_v5_production_excel_generate', 'fr', 'Générer et verrouiller', ''
|
||||
UNION ALL SELECT 'bib_v5_production_excel_generate', 'en', 'Generate and lock', ''
|
||||
UNION ALL SELECT 'bib_v5_production_excel_generate_help', 'fr', 'Cette action crée le fichier officiel et verrouille les séquences pour l’impression.', ''
|
||||
UNION ALL SELECT 'bib_v5_production_excel_generate_help', 'en', 'This creates the official file and locks the ranges for printing.', ''
|
||||
UNION ALL SELECT 'bib_v5_production_excel_download', 'fr', 'Télécharger de nouveau', ''
|
||||
UNION ALL SELECT 'bib_v5_production_excel_download', 'en', 'Download again', ''
|
||||
UNION ALL SELECT 'bib_v5_production_excel_locked', 'fr', 'En impression — séquences verrouillées', ''
|
||||
UNION ALL SELECT 'bib_v5_production_excel_locked', 'en', 'In print — ranges locked', ''
|
||||
UNION ALL SELECT 'bib_v5_production_excel_meta', 'fr', 'Version %d créée le %s par %s', ''
|
||||
UNION ALL SELECT 'bib_v5_production_excel_meta', 'en', 'Version %d created on %s by %s', ''
|
||||
UNION ALL SELECT 'bib_v5_production_excel_generating', 'fr', 'Génération en cours — rechargez la page dans quelques instants.', ''
|
||||
UNION ALL SELECT 'bib_v5_production_excel_generating', 'en', 'Generation in progress — reload the page shortly.', ''
|
||||
UNION ALL SELECT 'bib_v5_production_no_ranges', 'fr', 'Aucune séquence de dossards à produire.', ''
|
||||
UNION ALL SELECT 'bib_v5_production_no_ranges', 'en', 'No bib ranges to produce.', ''
|
||||
UNION ALL SELECT 'bib_v5_production_event_invalid', 'fr', 'Événement introuvable.', ''
|
||||
UNION ALL SELECT 'bib_v5_production_event_invalid', 'en', 'Event not found.', ''
|
||||
UNION ALL SELECT 'bib_v5_production_range_invalid', 'fr', 'Une séquence de dossards est invalide.', ''
|
||||
UNION ALL SELECT 'bib_v5_production_range_invalid', 'en', 'A bib range is invalid.', ''
|
||||
UNION ALL SELECT 'bib_v5_production_generation_error', 'fr', 'Impossible de générer le fichier Excel.', ''
|
||||
UNION ALL SELECT 'bib_v5_production_generation_error', 'en', 'Unable to generate the Excel file.', ''
|
||||
UNION ALL SELECT 'bib_v5_production_file_missing', 'fr', 'Le fichier Excel enregistré est introuvable.', ''
|
||||
UNION ALL SELECT 'bib_v5_production_file_missing', 'en', 'The saved Excel file could not be found.', ''
|
||||
UNION ALL SELECT 'bib_v5_production_col_bib', 'fr', 'Numéro de dossard', ''
|
||||
UNION ALL SELECT 'bib_v5_production_col_bib', 'en', 'Bib number', ''
|
||||
UNION ALL SELECT 'bib_v5_production_col_race', 'fr', 'Épreuve', ''
|
||||
UNION ALL SELECT 'bib_v5_production_col_race', 'en', 'Race', ''
|
||||
UNION ALL SELECT 'bib_v5_production_total', 'fr', 'TOTAL', ''
|
||||
UNION ALL SELECT 'bib_v5_production_total', 'en', 'TOTAL', ''
|
||||
UNION ALL SELECT 'bib_v5_production_locked_error', 'fr', 'Les séquences sont verrouillées parce que le fichier est en impression.', ''
|
||||
UNION ALL SELECT 'bib_v5_production_locked_error', 'en', 'The ranges are locked because the file is in print.', ''
|
||||
UNION ALL SELECT 'bib_v5_production_print_lock_col', 'fr', 'Impression', 'Indique que la séquence est figée dans le fichier officiel transmis à l’imprimeur.'
|
||||
UNION ALL SELECT 'bib_v5_production_print_lock_col', 'en', 'Printing', 'Indicates that the range is frozen in the official printer file.'
|
||||
UNION ALL SELECT 'bib_v5_production_print_lock_tip', 'fr', 'Verrouillé pour l’impression : cette séquence ne peut plus être modifiée.', ''
|
||||
UNION ALL SELECT 'bib_v5_production_print_lock_tip', 'en', 'Locked for printing: this range can no longer be modified.', ''
|
||||
UNION ALL SELECT 'bib_v5_production_dev_unlock', 'fr', 'Déverrouiller pour le développement', ''
|
||||
UNION ALL SELECT 'bib_v5_production_dev_unlock', 'en', 'Unlock for development', ''
|
||||
UNION ALL SELECT 'bib_v5_production_dev_unlock_help', 'fr', 'Réactive temporairement la modification de toutes les séquences de cet événement.', ''
|
||||
UNION ALL SELECT 'bib_v5_production_dev_unlock_help', 'en', 'Temporarily enables editing for all ranges in this event.', ''
|
||||
) src
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM info current_info
|
||||
WHERE current_info.info_clef = src.info_clef
|
||||
AND current_info.info_langue = src.info_langue
|
||||
AND current_info.info_prg = 'compte.php'
|
||||
);
|
||||
109
sql/MSIN-4470-corrections-interface-dossards.sql
Normal file
109
sql/MSIN-4470-corrections-interface-dossards.sql
Normal file
@ -0,0 +1,109 @@
|
||||
-- MSIN-4470 — Corrections de l'interface des dossards
|
||||
-- Prérequis : MSIN-4424-bib-allow-inter-epr.sql et MSIN-4467-refonte-visuelle-dossards.sql.
|
||||
-- Idempotent. Exécuter avant le déploiement PHP/CSS/JS.
|
||||
|
||||
UPDATE info
|
||||
SET info_texte = 'Autoriser les mêmes numéros de dossard entre épreuves (doublons)',
|
||||
info_description = 'MSIN-4470'
|
||||
WHERE info_clef = 'bib_v4_allow_inter_epr'
|
||||
AND info_langue = 'fr'
|
||||
AND info_prg = 'compte.php';
|
||||
|
||||
UPDATE info
|
||||
SET info_texte = 'Allow the same bib numbers across races (duplicates)',
|
||||
info_description = 'MSIN-4470'
|
||||
WHERE info_clef = 'bib_v4_allow_inter_epr'
|
||||
AND info_langue = 'en'
|
||||
AND info_prg = 'compte.php';
|
||||
|
||||
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
|
||||
)
|
||||
SELECT
|
||||
src.info_clef,
|
||||
src.info_langue,
|
||||
src.info_texte,
|
||||
'',
|
||||
'compte.php',
|
||||
'MSIN-4470',
|
||||
0,
|
||||
1,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
NOW()
|
||||
FROM (
|
||||
SELECT
|
||||
'bib_v4_allow_inter_epr' AS info_clef,
|
||||
'fr' AS info_langue,
|
||||
'Autoriser les mêmes numéros de dossard entre épreuves (doublons)' AS info_texte
|
||||
UNION ALL
|
||||
SELECT
|
||||
'bib_v4_allow_inter_epr',
|
||||
'en',
|
||||
'Allow the same bib numbers across races (duplicates)'
|
||||
) src
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM info current_info
|
||||
WHERE current_info.info_clef = src.info_clef
|
||||
AND current_info.info_langue = src.info_langue
|
||||
AND current_info.info_prg = 'compte.php'
|
||||
);
|
||||
|
||||
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
|
||||
)
|
||||
SELECT
|
||||
src.info_clef,
|
||||
src.info_langue,
|
||||
src.info_texte,
|
||||
'',
|
||||
'compte.php',
|
||||
'MSIN-4470',
|
||||
0,
|
||||
1,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
NOW()
|
||||
FROM (
|
||||
SELECT
|
||||
'bib_v4_dist_print_page' AS info_clef,
|
||||
'fr' AS info_langue,
|
||||
'Page %s / %s' AS info_texte
|
||||
UNION ALL
|
||||
SELECT
|
||||
'bib_v4_dist_print_page',
|
||||
'en',
|
||||
'Page %s / %s'
|
||||
) src
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM info current_info
|
||||
WHERE current_info.info_clef = src.info_clef
|
||||
AND current_info.info_langue = src.info_langue
|
||||
AND current_info.info_prg = 'compte.php'
|
||||
);
|
||||
44
sql/MSIN-4471-bib-dist-print-doc.sql
Normal file
44
sql/MSIN-4471-bib-dist-print-doc.sql
Normal file
@ -0,0 +1,44 @@
|
||||
-- MSIN-4471 — Pages intro PDF distribution des dossards
|
||||
-- Prérequis : tables doc_mod / doc_page / doc_anchor (MSIN-doc-module.sql)
|
||||
-- Idempotent : DELETE puis INSERT du module bib_dist_print
|
||||
-- Images : images/doc/dossardposition.png (page table participants)
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
DELETE FROM doc_page WHERE doc_mod_clef = 'bib_dist_print';
|
||||
DELETE FROM doc_anchor WHERE doc_anchor_clef = 'bib_v4_dist_print_doc';
|
||||
DELETE FROM doc_mod WHERE doc_mod_clef = 'bib_dist_print';
|
||||
|
||||
INSERT INTO `doc_mod` (`doc_mod_clef`, `doc_mod_prg`, `doc_mod_titre`, `doc_mod_actif`, `doc_mod_tri`, `doc_mod_creation`)
|
||||
VALUES ('bib_dist_print', 'compte.php', 'PDF distribution des dossards', 1, 15, NOW());
|
||||
|
||||
INSERT INTO `doc_anchor` (`doc_anchor_clef`, `doc_mod_clef`, `doc_prg`, `doc_actif`, `doc_creation`)
|
||||
VALUES ('bib_v4_dist_print_doc', 'bib_dist_print', 'compte.php', 1, NOW());
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- FR
|
||||
-- ---------------------------------------------------------------------------
|
||||
INSERT INTO `doc_page` (`doc_mod_clef`, `doc_page_clef`, `doc_langue`, `doc_titre`, `doc_sous_titre`, `doc_html`, `doc_prg`, `doc_tri`, `doc_actif`, `doc_creation`) VALUES
|
||||
('bib_dist_print', 'bib_dist_print_reco', 'fr', 'Conseils à la distribution', 'Pour une remise de dossards sans erreur — Responsable de la distribution des dossards',
|
||||
'<article class="ms1-doc-article"><p><em>À lire avant de commencer.</em> Quelques secondes à chaque remise évitent les échanges, les erreurs de chronométrage et les résultats incorrects.</p><h3>1. Familles et groupes — plusieurs dossards</h3><p>Lorsque vous remettez plusieurs dossards à une même famille ou à un groupe, assurez-vous que le prénom figure sur chaque dossard. S''il n''y est pas déjà, <strong>écrivez-le au verso</strong>. Ainsi, chacun part avec le bon numéro : un échange entre proches fausse le chronométrage et les résultats.</p><h3>2. Ne pas céder son dossard</h3><p>Un dossard inscrit à un nom <strong>ne doit pas</strong> être remis à une autre personne. Si un échange a déjà eu lieu, <strong>avisez immédiatement les organisateurs</strong> afin qu''ils corrigent la base de données avant la course.</p><h3>3. Dossard bien visible à l''avant</h3><p>Rappelez au participant d''épingler son dossard <strong>bien visible à l''avant du torse</strong>. C''est essentiel pour un chronométrage fiable et un bon temps officiel.</p><h3>4. Vérifier ses infos avant la course (code QR)</h3><p>Invitez les participants à scanner le code QR de l''événement pour consulter les résultats en ligne <strong>avant le départ</strong>. Ils pourront confirmer que leur nom, leur âge et leur numéro de dossard sont correctement associés à leur inscription.</p></article>',
|
||||
'compte.php', 10, 1, NOW()),
|
||||
('bib_dist_print', 'bib_dist_print_table', 'fr', 'Votre dossard', 'À placer sur la table — pour les participants',
|
||||
'<article class="ms1-doc-article"><p><em>Vous venez de recevoir votre dossard.</em> Prenez un moment pour vérifier ces points avant le départ.</p><h3>1. Vérifiez vos informations (code QR)</h3><p>Scannez le code QR de l''événement pour consulter les résultats en ligne. Vérifiez que <strong>votre nom est bien orthographié</strong>, que vous êtes dans la <strong>bonne catégorie</strong> et la <strong>bonne distance</strong>. En cas d''erreur, revenez voir l''organisation pour corriger <strong>avant la course</strong>.</p><h3>2. Portez votre dossard correctement</h3><p>Épinglez votre dossard <strong>bien visible à l''avant du torse</strong>, aux quatre coins. Un dossard mal placé ou caché peut compromettre votre chronométrage.</p><img src="/images/doc/dossardposition.png" alt="Position correcte du dossard à l''avant"><h3>3. Courez avec le bon dossard</h3><p>Assurez-vous de courir avec le dossard qui vous a été remis — celui inscrit à votre nom. Un mauvais numéro fausse les résultats.</p><h3>4. Ne cédez pas votre dossard</h3><p>Ne remettez pas votre dossard à une autre personne. Si un échange a déjà eu lieu, <strong>avisez immédiatement les organisateurs</strong> pour qu''ils corrigent la base de données.</p></article>',
|
||||
'compte.php', 20, 1, NOW()),
|
||||
('bib_dist_print', 'bib_dist_print_qr', 'fr', 'Code QR de l''événement', 'À afficher / découper pour l''événement',
|
||||
'<p>Utilisez ce code pour consulter vos résultats en ligne pendant la course.</p>',
|
||||
'compte.php', 30, 1, NOW());
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- EN
|
||||
-- ---------------------------------------------------------------------------
|
||||
INSERT INTO `doc_page` (`doc_mod_clef`, `doc_page_clef`, `doc_langue`, `doc_titre`, `doc_sous_titre`, `doc_html`, `doc_prg`, `doc_tri`, `doc_actif`, `doc_creation`) VALUES
|
||||
('bib_dist_print', 'bib_dist_print_reco', 'en', 'Distribution tips', 'For a smooth, error-free bib handout — Bib distribution lead',
|
||||
'<article class="ms1-doc-article"><p><em>Read before you start.</em> A few seconds at each handout prevent swaps, timing errors, and incorrect results.</p><h3>1. Families and groups — multiple bibs</h3><p>When handing out several bibs to the same family or group, make sure each bib shows a first name. If it is not already there, <strong>write it on the back</strong>. This way everyone leaves with the right number: a swap between relatives will skew timing and results.</p><h3>2. Do not give away your bib</h3><p>A bib registered under one name <strong>must not</strong> be given to someone else. If a swap has already happened, <strong>notify the organizers immediately</strong> so they can correct the database before the race.</p><h3>3. Bib clearly visible on the front</h3><p>Remind the participant to pin the bib <strong>clearly visible on the front of the torso</strong>. This is essential for reliable timing and an accurate official time.</p><h3>4. Check details before the race (QR code)</h3><p>Invite participants to scan the event QR code and view results online <strong>before the start</strong>. They can confirm that their name, age, and bib number are correctly linked to their registration.</p></article>',
|
||||
'compte.php', 10, 1, NOW()),
|
||||
('bib_dist_print', 'bib_dist_print_table', 'en', 'Your bib', 'Place on the table — for participants',
|
||||
'<article class="ms1-doc-article"><p><em>You just received your bib.</em> Take a moment to check these points before the start.</p><h3>1. Check your details (QR code)</h3><p>Scan the event QR code to view results online. Confirm that <strong>your name is spelled correctly</strong>, and that you are in the <strong>right category</strong> and <strong>right distance</strong>. If anything is wrong, see the organizers to correct it <strong>before the race</strong>.</p><h3>2. Wear your bib correctly</h3><p>Pin your bib <strong>clearly visible on the front of your torso</strong>, at all four corners. A hidden or poorly placed bib can compromise your timing.</p><img src="/images/doc/dossardposition.png" alt="Correct bib placement on the front"><h3>3. Run with the correct bib</h3><p>Make sure you run with the bib that was handed to you — the one registered under your name. The wrong number will skew results.</p><h3>4. Do not give away your bib</h3><p>Do not give your bib to someone else. If a swap has already happened, <strong>notify the organizers immediately</strong> so they can correct the database.</p></article>',
|
||||
'compte.php', 20, 1, NOW()),
|
||||
('bib_dist_print', 'bib_dist_print_qr', 'en', 'Event QR code', 'Display / cut out for the event',
|
||||
'<p>Use this code to view your results online during the race.</p>',
|
||||
'compte.php', 30, 1, NOW());
|
||||
32
sql/MSIN-4471-bib-dist-print-layout.sql
Normal file
32
sql/MSIN-4471-bib-dist-print-layout.sql
Normal file
@ -0,0 +1,32 @@
|
||||
-- MSIN-4471 — Libellés feuille bénévole PDF distribution (nom+prénom, case remise)
|
||||
-- Prérequis : clés bib_v4_dist_print_* (MSIN-4433)
|
||||
-- Idempotent : INSERT si absent ; UPDATE si déjà présent avec texte vide
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
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)
|
||||
SELECT 'bib_v4_dist_print_col_nom_prenom', 'fr', 'Nom, Prénom', '', 'compte.php', 'MSIN-4471', 0, 1, '', '', '', NOW()
|
||||
FROM DUAL WHERE NOT EXISTS (
|
||||
SELECT 1 FROM info WHERE info_clef = 'bib_v4_dist_print_col_nom_prenom' AND info_langue = 'fr' AND info_prg = 'compte.php'
|
||||
);
|
||||
|
||||
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)
|
||||
SELECT 'bib_v4_dist_print_col_nom_prenom', 'en', 'Last name, First name', '', 'compte.php', 'MSIN-4471', 0, 1, '', '', '', NOW()
|
||||
FROM DUAL WHERE NOT EXISTS (
|
||||
SELECT 1 FROM info WHERE info_clef = 'bib_v4_dist_print_col_nom_prenom' AND info_langue = 'en' AND info_prg = 'compte.php'
|
||||
);
|
||||
|
||||
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)
|
||||
SELECT 'bib_v4_dist_print_col_check', 'fr', 'OK', '', 'compte.php', 'MSIN-4471', 0, 1, '', '', '', NOW()
|
||||
FROM DUAL WHERE NOT EXISTS (
|
||||
SELECT 1 FROM info WHERE info_clef = 'bib_v4_dist_print_col_check' AND info_langue = 'fr' AND info_prg = 'compte.php'
|
||||
);
|
||||
|
||||
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)
|
||||
SELECT 'bib_v4_dist_print_col_check', 'en', 'OK', '', 'compte.php', 'MSIN-4471', 0, 1, '', '', '', NOW()
|
||||
FROM DUAL WHERE NOT EXISTS (
|
||||
SELECT 1 FROM info WHERE info_clef = 'bib_v4_dist_print_col_check' AND info_langue = 'en' AND info_prg = 'compte.php'
|
||||
);
|
||||
|
||||
UPDATE info SET info_texte = 'OK', info_description = 'MSIN-4471'
|
||||
WHERE info_clef = 'bib_v4_dist_print_col_check' AND info_prg = 'compte.php' AND (info_texte = '' OR info_texte IS NULL);
|
||||
12
sql/MSIN-4478-eve-acces-invite-pending-reinvite.sql
Normal file
12
sql/MSIN-4478-eve-acces-invite-pending-reinvite.sql
Normal file
@ -0,0 +1,12 @@
|
||||
-- MSIN-4478 — Réinvitation stub non activé (libellé Info)
|
||||
-- Contexte : invite / réinvite promoteur — mail « créez votre mot de passe » si compte jamais activé
|
||||
-- Notes : exécution UNIQUEMENT sur dev préprod ; autres env = Navicat structure + sync_static_db
|
||||
-- Idempotent : DELETE + INSERT de la clé ajoutée
|
||||
|
||||
DELETE FROM info
|
||||
WHERE info_clef = 'eve_acces_promoteur_ok_pending'
|
||||
AND info_prg = 'compte.php';
|
||||
|
||||
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
|
||||
('eve_acces_promoteur_ok_pending', 'fr', 'Invitation mise à jour. Un courriel avec le lien de mot de passe a été envoyé.', '', 'compte.php', '', 0, 1, '', '', '', NOW()),
|
||||
('eve_acces_promoteur_ok_pending', 'en', 'Invitation updated. An email with the password link was sent.', '', 'compte.php', '', 0, 1, '', '', '', NOW());
|
||||
9
sql/MSIN-4478-eve-acces-kit-total-pas-delegable.sql
Normal file
9
sql/MSIN-4478-eve-acces-kit-total-pas-delegable.sql
Normal file
@ -0,0 +1,9 @@
|
||||
-- MSIN-4478 — Sécurité : kit total jamais délégable
|
||||
-- Contexte : un kit role_grants_all=1 + role_delegable=1 donnait tous les droits (rapports, argent…) à un invité
|
||||
-- Notes : exécution UNIQUEMENT sur dev préprod ; autres env = Navicat structure + sync_static_db
|
||||
-- Idempotent
|
||||
|
||||
UPDATE inscriptions_eve_roles
|
||||
SET role_delegable = 0
|
||||
WHERE role_grants_all = 1
|
||||
AND role_delegable = 1;
|
||||
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());
|
||||
@ -6,6 +6,10 @@ require_once dirname(__FILE__) . '/php/inc_functions.php';
|
||||
require_once dirname(__FILE__) . '/../php/chronotrack_api/inc_bootstrap.php';
|
||||
|
||||
$intUsaId = fxSuperadmAjaxAuthOrJsonExit();
|
||||
// MSIN-4328 — libérer le verrou session : sinon un push long bloque Actualiser / preview (Erreur réseau).
|
||||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||
session_write_close();
|
||||
}
|
||||
|
||||
$strAction = trim((string)($_REQUEST['a'] ?? ''));
|
||||
$intEveId = intval($_REQUEST['eve_id'] ?? 0);
|
||||
@ -91,7 +95,7 @@ switch ($strAction) {
|
||||
$tabRetour = array('state' => 'error', 'message' => 'eve_id manquant');
|
||||
break;
|
||||
}
|
||||
set_time_limit(60);
|
||||
set_time_limit(120);
|
||||
$tabRetour = fxChronotrackApiSyncBuildPreview($intEveId);
|
||||
break;
|
||||
|
||||
@ -171,7 +175,7 @@ switch ($strAction) {
|
||||
break;
|
||||
|
||||
case 'sync_probe_custom':
|
||||
// MSIN-4328 — sonde création champ custom (comme import Custom)
|
||||
// MSIN-4461 — sonde entry_note_test (notes custom CT)
|
||||
if ($intEveId <= 0) {
|
||||
$tabRetour = array('state' => 'error', 'message' => 'eve_id manquant');
|
||||
break;
|
||||
@ -186,7 +190,12 @@ switch ($strAction) {
|
||||
break;
|
||||
}
|
||||
set_time_limit(180);
|
||||
$tabRetour = fxChronotrackApiSyncPushPrepare($intEveId);
|
||||
$blnForceFull = (intval($_REQUEST['force_full'] ?? 0) === 1);
|
||||
$blnVerboseLogs = (intval($_REQUEST['verbose_logs'] ?? 0) === 1);
|
||||
$tabRetour = fxChronotrackApiSyncPushPrepare($intEveId, array(
|
||||
'force_full' => $blnForceFull,
|
||||
'verbose_logs' => $blnVerboseLogs,
|
||||
));
|
||||
break;
|
||||
|
||||
case 'sync_push':
|
||||
@ -200,10 +209,17 @@ switch ($strAction) {
|
||||
if ($intLimit <= 0) {
|
||||
$intLimit = MSIN_API_CHRONOTRACK_SYNC_CHUNK_SIZE;
|
||||
}
|
||||
$tabRetour = fxChronotrackApiSyncPushEvent($intEveId, array(
|
||||
'offset' => $intOffset,
|
||||
'limit' => $intLimit,
|
||||
));
|
||||
try {
|
||||
$tabRetour = fxChronotrackApiSyncPushEvent($intEveId, array(
|
||||
'offset' => $intOffset,
|
||||
'limit' => $intLimit,
|
||||
));
|
||||
} catch (Throwable $e) {
|
||||
$tabRetour = array(
|
||||
'state' => 'error',
|
||||
'message' => 'Exception push : ' . $e->getMessage(),
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'sync_orphans':
|
||||
@ -219,4 +235,11 @@ switch ($strAction) {
|
||||
break;
|
||||
}
|
||||
|
||||
echo json_encode($tabRetour);
|
||||
if (!headers_sent()) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
}
|
||||
$strJsonOut = json_encode($tabRetour, JSON_UNESCAPED_UNICODE);
|
||||
if ($strJsonOut === false) {
|
||||
$strJsonOut = '{"state":"error","message":"json_encode failed"}';
|
||||
}
|
||||
echo $strJsonOut;
|
||||
|
||||
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;
|
||||
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
/**
|
||||
* MSIN API ChronoTrack — page superadmin (config par eve_id + dashboard).
|
||||
* MSIN API ChronoTrack — page superadmin (liste + opération course).
|
||||
* MSIN-4328 — focus=1 : fenêtre opération plein écran (sans menu gauche).
|
||||
*/
|
||||
$strLangue = 'fr';
|
||||
$strPage = 'chronotrack_api.php';
|
||||
@ -16,12 +17,13 @@ if (empty($_SESSION['usa_id']) || empty($_SESSION['usa_info'])) {
|
||||
|
||||
$intEveId = isset($_GET['eve_id']) ? intval($_GET['eve_id']) : 0;
|
||||
$blnDashboard = ($intEveId <= 0);
|
||||
$blnFocus = (!$blnDashboard && !empty($_GET['focus']));
|
||||
|
||||
include('inc_header.php');
|
||||
?>
|
||||
<div class="container-fluid" id="main">
|
||||
<div class="container-fluid<?php echo $blnFocus ? ' ct-api-focus-page' : ''; ?>" id="main">
|
||||
<div class="row">
|
||||
<div class="col-md-9 col-lg-10 order-first order-md-last py-3">
|
||||
<div class="<?php echo $blnFocus ? 'col-12' : 'col-md-9 col-lg-10 order-first order-md-last'; ?> py-3">
|
||||
<?php
|
||||
require_once dirname(__FILE__) . '/../php/inc_fx_superadm_v2_ui.php';
|
||||
if ($blnDashboard) {
|
||||
@ -30,16 +32,20 @@ include('inc_header.php');
|
||||
));
|
||||
fxChronotrackApiAdminRenderDashboard($strLangue, true);
|
||||
} else {
|
||||
fxSuperadmV2PanelOpen('ChronoTrack — Configuration événement', array(
|
||||
fxSuperadmV2PanelOpen($blnFocus ? 'ChronoTrack — Tableau de bord course' : 'ChronoTrack — Course', array(
|
||||
'zone_class' => 'superadm-content-v2-zone superadm-content-v2-zone--flush',
|
||||
));
|
||||
fxChronotrackApiAdminRenderEventPage($intEveId, $strLangue, false, true);
|
||||
fxChronotrackApiAdminRenderEventPage($intEveId, $strLangue, false, true, $blnFocus);
|
||||
}
|
||||
fxSuperadmV2PanelClose();
|
||||
?>
|
||||
<p> </p>
|
||||
</div>
|
||||
<?php include('inc_droite.php'); ?>
|
||||
<?php
|
||||
if (!$blnFocus) {
|
||||
include('inc_droite.php');
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<?php include('inc_footer.php'); ?>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
44
superadm/eve_resultats_qr.php
Normal file
44
superadm/eve_resultats_qr.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/**
|
||||
* MSIN-3487 — PNG QR résultats événement (logo MS1).
|
||||
* Endpoint léger (sans inc_manager) pour téléchargement direct.
|
||||
*
|
||||
* Affichage / DL : eve_resultats_qr.php?eve_id=760[&dl=1]
|
||||
*/
|
||||
session_start();
|
||||
|
||||
if (empty($_SESSION['usa_id'])) {
|
||||
http_response_code(403);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo 'Accès refusé.';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Settings publics pour $vDomaine uniquement (pas tout le stack superadm).
|
||||
require_once dirname(__FILE__) . '/../php/inc_settings.php';
|
||||
require_once dirname(__FILE__) . '/../php/inc_fx_eve_extra.php';
|
||||
|
||||
$intEveId = isset($_GET['eve_id']) ? intval($_GET['eve_id']) : 0;
|
||||
$blnDownload = !empty($_GET['dl']);
|
||||
|
||||
$binPng = fxKcEveResultatsQrPngBinary($intEveId, null, 10, true);
|
||||
if ($binPng === false) {
|
||||
http_response_code(400);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo 'QR impossible à générer.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$strFilename = 'qr_resultats_eve_' . $intEveId . '.png';
|
||||
|
||||
header('Content-Type: image/png');
|
||||
header('Content-Length: ' . strlen($binPng));
|
||||
header('Cache-Control: private, max-age=60');
|
||||
if ($blnDownload) {
|
||||
header('Content-Disposition: attachment; filename="' . $strFilename . '"');
|
||||
} else {
|
||||
header('Content-Disposition: inline; filename="' . $strFilename . '"');
|
||||
}
|
||||
|
||||
echo $binPng;
|
||||
exit;
|
||||
@ -90,7 +90,9 @@ if (isset($_SESSION['com_info']['com_id'])) {
|
||||
strpath = '/book/';
|
||||
}
|
||||
|
||||
document.location.href = '<?php echo $vDomaine; ?>' + strpath + code + '/' + intEprId + '?promoteur=1&action=mod&pec_id=' + intPecId;
|
||||
document.location.href = '<?php echo $vDomaine; ?>' + strpath + code + '/' + intEprId
|
||||
+ '?promoteur=1&action=mod&pec_id=' + intPecId
|
||||
+ '&return_url=' + encodeURIComponent(window.location.href);
|
||||
});
|
||||
|
||||
$('.btn_update_panier').click(function () {
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
// Require avant redirect : fxSuperadmConsumeRedirectAfterLogin est dans inc_functions.php.
|
||||
require_once('php/inc_functions.php');
|
||||
require_once('php/inc_fx_messages.php');
|
||||
global $db, $blnDev, $vDomaine;
|
||||
|
||||
if (!empty($_SESSION['usa_id'])) {
|
||||
header('Location: ' . fxSuperadmConsumeRedirectAfterLogin('index.php'));
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once('php/inc_functions.php');
|
||||
require_once('php/inc_fx_messages.php');
|
||||
global $db, $blnDev, $vDomaine;
|
||||
|
||||
if (isset($_POST['usa_login'])) {
|
||||
$sqlLogin = "SELECT * FROM inscriptions_comptes WHERE com_login = '" . $db->fxEscape($_POST['usa_login']) . "' AND com_actif = 1 AND com_superadm = 1";
|
||||
$recLogin = $db->fxGetRow($sqlLogin);
|
||||
|
||||
@ -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,13 @@ 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>
|
||||
<a class="btn btn-secondary" href="#list_rapports">
|
||||
Rapports
|
||||
</a>
|
||||
@ -1121,7 +1132,84 @@ if ($strAction == 'mod') {
|
||||
}
|
||||
|
||||
if ($blnIsEventModForm) {
|
||||
// MSIN-3487 — UI temporaire : lien résultats (table inscriptions_evenements_extra)
|
||||
require_once dirname(__FILE__) . '/../../php/inc_fx_eve_extra.php';
|
||||
|
||||
$strEveExtraFlash = '';
|
||||
if (isset($_POST['btn_eve_extra_save'])) {
|
||||
$strUrlPosted = isset($_POST['eve_resultats_url']) ? trim((string) $_POST['eve_resultats_url']) : '';
|
||||
if (fxEveExtraSetResultatsUrl($intEveIdForm, $strUrlPosted)) {
|
||||
$strEveExtraFlash = ($strUrlPosted === '')
|
||||
? 'Lien résultats effacé (fallback ms1.ca).'
|
||||
: 'Lien résultats enregistré.';
|
||||
} else {
|
||||
$strEveExtraFlash = 'Erreur à l\'enregistrement du lien résultats.';
|
||||
}
|
||||
}
|
||||
|
||||
$strEveResultatsUrl = (string) (fxEveExtraGetResultatsUrl($intEveIdForm) ?? '');
|
||||
$strEveQrPreview = fxKcEveResultatsQrUrl($intEveIdForm);
|
||||
// $intMenuActuel : figé en tête de fiche (évite le $tabResults écrasé par les sous-listes)
|
||||
$strEveExtraFormAction = $strhomeclient
|
||||
. '?t=' . urlencode(base64_encode($intMenuActuel))
|
||||
. '&a=mod&id=' . intval($intEveIdForm);
|
||||
?>
|
||||
<h1 id="list_eve_extra">Champs supplémentaires</h1>
|
||||
<p class="text-muted small">MSIN-3487 — temporaire (superadm). Lien public des résultats de course.</p>
|
||||
<?php if ($strEveExtraFlash !== '') { ?>
|
||||
<div class="alert alert-info py-2"><?php echo htmlspecialchars($strEveExtraFlash, ENT_QUOTES, 'UTF-8'); ?></div>
|
||||
<?php } ?>
|
||||
<form action="<?php echo htmlspecialchars($strEveExtraFormAction, ENT_QUOTES, 'UTF-8'); ?>"
|
||||
method="post" class="mb-4" id="frm_eve_extra" name="frm_eve_extra">
|
||||
<div class="form-group row">
|
||||
<label class="col-form-label col-sm-3" for="eve_resultats_url">Lien résultats</label>
|
||||
<div class="col-sm-9 col-lg-7">
|
||||
<input type="text" class="form-control rounded-0" id="eve_resultats_url" name="eve_resultats_url"
|
||||
value="<?php echo htmlspecialchars($strEveResultatsUrl, ENT_QUOTES, 'UTF-8'); ?>"
|
||||
placeholder="https://…" autocomplete="off">
|
||||
<small class="form-text text-muted">
|
||||
QR court :
|
||||
<code><?php echo htmlspecialchars($strEveQrPreview, ENT_QUOTES, 'UTF-8'); ?></code>
|
||||
— si vide → <code>https://ms1.ca</code>
|
||||
</small>
|
||||
<?php if ($intEveIdForm > 0) {
|
||||
// MSIN-3487 — génération inline (évite 403/404 sur l'endpoint séparé)
|
||||
$binEveQrPng = fxKcEveResultatsQrPngBinary($intEveIdForm, null, 8, true);
|
||||
if ($binEveQrPng !== false) {
|
||||
$strEveQrDataUri = 'data:image/png;base64,' . base64_encode($binEveQrPng);
|
||||
$strEveQrFilename = 'qr_resultats_eve_' . intval($intEveIdForm) . '.png';
|
||||
?>
|
||||
<div class="mt-3">
|
||||
<div class="mb-2">
|
||||
<img src="<?php echo htmlspecialchars($strEveQrDataUri, ENT_QUOTES, 'UTF-8'); ?>"
|
||||
alt="QR résultats événement <?php echo intval($intEveIdForm); ?>"
|
||||
width="220" height="220"
|
||||
class="border bg-white p-1">
|
||||
</div>
|
||||
<a class="btn btn-outline-secondary btn-sm rounded-0"
|
||||
href="<?php echo htmlspecialchars($strEveQrDataUri, ENT_QUOTES, 'UTF-8'); ?>"
|
||||
download="<?php echo htmlspecialchars($strEveQrFilename, ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<i class="fa fa-download" aria-hidden="true"></i> Télécharger le QR (PNG)
|
||||
</a>
|
||||
</div>
|
||||
<?php
|
||||
} else {
|
||||
?>
|
||||
<div class="mt-3 text-danger small">Impossible de générer le QR (GD / phpqrcode).</div>
|
||||
<?php
|
||||
}
|
||||
} ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-9 col-lg-7 offset-sm-3">
|
||||
<button type="submit" class="btn btn-success rounded-0" name="btn_eve_extra_save" value="1">
|
||||
<i class="fa fa-check" aria-hidden="true"></i> Enregistrer le lien résultats
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<h1 id="list_rapports">Rapport de l'événement</h1>
|
||||
<ul class="nav nav-pills flex-column">
|
||||
<?php
|
||||
|
||||
@ -510,9 +510,9 @@ function fxEveAccesAdminSaveRole($arrData, $arrPermKeys)
|
||||
if ($blnCodeLocked) {
|
||||
$blnGrantsAll = 1;
|
||||
}
|
||||
// MSIN-4460 — Kit total n'interdit plus la delegation : c'est le super admin qui decide.
|
||||
// Kits systeme uniquement : jamais redistribuables par un promoteur.
|
||||
if ($blnCodeLocked || in_array($strCode, array('owner', 'qr_debug'), true)) {
|
||||
// MSIN-4478 — kit total + kits systeme : jamais redistribuables par un promoteur
|
||||
// (évite qu'une invitation donne rapports / argent / tout).
|
||||
if ($blnCodeLocked || $blnGrantsAll || in_array($strCode, array('owner', 'qr_debug'), true)) {
|
||||
$blnDelegable = 0;
|
||||
}
|
||||
|
||||
@ -961,7 +961,7 @@ function fxEveAccesAdminRenderKitForm($intRoleId, $strError = '')
|
||||
Délégable par le promoteur (invitation équipe)
|
||||
</label>
|
||||
</div>
|
||||
<small class="text-muted">Le promoteur pourra assigner ce kit à un membre invité. Indépendant de « Kit total » — c’est vous qui choisissez.</small>
|
||||
<small class="text-muted">Le promoteur pourra assigner ce kit à un membre invité. Impossible si « Kit total » est coché (MSIN-4478).</small>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<?php if (fxEveAccesAdminHasGrantsAllColumn()) { ?>
|
||||
@ -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') ?>"
|
||||
@ -1101,14 +1111,23 @@ function fxEveAccesAdminRenderKitForm($intRoleId, $strError = '')
|
||||
if (blnAll) {
|
||||
$('.perm-checkbox').prop('checked', false);
|
||||
}
|
||||
// MSIN-4460 — Ne plus forcer / desactiver « delegable » quand Kit total est coche.
|
||||
// MSIN-4478 — Kit total = jamais délégable (invitation ne doit pas donner tout).
|
||||
var $deleg = $('#role_delegable');
|
||||
if ($deleg.length && !$deleg.prop('disabled')) {
|
||||
if (blnAll) {
|
||||
$deleg.prop('checked', false).prop('disabled', true);
|
||||
} else {
|
||||
$deleg.prop('disabled', false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$('#role_grants_all').on('change', syncGrantsAllUi);
|
||||
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);
|
||||
@ -1116,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