874 lines
32 KiB
PHP
874 lines
32 KiB
PHP
<?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 = ''
|
||
) {
|
||
$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);
|
||
}
|
||
|
||
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,
|
||
$tabLabels['printer_lines'],
|
||
$strHeaderStyleK
|
||
);
|
||
}
|
||
|
||
fxBibProductionXmlSetInlineString($objDom, $objTotal, 'A', $intTotalRow, $tabLabels['total']);
|
||
fxBibProductionXmlSetFormula($objDom, $objTotal, 'F', $intTotalRow, 'SUM(F4:F' . ($intTotalRow - 1) . ')');
|
||
fxBibProductionXmlSetFormula($objDom, $objTotal, 'I', $intTotalRow, 'SUM(I4:I' . ($intTotalRow - 1) . ')');
|
||
fxBibProductionXmlSetFormula($objDom, $objTotal, 'L', $intTotalRow, 'SUM(L4:L' . ($intTotalRow - 1) . ')');
|
||
// COUNT ne compte que les numéros (ignore en-tête texte et lignes blanches entre épreuves).
|
||
fxBibProductionXmlSetFormula($objDom, $objTotal, 'K', $intTotalRow, 'COUNT(Imprimeur!A:A)');
|
||
fxBibProductionXmlSetFormula(
|
||
$objDom,
|
||
$objTotal,
|
||
'J',
|
||
$intTotalRow,
|
||
'F' . $intTotalRow . '=K' . $intTotalRow
|
||
);
|
||
|
||
$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;
|
||
}
|