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 */ function fxImportResultatsTargetFields() { return array( array('key' => '', 'label' => '— Ignorer —', 'group' => ''), array('key' => 'par_external_id', 'label' => 'Clé unique (ChronoTrack / fichier)', 'group' => 'Identité'), array('key' => 'par_prenom', 'label' => 'Prénom', 'group' => 'Identité'), array('key' => 'par_nom', 'label' => 'Nom', 'group' => 'Identité'), array('key' => 'par_sexe', 'label' => 'Sexe', 'group' => 'Identité'), array('key' => 'par_naissance', 'label' => 'Date de naissance', 'group' => 'Identité'), array('key' => 'par_age', 'label' => 'Âge', 'group' => 'Identité'), array('key' => 'par_courriel', 'label' => 'Courriel', 'group' => 'Contact'), array('key' => 'par_telephone1', 'label' => 'Téléphone', 'group' => 'Contact'), array('key' => 'par_contact_urgence_nom', 'label' => 'Contact urgence — nom', 'group' => 'Contact'), array('key' => 'par_contact_urgence_telephone', 'label' => 'Contact urgence — téléphone', 'group' => 'Contact'), array('key' => 'par_adresse', 'label' => 'Adresse', 'group' => 'Contact'), array('key' => 'par_ville', 'label' => 'Ville', 'group' => 'Contact'), array('key' => 'par_codepostal', 'label' => 'Code postal', 'group' => 'Contact'), array('key' => 'no_bib', 'label' => 'Dossard', 'group' => 'Course'), array('key' => 'epr', 'label' => 'Épreuve (valeurs → épreuves MS1)', 'group' => 'Course'), array('key' => 'par_nom_equipe', 'label' => 'Nom d\'équipe', 'group' => 'Équipe'), array('key' => 'par_no_equipe', 'label' => 'No équipe', 'group' => 'Équipe'), ); } function fxImportResultatsEsc($str) { return htmlspecialchars((string)$str, ENT_QUOTES, 'UTF-8'); } function fxImportResultatsUploadDir() { $strDir = dirname(__FILE__) . '/../upload/import_resultats'; if (!is_dir($strDir)) { @mkdir($strDir, 0755, true); } return $strDir; } function fxImportResultatsNormalizeHeader($str) { $str = trim((string)$str); $str = preg_replace('/\s+/u', ' ', $str); return mb_strtolower($str, 'UTF-8'); } /** * Empreinte stable des en-têtes (même fichier client = même sig). */ function fxImportResultatsHeaderSig(array $tabHeaders) { $tabNorm = array(); foreach ($tabHeaders as $strH) { $tabNorm[] = fxImportResultatsNormalizeHeader($strH); } return sha1(implode("\n", $tabNorm)); } /** * Suggestion auto de champ selon le libellé d'en-tête. * MSIN-4482 — priorise match exact / aiguille longue ; évite faux positifs sur questions bilingues. */ function fxImportResultatsGuessField($strHeader) { $str = fxImportResultatsNormalizeHeader($strHeader); $str = str_replace(array('-', '_'), ' ', $str); // MSIN-4482 — inclut libellés type export client (ex. Échappée / Eventbrite-like) $tabRules = array( 'par_external_id' => array( 'attendee #', 'attendee id', 'external id', 'external_id', 'unique id', 'uniqueid', 'entry id', 'id unique', 'ct id', 'chronotrack id', 'reg id', 'participant id', 'attendee', ), 'par_prenom' => array('first name', 'firstname', 'given name', 'prenom', 'prénom'), 'par_nom' => array('last name', 'lastname', 'family name', 'surname'), 'par_sexe' => array('gender', 'sexe', 'sex', 'genre'), 'par_naissance' => array('birth date', 'date of birth', 'date naissance', 'naissance', 'birthday', 'dob'), 'par_age' => array('age', 'âge'), 'par_courriel' => array('courriel', 'e-mail', 'email', 'mail'), 'par_telephone1' => array('telephone', 'téléphone', 'phone', 'mobile', 'cell', 'tel'), 'par_contact_urgence_nom' => array('emergency contact name', 'contact urgence', 'nom en cas d\'urgence', 'urgence nom'), 'par_contact_urgence_telephone' => array('emergency contact number', 'emergency contact phone', 'numéro du contact en cas d\'urgence', 'urgence tel'), 'par_adresse' => array('adresse', 'address', 'street'), 'par_ville' => array('ville', 'city', 'town'), 'par_codepostal' => array('code postal', 'postal', 'zipcode', 'zip'), 'no_bib' => array('no dossard', 'bib number', 'dossard', 'no bib', 'bib#', 'bib'), 'epr' => array( 'ticket type', 'registration type', 'reg choice', 'event race', 'epreuve', 'épreuve', 'distance', 'race', 'course', 'ticket', ), 'par_nom_equipe' => array('team name', 'nom equipe', 'nom équipe', 'equipe', 'équipe', 'team'), 'par_no_equipe' => array('team number', 'team no', 'no equipe', 'no équipe', 'team id'), ); // 1) égalité exacte foreach ($tabRules as $strKey => $tabNeedles) { foreach ($tabNeedles as $strNeedle) { if ($str === $strNeedle) { return $strKey; } } } // Questions longues bilingues → pas de guess champ standard (resteront en step questions) if (mb_strlen($str, 'UTF-8') > 40 || strpos($str, '?') !== false || strpos($str, ' / ') !== false) { // Exceptions courtes déjà couvertes en exact ; ici on laisse vide // sauf préfixes connus très spécifiques if (strpos($str, 'attendee') === 0) { return 'par_external_id'; } return ''; } // 2) contient — aiguilles les plus longues d'abord $tabCandidates = array(); foreach ($tabRules as $strKey => $tabNeedles) { foreach ($tabNeedles as $strNeedle) { if ($strNeedle === '') { continue; } // aiguilles 1–3 lettres : mot entier seulement if (mb_strlen($strNeedle, 'UTF-8') <= 3) { if (preg_match('/(^|\\s)' . preg_quote($strNeedle, '/') . '(\\s|$)/u', $str)) { $tabCandidates[] = array('key' => $strKey, 'len' => mb_strlen($strNeedle, 'UTF-8')); } continue; } if (strpos($str, $strNeedle) !== false) { $tabCandidates[] = array('key' => $strKey, 'len' => mb_strlen($strNeedle, 'UTF-8')); } } } if (count($tabCandidates) < 1) { return ''; } usort($tabCandidates, function ($a, $b) { return $b['len'] - $a['len']; }); return $tabCandidates[0]['key']; } /** * Clé de partage templates entre événements du même org (pas de com_id sur evenements). * MSIN-4482 — stockée dans inscriptions_import_templates.com_id (legacy name = org_id). */ function fxImportResultatsComIdForEve($intEveId) { global $objDatabase; $intEveId = intval($intEveId); if ($intEveId <= 0) { return 0; } $sql = "SELECT org_id FROM inscriptions_evenements WHERE eve_id = " . $intEveId . " LIMIT 1"; $tab = $objDatabase->fxGetResults($sql); if (!is_array($tab) || empty($tab[1]['org_id'])) { return 0; } return intval($tab[1]['org_id']); } function fxImportResultatsEpreuvesList($intEveId) { global $objDatabase; $intEveId = intval($intEveId); $sql = "SELECT epr_id, epr_nom_fr, epr_type_fr, epr_date" . " FROM inscriptions_epreuves WHERE eve_id = " . $intEveId . " ORDER BY epr_date, epr_nom_fr"; $tab = $objDatabase->fxGetResults($sql); $tabOut = array(); if (!is_array($tab)) { return $tabOut; } for ($i = 1; $i <= count($tab); $i++) { $strLabel = trim((string)($tab[$i]['epr_type_fr'] ?? '')); $strNom = trim((string)($tab[$i]['epr_nom_fr'] ?? '')); if ($strLabel !== '' && $strNom !== '') { $strLabel .= ' — ' . $strNom; } elseif ($strNom !== '') { $strLabel = $strNom; } else { $strLabel = 'Épreuve #' . intval($tab[$i]['epr_id']); } $tabOut[] = array( 'epr_id' => intval($tab[$i]['epr_id']), 'label' => $strLabel, ); } return $tabOut; } function fxImportResultatsQuestionsList($intEveId) { global $objDatabase; $intEveId = intval($intEveId); // que_rapport_label_fr peut manquer sur vieux schémas — fallback question seule $sql = "SELECT que_id, que_question_fr, que_actif" . " FROM inscriptions_questions WHERE eve_id = " . $intEveId . " AND que_actif = 1 ORDER BY que_tri, que_id"; $tab = $objDatabase->fxGetResults($sql); $tabOut = array(); if (!is_array($tab)) { return $tabOut; } for ($i = 1; $i <= count($tab); $i++) { $strLabel = trim((string)($tab[$i]['que_question_fr'] ?? '')); $tabOut[] = array( 'que_id' => intval($tab[$i]['que_id']), 'label' => $strLabel !== '' ? $strLabel : ('Question #' . intval($tab[$i]['que_id'])), ); } return $tabOut; } /** * Lit CSV ou Excel → en-têtes + lignes (tableaux indexés 0..n). * @return array{state:string,message?:string,headers?:array,rows?:array} */ function fxImportResultatsParseFile($strPath, $strOriginalName = '') { $strExt = strtolower(pathinfo($strOriginalName !== '' ? $strOriginalName : $strPath, PATHINFO_EXTENSION)); if ($strExt === 'csv' || $strExt === 'txt') { return fxImportResultatsParseCsv($strPath); } if (in_array($strExt, array('xls', 'xlsx'), true)) { return fxImportResultatsParseExcel($strPath); } // Sniff CSV $strSample = @file_get_contents($strPath, false, null, 0, 2048); if (is_string($strSample) && (strpos($strSample, ',') !== false || strpos($strSample, ';') !== false)) { return fxImportResultatsParseCsv($strPath); } return array('state' => 'error', 'message' => 'Format non supporté (CSV, XLS, XLSX).'); } function fxImportResultatsDetectCsvDelimiter($strPath) { $strLine = ''; $fp = @fopen($strPath, 'rb'); if ($fp) { $strLine = (string)fgets($fp); fclose($fp); } $intSemi = substr_count($strLine, ';'); $intComma = substr_count($strLine, ','); $intTab = substr_count($strLine, "\t"); if ($intTab > $intSemi && $intTab > $intComma) { return "\t"; } return ($intSemi > $intComma) ? ';' : ','; } function fxImportResultatsParseCsv($strPath) { $strDelim = fxImportResultatsDetectCsvDelimiter($strPath); $fp = @fopen($strPath, 'rb'); if (!$fp) { return array('state' => 'error', 'message' => 'Impossible de lire le fichier.'); } // BOM UTF-8 $strBom = fread($fp, 3); if ($strBom !== "\xEF\xBB\xBF") { rewind($fp); } $tabHeaders = fgetcsv($fp, 0, $strDelim); if (!is_array($tabHeaders) || count($tabHeaders) < 1) { fclose($fp); return array('state' => 'error', 'message' => 'En-têtes manquants.'); } $tabHeaders = array_map('trim', $tabHeaders); $tabRows = array(); while (($tabRow = fgetcsv($fp, 0, $strDelim)) !== false) { if (!is_array($tabRow)) { continue; } // Ignorer lignes totalement vides $blnEmpty = true; foreach ($tabRow as $strCell) { if (trim((string)$strCell) !== '') { $blnEmpty = false; break; } } if ($blnEmpty) { continue; } // Aligner largeur while (count($tabRow) < count($tabHeaders)) { $tabRow[] = ''; } if (count($tabRow) > count($tabHeaders)) { $tabRow = array_slice($tabRow, 0, count($tabHeaders)); } $tabRows[] = $tabRow; } fclose($fp); return array('state' => 'ok', 'headers' => $tabHeaders, 'rows' => $tabRows); } function fxImportResultatsParseExcel($strPath) { $strPhpExcel = dirname(__FILE__) . '/../superadm/php/Classes/PHPExcel.php'; if (!is_file($strPhpExcel)) { $strPhpExcel = dirname(__FILE__) . '/Classes/PHPExcel.php'; } if (!is_file($strPhpExcel)) { return array('state' => 'error', 'message' => 'Lecteur Excel indisponible — utilisez un CSV.'); } require_once $strPhpExcel; try { $objBook = PHPExcel_IOFactory::load($strPath); $objSheet = $objBook->getActiveSheet(); $tabRaw = $objSheet->toArray(null, true, true, false); } catch (Exception $ex) { return array('state' => 'error', 'message' => 'Lecture Excel échouée : ' . $ex->getMessage()); } if (!is_array($tabRaw) || count($tabRaw) < 1) { return array('state' => 'error', 'message' => 'Fichier Excel vide.'); } $tabHeaders = array_map(function ($v) { return trim((string)$v); }, $tabRaw[0]); // Trim trailing empty headers while (count($tabHeaders) > 0 && end($tabHeaders) === '') { array_pop($tabHeaders); } if (count($tabHeaders) < 1) { return array('state' => 'error', 'message' => 'En-têtes manquants.'); } $tabRows = array(); for ($r = 1; $r < count($tabRaw); $r++) { $tabRow = $tabRaw[$r]; if (!is_array($tabRow)) { continue; } $tabRow = array_slice($tabRow, 0, count($tabHeaders)); while (count($tabRow) < count($tabHeaders)) { $tabRow[] = ''; } $blnEmpty = true; foreach ($tabRow as $strCell) { if (trim((string)$strCell) !== '') { $blnEmpty = false; break; } } if ($blnEmpty) { continue; } $tabRows[] = array_map(function ($v) { return trim((string)$v); }, $tabRow); } return array('state' => 'ok', 'headers' => $tabHeaders, 'rows' => $tabRows); } /** * Valeurs distinctes par colonne (capées). */ function fxImportResultatsDistincts(array $tabRows, $intColCount, $intMaxPerCol = 40) { $tabDistinct = array(); for ($c = 0; $c < $intColCount; $c++) { $tabDistinct[$c] = array(); } foreach ($tabRows as $tabRow) { for ($c = 0; $c < $intColCount; $c++) { $strVal = trim((string)($tabRow[$c] ?? '')); if ($strVal === '') { continue; } if (!isset($tabDistinct[$c][$strVal])) { if (count($tabDistinct[$c]) >= $intMaxPerCol) { continue; } $tabDistinct[$c][$strVal] = true; } } } $tabOut = array(); for ($c = 0; $c < $intColCount; $c++) { $tabOut[$c] = array_keys($tabDistinct[$c]); sort($tabOut[$c], SORT_NATURAL | SORT_FLAG_CASE); } return $tabOut; } function fxImportResultatsSaveUpload($intEveId, array $arrFile) { $intEveId = intval($intEveId); if ($intEveId <= 0) { return array('state' => 'error', 'message' => 'eve_id manquant'); } if (empty($arrFile['tmp_name']) || !is_uploaded_file($arrFile['tmp_name'])) { return array('state' => 'error', 'message' => 'Aucun fichier reçu'); } if (!empty($arrFile['error']) && intval($arrFile['error']) !== UPLOAD_ERR_OK) { return array('state' => 'error', 'message' => 'Erreur upload (code ' . intval($arrFile['error']) . ')'); } $strName = (string)($arrFile['name'] ?? 'import.csv'); $strToken = bin2hex(random_bytes(16)); $strSafeExt = strtolower(pathinfo($strName, PATHINFO_EXTENSION)); if (!in_array($strSafeExt, array('csv', 'txt', 'xls', 'xlsx'), true)) { $strSafeExt = 'csv'; } $strDest = fxImportResultatsUploadDir() . '/' . $intEveId . '_' . $strToken . '.' . $strSafeExt; if (!@move_uploaded_file($arrFile['tmp_name'], $strDest)) { return array('state' => 'error', 'message' => 'Échec enregistrement temporaire'); } $arrParsed = fxImportResultatsParseFile($strDest, $strName); if (($arrParsed['state'] ?? '') !== 'ok') { @unlink($strDest); return $arrParsed; } $tabHeaders = $arrParsed['headers']; $tabRows = $arrParsed['rows']; $strSig = fxImportResultatsHeaderSig($tabHeaders); $tabGuess = array(); foreach ($tabHeaders as $strH) { $tabGuess[] = fxImportResultatsGuessField($strH); } $tabSamples = array_slice($tabRows, 0, 8); // Retirer d'éventuelles lignes « en-tête » encore présentes dans les données $tabIdxFilter = array(); foreach ($tabHeaders as $intHi => $strH) { $strG = fxImportResultatsGuessField($strH); if ($strG !== '') { $tabIdxFilter[$strG] = $intHi; } } $tabRowsFiltered = array(); foreach ($tabRows as $tabRow) { if (fxImportResultatsRowLooksLikeHeader($tabRow, $tabHeaders, $tabIdxFilter)) { continue; } $tabRowsFiltered[] = $tabRow; } $tabRows = $tabRowsFiltered; $tabSamples = array_slice($tabRows, 0, 8); $tabDistinct = fxImportResultatsDistincts($tabRows, count($tabHeaders)); // Meta JSON à côté du fichier $arrMeta = array( 'eve_id' => $intEveId, 'token' => $strToken, 'original_name' => $strName, 'path' => $strDest, 'header_sig' => $strSig, 'headers' => $tabHeaders, 'row_count' => count($tabRows), 'created_at' => date('c'), ); @file_put_contents($strDest . '.meta.json', json_encode($arrMeta, JSON_UNESCAPED_UNICODE)); $tabTemplates = fxImportResultatsFindTemplates($intEveId, $strSig); return array( 'state' => 'ok', 'token' => $strToken, 'original_name' => $strName, 'header_sig' => $strSig, 'headers' => $tabHeaders, 'guess' => $tabGuess, 'row_count' => count($tabRows), 'samples' => $tabSamples, 'distincts' => $tabDistinct, 'templates' => $tabTemplates, 'epreuves' => fxImportResultatsEpreuvesList($intEveId), 'questions' => fxImportResultatsQuestionsList($intEveId), 'fields' => fxImportResultatsTargetFields(), ); } function fxImportResultatsResolveTokenPath($intEveId, $strToken) { $intEveId = intval($intEveId); $strToken = preg_replace('/[^a-f0-9]/', '', strtolower((string)$strToken)); if ($intEveId <= 0 || strlen($strToken) < 16) { return null; } $strDir = fxImportResultatsUploadDir(); $tabMatches = glob($strDir . '/' . $intEveId . '_' . $strToken . '.*'); if (!is_array($tabMatches)) { return null; } foreach ($tabMatches as $strPath) { if (substr($strPath, -10) === '.meta.json') { continue; } if (is_file($strPath)) { return $strPath; } } return null; } function fxImportResultatsFindTemplates($intEveId, $strSig) { global $objDatabase; $intEveId = intval($intEveId); $strSig = preg_replace('/[^a-f0-9]/', '', strtolower((string)$strSig)); $intOrgId = fxImportResultatsComIdForEve($intEveId); // Table absente (SQL pas encore joué) → pas d'erreur HTML dans le JSON AJAX $sqlCheck = "SELECT 1 AS ok FROM information_schema.TABLES" . " WHERE TABLE_SCHEMA = DATABASE()" . " AND TABLE_NAME = 'inscriptions_import_templates' LIMIT 1"; $tabCheck = $objDatabase->fxGetResults($sqlCheck); if (!is_array($tabCheck) || empty($tabCheck[1]['ok'])) { return array(); } $sql = "SELECT tpl_id, eve_id, com_id, tpl_name, tpl_header_sig, tpl_mapping_json, tpl_maj" . " FROM inscriptions_import_templates" . " WHERE tpl_actif = 1 AND tpl_header_sig = '" . addslashes($strSig) . "'" . " AND (eve_id = " . $intEveId . ($intOrgId > 0 ? (" OR com_id = " . $intOrgId) : '') . ")" . " ORDER BY (eve_id = " . $intEveId . ") DESC, tpl_maj DESC LIMIT 20"; $tab = $objDatabase->fxGetResults($sql); $tabOut = array(); if (!is_array($tab)) { return $tabOut; } for ($i = 1; $i <= count($tab); $i++) { $arrMap = json_decode((string)($tab[$i]['tpl_mapping_json'] ?? ''), true); $tabOut[] = array( 'tpl_id' => intval($tab[$i]['tpl_id']), 'eve_id' => intval($tab[$i]['eve_id']), 'com_id' => intval($tab[$i]['com_id']), 'name' => (string)$tab[$i]['tpl_name'], 'maj' => (string)$tab[$i]['tpl_maj'], 'mapping' => is_array($arrMap) ? $arrMap : array(), 'same_event' => (intval($tab[$i]['eve_id']) === $intEveId), ); } return $tabOut; } /** * @param array $arrMapping { columns: string[], epr_values: {value: epr_id}, questions: {colIndex: que_id|"new"} } */ function fxImportResultatsSaveTemplate($intEveId, $strToken, $strName, array $arrMapping, $intUsaId, $blnShareCom = true) { global $objDatabase; $intEveId = intval($intEveId); $strPath = fxImportResultatsResolveTokenPath($intEveId, $strToken); if ($strPath === null) { return array('state' => 'error', 'message' => 'Fichier temporaire introuvable — réuploadez.'); } $strMetaPath = $strPath . '.meta.json'; $arrMeta = array(); if (is_file($strMetaPath)) { $arrMeta = json_decode((string)@file_get_contents($strMetaPath), true); if (!is_array($arrMeta)) { $arrMeta = array(); } } $tabHeaders = isset($arrMeta['headers']) && is_array($arrMeta['headers']) ? $arrMeta['headers'] : array(); $strSig = isset($arrMeta['header_sig']) ? (string)$arrMeta['header_sig'] : fxImportResultatsHeaderSig($tabHeaders); if ($tabHeaders === array()) { $arrParsed = fxImportResultatsParseFile($strPath); if (($arrParsed['state'] ?? '') !== 'ok') { return $arrParsed; } $tabHeaders = $arrParsed['headers']; $strSig = fxImportResultatsHeaderSig($tabHeaders); } $strName = trim((string)$strName); if ($strName === '') { $strName = 'Template ' . date('Y-m-d H:i'); } $intComId = $blnShareCom ? fxImportResultatsComIdForEve($intEveId) : 0; $strNow = date('Y-m-d H:i:s'); $strHeadersJson = json_encode($tabHeaders, JSON_UNESCAPED_UNICODE); $strMappingJson = json_encode($arrMapping, JSON_UNESCAPED_UNICODE); if ($strHeadersJson === false || $strMappingJson === false) { return array('state' => 'error', 'message' => 'JSON mapping invalide'); } // Upsert soft : même eve + même sig + même nom → update $sqlFind = "SELECT tpl_id FROM inscriptions_import_templates" . " WHERE eve_id = " . $intEveId . " AND tpl_header_sig = '" . addslashes($strSig) . "'" . " AND tpl_name = '" . addslashes($strName) . "'" . " AND tpl_actif = 1 LIMIT 1"; $tabFind = $objDatabase->fxGetResults($sqlFind); if (is_array($tabFind) && !empty($tabFind[1]['tpl_id'])) { $intTplId = intval($tabFind[1]['tpl_id']); $sql = "UPDATE inscriptions_import_templates SET" . " com_id = " . intval($intComId) . "," . " tpl_headers_json = '" . addslashes($strHeadersJson) . "'," . " tpl_mapping_json = '" . addslashes($strMappingJson) . "'," . " tpl_usa_id = " . intval($intUsaId) . "," . " tpl_maj = '" . addslashes($strNow) . "'" . " WHERE tpl_id = " . $intTplId; $objDatabase->fxQuery($sql); } else { $sql = "INSERT INTO inscriptions_import_templates" . " (eve_id, com_id, tpl_name, tpl_header_sig, tpl_headers_json, tpl_mapping_json, tpl_actif, tpl_usa_id, tpl_creation, tpl_maj)" . " VALUES (" . intval($intEveId) . ", " . intval($intComId) . ", " . "'" . addslashes($strName) . "', " . "'" . addslashes($strSig) . "', " . "'" . addslashes($strHeadersJson) . "', " . "'" . addslashes($strMappingJson) . "', " . "1, " . intval($intUsaId) . ", " . "'" . addslashes($strNow) . "', " . "'" . addslashes($strNow) . "'" . ")"; $objDatabase->fxQuery($sql); $intTplId = intval($objDatabase->fxGetLastId()); } return array( 'state' => 'ok', 'tpl_id' => $intTplId, 'message' => 'Template enregistré — le prochain fichier avec les mêmes colonnes le proposera.', 'templates' => fxImportResultatsFindTemplates($intEveId, $strSig), ); } /** * Validation mapping (sans écriture resultats). */ function fxImportResultatsValidateMapping($intEveId, $strToken, array $arrMapping) { $intEveId = intval($intEveId); $strPath = fxImportResultatsResolveTokenPath($intEveId, $strToken); if ($strPath === null) { return array('state' => 'error', 'message' => 'Fichier temporaire introuvable — réuploadez.'); } $arrParsed = fxImportResultatsParseFile($strPath); if (($arrParsed['state'] ?? '') !== 'ok') { return $arrParsed; } $tabColumns = isset($arrMapping['columns']) && is_array($arrMapping['columns']) ? $arrMapping['columns'] : array(); $tabEprValues = isset($arrMapping['epr_values']) && is_array($arrMapping['epr_values']) ? $arrMapping['epr_values'] : array(); $blnHasExt = false; $blnHasEpr = false; $intEprCol = -1; foreach ($tabColumns as $intIdx => $strKey) { $strKey = (string)$strKey; if ($strKey === 'par_external_id') { $blnHasExt = true; } if ($strKey === 'epr') { $blnHasEpr = true; $intEprCol = intval($intIdx); } } $tabErrors = array(); $tabWarn = array(); if (!$blnHasExt) { $tabErrors[] = 'Mappez une colonne « Clé unique » (indispensable pour ChronoTrack / réimport).'; } if (!$blnHasEpr) { $tabErrors[] = 'Mappez une colonne « Épreuve ».'; } else { $tabDistinct = fxImportResultatsDistincts($arrParsed['rows'], count($arrParsed['headers'])); $tabVals = $tabDistinct[$intEprCol] ?? array(); $intUnmapped = 0; foreach ($tabVals as $strVal) { $intEpr = intval($tabEprValues[$strVal] ?? 0); if ($intEpr <= 0) { $intUnmapped++; } } if ($intUnmapped > 0) { $tabErrors[] = $intUnmapped . ' valeur(s) d\'épreuve sans correspondance MS1.'; } } if (!in_array('par_prenom', $tabColumns, true) && !in_array('par_nom', $tabColumns, true)) { $tabWarn[] = 'Ni prénom ni nom mappé — vérifiez le fichier.'; } if (count($tabErrors) > 0) { return array( 'state' => 'error', 'message' => implode(' ', $tabErrors), 'errors' => $tabErrors, 'warnings' => $tabWarn, 'row_count' => count($arrParsed['rows']), ); } return array( 'state' => 'ok', 'message' => 'Mapping prêt — ' . count($arrParsed['rows']) . ' ligne(s). Vous pouvez importer.', 'warnings' => $tabWarn, 'row_count' => count($arrParsed['rows']), 'commit_ready' => true, ); } function fxImportResultatsDbEsc($str) { global $objDatabase; if (isset($objDatabase) && is_object($objDatabase) && method_exists($objDatabase, 'fxEscape')) { return $objDatabase->fxEscape((string)$str); } return addslashes((string)$str); } /** * MSIN-4482 — faux no_panier / no_commande uniques (varchar 20). * Obligatoire : index UNIQUE sur ces champs → '' ne peut exister qu'une fois. */ function fxImportResultatsSyntheticPanier($intEveId, $strExternalId) { $str = 'I' . intval($intEveId) . 'X' . substr(md5((string)$strExternalId), 0, 12); return substr($str, 0, 20); } /** * MSIN-4482 — pec_id_original / par_id_original ont un UNIQUE : jamais inserer 0. * Valeur temporaire unique, puis UPDATE = vrai id apres INSERT. */ function fxImportResultatsTempOriginalId($strExternalId, $strSalt = '') { $intCrc = sprintf('%u', crc32((string)$strExternalId . '|' . (string)$strSalt)); // Plage haute pour limiter collision avec vrais pec_id / par_id historiques return 1900000000 + intval($intCrc % 90000000); } /** * MSIN-4482 — même modèle que fxChronotrackApiSyncGroupBlockedByType. */ function fxImportResultatsGroupBlockedByType(array $tabBlocked) { $tabTypeLabels = array( 'header_row' => 'Ligne en-tête (ignorée)', 'no_external' => 'Clé unique manquante', 'no_race' => 'Épreuve non mappée', 'insert_pec' => 'Échec création commande (pec)', 'insert_par' => 'Échec création participant', 'update_fail' => 'Échec mise à jour', 'other' => 'Autre', ); $tabTypeOrder = array( 'no_external', 'no_race', 'insert_pec', 'insert_par', 'update_fail', 'header_row', 'other', ); $tabGroups = array(); foreach ($tabBlocked as $arrItem) { $strCode = (string)($arrItem['code'] ?? 'other'); if (!isset($tabGroups[$strCode])) { $tabGroups[$strCode] = array( 'code' => $strCode, 'label' => isset($tabTypeLabels[$strCode]) ? $tabTypeLabels[$strCode] : $strCode, 'items' => array(), ); } $tabGroups[$strCode]['items'][] = $arrItem; } $tabOrdered = array(); foreach ($tabTypeOrder as $strCode) { if (isset($tabGroups[$strCode])) { $tabOrdered[] = $tabGroups[$strCode]; unset($tabGroups[$strCode]); } } foreach ($tabGroups as $arrGroup) { $tabOrdered[] = $arrGroup; } return $tabOrdered; } function fxImportResultatsRowLabel(array $tabRow, array $tabColIndex) { $strPrenom = isset($tabColIndex['par_prenom']) ? trim((string)($tabRow[$tabColIndex['par_prenom']] ?? '')) : ''; $strNom = isset($tabColIndex['par_nom']) ? trim((string)($tabRow[$tabColIndex['par_nom']] ?? '')) : ''; $str = trim($strPrenom . ' ' . $strNom); return ($str !== '') ? $str : '—'; } function fxImportResultatsDbError() { global $objDatabase; if (isset($objDatabase) && is_object($objDatabase) && isset($objDatabase->con)) { $str = mysqli_error($objDatabase->con); if (is_string($str) && $str !== '') { return $str; } } return ''; } /** MSIN-4482 — Male/Female/Other → M/F/N (aligné CT). */ function fxImportResultatsNormalizeSex($str) { $str = trim((string)$str); $strLow = mb_strtolower($str, 'UTF-8'); if ($strLow === 'f' || $strLow === 'female' || $strLow === 'féminin' || $strLow === 'feminin' || $strLow === 'femme') { return 'F'; } if ($strLow === 'm' || $strLow === 'h' || $strLow === 'male' || $strLow === 'masculin' || $strLow === 'homme') { return 'M'; } if ($strLow === 'other' || $strLow === 'autre' || $strLow === 'n' || $strLow === 'x') { return 'N'; } if ($str === 'F' || $str === 'M' || $str === 'N' || $str === 'H') { return ($str === 'H') ? 'M' : $str; } return ''; } /** MSIN-4482 — parse dates type 5/24/94 → Y-m-d. */ function fxImportResultatsNormalizeBirthdate($str) { $str = trim((string)$str); if ($str === '' || $str === '0000-00-00') { return ''; } if (preg_match('/^(\d{4})-(\d{2})-(\d{2})/', $str, $m)) { return $m[1] . '-' . $m[2] . '-' . $m[3]; } // m/d/y ou m/d/yyyy if (preg_match('/^(\d{1,2})[\/\-.](\d{1,2})[\/\-.](\d{2,4})$/', $str, $m)) { $intM = intval($m[1]); $intD = intval($m[2]); $intY = intval($m[3]); if ($intY < 100) { $intY += ($intY >= 70) ? 1900 : 2000; } if (checkdate($intM, $intD, $intY)) { return sprintf('%04d-%02d-%02d', $intY, $intM, $intD); } // essai j/m/a if (checkdate($intD, $intM, $intY)) { return sprintf('%04d-%02d-%02d', $intY, $intD, $intM); } } $intTs = strtotime($str); if ($intTs !== false) { return date('Y-m-d', $intTs); } return ''; } /** * Résout que_id pour colonnes questions (match existant ou création confirmée). * @return array colIndex => que_id */ function fxImportResultatsResolveQuestionIds($intEveId, array $tabHeaders, array $tabQuestionsMap) { global $objDatabase; $intEveId = intval($intEveId); $tabOut = array(); foreach ($tabQuestionsMap as $strCol => $strQue) { $intCol = intval($strCol); $strQue = trim((string)$strQue); if ($strQue === '' || $strQue === '0') { continue; } if ($strQue === 'new') { $strLabel = trim((string)($tabHeaders[$intCol] ?? ('Question ' . ($intCol + 1)))); if (mb_strlen($strLabel, 'UTF-8') > 100) { $strLabel = mb_substr($strLabel, 0, 97, 'UTF-8') . '...'; } // Réutiliser si même libellé déjà créé sur l'événement $sqlFind = "SELECT que_id FROM inscriptions_questions" . " WHERE eve_id = " . $intEveId . " AND que_question_fr = '" . fxImportResultatsDbEsc($strLabel) . "'" . " LIMIT 1"; $tabFind = $objDatabase->fxGetResults($sqlFind); if (is_array($tabFind) && !empty($tabFind[1]['que_id'])) { $tabOut[(string)$intCol] = intval($tabFind[1]['que_id']); continue; } $sqlIns = "INSERT INTO inscriptions_questions SET" . " eve_id = " . $intEveId . "," . " que_question_fr = '" . fxImportResultatsDbEsc($strLabel) . "'," . " que_question_en = '" . fxImportResultatsDbEsc($strLabel) . "'," . " que_choix_fr = ''," . " que_choix_en = ''," . " que_liste = 0," . " que_epreuves_incluses = ''," . " que_required = 0," . " que_tri = 900," . " que_participant_1 = 1," . " que_modifiable = 1," . " que_actif = 1," . " que_hidden = 0," . " que_cart_show = 1," . " que_cart_label_fr = '" . fxImportResultatsDbEsc($strLabel) . "'," . " que_cart_label_en = '" . fxImportResultatsDbEsc($strLabel) . "'"; if ($objDatabase->fxQuery($sqlIns) === false) { continue; } $tabOut[(string)$intCol] = intval($objDatabase->fxGetLastId()); continue; } $intQueId = intval($strQue); if ($intQueId > 0) { $tabOut[(string)$intCol] = $intQueId; } } fxImportResultatsEnsureQuestionsVisibleFiche($intEveId, $tabOut); return $tabOut; } /** * MSIN-4482 — la fiche exige que_cart_show=1 ; activer les questions utilisées à l'import * (matchées OU créées), sans toucher aux autres événements. */ function fxImportResultatsEnsureQuestionsVisibleFiche($intEveId, array $tabQueIdsByCol) { global $objDatabase; $intEveId = intval($intEveId); $tabIds = array(); foreach ($tabQueIdsByCol as $mixId) { $intId = intval($mixId); if ($intId > 0) { $tabIds[$intId] = true; } } if ($intEveId <= 0 || count($tabIds) < 1) { return; } $strIn = implode(',', array_map('intval', array_keys($tabIds))); $objDatabase->fxQuery( "UPDATE inscriptions_questions SET" . " que_cart_show = 1," . " que_cart_label_fr = IF(TRIM(IFNULL(que_cart_label_fr, '')) = '', que_question_fr, que_cart_label_fr)," . " que_cart_label_en = IF(" . " TRIM(IFNULL(que_cart_label_en, '')) = ''," . " IF(TRIM(IFNULL(que_question_en, '')) = '', que_question_fr, que_question_en)," . " que_cart_label_en" . " )" . " WHERE eve_id = " . $intEveId . " AND que_id IN (" . $strIn . ")" ); } function fxImportResultatsSaveQuestionAnswer($intParIdOriginal, $intPecId, $intQueId, $strHeader, $strValue) { global $objDatabase; $intParIdOriginal = intval($intParIdOriginal); $intPecId = intval($intPecId); $intQueId = intval($intQueId); $strValue = trim((string)$strValue); $strHeader = trim((string)$strHeader); if ($intQueId <= 0 || $intPecId <= 0) { return; } $strNow = function_exists('fxGetDateTime') ? fxGetDateTime() : date('Y-m-d H:i:s'); $sqlFind = "SELECT pqu_id FROM resultats_questions" . " WHERE pec_id = " . $intPecId . " AND que_id = " . $intQueId . " AND par_id = " . $intParIdOriginal . " LIMIT 1"; $tabFind = $objDatabase->fxGetResults($sqlFind); if (is_array($tabFind) && !empty($tabFind[1]['pqu_id'])) { $sql = "UPDATE resultats_questions SET" . " que_choix_fr = '" . fxImportResultatsDbEsc($strValue) . "'," . " que_choix_en = '" . fxImportResultatsDbEsc($strValue) . "'," . " pqu_maj = '" . fxImportResultatsDbEsc($strNow) . "'" . " WHERE pqu_id = " . intval($tabFind[1]['pqu_id']); $objDatabase->fxQuery($sql); @$objDatabase->fxQuery( "UPDATE resultats_questions SET que_actif = 1 WHERE pqu_id = " . intval($tabFind[1]['pqu_id']) ); return; } $sql = "INSERT INTO resultats_questions SET" . " par_id = " . $intParIdOriginal . "," . " pec_id = " . $intPecId . "," . " que_id = " . $intQueId . "," . " que_question_fr = '" . fxImportResultatsDbEsc($strHeader) . "'," . " que_question_en = '" . fxImportResultatsDbEsc($strHeader) . "'," . " que_choix_fr = '" . fxImportResultatsDbEsc($strValue) . "'," . " que_choix_en = '" . fxImportResultatsDbEsc($strValue) . "'," . " que_modifiable = 1," . " pqu_note = ''," . " pqu_maj = '" . fxImportResultatsDbEsc($strNow) . "'," . " no_panier = ''," . " no_commande = ''," . " pqu_id_original = 0"; if ($objDatabase->fxQuery($sql) === false) { return; } $intPqu = intval($objDatabase->fxGetLastId()); if ($intPqu > 0) { @$objDatabase->fxQuery( "UPDATE resultats_questions SET que_actif = 1 WHERE pqu_id = " . $intPqu ); } } /** * MSIN-4482 — détecte une ligne qui n'est que la répétition des en-têtes. */ function fxImportResultatsRowLooksLikeHeader(array $tabRow, array $tabHeaders, array $tabColIndex) { $strPrenom = isset($tabColIndex['par_prenom']) ? trim((string)($tabRow[$tabColIndex['par_prenom']] ?? '')) : ''; $strNom = isset($tabColIndex['par_nom']) ? trim((string)($tabRow[$tabColIndex['par_nom']] ?? '')) : ''; $strExt = isset($tabColIndex['par_external_id']) ? trim((string)($tabRow[$tabColIndex['par_external_id']] ?? '')) : ''; $strPrenomLow = fxImportResultatsNormalizeHeader($strPrenom); $strNomLow = fxImportResultatsNormalizeHeader($strNom); $strExtLow = fxImportResultatsNormalizeHeader($strExt); // Cas classique export EN / FR if ($strPrenomLow === 'first name' || $strPrenomLow === 'prenom' || $strPrenomLow === 'prénom') { return true; } if ($strNomLow === 'last name' || $strNomLow === 'nom') { return true; } if ($strExtLow === 'attendee #' || $strExtLow === 'attendee' || $strExtLow === 'external id' || $strExtLow === 'clé unique' || $strExtLow === 'cle unique') { return true; } // Ligne entière = en-têtes $intMatch = 0; $intCompare = 0; $intN = min(count($tabRow), count($tabHeaders)); for ($i = 0; $i < $intN; $i++) { $strCell = fxImportResultatsNormalizeHeader($tabRow[$i] ?? ''); $strH = fxImportResultatsNormalizeHeader($tabHeaders[$i] ?? ''); if ($strH === '') { continue; } $intCompare++; if ($strCell === $strH) { $intMatch++; } } if ($intCompare >= 3 && $intMatch >= max(3, (int)floor($intCompare * 0.7))) { return true; } return false; } /** * MSIN-4482 — écriture resultats_* (sans panier), upsert sur par_external_id. */ function fxImportResultatsCommit($intEveId, $strToken, array $arrMapping) { global $objDatabase; @set_time_limit(300); $arrVal = fxImportResultatsValidateMapping($intEveId, $strToken, $arrMapping); if (($arrVal['state'] ?? '') !== 'ok') { return $arrVal; } $intEveId = intval($intEveId); $strPath = fxImportResultatsResolveTokenPath($intEveId, $strToken); $arrParsed = fxImportResultatsParseFile($strPath); if (($arrParsed['state'] ?? '') !== 'ok') { return $arrParsed; } // Colonnes requises pour l'import $sqlCol = "SELECT COUNT(*) AS n FROM information_schema.COLUMNS" . " WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'resultats_participants'" . " AND COLUMN_NAME = 'par_external_id'"; $tabCol = $objDatabase->fxGetResults($sqlCol); if (!is_array($tabCol) || intval($tabCol[1]['n'] ?? 0) < 1) { return array( 'state' => 'error', 'message' => 'Colonnes import absentes — exécuter sql/MSIN-4482-import-resultats-templates.sql en dev préprod.', ); } $tabHeaders = $arrParsed['headers']; $tabRows = $arrParsed['rows']; $tabColumns = isset($arrMapping['columns']) && is_array($arrMapping['columns']) ? $arrMapping['columns'] : array(); $tabEprValues = isset($arrMapping['epr_values']) && is_array($arrMapping['epr_values']) ? $arrMapping['epr_values'] : array(); $tabQuestionsMap = isset($arrMapping['questions']) && is_array($arrMapping['questions']) ? $arrMapping['questions'] : array(); $tabColIndex = array(); foreach ($tabColumns as $intIdx => $strKey) { $strKey = (string)$strKey; if ($strKey === '') { continue; } $tabColIndex[$strKey] = intval($intIdx); } $tabQueIds = fxImportResultatsResolveQuestionIds($intEveId, $tabHeaders, $tabQuestionsMap); $strNow = function_exists('fxGetDateTime') ? fxGetDateTime() : date('Y-m-d H:i:s'); $strOrigine = MSIN_IMPORT_RESULTATS_ORIGIN; $intCreated = 0; $intUpdated = 0; $tabBlocked = array(); foreach ($tabRows as $intRowNum => $tabRow) { $intLine = $intRowNum + 2; // +1 header Excel, +1 index 0 $strNamePreview = fxImportResultatsRowLabel($tabRow, $tabColIndex); $strBibPreview = isset($tabColIndex['no_bib']) ? trim((string)($tabRow[$tabColIndex['no_bib']] ?? '')) : ''; $strSexePreview = ''; if (isset($tabColIndex['par_sexe'])) { $strSexePreview = fxImportResultatsNormalizeSex(trim((string)($tabRow[$tabColIndex['par_sexe']] ?? ''))); } if (fxImportResultatsRowLooksLikeHeader($tabRow, $tabHeaders, $tabColIndex)) { $tabBlocked[] = array( 'code' => 'header_row', 'message' => 'Ligne d\'en-tête détectée', 'line' => $intLine, 'name' => $strNamePreview, 'external_id' => '', 'no_bib' => $strBibPreview, 'par_sexe' => $strSexePreview, ); continue; } $strExt = ''; if (isset($tabColIndex['par_external_id'])) { $strExt = trim((string)($tabRow[$tabColIndex['par_external_id']] ?? '')); } if ($strExt === '') { $tabBlocked[] = array( 'code' => 'no_external', 'message' => 'Clé unique (Attendee # / external_id) vide', 'line' => $intLine, 'name' => $strNamePreview, 'external_id' => '', 'no_bib' => $strBibPreview, 'par_sexe' => $strSexePreview, ); continue; } $strEprRaw = ''; if (isset($tabColIndex['epr'])) { $strEprRaw = trim((string)($tabRow[$tabColIndex['epr']] ?? '')); } $intEprId = intval($tabEprValues[$strEprRaw] ?? 0); if ($intEprId <= 0) { $tabBlocked[] = array( 'code' => 'no_race', 'message' => ($strEprRaw !== '' ? ('« ' . $strEprRaw . ' » non mappée vers une épreuve MS1') : 'Valeur d\'épreuve vide'), 'line' => $intLine, 'name' => $strNamePreview, 'external_id' => $strExt, 'no_bib' => $strBibPreview, 'par_sexe' => $strSexePreview, ); continue; } $arrFields = array( 'par_prenom' => '', 'par_nom' => '', 'par_sexe' => '', 'par_naissance' => '', 'par_age' => 0, 'par_courriel' => '', 'par_telephone1' => '', 'par_adresse' => '', 'par_ville' => '', 'par_codepostal' => '', 'no_bib' => '', 'par_nom_equipe' => '', 'par_no_equipe' => 0, 'par_contact_urgence_nom' => '', 'par_contact_urgence_telephone' => '', ); foreach ($arrFields as $strKey => $mixDefault) { if (!isset($tabColIndex[$strKey])) { continue; } $strRaw = trim((string)($tabRow[$tabColIndex[$strKey]] ?? '')); if ($strKey === 'par_sexe') { $arrFields[$strKey] = fxImportResultatsNormalizeSex($strRaw); } elseif ($strKey === 'par_naissance') { $arrFields[$strKey] = fxImportResultatsNormalizeBirthdate($strRaw); } elseif ($strKey === 'par_age' || $strKey === 'par_no_equipe') { $arrFields[$strKey] = intval($strRaw); } else { $arrFields[$strKey] = $strRaw; } } if (intval($arrFields['par_age']) <= 0 && $arrFields['par_naissance'] !== '') { $intTs = strtotime($arrFields['par_naissance']); if ($intTs !== false) { $arrFields['par_age'] = intval(floor((time() - $intTs) / 31557600)); } } // Upsert participant import $sqlExist = "SELECT par_id, pec_id, par_id_original FROM resultats_participants" . " WHERE eve_id = " . $intEveId . " AND par_external_id = '" . fxImportResultatsDbEsc($strExt) . "'" . " LIMIT 1"; $tabExist = $objDatabase->fxGetResults($sqlExist); $blnUpdate = (is_array($tabExist) && !empty($tabExist[1]['par_id'])); if ($blnUpdate) { $intParId = intval($tabExist[1]['par_id']); $intPecId = intval($tabExist[1]['pec_id']); $intParOrig = intval($tabExist[1]['par_id_original']); if ($intParOrig <= 0) { $intParOrig = $intParId; } if ($intPecId > 0) { // p.pec_id peut être e.pec_id OU e.pec_id_original (selon imports précédents) $tabPecRow = $objDatabase->fxGetResults( "SELECT pec_id, pec_id_original FROM resultats_epreuves_commandees" . " WHERE eve_id = " . $intEveId . " AND (pec_id = " . $intPecId . " OR pec_id_original = " . $intPecId . ")" . " LIMIT 1" ); $intPecPk = is_array($tabPecRow) ? intval($tabPecRow[1]['pec_id'] ?? 0) : 0; $intPecOrigNow = is_array($tabPecRow) ? intval($tabPecRow[1]['pec_id_original'] ?? 0) : 0; if ($intPecPk > 0) { $objDatabase->fxQuery( "UPDATE resultats_epreuves_commandees SET" . " epr_id = " . $intEprId . "," . " pec_actif = 1," . " pec_maj = '" . fxImportResultatsDbEsc($strNow) . "'" . " WHERE pec_id = " . $intPecPk ); // Tenter pec_id_original = pec_id seulement si UNIQUE libre @$objDatabase->fxQuery( "UPDATE resultats_epreuves_commandees e SET pec_id_original = " . $intPecPk . " WHERE pec_id = " . $intPecPk . " AND pec_id_original <> " . $intPecPk . " AND NOT EXISTS (" . " SELECT 1 FROM (" . " SELECT pec_id FROM resultats_epreuves_commandees" . " WHERE pec_id_original = " . $intPecPk . " AND pec_id <> " . $intPecPk . " ) _c" . " )" ); $tabPecNow = $objDatabase->fxGetResults( "SELECT pec_id_original FROM resultats_epreuves_commandees WHERE pec_id = " . $intPecPk . " LIMIT 1" ); $intPecOrigNow = is_array($tabPecNow) ? intval($tabPecNow[1]['pec_id_original'] ?? 0) : $intPecOrigNow; if ($intPecOrigNow > 0) { $objDatabase->fxQuery( "UPDATE resultats_participants SET pec_id = " . $intPecOrigNow . " WHERE par_id = " . $intParId ); $intPecId = $intPecOrigNow; // clé liste / questions } } } $sqlUp = "UPDATE resultats_participants SET" . " epr_id = " . $intEprId . "," . " par_prenom = '" . fxImportResultatsDbEsc($arrFields['par_prenom']) . "'," . " par_nom = '" . fxImportResultatsDbEsc($arrFields['par_nom']) . "'," . " par_sexe = '" . fxImportResultatsDbEsc($arrFields['par_sexe']) . "'," . " par_naissance = " . ($arrFields['par_naissance'] !== '' ? ("'" . fxImportResultatsDbEsc($arrFields['par_naissance']) . "'") : "NULL") . "," . " par_age = " . intval($arrFields['par_age']) . "," . " par_courriel = '" . fxImportResultatsDbEsc($arrFields['par_courriel']) . "'," . " par_telephone1 = '" . fxImportResultatsDbEsc($arrFields['par_telephone1']) . "'," . " par_adresse = '" . fxImportResultatsDbEsc($arrFields['par_adresse']) . "'," . " par_ville = '" . fxImportResultatsDbEsc($arrFields['par_ville']) . "'," . " par_codepostal = '" . fxImportResultatsDbEsc($arrFields['par_codepostal']) . "'," . " no_bib = '" . fxImportResultatsDbEsc($arrFields['no_bib']) . "'," . " par_nom_equipe = '" . fxImportResultatsDbEsc($arrFields['par_nom_equipe']) . "'," . " par_no_equipe = " . intval($arrFields['par_no_equipe']) . "," . " par_origine = '" . fxImportResultatsDbEsc($strOrigine) . "'," . " par_maj = '" . fxImportResultatsDbEsc($strNow) . "'" . " WHERE par_id = " . $intParId; if ($objDatabase->fxQuery($sqlUp) === false) { $strErr = fxImportResultatsDbError(); $tabBlocked[] = array( 'code' => 'update_fail', 'message' => 'Échec UPDATE' . ($strErr !== '' ? (' — ' . $strErr) : ''), 'line' => $intLine, 'name' => $strNamePreview, 'external_id' => $strExt, 'no_bib' => $strBibPreview, 'par_sexe' => $strSexePreview, 'par_id' => $intParId, ); continue; } // urgence si colonnes présentes (ignorer erreur silencieuse) $sqlUrg = "UPDATE resultats_participants SET" . " par_contact_urgence_nom = '" . fxImportResultatsDbEsc($arrFields['par_contact_urgence_nom']) . "'," . " par_contact_urgence_telephone = '" . fxImportResultatsDbEsc($arrFields['par_contact_urgence_telephone']) . "'" . " WHERE par_id = " . $intParId; @$objDatabase->fxQuery($sqlUrg); $intUpdated++; } else { // MSIN-4482 — identifiants techniques uniques (pas un vrai panier/paiement) $strNoPanier = fxImportResultatsSyntheticPanier($intEveId, $strExt); $strNoCommande = $strNoPanier; $intTempPecOrig = fxImportResultatsTempOriginalId($strExt, 'pec-' . $intEveId); $sqlPec = "INSERT INTO resultats_epreuves_commandees SET" . " eve_id = " . $intEveId . "," . " epr_id = " . $intEprId . "," . " no_panier = '" . fxImportResultatsDbEsc($strNoPanier) . "'," . " pec_label = ''," . " pec_nom_equipe = '" . fxImportResultatsDbEsc($arrFields['par_nom_equipe']) . "'," . " pec_equipe = 0," . " com_id = 0," . " pec_maj = '" . fxImportResultatsDbEsc($strNow) . "'," . " pec_id_original = " . intval($intTempPecOrig) . "," . " no_commande = '" . fxImportResultatsDbEsc($strNoCommande) . "'," . " is_cancelled = 0," . " pec_actif = 1"; if ($objDatabase->fxQuery($sqlPec) === false) { $strErr = fxImportResultatsDbError(); $tabBlocked[] = array( 'code' => 'insert_pec', 'message' => 'Échec INSERT pec' . ($strErr !== '' ? (' — ' . $strErr) : ''), 'line' => $intLine, 'name' => $strNamePreview, 'external_id' => $strExt, 'no_bib' => $strBibPreview, 'par_sexe' => $strSexePreview, ); continue; } $intPecId = intval($objDatabase->fxGetLastId()); // MSIN-4482 — UNIQUE pec_id_original : n'écraser le temp que si pec_id est libre $blnOrigOk = false; $qryOrig = $objDatabase->fxQuery( "UPDATE resultats_epreuves_commandees SET pec_id_original = " . $intPecId . " WHERE pec_id = " . $intPecId . " AND NOT EXISTS (" . " SELECT 1 FROM (" . " SELECT pec_id FROM resultats_epreuves_commandees" . " WHERE pec_id_original = " . $intPecId . " AND pec_id <> " . $intPecId . " ) _c" . " )" ); if ($qryOrig !== false) { $tabChk = $objDatabase->fxGetResults( "SELECT pec_id_original FROM resultats_epreuves_commandees WHERE pec_id = " . $intPecId . " LIMIT 1" ); $blnOrigOk = (is_array($tabChk) && intval($tabChk[1]['pec_id_original'] ?? 0) === $intPecId); } // Lien participant pour la liste : toujours = pec_id_original effectif $intPecLink = $blnOrigOk ? $intPecId : intval($intTempPecOrig); // Questions + participant : clé = pec_id_original (join liste) $intPecId = $intPecLink; $intTempParOrig = fxImportResultatsTempOriginalId($strExt, 'par-' . $intEveId); $sqlPar = "INSERT INTO resultats_participants SET" . " eve_id = " . $intEveId . "," . " epr_id = " . $intEprId . "," . " par_prenom = '" . fxImportResultatsDbEsc($arrFields['par_prenom']) . "'," . " par_nom = '" . fxImportResultatsDbEsc($arrFields['par_nom']) . "'," . " par_naissance = " . ($arrFields['par_naissance'] !== '' ? ("'" . fxImportResultatsDbEsc($arrFields['par_naissance']) . "'") : "NULL") . "," . " par_age = " . intval($arrFields['par_age']) . "," . " par_sexe = '" . fxImportResultatsDbEsc($arrFields['par_sexe']) . "'," . " par_adresse = '" . fxImportResultatsDbEsc($arrFields['par_adresse']) . "'," . " par_ville = '" . fxImportResultatsDbEsc($arrFields['par_ville']) . "'," . " pro_id = 0," . " par_codepostal = '" . fxImportResultatsDbEsc($arrFields['par_codepostal']) . "'," . " pay_id = 0," . " par_telephone1 = '" . fxImportResultatsDbEsc($arrFields['par_telephone1']) . "'," . " par_courriel = '" . fxImportResultatsDbEsc($arrFields['par_courriel']) . "'," . " com_id = 0," . " par_nom_equipe = '" . fxImportResultatsDbEsc($arrFields['par_nom_equipe']) . "'," . " no_panier = '" . fxImportResultatsDbEsc($strNoPanier) . "'," . " par_maj = '" . fxImportResultatsDbEsc($strNow) . "'," . " no_commande = '" . fxImportResultatsDbEsc($strNoCommande) . "'," . " par_id_original = " . intval($intTempParOrig) . "," . " is_cancelled = 0," . " par_no_equipe = " . intval($arrFields['par_no_equipe']) . "," . " par_equipe = 0," . " pec_id = " . $intPecLink . "," . " rol_id = 2," . " no_bib = '" . fxImportResultatsDbEsc($arrFields['no_bib']) . "'," . " par_external_id = '" . fxImportResultatsDbEsc($strExt) . "'," . " par_origine = '" . fxImportResultatsDbEsc($strOrigine) . "'"; if ($objDatabase->fxQuery($sqlPar) === false) { $strErr = fxImportResultatsDbError(); $tabBlocked[] = array( 'code' => 'insert_par', 'message' => 'Échec INSERT participant' . ($strErr !== '' ? (' — ' . $strErr) : ''), 'line' => $intLine, 'name' => $strNamePreview, 'external_id' => $strExt, 'no_bib' => $strBibPreview, 'par_sexe' => $strSexePreview, 'pec_id' => $intPecId, ); continue; } $intParId = intval($objDatabase->fxGetLastId()); $intParOrig = $intParId; $objDatabase->fxQuery( "UPDATE resultats_participants SET par_id_original = " . $intParId . " WHERE par_id = " . $intParId ); $sqlUrg = "UPDATE resultats_participants SET" . " par_contact_urgence_nom = '" . fxImportResultatsDbEsc($arrFields['par_contact_urgence_nom']) . "'," . " par_contact_urgence_telephone = '" . fxImportResultatsDbEsc($arrFields['par_contact_urgence_telephone']) . "'" . " WHERE par_id = " . $intParId; @$objDatabase->fxQuery($sqlUrg); $intCreated++; } foreach ($tabQueIds as $strCol => $intQueId) { $intCol = intval($strCol); $strVal = trim((string)($tabRow[$intCol] ?? '')); $strH = (string)($tabHeaders[$intCol] ?? ''); fxImportResultatsSaveQuestionAnswer($intParOrig, $intPecId, $intQueId, $strH, $strVal); } } $intBlocked = count($tabBlocked); $tabBlockedByType = fxImportResultatsGroupBlockedByType($tabBlocked); $intTotal = count($tabRows); $strMsg = 'Import terminé : ' . $intCreated . ' créé(s), ' . $intUpdated . ' mis à jour.' . ($intBlocked > 0 ? (' ' . $intBlocked . ' exclus.') : ''); return array( 'state' => 'ok', 'message' => $strMsg, 'created' => $intCreated, 'updated' => $intUpdated, 'skipped' => $intBlocked, 'row_count' => $intTotal, 'blocked_count' => $intBlocked, 'blocked_by_type' => $tabBlockedByType, // Compteurs type CT (KPI) 'kpi_total' => $intTotal, 'kpi_ok' => $intCreated + $intUpdated, 'kpi_blocked' => $intBlocked, 'kpi_created' => $intCreated, 'kpi_updated' => $intUpdated, ); } /** * MSIN-4482 — compte ce que le reset effacerait (origine import_fichier seulement). */ function fxImportResultatsResetPreview($intEveId) { global $objDatabase; $intEveId = intval($intEveId); if ($intEveId <= 0) { return array('state' => 'error', 'message' => 'eve_id manquant'); } $strOrig = fxImportResultatsDbEsc(MSIN_IMPORT_RESULTATS_ORIGIN); $sqlPar = "SELECT COUNT(*) AS n FROM resultats_participants" . " WHERE eve_id = " . $intEveId . " AND par_origine = '" . $strOrig . "'"; $tabPar = $objDatabase->fxGetResults($sqlPar); $intPar = is_array($tabPar) ? intval($tabPar[1]['n'] ?? 0) : 0; $sqlPec = "SELECT COUNT(DISTINCT e.pec_id) AS n" . " FROM resultats_epreuves_commandees e" . " INNER JOIN resultats_participants p ON p.eve_id = e.eve_id" . " AND (p.pec_id = e.pec_id OR p.pec_id = e.pec_id_original)" . " WHERE e.eve_id = " . $intEveId . " AND p.par_origine = '" . $strOrig . "'"; $tabPec = $objDatabase->fxGetResults($sqlPec); $intPec = is_array($tabPec) ? intval($tabPec[1]['n'] ?? 0) : 0; $sqlQue = "SELECT COUNT(*) AS n FROM resultats_questions" . " WHERE par_id IN (" . " SELECT pid FROM (" . " SELECT par_id AS pid FROM resultats_participants" . " WHERE eve_id = " . $intEveId . " AND par_origine = '" . $strOrig . "'" . " UNION" . " SELECT par_id_original AS pid FROM resultats_participants" . " WHERE eve_id = " . $intEveId . " AND par_origine = '" . $strOrig . "'" . " AND par_id_original > 0" . " ) _ms1_q" . ")"; $tabQue = $objDatabase->fxGetResults($sqlQue); $intQue = is_array($tabQue) ? intval($tabQue[1]['n'] ?? 0) : 0; // Diagnostic : ce que la liste inscriptions verrait (même join + pec_actif) $sqlList = "SELECT COUNT(*) AS n" . " FROM resultats_epreuves_commandees e, resultats_participants p" . " WHERE e.eve_id = " . $intEveId . " AND p.pec_id = e.pec_id_original" . " AND p.par_origine = '" . $strOrig . "'" . " AND p.rol_id NOT IN (3, 4)" . " AND e.is_cancelled = 0 AND e.pec_actif = 1"; $tabList = $objDatabase->fxGetResults($sqlList); $intList = is_array($tabList) ? intval($tabList[1]['n'] ?? 0) : 0; return array( 'state' => 'ok', 'eve_id' => $intEveId, 'participants' => $intPar, 'pec' => $intPec, 'questions' => $intQue, 'list_visible' => $intList, 'message' => 'En BD : ' . $intPar . ' participant(s) import, ' . $intPec . ' pec, ' . $intQue . ' réponse(s) questions' . ' — dont ' . $intList . ' visible(s) dans la liste (pec_actif=1).', ); } /** * MSIN-4482 — efface UNIQUEMENT les données d'import (par_origine=import_fichier) de l'événement. * Ne touche pas aux inscriptions en ligne / panier réel / inscriptions_questions. * Exige confirm = "RESET". */ function fxImportResultatsReset($intEveId, $strConfirm) { global $objDatabase; @set_time_limit(300); $intEveId = intval($intEveId); if ($intEveId <= 0) { return array('state' => 'error', 'message' => 'eve_id manquant'); } if (trim((string)$strConfirm) !== 'RESET') { return array( 'state' => 'error', 'message' => 'Confirmation invalide — tapez exactement RESET.', ); } $arrPrev = fxImportResultatsResetPreview($intEveId); if (($arrPrev['state'] ?? '') !== 'ok') { return $arrPrev; } if (intval($arrPrev['participants'] ?? 0) < 1) { return array( 'state' => 'ok', 'message' => 'Rien à effacer — aucun participant import_fichier sur cet événement.', 'deleted_participants' => 0, 'deleted_pec' => 0, 'deleted_questions' => 0, ); } $strOrig = fxImportResultatsDbEsc(MSIN_IMPORT_RESULTATS_ORIGIN); $intDelQue = 0; $intDelPar = 0; $intDelPec = 0; // Sous-requête IDs participants import (évite JOIN DELETE lents / gros IN PHP) $strSubPar = "SELECT pid FROM (" . " SELECT par_id AS pid FROM resultats_participants" . " WHERE eve_id = " . $intEveId . " AND par_origine = '" . $strOrig . "'" . " UNION" . " SELECT par_id_original AS pid FROM resultats_participants" . " WHERE eve_id = " . $intEveId . " AND par_origine = '" . $strOrig . "'" . " AND par_id_original > 0" . ") _ms1_imp_par"; $strSubPec = "SELECT pid FROM (" . " SELECT pec_id AS pid FROM resultats_participants" . " WHERE eve_id = " . $intEveId . " AND par_origine = '" . $strOrig . "'" . " AND pec_id > 0" . ") _ms1_imp_pec"; // 1) Réponses questions des participants import seulement $sqlDelQue = "DELETE FROM resultats_questions WHERE par_id IN (" . $strSubPar . ")" . " OR pec_id IN (" . $strSubPec . ")"; if ($objDatabase->fxQuery($sqlDelQue) !== false) { $intDelQue = intval($objDatabase->affected_rows); } // 2) Produits éventuels @$objDatabase->fxQuery( "DELETE FROM resultats_produits_new WHERE par_id IN (" . $strSubPar . ")" . " OR pec_id IN (" . $strSubPec . ")" ); // 3) Coquilles pec liées à l'import (avant delete participants) $sqlDelPec = "DELETE FROM resultats_epreuves_commandees WHERE eve_id = " . $intEveId . " AND pec_id IN (" . $strSubPec . ")"; if ($objDatabase->fxQuery($sqlDelPec) !== false) { $intDelPec += intval($objDatabase->affected_rows); } // Aussi pec_id_original = pec participant $sqlDelPecOrig = "DELETE FROM resultats_epreuves_commandees WHERE eve_id = " . $intEveId . " AND pec_id_original IN (" . $strSubPec . ")"; if ($objDatabase->fxQuery($sqlDelPecOrig) !== false) { $intDelPec += intval($objDatabase->affected_rows); } $strPrefix = 'I' . $intEveId . 'X%'; $sqlDelPec2 = "DELETE FROM resultats_epreuves_commandees WHERE eve_id = " . $intEveId . " AND no_panier LIKE '" . fxImportResultatsDbEsc($strPrefix) . "'"; if ($objDatabase->fxQuery($sqlDelPec2) !== false) { $intDelPec += intval($objDatabase->affected_rows); } // 4) Participants import $sqlDelPar = "DELETE FROM resultats_participants WHERE eve_id = " . $intEveId . " AND par_origine = '" . $strOrig . "'"; if ($objDatabase->fxQuery($sqlDelPar) !== false) { $intDelPar = intval($objDatabase->affected_rows); } return array( 'state' => 'ok', 'message' => 'Reset import OK — participants ' . $intDelPar . ', pec ' . $intDelPec . ', réponses questions ' . $intDelQue . '. Catalogue questions événement intact. Inscriptions en ligne intactes.', 'deleted_participants' => $intDelPar, 'deleted_pec' => $intDelPec, 'deleted_questions' => $intDelQue, ); } /** * Panneau UI fiche événement superadm. */ function fxImportResultatsRenderEventPanel($intEveId) { global $vDomaine; $intEveId = intval($intEveId); if ($intEveId <= 0) { return; } $blnOkType = fxImportResultatsEveIsSansInscriptionEnLigne($intEveId); $intTeReq = fxImportResultatsTeIdSansInscriptionEnLigne(); $strAjax = rtrim((string)$vDomaine, '/') . '/superadm/ajax_import_resultats.php'; $strLoaderGif = rtrim((string)$vDomaine, '/') . '/images/ms1-runner-loader.gif'; $tabEpreuves = fxImportResultatsEpreuvesList($intEveId); $intNbEpr = count($tabEpreuves); require_once dirname(__FILE__) . '/inc_fx_superadm_v2_ui.php'; fxSuperadmV2PanelOpen('Import participants (hors transaction)', array( 'collapse_id' => 'importResultatsCollapse', 'zone_id' => 'import_resultats', 'zone_class' => 'superadm-content-v2-zone mt-3 mb-2', 'expanded' => true, 'header_extra' => 'MSIN-4482', )); ?>
Type d’événement requis
L’import hors transaction est réservé au type Sans inscription en ligne 0) { ?>(te_id=). Changez le Type d’événement en haut de la fiche, enregistrez, puis rechargez.
Le type n’existe pas encore en BD — exécuter sql/MSIN-4482-type-sans-inscription-en-ligne.sql en dev préprod.
Type Sans inscription en ligne — import autorisé (isolé du flux payant).
>

Déposez le fichier client (CSV / Excel). Alignez les colonnes une fois — le template est sauvegardé pour les prochains imports du même format. Incluez une colonne épreuve et une clé unique (ChronoTrack).
Aucune épreuve sur cet événement — créez-en avant d'importer.
épreuve(s) disponible(s) pour le mapping.
Reset = efface seulement par_origine=import_fichier sur cet événement (pas les inscriptions payantes).

Déjà en BD (origine import_fichier)
Participants
PEC (coquilles)
Réponses questions
Visibles liste*

* Visibles liste ≈ join liste inscriptions avec pec_actif=1 (diagnostic).

Glissez-déposez votre fichier ici
ou cliquez pour parcourir · CSV, XLS, XLSX
'importResultatsCollapse')); }