Files
ms1inscription-v5/php/inc_fx_bib_production.php

1741 lines
61 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

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

<?php
/**
* MSIN-4469 — Production Excel des dossards.
* MSIN-4515 — Colonnes Face (bleu) / Endos (rouge) / Info (blanc) par épreuve.
*
* 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.
*/
/** MSIN-4515 — Côtés Excel imprimeur. */
function fxBibProductionFieldSides() {
return ['face', 'endos', 'info'];
}
/**
* MSIN-4515 — Champs toujours proposés (pas des infos événement, pas des questions).
*
* @return array<string, array{label_key:string}>
*/
function fxBibProductionCoreFieldCatalog() {
return [
'no_bib' => ['label_key' => 'bib_v5_production_field_no_bib'],
'epr_nom' => ['label_key' => 'bib_v5_production_field_epr_nom'],
'par_prenom_nom' => ['label_key' => 'bib_v5_production_field_par_prenom_nom'],
];
}
/**
* MSIN-4515 — Ancien nom conservé pour compat ; préférer fxBibProductionCollectSelectableFields.
*
* @return array<string, array{label_key:string}>
*/
function fxBibProductionStandardFieldCatalog() {
return fxBibProductionCoreFieldCatalog();
}
/**
* MSIN-4515 — Infos participant actives de lévénement (catalogue configurable).
*
* @return array<int, array<string,mixed>>
*/
function fxBibProductionGetEventInfoFields($intEveId) {
$intEveId = (int)$intEveId;
if ($intEveId <= 0 || !function_exists('fxGetInfosParticipants')) {
return [];
}
$tabInfos = fxGetInfosParticipants($intEveId);
if (!is_array($tabInfos)) {
return [];
}
$tabOut = [];
$tabSkip = ['pro_id' => true, 'pay_id' => true];
foreach ($tabInfos as $tabInfo) {
if (!is_array($tabInfo)) {
continue;
}
$strCha = trim((string)($tabInfo['cha_nom'] ?? ''));
if ($strCha === '' || isset($tabSkip[$strCha])) {
continue;
}
$tabOut[] = $tabInfo;
}
return $tabOut;
}
/**
* MSIN-4515 — Une seule liste sélectionnable pour une épreuve.
* Cœur (n° / épreuve) + infos participant activées sur lévénement + questions.
*
* @return array<int, array{key:string,label:string}>
*/
function fxBibProductionCollectSelectableFields($intEveId, $intEprId, $strLangue = 'fr') {
$intEveId = (int)$intEveId;
$intEprId = (int)$intEprId;
$strLangue = ($strLangue === 'en') ? 'en' : 'fr';
$tabOut = [];
$tabSeen = [];
$fnAdd = function ($strKey, $strLabel) use (&$tabOut, &$tabSeen) {
$strKey = trim((string)$strKey);
$strLabel = trim((string)$strLabel);
if ($strKey === '' || isset($tabSeen[$strKey])) {
return;
}
$tabSeen[$strKey] = true;
$tabOut[] = [
'key' => $strKey,
'label' => $strLabel !== '' ? $strLabel : $strKey,
];
};
// 1) Toujours : n° dossard + épreuve
$fnAdd('no_bib', fxBibTexte('bib_v5_production_field_no_bib', 0));
$fnAdd('epr_nom', fxBibTexte('bib_v5_production_field_epr_nom', 0));
// 2) Infos participant cochées pour cet événement (urgence, prénom, etc.)
$blnHasPrenom = false;
$blnHasNom = false;
foreach (fxBibProductionGetEventInfoFields($intEveId) as $tabInfo) {
$strCha = trim((string)($tabInfo['cha_nom'] ?? ''));
if ($strCha === '') {
continue;
}
if ($strCha === 'par_prenom') {
$blnHasPrenom = true;
}
if ($strCha === 'par_nom') {
$blnHasNom = true;
}
$strLabel = trim((string)($tabInfo['cha_label_' . $strLangue] ?? ''));
if ($strLabel === '') {
$strLabel = trim((string)($tabInfo['cha_label_fr'] ?? ''));
}
if ($strLabel === '') {
$strLabel = $strCha;
}
$fnAdd($strCha, $strLabel);
}
// Prénom + nom seulement si les deux infos sont activées (évite le doublon confus).
if ($blnHasPrenom && $blnHasNom) {
$fnAdd('par_prenom_nom', fxBibTexte('bib_v5_production_field_par_prenom_nom', 0));
}
// 3) Questions de lépreuve (clés que:id — pas de collision avec par_*)
if ($intEprId > 0 && function_exists('fxBibGetQuestionSortOptions')) {
foreach (fxBibGetQuestionSortOptions($intEprId, $intEveId, $strLangue) as $tabOpt) {
$fnAdd((string)($tabOpt['key'] ?? ''), (string)($tabOpt['label'] ?? ''));
}
}
return $tabOut;
}
/** MSIN-4515 — Défaut = seulement le n° dossard en Face. */
function fxBibProductionDefaultFields() {
return [
['key' => 'no_bib', 'side' => 'face'],
];
}
/**
* MSIN-4515 — Parse JSON ba_bib_prod_fields.
*
* @param mixed $mixJson
* @return array<int, array{key:string,side:string}>
*/
function fxBibProductionParseFieldsJson($mixJson) {
if (is_array($mixJson)) {
$tabData = $mixJson;
} else {
$strJson = trim((string)$mixJson);
if ($strJson === '') {
return fxBibProductionDefaultFields();
}
$tabData = json_decode($strJson, true);
if (!is_array($tabData)) {
return fxBibProductionDefaultFields();
}
}
$tabSides = fxBibProductionFieldSides();
$tabOut = [];
$tabSeen = [];
foreach ($tabData as $tabItem) {
if (!is_array($tabItem)) {
continue;
}
$strKey = trim((string)($tabItem['key'] ?? ''));
$strSide = strtolower(trim((string)($tabItem['side'] ?? '')));
if ($strKey === '' || !in_array($strSide, $tabSides, true)) {
continue;
}
if (isset($tabSeen[$strKey])) {
continue;
}
$tabSeen[$strKey] = true;
$tabOut[] = ['key' => $strKey, 'side' => $strSide];
}
return !empty($tabOut) ? $tabOut : fxBibProductionDefaultFields();
}
/** MSIN-4515 — Encode pour BD. */
function fxBibProductionFieldsToJson(array $tabFields) {
$str = json_encode(array_values($tabFields), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
return $str === false ? '[]' : $str;
}
/**
* MSIN-4515 — Clé valide pour une épreuve (liste unifiée).
*
* @param array<int, array{key:string,label?:string}>|null $tabSelectable
*/
function fxBibProductionFieldKeyAllowed($strKey, $tabSelectable = null, $intEveId = 0, $intEprId = 0, $strLangue = 'fr') {
$strKey = trim((string)$strKey);
if ($strKey === '') {
return false;
}
if (!is_array($tabSelectable)) {
$tabSelectable = fxBibProductionCollectSelectableFields($intEveId, $intEprId, $strLangue);
}
foreach ($tabSelectable as $tabOpt) {
if ((string)($tabOpt['key'] ?? '') === $strKey) {
return true;
}
}
return false;
}
/**
* MSIN-4515 — Libellé colonne Excel.
*
* @param array<int, array{key:string,label?:string}>|null $tabSelectable
*/
function fxBibProductionFieldLabel($strKey, $tabSelectable = null, $strLangue = 'fr', $intEveId = 0, $intEprId = 0) {
$strKey = (string)$strKey;
if (!is_array($tabSelectable)) {
$tabSelectable = fxBibProductionCollectSelectableFields($intEveId, $intEprId, $strLangue);
}
foreach ($tabSelectable as $tabOpt) {
if ((string)($tabOpt['key'] ?? '') === $strKey) {
$strLabel = trim((string)($tabOpt['label'] ?? ''));
return $strLabel !== '' ? $strLabel : $strKey;
}
}
$tabCore = fxBibProductionCoreFieldCatalog();
if (isset($tabCore[$strKey])) {
$strLabel = fxBibTexte($tabCore[$strKey]['label_key'], 0);
return $strLabel !== '' ? $strLabel : $strKey;
}
return $strKey;
}
/**
* MSIN-4515 — Parse POST prod_fields[epr_id][key] = side|''.
*
* @return array<int, array<int, array{key:string,side:string}>>
*/
function fxBibProductionParseFieldsFromPost(array $tabPost) {
$tabRaw = $tabPost['prod_fields'] ?? [];
if (!is_array($tabRaw)) {
return [];
}
$tabSides = fxBibProductionFieldSides();
$tabOut = [];
foreach ($tabRaw as $mixEprId => $tabFields) {
$intEprId = (int)$mixEprId;
if ($intEprId <= 0 || !is_array($tabFields)) {
continue;
}
$tabParsed = [];
$tabSeen = [];
foreach ($tabFields as $strKey => $strSide) {
$strKey = trim((string)$strKey);
$strSide = strtolower(trim((string)$strSide));
if ($strKey === '' || $strSide === '' || $strSide === 'none') {
continue;
}
if (!in_array($strSide, $tabSides, true) || isset($tabSeen[$strKey])) {
continue;
}
$tabSeen[$strKey] = true;
$tabParsed[] = ['key' => $strKey, 'side' => $strSide];
}
$tabOut[$intEprId] = $tabParsed;
}
return $tabOut;
}
/**
* MSIN-4515 — Enregistre la config champs par épreuve.
*
* @param array<int, array<int, array{key:string,side:string}>> $tabSelections
* @return array{success:bool,message?:string}
*/
function fxBibProductionSaveFields($intEveId, array $tabSelections, $strLangue = 'fr') {
global $objDatabase;
$intEveId = (int)$intEveId;
if ($intEveId <= 0) {
return ['success' => false, 'message' => fxBibMsg('bib_v4_ajax_epr_invalid')];
}
if (fxBibProductionEventIsLocked($intEveId)) {
return ['success' => false, 'message' => fxBibMsg('bib_v5_production_locked_error')];
}
$tabEpreuves = $objDatabase->fxGetResults(
"SELECT epr_id, ba_bib_prod_fields
FROM inscriptions_epreuves
WHERE eve_id = $intEveId
AND epr_actif = 1"
);
if (!is_array($tabEpreuves)) {
$tabEpreuves = [];
}
foreach ($tabEpreuves as $tabEpr) {
$intEprId = (int)($tabEpr['epr_id'] ?? 0);
if ($intEprId <= 0) {
continue;
}
$tabSelectable = fxBibProductionCollectSelectableFields($intEveId, $intEprId, $strLangue);
$tabChosen = $tabSelections[$intEprId] ?? null;
if ($tabChosen === null) {
// POST partiel : ne pas écraser les autres épreuves.
continue;
}
$tabFiltered = [];
$tabSeen = [];
foreach ($tabChosen as $tabItem) {
$strKey = trim((string)($tabItem['key'] ?? ''));
$strSide = strtolower(trim((string)($tabItem['side'] ?? '')));
if ($strKey === '' || isset($tabSeen[$strKey])) {
continue;
}
if (!in_array($strSide, fxBibProductionFieldSides(), true)) {
continue;
}
if (!fxBibProductionFieldKeyAllowed($strKey, $tabSelectable)) {
continue;
}
$tabSeen[$strKey] = true;
$tabFiltered[] = ['key' => $strKey, 'side' => $strSide];
}
if (empty($tabFiltered)) {
$tabFiltered = fxBibProductionDefaultFields();
} else {
// MSIN-4515 — Toujours garder le n° dossard (sinon fichier imprimeur inutilisable).
$blnHasBib = false;
foreach ($tabFiltered as $tabItem) {
if (($tabItem['key'] ?? '') === 'no_bib') {
$blnHasBib = true;
break;
}
}
if (!$blnHasBib) {
array_unshift($tabFiltered, ['key' => 'no_bib', 'side' => 'face']);
}
}
$strJson = fxBibProductionFieldsToJson($tabFiltered);
$objDatabase->fxQuery(
"UPDATE inscriptions_epreuves
SET ba_bib_prod_fields = '" . $objDatabase->fxEscape($strJson) . "'
WHERE epr_id = $intEprId
AND eve_id = $intEveId
LIMIT 1"
);
}
return ['success' => true, 'message' => fxBibMsg('bib_v5_production_fields_saved')];
}
/**
* MSIN-4515 — Lettre de colonne Excel (0 = A).
*/
function fxBibProductionColumnLetter($intIndex) {
$intIndex = (int)$intIndex;
if ($intIndex < 0) {
$intIndex = 0;
}
$str = '';
$intN = $intIndex;
do {
$str = chr(65 + ($intN % 26)) . $str;
$intN = intdiv($intN, 26) - 1;
} while ($intN >= 0);
return $str;
}
/**
* MSIN-4515 — Union ordonnée des colonnes (face → endos → info).
*
* @param array<int, array<string,mixed>> $tabRaces
* @return array<int, array{key:string,side:string,label:string}>
*/
function fxBibProductionBuildColumnPlan(array $tabRaces) {
$tabSideRank = ['face' => 1, 'endos' => 2, 'info' => 3];
$tabByKey = [];
foreach ($tabRaces as $tabRace) {
$tabFields = isset($tabRace['fields']) && is_array($tabRace['fields'])
? $tabRace['fields']
: fxBibProductionDefaultFields();
$tabLabels = isset($tabRace['field_labels']) && is_array($tabRace['field_labels'])
? $tabRace['field_labels']
: [];
foreach ($tabFields as $tabField) {
$strKey = (string)($tabField['key'] ?? '');
$strSide = (string)($tabField['side'] ?? 'info');
if ($strKey === '') {
continue;
}
$strLabel = (string)($tabLabels[$strKey] ?? $strKey);
if (!isset($tabByKey[$strKey])) {
$tabByKey[$strKey] = [
'key' => $strKey,
'side' => $strSide,
'label' => $strLabel,
];
continue;
}
// Conflit de côté entre épreuves : privilégier face > endos > info.
$intOld = $tabSideRank[$tabByKey[$strKey]['side']] ?? 99;
$intNew = $tabSideRank[$strSide] ?? 99;
if ($intNew < $intOld) {
$tabByKey[$strKey]['side'] = $strSide;
}
if ($tabByKey[$strKey]['label'] === $strKey && $strLabel !== '') {
$tabByKey[$strKey]['label'] = $strLabel;
}
}
}
if (empty($tabByKey)) {
return [
['key' => 'no_bib', 'side' => 'face', 'label' => fxBibTexte('bib_v5_production_field_no_bib', 0)],
['key' => 'epr_nom', 'side' => 'info', 'label' => fxBibTexte('bib_v5_production_field_epr_nom', 0)],
];
}
$tabOut = array_values($tabByKey);
usort($tabOut, function ($a, $b) use ($tabSideRank) {
$intSa = $tabSideRank[$a['side']] ?? 99;
$intSb = $tabSideRank[$b['side']] ?? 99;
if ($intSa !== $intSb) {
return $intSa - $intSb;
}
return strcmp($a['key'], $b['key']);
});
return $tabOut;
}
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 lorsquaucun coureur ni aucun dossard assigné ne sy 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)) {
// MSIN-4515 — Champs Face/Endos/Info + libellés pour longlet Imprimeur.
$tabSelectable = fxBibProductionCollectSelectableFields($intEveId, $intEprId, $strLangue);
$tabFields = fxBibProductionParseFieldsJson($tabEpreuve['ba_bib_prod_fields'] ?? '');
// Ne garder que les clés encore sélectionnables (infos eve / questions à jour).
$tabFieldsFiltered = [];
foreach ($tabFields as $tabField) {
$strKey = (string)($tabField['key'] ?? '');
if (fxBibProductionFieldKeyAllowed($strKey, $tabSelectable)) {
$tabFieldsFiltered[] = $tabField;
}
}
if (empty($tabFieldsFiltered)) {
$tabFieldsFiltered = fxBibProductionDefaultFields();
}
$tabFieldLabels = [];
foreach ($tabFieldsFiltered as $tabField) {
$strKey = (string)($tabField['key'] ?? '');
if ($strKey === '') {
continue;
}
$tabFieldLabels[$strKey] = fxBibProductionFieldLabel(
$strKey,
$tabSelectable,
$strLangue,
$intEveId,
$intEprId
);
}
$tabRaces[] = [
'epr_id' => $intEprId,
// Le libellé type/catégorie est le nom court retenu pour la V1.
'label' => fxBibEpreuveDisplayName($tabEpreuve, $strLangue),
'ranges' => $tabRanges,
'fields' => $tabFieldsFiltered,
'field_labels' => $tabFieldLabels,
];
}
}
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
);
}
/**
* MSIN-4515 — Injecte styles Face (bleu) / Endos (rouge) / Info (gris) dans styles.xml.
*
* @return array{face:array{h:int,d:int},endos:array{h:int,d:int},info:array{h:int,d:int}}
*/
function fxBibProductionEnsureSideStyles(ZipArchive $objZip) {
$strStyles = $objZip->getFromName('xl/styles.xml');
if ($strStyles === false) {
throw new Exception('MSIN-4515 styles.xml missing');
}
if (strpos($strStyles, 'MSIN-4515-side-styles') !== false) {
// Déjà injecté : lire les indices depuis un commentaire XML.
if (preg_match(
'/MSIN-4515-side-styles face_h=(\d+) face_d=(\d+) endos_h=(\d+) endos_d=(\d+) info_h=(\d+) info_d=(\d+)/',
$strStyles,
$tabMatch
)) {
return [
'face' => ['h' => (int)$tabMatch[1], 'd' => (int)$tabMatch[2]],
'endos' => ['h' => (int)$tabMatch[3], 'd' => (int)$tabMatch[4]],
'info' => ['h' => (int)$tabMatch[5], 'd' => (int)$tabMatch[6]],
];
}
}
$objDom = fxBibProductionXmlLoad($strStyles);
$objXpath = new DOMXPath($objDom);
$objXpath->registerNamespace('m', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
$strNs = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
$objFills = $objXpath->query('//m:fills')->item(0);
$objCellXfs = $objXpath->query('//m:cellXfs')->item(0);
if (!($objFills instanceof DOMElement) || !($objCellXfs instanceof DOMElement)) {
throw new Exception('MSIN-4515 styles structure invalid');
}
$intFillCount = (int)$objFills->getAttribute('count');
$tabFillRgb = [
'face_h' => '5B9BD5',
'face_d' => 'BDD7EE',
'endos_h' => 'C00000',
'endos_d' => 'F4CCCC',
'info_h' => 'D9D9D9',
'info_d' => 'FFFFFF',
];
$tabFillIds = [];
foreach ($tabFillRgb as $strKey => $strRgb) {
$objFill = $objDom->createElementNS($strNs, 'fill');
$objPattern = $objDom->createElementNS($strNs, 'patternFill');
$objPattern->setAttribute('patternType', 'solid');
$objFg = $objDom->createElementNS($strNs, 'fgColor');
$objFg->setAttribute('rgb', 'FF' . $strRgb);
$objPattern->appendChild($objFg);
$objFill->appendChild($objPattern);
$objFills->appendChild($objFill);
$tabFillIds[$strKey] = $intFillCount;
$intFillCount++;
}
$objFills->setAttribute('count', (string)$intFillCount);
$intXfCount = (int)$objCellXfs->getAttribute('count');
$tabStyleIds = [];
$tabXfMap = [
'face_h' => $tabFillIds['face_h'],
'face_d' => $tabFillIds['face_d'],
'endos_h' => $tabFillIds['endos_h'],
'endos_d' => $tabFillIds['endos_d'],
'info_h' => $tabFillIds['info_h'],
'info_d' => $tabFillIds['info_d'],
];
foreach ($tabXfMap as $strKey => $intFillId) {
$objXf = $objDom->createElementNS($strNs, 'xf');
$objXf->setAttribute('numFmtId', '0');
$objXf->setAttribute('fontId', '0');
$objXf->setAttribute('fillId', (string)$intFillId);
$objXf->setAttribute('borderId', '0');
$objXf->setAttribute('xfId', '0');
$objXf->setAttribute('applyFill', '1');
$objXf->setAttribute('applyAlignment', '1');
$objAlign = $objDom->createElementNS($strNs, 'alignment');
$objAlign->setAttribute('horizontal', 'center');
$objAlign->setAttribute('vertical', 'center');
$objAlign->setAttribute('wrapText', '1');
$objXf->appendChild($objAlign);
$objCellXfs->appendChild($objXf);
$tabStyleIds[$strKey] = $intXfCount;
$intXfCount++;
}
$objCellXfs->setAttribute('count', (string)$intXfCount);
$strComment = sprintf(
'MSIN-4515-side-styles face_h=%d face_d=%d endos_h=%d endos_d=%d info_h=%d info_d=%d',
$tabStyleIds['face_h'],
$tabStyleIds['face_d'],
$tabStyleIds['endos_h'],
$tabStyleIds['endos_d'],
$tabStyleIds['info_h'],
$tabStyleIds['info_d']
);
$objDom->insertBefore($objDom->createComment($strComment), $objDom->documentElement);
$objZip->deleteName('xl/styles.xml');
$objZip->addFromString('xl/styles.xml', $objDom->saveXML());
return [
'face' => ['h' => $tabStyleIds['face_h'], 'd' => $tabStyleIds['face_d']],
'endos' => ['h' => $tabStyleIds['endos_h'], 'd' => $tabStyleIds['endos_d']],
'info' => ['h' => $tabStyleIds['info_h'], 'd' => $tabStyleIds['info_d']],
];
}
/**
* MSIN-4515 — Participants indexés par no_bib pour remplir les colonnes.
*
* @return array<string, array<string,mixed>>
*/
function fxBibProductionLoadParticipantsByBib($intEprId) {
global $objDatabase;
$intEprId = (int)$intEprId;
if ($intEprId <= 0) {
return [];
}
$sql = "
SELECT p.*
FROM resultats_participants p
WHERE p.epr_id = $intEprId
AND p.is_cancelled = 0
AND TRIM(CAST(p.no_bib AS CHAR)) <> ''
AND TRIM(CAST(p.no_bib AS CHAR)) <> '0'
";
$tabRows = $objDatabase->fxGetResults($sql);
if (!is_array($tabRows)) {
return [];
}
$tabOut = [];
foreach ($tabRows as $tabRow) {
$strBib = trim((string)($tabRow['no_bib'] ?? ''));
if ($strBib === '') {
continue;
}
// Premier gagnant si doublon (anomalie déjà gérée ailleurs).
if (!isset($tabOut[$strBib])) {
$tabOut[$strBib] = $tabRow;
}
}
return $tabOut;
}
/**
* MSIN-4515 — Valeur dune colonne pour une ligne dossard.
*
* @param array<string,mixed>|null $tabParticipant
* @param array<int,string> $tabAnswers que_id => réponse
*/
function fxBibProductionResolveCellValue(
$strKey,
$intBib,
$strRaceLabel,
$tabParticipant,
array $tabAnswers
) {
$strKey = (string)$strKey;
$fnUn = function ($str) {
$str = (string)$str;
return function_exists('fxUnescape') ? fxUnescape($str) : $str;
};
if ($strKey === 'no_bib') {
return (string)(int)$intBib;
}
if ($strKey === 'epr_nom') {
return (string)$strRaceLabel;
}
if (!is_array($tabParticipant)) {
return '';
}
switch ($strKey) {
case 'par_prenom':
return trim($fnUn($tabParticipant['par_prenom'] ?? ''));
case 'par_nom':
return trim($fnUn($tabParticipant['par_nom'] ?? ''));
case 'par_prenom_nom':
return trim(
$fnUn($tabParticipant['par_prenom'] ?? '')
. ' '
. $fnUn($tabParticipant['par_nom'] ?? '')
);
case 'par_sexe':
return trim((string)($tabParticipant['par_sexe'] ?? ''));
case 'par_age':
$strAge = trim((string)($tabParticipant['par_age'] ?? ''));
if ($strAge !== '' && $strAge !== '0') {
return $strAge;
}
$strNaissance = trim((string)($tabParticipant['par_naissance'] ?? ''));
if ($strNaissance === '' || $strNaissance === '0000-00-00') {
return '';
}
try {
$objBirth = new DateTime($strNaissance);
$objNow = new DateTime('today');
return (string)$objBirth->diff($objNow)->y;
} catch (Exception $objEx) {
return '';
}
default:
if (preg_match('/^que:(\d+)$/', $strKey, $tabMatch)) {
$intQueId = (int)$tabMatch[1];
return trim((string)($tabAnswers[$intQueId] ?? ''));
}
// Infos participant (par_contact_urgence_*, par_telephone1, etc.)
if (array_key_exists($strKey, $tabParticipant)) {
return trim($fnUn($tabParticipant[$strKey] ?? ''));
}
return '';
}
}
/**
* MSIN-4469 / MSIN-4515 — Onglet Imprimeur : 1 ligne / dossard, colonnes colorées.
*
* @param array<string,array{h:int,d:int}> $tabSideStyles
*/
function fxBibProductionBuildSupplierXml($strXml, array $tabSnapshot, array $tabLabels, array $tabSideStyles = []) {
$objDom = fxBibProductionXmlLoad($strXml);
$objXpath = new DOMXPath($objDom);
$objXpath->registerNamespace('m', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
$strNs = '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);
}
$tabColumns = fxBibProductionBuildColumnPlan($tabSnapshot['races'] ?? []);
$intColCount = count($tabColumns);
if ($intColCount < 1) {
throw new Exception('MSIN-4515 no printer columns');
}
if (empty($tabSideStyles)) {
$tabSideStyles = [
'face' => ['h' => 0, 'd' => 0],
'endos' => ['h' => 0, 'd' => 0],
'info' => ['h' => 0, 'd' => 0],
];
}
// Largeurs colonnes.
$objCols = $objXpath->query('//m:cols')->item(0);
if ($objCols instanceof DOMElement) {
while ($objCols->firstChild) {
$objCols->removeChild($objCols->firstChild);
}
for ($intC = 0; $intC < $intColCount; $intC++) {
$objCol = $objDom->createElementNS($strNs, 'col');
$objCol->setAttribute('min', (string)($intC + 1));
$objCol->setAttribute('max', (string)($intC + 1));
$strKey = $tabColumns[$intC]['key'];
$fltWidth = ($strKey === 'no_bib') ? 14.0 : (($strKey === 'epr_nom') ? 28.0 : 22.0);
$objCol->setAttribute('width', (string)$fltWidth);
$objCol->setAttribute('customWidth', '1');
$objCols->appendChild($objCol);
}
}
$objHeader = fxBibProductionXmlCloneRow($objHeaderTemplate, 1);
foreach ($tabColumns as $intC => $tabCol) {
$strLetter = fxBibProductionColumnLetter($intC);
$strSide = (string)$tabCol['side'];
$strStyle = (string)($tabSideStyles[$strSide]['h'] ?? 0);
$strLabel = (string)$tabCol['label'];
if ($tabCol['key'] === 'no_bib' && isset($tabLabels['bib']) && $tabLabels['bib'] !== '') {
$strLabel = $tabLabels['bib'];
} elseif ($tabCol['key'] === 'epr_nom' && isset($tabLabels['race']) && $tabLabels['race'] !== '') {
$strLabel = $tabLabels['race'];
}
fxBibProductionXmlSetInlineString($objDom, $objHeader, $strLetter, 1, $strLabel, $strStyle);
}
$objSheetData->appendChild($objHeader);
$strLangue = (string)($tabSnapshot['language'] ?? 'fr');
$intRow = 2;
foreach ($tabSnapshot['races'] as $intRaceIndex => $tabRace) {
if ($intRaceIndex > 0) {
$intRow++;
}
$intEprId = (int)($tabRace['epr_id'] ?? 0);
$tabRaceFields = isset($tabRace['fields']) && is_array($tabRace['fields'])
? $tabRace['fields']
: fxBibProductionDefaultFields();
$tabRaceKeys = [];
foreach ($tabRaceFields as $tabField) {
$tabRaceKeys[(string)$tabField['key']] = true;
}
$tabByBib = fxBibProductionLoadParticipantsByBib($intEprId);
$tabQueIds = [];
foreach ($tabRaceFields as $tabField) {
if (preg_match('/^que:(\d+)$/', (string)$tabField['key'], $tabMatch)) {
$tabQueIds[] = (int)$tabMatch[1];
}
}
$tabAnswersByParticipant = [];
if (!empty($tabQueIds) && !empty($tabByBib) && function_exists('fxBibGetDistPrintQuestionAnswers')) {
$tabAnswersByParticipant = fxBibGetDistPrintQuestionAnswers(
$intEprId,
$tabQueIds,
array_values($tabByBib),
$strLangue
);
}
foreach ($tabRace['ranges'] as $tabRange) {
for ($intBib = (int)$tabRange['start']; $intBib <= (int)$tabRange['finish']; $intBib++) {
$objRow = fxBibProductionXmlCloneRow($objDataTemplate, $intRow);
$strBibKey = (string)$intBib;
$tabParticipant = $tabByBib[$strBibKey] ?? null;
$tabAnswers = [];
if (is_array($tabParticipant) && function_exists('fxBibDistPrintAnswerKey')) {
$strAnsKey = fxBibDistPrintAnswerKey($tabParticipant);
$tabAnswers = $tabAnswersByParticipant[$strAnsKey] ?? [];
}
foreach ($tabColumns as $intC => $tabCol) {
$strLetter = fxBibProductionColumnLetter($intC);
$strSide = (string)$tabCol['side'];
$strStyle = (string)($tabSideStyles[$strSide]['d'] ?? 0);
$strKey = (string)$tabCol['key'];
// Colonne absente de la config de cette épreuve → cellule vide (style info).
if (!isset($tabRaceKeys[$strKey])) {
fxBibProductionXmlSetInlineString(
$objDom,
$objRow,
$strLetter,
$intRow,
'',
(string)($tabSideStyles['info']['d'] ?? 0)
);
continue;
}
$strValue = fxBibProductionResolveCellValue(
$strKey,
$intBib,
(string)($tabRace['label'] ?? ''),
$tabParticipant,
$tabAnswers
);
if ($strKey === 'no_bib' && $strValue !== '' && ctype_digit($strValue)) {
fxBibProductionXmlSetNumber(
$objDom,
$objRow,
$strLetter,
$intRow,
(int)$strValue,
$strStyle
);
} else {
fxBibProductionXmlSetInlineString(
$objDom,
$objRow,
$strLetter,
$intRow,
$strValue,
$strStyle
);
}
}
$objSheetData->appendChild($objRow);
$intRow++;
}
}
}
$strLastCol = fxBibProductionColumnLetter($intColCount - 1);
$objDimension = $objXpath->query('//m:dimension')->item(0);
if ($objDimension instanceof DOMElement) {
$objDimension->setAttribute('ref', 'A1:' . $strLastCol . 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');
}
$tabSideStyles = fxBibProductionEnsureSideStyles($objZip);
$strSupplierXml = fxBibProductionBuildSupplierXml(
$strSupplierXml,
$tabSnapshot,
$tabLabels,
$tabSideStyles
);
$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;
}