getPDO(); if ($_FILES['csv_file']['error'] !== UPLOAD_ERR_OK) { throw new Exception("Erreur lors du téléversement du fichier."); } $handle = fopen($_FILES['csv_file']['tmp_name'], 'r'); if (!$handle) throw new Exception("Impossible d'ouvrir le fichier CSV."); // Scan up to 8 rows looking for a header row with known column names. // Build colIdx[name] → position map; fall back to positional if header not found. // Matching uses prefix + variant logic so "contact.visible" matches "contact", // "promoteur·ice(s)" matches "promoteur", "Licence" matches "license", etc. $colIdx = null; $headerRowNum = 0; $knownHeaders = [ 'identifiant', 'titre', 'sous-titre', 'auteur', 'contact', 'promoteur', 'lecteur', 'ulb', 'externe', 'format', 'année', 'ap', 'orientation', 'finalité', 'mots-clés', 'synopsis', 'contexte', 'remarques', 'langue', 'autorisation', 'licence', 'license', 'points', 'lien baiu', ]; for ($scan = 0; $scan < 8; $scan++) { $hrow = fgetcsv($handle, 0, ',', '"', ''); if ($hrow === false) break; $headerRowNum++; $normRow = array_map(fn($s) => strtolower(trim((string)$s)), $hrow); $hits = 0; $map = []; $used = []; foreach ($knownHeaders as $h) { foreach ($normRow as $pos => $cell) { if (isset($used[$pos])) continue; // Exact match if ($cell === $h) { $hits++; $map[$h] = $pos; $used[$pos] = true; break; } // Licence/License cross-match if (($h === 'licence' && $cell === 'license') || ($h === 'license' && $cell === 'licence')) { $hits++; $map[$h] = $pos; $used[$pos] = true; break; } // Prefix match (for compound headers like "contact.visible") $hlen = strlen($h); if ($hlen >= 4 && str_starts_with($cell, $h)) { // Avoid short prefixes matching unrelated words if ($hlen >= 5 || $cell === $h) { $hits++; $map[$h] = $pos; $used[$pos] = true; break; } } // Substring match for short distinguishers (ulb, externe) if ($hlen >= 3 && $hlen <= 7 && str_contains($cell, $h)) { $hits++; $map[$h] = $pos; $used[$pos] = true; break; } } } // Require at least 11 known headers to trust the row. if ($hits >= 11) { $colIdx = $map; break; } } // If no header row found, rewind and fall back to positional (skip 4 rows). if ($colIdx === null) { rewind($handle); $headerRowNum = 4; for ($i = 0; $i < 4; $i++) fgetcsv($handle); } else { // Consume blank/instruction/template rows between header and data. // Stops when a row has a non-empty identifiant column that is not a // template placeholder (e.g. "Column1") or instruction snippet. $idPos = $colIdx['identifiant'] ?? 0; $peekRow = null; while (true) { $peek = fgetcsv($handle, 0, ',', '"', ''); if ($peek === false) break; $headerRowNum++; $val = trim((string)($peek[$idPos] ?? '')); if ($val === '' || str_starts_with(strtolower($val), 'column') || str_contains(strtolower($val), 'éparer')) { continue; // metadata row, skip } $peekRow = $peek; break; } } // Helper: get cell value by column name. // When header was found: only use mapped column (returns '' if missing from header). // When no header found: use positional fallback index. $cell = function(array $row, string $name, int $fallbackPos) use ($colIdx): string { if ($colIdx !== null) { $pos = $colIdx[$name] ?? null; if ($pos === null) { // Try licence/license cross-lookup if ($name === 'license') $pos = $colIdx['licence'] ?? null; elseif ($name === 'licence') $pos = $colIdx['license'] ?? null; } if ($pos === null) return ''; } else { $pos = $fallbackPos; } return isset($row[$pos]) ? trim((string)$row[$pos]) : ''; }; // Code → canonical name (legacy short-code CSV format) $orientationCodeMap = [ 'SC'=>'Sculpture','VI'=>'Vidéographie','CA'=>"Cinéma d'animation", 'IP'=>'Installation-Performance','PE'=>'Peinture','PH'=>'Photographie', 'DE'=>'Dessin','AN'=>'Arts Numériques','GR'=>'Graphisme', 'TY'=>'Typographie','DN'=>'Design Numérique','IL'=>'Illustration', 'BD'=>'Bande-Dessinée','SE'=>'Sérigraphie','GV'=>'Gravure', ]; // Alias map: normalise known variant spellings → canonical DB name. // Keys are lowercased+stripped for comparison. $orientationAliases = [ 'arts numériques' => 'Arts Numériques', 'design numérique' => 'Design Numérique', 'installation/performance' => 'Installation-Performance', 'installation performance' => 'Installation-Performance', 'cinema d\'animation' => "Cinéma d'animation", 'cinéma d\'animation' => "Cinéma d'animation", 'bande dessinée' => 'Bande-Dessinée', ]; // Resolve an orientation string (code or full name) → canonical DB name. $resolveOrientation = function(string $raw) use ($orientationCodeMap, $orientationAliases, $importPdo): ?int { $raw = trim($raw); if ($raw === '') return null; // 1. Try legacy short code if (isset($orientationCodeMap[$raw])) { $raw = $orientationCodeMap[$raw]; } // 2. Try alias map (lowercase key) $key = strtolower($raw); if (isset($orientationAliases[$key])) { $raw = $orientationAliases[$key]; } // 3. Exact DB match $s = $importPdo->prepare("SELECT id FROM orientations WHERE name = ?"); $s->execute([$raw]); $r = $s->fetch(); if ($r) return (int)$r['id']; // 4. Case-insensitive DB match $s = $importPdo->prepare("SELECT id FROM orientations WHERE LOWER(name) = LOWER(?)"); $s->execute([$raw]); $r = $s->fetch(); return $r ? (int)$r['id'] : null; }; // AP alias map: variant spellings → canonical code. $apAliases = [ 'l.i.e.n.s.' => 'LIENS', 'liens' => 'LIENS', 'lieux, interdisciplinarités, écologie, nécessité, systèmes' => 'LIENS', 'récits et expérimentation' => 'NS', 'recits et experimentation' => 'NS', 'narraion spéculative' => 'NS', 'narration spéculative' => 'NS', 'atelier pratiques situées' => 'APS', 'design & politique du multiple' => 'DPM', 'design et politique du multiple' => 'DPM', 'pacs' => 'PACS', 'pratique de l\'art' => 'PACS', 'pratiques artistiques & complexité scientifique' => 'PACS', ]; // Resolve an AP string (code or full name) → ap_program id. $resolveAP = function(string $raw) use ($apAliases, $importPdo): ?int { $raw = trim($raw); if ($raw === '') return null; // 1. Try alias map (lowercase key) → canonical code $key = strtolower($raw); if (isset($apAliases[$key])) { $raw = $apAliases[$key]; } // 2. Match by code $s = $importPdo->prepare("SELECT id FROM ap_programs WHERE code = ?"); $s->execute([$raw]); $r = $s->fetch(); if ($r) return (int)$r['id']; // 3. Code match (legacy) $s = $importPdo->prepare("SELECT id FROM ap_programs WHERE code = ?"); $s->execute([$raw]); $r = $s->fetch(); if ($r) return (int)$r['id']; // 4. Case-insensitive name match $s = $importPdo->prepare("SELECT id FROM ap_programs WHERE LOWER(name) = LOWER(?)"); $s->execute([$raw]); $r = $s->fetch(); return $r ? (int)$r['id'] : null; }; $lineNumber = $headerRowNum; $usePeek = isset($peekRow) && $peekRow !== null; while (true) { if ($usePeek) { $row = $peekRow; $usePeek = false; } else { $row = fgetcsv($handle, 0, ',', '"', ''); if ($row === false) break; } $lineNumber++; if (empty($row[0]) && empty($row[1])) continue; try { $importDb->beginTransaction(); $identifier = $cell($row, 'identifiant', 0); $title = $cell($row, 'titre', 1); $subtitle = $cell($row, 'sous-titre', 2); $authorsRaw = $cell($row, 'auteur', 3); $contact = $cell($row, 'contact', 4); // Normalise CSV artefacts: OUI/NON → empty (not a valid email) if ($contact !== '' && in_array(strtoupper(trim($contact)), ['NON', 'OUI'], true)) { $contact = ''; } $supervisorsRaw = $cell($row, 'promoteur', 5); $lecteursInternesRaw = $cell($row, 'lecteur', 6); // first "lecteur" col = interne $lecteursExternesRaw = $cell($row, 'externe', 7); // contains "externe" $promoteursUlbRaw = $cell($row, 'ulb', 8); // contains "ulb" $formatsRaw = $cell($row, 'format', 9); $yearRaw = $cell($row, 'année', 10); $year = $yearRaw !== '' ? intval($yearRaw) : 0; // Fallback: derive year from identifier (e.g. "2024-003" → 2024) if ($year === 0 && $identifier !== '' && preg_match('/^(\d{4})-/', $identifier, $m)) { $year = (int)$m[1]; } $apCode = $cell($row, 'ap', 11); $orientationCode = $cell($row, 'orientation', 12); $finalityName = $cell($row, 'finalité', 13); $keywordsRaw = $cell($row, 'mots-clés', 14); $synopsis = $cell($row, 'synopsis', 15); $context = $cell($row, 'contexte', 16); $remarks = $cell($row, 'remarques', 17); $languageRaw = $cell($row, 'langue', 18); $access = $cell($row, 'autorisation', 19); $license = $cell($row, 'license', 20); $juryPointsRaw = $cell($row, 'points', 21); $juryPoints = $juryPointsRaw !== '' ? floatval($juryPointsRaw) : null; $baiuLink = $cell($row, 'lien baiu', 22); if ($title === '' || $year === 0) { $missing = []; if ($title === '') $missing[] = 'titre'; if ($year === 0) $missing[] = 'année'; throw new Exception("Champ(s) requis manquant(s) : " . implode(', ', $missing) . " (id=\"" . ($identifier ?: '?') . "\", titre=\"" . substr($title, 0, 80) . "\")"); } $orientationId = $resolveOrientation($orientationCode); $apProgramId = $resolveAP($apCode); $finalityId = null; if (!empty($finalityName)) { $s = $importPdo->prepare("SELECT id FROM finality_types WHERE name = ?"); $s->execute([$finalityName]); $r = $s->fetch(); $finalityId = $r ? $r['id'] : null; } $accessTypeId = null; if (!empty($access)) { $s = $importPdo->prepare("SELECT id FROM access_types WHERE name = ?"); $s->execute([ucfirst(strtolower($access))]); $r = $s->fetch(); $accessTypeId = $r ? $r['id'] : null; } if ($accessTypeId === null) $accessTypeId = 1; if (!empty($identifier)) { $s = $importPdo->prepare("SELECT id FROM theses WHERE identifier = ?"); $s->execute([$identifier]); if ($s->fetch()) { $importDb->rollback(); $skippedCount++; $importResults[] = ['type'=>'skip', 'msg'=>"Ligne $lineNumber: identifiant \"$identifier\" déjà présent, ignoré."]; continue; } } $s = $importPdo->prepare(" INSERT INTO theses ( identifier, title, subtitle, year, orientation_id, ap_program_id, finality_id, synopsis, context_note, remarks, jury_points, baiu_link, access_type_id, is_published, submitted_at ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,0,CURRENT_TIMESTAMP) "); $s->execute([ !empty($identifier) ? $identifier : null, $title, !empty($subtitle) ? $subtitle : null, $year, $orientationId, $apProgramId, $finalityId, !empty($synopsis) ? $synopsis : null, !empty($context) ? $context : null, !empty($remarks) ? $remarks : null, $juryPoints, !empty($baiuLink) ? $baiuLink : null, $accessTypeId, ]); $thesisId = $importPdo->lastInsertId(); if (!empty($authorsRaw)) { foreach (array_map('trim', explode(',', $authorsRaw)) as $idx => $name) { if ($name) { $aId = $importDb->findOrCreateAuthor($name, $idx === 0 ? $contact : null); $s = $importPdo->prepare("INSERT INTO thesis_authors (thesis_id, author_id, author_order) VALUES (?,?,?)"); $s->execute([$thesisId, $aId, $idx + 1]); } } } // Insert supervisors with proper role/is_external/is_ulb flags $juryOrder = 0; // Promoteurs internes if (!empty($supervisorsRaw)) { foreach (array_map('trim', explode(',', $supervisorsRaw)) as $name) { if ($name) { $juryOrder++; $sId = $importDb->findOrCreateSupervisor($name); $stmt = $importPdo->prepare("INSERT INTO thesis_supervisors (thesis_id, supervisor_id, role, is_external, is_ulb, supervisor_order) VALUES (?,?,?,?,?,?)"); $stmt->execute([$thesisId, $sId, 'promoteur', 0, 0, $juryOrder]); } } } // Lecteurs internes if (!empty($lecteursInternesRaw)) { foreach (array_map('trim', explode(',', $lecteursInternesRaw)) as $name) { if ($name) { $juryOrder++; $sId = $importDb->findOrCreateSupervisor($name); $stmt = $importPdo->prepare("INSERT INTO thesis_supervisors (thesis_id, supervisor_id, role, is_external, is_ulb, supervisor_order) VALUES (?,?,?,?,?,?)"); $stmt->execute([$thesisId, $sId, 'lecteur', 0, 0, $juryOrder]); } } } // Lecteurs externes if (!empty($lecteursExternesRaw)) { foreach (array_map('trim', explode(',', $lecteursExternesRaw)) as $name) { if ($name) { $juryOrder++; $sId = $importDb->findOrCreateSupervisor($name); $stmt = $importPdo->prepare("INSERT INTO thesis_supervisors (thesis_id, supervisor_id, role, is_external, is_ulb, supervisor_order) VALUES (?,?,?,?,?,?)"); $stmt->execute([$thesisId, $sId, 'lecteur', 1, 0, $juryOrder]); } } } // Promoteurs ULB if (!empty($promoteursUlbRaw)) { foreach (array_map('trim', explode(',', $promoteursUlbRaw)) as $name) { if ($name) { $juryOrder++; $sId = $importDb->findOrCreateSupervisor($name); $stmt = $importPdo->prepare("INSERT INTO thesis_supervisors (thesis_id, supervisor_id, role, is_external, is_ulb, supervisor_order) VALUES (?,?,?,?,?,?)"); $stmt->execute([$thesisId, $sId, 'promoteur', 1, 1, $juryOrder]); } } } if (!empty($keywordsRaw)) { $normalizeTag = fn(string $t): string => strtolower(trim(preg_replace('/\s+/', ' ', $t))); $tags = array_values(array_unique(array_map($normalizeTag, explode(',', $keywordsRaw)))); $tags = array_filter($tags, fn($t) => $t !== ''); foreach (array_slice($tags, 0, 10) as $kw) { $tId = $importDb->findOrCreateTag($kw); if ($tId) { $s = $importPdo->prepare("INSERT INTO thesis_tags (thesis_id, tag_id) VALUES (?,?)"); $s->execute([$thesisId, $tId]); } } } if (!empty($languageRaw)) { foreach (array_map('trim', explode(',', $languageRaw)) as $langName) { $langName = strtolower($langName); if ($langName === '') continue; // Lookup case-insensitively; insert if missing (stored lowercase). $s = $importPdo->prepare("SELECT id FROM languages WHERE LOWER(name) = LOWER(?) AND deleted_at IS NULL"); $s->execute([$langName]); $r = $s->fetch(); $langId = $r ? (int)$r['id'] : null; if ($langId === null) { $importPdo->prepare("INSERT INTO languages (name) VALUES (?)")->execute([$langName]); $langId = (int)$importPdo->lastInsertId(); } $s2 = $importPdo->prepare("INSERT INTO thesis_languages (thesis_id, language_id) VALUES (?,?)"); $s2->execute([$thesisId, $langId]); } } if (!empty($formatsRaw)) { foreach (array_map('trim', explode(',', $formatsRaw)) as $fmt) { if ($fmt) { $s = $importPdo->prepare("SELECT id FROM format_types WHERE name = ?"); $s->execute([ucfirst(strtolower($fmt))]); $r = $s->fetch(); if ($r) { $s2 = $importPdo->prepare("INSERT INTO thesis_formats (thesis_id, format_id) VALUES (?,?)"); $s2->execute([$thesisId, $r['id']]); } } } } $importDb->commit(); $importedCount++; $importResults[] = ['type'=>'ok', 'msg'=>"\"$title\" (ID: $thesisId)"]; } catch (Exception $e) { $importDb->rollback(); $skippedCount++; $importResults[] = ['type'=>'error', 'msg'=>"Ligne $lineNumber: " . $e->getMessage()]; error_log("Import error on line $lineNumber: " . $e->getMessage()); } } fclose($handle); $importMessage = "Import terminé : $importedCount TFE importés, $skippedCount ignorés."; $importDone = true; } catch (Exception $e) { $importErrors[] = $e->getMessage(); error_log("CSV import error: " . $e->getMessage()); } } $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } try { $db = new Database(); $searchQuery = isset($_GET['search']) ? trim($_GET['search']) : ''; $yearFilter = isset($_GET['year']) ? intval($_GET['year']) : null; $orientationFilter = isset($_GET['orientation']) ? intval($_GET['orientation']) : null; $apFilter = isset($_GET['ap']) ? intval($_GET['ap']) : null; $sortCol = isset($_GET['sort']) ? trim($_GET['sort']) : 'submitted_at'; $sortDir = isset($_GET['dir']) ? trim($_GET['dir']) : 'desc'; $filters = []; if ($searchQuery) $filters['search'] = $searchQuery; if ($yearFilter) $filters['year'] = $yearFilter; if ($orientationFilter) $filters['orientation'] = $orientationFilter; if ($apFilter) $filters['ap'] = $apFilter; $filters['sort'] = $sortCol; $filters['dir'] = $sortDir; $theses = $db->getThesesList($filters, 0, 0); $totalCount = count($theses); $stats = $db->getThesesStats(); $years = $db->getAllYears(); $orientations = $db->getAllOrientations(); $apPrograms = $db->getAllAPPrograms(); $trashCount = $db->countTrashedTheses(); $tab = $_GET['tab'] ?? 'list'; $trashedTheses = ($tab === 'trash') ? $db->getTrashedTheses() : []; } catch (Exception $e) { error_log("Error loading theses list: " . $e->getMessage()); die("Erreur lors du chargement de la liste."); } $isHtmx = ($_SERVER['HTTP_HX_REQUEST'] ?? '') === 'true'; $isAdmin = true; $bodyClass = 'admin-body'; if ($isHtmx) { if ($tab === 'trash') { include APP_ROOT . '/templates/admin/index-trash.php'; } else { include APP_ROOT . '/templates/admin/index-table.php'; } } else { require_once APP_ROOT . '/templates/head.php'; include APP_ROOT . '/templates/header.php'; if ($tab === 'trash') { include APP_ROOT . '/templates/admin/index-trash.php'; } else { include APP_ROOT . '/templates/admin/index.php'; } require_once APP_ROOT . '/templates/admin/footer.php'; }