Files
ms1inscription-v5/php/inc_fx_import_resultats.php

1724 lines
72 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-4482 — Import participants hors transaction (parse, templates, UI superadm).
* Écriture resultats_* : étape suivante ; ici upload + mapping + persistance template.
*/
if (!defined('MSIN_IMPORT_RESULTATS_ORIGIN')) {
define('MSIN_IMPORT_RESULTATS_ORIGIN', 'import_fichier');
}
/**
* Champs cibles mappables (hors questions dynamiques).
* @return array<int, array{key:string,label:string,group:string}>
*/
function fxImportResultatsTargetFields() {
return array(
array('key' => '', 'label' => '— Ignorer —', 'group' => ''),
array('key' => 'par_external_id', 'label' => 'Clé unique (ChronoTrack / fichier)', 'group' => 'Identité'),
array('key' => 'par_prenom', 'label' => 'Prénom', 'group' => 'Identité'),
array('key' => 'par_nom', 'label' => 'Nom', 'group' => 'Identité'),
array('key' => 'par_sexe', 'label' => 'Sexe', 'group' => 'Identité'),
array('key' => 'par_naissance', 'label' => 'Date de naissance', 'group' => 'Identité'),
array('key' => 'par_age', 'label' => 'Âge', 'group' => 'Identité'),
array('key' => 'par_courriel', 'label' => 'Courriel', 'group' => 'Contact'),
array('key' => 'par_telephone1', 'label' => 'Téléphone', 'group' => 'Contact'),
array('key' => 'par_contact_urgence_nom', 'label' => 'Contact urgence — nom', 'group' => 'Contact'),
array('key' => 'par_contact_urgence_telephone', 'label' => 'Contact urgence — téléphone', 'group' => 'Contact'),
array('key' => 'par_adresse', 'label' => 'Adresse', 'group' => 'Contact'),
array('key' => 'par_ville', 'label' => 'Ville', 'group' => 'Contact'),
array('key' => 'par_codepostal', 'label' => 'Code postal', 'group' => 'Contact'),
array('key' => 'no_bib', 'label' => 'Dossard', 'group' => 'Course'),
array('key' => 'epr', 'label' => 'Épreuve (valeurs → épreuves MS1)', 'group' => 'Course'),
array('key' => 'par_nom_equipe', 'label' => 'Nom d\'équipe', 'group' => 'Équipe'),
array('key' => 'par_no_equipe', 'label' => 'No équipe', 'group' => 'Équipe'),
);
}
function fxImportResultatsEsc($str) {
return htmlspecialchars((string)$str, ENT_QUOTES, 'UTF-8');
}
function fxImportResultatsUploadDir() {
$strDir = dirname(__FILE__) . '/../upload/import_resultats';
if (!is_dir($strDir)) {
@mkdir($strDir, 0755, true);
}
return $strDir;
}
function fxImportResultatsNormalizeHeader($str) {
$str = trim((string)$str);
$str = preg_replace('/\s+/u', ' ', $str);
return mb_strtolower($str, 'UTF-8');
}
/**
* Empreinte stable des en-têtes (même fichier client = même sig).
*/
function fxImportResultatsHeaderSig(array $tabHeaders) {
$tabNorm = array();
foreach ($tabHeaders as $strH) {
$tabNorm[] = fxImportResultatsNormalizeHeader($strH);
}
return sha1(implode("\n", $tabNorm));
}
/**
* Suggestion auto de champ selon le libellé d'en-tête.
* MSIN-4482 — priorise match exact / aiguille longue ; évite faux positifs sur questions bilingues.
*/
function fxImportResultatsGuessField($strHeader) {
$str = fxImportResultatsNormalizeHeader($strHeader);
$str = str_replace(array('-', '_'), ' ', $str);
// MSIN-4482 — inclut libellés type export client (ex. Échappée / Eventbrite-like)
$tabRules = array(
'par_external_id' => array(
'attendee #', 'attendee id', 'external id', 'external_id', 'unique id', 'uniqueid',
'entry id', 'id unique', 'ct id', 'chronotrack id', 'reg id', 'participant id', 'attendee',
),
'par_prenom' => array('first name', 'firstname', 'given name', 'prenom', 'prénom'),
'par_nom' => array('last name', 'lastname', 'family name', 'surname'),
'par_sexe' => array('gender', 'sexe', 'sex', 'genre'),
'par_naissance' => array('birth date', 'date of birth', 'date naissance', 'naissance', 'birthday', 'dob'),
'par_age' => array('age', 'âge'),
'par_courriel' => array('courriel', 'e-mail', 'email', 'mail'),
'par_telephone1' => array('telephone', 'téléphone', 'phone', 'mobile', 'cell', 'tel'),
'par_contact_urgence_nom' => array('emergency contact name', 'contact urgence', 'nom en cas d\'urgence', 'urgence nom'),
'par_contact_urgence_telephone' => array('emergency contact number', 'emergency contact phone', 'numéro du contact en cas d\'urgence', 'urgence tel'),
'par_adresse' => array('adresse', 'address', 'street'),
'par_ville' => array('ville', 'city', 'town'),
'par_codepostal' => array('code postal', 'postal', 'zipcode', 'zip'),
'no_bib' => array('no dossard', 'bib number', 'dossard', 'no bib', 'bib#', 'bib'),
'epr' => array(
'ticket type', 'registration type', 'reg choice', 'event race',
'epreuve', 'épreuve', 'distance', 'race', 'course', 'ticket',
),
'par_nom_equipe' => array('team name', 'nom equipe', 'nom équipe', 'equipe', 'équipe', 'team'),
'par_no_equipe' => array('team number', 'team no', 'no equipe', 'no équipe', 'team id'),
);
// 1) égalité exacte
foreach ($tabRules as $strKey => $tabNeedles) {
foreach ($tabNeedles as $strNeedle) {
if ($str === $strNeedle) {
return $strKey;
}
}
}
// Questions longues bilingues → pas de guess champ standard (resteront en step questions)
if (mb_strlen($str, 'UTF-8') > 40 || strpos($str, '?') !== false || strpos($str, ' / ') !== false) {
// Exceptions courtes déjà couvertes en exact ; ici on laisse vide
// sauf préfixes connus très spécifiques
if (strpos($str, 'attendee') === 0) {
return 'par_external_id';
}
return '';
}
// 2) contient — aiguilles les plus longues d'abord
$tabCandidates = array();
foreach ($tabRules as $strKey => $tabNeedles) {
foreach ($tabNeedles as $strNeedle) {
if ($strNeedle === '') {
continue;
}
// aiguilles 13 lettres : mot entier seulement
if (mb_strlen($strNeedle, 'UTF-8') <= 3) {
if (preg_match('/(^|\\s)' . preg_quote($strNeedle, '/') . '(\\s|$)/u', $str)) {
$tabCandidates[] = array('key' => $strKey, 'len' => mb_strlen($strNeedle, 'UTF-8'));
}
continue;
}
if (strpos($str, $strNeedle) !== false) {
$tabCandidates[] = array('key' => $strKey, 'len' => mb_strlen($strNeedle, 'UTF-8'));
}
}
}
if (count($tabCandidates) < 1) {
return '';
}
usort($tabCandidates, function ($a, $b) {
return $b['len'] - $a['len'];
});
return $tabCandidates[0]['key'];
}
/**
* Clé de partage templates entre événements du même org (pas de com_id sur evenements).
* MSIN-4482 — stockée dans inscriptions_import_templates.com_id (legacy name = org_id).
*/
function fxImportResultatsComIdForEve($intEveId) {
global $objDatabase;
$intEveId = intval($intEveId);
if ($intEveId <= 0) {
return 0;
}
$sql = "SELECT org_id FROM inscriptions_evenements WHERE eve_id = " . $intEveId . " LIMIT 1";
$tab = $objDatabase->fxGetResults($sql);
if (!is_array($tab) || empty($tab[1]['org_id'])) {
return 0;
}
return intval($tab[1]['org_id']);
}
function fxImportResultatsEpreuvesList($intEveId) {
global $objDatabase;
$intEveId = intval($intEveId);
$sql = "SELECT epr_id, epr_nom_fr, epr_type_fr, epr_date"
. " FROM inscriptions_epreuves WHERE eve_id = " . $intEveId
. " ORDER BY epr_date, epr_nom_fr";
$tab = $objDatabase->fxGetResults($sql);
$tabOut = array();
if (!is_array($tab)) {
return $tabOut;
}
for ($i = 1; $i <= count($tab); $i++) {
$strLabel = trim((string)($tab[$i]['epr_type_fr'] ?? ''));
$strNom = trim((string)($tab[$i]['epr_nom_fr'] ?? ''));
if ($strLabel !== '' && $strNom !== '') {
$strLabel .= ' — ' . $strNom;
} elseif ($strNom !== '') {
$strLabel = $strNom;
} else {
$strLabel = 'Épreuve #' . intval($tab[$i]['epr_id']);
}
$tabOut[] = array(
'epr_id' => intval($tab[$i]['epr_id']),
'label' => $strLabel,
);
}
return $tabOut;
}
function fxImportResultatsQuestionsList($intEveId) {
global $objDatabase;
$intEveId = intval($intEveId);
// que_rapport_label_fr peut manquer sur vieux schémas — fallback question seule
$sql = "SELECT que_id, que_question_fr, que_actif"
. " FROM inscriptions_questions WHERE eve_id = " . $intEveId
. " AND que_actif = 1 ORDER BY que_tri, que_id";
$tab = $objDatabase->fxGetResults($sql);
$tabOut = array();
if (!is_array($tab)) {
return $tabOut;
}
for ($i = 1; $i <= count($tab); $i++) {
$strLabel = trim((string)($tab[$i]['que_question_fr'] ?? ''));
$tabOut[] = array(
'que_id' => intval($tab[$i]['que_id']),
'label' => $strLabel !== '' ? $strLabel : ('Question #' . intval($tab[$i]['que_id'])),
);
}
return $tabOut;
}
/**
* Lit CSV ou Excel → en-têtes + lignes (tableaux indexés 0..n).
* @return array{state:string,message?:string,headers?:array,rows?:array}
*/
function fxImportResultatsParseFile($strPath, $strOriginalName = '') {
$strExt = strtolower(pathinfo($strOriginalName !== '' ? $strOriginalName : $strPath, PATHINFO_EXTENSION));
if ($strExt === 'csv' || $strExt === 'txt') {
return fxImportResultatsParseCsv($strPath);
}
if (in_array($strExt, array('xls', 'xlsx'), true)) {
return fxImportResultatsParseExcel($strPath);
}
// Sniff CSV
$strSample = @file_get_contents($strPath, false, null, 0, 2048);
if (is_string($strSample) && (strpos($strSample, ',') !== false || strpos($strSample, ';') !== false)) {
return fxImportResultatsParseCsv($strPath);
}
return array('state' => 'error', 'message' => 'Format non supporté (CSV, XLS, XLSX).');
}
function fxImportResultatsDetectCsvDelimiter($strPath) {
$strLine = '';
$fp = @fopen($strPath, 'rb');
if ($fp) {
$strLine = (string)fgets($fp);
fclose($fp);
}
$intSemi = substr_count($strLine, ';');
$intComma = substr_count($strLine, ',');
$intTab = substr_count($strLine, "\t");
if ($intTab > $intSemi && $intTab > $intComma) {
return "\t";
}
return ($intSemi > $intComma) ? ';' : ',';
}
function fxImportResultatsParseCsv($strPath) {
$strDelim = fxImportResultatsDetectCsvDelimiter($strPath);
$fp = @fopen($strPath, 'rb');
if (!$fp) {
return array('state' => 'error', 'message' => 'Impossible de lire le fichier.');
}
// BOM UTF-8
$strBom = fread($fp, 3);
if ($strBom !== "\xEF\xBB\xBF") {
rewind($fp);
}
$tabHeaders = fgetcsv($fp, 0, $strDelim);
if (!is_array($tabHeaders) || count($tabHeaders) < 1) {
fclose($fp);
return array('state' => 'error', 'message' => 'En-têtes manquants.');
}
$tabHeaders = array_map('trim', $tabHeaders);
$tabRows = array();
while (($tabRow = fgetcsv($fp, 0, $strDelim)) !== false) {
if (!is_array($tabRow)) {
continue;
}
// Ignorer lignes totalement vides
$blnEmpty = true;
foreach ($tabRow as $strCell) {
if (trim((string)$strCell) !== '') {
$blnEmpty = false;
break;
}
}
if ($blnEmpty) {
continue;
}
// Aligner largeur
while (count($tabRow) < count($tabHeaders)) {
$tabRow[] = '';
}
if (count($tabRow) > count($tabHeaders)) {
$tabRow = array_slice($tabRow, 0, count($tabHeaders));
}
$tabRows[] = $tabRow;
}
fclose($fp);
return array('state' => 'ok', 'headers' => $tabHeaders, 'rows' => $tabRows);
}
function fxImportResultatsParseExcel($strPath) {
$strPhpExcel = dirname(__FILE__) . '/../superadm/php/Classes/PHPExcel.php';
if (!is_file($strPhpExcel)) {
$strPhpExcel = dirname(__FILE__) . '/Classes/PHPExcel.php';
}
if (!is_file($strPhpExcel)) {
return array('state' => 'error', 'message' => 'Lecteur Excel indisponible — utilisez un CSV.');
}
require_once $strPhpExcel;
try {
$objBook = PHPExcel_IOFactory::load($strPath);
$objSheet = $objBook->getActiveSheet();
$tabRaw = $objSheet->toArray(null, true, true, false);
} catch (Exception $ex) {
return array('state' => 'error', 'message' => 'Lecture Excel échouée : ' . $ex->getMessage());
}
if (!is_array($tabRaw) || count($tabRaw) < 1) {
return array('state' => 'error', 'message' => 'Fichier Excel vide.');
}
$tabHeaders = array_map(function ($v) {
return trim((string)$v);
}, $tabRaw[0]);
// Trim trailing empty headers
while (count($tabHeaders) > 0 && end($tabHeaders) === '') {
array_pop($tabHeaders);
}
if (count($tabHeaders) < 1) {
return array('state' => 'error', 'message' => 'En-têtes manquants.');
}
$tabRows = array();
for ($r = 1; $r < count($tabRaw); $r++) {
$tabRow = $tabRaw[$r];
if (!is_array($tabRow)) {
continue;
}
$tabRow = array_slice($tabRow, 0, count($tabHeaders));
while (count($tabRow) < count($tabHeaders)) {
$tabRow[] = '';
}
$blnEmpty = true;
foreach ($tabRow as $strCell) {
if (trim((string)$strCell) !== '') {
$blnEmpty = false;
break;
}
}
if ($blnEmpty) {
continue;
}
$tabRows[] = array_map(function ($v) {
return trim((string)$v);
}, $tabRow);
}
return array('state' => 'ok', 'headers' => $tabHeaders, 'rows' => $tabRows);
}
/**
* Valeurs distinctes par colonne (capées).
*/
function fxImportResultatsDistincts(array $tabRows, $intColCount, $intMaxPerCol = 40) {
$tabDistinct = array();
for ($c = 0; $c < $intColCount; $c++) {
$tabDistinct[$c] = array();
}
foreach ($tabRows as $tabRow) {
for ($c = 0; $c < $intColCount; $c++) {
$strVal = trim((string)($tabRow[$c] ?? ''));
if ($strVal === '') {
continue;
}
if (!isset($tabDistinct[$c][$strVal])) {
if (count($tabDistinct[$c]) >= $intMaxPerCol) {
continue;
}
$tabDistinct[$c][$strVal] = true;
}
}
}
$tabOut = array();
for ($c = 0; $c < $intColCount; $c++) {
$tabOut[$c] = array_keys($tabDistinct[$c]);
sort($tabOut[$c], SORT_NATURAL | SORT_FLAG_CASE);
}
return $tabOut;
}
function fxImportResultatsSaveUpload($intEveId, array $arrFile) {
$intEveId = intval($intEveId);
if ($intEveId <= 0) {
return array('state' => 'error', 'message' => 'eve_id manquant');
}
if (empty($arrFile['tmp_name']) || !is_uploaded_file($arrFile['tmp_name'])) {
return array('state' => 'error', 'message' => 'Aucun fichier reçu');
}
if (!empty($arrFile['error']) && intval($arrFile['error']) !== UPLOAD_ERR_OK) {
return array('state' => 'error', 'message' => 'Erreur upload (code ' . intval($arrFile['error']) . ')');
}
$strName = (string)($arrFile['name'] ?? 'import.csv');
$strToken = bin2hex(random_bytes(16));
$strSafeExt = strtolower(pathinfo($strName, PATHINFO_EXTENSION));
if (!in_array($strSafeExt, array('csv', 'txt', 'xls', 'xlsx'), true)) {
$strSafeExt = 'csv';
}
$strDest = fxImportResultatsUploadDir() . '/' . $intEveId . '_' . $strToken . '.' . $strSafeExt;
if (!@move_uploaded_file($arrFile['tmp_name'], $strDest)) {
return array('state' => 'error', 'message' => 'Échec enregistrement temporaire');
}
$arrParsed = fxImportResultatsParseFile($strDest, $strName);
if (($arrParsed['state'] ?? '') !== 'ok') {
@unlink($strDest);
return $arrParsed;
}
$tabHeaders = $arrParsed['headers'];
$tabRows = $arrParsed['rows'];
$strSig = fxImportResultatsHeaderSig($tabHeaders);
$tabGuess = array();
foreach ($tabHeaders as $strH) {
$tabGuess[] = fxImportResultatsGuessField($strH);
}
$tabSamples = array_slice($tabRows, 0, 8);
// Retirer d'éventuelles lignes « en-tête » encore présentes dans les données
$tabIdxFilter = array();
foreach ($tabHeaders as $intHi => $strH) {
$strG = fxImportResultatsGuessField($strH);
if ($strG !== '') {
$tabIdxFilter[$strG] = $intHi;
}
}
$tabRowsFiltered = array();
foreach ($tabRows as $tabRow) {
if (fxImportResultatsRowLooksLikeHeader($tabRow, $tabHeaders, $tabIdxFilter)) {
continue;
}
$tabRowsFiltered[] = $tabRow;
}
$tabRows = $tabRowsFiltered;
$tabSamples = array_slice($tabRows, 0, 8);
$tabDistinct = fxImportResultatsDistincts($tabRows, count($tabHeaders));
// Meta JSON à côté du fichier
$arrMeta = array(
'eve_id' => $intEveId,
'token' => $strToken,
'original_name' => $strName,
'path' => $strDest,
'header_sig' => $strSig,
'headers' => $tabHeaders,
'row_count' => count($tabRows),
'created_at' => date('c'),
);
@file_put_contents($strDest . '.meta.json', json_encode($arrMeta, JSON_UNESCAPED_UNICODE));
$tabTemplates = fxImportResultatsFindTemplates($intEveId, $strSig);
return array(
'state' => 'ok',
'token' => $strToken,
'original_name' => $strName,
'header_sig' => $strSig,
'headers' => $tabHeaders,
'guess' => $tabGuess,
'row_count' => count($tabRows),
'samples' => $tabSamples,
'distincts' => $tabDistinct,
'templates' => $tabTemplates,
'epreuves' => fxImportResultatsEpreuvesList($intEveId),
'questions' => fxImportResultatsQuestionsList($intEveId),
'fields' => fxImportResultatsTargetFields(),
);
}
function fxImportResultatsResolveTokenPath($intEveId, $strToken) {
$intEveId = intval($intEveId);
$strToken = preg_replace('/[^a-f0-9]/', '', strtolower((string)$strToken));
if ($intEveId <= 0 || strlen($strToken) < 16) {
return null;
}
$strDir = fxImportResultatsUploadDir();
$tabMatches = glob($strDir . '/' . $intEveId . '_' . $strToken . '.*');
if (!is_array($tabMatches)) {
return null;
}
foreach ($tabMatches as $strPath) {
if (substr($strPath, -10) === '.meta.json') {
continue;
}
if (is_file($strPath)) {
return $strPath;
}
}
return null;
}
function fxImportResultatsFindTemplates($intEveId, $strSig) {
global $objDatabase;
$intEveId = intval($intEveId);
$strSig = preg_replace('/[^a-f0-9]/', '', strtolower((string)$strSig));
$intOrgId = fxImportResultatsComIdForEve($intEveId);
// Table absente (SQL pas encore joué) → pas d'erreur HTML dans le JSON AJAX
$sqlCheck = "SELECT 1 AS ok FROM information_schema.TABLES"
. " WHERE TABLE_SCHEMA = DATABASE()"
. " AND TABLE_NAME = 'inscriptions_import_templates' LIMIT 1";
$tabCheck = $objDatabase->fxGetResults($sqlCheck);
if (!is_array($tabCheck) || empty($tabCheck[1]['ok'])) {
return array();
}
$sql = "SELECT tpl_id, eve_id, com_id, tpl_name, tpl_header_sig, tpl_mapping_json, tpl_maj"
. " FROM inscriptions_import_templates"
. " WHERE tpl_actif = 1 AND tpl_header_sig = '" . addslashes($strSig) . "'"
. " AND (eve_id = " . $intEveId
. ($intOrgId > 0 ? (" OR com_id = " . $intOrgId) : '')
. ")"
. " ORDER BY (eve_id = " . $intEveId . ") DESC, tpl_maj DESC LIMIT 20";
$tab = $objDatabase->fxGetResults($sql);
$tabOut = array();
if (!is_array($tab)) {
return $tabOut;
}
for ($i = 1; $i <= count($tab); $i++) {
$arrMap = json_decode((string)($tab[$i]['tpl_mapping_json'] ?? ''), true);
$tabOut[] = array(
'tpl_id' => intval($tab[$i]['tpl_id']),
'eve_id' => intval($tab[$i]['eve_id']),
'com_id' => intval($tab[$i]['com_id']),
'name' => (string)$tab[$i]['tpl_name'],
'maj' => (string)$tab[$i]['tpl_maj'],
'mapping' => is_array($arrMap) ? $arrMap : array(),
'same_event' => (intval($tab[$i]['eve_id']) === $intEveId),
);
}
return $tabOut;
}
/**
* @param array $arrMapping { columns: string[], epr_values: {value: epr_id}, questions: {colIndex: que_id|"new"} }
*/
function fxImportResultatsSaveTemplate($intEveId, $strToken, $strName, array $arrMapping, $intUsaId, $blnShareCom = true) {
global $objDatabase;
$intEveId = intval($intEveId);
$strPath = fxImportResultatsResolveTokenPath($intEveId, $strToken);
if ($strPath === null) {
return array('state' => 'error', 'message' => 'Fichier temporaire introuvable — réuploadez.');
}
$strMetaPath = $strPath . '.meta.json';
$arrMeta = array();
if (is_file($strMetaPath)) {
$arrMeta = json_decode((string)@file_get_contents($strMetaPath), true);
if (!is_array($arrMeta)) {
$arrMeta = array();
}
}
$tabHeaders = isset($arrMeta['headers']) && is_array($arrMeta['headers']) ? $arrMeta['headers'] : array();
$strSig = isset($arrMeta['header_sig']) ? (string)$arrMeta['header_sig'] : fxImportResultatsHeaderSig($tabHeaders);
if ($tabHeaders === array()) {
$arrParsed = fxImportResultatsParseFile($strPath);
if (($arrParsed['state'] ?? '') !== 'ok') {
return $arrParsed;
}
$tabHeaders = $arrParsed['headers'];
$strSig = fxImportResultatsHeaderSig($tabHeaders);
}
$strName = trim((string)$strName);
if ($strName === '') {
$strName = 'Template ' . date('Y-m-d H:i');
}
$intComId = $blnShareCom ? fxImportResultatsComIdForEve($intEveId) : 0;
$strNow = date('Y-m-d H:i:s');
$strHeadersJson = json_encode($tabHeaders, JSON_UNESCAPED_UNICODE);
$strMappingJson = json_encode($arrMapping, JSON_UNESCAPED_UNICODE);
if ($strHeadersJson === false || $strMappingJson === false) {
return array('state' => 'error', 'message' => 'JSON mapping invalide');
}
// Upsert soft : même eve + même sig + même nom → update
$sqlFind = "SELECT tpl_id FROM inscriptions_import_templates"
. " WHERE eve_id = " . $intEveId
. " AND tpl_header_sig = '" . addslashes($strSig) . "'"
. " AND tpl_name = '" . addslashes($strName) . "'"
. " AND tpl_actif = 1 LIMIT 1";
$tabFind = $objDatabase->fxGetResults($sqlFind);
if (is_array($tabFind) && !empty($tabFind[1]['tpl_id'])) {
$intTplId = intval($tabFind[1]['tpl_id']);
$sql = "UPDATE inscriptions_import_templates SET"
. " com_id = " . intval($intComId) . ","
. " tpl_headers_json = '" . addslashes($strHeadersJson) . "',"
. " tpl_mapping_json = '" . addslashes($strMappingJson) . "',"
. " tpl_usa_id = " . intval($intUsaId) . ","
. " tpl_maj = '" . addslashes($strNow) . "'"
. " WHERE tpl_id = " . $intTplId;
$objDatabase->fxQuery($sql);
} else {
$sql = "INSERT INTO inscriptions_import_templates"
. " (eve_id, com_id, tpl_name, tpl_header_sig, tpl_headers_json, tpl_mapping_json, tpl_actif, tpl_usa_id, tpl_creation, tpl_maj)"
. " VALUES ("
. intval($intEveId) . ", "
. intval($intComId) . ", "
. "'" . addslashes($strName) . "', "
. "'" . addslashes($strSig) . "', "
. "'" . addslashes($strHeadersJson) . "', "
. "'" . addslashes($strMappingJson) . "', "
. "1, "
. intval($intUsaId) . ", "
. "'" . addslashes($strNow) . "', "
. "'" . addslashes($strNow) . "'"
. ")";
$objDatabase->fxQuery($sql);
$intTplId = intval($objDatabase->fxGetLastId());
}
return array(
'state' => 'ok',
'tpl_id' => $intTplId,
'message' => 'Template enregistré — le prochain fichier avec les mêmes colonnes le proposera.',
'templates' => fxImportResultatsFindTemplates($intEveId, $strSig),
);
}
/**
* Validation mapping (sans écriture resultats).
*/
function fxImportResultatsValidateMapping($intEveId, $strToken, array $arrMapping) {
$intEveId = intval($intEveId);
$strPath = fxImportResultatsResolveTokenPath($intEveId, $strToken);
if ($strPath === null) {
return array('state' => 'error', 'message' => 'Fichier temporaire introuvable — réuploadez.');
}
$arrParsed = fxImportResultatsParseFile($strPath);
if (($arrParsed['state'] ?? '') !== 'ok') {
return $arrParsed;
}
$tabColumns = isset($arrMapping['columns']) && is_array($arrMapping['columns']) ? $arrMapping['columns'] : array();
$tabEprValues = isset($arrMapping['epr_values']) && is_array($arrMapping['epr_values']) ? $arrMapping['epr_values'] : array();
$blnHasExt = false;
$blnHasEpr = false;
$intEprCol = -1;
foreach ($tabColumns as $intIdx => $strKey) {
$strKey = (string)$strKey;
if ($strKey === 'par_external_id') {
$blnHasExt = true;
}
if ($strKey === 'epr') {
$blnHasEpr = true;
$intEprCol = intval($intIdx);
}
}
$tabErrors = array();
$tabWarn = array();
if (!$blnHasExt) {
$tabErrors[] = 'Mappez une colonne « Clé unique » (indispensable pour ChronoTrack / réimport).';
}
if (!$blnHasEpr) {
$tabErrors[] = 'Mappez une colonne « Épreuve ».';
} else {
$tabDistinct = fxImportResultatsDistincts($arrParsed['rows'], count($arrParsed['headers']));
$tabVals = $tabDistinct[$intEprCol] ?? array();
$intUnmapped = 0;
foreach ($tabVals as $strVal) {
$intEpr = intval($tabEprValues[$strVal] ?? 0);
if ($intEpr <= 0) {
$intUnmapped++;
}
}
if ($intUnmapped > 0) {
$tabErrors[] = $intUnmapped . ' valeur(s) d\'épreuve sans correspondance MS1.';
}
}
if (!in_array('par_prenom', $tabColumns, true) && !in_array('par_nom', $tabColumns, true)) {
$tabWarn[] = 'Ni prénom ni nom mappé — vérifiez le fichier.';
}
if (count($tabErrors) > 0) {
return array(
'state' => 'error',
'message' => implode(' ', $tabErrors),
'errors' => $tabErrors,
'warnings' => $tabWarn,
'row_count' => count($arrParsed['rows']),
);
}
return array(
'state' => 'ok',
'message' => 'Mapping prêt — ' . count($arrParsed['rows']) . ' ligne(s). Vous pouvez importer.',
'warnings' => $tabWarn,
'row_count' => count($arrParsed['rows']),
'commit_ready' => true,
);
}
function fxImportResultatsDbEsc($str) {
global $objDatabase;
if (isset($objDatabase) && is_object($objDatabase) && method_exists($objDatabase, 'fxEscape')) {
return $objDatabase->fxEscape((string)$str);
}
return addslashes((string)$str);
}
/**
* MSIN-4482 — faux no_panier / no_commande uniques (varchar 20).
* Obligatoire : index UNIQUE sur ces champs → '' ne peut exister qu'une fois.
*/
function fxImportResultatsSyntheticPanier($intEveId, $strExternalId) {
$str = 'I' . intval($intEveId) . 'X' . substr(md5((string)$strExternalId), 0, 12);
return substr($str, 0, 20);
}
function fxImportResultatsDbError() {
global $objDatabase;
if (isset($objDatabase) && is_object($objDatabase) && isset($objDatabase->con)) {
$str = mysqli_error($objDatabase->con);
if (is_string($str) && $str !== '') {
return $str;
}
}
return '';
}
/** MSIN-4482 — Male/Female/Other → M/F/N (aligné CT). */
function fxImportResultatsNormalizeSex($str) {
$str = trim((string)$str);
$strLow = mb_strtolower($str, 'UTF-8');
if ($strLow === 'f' || $strLow === 'female' || $strLow === 'féminin' || $strLow === 'feminin' || $strLow === 'femme') {
return 'F';
}
if ($strLow === 'm' || $strLow === 'h' || $strLow === 'male' || $strLow === 'masculin' || $strLow === 'homme') {
return 'M';
}
if ($strLow === 'other' || $strLow === 'autre' || $strLow === 'n' || $strLow === 'x') {
return 'N';
}
if ($str === 'F' || $str === 'M' || $str === 'N' || $str === 'H') {
return ($str === 'H') ? 'M' : $str;
}
return '';
}
/** MSIN-4482 — parse dates type 5/24/94 → Y-m-d. */
function fxImportResultatsNormalizeBirthdate($str) {
$str = trim((string)$str);
if ($str === '' || $str === '0000-00-00') {
return '';
}
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})/', $str, $m)) {
return $m[1] . '-' . $m[2] . '-' . $m[3];
}
// m/d/y ou m/d/yyyy
if (preg_match('/^(\d{1,2})[\/\-.](\d{1,2})[\/\-.](\d{2,4})$/', $str, $m)) {
$intM = intval($m[1]);
$intD = intval($m[2]);
$intY = intval($m[3]);
if ($intY < 100) {
$intY += ($intY >= 70) ? 1900 : 2000;
}
if (checkdate($intM, $intD, $intY)) {
return sprintf('%04d-%02d-%02d', $intY, $intM, $intD);
}
// essai j/m/a
if (checkdate($intD, $intM, $intY)) {
return sprintf('%04d-%02d-%02d', $intY, $intD, $intM);
}
}
$intTs = strtotime($str);
if ($intTs !== false) {
return date('Y-m-d', $intTs);
}
return '';
}
/**
* Résout que_id pour colonnes questions (match existant ou création confirmée).
* @return array<string,int> colIndex => que_id
*/
function fxImportResultatsResolveQuestionIds($intEveId, array $tabHeaders, array $tabQuestionsMap) {
global $objDatabase;
$intEveId = intval($intEveId);
$tabOut = array();
foreach ($tabQuestionsMap as $strCol => $strQue) {
$intCol = intval($strCol);
$strQue = trim((string)$strQue);
if ($strQue === '' || $strQue === '0') {
continue;
}
if ($strQue === 'new') {
$strLabel = trim((string)($tabHeaders[$intCol] ?? ('Question ' . ($intCol + 1))));
if (mb_strlen($strLabel, 'UTF-8') > 100) {
$strLabel = mb_substr($strLabel, 0, 97, 'UTF-8') . '...';
}
// Réutiliser si même libellé déjà créé sur l'événement
$sqlFind = "SELECT que_id FROM inscriptions_questions"
. " WHERE eve_id = " . $intEveId
. " AND que_question_fr = '" . fxImportResultatsDbEsc($strLabel) . "'"
. " LIMIT 1";
$tabFind = $objDatabase->fxGetResults($sqlFind);
if (is_array($tabFind) && !empty($tabFind[1]['que_id'])) {
$tabOut[(string)$intCol] = intval($tabFind[1]['que_id']);
continue;
}
$sqlIns = "INSERT INTO inscriptions_questions SET"
. " eve_id = " . $intEveId . ","
. " que_question_fr = '" . fxImportResultatsDbEsc($strLabel) . "',"
. " que_question_en = '" . fxImportResultatsDbEsc($strLabel) . "',"
. " que_choix_fr = '',"
. " que_choix_en = '',"
. " que_liste = 0,"
. " que_epreuves_incluses = '',"
. " que_required = 0,"
. " que_tri = 900,"
. " que_participant_1 = 1,"
. " que_modifiable = 1,"
. " que_actif = 1,"
. " que_hidden = 0";
if ($objDatabase->fxQuery($sqlIns) === false) {
continue;
}
$tabOut[(string)$intCol] = intval($objDatabase->fxGetLastId());
continue;
}
$intQueId = intval($strQue);
if ($intQueId > 0) {
$tabOut[(string)$intCol] = $intQueId;
}
}
return $tabOut;
}
function fxImportResultatsSaveQuestionAnswer($intParIdOriginal, $intPecId, $intQueId, $strHeader, $strValue) {
global $objDatabase;
$intParIdOriginal = intval($intParIdOriginal);
$intPecId = intval($intPecId);
$intQueId = intval($intQueId);
$strValue = trim((string)$strValue);
$strHeader = trim((string)$strHeader);
if ($intQueId <= 0 || $intPecId <= 0) {
return;
}
$strNow = function_exists('fxGetDateTime') ? fxGetDateTime() : date('Y-m-d H:i:s');
$sqlFind = "SELECT pqu_id FROM resultats_questions"
. " WHERE pec_id = " . $intPecId
. " AND que_id = " . $intQueId
. " AND par_id = " . $intParIdOriginal
. " LIMIT 1";
$tabFind = $objDatabase->fxGetResults($sqlFind);
if (is_array($tabFind) && !empty($tabFind[1]['pqu_id'])) {
$sql = "UPDATE resultats_questions SET"
. " que_choix_fr = '" . fxImportResultatsDbEsc($strValue) . "',"
. " que_choix_en = '" . fxImportResultatsDbEsc($strValue) . "',"
. " pqu_maj = '" . fxImportResultatsDbEsc($strNow) . "'"
. " WHERE pqu_id = " . intval($tabFind[1]['pqu_id']);
$objDatabase->fxQuery($sql);
return;
}
$sql = "INSERT INTO resultats_questions SET"
. " par_id = " . $intParIdOriginal . ","
. " pec_id = " . $intPecId . ","
. " que_id = " . $intQueId . ","
. " que_question_fr = '" . fxImportResultatsDbEsc($strHeader) . "',"
. " que_question_en = '" . fxImportResultatsDbEsc($strHeader) . "',"
. " que_choix_fr = '" . fxImportResultatsDbEsc($strValue) . "',"
. " que_choix_en = '" . fxImportResultatsDbEsc($strValue) . "',"
. " que_modifiable = 1,"
. " pqu_note = '',"
. " pqu_maj = '" . fxImportResultatsDbEsc($strNow) . "',"
. " no_panier = '',"
. " no_commande = '',"
. " pqu_id_original = 0";
$objDatabase->fxQuery($sql);
}
/**
* MSIN-4482 — détecte une ligne qui n'est que la répétition des en-têtes.
*/
function fxImportResultatsRowLooksLikeHeader(array $tabRow, array $tabHeaders, array $tabColIndex) {
$strPrenom = isset($tabColIndex['par_prenom'])
? trim((string)($tabRow[$tabColIndex['par_prenom']] ?? '')) : '';
$strNom = isset($tabColIndex['par_nom'])
? trim((string)($tabRow[$tabColIndex['par_nom']] ?? '')) : '';
$strExt = isset($tabColIndex['par_external_id'])
? trim((string)($tabRow[$tabColIndex['par_external_id']] ?? '')) : '';
$strPrenomLow = fxImportResultatsNormalizeHeader($strPrenom);
$strNomLow = fxImportResultatsNormalizeHeader($strNom);
$strExtLow = fxImportResultatsNormalizeHeader($strExt);
// Cas classique export EN / FR
if ($strPrenomLow === 'first name' || $strPrenomLow === 'prenom' || $strPrenomLow === 'prénom') {
return true;
}
if ($strNomLow === 'last name' || $strNomLow === 'nom') {
return true;
}
if ($strExtLow === 'attendee #' || $strExtLow === 'attendee' || $strExtLow === 'external id'
|| $strExtLow === 'clé unique' || $strExtLow === 'cle unique') {
return true;
}
// Ligne entière = en-têtes
$intMatch = 0;
$intCompare = 0;
$intN = min(count($tabRow), count($tabHeaders));
for ($i = 0; $i < $intN; $i++) {
$strCell = fxImportResultatsNormalizeHeader($tabRow[$i] ?? '');
$strH = fxImportResultatsNormalizeHeader($tabHeaders[$i] ?? '');
if ($strH === '') {
continue;
}
$intCompare++;
if ($strCell === $strH) {
$intMatch++;
}
}
if ($intCompare >= 3 && $intMatch >= max(3, (int)floor($intCompare * 0.7))) {
return true;
}
return false;
}
/**
* MSIN-4482 — écriture resultats_* (sans panier), upsert sur par_external_id.
*/
function fxImportResultatsCommit($intEveId, $strToken, array $arrMapping) {
global $objDatabase;
@set_time_limit(300);
$arrVal = fxImportResultatsValidateMapping($intEveId, $strToken, $arrMapping);
if (($arrVal['state'] ?? '') !== 'ok') {
return $arrVal;
}
$intEveId = intval($intEveId);
$strPath = fxImportResultatsResolveTokenPath($intEveId, $strToken);
$arrParsed = fxImportResultatsParseFile($strPath);
if (($arrParsed['state'] ?? '') !== 'ok') {
return $arrParsed;
}
// Colonnes requises pour l'import
$sqlCol = "SELECT COUNT(*) AS n FROM information_schema.COLUMNS"
. " WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'resultats_participants'"
. " AND COLUMN_NAME = 'par_external_id'";
$tabCol = $objDatabase->fxGetResults($sqlCol);
if (!is_array($tabCol) || intval($tabCol[1]['n'] ?? 0) < 1) {
return array(
'state' => 'error',
'message' => 'Colonnes import absentes — exécuter sql/MSIN-4482-import-resultats-templates.sql en dev préprod.',
);
}
$tabHeaders = $arrParsed['headers'];
$tabRows = $arrParsed['rows'];
$tabColumns = isset($arrMapping['columns']) && is_array($arrMapping['columns']) ? $arrMapping['columns'] : array();
$tabEprValues = isset($arrMapping['epr_values']) && is_array($arrMapping['epr_values']) ? $arrMapping['epr_values'] : array();
$tabQuestionsMap = isset($arrMapping['questions']) && is_array($arrMapping['questions']) ? $arrMapping['questions'] : array();
$tabColIndex = array();
foreach ($tabColumns as $intIdx => $strKey) {
$strKey = (string)$strKey;
if ($strKey === '') {
continue;
}
$tabColIndex[$strKey] = intval($intIdx);
}
$tabQueIds = fxImportResultatsResolveQuestionIds($intEveId, $tabHeaders, $tabQuestionsMap);
$strNow = function_exists('fxGetDateTime') ? fxGetDateTime() : date('Y-m-d H:i:s');
$strOrigine = MSIN_IMPORT_RESULTATS_ORIGIN;
$intCreated = 0;
$intUpdated = 0;
$intSkipped = 0;
$tabRowErrors = array();
foreach ($tabRows as $intRowNum => $tabRow) {
if (fxImportResultatsRowLooksLikeHeader($tabRow, $tabHeaders, $tabColIndex)) {
$intSkipped++;
continue;
}
$strExt = '';
if (isset($tabColIndex['par_external_id'])) {
$strExt = trim((string)($tabRow[$tabColIndex['par_external_id']] ?? ''));
}
if ($strExt === '') {
$intSkipped++;
if (count($tabRowErrors) < 15) {
$tabRowErrors[] = 'Ligne ' . ($intRowNum + 2) . ' : clé unique vide';
}
continue;
}
$strEprRaw = '';
if (isset($tabColIndex['epr'])) {
$strEprRaw = trim((string)($tabRow[$tabColIndex['epr']] ?? ''));
}
$intEprId = intval($tabEprValues[$strEprRaw] ?? 0);
if ($intEprId <= 0) {
$intSkipped++;
if (count($tabRowErrors) < 15) {
$tabRowErrors[] = 'Ligne ' . ($intRowNum + 2) . ' : épreuve non mappée (« ' . $strEprRaw . ' »)';
}
continue;
}
$arrFields = array(
'par_prenom' => '',
'par_nom' => '',
'par_sexe' => '',
'par_naissance' => '',
'par_age' => 0,
'par_courriel' => '',
'par_telephone1' => '',
'par_adresse' => '',
'par_ville' => '',
'par_codepostal' => '',
'no_bib' => '',
'par_nom_equipe' => '',
'par_no_equipe' => 0,
'par_contact_urgence_nom' => '',
'par_contact_urgence_telephone' => '',
);
foreach ($arrFields as $strKey => $mixDefault) {
if (!isset($tabColIndex[$strKey])) {
continue;
}
$strRaw = trim((string)($tabRow[$tabColIndex[$strKey]] ?? ''));
if ($strKey === 'par_sexe') {
$arrFields[$strKey] = fxImportResultatsNormalizeSex($strRaw);
} elseif ($strKey === 'par_naissance') {
$arrFields[$strKey] = fxImportResultatsNormalizeBirthdate($strRaw);
} elseif ($strKey === 'par_age' || $strKey === 'par_no_equipe') {
$arrFields[$strKey] = intval($strRaw);
} else {
$arrFields[$strKey] = $strRaw;
}
}
if (intval($arrFields['par_age']) <= 0 && $arrFields['par_naissance'] !== '') {
$intTs = strtotime($arrFields['par_naissance']);
if ($intTs !== false) {
$arrFields['par_age'] = intval(floor((time() - $intTs) / 31557600));
}
}
// Upsert participant import
$sqlExist = "SELECT par_id, pec_id, par_id_original FROM resultats_participants"
. " WHERE eve_id = " . $intEveId
. " AND par_external_id = '" . fxImportResultatsDbEsc($strExt) . "'"
. " LIMIT 1";
$tabExist = $objDatabase->fxGetResults($sqlExist);
$blnUpdate = (is_array($tabExist) && !empty($tabExist[1]['par_id']));
if ($blnUpdate) {
$intParId = intval($tabExist[1]['par_id']);
$intPecId = intval($tabExist[1]['pec_id']);
$intParOrig = intval($tabExist[1]['par_id_original']);
if ($intParOrig <= 0) {
$intParOrig = $intParId;
}
if ($intPecId > 0) {
$sqlPec = "UPDATE resultats_epreuves_commandees SET"
. " epr_id = " . $intEprId . ","
. " pec_maj = '" . fxImportResultatsDbEsc($strNow) . "'"
. " WHERE pec_id = " . $intPecId;
$objDatabase->fxQuery($sqlPec);
}
$sqlUp = "UPDATE resultats_participants SET"
. " epr_id = " . $intEprId . ","
. " par_prenom = '" . fxImportResultatsDbEsc($arrFields['par_prenom']) . "',"
. " par_nom = '" . fxImportResultatsDbEsc($arrFields['par_nom']) . "',"
. " par_sexe = '" . fxImportResultatsDbEsc($arrFields['par_sexe']) . "',"
. " par_naissance = " . ($arrFields['par_naissance'] !== '' ? ("'" . fxImportResultatsDbEsc($arrFields['par_naissance']) . "'") : "NULL") . ","
. " par_age = " . intval($arrFields['par_age']) . ","
. " par_courriel = '" . fxImportResultatsDbEsc($arrFields['par_courriel']) . "',"
. " par_telephone1 = '" . fxImportResultatsDbEsc($arrFields['par_telephone1']) . "',"
. " par_adresse = '" . fxImportResultatsDbEsc($arrFields['par_adresse']) . "',"
. " par_ville = '" . fxImportResultatsDbEsc($arrFields['par_ville']) . "',"
. " par_codepostal = '" . fxImportResultatsDbEsc($arrFields['par_codepostal']) . "',"
. " no_bib = '" . fxImportResultatsDbEsc($arrFields['no_bib']) . "',"
. " par_nom_equipe = '" . fxImportResultatsDbEsc($arrFields['par_nom_equipe']) . "',"
. " par_no_equipe = " . intval($arrFields['par_no_equipe']) . ","
. " par_origine = '" . fxImportResultatsDbEsc($strOrigine) . "',"
. " par_maj = '" . fxImportResultatsDbEsc($strNow) . "'"
. " WHERE par_id = " . $intParId;
if ($objDatabase->fxQuery($sqlUp) === false) {
$intSkipped++;
if (count($tabRowErrors) < 15) {
$tabRowErrors[] = 'Ligne ' . ($intRowNum + 2) . ' : échec UPDATE';
}
continue;
}
// urgence si colonnes présentes (ignorer erreur silencieuse)
$sqlUrg = "UPDATE resultats_participants SET"
. " par_contact_urgence_nom = '" . fxImportResultatsDbEsc($arrFields['par_contact_urgence_nom']) . "',"
. " par_contact_urgence_telephone = '" . fxImportResultatsDbEsc($arrFields['par_contact_urgence_telephone']) . "'"
. " WHERE par_id = " . $intParId;
@$objDatabase->fxQuery($sqlUrg);
$intUpdated++;
} else {
// MSIN-4482 — identifiants techniques uniques (pas un vrai panier/paiement)
$strNoPanier = fxImportResultatsSyntheticPanier($intEveId, $strExt);
$strNoCommande = $strNoPanier;
$sqlPec = "INSERT INTO resultats_epreuves_commandees SET"
. " eve_id = " . $intEveId . ","
. " epr_id = " . $intEprId . ","
. " no_panier = '" . fxImportResultatsDbEsc($strNoPanier) . "',"
. " pec_label = '',"
. " pec_nom_equipe = '" . fxImportResultatsDbEsc($arrFields['par_nom_equipe']) . "',"
. " pec_equipe = 0,"
. " com_id = 0,"
. " pec_maj = '" . fxImportResultatsDbEsc($strNow) . "',"
. " pec_id_original = 0,"
. " no_commande = '" . fxImportResultatsDbEsc($strNoCommande) . "',"
. " is_cancelled = 0";
if ($objDatabase->fxQuery($sqlPec) === false) {
$intSkipped++;
if (count($tabRowErrors) < 15) {
$strErr = fxImportResultatsDbError();
$tabRowErrors[] = 'Ligne ' . ($intRowNum + 2) . ' : échec INSERT pec'
. ($strErr !== '' ? (' — ' . $strErr) : '');
}
continue;
}
$intPecId = intval($objDatabase->fxGetLastId());
$objDatabase->fxQuery(
"UPDATE resultats_epreuves_commandees SET pec_id_original = " . $intPecId
. " WHERE pec_id = " . $intPecId
);
$sqlPar = "INSERT INTO resultats_participants SET"
. " eve_id = " . $intEveId . ","
. " epr_id = " . $intEprId . ","
. " par_prenom = '" . fxImportResultatsDbEsc($arrFields['par_prenom']) . "',"
. " par_nom = '" . fxImportResultatsDbEsc($arrFields['par_nom']) . "',"
. " par_naissance = " . ($arrFields['par_naissance'] !== '' ? ("'" . fxImportResultatsDbEsc($arrFields['par_naissance']) . "'") : "NULL") . ","
. " par_age = " . intval($arrFields['par_age']) . ","
. " par_sexe = '" . fxImportResultatsDbEsc($arrFields['par_sexe']) . "',"
. " par_adresse = '" . fxImportResultatsDbEsc($arrFields['par_adresse']) . "',"
. " par_ville = '" . fxImportResultatsDbEsc($arrFields['par_ville']) . "',"
. " pro_id = 0,"
. " par_codepostal = '" . fxImportResultatsDbEsc($arrFields['par_codepostal']) . "',"
. " pay_id = 0,"
. " par_telephone1 = '" . fxImportResultatsDbEsc($arrFields['par_telephone1']) . "',"
. " par_courriel = '" . fxImportResultatsDbEsc($arrFields['par_courriel']) . "',"
. " com_id = 0,"
. " par_nom_equipe = '" . fxImportResultatsDbEsc($arrFields['par_nom_equipe']) . "',"
. " no_panier = '" . fxImportResultatsDbEsc($strNoPanier) . "',"
. " par_maj = '" . fxImportResultatsDbEsc($strNow) . "',"
. " no_commande = '" . fxImportResultatsDbEsc($strNoCommande) . "',"
. " par_id_original = 0,"
. " is_cancelled = 0,"
. " par_no_equipe = " . intval($arrFields['par_no_equipe']) . ","
. " par_equipe = 0,"
. " pec_id = " . $intPecId . ","
. " rol_id = 2,"
. " no_bib = '" . fxImportResultatsDbEsc($arrFields['no_bib']) . "',"
. " par_external_id = '" . fxImportResultatsDbEsc($strExt) . "',"
. " par_origine = '" . fxImportResultatsDbEsc($strOrigine) . "'";
if ($objDatabase->fxQuery($sqlPar) === false) {
$intSkipped++;
if (count($tabRowErrors) < 15) {
$strErr = fxImportResultatsDbError();
$tabRowErrors[] = 'Ligne ' . ($intRowNum + 2) . ' : échec INSERT participant'
. ($strErr !== '' ? (' — ' . $strErr) : '');
}
continue;
}
$intParId = intval($objDatabase->fxGetLastId());
$intParOrig = $intParId;
$objDatabase->fxQuery(
"UPDATE resultats_participants SET par_id_original = " . $intParId
. " WHERE par_id = " . $intParId
);
$sqlUrg = "UPDATE resultats_participants SET"
. " par_contact_urgence_nom = '" . fxImportResultatsDbEsc($arrFields['par_contact_urgence_nom']) . "',"
. " par_contact_urgence_telephone = '" . fxImportResultatsDbEsc($arrFields['par_contact_urgence_telephone']) . "'"
. " WHERE par_id = " . $intParId;
@$objDatabase->fxQuery($sqlUrg);
$intCreated++;
}
foreach ($tabQueIds as $strCol => $intQueId) {
$intCol = intval($strCol);
$strVal = trim((string)($tabRow[$intCol] ?? ''));
$strH = (string)($tabHeaders[$intCol] ?? '');
fxImportResultatsSaveQuestionAnswer($intParOrig, $intPecId, $intQueId, $strH, $strVal);
}
}
$strMsg = 'Import terminé : ' . $intCreated . ' créé(s), ' . $intUpdated . ' mis à jour, '
. $intSkipped . ' ignoré(s) sur ' . count($tabRows) . ' ligne(s).';
if (count($tabRowErrors) > 0) {
$strMsg .= ' Exemples : ' . implode(' · ', $tabRowErrors);
}
return array(
'state' => 'ok',
'message' => $strMsg,
'created' => $intCreated,
'updated' => $intUpdated,
'skipped' => $intSkipped,
'row_count' => count($tabRows),
'errors_sample' => $tabRowErrors,
);
}
/**
* Panneau UI fiche événement superadm.
*/
function fxImportResultatsRenderEventPanel($intEveId) {
global $vDomaine;
$intEveId = intval($intEveId);
if ($intEveId <= 0) {
return;
}
$strAjax = rtrim((string)$vDomaine, '/') . '/superadm/ajax_import_resultats.php';
$tabEpreuves = fxImportResultatsEpreuvesList($intEveId);
$intNbEpr = count($tabEpreuves);
require_once dirname(__FILE__) . '/inc_fx_superadm_v2_ui.php';
fxSuperadmV2PanelOpen('Import participants (hors transaction)', array(
'collapse_id' => 'importResultatsCollapse',
'zone_id' => 'import_resultats',
'zone_class' => 'superadm-content-v2-zone mt-3 mb-2',
'expanded' => true,
'header_extra' => '<span class="superadm-v2-panel__header-extra ml-2"><span class="badge badge-light">MSIN-4482</span></span>',
));
?>
<div id="ms1-import-resultats" class="ms1-import"
data-eve-id="<?php echo $intEveId; ?>"
data-ajax="<?php echo fxImportResultatsEsc($strAjax); ?>">
<p class="text-muted small mb-3">
Déposez le fichier client (CSV / Excel). Alignez les colonnes une fois —
le <strong>template</strong> est sauvegardé pour les prochains imports du même format.
Incluez une colonne <strong>épreuve</strong> et une <strong>clé unique</strong> (ChronoTrack).
<?php if ($intNbEpr < 1) { ?>
<br><span class="text-danger">Aucune épreuve sur cet événement — créez-en avant d'importer.</span>
<?php } else { ?>
<br><?php echo intval($intNbEpr); ?> épreuve(s) disponible(s) pour le mapping.
<?php } ?>
</p>
<div class="ms1-import__drop" id="ms1_import_drop" tabindex="0" role="button"
aria-label="Zone de dépôt du fichier d'import">
<div class="ms1-import__drop-icon" aria-hidden="true"><i class="fa fa-cloud-upload"></i></div>
<div class="ms1-import__drop-title">Glissez-déposez votre fichier ici</div>
<div class="ms1-import__drop-sub">ou cliquez pour parcourir · CSV, XLS, XLSX</div>
<input type="file" id="ms1_import_file" accept=".csv,.txt,.xls,.xlsx,text/csv,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" hidden>
</div>
<div id="ms1_import_status" class="ms1-import__status mt-2" hidden></div>
<div id="ms1_import_step_map" class="ms1-import__step mt-3" hidden>
<div class="d-flex flex-wrap align-items-center justify-content-between mb-2">
<div>
<strong id="ms1_import_file_label"></strong>
<span class="text-muted small" id="ms1_import_row_label"></span>
</div>
<button type="button" class="btn btn-sm btn-outline-secondary" id="ms1_import_reset">Autre fichier</button>
</div>
<div id="ms1_import_templates" class="ms1-import__templates mb-3" hidden></div>
<h6 class="ms1-import__h">1. Colonnes → champs MS1</h6>
<div class="table-responsive">
<table class="table table-sm table-bordered mb-3" id="ms1_import_map_table">
<thead>
<tr>
<th>Colonne fichier</th>
<th>Aperçu</th>
<th>Champ MS1</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div id="ms1_import_epr_box" class="ms1-import__epr mb-3" hidden>
<h6 class="ms1-import__h">2. Valeurs d'épreuve → épreuves de l'événement</h6>
<div class="table-responsive">
<table class="table table-sm table-bordered mb-0" id="ms1_import_epr_table">
<thead>
<tr>
<th>Valeur dans le fichier</th>
<th>Épreuve MS1</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div id="ms1_import_que_box" class="ms1-import__que mb-3" hidden>
<h6 class="ms1-import__h">3. Questions (option C — match ou créer)</h6>
<p class="small text-muted mb-2">Colonnes non mappées à un champ standard peuvent être liées à une question existante ou marquées « Créer à l'import ».</p>
<div class="table-responsive">
<table class="table table-sm table-bordered mb-0" id="ms1_import_que_table">
<thead>
<tr>
<th>Colonne</th>
<th>Question</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="ms1-import__actions form-inline flex-wrap">
<label class="mr-2 mb-2" for="ms1_import_tpl_name">Nom du template</label>
<input type="text" class="form-control form-control-sm mr-2 mb-2" id="ms1_import_tpl_name"
placeholder="ex. Format client ACME" style="min-width:14rem">
<button type="button" class="btn btn-outline-primary btn-sm mb-2 mr-2" id="ms1_import_save_tpl">
<i class="fa fa-save" aria-hidden="true"></i> Enregistrer le template
</button>
<button type="button" class="btn btn-outline-secondary btn-sm mb-2 mr-2" id="ms1_import_validate">
<i class="fa fa-check" aria-hidden="true"></i> Valider le mapping
</button>
<button type="button" class="btn btn-success btn-sm mb-2" id="ms1_import_commit">
<i class="fa fa-play" aria-hidden="true"></i> Importer dans MS1
</button>
</div>
</div>
</div>
<script>
(function ($) {
var $root = $('#ms1-import-resultats');
if (!$root.length) return;
var eveId = parseInt($root.data('eve-id'), 10) || 0;
var ajaxUrl = String($root.data('ajax') || 'ajax_import_resultats.php');
var state = null;
function showStatus(type, msg) {
var $s = $('#ms1_import_status');
$s.removeAttr('hidden').removeClass('alert-success alert-danger alert-info alert-warning')
.addClass('alert alert-' + (type || 'info')).text(msg || '');
}
function parseAjaxJson(raw) {
if (raw && typeof raw === 'object') return raw;
var s = String(raw || '');
var i = s.indexOf('{');
var j = s.lastIndexOf('}');
if (i < 0 || j <= i) return null;
try {
return JSON.parse(s.substring(i, j + 1));
} catch (e) {
return null;
}
}
function ensurePanelOpen() {
var $c = $('#importResultatsCollapse');
if ($c.length && !$c.hasClass('show') && window.jQuery) {
$c.collapse('show');
}
if ($root.length && $root[0].scrollIntoView) {
try { $root[0].scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (e) {}
}
}
function resetUi() {
state = null;
$('#ms1_import_step_map').attr('hidden', true);
$('#ms1_import_templates').attr('hidden', true).empty();
$('#ms1_import_epr_box').attr('hidden', true);
$('#ms1_import_que_box').attr('hidden', true);
$('#ms1_import_file').val('');
$('#ms1_import_drop').removeClass('is-busy is-ready');
}
function esc(s) {
return $('<div/>').text(s == null ? '' : String(s)).html();
}
function collectMapping() {
var columns = [];
$('#ms1_import_map_table tbody select.js-col-map').each(function () {
columns.push($(this).val() || '');
});
var epr_values = {};
$('#ms1_import_epr_table tbody tr').each(function () {
var v = $(this).attr('data-val');
var epr = parseInt($(this).find('select').val(), 10) || 0;
if (v != null) epr_values[v] = epr;
});
var questions = {};
$('#ms1_import_que_table tbody tr').each(function () {
var idx = $(this).attr('data-col');
var q = $(this).find('select').val() || '';
if (idx != null && q !== '') questions[String(idx)] = q;
});
return { columns: columns, epr_values: epr_values, questions: questions };
}
function fieldOptionsHtml(selected) {
if (!state || !state.fields) return '';
var html = '';
var lastGroup = null;
state.fields.forEach(function (f) {
if (f.group && f.group !== lastGroup) {
if (lastGroup !== null) html += '</optgroup>';
html += '<optgroup label="' + esc(f.group) + '">';
lastGroup = f.group;
}
if (!f.group && lastGroup !== null) {
html += '</optgroup>';
lastGroup = null;
}
var sel = (String(f.key) === String(selected || '')) ? ' selected' : '';
html += '<option value="' + esc(f.key) + '"' + sel + '>' + esc(f.label) + '</option>';
});
if (lastGroup !== null) html += '</optgroup>';
return html;
}
function eprOptionsHtml(selected) {
var html = '<option value="0">— Choisir —</option>';
(state.epreuves || []).forEach(function (e) {
var sel = (parseInt(e.epr_id, 10) === parseInt(selected, 10)) ? ' selected' : '';
html += '<option value="' + esc(e.epr_id) + '"' + sel + '>' + esc(e.label) + '</option>';
});
return html;
}
function queOptionsHtml(selected) {
var html = '<option value="">— Pas une question —</option>';
html += '<option value="new"' + (selected === 'new' ? ' selected' : '') + '>Créer à l\'import (confirmé)</option>';
(state.questions || []).forEach(function (q) {
var sel = (String(q.que_id) === String(selected)) ? ' selected' : '';
html += '<option value="' + esc(q.que_id) + '"' + sel + '>' + esc(q.label) + '</option>';
});
return html;
}
function renderEprBox() {
var eprCol = -1;
$('#ms1_import_map_table tbody select.js-col-map').each(function (i) {
if ($(this).val() === 'epr') eprCol = i;
});
var $box = $('#ms1_import_epr_box');
var $tb = $('#ms1_import_epr_table tbody').empty();
if (eprCol < 0) {
$box.attr('hidden', true);
return;
}
var vals = (state.distincts && state.distincts[eprCol]) ? state.distincts[eprCol] : [];
var saved = (state.appliedMapping && state.appliedMapping.epr_values) ? state.appliedMapping.epr_values : {};
vals.forEach(function (v) {
var cur = saved[v] || 0;
$tb.append(
'<tr data-val="' + esc(v) + '"><td><code>' + esc(v) + '</code></td>'
+ '<td><select class="form-control form-control-sm">' + eprOptionsHtml(cur) + '</select></td></tr>'
);
});
$box.removeAttr('hidden');
}
function renderQueBox() {
var $box = $('#ms1_import_que_box');
var $tb = $('#ms1_import_que_table tbody').empty();
var saved = (state.appliedMapping && state.appliedMapping.questions) ? state.appliedMapping.questions : {};
var any = false;
$('#ms1_import_map_table tbody select.js-col-map').each(function (i) {
if (($(this).val() || '') !== '') return;
any = true;
var h = state.headers[i] || ('Col ' + (i + 1));
var cur = saved[String(i)] || saved[i] || '';
$tb.append(
'<tr data-col="' + i + '"><td>' + esc(h) + '</td>'
+ '<td><select class="form-control form-control-sm js-que-map">' + queOptionsHtml(cur) + '</select></td></tr>'
);
});
if (any) $box.removeAttr('hidden');
else $box.attr('hidden', true);
}
function applyMappingObj(mapping) {
if (!mapping || !mapping.columns) return;
state.appliedMapping = mapping;
$('#ms1_import_map_table tbody select.js-col-map').each(function (i) {
if (mapping.columns[i] != null) {
$(this).val(String(mapping.columns[i]));
}
});
renderEprBox();
renderQueBox();
}
function renderMap() {
var $tb = $('#ms1_import_map_table tbody').empty();
(state.headers || []).forEach(function (h, i) {
var sample = '';
if (state.samples && state.samples[0]) {
sample = state.samples[0][i] != null ? String(state.samples[0][i]) : '';
}
var guess = (state.guess && state.guess[i]) ? state.guess[i] : '';
$tb.append(
'<tr><td><strong>' + esc(h) + '</strong></td>'
+ '<td class="small text-muted">' + esc(sample) + '</td>'
+ '<td><select class="form-control form-control-sm js-col-map" data-col="' + i + '">'
+ fieldOptionsHtml(guess) + '</select></td></tr>'
);
});
$tb.off('change', 'select.js-col-map').on('change', 'select.js-col-map', function () {
renderEprBox();
renderQueBox();
});
renderEprBox();
renderQueBox();
var $tpl = $('#ms1_import_templates').empty();
if (state.templates && state.templates.length) {
var html = '<div class="alert alert-success py-2 mb-0"><strong>Template trouvé</strong> pour ces colonnes — '
+ 'réappliquez-le pour éviter de réaligner :</div><div class="list-group mt-2">';
state.templates.forEach(function (t) {
html += '<button type="button" class="list-group-item list-group-item-action js-apply-tpl" data-tpl="'
+ esc(t.tpl_id) + '"><i class="fa fa-magic" aria-hidden="true"></i> '
+ esc(t.name || ('Template #' + t.tpl_id))
+ ' <span class="text-muted small">(' + esc(t.maj || '') + (t.same_event ? '' : ' · autre événement même org') + ')</span></button>';
});
html += '</div>';
$tpl.html(html).removeAttr('hidden');
$tpl.find('.js-apply-tpl').on('click', function () {
var id = parseInt($(this).data('tpl'), 10);
var found = null;
(state.templates || []).forEach(function (t) {
if (parseInt(t.tpl_id, 10) === id) found = t;
});
if (found && found.mapping) {
applyMappingObj(found.mapping);
$('#ms1_import_tpl_name').val(found.name || '');
showStatus('success', 'Template « ' + (found.name || id) + ' » appliqué.');
}
});
} else {
$tpl.attr('hidden', true);
}
$('#ms1_import_file_label').text(state.original_name || 'Fichier');
$('#ms1_import_row_label').text(' · ' + (state.row_count || 0) + ' ligne(s)');
$('#ms1_import_step_map').removeAttr('hidden').show();
$('#ms1_import_drop').addClass('is-ready');
ensurePanelOpen();
}
function uploadFile(file) {
if (!file) return;
var fd = new FormData();
fd.append('a', 'upload');
fd.append('eve_id', String(eveId));
fd.append('file', file);
$('#ms1_import_drop').addClass('is-busy');
showStatus('info', 'Lecture du fichier…');
$.ajax({
url: ajaxUrl,
method: 'POST',
data: fd,
processData: false,
contentType: false,
dataType: 'text'
}).done(function (raw) {
$('#ms1_import_drop').removeClass('is-busy');
var res = parseAjaxJson(raw);
if (!res || res.state !== 'ok') {
showStatus('danger', (res && res.message) ? res.message : 'Échec lecture (réponse invalide)');
return;
}
state = res;
state.appliedMapping = null;
showStatus('success', 'Fichier lu — ' + (res.row_count || 0) + ' ligne(s). Alignez les colonnes ci-dessous.');
renderMap();
}).fail(function (xhr) {
$('#ms1_import_drop').removeClass('is-busy');
var res = parseAjaxJson(xhr && xhr.responseText);
if (res && res.state === 'ok') {
state = res;
state.appliedMapping = null;
showStatus('success', 'Fichier lu — ' + (res.row_count || 0) + ' ligne(s).');
renderMap();
return;
}
showStatus('danger', 'Erreur upload (HTTP ' + (xhr && xhr.status ? xhr.status : '?') + ').');
});
}
var $drop = $('#ms1_import_drop');
var $file = $('#ms1_import_file');
$drop.on('click', function (e) {
if ($(e.target).is('input')) return;
$file.trigger('click');
});
$drop.on('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
$file.trigger('click');
}
});
$file.on('change', function () {
if (this.files && this.files[0]) uploadFile(this.files[0]);
});
$drop.on('drag dragstart dragend dragover dragenter dragleave drop', function (e) {
e.preventDefault();
e.stopPropagation();
});
$drop.on('dragover dragenter', function () { $drop.addClass('is-drag'); });
$drop.on('dragleave dragend drop', function () { $drop.removeClass('is-drag'); });
$drop.on('drop', function (e) {
var files = e.originalEvent && e.originalEvent.dataTransfer
? e.originalEvent.dataTransfer.files : null;
if (files && files[0]) uploadFile(files[0]);
});
$('#ms1_import_reset').on('click', function () {
resetUi();
showStatus('info', 'Choisissez un nouveau fichier.');
});
$('#ms1_import_save_tpl').on('click', function () {
if (!state || !state.token) return;
var mapping = collectMapping();
$.ajax({
url: ajaxUrl,
method: 'POST',
dataType: 'text',
data: {
a: 'save_template',
eve_id: eveId,
token: state.token,
tpl_name: $('#ms1_import_tpl_name').val() || '',
mapping: JSON.stringify(mapping)
}
}).done(function (raw) {
var res = parseAjaxJson(raw);
if (!res || res.state !== 'ok') {
showStatus('danger', (res && res.message) ? res.message : 'Échec sauvegarde template');
return;
}
state.templates = res.templates || state.templates;
showStatus('success', res.message || 'Template enregistré.');
renderMap();
applyMappingObj(mapping);
}).fail(function () {
showStatus('danger', 'Erreur réseau (template).');
});
});
$('#ms1_import_validate').on('click', function () {
if (!state || !state.token) return;
var mapping = collectMapping();
$.ajax({
url: ajaxUrl,
method: 'POST',
dataType: 'text',
data: {
a: 'validate',
eve_id: eveId,
token: state.token,
mapping: JSON.stringify(mapping)
}
}).done(function (raw) {
var res = parseAjaxJson(raw);
if (!res || res.state !== 'ok') {
showStatus('warning', (res && res.message) ? res.message : 'Mapping incomplet');
return;
}
showStatus('success', res.message || 'OK');
}).fail(function () {
showStatus('danger', 'Erreur réseau (validation).');
});
});
$('#ms1_import_commit').on('click', function () {
if (!state || !state.token) return;
var mapping = collectMapping();
if (!window.confirm('Importer / mettre à jour les participants dans resultats_* pour cet événement ?\n(Sans panier ni paiement — upsert sur la clé unique.)')) {
return;
}
var $btn = $(this);
$btn.prop('disabled', true);
showStatus('info', 'Import en cours — cela peut prendre une minute pour un gros fichier…');
$.ajax({
url: ajaxUrl,
method: 'POST',
dataType: 'text',
data: {
a: 'commit',
eve_id: eveId,
token: state.token,
mapping: JSON.stringify(mapping)
}
}).done(function (raw) {
$btn.prop('disabled', false);
var res = parseAjaxJson(raw);
if (!res || res.state !== 'ok') {
showStatus('danger', (res && res.message) ? res.message : 'Échec import');
return;
}
showStatus('success', res.message || 'Import terminé.');
}).fail(function (xhr) {
$btn.prop('disabled', false);
var res = parseAjaxJson(xhr && xhr.responseText);
if (res && res.state === 'ok') {
showStatus('success', res.message || 'Import terminé.');
return;
}
showStatus('danger', 'Erreur réseau (import). Timeout possible sur très gros fichiers — réessayez ou coupez le fichier.');
});
});
})(window.jQuery);
</script>
<?php
fxSuperadmV2PanelClose(array('collapse_id' => 'importResultatsCollapse'));
}