diff --git a/TODO.md b/TODO.md index 38b103b..698d78f 100644 --- a/TODO.md +++ b/TODO.md @@ -78,6 +78,7 @@ - [x] Variables.css → backward-compat wrapper importing colors.css + typography.css - [x] Update comment references from common.css → component files - [x] Reverted CSS nesting (native CSS nesting breaks in browsers without support; no build step available) +- [x] Unnest header.css (was missed in original nesting revert — admin nav-left-links rendered vertically) - [x] reset.css: modern-normalize base (not Tailwind Preflight) to avoid border/list/heading regressions - [x] search.css: restored !important flags on input (overrides forms.css base selectors) - [x] acces.php: copy password button now shows toast feedback diff --git a/app/public/admin/actions/export.php b/app/public/admin/actions/export.php index b130392..139e4ad 100644 --- a/app/public/admin/actions/export.php +++ b/app/public/admin/actions/export.php @@ -21,6 +21,18 @@ $doCsv = !empty($_GET['csv']); $doFiles = !empty($_GET['files']); $doDb = !empty($_GET['db']); +// Optional: filter by selected thesis IDs (bulk selection export) +$idsRaw = $_GET['ids'] ?? ''; +$selectedIds = []; +if ($idsRaw !== '') { + foreach (explode(',', $idsRaw) as $id) { + $id = (int) trim($id); + if ($id > 0) { + $selectedIds[] = $id; + } + } +} + if (!$doCsv && !$doFiles && !$doDb) { $doCsv = true; } @@ -40,7 +52,7 @@ if ($doCsv) { $out = fopen('php://output', 'w'); fputcsv($out, ExportController::CSV_HEADERS, ',', '"', ''); - $rows = $controller->exportAllTheses(); + $rows = $controller->exportAllTheses($selectedIds); foreach ($rows as $csvLine) { fputcsv($out, $csvLine, ',', '"', ''); } @@ -53,10 +65,14 @@ if ($doCsv) { } if ($doFiles) { + if (!class_exists('ZipArchive')) { + http_response_code(500); + exit('Module PHP zip non installé sur le serveur. Contactez l\'administrateur système (apt install php8.4-zip).'); + } try { - $zipPath = $controller->createExportZip(); + $zipPath = $controller->createExportZip($selectedIds); $fileSize = filesize($zipPath); - $fileCount = count($controller->getAllThesisFiles()); + $fileCount = count($controller->getAllThesisFiles($selectedIds)); AdminLogger::make()->logFilesExport($fileCount, (int)$fileSize); diff --git a/app/public/assets/css/admin.css b/app/public/assets/css/admin.css index 721bafe..4e2d4b0 100644 --- a/app/public/assets/css/admin.css +++ b/app/public/assets/css/admin.css @@ -74,6 +74,16 @@ padding: 0 0 var(--space-2xl); } +#admin-table-wrap table thead { + position: sticky; + top: 0; + z-index: 5; +} + +#admin-table-wrap table thead th { + background: var(--bg-primary); +} + .admin-body main > table tbody tr:nth-child(even) { background: var(--bg-secondary); } diff --git a/app/src/Controllers/ExportController.php b/app/src/Controllers/ExportController.php index 2992711..822f9b3 100644 --- a/app/src/Controllers/ExportController.php +++ b/app/src/Controllers/ExportController.php @@ -40,16 +40,17 @@ class ExportController // ── Files export ──────────────────────────────────────────────────── /** - * Fetch all thesis file records with their thesis identifier. + * Fetch thesis file records with their thesis identifier. * + * @param int[] $thesisIds Optional filter by thesis IDs. * @return list */ - public function getAllThesisFiles(): array + public function getAllThesisFiles(array $thesisIds = []): array { - return $this->db->getAllThesisFilesForExport(); + return $this->db->getAllThesisFilesForExport($thesisIds); } /** @@ -58,12 +59,13 @@ class ExportController * The manifest maps identifier → { title, files: [{type, path, name, size, mime, hash, label}] } * and is used on restore to re-link files to DB records. * + * @param int[] $thesisIds Optional filter by thesis IDs. * @return array */ - public function buildExportManifest(): array + public function buildExportManifest(array $thesisIds = []): array { - $files = $this->getAllThesisFiles(); - $theses = $this->db->getAllThesesForExport(); + $files = $this->getAllThesisFiles($thesisIds); + $theses = $this->db->getAllThesesForExport($thesisIds); // Index theses by id for O(1) lookup $byId = []; @@ -115,17 +117,18 @@ class ExportController * Returns the path to the temporary zip file. Caller is responsible * for unlink() after streaming. * + * @param int[] $thesisIds Optional filter by thesis IDs. * @param string|null $baseDir Base directory path for files inside the zip. * Defaults to "files" (so files are at "files/theses/..."). * @return string Absolute path to the generated zip file. * @throws Exception if zip creation fails. */ - public function createExportZip(?string $baseDir = null): string + public function createExportZip(array $thesisIds = [], ?string $baseDir = null): string { $baseDir ??= 'files'; $storageRoot = defined('STORAGE_ROOT') ? STORAGE_ROOT : APP_ROOT . '/storage'; - $files = $this->getAllThesisFiles(); - $manifest = $this->buildExportManifest(); + $files = $this->getAllThesisFiles($thesisIds); + $manifest = $this->buildExportManifest($thesisIds); $tmpPath = tempnam(sys_get_temp_dir(), 'xamxam-export-'); if ($tmpPath === false) { @@ -214,12 +217,13 @@ class ExportController * * Uses batch queries (one per related table) to avoid N+1. * + * @param int[] $thesisIds Optional filter by thesis IDs. * @return list> Each inner list has CSV_HEADERS_COUNT elements. */ - public function exportAllTheses(): array + public function exportAllTheses(array $thesisIds = []): array { // 1) Base thesis data (includes license_name via migration; fallback to license_type from the view) - $theses = $this->db->getAllThesesForExport(); + $theses = $this->db->getAllThesesForExport($thesisIds); if ($theses === []) { return []; } diff --git a/app/src/Database.php b/app/src/Database.php index 7ae8b39..924edb3 100644 --- a/app/src/Database.php +++ b/app/src/Database.php @@ -2540,9 +2540,12 @@ class Database * Fetch all theses (admin — includes unpublished) with every column * needed for the CSV export. */ - public function getAllThesesForExport(): array + /** + * @param int[] $thesisIds Optional filter by thesis IDs. + */ + public function getAllThesesForExport(array $thesisIds = []): array { - return $this->pdo->query(' + $sql = ' SELECT t.id, t.identifier, t.title, t.subtitle, t.year, o.name AS orientation, @@ -2561,88 +2564,143 @@ class Database LEFT JOIN finality_types ft ON t.finality_id = ft.id LEFT JOIN access_types at ON t.access_type_id = at.id LEFT JOIN license_types lt ON t.license_id = lt.id - ORDER BY t.year DESC, t.title ASC - ')->fetchAll(); + '; + if ($thesisIds) { + $placeholders = implode(',', array_fill(0, count($thesisIds), '?')); + $sql .= " WHERE t.id IN ($placeholders)"; + $stmt = $this->pdo->prepare($sql . ' ORDER BY t.year DESC, t.title ASC'); + $stmt->execute($thesisIds); + return $stmt->fetchAll(); + } + return $this->pdo->query($sql . ' ORDER BY t.year DESC, t.title ASC')->fetchAll(); } /** * All thesis→author rows with author name and email. + * @param int[] $thesisIds Optional filter. */ - public function getAllThesisAuthorsForExport(): array + public function getAllThesisAuthorsForExport(array $thesisIds = []): array { - return $this->pdo->query(' + $sql = ' SELECT ta.thesis_id, a.name, a.email FROM thesis_authors ta JOIN authors a ON a.id = ta.author_id - ORDER BY ta.thesis_id, ta.author_order - ')->fetchAll(); + '; + if ($thesisIds) { + $placeholders = implode(',', array_fill(0, count($thesisIds), '?')); + $sql .= " WHERE ta.thesis_id IN ($placeholders)"; + $stmt = $this->pdo->prepare($sql . ' ORDER BY ta.thesis_id, ta.author_order'); + $stmt->execute($thesisIds); + return $stmt->fetchAll(); + } + return $this->pdo->query($sql . ' ORDER BY ta.thesis_id, ta.author_order')->fetchAll(); } /** * All thesis→supervisor rows with name. + * @param int[] $thesisIds Optional filter. */ - public function getAllThesisSupervisorsForExport(): array + public function getAllThesisSupervisorsForExport(array $thesisIds = []): array { - return $this->pdo->query(' + $sql = ' SELECT ts.thesis_id, s.name, ts.role, ts.is_external, ts.is_ulb FROM thesis_supervisors ts JOIN supervisors s ON s.id = ts.supervisor_id - ORDER BY ts.thesis_id, ts.supervisor_order - ')->fetchAll(); + '; + if ($thesisIds) { + $placeholders = implode(',', array_fill(0, count($thesisIds), '?')); + $sql .= " WHERE ts.thesis_id IN ($placeholders)"; + $stmt = $this->pdo->prepare($sql . ' ORDER BY ts.thesis_id, ts.supervisor_order'); + $stmt->execute($thesisIds); + return $stmt->fetchAll(); + } + return $this->pdo->query($sql . ' ORDER BY ts.thesis_id, ts.supervisor_order')->fetchAll(); } /** * All thesis→tag rows with tag name. + * @param int[] $thesisIds Optional filter. */ - public function getAllThesisTagsForExport(): array + public function getAllThesisTagsForExport(array $thesisIds = []): array { - return $this->pdo->query(' + $sql = ' SELECT tt.thesis_id, t.name FROM thesis_tags tt JOIN tags t ON t.id = tt.tag_id - ORDER BY tt.thesis_id, t.name - ')->fetchAll(); + '; + if ($thesisIds) { + $placeholders = implode(',', array_fill(0, count($thesisIds), '?')); + $sql .= " WHERE tt.thesis_id IN ($placeholders)"; + $stmt = $this->pdo->prepare($sql . ' ORDER BY tt.thesis_id, t.name'); + $stmt->execute($thesisIds); + return $stmt->fetchAll(); + } + return $this->pdo->query($sql . ' ORDER BY tt.thesis_id, t.name')->fetchAll(); } /** * All thesis→language rows with language name. + * @param int[] $thesisIds Optional filter. */ - public function getAllThesisLanguagesForExport(): array + public function getAllThesisLanguagesForExport(array $thesisIds = []): array { - return $this->pdo->query(' + $sql = ' SELECT tl.thesis_id, l.name FROM thesis_languages tl JOIN languages l ON l.id = tl.language_id - ORDER BY tl.thesis_id, l.name - ')->fetchAll(); + '; + if ($thesisIds) { + $placeholders = implode(',', array_fill(0, count($thesisIds), '?')); + $sql .= " WHERE tl.thesis_id IN ($placeholders)"; + $stmt = $this->pdo->prepare($sql . ' ORDER BY tl.thesis_id, l.name'); + $stmt->execute($thesisIds); + return $stmt->fetchAll(); + } + return $this->pdo->query($sql . ' ORDER BY tl.thesis_id, l.name')->fetchAll(); } /** * All thesis→format rows with format name. + * @param int[] $thesisIds Optional filter. */ - public function getAllThesisFormatsForExport(): array + public function getAllThesisFormatsForExport(array $thesisIds = []): array { - return $this->pdo->query(' + $sql = ' SELECT tf.thesis_id, ft.name FROM thesis_formats tf JOIN format_types ft ON ft.id = tf.format_id - ORDER BY tf.thesis_id, ft.name - ')->fetchAll(); + '; + if ($thesisIds) { + $placeholders = implode(',', array_fill(0, count($thesisIds), '?')); + $sql .= " WHERE tf.thesis_id IN ($placeholders)"; + $stmt = $this->pdo->prepare($sql . ' ORDER BY tf.thesis_id, ft.name'); + $stmt->execute($thesisIds); + return $stmt->fetchAll(); + } + return $this->pdo->query($sql . ' ORDER BY tf.thesis_id, ft.name')->fetchAll(); } /** * All thesis files for the file-export ZIP. * Includes every thesis_files column + the thesis identifier for manifest * construction. + * @param int[] $thesisIds Optional filter. */ - public function getAllThesisFilesForExport(): array + public function getAllThesisFilesForExport(array $thesisIds = []): array { - return $this->pdo->query(' + $sql = ' SELECT tf.*, t.identifier FROM thesis_files tf JOIN theses t ON t.id = tf.thesis_id - ORDER BY t.year DESC, t.title ASC, tf.sort_order ASC - ')->fetchAll(); + '; + if ($thesisIds) { + $placeholders = implode(',', array_fill(0, count($thesisIds), '?')); + $sql .= " WHERE tf.thesis_id IN ($placeholders)"; + $stmt = $this->pdo->prepare($sql . ' ORDER BY t.year DESC, t.title ASC, tf.sort_order ASC'); + $stmt->execute($thesisIds); + return $stmt->fetchAll(); + } + return $this->pdo->query($sql . ' ORDER BY t.year DESC, t.title ASC, tf.sort_order ASC')->fetchAll(); } // ======================================================================== diff --git a/app/templates/admin/index-table.php b/app/templates/admin/index-table.php index 0e2ff9f..838199e 100644 --- a/app/templates/admin/index-table.php +++ b/app/templates/admin/index-table.php @@ -31,6 +31,8 @@ $sortArrow = function(string $col) use ($sortCol, $sortDir): string {
+ +
diff --git a/app/templates/admin/index.php b/app/templates/admin/index.php index d5274d0..a234945 100644 --- a/app/templates/admin/index.php +++ b/app/templates/admin/index.php @@ -1,8 +1,11 @@