mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-07 11:39:18 +02:00
Move CSV import to inline dialog on list page
This commit is contained in:
6
TODO.md
6
TODO.md
@@ -1,6 +1,12 @@
|
|||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
## Done
|
## 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] 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] New `public/admin/parametres.php` with Maintenance + Compte administrateur sections
|
||||||
- [x] Nav updated: “Compte” replaced by “Paramètres” linking to `parametres.php`
|
- [x] Nav updated: “Compte” replaced by “Paramètres” linking to `parametres.php`
|
||||||
|
|||||||
@@ -1,370 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
// Bootstrap application
|
// import.php is no longer a standalone page.
|
||||||
require_once __DIR__ . "/../../config/bootstrap.php";
|
// CSV import is handled inline on the list page via a dialog.
|
||||||
require_once __DIR__ . '/../../src/AdminAuth.php';
|
header('Location: /admin/');
|
||||||
|
exit();
|
||||||
// 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'; ?>
|
|
||||||
|
|||||||
@@ -10,6 +10,211 @@ if (empty($_SESSION['csrf_token'])) {
|
|||||||
$pageTitle = "Liste des TFE";
|
$pageTitle = "Liste des TFE";
|
||||||
require_once __DIR__ . '/../../src/Database.php';
|
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 {
|
try {
|
||||||
$db = new Database();
|
$db = new Database();
|
||||||
$searchQuery = isset($_GET['search']) ? trim($_GET['search']) : '';
|
$searchQuery = isset($_GET['search']) ? trim($_GET['search']) : '';
|
||||||
@@ -72,11 +277,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main id="main-content">
|
<main id="main-content">
|
||||||
|
<div class="admin-list-header">
|
||||||
|
<div class="admin-list-header__top">
|
||||||
<h1>Liste des TFE</h1>
|
<h1>Liste des TFE</h1>
|
||||||
|
|
||||||
<?php include APP_ROOT . '/templates/partials/flash-messages.php'; ?>
|
|
||||||
|
|
||||||
<!-- Stats (always reflects full DB, independent of active filters) -->
|
|
||||||
<dl class="admin-stats">
|
<dl class="admin-stats">
|
||||||
<div class="admin-stat">
|
<div class="admin-stat">
|
||||||
<dt class="admin-stat__label">TFE total</dt>
|
<dt class="admin-stat__label">TFE total</dt>
|
||||||
@@ -91,6 +294,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
<dd class="admin-stat__number"><?= $stats['pending'] ?></dd>
|
<dd class="admin-stat__number"><?= $stats['pending'] ?></dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</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'; ?>
|
||||||
|
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
<form class="admin-filters" method="get" action="/admin/">
|
<form class="admin-filters" method="get" action="/admin/">
|
||||||
@@ -216,4 +427,66 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
?>
|
?>
|
||||||
</main>
|
</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()">✕</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'; ?>
|
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>
|
||||||
|
|||||||
@@ -38,7 +38,8 @@
|
|||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-body main > h1 {
|
.admin-body main > h1,
|
||||||
|
.admin-list-header > h1 {
|
||||||
font-size: var(--step-2);
|
font-size: var(--step-2);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
@@ -306,22 +307,22 @@
|
|||||||
.admin-stats {
|
.admin-stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--space-s);
|
gap: var(--space-s);
|
||||||
margin-bottom: var(--space-l);
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-stat {
|
.admin-stat {
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border: 1px solid var(--border-primary);
|
border: 1px solid var(--border-primary);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: var(--space-s) var(--space-m);
|
padding: var(--space-2xs) var(--space-s);
|
||||||
min-width: 140px;
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-stat__number {
|
.admin-stat__number {
|
||||||
font-size: var(--step-3);
|
font-size: var(--step-1);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--accent-primary);
|
color: var(--accent-primary);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@@ -330,7 +331,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.admin-stat__label {
|
.admin-stat__label {
|
||||||
font-size: var(--step--1);
|
font-size: var(--step--2);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
margin-top: var(--space-3xs);
|
margin-top: var(--space-3xs);
|
||||||
order: 2;
|
order: 2;
|
||||||
@@ -905,6 +906,123 @@
|
|||||||
margin-top: var(--space-2xs);
|
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 ─────────────────────────────────────────────── */
|
/* ── Settings page sections ─────────────────────────────────────────────── */
|
||||||
.admin-settings-section {
|
.admin-settings-section {
|
||||||
border: 1px solid var(--border-primary);
|
border: 1px solid var(--border-primary);
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ $_thesisId = $_GET['id'] ?? null;
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="/admin/" <?= $_currentPage === 'index.php' ? 'aria-current="page"' : '' ?>>Liste des TFE</a></li>
|
<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/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/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/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>
|
<li><a href="/admin/system.php" <?= in_array($_currentPage, ['system.php', 'status.php', 'logs.php']) ? 'aria-current="page"' : '' ?>>Système</a></li>
|
||||||
|
|||||||
Reference in New Issue
Block a user