Files
ms1inscription-v5/php/inc_fx_import_resultats.php

4525 lines
192 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

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

<?php
/**
* MSIN-4482 / MSIN-4520 — Import participants hors transaction (parse, templates, UI superadm).
* MSIN-4520 — Excel multi-onglets : choix onglets = épreuves ou feuille + colonne.
*/
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' => 'pro_id', 'label' => 'Région / province', 'group' => 'Contact'),
array('key' => 'par_codepostal', 'label' => 'Code postal', 'group' => 'Contact'),
array('key' => 'pay_id', 'label' => 'Pays', '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');
}
/**
* MSIN-4520 — répare mojibake fréquent « Région » (UTF-8 lu comme Latin-1).
*/
function fxImportResultatsRepairMojibake($str) {
$str = (string)$str;
if ($str === '' || (strpos($str, 'Ã') === false && strpos($str, 'Â') === false)) {
return $str;
}
if (function_exists('mb_convert_encoding')) {
$strBytes = @mb_convert_encoding($str, 'ISO-8859-1', 'UTF-8');
if (is_string($strBytes) && $strBytes !== ''
&& function_exists('mb_check_encoding')
&& @mb_check_encoding($strBytes, 'UTF-8')
) {
return $strBytes;
}
}
return $str;
}
/**
* MSIN-4520 — clé comparable sans accents (Quebec ≈ Québec).
*/
function fxImportResultatsFoldKey($str) {
$str = fxImportResultatsRepairMojibake((string)$str);
$str = fxImportResultatsNormalizeHeader($str);
$str = strtr($str, array(
'à' => 'a', 'á' => 'a', 'â' => 'a', 'ä' => 'a', 'ã' => 'a', 'å' => 'a',
'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e',
'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i',
'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'ö' => 'o', 'õ' => 'o',
'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u',
'ý' => 'y', 'ÿ' => 'y', 'ç' => 'c', 'ñ' => 'n',
'œ' => 'oe', 'æ' => 'ae',
));
$str = strtolower($str);
$str = preg_replace('/[^a-z0-9\s\/]+/', '', $str);
return preg_replace('/\s+/', ' ', trim($str));
}
/**
* 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) {
// MSIN-4520 — réparer mojibake avant guess (Région → région)
$strHeader = fxImportResultatsRepairMojibake((string)$strHeader);
$str = fxImportResultatsNormalizeHeader($strHeader);
$str = str_replace(array('-', '_'), ' ', $str);
$strFold = fxImportResultatsFoldKey($strHeader);
// 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', 'birthdate', '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'),
'pro_id' => array(
'region / province / state', 'région / province / state', 'province / state',
'province', 'state', 'region', 'région', 'etat', 'état',
),
'par_codepostal' => array('code postal', 'postal', 'zipcode', 'zip'),
'pay_id' => array('pays', 'country', 'nationalite', 'nationalité', 'nationality', 'nation'),
'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 (normalisée + folded)
foreach ($tabRules as $strKey => $tabNeedles) {
foreach ($tabNeedles as $strNeedle) {
if ($str === $strNeedle || $strFold === fxImportResultatsFoldKey($strNeedle)) {
return $strKey;
}
}
}
// MSIN-4520 — en-têtes bilingues région/pays : ne pas bloquer sur « / »
$blnGeoHeader = (bool)preg_match('/\b(province|state|region|region|pays|country|nationalit)/u', $strFold);
// Questions longues bilingues → pas de guess champ standard (resteront en step questions)
if (!$blnGeoHeader && (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 (sur forme folded)
$tabCandidates = array();
foreach ($tabRules as $strKey => $tabNeedles) {
foreach ($tabNeedles as $strNeedle) {
if ($strNeedle === '') {
continue;
}
$strNeedleFold = fxImportResultatsFoldKey($strNeedle);
// aiguilles 13 lettres : mot entier seulement
if (mb_strlen($strNeedleFold, 'UTF-8') <= 3) {
if (preg_match('/(^|\\s)' . preg_quote($strNeedleFold, '/') . '(\\s|$)/u', $strFold)) {
$tabCandidates[] = array('key' => $strKey, 'len' => mb_strlen($strNeedleFold, 'UTF-8'));
}
continue;
}
if (strpos($strFold, $strNeedleFold) !== false) {
$tabCandidates[] = array('key' => $strKey, 'len' => mb_strlen($strNeedleFold, '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,
'epr_nom_fr' => $strNom,
'epr_type_fr' => trim((string)($tab[$i]['epr_type_fr'] ?? '')),
);
}
return $tabOut;
}
/**
* MSIN-4520 — suggestion epr_id pour une valeur fichier / nom d'onglet.
*/
function fxImportResultatsGuessEprId($strValue, array $tabEpreuves) {
$strVal = fxImportResultatsNormalizeHeader($strValue);
if ($strVal === '' || count($tabEpreuves) < 1) {
return 0;
}
$intBest = 0;
$intBestScore = 0;
foreach ($tabEpreuves as $arrE) {
$intEpr = intval($arrE['epr_id'] ?? 0);
if ($intEpr <= 0) {
continue;
}
$tabCands = array(
fxImportResultatsNormalizeHeader($arrE['label'] ?? ''),
fxImportResultatsNormalizeHeader($arrE['epr_nom_fr'] ?? ''),
fxImportResultatsNormalizeHeader($arrE['epr_type_fr'] ?? ''),
);
foreach ($tabCands as $strCand) {
if ($strCand === '') {
continue;
}
$intScore = 0;
if ($strCand === $strVal) {
$intScore = 100;
} elseif (strpos($strCand, $strVal) !== false || strpos($strVal, $strCand) !== false) {
$intScore = 80;
} elseif (preg_match('/(\d+)\s*k/u', $strVal, $m1) && preg_match('/(\d+)\s*k/u', $strCand, $m2)
&& $m1[1] === $m2[1]) {
$intScore = 70;
}
if ($intScore > $intBestScore) {
$intBestScore = $intScore;
$intBest = $intEpr;
}
}
}
return ($intBestScore >= 70) ? $intBest : 0;
}
/**
* MSIN-4520 — liste pays pour dropdowns mapping import.
* @return array<int,array{pay_id:int,label:string}>
*/
function fxImportResultatsPaysList() {
global $objDatabase;
static $tabOut = null;
if ($tabOut !== null) {
return $tabOut;
}
$tabOut = array();
$sql = "SELECT pay_id, pay_nom_fr, pay_iso FROM inscriptions_pays"
. " WHERE pay_actif = 1 ORDER BY pay_trie, pay_nom_fr";
$tab = $objDatabase->fxGetResults($sql);
if (!is_array($tab)) {
return $tabOut;
}
for ($i = 1; $i <= count($tab); $i++) {
$intId = intval($tab[$i]['pay_id'] ?? 0);
if ($intId <= 0) {
continue;
}
$strLabel = trim((string)($tab[$i]['pay_nom_fr'] ?? ''));
$strIso = trim((string)($tab[$i]['pay_iso'] ?? ''));
if ($strIso !== '') {
$strLabel = ($strLabel !== '' ? ($strLabel . ' (' . $strIso . ')') : $strIso);
}
if ($strLabel === '') {
$strLabel = 'Pays #' . $intId;
}
$tabOut[] = array('pay_id' => $intId, 'label' => $strLabel);
}
return $tabOut;
}
/**
* MSIN-4520 — liste provinces pour dropdowns mapping import.
* @return array<int,array{pro_id:int,label:string}>
*/
function fxImportResultatsProvincesList() {
global $objDatabase;
static $tabOut = null;
if ($tabOut !== null) {
return $tabOut;
}
$tabOut = array();
$sql = "SELECT pro_id, pro_nom_fr, pro_iso FROM inscriptions_provinces"
. " WHERE pro_actif = 1 ORDER BY pro_trie, pro_nom_fr";
$tab = $objDatabase->fxGetResults($sql);
if (!is_array($tab)) {
return $tabOut;
}
for ($i = 1; $i <= count($tab); $i++) {
$intId = intval($tab[$i]['pro_id'] ?? 0);
if ($intId <= 0) {
continue;
}
$strLabel = trim((string)($tab[$i]['pro_nom_fr'] ?? ''));
$strIso = trim((string)($tab[$i]['pro_iso'] ?? ''));
if ($strIso !== '') {
$strLabel = ($strLabel !== '' ? ($strLabel . ' (' . $strIso . ')') : $strIso);
}
if ($strLabel === '') {
$strLabel = 'Province #' . $intId;
}
$tabOut[] = array('pro_id' => $intId, '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;
}
/**
* MSIN-4520 — chemin du meta JSON collé au fichier upload.
*/
function fxImportResultatsMetaPath($strPath) {
return (string)$strPath . '.meta.json';
}
/**
* MSIN-4520 — lit le meta d'un upload (sheet_mode, onglets choisis…).
* @return array<string,mixed>
*/
function fxImportResultatsLoadMeta($strPath) {
$strMeta = fxImportResultatsMetaPath($strPath);
if (!is_file($strMeta)) {
return array();
}
$arr = json_decode((string)@file_get_contents($strMeta), true);
return is_array($arr) ? $arr : array();
}
/**
* MSIN-4520 — écrit / fusionne le meta JSON.
* @param array<string,mixed> $arrMeta
*/
function fxImportResultatsSaveMeta($strPath, array $arrMeta) {
@file_put_contents(fxImportResultatsMetaPath($strPath), json_encode($arrMeta, JSON_UNESCAPED_UNICODE));
}
/**
* Lit CSV ou Excel → en-têtes + lignes (tableaux indexés 0..n).
* MSIN-4520 — Excel multi-onglets : options lues depuis le meta si absentes.
* @param array|null $arrOptions { sheet_mode?: string, sheet?: string, sheets?: string[] }
* @return array{state:string,message?:string,headers?:array,rows?:array}
*/
function fxImportResultatsParseFile($strPath, $strOriginalName = '', $arrOptions = null) {
$strExt = strtolower(pathinfo($strOriginalName !== '' ? $strOriginalName : $strPath, PATHINFO_EXTENSION));
if ($strExt === 'csv' || $strExt === 'txt') {
return fxImportResultatsParseCsv($strPath);
}
if (in_array($strExt, array('xls', 'xlsx'), true)) {
if (!is_array($arrOptions)) {
$arrMeta = fxImportResultatsLoadMeta($strPath);
$arrOptions = array(
'sheet_mode' => (string)($arrMeta['sheet_mode'] ?? ''),
'sheet' => (string)($arrMeta['sheet'] ?? ''),
'sheets' => isset($arrMeta['sheets']) && is_array($arrMeta['sheets']) ? $arrMeta['sheets'] : array(),
);
}
return fxImportResultatsParseExcel($strPath, $arrOptions);
}
// 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);
}
/**
* MSIN-4520 — charge PHPExcel (chemin superadm ou php/).
* @return string|null message erreur ou null si OK
*/
function fxImportResultatsRequirePhpExcel() {
static $blnLoaded = false;
if ($blnLoaded) {
return null;
}
$strPhpExcel = dirname(__FILE__) . '/../superadm/php/Classes/PHPExcel.php';
if (!is_file($strPhpExcel)) {
$strPhpExcel = dirname(__FILE__) . '/Classes/PHPExcel.php';
}
if (!is_file($strPhpExcel)) {
return 'Lecteur Excel indisponible — utilisez un CSV.';
}
require_once $strPhpExcel;
$blnLoaded = true;
return null;
}
/**
* MSIN-4520 — nom d'onglet = feuille « résumé / détail / tous » (à exclure par défaut).
*/
function fxImportResultatsSheetNameIsSummary($strName) {
$str = fxImportResultatsNormalizeHeader($strName);
if ($str === 'tous' || $str === 'all' || $str === 'total' || $str === 'summary' || $str === 'resume' || $str === 'résumé') {
return true;
}
if (preg_match('/\b(detail|détail|détails|details|resume|résumé|summary)\b/u', $str)) {
return true;
}
if (preg_match('/[-–—]\s*(detail|détail)/u', $str)) {
return true;
}
return false;
}
/**
* MSIN-4520 — la 1re ligne ressemble-t-elle à des en-têtes participants ?
*/
function fxImportResultatsSheetRawLooksLikeData(array $tabRaw) {
if (count($tabRaw) < 2 || !is_array($tabRaw[0])) {
return false;
}
$tabHeaders = array();
foreach ($tabRaw[0] as $v) {
$str = trim((string)$v);
if ($str !== '') {
$tabHeaders[] = $str;
}
}
if (count($tabHeaders) < 4) {
return false;
}
$strFirst = fxImportResultatsNormalizeHeader($tabHeaders[0]);
if ($strFirst === 'total' || $strFirst === 'vague' || $strFirst === 'count' || $strFirst === 'nb') {
return false;
}
$intHits = 0;
foreach ($tabHeaders as $strH) {
if (fxImportResultatsGuessField($strH) !== '') {
$intHits++;
}
}
return $intHits >= 2;
}
/**
* MSIN-4520 — inventaire léger des onglets d'un .xlsx via Zip (sans PHPExcel).
* Évite 503 (qos / timeout) sur gros multi-onglets : plus de load+toArray de tout le classeur.
* @return array{state:string,message?:string,sheets?:array,sheet_count?:int}
*/
function fxImportResultatsListExcelSheetsXlsxZip($strPath) {
if (!class_exists('ZipArchive')) {
return array('state' => 'error', 'message' => 'ZipArchive indisponible');
}
$zip = new ZipArchive();
if ($zip->open($strPath) !== true) {
return array('state' => 'error', 'message' => 'Impossible d\'ouvrir le fichier xlsx');
}
$strWb = $zip->getFromName('xl/workbook.xml');
$zip->close();
if ($strWb === false || $strWb === '') {
return array('state' => 'error', 'message' => 'workbook.xml manquant dans le xlsx');
}
$tabNames = array();
$objXml = @simplexml_load_string($strWb);
if ($objXml instanceof SimpleXMLElement) {
$objXml->registerXPathNamespace('m', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
$tabNodes = $objXml->xpath('//m:sheets/m:sheet');
if (is_array($tabNodes)) {
foreach ($tabNodes as $objSheet) {
$arrAttr = $objSheet->attributes();
$strName = isset($arrAttr['name']) ? trim((string)$arrAttr['name']) : '';
if ($strName !== '') {
$tabNames[] = $strName;
}
}
}
}
if (count($tabNames) < 1
&& preg_match_all('/<sheet\b[^>]*\bname="([^"]+)"/i', $strWb, $tabM)
) {
foreach ($tabM[1] as $strName) {
$strName = html_entity_decode(trim((string)$strName), ENT_QUOTES | ENT_XML1, 'UTF-8');
if ($strName !== '') {
$tabNames[] = $strName;
}
}
}
if (count($tabNames) < 1) {
return array('state' => 'error', 'message' => 'Aucun onglet trouvé dans le xlsx');
}
$tabSheets = array();
foreach ($tabNames as $i => $strName) {
$blnSummary = fxImportResultatsSheetNameIsSummary($strName);
$blnData = !$blnSummary;
$tabSheets[] = array(
'name' => $strName,
'index' => $i,
'row_count' => null,
'looks_like_data' => $blnData,
'is_summary' => $blnSummary,
'suggested' => ($blnData && !$blnSummary),
);
}
return array(
'state' => 'ok',
'sheets' => $tabSheets,
'sheet_count' => count($tabSheets),
'list_mode' => 'xlsx_zip',
);
}
/**
* MSIN-4520 — échantillon des N premières lignes (évite toArray complet).
* @return array
*/
function fxImportResultatsSheetSampleRows($objSheet, $intMaxRows = 25) {
$tabRaw = array();
$intMaxRows = max(2, intval($intMaxRows));
try {
$objRows = $objSheet->getRowIterator(1, $intMaxRows);
foreach ($objRows as $objRow) {
$tabLine = array();
$objCells = $objRow->getCellIterator();
$objCells->setIterateOnlyExistingCells(false);
$intCol = 0;
foreach ($objCells as $objCell) {
if ($intCol >= 80) {
break;
}
$tabLine[] = $objCell->getCalculatedValue();
$intCol++;
}
$tabRaw[] = $tabLine;
}
} catch (Exception $ex) {
return array();
}
return $tabRaw;
}
/**
* MSIN-4520 — inventaire des onglets Excel (sans tout fusionner).
* xlsx : Zip léger d'abord ; PHPExcel seulement en secours / .xls.
* @return array{state:string,message?:string,sheets?:array}
*/
function fxImportResultatsListExcelSheets($strPath) {
@set_time_limit(120);
$strPath = (string)$strPath;
$strExt = strtolower(pathinfo($strPath, PATHINFO_EXTENSION));
$strZipMsg = '';
if ($strExt === 'xlsx') {
$arrZip = fxImportResultatsListExcelSheetsXlsxZip($strPath);
if (($arrZip['state'] ?? '') === 'ok') {
return $arrZip;
}
$strZipMsg = (string)($arrZip['message'] ?? '');
}
$strErr = fxImportResultatsRequirePhpExcel();
if ($strErr !== null) {
return array(
'state' => 'error',
'message' => $strErr . ($strZipMsg !== '' ? (' / ' . $strZipMsg) : ''),
);
}
try {
$objReader = PHPExcel_IOFactory::createReaderForFile($strPath);
if (method_exists($objReader, 'setReadDataOnly')) {
$objReader->setReadDataOnly(true);
}
$objBook = $objReader->load($strPath);
} catch (Exception $ex) {
return array('state' => 'error', 'message' => 'Lecture Excel échouée : ' . $ex->getMessage());
}
$tabSheets = array();
$intNb = $objBook->getSheetCount();
for ($i = 0; $i < $intNb; $i++) {
$objSheet = $objBook->getSheet($i);
$strName = trim((string)$objSheet->getTitle());
$tabRaw = fxImportResultatsSheetSampleRows($objSheet, 25);
if (!is_array($tabRaw)) {
$tabRaw = array();
}
$blnData = fxImportResultatsSheetRawLooksLikeData($tabRaw);
$blnSummary = fxImportResultatsSheetNameIsSummary($strName);
$tabSheets[] = array(
'name' => $strName,
'index' => $i,
'row_count' => null,
'looks_like_data' => $blnData,
'is_summary' => $blnSummary,
'suggested' => ($blnData && !$blnSummary),
);
}
return array(
'state' => 'ok',
'sheets' => $tabSheets,
'sheet_count' => count($tabSheets),
'list_mode' => 'phpexcel_sample',
);
}
/**
* Convertit toArray d'une feuille → headers + rows.
* @return array{state:string,message?:string,headers?:array,rows?:array}
*/
function fxImportResultatsSheetRawToRows(array $tabRaw) {
if (!is_array($tabRaw) || count($tabRaw) < 1) {
return array('state' => 'error', 'message' => 'Feuille Excel vide.');
}
$tabHeaders = array_map(function ($v) {
return trim((string)$v);
}, $tabRaw[0]);
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);
}
/**
* MSIN-4520 — fusionne plusieurs feuilles ; 1re colonne synthétique = nom d'onglet (→ épreuve).
* @param array<int,array{name:string,headers:array,rows:array}> $tabParts
* @return array{state:string,message?:string,headers?:array,rows?:array}
*/
function fxImportResultatsMergeSheetsAsEpr(array $tabParts) {
if (count($tabParts) < 1) {
return array('state' => 'error', 'message' => 'Aucun onglet sélectionné.');
}
$strOngletHeader = 'Onglet (épreuve)';
$tabUnion = array($strOngletHeader);
$tabSeen = array(fxImportResultatsNormalizeHeader($strOngletHeader) => true);
foreach ($tabParts as $arrPart) {
foreach ($arrPart['headers'] as $strH) {
$strH = trim((string)$strH);
if ($strH === '') {
continue;
}
$strKey = fxImportResultatsNormalizeHeader($strH);
if (isset($tabSeen[$strKey])) {
continue;
}
$tabSeen[$strKey] = true;
$tabUnion[] = $strH;
}
}
$tabRows = array();
foreach ($tabParts as $arrPart) {
$strSheet = (string)$arrPart['name'];
$tabMapIdx = array();
foreach ($arrPart['headers'] as $intCi => $strH) {
$tabMapIdx[fxImportResultatsNormalizeHeader($strH)] = intval($intCi);
}
foreach ($arrPart['rows'] as $tabRow) {
$tabOut = array();
foreach ($tabUnion as $intUi => $strUH) {
if ($intUi === 0) {
$tabOut[] = $strSheet;
continue;
}
$strKey = fxImportResultatsNormalizeHeader($strUH);
$intSrc = isset($tabMapIdx[$strKey]) ? $tabMapIdx[$strKey] : -1;
$tabOut[] = ($intSrc >= 0) ? trim((string)($tabRow[$intSrc] ?? '')) : '';
}
$tabRows[] = $tabOut;
}
}
if (count($tabRows) < 1) {
return array('state' => 'error', 'message' => 'Aucune ligne dans les onglets sélectionnés.');
}
return array(
'state' => 'ok',
'headers' => $tabUnion,
'rows' => $tabRows,
'sheet_mode' => 'sheets',
);
}
/**
* @param array $arrOptions { sheet_mode?: string, sheet?: string, sheets?: string[] }
* @return array{state:string,message?:string,headers?:array,rows?:array}
*/
function fxImportResultatsParseExcel($strPath, array $arrOptions = array()) {
@set_time_limit(300);
@ini_set('memory_limit', '512M');
$strErr = fxImportResultatsRequirePhpExcel();
if ($strErr !== null) {
return array('state' => 'error', 'message' => $strErr);
}
$strMode = (string)($arrOptions['sheet_mode'] ?? '');
$tabLoadOnly = null;
if ($strMode === 'sheets') {
$tabWant = isset($arrOptions['sheets']) && is_array($arrOptions['sheets']) ? $arrOptions['sheets'] : array();
$tabLoadOnly = array();
foreach ($tabWant as $strN) {
$strN = trim((string)$strN);
if ($strN !== '') {
$tabLoadOnly[] = $strN;
}
}
if (count($tabLoadOnly) < 1) {
return array('state' => 'error', 'message' => 'Sélectionnez au moins un onglet.');
}
} elseif ($strMode === 'column' || $strMode === '') {
$strSheetOpt = trim((string)($arrOptions['sheet'] ?? ''));
if ($strSheetOpt !== '') {
$tabLoadOnly = array($strSheetOpt);
}
}
try {
$objReader = PHPExcel_IOFactory::createReaderForFile($strPath);
if (method_exists($objReader, 'setReadDataOnly')) {
$objReader->setReadDataOnly(true);
}
// MSIN-4520 — ne charger que les onglets demandés (pas les 14 feuilles Boréalys)
if (is_array($tabLoadOnly) && count($tabLoadOnly) > 0 && method_exists($objReader, 'setLoadSheetsOnly')) {
$objReader->setLoadSheetsOnly($tabLoadOnly);
}
$objBook = $objReader->load($strPath);
} catch (Exception $ex) {
return array('state' => 'error', 'message' => 'Lecture Excel échouée : ' . $ex->getMessage());
}
if ($strMode === 'sheets') {
$tabWantNorm = array();
foreach ($tabLoadOnly as $strN) {
$tabWantNorm[$strN] = true;
}
$tabParts = array();
$intNb = $objBook->getSheetCount();
for ($i = 0; $i < $intNb; $i++) {
$objSheet = $objBook->getSheet($i);
$strName = trim((string)$objSheet->getTitle());
if (!isset($tabWantNorm[$strName])) {
continue;
}
$tabRaw = $objSheet->toArray(null, true, true, false);
if (!is_array($tabRaw)) {
continue;
}
$arrOne = fxImportResultatsSheetRawToRows($tabRaw);
if (($arrOne['state'] ?? '') !== 'ok') {
return array(
'state' => 'error',
'message' => 'Onglet « ' . $strName . ' » : ' . ($arrOne['message'] ?? 'lecture échouée'),
);
}
$tabParts[] = array(
'name' => $strName,
'headers' => $arrOne['headers'],
'rows' => $arrOne['rows'],
);
unset($tabWantNorm[$strName]);
}
if (count($tabWantNorm) > 0) {
$strMissing = implode(', ', array_keys($tabWantNorm));
return array('state' => 'error', 'message' => 'Onglet(s) introuvable(s) : ' . $strMissing);
}
return fxImportResultatsMergeSheetsAsEpr($tabParts);
}
// Mode colonne (défaut) : une feuille — nom demandé ou active
$strSheet = trim((string)($arrOptions['sheet'] ?? ''));
try {
if ($strSheet !== '') {
$objSheet = $objBook->getSheetByName($strSheet);
if ($objSheet === null) {
return array('state' => 'error', 'message' => 'Onglet « ' . $strSheet . ' » introuvable.');
}
} else {
$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)) {
return array('state' => 'error', 'message' => 'Fichier Excel vide.');
}
$arrParsed = fxImportResultatsSheetRawToRows($tabRaw);
if (($arrParsed['state'] ?? '') === 'ok') {
$arrParsed['sheet_mode'] = 'column';
$arrParsed['sheet'] = trim((string)$objSheet->getTitle());
}
return $arrParsed;
}
/**
* 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;
}
/**
* MSIN-4482 / MSIN-4520 — construit la réponse mapping après parse (upload ou choose_sheets).
* @param array $arrParsed headers+rows ok
* @param array $arrMeta meta déjà partiellement rempli
* @return array
*/
function fxImportResultatsBuildMappingPayload($intEveId, $strToken, $strDest, $strName, array $arrParsed, array $arrMeta = array()) {
$tabHeaders = $arrParsed['headers'];
$tabRows = $arrParsed['rows'];
$strSig = fxImportResultatsHeaderSig($tabHeaders);
$tabGuess = array();
foreach ($tabHeaders as $intHi => $strH) {
$strG = fxImportResultatsGuessField($strH);
// MSIN-4520 — colonne synthétique onglet = épreuve (priorité sur Competition)
if ($intHi === 0 && fxImportResultatsNormalizeHeader($strH) === fxImportResultatsNormalizeHeader('Onglet (épreuve)')) {
$strG = 'epr';
}
$tabGuess[] = $strG;
}
// Une seule colonne epr : si onglet déjà epr, retirer les autres guesses epr
$blnOngletEpr = isset($tabGuess[0]) && $tabGuess[0] === 'epr'
&& fxImportResultatsNormalizeHeader($tabHeaders[0] ?? '') === fxImportResultatsNormalizeHeader('Onglet (épreuve)');
if ($blnOngletEpr) {
for ($i = 1; $i < count($tabGuess); $i++) {
if ($tabGuess[$i] === 'epr') {
$tabGuess[$i] = '';
}
}
}
$tabIdxFilter = array();
foreach ($tabHeaders as $intHi => $strH) {
$strG = $tabGuess[$intHi] ?? 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));
$arrMetaOut = array_merge($arrMeta, array(
'eve_id' => intval($intEveId),
'token' => $strToken,
'original_name' => $strName,
'path' => $strDest,
'header_sig' => $strSig,
'headers' => $tabHeaders,
'row_count' => count($tabRows),
'sheet_mode' => (string)($arrParsed['sheet_mode'] ?? ($arrMeta['sheet_mode'] ?? 'column')),
'created_at' => date('c'),
));
if (!empty($arrParsed['sheet'])) {
$arrMetaOut['sheet'] = (string)$arrParsed['sheet'];
}
if (!empty($arrParsed['sheets']) && is_array($arrParsed['sheets'])) {
$arrMetaOut['sheets'] = $arrParsed['sheets'];
}
fxImportResultatsSaveMeta($strDest, $arrMetaOut);
$tabTemplates = fxImportResultatsFindTemplates($intEveId, $strSig);
$tabEpreuves = fxImportResultatsEpreuvesList($intEveId);
// MSIN-4520 — pré-remplir epr / pays / province pour valeurs distinctes reconnues
$tabEprGuess = array();
$tabPayGuess = array();
$tabProGuess = array();
$intEprGuessCol = -1;
$intPayGuessCol = -1;
$intProGuessCol = -1;
foreach ($tabGuess as $intGi => $strGk) {
if ($strGk === 'epr' && $intEprGuessCol < 0) {
$intEprGuessCol = $intGi;
}
if ($strGk === 'pay_id' && $intPayGuessCol < 0) {
$intPayGuessCol = $intGi;
}
if ($strGk === 'pro_id' && $intProGuessCol < 0) {
$intProGuessCol = $intGi;
}
}
if ($intEprGuessCol >= 0 && !empty($tabDistinct[$intEprGuessCol])) {
foreach ($tabDistinct[$intEprGuessCol] as $strVal) {
$intG = fxImportResultatsGuessEprId($strVal, $tabEpreuves);
if ($intG > 0) {
$tabEprGuess[$strVal] = $intG;
}
}
}
if ($intPayGuessCol >= 0 && !empty($tabDistinct[$intPayGuessCol])) {
foreach ($tabDistinct[$intPayGuessCol] as $strVal) {
$intG = fxImportResultatsResolvePayId($strVal);
if ($intG > 0) {
$tabPayGuess[$strVal] = $intG;
}
}
}
if ($intProGuessCol >= 0 && !empty($tabDistinct[$intProGuessCol])) {
foreach ($tabDistinct[$intProGuessCol] as $strVal) {
$intG = fxImportResultatsResolveProId($strVal);
if ($intG > 0) {
$tabProGuess[$strVal] = $intG;
}
}
}
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' => $tabEpreuves,
'pays' => fxImportResultatsPaysList(),
'provinces' => fxImportResultatsProvincesList(),
'questions' => fxImportResultatsQuestionsList($intEveId),
'fields' => fxImportResultatsTargetFields(),
'sheet_mode' => $arrMetaOut['sheet_mode'],
'sheet' => (string)($arrMetaOut['sheet'] ?? ''),
'sheets_used' => (isset($arrMetaOut['sheets']) && is_array($arrMetaOut['sheets'])) ? $arrMetaOut['sheets'] : array(),
'epr_guess' => $tabEprGuess,
'pay_guess' => $tabPayGuess,
'pro_guess' => $tabProGuess,
'needs_sheet_choice' => false,
);
}
function fxImportResultatsSaveUpload($intEveId, array $arrFile) {
@set_time_limit(180);
@ini_set('memory_limit', '512M');
$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');
}
// MSIN-4520 — Excel multi-onglets : profil événement (onglets = épreuves) ou question
if (in_array($strSafeExt, array('xls', 'xlsx'), true)) {
$arrList = fxImportResultatsListExcelSheets($strDest);
if (($arrList['state'] ?? '') !== 'ok') {
@unlink($strDest);
return $arrList;
}
$tabSheets = $arrList['sheets'] ?? array();
if (count($tabSheets) > 1) {
$arrMeta = array(
'eve_id' => $intEveId,
'token' => $strToken,
'original_name' => $strName,
'path' => $strDest,
'sheet_mode' => '',
'needs_sheet_choice' => true,
'created_at' => date('c'),
);
fxImportResultatsSaveMeta($strDest, $arrMeta);
// Profil sauvé pour CET événement → skip la question, mapping prêt à importer
$arrProfile = fxImportResultatsFindEventImportProfile($intEveId);
$arrMapProf = is_array($arrProfile) ? ($arrProfile['mapping'] ?? array()) : array();
$strModeProf = (string)($arrMapProf['sheet_mode'] ?? '');
if ($strModeProf === 'sheets') {
$tabWant = isset($arrMapProf['sheets']) && is_array($arrMapProf['sheets']) ? $arrMapProf['sheets'] : array();
$tabUse = fxImportResultatsIntersectSheetNames($tabWant, $tabSheets);
// Ancien profil sans liste d'onglets : reprendre les suggestions fichier
if (count($tabUse) < 1) {
foreach ($tabSheets as $arrS) {
if (!empty($arrS['suggested']) && !empty($arrS['name'])) {
$tabUse[] = (string)$arrS['name'];
}
}
}
if (count($tabUse) >= 1) {
$arrAuto = fxImportResultatsChooseSheets($intEveId, $strToken, 'sheets', '', $tabUse);
if (($arrAuto['state'] ?? '') === 'ok') {
$arrAuto['auto_applied'] = true;
$arrAuto['applied_mapping'] = $arrMapProf;
$arrAuto['applied_template'] = array(
'tpl_id' => intval($arrProfile['tpl_id'] ?? 0),
'name' => (string)($arrProfile['name'] ?? ''),
'same_event' => true,
);
$arrAuto['message'] = 'Profil « '
. (string)($arrProfile['name'] ?? 'événement')
. ' » réappliqué (onglets = épreuves, ' . count($tabUse) . ' onglet(s)) — '
. intval($arrAuto['row_count'] ?? 0)
. ' ligne(s). Prêt à importer.';
return $arrAuto;
}
}
} elseif ($strModeProf === 'column') {
$strSheetProf = trim((string)($arrMapProf['sheet'] ?? ''));
$blnSheetOk = false;
if ($strSheetProf !== '') {
foreach ($tabSheets as $arrS) {
if ((string)($arrS['name'] ?? '') === $strSheetProf) {
$blnSheetOk = true;
break;
}
}
}
if ($blnSheetOk) {
$arrAuto = fxImportResultatsChooseSheets($intEveId, $strToken, 'column', $strSheetProf, array());
if (($arrAuto['state'] ?? '') === 'ok') {
$arrAuto['auto_applied'] = true;
$arrAuto['applied_mapping'] = $arrMapProf;
$arrAuto['applied_template'] = array(
'tpl_id' => intval($arrProfile['tpl_id'] ?? 0),
'name' => (string)($arrProfile['name'] ?? ''),
'same_event' => true,
);
$arrAuto['message'] = 'Profil « '
. (string)($arrProfile['name'] ?? 'événement')
. ' » réappliqué (feuille « ' . $strSheetProf . ' ») — prêt à importer.';
return $arrAuto;
}
}
}
return array(
'state' => 'ok',
'token' => $strToken,
'original_name' => $strName,
'needs_sheet_choice' => true,
'sheet_count' => count($tabSheets),
'sheets' => $tabSheets,
'epreuves' => fxImportResultatsEpreuvesList($intEveId),
'event_profile' => $arrProfile,
'message' => 'Ce fichier a ' . count($tabSheets) . ' onglets. Les onglets doivent-ils servir à classer dans les épreuves ?',
);
}
}
$arrParsed = fxImportResultatsParseFile($strDest, $strName, array('sheet_mode' => 'column'));
if (($arrParsed['state'] ?? '') !== 'ok') {
@unlink($strDest);
@unlink(fxImportResultatsMetaPath($strDest));
return $arrParsed;
}
return fxImportResultatsBuildMappingPayload($intEveId, $strToken, $strDest, $strName, $arrParsed, array(
'sheet_mode' => 'column',
));
}
/**
* MSIN-4520 — après upload multi-onglets : mode colonne (1 feuille) ou onglets = épreuves.
* @param string $strMode column|sheets
* @param string $strSheet onglet unique (mode column)
* @param array $tabSheets noms d'onglets (mode sheets)
* @return array
*/
function fxImportResultatsChooseSheets($intEveId, $strToken, $strMode, $strSheet = '', array $tabSheets = array()) {
$intEveId = intval($intEveId);
$strPath = fxImportResultatsResolveTokenPath($intEveId, $strToken);
if ($strPath === null) {
return array('state' => 'error', 'message' => 'Fichier temporaire introuvable — réuploadez.');
}
$arrMeta = fxImportResultatsLoadMeta($strPath);
$strName = (string)($arrMeta['original_name'] ?? basename($strPath));
$strMode = trim((string)$strMode);
if ($strMode !== 'column' && $strMode !== 'sheets') {
return array('state' => 'error', 'message' => 'Choix d\'onglets invalide.');
}
if ($strMode === 'column') {
$strSheet = trim((string)$strSheet);
if ($strSheet === '') {
// Première feuille « data » suggérée, sinon active (vide = active PHPExcel)
$arrList = fxImportResultatsListExcelSheets($strPath);
if (($arrList['state'] ?? '') === 'ok') {
foreach ($arrList['sheets'] as $arrS) {
if (!empty($arrS['suggested'])) {
$strSheet = (string)$arrS['name'];
break;
}
}
if ($strSheet === '' && !empty($arrList['sheets'][0]['name'])) {
$strSheet = (string)$arrList['sheets'][0]['name'];
}
}
}
$arrOpts = array('sheet_mode' => 'column', 'sheet' => $strSheet);
$arrParsed = fxImportResultatsParseExcel($strPath, $arrOpts);
if (($arrParsed['state'] ?? '') !== 'ok') {
return $arrParsed;
}
return fxImportResultatsBuildMappingPayload($intEveId, $strToken, $strPath, $strName, $arrParsed, array(
'sheet_mode' => 'column',
'sheet' => $strSheet,
'sheets' => array(),
));
}
$tabClean = array();
foreach ($tabSheets as $strN) {
$strN = trim((string)$strN);
if ($strN !== '' && !in_array($strN, $tabClean, true)) {
$tabClean[] = $strN;
}
}
if (count($tabClean) < 1) {
return array('state' => 'error', 'message' => 'Cochez au moins un onglet à importer.');
}
$arrParsed = fxImportResultatsParseExcel($strPath, array(
'sheet_mode' => 'sheets',
'sheets' => $tabClean,
));
if (($arrParsed['state'] ?? '') !== 'ok') {
return $arrParsed;
}
$arrParsed['sheets'] = $tabClean;
return fxImportResultatsBuildMappingPayload($intEveId, $strToken, $strPath, $strName, $arrParsed, array(
'sheet_mode' => 'sheets',
'sheets' => $tabClean,
'sheet' => '',
));
}
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;
}
/**
* MSIN-4520 — dernier profil d'import propre à CET événement (hors empreinte colonnes).
* Sert à réappliquer « onglets = épreuves » dès le drop du prochain fichier.
* @return array|null
*/
function fxImportResultatsFindEventImportProfile($intEveId) {
global $objDatabase;
$intEveId = intval($intEveId);
if ($intEveId <= 0) {
return null;
}
$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 null;
}
$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 eve_id = " . $intEveId
. " ORDER BY tpl_maj DESC LIMIT 25";
$tab = $objDatabase->fxGetResults($sql);
if (!is_array($tab) || count($tab) < 1) {
return null;
}
$arrFallback = null;
for ($i = 1; $i <= count($tab); $i++) {
$arrMap = json_decode((string)($tab[$i]['tpl_mapping_json'] ?? ''), true);
if (!is_array($arrMap)) {
$arrMap = array();
}
$arrRow = 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' => $arrMap,
'same_event' => true,
'sheet_mode' => (string)($arrMap['sheet_mode'] ?? ''),
);
if ($arrFallback === null) {
$arrFallback = $arrRow;
}
// Priorité au profil multi-onglets
if (($arrMap['sheet_mode'] ?? '') === 'sheets') {
return $arrRow;
}
}
return $arrFallback;
}
/**
* MSIN-4520 — intersection noms d'onglets profil ↔ fichier actuel.
* @param array $tabWant noms sauvés dans le template
* @param array $tabFileSheets [{name,...}, ...]
* @return string[]
*/
function fxImportResultatsIntersectSheetNames(array $tabWant, array $tabFileSheets) {
$tabAvail = array();
foreach ($tabFileSheets as $arrS) {
$strN = trim((string)($arrS['name'] ?? ''));
if ($strN !== '') {
$tabAvail[$strN] = true;
}
}
$tabOut = array();
foreach ($tabWant as $strN) {
$strN = trim((string)$strN);
if ($strN !== '' && isset($tabAvail[$strN]) && !in_array($strN, $tabOut, true)) {
$tabOut[] = $strN;
}
}
return $tabOut;
}
/**
* @param array $arrMapping {
* columns: string[],
* epr_values: {value: epr_id},
* questions: {colIndex: que_id|"new"},
* sheet_mode?: string,
* sheets?: string[],
* sheet?: string
* }
*/
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.');
}
$arrMeta = fxImportResultatsLoadMeta($strPath);
$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);
}
// MSIN-4520 — figer le choix onglets dans le profil (même si le JS oublie)
if (empty($arrMapping['sheet_mode']) && !empty($arrMeta['sheet_mode'])) {
$arrMapping['sheet_mode'] = (string)$arrMeta['sheet_mode'];
}
if (empty($arrMapping['sheets']) && !empty($arrMeta['sheets']) && is_array($arrMeta['sheets'])) {
$arrMapping['sheets'] = $arrMeta['sheets'];
}
if (empty($arrMapping['sheet']) && !empty($arrMeta['sheet'])) {
$arrMapping['sheet'] = (string)$arrMeta['sheet'];
}
if (($arrMapping['sheet_mode'] ?? '') === 'sheets' && empty($arrMapping['sheets']) && is_array($arrMeta['sheets'] ?? null)) {
$arrMapping['sheets'] = $arrMeta['sheets'];
}
$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());
}
$strMsg = 'Profil enregistré pour cet événement.';
if (($arrMapping['sheet_mode'] ?? '') === 'sheets') {
$intNb = is_array($arrMapping['sheets'] ?? null) ? count($arrMapping['sheets']) : 0;
$strMsg .= ' Prochain fichier multi-onglets : réapplication auto (onglets = épreuves'
. ($intNb > 0 ? ', ' . $intNb . ' onglet(s)' : '')
. ') — prêt à importer.';
} else {
$strMsg .= ' Le prochain fichier avec les mêmes colonnes le proposera.';
}
return array(
'state' => 'ok',
'tpl_id' => $intTplId,
'message' => $strMsg,
'sheet_mode' => (string)($arrMapping['sheet_mode'] ?? ''),
'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',
'bad_naissance'=> 'Date de naissance non reconnue',
'bad_pays' => 'Pays non matché',
'bad_province' => 'Province non matchée',
'other' => 'Autre',
);
$tabTypeOrder = array(
'no_external', 'no_race', 'insert_pec', 'insert_par', 'update_fail',
'bad_naissance', 'bad_pays', 'bad_province', '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 / MSIN-4520 — parse dates fichier (m-d-Y, datetime Excel, serial). */
function fxImportResultatsNormalizeBirthdate($str) {
$str = trim((string)$str);
if ($str === '' || $str === '0000-00-00' || strtolower($str) === 'null') {
return '';
}
// Serial Excel (jours depuis 1899-12-30) — PHPExcel peut renvoyer un float
if (is_numeric($str)) {
$float = floatval($str);
// Plage raisonnable naissance : ~19002100 ≈ serial 1 … 73050
if ($float > 200 && $float < 80000) {
$intDays = intval($float);
$objEpoch = new DateTime('1899-12-30');
$objEpoch->modify('+' . $intDays . ' days');
$intY = intval($objEpoch->format('Y'));
if ($intY >= 1900 && $intY <= 2100) {
return $objEpoch->format('Y-m-d');
}
}
}
// Couper l'heure : « 02-14-1988 00:00:00 » / « 02-14-1988 00:00: »
if (preg_match('/^(\d{1,4}[\/\-.]\d{1,2}[\/\-.]\d{1,4})\b/', $str, $mDateOnly)) {
$strDatePart = $mDateOnly[1];
} else {
$strDatePart = $str;
}
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})/', $strDatePart, $m)) {
$intY = intval($m[1]);
$intM = intval($m[2]);
$intD = intval($m[3]);
if (checkdate($intM, $intD, $intY)) {
return sprintf('%04d-%02d-%02d', $intY, $intM, $intD);
}
}
// m/d/y ou m-d-y (US fréquent dans exports clients)
if (preg_match('/^(\d{1,2})[\/\-.](\d{1,2})[\/\-.](\d{2,4})$/', $strDatePart, $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);
}
}
// strtotime sur la partie date seule, puis chaîne complète
foreach (array($strDatePart, $str) as $strTry) {
$intTs = strtotime($strTry);
if ($intTs !== false) {
$intY = intval(date('Y', $intTs));
if ($intY >= 1900 && $intY <= 2100) {
return date('Y-m-d', $intTs);
}
}
}
return '';
}
/**
* MSIN-4520 — écrit un CSV d'anomalies import (téléchargeable).
* @param array $tabAnomalies
* @return array{path?:string,token?:string,count:int}
*/
function fxImportResultatsWriteAnomaliesCsv($intEveId, $strToken, array $tabAnomalies) {
$intEveId = intval($intEveId);
$strToken = preg_replace('/[^a-f0-9]/', '', strtolower((string)$strToken));
$intCount = count($tabAnomalies);
if ($intEveId <= 0 || $strToken === '' || $intCount < 1) {
return array('count' => 0);
}
$strPath = fxImportResultatsUploadDir() . '/' . $intEveId . '_' . $strToken . '_anomalies.csv';
$fp = @fopen($strPath, 'wb');
if (!$fp) {
return array('count' => $intCount);
}
// BOM UTF-8 pour Excel
fwrite($fp, "\xEF\xBB\xBF");
fputcsv($fp, array('ligne', 'code', 'message', 'nom', 'external_id', 'valeur_brute', 'dossard'), ';');
foreach ($tabAnomalies as $arrA) {
fputcsv($fp, array(
(string)($arrA['line'] ?? ''),
(string)($arrA['code'] ?? ''),
(string)($arrA['message'] ?? ''),
(string)($arrA['name'] ?? ''),
(string)($arrA['external_id'] ?? ''),
(string)($arrA['raw'] ?? ''),
(string)($arrA['no_bib'] ?? ''),
), ';');
}
fclose($fp);
return array(
'count' => $intCount,
'token' => $strToken,
'path' => $strPath,
'filename' => basename($strPath),
);
}
/**
* MSIN-4520 — sert le CSV d'anomalies (téléchargement).
* @return array|null payload erreur, ou null si headers envoyés + exit caller
*/
function fxImportResultatsDownloadAnomalies($intEveId, $strToken) {
$intEveId = intval($intEveId);
$strToken = preg_replace('/[^a-f0-9]/', '', strtolower((string)$strToken));
if ($intEveId <= 0 || strlen($strToken) < 16) {
return array('state' => 'error', 'message' => 'token invalide');
}
$strPath = fxImportResultatsUploadDir() . '/' . $intEveId . '_' . $strToken . '_anomalies.csv';
if (!is_file($strPath)) {
return array('state' => 'error', 'message' => 'Fichier anomalies introuvable — réimportez.');
}
return array(
'state' => 'ok',
'path' => $strPath,
'filename' => 'import_anomalies_eve' . $intEveId . '.csv',
);
}
/**
* MSIN-4520 — catalogue pays (cache) pour résolution import.
* @return array<int,array{id:int,keys:string[]}>
*/
function fxImportResultatsPaysCatalog() {
global $objDatabase;
static $tabCache = null;
if ($tabCache !== null) {
return $tabCache;
}
$tabCache = array();
$sql = "SELECT pay_id, pay_nom_fr, pay_nom_en, pay_iso"
. " FROM inscriptions_pays WHERE pay_actif = 1";
// pay_iso3 optionnel selon schémas
$tab = $objDatabase->fxGetResults($sql);
if (!is_array($tab)) {
return $tabCache;
}
for ($i = 1; $i <= count($tab); $i++) {
$intId = intval($tab[$i]['pay_id'] ?? 0);
if ($intId <= 0) {
continue;
}
$tabKeys = array();
foreach (array('pay_nom_fr', 'pay_nom_en', 'pay_iso') as $strCol) {
$strV = trim((string)($tab[$i][$strCol] ?? ''));
if ($strV !== '') {
$tabKeys[] = fxImportResultatsFoldKey($strV);
}
}
$tabCache[] = array('id' => $intId, 'keys' => $tabKeys);
}
return $tabCache;
}
/**
* MSIN-4520 — catalogue provinces (cache).
* @return array<int,array{id:int,keys:string[]}>
*/
function fxImportResultatsProvincesCatalog() {
global $objDatabase;
static $tabCache = null;
if ($tabCache !== null) {
return $tabCache;
}
$tabCache = array();
$sql = "SELECT pro_id, pro_nom_fr, pro_nom_en, pro_iso"
. " FROM inscriptions_provinces WHERE pro_actif = 1";
$tab = $objDatabase->fxGetResults($sql);
if (!is_array($tab)) {
return $tabCache;
}
for ($i = 1; $i <= count($tab); $i++) {
$intId = intval($tab[$i]['pro_id'] ?? 0);
if ($intId <= 0) {
continue;
}
$tabKeys = array();
foreach (array('pro_nom_fr', 'pro_nom_en', 'pro_iso') as $strCol) {
$strV = trim((string)($tab[$i][$strCol] ?? ''));
if ($strV !== '') {
$tabKeys[] = fxImportResultatsFoldKey($strV);
}
}
$tabCache[] = array('id' => $intId, 'keys' => $tabKeys);
}
return $tabCache;
}
/**
* MSIN-4520 — valeur fichier → pay_id (nom, ISO2, alias courants).
*/
function fxImportResultatsResolvePayId($strValue) {
$strFold = fxImportResultatsFoldKey($strValue);
if ($strFold === '') {
return 0;
}
// Alias fréquents fichier client
$tabAlias = array(
'ca' => array('canada', 'can'),
'canada' => array('ca', 'can'),
'us' => array('usa', 'united states', 'etats unis', 'etats-unis', 'u s a'),
'usa' => array('us', 'united states', 'etats unis'),
'united states' => array('us', 'usa'),
'france' => array('fr'),
'fr' => array('france'),
);
$tabTry = array($strFold);
if (isset($tabAlias[$strFold])) {
$tabTry = array_merge($tabTry, $tabAlias[$strFold]);
}
foreach (fxImportResultatsPaysCatalog() as $arrPay) {
foreach ($arrPay['keys'] as $strKey) {
if ($strKey === '') {
continue;
}
if (in_array($strKey, $tabTry, true) || in_array($strFold, array($strKey), true)) {
return intval($arrPay['id']);
}
}
}
// Contient (ex. « Canada (CA) »)
foreach (fxImportResultatsPaysCatalog() as $arrPay) {
foreach ($arrPay['keys'] as $strKey) {
if ($strKey !== '' && mb_strlen($strKey, 'UTF-8') >= 3
&& (strpos($strFold, $strKey) !== false || strpos($strKey, $strFold) !== false)
) {
return intval($arrPay['id']);
}
}
}
return 0;
}
/**
* MSIN-4520 — valeur fichier → pro_id (Québec / Quebec / QC…).
*/
function fxImportResultatsResolveProId($strValue) {
$strFold = fxImportResultatsFoldKey($strValue);
if ($strFold === '') {
return 0;
}
$tabAlias = array(
'quebec' => array('qc', 'pq', 'québec'),
'qc' => array('quebec'),
'ontario' => array('on'),
'on' => array('ontario'),
'alberta' => array('ab'),
'ab' => array('alberta'),
'british columbia' => array('bc', 'colombie britannique'),
'bc' => array('british columbia', 'colombie britannique'),
'colombie britannique' => array('bc', 'british columbia'),
);
$tabTry = array($strFold);
if (isset($tabAlias[$strFold])) {
foreach ($tabAlias[$strFold] as $strA) {
$tabTry[] = fxImportResultatsFoldKey($strA);
}
}
foreach (fxImportResultatsProvincesCatalog() as $arrPro) {
foreach ($arrPro['keys'] as $strKey) {
if ($strKey !== '' && in_array($strKey, $tabTry, true)) {
return intval($arrPro['id']);
}
}
}
foreach (fxImportResultatsProvincesCatalog() as $arrPro) {
foreach ($arrPro['keys'] as $strKey) {
if ($strKey !== '' && mb_strlen($strKey, 'UTF-8') >= 3
&& (strpos($strFold, $strKey) !== false || strpos($strKey, $strFold) !== false)
) {
return intval($arrPro['id']);
}
}
}
return 0;
}
/**
* 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_validation = '',"
. " 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();
$tabPayValues = isset($arrMapping['pay_values']) && is_array($arrMapping['pay_values']) ? $arrMapping['pay_values'] : array();
$tabProValues = isset($arrMapping['pro_values']) && is_array($arrMapping['pro_values']) ? $arrMapping['pro_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();
$tabAnomalies = 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' => '',
'pro_id' => 0,
'par_codepostal' => '',
'pay_id' => 0,
'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') {
$strNorm = fxImportResultatsNormalizeBirthdate($strRaw);
$arrFields[$strKey] = $strNorm;
// MSIN-4520 — ne pas décider en silence : anomalie si valeur présente mais non parsée
if ($strRaw !== '' && $strNorm === '') {
$tabAnomalies[] = array(
'code' => 'bad_naissance',
'message' => 'Date de naissance non reconnue (participant importé sans date)',
'line' => $intLine,
'name' => $strNamePreview,
'external_id' => $strExt,
'no_bib' => $strBibPreview,
'raw' => $strRaw,
);
}
} elseif ($strKey === 'par_age' || $strKey === 'par_no_equipe') {
$arrFields[$strKey] = intval($strRaw);
} elseif ($strKey === 'pay_id') {
// MSIN-4520 — mapping manuel prioritaire, sinon auto-resolve
if (array_key_exists($strRaw, $tabPayValues)) {
$arrFields[$strKey] = intval($tabPayValues[$strRaw]);
} else {
$arrFields[$strKey] = fxImportResultatsResolvePayId($strRaw);
}
if ($strRaw !== '' && intval($arrFields[$strKey]) <= 0) {
$tabAnomalies[] = array(
'code' => 'bad_pays',
'message' => 'Pays non matché (participant importé avec pay_id=0)',
'line' => $intLine,
'name' => $strNamePreview,
'external_id' => $strExt,
'no_bib' => $strBibPreview,
'raw' => $strRaw,
);
}
} elseif ($strKey === 'pro_id') {
if (array_key_exists($strRaw, $tabProValues)) {
$arrFields[$strKey] = intval($tabProValues[$strRaw]);
} else {
$arrFields[$strKey] = fxImportResultatsResolveProId($strRaw);
}
if ($strRaw !== '' && intval($arrFields[$strKey]) <= 0) {
$tabAnomalies[] = array(
'code' => 'bad_province',
'message' => 'Province/région non matchée (participant importé avec pro_id=0)',
'line' => $intLine,
'name' => $strNamePreview,
'external_id' => $strExt,
'no_bib' => $strBibPreview,
'raw' => $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']) . "',"
. " pro_id = " . intval($arrFields['pro_id']) . ","
. " par_codepostal = '" . fxImportResultatsDbEsc($arrFields['par_codepostal']) . "',"
. " pay_id = " . intval($arrFields['pay_id']) . ","
. " 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 = " . intval($arrFields['pro_id']) . ","
. " par_codepostal = '" . fxImportResultatsDbEsc($arrFields['par_codepostal']) . "',"
. " pay_id = " . intval($arrFields['pay_id']) . ","
. " 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);
$intAnomalies = count($tabAnomalies);
// MSIN-4520 — détail anomalies = CSV seulement (pas de tableau à lécran)
$arrCsv = fxImportResultatsWriteAnomaliesCsv($intEveId, $strToken, $tabAnomalies);
$blnCsvOk = !empty($arrCsv['path']) && is_file((string)$arrCsv['path']);
$intTotal = count($tabRows);
$strMsg = 'Import terminé : ' . $intCreated . ' créé(s), ' . $intUpdated . ' mis à jour.'
. ($intBlocked > 0 ? (' ' . $intBlocked . ' exclus.') : '')
. ($intAnomalies > 0
? (' ' . $intAnomalies . ' anomalie(s) — télécharger le CSV sous les compteurs.')
: '');
return array(
'state' => 'ok',
'message' => $strMsg,
'created' => $intCreated,
'updated' => $intUpdated,
'skipped' => $intBlocked,
'row_count' => $intTotal,
'blocked_count' => $intBlocked,
'blocked_by_type' => $tabBlockedByType,
'anomaly_count' => $intAnomalies,
'anomalies_token' => (string)($arrCsv['token'] ?? $strToken),
'anomalies_csv' => $blnCsvOk,
// Compteurs type CT (KPI)
'kpi_total' => $intTotal,
'kpi_ok' => $intCreated + $intUpdated,
'kpi_blocked' => $intBlocked,
'kpi_anomalies' => $intAnomalies,
'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-4520</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>profil / template</strong> est sauvegardé pour cet événement
(y compris <em>onglets = épreuves</em>). Au prochain dépôt, le mapping est
réappliqué automatiquement — il reste à cliquer <strong>Importer</strong>.
Incluez une colonne <strong>épreuve</strong> <em>ou</em> utilisez des <strong>onglets Excel = épreuves</strong>,
plus 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_flash" class="alert py-2 mt-2 mb-0" hidden></div>
<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>
<!-- MSIN-4520 — choix multi-onglets avant mapping -->
<div id="ms1_import_step_sheets" 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_sheets_file_label"></strong>
<span class="text-muted small" id="ms1_import_sheets_count_label"></span>
</div>
<button type="button" class="btn btn-sm btn-outline-secondary" id="ms1_import_sheets_cancel">Autre fichier</button>
</div>
<div class="alert alert-info py-2">
<strong>Plusieurs onglets détectés.</strong>
Les onglets doivent-ils servir à classer les participants dans les bonnes épreuves&nbsp;?
<div class="small text-muted mt-1 mb-0">
Ex. Boréalys&nbsp;: un onglet par distance (100K, Relais…) — les onglets «&nbsp;Tous&nbsp;» / «&nbsp;détail&nbsp;» sont exclus par défaut.
</div>
</div>
<div class="btn-group mb-3" role="group" aria-label="Mode onglets">
<button type="button" class="btn btn-success btn-sm" id="ms1_import_sheets_yes">
Oui — onglets = épreuves
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" id="ms1_import_sheets_no">
Non — une feuille + colonne épreuve
</button>
</div>
<div id="ms1_import_sheets_pick" hidden>
<h6 class="ms1-import__h">Onglets à importer</h6>
<p class="small text-muted mb-2">Décochez les résumés / doublons. Chaque onglet coché sera mappé vers une épreuve MS1.</p>
<div class="table-responsive mb-2">
<table class="table table-sm table-bordered mb-0" id="ms1_import_sheets_table">
<thead>
<tr>
<th style="width:2.5rem;"></th>
<th>Onglet</th>
<th>Lignes</th>
<th>Type</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="form-inline flex-wrap mb-2" id="ms1_import_sheets_single_wrap" hidden>
<label class="mr-2 mb-2" for="ms1_import_sheets_single">Feuille à lire</label>
<select class="form-control form-control-sm mb-2" id="ms1_import_sheets_single" style="min-width:14rem;"></select>
</div>
<button type="button" class="btn btn-primary btn-sm" id="ms1_import_sheets_continue">
Continuer vers le mapping
</button>
</div>
</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 class="ct-api-kpi ct-api-kpi--warn" data-kpi="anomalies"><div class="ct-api-kpi__label">Anomalies</div>
<div class="ct-api-kpi__value" id="ms1_import_kpi_anomalies">—</div></div>
</div>
<div id="ms1_import_anom_banner" class="alert alert-warning mb-3" hidden>
<div class="d-flex flex-wrap align-items-center justify-content-between">
<div>
<strong>Anomalies dimport</strong>
<span id="ms1_import_anom_banner_txt" class="ml-1"></span>
</div>
<button type="button" class="btn btn-warning btn-sm mt-2 mt-sm-0" id="ms1_import_anom_download_banner">
<i class="fa fa-download" aria-hidden="true"></i> Télécharger le CSV des anomalies
</button>
</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" id="ms1_import_epr_title">2. Valeurs d'épreuve → épreuves de l'événement</h6>
<p class="small text-muted mb-2" id="ms1_import_epr_hint" hidden></p>
<div class="table-responsive">
<table class="table table-sm table-bordered mb-0" id="ms1_import_epr_table">
<thead>
<tr>
<th id="ms1_import_epr_col_label">Valeur dans le fichier</th>
<th>Épreuve MS1</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div id="ms1_import_pay_box" class="ms1-import__pay mb-3" hidden>
<h6 class="ms1-import__h">Pays — valeurs fichier → catalogue MS1</h6>
<p class="small text-muted mb-2">Prérempli automatiquement quand reconnu ; corrigez seulement les lignes vides ou fausses (sauvé dans le profil).</p>
<div class="table-responsive">
<table class="table table-sm table-bordered mb-0" id="ms1_import_pay_table">
<thead>
<tr>
<th>Valeur dans le fichier</th>
<th>Pays MS1</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div id="ms1_import_pro_box" class="ms1-import__pro mb-3" hidden>
<h6 class="ms1-import__h">Région / province — valeurs fichier → catalogue MS1</h6>
<p class="small text-muted mb-2">Même logique que le pays (auto + correction manuelle).</p>
<div class="table-responsive">
<table class="table table-sm table-bordered mb-0" id="ms1_import_pro_table">
<thead>
<tr>
<th>Valeur dans le fichier</th>
<th>Province 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(6, 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;
align-items: center;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 0.5rem;
}
#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;
}
/* MSIN-4520 — reset / statut BD restent cliquables même si limport laisse is-busy-op */
#ms1-import-resultats.is-busy-op #ms1_import_db_status_wrap {
pointer-events: auto;
opacity: 1;
position: relative;
z-index: 2;
}
/* 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();
}
// Forcer la disparition de loverlay (fadeOut seul peut laisser des clics morts)
var $pre = $('#preloader');
if ($pre.length) {
$pre.stop(true, true).hide();
}
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 intAnom = (res && res.anomaly_count) ? res.anomaly_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_kpi_anomalies').text(String(intAnom));
$('#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>');
} else {
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);
}
renderAnomaliesPanel(res);
}
function renderAnomaliesPanel(res) {
var $banner = $('#ms1_import_anom_banner');
var intAnom = (res && res.anomaly_count) ? res.anomaly_count : 0;
var tok = (res && res.anomalies_token) ? res.anomalies_token : ((state && state.token) ? state.token : '');
if (intAnom <= 0) {
$banner.prop('hidden', true);
return;
}
$banner.prop('hidden', false);
$('#ms1_import_anom_banner_txt').text(
intAnom + ' ligne(s) importée(s) avec un champ non appliqué — détail dans le CSV.'
);
$('#ms1_import_anom_download_banner').data('anom-token', tok);
try {
if ($banner[0] && $banner[0].scrollIntoView) {
$banner[0].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
} catch (e) {}
}
function downloadAnomaliesCsv(tok) {
tok = tok || (state && state.token) || '';
if (!tok) {
showStatus('warning', 'Aucun CSV d\'anomalies — le fichier n\'a peut-être pas pu être écrit sur le serveur. Réimportez.');
return;
}
var url = ajaxUrl
+ '?a=download_anomalies'
+ '&eve_id=' + encodeURIComponent(String(eveId))
+ '&token=' + encodeURIComponent(String(tok));
window.location.href = url;
}
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_sheets').attr('hidden', true);
$('#ms1_import_sheets_pick').attr('hidden', true);
$('#ms1_import_sheets_single_wrap').attr('hidden', true);
$('#ms1_import_step_map').attr('hidden', true);
$('#ms1_import_templates').attr('hidden', true).empty();
$('#ms1_import_epr_box').attr('hidden', true);
$('#ms1_import_pay_box').attr('hidden', true);
$('#ms1_import_pro_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_anom_banner').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 pay_values = {};
$('#ms1_import_pay_table tbody tr').each(function () {
var v = $(this).attr('data-val');
var pay = parseInt($(this).find('select').val(), 10) || 0;
if (v != null) pay_values[v] = pay;
});
var pro_values = {};
$('#ms1_import_pro_table tbody tr').each(function () {
var v = $(this).attr('data-val');
var pro = parseInt($(this).find('select').val(), 10) || 0;
if (v != null) pro_values[v] = pro;
});
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;
});
// MSIN-4520 — figer mode onglets + pays/province dans le profil événement
var mapping = {
columns: columns,
epr_values: epr_values,
pay_values: pay_values,
pro_values: pro_values,
questions: questions
};
mapping.sheet_mode = String((state && state.sheet_mode) || 'column');
if (mapping.sheet_mode === 'sheets') {
mapping.sheets = (state && state.sheets_used)
? state.sheets_used
: (state && state.appliedMapping && state.appliedMapping.sheets
? state.appliedMapping.sheets
: []);
// Valeurs d'épreuve actuelles = noms d'onglets utilisés
if ((!mapping.sheets || !mapping.sheets.length) && state && state.distincts) {
var eprCol = -1;
columns.forEach(function (k, i) { if (k === 'epr') eprCol = i; });
if (eprCol >= 0 && state.distincts[eprCol]) {
mapping.sheets = state.distincts[eprCol].slice();
}
}
mapping.sheet = '';
} else {
mapping.sheet = String((state && state.sheet) || (state && state.appliedMapping && state.appliedMapping.sheet) || '');
mapping.sheets = [];
}
return mapping;
}
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 payOptionsHtml(selected) {
var html = '<option value="0">— Choisir / inconnu —</option>';
(state.pays || []).forEach(function (p) {
var sel = (parseInt(p.pay_id, 10) === parseInt(selected, 10)) ? ' selected' : '';
html += '<option value="' + esc(p.pay_id) + '"' + sel + '>' + esc(p.label) + '</option>';
});
return html;
}
function proOptionsHtml(selected) {
var html = '<option value="0">— Choisir / inconnu —</option>';
(state.provinces || []).forEach(function (p) {
var sel = (parseInt(p.pro_id, 10) === parseInt(selected, 10)) ? ' selected' : '';
html += '<option value="' + esc(p.pro_id) + '"' + sel + '>' + esc(p.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 blnSheets = String(state.sheet_mode || '') === 'sheets';
$('#ms1_import_epr_title').text(blnSheets
? '2. Onglets → épreuves de l\'événement'
: '2. Valeurs d\'épreuve → épreuves de l\'événement');
$('#ms1_import_epr_col_label').text(blnSheets ? 'Onglet Excel' : 'Valeur dans le fichier');
if (blnSheets) {
$('#ms1_import_epr_hint').text('Chaque onglet importé est traité comme une valeur d\'épreuve.').removeAttr('hidden');
} else {
$('#ms1_import_epr_hint').attr('hidden', true).text('');
}
var vals = (state.distincts && state.distincts[eprCol]) ? state.distincts[eprCol] : [];
var saved = (state.appliedMapping && state.appliedMapping.epr_values) ? state.appliedMapping.epr_values : {};
var guessed = state.epr_guess || {};
vals.forEach(function (v) {
var cur = saved[v] || guessed[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 renderPayBox() {
var payCol = -1;
$('#ms1_import_map_table tbody select.js-col-map').each(function (i) {
if ($(this).val() === 'pay_id') payCol = i;
});
var $box = $('#ms1_import_pay_box');
var $tb = $('#ms1_import_pay_table tbody').empty();
if (payCol < 0) {
$box.attr('hidden', true);
return;
}
var vals = (state.distincts && state.distincts[payCol]) ? state.distincts[payCol] : [];
var saved = (state.appliedMapping && state.appliedMapping.pay_values) ? state.appliedMapping.pay_values : {};
var guessed = state.pay_guess || {};
vals.forEach(function (v) {
var cur = (saved[v] != null) ? saved[v] : (guessed[v] || 0);
$tb.append(
'<tr data-val="' + esc(v) + '"><td><code>' + esc(v) + '</code></td>'
+ '<td><select class="form-control form-control-sm">' + payOptionsHtml(cur) + '</select></td></tr>'
);
});
$box.removeAttr('hidden');
}
function renderProBox() {
var proCol = -1;
$('#ms1_import_map_table tbody select.js-col-map').each(function (i) {
if ($(this).val() === 'pro_id') proCol = i;
});
var $box = $('#ms1_import_pro_box');
var $tb = $('#ms1_import_pro_table tbody').empty();
if (proCol < 0) {
$box.attr('hidden', true);
return;
}
var vals = (state.distincts && state.distincts[proCol]) ? state.distincts[proCol] : [];
var saved = (state.appliedMapping && state.appliedMapping.pro_values) ? state.appliedMapping.pro_values : {};
var guessed = state.pro_guess || {};
vals.forEach(function (v) {
var cur = (saved[v] != null) ? saved[v] : (guessed[v] || 0);
$tb.append(
'<tr data-val="' + esc(v) + '"><td><code>' + esc(v) + '</code></td>'
+ '<td><select class="form-control form-control-sm">' + proOptionsHtml(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();
renderPayBox();
renderProBox();
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();
renderPayBox();
renderProBox();
renderQueBox();
});
renderEprBox();
renderPayBox();
renderProBox();
renderQueBox();
var blnAuto = !!state.auto_applied;
var $tpl = $('#ms1_import_templates').empty();
if (blnAuto && state.applied_mapping) {
applyMappingObj(state.applied_mapping);
if (state.applied_template && state.applied_template.name) {
$('#ms1_import_tpl_name').val(state.applied_template.name);
}
var strModeLbl = (state.sheet_mode === 'sheets')
? 'onglets = épreuves'
: 'feuille + colonne épreuve';
var htmlAuto = '<div class="alert alert-success py-2 mb-0">'
+ '<strong>Profil événement réappliqué</strong> (' + esc(strModeLbl) + '). '
+ 'Mapping colonnes + épreuves restaurés — vous pouvez importer tout de suite.'
+ '</div>';
$tpl.html(htmlAuto).removeAttr('hidden');
} else 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)'
+ (state.sheet_mode === 'sheets' ? ' · onglets = épreuves' : ''));
$('#ms1_import_step_map').removeAttr('hidden').show();
$('#ms1_import_drop').addClass('is-ready');
if (blnAuto) {
$('#ms1_import_commit').addClass('btn-lg').trigger('focus');
} else {
$('#ms1_import_commit').removeClass('btn-lg');
}
ensurePanelOpen();
}
function renderSheetChoice() {
var sheets = state.sheets || [];
$('#ms1_import_sheets_file_label').text(state.original_name || 'Fichier');
$('#ms1_import_sheets_count_label').text(' · ' + sheets.length + ' onglet(s)');
var $tb = $('#ms1_import_sheets_table tbody').empty();
var $sel = $('#ms1_import_sheets_single').empty();
sheets.forEach(function (s, i) {
var name = s.name || ('Feuille ' + (i + 1));
var checked = s.suggested ? ' checked' : '';
var typeLabel = s.is_summary
? '<span class="badge badge-secondary">résumé / tous</span>'
: (s.looks_like_data
? '<span class="badge badge-success">participants</span>'
: '<span class="badge badge-warning">autre</span>');
$tb.append(
'<tr><td><input type="checkbox" class="js-sheet-chk" value="' + esc(name) + '"' + checked + '></td>'
+ '<td><strong>' + esc(name) + '</strong></td>'
+ '<td>' + esc(String(s.row_count != null ? s.row_count : '—')) + '</td>'
+ '<td>' + typeLabel + '</td></tr>'
);
$sel.append('<option value="' + esc(name) + '">' + esc(name)
+ (s.row_count != null ? (' (' + s.row_count + ')') : '') + '</option>');
});
// Mode colonne : préférer 1re feuille data, sinon « Tous »
var defSheet = '';
sheets.forEach(function (s) {
if (!defSheet && s.suggested) defSheet = s.name;
});
if (!defSheet) {
sheets.forEach(function (s) {
if (!defSheet && s.looks_like_data) defSheet = s.name;
});
}
if (!defSheet && sheets[0]) defSheet = sheets[0].name;
if (defSheet) $sel.val(defSheet);
state._sheetPickMode = null;
$('#ms1_import_sheets_pick').attr('hidden', true);
$('#ms1_import_sheets_single_wrap').attr('hidden', true);
$('#ms1_import_step_map').attr('hidden', true);
$('#ms1_import_step_sheets').removeAttr('hidden').show();
$('#ms1_import_drop').addClass('is-ready');
ensurePanelOpen();
}
function applyChooseSheets(mode) {
if (!state || !state.token) return;
var payload = {
a: 'choose_sheets',
eve_id: eveId,
token: state.token,
sheet_mode: mode
};
if (mode === 'sheets') {
var picked = [];
$('#ms1_import_sheets_table .js-sheet-chk:checked').each(function () {
picked.push($(this).val());
});
if (!picked.length) {
showStatus('warning', 'Cochez au moins un onglet à importer.');
return;
}
payload.sheets = JSON.stringify(picked);
} else {
payload.sheet = $('#ms1_import_sheets_single').val() || '';
}
setBusy(true, mode === 'sheets' ? 'Fusion des onglets…' : 'Lecture de la feuille…',
'Préparation du mapping — quelques secondes.');
showStatus('info', 'Préparation du mapping…');
$.ajax({
url: ajaxUrl,
method: 'POST',
dataType: 'text',
timeout: 180000,
data: payload
}).done(function (raw) {
setBusy(false);
var res = parseAjaxJson(raw);
if (!res || res.state !== 'ok') {
showStatus('danger', (res && res.message) ? res.message : 'Échec préparation onglets');
return;
}
state = res;
state.appliedMapping = null;
$('#ms1_import_step_sheets').attr('hidden', true);
showStatus('success', 'Fichier prêt — ' + (res.row_count || 0) + ' ligne(s). Alignez les colonnes ci-dessous.');
renderMap();
}).fail(function (xhr) {
setBusy(false);
var res = parseAjaxJson(xhr && xhr.responseText);
if (res && res.state === 'ok') {
state = res;
state.appliedMapping = null;
$('#ms1_import_step_sheets').attr('hidden', true);
renderMap();
return;
}
showStatus('danger', 'Erreur réseau (choix onglets).');
});
}
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 = res.applied_mapping || null;
if (res.needs_sheet_choice && !res.auto_applied) {
showStatus('info', res.message || 'Plusieurs onglets détectés — faites un choix ci-dessous.');
renderSheetChoice();
return;
}
if (res.auto_applied) {
showStatus('success', res.message || 'Profil événement réappliqué — prêt à importer.');
renderMap();
return;
}
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 = res.applied_mapping || null;
if (res.needs_sheet_choice && !res.auto_applied) {
renderSheetChoice();
return;
}
showStatus('success', res.message || ('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_anom_download_banner').on('click', function () {
downloadAnomaliesCsv($(this).data('anom-token'));
});
$('#ms1_import_sheets_cancel').on('click', function () {
resetUi();
showStatus('info', 'Choisissez un nouveau fichier.');
});
$('#ms1_import_sheets_yes').on('click', function () {
state._sheetPickMode = 'sheets';
$('#ms1_import_sheets_pick').removeAttr('hidden');
$('#ms1_import_sheets_single_wrap').attr('hidden', true);
$('#ms1_import_sheets_table').closest('.table-responsive').show();
showStatus('info', 'Cochez les onglets (= épreuves), puis Continuer.');
});
$('#ms1_import_sheets_no').on('click', function () {
state._sheetPickMode = 'column';
$('#ms1_import_sheets_pick').removeAttr('hidden');
$('#ms1_import_sheets_single_wrap').removeAttr('hidden');
$('#ms1_import_sheets_table').closest('.table-responsive').hide();
showStatus('info', 'Choisissez la feuille à lire (colonne épreuve dans le fichier), puis Continuer.');
});
$('#ms1_import_sheets_continue').on('click', function () {
var mode = state && state._sheetPickMode ? state._sheetPickMode : '';
if (mode !== 'sheets' && mode !== 'column') {
showStatus('warning', 'Choisissez dabord Oui (onglets = épreuves) ou Non (une feuille).');
return;
}
applyChooseSheets(mode);
});
$('#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) {
var $btn = $('#ms1_import_refresh_status');
var $ico = $btn.find('i.fa');
if (!blnSilent) {
$btn.prop('disabled', true);
$ico.addClass('fa-spin');
$('#ms1_import_db_par, #ms1_import_db_pec, #ms1_import_db_que, #ms1_import_db_list').text('…');
setBusy(true,
'Actualisation des compteurs…',
'Lecture BD (participants import / pec / questions) — le coureur tourne = ça travaille.'
);
showStatus('info', 'Actualisation en cours…');
}
$.ajax({
url: ajaxUrl,
method: 'POST',
dataType: 'text',
timeout: 120000,
data: { a: 'reset_preview', eve_id: eveId }
}).done(function (raw) {
if (!blnSilent) {
setBusy(false);
$btn.prop('disabled', false);
$ico.removeClass('fa-spin');
}
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 (xhr) {
if (!blnSilent) {
setBusy(false);
$btn.prop('disabled', false);
$ico.removeClass('fa-spin');
showStatus('danger', 'Erreur réseau (statut BD, HTTP '
+ (xhr && xhr.status ? xhr.status : '?') + ').');
}
$('#ms1_import_db_par, #ms1_import_db_pec, #ms1_import_db_que, #ms1_import_db_list').text('?');
});
}
// Statut chargé dès l'ouverture du panneau
refreshDbStatus(true);
function showResetFlash(type, msg) {
var $f = $('#ms1_import_reset_flash');
if (!$f.length) {
showStatus(type, msg);
return;
}
$f.prop('hidden', false)
.removeClass('alert-success alert-danger alert-info alert-warning')
.addClass('alert alert-' + (type || 'info'))
.text(msg || '');
showStatus(type, msg);
}
function hideResetConfirm() {
$('#ms1_import_reset_confirm').prop('hidden', true);
$('#ms1_import_reset_confirm_input').val('');
}
$('#ms1_import_refresh_status').on('click', function () {
refreshDbStatus(false);
});
// MSIN-4520 — toujours recompter via AJAX + feedback visible sous le bouton
$root.on('click', '#ms1_import_reset_data', function () {
setBusy(false); // débloque pointer-events si un import a laissé is-busy-op
var $btn = $(this);
var $box = $('#ms1_import_reset_confirm');
hideResetConfirm();
$btn.prop('disabled', true);
showResetFlash('info', 'Comptage BD pour le reset…');
$.ajax({
url: ajaxUrl,
method: 'POST',
dataType: 'text',
timeout: 120000,
data: { a: 'reset_preview', eve_id: eveId }
}).done(function (raw) {
$btn.prop('disabled', false);
var res = parseAjaxJson(raw);
if (!res || res.state !== 'ok') {
showResetFlash('danger', (res && res.message) ? res.message : 'Échec lecture statut BD (reset).');
return;
}
renderDbStatus(res);
var nPar = parseInt(res.participants, 10) || 0;
var nPec = parseInt(res.pec, 10) || 0;
var nQue = parseInt(res.questions, 10) || 0;
var nList = parseInt(res.list_visible, 10) || 0;
if (nPar < 1) {
hideResetConfirm();
showResetFlash('warning', 'Rien à effacer — 0 participant avec par_origine=import_fichier sur cet événement.');
return;
}
var strRecap = 'Vous allez effacer : <strong>' + esc(String(nPar)) + '</strong> participants, '
+ '<strong>' + esc(String(nPec)) + '</strong> pec, '
+ '<strong>' + esc(String(nQue)) + '</strong> réponses questions '
+ '(dont ' + esc(String(nList)) + ' visibles liste).';
$('#ms1_import_reset_confirm_msg').html(strRecap);
$('#ms1_import_reset_confirm_input').val('');
$box.prop('hidden', false);
showResetFlash('warning', 'Confirmation requise juste sous le bouton — tapez RESET puis Confirmer.');
try {
$box[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
$('#ms1_import_reset_confirm_input').trigger('focus');
} catch (e) {}
}).fail(function (xhr) {
$btn.prop('disabled', false);
showResetFlash('danger', 'Erreur réseau (comptage reset, HTTP '
+ (xhr && xhr.status ? xhr.status : '?') + ').');
});
});
$root.on('click', '#ms1_import_reset_confirm_cancel', function () {
hideResetConfirm();
showResetFlash('info', 'Reset annulé.');
});
$root.on('click', '#ms1_import_reset_confirm_go', function () {
var strConfirm = $.trim($('#ms1_import_reset_confirm_input').val() || '');
if (strConfirm !== 'RESET') {
showResetFlash('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.'
);
showResetFlash('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') {
showResetFlash('danger', (res && res.message) ? res.message : 'Échec reset');
return;
}
hideResetConfirm();
showResetFlash('success', res.message || 'Reset OK');
$('#ms1_import_kpi_row').prop('hidden', true);
$('#ms1_import_exclus_shell').prop('hidden', true);
$('#ms1_import_anom_banner').prop('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') {
hideResetConfirm();
showResetFlash('success', res.message || 'Reset OK');
refreshDbStatus(true);
return;
}
showResetFlash('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'));
}