Move CSV import to inline dialog on list page

This commit is contained in:
Pontoporeia
2026-04-08 15:12:36 +02:00
parent 603af07b68
commit 4199b206db
5 changed files with 424 additions and 393 deletions

View File

@@ -1,6 +1,12 @@
# TODO
## Done
- [x] Move CSV import from standalone import.php to inline dialog on the list page
- [x] Import logic embedded in index.php (processed before render)
- [x] `<dialog>` with form, results log, and auto-opens on post-submit
- [x] "Importer un CSV" button in list header row next to h1
- [x] Nav link to import.php removed; import.php now redirects to /admin/
- [x] CSS: `.admin-dialog`, `.admin-list-header`, `.admin-import-log`
- [x] Create Paramètres page consolidating maintenance toggle and account settings into two sections
- [x] New `public/admin/parametres.php` with Maintenance + Compte administrateur sections
- [x] Nav updated: “Compte” replaced by “Paramètres” linking to `parametres.php`

View File

@@ -1,370 +1,5 @@
<?php
// Bootstrap application
require_once __DIR__ . "/../../config/bootstrap.php";
require_once __DIR__ . '/../../src/AdminAuth.php';
// CSV Import page for Post-ERG thesis database
// This page allows importing thesis data from CSV files
// PHP-level auth guard (defence-in-depth behind nginx Basic Auth)
AdminAuth::requireLogin();
// Generate CSRF token
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
require_once __DIR__ . '/../../src/Database.php';
$pageTitle = "Import";
$message = '';
$errors = [];
$importedCount = 0;
$skippedCount = 0;
$importResults = [];
// Handle CSV upload and import
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
// Verify CSRF token
if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
$errors[] = "Erreur de sécurité : token invalide.";
} else {
try {
$db = new Database();
$pdo = $db->getPDO();
// Check file upload
if ($_FILES['csv_file']['error'] !== UPLOAD_ERR_OK) {
throw new Exception("Erreur lors du téléversement du fichier.");
}
// Read CSV file
$csvFile = $_FILES['csv_file']['tmp_name'];
$handle = fopen($csvFile, 'r');
if (!$handle) {
throw new Exception("Impossible d'ouvrir le fichier CSV.");
}
// Skip first two rows (empty and headers)
fgetcsv($handle, 0, ',', '"', ''); // Empty row
$headers = fgetcsv($handle, 0, ',', '"', ''); // Header row
fgetcsv($handle, 0, ',', '"', ''); // Description row
$headers = fgetcsv($handle, 0, ',', '"', ''); // Actual column names
// Map CSV columns
$columnMap = [
0 => 'identifier', // Identifiant
1 => 'title', // Titre
2 => 'subtitle', // Sous-titre
3 => 'authors', // Auteur·ice(s)
4 => 'contact', // Contact
5 => 'supervisors', // Promoteur·ice(s)
6 => 'formats', // Format
7 => 'year', // Année
8 => 'ap', // AP
9 => 'orientation', // Orientation
10 => 'finality', // Finalité
11 => 'keywords', // Mots-clés
12 => 'synopsis', // Synopsis
13 => 'context', // Contexte
14 => 'remarks', // Remarques
15 => 'language', // Langue
16 => 'access', // Autorisation
17 => 'license', // License
18 => 'size_info', // taille
19 => 'jury_points', // Points sur 20
20 => 'baiu_link', // lien BAIU
];
// Orientation abbreviation mapping
$orientationMap = [
'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',
];
// Process each row
$lineNumber = 5; // Start after headers
while (($row = fgetcsv($handle, 0, ',', '"', '')) !== false) {
$lineNumber++;
// Skip empty rows
if (empty($row[0]) && empty($row[1])) {
continue;
}
try {
$db->beginTransaction();
// Extract data
$identifier = trim($row[0] ?? '');
$title = trim($row[1] ?? '');
$subtitle = trim($row[2] ?? '');
$authorsRaw = trim($row[3] ?? '');
$contact = trim($row[4] ?? '');
$supervisorsRaw = trim($row[5] ?? '');
$formatsRaw = trim($row[6] ?? '');
$year = intval($row[7] ?? 0);
$apCode = trim($row[8] ?? '');
$orientationCode = trim($row[9] ?? '');
$finalityName = trim($row[10] ?? '');
$keywordsRaw = trim($row[11] ?? '');
$synopsis = trim($row[12] ?? '');
$context = trim($row[13] ?? '');
$remarks = trim($row[14] ?? '');
$languageRaw = trim($row[15] ?? '');
$access = trim($row[16] ?? '');
$license = trim($row[17] ?? '');
$sizeInfo = trim($row[18] ?? '');
$juryPoints = !empty($row[19]) ? floatval($row[19]) : null;
$baiuLink = trim($row[20] ?? '');
// Validate required fields
if (empty($title) || empty($year)) {
throw new Exception("Ligne $lineNumber: Titre et année requis.");
}
// Map orientation
$orientationName = isset($orientationMap[$orientationCode]) ? $orientationMap[$orientationCode] : null;
$orientationId = null;
if ($orientationName) {
$stmtOr = $pdo->prepare("SELECT id FROM orientations WHERE name = ?");
$stmtOr->execute([$orientationName]);
$rowOr = $stmtOr->fetch();
$orientationId = $rowOr ? $rowOr['id'] : null;
}
// Map AP program
$apProgramId = null;
if (!empty($apCode)) {
$stmt = $pdo->prepare("SELECT id FROM ap_programs WHERE code = ?");
$stmt->execute([$apCode]);
$result = $stmt->fetch();
if ($result) {
$apProgramId = $result['id'];
}
}
// Map finality
$finalityId = null;
if (!empty($finalityName)) {
$stmtFin = $pdo->prepare("SELECT id FROM finality_types WHERE name = ?");
$stmtFin->execute([$finalityName]);
$rowFin = $stmtFin->fetch();
$finalityId = $rowFin ? $rowFin['id'] : null;
}
// Map access type (Autorisation column)
// CSV values are expected to match access_types.name: "Libre", "Interne", "Interdit"
$accessTypeId = null;
if (!empty($access)) {
$stmtAcc = $pdo->prepare("SELECT id FROM access_types WHERE name = ?");
$stmtAcc->execute([ucfirst(strtolower($access))]);
$rowAcc = $stmtAcc->fetch();
$accessTypeId = $rowAcc ? $rowAcc['id'] : null;
}
// Default to Libre (id=1) when not specified so imported theses are visible
if ($accessTypeId === null) {
$accessTypeId = 1;
}
// Skip if identifier already exists
if (!empty($identifier)) {
$stmtCheck = $pdo->prepare("SELECT id FROM theses WHERE identifier = ?");
$stmtCheck->execute([$identifier]);
if ($stmtCheck->fetch()) {
$db->rollback();
$skippedCount++;
$importResults[] = "⚠ Ligne $lineNumber: identifiant \"$identifier\" déjà présent, ignoré.";
continue;
}
}
// Insert thesis
$stmt = $pdo->prepare("
INSERT INTO theses (
identifier, title, subtitle, year,
orientation_id, ap_program_id, finality_id,
synopsis, context_note, remarks,
file_size_info, jury_points, baiu_link,
access_type_id, is_published,
submitted_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, CURRENT_TIMESTAMP)
");
$stmt->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,
!empty($sizeInfo) ? $sizeInfo : null,
$juryPoints,
!empty($baiuLink) ? $baiuLink : null,
$accessTypeId
]);
$thesisId = $pdo->lastInsertId();
// Add authors
if (!empty($authorsRaw)) {
$authors = array_map('trim', explode(',', $authorsRaw));
foreach ($authors as $index => $authorName) {
if (!empty($authorName)) {
$authorId = $db->findOrCreateAuthor($authorName, $index === 0 ? $contact : null);
$stmt = $pdo->prepare("INSERT INTO thesis_authors (thesis_id, author_id, author_order) VALUES (?, ?, ?)");
$stmt->execute([$thesisId, $authorId, $index + 1]);
}
}
}
// Add supervisors
if (!empty($supervisorsRaw)) {
$supervisors = array_map('trim', explode(',', $supervisorsRaw));
foreach ($supervisors as $index => $supervisorName) {
if (!empty($supervisorName)) {
$supervisorId = $db->findOrCreateSupervisor($supervisorName);
$stmt = $pdo->prepare("INSERT INTO thesis_supervisors (thesis_id, supervisor_id, supervisor_order) VALUES (?, ?, ?)");
$stmt->execute([$thesisId, $supervisorId, $index + 1]);
}
}
}
// Add keywords
if (!empty($keywordsRaw)) {
$keywords = array_map('trim', explode(',', $keywordsRaw));
$keywords = array_slice($keywords, 0, 10); // Max 10
foreach ($keywords as $keyword) {
if (!empty($keyword)) {
$tagId = $db->findOrCreateTag($keyword);
if ($tagId) {
$stmtTag = $pdo->prepare("INSERT INTO thesis_tags (thesis_id, tag_id) VALUES (?, ?)");
$stmtTag->execute([$thesisId, $tagId]);
}
}
}
}
// Add language
if (!empty($languageRaw)) {
$stmtLang = $pdo->prepare("SELECT id FROM languages WHERE name = ?");
$stmtLang->execute([ucfirst(strtolower($languageRaw))]);
$rowLang = $stmtLang->fetch();
$languageId = $rowLang ? $rowLang['id'] : null;
if ($languageId) {
$stmt = $pdo->prepare("INSERT INTO thesis_languages (thesis_id, language_id) VALUES (?, ?)");
$stmt->execute([$thesisId, $languageId]);
}
}
// Add formats
if (!empty($formatsRaw)) {
$formats = array_map('trim', explode(',', $formatsRaw));
foreach ($formats as $formatName) {
if (!empty($formatName)) {
$stmtFmt = $pdo->prepare("SELECT id FROM format_types WHERE name = ?");
$stmtFmt->execute([ucfirst(strtolower($formatName))]);
$rowFmt = $stmtFmt->fetch();
$formatId = $rowFmt ? $rowFmt['id'] : null;
if ($formatId) {
$stmt = $pdo->prepare("INSERT INTO thesis_formats (thesis_id, format_id) VALUES (?, ?)");
$stmt->execute([$thesisId, $formatId]);
}
}
}
}
$db->commit();
$importedCount++;
$importResults[] = " Ligne $lineNumber: \"$title\" importé (ID: $thesisId)";
} catch (Exception $e) {
$db->rollback();
$skippedCount++;
$importResults[] = " Ligne $lineNumber: " . $e->getMessage();
error_log("Import error on line $lineNumber: " . $e->getMessage());
}
}
fclose($handle);
$message = "Import terminé : $importedCount TFE importés, $skippedCount ignorés.";
} catch (Exception $e) {
$errors[] = $e->getMessage();
error_log("CSV import error: " . $e->getMessage());
}
}
// Regenerate CSRF token
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>
<?php $isAdmin = true; $bodyClass = 'admin-body'; require_once APP_ROOT . '/templates/head.php'; ?>
<?php include APP_ROOT . '/templates/header.php'; ?>
<main id="main-content">
<h1>Importer une liste de TFE</h1>
<?php if (!empty($errors)): ?>
<div role="alert" data-type="error">
<strong>⚠ Erreurs :</strong>
<ul class="admin-error-list">
<?php foreach ($errors as $err): ?>
<li><?= htmlspecialchars($err) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php if ($message): ?>
<p role="status" data-type="success">✓ <?= htmlspecialchars($message) ?></p>
<?php endif; ?>
<form action="import.php" method="post" enctype="multipart/form-data" class="admin-import-area admin-form">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<div>
<label>Fichier CSV :</label>
<div class="admin-file-input">
<input type="file" id="csv_file" name="csv_file" accept=".csv" required>
<small class="admin-file-hint">
Colonnes attendues : Identifiant, Titre, Sous-titre, Auteur·ice(s), Contact, Promoteur·ice(s), Format, Année, AP, Orientation, Finalité, Mots-clés, Synopsis, Contexte, Remarques, Langue, Autorisation, License, taille, Points sur 20, lien BAIU<br>
— Deux premières lignes ignorées (en-tête) — Séparateur : virgule — Encodage : UTF-8
</small>
</div>
</div>
<div class="admin-form-footer">
<button type="submit" class="admin-btn">Importer</button>
</div>
</form>
<?php if (!empty($importResults)): ?>
<div class="admin-import-results">
<h2 class="admin-import-results__title">Résultats de l'import</h2>
<div class="info-message">
<pre><?php foreach ($importResults as $r) echo htmlspecialchars($r) . "\n"; ?></pre>
</div>
</div>
<?php endif; ?>
</main>
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>
// import.php is no longer a standalone page.
// CSV import is handled inline on the list page via a dialog.
header('Location: /admin/');
exit();

View File

@@ -10,6 +10,211 @@ if (empty($_SESSION['csrf_token'])) {
$pageTitle = "Liste des TFE";
require_once __DIR__ . '/../../src/Database.php';
// ── CSV Import (inline, submitted to this same page) ─────────────────────────
$importMessage = '';
$importErrors = [];
$importResults = [];
$importDone = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
$importErrors[] = "Erreur de sécurité : token invalide.";
} else {
$importedCount = 0;
$skippedCount = 0;
try {
$importDb = new Database();
$importPdo = $importDb->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.");
fgetcsv($handle, 0, ',', '"', '');
fgetcsv($handle, 0, ',', '"', '');
fgetcsv($handle, 0, ',', '"', '');
fgetcsv($handle, 0, ',', '"', ''); // skip 4 header rows
$orientationMap = [
'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',
];
$lineNumber = 5;
while (($row = fgetcsv($handle, 0, ',', '"', '')) !== false) {
$lineNumber++;
if (empty($row[0]) && empty($row[1])) continue;
try {
$importDb->beginTransaction();
$identifier = trim($row[0] ?? '');
$title = trim($row[1] ?? '');
$subtitle = trim($row[2] ?? '');
$authorsRaw = trim($row[3] ?? '');
$contact = trim($row[4] ?? '');
$supervisorsRaw = trim($row[5] ?? '');
$formatsRaw = trim($row[6] ?? '');
$year = intval($row[7] ?? 0);
$apCode = trim($row[8] ?? '');
$orientationCode = trim($row[9] ?? '');
$finalityName = trim($row[10] ?? '');
$keywordsRaw = trim($row[11] ?? '');
$synopsis = trim($row[12] ?? '');
$context = trim($row[13] ?? '');
$remarks = trim($row[14] ?? '');
$languageRaw = trim($row[15] ?? '');
$access = trim($row[16] ?? '');
$license = trim($row[17] ?? '');
$sizeInfo = trim($row[18] ?? '');
$juryPoints = !empty($row[19]) ? floatval($row[19]) : null;
$baiuLink = trim($row[20] ?? '');
if (empty($title) || empty($year)) throw new Exception("Titre et année requis.");
$orientationName = $orientationMap[$orientationCode] ?? null;
$orientationId = null;
if ($orientationName) {
$s = $importPdo->prepare("SELECT id FROM orientations WHERE name = ?");
$s->execute([$orientationName]);
$r = $s->fetch(); $orientationId = $r ? $r['id'] : null;
}
$apProgramId = null;
if (!empty($apCode)) {
$s = $importPdo->prepare("SELECT id FROM ap_programs WHERE code = ?");
$s->execute([$apCode]);
$r = $s->fetch(); $apProgramId = $r ? $r['id'] : null;
}
$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,
file_size_info, jury_points, baiu_link,
access_type_id, is_published, submitted_at
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,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,
!empty($sizeInfo) ? $sizeInfo : 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]);
}
}
}
if (!empty($supervisorsRaw)) {
foreach (array_map('trim', explode(',', $supervisorsRaw)) as $idx => $name) {
if ($name) {
$sId = $importDb->findOrCreateSupervisor($name);
$s = $importPdo->prepare("INSERT INTO thesis_supervisors (thesis_id, supervisor_id, supervisor_order) VALUES (?,?,?)");
$s->execute([$thesisId, $sId, $idx + 1]);
}
}
}
if (!empty($keywordsRaw)) {
foreach (array_slice(array_map('trim', explode(',', $keywordsRaw)), 0, 10) as $kw) {
if ($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)) {
$s = $importPdo->prepare("SELECT id FROM languages WHERE name = ?");
$s->execute([ucfirst(strtolower($languageRaw))]);
$r = $s->fetch();
if ($r) {
$s2 = $importPdo->prepare("INSERT INTO thesis_languages (thesis_id, language_id) VALUES (?,?)");
$s2->execute([$thesisId, $r['id']]);
}
}
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']) : '';
@@ -72,26 +277,32 @@ document.addEventListener('DOMContentLoaded', () => {
</script>
<main id="main-content">
<h1>Liste des TFE</h1>
<div class="admin-list-header">
<div class="admin-list-header__top">
<h1>Liste des TFE</h1>
<dl class="admin-stats">
<div class="admin-stat">
<dt class="admin-stat__label">TFE total</dt>
<dd class="admin-stat__number"><?= $stats['total'] ?></dd>
</div>
<div class="admin-stat">
<dt class="admin-stat__label">Publiés</dt>
<dd class="admin-stat__number"><?= $stats['published'] ?></dd>
</div>
<div class="admin-stat">
<dt class="admin-stat__label">En attente</dt>
<dd class="admin-stat__number"><?= $stats['pending'] ?></dd>
</div>
</dl>
</div>
<button type="button" class="admin-btn" id="import-dialog-btn"
onclick="document.getElementById('import-dialog').showModal()">
Importer un CSV
</button>
</div>
<?php include APP_ROOT . '/templates/partials/flash-messages.php'; ?>
<!-- Stats (always reflects full DB, independent of active filters) -->
<dl class="admin-stats">
<div class="admin-stat">
<dt class="admin-stat__label">TFE total</dt>
<dd class="admin-stat__number"><?= $stats['total'] ?></dd>
</div>
<div class="admin-stat">
<dt class="admin-stat__label">Publiés</dt>
<dd class="admin-stat__number"><?= $stats['published'] ?></dd>
</div>
<div class="admin-stat">
<dt class="admin-stat__label">En attente</dt>
<dd class="admin-stat__number"><?= $stats['pending'] ?></dd>
</div>
</dl>
<!-- Filters -->
<form class="admin-filters" method="get" action="/admin/">
<input type="text" name="search" placeholder="Titre, auteur..."
@@ -216,4 +427,66 @@ document.addEventListener('DOMContentLoaded', () => {
?>
</main>
<!-- ══════════════════════════════════════════════════════════════
IMPORT DIALOG
══════════════════════════════════════════════════════════════ -->
<dialog id="import-dialog" class="admin-dialog" aria-labelledby="import-dialog-title">
<div class="admin-dialog__header">
<h2 id="import-dialog-title">Importer une liste de TFE</h2>
<button type="button" class="admin-dialog__close" aria-label="Fermer"
onclick="document.getElementById('import-dialog').close()">&#x2715;</button>
</div>
<?php if (!empty($importErrors)): ?>
<div role="alert" data-type="error" class="admin-dialog__alert">
<strong>⚠ Erreurs :</strong>
<ul class="admin-error-list">
<?php foreach ($importErrors as $err): ?>
<li><?= htmlspecialchars($err) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php if ($importMessage): ?>
<p role="status" data-type="success">✓ <?= htmlspecialchars($importMessage) ?></p>
<?php endif; ?>
<form method="post" enctype="multipart/form-data" class="admin-form">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<div>
<label for="csv_file">Fichier CSV</label>
<div class="admin-file-input">
<input type="file" id="csv_file" name="csv_file" accept=".csv" required>
<small class="admin-file-hint">
Colonnes : Identifiant, Titre, Sous-titre, Auteur·ice(s), Contact, Promoteur·ice(s), Format, Année, AP, Orientation, Finalité, Mots-clés, Synopsis, Contexte, Remarques, Langue, Autorisation, License, taille, Points sur 20, lien BAIU<br>
Quatre premières lignes ignorées — Séparateur : virgule — UTF-8
</small>
</div>
</div>
<div class="admin-form-footer">
<button type="submit" class="admin-btn">Importer</button>
<button type="button" class="admin-btn-secondary"
onclick="document.getElementById('import-dialog').close()">Annuler</button>
</div>
</form>
<?php if (!empty($importResults)): ?>
<div class="admin-import-results">
<h3 class="admin-import-results__title">Résultats</h3>
<ul class="admin-import-log">
<?php foreach ($importResults as $r): ?>
<li class="admin-import-log__item admin-import-log__item--<?= $r['type'] ?>"><?= htmlspecialchars($r['msg']) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
</dialog>
<?php if ($importDone || !empty($importErrors)): ?>
<script>document.getElementById('import-dialog').showModal();</script>
<?php endif; ?>
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>

View File

@@ -38,7 +38,8 @@
margin-inline: auto;
}
.admin-body main > h1 {
.admin-body main > h1,
.admin-list-header > h1 {
font-size: var(--step-2);
font-weight: 600;
letter-spacing: 0.08em;
@@ -306,22 +307,22 @@
.admin-stats {
display: flex;
gap: var(--space-s);
margin-bottom: var(--space-l);
flex-wrap: wrap;
margin: 0;
}
.admin-stat {
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 4px;
padding: var(--space-s) var(--space-m);
min-width: 140px;
padding: var(--space-2xs) var(--space-s);
min-width: 0;
display: flex;
flex-direction: column;
}
.admin-stat__number {
font-size: var(--step-3);
font-size: var(--step-1);
font-weight: 700;
color: var(--accent-primary);
line-height: 1;
@@ -330,7 +331,7 @@
}
.admin-stat__label {
font-size: var(--step--1);
font-size: var(--step--2);
color: var(--text-secondary);
margin-top: var(--space-3xs);
order: 2;
@@ -905,6 +906,123 @@
margin-top: var(--space-2xs);
}
/* ── List page header (title + stats row, then button) ──────────────────── */
.admin-list-header {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: var(--space-s);
margin-bottom: var(--space-l);
}
.admin-list-header__top {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--space-m);
flex-wrap: wrap;
width: 100%;
}
.admin-list-header h1 {
margin: 0;
}
/* ── Dialog ───────────────────────────────────────────────────────────── */
.admin-dialog {
border: 1px solid var(--border-primary);
border-radius: 6px;
background: var(--bg-primary);
color: var(--text-primary);
padding: 0;
max-width: 680px;
width: 100%;
box-shadow: 0 8px 32px rgba(0,0,0,0.18);
}
.admin-dialog::backdrop {
background: rgba(0,0,0,0.45);
}
.admin-dialog__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-m) var(--space-l);
border-bottom: 1px solid var(--border-primary);
}
.admin-dialog__header h2 {
font-size: var(--step-0);
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
margin: 0;
color: var(--text-secondary);
}
.admin-dialog__close {
background: none;
border: none;
cursor: pointer;
font-size: var(--step-0);
color: var(--text-tertiary);
padding: var(--space-3xs);
line-height: 1;
border-radius: 3px;
transition: color 0.15s;
}
.admin-dialog__close:hover {
color: var(--text-primary);
}
.admin-dialog .admin-form,
.admin-dialog .admin-import-results,
.admin-dialog [role="alert"],
.admin-dialog [role="status"],
.admin-dialog__alert {
margin: 0;
padding: var(--space-m) var(--space-l);
}
.admin-dialog .admin-form {
padding-top: var(--space-m);
padding-bottom: 0;
}
.admin-dialog .admin-form-footer {
display: flex;
gap: var(--space-xs);
padding-bottom: var(--space-m);
}
/* ── Import results log ─────────────────────────────────────────────── */
.admin-import-log {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-3xs);
max-height: 260px;
overflow-y: auto;
font-size: var(--step--2);
font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
border: 1px solid var(--border-primary);
border-radius: 3px;
padding: var(--space-xs);
background: var(--bg-secondary);
}
.admin-import-log__item::before {
margin-right: var(--space-3xs);
}
.admin-import-log__item--ok::before { content: '✓'; color: var(--accent-green); }
.admin-import-log__item--skip::before { content: '⚠'; color: var(--warning); }
.admin-import-log__item--error::before { content: '✗'; color: var(--error); }
/* ── Settings page sections ─────────────────────────────────────────────── */
.admin-settings-section {
border: 1px solid var(--border-primary);

View File

@@ -18,7 +18,6 @@ $_thesisId = $_GET['id'] ?? null;
<ul>
<li><a href="/admin/" <?= $_currentPage === 'index.php' ? 'aria-current="page"' : '' ?>>Liste des TFE</a></li>
<li><a href="/admin/add.php" <?= $_currentPage === 'add.php' ? 'aria-current="page"' : '' ?>>Ajouter un TFE</a></li>
<li><a href="/admin/import.php" <?= $_currentPage === 'import.php' ? 'aria-current="page"' : '' ?>>Importer une liste de TFE</a></li>
<li><a href="/admin/pages.php" <?= in_array($_currentPage, ['pages.php', 'pages-edit.php']) ? 'aria-current="page"' : '' ?>>Pages statiques</a></li>
<li><a href="/admin/tags.php" <?= $_currentPage === 'tags.php' ? 'aria-current="page"' : '' ?>>Mots-clés</a></li>
<li><a href="/admin/system.php" <?= in_array($_currentPage, ['system.php', 'status.php', 'logs.php']) ? 'aria-current="page"' : '' ?>>Système</a></li>