2738 lines
119 KiB
PHP
2738 lines
119 KiB
PHP
<?php
|
||
/**
|
||
* MSIN-4482 — Import participants hors transaction (parse, templates, UI superadm).
|
||
* Écriture resultats_* : étape suivante ; ici upload + mapping + persistance template.
|
||
*/
|
||
|
||
if (!defined('MSIN_IMPORT_RESULTATS_ORIGIN')) {
|
||
define('MSIN_IMPORT_RESULTATS_ORIGIN', 'import_fichier');
|
||
}
|
||
if (!defined('MSIN_TE_NOM_SANS_INSCRIPTION_EN_LIGNE')) {
|
||
define('MSIN_TE_NOM_SANS_INSCRIPTION_EN_LIGNE', 'Sans inscription en ligne');
|
||
}
|
||
|
||
/**
|
||
* MSIN-4482 — te_id du type « Sans inscription en ligne » (0 si pas encore créé en BD).
|
||
*/
|
||
function fxImportResultatsTeIdSansInscriptionEnLigne() {
|
||
global $objDatabase;
|
||
static $intTeId = null;
|
||
if ($intTeId !== null) {
|
||
return $intTeId;
|
||
}
|
||
$intTeId = 0;
|
||
$sql = "SELECT te_id FROM inscriptions_types_evenements"
|
||
. " WHERE te_nom_fr = '" . fxImportResultatsDbEsc(MSIN_TE_NOM_SANS_INSCRIPTION_EN_LIGNE) . "'"
|
||
. " AND te_actif = 1 LIMIT 1";
|
||
$tab = $objDatabase->fxGetResults($sql);
|
||
if (is_array($tab) && !empty($tab[1]['te_id'])) {
|
||
$intTeId = intval($tab[1]['te_id']);
|
||
}
|
||
return $intTeId;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4482 — true si l'événement est typé hors inscription en ligne.
|
||
*/
|
||
function fxImportResultatsEveIsSansInscriptionEnLigne($intEveId) {
|
||
global $objDatabase;
|
||
$intEveId = intval($intEveId);
|
||
$intTeNeed = fxImportResultatsTeIdSansInscriptionEnLigne();
|
||
if ($intEveId <= 0 || $intTeNeed <= 0) {
|
||
return false;
|
||
}
|
||
$sql = "SELECT te_id FROM inscriptions_evenements WHERE eve_id = " . $intEveId . " LIMIT 1";
|
||
$tab = $objDatabase->fxGetResults($sql);
|
||
if (!is_array($tab) || empty($tab[1]['te_id'])) {
|
||
return false;
|
||
}
|
||
return intval($tab[1]['te_id']) === $intTeNeed;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4482 — refuse les actions import si l'eve n'est pas du bon type (protège le business payant).
|
||
* @return array|null null = OK ; sinon payload erreur JSON
|
||
*/
|
||
function fxImportResultatsGuardEveSansInscription($intEveId) {
|
||
$intEveId = intval($intEveId);
|
||
if ($intEveId <= 0) {
|
||
return array('state' => 'error', 'message' => 'eve_id manquant');
|
||
}
|
||
$intTe = fxImportResultatsTeIdSansInscriptionEnLigne();
|
||
if ($intTe <= 0) {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => 'Type « Sans inscription en ligne » absent en BD — exécuter sql/MSIN-4482-type-sans-inscription-en-ligne.sql en dev préprod.',
|
||
);
|
||
}
|
||
if (!fxImportResultatsEveIsSansInscriptionEnLigne($intEveId)) {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => 'Import hors transaction réservé aux événements de type « Sans inscription en ligne ». Changez le Type d\'événement sur la fiche, enregistrez, puis réessayez.',
|
||
'te_id_required' => $intTe,
|
||
);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Champs cibles mappables (hors questions dynamiques).
|
||
* @return array<int, array{key:string,label:string,group:string}>
|
||
*/
|
||
function fxImportResultatsTargetFields() {
|
||
return array(
|
||
array('key' => '', 'label' => '— Ignorer —', 'group' => ''),
|
||
array('key' => 'par_external_id', 'label' => 'Clé unique (ChronoTrack / fichier)', 'group' => 'Identité'),
|
||
array('key' => 'par_prenom', 'label' => 'Prénom', 'group' => 'Identité'),
|
||
array('key' => 'par_nom', 'label' => 'Nom', 'group' => 'Identité'),
|
||
array('key' => 'par_sexe', 'label' => 'Sexe', 'group' => 'Identité'),
|
||
array('key' => 'par_naissance', 'label' => 'Date de naissance', 'group' => 'Identité'),
|
||
array('key' => 'par_age', 'label' => 'Âge', 'group' => 'Identité'),
|
||
array('key' => 'par_courriel', 'label' => 'Courriel', 'group' => 'Contact'),
|
||
array('key' => 'par_telephone1', 'label' => 'Téléphone', 'group' => 'Contact'),
|
||
array('key' => 'par_contact_urgence_nom', 'label' => 'Contact urgence — nom', 'group' => 'Contact'),
|
||
array('key' => 'par_contact_urgence_telephone', 'label' => 'Contact urgence — téléphone', 'group' => 'Contact'),
|
||
array('key' => 'par_adresse', 'label' => 'Adresse', 'group' => 'Contact'),
|
||
array('key' => 'par_ville', 'label' => 'Ville', 'group' => 'Contact'),
|
||
array('key' => 'par_codepostal', 'label' => 'Code postal', 'group' => 'Contact'),
|
||
array('key' => 'no_bib', 'label' => 'Dossard', 'group' => 'Course'),
|
||
array('key' => 'epr', 'label' => 'Épreuve (valeurs → épreuves MS1)', 'group' => 'Course'),
|
||
array('key' => 'par_nom_equipe', 'label' => 'Nom d\'équipe', 'group' => 'Équipe'),
|
||
array('key' => 'par_no_equipe', 'label' => 'No équipe', 'group' => 'Équipe'),
|
||
);
|
||
}
|
||
|
||
function fxImportResultatsEsc($str) {
|
||
return htmlspecialchars((string)$str, ENT_QUOTES, 'UTF-8');
|
||
}
|
||
|
||
function fxImportResultatsUploadDir() {
|
||
$strDir = dirname(__FILE__) . '/../upload/import_resultats';
|
||
if (!is_dir($strDir)) {
|
||
@mkdir($strDir, 0755, true);
|
||
}
|
||
return $strDir;
|
||
}
|
||
|
||
function fxImportResultatsNormalizeHeader($str) {
|
||
$str = trim((string)$str);
|
||
$str = preg_replace('/\s+/u', ' ', $str);
|
||
return mb_strtolower($str, 'UTF-8');
|
||
}
|
||
|
||
/**
|
||
* Empreinte stable des en-têtes (même fichier client = même sig).
|
||
*/
|
||
function fxImportResultatsHeaderSig(array $tabHeaders) {
|
||
$tabNorm = array();
|
||
foreach ($tabHeaders as $strH) {
|
||
$tabNorm[] = fxImportResultatsNormalizeHeader($strH);
|
||
}
|
||
return sha1(implode("\n", $tabNorm));
|
||
}
|
||
|
||
/**
|
||
* Suggestion auto de champ selon le libellé d'en-tête.
|
||
* MSIN-4482 — priorise match exact / aiguille longue ; évite faux positifs sur questions bilingues.
|
||
*/
|
||
function fxImportResultatsGuessField($strHeader) {
|
||
$str = fxImportResultatsNormalizeHeader($strHeader);
|
||
$str = str_replace(array('-', '_'), ' ', $str);
|
||
|
||
// MSIN-4482 — inclut libellés type export client (ex. Échappée / Eventbrite-like)
|
||
$tabRules = array(
|
||
'par_external_id' => array(
|
||
'attendee #', 'attendee id', 'external id', 'external_id', 'unique id', 'uniqueid',
|
||
'entry id', 'id unique', 'ct id', 'chronotrack id', 'reg id', 'participant id', 'attendee',
|
||
),
|
||
'par_prenom' => array('first name', 'firstname', 'given name', 'prenom', 'prénom'),
|
||
'par_nom' => array('last name', 'lastname', 'family name', 'surname'),
|
||
'par_sexe' => array('gender', 'sexe', 'sex', 'genre'),
|
||
'par_naissance' => array('birth date', 'date of birth', 'date naissance', 'naissance', 'birthday', 'dob'),
|
||
'par_age' => array('age', 'âge'),
|
||
'par_courriel' => array('courriel', 'e-mail', 'email', 'mail'),
|
||
'par_telephone1' => array('telephone', 'téléphone', 'phone', 'mobile', 'cell', 'tel'),
|
||
'par_contact_urgence_nom' => array('emergency contact name', 'contact urgence', 'nom en cas d\'urgence', 'urgence nom'),
|
||
'par_contact_urgence_telephone' => array('emergency contact number', 'emergency contact phone', 'numéro du contact en cas d\'urgence', 'urgence tel'),
|
||
'par_adresse' => array('adresse', 'address', 'street'),
|
||
'par_ville' => array('ville', 'city', 'town'),
|
||
'par_codepostal' => array('code postal', 'postal', 'zipcode', 'zip'),
|
||
'no_bib' => array('no dossard', 'bib number', 'dossard', 'no bib', 'bib#', 'bib'),
|
||
'epr' => array(
|
||
'ticket type', 'registration type', 'reg choice', 'event race',
|
||
'epreuve', 'épreuve', 'distance', 'race', 'course', 'ticket',
|
||
),
|
||
'par_nom_equipe' => array('team name', 'nom equipe', 'nom équipe', 'equipe', 'équipe', 'team'),
|
||
'par_no_equipe' => array('team number', 'team no', 'no equipe', 'no équipe', 'team id'),
|
||
);
|
||
|
||
// 1) égalité exacte
|
||
foreach ($tabRules as $strKey => $tabNeedles) {
|
||
foreach ($tabNeedles as $strNeedle) {
|
||
if ($str === $strNeedle) {
|
||
return $strKey;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Questions longues bilingues → pas de guess champ standard (resteront en step questions)
|
||
if (mb_strlen($str, 'UTF-8') > 40 || strpos($str, '?') !== false || strpos($str, ' / ') !== false) {
|
||
// Exceptions courtes déjà couvertes en exact ; ici on laisse vide
|
||
// sauf préfixes connus très spécifiques
|
||
if (strpos($str, 'attendee') === 0) {
|
||
return 'par_external_id';
|
||
}
|
||
return '';
|
||
}
|
||
|
||
// 2) contient — aiguilles les plus longues d'abord
|
||
$tabCandidates = array();
|
||
foreach ($tabRules as $strKey => $tabNeedles) {
|
||
foreach ($tabNeedles as $strNeedle) {
|
||
if ($strNeedle === '') {
|
||
continue;
|
||
}
|
||
// aiguilles 1–3 lettres : mot entier seulement
|
||
if (mb_strlen($strNeedle, 'UTF-8') <= 3) {
|
||
if (preg_match('/(^|\\s)' . preg_quote($strNeedle, '/') . '(\\s|$)/u', $str)) {
|
||
$tabCandidates[] = array('key' => $strKey, 'len' => mb_strlen($strNeedle, 'UTF-8'));
|
||
}
|
||
continue;
|
||
}
|
||
if (strpos($str, $strNeedle) !== false) {
|
||
$tabCandidates[] = array('key' => $strKey, 'len' => mb_strlen($strNeedle, 'UTF-8'));
|
||
}
|
||
}
|
||
}
|
||
if (count($tabCandidates) < 1) {
|
||
return '';
|
||
}
|
||
usort($tabCandidates, function ($a, $b) {
|
||
return $b['len'] - $a['len'];
|
||
});
|
||
return $tabCandidates[0]['key'];
|
||
}
|
||
|
||
/**
|
||
* Clé de partage templates entre événements du même org (pas de com_id sur evenements).
|
||
* MSIN-4482 — stockée dans inscriptions_import_templates.com_id (legacy name = org_id).
|
||
*/
|
||
function fxImportResultatsComIdForEve($intEveId) {
|
||
global $objDatabase;
|
||
$intEveId = intval($intEveId);
|
||
if ($intEveId <= 0) {
|
||
return 0;
|
||
}
|
||
$sql = "SELECT org_id FROM inscriptions_evenements WHERE eve_id = " . $intEveId . " LIMIT 1";
|
||
$tab = $objDatabase->fxGetResults($sql);
|
||
if (!is_array($tab) || empty($tab[1]['org_id'])) {
|
||
return 0;
|
||
}
|
||
return intval($tab[1]['org_id']);
|
||
}
|
||
|
||
function fxImportResultatsEpreuvesList($intEveId) {
|
||
global $objDatabase;
|
||
$intEveId = intval($intEveId);
|
||
$sql = "SELECT epr_id, epr_nom_fr, epr_type_fr, epr_date"
|
||
. " FROM inscriptions_epreuves WHERE eve_id = " . $intEveId
|
||
. " ORDER BY epr_date, epr_nom_fr";
|
||
$tab = $objDatabase->fxGetResults($sql);
|
||
$tabOut = array();
|
||
if (!is_array($tab)) {
|
||
return $tabOut;
|
||
}
|
||
for ($i = 1; $i <= count($tab); $i++) {
|
||
$strLabel = trim((string)($tab[$i]['epr_type_fr'] ?? ''));
|
||
$strNom = trim((string)($tab[$i]['epr_nom_fr'] ?? ''));
|
||
if ($strLabel !== '' && $strNom !== '') {
|
||
$strLabel .= ' — ' . $strNom;
|
||
} elseif ($strNom !== '') {
|
||
$strLabel = $strNom;
|
||
} else {
|
||
$strLabel = 'Épreuve #' . intval($tab[$i]['epr_id']);
|
||
}
|
||
$tabOut[] = array(
|
||
'epr_id' => intval($tab[$i]['epr_id']),
|
||
'label' => $strLabel,
|
||
);
|
||
}
|
||
return $tabOut;
|
||
}
|
||
|
||
function fxImportResultatsQuestionsList($intEveId) {
|
||
global $objDatabase;
|
||
$intEveId = intval($intEveId);
|
||
// que_rapport_label_fr peut manquer sur vieux schémas — fallback question seule
|
||
$sql = "SELECT que_id, que_question_fr, que_actif"
|
||
. " FROM inscriptions_questions WHERE eve_id = " . $intEveId
|
||
. " AND que_actif = 1 ORDER BY que_tri, que_id";
|
||
$tab = $objDatabase->fxGetResults($sql);
|
||
$tabOut = array();
|
||
if (!is_array($tab)) {
|
||
return $tabOut;
|
||
}
|
||
for ($i = 1; $i <= count($tab); $i++) {
|
||
$strLabel = trim((string)($tab[$i]['que_question_fr'] ?? ''));
|
||
$tabOut[] = array(
|
||
'que_id' => intval($tab[$i]['que_id']),
|
||
'label' => $strLabel !== '' ? $strLabel : ('Question #' . intval($tab[$i]['que_id'])),
|
||
);
|
||
}
|
||
return $tabOut;
|
||
}
|
||
|
||
/**
|
||
* Lit CSV ou Excel → en-têtes + lignes (tableaux indexés 0..n).
|
||
* @return array{state:string,message?:string,headers?:array,rows?:array}
|
||
*/
|
||
function fxImportResultatsParseFile($strPath, $strOriginalName = '') {
|
||
$strExt = strtolower(pathinfo($strOriginalName !== '' ? $strOriginalName : $strPath, PATHINFO_EXTENSION));
|
||
if ($strExt === 'csv' || $strExt === 'txt') {
|
||
return fxImportResultatsParseCsv($strPath);
|
||
}
|
||
if (in_array($strExt, array('xls', 'xlsx'), true)) {
|
||
return fxImportResultatsParseExcel($strPath);
|
||
}
|
||
// Sniff CSV
|
||
$strSample = @file_get_contents($strPath, false, null, 0, 2048);
|
||
if (is_string($strSample) && (strpos($strSample, ',') !== false || strpos($strSample, ';') !== false)) {
|
||
return fxImportResultatsParseCsv($strPath);
|
||
}
|
||
return array('state' => 'error', 'message' => 'Format non supporté (CSV, XLS, XLSX).');
|
||
}
|
||
|
||
function fxImportResultatsDetectCsvDelimiter($strPath) {
|
||
$strLine = '';
|
||
$fp = @fopen($strPath, 'rb');
|
||
if ($fp) {
|
||
$strLine = (string)fgets($fp);
|
||
fclose($fp);
|
||
}
|
||
$intSemi = substr_count($strLine, ';');
|
||
$intComma = substr_count($strLine, ',');
|
||
$intTab = substr_count($strLine, "\t");
|
||
if ($intTab > $intSemi && $intTab > $intComma) {
|
||
return "\t";
|
||
}
|
||
return ($intSemi > $intComma) ? ';' : ',';
|
||
}
|
||
|
||
function fxImportResultatsParseCsv($strPath) {
|
||
$strDelim = fxImportResultatsDetectCsvDelimiter($strPath);
|
||
$fp = @fopen($strPath, 'rb');
|
||
if (!$fp) {
|
||
return array('state' => 'error', 'message' => 'Impossible de lire le fichier.');
|
||
}
|
||
// BOM UTF-8
|
||
$strBom = fread($fp, 3);
|
||
if ($strBom !== "\xEF\xBB\xBF") {
|
||
rewind($fp);
|
||
}
|
||
$tabHeaders = fgetcsv($fp, 0, $strDelim);
|
||
if (!is_array($tabHeaders) || count($tabHeaders) < 1) {
|
||
fclose($fp);
|
||
return array('state' => 'error', 'message' => 'En-têtes manquants.');
|
||
}
|
||
$tabHeaders = array_map('trim', $tabHeaders);
|
||
$tabRows = array();
|
||
while (($tabRow = fgetcsv($fp, 0, $strDelim)) !== false) {
|
||
if (!is_array($tabRow)) {
|
||
continue;
|
||
}
|
||
// Ignorer lignes totalement vides
|
||
$blnEmpty = true;
|
||
foreach ($tabRow as $strCell) {
|
||
if (trim((string)$strCell) !== '') {
|
||
$blnEmpty = false;
|
||
break;
|
||
}
|
||
}
|
||
if ($blnEmpty) {
|
||
continue;
|
||
}
|
||
// Aligner largeur
|
||
while (count($tabRow) < count($tabHeaders)) {
|
||
$tabRow[] = '';
|
||
}
|
||
if (count($tabRow) > count($tabHeaders)) {
|
||
$tabRow = array_slice($tabRow, 0, count($tabHeaders));
|
||
}
|
||
$tabRows[] = $tabRow;
|
||
}
|
||
fclose($fp);
|
||
return array('state' => 'ok', 'headers' => $tabHeaders, 'rows' => $tabRows);
|
||
}
|
||
|
||
function fxImportResultatsParseExcel($strPath) {
|
||
$strPhpExcel = dirname(__FILE__) . '/../superadm/php/Classes/PHPExcel.php';
|
||
if (!is_file($strPhpExcel)) {
|
||
$strPhpExcel = dirname(__FILE__) . '/Classes/PHPExcel.php';
|
||
}
|
||
if (!is_file($strPhpExcel)) {
|
||
return array('state' => 'error', 'message' => 'Lecteur Excel indisponible — utilisez un CSV.');
|
||
}
|
||
require_once $strPhpExcel;
|
||
try {
|
||
$objBook = PHPExcel_IOFactory::load($strPath);
|
||
$objSheet = $objBook->getActiveSheet();
|
||
$tabRaw = $objSheet->toArray(null, true, true, false);
|
||
} catch (Exception $ex) {
|
||
return array('state' => 'error', 'message' => 'Lecture Excel échouée : ' . $ex->getMessage());
|
||
}
|
||
if (!is_array($tabRaw) || count($tabRaw) < 1) {
|
||
return array('state' => 'error', 'message' => 'Fichier Excel vide.');
|
||
}
|
||
$tabHeaders = array_map(function ($v) {
|
||
return trim((string)$v);
|
||
}, $tabRaw[0]);
|
||
// Trim trailing empty headers
|
||
while (count($tabHeaders) > 0 && end($tabHeaders) === '') {
|
||
array_pop($tabHeaders);
|
||
}
|
||
if (count($tabHeaders) < 1) {
|
||
return array('state' => 'error', 'message' => 'En-têtes manquants.');
|
||
}
|
||
$tabRows = array();
|
||
for ($r = 1; $r < count($tabRaw); $r++) {
|
||
$tabRow = $tabRaw[$r];
|
||
if (!is_array($tabRow)) {
|
||
continue;
|
||
}
|
||
$tabRow = array_slice($tabRow, 0, count($tabHeaders));
|
||
while (count($tabRow) < count($tabHeaders)) {
|
||
$tabRow[] = '';
|
||
}
|
||
$blnEmpty = true;
|
||
foreach ($tabRow as $strCell) {
|
||
if (trim((string)$strCell) !== '') {
|
||
$blnEmpty = false;
|
||
break;
|
||
}
|
||
}
|
||
if ($blnEmpty) {
|
||
continue;
|
||
}
|
||
$tabRows[] = array_map(function ($v) {
|
||
return trim((string)$v);
|
||
}, $tabRow);
|
||
}
|
||
return array('state' => 'ok', 'headers' => $tabHeaders, 'rows' => $tabRows);
|
||
}
|
||
|
||
/**
|
||
* Valeurs distinctes par colonne (capées).
|
||
*/
|
||
function fxImportResultatsDistincts(array $tabRows, $intColCount, $intMaxPerCol = 40) {
|
||
$tabDistinct = array();
|
||
for ($c = 0; $c < $intColCount; $c++) {
|
||
$tabDistinct[$c] = array();
|
||
}
|
||
foreach ($tabRows as $tabRow) {
|
||
for ($c = 0; $c < $intColCount; $c++) {
|
||
$strVal = trim((string)($tabRow[$c] ?? ''));
|
||
if ($strVal === '') {
|
||
continue;
|
||
}
|
||
if (!isset($tabDistinct[$c][$strVal])) {
|
||
if (count($tabDistinct[$c]) >= $intMaxPerCol) {
|
||
continue;
|
||
}
|
||
$tabDistinct[$c][$strVal] = true;
|
||
}
|
||
}
|
||
}
|
||
$tabOut = array();
|
||
for ($c = 0; $c < $intColCount; $c++) {
|
||
$tabOut[$c] = array_keys($tabDistinct[$c]);
|
||
sort($tabOut[$c], SORT_NATURAL | SORT_FLAG_CASE);
|
||
}
|
||
return $tabOut;
|
||
}
|
||
|
||
function fxImportResultatsSaveUpload($intEveId, array $arrFile) {
|
||
$intEveId = intval($intEveId);
|
||
if ($intEveId <= 0) {
|
||
return array('state' => 'error', 'message' => 'eve_id manquant');
|
||
}
|
||
if (empty($arrFile['tmp_name']) || !is_uploaded_file($arrFile['tmp_name'])) {
|
||
return array('state' => 'error', 'message' => 'Aucun fichier reçu');
|
||
}
|
||
if (!empty($arrFile['error']) && intval($arrFile['error']) !== UPLOAD_ERR_OK) {
|
||
return array('state' => 'error', 'message' => 'Erreur upload (code ' . intval($arrFile['error']) . ')');
|
||
}
|
||
$strName = (string)($arrFile['name'] ?? 'import.csv');
|
||
$strToken = bin2hex(random_bytes(16));
|
||
$strSafeExt = strtolower(pathinfo($strName, PATHINFO_EXTENSION));
|
||
if (!in_array($strSafeExt, array('csv', 'txt', 'xls', 'xlsx'), true)) {
|
||
$strSafeExt = 'csv';
|
||
}
|
||
$strDest = fxImportResultatsUploadDir() . '/' . $intEveId . '_' . $strToken . '.' . $strSafeExt;
|
||
if (!@move_uploaded_file($arrFile['tmp_name'], $strDest)) {
|
||
return array('state' => 'error', 'message' => 'Échec enregistrement temporaire');
|
||
}
|
||
$arrParsed = fxImportResultatsParseFile($strDest, $strName);
|
||
if (($arrParsed['state'] ?? '') !== 'ok') {
|
||
@unlink($strDest);
|
||
return $arrParsed;
|
||
}
|
||
$tabHeaders = $arrParsed['headers'];
|
||
$tabRows = $arrParsed['rows'];
|
||
$strSig = fxImportResultatsHeaderSig($tabHeaders);
|
||
$tabGuess = array();
|
||
foreach ($tabHeaders as $strH) {
|
||
$tabGuess[] = fxImportResultatsGuessField($strH);
|
||
}
|
||
$tabSamples = array_slice($tabRows, 0, 8);
|
||
// Retirer d'éventuelles lignes « en-tête » encore présentes dans les données
|
||
$tabIdxFilter = array();
|
||
foreach ($tabHeaders as $intHi => $strH) {
|
||
$strG = fxImportResultatsGuessField($strH);
|
||
if ($strG !== '') {
|
||
$tabIdxFilter[$strG] = $intHi;
|
||
}
|
||
}
|
||
$tabRowsFiltered = array();
|
||
foreach ($tabRows as $tabRow) {
|
||
if (fxImportResultatsRowLooksLikeHeader($tabRow, $tabHeaders, $tabIdxFilter)) {
|
||
continue;
|
||
}
|
||
$tabRowsFiltered[] = $tabRow;
|
||
}
|
||
$tabRows = $tabRowsFiltered;
|
||
$tabSamples = array_slice($tabRows, 0, 8);
|
||
$tabDistinct = fxImportResultatsDistincts($tabRows, count($tabHeaders));
|
||
|
||
// Meta JSON à côté du fichier
|
||
$arrMeta = array(
|
||
'eve_id' => $intEveId,
|
||
'token' => $strToken,
|
||
'original_name' => $strName,
|
||
'path' => $strDest,
|
||
'header_sig' => $strSig,
|
||
'headers' => $tabHeaders,
|
||
'row_count' => count($tabRows),
|
||
'created_at' => date('c'),
|
||
);
|
||
@file_put_contents($strDest . '.meta.json', json_encode($arrMeta, JSON_UNESCAPED_UNICODE));
|
||
|
||
$tabTemplates = fxImportResultatsFindTemplates($intEveId, $strSig);
|
||
|
||
return array(
|
||
'state' => 'ok',
|
||
'token' => $strToken,
|
||
'original_name' => $strName,
|
||
'header_sig' => $strSig,
|
||
'headers' => $tabHeaders,
|
||
'guess' => $tabGuess,
|
||
'row_count' => count($tabRows),
|
||
'samples' => $tabSamples,
|
||
'distincts' => $tabDistinct,
|
||
'templates' => $tabTemplates,
|
||
'epreuves' => fxImportResultatsEpreuvesList($intEveId),
|
||
'questions' => fxImportResultatsQuestionsList($intEveId),
|
||
'fields' => fxImportResultatsTargetFields(),
|
||
);
|
||
}
|
||
|
||
function fxImportResultatsResolveTokenPath($intEveId, $strToken) {
|
||
$intEveId = intval($intEveId);
|
||
$strToken = preg_replace('/[^a-f0-9]/', '', strtolower((string)$strToken));
|
||
if ($intEveId <= 0 || strlen($strToken) < 16) {
|
||
return null;
|
||
}
|
||
$strDir = fxImportResultatsUploadDir();
|
||
$tabMatches = glob($strDir . '/' . $intEveId . '_' . $strToken . '.*');
|
||
if (!is_array($tabMatches)) {
|
||
return null;
|
||
}
|
||
foreach ($tabMatches as $strPath) {
|
||
if (substr($strPath, -10) === '.meta.json') {
|
||
continue;
|
||
}
|
||
if (is_file($strPath)) {
|
||
return $strPath;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function fxImportResultatsFindTemplates($intEveId, $strSig) {
|
||
global $objDatabase;
|
||
$intEveId = intval($intEveId);
|
||
$strSig = preg_replace('/[^a-f0-9]/', '', strtolower((string)$strSig));
|
||
$intOrgId = fxImportResultatsComIdForEve($intEveId);
|
||
// Table absente (SQL pas encore joué) → pas d'erreur HTML dans le JSON AJAX
|
||
$sqlCheck = "SELECT 1 AS ok FROM information_schema.TABLES"
|
||
. " WHERE TABLE_SCHEMA = DATABASE()"
|
||
. " AND TABLE_NAME = 'inscriptions_import_templates' LIMIT 1";
|
||
$tabCheck = $objDatabase->fxGetResults($sqlCheck);
|
||
if (!is_array($tabCheck) || empty($tabCheck[1]['ok'])) {
|
||
return array();
|
||
}
|
||
$sql = "SELECT tpl_id, eve_id, com_id, tpl_name, tpl_header_sig, tpl_mapping_json, tpl_maj"
|
||
. " FROM inscriptions_import_templates"
|
||
. " WHERE tpl_actif = 1 AND tpl_header_sig = '" . addslashes($strSig) . "'"
|
||
. " AND (eve_id = " . $intEveId
|
||
. ($intOrgId > 0 ? (" OR com_id = " . $intOrgId) : '')
|
||
. ")"
|
||
. " ORDER BY (eve_id = " . $intEveId . ") DESC, tpl_maj DESC LIMIT 20";
|
||
$tab = $objDatabase->fxGetResults($sql);
|
||
$tabOut = array();
|
||
if (!is_array($tab)) {
|
||
return $tabOut;
|
||
}
|
||
for ($i = 1; $i <= count($tab); $i++) {
|
||
$arrMap = json_decode((string)($tab[$i]['tpl_mapping_json'] ?? ''), true);
|
||
$tabOut[] = array(
|
||
'tpl_id' => intval($tab[$i]['tpl_id']),
|
||
'eve_id' => intval($tab[$i]['eve_id']),
|
||
'com_id' => intval($tab[$i]['com_id']),
|
||
'name' => (string)$tab[$i]['tpl_name'],
|
||
'maj' => (string)$tab[$i]['tpl_maj'],
|
||
'mapping' => is_array($arrMap) ? $arrMap : array(),
|
||
'same_event' => (intval($tab[$i]['eve_id']) === $intEveId),
|
||
);
|
||
}
|
||
return $tabOut;
|
||
}
|
||
|
||
/**
|
||
* @param array $arrMapping { columns: string[], epr_values: {value: epr_id}, questions: {colIndex: que_id|"new"} }
|
||
*/
|
||
function fxImportResultatsSaveTemplate($intEveId, $strToken, $strName, array $arrMapping, $intUsaId, $blnShareCom = true) {
|
||
global $objDatabase;
|
||
|
||
$intEveId = intval($intEveId);
|
||
$strPath = fxImportResultatsResolveTokenPath($intEveId, $strToken);
|
||
if ($strPath === null) {
|
||
return array('state' => 'error', 'message' => 'Fichier temporaire introuvable — réuploadez.');
|
||
}
|
||
$strMetaPath = $strPath . '.meta.json';
|
||
$arrMeta = array();
|
||
if (is_file($strMetaPath)) {
|
||
$arrMeta = json_decode((string)@file_get_contents($strMetaPath), true);
|
||
if (!is_array($arrMeta)) {
|
||
$arrMeta = array();
|
||
}
|
||
}
|
||
$tabHeaders = isset($arrMeta['headers']) && is_array($arrMeta['headers']) ? $arrMeta['headers'] : array();
|
||
$strSig = isset($arrMeta['header_sig']) ? (string)$arrMeta['header_sig'] : fxImportResultatsHeaderSig($tabHeaders);
|
||
if ($tabHeaders === array()) {
|
||
$arrParsed = fxImportResultatsParseFile($strPath);
|
||
if (($arrParsed['state'] ?? '') !== 'ok') {
|
||
return $arrParsed;
|
||
}
|
||
$tabHeaders = $arrParsed['headers'];
|
||
$strSig = fxImportResultatsHeaderSig($tabHeaders);
|
||
}
|
||
|
||
$strName = trim((string)$strName);
|
||
if ($strName === '') {
|
||
$strName = 'Template ' . date('Y-m-d H:i');
|
||
}
|
||
$intComId = $blnShareCom ? fxImportResultatsComIdForEve($intEveId) : 0;
|
||
$strNow = date('Y-m-d H:i:s');
|
||
$strHeadersJson = json_encode($tabHeaders, JSON_UNESCAPED_UNICODE);
|
||
$strMappingJson = json_encode($arrMapping, JSON_UNESCAPED_UNICODE);
|
||
if ($strHeadersJson === false || $strMappingJson === false) {
|
||
return array('state' => 'error', 'message' => 'JSON mapping invalide');
|
||
}
|
||
|
||
// Upsert soft : même eve + même sig + même nom → update
|
||
$sqlFind = "SELECT tpl_id FROM inscriptions_import_templates"
|
||
. " WHERE eve_id = " . $intEveId
|
||
. " AND tpl_header_sig = '" . addslashes($strSig) . "'"
|
||
. " AND tpl_name = '" . addslashes($strName) . "'"
|
||
. " AND tpl_actif = 1 LIMIT 1";
|
||
$tabFind = $objDatabase->fxGetResults($sqlFind);
|
||
if (is_array($tabFind) && !empty($tabFind[1]['tpl_id'])) {
|
||
$intTplId = intval($tabFind[1]['tpl_id']);
|
||
$sql = "UPDATE inscriptions_import_templates SET"
|
||
. " com_id = " . intval($intComId) . ","
|
||
. " tpl_headers_json = '" . addslashes($strHeadersJson) . "',"
|
||
. " tpl_mapping_json = '" . addslashes($strMappingJson) . "',"
|
||
. " tpl_usa_id = " . intval($intUsaId) . ","
|
||
. " tpl_maj = '" . addslashes($strNow) . "'"
|
||
. " WHERE tpl_id = " . $intTplId;
|
||
$objDatabase->fxQuery($sql);
|
||
} else {
|
||
$sql = "INSERT INTO inscriptions_import_templates"
|
||
. " (eve_id, com_id, tpl_name, tpl_header_sig, tpl_headers_json, tpl_mapping_json, tpl_actif, tpl_usa_id, tpl_creation, tpl_maj)"
|
||
. " VALUES ("
|
||
. intval($intEveId) . ", "
|
||
. intval($intComId) . ", "
|
||
. "'" . addslashes($strName) . "', "
|
||
. "'" . addslashes($strSig) . "', "
|
||
. "'" . addslashes($strHeadersJson) . "', "
|
||
. "'" . addslashes($strMappingJson) . "', "
|
||
. "1, "
|
||
. intval($intUsaId) . ", "
|
||
. "'" . addslashes($strNow) . "', "
|
||
. "'" . addslashes($strNow) . "'"
|
||
. ")";
|
||
$objDatabase->fxQuery($sql);
|
||
$intTplId = intval($objDatabase->fxGetLastId());
|
||
}
|
||
|
||
return array(
|
||
'state' => 'ok',
|
||
'tpl_id' => $intTplId,
|
||
'message' => 'Template enregistré — le prochain fichier avec les mêmes colonnes le proposera.',
|
||
'templates' => fxImportResultatsFindTemplates($intEveId, $strSig),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Validation mapping (sans écriture resultats).
|
||
*/
|
||
function fxImportResultatsValidateMapping($intEveId, $strToken, array $arrMapping) {
|
||
$intEveId = intval($intEveId);
|
||
$strPath = fxImportResultatsResolveTokenPath($intEveId, $strToken);
|
||
if ($strPath === null) {
|
||
return array('state' => 'error', 'message' => 'Fichier temporaire introuvable — réuploadez.');
|
||
}
|
||
$arrParsed = fxImportResultatsParseFile($strPath);
|
||
if (($arrParsed['state'] ?? '') !== 'ok') {
|
||
return $arrParsed;
|
||
}
|
||
$tabColumns = isset($arrMapping['columns']) && is_array($arrMapping['columns']) ? $arrMapping['columns'] : array();
|
||
$tabEprValues = isset($arrMapping['epr_values']) && is_array($arrMapping['epr_values']) ? $arrMapping['epr_values'] : array();
|
||
|
||
$blnHasExt = false;
|
||
$blnHasEpr = false;
|
||
$intEprCol = -1;
|
||
foreach ($tabColumns as $intIdx => $strKey) {
|
||
$strKey = (string)$strKey;
|
||
if ($strKey === 'par_external_id') {
|
||
$blnHasExt = true;
|
||
}
|
||
if ($strKey === 'epr') {
|
||
$blnHasEpr = true;
|
||
$intEprCol = intval($intIdx);
|
||
}
|
||
}
|
||
|
||
$tabErrors = array();
|
||
$tabWarn = array();
|
||
if (!$blnHasExt) {
|
||
$tabErrors[] = 'Mappez une colonne « Clé unique » (indispensable pour ChronoTrack / réimport).';
|
||
}
|
||
if (!$blnHasEpr) {
|
||
$tabErrors[] = 'Mappez une colonne « Épreuve ».';
|
||
} else {
|
||
$tabDistinct = fxImportResultatsDistincts($arrParsed['rows'], count($arrParsed['headers']));
|
||
$tabVals = $tabDistinct[$intEprCol] ?? array();
|
||
$intUnmapped = 0;
|
||
foreach ($tabVals as $strVal) {
|
||
$intEpr = intval($tabEprValues[$strVal] ?? 0);
|
||
if ($intEpr <= 0) {
|
||
$intUnmapped++;
|
||
}
|
||
}
|
||
if ($intUnmapped > 0) {
|
||
$tabErrors[] = $intUnmapped . ' valeur(s) d\'épreuve sans correspondance MS1.';
|
||
}
|
||
}
|
||
if (!in_array('par_prenom', $tabColumns, true) && !in_array('par_nom', $tabColumns, true)) {
|
||
$tabWarn[] = 'Ni prénom ni nom mappé — vérifiez le fichier.';
|
||
}
|
||
|
||
if (count($tabErrors) > 0) {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => implode(' ', $tabErrors),
|
||
'errors' => $tabErrors,
|
||
'warnings' => $tabWarn,
|
||
'row_count' => count($arrParsed['rows']),
|
||
);
|
||
}
|
||
|
||
return array(
|
||
'state' => 'ok',
|
||
'message' => 'Mapping prêt — ' . count($arrParsed['rows']) . ' ligne(s). Vous pouvez importer.',
|
||
'warnings' => $tabWarn,
|
||
'row_count' => count($arrParsed['rows']),
|
||
'commit_ready' => true,
|
||
);
|
||
}
|
||
|
||
function fxImportResultatsDbEsc($str) {
|
||
global $objDatabase;
|
||
if (isset($objDatabase) && is_object($objDatabase) && method_exists($objDatabase, 'fxEscape')) {
|
||
return $objDatabase->fxEscape((string)$str);
|
||
}
|
||
return addslashes((string)$str);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4482 — faux no_panier / no_commande uniques (varchar 20).
|
||
* Obligatoire : index UNIQUE sur ces champs → '' ne peut exister qu'une fois.
|
||
*/
|
||
function fxImportResultatsSyntheticPanier($intEveId, $strExternalId) {
|
||
$str = 'I' . intval($intEveId) . 'X' . substr(md5((string)$strExternalId), 0, 12);
|
||
return substr($str, 0, 20);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4482 — pec_id_original / par_id_original ont un UNIQUE : jamais inserer 0.
|
||
* Valeur temporaire unique, puis UPDATE = vrai id apres INSERT.
|
||
*/
|
||
function fxImportResultatsTempOriginalId($strExternalId, $strSalt = '') {
|
||
$intCrc = sprintf('%u', crc32((string)$strExternalId . '|' . (string)$strSalt));
|
||
// Plage haute pour limiter collision avec vrais pec_id / par_id historiques
|
||
return 1900000000 + intval($intCrc % 90000000);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4482 — même modèle que fxChronotrackApiSyncGroupBlockedByType.
|
||
*/
|
||
function fxImportResultatsGroupBlockedByType(array $tabBlocked) {
|
||
$tabTypeLabels = array(
|
||
'header_row' => 'Ligne en-tête (ignorée)',
|
||
'no_external' => 'Clé unique manquante',
|
||
'no_race' => 'Épreuve non mappée',
|
||
'insert_pec' => 'Échec création commande (pec)',
|
||
'insert_par' => 'Échec création participant',
|
||
'update_fail' => 'Échec mise à jour',
|
||
'other' => 'Autre',
|
||
);
|
||
$tabTypeOrder = array(
|
||
'no_external', 'no_race', 'insert_pec', 'insert_par', 'update_fail', 'header_row', 'other',
|
||
);
|
||
$tabGroups = array();
|
||
foreach ($tabBlocked as $arrItem) {
|
||
$strCode = (string)($arrItem['code'] ?? 'other');
|
||
if (!isset($tabGroups[$strCode])) {
|
||
$tabGroups[$strCode] = array(
|
||
'code' => $strCode,
|
||
'label' => isset($tabTypeLabels[$strCode]) ? $tabTypeLabels[$strCode] : $strCode,
|
||
'items' => array(),
|
||
);
|
||
}
|
||
$tabGroups[$strCode]['items'][] = $arrItem;
|
||
}
|
||
$tabOrdered = array();
|
||
foreach ($tabTypeOrder as $strCode) {
|
||
if (isset($tabGroups[$strCode])) {
|
||
$tabOrdered[] = $tabGroups[$strCode];
|
||
unset($tabGroups[$strCode]);
|
||
}
|
||
}
|
||
foreach ($tabGroups as $arrGroup) {
|
||
$tabOrdered[] = $arrGroup;
|
||
}
|
||
return $tabOrdered;
|
||
}
|
||
|
||
function fxImportResultatsRowLabel(array $tabRow, array $tabColIndex) {
|
||
$strPrenom = isset($tabColIndex['par_prenom']) ? trim((string)($tabRow[$tabColIndex['par_prenom']] ?? '')) : '';
|
||
$strNom = isset($tabColIndex['par_nom']) ? trim((string)($tabRow[$tabColIndex['par_nom']] ?? '')) : '';
|
||
$str = trim($strPrenom . ' ' . $strNom);
|
||
return ($str !== '') ? $str : '—';
|
||
}
|
||
|
||
function fxImportResultatsDbError() {
|
||
global $objDatabase;
|
||
if (isset($objDatabase) && is_object($objDatabase) && isset($objDatabase->con)) {
|
||
$str = mysqli_error($objDatabase->con);
|
||
if (is_string($str) && $str !== '') {
|
||
return $str;
|
||
}
|
||
}
|
||
return '';
|
||
}
|
||
|
||
/** MSIN-4482 — Male/Female/Other → M/F/N (aligné CT). */
|
||
function fxImportResultatsNormalizeSex($str) {
|
||
$str = trim((string)$str);
|
||
$strLow = mb_strtolower($str, 'UTF-8');
|
||
if ($strLow === 'f' || $strLow === 'female' || $strLow === 'féminin' || $strLow === 'feminin' || $strLow === 'femme') {
|
||
return 'F';
|
||
}
|
||
if ($strLow === 'm' || $strLow === 'h' || $strLow === 'male' || $strLow === 'masculin' || $strLow === 'homme') {
|
||
return 'M';
|
||
}
|
||
if ($strLow === 'other' || $strLow === 'autre' || $strLow === 'n' || $strLow === 'x') {
|
||
return 'N';
|
||
}
|
||
if ($str === 'F' || $str === 'M' || $str === 'N' || $str === 'H') {
|
||
return ($str === 'H') ? 'M' : $str;
|
||
}
|
||
return '';
|
||
}
|
||
|
||
/** MSIN-4482 — parse dates type 5/24/94 → Y-m-d. */
|
||
function fxImportResultatsNormalizeBirthdate($str) {
|
||
$str = trim((string)$str);
|
||
if ($str === '' || $str === '0000-00-00') {
|
||
return '';
|
||
}
|
||
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})/', $str, $m)) {
|
||
return $m[1] . '-' . $m[2] . '-' . $m[3];
|
||
}
|
||
// m/d/y ou m/d/yyyy
|
||
if (preg_match('/^(\d{1,2})[\/\-.](\d{1,2})[\/\-.](\d{2,4})$/', $str, $m)) {
|
||
$intM = intval($m[1]);
|
||
$intD = intval($m[2]);
|
||
$intY = intval($m[3]);
|
||
if ($intY < 100) {
|
||
$intY += ($intY >= 70) ? 1900 : 2000;
|
||
}
|
||
if (checkdate($intM, $intD, $intY)) {
|
||
return sprintf('%04d-%02d-%02d', $intY, $intM, $intD);
|
||
}
|
||
// essai j/m/a
|
||
if (checkdate($intD, $intM, $intY)) {
|
||
return sprintf('%04d-%02d-%02d', $intY, $intD, $intM);
|
||
}
|
||
}
|
||
$intTs = strtotime($str);
|
||
if ($intTs !== false) {
|
||
return date('Y-m-d', $intTs);
|
||
}
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
* Résout que_id pour colonnes questions (match existant ou création confirmée).
|
||
* @return array<string,int> colIndex => que_id
|
||
*/
|
||
function fxImportResultatsResolveQuestionIds($intEveId, array $tabHeaders, array $tabQuestionsMap) {
|
||
global $objDatabase;
|
||
|
||
$intEveId = intval($intEveId);
|
||
$tabOut = array();
|
||
foreach ($tabQuestionsMap as $strCol => $strQue) {
|
||
$intCol = intval($strCol);
|
||
$strQue = trim((string)$strQue);
|
||
if ($strQue === '' || $strQue === '0') {
|
||
continue;
|
||
}
|
||
if ($strQue === 'new') {
|
||
$strLabel = trim((string)($tabHeaders[$intCol] ?? ('Question ' . ($intCol + 1))));
|
||
if (mb_strlen($strLabel, 'UTF-8') > 100) {
|
||
$strLabel = mb_substr($strLabel, 0, 97, 'UTF-8') . '...';
|
||
}
|
||
// Réutiliser si même libellé déjà créé sur l'événement
|
||
$sqlFind = "SELECT que_id FROM inscriptions_questions"
|
||
. " WHERE eve_id = " . $intEveId
|
||
. " AND que_question_fr = '" . fxImportResultatsDbEsc($strLabel) . "'"
|
||
. " LIMIT 1";
|
||
$tabFind = $objDatabase->fxGetResults($sqlFind);
|
||
if (is_array($tabFind) && !empty($tabFind[1]['que_id'])) {
|
||
$tabOut[(string)$intCol] = intval($tabFind[1]['que_id']);
|
||
continue;
|
||
}
|
||
$sqlIns = "INSERT INTO inscriptions_questions SET"
|
||
. " eve_id = " . $intEveId . ","
|
||
. " que_question_fr = '" . fxImportResultatsDbEsc($strLabel) . "',"
|
||
. " que_question_en = '" . fxImportResultatsDbEsc($strLabel) . "',"
|
||
. " que_choix_fr = '',"
|
||
. " que_choix_en = '',"
|
||
. " que_liste = 0,"
|
||
. " que_epreuves_incluses = '',"
|
||
. " que_required = 0,"
|
||
. " que_tri = 900,"
|
||
. " que_participant_1 = 1,"
|
||
. " que_modifiable = 1,"
|
||
. " que_actif = 1,"
|
||
. " que_hidden = 0,"
|
||
. " que_cart_show = 1,"
|
||
. " que_cart_label_fr = '" . fxImportResultatsDbEsc($strLabel) . "',"
|
||
. " que_cart_label_en = '" . fxImportResultatsDbEsc($strLabel) . "'";
|
||
if ($objDatabase->fxQuery($sqlIns) === false) {
|
||
continue;
|
||
}
|
||
$tabOut[(string)$intCol] = intval($objDatabase->fxGetLastId());
|
||
continue;
|
||
}
|
||
$intQueId = intval($strQue);
|
||
if ($intQueId > 0) {
|
||
$tabOut[(string)$intCol] = $intQueId;
|
||
}
|
||
}
|
||
fxImportResultatsEnsureQuestionsVisibleFiche($intEveId, $tabOut);
|
||
return $tabOut;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4482 — la fiche exige que_cart_show=1 ; activer les questions utilisées à l'import
|
||
* (matchées OU créées), sans toucher aux autres événements.
|
||
*/
|
||
function fxImportResultatsEnsureQuestionsVisibleFiche($intEveId, array $tabQueIdsByCol) {
|
||
global $objDatabase;
|
||
|
||
$intEveId = intval($intEveId);
|
||
$tabIds = array();
|
||
foreach ($tabQueIdsByCol as $mixId) {
|
||
$intId = intval($mixId);
|
||
if ($intId > 0) {
|
||
$tabIds[$intId] = true;
|
||
}
|
||
}
|
||
if ($intEveId <= 0 || count($tabIds) < 1) {
|
||
return;
|
||
}
|
||
$strIn = implode(',', array_map('intval', array_keys($tabIds)));
|
||
$objDatabase->fxQuery(
|
||
"UPDATE inscriptions_questions SET"
|
||
. " que_cart_show = 1,"
|
||
. " que_cart_label_fr = IF(TRIM(IFNULL(que_cart_label_fr, '')) = '', que_question_fr, que_cart_label_fr),"
|
||
. " que_cart_label_en = IF("
|
||
. " TRIM(IFNULL(que_cart_label_en, '')) = '',"
|
||
. " IF(TRIM(IFNULL(que_question_en, '')) = '', que_question_fr, que_question_en),"
|
||
. " que_cart_label_en"
|
||
. " )"
|
||
. " WHERE eve_id = " . $intEveId
|
||
. " AND que_id IN (" . $strIn . ")"
|
||
);
|
||
}
|
||
|
||
function fxImportResultatsSaveQuestionAnswer($intParIdOriginal, $intPecId, $intQueId, $strHeader, $strValue) {
|
||
global $objDatabase;
|
||
|
||
$intParIdOriginal = intval($intParIdOriginal);
|
||
$intPecId = intval($intPecId);
|
||
$intQueId = intval($intQueId);
|
||
$strValue = trim((string)$strValue);
|
||
$strHeader = trim((string)$strHeader);
|
||
if ($intQueId <= 0 || $intPecId <= 0) {
|
||
return;
|
||
}
|
||
$strNow = function_exists('fxGetDateTime') ? fxGetDateTime() : date('Y-m-d H:i:s');
|
||
$sqlFind = "SELECT pqu_id FROM resultats_questions"
|
||
. " WHERE pec_id = " . $intPecId
|
||
. " AND que_id = " . $intQueId
|
||
. " AND par_id = " . $intParIdOriginal
|
||
. " LIMIT 1";
|
||
$tabFind = $objDatabase->fxGetResults($sqlFind);
|
||
if (is_array($tabFind) && !empty($tabFind[1]['pqu_id'])) {
|
||
$sql = "UPDATE resultats_questions SET"
|
||
. " que_choix_fr = '" . fxImportResultatsDbEsc($strValue) . "',"
|
||
. " que_choix_en = '" . fxImportResultatsDbEsc($strValue) . "',"
|
||
. " pqu_maj = '" . fxImportResultatsDbEsc($strNow) . "'"
|
||
. " WHERE pqu_id = " . intval($tabFind[1]['pqu_id']);
|
||
$objDatabase->fxQuery($sql);
|
||
@$objDatabase->fxQuery(
|
||
"UPDATE resultats_questions SET que_actif = 1 WHERE pqu_id = " . intval($tabFind[1]['pqu_id'])
|
||
);
|
||
return;
|
||
}
|
||
$sql = "INSERT INTO resultats_questions SET"
|
||
. " par_id = " . $intParIdOriginal . ","
|
||
. " pec_id = " . $intPecId . ","
|
||
. " que_id = " . $intQueId . ","
|
||
. " que_question_fr = '" . fxImportResultatsDbEsc($strHeader) . "',"
|
||
. " que_question_en = '" . fxImportResultatsDbEsc($strHeader) . "',"
|
||
. " que_choix_fr = '" . fxImportResultatsDbEsc($strValue) . "',"
|
||
. " que_choix_en = '" . fxImportResultatsDbEsc($strValue) . "',"
|
||
. " que_modifiable = 1,"
|
||
. " pqu_note = '',"
|
||
. " pqu_maj = '" . fxImportResultatsDbEsc($strNow) . "',"
|
||
. " no_panier = '',"
|
||
. " no_commande = '',"
|
||
. " pqu_id_original = 0";
|
||
if ($objDatabase->fxQuery($sql) === false) {
|
||
return;
|
||
}
|
||
$intPqu = intval($objDatabase->fxGetLastId());
|
||
if ($intPqu > 0) {
|
||
@$objDatabase->fxQuery(
|
||
"UPDATE resultats_questions SET que_actif = 1 WHERE pqu_id = " . $intPqu
|
||
);
|
||
}
|
||
}
|
||
/**
|
||
* MSIN-4482 — détecte une ligne qui n'est que la répétition des en-têtes.
|
||
*/
|
||
function fxImportResultatsRowLooksLikeHeader(array $tabRow, array $tabHeaders, array $tabColIndex) {
|
||
$strPrenom = isset($tabColIndex['par_prenom'])
|
||
? trim((string)($tabRow[$tabColIndex['par_prenom']] ?? '')) : '';
|
||
$strNom = isset($tabColIndex['par_nom'])
|
||
? trim((string)($tabRow[$tabColIndex['par_nom']] ?? '')) : '';
|
||
$strExt = isset($tabColIndex['par_external_id'])
|
||
? trim((string)($tabRow[$tabColIndex['par_external_id']] ?? '')) : '';
|
||
|
||
$strPrenomLow = fxImportResultatsNormalizeHeader($strPrenom);
|
||
$strNomLow = fxImportResultatsNormalizeHeader($strNom);
|
||
$strExtLow = fxImportResultatsNormalizeHeader($strExt);
|
||
|
||
// Cas classique export EN / FR
|
||
if ($strPrenomLow === 'first name' || $strPrenomLow === 'prenom' || $strPrenomLow === 'prénom') {
|
||
return true;
|
||
}
|
||
if ($strNomLow === 'last name' || $strNomLow === 'nom') {
|
||
return true;
|
||
}
|
||
if ($strExtLow === 'attendee #' || $strExtLow === 'attendee' || $strExtLow === 'external id'
|
||
|| $strExtLow === 'clé unique' || $strExtLow === 'cle unique') {
|
||
return true;
|
||
}
|
||
|
||
// Ligne entière = en-têtes
|
||
$intMatch = 0;
|
||
$intCompare = 0;
|
||
$intN = min(count($tabRow), count($tabHeaders));
|
||
for ($i = 0; $i < $intN; $i++) {
|
||
$strCell = fxImportResultatsNormalizeHeader($tabRow[$i] ?? '');
|
||
$strH = fxImportResultatsNormalizeHeader($tabHeaders[$i] ?? '');
|
||
if ($strH === '') {
|
||
continue;
|
||
}
|
||
$intCompare++;
|
||
if ($strCell === $strH) {
|
||
$intMatch++;
|
||
}
|
||
}
|
||
if ($intCompare >= 3 && $intMatch >= max(3, (int)floor($intCompare * 0.7))) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4482 — écriture resultats_* (sans panier), upsert sur par_external_id.
|
||
*/
|
||
function fxImportResultatsCommit($intEveId, $strToken, array $arrMapping) {
|
||
global $objDatabase;
|
||
|
||
@set_time_limit(300);
|
||
|
||
$arrVal = fxImportResultatsValidateMapping($intEveId, $strToken, $arrMapping);
|
||
if (($arrVal['state'] ?? '') !== 'ok') {
|
||
return $arrVal;
|
||
}
|
||
|
||
$intEveId = intval($intEveId);
|
||
$strPath = fxImportResultatsResolveTokenPath($intEveId, $strToken);
|
||
$arrParsed = fxImportResultatsParseFile($strPath);
|
||
if (($arrParsed['state'] ?? '') !== 'ok') {
|
||
return $arrParsed;
|
||
}
|
||
|
||
// Colonnes requises pour l'import
|
||
$sqlCol = "SELECT COUNT(*) AS n FROM information_schema.COLUMNS"
|
||
. " WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'resultats_participants'"
|
||
. " AND COLUMN_NAME = 'par_external_id'";
|
||
$tabCol = $objDatabase->fxGetResults($sqlCol);
|
||
if (!is_array($tabCol) || intval($tabCol[1]['n'] ?? 0) < 1) {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => 'Colonnes import absentes — exécuter sql/MSIN-4482-import-resultats-templates.sql en dev préprod.',
|
||
);
|
||
}
|
||
|
||
$tabHeaders = $arrParsed['headers'];
|
||
$tabRows = $arrParsed['rows'];
|
||
$tabColumns = isset($arrMapping['columns']) && is_array($arrMapping['columns']) ? $arrMapping['columns'] : array();
|
||
$tabEprValues = isset($arrMapping['epr_values']) && is_array($arrMapping['epr_values']) ? $arrMapping['epr_values'] : array();
|
||
$tabQuestionsMap = isset($arrMapping['questions']) && is_array($arrMapping['questions']) ? $arrMapping['questions'] : array();
|
||
|
||
$tabColIndex = array();
|
||
foreach ($tabColumns as $intIdx => $strKey) {
|
||
$strKey = (string)$strKey;
|
||
if ($strKey === '') {
|
||
continue;
|
||
}
|
||
$tabColIndex[$strKey] = intval($intIdx);
|
||
}
|
||
|
||
$tabQueIds = fxImportResultatsResolveQuestionIds($intEveId, $tabHeaders, $tabQuestionsMap);
|
||
$strNow = function_exists('fxGetDateTime') ? fxGetDateTime() : date('Y-m-d H:i:s');
|
||
$strOrigine = MSIN_IMPORT_RESULTATS_ORIGIN;
|
||
|
||
$intCreated = 0;
|
||
$intUpdated = 0;
|
||
$tabBlocked = array();
|
||
|
||
foreach ($tabRows as $intRowNum => $tabRow) {
|
||
$intLine = $intRowNum + 2; // +1 header Excel, +1 index 0
|
||
$strNamePreview = fxImportResultatsRowLabel($tabRow, $tabColIndex);
|
||
$strBibPreview = isset($tabColIndex['no_bib']) ? trim((string)($tabRow[$tabColIndex['no_bib']] ?? '')) : '';
|
||
$strSexePreview = '';
|
||
if (isset($tabColIndex['par_sexe'])) {
|
||
$strSexePreview = fxImportResultatsNormalizeSex(trim((string)($tabRow[$tabColIndex['par_sexe']] ?? '')));
|
||
}
|
||
|
||
if (fxImportResultatsRowLooksLikeHeader($tabRow, $tabHeaders, $tabColIndex)) {
|
||
$tabBlocked[] = array(
|
||
'code' => 'header_row',
|
||
'message' => 'Ligne d\'en-tête détectée',
|
||
'line' => $intLine,
|
||
'name' => $strNamePreview,
|
||
'external_id' => '',
|
||
'no_bib' => $strBibPreview,
|
||
'par_sexe' => $strSexePreview,
|
||
);
|
||
continue;
|
||
}
|
||
|
||
$strExt = '';
|
||
if (isset($tabColIndex['par_external_id'])) {
|
||
$strExt = trim((string)($tabRow[$tabColIndex['par_external_id']] ?? ''));
|
||
}
|
||
if ($strExt === '') {
|
||
$tabBlocked[] = array(
|
||
'code' => 'no_external',
|
||
'message' => 'Clé unique (Attendee # / external_id) vide',
|
||
'line' => $intLine,
|
||
'name' => $strNamePreview,
|
||
'external_id' => '',
|
||
'no_bib' => $strBibPreview,
|
||
'par_sexe' => $strSexePreview,
|
||
);
|
||
continue;
|
||
}
|
||
|
||
$strEprRaw = '';
|
||
if (isset($tabColIndex['epr'])) {
|
||
$strEprRaw = trim((string)($tabRow[$tabColIndex['epr']] ?? ''));
|
||
}
|
||
$intEprId = intval($tabEprValues[$strEprRaw] ?? 0);
|
||
if ($intEprId <= 0) {
|
||
$tabBlocked[] = array(
|
||
'code' => 'no_race',
|
||
'message' => ($strEprRaw !== ''
|
||
? ('« ' . $strEprRaw . ' » non mappée vers une épreuve MS1')
|
||
: 'Valeur d\'épreuve vide'),
|
||
'line' => $intLine,
|
||
'name' => $strNamePreview,
|
||
'external_id' => $strExt,
|
||
'no_bib' => $strBibPreview,
|
||
'par_sexe' => $strSexePreview,
|
||
);
|
||
continue;
|
||
}
|
||
|
||
$arrFields = array(
|
||
'par_prenom' => '',
|
||
'par_nom' => '',
|
||
'par_sexe' => '',
|
||
'par_naissance' => '',
|
||
'par_age' => 0,
|
||
'par_courriel' => '',
|
||
'par_telephone1' => '',
|
||
'par_adresse' => '',
|
||
'par_ville' => '',
|
||
'par_codepostal' => '',
|
||
'no_bib' => '',
|
||
'par_nom_equipe' => '',
|
||
'par_no_equipe' => 0,
|
||
'par_contact_urgence_nom' => '',
|
||
'par_contact_urgence_telephone' => '',
|
||
);
|
||
foreach ($arrFields as $strKey => $mixDefault) {
|
||
if (!isset($tabColIndex[$strKey])) {
|
||
continue;
|
||
}
|
||
$strRaw = trim((string)($tabRow[$tabColIndex[$strKey]] ?? ''));
|
||
if ($strKey === 'par_sexe') {
|
||
$arrFields[$strKey] = fxImportResultatsNormalizeSex($strRaw);
|
||
} elseif ($strKey === 'par_naissance') {
|
||
$arrFields[$strKey] = fxImportResultatsNormalizeBirthdate($strRaw);
|
||
} elseif ($strKey === 'par_age' || $strKey === 'par_no_equipe') {
|
||
$arrFields[$strKey] = intval($strRaw);
|
||
} else {
|
||
$arrFields[$strKey] = $strRaw;
|
||
}
|
||
}
|
||
if (intval($arrFields['par_age']) <= 0 && $arrFields['par_naissance'] !== '') {
|
||
$intTs = strtotime($arrFields['par_naissance']);
|
||
if ($intTs !== false) {
|
||
$arrFields['par_age'] = intval(floor((time() - $intTs) / 31557600));
|
||
}
|
||
}
|
||
|
||
// Upsert participant import
|
||
$sqlExist = "SELECT par_id, pec_id, par_id_original FROM resultats_participants"
|
||
. " WHERE eve_id = " . $intEveId
|
||
. " AND par_external_id = '" . fxImportResultatsDbEsc($strExt) . "'"
|
||
. " LIMIT 1";
|
||
$tabExist = $objDatabase->fxGetResults($sqlExist);
|
||
$blnUpdate = (is_array($tabExist) && !empty($tabExist[1]['par_id']));
|
||
|
||
if ($blnUpdate) {
|
||
$intParId = intval($tabExist[1]['par_id']);
|
||
$intPecId = intval($tabExist[1]['pec_id']);
|
||
$intParOrig = intval($tabExist[1]['par_id_original']);
|
||
if ($intParOrig <= 0) {
|
||
$intParOrig = $intParId;
|
||
}
|
||
|
||
if ($intPecId > 0) {
|
||
// p.pec_id peut être e.pec_id OU e.pec_id_original (selon imports précédents)
|
||
$tabPecRow = $objDatabase->fxGetResults(
|
||
"SELECT pec_id, pec_id_original FROM resultats_epreuves_commandees"
|
||
. " WHERE eve_id = " . $intEveId
|
||
. " AND (pec_id = " . $intPecId . " OR pec_id_original = " . $intPecId . ")"
|
||
. " LIMIT 1"
|
||
);
|
||
$intPecPk = is_array($tabPecRow) ? intval($tabPecRow[1]['pec_id'] ?? 0) : 0;
|
||
$intPecOrigNow = is_array($tabPecRow) ? intval($tabPecRow[1]['pec_id_original'] ?? 0) : 0;
|
||
if ($intPecPk > 0) {
|
||
$objDatabase->fxQuery(
|
||
"UPDATE resultats_epreuves_commandees SET"
|
||
. " epr_id = " . $intEprId . ","
|
||
. " pec_actif = 1,"
|
||
. " pec_maj = '" . fxImportResultatsDbEsc($strNow) . "'"
|
||
. " WHERE pec_id = " . $intPecPk
|
||
);
|
||
// Tenter pec_id_original = pec_id seulement si UNIQUE libre
|
||
@$objDatabase->fxQuery(
|
||
"UPDATE resultats_epreuves_commandees e SET pec_id_original = " . $intPecPk
|
||
. " WHERE pec_id = " . $intPecPk
|
||
. " AND pec_id_original <> " . $intPecPk
|
||
. " AND NOT EXISTS ("
|
||
. " SELECT 1 FROM ("
|
||
. " SELECT pec_id FROM resultats_epreuves_commandees"
|
||
. " WHERE pec_id_original = " . $intPecPk
|
||
. " AND pec_id <> " . $intPecPk
|
||
. " ) _c"
|
||
. " )"
|
||
);
|
||
$tabPecNow = $objDatabase->fxGetResults(
|
||
"SELECT pec_id_original FROM resultats_epreuves_commandees WHERE pec_id = "
|
||
. $intPecPk . " LIMIT 1"
|
||
);
|
||
$intPecOrigNow = is_array($tabPecNow)
|
||
? intval($tabPecNow[1]['pec_id_original'] ?? 0)
|
||
: $intPecOrigNow;
|
||
if ($intPecOrigNow > 0) {
|
||
$objDatabase->fxQuery(
|
||
"UPDATE resultats_participants SET pec_id = " . $intPecOrigNow
|
||
. " WHERE par_id = " . $intParId
|
||
);
|
||
$intPecId = $intPecOrigNow; // clé liste / questions
|
||
}
|
||
}
|
||
}
|
||
|
||
$sqlUp = "UPDATE resultats_participants SET"
|
||
. " epr_id = " . $intEprId . ","
|
||
. " par_prenom = '" . fxImportResultatsDbEsc($arrFields['par_prenom']) . "',"
|
||
. " par_nom = '" . fxImportResultatsDbEsc($arrFields['par_nom']) . "',"
|
||
. " par_sexe = '" . fxImportResultatsDbEsc($arrFields['par_sexe']) . "',"
|
||
. " par_naissance = " . ($arrFields['par_naissance'] !== '' ? ("'" . fxImportResultatsDbEsc($arrFields['par_naissance']) . "'") : "NULL") . ","
|
||
. " par_age = " . intval($arrFields['par_age']) . ","
|
||
. " par_courriel = '" . fxImportResultatsDbEsc($arrFields['par_courriel']) . "',"
|
||
. " par_telephone1 = '" . fxImportResultatsDbEsc($arrFields['par_telephone1']) . "',"
|
||
. " par_adresse = '" . fxImportResultatsDbEsc($arrFields['par_adresse']) . "',"
|
||
. " par_ville = '" . fxImportResultatsDbEsc($arrFields['par_ville']) . "',"
|
||
. " par_codepostal = '" . fxImportResultatsDbEsc($arrFields['par_codepostal']) . "',"
|
||
. " no_bib = '" . fxImportResultatsDbEsc($arrFields['no_bib']) . "',"
|
||
. " par_nom_equipe = '" . fxImportResultatsDbEsc($arrFields['par_nom_equipe']) . "',"
|
||
. " par_no_equipe = " . intval($arrFields['par_no_equipe']) . ","
|
||
. " par_origine = '" . fxImportResultatsDbEsc($strOrigine) . "',"
|
||
. " par_maj = '" . fxImportResultatsDbEsc($strNow) . "'"
|
||
. " WHERE par_id = " . $intParId;
|
||
if ($objDatabase->fxQuery($sqlUp) === false) {
|
||
$strErr = fxImportResultatsDbError();
|
||
$tabBlocked[] = array(
|
||
'code' => 'update_fail',
|
||
'message' => 'Échec UPDATE' . ($strErr !== '' ? (' — ' . $strErr) : ''),
|
||
'line' => $intLine,
|
||
'name' => $strNamePreview,
|
||
'external_id' => $strExt,
|
||
'no_bib' => $strBibPreview,
|
||
'par_sexe' => $strSexePreview,
|
||
'par_id' => $intParId,
|
||
);
|
||
continue;
|
||
}
|
||
// urgence si colonnes présentes (ignorer erreur silencieuse)
|
||
$sqlUrg = "UPDATE resultats_participants SET"
|
||
. " par_contact_urgence_nom = '" . fxImportResultatsDbEsc($arrFields['par_contact_urgence_nom']) . "',"
|
||
. " par_contact_urgence_telephone = '" . fxImportResultatsDbEsc($arrFields['par_contact_urgence_telephone']) . "'"
|
||
. " WHERE par_id = " . $intParId;
|
||
@$objDatabase->fxQuery($sqlUrg);
|
||
|
||
$intUpdated++;
|
||
} else {
|
||
// MSIN-4482 — identifiants techniques uniques (pas un vrai panier/paiement)
|
||
$strNoPanier = fxImportResultatsSyntheticPanier($intEveId, $strExt);
|
||
$strNoCommande = $strNoPanier;
|
||
$intTempPecOrig = fxImportResultatsTempOriginalId($strExt, 'pec-' . $intEveId);
|
||
|
||
$sqlPec = "INSERT INTO resultats_epreuves_commandees SET"
|
||
. " eve_id = " . $intEveId . ","
|
||
. " epr_id = " . $intEprId . ","
|
||
. " no_panier = '" . fxImportResultatsDbEsc($strNoPanier) . "',"
|
||
. " pec_label = '',"
|
||
. " pec_nom_equipe = '" . fxImportResultatsDbEsc($arrFields['par_nom_equipe']) . "',"
|
||
. " pec_equipe = 0,"
|
||
. " com_id = 0,"
|
||
. " pec_maj = '" . fxImportResultatsDbEsc($strNow) . "',"
|
||
. " pec_id_original = " . intval($intTempPecOrig) . ","
|
||
. " no_commande = '" . fxImportResultatsDbEsc($strNoCommande) . "',"
|
||
. " is_cancelled = 0,"
|
||
. " pec_actif = 1";
|
||
if ($objDatabase->fxQuery($sqlPec) === false) {
|
||
$strErr = fxImportResultatsDbError();
|
||
$tabBlocked[] = array(
|
||
'code' => 'insert_pec',
|
||
'message' => 'Échec INSERT pec' . ($strErr !== '' ? (' — ' . $strErr) : ''),
|
||
'line' => $intLine,
|
||
'name' => $strNamePreview,
|
||
'external_id' => $strExt,
|
||
'no_bib' => $strBibPreview,
|
||
'par_sexe' => $strSexePreview,
|
||
);
|
||
continue;
|
||
}
|
||
$intPecId = intval($objDatabase->fxGetLastId());
|
||
// MSIN-4482 — UNIQUE pec_id_original : n'écraser le temp que si pec_id est libre
|
||
$blnOrigOk = false;
|
||
$qryOrig = $objDatabase->fxQuery(
|
||
"UPDATE resultats_epreuves_commandees SET pec_id_original = " . $intPecId
|
||
. " WHERE pec_id = " . $intPecId
|
||
. " AND NOT EXISTS ("
|
||
. " SELECT 1 FROM ("
|
||
. " SELECT pec_id FROM resultats_epreuves_commandees"
|
||
. " WHERE pec_id_original = " . $intPecId
|
||
. " AND pec_id <> " . $intPecId
|
||
. " ) _c"
|
||
. " )"
|
||
);
|
||
if ($qryOrig !== false) {
|
||
$tabChk = $objDatabase->fxGetResults(
|
||
"SELECT pec_id_original FROM resultats_epreuves_commandees WHERE pec_id = "
|
||
. $intPecId . " LIMIT 1"
|
||
);
|
||
$blnOrigOk = (is_array($tabChk) && intval($tabChk[1]['pec_id_original'] ?? 0) === $intPecId);
|
||
}
|
||
// Lien participant pour la liste : toujours = pec_id_original effectif
|
||
$intPecLink = $blnOrigOk ? $intPecId : intval($intTempPecOrig);
|
||
// Questions + participant : clé = pec_id_original (join liste)
|
||
$intPecId = $intPecLink;
|
||
|
||
$intTempParOrig = fxImportResultatsTempOriginalId($strExt, 'par-' . $intEveId);
|
||
$sqlPar = "INSERT INTO resultats_participants SET"
|
||
. " eve_id = " . $intEveId . ","
|
||
. " epr_id = " . $intEprId . ","
|
||
. " par_prenom = '" . fxImportResultatsDbEsc($arrFields['par_prenom']) . "',"
|
||
. " par_nom = '" . fxImportResultatsDbEsc($arrFields['par_nom']) . "',"
|
||
. " par_naissance = " . ($arrFields['par_naissance'] !== '' ? ("'" . fxImportResultatsDbEsc($arrFields['par_naissance']) . "'") : "NULL") . ","
|
||
. " par_age = " . intval($arrFields['par_age']) . ","
|
||
. " par_sexe = '" . fxImportResultatsDbEsc($arrFields['par_sexe']) . "',"
|
||
. " par_adresse = '" . fxImportResultatsDbEsc($arrFields['par_adresse']) . "',"
|
||
. " par_ville = '" . fxImportResultatsDbEsc($arrFields['par_ville']) . "',"
|
||
. " pro_id = 0,"
|
||
. " par_codepostal = '" . fxImportResultatsDbEsc($arrFields['par_codepostal']) . "',"
|
||
. " pay_id = 0,"
|
||
. " par_telephone1 = '" . fxImportResultatsDbEsc($arrFields['par_telephone1']) . "',"
|
||
. " par_courriel = '" . fxImportResultatsDbEsc($arrFields['par_courriel']) . "',"
|
||
. " com_id = 0,"
|
||
. " par_nom_equipe = '" . fxImportResultatsDbEsc($arrFields['par_nom_equipe']) . "',"
|
||
. " no_panier = '" . fxImportResultatsDbEsc($strNoPanier) . "',"
|
||
. " par_maj = '" . fxImportResultatsDbEsc($strNow) . "',"
|
||
. " no_commande = '" . fxImportResultatsDbEsc($strNoCommande) . "',"
|
||
. " par_id_original = " . intval($intTempParOrig) . ","
|
||
. " is_cancelled = 0,"
|
||
. " par_no_equipe = " . intval($arrFields['par_no_equipe']) . ","
|
||
. " par_equipe = 0,"
|
||
. " pec_id = " . $intPecLink . ","
|
||
. " rol_id = 2,"
|
||
. " no_bib = '" . fxImportResultatsDbEsc($arrFields['no_bib']) . "',"
|
||
. " par_external_id = '" . fxImportResultatsDbEsc($strExt) . "',"
|
||
. " par_origine = '" . fxImportResultatsDbEsc($strOrigine) . "'";
|
||
if ($objDatabase->fxQuery($sqlPar) === false) {
|
||
$strErr = fxImportResultatsDbError();
|
||
$tabBlocked[] = array(
|
||
'code' => 'insert_par',
|
||
'message' => 'Échec INSERT participant' . ($strErr !== '' ? (' — ' . $strErr) : ''),
|
||
'line' => $intLine,
|
||
'name' => $strNamePreview,
|
||
'external_id' => $strExt,
|
||
'no_bib' => $strBibPreview,
|
||
'par_sexe' => $strSexePreview,
|
||
'pec_id' => $intPecId,
|
||
);
|
||
continue;
|
||
}
|
||
$intParId = intval($objDatabase->fxGetLastId());
|
||
$intParOrig = $intParId;
|
||
$objDatabase->fxQuery(
|
||
"UPDATE resultats_participants SET par_id_original = " . $intParId
|
||
. " WHERE par_id = " . $intParId
|
||
);
|
||
$sqlUrg = "UPDATE resultats_participants SET"
|
||
. " par_contact_urgence_nom = '" . fxImportResultatsDbEsc($arrFields['par_contact_urgence_nom']) . "',"
|
||
. " par_contact_urgence_telephone = '" . fxImportResultatsDbEsc($arrFields['par_contact_urgence_telephone']) . "'"
|
||
. " WHERE par_id = " . $intParId;
|
||
@$objDatabase->fxQuery($sqlUrg);
|
||
|
||
$intCreated++;
|
||
}
|
||
|
||
foreach ($tabQueIds as $strCol => $intQueId) {
|
||
$intCol = intval($strCol);
|
||
$strVal = trim((string)($tabRow[$intCol] ?? ''));
|
||
$strH = (string)($tabHeaders[$intCol] ?? '');
|
||
fxImportResultatsSaveQuestionAnswer($intParOrig, $intPecId, $intQueId, $strH, $strVal);
|
||
}
|
||
}
|
||
|
||
$intBlocked = count($tabBlocked);
|
||
$tabBlockedByType = fxImportResultatsGroupBlockedByType($tabBlocked);
|
||
$intTotal = count($tabRows);
|
||
$strMsg = 'Import terminé : ' . $intCreated . ' créé(s), ' . $intUpdated . ' mis à jour.'
|
||
. ($intBlocked > 0 ? (' ' . $intBlocked . ' exclus.') : '');
|
||
|
||
return array(
|
||
'state' => 'ok',
|
||
'message' => $strMsg,
|
||
'created' => $intCreated,
|
||
'updated' => $intUpdated,
|
||
'skipped' => $intBlocked,
|
||
'row_count' => $intTotal,
|
||
'blocked_count' => $intBlocked,
|
||
'blocked_by_type' => $tabBlockedByType,
|
||
// Compteurs type CT (KPI)
|
||
'kpi_total' => $intTotal,
|
||
'kpi_ok' => $intCreated + $intUpdated,
|
||
'kpi_blocked' => $intBlocked,
|
||
'kpi_created' => $intCreated,
|
||
'kpi_updated' => $intUpdated,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4482 — compte ce que le reset effacerait (origine import_fichier seulement).
|
||
*/
|
||
function fxImportResultatsResetPreview($intEveId) {
|
||
global $objDatabase;
|
||
|
||
$intEveId = intval($intEveId);
|
||
if ($intEveId <= 0) {
|
||
return array('state' => 'error', 'message' => 'eve_id manquant');
|
||
}
|
||
|
||
$strOrig = fxImportResultatsDbEsc(MSIN_IMPORT_RESULTATS_ORIGIN);
|
||
|
||
$sqlPar = "SELECT COUNT(*) AS n FROM resultats_participants"
|
||
. " WHERE eve_id = " . $intEveId
|
||
. " AND par_origine = '" . $strOrig . "'";
|
||
$tabPar = $objDatabase->fxGetResults($sqlPar);
|
||
$intPar = is_array($tabPar) ? intval($tabPar[1]['n'] ?? 0) : 0;
|
||
|
||
$sqlPec = "SELECT COUNT(DISTINCT e.pec_id) AS n"
|
||
. " FROM resultats_epreuves_commandees e"
|
||
. " INNER JOIN resultats_participants p ON p.eve_id = e.eve_id"
|
||
. " AND (p.pec_id = e.pec_id OR p.pec_id = e.pec_id_original)"
|
||
. " WHERE e.eve_id = " . $intEveId
|
||
. " AND p.par_origine = '" . $strOrig . "'";
|
||
$tabPec = $objDatabase->fxGetResults($sqlPec);
|
||
$intPec = is_array($tabPec) ? intval($tabPec[1]['n'] ?? 0) : 0;
|
||
|
||
$sqlQue = "SELECT COUNT(*) AS n FROM resultats_questions"
|
||
. " WHERE par_id IN ("
|
||
. " SELECT pid FROM ("
|
||
. " SELECT par_id AS pid FROM resultats_participants"
|
||
. " WHERE eve_id = " . $intEveId . " AND par_origine = '" . $strOrig . "'"
|
||
. " UNION"
|
||
. " SELECT par_id_original AS pid FROM resultats_participants"
|
||
. " WHERE eve_id = " . $intEveId . " AND par_origine = '" . $strOrig . "'"
|
||
. " AND par_id_original > 0"
|
||
. " ) _ms1_q"
|
||
. ")";
|
||
$tabQue = $objDatabase->fxGetResults($sqlQue);
|
||
$intQue = is_array($tabQue) ? intval($tabQue[1]['n'] ?? 0) : 0;
|
||
|
||
// Diagnostic : ce que la liste inscriptions verrait (même join + pec_actif)
|
||
$sqlList = "SELECT COUNT(*) AS n"
|
||
. " FROM resultats_epreuves_commandees e, resultats_participants p"
|
||
. " WHERE e.eve_id = " . $intEveId
|
||
. " AND p.pec_id = e.pec_id_original"
|
||
. " AND p.par_origine = '" . $strOrig . "'"
|
||
. " AND p.rol_id NOT IN (3, 4)"
|
||
. " AND e.is_cancelled = 0 AND e.pec_actif = 1";
|
||
$tabList = $objDatabase->fxGetResults($sqlList);
|
||
$intList = is_array($tabList) ? intval($tabList[1]['n'] ?? 0) : 0;
|
||
|
||
return array(
|
||
'state' => 'ok',
|
||
'eve_id' => $intEveId,
|
||
'participants' => $intPar,
|
||
'pec' => $intPec,
|
||
'questions' => $intQue,
|
||
'list_visible' => $intList,
|
||
'message' => 'En BD : ' . $intPar . ' participant(s) import, '
|
||
. $intPec . ' pec, ' . $intQue . ' réponse(s) questions'
|
||
. ' — dont ' . $intList . ' visible(s) dans la liste (pec_actif=1).',
|
||
);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4482 — efface UNIQUEMENT les données d'import (par_origine=import_fichier) de l'événement.
|
||
* Ne touche pas aux inscriptions en ligne / panier réel / inscriptions_questions.
|
||
* Exige confirm = "RESET".
|
||
*/
|
||
function fxImportResultatsReset($intEveId, $strConfirm) {
|
||
global $objDatabase;
|
||
|
||
@set_time_limit(300);
|
||
|
||
$intEveId = intval($intEveId);
|
||
if ($intEveId <= 0) {
|
||
return array('state' => 'error', 'message' => 'eve_id manquant');
|
||
}
|
||
if (trim((string)$strConfirm) !== 'RESET') {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => 'Confirmation invalide — tapez exactement RESET.',
|
||
);
|
||
}
|
||
|
||
$arrPrev = fxImportResultatsResetPreview($intEveId);
|
||
if (($arrPrev['state'] ?? '') !== 'ok') {
|
||
return $arrPrev;
|
||
}
|
||
if (intval($arrPrev['participants'] ?? 0) < 1) {
|
||
return array(
|
||
'state' => 'ok',
|
||
'message' => 'Rien à effacer — aucun participant import_fichier sur cet événement.',
|
||
'deleted_participants' => 0,
|
||
'deleted_pec' => 0,
|
||
'deleted_questions' => 0,
|
||
);
|
||
}
|
||
|
||
$strOrig = fxImportResultatsDbEsc(MSIN_IMPORT_RESULTATS_ORIGIN);
|
||
$intDelQue = 0;
|
||
$intDelPar = 0;
|
||
$intDelPec = 0;
|
||
|
||
// Sous-requête IDs participants import (évite JOIN DELETE lents / gros IN PHP)
|
||
$strSubPar = "SELECT pid FROM ("
|
||
. " SELECT par_id AS pid FROM resultats_participants"
|
||
. " WHERE eve_id = " . $intEveId . " AND par_origine = '" . $strOrig . "'"
|
||
. " UNION"
|
||
. " SELECT par_id_original AS pid FROM resultats_participants"
|
||
. " WHERE eve_id = " . $intEveId . " AND par_origine = '" . $strOrig . "'"
|
||
. " AND par_id_original > 0"
|
||
. ") _ms1_imp_par";
|
||
$strSubPec = "SELECT pid FROM ("
|
||
. " SELECT pec_id AS pid FROM resultats_participants"
|
||
. " WHERE eve_id = " . $intEveId . " AND par_origine = '" . $strOrig . "'"
|
||
. " AND pec_id > 0"
|
||
. ") _ms1_imp_pec";
|
||
|
||
// 1) Réponses questions des participants import seulement
|
||
$sqlDelQue = "DELETE FROM resultats_questions WHERE par_id IN (" . $strSubPar . ")"
|
||
. " OR pec_id IN (" . $strSubPec . ")";
|
||
if ($objDatabase->fxQuery($sqlDelQue) !== false) {
|
||
$intDelQue = intval($objDatabase->affected_rows);
|
||
}
|
||
|
||
// 2) Produits éventuels
|
||
@$objDatabase->fxQuery(
|
||
"DELETE FROM resultats_produits_new WHERE par_id IN (" . $strSubPar . ")"
|
||
. " OR pec_id IN (" . $strSubPec . ")"
|
||
);
|
||
|
||
// 3) Coquilles pec liées à l'import (avant delete participants)
|
||
$sqlDelPec = "DELETE FROM resultats_epreuves_commandees WHERE eve_id = " . $intEveId
|
||
. " AND pec_id IN (" . $strSubPec . ")";
|
||
if ($objDatabase->fxQuery($sqlDelPec) !== false) {
|
||
$intDelPec += intval($objDatabase->affected_rows);
|
||
}
|
||
// Aussi pec_id_original = pec participant
|
||
$sqlDelPecOrig = "DELETE FROM resultats_epreuves_commandees WHERE eve_id = " . $intEveId
|
||
. " AND pec_id_original IN (" . $strSubPec . ")";
|
||
if ($objDatabase->fxQuery($sqlDelPecOrig) !== false) {
|
||
$intDelPec += intval($objDatabase->affected_rows);
|
||
}
|
||
$strPrefix = 'I' . $intEveId . 'X%';
|
||
$sqlDelPec2 = "DELETE FROM resultats_epreuves_commandees WHERE eve_id = " . $intEveId
|
||
. " AND no_panier LIKE '" . fxImportResultatsDbEsc($strPrefix) . "'";
|
||
if ($objDatabase->fxQuery($sqlDelPec2) !== false) {
|
||
$intDelPec += intval($objDatabase->affected_rows);
|
||
}
|
||
|
||
// 4) Participants import
|
||
$sqlDelPar = "DELETE FROM resultats_participants WHERE eve_id = " . $intEveId
|
||
. " AND par_origine = '" . $strOrig . "'";
|
||
if ($objDatabase->fxQuery($sqlDelPar) !== false) {
|
||
$intDelPar = intval($objDatabase->affected_rows);
|
||
}
|
||
|
||
return array(
|
||
'state' => 'ok',
|
||
'message' => 'Reset import OK — participants ' . $intDelPar
|
||
. ', pec ' . $intDelPec
|
||
. ', réponses questions ' . $intDelQue
|
||
. '. Catalogue questions événement intact. Inscriptions en ligne intactes.',
|
||
'deleted_participants' => $intDelPar,
|
||
'deleted_pec' => $intDelPec,
|
||
'deleted_questions' => $intDelQue,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Panneau UI fiche événement superadm.
|
||
*/
|
||
function fxImportResultatsRenderEventPanel($intEveId) {
|
||
global $vDomaine;
|
||
|
||
$intEveId = intval($intEveId);
|
||
if ($intEveId <= 0) {
|
||
return;
|
||
}
|
||
$blnOkType = fxImportResultatsEveIsSansInscriptionEnLigne($intEveId);
|
||
$intTeReq = fxImportResultatsTeIdSansInscriptionEnLigne();
|
||
$strAjax = rtrim((string)$vDomaine, '/') . '/superadm/ajax_import_resultats.php';
|
||
$strLoaderGif = rtrim((string)$vDomaine, '/') . '/images/ms1-runner-loader.gif';
|
||
$tabEpreuves = fxImportResultatsEpreuvesList($intEveId);
|
||
$intNbEpr = count($tabEpreuves);
|
||
|
||
require_once dirname(__FILE__) . '/inc_fx_superadm_v2_ui.php';
|
||
fxSuperadmV2PanelOpen('Import participants (hors transaction)', array(
|
||
'collapse_id' => 'importResultatsCollapse',
|
||
'zone_id' => 'import_resultats',
|
||
'zone_class' => 'superadm-content-v2-zone mt-3 mb-2',
|
||
'expanded' => true,
|
||
'header_extra' => '<span class="superadm-v2-panel__header-extra ml-2"><span class="badge badge-light">MSIN-4482</span></span>',
|
||
));
|
||
?>
|
||
<link rel="stylesheet" href="<?php echo fxImportResultatsEsc(rtrim((string)$vDomaine, '/') . '/css/ms1-loader.css'); ?>?v=<?php echo defined('_VERSION_CODE') ? _VERSION_CODE : '1'; ?>">
|
||
<div id="ms1-import-resultats" class="ms1-import"
|
||
data-eve-id="<?php echo $intEveId; ?>"
|
||
data-eve-ok="<?php echo $blnOkType ? '1' : '0'; ?>"
|
||
data-loader-src="<?php echo fxImportResultatsEsc($strLoaderGif); ?>"
|
||
data-ajax="<?php echo fxImportResultatsEsc($strAjax); ?>">
|
||
<?php if (!$blnOkType) { ?>
|
||
<div class="alert alert-warning">
|
||
<strong>Type d’événement requis</strong><br>
|
||
L’import 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 n’existe pas encore en BD — exécuter
|
||
<code>sql/MSIN-4482-type-sans-inscription-en-ligne.sql</code> en dev préprod.</span>
|
||
<?php } ?>
|
||
</div>
|
||
<?php } else { ?>
|
||
<div class="alert alert-success py-2">
|
||
Type <strong>Sans inscription en ligne</strong> — import autorisé (isolé du flux payant).
|
||
</div>
|
||
<?php } ?>
|
||
<div id="ms1_import_body" <?php echo $blnOkType ? '' : 'hidden'; ?>>
|
||
<p class="text-muted small mb-3">
|
||
Déposez le fichier client (CSV / Excel). Alignez les colonnes une fois —
|
||
le <strong>template</strong> est sauvegardé pour les prochains imports du même format.
|
||
Incluez une colonne <strong>épreuve</strong> et une <strong>clé unique</strong> (ChronoTrack).
|
||
<?php if ($intNbEpr < 1) { ?>
|
||
<br><span class="text-danger">Aucune épreuve sur cet événement — créez-en avant d'importer.</span>
|
||
<?php } else { ?>
|
||
<br><?php echo intval($intNbEpr); ?> épreuve(s) disponible(s) pour le mapping.
|
||
<?php } ?>
|
||
<br><span class="text-muted">Reset = efface seulement <code>par_origine=import_fichier</code> sur cet événement (pas les inscriptions payantes).</span>
|
||
</p>
|
||
|
||
<div class="mb-3" id="ms1_import_db_status_wrap">
|
||
<div class="d-flex flex-wrap align-items-center justify-content-between mb-2">
|
||
<strong class="small mb-0">Déjà en BD (origine import_fichier)</strong>
|
||
<div>
|
||
<button type="button" class="btn btn-outline-secondary btn-sm mr-1" id="ms1_import_refresh_status">
|
||
<i class="fa fa-refresh" aria-hidden="true"></i> Actualiser
|
||
</button>
|
||
<button type="button" class="btn btn-outline-danger btn-sm" id="ms1_import_reset_data">
|
||
<i class="fa fa-trash" aria-hidden="true"></i> Reset données import
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="ct-api-kpi-row" id="ms1_import_db_kpi_row">
|
||
<div class="ct-api-kpi" data-kpi="db_par"><div class="ct-api-kpi__label">Participants</div>
|
||
<div class="ct-api-kpi__value" id="ms1_import_db_par">…</div></div>
|
||
<div class="ct-api-kpi" data-kpi="db_pec"><div class="ct-api-kpi__label">PEC (coquilles)</div>
|
||
<div class="ct-api-kpi__value" id="ms1_import_db_pec">…</div></div>
|
||
<div class="ct-api-kpi" data-kpi="db_que"><div class="ct-api-kpi__label">Réponses questions</div>
|
||
<div class="ct-api-kpi__value" id="ms1_import_db_que">…</div></div>
|
||
<div class="ct-api-kpi ct-api-kpi--warn" data-kpi="db_list"><div class="ct-api-kpi__label">Visibles liste*</div>
|
||
<div class="ct-api-kpi__value" id="ms1_import_db_list">…</div></div>
|
||
</div>
|
||
<p class="small text-muted mb-0 mt-1" id="ms1_import_db_hint">
|
||
* Visibles liste ≈ join liste inscriptions avec <code>pec_actif=1</code> (diagnostic).
|
||
</p>
|
||
<div id="ms1_import_reset_confirm" class="alert alert-danger mt-3 mb-0" hidden>
|
||
<strong>Confirmer le reset import</strong>
|
||
<p class="small mb-2 mt-1" id="ms1_import_reset_confirm_msg"></p>
|
||
<p class="small mb-2">N’efface <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 l’effacement
|
||
</button>
|
||
<button type="button" class="btn btn-outline-secondary btn-sm mb-2" id="ms1_import_reset_confirm_cancel">
|
||
Annuler
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ms1-import__drop" id="ms1_import_drop" tabindex="0" role="button"
|
||
aria-label="Zone de dépôt du fichier d'import">
|
||
<div class="ms1-import__drop-icon" aria-hidden="true"><i class="fa fa-cloud-upload"></i></div>
|
||
<div class="ms1-import__drop-title">Glissez-déposez votre fichier ici</div>
|
||
<div class="ms1-import__drop-sub">ou cliquez pour parcourir · CSV, XLS, XLSX</div>
|
||
<input type="file" id="ms1_import_file" accept=".csv,.txt,.xls,.xlsx,text/csv,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" hidden>
|
||
</div>
|
||
|
||
<div id="ms1_import_busy" class="ms1-import__busy" hidden>
|
||
<div class="ms1-import__busy-inner">
|
||
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
|
||
<div>
|
||
<div class="ms1-import__busy-title" id="ms1_import_busy_title">Traitement en cours…</div>
|
||
<div class="ms1-import__busy-sub" id="ms1_import_busy_sub">Ne fermez pas la page.</div>
|
||
<div class="ms1-import__busy-time">Temps écoulé : <strong id="ms1_import_busy_sec">0</strong> s</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div id="ms1_import_status" class="ms1-import__status mt-2" hidden></div>
|
||
|
||
<div class="ct-api-kpi-row mb-3 mt-2" id="ms1_import_kpi_row" hidden>
|
||
<div class="ct-api-kpi" data-kpi="total"><div class="ct-api-kpi__label">Lignes fichier</div>
|
||
<div class="ct-api-kpi__value" id="ms1_import_kpi_total">—</div></div>
|
||
<div class="ct-api-kpi ct-api-kpi--ok" data-kpi="ok"><div class="ct-api-kpi__label">Importés</div>
|
||
<div class="ct-api-kpi__value" id="ms1_import_kpi_ok">—</div></div>
|
||
<div class="ct-api-kpi ct-api-kpi--ok" data-kpi="created"><div class="ct-api-kpi__label">Créés</div>
|
||
<div class="ct-api-kpi__value" id="ms1_import_kpi_created">—</div></div>
|
||
<div class="ct-api-kpi ct-api-kpi--warn" data-kpi="updated"><div class="ct-api-kpi__label">Mis à jour</div>
|
||
<div class="ct-api-kpi__value" id="ms1_import_kpi_updated">—</div></div>
|
||
<div class="ct-api-kpi ct-api-kpi--danger" data-kpi="blocked"><div class="ct-api-kpi__label">Exclus</div>
|
||
<div class="ct-api-kpi__value" id="ms1_import_kpi_blocked">—</div></div>
|
||
</div>
|
||
|
||
<div class="ct-api-exclus-shell mb-3" id="ms1_import_exclus_shell" hidden>
|
||
<div class="ct-api-exclus-shell__head">
|
||
<span class="ct-api-exclus-shell__title-wrap">
|
||
<span class="ct-api-exclus-shell__title">Exclus</span>
|
||
<span class="badge badge-danger ml-2"><span id="ms1_import_hdr_exclus">0</span> exclus</span>
|
||
</span>
|
||
</div>
|
||
<div class="ct-api-exclus-shell__body">
|
||
<div id="ms1_import_blocked_wrap">
|
||
<div class="ct-api-exclus-empty">Aucun exclus pour le moment.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="ms1_import_step_map" class="ms1-import__step mt-3" hidden>
|
||
<div class="d-flex flex-wrap align-items-center justify-content-between mb-2">
|
||
<div>
|
||
<strong id="ms1_import_file_label"></strong>
|
||
<span class="text-muted small" id="ms1_import_row_label"></span>
|
||
</div>
|
||
<button type="button" class="btn btn-sm btn-outline-secondary" id="ms1_import_reset">Autre fichier</button>
|
||
</div>
|
||
|
||
<div id="ms1_import_templates" class="ms1-import__templates mb-3" hidden></div>
|
||
|
||
<h6 class="ms1-import__h">1. Colonnes → champs MS1</h6>
|
||
<div class="table-responsive">
|
||
<table class="table table-sm table-bordered mb-3" id="ms1_import_map_table">
|
||
<thead>
|
||
<tr>
|
||
<th>Colonne fichier</th>
|
||
<th>Aperçu</th>
|
||
<th>Champ MS1</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div id="ms1_import_epr_box" class="ms1-import__epr mb-3" hidden>
|
||
<h6 class="ms1-import__h">2. Valeurs d'épreuve → épreuves de l'événement</h6>
|
||
<div class="table-responsive">
|
||
<table class="table table-sm table-bordered mb-0" id="ms1_import_epr_table">
|
||
<thead>
|
||
<tr>
|
||
<th>Valeur dans le fichier</th>
|
||
<th>Épreuve MS1</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="ms1_import_que_box" class="ms1-import__que mb-3" hidden>
|
||
<h6 class="ms1-import__h">3. Questions (option C — match ou créer)</h6>
|
||
<p class="small text-muted mb-2">Colonnes non mappées à un champ standard peuvent être liées à une question existante ou marquées « Créer à l'import ».</p>
|
||
<div class="table-responsive">
|
||
<table class="table table-sm table-bordered mb-0" id="ms1_import_que_table">
|
||
<thead>
|
||
<tr>
|
||
<th>Colonne</th>
|
||
<th>Question</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ms1-import__actions form-inline flex-wrap">
|
||
<label class="mr-2 mb-2" for="ms1_import_tpl_name">Nom du template</label>
|
||
<input type="text" class="form-control form-control-sm mr-2 mb-2" id="ms1_import_tpl_name"
|
||
placeholder="ex. Format client ACME" style="min-width:14rem">
|
||
<button type="button" class="btn btn-outline-primary btn-sm mb-2 mr-2" id="ms1_import_save_tpl">
|
||
<i class="fa fa-save" aria-hidden="true"></i> Enregistrer le template
|
||
</button>
|
||
<button type="button" class="btn btn-outline-secondary btn-sm mb-2 mr-2" id="ms1_import_validate">
|
||
<i class="fa fa-check" aria-hidden="true"></i> Valider le mapping
|
||
</button>
|
||
<button type="button" class="btn btn-success btn-sm mb-2" id="ms1_import_commit">
|
||
<i class="fa fa-play" aria-hidden="true"></i> Importer dans MS1
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div><!-- #ms1_import_body -->
|
||
</div>
|
||
<style>
|
||
/* MSIN-4482 — même langage visuel que ChronoTrack (KPI / Exclus) */
|
||
#ms1-import-resultats .ct-api-kpi-row {
|
||
display: grid;
|
||
gap: 0.65rem;
|
||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||
}
|
||
#ms1_import_db_kpi_row.ct-api-kpi-row {
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
}
|
||
@media (max-width: 992px) {
|
||
#ms1-import-resultats .ct-api-kpi-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||
}
|
||
#ms1-import-resultats .ct-api-kpi {
|
||
background: #f8fafc;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 8px;
|
||
padding: 0.55rem 0.7rem;
|
||
}
|
||
#ms1-import-resultats .ct-api-kpi--ok { background: #eff6ff; border-color: #bfdbfe; }
|
||
#ms1-import-resultats .ct-api-kpi--danger { background: #fef2f2; border-color: #fecaca; }
|
||
#ms1-import-resultats .ct-api-kpi--warn { background: #fffbeb; border-color: #fde68a; }
|
||
#ms1-import-resultats .ct-api-kpi__label {
|
||
color: #64748b;
|
||
font-size: 0.72rem;
|
||
font-weight: 600;
|
||
letter-spacing: 0.02em;
|
||
text-transform: uppercase;
|
||
}
|
||
#ms1-import-resultats .ct-api-kpi__value {
|
||
color: #0f172a;
|
||
font-size: 1.35rem;
|
||
font-weight: 700;
|
||
line-height: 1.2;
|
||
}
|
||
#ms1-import-resultats .ct-api-kpi--ok .ct-api-kpi__value { color: #1d4ed8; }
|
||
#ms1-import-resultats .ct-api-kpi--danger .ct-api-kpi__value { color: #b91c1c; }
|
||
#ms1-import-resultats .ct-api-kpi--warn .ct-api-kpi__value { color: #b45309; }
|
||
#ms1-import-resultats .ct-api-exclus-shell {
|
||
background: #fff;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
}
|
||
#ms1-import-resultats .ct-api-exclus-shell__head {
|
||
background: #f8fafc;
|
||
border-bottom: 1px solid #e2e8f0;
|
||
padding: 0.65rem 0.85rem;
|
||
}
|
||
#ms1-import-resultats .ct-api-exclus-shell__title { font-weight: 700; }
|
||
#ms1-import-resultats .ct-api-exclus-shell__body { padding: 12px 14px; }
|
||
#ms1-import-resultats .ct-api-exclus-empty { color: #64748b; font-size: 0.875rem; }
|
||
#ms1-import-resultats .ct-api-exclus-chip {
|
||
align-items: center;
|
||
background: #fef2f2;
|
||
border: 1px solid #fecaca;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 0.75rem;
|
||
padding: 0.55rem 0.75rem;
|
||
}
|
||
#ms1-import-resultats .ct-api-exclus-chip__count {
|
||
color: #b91c1c;
|
||
font-size: 1.25rem;
|
||
font-weight: 700;
|
||
}
|
||
#ms1_import_blocked_wrap .ct-api-blocked-table { font-size: 0.8rem; }
|
||
#ms1_import_blocked_wrap .ct-api-blocked-table th { white-space: nowrap; }
|
||
#ms1_import_blocked_wrap .ct-api-blocked-scroll { max-height: 55vh; overflow: auto; }
|
||
#ms1-import-resultats .ms1-import__busy {
|
||
background: #fff7ed;
|
||
border: 2px solid #f59e0b;
|
||
border-radius: 8px;
|
||
margin: 0.75rem 0;
|
||
padding: 0.85rem 1rem;
|
||
}
|
||
#ms1-import-resultats .ms1-import__busy-inner {
|
||
align-items: flex-start;
|
||
display: flex;
|
||
gap: 0.85rem;
|
||
}
|
||
#ms1-import-resultats .ms1-import__busy .fa-spinner {
|
||
color: #b45309;
|
||
font-size: 1.6rem;
|
||
margin-top: 0.15rem;
|
||
}
|
||
#ms1-import-resultats .ms1-import__busy-title {
|
||
color: #9a3412;
|
||
font-size: 1rem;
|
||
font-weight: 700;
|
||
}
|
||
#ms1-import-resultats .ms1-import__busy-sub {
|
||
color: #92400e;
|
||
font-size: 0.875rem;
|
||
margin-top: 0.15rem;
|
||
}
|
||
#ms1-import-resultats .ms1-import__busy-time {
|
||
color: #78350f;
|
||
font-size: 0.8rem;
|
||
margin-top: 0.35rem;
|
||
}
|
||
#ms1-import-resultats.is-busy-op {
|
||
opacity: 0.92;
|
||
pointer-events: none;
|
||
}
|
||
#ms1-import-resultats.is-busy-op .ms1-import__busy {
|
||
pointer-events: auto;
|
||
}
|
||
/* Coureur dans #preloader superadm (style_blue #spinner = gros FA) */
|
||
#preloader #spinner .ms1-loader__runner {
|
||
display: block;
|
||
height: 80px;
|
||
margin: 0 auto 0.5rem;
|
||
object-fit: contain;
|
||
width: 80px;
|
||
}
|
||
#preloader #spinner .ms1-import-preload-time {
|
||
color: #555;
|
||
font-size: 0.85rem;
|
||
text-align: center;
|
||
}
|
||
</style>
|
||
<script>
|
||
(function ($) {
|
||
var $root = $('#ms1-import-resultats');
|
||
if (!$root.length) return;
|
||
var eveId = parseInt($root.data('eve-id'), 10) || 0;
|
||
var eveOk = String($root.data('eve-ok') || '0') === '1';
|
||
var ajaxUrl = String($root.data('ajax') || 'ajax_import_resultats.php');
|
||
var state = null;
|
||
var busyTimer = null;
|
||
var busyStartedAt = 0;
|
||
|
||
if (!eveOk) {
|
||
return;
|
||
}
|
||
|
||
function showStatus(type, msg) {
|
||
var $s = $('#ms1_import_status');
|
||
$s.removeAttr('hidden').removeClass('alert-success alert-danger alert-info alert-warning')
|
||
.addClass('alert alert-' + (type || 'info')).text(msg || '');
|
||
}
|
||
|
||
function ensureImportPreloader() {
|
||
var $p = $('#preloader');
|
||
if ($p.length) {
|
||
// Superadm header a parfois un vieux spinner FA sans coureur
|
||
if (!$p.find('.ms1-loader__runner').length) {
|
||
var src = String($root.data('loader-src') || '/images/ms1-runner-loader.gif');
|
||
$p.find('#spinner').prepend(
|
||
'<img class="ms1-loader__runner" src="' + src + '" alt="" role="presentation" decoding="async">'
|
||
);
|
||
}
|
||
if (!$('#ms1_import_preload_sec').length) {
|
||
$p.find('#spinner').append(
|
||
'<div class="ms1-import-preload-time small mt-2">Temps écoulé : <strong id="ms1_import_preload_sec">0</strong> s</div>'
|
||
);
|
||
}
|
||
return $p;
|
||
}
|
||
var src = String($root.data('loader-src') || '/images/ms1-runner-loader.gif');
|
||
$('body').append(
|
||
'<div id="preloader" style="display:none;">'
|
||
+ '<div id="spinner" class="ms1-loader ms1-loader--overlay">'
|
||
+ '<img class="ms1-loader__runner" src="' + $('<div/>').text(src).html() + '" alt="" role="presentation" decoding="async">'
|
||
+ '<span id="preloader_text" class="ms1-loader__label"></span>'
|
||
+ '<div class="ms1-import-preload-time small mt-2">Temps écoulé : <strong id="ms1_import_preload_sec">0</strong> s</div>'
|
||
+ '</div></div>'
|
||
);
|
||
return $('#preloader');
|
||
}
|
||
|
||
function setBusy(blnOn, strTitle, strSub) {
|
||
var $b = $('#ms1_import_busy');
|
||
if (busyTimer) {
|
||
clearInterval(busyTimer);
|
||
busyTimer = null;
|
||
}
|
||
if (!blnOn) {
|
||
$b.attr('hidden', true);
|
||
$root.removeClass('is-busy-op');
|
||
if (window.Ms1Loader && typeof window.Ms1Loader.hide === 'function') {
|
||
window.Ms1Loader.hide();
|
||
} else {
|
||
$('#preloader').fadeOut(200);
|
||
}
|
||
return;
|
||
}
|
||
var strMsg = strTitle || 'Traitement en cours…';
|
||
if (strSub) {
|
||
strMsg = strMsg + '<br><span style="font-size:0.85rem;font-weight:500;">' + $('<div/>').text(strSub).html() + '</span>';
|
||
}
|
||
busyStartedAt = Date.now();
|
||
$('#ms1_import_busy_title').text(strTitle || 'Traitement en cours…');
|
||
$('#ms1_import_busy_sub').text(strSub || 'Ne fermez pas la page — c’est 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 : 1–2 min possible. Le coureur continue = OK.');
|
||
} else if (intSec === 90) {
|
||
$('#ms1_import_busy_sub').text('Patience… si ça dépasse 3 min, Actualiser les compteurs après.');
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
function renderImportBlockedPanel(res) {
|
||
var $wrap = $('#ms1_import_blocked_wrap');
|
||
var $shell = $('#ms1_import_exclus_shell');
|
||
var $kpi = $('#ms1_import_kpi_row');
|
||
if (!$wrap.length) return;
|
||
|
||
var intTotal = (res && res.kpi_total != null) ? res.kpi_total : (res.row_count || 0);
|
||
var intOk = (res && res.kpi_ok != null) ? res.kpi_ok : ((res.created || 0) + (res.updated || 0));
|
||
var intCreated = res.kpi_created != null ? res.kpi_created : (res.created || 0);
|
||
var intUpdated = res.kpi_updated != null ? res.kpi_updated : (res.updated || 0);
|
||
var intBlocked = (res && res.blocked_count) ? res.blocked_count : 0;
|
||
var tabGroups = (res && res.blocked_by_type) ? res.blocked_by_type : [];
|
||
|
||
$('#ms1_import_kpi_total').text(String(intTotal));
|
||
$('#ms1_import_kpi_ok').text(String(intOk));
|
||
$('#ms1_import_kpi_created').text(String(intCreated));
|
||
$('#ms1_import_kpi_updated').text(String(intUpdated));
|
||
$('#ms1_import_kpi_blocked').text(String(intBlocked));
|
||
$('#ms1_import_hdr_exclus').text(String(intBlocked));
|
||
$kpi.removeAttr('hidden');
|
||
$shell.removeAttr('hidden');
|
||
|
||
if (intBlocked <= 0 || !tabGroups.length) {
|
||
$wrap.html('<div class="ct-api-exclus-empty">Aucun exclus pour le moment.</div>');
|
||
return;
|
||
}
|
||
|
||
var html = '<div class="ct-api-exclus-chip">';
|
||
html += '<div><strong class="text-danger">Exclus import</strong>';
|
||
html += '<div class="small text-muted">Lignes non importées (à corriger dans le fichier ou le mapping)</div></div>';
|
||
html += '<span class="ct-api-exclus-chip__count">' + intBlocked + '</span></div>';
|
||
|
||
html += '<div id="ms1_import_blocked_detail">';
|
||
$.each(tabGroups, function (_, grp) {
|
||
var intCnt = (grp.items && grp.items.length) ? grp.items.length : 0;
|
||
if (intCnt === 0) return;
|
||
html += '<div class="mb-3">';
|
||
html += '<div class="d-flex align-items-center mb-1">';
|
||
html += '<strong class="text-danger small">' + esc(grp.label || grp.code) + '</strong>';
|
||
html += ' <span class="badge badge-secondary ml-1">' + intCnt + '</span>';
|
||
html += '</div>';
|
||
html += '<div class="ct-api-blocked-scroll border rounded">';
|
||
html += '<table class="table table-sm table-striped ct-api-blocked-table mb-0">';
|
||
html += '<thead class="thead-light"><tr>';
|
||
html += '<th>Ligne</th><th>Nom</th><th>external_id</th><th>Dossard</th><th>Sexe</th><th>Raison</th>';
|
||
html += '</tr></thead><tbody>';
|
||
$.each(grp.items, function (__, item) {
|
||
html += '<tr>';
|
||
html += '<td>' + esc(String(item.line || '')) + '</td>';
|
||
html += '<td>' + esc(item.name || '—') + '</td>';
|
||
html += '<td>' + esc(item.external_id || '—') + '</td>';
|
||
html += '<td>' + esc(item.no_bib ? ('#' + item.no_bib) : '—') + '</td>';
|
||
html += '<td>' + esc(item.par_sexe || '—') + '</td>';
|
||
html += '<td class="text-muted">' + esc(item.message || '') + '</td>';
|
||
html += '</tr>';
|
||
});
|
||
html += '</tbody></table></div></div>';
|
||
});
|
||
html += '</div>';
|
||
$wrap.html(html);
|
||
}
|
||
|
||
function parseAjaxJson(raw) {
|
||
if (raw && typeof raw === 'object') return raw;
|
||
var s = String(raw || '');
|
||
var i = s.indexOf('{');
|
||
var j = s.lastIndexOf('}');
|
||
if (i < 0 || j <= i) return null;
|
||
try {
|
||
return JSON.parse(s.substring(i, j + 1));
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function ensurePanelOpen() {
|
||
var $c = $('#importResultatsCollapse');
|
||
if ($c.length && !$c.hasClass('show') && window.jQuery) {
|
||
$c.collapse('show');
|
||
}
|
||
if ($root.length && $root[0].scrollIntoView) {
|
||
try { $root[0].scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (e) {}
|
||
}
|
||
}
|
||
|
||
function resetUi() {
|
||
state = null;
|
||
$('#ms1_import_step_map').attr('hidden', true);
|
||
$('#ms1_import_templates').attr('hidden', true).empty();
|
||
$('#ms1_import_epr_box').attr('hidden', true);
|
||
$('#ms1_import_que_box').attr('hidden', true);
|
||
$('#ms1_import_kpi_row').attr('hidden', true);
|
||
$('#ms1_import_exclus_shell').attr('hidden', true);
|
||
$('#ms1_import_blocked_wrap').html('<div class="ct-api-exclus-empty">Aucun exclus pour le moment.</div>');
|
||
$('#ms1_import_file').val('');
|
||
$('#ms1_import_drop').removeClass('is-busy is-ready');
|
||
}
|
||
|
||
function esc(s) {
|
||
return $('<div/>').text(s == null ? '' : String(s)).html();
|
||
}
|
||
|
||
function collectMapping() {
|
||
var columns = [];
|
||
$('#ms1_import_map_table tbody select.js-col-map').each(function () {
|
||
columns.push($(this).val() || '');
|
||
});
|
||
var epr_values = {};
|
||
$('#ms1_import_epr_table tbody tr').each(function () {
|
||
var v = $(this).attr('data-val');
|
||
var epr = parseInt($(this).find('select').val(), 10) || 0;
|
||
if (v != null) epr_values[v] = epr;
|
||
});
|
||
var questions = {};
|
||
$('#ms1_import_que_table tbody tr').each(function () {
|
||
var idx = $(this).attr('data-col');
|
||
var q = $(this).find('select').val() || '';
|
||
if (idx != null && q !== '') questions[String(idx)] = q;
|
||
});
|
||
return { columns: columns, epr_values: epr_values, questions: questions };
|
||
}
|
||
|
||
function fieldOptionsHtml(selected) {
|
||
if (!state || !state.fields) return '';
|
||
var html = '';
|
||
var lastGroup = null;
|
||
state.fields.forEach(function (f) {
|
||
if (f.group && f.group !== lastGroup) {
|
||
if (lastGroup !== null) html += '</optgroup>';
|
||
html += '<optgroup label="' + esc(f.group) + '">';
|
||
lastGroup = f.group;
|
||
}
|
||
if (!f.group && lastGroup !== null) {
|
||
html += '</optgroup>';
|
||
lastGroup = null;
|
||
}
|
||
var sel = (String(f.key) === String(selected || '')) ? ' selected' : '';
|
||
html += '<option value="' + esc(f.key) + '"' + sel + '>' + esc(f.label) + '</option>';
|
||
});
|
||
if (lastGroup !== null) html += '</optgroup>';
|
||
return html;
|
||
}
|
||
|
||
function eprOptionsHtml(selected) {
|
||
var html = '<option value="0">— Choisir —</option>';
|
||
(state.epreuves || []).forEach(function (e) {
|
||
var sel = (parseInt(e.epr_id, 10) === parseInt(selected, 10)) ? ' selected' : '';
|
||
html += '<option value="' + esc(e.epr_id) + '"' + sel + '>' + esc(e.label) + '</option>';
|
||
});
|
||
return html;
|
||
}
|
||
|
||
function queOptionsHtml(selected) {
|
||
var html = '<option value="">— Pas une question —</option>';
|
||
html += '<option value="new"' + (selected === 'new' ? ' selected' : '') + '>Créer à l\'import (confirmé)</option>';
|
||
(state.questions || []).forEach(function (q) {
|
||
var sel = (String(q.que_id) === String(selected)) ? ' selected' : '';
|
||
html += '<option value="' + esc(q.que_id) + '"' + sel + '>' + esc(q.label) + '</option>';
|
||
});
|
||
return html;
|
||
}
|
||
|
||
function renderEprBox() {
|
||
var eprCol = -1;
|
||
$('#ms1_import_map_table tbody select.js-col-map').each(function (i) {
|
||
if ($(this).val() === 'epr') eprCol = i;
|
||
});
|
||
var $box = $('#ms1_import_epr_box');
|
||
var $tb = $('#ms1_import_epr_table tbody').empty();
|
||
if (eprCol < 0) {
|
||
$box.attr('hidden', true);
|
||
return;
|
||
}
|
||
var vals = (state.distincts && state.distincts[eprCol]) ? state.distincts[eprCol] : [];
|
||
var saved = (state.appliedMapping && state.appliedMapping.epr_values) ? state.appliedMapping.epr_values : {};
|
||
vals.forEach(function (v) {
|
||
var cur = saved[v] || 0;
|
||
$tb.append(
|
||
'<tr data-val="' + esc(v) + '"><td><code>' + esc(v) + '</code></td>'
|
||
+ '<td><select class="form-control form-control-sm">' + eprOptionsHtml(cur) + '</select></td></tr>'
|
||
);
|
||
});
|
||
$box.removeAttr('hidden');
|
||
}
|
||
|
||
function renderQueBox() {
|
||
var $box = $('#ms1_import_que_box');
|
||
var $tb = $('#ms1_import_que_table tbody').empty();
|
||
var saved = (state.appliedMapping && state.appliedMapping.questions) ? state.appliedMapping.questions : {};
|
||
var any = false;
|
||
$('#ms1_import_map_table tbody select.js-col-map').each(function (i) {
|
||
if (($(this).val() || '') !== '') return;
|
||
any = true;
|
||
var h = state.headers[i] || ('Col ' + (i + 1));
|
||
var cur = saved[String(i)] || saved[i] || '';
|
||
$tb.append(
|
||
'<tr data-col="' + i + '"><td>' + esc(h) + '</td>'
|
||
+ '<td><select class="form-control form-control-sm js-que-map">' + queOptionsHtml(cur) + '</select></td></tr>'
|
||
);
|
||
});
|
||
if (any) $box.removeAttr('hidden');
|
||
else $box.attr('hidden', true);
|
||
}
|
||
|
||
function applyMappingObj(mapping) {
|
||
if (!mapping || !mapping.columns) return;
|
||
state.appliedMapping = mapping;
|
||
$('#ms1_import_map_table tbody select.js-col-map').each(function (i) {
|
||
if (mapping.columns[i] != null) {
|
||
$(this).val(String(mapping.columns[i]));
|
||
}
|
||
});
|
||
renderEprBox();
|
||
renderQueBox();
|
||
}
|
||
|
||
function renderMap() {
|
||
var $tb = $('#ms1_import_map_table tbody').empty();
|
||
(state.headers || []).forEach(function (h, i) {
|
||
var sample = '';
|
||
if (state.samples && state.samples[0]) {
|
||
sample = state.samples[0][i] != null ? String(state.samples[0][i]) : '';
|
||
}
|
||
var guess = (state.guess && state.guess[i]) ? state.guess[i] : '';
|
||
$tb.append(
|
||
'<tr><td><strong>' + esc(h) + '</strong></td>'
|
||
+ '<td class="small text-muted">' + esc(sample) + '</td>'
|
||
+ '<td><select class="form-control form-control-sm js-col-map" data-col="' + i + '">'
|
||
+ fieldOptionsHtml(guess) + '</select></td></tr>'
|
||
);
|
||
});
|
||
$tb.off('change', 'select.js-col-map').on('change', 'select.js-col-map', function () {
|
||
renderEprBox();
|
||
renderQueBox();
|
||
});
|
||
renderEprBox();
|
||
renderQueBox();
|
||
|
||
var $tpl = $('#ms1_import_templates').empty();
|
||
if (state.templates && state.templates.length) {
|
||
// Défaut = template déjà utilisé sur CET événement (plus récent) ; sinon aucun auto-apply
|
||
var defaultTpl = null;
|
||
(state.templates || []).forEach(function (t) {
|
||
if (t.same_event && !defaultTpl) {
|
||
defaultTpl = t;
|
||
}
|
||
});
|
||
var html = '<div class="alert alert-success py-2 mb-0"><strong>Template(s) trouvé(s)</strong> pour ces colonnes.';
|
||
if (defaultTpl) {
|
||
html += ' Le template de cet événement est appliqué par défaut.';
|
||
} else {
|
||
html += ' Aucun template propre à cet événement — choisissez-en un ou alignez manuellement.';
|
||
}
|
||
html += '</div><div class="list-group mt-2">';
|
||
state.templates.forEach(function (t) {
|
||
var blnDef = defaultTpl && parseInt(t.tpl_id, 10) === parseInt(defaultTpl.tpl_id, 10);
|
||
html += '<button type="button" class="list-group-item list-group-item-action js-apply-tpl'
|
||
+ (blnDef ? ' active' : '') + '" data-tpl="'
|
||
+ esc(t.tpl_id) + '"><i class="fa fa-magic" aria-hidden="true"></i> '
|
||
+ esc(t.name || ('Template #' + t.tpl_id))
|
||
+ (blnDef ? ' <span class="badge badge-light ml-1">défaut événement</span>' : '')
|
||
+ ' <span class="text-muted small">(' + esc(t.maj || '')
|
||
+ (t.same_event ? '' : ' · autre événement même org') + ')</span></button>';
|
||
});
|
||
html += '</div>';
|
||
$tpl.html(html).removeAttr('hidden');
|
||
$tpl.find('.js-apply-tpl').on('click', function () {
|
||
var id = parseInt($(this).data('tpl'), 10);
|
||
var found = null;
|
||
(state.templates || []).forEach(function (t) {
|
||
if (parseInt(t.tpl_id, 10) === id) found = t;
|
||
});
|
||
if (found && found.mapping) {
|
||
applyMappingObj(found.mapping);
|
||
$('#ms1_import_tpl_name').val(found.name || '');
|
||
$tpl.find('.js-apply-tpl').removeClass('active');
|
||
$(this).addClass('active');
|
||
showStatus('success', 'Template « ' + (found.name || id) + ' » appliqué.');
|
||
}
|
||
});
|
||
if (defaultTpl && defaultTpl.mapping) {
|
||
applyMappingObj(defaultTpl.mapping);
|
||
$('#ms1_import_tpl_name').val(defaultTpl.name || '');
|
||
showStatus('success', 'Fichier lu — template « '
|
||
+ (defaultTpl.name || defaultTpl.tpl_id)
|
||
+ ' » (cet événement) appliqué automatiquement.');
|
||
}
|
||
} else {
|
||
$tpl.attr('hidden', true);
|
||
}
|
||
|
||
$('#ms1_import_file_label').text(state.original_name || 'Fichier');
|
||
$('#ms1_import_row_label').text(' · ' + (state.row_count || 0) + ' ligne(s)');
|
||
$('#ms1_import_step_map').removeAttr('hidden').show();
|
||
$('#ms1_import_drop').addClass('is-ready');
|
||
ensurePanelOpen();
|
||
}
|
||
|
||
function uploadFile(file) {
|
||
if (!file) return;
|
||
var fd = new FormData();
|
||
fd.append('a', 'upload');
|
||
fd.append('eve_id', String(eveId));
|
||
fd.append('file', file);
|
||
$('#ms1_import_drop').addClass('is-busy');
|
||
setBusy(true, 'Lecture du fichier…', 'Parsing CSV / Excel — quelques secondes en général.');
|
||
showStatus('info', 'Lecture du fichier…');
|
||
$.ajax({
|
||
url: ajaxUrl,
|
||
method: 'POST',
|
||
data: fd,
|
||
processData: false,
|
||
contentType: false,
|
||
dataType: 'text',
|
||
timeout: 120000
|
||
}).done(function (raw) {
|
||
setBusy(false);
|
||
$('#ms1_import_drop').removeClass('is-busy');
|
||
var res = parseAjaxJson(raw);
|
||
if (!res || res.state !== 'ok') {
|
||
showStatus('danger', (res && res.message) ? res.message : 'Échec lecture (réponse invalide)');
|
||
return;
|
||
}
|
||
state = res;
|
||
state.appliedMapping = null;
|
||
// Message générique seulement si pas d'auto-template (renderMap écrase le status si défaut)
|
||
showStatus('success', 'Fichier lu — ' + (res.row_count || 0) + ' ligne(s). Alignez les colonnes ci-dessous.');
|
||
renderMap();
|
||
}).fail(function (xhr) {
|
||
setBusy(false);
|
||
$('#ms1_import_drop').removeClass('is-busy');
|
||
var res = parseAjaxJson(xhr && xhr.responseText);
|
||
if (res && res.state === 'ok') {
|
||
state = res;
|
||
state.appliedMapping = null;
|
||
showStatus('success', 'Fichier lu — ' + (res.row_count || 0) + ' ligne(s).');
|
||
renderMap();
|
||
return;
|
||
}
|
||
showStatus('danger', 'Erreur upload (HTTP ' + (xhr && xhr.status ? xhr.status : '?') + ').');
|
||
});
|
||
}
|
||
|
||
var $drop = $('#ms1_import_drop');
|
||
var $file = $('#ms1_import_file');
|
||
$drop.on('click', function (e) {
|
||
if ($(e.target).is('input')) return;
|
||
$file.trigger('click');
|
||
});
|
||
$drop.on('keydown', function (e) {
|
||
if (e.key === 'Enter' || e.key === ' ') {
|
||
e.preventDefault();
|
||
$file.trigger('click');
|
||
}
|
||
});
|
||
$file.on('change', function () {
|
||
if (this.files && this.files[0]) uploadFile(this.files[0]);
|
||
});
|
||
$drop.on('drag dragstart dragend dragover dragenter dragleave drop', function (e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
});
|
||
$drop.on('dragover dragenter', function () { $drop.addClass('is-drag'); });
|
||
$drop.on('dragleave dragend drop', function () { $drop.removeClass('is-drag'); });
|
||
$drop.on('drop', function (e) {
|
||
var files = e.originalEvent && e.originalEvent.dataTransfer
|
||
? e.originalEvent.dataTransfer.files : null;
|
||
if (files && files[0]) uploadFile(files[0]);
|
||
});
|
||
|
||
$('#ms1_import_reset').on('click', function () {
|
||
resetUi();
|
||
showStatus('info', 'Choisissez un nouveau fichier.');
|
||
});
|
||
|
||
$('#ms1_import_save_tpl').on('click', function () {
|
||
if (!state || !state.token) return;
|
||
var mapping = collectMapping();
|
||
$.ajax({
|
||
url: ajaxUrl,
|
||
method: 'POST',
|
||
dataType: 'text',
|
||
data: {
|
||
a: 'save_template',
|
||
eve_id: eveId,
|
||
token: state.token,
|
||
tpl_name: $('#ms1_import_tpl_name').val() || '',
|
||
mapping: JSON.stringify(mapping)
|
||
}
|
||
}).done(function (raw) {
|
||
var res = parseAjaxJson(raw);
|
||
if (!res || res.state !== 'ok') {
|
||
showStatus('danger', (res && res.message) ? res.message : 'Échec sauvegarde template');
|
||
return;
|
||
}
|
||
state.templates = res.templates || state.templates;
|
||
showStatus('success', res.message || 'Template enregistré.');
|
||
renderMap();
|
||
applyMappingObj(mapping);
|
||
}).fail(function () {
|
||
showStatus('danger', 'Erreur réseau (template).');
|
||
});
|
||
});
|
||
|
||
$('#ms1_import_validate').on('click', function () {
|
||
if (!state || !state.token) return;
|
||
var mapping = collectMapping();
|
||
$.ajax({
|
||
url: ajaxUrl,
|
||
method: 'POST',
|
||
dataType: 'text',
|
||
data: {
|
||
a: 'validate',
|
||
eve_id: eveId,
|
||
token: state.token,
|
||
mapping: JSON.stringify(mapping)
|
||
}
|
||
}).done(function (raw) {
|
||
var res = parseAjaxJson(raw);
|
||
if (!res || res.state !== 'ok') {
|
||
showStatus('warning', (res && res.message) ? res.message : 'Mapping incomplet');
|
||
return;
|
||
}
|
||
showStatus('success', res.message || 'OK');
|
||
}).fail(function () {
|
||
showStatus('danger', 'Erreur réseau (validation).');
|
||
});
|
||
});
|
||
|
||
function renderDbStatus(res) {
|
||
if (!res || res.state !== 'ok') return;
|
||
$('#ms1_import_db_par').text(String(res.participants != null ? res.participants : '—'));
|
||
$('#ms1_import_db_pec').text(String(res.pec != null ? res.pec : '—'));
|
||
$('#ms1_import_db_que').text(String(res.questions != null ? res.questions : '—'));
|
||
$('#ms1_import_db_list').text(String(res.list_visible != null ? res.list_visible : '—'));
|
||
if (res.message) {
|
||
$('#ms1_import_db_hint').text(res.message);
|
||
}
|
||
}
|
||
|
||
function refreshDbStatus(blnSilent) {
|
||
$.ajax({
|
||
url: ajaxUrl,
|
||
method: 'POST',
|
||
dataType: 'text',
|
||
data: { a: 'reset_preview', eve_id: eveId }
|
||
}).done(function (raw) {
|
||
var res = parseAjaxJson(raw);
|
||
if (!res || res.state !== 'ok') {
|
||
if (!blnSilent) {
|
||
showStatus('danger', (res && res.message) ? res.message : 'Échec lecture statut BD');
|
||
}
|
||
$('#ms1_import_db_par, #ms1_import_db_pec, #ms1_import_db_que, #ms1_import_db_list').text('?');
|
||
return;
|
||
}
|
||
renderDbStatus(res);
|
||
if (!blnSilent) {
|
||
showStatus('info', res.message || 'Statut BD à jour.');
|
||
}
|
||
}).fail(function () {
|
||
$('#ms1_import_db_par, #ms1_import_db_pec, #ms1_import_db_que, #ms1_import_db_list').text('?');
|
||
if (!blnSilent) {
|
||
showStatus('danger', 'Erreur réseau (statut BD).');
|
||
}
|
||
});
|
||
}
|
||
|
||
// Statut chargé dès l'ouverture du panneau
|
||
refreshDbStatus(true);
|
||
|
||
$('#ms1_import_refresh_status').on('click', function () {
|
||
refreshDbStatus(false);
|
||
});
|
||
|
||
$('#ms1_import_reset_data').on('click', function () {
|
||
var $box = $('#ms1_import_reset_confirm');
|
||
// KPI déjà affichés = pas de 2e round-trip lent avant la confirmation
|
||
var nPar = parseInt($('#ms1_import_db_par').text(), 10);
|
||
var nPec = parseInt($('#ms1_import_db_pec').text(), 10);
|
||
var nQue = parseInt($('#ms1_import_db_que').text(), 10);
|
||
var nList = parseInt($('#ms1_import_db_list').text(), 10);
|
||
if (isNaN(nPar)) {
|
||
showStatus('info', 'Comptage BD en cours — réessayez juste après.');
|
||
refreshDbStatus(false);
|
||
return;
|
||
}
|
||
if (nPar < 1) {
|
||
$box.attr('hidden', true);
|
||
showStatus('info', 'Rien à effacer — 0 participant import en BD.');
|
||
return;
|
||
}
|
||
var strRecap = 'Vous allez effacer : <strong>' + esc(String(nPar)) + '</strong> participants, '
|
||
+ '<strong>' + esc(String(isNaN(nPec) ? '?' : nPec)) + '</strong> pec, '
|
||
+ '<strong>' + esc(String(isNaN(nQue) ? '?' : nQue)) + '</strong> réponses questions '
|
||
+ '(dont ' + esc(String(isNaN(nList) ? '?' : nList)) + ' visibles liste).';
|
||
$('#ms1_import_reset_confirm_msg').html(strRecap);
|
||
$('#ms1_import_reset_confirm_input').val('');
|
||
$box.removeAttr('hidden');
|
||
showStatus('warning', 'Confirmation requise ci-dessous — tapez RESET puis Confirmer.');
|
||
try {
|
||
$box[0].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||
$('#ms1_import_reset_confirm_input').trigger('focus');
|
||
} catch (e) {}
|
||
});
|
||
|
||
$('#ms1_import_reset_confirm_cancel').on('click', function () {
|
||
$('#ms1_import_reset_confirm').attr('hidden', true);
|
||
$('#ms1_import_reset_confirm_input').val('');
|
||
showStatus('info', 'Reset annulé.');
|
||
});
|
||
|
||
$('#ms1_import_reset_confirm_go').on('click', function () {
|
||
var strConfirm = $.trim($('#ms1_import_reset_confirm_input').val() || '');
|
||
if (strConfirm !== 'RESET') {
|
||
showStatus('danger', 'Tapez exactement RESET dans le champ (majuscules).');
|
||
return;
|
||
}
|
||
var $go = $(this);
|
||
$go.prop('disabled', true);
|
||
$('#ms1_import_reset_data').prop('disabled', true);
|
||
setBusy(true,
|
||
'Reset import en cours…',
|
||
'Suppression de milliers de lignes — 30 s à 2 min selon la charge. Le chrono tourne = ça travaille.'
|
||
);
|
||
showStatus('warning', 'Reset en cours — ne fermez pas la page.');
|
||
$.ajax({
|
||
url: ajaxUrl,
|
||
method: 'POST',
|
||
dataType: 'text',
|
||
timeout: 300000,
|
||
data: { a: 'reset', eve_id: eveId, confirm: strConfirm }
|
||
}).done(function (raw2) {
|
||
setBusy(false);
|
||
$go.prop('disabled', false);
|
||
$('#ms1_import_reset_data').prop('disabled', false);
|
||
var res = parseAjaxJson(raw2);
|
||
if (!res || res.state !== 'ok') {
|
||
showStatus('danger', (res && res.message) ? res.message : 'Échec reset');
|
||
return;
|
||
}
|
||
$('#ms1_import_reset_confirm').attr('hidden', true);
|
||
$('#ms1_import_reset_confirm_input').val('');
|
||
showStatus('success', res.message || 'Reset OK');
|
||
$('#ms1_import_kpi_row').attr('hidden', true);
|
||
$('#ms1_import_exclus_shell').attr('hidden', true);
|
||
refreshDbStatus(true);
|
||
}).fail(function (xhr) {
|
||
setBusy(false);
|
||
$go.prop('disabled', false);
|
||
$('#ms1_import_reset_data').prop('disabled', false);
|
||
var res = parseAjaxJson(xhr && xhr.responseText);
|
||
if (res && res.state === 'ok') {
|
||
$('#ms1_import_reset_confirm').attr('hidden', true);
|
||
showStatus('success', res.message || 'Reset OK');
|
||
refreshDbStatus(true);
|
||
return;
|
||
}
|
||
showStatus('danger', 'Erreur / timeout reset (HTTP ' + (xhr && xhr.status ? xhr.status : '?') + '). Vérifiez les compteurs avec Actualiser.');
|
||
refreshDbStatus(true);
|
||
});
|
||
});
|
||
|
||
$('#ms1_import_commit').on('click', function () {
|
||
if (!state || !state.token) return;
|
||
var mapping = collectMapping();
|
||
var nRows = parseInt(state.row_count, 10) || 0;
|
||
var strEta = nRows > 500
|
||
? ('~' + nRows + ' lignes : 1 à 3 minutes (écritures une par une en BD).')
|
||
: 'Quelques dizaines de secondes en général.';
|
||
if (!window.confirm('Importer / mettre à jour les participants dans resultats_* pour cet événement ?\n(Sans panier ni paiement — upsert sur la clé unique.)\n\n' + strEta)) {
|
||
return;
|
||
}
|
||
var $btn = $(this);
|
||
$btn.prop('disabled', true);
|
||
setBusy(true, 'Import en cours…', strEta + ' Chrono ci-dessous = ça travaille, pas un plantage.');
|
||
showStatus('warning', 'Import en cours — ne fermez pas la page.');
|
||
$.ajax({
|
||
url: ajaxUrl,
|
||
method: 'POST',
|
||
dataType: 'text',
|
||
timeout: 600000,
|
||
data: {
|
||
a: 'commit',
|
||
eve_id: eveId,
|
||
token: state.token,
|
||
mapping: JSON.stringify(mapping)
|
||
}
|
||
}).done(function (raw) {
|
||
setBusy(false);
|
||
$btn.prop('disabled', false);
|
||
var res = parseAjaxJson(raw);
|
||
if (!res || res.state !== 'ok') {
|
||
showStatus('danger', (res && res.message) ? res.message : 'Échec import');
|
||
return;
|
||
}
|
||
showStatus((res.blocked_count > 0) ? 'warning' : 'success', res.message || 'Import terminé.');
|
||
renderImportBlockedPanel(res);
|
||
refreshDbStatus(true);
|
||
ensurePanelOpen();
|
||
}).fail(function (xhr) {
|
||
setBusy(false);
|
||
$btn.prop('disabled', false);
|
||
var res = parseAjaxJson(xhr && xhr.responseText);
|
||
if (res && res.state === 'ok') {
|
||
showStatus((res.blocked_count > 0) ? 'warning' : 'success', res.message || 'Import terminé.');
|
||
renderImportBlockedPanel(res);
|
||
refreshDbStatus(true);
|
||
return;
|
||
}
|
||
showStatus('danger', 'Erreur réseau (import). Timeout possible sur très gros fichiers — réessayez ou coupez le fichier.');
|
||
});
|
||
});
|
||
})(window.jQuery);
|
||
</script>
|
||
<?php
|
||
fxSuperadmV2PanelClose(array('collapse_id' => 'importResultatsCollapse'));
|
||
}
|