Files
ms1inscription-v5/php/inc_fx_import_resultats.php

2738 lines
119 KiB
PHP
Raw Permalink 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');
}
if (!defined('MSIN_TE_NOM_SANS_INSCRIPTION_EN_LIGNE')) {
define('MSIN_TE_NOM_SANS_INSCRIPTION_EN_LIGNE', 'Sans inscription en ligne');
}
/**
* MSIN-4482 — te_id du type « Sans inscription en ligne » (0 si pas encore créé en BD).
*/
function fxImportResultatsTeIdSansInscriptionEnLigne() {
global $objDatabase;
static $intTeId = null;
if ($intTeId !== null) {
return $intTeId;
}
$intTeId = 0;
$sql = "SELECT te_id FROM inscriptions_types_evenements"
. " WHERE te_nom_fr = '" . fxImportResultatsDbEsc(MSIN_TE_NOM_SANS_INSCRIPTION_EN_LIGNE) . "'"
. " AND te_actif = 1 LIMIT 1";
$tab = $objDatabase->fxGetResults($sql);
if (is_array($tab) && !empty($tab[1]['te_id'])) {
$intTeId = intval($tab[1]['te_id']);
}
return $intTeId;
}
/**
* MSIN-4482 — true si l'événement est typé hors inscription en ligne.
*/
function fxImportResultatsEveIsSansInscriptionEnLigne($intEveId) {
global $objDatabase;
$intEveId = intval($intEveId);
$intTeNeed = fxImportResultatsTeIdSansInscriptionEnLigne();
if ($intEveId <= 0 || $intTeNeed <= 0) {
return false;
}
$sql = "SELECT te_id FROM inscriptions_evenements WHERE eve_id = " . $intEveId . " LIMIT 1";
$tab = $objDatabase->fxGetResults($sql);
if (!is_array($tab) || empty($tab[1]['te_id'])) {
return false;
}
return intval($tab[1]['te_id']) === $intTeNeed;
}
/**
* MSIN-4482 — refuse les actions import si l'eve n'est pas du bon type (protège le business payant).
* @return array|null null = OK ; sinon payload erreur JSON
*/
function fxImportResultatsGuardEveSansInscription($intEveId) {
$intEveId = intval($intEveId);
if ($intEveId <= 0) {
return array('state' => 'error', 'message' => 'eve_id manquant');
}
$intTe = fxImportResultatsTeIdSansInscriptionEnLigne();
if ($intTe <= 0) {
return array(
'state' => 'error',
'message' => 'Type « Sans inscription en ligne » absent en BD — exécuter sql/MSIN-4482-type-sans-inscription-en-ligne.sql en dev préprod.',
);
}
if (!fxImportResultatsEveIsSansInscriptionEnLigne($intEveId)) {
return array(
'state' => 'error',
'message' => 'Import hors transaction réservé aux événements de type « Sans inscription en ligne ». Changez le Type d\'événement sur la fiche, enregistrez, puis réessayez.',
'te_id_required' => $intTe,
);
}
return null;
}
/**
* 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);
}
/**
* MSIN-4482 — pec_id_original / par_id_original ont un UNIQUE : jamais inserer 0.
* Valeur temporaire unique, puis UPDATE = vrai id apres INSERT.
*/
function fxImportResultatsTempOriginalId($strExternalId, $strSalt = '') {
$intCrc = sprintf('%u', crc32((string)$strExternalId . '|' . (string)$strSalt));
// Plage haute pour limiter collision avec vrais pec_id / par_id historiques
return 1900000000 + intval($intCrc % 90000000);
}
/**
* MSIN-4482 — même modèle que fxChronotrackApiSyncGroupBlockedByType.
*/
function fxImportResultatsGroupBlockedByType(array $tabBlocked) {
$tabTypeLabels = array(
'header_row' => 'Ligne en-tête (ignorée)',
'no_external' => 'Clé unique manquante',
'no_race' => 'Épreuve non mappée',
'insert_pec' => 'Échec création commande (pec)',
'insert_par' => 'Échec création participant',
'update_fail' => 'Échec mise à jour',
'other' => 'Autre',
);
$tabTypeOrder = array(
'no_external', 'no_race', 'insert_pec', 'insert_par', 'update_fail', 'header_row', 'other',
);
$tabGroups = array();
foreach ($tabBlocked as $arrItem) {
$strCode = (string)($arrItem['code'] ?? 'other');
if (!isset($tabGroups[$strCode])) {
$tabGroups[$strCode] = array(
'code' => $strCode,
'label' => isset($tabTypeLabels[$strCode]) ? $tabTypeLabels[$strCode] : $strCode,
'items' => array(),
);
}
$tabGroups[$strCode]['items'][] = $arrItem;
}
$tabOrdered = array();
foreach ($tabTypeOrder as $strCode) {
if (isset($tabGroups[$strCode])) {
$tabOrdered[] = $tabGroups[$strCode];
unset($tabGroups[$strCode]);
}
}
foreach ($tabGroups as $arrGroup) {
$tabOrdered[] = $arrGroup;
}
return $tabOrdered;
}
function fxImportResultatsRowLabel(array $tabRow, array $tabColIndex) {
$strPrenom = isset($tabColIndex['par_prenom']) ? trim((string)($tabRow[$tabColIndex['par_prenom']] ?? '')) : '';
$strNom = isset($tabColIndex['par_nom']) ? trim((string)($tabRow[$tabColIndex['par_nom']] ?? '')) : '';
$str = trim($strPrenom . ' ' . $strNom);
return ($str !== '') ? $str : '—';
}
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,"
. " que_cart_show = 1,"
. " que_cart_label_fr = '" . fxImportResultatsDbEsc($strLabel) . "',"
. " que_cart_label_en = '" . fxImportResultatsDbEsc($strLabel) . "'";
if ($objDatabase->fxQuery($sqlIns) === false) {
continue;
}
$tabOut[(string)$intCol] = intval($objDatabase->fxGetLastId());
continue;
}
$intQueId = intval($strQue);
if ($intQueId > 0) {
$tabOut[(string)$intCol] = $intQueId;
}
}
fxImportResultatsEnsureQuestionsVisibleFiche($intEveId, $tabOut);
return $tabOut;
}
/**
* MSIN-4482 — la fiche exige que_cart_show=1 ; activer les questions utilisées à l'import
* (matchées OU créées), sans toucher aux autres événements.
*/
function fxImportResultatsEnsureQuestionsVisibleFiche($intEveId, array $tabQueIdsByCol) {
global $objDatabase;
$intEveId = intval($intEveId);
$tabIds = array();
foreach ($tabQueIdsByCol as $mixId) {
$intId = intval($mixId);
if ($intId > 0) {
$tabIds[$intId] = true;
}
}
if ($intEveId <= 0 || count($tabIds) < 1) {
return;
}
$strIn = implode(',', array_map('intval', array_keys($tabIds)));
$objDatabase->fxQuery(
"UPDATE inscriptions_questions SET"
. " que_cart_show = 1,"
. " que_cart_label_fr = IF(TRIM(IFNULL(que_cart_label_fr, '')) = '', que_question_fr, que_cart_label_fr),"
. " que_cart_label_en = IF("
. " TRIM(IFNULL(que_cart_label_en, '')) = '',"
. " IF(TRIM(IFNULL(que_question_en, '')) = '', que_question_fr, que_question_en),"
. " que_cart_label_en"
. " )"
. " WHERE eve_id = " . $intEveId
. " AND que_id IN (" . $strIn . ")"
);
}
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);
@$objDatabase->fxQuery(
"UPDATE resultats_questions SET que_actif = 1 WHERE pqu_id = " . intval($tabFind[1]['pqu_id'])
);
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";
if ($objDatabase->fxQuery($sql) === false) {
return;
}
$intPqu = intval($objDatabase->fxGetLastId());
if ($intPqu > 0) {
@$objDatabase->fxQuery(
"UPDATE resultats_questions SET que_actif = 1 WHERE pqu_id = " . $intPqu
);
}
}
/**
* 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;
$tabBlocked = array();
foreach ($tabRows as $intRowNum => $tabRow) {
$intLine = $intRowNum + 2; // +1 header Excel, +1 index 0
$strNamePreview = fxImportResultatsRowLabel($tabRow, $tabColIndex);
$strBibPreview = isset($tabColIndex['no_bib']) ? trim((string)($tabRow[$tabColIndex['no_bib']] ?? '')) : '';
$strSexePreview = '';
if (isset($tabColIndex['par_sexe'])) {
$strSexePreview = fxImportResultatsNormalizeSex(trim((string)($tabRow[$tabColIndex['par_sexe']] ?? '')));
}
if (fxImportResultatsRowLooksLikeHeader($tabRow, $tabHeaders, $tabColIndex)) {
$tabBlocked[] = array(
'code' => 'header_row',
'message' => 'Ligne d\'en-tête détectée',
'line' => $intLine,
'name' => $strNamePreview,
'external_id' => '',
'no_bib' => $strBibPreview,
'par_sexe' => $strSexePreview,
);
continue;
}
$strExt = '';
if (isset($tabColIndex['par_external_id'])) {
$strExt = trim((string)($tabRow[$tabColIndex['par_external_id']] ?? ''));
}
if ($strExt === '') {
$tabBlocked[] = array(
'code' => 'no_external',
'message' => 'Clé unique (Attendee # / external_id) vide',
'line' => $intLine,
'name' => $strNamePreview,
'external_id' => '',
'no_bib' => $strBibPreview,
'par_sexe' => $strSexePreview,
);
continue;
}
$strEprRaw = '';
if (isset($tabColIndex['epr'])) {
$strEprRaw = trim((string)($tabRow[$tabColIndex['epr']] ?? ''));
}
$intEprId = intval($tabEprValues[$strEprRaw] ?? 0);
if ($intEprId <= 0) {
$tabBlocked[] = array(
'code' => 'no_race',
'message' => ($strEprRaw !== ''
? ('« ' . $strEprRaw . ' » non mappée vers une épreuve MS1')
: 'Valeur d\'épreuve vide'),
'line' => $intLine,
'name' => $strNamePreview,
'external_id' => $strExt,
'no_bib' => $strBibPreview,
'par_sexe' => $strSexePreview,
);
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) {
// p.pec_id peut être e.pec_id OU e.pec_id_original (selon imports précédents)
$tabPecRow = $objDatabase->fxGetResults(
"SELECT pec_id, pec_id_original FROM resultats_epreuves_commandees"
. " WHERE eve_id = " . $intEveId
. " AND (pec_id = " . $intPecId . " OR pec_id_original = " . $intPecId . ")"
. " LIMIT 1"
);
$intPecPk = is_array($tabPecRow) ? intval($tabPecRow[1]['pec_id'] ?? 0) : 0;
$intPecOrigNow = is_array($tabPecRow) ? intval($tabPecRow[1]['pec_id_original'] ?? 0) : 0;
if ($intPecPk > 0) {
$objDatabase->fxQuery(
"UPDATE resultats_epreuves_commandees SET"
. " epr_id = " . $intEprId . ","
. " pec_actif = 1,"
. " pec_maj = '" . fxImportResultatsDbEsc($strNow) . "'"
. " WHERE pec_id = " . $intPecPk
);
// Tenter pec_id_original = pec_id seulement si UNIQUE libre
@$objDatabase->fxQuery(
"UPDATE resultats_epreuves_commandees e SET pec_id_original = " . $intPecPk
. " WHERE pec_id = " . $intPecPk
. " AND pec_id_original <> " . $intPecPk
. " AND NOT EXISTS ("
. " SELECT 1 FROM ("
. " SELECT pec_id FROM resultats_epreuves_commandees"
. " WHERE pec_id_original = " . $intPecPk
. " AND pec_id <> " . $intPecPk
. " ) _c"
. " )"
);
$tabPecNow = $objDatabase->fxGetResults(
"SELECT pec_id_original FROM resultats_epreuves_commandees WHERE pec_id = "
. $intPecPk . " LIMIT 1"
);
$intPecOrigNow = is_array($tabPecNow)
? intval($tabPecNow[1]['pec_id_original'] ?? 0)
: $intPecOrigNow;
if ($intPecOrigNow > 0) {
$objDatabase->fxQuery(
"UPDATE resultats_participants SET pec_id = " . $intPecOrigNow
. " WHERE par_id = " . $intParId
);
$intPecId = $intPecOrigNow; // clé liste / questions
}
}
}
$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) {
$strErr = fxImportResultatsDbError();
$tabBlocked[] = array(
'code' => 'update_fail',
'message' => 'Échec UPDATE' . ($strErr !== '' ? (' — ' . $strErr) : ''),
'line' => $intLine,
'name' => $strNamePreview,
'external_id' => $strExt,
'no_bib' => $strBibPreview,
'par_sexe' => $strSexePreview,
'par_id' => $intParId,
);
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;
$intTempPecOrig = fxImportResultatsTempOriginalId($strExt, 'pec-' . $intEveId);
$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 = " . intval($intTempPecOrig) . ","
. " no_commande = '" . fxImportResultatsDbEsc($strNoCommande) . "',"
. " is_cancelled = 0,"
. " pec_actif = 1";
if ($objDatabase->fxQuery($sqlPec) === false) {
$strErr = fxImportResultatsDbError();
$tabBlocked[] = array(
'code' => 'insert_pec',
'message' => 'Échec INSERT pec' . ($strErr !== '' ? (' — ' . $strErr) : ''),
'line' => $intLine,
'name' => $strNamePreview,
'external_id' => $strExt,
'no_bib' => $strBibPreview,
'par_sexe' => $strSexePreview,
);
continue;
}
$intPecId = intval($objDatabase->fxGetLastId());
// MSIN-4482 — UNIQUE pec_id_original : n'écraser le temp que si pec_id est libre
$blnOrigOk = false;
$qryOrig = $objDatabase->fxQuery(
"UPDATE resultats_epreuves_commandees SET pec_id_original = " . $intPecId
. " WHERE pec_id = " . $intPecId
. " AND NOT EXISTS ("
. " SELECT 1 FROM ("
. " SELECT pec_id FROM resultats_epreuves_commandees"
. " WHERE pec_id_original = " . $intPecId
. " AND pec_id <> " . $intPecId
. " ) _c"
. " )"
);
if ($qryOrig !== false) {
$tabChk = $objDatabase->fxGetResults(
"SELECT pec_id_original FROM resultats_epreuves_commandees WHERE pec_id = "
. $intPecId . " LIMIT 1"
);
$blnOrigOk = (is_array($tabChk) && intval($tabChk[1]['pec_id_original'] ?? 0) === $intPecId);
}
// Lien participant pour la liste : toujours = pec_id_original effectif
$intPecLink = $blnOrigOk ? $intPecId : intval($intTempPecOrig);
// Questions + participant : clé = pec_id_original (join liste)
$intPecId = $intPecLink;
$intTempParOrig = fxImportResultatsTempOriginalId($strExt, 'par-' . $intEveId);
$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 = " . intval($intTempParOrig) . ","
. " is_cancelled = 0,"
. " par_no_equipe = " . intval($arrFields['par_no_equipe']) . ","
. " par_equipe = 0,"
. " pec_id = " . $intPecLink . ","
. " rol_id = 2,"
. " no_bib = '" . fxImportResultatsDbEsc($arrFields['no_bib']) . "',"
. " par_external_id = '" . fxImportResultatsDbEsc($strExt) . "',"
. " par_origine = '" . fxImportResultatsDbEsc($strOrigine) . "'";
if ($objDatabase->fxQuery($sqlPar) === false) {
$strErr = fxImportResultatsDbError();
$tabBlocked[] = array(
'code' => 'insert_par',
'message' => 'Échec INSERT participant' . ($strErr !== '' ? (' — ' . $strErr) : ''),
'line' => $intLine,
'name' => $strNamePreview,
'external_id' => $strExt,
'no_bib' => $strBibPreview,
'par_sexe' => $strSexePreview,
'pec_id' => $intPecId,
);
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);
}
}
$intBlocked = count($tabBlocked);
$tabBlockedByType = fxImportResultatsGroupBlockedByType($tabBlocked);
$intTotal = count($tabRows);
$strMsg = 'Import terminé : ' . $intCreated . ' créé(s), ' . $intUpdated . ' mis à jour.'
. ($intBlocked > 0 ? (' ' . $intBlocked . ' exclus.') : '');
return array(
'state' => 'ok',
'message' => $strMsg,
'created' => $intCreated,
'updated' => $intUpdated,
'skipped' => $intBlocked,
'row_count' => $intTotal,
'blocked_count' => $intBlocked,
'blocked_by_type' => $tabBlockedByType,
// Compteurs type CT (KPI)
'kpi_total' => $intTotal,
'kpi_ok' => $intCreated + $intUpdated,
'kpi_blocked' => $intBlocked,
'kpi_created' => $intCreated,
'kpi_updated' => $intUpdated,
);
}
/**
* MSIN-4482 — compte ce que le reset effacerait (origine import_fichier seulement).
*/
function fxImportResultatsResetPreview($intEveId) {
global $objDatabase;
$intEveId = intval($intEveId);
if ($intEveId <= 0) {
return array('state' => 'error', 'message' => 'eve_id manquant');
}
$strOrig = fxImportResultatsDbEsc(MSIN_IMPORT_RESULTATS_ORIGIN);
$sqlPar = "SELECT COUNT(*) AS n FROM resultats_participants"
. " WHERE eve_id = " . $intEveId
. " AND par_origine = '" . $strOrig . "'";
$tabPar = $objDatabase->fxGetResults($sqlPar);
$intPar = is_array($tabPar) ? intval($tabPar[1]['n'] ?? 0) : 0;
$sqlPec = "SELECT COUNT(DISTINCT e.pec_id) AS n"
. " FROM resultats_epreuves_commandees e"
. " INNER JOIN resultats_participants p ON p.eve_id = e.eve_id"
. " AND (p.pec_id = e.pec_id OR p.pec_id = e.pec_id_original)"
. " WHERE e.eve_id = " . $intEveId
. " AND p.par_origine = '" . $strOrig . "'";
$tabPec = $objDatabase->fxGetResults($sqlPec);
$intPec = is_array($tabPec) ? intval($tabPec[1]['n'] ?? 0) : 0;
$sqlQue = "SELECT COUNT(*) AS n FROM resultats_questions"
. " WHERE par_id IN ("
. " SELECT pid FROM ("
. " SELECT par_id AS pid FROM resultats_participants"
. " WHERE eve_id = " . $intEveId . " AND par_origine = '" . $strOrig . "'"
. " UNION"
. " SELECT par_id_original AS pid FROM resultats_participants"
. " WHERE eve_id = " . $intEveId . " AND par_origine = '" . $strOrig . "'"
. " AND par_id_original > 0"
. " ) _ms1_q"
. ")";
$tabQue = $objDatabase->fxGetResults($sqlQue);
$intQue = is_array($tabQue) ? intval($tabQue[1]['n'] ?? 0) : 0;
// Diagnostic : ce que la liste inscriptions verrait (même join + pec_actif)
$sqlList = "SELECT COUNT(*) AS n"
. " FROM resultats_epreuves_commandees e, resultats_participants p"
. " WHERE e.eve_id = " . $intEveId
. " AND p.pec_id = e.pec_id_original"
. " AND p.par_origine = '" . $strOrig . "'"
. " AND p.rol_id NOT IN (3, 4)"
. " AND e.is_cancelled = 0 AND e.pec_actif = 1";
$tabList = $objDatabase->fxGetResults($sqlList);
$intList = is_array($tabList) ? intval($tabList[1]['n'] ?? 0) : 0;
return array(
'state' => 'ok',
'eve_id' => $intEveId,
'participants' => $intPar,
'pec' => $intPec,
'questions' => $intQue,
'list_visible' => $intList,
'message' => 'En BD : ' . $intPar . ' participant(s) import, '
. $intPec . ' pec, ' . $intQue . ' réponse(s) questions'
. ' — dont ' . $intList . ' visible(s) dans la liste (pec_actif=1).',
);
}
/**
* MSIN-4482 — efface UNIQUEMENT les données d'import (par_origine=import_fichier) de l'événement.
* Ne touche pas aux inscriptions en ligne / panier réel / inscriptions_questions.
* Exige confirm = "RESET".
*/
function fxImportResultatsReset($intEveId, $strConfirm) {
global $objDatabase;
@set_time_limit(300);
$intEveId = intval($intEveId);
if ($intEveId <= 0) {
return array('state' => 'error', 'message' => 'eve_id manquant');
}
if (trim((string)$strConfirm) !== 'RESET') {
return array(
'state' => 'error',
'message' => 'Confirmation invalide — tapez exactement RESET.',
);
}
$arrPrev = fxImportResultatsResetPreview($intEveId);
if (($arrPrev['state'] ?? '') !== 'ok') {
return $arrPrev;
}
if (intval($arrPrev['participants'] ?? 0) < 1) {
return array(
'state' => 'ok',
'message' => 'Rien à effacer — aucun participant import_fichier sur cet événement.',
'deleted_participants' => 0,
'deleted_pec' => 0,
'deleted_questions' => 0,
);
}
$strOrig = fxImportResultatsDbEsc(MSIN_IMPORT_RESULTATS_ORIGIN);
$intDelQue = 0;
$intDelPar = 0;
$intDelPec = 0;
// Sous-requête IDs participants import (évite JOIN DELETE lents / gros IN PHP)
$strSubPar = "SELECT pid FROM ("
. " SELECT par_id AS pid FROM resultats_participants"
. " WHERE eve_id = " . $intEveId . " AND par_origine = '" . $strOrig . "'"
. " UNION"
. " SELECT par_id_original AS pid FROM resultats_participants"
. " WHERE eve_id = " . $intEveId . " AND par_origine = '" . $strOrig . "'"
. " AND par_id_original > 0"
. ") _ms1_imp_par";
$strSubPec = "SELECT pid FROM ("
. " SELECT pec_id AS pid FROM resultats_participants"
. " WHERE eve_id = " . $intEveId . " AND par_origine = '" . $strOrig . "'"
. " AND pec_id > 0"
. ") _ms1_imp_pec";
// 1) Réponses questions des participants import seulement
$sqlDelQue = "DELETE FROM resultats_questions WHERE par_id IN (" . $strSubPar . ")"
. " OR pec_id IN (" . $strSubPec . ")";
if ($objDatabase->fxQuery($sqlDelQue) !== false) {
$intDelQue = intval($objDatabase->affected_rows);
}
// 2) Produits éventuels
@$objDatabase->fxQuery(
"DELETE FROM resultats_produits_new WHERE par_id IN (" . $strSubPar . ")"
. " OR pec_id IN (" . $strSubPec . ")"
);
// 3) Coquilles pec liées à l'import (avant delete participants)
$sqlDelPec = "DELETE FROM resultats_epreuves_commandees WHERE eve_id = " . $intEveId
. " AND pec_id IN (" . $strSubPec . ")";
if ($objDatabase->fxQuery($sqlDelPec) !== false) {
$intDelPec += intval($objDatabase->affected_rows);
}
// Aussi pec_id_original = pec participant
$sqlDelPecOrig = "DELETE FROM resultats_epreuves_commandees WHERE eve_id = " . $intEveId
. " AND pec_id_original IN (" . $strSubPec . ")";
if ($objDatabase->fxQuery($sqlDelPecOrig) !== false) {
$intDelPec += intval($objDatabase->affected_rows);
}
$strPrefix = 'I' . $intEveId . 'X%';
$sqlDelPec2 = "DELETE FROM resultats_epreuves_commandees WHERE eve_id = " . $intEveId
. " AND no_panier LIKE '" . fxImportResultatsDbEsc($strPrefix) . "'";
if ($objDatabase->fxQuery($sqlDelPec2) !== false) {
$intDelPec += intval($objDatabase->affected_rows);
}
// 4) Participants import
$sqlDelPar = "DELETE FROM resultats_participants WHERE eve_id = " . $intEveId
. " AND par_origine = '" . $strOrig . "'";
if ($objDatabase->fxQuery($sqlDelPar) !== false) {
$intDelPar = intval($objDatabase->affected_rows);
}
return array(
'state' => 'ok',
'message' => 'Reset import OK — participants ' . $intDelPar
. ', pec ' . $intDelPec
. ', réponses questions ' . $intDelQue
. '. Catalogue questions événement intact. Inscriptions en ligne intactes.',
'deleted_participants' => $intDelPar,
'deleted_pec' => $intDelPec,
'deleted_questions' => $intDelQue,
);
}
/**
* Panneau UI fiche événement superadm.
*/
function fxImportResultatsRenderEventPanel($intEveId) {
global $vDomaine;
$intEveId = intval($intEveId);
if ($intEveId <= 0) {
return;
}
$blnOkType = fxImportResultatsEveIsSansInscriptionEnLigne($intEveId);
$intTeReq = fxImportResultatsTeIdSansInscriptionEnLigne();
$strAjax = rtrim((string)$vDomaine, '/') . '/superadm/ajax_import_resultats.php';
$strLoaderGif = rtrim((string)$vDomaine, '/') . '/images/ms1-runner-loader.gif';
$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>',
));
?>
<link rel="stylesheet" href="<?php echo fxImportResultatsEsc(rtrim((string)$vDomaine, '/') . '/css/ms1-loader.css'); ?>?v=<?php echo defined('_VERSION_CODE') ? _VERSION_CODE : '1'; ?>">
<div id="ms1-import-resultats" class="ms1-import"
data-eve-id="<?php echo $intEveId; ?>"
data-eve-ok="<?php echo $blnOkType ? '1' : '0'; ?>"
data-loader-src="<?php echo fxImportResultatsEsc($strLoaderGif); ?>"
data-ajax="<?php echo fxImportResultatsEsc($strAjax); ?>">
<?php if (!$blnOkType) { ?>
<div class="alert alert-warning">
<strong>Type dévénement requis</strong><br>
Limport hors transaction est réservé au type
<strong>Sans inscription en ligne</strong>
<?php if ($intTeReq > 0) { ?>(te_id=<?php echo intval($intTeReq); ?>)<?php } ?>.
Changez le <em>Type dévénement</em> en haut de la fiche, enregistrez, puis rechargez.
<?php if ($intTeReq <= 0) { ?>
<br><span class="text-danger">Le type nexiste pas encore en BD — exécuter
<code>sql/MSIN-4482-type-sans-inscription-en-ligne.sql</code> en dev préprod.</span>
<?php } ?>
</div>
<?php } else { ?>
<div class="alert alert-success py-2">
Type <strong>Sans inscription en ligne</strong> — import autorisé (isolé du flux payant).
</div>
<?php } ?>
<div id="ms1_import_body" <?php echo $blnOkType ? '' : 'hidden'; ?>>
<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 } ?>
<br><span class="text-muted">Reset = efface seulement <code>par_origine=import_fichier</code> sur cet événement (pas les inscriptions payantes).</span>
</p>
<div class="mb-3" id="ms1_import_db_status_wrap">
<div class="d-flex flex-wrap align-items-center justify-content-between mb-2">
<strong class="small mb-0">Déjà en BD (origine import_fichier)</strong>
<div>
<button type="button" class="btn btn-outline-secondary btn-sm mr-1" id="ms1_import_refresh_status">
<i class="fa fa-refresh" aria-hidden="true"></i> Actualiser
</button>
<button type="button" class="btn btn-outline-danger btn-sm" id="ms1_import_reset_data">
<i class="fa fa-trash" aria-hidden="true"></i> Reset données import
</button>
</div>
</div>
<div class="ct-api-kpi-row" id="ms1_import_db_kpi_row">
<div class="ct-api-kpi" data-kpi="db_par"><div class="ct-api-kpi__label">Participants</div>
<div class="ct-api-kpi__value" id="ms1_import_db_par">…</div></div>
<div class="ct-api-kpi" data-kpi="db_pec"><div class="ct-api-kpi__label">PEC (coquilles)</div>
<div class="ct-api-kpi__value" id="ms1_import_db_pec">…</div></div>
<div class="ct-api-kpi" data-kpi="db_que"><div class="ct-api-kpi__label">Réponses questions</div>
<div class="ct-api-kpi__value" id="ms1_import_db_que">…</div></div>
<div class="ct-api-kpi ct-api-kpi--warn" data-kpi="db_list"><div class="ct-api-kpi__label">Visibles liste*</div>
<div class="ct-api-kpi__value" id="ms1_import_db_list">…</div></div>
</div>
<p class="small text-muted mb-0 mt-1" id="ms1_import_db_hint">
* Visibles liste ≈ join liste inscriptions avec <code>pec_actif=1</code> (diagnostic).
</p>
<div id="ms1_import_reset_confirm" class="alert alert-danger mt-3 mb-0" hidden>
<strong>Confirmer le reset import</strong>
<p class="small mb-2 mt-1" id="ms1_import_reset_confirm_msg"></p>
<p class="small mb-2">Nefface <strong>pas</strong> le catalogue de questions ni les inscriptions en ligne.</p>
<div class="form-inline flex-wrap">
<label class="mr-2 mb-2" for="ms1_import_reset_confirm_input">Tapez <code>RESET</code></label>
<input type="text" class="form-control form-control-sm mr-2 mb-2" id="ms1_import_reset_confirm_input"
autocomplete="off" style="max-width:8rem">
<button type="button" class="btn btn-danger btn-sm mb-2 mr-2" id="ms1_import_reset_confirm_go">
Confirmer leffacement
</button>
<button type="button" class="btn btn-outline-secondary btn-sm mb-2" id="ms1_import_reset_confirm_cancel">
Annuler
</button>
</div>
</div>
</div>
<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_busy" class="ms1-import__busy" hidden>
<div class="ms1-import__busy-inner">
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
<div>
<div class="ms1-import__busy-title" id="ms1_import_busy_title">Traitement en cours…</div>
<div class="ms1-import__busy-sub" id="ms1_import_busy_sub">Ne fermez pas la page.</div>
<div class="ms1-import__busy-time">Temps écoulé : <strong id="ms1_import_busy_sec">0</strong> s</div>
</div>
</div>
</div>
<div id="ms1_import_status" class="ms1-import__status mt-2" hidden></div>
<div class="ct-api-kpi-row mb-3 mt-2" id="ms1_import_kpi_row" hidden>
<div class="ct-api-kpi" data-kpi="total"><div class="ct-api-kpi__label">Lignes fichier</div>
<div class="ct-api-kpi__value" id="ms1_import_kpi_total">—</div></div>
<div class="ct-api-kpi ct-api-kpi--ok" data-kpi="ok"><div class="ct-api-kpi__label">Importés</div>
<div class="ct-api-kpi__value" id="ms1_import_kpi_ok">—</div></div>
<div class="ct-api-kpi ct-api-kpi--ok" data-kpi="created"><div class="ct-api-kpi__label">Créés</div>
<div class="ct-api-kpi__value" id="ms1_import_kpi_created">—</div></div>
<div class="ct-api-kpi ct-api-kpi--warn" data-kpi="updated"><div class="ct-api-kpi__label">Mis à jour</div>
<div class="ct-api-kpi__value" id="ms1_import_kpi_updated">—</div></div>
<div class="ct-api-kpi ct-api-kpi--danger" data-kpi="blocked"><div class="ct-api-kpi__label">Exclus</div>
<div class="ct-api-kpi__value" id="ms1_import_kpi_blocked">—</div></div>
</div>
<div class="ct-api-exclus-shell mb-3" id="ms1_import_exclus_shell" hidden>
<div class="ct-api-exclus-shell__head">
<span class="ct-api-exclus-shell__title-wrap">
<span class="ct-api-exclus-shell__title">Exclus</span>
<span class="badge badge-danger ml-2"><span id="ms1_import_hdr_exclus">0</span> exclus</span>
</span>
</div>
<div class="ct-api-exclus-shell__body">
<div id="ms1_import_blocked_wrap">
<div class="ct-api-exclus-empty">Aucun exclus pour le moment.</div>
</div>
</div>
</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><!-- #ms1_import_body -->
</div>
<style>
/* MSIN-4482 — même langage visuel que ChronoTrack (KPI / Exclus) */
#ms1-import-resultats .ct-api-kpi-row {
display: grid;
gap: 0.65rem;
grid-template-columns: repeat(5, minmax(0, 1fr));
}
#ms1_import_db_kpi_row.ct-api-kpi-row {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
@media (max-width: 992px) {
#ms1-import-resultats .ct-api-kpi-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
#ms1-import-resultats .ct-api-kpi {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 0.55rem 0.7rem;
}
#ms1-import-resultats .ct-api-kpi--ok { background: #eff6ff; border-color: #bfdbfe; }
#ms1-import-resultats .ct-api-kpi--danger { background: #fef2f2; border-color: #fecaca; }
#ms1-import-resultats .ct-api-kpi--warn { background: #fffbeb; border-color: #fde68a; }
#ms1-import-resultats .ct-api-kpi__label {
color: #64748b;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.02em;
text-transform: uppercase;
}
#ms1-import-resultats .ct-api-kpi__value {
color: #0f172a;
font-size: 1.35rem;
font-weight: 700;
line-height: 1.2;
}
#ms1-import-resultats .ct-api-kpi--ok .ct-api-kpi__value { color: #1d4ed8; }
#ms1-import-resultats .ct-api-kpi--danger .ct-api-kpi__value { color: #b91c1c; }
#ms1-import-resultats .ct-api-kpi--warn .ct-api-kpi__value { color: #b45309; }
#ms1-import-resultats .ct-api-exclus-shell {
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 8px;
overflow: hidden;
}
#ms1-import-resultats .ct-api-exclus-shell__head {
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
padding: 0.65rem 0.85rem;
}
#ms1-import-resultats .ct-api-exclus-shell__title { font-weight: 700; }
#ms1-import-resultats .ct-api-exclus-shell__body { padding: 12px 14px; }
#ms1-import-resultats .ct-api-exclus-empty { color: #64748b; font-size: 0.875rem; }
#ms1-import-resultats .ct-api-exclus-chip {
align-items: center;
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 8px;
display: flex;
justify-content: space-between;
margin-bottom: 0.75rem;
padding: 0.55rem 0.75rem;
}
#ms1-import-resultats .ct-api-exclus-chip__count {
color: #b91c1c;
font-size: 1.25rem;
font-weight: 700;
}
#ms1_import_blocked_wrap .ct-api-blocked-table { font-size: 0.8rem; }
#ms1_import_blocked_wrap .ct-api-blocked-table th { white-space: nowrap; }
#ms1_import_blocked_wrap .ct-api-blocked-scroll { max-height: 55vh; overflow: auto; }
#ms1-import-resultats .ms1-import__busy {
background: #fff7ed;
border: 2px solid #f59e0b;
border-radius: 8px;
margin: 0.75rem 0;
padding: 0.85rem 1rem;
}
#ms1-import-resultats .ms1-import__busy-inner {
align-items: flex-start;
display: flex;
gap: 0.85rem;
}
#ms1-import-resultats .ms1-import__busy .fa-spinner {
color: #b45309;
font-size: 1.6rem;
margin-top: 0.15rem;
}
#ms1-import-resultats .ms1-import__busy-title {
color: #9a3412;
font-size: 1rem;
font-weight: 700;
}
#ms1-import-resultats .ms1-import__busy-sub {
color: #92400e;
font-size: 0.875rem;
margin-top: 0.15rem;
}
#ms1-import-resultats .ms1-import__busy-time {
color: #78350f;
font-size: 0.8rem;
margin-top: 0.35rem;
}
#ms1-import-resultats.is-busy-op {
opacity: 0.92;
pointer-events: none;
}
#ms1-import-resultats.is-busy-op .ms1-import__busy {
pointer-events: auto;
}
/* Coureur dans #preloader superadm (style_blue #spinner = gros FA) */
#preloader #spinner .ms1-loader__runner {
display: block;
height: 80px;
margin: 0 auto 0.5rem;
object-fit: contain;
width: 80px;
}
#preloader #spinner .ms1-import-preload-time {
color: #555;
font-size: 0.85rem;
text-align: center;
}
</style>
<script>
(function ($) {
var $root = $('#ms1-import-resultats');
if (!$root.length) return;
var eveId = parseInt($root.data('eve-id'), 10) || 0;
var eveOk = String($root.data('eve-ok') || '0') === '1';
var ajaxUrl = String($root.data('ajax') || 'ajax_import_resultats.php');
var state = null;
var busyTimer = null;
var busyStartedAt = 0;
if (!eveOk) {
return;
}
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 ensureImportPreloader() {
var $p = $('#preloader');
if ($p.length) {
// Superadm header a parfois un vieux spinner FA sans coureur
if (!$p.find('.ms1-loader__runner').length) {
var src = String($root.data('loader-src') || '/images/ms1-runner-loader.gif');
$p.find('#spinner').prepend(
'<img class="ms1-loader__runner" src="' + src + '" alt="" role="presentation" decoding="async">'
);
}
if (!$('#ms1_import_preload_sec').length) {
$p.find('#spinner').append(
'<div class="ms1-import-preload-time small mt-2">Temps écoulé : <strong id="ms1_import_preload_sec">0</strong> s</div>'
);
}
return $p;
}
var src = String($root.data('loader-src') || '/images/ms1-runner-loader.gif');
$('body').append(
'<div id="preloader" style="display:none;">'
+ '<div id="spinner" class="ms1-loader ms1-loader--overlay">'
+ '<img class="ms1-loader__runner" src="' + $('<div/>').text(src).html() + '" alt="" role="presentation" decoding="async">'
+ '<span id="preloader_text" class="ms1-loader__label"></span>'
+ '<div class="ms1-import-preload-time small mt-2">Temps écoulé : <strong id="ms1_import_preload_sec">0</strong> s</div>'
+ '</div></div>'
);
return $('#preloader');
}
function setBusy(blnOn, strTitle, strSub) {
var $b = $('#ms1_import_busy');
if (busyTimer) {
clearInterval(busyTimer);
busyTimer = null;
}
if (!blnOn) {
$b.attr('hidden', true);
$root.removeClass('is-busy-op');
if (window.Ms1Loader && typeof window.Ms1Loader.hide === 'function') {
window.Ms1Loader.hide();
} else {
$('#preloader').fadeOut(200);
}
return;
}
var strMsg = strTitle || 'Traitement en cours…';
if (strSub) {
strMsg = strMsg + '<br><span style="font-size:0.85rem;font-weight:500;">' + $('<div/>').text(strSub).html() + '</span>';
}
busyStartedAt = Date.now();
$('#ms1_import_busy_title').text(strTitle || 'Traitement en cours…');
$('#ms1_import_busy_sub').text(strSub || 'Ne fermez pas la page — cest normal que ça prenne du temps.');
$('#ms1_import_busy_sec').text('0');
$b.removeAttr('hidden');
$root.addClass('is-busy-op');
// Le petit bonhomme (coureur MS1) — overlay plein écran
var $pre = ensureImportPreloader();
$('#preloader_text').html(strMsg);
$('#ms1_import_preload_sec').text('0');
if (window.Ms1Loader && typeof window.Ms1Loader.show === 'function') {
window.Ms1Loader.show(strMsg);
} else {
$pre.show();
}
busyTimer = setInterval(function () {
var intSec = Math.floor((Date.now() - busyStartedAt) / 1000);
$('#ms1_import_busy_sec, #ms1_import_preload_sec').text(String(intSec));
if (intSec === 15) {
var s15 = 'Toujours en cours — serveur qui travaille, pas un plantage.';
$('#ms1_import_busy_sub').text(s15);
} else if (intSec === 45) {
$('#ms1_import_busy_sub').text('Gros volume : 12 min possible. Le coureur continue = OK.');
} else if (intSec === 90) {
$('#ms1_import_busy_sub').text('Patience… si ça dépasse 3 min, Actualiser les compteurs après.');
}
}, 1000);
}
function renderImportBlockedPanel(res) {
var $wrap = $('#ms1_import_blocked_wrap');
var $shell = $('#ms1_import_exclus_shell');
var $kpi = $('#ms1_import_kpi_row');
if (!$wrap.length) return;
var intTotal = (res && res.kpi_total != null) ? res.kpi_total : (res.row_count || 0);
var intOk = (res && res.kpi_ok != null) ? res.kpi_ok : ((res.created || 0) + (res.updated || 0));
var intCreated = res.kpi_created != null ? res.kpi_created : (res.created || 0);
var intUpdated = res.kpi_updated != null ? res.kpi_updated : (res.updated || 0);
var intBlocked = (res && res.blocked_count) ? res.blocked_count : 0;
var tabGroups = (res && res.blocked_by_type) ? res.blocked_by_type : [];
$('#ms1_import_kpi_total').text(String(intTotal));
$('#ms1_import_kpi_ok').text(String(intOk));
$('#ms1_import_kpi_created').text(String(intCreated));
$('#ms1_import_kpi_updated').text(String(intUpdated));
$('#ms1_import_kpi_blocked').text(String(intBlocked));
$('#ms1_import_hdr_exclus').text(String(intBlocked));
$kpi.removeAttr('hidden');
$shell.removeAttr('hidden');
if (intBlocked <= 0 || !tabGroups.length) {
$wrap.html('<div class="ct-api-exclus-empty">Aucun exclus pour le moment.</div>');
return;
}
var html = '<div class="ct-api-exclus-chip">';
html += '<div><strong class="text-danger">Exclus import</strong>';
html += '<div class="small text-muted">Lignes non importées (à corriger dans le fichier ou le mapping)</div></div>';
html += '<span class="ct-api-exclus-chip__count">' + intBlocked + '</span></div>';
html += '<div id="ms1_import_blocked_detail">';
$.each(tabGroups, function (_, grp) {
var intCnt = (grp.items && grp.items.length) ? grp.items.length : 0;
if (intCnt === 0) return;
html += '<div class="mb-3">';
html += '<div class="d-flex align-items-center mb-1">';
html += '<strong class="text-danger small">' + esc(grp.label || grp.code) + '</strong>';
html += ' <span class="badge badge-secondary ml-1">' + intCnt + '</span>';
html += '</div>';
html += '<div class="ct-api-blocked-scroll border rounded">';
html += '<table class="table table-sm table-striped ct-api-blocked-table mb-0">';
html += '<thead class="thead-light"><tr>';
html += '<th>Ligne</th><th>Nom</th><th>external_id</th><th>Dossard</th><th>Sexe</th><th>Raison</th>';
html += '</tr></thead><tbody>';
$.each(grp.items, function (__, item) {
html += '<tr>';
html += '<td>' + esc(String(item.line || '')) + '</td>';
html += '<td>' + esc(item.name || '—') + '</td>';
html += '<td>' + esc(item.external_id || '—') + '</td>';
html += '<td>' + esc(item.no_bib ? ('#' + item.no_bib) : '—') + '</td>';
html += '<td>' + esc(item.par_sexe || '—') + '</td>';
html += '<td class="text-muted">' + esc(item.message || '') + '</td>';
html += '</tr>';
});
html += '</tbody></table></div></div>';
});
html += '</div>';
$wrap.html(html);
}
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_kpi_row').attr('hidden', true);
$('#ms1_import_exclus_shell').attr('hidden', true);
$('#ms1_import_blocked_wrap').html('<div class="ct-api-exclus-empty">Aucun exclus pour le moment.</div>');
$('#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) {
// Défaut = template déjà utilisé sur CET événement (plus récent) ; sinon aucun auto-apply
var defaultTpl = null;
(state.templates || []).forEach(function (t) {
if (t.same_event && !defaultTpl) {
defaultTpl = t;
}
});
var html = '<div class="alert alert-success py-2 mb-0"><strong>Template(s) trouvé(s)</strong> pour ces colonnes.';
if (defaultTpl) {
html += ' Le template de cet événement est appliqué par défaut.';
} else {
html += ' Aucun template propre à cet événement — choisissez-en un ou alignez manuellement.';
}
html += '</div><div class="list-group mt-2">';
state.templates.forEach(function (t) {
var blnDef = defaultTpl && parseInt(t.tpl_id, 10) === parseInt(defaultTpl.tpl_id, 10);
html += '<button type="button" class="list-group-item list-group-item-action js-apply-tpl'
+ (blnDef ? ' active' : '') + '" data-tpl="'
+ esc(t.tpl_id) + '"><i class="fa fa-magic" aria-hidden="true"></i> '
+ esc(t.name || ('Template #' + t.tpl_id))
+ (blnDef ? ' <span class="badge badge-light ml-1">défaut événement</span>' : '')
+ ' <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 || '');
$tpl.find('.js-apply-tpl').removeClass('active');
$(this).addClass('active');
showStatus('success', 'Template « ' + (found.name || id) + ' » appliqué.');
}
});
if (defaultTpl && defaultTpl.mapping) {
applyMappingObj(defaultTpl.mapping);
$('#ms1_import_tpl_name').val(defaultTpl.name || '');
showStatus('success', 'Fichier lu — template « '
+ (defaultTpl.name || defaultTpl.tpl_id)
+ ' » (cet événement) appliqué automatiquement.');
}
} 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');
setBusy(true, 'Lecture du fichier…', 'Parsing CSV / Excel — quelques secondes en général.');
showStatus('info', 'Lecture du fichier…');
$.ajax({
url: ajaxUrl,
method: 'POST',
data: fd,
processData: false,
contentType: false,
dataType: 'text',
timeout: 120000
}).done(function (raw) {
setBusy(false);
$('#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;
// Message générique seulement si pas d'auto-template (renderMap écrase le status si défaut)
showStatus('success', 'Fichier lu — ' + (res.row_count || 0) + ' ligne(s). Alignez les colonnes ci-dessous.');
renderMap();
}).fail(function (xhr) {
setBusy(false);
$('#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).');
});
});
function renderDbStatus(res) {
if (!res || res.state !== 'ok') return;
$('#ms1_import_db_par').text(String(res.participants != null ? res.participants : '—'));
$('#ms1_import_db_pec').text(String(res.pec != null ? res.pec : '—'));
$('#ms1_import_db_que').text(String(res.questions != null ? res.questions : '—'));
$('#ms1_import_db_list').text(String(res.list_visible != null ? res.list_visible : '—'));
if (res.message) {
$('#ms1_import_db_hint').text(res.message);
}
}
function refreshDbStatus(blnSilent) {
$.ajax({
url: ajaxUrl,
method: 'POST',
dataType: 'text',
data: { a: 'reset_preview', eve_id: eveId }
}).done(function (raw) {
var res = parseAjaxJson(raw);
if (!res || res.state !== 'ok') {
if (!blnSilent) {
showStatus('danger', (res && res.message) ? res.message : 'Échec lecture statut BD');
}
$('#ms1_import_db_par, #ms1_import_db_pec, #ms1_import_db_que, #ms1_import_db_list').text('?');
return;
}
renderDbStatus(res);
if (!blnSilent) {
showStatus('info', res.message || 'Statut BD à jour.');
}
}).fail(function () {
$('#ms1_import_db_par, #ms1_import_db_pec, #ms1_import_db_que, #ms1_import_db_list').text('?');
if (!blnSilent) {
showStatus('danger', 'Erreur réseau (statut BD).');
}
});
}
// Statut chargé dès l'ouverture du panneau
refreshDbStatus(true);
$('#ms1_import_refresh_status').on('click', function () {
refreshDbStatus(false);
});
$('#ms1_import_reset_data').on('click', function () {
var $box = $('#ms1_import_reset_confirm');
// KPI déjà affichés = pas de 2e round-trip lent avant la confirmation
var nPar = parseInt($('#ms1_import_db_par').text(), 10);
var nPec = parseInt($('#ms1_import_db_pec').text(), 10);
var nQue = parseInt($('#ms1_import_db_que').text(), 10);
var nList = parseInt($('#ms1_import_db_list').text(), 10);
if (isNaN(nPar)) {
showStatus('info', 'Comptage BD en cours — réessayez juste après.');
refreshDbStatus(false);
return;
}
if (nPar < 1) {
$box.attr('hidden', true);
showStatus('info', 'Rien à effacer — 0 participant import en BD.');
return;
}
var strRecap = 'Vous allez effacer : <strong>' + esc(String(nPar)) + '</strong> participants, '
+ '<strong>' + esc(String(isNaN(nPec) ? '?' : nPec)) + '</strong> pec, '
+ '<strong>' + esc(String(isNaN(nQue) ? '?' : nQue)) + '</strong> réponses questions '
+ '(dont ' + esc(String(isNaN(nList) ? '?' : nList)) + ' visibles liste).';
$('#ms1_import_reset_confirm_msg').html(strRecap);
$('#ms1_import_reset_confirm_input').val('');
$box.removeAttr('hidden');
showStatus('warning', 'Confirmation requise ci-dessous — tapez RESET puis Confirmer.');
try {
$box[0].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
$('#ms1_import_reset_confirm_input').trigger('focus');
} catch (e) {}
});
$('#ms1_import_reset_confirm_cancel').on('click', function () {
$('#ms1_import_reset_confirm').attr('hidden', true);
$('#ms1_import_reset_confirm_input').val('');
showStatus('info', 'Reset annulé.');
});
$('#ms1_import_reset_confirm_go').on('click', function () {
var strConfirm = $.trim($('#ms1_import_reset_confirm_input').val() || '');
if (strConfirm !== 'RESET') {
showStatus('danger', 'Tapez exactement RESET dans le champ (majuscules).');
return;
}
var $go = $(this);
$go.prop('disabled', true);
$('#ms1_import_reset_data').prop('disabled', true);
setBusy(true,
'Reset import en cours…',
'Suppression de milliers de lignes — 30 s à 2 min selon la charge. Le chrono tourne = ça travaille.'
);
showStatus('warning', 'Reset en cours — ne fermez pas la page.');
$.ajax({
url: ajaxUrl,
method: 'POST',
dataType: 'text',
timeout: 300000,
data: { a: 'reset', eve_id: eveId, confirm: strConfirm }
}).done(function (raw2) {
setBusy(false);
$go.prop('disabled', false);
$('#ms1_import_reset_data').prop('disabled', false);
var res = parseAjaxJson(raw2);
if (!res || res.state !== 'ok') {
showStatus('danger', (res && res.message) ? res.message : 'Échec reset');
return;
}
$('#ms1_import_reset_confirm').attr('hidden', true);
$('#ms1_import_reset_confirm_input').val('');
showStatus('success', res.message || 'Reset OK');
$('#ms1_import_kpi_row').attr('hidden', true);
$('#ms1_import_exclus_shell').attr('hidden', true);
refreshDbStatus(true);
}).fail(function (xhr) {
setBusy(false);
$go.prop('disabled', false);
$('#ms1_import_reset_data').prop('disabled', false);
var res = parseAjaxJson(xhr && xhr.responseText);
if (res && res.state === 'ok') {
$('#ms1_import_reset_confirm').attr('hidden', true);
showStatus('success', res.message || 'Reset OK');
refreshDbStatus(true);
return;
}
showStatus('danger', 'Erreur / timeout reset (HTTP ' + (xhr && xhr.status ? xhr.status : '?') + '). Vérifiez les compteurs avec Actualiser.');
refreshDbStatus(true);
});
});
$('#ms1_import_commit').on('click', function () {
if (!state || !state.token) return;
var mapping = collectMapping();
var nRows = parseInt(state.row_count, 10) || 0;
var strEta = nRows > 500
? ('~' + nRows + ' lignes : 1 à 3 minutes (écritures une par une en BD).')
: 'Quelques dizaines de secondes en général.';
if (!window.confirm('Importer / mettre à jour les participants dans resultats_* pour cet événement ?\n(Sans panier ni paiement — upsert sur la clé unique.)\n\n' + strEta)) {
return;
}
var $btn = $(this);
$btn.prop('disabled', true);
setBusy(true, 'Import en cours…', strEta + ' Chrono ci-dessous = ça travaille, pas un plantage.');
showStatus('warning', 'Import en cours — ne fermez pas la page.');
$.ajax({
url: ajaxUrl,
method: 'POST',
dataType: 'text',
timeout: 600000,
data: {
a: 'commit',
eve_id: eveId,
token: state.token,
mapping: JSON.stringify(mapping)
}
}).done(function (raw) {
setBusy(false);
$btn.prop('disabled', false);
var res = parseAjaxJson(raw);
if (!res || res.state !== 'ok') {
showStatus('danger', (res && res.message) ? res.message : 'Échec import');
return;
}
showStatus((res.blocked_count > 0) ? 'warning' : 'success', res.message || 'Import terminé.');
renderImportBlockedPanel(res);
refreshDbStatus(true);
ensurePanelOpen();
}).fail(function (xhr) {
setBusy(false);
$btn.prop('disabled', false);
var res = parseAjaxJson(xhr && xhr.responseText);
if (res && res.state === 'ok') {
showStatus((res.blocked_count > 0) ? 'warning' : 'success', res.message || 'Import terminé.');
renderImportBlockedPanel(res);
refreshDbStatus(true);
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'));
}