diff --git a/TODO.md b/TODO.md index 5bf5985..a5f95d5 100644 --- a/TODO.md +++ b/TODO.md @@ -14,3 +14,41 @@ - [x] Fix: format-extras not appearing — moved #format-extras-block inside Fichiers fieldset (after annexes), uses hx-select to extract from response - [x] Remove duration_pages, duration_minutes, file_size_info entirely (form, schema, DB, views, controllers, tests, CSV export, email) - [x] Rename cc4r → cc2r everywhere (DB column, schema, PHP code) to fix pre-existing naming inconsistency +- [x] Merge Publication fieldset's is_published checkbox into Backoffice fieldset +- [x] Fix: PHP parse error in admin/index.php — `''` escape in single-quoted string not valid in PHP 8.5 +- [x] Add explanation hint to is_published checkbox +- [x] Admin index: use AP code instead of full name in list and filter dropdown +- [x] Admin index: remove pagination, show all theses in table +- [x] Admin index: HTMX column sorting (click header → reload table via HTMX) +- [x] Admin index: prevent action buttons from stacking vertically +- [x] Admin index: compact icon-only buttons (SVG) with tooltips replacing text labels +- [x] Admin index: reduce status badge font size +- [x] Admin index: change Voir icon to spectacles/circles SVG +- [x] Admin index: split Statut column into Publié and Accès +- [x] Admin index: tighten table cell padding to --space-3xs +- [x] Admin index: remove main padding, add padding to .admin-list-toolbar and #admin-table-wrap +- [x] Admin index: remove subtitles from Titre column +- [x] Admin index: add alternating row background colors +- [x] Admin index: remove #admin-table-container wrapper element, use #admin-table-wrap +- [x] Admin index: rearrange toolbar — stats beside title, buttons in single row, search inline with selects on right +- [x] Admin index: fix toolbar search inputs vertical stacking (add flex-direction: row) +- [x] Admin index: stats as fieldsets with legend labels (Total/Publiés/Attente), centered content +- [x] Admin index: remove horizontal padding from toolbar and table-wrap (keep bottom padding only) +- [x] Admin index: make Filtrer/Réinitialiser buttons same size as inputs (add btn--sm) +- [x] Admin index: rename Importer un CSV → Importer, merge Export CSV + Export fichiers → Exporter modal with checkboxes +- [x] Create unified /admin/actions/export.php endpoint with ?csv=1&files=1&db=1 support +- [x] Admin index: move export DB from parametres into exporter modal +- [x] Admin index: color stats — green for Publiés, yellow for Attente +- [x] Remove export-db fieldset and dialog from parametres.php +- [x] Replace large JS script in admin index with minimal version (8 lines vs ~70) +- [x] Bulk actions: form wraps all checkboxes, no dynamic DOM building in JS +- [x] Replace emoji/text buttons in acces.php/acces-etudiante.php with Phosphor SVG icon buttons +- [x] Replace text button in contenus.php with pencil SVG icon button +- [x] Add Phosphor Icons credit to about page +- [x] Add back-to-list arrow button to add/edit/recapitulatif/contenus-edit/tags page titles, make bigger (32px) +- [x] Remove Voir button from admin index — row click navigates to recapitulatif +- [x] Add hover highlight on clickable table rows +- [x] AP column no-wrap, N/A values greyed +- [x] Tags page: back button, admin-main--list, no padding, icon buttons, #admin-table-wrap +- [x] Move #bulk-actions into fixed-height #bulk-meta-bar at top, prevent layout shift +- [x] Credits: move Iconographie below Typographies diff --git a/app/public/admin/actions/export.php b/app/public/admin/actions/export.php new file mode 100644 index 0000000..e3d8903 --- /dev/null +++ b/app/public/admin/actions/export.php @@ -0,0 +1,98 @@ +logCsvExport(); + + $filename = 'xamxam-export-' . date('Y-m-d') . '.csv'; + + header('Content-Type: text/csv; charset=UTF-8'); + header('Content-Disposition: attachment; filename="' . $filename . '"'); + header('Cache-Control: no-cache, must-revalidate'); + + echo "\xEF\xBB\xBF"; + + $out = fopen('php://output', 'w'); + fputcsv($out, ExportController::CSV_HEADERS, ',', '"', ''); + $rows = $controller->exportAllTheses(); + foreach ($rows as $csvLine) { + fputcsv($out, $csvLine, ',', '"', ''); + } + fclose($out); + + // If only CSV, we're done + if (!$doFiles) { + exit; + } +} + +if ($doFiles) { + try { + $zipPath = $controller->createExportZip(); + $fileSize = filesize($zipPath); + $fileCount = count($controller->getAllThesisFiles()); + + AdminLogger::make()->logFilesExport($fileCount, (int)$fileSize); + + $filename = 'xamxam-files-' . date('Y-m-d') . '.zip'; + + header('Content-Type: application/zip'); + header('Content-Disposition: attachment; filename="' . $filename . '"'); + header('Content-Length: ' . $fileSize); + header('Cache-Control: no-cache, must-revalidate'); + + readfile($zipPath); + @unlink($zipPath); + } catch (Exception $e) { + error_log('Files export error: ' . $e->getMessage()); + http_response_code(500); + exit('Erreur lors de la création de l\'archive : ' . htmlspecialchars($e->getMessage())); + } +} + +if ($doDb) { + $dbPath = $controller->getDatabasePath(); + + if (!file_exists($dbPath)) { + http_response_code(500); + exit('Base de données introuvable.'); + } + + $filename = 'xamxam-db-' . date('Y-m-d') . '.sqlite'; + + header('Content-Type: application/octet-stream'); + header('Content-Disposition: attachment; filename="' . $filename . '"'); + header('Content-Length: ' . filesize($dbPath)); + header('Cache-Control: no-cache, must-revalidate'); + + AdminLogger::make()->logDbExport(); + + readfile($dbPath); +} + +exit; diff --git a/app/public/admin/index.php b/app/public/admin/index.php index fe702f0..f48dc03 100644 --- a/app/public/admin/index.php +++ b/app/public/admin/index.php @@ -403,14 +403,8 @@ try { $filters['sort'] = $sortCol; $filters['dir'] = $sortDir; - $perPage = 25; - $page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1; - $totalCount = $db->getThesesListCount($filters); - $totalPages = $totalCount > 0 ? (int) ceil($totalCount / $perPage) : 1; - $page = min($page, $totalPages); - $offset = ($page - 1) * $perPage; - - $theses = $db->getThesesList($filters, $perPage, $offset); + $theses = $db->getThesesList($filters, 0, 0); + $totalCount = count($theses); $stats = $db->getThesesStats(); $years = $db->getAllYears(); $orientations = $db->getAllOrientations(); @@ -420,9 +414,14 @@ try { die("Erreur lors du chargement de la liste."); } +$isHtmx = ($_SERVER['HTTP_HX_REQUEST'] ?? '') === 'true'; $isAdmin = true; $bodyClass = 'admin-body'; -require_once APP_ROOT . '/templates/head.php'; -include APP_ROOT . '/templates/header.php'; -include APP_ROOT . '/templates/admin/index.php'; -require_once APP_ROOT . '/templates/admin/footer.php'; +if ($isHtmx) { + include APP_ROOT . '/templates/admin/index-table.php'; +} else { + require_once APP_ROOT . '/templates/head.php'; + include APP_ROOT . '/templates/header.php'; + include APP_ROOT . '/templates/admin/index.php'; + require_once APP_ROOT . '/templates/admin/footer.php'; +} diff --git a/app/public/assets/css/admin.css b/app/public/assets/css/admin.css index 6f6646a..6927402 100644 --- a/app/public/assets/css/admin.css +++ b/app/public/assets/css/admin.css @@ -57,11 +57,54 @@ ); } +.admin-main--list { + padding: 0 !important; +} + +#admin-table-wrap { + padding: 0 0 var(--space-2xl); +} + +.admin-body main > table tbody tr:nth-child(even) { + background: var(--bg-secondary); +} + +.admin-table-row:hover { + background: var(--blue-muted-bg) !important; +} + .admin-body main > h1, .admin-list-header > h1 { text-transform: uppercase; letter-spacing: 0.08em; margin: 0 0 var(--space-l) 0; + display: flex; + align-items: center; + gap: var(--space-2xs); +} + +.admin-back-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: var(--radius); + color: var(--text-secondary); + text-decoration: none; + transition: background 0.15s, color 0.15s; + flex-shrink: 0; +} + +.admin-back-btn:hover { + background: var(--bg-secondary); + color: var(--text-primary); +} + +.admin-back-btn svg { + width: 22px; + height: 22px; + fill: currentColor; } /* ── Form styles → see form.css ─────────────────────────────────────────── */ @@ -158,7 +201,7 @@ /* ── Stats cards ────────────────────────────────────────────────────────── */ .admin-stats { display: flex; - gap: var(--space-s); + gap: var(--space-2xs); flex-wrap: wrap; margin: 0; } @@ -167,10 +210,44 @@ background: var(--bg-secondary); border: 1px solid var(--border-primary); border-radius: var(--radius); - padding: var(--space-2xs) var(--space-s); + padding: var(--space-2xs) var(--space-xs); min-width: 0; display: flex; flex-direction: column; + align-items: center; + justify-content: center; + margin: 0; + gap: var(--space-3xs); +} + +.admin-stat--pub .admin-stat__number { + color: var(--accent-green); +} + +.admin-stat--pub .admin-stat__number:empty::after, +.admin-stat--pub .admin-stat__number[data-empty] { + color: var(--text-tertiary); +} + +.admin-stat--pend .admin-stat__number { + color: var(--accent-yellow); +} + +.admin-stat--pend .admin-stat__number:empty::after, +.admin-stat--pend .admin-stat__number[data-empty] { + color: var(--text-tertiary); +} + +.admin-stat legend { + font-size: var(--step--2); + font-weight: 400; + text-transform: none; + letter-spacing: normal; + color: var(--text-secondary); + padding: 0; + float: none; + margin: 0; + width: auto; } .admin-stat__number { @@ -178,16 +255,9 @@ font-weight: 700; color: var(--accent-primary); line-height: 1; - order: 1; - margin: 0; } -.admin-stat__label { - font-size: var(--step--2); - color: var(--text-secondary); - margin-top: var(--space-3xs); - order: 2; -} +/* ── Stats (inline badge-like counters) ────────────────────────────── */ /* ── Maintenance bar ────────────────────────────────────────────────────── */ .admin-maintenance-bar { @@ -223,7 +293,8 @@ /* Empty-state message below the thesis table */ .admin-empty { color: var(--text-secondary); - padding: var(--space-s) 0; + padding: var(--space-s) var(--space-2xs); + text-align: center; } /* Identifier column in the thesis table */ @@ -273,6 +344,21 @@ margin-top: var(--space-m); } +.admin-body main > table th, +.admin-body main > table td { + padding: var(--space-3xs); +} + +.admin-body main > table td.admin-ap-col, +.admin-body main > table th.admin-ap-col { + white-space: nowrap; +} + +.admin-na { + color: var(--text-tertiary); + font-style: italic; +} + /* Sortable column headers */ .admin-sort-link { color: inherit; @@ -346,11 +432,107 @@ color: var(--error); } +/* ── Compact table badges ─────────────────────────────────────────── */ +.status-badge { + font-size: 0.7rem; + padding: 2px var(--space-3xs); +} + +/* ── Compact icon buttons for table rows ──────────────────────────── */ +.admin-icon-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + padding: 3px; + border-radius: var(--radius); + border: 1px solid transparent; + background: transparent; + color: var(--text-secondary); + cursor: pointer; + text-decoration: none; + transition: background 0.15s, color 0.15s, border-color 0.15s; + line-height: 1; +} + +.admin-icon-btn svg { + width: 16px; + height: 16px; + fill: currentColor; + flex-shrink: 0; +} + +.admin-icon-btn:hover { + background: var(--bg-secondary); + color: var(--text-primary); + border-color: var(--border-primary); +} + +.admin-icon-btn--view:hover { + background: var(--blue-muted-bg); + color: var(--accent-blue); + border-color: var(--blue-muted-border); +} + +.admin-icon-btn--edit:hover { + background: var(--yellow-muted-bg); + color: var(--accent-yellow); + border-color: var(--yellow-muted-border); +} + +.admin-icon-btn--publish:hover { + background: var(--green-muted-bg); + color: var(--accent-green); + border-color: var(--green-muted-border); +} + +.admin-icon-btn--unpublish:hover { + background: var(--bg-secondary); + color: var(--text-secondary); + border-color: var(--border-primary); +} + +.admin-icon-btn--delete:hover { + background: var(--error-muted-bg); + color: var(--error); + border-color: var(--danger-border-muted); +} + +.admin-icon-btn--copy:hover { + background: var(--blue-muted-bg); + color: var(--accent-blue); + border-color: var(--blue-muted-border); +} + +.admin-icon-btn--key:hover { + background: var(--yellow-muted-bg); + color: var(--accent-yellow); + border-color: var(--yellow-muted-border); +} + +.admin-icon-btn--archive:hover { + background: var(--bg-secondary); + color: var(--text-secondary); + border-color: var(--border-primary); +} + +.admin-icon-btn--merge:hover { + background: var(--yellow-muted-bg); + color: var(--accent-yellow); + border-color: var(--yellow-muted-border); +} + /* ── Action buttons in table — see common.css .btn base class ──────────── */ .admin-actions { display: flex; gap: var(--space-3xs); - flex-wrap: wrap; + flex-wrap: nowrap; + white-space: nowrap; +} + +.admin-actions-col { + white-space: nowrap; } /* Legacy table-action size — now just an alias */ @@ -691,13 +873,14 @@ margin-top: var(--space-2xs); } -/* ── List page toolbar (title + filters + stats + import, one row) ───────── */ +/* ── List page toolbar (generic grid header + right slot) ──────────────── */ .admin-list-toolbar { display: grid; grid-template-columns: 1fr auto; gap: var(--space-m) var(--space-l); margin-bottom: var(--space-m); align-items: center; + padding: var(--space-m) 0 0; } .admin-list-toolbar h1 { @@ -749,6 +932,60 @@ gap: var(--space-xs); } +/* ── List page toolbar (theses index — single-line search) ─────────────── */ +.admin-list-toolbar--list { + display: flex; + flex-direction: column; + gap: var(--space-s); +} + +.admin-list-toolbar--list .admin-toolbar-top { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--space-m); +} + +.admin-list-toolbar--list .admin-toolbar-title-row { + display: flex; + align-items: center; + gap: var(--space-m); +} + +.admin-list-toolbar--list .admin-toolbar-title-row h1 { + margin: 0; + white-space: nowrap; +} + +.admin-list-toolbar--list .admin-toolbar-top .admin-btn-group { + display: flex; + gap: var(--space-2xs); + flex-wrap: wrap; + align-items: center; +} + +.admin-list-toolbar--list .admin-filters { + display: flex; + flex-direction: row; + gap: var(--space-xs); + align-items: center; + margin-bottom: 0; +} + +.admin-list-toolbar--list .admin-filters input[type="text"] { + flex: 1 1 auto; + min-width: 8rem; +} + +.admin-list-toolbar--list .admin-filters select { + min-width: 6rem; + flex-shrink: 0; +} + +.admin-list-toolbar--list .admin-filters .btn { + flex-shrink: 0; +} + .admin-btn-group { display: flex; align-items: center; diff --git a/app/src/Database.php b/app/src/Database.php index 955ffe7..c1d4d1a 100644 --- a/app/src/Database.php +++ b/app/src/Database.php @@ -763,7 +763,7 @@ class Database 'title' => 't.title', 'year' => 't.year', 'orientation' => 'o.name', - 'ap_program' => 'ap.name', + 'ap_program' => 'ap.code', 'is_published' => 't.is_published', 'submitted_at' => 't.submitted_at', ]; @@ -834,7 +834,7 @@ class Database $sql = 'SELECT t.id, t.identifier, t.title, t.subtitle, t.year, o.name as orientation, - ap.name as ap_program, + ap.code as ap_program, GROUP_CONCAT(DISTINCT a.name ORDER BY a.name ASC) as authors, t.submitted_at, t.is_published, diff --git a/app/storage/banners/.gitkeep b/app/storage/banners/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/storage/covers/50f5ee3dd16dd26b1eea2cc7f9719fae.png b/app/storage/covers/50f5ee3dd16dd26b1eea2cc7f9719fae.png new file mode 100644 index 0000000..d9212f5 Binary files /dev/null and b/app/storage/covers/50f5ee3dd16dd26b1eea2cc7f9719fae.png differ diff --git a/app/templates/admin/acces-etudiante.php b/app/templates/admin/acces-etudiante.php index 81fdc75..2f43d4b 100644 --- a/app/templates/admin/acces-etudiante.php +++ b/app/templates/admin/acces-etudiante.php @@ -65,43 +65,46 @@ = $statusLabel ?> -