diff --git a/TODO.md b/TODO.md index 05b9175..fc2776c 100644 --- a/TODO.md +++ b/TODO.md @@ -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] `` 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` diff --git a/public/admin/import.php b/public/admin/import.php index d4ef1ca..bcc84d9 100644 --- a/public/admin/import.php +++ b/public/admin/import.php @@ -1,370 +1,5 @@ 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)); -} -?> - - - -
-

Importer une liste de TFE

- - -
- ⚠ Erreurs : -
    - -
  • - -
-
- - - -

- - -
- - -
- -
- - - 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
- — Deux premières lignes ignorées (en-tête) — Séparateur : virgule — Encodage : UTF-8 -
-
-
- - -
- - -
-

Résultats de l'import

-
-
-
-
- -
- - \ No newline at end of file +// import.php is no longer a standalone page. +// CSV import is handled inline on the list page via a dialog. +header('Location: /admin/'); +exit(); diff --git a/public/admin/index.php b/public/admin/index.php index 851bd6a..7379d5c 100644 --- a/public/admin/index.php +++ b/public/admin/index.php @@ -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', () => {
-

Liste des TFE

+
+
+

Liste des TFE

+
+
+
TFE total
+
+
+
+
Publiés
+
+
+
+
En attente
+
+
+
+
+ +
- -
-
-
TFE total
-
-
-
-
Publiés
-
-
-
-
En attente
-
-
-
-
{ ?>
+ + +
+

Importer une liste de TFE

+ +
+ + + + + + +

+ + + + + +
+ +
+ + + 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
+ Quatre premières lignes ignorées — Séparateur : virgule — UTF-8 +
+
+
+ + + + + +
+

Résultats

+
    + +
  • + +
+
+ +
+ + + + + diff --git a/public/assets/css/admin.css b/public/assets/css/admin.css index c8d6998..c93d0a5 100644 --- a/public/assets/css/admin.css +++ b/public/assets/css/admin.css @@ -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); diff --git a/templates/header.php b/templates/header.php index a60cee8..0ce0300 100644 --- a/templates/header.php +++ b/templates/header.php @@ -18,7 +18,6 @@ $_thesisId = $_GET['id'] ?? null;