1724 lines
72 KiB
PHP
1724 lines
72 KiB
PHP
<?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 1–3 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'));
|
||
}
|