4525 lines
192 KiB
PHP
4525 lines
192 KiB
PHP
<?php
|
||
/**
|
||
* MSIN-4482 / MSIN-4520 — Import participants hors transaction (parse, templates, UI superadm).
|
||
* MSIN-4520 — Excel multi-onglets : choix onglets = épreuves ou feuille + colonne.
|
||
*/
|
||
|
||
if (!defined('MSIN_IMPORT_RESULTATS_ORIGIN')) {
|
||
define('MSIN_IMPORT_RESULTATS_ORIGIN', 'import_fichier');
|
||
}
|
||
if (!defined('MSIN_TE_NOM_SANS_INSCRIPTION_EN_LIGNE')) {
|
||
define('MSIN_TE_NOM_SANS_INSCRIPTION_EN_LIGNE', 'Sans inscription en ligne');
|
||
}
|
||
|
||
/**
|
||
* MSIN-4482 — te_id du type « Sans inscription en ligne » (0 si pas encore créé en BD).
|
||
*/
|
||
function fxImportResultatsTeIdSansInscriptionEnLigne() {
|
||
global $objDatabase;
|
||
static $intTeId = null;
|
||
if ($intTeId !== null) {
|
||
return $intTeId;
|
||
}
|
||
$intTeId = 0;
|
||
$sql = "SELECT te_id FROM inscriptions_types_evenements"
|
||
. " WHERE te_nom_fr = '" . fxImportResultatsDbEsc(MSIN_TE_NOM_SANS_INSCRIPTION_EN_LIGNE) . "'"
|
||
. " AND te_actif = 1 LIMIT 1";
|
||
$tab = $objDatabase->fxGetResults($sql);
|
||
if (is_array($tab) && !empty($tab[1]['te_id'])) {
|
||
$intTeId = intval($tab[1]['te_id']);
|
||
}
|
||
return $intTeId;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4482 — true si l'événement est typé hors inscription en ligne.
|
||
*/
|
||
function fxImportResultatsEveIsSansInscriptionEnLigne($intEveId) {
|
||
global $objDatabase;
|
||
$intEveId = intval($intEveId);
|
||
$intTeNeed = fxImportResultatsTeIdSansInscriptionEnLigne();
|
||
if ($intEveId <= 0 || $intTeNeed <= 0) {
|
||
return false;
|
||
}
|
||
$sql = "SELECT te_id FROM inscriptions_evenements WHERE eve_id = " . $intEveId . " LIMIT 1";
|
||
$tab = $objDatabase->fxGetResults($sql);
|
||
if (!is_array($tab) || empty($tab[1]['te_id'])) {
|
||
return false;
|
||
}
|
||
return intval($tab[1]['te_id']) === $intTeNeed;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4482 — refuse les actions import si l'eve n'est pas du bon type (protège le business payant).
|
||
* @return array|null null = OK ; sinon payload erreur JSON
|
||
*/
|
||
function fxImportResultatsGuardEveSansInscription($intEveId) {
|
||
$intEveId = intval($intEveId);
|
||
if ($intEveId <= 0) {
|
||
return array('state' => 'error', 'message' => 'eve_id manquant');
|
||
}
|
||
$intTe = fxImportResultatsTeIdSansInscriptionEnLigne();
|
||
if ($intTe <= 0) {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => 'Type « Sans inscription en ligne » absent en BD — exécuter sql/MSIN-4482-type-sans-inscription-en-ligne.sql en dev préprod.',
|
||
);
|
||
}
|
||
if (!fxImportResultatsEveIsSansInscriptionEnLigne($intEveId)) {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => 'Import hors transaction réservé aux événements de type « Sans inscription en ligne ». Changez le Type d\'événement sur la fiche, enregistrez, puis réessayez.',
|
||
'te_id_required' => $intTe,
|
||
);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Champs cibles mappables (hors questions dynamiques).
|
||
* @return array<int, array{key:string,label:string,group:string}>
|
||
*/
|
||
function fxImportResultatsTargetFields() {
|
||
return array(
|
||
array('key' => '', 'label' => '— Ignorer —', 'group' => ''),
|
||
array('key' => 'par_external_id', 'label' => 'Clé unique (ChronoTrack / fichier)', 'group' => 'Identité'),
|
||
array('key' => 'par_prenom', 'label' => 'Prénom', 'group' => 'Identité'),
|
||
array('key' => 'par_nom', 'label' => 'Nom', 'group' => 'Identité'),
|
||
array('key' => 'par_sexe', 'label' => 'Sexe', 'group' => 'Identité'),
|
||
array('key' => 'par_naissance', 'label' => 'Date de naissance', 'group' => 'Identité'),
|
||
array('key' => 'par_age', 'label' => 'Âge', 'group' => 'Identité'),
|
||
array('key' => 'par_courriel', 'label' => 'Courriel', 'group' => 'Contact'),
|
||
array('key' => 'par_telephone1', 'label' => 'Téléphone', 'group' => 'Contact'),
|
||
array('key' => 'par_contact_urgence_nom', 'label' => 'Contact urgence — nom', 'group' => 'Contact'),
|
||
array('key' => 'par_contact_urgence_telephone', 'label' => 'Contact urgence — téléphone', 'group' => 'Contact'),
|
||
array('key' => 'par_adresse', 'label' => 'Adresse', 'group' => 'Contact'),
|
||
array('key' => 'par_ville', 'label' => 'Ville', 'group' => 'Contact'),
|
||
array('key' => 'pro_id', 'label' => 'Région / province', 'group' => 'Contact'),
|
||
array('key' => 'par_codepostal', 'label' => 'Code postal', 'group' => 'Contact'),
|
||
array('key' => 'pay_id', 'label' => 'Pays', 'group' => 'Contact'),
|
||
array('key' => 'no_bib', 'label' => 'Dossard', 'group' => 'Course'),
|
||
array('key' => 'epr', 'label' => 'Épreuve (valeurs → épreuves MS1)', 'group' => 'Course'),
|
||
array('key' => 'par_nom_equipe', 'label' => 'Nom d\'équipe', 'group' => 'Équipe'),
|
||
array('key' => 'par_no_equipe', 'label' => 'No équipe', 'group' => 'Équipe'),
|
||
);
|
||
}
|
||
|
||
function fxImportResultatsEsc($str) {
|
||
return htmlspecialchars((string)$str, ENT_QUOTES, 'UTF-8');
|
||
}
|
||
|
||
function fxImportResultatsUploadDir() {
|
||
$strDir = dirname(__FILE__) . '/../upload/import_resultats';
|
||
if (!is_dir($strDir)) {
|
||
@mkdir($strDir, 0755, true);
|
||
}
|
||
return $strDir;
|
||
}
|
||
|
||
function fxImportResultatsNormalizeHeader($str) {
|
||
$str = trim((string)$str);
|
||
$str = preg_replace('/\s+/u', ' ', $str);
|
||
return mb_strtolower($str, 'UTF-8');
|
||
}
|
||
|
||
/**
|
||
* MSIN-4520 — répare mojibake fréquent « Région » (UTF-8 lu comme Latin-1).
|
||
*/
|
||
function fxImportResultatsRepairMojibake($str) {
|
||
$str = (string)$str;
|
||
if ($str === '' || (strpos($str, 'Ã') === false && strpos($str, 'Â') === false)) {
|
||
return $str;
|
||
}
|
||
if (function_exists('mb_convert_encoding')) {
|
||
$strBytes = @mb_convert_encoding($str, 'ISO-8859-1', 'UTF-8');
|
||
if (is_string($strBytes) && $strBytes !== ''
|
||
&& function_exists('mb_check_encoding')
|
||
&& @mb_check_encoding($strBytes, 'UTF-8')
|
||
) {
|
||
return $strBytes;
|
||
}
|
||
}
|
||
return $str;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4520 — clé comparable sans accents (Quebec ≈ Québec).
|
||
*/
|
||
function fxImportResultatsFoldKey($str) {
|
||
$str = fxImportResultatsRepairMojibake((string)$str);
|
||
$str = fxImportResultatsNormalizeHeader($str);
|
||
$str = strtr($str, array(
|
||
'à' => 'a', 'á' => 'a', 'â' => 'a', 'ä' => 'a', 'ã' => 'a', 'å' => 'a',
|
||
'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e',
|
||
'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i',
|
||
'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'ö' => 'o', 'õ' => 'o',
|
||
'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u',
|
||
'ý' => 'y', 'ÿ' => 'y', 'ç' => 'c', 'ñ' => 'n',
|
||
'œ' => 'oe', 'æ' => 'ae',
|
||
));
|
||
$str = strtolower($str);
|
||
$str = preg_replace('/[^a-z0-9\s\/]+/', '', $str);
|
||
return preg_replace('/\s+/', ' ', trim($str));
|
||
}
|
||
|
||
/**
|
||
* Empreinte stable des en-têtes (même fichier client = même sig).
|
||
*/
|
||
function fxImportResultatsHeaderSig(array $tabHeaders) {
|
||
$tabNorm = array();
|
||
foreach ($tabHeaders as $strH) {
|
||
$tabNorm[] = fxImportResultatsNormalizeHeader($strH);
|
||
}
|
||
return sha1(implode("\n", $tabNorm));
|
||
}
|
||
|
||
/**
|
||
* Suggestion auto de champ selon le libellé d'en-tête.
|
||
* MSIN-4482 — priorise match exact / aiguille longue ; évite faux positifs sur questions bilingues.
|
||
*/
|
||
function fxImportResultatsGuessField($strHeader) {
|
||
// MSIN-4520 — réparer mojibake avant guess (Région → région)
|
||
$strHeader = fxImportResultatsRepairMojibake((string)$strHeader);
|
||
$str = fxImportResultatsNormalizeHeader($strHeader);
|
||
$str = str_replace(array('-', '_'), ' ', $str);
|
||
$strFold = fxImportResultatsFoldKey($strHeader);
|
||
|
||
// MSIN-4482 — inclut libellés type export client (ex. Échappée / Eventbrite-like)
|
||
$tabRules = array(
|
||
'par_external_id' => array(
|
||
'attendee #', 'attendee id', 'external id', 'external_id', 'unique id', 'uniqueid',
|
||
'entry id', 'id unique', 'ct id', 'chronotrack id', 'reg id', 'participant id', 'attendee',
|
||
),
|
||
'par_prenom' => array('first name', 'firstname', 'given name', 'prenom', 'prénom'),
|
||
'par_nom' => array('last name', 'lastname', 'family name', 'surname'),
|
||
'par_sexe' => array('gender', 'sexe', 'sex', 'genre'),
|
||
'par_naissance' => array('birth date', 'date of birth', 'date naissance', 'naissance', 'birthday', 'birthdate', 'dob'),
|
||
'par_age' => array('age', 'âge'),
|
||
'par_courriel' => array('courriel', 'e-mail', 'email', 'mail'),
|
||
'par_telephone1' => array('telephone', 'téléphone', 'phone', 'mobile', 'cell', 'tel'),
|
||
'par_contact_urgence_nom' => array('emergency contact name', 'contact urgence', 'nom en cas d\'urgence', 'urgence nom'),
|
||
'par_contact_urgence_telephone' => array('emergency contact number', 'emergency contact phone', 'numéro du contact en cas d\'urgence', 'urgence tel'),
|
||
'par_adresse' => array('adresse', 'address', 'street'),
|
||
'par_ville' => array('ville', 'city', 'town'),
|
||
'pro_id' => array(
|
||
'region / province / state', 'région / province / state', 'province / state',
|
||
'province', 'state', 'region', 'région', 'etat', 'état',
|
||
),
|
||
'par_codepostal' => array('code postal', 'postal', 'zipcode', 'zip'),
|
||
'pay_id' => array('pays', 'country', 'nationalite', 'nationalité', 'nationality', 'nation'),
|
||
'no_bib' => array('no dossard', 'bib number', 'dossard', 'no bib', 'bib#', 'bib'),
|
||
'epr' => array(
|
||
'ticket type', 'registration type', 'reg choice', 'event race',
|
||
'epreuve', 'épreuve', 'distance', 'race', 'course', 'ticket',
|
||
),
|
||
'par_nom_equipe' => array('team name', 'nom equipe', 'nom équipe', 'equipe', 'équipe', 'team'),
|
||
'par_no_equipe' => array('team number', 'team no', 'no equipe', 'no équipe', 'team id'),
|
||
);
|
||
|
||
// 1) égalité exacte (normalisée + folded)
|
||
foreach ($tabRules as $strKey => $tabNeedles) {
|
||
foreach ($tabNeedles as $strNeedle) {
|
||
if ($str === $strNeedle || $strFold === fxImportResultatsFoldKey($strNeedle)) {
|
||
return $strKey;
|
||
}
|
||
}
|
||
}
|
||
|
||
// MSIN-4520 — en-têtes bilingues région/pays : ne pas bloquer sur « / »
|
||
$blnGeoHeader = (bool)preg_match('/\b(province|state|region|region|pays|country|nationalit)/u', $strFold);
|
||
|
||
// Questions longues bilingues → pas de guess champ standard (resteront en step questions)
|
||
if (!$blnGeoHeader && (mb_strlen($str, 'UTF-8') > 40 || strpos($str, '?') !== false || strpos($str, ' / ') !== false)) {
|
||
// Exceptions courtes déjà couvertes en exact ; ici on laisse vide
|
||
// sauf préfixes connus très spécifiques
|
||
if (strpos($str, 'attendee') === 0) {
|
||
return 'par_external_id';
|
||
}
|
||
return '';
|
||
}
|
||
|
||
// 2) contient — aiguilles les plus longues d'abord (sur forme folded)
|
||
$tabCandidates = array();
|
||
foreach ($tabRules as $strKey => $tabNeedles) {
|
||
foreach ($tabNeedles as $strNeedle) {
|
||
if ($strNeedle === '') {
|
||
continue;
|
||
}
|
||
$strNeedleFold = fxImportResultatsFoldKey($strNeedle);
|
||
// aiguilles 1–3 lettres : mot entier seulement
|
||
if (mb_strlen($strNeedleFold, 'UTF-8') <= 3) {
|
||
if (preg_match('/(^|\\s)' . preg_quote($strNeedleFold, '/') . '(\\s|$)/u', $strFold)) {
|
||
$tabCandidates[] = array('key' => $strKey, 'len' => mb_strlen($strNeedleFold, 'UTF-8'));
|
||
}
|
||
continue;
|
||
}
|
||
if (strpos($strFold, $strNeedleFold) !== false) {
|
||
$tabCandidates[] = array('key' => $strKey, 'len' => mb_strlen($strNeedleFold, 'UTF-8'));
|
||
}
|
||
}
|
||
}
|
||
if (count($tabCandidates) < 1) {
|
||
return '';
|
||
}
|
||
usort($tabCandidates, function ($a, $b) {
|
||
return $b['len'] - $a['len'];
|
||
});
|
||
return $tabCandidates[0]['key'];
|
||
}
|
||
|
||
/**
|
||
* Clé de partage templates entre événements du même org (pas de com_id sur evenements).
|
||
* MSIN-4482 — stockée dans inscriptions_import_templates.com_id (legacy name = org_id).
|
||
*/
|
||
function fxImportResultatsComIdForEve($intEveId) {
|
||
global $objDatabase;
|
||
$intEveId = intval($intEveId);
|
||
if ($intEveId <= 0) {
|
||
return 0;
|
||
}
|
||
$sql = "SELECT org_id FROM inscriptions_evenements WHERE eve_id = " . $intEveId . " LIMIT 1";
|
||
$tab = $objDatabase->fxGetResults($sql);
|
||
if (!is_array($tab) || empty($tab[1]['org_id'])) {
|
||
return 0;
|
||
}
|
||
return intval($tab[1]['org_id']);
|
||
}
|
||
|
||
function fxImportResultatsEpreuvesList($intEveId) {
|
||
global $objDatabase;
|
||
$intEveId = intval($intEveId);
|
||
$sql = "SELECT epr_id, epr_nom_fr, epr_type_fr, epr_date"
|
||
. " FROM inscriptions_epreuves WHERE eve_id = " . $intEveId
|
||
. " ORDER BY epr_date, epr_nom_fr";
|
||
$tab = $objDatabase->fxGetResults($sql);
|
||
$tabOut = array();
|
||
if (!is_array($tab)) {
|
||
return $tabOut;
|
||
}
|
||
for ($i = 1; $i <= count($tab); $i++) {
|
||
$strLabel = trim((string)($tab[$i]['epr_type_fr'] ?? ''));
|
||
$strNom = trim((string)($tab[$i]['epr_nom_fr'] ?? ''));
|
||
if ($strLabel !== '' && $strNom !== '') {
|
||
$strLabel .= ' — ' . $strNom;
|
||
} elseif ($strNom !== '') {
|
||
$strLabel = $strNom;
|
||
} else {
|
||
$strLabel = 'Épreuve #' . intval($tab[$i]['epr_id']);
|
||
}
|
||
$tabOut[] = array(
|
||
'epr_id' => intval($tab[$i]['epr_id']),
|
||
'label' => $strLabel,
|
||
'epr_nom_fr' => $strNom,
|
||
'epr_type_fr' => trim((string)($tab[$i]['epr_type_fr'] ?? '')),
|
||
);
|
||
}
|
||
return $tabOut;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4520 — suggestion epr_id pour une valeur fichier / nom d'onglet.
|
||
*/
|
||
function fxImportResultatsGuessEprId($strValue, array $tabEpreuves) {
|
||
$strVal = fxImportResultatsNormalizeHeader($strValue);
|
||
if ($strVal === '' || count($tabEpreuves) < 1) {
|
||
return 0;
|
||
}
|
||
$intBest = 0;
|
||
$intBestScore = 0;
|
||
foreach ($tabEpreuves as $arrE) {
|
||
$intEpr = intval($arrE['epr_id'] ?? 0);
|
||
if ($intEpr <= 0) {
|
||
continue;
|
||
}
|
||
$tabCands = array(
|
||
fxImportResultatsNormalizeHeader($arrE['label'] ?? ''),
|
||
fxImportResultatsNormalizeHeader($arrE['epr_nom_fr'] ?? ''),
|
||
fxImportResultatsNormalizeHeader($arrE['epr_type_fr'] ?? ''),
|
||
);
|
||
foreach ($tabCands as $strCand) {
|
||
if ($strCand === '') {
|
||
continue;
|
||
}
|
||
$intScore = 0;
|
||
if ($strCand === $strVal) {
|
||
$intScore = 100;
|
||
} elseif (strpos($strCand, $strVal) !== false || strpos($strVal, $strCand) !== false) {
|
||
$intScore = 80;
|
||
} elseif (preg_match('/(\d+)\s*k/u', $strVal, $m1) && preg_match('/(\d+)\s*k/u', $strCand, $m2)
|
||
&& $m1[1] === $m2[1]) {
|
||
$intScore = 70;
|
||
}
|
||
if ($intScore > $intBestScore) {
|
||
$intBestScore = $intScore;
|
||
$intBest = $intEpr;
|
||
}
|
||
}
|
||
}
|
||
return ($intBestScore >= 70) ? $intBest : 0;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4520 — liste pays pour dropdowns mapping import.
|
||
* @return array<int,array{pay_id:int,label:string}>
|
||
*/
|
||
function fxImportResultatsPaysList() {
|
||
global $objDatabase;
|
||
static $tabOut = null;
|
||
if ($tabOut !== null) {
|
||
return $tabOut;
|
||
}
|
||
$tabOut = array();
|
||
$sql = "SELECT pay_id, pay_nom_fr, pay_iso FROM inscriptions_pays"
|
||
. " WHERE pay_actif = 1 ORDER BY pay_trie, pay_nom_fr";
|
||
$tab = $objDatabase->fxGetResults($sql);
|
||
if (!is_array($tab)) {
|
||
return $tabOut;
|
||
}
|
||
for ($i = 1; $i <= count($tab); $i++) {
|
||
$intId = intval($tab[$i]['pay_id'] ?? 0);
|
||
if ($intId <= 0) {
|
||
continue;
|
||
}
|
||
$strLabel = trim((string)($tab[$i]['pay_nom_fr'] ?? ''));
|
||
$strIso = trim((string)($tab[$i]['pay_iso'] ?? ''));
|
||
if ($strIso !== '') {
|
||
$strLabel = ($strLabel !== '' ? ($strLabel . ' (' . $strIso . ')') : $strIso);
|
||
}
|
||
if ($strLabel === '') {
|
||
$strLabel = 'Pays #' . $intId;
|
||
}
|
||
$tabOut[] = array('pay_id' => $intId, 'label' => $strLabel);
|
||
}
|
||
return $tabOut;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4520 — liste provinces pour dropdowns mapping import.
|
||
* @return array<int,array{pro_id:int,label:string}>
|
||
*/
|
||
function fxImportResultatsProvincesList() {
|
||
global $objDatabase;
|
||
static $tabOut = null;
|
||
if ($tabOut !== null) {
|
||
return $tabOut;
|
||
}
|
||
$tabOut = array();
|
||
$sql = "SELECT pro_id, pro_nom_fr, pro_iso FROM inscriptions_provinces"
|
||
. " WHERE pro_actif = 1 ORDER BY pro_trie, pro_nom_fr";
|
||
$tab = $objDatabase->fxGetResults($sql);
|
||
if (!is_array($tab)) {
|
||
return $tabOut;
|
||
}
|
||
for ($i = 1; $i <= count($tab); $i++) {
|
||
$intId = intval($tab[$i]['pro_id'] ?? 0);
|
||
if ($intId <= 0) {
|
||
continue;
|
||
}
|
||
$strLabel = trim((string)($tab[$i]['pro_nom_fr'] ?? ''));
|
||
$strIso = trim((string)($tab[$i]['pro_iso'] ?? ''));
|
||
if ($strIso !== '') {
|
||
$strLabel = ($strLabel !== '' ? ($strLabel . ' (' . $strIso . ')') : $strIso);
|
||
}
|
||
if ($strLabel === '') {
|
||
$strLabel = 'Province #' . $intId;
|
||
}
|
||
$tabOut[] = array('pro_id' => $intId, 'label' => $strLabel);
|
||
}
|
||
return $tabOut;
|
||
}
|
||
|
||
function fxImportResultatsQuestionsList($intEveId) {
|
||
global $objDatabase;
|
||
$intEveId = intval($intEveId);
|
||
// que_rapport_label_fr peut manquer sur vieux schémas — fallback question seule
|
||
$sql = "SELECT que_id, que_question_fr, que_actif"
|
||
. " FROM inscriptions_questions WHERE eve_id = " . $intEveId
|
||
. " AND que_actif = 1 ORDER BY que_tri, que_id";
|
||
$tab = $objDatabase->fxGetResults($sql);
|
||
$tabOut = array();
|
||
if (!is_array($tab)) {
|
||
return $tabOut;
|
||
}
|
||
for ($i = 1; $i <= count($tab); $i++) {
|
||
$strLabel = trim((string)($tab[$i]['que_question_fr'] ?? ''));
|
||
$tabOut[] = array(
|
||
'que_id' => intval($tab[$i]['que_id']),
|
||
'label' => $strLabel !== '' ? $strLabel : ('Question #' . intval($tab[$i]['que_id'])),
|
||
);
|
||
}
|
||
return $tabOut;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4520 — chemin du meta JSON collé au fichier upload.
|
||
*/
|
||
function fxImportResultatsMetaPath($strPath) {
|
||
return (string)$strPath . '.meta.json';
|
||
}
|
||
|
||
/**
|
||
* MSIN-4520 — lit le meta d'un upload (sheet_mode, onglets choisis…).
|
||
* @return array<string,mixed>
|
||
*/
|
||
function fxImportResultatsLoadMeta($strPath) {
|
||
$strMeta = fxImportResultatsMetaPath($strPath);
|
||
if (!is_file($strMeta)) {
|
||
return array();
|
||
}
|
||
$arr = json_decode((string)@file_get_contents($strMeta), true);
|
||
return is_array($arr) ? $arr : array();
|
||
}
|
||
|
||
/**
|
||
* MSIN-4520 — écrit / fusionne le meta JSON.
|
||
* @param array<string,mixed> $arrMeta
|
||
*/
|
||
function fxImportResultatsSaveMeta($strPath, array $arrMeta) {
|
||
@file_put_contents(fxImportResultatsMetaPath($strPath), json_encode($arrMeta, JSON_UNESCAPED_UNICODE));
|
||
}
|
||
|
||
/**
|
||
* Lit CSV ou Excel → en-têtes + lignes (tableaux indexés 0..n).
|
||
* MSIN-4520 — Excel multi-onglets : options lues depuis le meta si absentes.
|
||
* @param array|null $arrOptions { sheet_mode?: string, sheet?: string, sheets?: string[] }
|
||
* @return array{state:string,message?:string,headers?:array,rows?:array}
|
||
*/
|
||
function fxImportResultatsParseFile($strPath, $strOriginalName = '', $arrOptions = null) {
|
||
$strExt = strtolower(pathinfo($strOriginalName !== '' ? $strOriginalName : $strPath, PATHINFO_EXTENSION));
|
||
if ($strExt === 'csv' || $strExt === 'txt') {
|
||
return fxImportResultatsParseCsv($strPath);
|
||
}
|
||
if (in_array($strExt, array('xls', 'xlsx'), true)) {
|
||
if (!is_array($arrOptions)) {
|
||
$arrMeta = fxImportResultatsLoadMeta($strPath);
|
||
$arrOptions = array(
|
||
'sheet_mode' => (string)($arrMeta['sheet_mode'] ?? ''),
|
||
'sheet' => (string)($arrMeta['sheet'] ?? ''),
|
||
'sheets' => isset($arrMeta['sheets']) && is_array($arrMeta['sheets']) ? $arrMeta['sheets'] : array(),
|
||
);
|
||
}
|
||
return fxImportResultatsParseExcel($strPath, $arrOptions);
|
||
}
|
||
// Sniff CSV
|
||
$strSample = @file_get_contents($strPath, false, null, 0, 2048);
|
||
if (is_string($strSample) && (strpos($strSample, ',') !== false || strpos($strSample, ';') !== false)) {
|
||
return fxImportResultatsParseCsv($strPath);
|
||
}
|
||
return array('state' => 'error', 'message' => 'Format non supporté (CSV, XLS, XLSX).');
|
||
}
|
||
|
||
function fxImportResultatsDetectCsvDelimiter($strPath) {
|
||
$strLine = '';
|
||
$fp = @fopen($strPath, 'rb');
|
||
if ($fp) {
|
||
$strLine = (string)fgets($fp);
|
||
fclose($fp);
|
||
}
|
||
$intSemi = substr_count($strLine, ';');
|
||
$intComma = substr_count($strLine, ',');
|
||
$intTab = substr_count($strLine, "\t");
|
||
if ($intTab > $intSemi && $intTab > $intComma) {
|
||
return "\t";
|
||
}
|
||
return ($intSemi > $intComma) ? ';' : ',';
|
||
}
|
||
|
||
function fxImportResultatsParseCsv($strPath) {
|
||
$strDelim = fxImportResultatsDetectCsvDelimiter($strPath);
|
||
$fp = @fopen($strPath, 'rb');
|
||
if (!$fp) {
|
||
return array('state' => 'error', 'message' => 'Impossible de lire le fichier.');
|
||
}
|
||
// BOM UTF-8
|
||
$strBom = fread($fp, 3);
|
||
if ($strBom !== "\xEF\xBB\xBF") {
|
||
rewind($fp);
|
||
}
|
||
$tabHeaders = fgetcsv($fp, 0, $strDelim);
|
||
if (!is_array($tabHeaders) || count($tabHeaders) < 1) {
|
||
fclose($fp);
|
||
return array('state' => 'error', 'message' => 'En-têtes manquants.');
|
||
}
|
||
$tabHeaders = array_map('trim', $tabHeaders);
|
||
$tabRows = array();
|
||
while (($tabRow = fgetcsv($fp, 0, $strDelim)) !== false) {
|
||
if (!is_array($tabRow)) {
|
||
continue;
|
||
}
|
||
// Ignorer lignes totalement vides
|
||
$blnEmpty = true;
|
||
foreach ($tabRow as $strCell) {
|
||
if (trim((string)$strCell) !== '') {
|
||
$blnEmpty = false;
|
||
break;
|
||
}
|
||
}
|
||
if ($blnEmpty) {
|
||
continue;
|
||
}
|
||
// Aligner largeur
|
||
while (count($tabRow) < count($tabHeaders)) {
|
||
$tabRow[] = '';
|
||
}
|
||
if (count($tabRow) > count($tabHeaders)) {
|
||
$tabRow = array_slice($tabRow, 0, count($tabHeaders));
|
||
}
|
||
$tabRows[] = $tabRow;
|
||
}
|
||
fclose($fp);
|
||
return array('state' => 'ok', 'headers' => $tabHeaders, 'rows' => $tabRows);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4520 — charge PHPExcel (chemin superadm ou php/).
|
||
* @return string|null message erreur ou null si OK
|
||
*/
|
||
function fxImportResultatsRequirePhpExcel() {
|
||
static $blnLoaded = false;
|
||
if ($blnLoaded) {
|
||
return null;
|
||
}
|
||
$strPhpExcel = dirname(__FILE__) . '/../superadm/php/Classes/PHPExcel.php';
|
||
if (!is_file($strPhpExcel)) {
|
||
$strPhpExcel = dirname(__FILE__) . '/Classes/PHPExcel.php';
|
||
}
|
||
if (!is_file($strPhpExcel)) {
|
||
return 'Lecteur Excel indisponible — utilisez un CSV.';
|
||
}
|
||
require_once $strPhpExcel;
|
||
$blnLoaded = true;
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4520 — nom d'onglet = feuille « résumé / détail / tous » (à exclure par défaut).
|
||
*/
|
||
function fxImportResultatsSheetNameIsSummary($strName) {
|
||
$str = fxImportResultatsNormalizeHeader($strName);
|
||
if ($str === 'tous' || $str === 'all' || $str === 'total' || $str === 'summary' || $str === 'resume' || $str === 'résumé') {
|
||
return true;
|
||
}
|
||
if (preg_match('/\b(detail|détail|détails|details|resume|résumé|summary)\b/u', $str)) {
|
||
return true;
|
||
}
|
||
if (preg_match('/[-–—]\s*(detail|détail)/u', $str)) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4520 — la 1re ligne ressemble-t-elle à des en-têtes participants ?
|
||
*/
|
||
function fxImportResultatsSheetRawLooksLikeData(array $tabRaw) {
|
||
if (count($tabRaw) < 2 || !is_array($tabRaw[0])) {
|
||
return false;
|
||
}
|
||
$tabHeaders = array();
|
||
foreach ($tabRaw[0] as $v) {
|
||
$str = trim((string)$v);
|
||
if ($str !== '') {
|
||
$tabHeaders[] = $str;
|
||
}
|
||
}
|
||
if (count($tabHeaders) < 4) {
|
||
return false;
|
||
}
|
||
$strFirst = fxImportResultatsNormalizeHeader($tabHeaders[0]);
|
||
if ($strFirst === 'total' || $strFirst === 'vague' || $strFirst === 'count' || $strFirst === 'nb') {
|
||
return false;
|
||
}
|
||
$intHits = 0;
|
||
foreach ($tabHeaders as $strH) {
|
||
if (fxImportResultatsGuessField($strH) !== '') {
|
||
$intHits++;
|
||
}
|
||
}
|
||
return $intHits >= 2;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4520 — inventaire léger des onglets d'un .xlsx via Zip (sans PHPExcel).
|
||
* Évite 503 (qos / timeout) sur gros multi-onglets : plus de load+toArray de tout le classeur.
|
||
* @return array{state:string,message?:string,sheets?:array,sheet_count?:int}
|
||
*/
|
||
function fxImportResultatsListExcelSheetsXlsxZip($strPath) {
|
||
if (!class_exists('ZipArchive')) {
|
||
return array('state' => 'error', 'message' => 'ZipArchive indisponible');
|
||
}
|
||
$zip = new ZipArchive();
|
||
if ($zip->open($strPath) !== true) {
|
||
return array('state' => 'error', 'message' => 'Impossible d\'ouvrir le fichier xlsx');
|
||
}
|
||
$strWb = $zip->getFromName('xl/workbook.xml');
|
||
$zip->close();
|
||
if ($strWb === false || $strWb === '') {
|
||
return array('state' => 'error', 'message' => 'workbook.xml manquant dans le xlsx');
|
||
}
|
||
|
||
$tabNames = array();
|
||
$objXml = @simplexml_load_string($strWb);
|
||
if ($objXml instanceof SimpleXMLElement) {
|
||
$objXml->registerXPathNamespace('m', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
|
||
$tabNodes = $objXml->xpath('//m:sheets/m:sheet');
|
||
if (is_array($tabNodes)) {
|
||
foreach ($tabNodes as $objSheet) {
|
||
$arrAttr = $objSheet->attributes();
|
||
$strName = isset($arrAttr['name']) ? trim((string)$arrAttr['name']) : '';
|
||
if ($strName !== '') {
|
||
$tabNames[] = $strName;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (count($tabNames) < 1
|
||
&& preg_match_all('/<sheet\b[^>]*\bname="([^"]+)"/i', $strWb, $tabM)
|
||
) {
|
||
foreach ($tabM[1] as $strName) {
|
||
$strName = html_entity_decode(trim((string)$strName), ENT_QUOTES | ENT_XML1, 'UTF-8');
|
||
if ($strName !== '') {
|
||
$tabNames[] = $strName;
|
||
}
|
||
}
|
||
}
|
||
if (count($tabNames) < 1) {
|
||
return array('state' => 'error', 'message' => 'Aucun onglet trouvé dans le xlsx');
|
||
}
|
||
|
||
$tabSheets = array();
|
||
foreach ($tabNames as $i => $strName) {
|
||
$blnSummary = fxImportResultatsSheetNameIsSummary($strName);
|
||
$blnData = !$blnSummary;
|
||
$tabSheets[] = array(
|
||
'name' => $strName,
|
||
'index' => $i,
|
||
'row_count' => null,
|
||
'looks_like_data' => $blnData,
|
||
'is_summary' => $blnSummary,
|
||
'suggested' => ($blnData && !$blnSummary),
|
||
);
|
||
}
|
||
return array(
|
||
'state' => 'ok',
|
||
'sheets' => $tabSheets,
|
||
'sheet_count' => count($tabSheets),
|
||
'list_mode' => 'xlsx_zip',
|
||
);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4520 — échantillon des N premières lignes (évite toArray complet).
|
||
* @return array
|
||
*/
|
||
function fxImportResultatsSheetSampleRows($objSheet, $intMaxRows = 25) {
|
||
$tabRaw = array();
|
||
$intMaxRows = max(2, intval($intMaxRows));
|
||
try {
|
||
$objRows = $objSheet->getRowIterator(1, $intMaxRows);
|
||
foreach ($objRows as $objRow) {
|
||
$tabLine = array();
|
||
$objCells = $objRow->getCellIterator();
|
||
$objCells->setIterateOnlyExistingCells(false);
|
||
$intCol = 0;
|
||
foreach ($objCells as $objCell) {
|
||
if ($intCol >= 80) {
|
||
break;
|
||
}
|
||
$tabLine[] = $objCell->getCalculatedValue();
|
||
$intCol++;
|
||
}
|
||
$tabRaw[] = $tabLine;
|
||
}
|
||
} catch (Exception $ex) {
|
||
return array();
|
||
}
|
||
return $tabRaw;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4520 — inventaire des onglets Excel (sans tout fusionner).
|
||
* xlsx : Zip léger d'abord ; PHPExcel seulement en secours / .xls.
|
||
* @return array{state:string,message?:string,sheets?:array}
|
||
*/
|
||
function fxImportResultatsListExcelSheets($strPath) {
|
||
@set_time_limit(120);
|
||
$strPath = (string)$strPath;
|
||
$strExt = strtolower(pathinfo($strPath, PATHINFO_EXTENSION));
|
||
$strZipMsg = '';
|
||
|
||
if ($strExt === 'xlsx') {
|
||
$arrZip = fxImportResultatsListExcelSheetsXlsxZip($strPath);
|
||
if (($arrZip['state'] ?? '') === 'ok') {
|
||
return $arrZip;
|
||
}
|
||
$strZipMsg = (string)($arrZip['message'] ?? '');
|
||
}
|
||
|
||
$strErr = fxImportResultatsRequirePhpExcel();
|
||
if ($strErr !== null) {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => $strErr . ($strZipMsg !== '' ? (' / ' . $strZipMsg) : ''),
|
||
);
|
||
}
|
||
try {
|
||
$objReader = PHPExcel_IOFactory::createReaderForFile($strPath);
|
||
if (method_exists($objReader, 'setReadDataOnly')) {
|
||
$objReader->setReadDataOnly(true);
|
||
}
|
||
$objBook = $objReader->load($strPath);
|
||
} catch (Exception $ex) {
|
||
return array('state' => 'error', 'message' => 'Lecture Excel échouée : ' . $ex->getMessage());
|
||
}
|
||
$tabSheets = array();
|
||
$intNb = $objBook->getSheetCount();
|
||
for ($i = 0; $i < $intNb; $i++) {
|
||
$objSheet = $objBook->getSheet($i);
|
||
$strName = trim((string)$objSheet->getTitle());
|
||
$tabRaw = fxImportResultatsSheetSampleRows($objSheet, 25);
|
||
if (!is_array($tabRaw)) {
|
||
$tabRaw = array();
|
||
}
|
||
$blnData = fxImportResultatsSheetRawLooksLikeData($tabRaw);
|
||
$blnSummary = fxImportResultatsSheetNameIsSummary($strName);
|
||
$tabSheets[] = array(
|
||
'name' => $strName,
|
||
'index' => $i,
|
||
'row_count' => null,
|
||
'looks_like_data' => $blnData,
|
||
'is_summary' => $blnSummary,
|
||
'suggested' => ($blnData && !$blnSummary),
|
||
);
|
||
}
|
||
return array(
|
||
'state' => 'ok',
|
||
'sheets' => $tabSheets,
|
||
'sheet_count' => count($tabSheets),
|
||
'list_mode' => 'phpexcel_sample',
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Convertit toArray d'une feuille → headers + rows.
|
||
* @return array{state:string,message?:string,headers?:array,rows?:array}
|
||
*/
|
||
function fxImportResultatsSheetRawToRows(array $tabRaw) {
|
||
if (!is_array($tabRaw) || count($tabRaw) < 1) {
|
||
return array('state' => 'error', 'message' => 'Feuille Excel vide.');
|
||
}
|
||
$tabHeaders = array_map(function ($v) {
|
||
return trim((string)$v);
|
||
}, $tabRaw[0]);
|
||
while (count($tabHeaders) > 0 && end($tabHeaders) === '') {
|
||
array_pop($tabHeaders);
|
||
}
|
||
if (count($tabHeaders) < 1) {
|
||
return array('state' => 'error', 'message' => 'En-têtes manquants.');
|
||
}
|
||
$tabRows = array();
|
||
for ($r = 1; $r < count($tabRaw); $r++) {
|
||
$tabRow = $tabRaw[$r];
|
||
if (!is_array($tabRow)) {
|
||
continue;
|
||
}
|
||
$tabRow = array_slice($tabRow, 0, count($tabHeaders));
|
||
while (count($tabRow) < count($tabHeaders)) {
|
||
$tabRow[] = '';
|
||
}
|
||
$blnEmpty = true;
|
||
foreach ($tabRow as $strCell) {
|
||
if (trim((string)$strCell) !== '') {
|
||
$blnEmpty = false;
|
||
break;
|
||
}
|
||
}
|
||
if ($blnEmpty) {
|
||
continue;
|
||
}
|
||
$tabRows[] = array_map(function ($v) {
|
||
return trim((string)$v);
|
||
}, $tabRow);
|
||
}
|
||
return array('state' => 'ok', 'headers' => $tabHeaders, 'rows' => $tabRows);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4520 — fusionne plusieurs feuilles ; 1re colonne synthétique = nom d'onglet (→ épreuve).
|
||
* @param array<int,array{name:string,headers:array,rows:array}> $tabParts
|
||
* @return array{state:string,message?:string,headers?:array,rows?:array}
|
||
*/
|
||
function fxImportResultatsMergeSheetsAsEpr(array $tabParts) {
|
||
if (count($tabParts) < 1) {
|
||
return array('state' => 'error', 'message' => 'Aucun onglet sélectionné.');
|
||
}
|
||
$strOngletHeader = 'Onglet (épreuve)';
|
||
$tabUnion = array($strOngletHeader);
|
||
$tabSeen = array(fxImportResultatsNormalizeHeader($strOngletHeader) => true);
|
||
foreach ($tabParts as $arrPart) {
|
||
foreach ($arrPart['headers'] as $strH) {
|
||
$strH = trim((string)$strH);
|
||
if ($strH === '') {
|
||
continue;
|
||
}
|
||
$strKey = fxImportResultatsNormalizeHeader($strH);
|
||
if (isset($tabSeen[$strKey])) {
|
||
continue;
|
||
}
|
||
$tabSeen[$strKey] = true;
|
||
$tabUnion[] = $strH;
|
||
}
|
||
}
|
||
$tabRows = array();
|
||
foreach ($tabParts as $arrPart) {
|
||
$strSheet = (string)$arrPart['name'];
|
||
$tabMapIdx = array();
|
||
foreach ($arrPart['headers'] as $intCi => $strH) {
|
||
$tabMapIdx[fxImportResultatsNormalizeHeader($strH)] = intval($intCi);
|
||
}
|
||
foreach ($arrPart['rows'] as $tabRow) {
|
||
$tabOut = array();
|
||
foreach ($tabUnion as $intUi => $strUH) {
|
||
if ($intUi === 0) {
|
||
$tabOut[] = $strSheet;
|
||
continue;
|
||
}
|
||
$strKey = fxImportResultatsNormalizeHeader($strUH);
|
||
$intSrc = isset($tabMapIdx[$strKey]) ? $tabMapIdx[$strKey] : -1;
|
||
$tabOut[] = ($intSrc >= 0) ? trim((string)($tabRow[$intSrc] ?? '')) : '';
|
||
}
|
||
$tabRows[] = $tabOut;
|
||
}
|
||
}
|
||
if (count($tabRows) < 1) {
|
||
return array('state' => 'error', 'message' => 'Aucune ligne dans les onglets sélectionnés.');
|
||
}
|
||
return array(
|
||
'state' => 'ok',
|
||
'headers' => $tabUnion,
|
||
'rows' => $tabRows,
|
||
'sheet_mode' => 'sheets',
|
||
);
|
||
}
|
||
|
||
/**
|
||
* @param array $arrOptions { sheet_mode?: string, sheet?: string, sheets?: string[] }
|
||
* @return array{state:string,message?:string,headers?:array,rows?:array}
|
||
*/
|
||
function fxImportResultatsParseExcel($strPath, array $arrOptions = array()) {
|
||
@set_time_limit(300);
|
||
@ini_set('memory_limit', '512M');
|
||
$strErr = fxImportResultatsRequirePhpExcel();
|
||
if ($strErr !== null) {
|
||
return array('state' => 'error', 'message' => $strErr);
|
||
}
|
||
|
||
$strMode = (string)($arrOptions['sheet_mode'] ?? '');
|
||
$tabLoadOnly = null;
|
||
if ($strMode === 'sheets') {
|
||
$tabWant = isset($arrOptions['sheets']) && is_array($arrOptions['sheets']) ? $arrOptions['sheets'] : array();
|
||
$tabLoadOnly = array();
|
||
foreach ($tabWant as $strN) {
|
||
$strN = trim((string)$strN);
|
||
if ($strN !== '') {
|
||
$tabLoadOnly[] = $strN;
|
||
}
|
||
}
|
||
if (count($tabLoadOnly) < 1) {
|
||
return array('state' => 'error', 'message' => 'Sélectionnez au moins un onglet.');
|
||
}
|
||
} elseif ($strMode === 'column' || $strMode === '') {
|
||
$strSheetOpt = trim((string)($arrOptions['sheet'] ?? ''));
|
||
if ($strSheetOpt !== '') {
|
||
$tabLoadOnly = array($strSheetOpt);
|
||
}
|
||
}
|
||
|
||
try {
|
||
$objReader = PHPExcel_IOFactory::createReaderForFile($strPath);
|
||
if (method_exists($objReader, 'setReadDataOnly')) {
|
||
$objReader->setReadDataOnly(true);
|
||
}
|
||
// MSIN-4520 — ne charger que les onglets demandés (pas les 14 feuilles Boréalys)
|
||
if (is_array($tabLoadOnly) && count($tabLoadOnly) > 0 && method_exists($objReader, 'setLoadSheetsOnly')) {
|
||
$objReader->setLoadSheetsOnly($tabLoadOnly);
|
||
}
|
||
$objBook = $objReader->load($strPath);
|
||
} catch (Exception $ex) {
|
||
return array('state' => 'error', 'message' => 'Lecture Excel échouée : ' . $ex->getMessage());
|
||
}
|
||
|
||
if ($strMode === 'sheets') {
|
||
$tabWantNorm = array();
|
||
foreach ($tabLoadOnly as $strN) {
|
||
$tabWantNorm[$strN] = true;
|
||
}
|
||
$tabParts = array();
|
||
$intNb = $objBook->getSheetCount();
|
||
for ($i = 0; $i < $intNb; $i++) {
|
||
$objSheet = $objBook->getSheet($i);
|
||
$strName = trim((string)$objSheet->getTitle());
|
||
if (!isset($tabWantNorm[$strName])) {
|
||
continue;
|
||
}
|
||
$tabRaw = $objSheet->toArray(null, true, true, false);
|
||
if (!is_array($tabRaw)) {
|
||
continue;
|
||
}
|
||
$arrOne = fxImportResultatsSheetRawToRows($tabRaw);
|
||
if (($arrOne['state'] ?? '') !== 'ok') {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => 'Onglet « ' . $strName . ' » : ' . ($arrOne['message'] ?? 'lecture échouée'),
|
||
);
|
||
}
|
||
$tabParts[] = array(
|
||
'name' => $strName,
|
||
'headers' => $arrOne['headers'],
|
||
'rows' => $arrOne['rows'],
|
||
);
|
||
unset($tabWantNorm[$strName]);
|
||
}
|
||
if (count($tabWantNorm) > 0) {
|
||
$strMissing = implode(', ', array_keys($tabWantNorm));
|
||
return array('state' => 'error', 'message' => 'Onglet(s) introuvable(s) : ' . $strMissing);
|
||
}
|
||
return fxImportResultatsMergeSheetsAsEpr($tabParts);
|
||
}
|
||
|
||
// Mode colonne (défaut) : une feuille — nom demandé ou active
|
||
$strSheet = trim((string)($arrOptions['sheet'] ?? ''));
|
||
try {
|
||
if ($strSheet !== '') {
|
||
$objSheet = $objBook->getSheetByName($strSheet);
|
||
if ($objSheet === null) {
|
||
return array('state' => 'error', 'message' => 'Onglet « ' . $strSheet . ' » introuvable.');
|
||
}
|
||
} else {
|
||
$objSheet = $objBook->getActiveSheet();
|
||
}
|
||
$tabRaw = $objSheet->toArray(null, true, true, false);
|
||
} catch (Exception $ex) {
|
||
return array('state' => 'error', 'message' => 'Lecture Excel échouée : ' . $ex->getMessage());
|
||
}
|
||
if (!is_array($tabRaw)) {
|
||
return array('state' => 'error', 'message' => 'Fichier Excel vide.');
|
||
}
|
||
$arrParsed = fxImportResultatsSheetRawToRows($tabRaw);
|
||
if (($arrParsed['state'] ?? '') === 'ok') {
|
||
$arrParsed['sheet_mode'] = 'column';
|
||
$arrParsed['sheet'] = trim((string)$objSheet->getTitle());
|
||
}
|
||
return $arrParsed;
|
||
}
|
||
|
||
/**
|
||
* Valeurs distinctes par colonne (capées).
|
||
*/
|
||
function fxImportResultatsDistincts(array $tabRows, $intColCount, $intMaxPerCol = 40) {
|
||
$tabDistinct = array();
|
||
for ($c = 0; $c < $intColCount; $c++) {
|
||
$tabDistinct[$c] = array();
|
||
}
|
||
foreach ($tabRows as $tabRow) {
|
||
for ($c = 0; $c < $intColCount; $c++) {
|
||
$strVal = trim((string)($tabRow[$c] ?? ''));
|
||
if ($strVal === '') {
|
||
continue;
|
||
}
|
||
if (!isset($tabDistinct[$c][$strVal])) {
|
||
if (count($tabDistinct[$c]) >= $intMaxPerCol) {
|
||
continue;
|
||
}
|
||
$tabDistinct[$c][$strVal] = true;
|
||
}
|
||
}
|
||
}
|
||
$tabOut = array();
|
||
for ($c = 0; $c < $intColCount; $c++) {
|
||
$tabOut[$c] = array_keys($tabDistinct[$c]);
|
||
sort($tabOut[$c], SORT_NATURAL | SORT_FLAG_CASE);
|
||
}
|
||
return $tabOut;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4482 / MSIN-4520 — construit la réponse mapping après parse (upload ou choose_sheets).
|
||
* @param array $arrParsed headers+rows ok
|
||
* @param array $arrMeta meta déjà partiellement rempli
|
||
* @return array
|
||
*/
|
||
function fxImportResultatsBuildMappingPayload($intEveId, $strToken, $strDest, $strName, array $arrParsed, array $arrMeta = array()) {
|
||
$tabHeaders = $arrParsed['headers'];
|
||
$tabRows = $arrParsed['rows'];
|
||
$strSig = fxImportResultatsHeaderSig($tabHeaders);
|
||
$tabGuess = array();
|
||
foreach ($tabHeaders as $intHi => $strH) {
|
||
$strG = fxImportResultatsGuessField($strH);
|
||
// MSIN-4520 — colonne synthétique onglet = épreuve (priorité sur Competition)
|
||
if ($intHi === 0 && fxImportResultatsNormalizeHeader($strH) === fxImportResultatsNormalizeHeader('Onglet (épreuve)')) {
|
||
$strG = 'epr';
|
||
}
|
||
$tabGuess[] = $strG;
|
||
}
|
||
// Une seule colonne epr : si onglet déjà epr, retirer les autres guesses epr
|
||
$blnOngletEpr = isset($tabGuess[0]) && $tabGuess[0] === 'epr'
|
||
&& fxImportResultatsNormalizeHeader($tabHeaders[0] ?? '') === fxImportResultatsNormalizeHeader('Onglet (épreuve)');
|
||
if ($blnOngletEpr) {
|
||
for ($i = 1; $i < count($tabGuess); $i++) {
|
||
if ($tabGuess[$i] === 'epr') {
|
||
$tabGuess[$i] = '';
|
||
}
|
||
}
|
||
}
|
||
|
||
$tabIdxFilter = array();
|
||
foreach ($tabHeaders as $intHi => $strH) {
|
||
$strG = $tabGuess[$intHi] ?? fxImportResultatsGuessField($strH);
|
||
if ($strG !== '') {
|
||
$tabIdxFilter[$strG] = $intHi;
|
||
}
|
||
}
|
||
$tabRowsFiltered = array();
|
||
foreach ($tabRows as $tabRow) {
|
||
if (fxImportResultatsRowLooksLikeHeader($tabRow, $tabHeaders, $tabIdxFilter)) {
|
||
continue;
|
||
}
|
||
$tabRowsFiltered[] = $tabRow;
|
||
}
|
||
$tabRows = $tabRowsFiltered;
|
||
$tabSamples = array_slice($tabRows, 0, 8);
|
||
$tabDistinct = fxImportResultatsDistincts($tabRows, count($tabHeaders));
|
||
|
||
$arrMetaOut = array_merge($arrMeta, array(
|
||
'eve_id' => intval($intEveId),
|
||
'token' => $strToken,
|
||
'original_name' => $strName,
|
||
'path' => $strDest,
|
||
'header_sig' => $strSig,
|
||
'headers' => $tabHeaders,
|
||
'row_count' => count($tabRows),
|
||
'sheet_mode' => (string)($arrParsed['sheet_mode'] ?? ($arrMeta['sheet_mode'] ?? 'column')),
|
||
'created_at' => date('c'),
|
||
));
|
||
if (!empty($arrParsed['sheet'])) {
|
||
$arrMetaOut['sheet'] = (string)$arrParsed['sheet'];
|
||
}
|
||
if (!empty($arrParsed['sheets']) && is_array($arrParsed['sheets'])) {
|
||
$arrMetaOut['sheets'] = $arrParsed['sheets'];
|
||
}
|
||
fxImportResultatsSaveMeta($strDest, $arrMetaOut);
|
||
|
||
$tabTemplates = fxImportResultatsFindTemplates($intEveId, $strSig);
|
||
$tabEpreuves = fxImportResultatsEpreuvesList($intEveId);
|
||
|
||
// MSIN-4520 — pré-remplir epr / pays / province pour valeurs distinctes reconnues
|
||
$tabEprGuess = array();
|
||
$tabPayGuess = array();
|
||
$tabProGuess = array();
|
||
$intEprGuessCol = -1;
|
||
$intPayGuessCol = -1;
|
||
$intProGuessCol = -1;
|
||
foreach ($tabGuess as $intGi => $strGk) {
|
||
if ($strGk === 'epr' && $intEprGuessCol < 0) {
|
||
$intEprGuessCol = $intGi;
|
||
}
|
||
if ($strGk === 'pay_id' && $intPayGuessCol < 0) {
|
||
$intPayGuessCol = $intGi;
|
||
}
|
||
if ($strGk === 'pro_id' && $intProGuessCol < 0) {
|
||
$intProGuessCol = $intGi;
|
||
}
|
||
}
|
||
if ($intEprGuessCol >= 0 && !empty($tabDistinct[$intEprGuessCol])) {
|
||
foreach ($tabDistinct[$intEprGuessCol] as $strVal) {
|
||
$intG = fxImportResultatsGuessEprId($strVal, $tabEpreuves);
|
||
if ($intG > 0) {
|
||
$tabEprGuess[$strVal] = $intG;
|
||
}
|
||
}
|
||
}
|
||
if ($intPayGuessCol >= 0 && !empty($tabDistinct[$intPayGuessCol])) {
|
||
foreach ($tabDistinct[$intPayGuessCol] as $strVal) {
|
||
$intG = fxImportResultatsResolvePayId($strVal);
|
||
if ($intG > 0) {
|
||
$tabPayGuess[$strVal] = $intG;
|
||
}
|
||
}
|
||
}
|
||
if ($intProGuessCol >= 0 && !empty($tabDistinct[$intProGuessCol])) {
|
||
foreach ($tabDistinct[$intProGuessCol] as $strVal) {
|
||
$intG = fxImportResultatsResolveProId($strVal);
|
||
if ($intG > 0) {
|
||
$tabProGuess[$strVal] = $intG;
|
||
}
|
||
}
|
||
}
|
||
|
||
return array(
|
||
'state' => 'ok',
|
||
'token' => $strToken,
|
||
'original_name' => $strName,
|
||
'header_sig' => $strSig,
|
||
'headers' => $tabHeaders,
|
||
'guess' => $tabGuess,
|
||
'row_count' => count($tabRows),
|
||
'samples' => $tabSamples,
|
||
'distincts' => $tabDistinct,
|
||
'templates' => $tabTemplates,
|
||
'epreuves' => $tabEpreuves,
|
||
'pays' => fxImportResultatsPaysList(),
|
||
'provinces' => fxImportResultatsProvincesList(),
|
||
'questions' => fxImportResultatsQuestionsList($intEveId),
|
||
'fields' => fxImportResultatsTargetFields(),
|
||
'sheet_mode' => $arrMetaOut['sheet_mode'],
|
||
'sheet' => (string)($arrMetaOut['sheet'] ?? ''),
|
||
'sheets_used' => (isset($arrMetaOut['sheets']) && is_array($arrMetaOut['sheets'])) ? $arrMetaOut['sheets'] : array(),
|
||
'epr_guess' => $tabEprGuess,
|
||
'pay_guess' => $tabPayGuess,
|
||
'pro_guess' => $tabProGuess,
|
||
'needs_sheet_choice' => false,
|
||
);
|
||
}
|
||
|
||
function fxImportResultatsSaveUpload($intEveId, array $arrFile) {
|
||
@set_time_limit(180);
|
||
@ini_set('memory_limit', '512M');
|
||
$intEveId = intval($intEveId);
|
||
if ($intEveId <= 0) {
|
||
return array('state' => 'error', 'message' => 'eve_id manquant');
|
||
}
|
||
if (empty($arrFile['tmp_name']) || !is_uploaded_file($arrFile['tmp_name'])) {
|
||
return array('state' => 'error', 'message' => 'Aucun fichier reçu');
|
||
}
|
||
if (!empty($arrFile['error']) && intval($arrFile['error']) !== UPLOAD_ERR_OK) {
|
||
return array('state' => 'error', 'message' => 'Erreur upload (code ' . intval($arrFile['error']) . ')');
|
||
}
|
||
$strName = (string)($arrFile['name'] ?? 'import.csv');
|
||
$strToken = bin2hex(random_bytes(16));
|
||
$strSafeExt = strtolower(pathinfo($strName, PATHINFO_EXTENSION));
|
||
if (!in_array($strSafeExt, array('csv', 'txt', 'xls', 'xlsx'), true)) {
|
||
$strSafeExt = 'csv';
|
||
}
|
||
$strDest = fxImportResultatsUploadDir() . '/' . $intEveId . '_' . $strToken . '.' . $strSafeExt;
|
||
if (!@move_uploaded_file($arrFile['tmp_name'], $strDest)) {
|
||
return array('state' => 'error', 'message' => 'Échec enregistrement temporaire');
|
||
}
|
||
|
||
// MSIN-4520 — Excel multi-onglets : profil événement (onglets = épreuves) ou question
|
||
if (in_array($strSafeExt, array('xls', 'xlsx'), true)) {
|
||
$arrList = fxImportResultatsListExcelSheets($strDest);
|
||
if (($arrList['state'] ?? '') !== 'ok') {
|
||
@unlink($strDest);
|
||
return $arrList;
|
||
}
|
||
$tabSheets = $arrList['sheets'] ?? array();
|
||
if (count($tabSheets) > 1) {
|
||
$arrMeta = array(
|
||
'eve_id' => $intEveId,
|
||
'token' => $strToken,
|
||
'original_name' => $strName,
|
||
'path' => $strDest,
|
||
'sheet_mode' => '',
|
||
'needs_sheet_choice' => true,
|
||
'created_at' => date('c'),
|
||
);
|
||
fxImportResultatsSaveMeta($strDest, $arrMeta);
|
||
|
||
// Profil sauvé pour CET événement → skip la question, mapping prêt à importer
|
||
$arrProfile = fxImportResultatsFindEventImportProfile($intEveId);
|
||
$arrMapProf = is_array($arrProfile) ? ($arrProfile['mapping'] ?? array()) : array();
|
||
$strModeProf = (string)($arrMapProf['sheet_mode'] ?? '');
|
||
|
||
if ($strModeProf === 'sheets') {
|
||
$tabWant = isset($arrMapProf['sheets']) && is_array($arrMapProf['sheets']) ? $arrMapProf['sheets'] : array();
|
||
$tabUse = fxImportResultatsIntersectSheetNames($tabWant, $tabSheets);
|
||
// Ancien profil sans liste d'onglets : reprendre les suggestions fichier
|
||
if (count($tabUse) < 1) {
|
||
foreach ($tabSheets as $arrS) {
|
||
if (!empty($arrS['suggested']) && !empty($arrS['name'])) {
|
||
$tabUse[] = (string)$arrS['name'];
|
||
}
|
||
}
|
||
}
|
||
if (count($tabUse) >= 1) {
|
||
$arrAuto = fxImportResultatsChooseSheets($intEveId, $strToken, 'sheets', '', $tabUse);
|
||
if (($arrAuto['state'] ?? '') === 'ok') {
|
||
$arrAuto['auto_applied'] = true;
|
||
$arrAuto['applied_mapping'] = $arrMapProf;
|
||
$arrAuto['applied_template'] = array(
|
||
'tpl_id' => intval($arrProfile['tpl_id'] ?? 0),
|
||
'name' => (string)($arrProfile['name'] ?? ''),
|
||
'same_event' => true,
|
||
);
|
||
$arrAuto['message'] = 'Profil « '
|
||
. (string)($arrProfile['name'] ?? 'événement')
|
||
. ' » réappliqué (onglets = épreuves, ' . count($tabUse) . ' onglet(s)) — '
|
||
. intval($arrAuto['row_count'] ?? 0)
|
||
. ' ligne(s). Prêt à importer.';
|
||
return $arrAuto;
|
||
}
|
||
}
|
||
} elseif ($strModeProf === 'column') {
|
||
$strSheetProf = trim((string)($arrMapProf['sheet'] ?? ''));
|
||
$blnSheetOk = false;
|
||
if ($strSheetProf !== '') {
|
||
foreach ($tabSheets as $arrS) {
|
||
if ((string)($arrS['name'] ?? '') === $strSheetProf) {
|
||
$blnSheetOk = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if ($blnSheetOk) {
|
||
$arrAuto = fxImportResultatsChooseSheets($intEveId, $strToken, 'column', $strSheetProf, array());
|
||
if (($arrAuto['state'] ?? '') === 'ok') {
|
||
$arrAuto['auto_applied'] = true;
|
||
$arrAuto['applied_mapping'] = $arrMapProf;
|
||
$arrAuto['applied_template'] = array(
|
||
'tpl_id' => intval($arrProfile['tpl_id'] ?? 0),
|
||
'name' => (string)($arrProfile['name'] ?? ''),
|
||
'same_event' => true,
|
||
);
|
||
$arrAuto['message'] = 'Profil « '
|
||
. (string)($arrProfile['name'] ?? 'événement')
|
||
. ' » réappliqué (feuille « ' . $strSheetProf . ' ») — prêt à importer.';
|
||
return $arrAuto;
|
||
}
|
||
}
|
||
}
|
||
|
||
return array(
|
||
'state' => 'ok',
|
||
'token' => $strToken,
|
||
'original_name' => $strName,
|
||
'needs_sheet_choice' => true,
|
||
'sheet_count' => count($tabSheets),
|
||
'sheets' => $tabSheets,
|
||
'epreuves' => fxImportResultatsEpreuvesList($intEveId),
|
||
'event_profile' => $arrProfile,
|
||
'message' => 'Ce fichier a ' . count($tabSheets) . ' onglets. Les onglets doivent-ils servir à classer dans les épreuves ?',
|
||
);
|
||
}
|
||
}
|
||
|
||
$arrParsed = fxImportResultatsParseFile($strDest, $strName, array('sheet_mode' => 'column'));
|
||
if (($arrParsed['state'] ?? '') !== 'ok') {
|
||
@unlink($strDest);
|
||
@unlink(fxImportResultatsMetaPath($strDest));
|
||
return $arrParsed;
|
||
}
|
||
return fxImportResultatsBuildMappingPayload($intEveId, $strToken, $strDest, $strName, $arrParsed, array(
|
||
'sheet_mode' => 'column',
|
||
));
|
||
}
|
||
|
||
/**
|
||
* MSIN-4520 — après upload multi-onglets : mode colonne (1 feuille) ou onglets = épreuves.
|
||
* @param string $strMode column|sheets
|
||
* @param string $strSheet onglet unique (mode column)
|
||
* @param array $tabSheets noms d'onglets (mode sheets)
|
||
* @return array
|
||
*/
|
||
function fxImportResultatsChooseSheets($intEveId, $strToken, $strMode, $strSheet = '', array $tabSheets = array()) {
|
||
$intEveId = intval($intEveId);
|
||
$strPath = fxImportResultatsResolveTokenPath($intEveId, $strToken);
|
||
if ($strPath === null) {
|
||
return array('state' => 'error', 'message' => 'Fichier temporaire introuvable — réuploadez.');
|
||
}
|
||
$arrMeta = fxImportResultatsLoadMeta($strPath);
|
||
$strName = (string)($arrMeta['original_name'] ?? basename($strPath));
|
||
$strMode = trim((string)$strMode);
|
||
if ($strMode !== 'column' && $strMode !== 'sheets') {
|
||
return array('state' => 'error', 'message' => 'Choix d\'onglets invalide.');
|
||
}
|
||
|
||
if ($strMode === 'column') {
|
||
$strSheet = trim((string)$strSheet);
|
||
if ($strSheet === '') {
|
||
// Première feuille « data » suggérée, sinon active (vide = active PHPExcel)
|
||
$arrList = fxImportResultatsListExcelSheets($strPath);
|
||
if (($arrList['state'] ?? '') === 'ok') {
|
||
foreach ($arrList['sheets'] as $arrS) {
|
||
if (!empty($arrS['suggested'])) {
|
||
$strSheet = (string)$arrS['name'];
|
||
break;
|
||
}
|
||
}
|
||
if ($strSheet === '' && !empty($arrList['sheets'][0]['name'])) {
|
||
$strSheet = (string)$arrList['sheets'][0]['name'];
|
||
}
|
||
}
|
||
}
|
||
$arrOpts = array('sheet_mode' => 'column', 'sheet' => $strSheet);
|
||
$arrParsed = fxImportResultatsParseExcel($strPath, $arrOpts);
|
||
if (($arrParsed['state'] ?? '') !== 'ok') {
|
||
return $arrParsed;
|
||
}
|
||
return fxImportResultatsBuildMappingPayload($intEveId, $strToken, $strPath, $strName, $arrParsed, array(
|
||
'sheet_mode' => 'column',
|
||
'sheet' => $strSheet,
|
||
'sheets' => array(),
|
||
));
|
||
}
|
||
|
||
$tabClean = array();
|
||
foreach ($tabSheets as $strN) {
|
||
$strN = trim((string)$strN);
|
||
if ($strN !== '' && !in_array($strN, $tabClean, true)) {
|
||
$tabClean[] = $strN;
|
||
}
|
||
}
|
||
if (count($tabClean) < 1) {
|
||
return array('state' => 'error', 'message' => 'Cochez au moins un onglet à importer.');
|
||
}
|
||
$arrParsed = fxImportResultatsParseExcel($strPath, array(
|
||
'sheet_mode' => 'sheets',
|
||
'sheets' => $tabClean,
|
||
));
|
||
if (($arrParsed['state'] ?? '') !== 'ok') {
|
||
return $arrParsed;
|
||
}
|
||
$arrParsed['sheets'] = $tabClean;
|
||
return fxImportResultatsBuildMappingPayload($intEveId, $strToken, $strPath, $strName, $arrParsed, array(
|
||
'sheet_mode' => 'sheets',
|
||
'sheets' => $tabClean,
|
||
'sheet' => '',
|
||
));
|
||
}
|
||
|
||
function fxImportResultatsResolveTokenPath($intEveId, $strToken) {
|
||
$intEveId = intval($intEveId);
|
||
$strToken = preg_replace('/[^a-f0-9]/', '', strtolower((string)$strToken));
|
||
if ($intEveId <= 0 || strlen($strToken) < 16) {
|
||
return null;
|
||
}
|
||
$strDir = fxImportResultatsUploadDir();
|
||
$tabMatches = glob($strDir . '/' . $intEveId . '_' . $strToken . '.*');
|
||
if (!is_array($tabMatches)) {
|
||
return null;
|
||
}
|
||
foreach ($tabMatches as $strPath) {
|
||
if (substr($strPath, -10) === '.meta.json') {
|
||
continue;
|
||
}
|
||
if (is_file($strPath)) {
|
||
return $strPath;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function fxImportResultatsFindTemplates($intEveId, $strSig) {
|
||
global $objDatabase;
|
||
$intEveId = intval($intEveId);
|
||
$strSig = preg_replace('/[^a-f0-9]/', '', strtolower((string)$strSig));
|
||
$intOrgId = fxImportResultatsComIdForEve($intEveId);
|
||
// Table absente (SQL pas encore joué) → pas d'erreur HTML dans le JSON AJAX
|
||
$sqlCheck = "SELECT 1 AS ok FROM information_schema.TABLES"
|
||
. " WHERE TABLE_SCHEMA = DATABASE()"
|
||
. " AND TABLE_NAME = 'inscriptions_import_templates' LIMIT 1";
|
||
$tabCheck = $objDatabase->fxGetResults($sqlCheck);
|
||
if (!is_array($tabCheck) || empty($tabCheck[1]['ok'])) {
|
||
return array();
|
||
}
|
||
$sql = "SELECT tpl_id, eve_id, com_id, tpl_name, tpl_header_sig, tpl_mapping_json, tpl_maj"
|
||
. " FROM inscriptions_import_templates"
|
||
. " WHERE tpl_actif = 1 AND tpl_header_sig = '" . addslashes($strSig) . "'"
|
||
. " AND (eve_id = " . $intEveId
|
||
. ($intOrgId > 0 ? (" OR com_id = " . $intOrgId) : '')
|
||
. ")"
|
||
. " ORDER BY (eve_id = " . $intEveId . ") DESC, tpl_maj DESC LIMIT 20";
|
||
$tab = $objDatabase->fxGetResults($sql);
|
||
$tabOut = array();
|
||
if (!is_array($tab)) {
|
||
return $tabOut;
|
||
}
|
||
for ($i = 1; $i <= count($tab); $i++) {
|
||
$arrMap = json_decode((string)($tab[$i]['tpl_mapping_json'] ?? ''), true);
|
||
$tabOut[] = array(
|
||
'tpl_id' => intval($tab[$i]['tpl_id']),
|
||
'eve_id' => intval($tab[$i]['eve_id']),
|
||
'com_id' => intval($tab[$i]['com_id']),
|
||
'name' => (string)$tab[$i]['tpl_name'],
|
||
'maj' => (string)$tab[$i]['tpl_maj'],
|
||
'mapping' => is_array($arrMap) ? $arrMap : array(),
|
||
'same_event' => (intval($tab[$i]['eve_id']) === $intEveId),
|
||
);
|
||
}
|
||
return $tabOut;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4520 — dernier profil d'import propre à CET événement (hors empreinte colonnes).
|
||
* Sert à réappliquer « onglets = épreuves » dès le drop du prochain fichier.
|
||
* @return array|null
|
||
*/
|
||
function fxImportResultatsFindEventImportProfile($intEveId) {
|
||
global $objDatabase;
|
||
$intEveId = intval($intEveId);
|
||
if ($intEveId <= 0) {
|
||
return null;
|
||
}
|
||
$sqlCheck = "SELECT 1 AS ok FROM information_schema.TABLES"
|
||
. " WHERE TABLE_SCHEMA = DATABASE()"
|
||
. " AND TABLE_NAME = 'inscriptions_import_templates' LIMIT 1";
|
||
$tabCheck = $objDatabase->fxGetResults($sqlCheck);
|
||
if (!is_array($tabCheck) || empty($tabCheck[1]['ok'])) {
|
||
return null;
|
||
}
|
||
$sql = "SELECT tpl_id, eve_id, com_id, tpl_name, tpl_header_sig, tpl_mapping_json, tpl_maj"
|
||
. " FROM inscriptions_import_templates"
|
||
. " WHERE tpl_actif = 1 AND eve_id = " . $intEveId
|
||
. " ORDER BY tpl_maj DESC LIMIT 25";
|
||
$tab = $objDatabase->fxGetResults($sql);
|
||
if (!is_array($tab) || count($tab) < 1) {
|
||
return null;
|
||
}
|
||
$arrFallback = null;
|
||
for ($i = 1; $i <= count($tab); $i++) {
|
||
$arrMap = json_decode((string)($tab[$i]['tpl_mapping_json'] ?? ''), true);
|
||
if (!is_array($arrMap)) {
|
||
$arrMap = array();
|
||
}
|
||
$arrRow = array(
|
||
'tpl_id' => intval($tab[$i]['tpl_id']),
|
||
'eve_id' => intval($tab[$i]['eve_id']),
|
||
'com_id' => intval($tab[$i]['com_id']),
|
||
'name' => (string)$tab[$i]['tpl_name'],
|
||
'maj' => (string)$tab[$i]['tpl_maj'],
|
||
'mapping' => $arrMap,
|
||
'same_event' => true,
|
||
'sheet_mode' => (string)($arrMap['sheet_mode'] ?? ''),
|
||
);
|
||
if ($arrFallback === null) {
|
||
$arrFallback = $arrRow;
|
||
}
|
||
// Priorité au profil multi-onglets
|
||
if (($arrMap['sheet_mode'] ?? '') === 'sheets') {
|
||
return $arrRow;
|
||
}
|
||
}
|
||
return $arrFallback;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4520 — intersection noms d'onglets profil ↔ fichier actuel.
|
||
* @param array $tabWant noms sauvés dans le template
|
||
* @param array $tabFileSheets [{name,...}, ...]
|
||
* @return string[]
|
||
*/
|
||
function fxImportResultatsIntersectSheetNames(array $tabWant, array $tabFileSheets) {
|
||
$tabAvail = array();
|
||
foreach ($tabFileSheets as $arrS) {
|
||
$strN = trim((string)($arrS['name'] ?? ''));
|
||
if ($strN !== '') {
|
||
$tabAvail[$strN] = true;
|
||
}
|
||
}
|
||
$tabOut = array();
|
||
foreach ($tabWant as $strN) {
|
||
$strN = trim((string)$strN);
|
||
if ($strN !== '' && isset($tabAvail[$strN]) && !in_array($strN, $tabOut, true)) {
|
||
$tabOut[] = $strN;
|
||
}
|
||
}
|
||
return $tabOut;
|
||
}
|
||
|
||
/**
|
||
* @param array $arrMapping {
|
||
* columns: string[],
|
||
* epr_values: {value: epr_id},
|
||
* questions: {colIndex: que_id|"new"},
|
||
* sheet_mode?: string,
|
||
* sheets?: string[],
|
||
* sheet?: string
|
||
* }
|
||
*/
|
||
function fxImportResultatsSaveTemplate($intEveId, $strToken, $strName, array $arrMapping, $intUsaId, $blnShareCom = true) {
|
||
global $objDatabase;
|
||
|
||
$intEveId = intval($intEveId);
|
||
$strPath = fxImportResultatsResolveTokenPath($intEveId, $strToken);
|
||
if ($strPath === null) {
|
||
return array('state' => 'error', 'message' => 'Fichier temporaire introuvable — réuploadez.');
|
||
}
|
||
$arrMeta = fxImportResultatsLoadMeta($strPath);
|
||
$tabHeaders = isset($arrMeta['headers']) && is_array($arrMeta['headers']) ? $arrMeta['headers'] : array();
|
||
$strSig = isset($arrMeta['header_sig']) ? (string)$arrMeta['header_sig'] : fxImportResultatsHeaderSig($tabHeaders);
|
||
if ($tabHeaders === array()) {
|
||
$arrParsed = fxImportResultatsParseFile($strPath);
|
||
if (($arrParsed['state'] ?? '') !== 'ok') {
|
||
return $arrParsed;
|
||
}
|
||
$tabHeaders = $arrParsed['headers'];
|
||
$strSig = fxImportResultatsHeaderSig($tabHeaders);
|
||
}
|
||
|
||
// MSIN-4520 — figer le choix onglets dans le profil (même si le JS oublie)
|
||
if (empty($arrMapping['sheet_mode']) && !empty($arrMeta['sheet_mode'])) {
|
||
$arrMapping['sheet_mode'] = (string)$arrMeta['sheet_mode'];
|
||
}
|
||
if (empty($arrMapping['sheets']) && !empty($arrMeta['sheets']) && is_array($arrMeta['sheets'])) {
|
||
$arrMapping['sheets'] = $arrMeta['sheets'];
|
||
}
|
||
if (empty($arrMapping['sheet']) && !empty($arrMeta['sheet'])) {
|
||
$arrMapping['sheet'] = (string)$arrMeta['sheet'];
|
||
}
|
||
if (($arrMapping['sheet_mode'] ?? '') === 'sheets' && empty($arrMapping['sheets']) && is_array($arrMeta['sheets'] ?? null)) {
|
||
$arrMapping['sheets'] = $arrMeta['sheets'];
|
||
}
|
||
|
||
$strName = trim((string)$strName);
|
||
if ($strName === '') {
|
||
$strName = 'Template ' . date('Y-m-d H:i');
|
||
}
|
||
$intComId = $blnShareCom ? fxImportResultatsComIdForEve($intEveId) : 0;
|
||
$strNow = date('Y-m-d H:i:s');
|
||
$strHeadersJson = json_encode($tabHeaders, JSON_UNESCAPED_UNICODE);
|
||
$strMappingJson = json_encode($arrMapping, JSON_UNESCAPED_UNICODE);
|
||
if ($strHeadersJson === false || $strMappingJson === false) {
|
||
return array('state' => 'error', 'message' => 'JSON mapping invalide');
|
||
}
|
||
|
||
// Upsert soft : même eve + même sig + même nom → update
|
||
$sqlFind = "SELECT tpl_id FROM inscriptions_import_templates"
|
||
. " WHERE eve_id = " . $intEveId
|
||
. " AND tpl_header_sig = '" . addslashes($strSig) . "'"
|
||
. " AND tpl_name = '" . addslashes($strName) . "'"
|
||
. " AND tpl_actif = 1 LIMIT 1";
|
||
$tabFind = $objDatabase->fxGetResults($sqlFind);
|
||
if (is_array($tabFind) && !empty($tabFind[1]['tpl_id'])) {
|
||
$intTplId = intval($tabFind[1]['tpl_id']);
|
||
$sql = "UPDATE inscriptions_import_templates SET"
|
||
. " com_id = " . intval($intComId) . ","
|
||
. " tpl_headers_json = '" . addslashes($strHeadersJson) . "',"
|
||
. " tpl_mapping_json = '" . addslashes($strMappingJson) . "',"
|
||
. " tpl_usa_id = " . intval($intUsaId) . ","
|
||
. " tpl_maj = '" . addslashes($strNow) . "'"
|
||
. " WHERE tpl_id = " . $intTplId;
|
||
$objDatabase->fxQuery($sql);
|
||
} else {
|
||
$sql = "INSERT INTO inscriptions_import_templates"
|
||
. " (eve_id, com_id, tpl_name, tpl_header_sig, tpl_headers_json, tpl_mapping_json, tpl_actif, tpl_usa_id, tpl_creation, tpl_maj)"
|
||
. " VALUES ("
|
||
. intval($intEveId) . ", "
|
||
. intval($intComId) . ", "
|
||
. "'" . addslashes($strName) . "', "
|
||
. "'" . addslashes($strSig) . "', "
|
||
. "'" . addslashes($strHeadersJson) . "', "
|
||
. "'" . addslashes($strMappingJson) . "', "
|
||
. "1, "
|
||
. intval($intUsaId) . ", "
|
||
. "'" . addslashes($strNow) . "', "
|
||
. "'" . addslashes($strNow) . "'"
|
||
. ")";
|
||
$objDatabase->fxQuery($sql);
|
||
$intTplId = intval($objDatabase->fxGetLastId());
|
||
}
|
||
|
||
$strMsg = 'Profil enregistré pour cet événement.';
|
||
if (($arrMapping['sheet_mode'] ?? '') === 'sheets') {
|
||
$intNb = is_array($arrMapping['sheets'] ?? null) ? count($arrMapping['sheets']) : 0;
|
||
$strMsg .= ' Prochain fichier multi-onglets : réapplication auto (onglets = épreuves'
|
||
. ($intNb > 0 ? ', ' . $intNb . ' onglet(s)' : '')
|
||
. ') — prêt à importer.';
|
||
} else {
|
||
$strMsg .= ' Le prochain fichier avec les mêmes colonnes le proposera.';
|
||
}
|
||
|
||
return array(
|
||
'state' => 'ok',
|
||
'tpl_id' => $intTplId,
|
||
'message' => $strMsg,
|
||
'sheet_mode' => (string)($arrMapping['sheet_mode'] ?? ''),
|
||
'templates' => fxImportResultatsFindTemplates($intEveId, $strSig),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Validation mapping (sans écriture resultats).
|
||
*/
|
||
function fxImportResultatsValidateMapping($intEveId, $strToken, array $arrMapping) {
|
||
$intEveId = intval($intEveId);
|
||
$strPath = fxImportResultatsResolveTokenPath($intEveId, $strToken);
|
||
if ($strPath === null) {
|
||
return array('state' => 'error', 'message' => 'Fichier temporaire introuvable — réuploadez.');
|
||
}
|
||
$arrParsed = fxImportResultatsParseFile($strPath);
|
||
if (($arrParsed['state'] ?? '') !== 'ok') {
|
||
return $arrParsed;
|
||
}
|
||
$tabColumns = isset($arrMapping['columns']) && is_array($arrMapping['columns']) ? $arrMapping['columns'] : array();
|
||
$tabEprValues = isset($arrMapping['epr_values']) && is_array($arrMapping['epr_values']) ? $arrMapping['epr_values'] : array();
|
||
|
||
$blnHasExt = false;
|
||
$blnHasEpr = false;
|
||
$intEprCol = -1;
|
||
foreach ($tabColumns as $intIdx => $strKey) {
|
||
$strKey = (string)$strKey;
|
||
if ($strKey === 'par_external_id') {
|
||
$blnHasExt = true;
|
||
}
|
||
if ($strKey === 'epr') {
|
||
$blnHasEpr = true;
|
||
$intEprCol = intval($intIdx);
|
||
}
|
||
}
|
||
|
||
$tabErrors = array();
|
||
$tabWarn = array();
|
||
if (!$blnHasExt) {
|
||
$tabErrors[] = 'Mappez une colonne « Clé unique » (indispensable pour ChronoTrack / réimport).';
|
||
}
|
||
if (!$blnHasEpr) {
|
||
$tabErrors[] = 'Mappez une colonne « Épreuve ».';
|
||
} else {
|
||
$tabDistinct = fxImportResultatsDistincts($arrParsed['rows'], count($arrParsed['headers']));
|
||
$tabVals = $tabDistinct[$intEprCol] ?? array();
|
||
$intUnmapped = 0;
|
||
foreach ($tabVals as $strVal) {
|
||
$intEpr = intval($tabEprValues[$strVal] ?? 0);
|
||
if ($intEpr <= 0) {
|
||
$intUnmapped++;
|
||
}
|
||
}
|
||
if ($intUnmapped > 0) {
|
||
$tabErrors[] = $intUnmapped . ' valeur(s) d\'épreuve sans correspondance MS1.';
|
||
}
|
||
}
|
||
if (!in_array('par_prenom', $tabColumns, true) && !in_array('par_nom', $tabColumns, true)) {
|
||
$tabWarn[] = 'Ni prénom ni nom mappé — vérifiez le fichier.';
|
||
}
|
||
|
||
if (count($tabErrors) > 0) {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => implode(' ', $tabErrors),
|
||
'errors' => $tabErrors,
|
||
'warnings' => $tabWarn,
|
||
'row_count' => count($arrParsed['rows']),
|
||
);
|
||
}
|
||
|
||
return array(
|
||
'state' => 'ok',
|
||
'message' => 'Mapping prêt — ' . count($arrParsed['rows']) . ' ligne(s). Vous pouvez importer.',
|
||
'warnings' => $tabWarn,
|
||
'row_count' => count($arrParsed['rows']),
|
||
'commit_ready' => true,
|
||
);
|
||
}
|
||
|
||
function fxImportResultatsDbEsc($str) {
|
||
global $objDatabase;
|
||
if (isset($objDatabase) && is_object($objDatabase) && method_exists($objDatabase, 'fxEscape')) {
|
||
return $objDatabase->fxEscape((string)$str);
|
||
}
|
||
return addslashes((string)$str);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4482 — faux no_panier / no_commande uniques (varchar 20).
|
||
* Obligatoire : index UNIQUE sur ces champs → '' ne peut exister qu'une fois.
|
||
*/
|
||
function fxImportResultatsSyntheticPanier($intEveId, $strExternalId) {
|
||
$str = 'I' . intval($intEveId) . 'X' . substr(md5((string)$strExternalId), 0, 12);
|
||
return substr($str, 0, 20);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4482 — pec_id_original / par_id_original ont un UNIQUE : jamais inserer 0.
|
||
* Valeur temporaire unique, puis UPDATE = vrai id apres INSERT.
|
||
*/
|
||
function fxImportResultatsTempOriginalId($strExternalId, $strSalt = '') {
|
||
$intCrc = sprintf('%u', crc32((string)$strExternalId . '|' . (string)$strSalt));
|
||
// Plage haute pour limiter collision avec vrais pec_id / par_id historiques
|
||
return 1900000000 + intval($intCrc % 90000000);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4482 — même modèle que fxChronotrackApiSyncGroupBlockedByType.
|
||
*/
|
||
function fxImportResultatsGroupBlockedByType(array $tabBlocked) {
|
||
$tabTypeLabels = array(
|
||
'header_row' => 'Ligne en-tête (ignorée)',
|
||
'no_external' => 'Clé unique manquante',
|
||
'no_race' => 'Épreuve non mappée',
|
||
'insert_pec' => 'Échec création commande (pec)',
|
||
'insert_par' => 'Échec création participant',
|
||
'update_fail' => 'Échec mise à jour',
|
||
'bad_naissance'=> 'Date de naissance non reconnue',
|
||
'bad_pays' => 'Pays non matché',
|
||
'bad_province' => 'Province non matchée',
|
||
'other' => 'Autre',
|
||
);
|
||
$tabTypeOrder = array(
|
||
'no_external', 'no_race', 'insert_pec', 'insert_par', 'update_fail',
|
||
'bad_naissance', 'bad_pays', 'bad_province', 'header_row', 'other',
|
||
);
|
||
$tabGroups = array();
|
||
foreach ($tabBlocked as $arrItem) {
|
||
$strCode = (string)($arrItem['code'] ?? 'other');
|
||
if (!isset($tabGroups[$strCode])) {
|
||
$tabGroups[$strCode] = array(
|
||
'code' => $strCode,
|
||
'label' => isset($tabTypeLabels[$strCode]) ? $tabTypeLabels[$strCode] : $strCode,
|
||
'items' => array(),
|
||
);
|
||
}
|
||
$tabGroups[$strCode]['items'][] = $arrItem;
|
||
}
|
||
$tabOrdered = array();
|
||
foreach ($tabTypeOrder as $strCode) {
|
||
if (isset($tabGroups[$strCode])) {
|
||
$tabOrdered[] = $tabGroups[$strCode];
|
||
unset($tabGroups[$strCode]);
|
||
}
|
||
}
|
||
foreach ($tabGroups as $arrGroup) {
|
||
$tabOrdered[] = $arrGroup;
|
||
}
|
||
return $tabOrdered;
|
||
}
|
||
|
||
function fxImportResultatsRowLabel(array $tabRow, array $tabColIndex) {
|
||
$strPrenom = isset($tabColIndex['par_prenom']) ? trim((string)($tabRow[$tabColIndex['par_prenom']] ?? '')) : '';
|
||
$strNom = isset($tabColIndex['par_nom']) ? trim((string)($tabRow[$tabColIndex['par_nom']] ?? '')) : '';
|
||
$str = trim($strPrenom . ' ' . $strNom);
|
||
return ($str !== '') ? $str : '—';
|
||
}
|
||
|
||
function fxImportResultatsDbError() {
|
||
global $objDatabase;
|
||
if (isset($objDatabase) && is_object($objDatabase) && isset($objDatabase->con)) {
|
||
$str = mysqli_error($objDatabase->con);
|
||
if (is_string($str) && $str !== '') {
|
||
return $str;
|
||
}
|
||
}
|
||
return '';
|
||
}
|
||
|
||
/** MSIN-4482 — Male/Female/Other → M/F/N (aligné CT). */
|
||
function fxImportResultatsNormalizeSex($str) {
|
||
$str = trim((string)$str);
|
||
$strLow = mb_strtolower($str, 'UTF-8');
|
||
if ($strLow === 'f' || $strLow === 'female' || $strLow === 'féminin' || $strLow === 'feminin' || $strLow === 'femme') {
|
||
return 'F';
|
||
}
|
||
if ($strLow === 'm' || $strLow === 'h' || $strLow === 'male' || $strLow === 'masculin' || $strLow === 'homme') {
|
||
return 'M';
|
||
}
|
||
if ($strLow === 'other' || $strLow === 'autre' || $strLow === 'n' || $strLow === 'x') {
|
||
return 'N';
|
||
}
|
||
if ($str === 'F' || $str === 'M' || $str === 'N' || $str === 'H') {
|
||
return ($str === 'H') ? 'M' : $str;
|
||
}
|
||
return '';
|
||
}
|
||
|
||
/** MSIN-4482 / MSIN-4520 — parse dates fichier (m-d-Y, datetime Excel, serial). */
|
||
function fxImportResultatsNormalizeBirthdate($str) {
|
||
$str = trim((string)$str);
|
||
if ($str === '' || $str === '0000-00-00' || strtolower($str) === 'null') {
|
||
return '';
|
||
}
|
||
|
||
// Serial Excel (jours depuis 1899-12-30) — PHPExcel peut renvoyer un float
|
||
if (is_numeric($str)) {
|
||
$float = floatval($str);
|
||
// Plage raisonnable naissance : ~1900–2100 ≈ serial 1 … 73050
|
||
if ($float > 200 && $float < 80000) {
|
||
$intDays = intval($float);
|
||
$objEpoch = new DateTime('1899-12-30');
|
||
$objEpoch->modify('+' . $intDays . ' days');
|
||
$intY = intval($objEpoch->format('Y'));
|
||
if ($intY >= 1900 && $intY <= 2100) {
|
||
return $objEpoch->format('Y-m-d');
|
||
}
|
||
}
|
||
}
|
||
|
||
// Couper l'heure : « 02-14-1988 00:00:00 » / « 02-14-1988 00:00: »
|
||
if (preg_match('/^(\d{1,4}[\/\-.]\d{1,2}[\/\-.]\d{1,4})\b/', $str, $mDateOnly)) {
|
||
$strDatePart = $mDateOnly[1];
|
||
} else {
|
||
$strDatePart = $str;
|
||
}
|
||
|
||
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})/', $strDatePart, $m)) {
|
||
$intY = intval($m[1]);
|
||
$intM = intval($m[2]);
|
||
$intD = intval($m[3]);
|
||
if (checkdate($intM, $intD, $intY)) {
|
||
return sprintf('%04d-%02d-%02d', $intY, $intM, $intD);
|
||
}
|
||
}
|
||
|
||
// m/d/y ou m-d-y (US fréquent dans exports clients)
|
||
if (preg_match('/^(\d{1,2})[\/\-.](\d{1,2})[\/\-.](\d{2,4})$/', $strDatePart, $m)) {
|
||
$intM = intval($m[1]);
|
||
$intD = intval($m[2]);
|
||
$intY = intval($m[3]);
|
||
if ($intY < 100) {
|
||
$intY += ($intY >= 70) ? 1900 : 2000;
|
||
}
|
||
if (checkdate($intM, $intD, $intY)) {
|
||
return sprintf('%04d-%02d-%02d', $intY, $intM, $intD);
|
||
}
|
||
// essai j/m/a
|
||
if (checkdate($intD, $intM, $intY)) {
|
||
return sprintf('%04d-%02d-%02d', $intY, $intD, $intM);
|
||
}
|
||
}
|
||
|
||
// strtotime sur la partie date seule, puis chaîne complète
|
||
foreach (array($strDatePart, $str) as $strTry) {
|
||
$intTs = strtotime($strTry);
|
||
if ($intTs !== false) {
|
||
$intY = intval(date('Y', $intTs));
|
||
if ($intY >= 1900 && $intY <= 2100) {
|
||
return date('Y-m-d', $intTs);
|
||
}
|
||
}
|
||
}
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
* MSIN-4520 — écrit un CSV d'anomalies import (téléchargeable).
|
||
* @param array $tabAnomalies
|
||
* @return array{path?:string,token?:string,count:int}
|
||
*/
|
||
function fxImportResultatsWriteAnomaliesCsv($intEveId, $strToken, array $tabAnomalies) {
|
||
$intEveId = intval($intEveId);
|
||
$strToken = preg_replace('/[^a-f0-9]/', '', strtolower((string)$strToken));
|
||
$intCount = count($tabAnomalies);
|
||
if ($intEveId <= 0 || $strToken === '' || $intCount < 1) {
|
||
return array('count' => 0);
|
||
}
|
||
$strPath = fxImportResultatsUploadDir() . '/' . $intEveId . '_' . $strToken . '_anomalies.csv';
|
||
$fp = @fopen($strPath, 'wb');
|
||
if (!$fp) {
|
||
return array('count' => $intCount);
|
||
}
|
||
// BOM UTF-8 pour Excel
|
||
fwrite($fp, "\xEF\xBB\xBF");
|
||
fputcsv($fp, array('ligne', 'code', 'message', 'nom', 'external_id', 'valeur_brute', 'dossard'), ';');
|
||
foreach ($tabAnomalies as $arrA) {
|
||
fputcsv($fp, array(
|
||
(string)($arrA['line'] ?? ''),
|
||
(string)($arrA['code'] ?? ''),
|
||
(string)($arrA['message'] ?? ''),
|
||
(string)($arrA['name'] ?? ''),
|
||
(string)($arrA['external_id'] ?? ''),
|
||
(string)($arrA['raw'] ?? ''),
|
||
(string)($arrA['no_bib'] ?? ''),
|
||
), ';');
|
||
}
|
||
fclose($fp);
|
||
return array(
|
||
'count' => $intCount,
|
||
'token' => $strToken,
|
||
'path' => $strPath,
|
||
'filename' => basename($strPath),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4520 — sert le CSV d'anomalies (téléchargement).
|
||
* @return array|null payload erreur, ou null si headers envoyés + exit caller
|
||
*/
|
||
function fxImportResultatsDownloadAnomalies($intEveId, $strToken) {
|
||
$intEveId = intval($intEveId);
|
||
$strToken = preg_replace('/[^a-f0-9]/', '', strtolower((string)$strToken));
|
||
if ($intEveId <= 0 || strlen($strToken) < 16) {
|
||
return array('state' => 'error', 'message' => 'token invalide');
|
||
}
|
||
$strPath = fxImportResultatsUploadDir() . '/' . $intEveId . '_' . $strToken . '_anomalies.csv';
|
||
if (!is_file($strPath)) {
|
||
return array('state' => 'error', 'message' => 'Fichier anomalies introuvable — réimportez.');
|
||
}
|
||
return array(
|
||
'state' => 'ok',
|
||
'path' => $strPath,
|
||
'filename' => 'import_anomalies_eve' . $intEveId . '.csv',
|
||
);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4520 — catalogue pays (cache) pour résolution import.
|
||
* @return array<int,array{id:int,keys:string[]}>
|
||
*/
|
||
function fxImportResultatsPaysCatalog() {
|
||
global $objDatabase;
|
||
static $tabCache = null;
|
||
if ($tabCache !== null) {
|
||
return $tabCache;
|
||
}
|
||
$tabCache = array();
|
||
$sql = "SELECT pay_id, pay_nom_fr, pay_nom_en, pay_iso"
|
||
. " FROM inscriptions_pays WHERE pay_actif = 1";
|
||
// pay_iso3 optionnel selon schémas
|
||
$tab = $objDatabase->fxGetResults($sql);
|
||
if (!is_array($tab)) {
|
||
return $tabCache;
|
||
}
|
||
for ($i = 1; $i <= count($tab); $i++) {
|
||
$intId = intval($tab[$i]['pay_id'] ?? 0);
|
||
if ($intId <= 0) {
|
||
continue;
|
||
}
|
||
$tabKeys = array();
|
||
foreach (array('pay_nom_fr', 'pay_nom_en', 'pay_iso') as $strCol) {
|
||
$strV = trim((string)($tab[$i][$strCol] ?? ''));
|
||
if ($strV !== '') {
|
||
$tabKeys[] = fxImportResultatsFoldKey($strV);
|
||
}
|
||
}
|
||
$tabCache[] = array('id' => $intId, 'keys' => $tabKeys);
|
||
}
|
||
return $tabCache;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4520 — catalogue provinces (cache).
|
||
* @return array<int,array{id:int,keys:string[]}>
|
||
*/
|
||
function fxImportResultatsProvincesCatalog() {
|
||
global $objDatabase;
|
||
static $tabCache = null;
|
||
if ($tabCache !== null) {
|
||
return $tabCache;
|
||
}
|
||
$tabCache = array();
|
||
$sql = "SELECT pro_id, pro_nom_fr, pro_nom_en, pro_iso"
|
||
. " FROM inscriptions_provinces WHERE pro_actif = 1";
|
||
$tab = $objDatabase->fxGetResults($sql);
|
||
if (!is_array($tab)) {
|
||
return $tabCache;
|
||
}
|
||
for ($i = 1; $i <= count($tab); $i++) {
|
||
$intId = intval($tab[$i]['pro_id'] ?? 0);
|
||
if ($intId <= 0) {
|
||
continue;
|
||
}
|
||
$tabKeys = array();
|
||
foreach (array('pro_nom_fr', 'pro_nom_en', 'pro_iso') as $strCol) {
|
||
$strV = trim((string)($tab[$i][$strCol] ?? ''));
|
||
if ($strV !== '') {
|
||
$tabKeys[] = fxImportResultatsFoldKey($strV);
|
||
}
|
||
}
|
||
$tabCache[] = array('id' => $intId, 'keys' => $tabKeys);
|
||
}
|
||
return $tabCache;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4520 — valeur fichier → pay_id (nom, ISO2, alias courants).
|
||
*/
|
||
function fxImportResultatsResolvePayId($strValue) {
|
||
$strFold = fxImportResultatsFoldKey($strValue);
|
||
if ($strFold === '') {
|
||
return 0;
|
||
}
|
||
// Alias fréquents fichier client
|
||
$tabAlias = array(
|
||
'ca' => array('canada', 'can'),
|
||
'canada' => array('ca', 'can'),
|
||
'us' => array('usa', 'united states', 'etats unis', 'etats-unis', 'u s a'),
|
||
'usa' => array('us', 'united states', 'etats unis'),
|
||
'united states' => array('us', 'usa'),
|
||
'france' => array('fr'),
|
||
'fr' => array('france'),
|
||
);
|
||
$tabTry = array($strFold);
|
||
if (isset($tabAlias[$strFold])) {
|
||
$tabTry = array_merge($tabTry, $tabAlias[$strFold]);
|
||
}
|
||
foreach (fxImportResultatsPaysCatalog() as $arrPay) {
|
||
foreach ($arrPay['keys'] as $strKey) {
|
||
if ($strKey === '') {
|
||
continue;
|
||
}
|
||
if (in_array($strKey, $tabTry, true) || in_array($strFold, array($strKey), true)) {
|
||
return intval($arrPay['id']);
|
||
}
|
||
}
|
||
}
|
||
// Contient (ex. « Canada (CA) »)
|
||
foreach (fxImportResultatsPaysCatalog() as $arrPay) {
|
||
foreach ($arrPay['keys'] as $strKey) {
|
||
if ($strKey !== '' && mb_strlen($strKey, 'UTF-8') >= 3
|
||
&& (strpos($strFold, $strKey) !== false || strpos($strKey, $strFold) !== false)
|
||
) {
|
||
return intval($arrPay['id']);
|
||
}
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4520 — valeur fichier → pro_id (Québec / Quebec / QC…).
|
||
*/
|
||
function fxImportResultatsResolveProId($strValue) {
|
||
$strFold = fxImportResultatsFoldKey($strValue);
|
||
if ($strFold === '') {
|
||
return 0;
|
||
}
|
||
$tabAlias = array(
|
||
'quebec' => array('qc', 'pq', 'québec'),
|
||
'qc' => array('quebec'),
|
||
'ontario' => array('on'),
|
||
'on' => array('ontario'),
|
||
'alberta' => array('ab'),
|
||
'ab' => array('alberta'),
|
||
'british columbia' => array('bc', 'colombie britannique'),
|
||
'bc' => array('british columbia', 'colombie britannique'),
|
||
'colombie britannique' => array('bc', 'british columbia'),
|
||
);
|
||
$tabTry = array($strFold);
|
||
if (isset($tabAlias[$strFold])) {
|
||
foreach ($tabAlias[$strFold] as $strA) {
|
||
$tabTry[] = fxImportResultatsFoldKey($strA);
|
||
}
|
||
}
|
||
foreach (fxImportResultatsProvincesCatalog() as $arrPro) {
|
||
foreach ($arrPro['keys'] as $strKey) {
|
||
if ($strKey !== '' && in_array($strKey, $tabTry, true)) {
|
||
return intval($arrPro['id']);
|
||
}
|
||
}
|
||
}
|
||
foreach (fxImportResultatsProvincesCatalog() as $arrPro) {
|
||
foreach ($arrPro['keys'] as $strKey) {
|
||
if ($strKey !== '' && mb_strlen($strKey, 'UTF-8') >= 3
|
||
&& (strpos($strFold, $strKey) !== false || strpos($strKey, $strFold) !== false)
|
||
) {
|
||
return intval($arrPro['id']);
|
||
}
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* Résout que_id pour colonnes questions (match existant ou création confirmée).
|
||
* @return array<string,int> colIndex => que_id
|
||
*/
|
||
function fxImportResultatsResolveQuestionIds($intEveId, array $tabHeaders, array $tabQuestionsMap) {
|
||
global $objDatabase;
|
||
|
||
$intEveId = intval($intEveId);
|
||
$tabOut = array();
|
||
foreach ($tabQuestionsMap as $strCol => $strQue) {
|
||
$intCol = intval($strCol);
|
||
$strQue = trim((string)$strQue);
|
||
if ($strQue === '' || $strQue === '0') {
|
||
continue;
|
||
}
|
||
if ($strQue === 'new') {
|
||
$strLabel = trim((string)($tabHeaders[$intCol] ?? ('Question ' . ($intCol + 1))));
|
||
if (mb_strlen($strLabel, 'UTF-8') > 100) {
|
||
$strLabel = mb_substr($strLabel, 0, 97, 'UTF-8') . '...';
|
||
}
|
||
// Réutiliser si même libellé déjà créé sur l'événement
|
||
$sqlFind = "SELECT que_id FROM inscriptions_questions"
|
||
. " WHERE eve_id = " . $intEveId
|
||
. " AND que_question_fr = '" . fxImportResultatsDbEsc($strLabel) . "'"
|
||
. " LIMIT 1";
|
||
$tabFind = $objDatabase->fxGetResults($sqlFind);
|
||
if (is_array($tabFind) && !empty($tabFind[1]['que_id'])) {
|
||
$tabOut[(string)$intCol] = intval($tabFind[1]['que_id']);
|
||
continue;
|
||
}
|
||
$sqlIns = "INSERT INTO inscriptions_questions SET"
|
||
. " eve_id = " . $intEveId . ","
|
||
. " que_question_fr = '" . fxImportResultatsDbEsc($strLabel) . "',"
|
||
. " que_question_en = '" . fxImportResultatsDbEsc($strLabel) . "',"
|
||
. " que_choix_fr = '',"
|
||
. " que_choix_en = '',"
|
||
. " que_liste = 0,"
|
||
. " que_epreuves_incluses = '',"
|
||
. " que_required = 0,"
|
||
. " que_tri = 900,"
|
||
. " que_participant_1 = 1,"
|
||
. " que_modifiable = 1,"
|
||
. " que_actif = 1,"
|
||
. " que_hidden = 0,"
|
||
. " que_validation = '',"
|
||
. " que_cart_show = 1,"
|
||
. " que_cart_label_fr = '" . fxImportResultatsDbEsc($strLabel) . "',"
|
||
. " que_cart_label_en = '" . fxImportResultatsDbEsc($strLabel) . "'";
|
||
if ($objDatabase->fxQuery($sqlIns) === false) {
|
||
continue;
|
||
}
|
||
$tabOut[(string)$intCol] = intval($objDatabase->fxGetLastId());
|
||
continue;
|
||
}
|
||
$intQueId = intval($strQue);
|
||
if ($intQueId > 0) {
|
||
$tabOut[(string)$intCol] = $intQueId;
|
||
}
|
||
}
|
||
fxImportResultatsEnsureQuestionsVisibleFiche($intEveId, $tabOut);
|
||
return $tabOut;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4482 — la fiche exige que_cart_show=1 ; activer les questions utilisées à l'import
|
||
* (matchées OU créées), sans toucher aux autres événements.
|
||
*/
|
||
function fxImportResultatsEnsureQuestionsVisibleFiche($intEveId, array $tabQueIdsByCol) {
|
||
global $objDatabase;
|
||
|
||
$intEveId = intval($intEveId);
|
||
$tabIds = array();
|
||
foreach ($tabQueIdsByCol as $mixId) {
|
||
$intId = intval($mixId);
|
||
if ($intId > 0) {
|
||
$tabIds[$intId] = true;
|
||
}
|
||
}
|
||
if ($intEveId <= 0 || count($tabIds) < 1) {
|
||
return;
|
||
}
|
||
$strIn = implode(',', array_map('intval', array_keys($tabIds)));
|
||
$objDatabase->fxQuery(
|
||
"UPDATE inscriptions_questions SET"
|
||
. " que_cart_show = 1,"
|
||
. " que_cart_label_fr = IF(TRIM(IFNULL(que_cart_label_fr, '')) = '', que_question_fr, que_cart_label_fr),"
|
||
. " que_cart_label_en = IF("
|
||
. " TRIM(IFNULL(que_cart_label_en, '')) = '',"
|
||
. " IF(TRIM(IFNULL(que_question_en, '')) = '', que_question_fr, que_question_en),"
|
||
. " que_cart_label_en"
|
||
. " )"
|
||
. " WHERE eve_id = " . $intEveId
|
||
. " AND que_id IN (" . $strIn . ")"
|
||
);
|
||
}
|
||
|
||
function fxImportResultatsSaveQuestionAnswer($intParIdOriginal, $intPecId, $intQueId, $strHeader, $strValue) {
|
||
global $objDatabase;
|
||
|
||
$intParIdOriginal = intval($intParIdOriginal);
|
||
$intPecId = intval($intPecId);
|
||
$intQueId = intval($intQueId);
|
||
$strValue = trim((string)$strValue);
|
||
$strHeader = trim((string)$strHeader);
|
||
if ($intQueId <= 0 || $intPecId <= 0) {
|
||
return;
|
||
}
|
||
$strNow = function_exists('fxGetDateTime') ? fxGetDateTime() : date('Y-m-d H:i:s');
|
||
$sqlFind = "SELECT pqu_id FROM resultats_questions"
|
||
. " WHERE pec_id = " . $intPecId
|
||
. " AND que_id = " . $intQueId
|
||
. " AND par_id = " . $intParIdOriginal
|
||
. " LIMIT 1";
|
||
$tabFind = $objDatabase->fxGetResults($sqlFind);
|
||
if (is_array($tabFind) && !empty($tabFind[1]['pqu_id'])) {
|
||
$sql = "UPDATE resultats_questions SET"
|
||
. " que_choix_fr = '" . fxImportResultatsDbEsc($strValue) . "',"
|
||
. " que_choix_en = '" . fxImportResultatsDbEsc($strValue) . "',"
|
||
. " pqu_maj = '" . fxImportResultatsDbEsc($strNow) . "'"
|
||
. " WHERE pqu_id = " . intval($tabFind[1]['pqu_id']);
|
||
$objDatabase->fxQuery($sql);
|
||
@$objDatabase->fxQuery(
|
||
"UPDATE resultats_questions SET que_actif = 1 WHERE pqu_id = " . intval($tabFind[1]['pqu_id'])
|
||
);
|
||
return;
|
||
}
|
||
$sql = "INSERT INTO resultats_questions SET"
|
||
. " par_id = " . $intParIdOriginal . ","
|
||
. " pec_id = " . $intPecId . ","
|
||
. " que_id = " . $intQueId . ","
|
||
. " que_question_fr = '" . fxImportResultatsDbEsc($strHeader) . "',"
|
||
. " que_question_en = '" . fxImportResultatsDbEsc($strHeader) . "',"
|
||
. " que_choix_fr = '" . fxImportResultatsDbEsc($strValue) . "',"
|
||
. " que_choix_en = '" . fxImportResultatsDbEsc($strValue) . "',"
|
||
. " que_modifiable = 1,"
|
||
. " pqu_note = '',"
|
||
. " pqu_maj = '" . fxImportResultatsDbEsc($strNow) . "',"
|
||
. " no_panier = '',"
|
||
. " no_commande = '',"
|
||
. " pqu_id_original = 0";
|
||
if ($objDatabase->fxQuery($sql) === false) {
|
||
return;
|
||
}
|
||
$intPqu = intval($objDatabase->fxGetLastId());
|
||
if ($intPqu > 0) {
|
||
@$objDatabase->fxQuery(
|
||
"UPDATE resultats_questions SET que_actif = 1 WHERE pqu_id = " . $intPqu
|
||
);
|
||
}
|
||
}
|
||
/**
|
||
* MSIN-4482 — détecte une ligne qui n'est que la répétition des en-têtes.
|
||
*/
|
||
function fxImportResultatsRowLooksLikeHeader(array $tabRow, array $tabHeaders, array $tabColIndex) {
|
||
$strPrenom = isset($tabColIndex['par_prenom'])
|
||
? trim((string)($tabRow[$tabColIndex['par_prenom']] ?? '')) : '';
|
||
$strNom = isset($tabColIndex['par_nom'])
|
||
? trim((string)($tabRow[$tabColIndex['par_nom']] ?? '')) : '';
|
||
$strExt = isset($tabColIndex['par_external_id'])
|
||
? trim((string)($tabRow[$tabColIndex['par_external_id']] ?? '')) : '';
|
||
|
||
$strPrenomLow = fxImportResultatsNormalizeHeader($strPrenom);
|
||
$strNomLow = fxImportResultatsNormalizeHeader($strNom);
|
||
$strExtLow = fxImportResultatsNormalizeHeader($strExt);
|
||
|
||
// Cas classique export EN / FR
|
||
if ($strPrenomLow === 'first name' || $strPrenomLow === 'prenom' || $strPrenomLow === 'prénom') {
|
||
return true;
|
||
}
|
||
if ($strNomLow === 'last name' || $strNomLow === 'nom') {
|
||
return true;
|
||
}
|
||
if ($strExtLow === 'attendee #' || $strExtLow === 'attendee' || $strExtLow === 'external id'
|
||
|| $strExtLow === 'clé unique' || $strExtLow === 'cle unique') {
|
||
return true;
|
||
}
|
||
|
||
// Ligne entière = en-têtes
|
||
$intMatch = 0;
|
||
$intCompare = 0;
|
||
$intN = min(count($tabRow), count($tabHeaders));
|
||
for ($i = 0; $i < $intN; $i++) {
|
||
$strCell = fxImportResultatsNormalizeHeader($tabRow[$i] ?? '');
|
||
$strH = fxImportResultatsNormalizeHeader($tabHeaders[$i] ?? '');
|
||
if ($strH === '') {
|
||
continue;
|
||
}
|
||
$intCompare++;
|
||
if ($strCell === $strH) {
|
||
$intMatch++;
|
||
}
|
||
}
|
||
if ($intCompare >= 3 && $intMatch >= max(3, (int)floor($intCompare * 0.7))) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* MSIN-4482 — écriture resultats_* (sans panier), upsert sur par_external_id.
|
||
*/
|
||
function fxImportResultatsCommit($intEveId, $strToken, array $arrMapping) {
|
||
global $objDatabase;
|
||
|
||
@set_time_limit(300);
|
||
|
||
$arrVal = fxImportResultatsValidateMapping($intEveId, $strToken, $arrMapping);
|
||
if (($arrVal['state'] ?? '') !== 'ok') {
|
||
return $arrVal;
|
||
}
|
||
|
||
$intEveId = intval($intEveId);
|
||
$strPath = fxImportResultatsResolveTokenPath($intEveId, $strToken);
|
||
$arrParsed = fxImportResultatsParseFile($strPath);
|
||
if (($arrParsed['state'] ?? '') !== 'ok') {
|
||
return $arrParsed;
|
||
}
|
||
|
||
// Colonnes requises pour l'import
|
||
$sqlCol = "SELECT COUNT(*) AS n FROM information_schema.COLUMNS"
|
||
. " WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'resultats_participants'"
|
||
. " AND COLUMN_NAME = 'par_external_id'";
|
||
$tabCol = $objDatabase->fxGetResults($sqlCol);
|
||
if (!is_array($tabCol) || intval($tabCol[1]['n'] ?? 0) < 1) {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => 'Colonnes import absentes — exécuter sql/MSIN-4482-import-resultats-templates.sql en dev préprod.',
|
||
);
|
||
}
|
||
|
||
$tabHeaders = $arrParsed['headers'];
|
||
$tabRows = $arrParsed['rows'];
|
||
$tabColumns = isset($arrMapping['columns']) && is_array($arrMapping['columns']) ? $arrMapping['columns'] : array();
|
||
$tabEprValues = isset($arrMapping['epr_values']) && is_array($arrMapping['epr_values']) ? $arrMapping['epr_values'] : array();
|
||
$tabPayValues = isset($arrMapping['pay_values']) && is_array($arrMapping['pay_values']) ? $arrMapping['pay_values'] : array();
|
||
$tabProValues = isset($arrMapping['pro_values']) && is_array($arrMapping['pro_values']) ? $arrMapping['pro_values'] : array();
|
||
$tabQuestionsMap = isset($arrMapping['questions']) && is_array($arrMapping['questions']) ? $arrMapping['questions'] : array();
|
||
|
||
$tabColIndex = array();
|
||
foreach ($tabColumns as $intIdx => $strKey) {
|
||
$strKey = (string)$strKey;
|
||
if ($strKey === '') {
|
||
continue;
|
||
}
|
||
$tabColIndex[$strKey] = intval($intIdx);
|
||
}
|
||
|
||
$tabQueIds = fxImportResultatsResolveQuestionIds($intEveId, $tabHeaders, $tabQuestionsMap);
|
||
$strNow = function_exists('fxGetDateTime') ? fxGetDateTime() : date('Y-m-d H:i:s');
|
||
$strOrigine = MSIN_IMPORT_RESULTATS_ORIGIN;
|
||
|
||
$intCreated = 0;
|
||
$intUpdated = 0;
|
||
$tabBlocked = array();
|
||
$tabAnomalies = array();
|
||
|
||
foreach ($tabRows as $intRowNum => $tabRow) {
|
||
$intLine = $intRowNum + 2; // +1 header Excel, +1 index 0
|
||
$strNamePreview = fxImportResultatsRowLabel($tabRow, $tabColIndex);
|
||
$strBibPreview = isset($tabColIndex['no_bib']) ? trim((string)($tabRow[$tabColIndex['no_bib']] ?? '')) : '';
|
||
$strSexePreview = '';
|
||
if (isset($tabColIndex['par_sexe'])) {
|
||
$strSexePreview = fxImportResultatsNormalizeSex(trim((string)($tabRow[$tabColIndex['par_sexe']] ?? '')));
|
||
}
|
||
|
||
if (fxImportResultatsRowLooksLikeHeader($tabRow, $tabHeaders, $tabColIndex)) {
|
||
$tabBlocked[] = array(
|
||
'code' => 'header_row',
|
||
'message' => 'Ligne d\'en-tête détectée',
|
||
'line' => $intLine,
|
||
'name' => $strNamePreview,
|
||
'external_id' => '',
|
||
'no_bib' => $strBibPreview,
|
||
'par_sexe' => $strSexePreview,
|
||
);
|
||
continue;
|
||
}
|
||
|
||
$strExt = '';
|
||
if (isset($tabColIndex['par_external_id'])) {
|
||
$strExt = trim((string)($tabRow[$tabColIndex['par_external_id']] ?? ''));
|
||
}
|
||
if ($strExt === '') {
|
||
$tabBlocked[] = array(
|
||
'code' => 'no_external',
|
||
'message' => 'Clé unique (Attendee # / external_id) vide',
|
||
'line' => $intLine,
|
||
'name' => $strNamePreview,
|
||
'external_id' => '',
|
||
'no_bib' => $strBibPreview,
|
||
'par_sexe' => $strSexePreview,
|
||
);
|
||
continue;
|
||
}
|
||
|
||
$strEprRaw = '';
|
||
if (isset($tabColIndex['epr'])) {
|
||
$strEprRaw = trim((string)($tabRow[$tabColIndex['epr']] ?? ''));
|
||
}
|
||
$intEprId = intval($tabEprValues[$strEprRaw] ?? 0);
|
||
if ($intEprId <= 0) {
|
||
$tabBlocked[] = array(
|
||
'code' => 'no_race',
|
||
'message' => ($strEprRaw !== ''
|
||
? ('« ' . $strEprRaw . ' » non mappée vers une épreuve MS1')
|
||
: 'Valeur d\'épreuve vide'),
|
||
'line' => $intLine,
|
||
'name' => $strNamePreview,
|
||
'external_id' => $strExt,
|
||
'no_bib' => $strBibPreview,
|
||
'par_sexe' => $strSexePreview,
|
||
);
|
||
continue;
|
||
}
|
||
|
||
$arrFields = array(
|
||
'par_prenom' => '',
|
||
'par_nom' => '',
|
||
'par_sexe' => '',
|
||
'par_naissance' => '',
|
||
'par_age' => 0,
|
||
'par_courriel' => '',
|
||
'par_telephone1' => '',
|
||
'par_adresse' => '',
|
||
'par_ville' => '',
|
||
'pro_id' => 0,
|
||
'par_codepostal' => '',
|
||
'pay_id' => 0,
|
||
'no_bib' => '',
|
||
'par_nom_equipe' => '',
|
||
'par_no_equipe' => 0,
|
||
'par_contact_urgence_nom' => '',
|
||
'par_contact_urgence_telephone' => '',
|
||
);
|
||
foreach ($arrFields as $strKey => $mixDefault) {
|
||
if (!isset($tabColIndex[$strKey])) {
|
||
continue;
|
||
}
|
||
$strRaw = trim((string)($tabRow[$tabColIndex[$strKey]] ?? ''));
|
||
if ($strKey === 'par_sexe') {
|
||
$arrFields[$strKey] = fxImportResultatsNormalizeSex($strRaw);
|
||
} elseif ($strKey === 'par_naissance') {
|
||
$strNorm = fxImportResultatsNormalizeBirthdate($strRaw);
|
||
$arrFields[$strKey] = $strNorm;
|
||
// MSIN-4520 — ne pas décider en silence : anomalie si valeur présente mais non parsée
|
||
if ($strRaw !== '' && $strNorm === '') {
|
||
$tabAnomalies[] = array(
|
||
'code' => 'bad_naissance',
|
||
'message' => 'Date de naissance non reconnue (participant importé sans date)',
|
||
'line' => $intLine,
|
||
'name' => $strNamePreview,
|
||
'external_id' => $strExt,
|
||
'no_bib' => $strBibPreview,
|
||
'raw' => $strRaw,
|
||
);
|
||
}
|
||
} elseif ($strKey === 'par_age' || $strKey === 'par_no_equipe') {
|
||
$arrFields[$strKey] = intval($strRaw);
|
||
} elseif ($strKey === 'pay_id') {
|
||
// MSIN-4520 — mapping manuel prioritaire, sinon auto-resolve
|
||
if (array_key_exists($strRaw, $tabPayValues)) {
|
||
$arrFields[$strKey] = intval($tabPayValues[$strRaw]);
|
||
} else {
|
||
$arrFields[$strKey] = fxImportResultatsResolvePayId($strRaw);
|
||
}
|
||
if ($strRaw !== '' && intval($arrFields[$strKey]) <= 0) {
|
||
$tabAnomalies[] = array(
|
||
'code' => 'bad_pays',
|
||
'message' => 'Pays non matché (participant importé avec pay_id=0)',
|
||
'line' => $intLine,
|
||
'name' => $strNamePreview,
|
||
'external_id' => $strExt,
|
||
'no_bib' => $strBibPreview,
|
||
'raw' => $strRaw,
|
||
);
|
||
}
|
||
} elseif ($strKey === 'pro_id') {
|
||
if (array_key_exists($strRaw, $tabProValues)) {
|
||
$arrFields[$strKey] = intval($tabProValues[$strRaw]);
|
||
} else {
|
||
$arrFields[$strKey] = fxImportResultatsResolveProId($strRaw);
|
||
}
|
||
if ($strRaw !== '' && intval($arrFields[$strKey]) <= 0) {
|
||
$tabAnomalies[] = array(
|
||
'code' => 'bad_province',
|
||
'message' => 'Province/région non matchée (participant importé avec pro_id=0)',
|
||
'line' => $intLine,
|
||
'name' => $strNamePreview,
|
||
'external_id' => $strExt,
|
||
'no_bib' => $strBibPreview,
|
||
'raw' => $strRaw,
|
||
);
|
||
}
|
||
} else {
|
||
$arrFields[$strKey] = $strRaw;
|
||
}
|
||
}
|
||
if (intval($arrFields['par_age']) <= 0 && $arrFields['par_naissance'] !== '') {
|
||
$intTs = strtotime($arrFields['par_naissance']);
|
||
if ($intTs !== false) {
|
||
$arrFields['par_age'] = intval(floor((time() - $intTs) / 31557600));
|
||
}
|
||
}
|
||
|
||
// Upsert participant import
|
||
$sqlExist = "SELECT par_id, pec_id, par_id_original FROM resultats_participants"
|
||
. " WHERE eve_id = " . $intEveId
|
||
. " AND par_external_id = '" . fxImportResultatsDbEsc($strExt) . "'"
|
||
. " LIMIT 1";
|
||
$tabExist = $objDatabase->fxGetResults($sqlExist);
|
||
$blnUpdate = (is_array($tabExist) && !empty($tabExist[1]['par_id']));
|
||
|
||
if ($blnUpdate) {
|
||
$intParId = intval($tabExist[1]['par_id']);
|
||
$intPecId = intval($tabExist[1]['pec_id']);
|
||
$intParOrig = intval($tabExist[1]['par_id_original']);
|
||
if ($intParOrig <= 0) {
|
||
$intParOrig = $intParId;
|
||
}
|
||
|
||
if ($intPecId > 0) {
|
||
// p.pec_id peut être e.pec_id OU e.pec_id_original (selon imports précédents)
|
||
$tabPecRow = $objDatabase->fxGetResults(
|
||
"SELECT pec_id, pec_id_original FROM resultats_epreuves_commandees"
|
||
. " WHERE eve_id = " . $intEveId
|
||
. " AND (pec_id = " . $intPecId . " OR pec_id_original = " . $intPecId . ")"
|
||
. " LIMIT 1"
|
||
);
|
||
$intPecPk = is_array($tabPecRow) ? intval($tabPecRow[1]['pec_id'] ?? 0) : 0;
|
||
$intPecOrigNow = is_array($tabPecRow) ? intval($tabPecRow[1]['pec_id_original'] ?? 0) : 0;
|
||
if ($intPecPk > 0) {
|
||
$objDatabase->fxQuery(
|
||
"UPDATE resultats_epreuves_commandees SET"
|
||
. " epr_id = " . $intEprId . ","
|
||
. " pec_actif = 1,"
|
||
. " pec_maj = '" . fxImportResultatsDbEsc($strNow) . "'"
|
||
. " WHERE pec_id = " . $intPecPk
|
||
);
|
||
// Tenter pec_id_original = pec_id seulement si UNIQUE libre
|
||
@$objDatabase->fxQuery(
|
||
"UPDATE resultats_epreuves_commandees e SET pec_id_original = " . $intPecPk
|
||
. " WHERE pec_id = " . $intPecPk
|
||
. " AND pec_id_original <> " . $intPecPk
|
||
. " AND NOT EXISTS ("
|
||
. " SELECT 1 FROM ("
|
||
. " SELECT pec_id FROM resultats_epreuves_commandees"
|
||
. " WHERE pec_id_original = " . $intPecPk
|
||
. " AND pec_id <> " . $intPecPk
|
||
. " ) _c"
|
||
. " )"
|
||
);
|
||
$tabPecNow = $objDatabase->fxGetResults(
|
||
"SELECT pec_id_original FROM resultats_epreuves_commandees WHERE pec_id = "
|
||
. $intPecPk . " LIMIT 1"
|
||
);
|
||
$intPecOrigNow = is_array($tabPecNow)
|
||
? intval($tabPecNow[1]['pec_id_original'] ?? 0)
|
||
: $intPecOrigNow;
|
||
if ($intPecOrigNow > 0) {
|
||
$objDatabase->fxQuery(
|
||
"UPDATE resultats_participants SET pec_id = " . $intPecOrigNow
|
||
. " WHERE par_id = " . $intParId
|
||
);
|
||
$intPecId = $intPecOrigNow; // clé liste / questions
|
||
}
|
||
}
|
||
}
|
||
|
||
$sqlUp = "UPDATE resultats_participants SET"
|
||
. " epr_id = " . $intEprId . ","
|
||
. " par_prenom = '" . fxImportResultatsDbEsc($arrFields['par_prenom']) . "',"
|
||
. " par_nom = '" . fxImportResultatsDbEsc($arrFields['par_nom']) . "',"
|
||
. " par_sexe = '" . fxImportResultatsDbEsc($arrFields['par_sexe']) . "',"
|
||
. " par_naissance = " . ($arrFields['par_naissance'] !== '' ? ("'" . fxImportResultatsDbEsc($arrFields['par_naissance']) . "'") : "NULL") . ","
|
||
. " par_age = " . intval($arrFields['par_age']) . ","
|
||
. " par_courriel = '" . fxImportResultatsDbEsc($arrFields['par_courriel']) . "',"
|
||
. " par_telephone1 = '" . fxImportResultatsDbEsc($arrFields['par_telephone1']) . "',"
|
||
. " par_adresse = '" . fxImportResultatsDbEsc($arrFields['par_adresse']) . "',"
|
||
. " par_ville = '" . fxImportResultatsDbEsc($arrFields['par_ville']) . "',"
|
||
. " pro_id = " . intval($arrFields['pro_id']) . ","
|
||
. " par_codepostal = '" . fxImportResultatsDbEsc($arrFields['par_codepostal']) . "',"
|
||
. " pay_id = " . intval($arrFields['pay_id']) . ","
|
||
. " no_bib = '" . fxImportResultatsDbEsc($arrFields['no_bib']) . "',"
|
||
. " par_nom_equipe = '" . fxImportResultatsDbEsc($arrFields['par_nom_equipe']) . "',"
|
||
. " par_no_equipe = " . intval($arrFields['par_no_equipe']) . ","
|
||
. " par_origine = '" . fxImportResultatsDbEsc($strOrigine) . "',"
|
||
. " par_maj = '" . fxImportResultatsDbEsc($strNow) . "'"
|
||
. " WHERE par_id = " . $intParId;
|
||
if ($objDatabase->fxQuery($sqlUp) === false) {
|
||
$strErr = fxImportResultatsDbError();
|
||
$tabBlocked[] = array(
|
||
'code' => 'update_fail',
|
||
'message' => 'Échec UPDATE' . ($strErr !== '' ? (' — ' . $strErr) : ''),
|
||
'line' => $intLine,
|
||
'name' => $strNamePreview,
|
||
'external_id' => $strExt,
|
||
'no_bib' => $strBibPreview,
|
||
'par_sexe' => $strSexePreview,
|
||
'par_id' => $intParId,
|
||
);
|
||
continue;
|
||
}
|
||
// urgence si colonnes présentes (ignorer erreur silencieuse)
|
||
$sqlUrg = "UPDATE resultats_participants SET"
|
||
. " par_contact_urgence_nom = '" . fxImportResultatsDbEsc($arrFields['par_contact_urgence_nom']) . "',"
|
||
. " par_contact_urgence_telephone = '" . fxImportResultatsDbEsc($arrFields['par_contact_urgence_telephone']) . "'"
|
||
. " WHERE par_id = " . $intParId;
|
||
@$objDatabase->fxQuery($sqlUrg);
|
||
|
||
$intUpdated++;
|
||
} else {
|
||
// MSIN-4482 — identifiants techniques uniques (pas un vrai panier/paiement)
|
||
$strNoPanier = fxImportResultatsSyntheticPanier($intEveId, $strExt);
|
||
$strNoCommande = $strNoPanier;
|
||
$intTempPecOrig = fxImportResultatsTempOriginalId($strExt, 'pec-' . $intEveId);
|
||
|
||
$sqlPec = "INSERT INTO resultats_epreuves_commandees SET"
|
||
. " eve_id = " . $intEveId . ","
|
||
. " epr_id = " . $intEprId . ","
|
||
. " no_panier = '" . fxImportResultatsDbEsc($strNoPanier) . "',"
|
||
. " pec_label = '',"
|
||
. " pec_nom_equipe = '" . fxImportResultatsDbEsc($arrFields['par_nom_equipe']) . "',"
|
||
. " pec_equipe = 0,"
|
||
. " com_id = 0,"
|
||
. " pec_maj = '" . fxImportResultatsDbEsc($strNow) . "',"
|
||
. " pec_id_original = " . intval($intTempPecOrig) . ","
|
||
. " no_commande = '" . fxImportResultatsDbEsc($strNoCommande) . "',"
|
||
. " is_cancelled = 0,"
|
||
. " pec_actif = 1";
|
||
if ($objDatabase->fxQuery($sqlPec) === false) {
|
||
$strErr = fxImportResultatsDbError();
|
||
$tabBlocked[] = array(
|
||
'code' => 'insert_pec',
|
||
'message' => 'Échec INSERT pec' . ($strErr !== '' ? (' — ' . $strErr) : ''),
|
||
'line' => $intLine,
|
||
'name' => $strNamePreview,
|
||
'external_id' => $strExt,
|
||
'no_bib' => $strBibPreview,
|
||
'par_sexe' => $strSexePreview,
|
||
);
|
||
continue;
|
||
}
|
||
$intPecId = intval($objDatabase->fxGetLastId());
|
||
// MSIN-4482 — UNIQUE pec_id_original : n'écraser le temp que si pec_id est libre
|
||
$blnOrigOk = false;
|
||
$qryOrig = $objDatabase->fxQuery(
|
||
"UPDATE resultats_epreuves_commandees SET pec_id_original = " . $intPecId
|
||
. " WHERE pec_id = " . $intPecId
|
||
. " AND NOT EXISTS ("
|
||
. " SELECT 1 FROM ("
|
||
. " SELECT pec_id FROM resultats_epreuves_commandees"
|
||
. " WHERE pec_id_original = " . $intPecId
|
||
. " AND pec_id <> " . $intPecId
|
||
. " ) _c"
|
||
. " )"
|
||
);
|
||
if ($qryOrig !== false) {
|
||
$tabChk = $objDatabase->fxGetResults(
|
||
"SELECT pec_id_original FROM resultats_epreuves_commandees WHERE pec_id = "
|
||
. $intPecId . " LIMIT 1"
|
||
);
|
||
$blnOrigOk = (is_array($tabChk) && intval($tabChk[1]['pec_id_original'] ?? 0) === $intPecId);
|
||
}
|
||
// Lien participant pour la liste : toujours = pec_id_original effectif
|
||
$intPecLink = $blnOrigOk ? $intPecId : intval($intTempPecOrig);
|
||
// Questions + participant : clé = pec_id_original (join liste)
|
||
$intPecId = $intPecLink;
|
||
|
||
$intTempParOrig = fxImportResultatsTempOriginalId($strExt, 'par-' . $intEveId);
|
||
$sqlPar = "INSERT INTO resultats_participants SET"
|
||
. " eve_id = " . $intEveId . ","
|
||
. " epr_id = " . $intEprId . ","
|
||
. " par_prenom = '" . fxImportResultatsDbEsc($arrFields['par_prenom']) . "',"
|
||
. " par_nom = '" . fxImportResultatsDbEsc($arrFields['par_nom']) . "',"
|
||
. " par_naissance = " . ($arrFields['par_naissance'] !== '' ? ("'" . fxImportResultatsDbEsc($arrFields['par_naissance']) . "'") : "NULL") . ","
|
||
. " par_age = " . intval($arrFields['par_age']) . ","
|
||
. " par_sexe = '" . fxImportResultatsDbEsc($arrFields['par_sexe']) . "',"
|
||
. " par_adresse = '" . fxImportResultatsDbEsc($arrFields['par_adresse']) . "',"
|
||
. " par_ville = '" . fxImportResultatsDbEsc($arrFields['par_ville']) . "',"
|
||
. " pro_id = " . intval($arrFields['pro_id']) . ","
|
||
. " par_codepostal = '" . fxImportResultatsDbEsc($arrFields['par_codepostal']) . "',"
|
||
. " pay_id = " . intval($arrFields['pay_id']) . ","
|
||
. " par_telephone1 = '" . fxImportResultatsDbEsc($arrFields['par_telephone1']) . "',"
|
||
. " par_courriel = '" . fxImportResultatsDbEsc($arrFields['par_courriel']) . "',"
|
||
. " com_id = 0,"
|
||
. " par_nom_equipe = '" . fxImportResultatsDbEsc($arrFields['par_nom_equipe']) . "',"
|
||
. " no_panier = '" . fxImportResultatsDbEsc($strNoPanier) . "',"
|
||
. " par_maj = '" . fxImportResultatsDbEsc($strNow) . "',"
|
||
. " no_commande = '" . fxImportResultatsDbEsc($strNoCommande) . "',"
|
||
. " par_id_original = " . intval($intTempParOrig) . ","
|
||
. " is_cancelled = 0,"
|
||
. " par_no_equipe = " . intval($arrFields['par_no_equipe']) . ","
|
||
. " par_equipe = 0,"
|
||
. " pec_id = " . $intPecLink . ","
|
||
. " rol_id = 2,"
|
||
. " no_bib = '" . fxImportResultatsDbEsc($arrFields['no_bib']) . "',"
|
||
. " par_external_id = '" . fxImportResultatsDbEsc($strExt) . "',"
|
||
. " par_origine = '" . fxImportResultatsDbEsc($strOrigine) . "'";
|
||
if ($objDatabase->fxQuery($sqlPar) === false) {
|
||
$strErr = fxImportResultatsDbError();
|
||
$tabBlocked[] = array(
|
||
'code' => 'insert_par',
|
||
'message' => 'Échec INSERT participant' . ($strErr !== '' ? (' — ' . $strErr) : ''),
|
||
'line' => $intLine,
|
||
'name' => $strNamePreview,
|
||
'external_id' => $strExt,
|
||
'no_bib' => $strBibPreview,
|
||
'par_sexe' => $strSexePreview,
|
||
'pec_id' => $intPecId,
|
||
);
|
||
continue;
|
||
}
|
||
$intParId = intval($objDatabase->fxGetLastId());
|
||
$intParOrig = $intParId;
|
||
$objDatabase->fxQuery(
|
||
"UPDATE resultats_participants SET par_id_original = " . $intParId
|
||
. " WHERE par_id = " . $intParId
|
||
);
|
||
$sqlUrg = "UPDATE resultats_participants SET"
|
||
. " par_contact_urgence_nom = '" . fxImportResultatsDbEsc($arrFields['par_contact_urgence_nom']) . "',"
|
||
. " par_contact_urgence_telephone = '" . fxImportResultatsDbEsc($arrFields['par_contact_urgence_telephone']) . "'"
|
||
. " WHERE par_id = " . $intParId;
|
||
@$objDatabase->fxQuery($sqlUrg);
|
||
|
||
$intCreated++;
|
||
}
|
||
|
||
foreach ($tabQueIds as $strCol => $intQueId) {
|
||
$intCol = intval($strCol);
|
||
$strVal = trim((string)($tabRow[$intCol] ?? ''));
|
||
$strH = (string)($tabHeaders[$intCol] ?? '');
|
||
fxImportResultatsSaveQuestionAnswer($intParOrig, $intPecId, $intQueId, $strH, $strVal);
|
||
}
|
||
}
|
||
|
||
$intBlocked = count($tabBlocked);
|
||
$tabBlockedByType = fxImportResultatsGroupBlockedByType($tabBlocked);
|
||
$intAnomalies = count($tabAnomalies);
|
||
// MSIN-4520 — détail anomalies = CSV seulement (pas de tableau à l’écran)
|
||
$arrCsv = fxImportResultatsWriteAnomaliesCsv($intEveId, $strToken, $tabAnomalies);
|
||
$blnCsvOk = !empty($arrCsv['path']) && is_file((string)$arrCsv['path']);
|
||
$intTotal = count($tabRows);
|
||
$strMsg = 'Import terminé : ' . $intCreated . ' créé(s), ' . $intUpdated . ' mis à jour.'
|
||
. ($intBlocked > 0 ? (' ' . $intBlocked . ' exclus.') : '')
|
||
. ($intAnomalies > 0
|
||
? (' ' . $intAnomalies . ' anomalie(s) — télécharger le CSV sous les compteurs.')
|
||
: '');
|
||
|
||
return array(
|
||
'state' => 'ok',
|
||
'message' => $strMsg,
|
||
'created' => $intCreated,
|
||
'updated' => $intUpdated,
|
||
'skipped' => $intBlocked,
|
||
'row_count' => $intTotal,
|
||
'blocked_count' => $intBlocked,
|
||
'blocked_by_type' => $tabBlockedByType,
|
||
'anomaly_count' => $intAnomalies,
|
||
'anomalies_token' => (string)($arrCsv['token'] ?? $strToken),
|
||
'anomalies_csv' => $blnCsvOk,
|
||
// Compteurs type CT (KPI)
|
||
'kpi_total' => $intTotal,
|
||
'kpi_ok' => $intCreated + $intUpdated,
|
||
'kpi_blocked' => $intBlocked,
|
||
'kpi_anomalies' => $intAnomalies,
|
||
'kpi_created' => $intCreated,
|
||
'kpi_updated' => $intUpdated,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4482 — compte ce que le reset effacerait (origine import_fichier seulement).
|
||
*/
|
||
function fxImportResultatsResetPreview($intEveId) {
|
||
global $objDatabase;
|
||
|
||
$intEveId = intval($intEveId);
|
||
if ($intEveId <= 0) {
|
||
return array('state' => 'error', 'message' => 'eve_id manquant');
|
||
}
|
||
|
||
$strOrig = fxImportResultatsDbEsc(MSIN_IMPORT_RESULTATS_ORIGIN);
|
||
|
||
$sqlPar = "SELECT COUNT(*) AS n FROM resultats_participants"
|
||
. " WHERE eve_id = " . $intEveId
|
||
. " AND par_origine = '" . $strOrig . "'";
|
||
$tabPar = $objDatabase->fxGetResults($sqlPar);
|
||
$intPar = is_array($tabPar) ? intval($tabPar[1]['n'] ?? 0) : 0;
|
||
|
||
$sqlPec = "SELECT COUNT(DISTINCT e.pec_id) AS n"
|
||
. " FROM resultats_epreuves_commandees e"
|
||
. " INNER JOIN resultats_participants p ON p.eve_id = e.eve_id"
|
||
. " AND (p.pec_id = e.pec_id OR p.pec_id = e.pec_id_original)"
|
||
. " WHERE e.eve_id = " . $intEveId
|
||
. " AND p.par_origine = '" . $strOrig . "'";
|
||
$tabPec = $objDatabase->fxGetResults($sqlPec);
|
||
$intPec = is_array($tabPec) ? intval($tabPec[1]['n'] ?? 0) : 0;
|
||
|
||
$sqlQue = "SELECT COUNT(*) AS n FROM resultats_questions"
|
||
. " WHERE par_id IN ("
|
||
. " SELECT pid FROM ("
|
||
. " SELECT par_id AS pid FROM resultats_participants"
|
||
. " WHERE eve_id = " . $intEveId . " AND par_origine = '" . $strOrig . "'"
|
||
. " UNION"
|
||
. " SELECT par_id_original AS pid FROM resultats_participants"
|
||
. " WHERE eve_id = " . $intEveId . " AND par_origine = '" . $strOrig . "'"
|
||
. " AND par_id_original > 0"
|
||
. " ) _ms1_q"
|
||
. ")";
|
||
$tabQue = $objDatabase->fxGetResults($sqlQue);
|
||
$intQue = is_array($tabQue) ? intval($tabQue[1]['n'] ?? 0) : 0;
|
||
|
||
// Diagnostic : ce que la liste inscriptions verrait (même join + pec_actif)
|
||
$sqlList = "SELECT COUNT(*) AS n"
|
||
. " FROM resultats_epreuves_commandees e, resultats_participants p"
|
||
. " WHERE e.eve_id = " . $intEveId
|
||
. " AND p.pec_id = e.pec_id_original"
|
||
. " AND p.par_origine = '" . $strOrig . "'"
|
||
. " AND p.rol_id NOT IN (3, 4)"
|
||
. " AND e.is_cancelled = 0 AND e.pec_actif = 1";
|
||
$tabList = $objDatabase->fxGetResults($sqlList);
|
||
$intList = is_array($tabList) ? intval($tabList[1]['n'] ?? 0) : 0;
|
||
|
||
return array(
|
||
'state' => 'ok',
|
||
'eve_id' => $intEveId,
|
||
'participants' => $intPar,
|
||
'pec' => $intPec,
|
||
'questions' => $intQue,
|
||
'list_visible' => $intList,
|
||
'message' => 'En BD : ' . $intPar . ' participant(s) import, '
|
||
. $intPec . ' pec, ' . $intQue . ' réponse(s) questions'
|
||
. ' — dont ' . $intList . ' visible(s) dans la liste (pec_actif=1).',
|
||
);
|
||
}
|
||
|
||
/**
|
||
* MSIN-4482 — efface UNIQUEMENT les données d'import (par_origine=import_fichier) de l'événement.
|
||
* Ne touche pas aux inscriptions en ligne / panier réel / inscriptions_questions.
|
||
* Exige confirm = "RESET".
|
||
*/
|
||
function fxImportResultatsReset($intEveId, $strConfirm) {
|
||
global $objDatabase;
|
||
|
||
@set_time_limit(300);
|
||
|
||
$intEveId = intval($intEveId);
|
||
if ($intEveId <= 0) {
|
||
return array('state' => 'error', 'message' => 'eve_id manquant');
|
||
}
|
||
if (trim((string)$strConfirm) !== 'RESET') {
|
||
return array(
|
||
'state' => 'error',
|
||
'message' => 'Confirmation invalide — tapez exactement RESET.',
|
||
);
|
||
}
|
||
|
||
$arrPrev = fxImportResultatsResetPreview($intEveId);
|
||
if (($arrPrev['state'] ?? '') !== 'ok') {
|
||
return $arrPrev;
|
||
}
|
||
if (intval($arrPrev['participants'] ?? 0) < 1) {
|
||
return array(
|
||
'state' => 'ok',
|
||
'message' => 'Rien à effacer — aucun participant import_fichier sur cet événement.',
|
||
'deleted_participants' => 0,
|
||
'deleted_pec' => 0,
|
||
'deleted_questions' => 0,
|
||
);
|
||
}
|
||
|
||
$strOrig = fxImportResultatsDbEsc(MSIN_IMPORT_RESULTATS_ORIGIN);
|
||
$intDelQue = 0;
|
||
$intDelPar = 0;
|
||
$intDelPec = 0;
|
||
|
||
// Sous-requête IDs participants import (évite JOIN DELETE lents / gros IN PHP)
|
||
$strSubPar = "SELECT pid FROM ("
|
||
. " SELECT par_id AS pid FROM resultats_participants"
|
||
. " WHERE eve_id = " . $intEveId . " AND par_origine = '" . $strOrig . "'"
|
||
. " UNION"
|
||
. " SELECT par_id_original AS pid FROM resultats_participants"
|
||
. " WHERE eve_id = " . $intEveId . " AND par_origine = '" . $strOrig . "'"
|
||
. " AND par_id_original > 0"
|
||
. ") _ms1_imp_par";
|
||
$strSubPec = "SELECT pid FROM ("
|
||
. " SELECT pec_id AS pid FROM resultats_participants"
|
||
. " WHERE eve_id = " . $intEveId . " AND par_origine = '" . $strOrig . "'"
|
||
. " AND pec_id > 0"
|
||
. ") _ms1_imp_pec";
|
||
|
||
// 1) Réponses questions des participants import seulement
|
||
$sqlDelQue = "DELETE FROM resultats_questions WHERE par_id IN (" . $strSubPar . ")"
|
||
. " OR pec_id IN (" . $strSubPec . ")";
|
||
if ($objDatabase->fxQuery($sqlDelQue) !== false) {
|
||
$intDelQue = intval($objDatabase->affected_rows);
|
||
}
|
||
|
||
// 2) Produits éventuels
|
||
@$objDatabase->fxQuery(
|
||
"DELETE FROM resultats_produits_new WHERE par_id IN (" . $strSubPar . ")"
|
||
. " OR pec_id IN (" . $strSubPec . ")"
|
||
);
|
||
|
||
// 3) Coquilles pec liées à l'import (avant delete participants)
|
||
$sqlDelPec = "DELETE FROM resultats_epreuves_commandees WHERE eve_id = " . $intEveId
|
||
. " AND pec_id IN (" . $strSubPec . ")";
|
||
if ($objDatabase->fxQuery($sqlDelPec) !== false) {
|
||
$intDelPec += intval($objDatabase->affected_rows);
|
||
}
|
||
// Aussi pec_id_original = pec participant
|
||
$sqlDelPecOrig = "DELETE FROM resultats_epreuves_commandees WHERE eve_id = " . $intEveId
|
||
. " AND pec_id_original IN (" . $strSubPec . ")";
|
||
if ($objDatabase->fxQuery($sqlDelPecOrig) !== false) {
|
||
$intDelPec += intval($objDatabase->affected_rows);
|
||
}
|
||
$strPrefix = 'I' . $intEveId . 'X%';
|
||
$sqlDelPec2 = "DELETE FROM resultats_epreuves_commandees WHERE eve_id = " . $intEveId
|
||
. " AND no_panier LIKE '" . fxImportResultatsDbEsc($strPrefix) . "'";
|
||
if ($objDatabase->fxQuery($sqlDelPec2) !== false) {
|
||
$intDelPec += intval($objDatabase->affected_rows);
|
||
}
|
||
|
||
// 4) Participants import
|
||
$sqlDelPar = "DELETE FROM resultats_participants WHERE eve_id = " . $intEveId
|
||
. " AND par_origine = '" . $strOrig . "'";
|
||
if ($objDatabase->fxQuery($sqlDelPar) !== false) {
|
||
$intDelPar = intval($objDatabase->affected_rows);
|
||
}
|
||
|
||
return array(
|
||
'state' => 'ok',
|
||
'message' => 'Reset import OK — participants ' . $intDelPar
|
||
. ', pec ' . $intDelPec
|
||
. ', réponses questions ' . $intDelQue
|
||
. '. Catalogue questions événement intact. Inscriptions en ligne intactes.',
|
||
'deleted_participants' => $intDelPar,
|
||
'deleted_pec' => $intDelPec,
|
||
'deleted_questions' => $intDelQue,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Panneau UI fiche événement superadm.
|
||
*/
|
||
function fxImportResultatsRenderEventPanel($intEveId) {
|
||
global $vDomaine;
|
||
|
||
$intEveId = intval($intEveId);
|
||
if ($intEveId <= 0) {
|
||
return;
|
||
}
|
||
$blnOkType = fxImportResultatsEveIsSansInscriptionEnLigne($intEveId);
|
||
$intTeReq = fxImportResultatsTeIdSansInscriptionEnLigne();
|
||
$strAjax = rtrim((string)$vDomaine, '/') . '/superadm/ajax_import_resultats.php';
|
||
$strLoaderGif = rtrim((string)$vDomaine, '/') . '/images/ms1-runner-loader.gif';
|
||
$tabEpreuves = fxImportResultatsEpreuvesList($intEveId);
|
||
$intNbEpr = count($tabEpreuves);
|
||
|
||
require_once dirname(__FILE__) . '/inc_fx_superadm_v2_ui.php';
|
||
fxSuperadmV2PanelOpen('Import participants (hors transaction)', array(
|
||
'collapse_id' => 'importResultatsCollapse',
|
||
'zone_id' => 'import_resultats',
|
||
'zone_class' => 'superadm-content-v2-zone mt-3 mb-2',
|
||
'expanded' => true,
|
||
'header_extra' => '<span class="superadm-v2-panel__header-extra ml-2"><span class="badge badge-light">MSIN-4520</span></span>',
|
||
));
|
||
?>
|
||
<link rel="stylesheet" href="<?php echo fxImportResultatsEsc(rtrim((string)$vDomaine, '/') . '/css/ms1-loader.css'); ?>?v=<?php echo defined('_VERSION_CODE') ? _VERSION_CODE : '1'; ?>">
|
||
<div id="ms1-import-resultats" class="ms1-import"
|
||
data-eve-id="<?php echo $intEveId; ?>"
|
||
data-eve-ok="<?php echo $blnOkType ? '1' : '0'; ?>"
|
||
data-loader-src="<?php echo fxImportResultatsEsc($strLoaderGif); ?>"
|
||
data-ajax="<?php echo fxImportResultatsEsc($strAjax); ?>">
|
||
<?php if (!$blnOkType) { ?>
|
||
<div class="alert alert-warning">
|
||
<strong>Type d’événement requis</strong><br>
|
||
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>profil / template</strong> est sauvegardé pour cet événement
|
||
(y compris <em>onglets = épreuves</em>). Au prochain dépôt, le mapping est
|
||
réappliqué automatiquement — il reste à cliquer <strong>Importer</strong>.
|
||
Incluez une colonne <strong>épreuve</strong> <em>ou</em> utilisez des <strong>onglets Excel = épreuves</strong>,
|
||
plus une <strong>clé unique</strong> (ChronoTrack).
|
||
<?php if ($intNbEpr < 1) { ?>
|
||
<br><span class="text-danger">Aucune épreuve sur cet événement — créez-en avant d'importer.</span>
|
||
<?php } else { ?>
|
||
<br><?php echo intval($intNbEpr); ?> épreuve(s) disponible(s) pour le mapping.
|
||
<?php } ?>
|
||
<br><span class="text-muted">Reset = efface seulement <code>par_origine=import_fichier</code> sur cet événement (pas les inscriptions payantes).</span>
|
||
</p>
|
||
|
||
<div class="mb-3" id="ms1_import_db_status_wrap">
|
||
<div class="d-flex flex-wrap align-items-center justify-content-between mb-2">
|
||
<strong class="small mb-0">Déjà en BD (origine import_fichier)</strong>
|
||
<div>
|
||
<button type="button" class="btn btn-outline-secondary btn-sm mr-1" id="ms1_import_refresh_status">
|
||
<i class="fa fa-refresh" aria-hidden="true"></i> Actualiser
|
||
</button>
|
||
<button type="button" class="btn btn-outline-danger btn-sm" id="ms1_import_reset_data">
|
||
<i class="fa fa-trash" aria-hidden="true"></i> Reset données import
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="ct-api-kpi-row" id="ms1_import_db_kpi_row">
|
||
<div class="ct-api-kpi" data-kpi="db_par"><div class="ct-api-kpi__label">Participants</div>
|
||
<div class="ct-api-kpi__value" id="ms1_import_db_par">…</div></div>
|
||
<div class="ct-api-kpi" data-kpi="db_pec"><div class="ct-api-kpi__label">PEC (coquilles)</div>
|
||
<div class="ct-api-kpi__value" id="ms1_import_db_pec">…</div></div>
|
||
<div class="ct-api-kpi" data-kpi="db_que"><div class="ct-api-kpi__label">Réponses questions</div>
|
||
<div class="ct-api-kpi__value" id="ms1_import_db_que">…</div></div>
|
||
<div class="ct-api-kpi ct-api-kpi--warn" data-kpi="db_list"><div class="ct-api-kpi__label">Visibles liste*</div>
|
||
<div class="ct-api-kpi__value" id="ms1_import_db_list">…</div></div>
|
||
</div>
|
||
<p class="small text-muted mb-0 mt-1" id="ms1_import_db_hint">
|
||
* Visibles liste ≈ join liste inscriptions avec <code>pec_actif=1</code> (diagnostic).
|
||
</p>
|
||
<div id="ms1_import_reset_flash" class="alert py-2 mt-2 mb-0" hidden></div>
|
||
<div id="ms1_import_reset_confirm" class="alert alert-danger mt-3 mb-0" hidden>
|
||
<strong>Confirmer le reset import</strong>
|
||
<p class="small mb-2 mt-1" id="ms1_import_reset_confirm_msg"></p>
|
||
<p class="small mb-2">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>
|
||
|
||
<!-- MSIN-4520 — choix multi-onglets avant mapping -->
|
||
<div id="ms1_import_step_sheets" class="ms1-import__step mt-3" hidden>
|
||
<div class="d-flex flex-wrap align-items-center justify-content-between mb-2">
|
||
<div>
|
||
<strong id="ms1_import_sheets_file_label"></strong>
|
||
<span class="text-muted small" id="ms1_import_sheets_count_label"></span>
|
||
</div>
|
||
<button type="button" class="btn btn-sm btn-outline-secondary" id="ms1_import_sheets_cancel">Autre fichier</button>
|
||
</div>
|
||
<div class="alert alert-info py-2">
|
||
<strong>Plusieurs onglets détectés.</strong>
|
||
Les onglets doivent-ils servir à classer les participants dans les bonnes épreuves ?
|
||
<div class="small text-muted mt-1 mb-0">
|
||
Ex. Boréalys : un onglet par distance (100K, Relais…) — les onglets « Tous » / « détail » sont exclus par défaut.
|
||
</div>
|
||
</div>
|
||
<div class="btn-group mb-3" role="group" aria-label="Mode onglets">
|
||
<button type="button" class="btn btn-success btn-sm" id="ms1_import_sheets_yes">
|
||
Oui — onglets = épreuves
|
||
</button>
|
||
<button type="button" class="btn btn-outline-secondary btn-sm" id="ms1_import_sheets_no">
|
||
Non — une feuille + colonne épreuve
|
||
</button>
|
||
</div>
|
||
<div id="ms1_import_sheets_pick" hidden>
|
||
<h6 class="ms1-import__h">Onglets à importer</h6>
|
||
<p class="small text-muted mb-2">Décochez les résumés / doublons. Chaque onglet coché sera mappé vers une épreuve MS1.</p>
|
||
<div class="table-responsive mb-2">
|
||
<table class="table table-sm table-bordered mb-0" id="ms1_import_sheets_table">
|
||
<thead>
|
||
<tr>
|
||
<th style="width:2.5rem;"></th>
|
||
<th>Onglet</th>
|
||
<th>Lignes</th>
|
||
<th>Type</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
</div>
|
||
<div class="form-inline flex-wrap mb-2" id="ms1_import_sheets_single_wrap" hidden>
|
||
<label class="mr-2 mb-2" for="ms1_import_sheets_single">Feuille à lire</label>
|
||
<select class="form-control form-control-sm mb-2" id="ms1_import_sheets_single" style="min-width:14rem;"></select>
|
||
</div>
|
||
<button type="button" class="btn btn-primary btn-sm" id="ms1_import_sheets_continue">
|
||
Continuer vers le mapping
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ct-api-kpi-row mb-3 mt-2" id="ms1_import_kpi_row" hidden>
|
||
<div class="ct-api-kpi" data-kpi="total"><div class="ct-api-kpi__label">Lignes fichier</div>
|
||
<div class="ct-api-kpi__value" id="ms1_import_kpi_total">—</div></div>
|
||
<div class="ct-api-kpi ct-api-kpi--ok" data-kpi="ok"><div class="ct-api-kpi__label">Importés</div>
|
||
<div class="ct-api-kpi__value" id="ms1_import_kpi_ok">—</div></div>
|
||
<div class="ct-api-kpi ct-api-kpi--ok" data-kpi="created"><div class="ct-api-kpi__label">Créés</div>
|
||
<div class="ct-api-kpi__value" id="ms1_import_kpi_created">—</div></div>
|
||
<div class="ct-api-kpi ct-api-kpi--warn" data-kpi="updated"><div class="ct-api-kpi__label">Mis à jour</div>
|
||
<div class="ct-api-kpi__value" id="ms1_import_kpi_updated">—</div></div>
|
||
<div class="ct-api-kpi ct-api-kpi--danger" data-kpi="blocked"><div class="ct-api-kpi__label">Exclus</div>
|
||
<div class="ct-api-kpi__value" id="ms1_import_kpi_blocked">—</div></div>
|
||
<div class="ct-api-kpi ct-api-kpi--warn" data-kpi="anomalies"><div class="ct-api-kpi__label">Anomalies</div>
|
||
<div class="ct-api-kpi__value" id="ms1_import_kpi_anomalies">—</div></div>
|
||
</div>
|
||
|
||
<div id="ms1_import_anom_banner" class="alert alert-warning mb-3" hidden>
|
||
<div class="d-flex flex-wrap align-items-center justify-content-between">
|
||
<div>
|
||
<strong>Anomalies d’import</strong>
|
||
<span id="ms1_import_anom_banner_txt" class="ml-1"></span>
|
||
</div>
|
||
<button type="button" class="btn btn-warning btn-sm mt-2 mt-sm-0" id="ms1_import_anom_download_banner">
|
||
<i class="fa fa-download" aria-hidden="true"></i> Télécharger le CSV des anomalies
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ct-api-exclus-shell mb-3" id="ms1_import_exclus_shell" hidden>
|
||
<div class="ct-api-exclus-shell__head">
|
||
<span class="ct-api-exclus-shell__title-wrap">
|
||
<span class="ct-api-exclus-shell__title">Exclus</span>
|
||
<span class="badge badge-danger ml-2"><span id="ms1_import_hdr_exclus">0</span> exclus</span>
|
||
</span>
|
||
</div>
|
||
<div class="ct-api-exclus-shell__body">
|
||
<div id="ms1_import_blocked_wrap">
|
||
<div class="ct-api-exclus-empty">Aucun exclus pour le moment.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="ms1_import_step_map" class="ms1-import__step mt-3" hidden>
|
||
<div class="d-flex flex-wrap align-items-center justify-content-between mb-2">
|
||
<div>
|
||
<strong id="ms1_import_file_label"></strong>
|
||
<span class="text-muted small" id="ms1_import_row_label"></span>
|
||
</div>
|
||
<button type="button" class="btn btn-sm btn-outline-secondary" id="ms1_import_reset">Autre fichier</button>
|
||
</div>
|
||
|
||
<div id="ms1_import_templates" class="ms1-import__templates mb-3" hidden></div>
|
||
|
||
<h6 class="ms1-import__h">1. Colonnes → champs MS1</h6>
|
||
<div class="table-responsive">
|
||
<table class="table table-sm table-bordered mb-3" id="ms1_import_map_table">
|
||
<thead>
|
||
<tr>
|
||
<th>Colonne fichier</th>
|
||
<th>Aperçu</th>
|
||
<th>Champ MS1</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div id="ms1_import_epr_box" class="ms1-import__epr mb-3" hidden>
|
||
<h6 class="ms1-import__h" id="ms1_import_epr_title">2. Valeurs d'épreuve → épreuves de l'événement</h6>
|
||
<p class="small text-muted mb-2" id="ms1_import_epr_hint" hidden></p>
|
||
<div class="table-responsive">
|
||
<table class="table table-sm table-bordered mb-0" id="ms1_import_epr_table">
|
||
<thead>
|
||
<tr>
|
||
<th id="ms1_import_epr_col_label">Valeur dans le fichier</th>
|
||
<th>Épreuve MS1</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="ms1_import_pay_box" class="ms1-import__pay mb-3" hidden>
|
||
<h6 class="ms1-import__h">Pays — valeurs fichier → catalogue MS1</h6>
|
||
<p class="small text-muted mb-2">Prérempli automatiquement quand reconnu ; corrigez seulement les lignes vides ou fausses (sauvé dans le profil).</p>
|
||
<div class="table-responsive">
|
||
<table class="table table-sm table-bordered mb-0" id="ms1_import_pay_table">
|
||
<thead>
|
||
<tr>
|
||
<th>Valeur dans le fichier</th>
|
||
<th>Pays MS1</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="ms1_import_pro_box" class="ms1-import__pro mb-3" hidden>
|
||
<h6 class="ms1-import__h">Région / province — valeurs fichier → catalogue MS1</h6>
|
||
<p class="small text-muted mb-2">Même logique que le pays (auto + correction manuelle).</p>
|
||
<div class="table-responsive">
|
||
<table class="table table-sm table-bordered mb-0" id="ms1_import_pro_table">
|
||
<thead>
|
||
<tr>
|
||
<th>Valeur dans le fichier</th>
|
||
<th>Province MS1</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="ms1_import_que_box" class="ms1-import__que mb-3" hidden>
|
||
<h6 class="ms1-import__h">3. Questions (option C — match ou créer)</h6>
|
||
<p class="small text-muted mb-2">Colonnes non mappées à un champ standard peuvent être liées à une question existante ou marquées « Créer à l'import ».</p>
|
||
<div class="table-responsive">
|
||
<table class="table table-sm table-bordered mb-0" id="ms1_import_que_table">
|
||
<thead>
|
||
<tr>
|
||
<th>Colonne</th>
|
||
<th>Question</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ms1-import__actions form-inline flex-wrap">
|
||
<label class="mr-2 mb-2" for="ms1_import_tpl_name">Nom du template</label>
|
||
<input type="text" class="form-control form-control-sm mr-2 mb-2" id="ms1_import_tpl_name"
|
||
placeholder="ex. Format client ACME" style="min-width:14rem">
|
||
<button type="button" class="btn btn-outline-primary btn-sm mb-2 mr-2" id="ms1_import_save_tpl">
|
||
<i class="fa fa-save" aria-hidden="true"></i> Enregistrer le template
|
||
</button>
|
||
<button type="button" class="btn btn-outline-secondary btn-sm mb-2 mr-2" id="ms1_import_validate">
|
||
<i class="fa fa-check" aria-hidden="true"></i> Valider le mapping
|
||
</button>
|
||
<button type="button" class="btn btn-success btn-sm mb-2" id="ms1_import_commit">
|
||
<i class="fa fa-play" aria-hidden="true"></i> Importer dans MS1
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div><!-- #ms1_import_body -->
|
||
</div>
|
||
<style>
|
||
/* MSIN-4482 — même langage visuel que ChronoTrack (KPI / Exclus) */
|
||
#ms1-import-resultats .ct-api-kpi-row {
|
||
display: grid;
|
||
gap: 0.65rem;
|
||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||
}
|
||
#ms1_import_db_kpi_row.ct-api-kpi-row {
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
}
|
||
@media (max-width: 992px) {
|
||
#ms1-import-resultats .ct-api-kpi-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||
}
|
||
#ms1-import-resultats .ct-api-kpi {
|
||
background: #f8fafc;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 8px;
|
||
padding: 0.55rem 0.7rem;
|
||
}
|
||
#ms1-import-resultats .ct-api-kpi--ok { background: #eff6ff; border-color: #bfdbfe; }
|
||
#ms1-import-resultats .ct-api-kpi--danger { background: #fef2f2; border-color: #fecaca; }
|
||
#ms1-import-resultats .ct-api-kpi--warn { background: #fffbeb; border-color: #fde68a; }
|
||
#ms1-import-resultats .ct-api-kpi__label {
|
||
color: #64748b;
|
||
font-size: 0.72rem;
|
||
font-weight: 600;
|
||
letter-spacing: 0.02em;
|
||
text-transform: uppercase;
|
||
}
|
||
#ms1-import-resultats .ct-api-kpi__value {
|
||
color: #0f172a;
|
||
font-size: 1.35rem;
|
||
font-weight: 700;
|
||
line-height: 1.2;
|
||
}
|
||
#ms1-import-resultats .ct-api-kpi--ok .ct-api-kpi__value { color: #1d4ed8; }
|
||
#ms1-import-resultats .ct-api-kpi--danger .ct-api-kpi__value { color: #b91c1c; }
|
||
#ms1-import-resultats .ct-api-kpi--warn .ct-api-kpi__value { color: #b45309; }
|
||
#ms1-import-resultats .ct-api-exclus-shell {
|
||
background: #fff;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
}
|
||
#ms1-import-resultats .ct-api-exclus-shell__head {
|
||
background: #f8fafc;
|
||
border-bottom: 1px solid #e2e8f0;
|
||
padding: 0.65rem 0.85rem;
|
||
align-items: center;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: space-between;
|
||
gap: 0.5rem;
|
||
}
|
||
#ms1-import-resultats .ct-api-exclus-shell__title { font-weight: 700; }
|
||
#ms1-import-resultats .ct-api-exclus-shell__body { padding: 12px 14px; }
|
||
#ms1-import-resultats .ct-api-exclus-empty { color: #64748b; font-size: 0.875rem; }
|
||
#ms1-import-resultats .ct-api-exclus-chip {
|
||
align-items: center;
|
||
background: #fef2f2;
|
||
border: 1px solid #fecaca;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 0.75rem;
|
||
padding: 0.55rem 0.75rem;
|
||
}
|
||
#ms1-import-resultats .ct-api-exclus-chip__count {
|
||
color: #b91c1c;
|
||
font-size: 1.25rem;
|
||
font-weight: 700;
|
||
}
|
||
#ms1_import_blocked_wrap .ct-api-blocked-table { font-size: 0.8rem; }
|
||
#ms1_import_blocked_wrap .ct-api-blocked-table th { white-space: nowrap; }
|
||
#ms1_import_blocked_wrap .ct-api-blocked-scroll { max-height: 55vh; overflow: auto; }
|
||
#ms1-import-resultats .ms1-import__busy {
|
||
background: #fff7ed;
|
||
border: 2px solid #f59e0b;
|
||
border-radius: 8px;
|
||
margin: 0.75rem 0;
|
||
padding: 0.85rem 1rem;
|
||
}
|
||
#ms1-import-resultats .ms1-import__busy-inner {
|
||
align-items: flex-start;
|
||
display: flex;
|
||
gap: 0.85rem;
|
||
}
|
||
#ms1-import-resultats .ms1-import__busy .fa-spinner {
|
||
color: #b45309;
|
||
font-size: 1.6rem;
|
||
margin-top: 0.15rem;
|
||
}
|
||
#ms1-import-resultats .ms1-import__busy-title {
|
||
color: #9a3412;
|
||
font-size: 1rem;
|
||
font-weight: 700;
|
||
}
|
||
#ms1-import-resultats .ms1-import__busy-sub {
|
||
color: #92400e;
|
||
font-size: 0.875rem;
|
||
margin-top: 0.15rem;
|
||
}
|
||
#ms1-import-resultats .ms1-import__busy-time {
|
||
color: #78350f;
|
||
font-size: 0.8rem;
|
||
margin-top: 0.35rem;
|
||
}
|
||
#ms1-import-resultats.is-busy-op {
|
||
opacity: 0.92;
|
||
pointer-events: none;
|
||
}
|
||
#ms1-import-resultats.is-busy-op .ms1-import__busy {
|
||
pointer-events: auto;
|
||
}
|
||
/* MSIN-4520 — reset / statut BD restent cliquables même si l’import laisse is-busy-op */
|
||
#ms1-import-resultats.is-busy-op #ms1_import_db_status_wrap {
|
||
pointer-events: auto;
|
||
opacity: 1;
|
||
position: relative;
|
||
z-index: 2;
|
||
}
|
||
/* Coureur dans #preloader superadm (style_blue #spinner = gros FA) */
|
||
#preloader #spinner .ms1-loader__runner {
|
||
display: block;
|
||
height: 80px;
|
||
margin: 0 auto 0.5rem;
|
||
object-fit: contain;
|
||
width: 80px;
|
||
}
|
||
#preloader #spinner .ms1-import-preload-time {
|
||
color: #555;
|
||
font-size: 0.85rem;
|
||
text-align: center;
|
||
}
|
||
</style>
|
||
<script>
|
||
(function ($) {
|
||
var $root = $('#ms1-import-resultats');
|
||
if (!$root.length) return;
|
||
var eveId = parseInt($root.data('eve-id'), 10) || 0;
|
||
var eveOk = String($root.data('eve-ok') || '0') === '1';
|
||
var ajaxUrl = String($root.data('ajax') || 'ajax_import_resultats.php');
|
||
var state = null;
|
||
var busyTimer = null;
|
||
var busyStartedAt = 0;
|
||
|
||
if (!eveOk) {
|
||
return;
|
||
}
|
||
|
||
function showStatus(type, msg) {
|
||
var $s = $('#ms1_import_status');
|
||
$s.removeAttr('hidden').removeClass('alert-success alert-danger alert-info alert-warning')
|
||
.addClass('alert alert-' + (type || 'info')).text(msg || '');
|
||
}
|
||
|
||
function ensureImportPreloader() {
|
||
var $p = $('#preloader');
|
||
if ($p.length) {
|
||
// Superadm header a parfois un vieux spinner FA sans coureur
|
||
if (!$p.find('.ms1-loader__runner').length) {
|
||
var src = String($root.data('loader-src') || '/images/ms1-runner-loader.gif');
|
||
$p.find('#spinner').prepend(
|
||
'<img class="ms1-loader__runner" src="' + src + '" alt="" role="presentation" decoding="async">'
|
||
);
|
||
}
|
||
if (!$('#ms1_import_preload_sec').length) {
|
||
$p.find('#spinner').append(
|
||
'<div class="ms1-import-preload-time small mt-2">Temps écoulé : <strong id="ms1_import_preload_sec">0</strong> s</div>'
|
||
);
|
||
}
|
||
return $p;
|
||
}
|
||
var src = String($root.data('loader-src') || '/images/ms1-runner-loader.gif');
|
||
$('body').append(
|
||
'<div id="preloader" style="display:none;">'
|
||
+ '<div id="spinner" class="ms1-loader ms1-loader--overlay">'
|
||
+ '<img class="ms1-loader__runner" src="' + $('<div/>').text(src).html() + '" alt="" role="presentation" decoding="async">'
|
||
+ '<span id="preloader_text" class="ms1-loader__label"></span>'
|
||
+ '<div class="ms1-import-preload-time small mt-2">Temps écoulé : <strong id="ms1_import_preload_sec">0</strong> s</div>'
|
||
+ '</div></div>'
|
||
);
|
||
return $('#preloader');
|
||
}
|
||
|
||
function setBusy(blnOn, strTitle, strSub) {
|
||
var $b = $('#ms1_import_busy');
|
||
if (busyTimer) {
|
||
clearInterval(busyTimer);
|
||
busyTimer = null;
|
||
}
|
||
if (!blnOn) {
|
||
$b.attr('hidden', true);
|
||
$root.removeClass('is-busy-op');
|
||
if (window.Ms1Loader && typeof window.Ms1Loader.hide === 'function') {
|
||
window.Ms1Loader.hide();
|
||
}
|
||
// Forcer la disparition de l’overlay (fadeOut seul peut laisser des clics morts)
|
||
var $pre = $('#preloader');
|
||
if ($pre.length) {
|
||
$pre.stop(true, true).hide();
|
||
}
|
||
return;
|
||
}
|
||
var strMsg = strTitle || 'Traitement en cours…';
|
||
if (strSub) {
|
||
strMsg = strMsg + '<br><span style="font-size:0.85rem;font-weight:500;">' + $('<div/>').text(strSub).html() + '</span>';
|
||
}
|
||
busyStartedAt = Date.now();
|
||
$('#ms1_import_busy_title').text(strTitle || 'Traitement en cours…');
|
||
$('#ms1_import_busy_sub').text(strSub || 'Ne fermez pas la page — 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 intAnom = (res && res.anomaly_count) ? res.anomaly_count : 0;
|
||
var tabGroups = (res && res.blocked_by_type) ? res.blocked_by_type : [];
|
||
|
||
$('#ms1_import_kpi_total').text(String(intTotal));
|
||
$('#ms1_import_kpi_ok').text(String(intOk));
|
||
$('#ms1_import_kpi_created').text(String(intCreated));
|
||
$('#ms1_import_kpi_updated').text(String(intUpdated));
|
||
$('#ms1_import_kpi_blocked').text(String(intBlocked));
|
||
$('#ms1_import_kpi_anomalies').text(String(intAnom));
|
||
$('#ms1_import_hdr_exclus').text(String(intBlocked));
|
||
$kpi.removeAttr('hidden');
|
||
$shell.removeAttr('hidden');
|
||
|
||
if (intBlocked <= 0 || !tabGroups.length) {
|
||
$wrap.html('<div class="ct-api-exclus-empty">Aucun exclus pour le moment.</div>');
|
||
} else {
|
||
var html = '<div class="ct-api-exclus-chip">';
|
||
html += '<div><strong class="text-danger">Exclus import</strong>';
|
||
html += '<div class="small text-muted">Lignes non importées (à corriger dans le fichier ou le mapping)</div></div>';
|
||
html += '<span class="ct-api-exclus-chip__count">' + intBlocked + '</span></div>';
|
||
|
||
html += '<div id="ms1_import_blocked_detail">';
|
||
$.each(tabGroups, function (_, grp) {
|
||
var intCnt = (grp.items && grp.items.length) ? grp.items.length : 0;
|
||
if (intCnt === 0) return;
|
||
html += '<div class="mb-3">';
|
||
html += '<div class="d-flex align-items-center mb-1">';
|
||
html += '<strong class="text-danger small">' + esc(grp.label || grp.code) + '</strong>';
|
||
html += ' <span class="badge badge-secondary ml-1">' + intCnt + '</span>';
|
||
html += '</div>';
|
||
html += '<div class="ct-api-blocked-scroll border rounded">';
|
||
html += '<table class="table table-sm table-striped ct-api-blocked-table mb-0">';
|
||
html += '<thead class="thead-light"><tr>';
|
||
html += '<th>Ligne</th><th>Nom</th><th>external_id</th><th>Dossard</th><th>Sexe</th><th>Raison</th>';
|
||
html += '</tr></thead><tbody>';
|
||
$.each(grp.items, function (__, item) {
|
||
html += '<tr>';
|
||
html += '<td>' + esc(String(item.line || '')) + '</td>';
|
||
html += '<td>' + esc(item.name || '—') + '</td>';
|
||
html += '<td>' + esc(item.external_id || '—') + '</td>';
|
||
html += '<td>' + esc(item.no_bib ? ('#' + item.no_bib) : '—') + '</td>';
|
||
html += '<td>' + esc(item.par_sexe || '—') + '</td>';
|
||
html += '<td class="text-muted">' + esc(item.message || '') + '</td>';
|
||
html += '</tr>';
|
||
});
|
||
html += '</tbody></table></div></div>';
|
||
});
|
||
html += '</div>';
|
||
$wrap.html(html);
|
||
}
|
||
|
||
renderAnomaliesPanel(res);
|
||
}
|
||
|
||
function renderAnomaliesPanel(res) {
|
||
var $banner = $('#ms1_import_anom_banner');
|
||
var intAnom = (res && res.anomaly_count) ? res.anomaly_count : 0;
|
||
var tok = (res && res.anomalies_token) ? res.anomalies_token : ((state && state.token) ? state.token : '');
|
||
if (intAnom <= 0) {
|
||
$banner.prop('hidden', true);
|
||
return;
|
||
}
|
||
$banner.prop('hidden', false);
|
||
$('#ms1_import_anom_banner_txt').text(
|
||
intAnom + ' ligne(s) importée(s) avec un champ non appliqué — détail dans le CSV.'
|
||
);
|
||
$('#ms1_import_anom_download_banner').data('anom-token', tok);
|
||
try {
|
||
if ($banner[0] && $banner[0].scrollIntoView) {
|
||
$banner[0].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
|
||
function downloadAnomaliesCsv(tok) {
|
||
tok = tok || (state && state.token) || '';
|
||
if (!tok) {
|
||
showStatus('warning', 'Aucun CSV d\'anomalies — le fichier n\'a peut-être pas pu être écrit sur le serveur. Réimportez.');
|
||
return;
|
||
}
|
||
var url = ajaxUrl
|
||
+ '?a=download_anomalies'
|
||
+ '&eve_id=' + encodeURIComponent(String(eveId))
|
||
+ '&token=' + encodeURIComponent(String(tok));
|
||
window.location.href = url;
|
||
}
|
||
|
||
function parseAjaxJson(raw) {
|
||
if (raw && typeof raw === 'object') return raw;
|
||
var s = String(raw || '');
|
||
var i = s.indexOf('{');
|
||
var j = s.lastIndexOf('}');
|
||
if (i < 0 || j <= i) return null;
|
||
try {
|
||
return JSON.parse(s.substring(i, j + 1));
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function ensurePanelOpen() {
|
||
var $c = $('#importResultatsCollapse');
|
||
if ($c.length && !$c.hasClass('show') && window.jQuery) {
|
||
$c.collapse('show');
|
||
}
|
||
if ($root.length && $root[0].scrollIntoView) {
|
||
try { $root[0].scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (e) {}
|
||
}
|
||
}
|
||
|
||
function resetUi() {
|
||
state = null;
|
||
$('#ms1_import_step_sheets').attr('hidden', true);
|
||
$('#ms1_import_sheets_pick').attr('hidden', true);
|
||
$('#ms1_import_sheets_single_wrap').attr('hidden', true);
|
||
$('#ms1_import_step_map').attr('hidden', true);
|
||
$('#ms1_import_templates').attr('hidden', true).empty();
|
||
$('#ms1_import_epr_box').attr('hidden', true);
|
||
$('#ms1_import_pay_box').attr('hidden', true);
|
||
$('#ms1_import_pro_box').attr('hidden', true);
|
||
$('#ms1_import_que_box').attr('hidden', true);
|
||
$('#ms1_import_kpi_row').attr('hidden', true);
|
||
$('#ms1_import_exclus_shell').attr('hidden', true);
|
||
$('#ms1_import_anom_banner').attr('hidden', true);
|
||
$('#ms1_import_blocked_wrap').html('<div class="ct-api-exclus-empty">Aucun exclus pour le moment.</div>');
|
||
$('#ms1_import_file').val('');
|
||
$('#ms1_import_drop').removeClass('is-busy is-ready');
|
||
}
|
||
|
||
function esc(s) {
|
||
return $('<div/>').text(s == null ? '' : String(s)).html();
|
||
}
|
||
|
||
function collectMapping() {
|
||
var columns = [];
|
||
$('#ms1_import_map_table tbody select.js-col-map').each(function () {
|
||
columns.push($(this).val() || '');
|
||
});
|
||
var epr_values = {};
|
||
$('#ms1_import_epr_table tbody tr').each(function () {
|
||
var v = $(this).attr('data-val');
|
||
var epr = parseInt($(this).find('select').val(), 10) || 0;
|
||
if (v != null) epr_values[v] = epr;
|
||
});
|
||
var pay_values = {};
|
||
$('#ms1_import_pay_table tbody tr').each(function () {
|
||
var v = $(this).attr('data-val');
|
||
var pay = parseInt($(this).find('select').val(), 10) || 0;
|
||
if (v != null) pay_values[v] = pay;
|
||
});
|
||
var pro_values = {};
|
||
$('#ms1_import_pro_table tbody tr').each(function () {
|
||
var v = $(this).attr('data-val');
|
||
var pro = parseInt($(this).find('select').val(), 10) || 0;
|
||
if (v != null) pro_values[v] = pro;
|
||
});
|
||
var questions = {};
|
||
$('#ms1_import_que_table tbody tr').each(function () {
|
||
var idx = $(this).attr('data-col');
|
||
var q = $(this).find('select').val() || '';
|
||
if (idx != null && q !== '') questions[String(idx)] = q;
|
||
});
|
||
// MSIN-4520 — figer mode onglets + pays/province dans le profil événement
|
||
var mapping = {
|
||
columns: columns,
|
||
epr_values: epr_values,
|
||
pay_values: pay_values,
|
||
pro_values: pro_values,
|
||
questions: questions
|
||
};
|
||
mapping.sheet_mode = String((state && state.sheet_mode) || 'column');
|
||
if (mapping.sheet_mode === 'sheets') {
|
||
mapping.sheets = (state && state.sheets_used)
|
||
? state.sheets_used
|
||
: (state && state.appliedMapping && state.appliedMapping.sheets
|
||
? state.appliedMapping.sheets
|
||
: []);
|
||
// Valeurs d'épreuve actuelles = noms d'onglets utilisés
|
||
if ((!mapping.sheets || !mapping.sheets.length) && state && state.distincts) {
|
||
var eprCol = -1;
|
||
columns.forEach(function (k, i) { if (k === 'epr') eprCol = i; });
|
||
if (eprCol >= 0 && state.distincts[eprCol]) {
|
||
mapping.sheets = state.distincts[eprCol].slice();
|
||
}
|
||
}
|
||
mapping.sheet = '';
|
||
} else {
|
||
mapping.sheet = String((state && state.sheet) || (state && state.appliedMapping && state.appliedMapping.sheet) || '');
|
||
mapping.sheets = [];
|
||
}
|
||
return mapping;
|
||
}
|
||
|
||
function fieldOptionsHtml(selected) {
|
||
if (!state || !state.fields) return '';
|
||
var html = '';
|
||
var lastGroup = null;
|
||
state.fields.forEach(function (f) {
|
||
if (f.group && f.group !== lastGroup) {
|
||
if (lastGroup !== null) html += '</optgroup>';
|
||
html += '<optgroup label="' + esc(f.group) + '">';
|
||
lastGroup = f.group;
|
||
}
|
||
if (!f.group && lastGroup !== null) {
|
||
html += '</optgroup>';
|
||
lastGroup = null;
|
||
}
|
||
var sel = (String(f.key) === String(selected || '')) ? ' selected' : '';
|
||
html += '<option value="' + esc(f.key) + '"' + sel + '>' + esc(f.label) + '</option>';
|
||
});
|
||
if (lastGroup !== null) html += '</optgroup>';
|
||
return html;
|
||
}
|
||
|
||
function eprOptionsHtml(selected) {
|
||
var html = '<option value="0">— Choisir —</option>';
|
||
(state.epreuves || []).forEach(function (e) {
|
||
var sel = (parseInt(e.epr_id, 10) === parseInt(selected, 10)) ? ' selected' : '';
|
||
html += '<option value="' + esc(e.epr_id) + '"' + sel + '>' + esc(e.label) + '</option>';
|
||
});
|
||
return html;
|
||
}
|
||
|
||
function payOptionsHtml(selected) {
|
||
var html = '<option value="0">— Choisir / inconnu —</option>';
|
||
(state.pays || []).forEach(function (p) {
|
||
var sel = (parseInt(p.pay_id, 10) === parseInt(selected, 10)) ? ' selected' : '';
|
||
html += '<option value="' + esc(p.pay_id) + '"' + sel + '>' + esc(p.label) + '</option>';
|
||
});
|
||
return html;
|
||
}
|
||
|
||
function proOptionsHtml(selected) {
|
||
var html = '<option value="0">— Choisir / inconnu —</option>';
|
||
(state.provinces || []).forEach(function (p) {
|
||
var sel = (parseInt(p.pro_id, 10) === parseInt(selected, 10)) ? ' selected' : '';
|
||
html += '<option value="' + esc(p.pro_id) + '"' + sel + '>' + esc(p.label) + '</option>';
|
||
});
|
||
return html;
|
||
}
|
||
|
||
function queOptionsHtml(selected) {
|
||
var html = '<option value="">— Pas une question —</option>';
|
||
html += '<option value="new"' + (selected === 'new' ? ' selected' : '') + '>Créer à l\'import (confirmé)</option>';
|
||
(state.questions || []).forEach(function (q) {
|
||
var sel = (String(q.que_id) === String(selected)) ? ' selected' : '';
|
||
html += '<option value="' + esc(q.que_id) + '"' + sel + '>' + esc(q.label) + '</option>';
|
||
});
|
||
return html;
|
||
}
|
||
|
||
function renderEprBox() {
|
||
var eprCol = -1;
|
||
$('#ms1_import_map_table tbody select.js-col-map').each(function (i) {
|
||
if ($(this).val() === 'epr') eprCol = i;
|
||
});
|
||
var $box = $('#ms1_import_epr_box');
|
||
var $tb = $('#ms1_import_epr_table tbody').empty();
|
||
if (eprCol < 0) {
|
||
$box.attr('hidden', true);
|
||
return;
|
||
}
|
||
var blnSheets = String(state.sheet_mode || '') === 'sheets';
|
||
$('#ms1_import_epr_title').text(blnSheets
|
||
? '2. Onglets → épreuves de l\'événement'
|
||
: '2. Valeurs d\'épreuve → épreuves de l\'événement');
|
||
$('#ms1_import_epr_col_label').text(blnSheets ? 'Onglet Excel' : 'Valeur dans le fichier');
|
||
if (blnSheets) {
|
||
$('#ms1_import_epr_hint').text('Chaque onglet importé est traité comme une valeur d\'épreuve.').removeAttr('hidden');
|
||
} else {
|
||
$('#ms1_import_epr_hint').attr('hidden', true).text('');
|
||
}
|
||
var vals = (state.distincts && state.distincts[eprCol]) ? state.distincts[eprCol] : [];
|
||
var saved = (state.appliedMapping && state.appliedMapping.epr_values) ? state.appliedMapping.epr_values : {};
|
||
var guessed = state.epr_guess || {};
|
||
vals.forEach(function (v) {
|
||
var cur = saved[v] || guessed[v] || 0;
|
||
$tb.append(
|
||
'<tr data-val="' + esc(v) + '"><td><code>' + esc(v) + '</code></td>'
|
||
+ '<td><select class="form-control form-control-sm">' + eprOptionsHtml(cur) + '</select></td></tr>'
|
||
);
|
||
});
|
||
$box.removeAttr('hidden');
|
||
}
|
||
|
||
function renderPayBox() {
|
||
var payCol = -1;
|
||
$('#ms1_import_map_table tbody select.js-col-map').each(function (i) {
|
||
if ($(this).val() === 'pay_id') payCol = i;
|
||
});
|
||
var $box = $('#ms1_import_pay_box');
|
||
var $tb = $('#ms1_import_pay_table tbody').empty();
|
||
if (payCol < 0) {
|
||
$box.attr('hidden', true);
|
||
return;
|
||
}
|
||
var vals = (state.distincts && state.distincts[payCol]) ? state.distincts[payCol] : [];
|
||
var saved = (state.appliedMapping && state.appliedMapping.pay_values) ? state.appliedMapping.pay_values : {};
|
||
var guessed = state.pay_guess || {};
|
||
vals.forEach(function (v) {
|
||
var cur = (saved[v] != null) ? saved[v] : (guessed[v] || 0);
|
||
$tb.append(
|
||
'<tr data-val="' + esc(v) + '"><td><code>' + esc(v) + '</code></td>'
|
||
+ '<td><select class="form-control form-control-sm">' + payOptionsHtml(cur) + '</select></td></tr>'
|
||
);
|
||
});
|
||
$box.removeAttr('hidden');
|
||
}
|
||
|
||
function renderProBox() {
|
||
var proCol = -1;
|
||
$('#ms1_import_map_table tbody select.js-col-map').each(function (i) {
|
||
if ($(this).val() === 'pro_id') proCol = i;
|
||
});
|
||
var $box = $('#ms1_import_pro_box');
|
||
var $tb = $('#ms1_import_pro_table tbody').empty();
|
||
if (proCol < 0) {
|
||
$box.attr('hidden', true);
|
||
return;
|
||
}
|
||
var vals = (state.distincts && state.distincts[proCol]) ? state.distincts[proCol] : [];
|
||
var saved = (state.appliedMapping && state.appliedMapping.pro_values) ? state.appliedMapping.pro_values : {};
|
||
var guessed = state.pro_guess || {};
|
||
vals.forEach(function (v) {
|
||
var cur = (saved[v] != null) ? saved[v] : (guessed[v] || 0);
|
||
$tb.append(
|
||
'<tr data-val="' + esc(v) + '"><td><code>' + esc(v) + '</code></td>'
|
||
+ '<td><select class="form-control form-control-sm">' + proOptionsHtml(cur) + '</select></td></tr>'
|
||
);
|
||
});
|
||
$box.removeAttr('hidden');
|
||
}
|
||
|
||
function renderQueBox() {
|
||
var $box = $('#ms1_import_que_box');
|
||
var $tb = $('#ms1_import_que_table tbody').empty();
|
||
var saved = (state.appliedMapping && state.appliedMapping.questions) ? state.appliedMapping.questions : {};
|
||
var any = false;
|
||
$('#ms1_import_map_table tbody select.js-col-map').each(function (i) {
|
||
if (($(this).val() || '') !== '') return;
|
||
any = true;
|
||
var h = state.headers[i] || ('Col ' + (i + 1));
|
||
var cur = saved[String(i)] || saved[i] || '';
|
||
$tb.append(
|
||
'<tr data-col="' + i + '"><td>' + esc(h) + '</td>'
|
||
+ '<td><select class="form-control form-control-sm js-que-map">' + queOptionsHtml(cur) + '</select></td></tr>'
|
||
);
|
||
});
|
||
if (any) $box.removeAttr('hidden');
|
||
else $box.attr('hidden', true);
|
||
}
|
||
|
||
function applyMappingObj(mapping) {
|
||
if (!mapping || !mapping.columns) return;
|
||
state.appliedMapping = mapping;
|
||
$('#ms1_import_map_table tbody select.js-col-map').each(function (i) {
|
||
if (mapping.columns[i] != null) {
|
||
$(this).val(String(mapping.columns[i]));
|
||
}
|
||
});
|
||
renderEprBox();
|
||
renderPayBox();
|
||
renderProBox();
|
||
renderQueBox();
|
||
}
|
||
|
||
function renderMap() {
|
||
var $tb = $('#ms1_import_map_table tbody').empty();
|
||
(state.headers || []).forEach(function (h, i) {
|
||
var sample = '';
|
||
if (state.samples && state.samples[0]) {
|
||
sample = state.samples[0][i] != null ? String(state.samples[0][i]) : '';
|
||
}
|
||
var guess = (state.guess && state.guess[i]) ? state.guess[i] : '';
|
||
$tb.append(
|
||
'<tr><td><strong>' + esc(h) + '</strong></td>'
|
||
+ '<td class="small text-muted">' + esc(sample) + '</td>'
|
||
+ '<td><select class="form-control form-control-sm js-col-map" data-col="' + i + '">'
|
||
+ fieldOptionsHtml(guess) + '</select></td></tr>'
|
||
);
|
||
});
|
||
$tb.off('change', 'select.js-col-map').on('change', 'select.js-col-map', function () {
|
||
renderEprBox();
|
||
renderPayBox();
|
||
renderProBox();
|
||
renderQueBox();
|
||
});
|
||
renderEprBox();
|
||
renderPayBox();
|
||
renderProBox();
|
||
renderQueBox();
|
||
|
||
var blnAuto = !!state.auto_applied;
|
||
var $tpl = $('#ms1_import_templates').empty();
|
||
if (blnAuto && state.applied_mapping) {
|
||
applyMappingObj(state.applied_mapping);
|
||
if (state.applied_template && state.applied_template.name) {
|
||
$('#ms1_import_tpl_name').val(state.applied_template.name);
|
||
}
|
||
var strModeLbl = (state.sheet_mode === 'sheets')
|
||
? 'onglets = épreuves'
|
||
: 'feuille + colonne épreuve';
|
||
var htmlAuto = '<div class="alert alert-success py-2 mb-0">'
|
||
+ '<strong>Profil événement réappliqué</strong> (' + esc(strModeLbl) + '). '
|
||
+ 'Mapping colonnes + épreuves restaurés — vous pouvez importer tout de suite.'
|
||
+ '</div>';
|
||
$tpl.html(htmlAuto).removeAttr('hidden');
|
||
} else if (state.templates && state.templates.length) {
|
||
// Défaut = template déjà utilisé sur CET événement (plus récent) ; sinon aucun auto-apply
|
||
var defaultTpl = null;
|
||
(state.templates || []).forEach(function (t) {
|
||
if (t.same_event && !defaultTpl) {
|
||
defaultTpl = t;
|
||
}
|
||
});
|
||
var html = '<div class="alert alert-success py-2 mb-0"><strong>Template(s) trouvé(s)</strong> pour ces colonnes.';
|
||
if (defaultTpl) {
|
||
html += ' Le template de cet événement est appliqué par défaut.';
|
||
} else {
|
||
html += ' Aucun template propre à cet événement — choisissez-en un ou alignez manuellement.';
|
||
}
|
||
html += '</div><div class="list-group mt-2">';
|
||
state.templates.forEach(function (t) {
|
||
var blnDef = defaultTpl && parseInt(t.tpl_id, 10) === parseInt(defaultTpl.tpl_id, 10);
|
||
html += '<button type="button" class="list-group-item list-group-item-action js-apply-tpl'
|
||
+ (blnDef ? ' active' : '') + '" data-tpl="'
|
||
+ esc(t.tpl_id) + '"><i class="fa fa-magic" aria-hidden="true"></i> '
|
||
+ esc(t.name || ('Template #' + t.tpl_id))
|
||
+ (blnDef ? ' <span class="badge badge-light ml-1">défaut événement</span>' : '')
|
||
+ ' <span class="text-muted small">(' + esc(t.maj || '')
|
||
+ (t.same_event ? '' : ' · autre événement même org') + ')</span></button>';
|
||
});
|
||
html += '</div>';
|
||
$tpl.html(html).removeAttr('hidden');
|
||
$tpl.find('.js-apply-tpl').on('click', function () {
|
||
var id = parseInt($(this).data('tpl'), 10);
|
||
var found = null;
|
||
(state.templates || []).forEach(function (t) {
|
||
if (parseInt(t.tpl_id, 10) === id) found = t;
|
||
});
|
||
if (found && found.mapping) {
|
||
applyMappingObj(found.mapping);
|
||
$('#ms1_import_tpl_name').val(found.name || '');
|
||
$tpl.find('.js-apply-tpl').removeClass('active');
|
||
$(this).addClass('active');
|
||
showStatus('success', 'Template « ' + (found.name || id) + ' » appliqué.');
|
||
}
|
||
});
|
||
if (defaultTpl && defaultTpl.mapping) {
|
||
applyMappingObj(defaultTpl.mapping);
|
||
$('#ms1_import_tpl_name').val(defaultTpl.name || '');
|
||
showStatus('success', 'Fichier lu — template « '
|
||
+ (defaultTpl.name || defaultTpl.tpl_id)
|
||
+ ' » (cet événement) appliqué automatiquement.');
|
||
}
|
||
} else {
|
||
$tpl.attr('hidden', true);
|
||
}
|
||
|
||
$('#ms1_import_file_label').text(state.original_name || 'Fichier');
|
||
$('#ms1_import_row_label').text(' · ' + (state.row_count || 0) + ' ligne(s)'
|
||
+ (state.sheet_mode === 'sheets' ? ' · onglets = épreuves' : ''));
|
||
$('#ms1_import_step_map').removeAttr('hidden').show();
|
||
$('#ms1_import_drop').addClass('is-ready');
|
||
if (blnAuto) {
|
||
$('#ms1_import_commit').addClass('btn-lg').trigger('focus');
|
||
} else {
|
||
$('#ms1_import_commit').removeClass('btn-lg');
|
||
}
|
||
ensurePanelOpen();
|
||
}
|
||
|
||
function renderSheetChoice() {
|
||
var sheets = state.sheets || [];
|
||
$('#ms1_import_sheets_file_label').text(state.original_name || 'Fichier');
|
||
$('#ms1_import_sheets_count_label').text(' · ' + sheets.length + ' onglet(s)');
|
||
var $tb = $('#ms1_import_sheets_table tbody').empty();
|
||
var $sel = $('#ms1_import_sheets_single').empty();
|
||
sheets.forEach(function (s, i) {
|
||
var name = s.name || ('Feuille ' + (i + 1));
|
||
var checked = s.suggested ? ' checked' : '';
|
||
var typeLabel = s.is_summary
|
||
? '<span class="badge badge-secondary">résumé / tous</span>'
|
||
: (s.looks_like_data
|
||
? '<span class="badge badge-success">participants</span>'
|
||
: '<span class="badge badge-warning">autre</span>');
|
||
$tb.append(
|
||
'<tr><td><input type="checkbox" class="js-sheet-chk" value="' + esc(name) + '"' + checked + '></td>'
|
||
+ '<td><strong>' + esc(name) + '</strong></td>'
|
||
+ '<td>' + esc(String(s.row_count != null ? s.row_count : '—')) + '</td>'
|
||
+ '<td>' + typeLabel + '</td></tr>'
|
||
);
|
||
$sel.append('<option value="' + esc(name) + '">' + esc(name)
|
||
+ (s.row_count != null ? (' (' + s.row_count + ')') : '') + '</option>');
|
||
});
|
||
// Mode colonne : préférer 1re feuille data, sinon « Tous »
|
||
var defSheet = '';
|
||
sheets.forEach(function (s) {
|
||
if (!defSheet && s.suggested) defSheet = s.name;
|
||
});
|
||
if (!defSheet) {
|
||
sheets.forEach(function (s) {
|
||
if (!defSheet && s.looks_like_data) defSheet = s.name;
|
||
});
|
||
}
|
||
if (!defSheet && sheets[0]) defSheet = sheets[0].name;
|
||
if (defSheet) $sel.val(defSheet);
|
||
state._sheetPickMode = null;
|
||
$('#ms1_import_sheets_pick').attr('hidden', true);
|
||
$('#ms1_import_sheets_single_wrap').attr('hidden', true);
|
||
$('#ms1_import_step_map').attr('hidden', true);
|
||
$('#ms1_import_step_sheets').removeAttr('hidden').show();
|
||
$('#ms1_import_drop').addClass('is-ready');
|
||
ensurePanelOpen();
|
||
}
|
||
|
||
function applyChooseSheets(mode) {
|
||
if (!state || !state.token) return;
|
||
var payload = {
|
||
a: 'choose_sheets',
|
||
eve_id: eveId,
|
||
token: state.token,
|
||
sheet_mode: mode
|
||
};
|
||
if (mode === 'sheets') {
|
||
var picked = [];
|
||
$('#ms1_import_sheets_table .js-sheet-chk:checked').each(function () {
|
||
picked.push($(this).val());
|
||
});
|
||
if (!picked.length) {
|
||
showStatus('warning', 'Cochez au moins un onglet à importer.');
|
||
return;
|
||
}
|
||
payload.sheets = JSON.stringify(picked);
|
||
} else {
|
||
payload.sheet = $('#ms1_import_sheets_single').val() || '';
|
||
}
|
||
setBusy(true, mode === 'sheets' ? 'Fusion des onglets…' : 'Lecture de la feuille…',
|
||
'Préparation du mapping — quelques secondes.');
|
||
showStatus('info', 'Préparation du mapping…');
|
||
$.ajax({
|
||
url: ajaxUrl,
|
||
method: 'POST',
|
||
dataType: 'text',
|
||
timeout: 180000,
|
||
data: payload
|
||
}).done(function (raw) {
|
||
setBusy(false);
|
||
var res = parseAjaxJson(raw);
|
||
if (!res || res.state !== 'ok') {
|
||
showStatus('danger', (res && res.message) ? res.message : 'Échec préparation onglets');
|
||
return;
|
||
}
|
||
state = res;
|
||
state.appliedMapping = null;
|
||
$('#ms1_import_step_sheets').attr('hidden', true);
|
||
showStatus('success', 'Fichier prêt — ' + (res.row_count || 0) + ' ligne(s). Alignez les colonnes ci-dessous.');
|
||
renderMap();
|
||
}).fail(function (xhr) {
|
||
setBusy(false);
|
||
var res = parseAjaxJson(xhr && xhr.responseText);
|
||
if (res && res.state === 'ok') {
|
||
state = res;
|
||
state.appliedMapping = null;
|
||
$('#ms1_import_step_sheets').attr('hidden', true);
|
||
renderMap();
|
||
return;
|
||
}
|
||
showStatus('danger', 'Erreur réseau (choix onglets).');
|
||
});
|
||
}
|
||
|
||
function uploadFile(file) {
|
||
if (!file) return;
|
||
var fd = new FormData();
|
||
fd.append('a', 'upload');
|
||
fd.append('eve_id', String(eveId));
|
||
fd.append('file', file);
|
||
$('#ms1_import_drop').addClass('is-busy');
|
||
setBusy(true, 'Lecture du fichier…', 'Parsing CSV / Excel — quelques secondes en général.');
|
||
showStatus('info', 'Lecture du fichier…');
|
||
$.ajax({
|
||
url: ajaxUrl,
|
||
method: 'POST',
|
||
data: fd,
|
||
processData: false,
|
||
contentType: false,
|
||
dataType: 'text',
|
||
timeout: 120000
|
||
}).done(function (raw) {
|
||
setBusy(false);
|
||
$('#ms1_import_drop').removeClass('is-busy');
|
||
var res = parseAjaxJson(raw);
|
||
if (!res || res.state !== 'ok') {
|
||
showStatus('danger', (res && res.message) ? res.message : 'Échec lecture (réponse invalide)');
|
||
return;
|
||
}
|
||
state = res;
|
||
state.appliedMapping = res.applied_mapping || null;
|
||
if (res.needs_sheet_choice && !res.auto_applied) {
|
||
showStatus('info', res.message || 'Plusieurs onglets détectés — faites un choix ci-dessous.');
|
||
renderSheetChoice();
|
||
return;
|
||
}
|
||
if (res.auto_applied) {
|
||
showStatus('success', res.message || 'Profil événement réappliqué — prêt à importer.');
|
||
renderMap();
|
||
return;
|
||
}
|
||
showStatus('success', 'Fichier lu — ' + (res.row_count || 0) + ' ligne(s). Alignez les colonnes ci-dessous.');
|
||
renderMap();
|
||
}).fail(function (xhr) {
|
||
setBusy(false);
|
||
$('#ms1_import_drop').removeClass('is-busy');
|
||
var res = parseAjaxJson(xhr && xhr.responseText);
|
||
if (res && res.state === 'ok') {
|
||
state = res;
|
||
state.appliedMapping = res.applied_mapping || null;
|
||
if (res.needs_sheet_choice && !res.auto_applied) {
|
||
renderSheetChoice();
|
||
return;
|
||
}
|
||
showStatus('success', res.message || ('Fichier lu — ' + (res.row_count || 0) + ' ligne(s).'));
|
||
renderMap();
|
||
return;
|
||
}
|
||
showStatus('danger', 'Erreur upload (HTTP ' + (xhr && xhr.status ? xhr.status : '?') + ').');
|
||
});
|
||
}
|
||
|
||
var $drop = $('#ms1_import_drop');
|
||
var $file = $('#ms1_import_file');
|
||
$drop.on('click', function (e) {
|
||
if ($(e.target).is('input')) return;
|
||
$file.trigger('click');
|
||
});
|
||
$drop.on('keydown', function (e) {
|
||
if (e.key === 'Enter' || e.key === ' ') {
|
||
e.preventDefault();
|
||
$file.trigger('click');
|
||
}
|
||
});
|
||
$file.on('change', function () {
|
||
if (this.files && this.files[0]) uploadFile(this.files[0]);
|
||
});
|
||
$drop.on('drag dragstart dragend dragover dragenter dragleave drop', function (e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
});
|
||
$drop.on('dragover dragenter', function () { $drop.addClass('is-drag'); });
|
||
$drop.on('dragleave dragend drop', function () { $drop.removeClass('is-drag'); });
|
||
$drop.on('drop', function (e) {
|
||
var files = e.originalEvent && e.originalEvent.dataTransfer
|
||
? e.originalEvent.dataTransfer.files : null;
|
||
if (files && files[0]) uploadFile(files[0]);
|
||
});
|
||
|
||
$('#ms1_import_reset').on('click', function () {
|
||
resetUi();
|
||
showStatus('info', 'Choisissez un nouveau fichier.');
|
||
});
|
||
|
||
$('#ms1_import_anom_download_banner').on('click', function () {
|
||
downloadAnomaliesCsv($(this).data('anom-token'));
|
||
});
|
||
|
||
$('#ms1_import_sheets_cancel').on('click', function () {
|
||
resetUi();
|
||
showStatus('info', 'Choisissez un nouveau fichier.');
|
||
});
|
||
|
||
$('#ms1_import_sheets_yes').on('click', function () {
|
||
state._sheetPickMode = 'sheets';
|
||
$('#ms1_import_sheets_pick').removeAttr('hidden');
|
||
$('#ms1_import_sheets_single_wrap').attr('hidden', true);
|
||
$('#ms1_import_sheets_table').closest('.table-responsive').show();
|
||
showStatus('info', 'Cochez les onglets (= épreuves), puis Continuer.');
|
||
});
|
||
|
||
$('#ms1_import_sheets_no').on('click', function () {
|
||
state._sheetPickMode = 'column';
|
||
$('#ms1_import_sheets_pick').removeAttr('hidden');
|
||
$('#ms1_import_sheets_single_wrap').removeAttr('hidden');
|
||
$('#ms1_import_sheets_table').closest('.table-responsive').hide();
|
||
showStatus('info', 'Choisissez la feuille à lire (colonne épreuve dans le fichier), puis Continuer.');
|
||
});
|
||
|
||
$('#ms1_import_sheets_continue').on('click', function () {
|
||
var mode = state && state._sheetPickMode ? state._sheetPickMode : '';
|
||
if (mode !== 'sheets' && mode !== 'column') {
|
||
showStatus('warning', 'Choisissez d’abord Oui (onglets = épreuves) ou Non (une feuille).');
|
||
return;
|
||
}
|
||
applyChooseSheets(mode);
|
||
});
|
||
|
||
$('#ms1_import_save_tpl').on('click', function () {
|
||
if (!state || !state.token) return;
|
||
var mapping = collectMapping();
|
||
$.ajax({
|
||
url: ajaxUrl,
|
||
method: 'POST',
|
||
dataType: 'text',
|
||
data: {
|
||
a: 'save_template',
|
||
eve_id: eveId,
|
||
token: state.token,
|
||
tpl_name: $('#ms1_import_tpl_name').val() || '',
|
||
mapping: JSON.stringify(mapping)
|
||
}
|
||
}).done(function (raw) {
|
||
var res = parseAjaxJson(raw);
|
||
if (!res || res.state !== 'ok') {
|
||
showStatus('danger', (res && res.message) ? res.message : 'Échec sauvegarde template');
|
||
return;
|
||
}
|
||
state.templates = res.templates || state.templates;
|
||
showStatus('success', res.message || 'Template enregistré.');
|
||
renderMap();
|
||
applyMappingObj(mapping);
|
||
}).fail(function () {
|
||
showStatus('danger', 'Erreur réseau (template).');
|
||
});
|
||
});
|
||
|
||
$('#ms1_import_validate').on('click', function () {
|
||
if (!state || !state.token) return;
|
||
var mapping = collectMapping();
|
||
$.ajax({
|
||
url: ajaxUrl,
|
||
method: 'POST',
|
||
dataType: 'text',
|
||
data: {
|
||
a: 'validate',
|
||
eve_id: eveId,
|
||
token: state.token,
|
||
mapping: JSON.stringify(mapping)
|
||
}
|
||
}).done(function (raw) {
|
||
var res = parseAjaxJson(raw);
|
||
if (!res || res.state !== 'ok') {
|
||
showStatus('warning', (res && res.message) ? res.message : 'Mapping incomplet');
|
||
return;
|
||
}
|
||
showStatus('success', res.message || 'OK');
|
||
}).fail(function () {
|
||
showStatus('danger', 'Erreur réseau (validation).');
|
||
});
|
||
});
|
||
|
||
function renderDbStatus(res) {
|
||
if (!res || res.state !== 'ok') return;
|
||
$('#ms1_import_db_par').text(String(res.participants != null ? res.participants : '—'));
|
||
$('#ms1_import_db_pec').text(String(res.pec != null ? res.pec : '—'));
|
||
$('#ms1_import_db_que').text(String(res.questions != null ? res.questions : '—'));
|
||
$('#ms1_import_db_list').text(String(res.list_visible != null ? res.list_visible : '—'));
|
||
if (res.message) {
|
||
$('#ms1_import_db_hint').text(res.message);
|
||
}
|
||
}
|
||
|
||
function refreshDbStatus(blnSilent) {
|
||
var $btn = $('#ms1_import_refresh_status');
|
||
var $ico = $btn.find('i.fa');
|
||
if (!blnSilent) {
|
||
$btn.prop('disabled', true);
|
||
$ico.addClass('fa-spin');
|
||
$('#ms1_import_db_par, #ms1_import_db_pec, #ms1_import_db_que, #ms1_import_db_list').text('…');
|
||
setBusy(true,
|
||
'Actualisation des compteurs…',
|
||
'Lecture BD (participants import / pec / questions) — le coureur tourne = ça travaille.'
|
||
);
|
||
showStatus('info', 'Actualisation en cours…');
|
||
}
|
||
$.ajax({
|
||
url: ajaxUrl,
|
||
method: 'POST',
|
||
dataType: 'text',
|
||
timeout: 120000,
|
||
data: { a: 'reset_preview', eve_id: eveId }
|
||
}).done(function (raw) {
|
||
if (!blnSilent) {
|
||
setBusy(false);
|
||
$btn.prop('disabled', false);
|
||
$ico.removeClass('fa-spin');
|
||
}
|
||
var res = parseAjaxJson(raw);
|
||
if (!res || res.state !== 'ok') {
|
||
if (!blnSilent) {
|
||
showStatus('danger', (res && res.message) ? res.message : 'Échec lecture statut BD');
|
||
}
|
||
$('#ms1_import_db_par, #ms1_import_db_pec, #ms1_import_db_que, #ms1_import_db_list').text('?');
|
||
return;
|
||
}
|
||
renderDbStatus(res);
|
||
if (!blnSilent) {
|
||
showStatus('info', res.message || 'Statut BD à jour.');
|
||
}
|
||
}).fail(function (xhr) {
|
||
if (!blnSilent) {
|
||
setBusy(false);
|
||
$btn.prop('disabled', false);
|
||
$ico.removeClass('fa-spin');
|
||
showStatus('danger', 'Erreur réseau (statut BD, HTTP '
|
||
+ (xhr && xhr.status ? xhr.status : '?') + ').');
|
||
}
|
||
$('#ms1_import_db_par, #ms1_import_db_pec, #ms1_import_db_que, #ms1_import_db_list').text('?');
|
||
});
|
||
}
|
||
|
||
// Statut chargé dès l'ouverture du panneau
|
||
refreshDbStatus(true);
|
||
|
||
function showResetFlash(type, msg) {
|
||
var $f = $('#ms1_import_reset_flash');
|
||
if (!$f.length) {
|
||
showStatus(type, msg);
|
||
return;
|
||
}
|
||
$f.prop('hidden', false)
|
||
.removeClass('alert-success alert-danger alert-info alert-warning')
|
||
.addClass('alert alert-' + (type || 'info'))
|
||
.text(msg || '');
|
||
showStatus(type, msg);
|
||
}
|
||
|
||
function hideResetConfirm() {
|
||
$('#ms1_import_reset_confirm').prop('hidden', true);
|
||
$('#ms1_import_reset_confirm_input').val('');
|
||
}
|
||
|
||
$('#ms1_import_refresh_status').on('click', function () {
|
||
refreshDbStatus(false);
|
||
});
|
||
|
||
// MSIN-4520 — toujours recompter via AJAX + feedback visible sous le bouton
|
||
$root.on('click', '#ms1_import_reset_data', function () {
|
||
setBusy(false); // débloque pointer-events si un import a laissé is-busy-op
|
||
var $btn = $(this);
|
||
var $box = $('#ms1_import_reset_confirm');
|
||
hideResetConfirm();
|
||
$btn.prop('disabled', true);
|
||
showResetFlash('info', 'Comptage BD pour le reset…');
|
||
$.ajax({
|
||
url: ajaxUrl,
|
||
method: 'POST',
|
||
dataType: 'text',
|
||
timeout: 120000,
|
||
data: { a: 'reset_preview', eve_id: eveId }
|
||
}).done(function (raw) {
|
||
$btn.prop('disabled', false);
|
||
var res = parseAjaxJson(raw);
|
||
if (!res || res.state !== 'ok') {
|
||
showResetFlash('danger', (res && res.message) ? res.message : 'Échec lecture statut BD (reset).');
|
||
return;
|
||
}
|
||
renderDbStatus(res);
|
||
var nPar = parseInt(res.participants, 10) || 0;
|
||
var nPec = parseInt(res.pec, 10) || 0;
|
||
var nQue = parseInt(res.questions, 10) || 0;
|
||
var nList = parseInt(res.list_visible, 10) || 0;
|
||
if (nPar < 1) {
|
||
hideResetConfirm();
|
||
showResetFlash('warning', 'Rien à effacer — 0 participant avec par_origine=import_fichier sur cet événement.');
|
||
return;
|
||
}
|
||
var strRecap = 'Vous allez effacer : <strong>' + esc(String(nPar)) + '</strong> participants, '
|
||
+ '<strong>' + esc(String(nPec)) + '</strong> pec, '
|
||
+ '<strong>' + esc(String(nQue)) + '</strong> réponses questions '
|
||
+ '(dont ' + esc(String(nList)) + ' visibles liste).';
|
||
$('#ms1_import_reset_confirm_msg').html(strRecap);
|
||
$('#ms1_import_reset_confirm_input').val('');
|
||
$box.prop('hidden', false);
|
||
showResetFlash('warning', 'Confirmation requise juste sous le bouton — tapez RESET puis Confirmer.');
|
||
try {
|
||
$box[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
$('#ms1_import_reset_confirm_input').trigger('focus');
|
||
} catch (e) {}
|
||
}).fail(function (xhr) {
|
||
$btn.prop('disabled', false);
|
||
showResetFlash('danger', 'Erreur réseau (comptage reset, HTTP '
|
||
+ (xhr && xhr.status ? xhr.status : '?') + ').');
|
||
});
|
||
});
|
||
|
||
$root.on('click', '#ms1_import_reset_confirm_cancel', function () {
|
||
hideResetConfirm();
|
||
showResetFlash('info', 'Reset annulé.');
|
||
});
|
||
|
||
$root.on('click', '#ms1_import_reset_confirm_go', function () {
|
||
var strConfirm = $.trim($('#ms1_import_reset_confirm_input').val() || '');
|
||
if (strConfirm !== 'RESET') {
|
||
showResetFlash('danger', 'Tapez exactement RESET dans le champ (majuscules).');
|
||
return;
|
||
}
|
||
var $go = $(this);
|
||
$go.prop('disabled', true);
|
||
$('#ms1_import_reset_data').prop('disabled', true);
|
||
setBusy(true,
|
||
'Reset import en cours…',
|
||
'Suppression de milliers de lignes — 30 s à 2 min selon la charge. Le chrono tourne = ça travaille.'
|
||
);
|
||
showResetFlash('warning', 'Reset en cours — ne fermez pas la page.');
|
||
$.ajax({
|
||
url: ajaxUrl,
|
||
method: 'POST',
|
||
dataType: 'text',
|
||
timeout: 300000,
|
||
data: { a: 'reset', eve_id: eveId, confirm: strConfirm }
|
||
}).done(function (raw2) {
|
||
setBusy(false);
|
||
$go.prop('disabled', false);
|
||
$('#ms1_import_reset_data').prop('disabled', false);
|
||
var res = parseAjaxJson(raw2);
|
||
if (!res || res.state !== 'ok') {
|
||
showResetFlash('danger', (res && res.message) ? res.message : 'Échec reset');
|
||
return;
|
||
}
|
||
hideResetConfirm();
|
||
showResetFlash('success', res.message || 'Reset OK');
|
||
$('#ms1_import_kpi_row').prop('hidden', true);
|
||
$('#ms1_import_exclus_shell').prop('hidden', true);
|
||
$('#ms1_import_anom_banner').prop('hidden', true);
|
||
refreshDbStatus(true);
|
||
}).fail(function (xhr) {
|
||
setBusy(false);
|
||
$go.prop('disabled', false);
|
||
$('#ms1_import_reset_data').prop('disabled', false);
|
||
var res = parseAjaxJson(xhr && xhr.responseText);
|
||
if (res && res.state === 'ok') {
|
||
hideResetConfirm();
|
||
showResetFlash('success', res.message || 'Reset OK');
|
||
refreshDbStatus(true);
|
||
return;
|
||
}
|
||
showResetFlash('danger', 'Erreur / timeout reset (HTTP ' + (xhr && xhr.status ? xhr.status : '?')
|
||
+ '). Vérifiez les compteurs avec Actualiser.');
|
||
refreshDbStatus(true);
|
||
});
|
||
});
|
||
|
||
$('#ms1_import_commit').on('click', function () {
|
||
if (!state || !state.token) return;
|
||
var mapping = collectMapping();
|
||
var nRows = parseInt(state.row_count, 10) || 0;
|
||
var strEta = nRows > 500
|
||
? ('~' + nRows + ' lignes : 1 à 3 minutes (écritures une par une en BD).')
|
||
: 'Quelques dizaines de secondes en général.';
|
||
if (!window.confirm('Importer / mettre à jour les participants dans resultats_* pour cet événement ?\n(Sans panier ni paiement — upsert sur la clé unique.)\n\n' + strEta)) {
|
||
return;
|
||
}
|
||
var $btn = $(this);
|
||
$btn.prop('disabled', true);
|
||
setBusy(true, 'Import en cours…', strEta + ' Chrono ci-dessous = ça travaille, pas un plantage.');
|
||
showStatus('warning', 'Import en cours — ne fermez pas la page.');
|
||
$.ajax({
|
||
url: ajaxUrl,
|
||
method: 'POST',
|
||
dataType: 'text',
|
||
timeout: 600000,
|
||
data: {
|
||
a: 'commit',
|
||
eve_id: eveId,
|
||
token: state.token,
|
||
mapping: JSON.stringify(mapping)
|
||
}
|
||
}).done(function (raw) {
|
||
setBusy(false);
|
||
$btn.prop('disabled', false);
|
||
var res = parseAjaxJson(raw);
|
||
if (!res || res.state !== 'ok') {
|
||
showStatus('danger', (res && res.message) ? res.message : 'Échec import');
|
||
return;
|
||
}
|
||
showStatus((res.blocked_count > 0) ? 'warning' : 'success', res.message || 'Import terminé.');
|
||
renderImportBlockedPanel(res);
|
||
refreshDbStatus(true);
|
||
ensurePanelOpen();
|
||
}).fail(function (xhr) {
|
||
setBusy(false);
|
||
$btn.prop('disabled', false);
|
||
var res = parseAjaxJson(xhr && xhr.responseText);
|
||
if (res && res.state === 'ok') {
|
||
showStatus((res.blocked_count > 0) ? 'warning' : 'success', res.message || 'Import terminé.');
|
||
renderImportBlockedPanel(res);
|
||
refreshDbStatus(true);
|
||
return;
|
||
}
|
||
showStatus('danger', 'Erreur réseau (import). Timeout possible sur très gros fichiers — réessayez ou coupez le fichier.');
|
||
});
|
||
});
|
||
})(window.jQuery);
|
||
</script>
|
||
<?php
|
||
fxSuperadmV2PanelClose(array('collapse_id' => 'importResultatsCollapse'));
|
||
}
|