mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 08:09:18 +02:00
refactor: Admin index — replace emoji buttons with Phosphor SVG icons, add back buttons + row click navigation, minimal JS, move export DB to Exporter modal, color stats, bulk bar anti-shift, credits reorder, tags icons
This commit is contained in:
38
TODO.md
38
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
|
||||
|
||||
98
app/public/admin/actions/export.php
Normal file
98
app/public/admin/actions/export.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
/**
|
||||
* Unified export dispatcher.
|
||||
*
|
||||
* Query params:
|
||||
* csv=1 — export CSV
|
||||
* files=1 — export ZIP of files
|
||||
*
|
||||
* If both are requested, CSV is streamed first (as a download) then the ZIP.
|
||||
* In practice, browsers handle this as two sequential downloads.
|
||||
*/
|
||||
require_once __DIR__ . "/../../../bootstrap.php";
|
||||
require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
require_once APP_ROOT . '/src/Controllers/ExportController.php';
|
||||
require_once APP_ROOT . '/src/AdminLogger.php';
|
||||
|
||||
$doCsv = !empty($_GET['csv']);
|
||||
$doFiles = !empty($_GET['files']);
|
||||
$doDb = !empty($_GET['db']);
|
||||
|
||||
if (!$doCsv && !$doFiles && !$doDb) {
|
||||
$doCsv = true;
|
||||
}
|
||||
|
||||
$controller = ExportController::create();
|
||||
|
||||
if ($doCsv) {
|
||||
AdminLogger::make()->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;
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
BIN
app/storage/covers/50f5ee3dd16dd26b1eea2cc7f9719fae.png
Normal file
BIN
app/storage/covers/50f5ee3dd16dd26b1eea2cc7f9719fae.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
@@ -65,43 +65,46 @@
|
||||
<span style="display:inline-block;padding:var(--space-3xs) var(--space-2xs);border-radius:3px;font-size:var(--step--2);font-weight:500;letter-spacing:0.04em;background:var(--error-muted-bg);color:var(--error);"><?= $statusLabel ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= $hasPassword ? '🔒 Oui' : 'Non' ?></td>
|
||||
<td><?= $hasPassword ? 'Oui' : 'Non' ?></td>
|
||||
<td style="text-align:center;"><?= intval($link['usage_count']) ?></td>
|
||||
<td><?= $expires ?></td>
|
||||
<td><?= $created ?></td>
|
||||
<td>
|
||||
<td class="admin-actions-col">
|
||||
<div class="admin-actions">
|
||||
<a href="/partage/<?= urlencode($link['slug']) ?>" target="_blank" rel="noopener"
|
||||
class="btn btn--sm btn--green admin-btn-visit" title="Visiter le formulaire">
|
||||
👁 Visiter
|
||||
class="admin-icon-btn admin-icon-btn--view" title="Visiter le formulaire">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M232.4,114.49,88.32,26.35a16,16,0,0,0-16.2-.3A15.86,15.86,0,0,0,64,39.87V216.13A15.94,15.94,0,0,0,80,232a16.07,16.07,0,0,0,8.36-2.35L232.4,141.51a15.81,15.81,0,0,0,0-27ZM80,215.94V40l143.83,88Z"></path></svg>
|
||||
</a>
|
||||
<button type="button" class="btn btn--sm btn--blue admin-btn-view"
|
||||
onclick="copyUrl(<?= $link['id'] ?>)" title="Copier l'URL">
|
||||
Copier
|
||||
<button type="button" class="admin-icon-btn admin-icon-btn--copy" title="Copier l'URL"
|
||||
onclick="copyUrl(<?= $link['id'] ?>)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32ZM160,208H48V96H160Zm48-48H176V88a8,8,0,0,0-8-8H96V48H208Z"></path></svg>
|
||||
</button>
|
||||
<form method="post" action="actions/acces-etudiante.php" class="publish-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="action" value="toggle">
|
||||
<input type="hidden" name="id" value="<?= $link['id'] ?>">
|
||||
<button type="submit"
|
||||
class="btn btn--sm <?= $link['is_active'] ? 'btn--muted admin-btn-unpublish' : 'btn--green admin-btn-publish' ?>"
|
||||
class="admin-icon-btn <?= $link['is_active'] ? 'admin-icon-btn--unpublish' : 'admin-icon-btn--publish' ?>"
|
||||
title="<?= $link['is_active'] ? 'Désactiver' : 'Activer' ?>">
|
||||
<?= $link['is_active'] ? '⏸' : '▶' ?>
|
||||
<?php if ($link['is_active']): ?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M200,32H160a16,16,0,0,0-16,16V208a16,16,0,0,0,16,16h40a16,16,0,0,0,16-16V48A16,16,0,0,0,200,32Zm0,176H160V48h40ZM96,32H56A16,16,0,0,0,40,48V208a16,16,0,0,0,16,16H96a16,16,0,0,0,16-16V48A16,16,0,0,0,96,32Zm0,176H56V48H96Z"></path></svg>
|
||||
<?php else: ?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M232.4,114.49,88.32,26.35a16,16,0,0,0-16.2-.3A15.86,15.86,0,0,0,64,39.87V216.13A15.94,15.94,0,0,0,80,232a16.07,16.07,0,0,0,8.36-2.35L232.4,141.51a15.81,15.81,0,0,0,0-27ZM80,215.94V40l143.83,88Z"></path></svg>
|
||||
<?php endif; ?>
|
||||
</button>
|
||||
</form>
|
||||
<button type="button" class="btn btn--sm btn--yellow admin-btn-edit"
|
||||
onclick="openPasswordDialog(<?= $link['id'] ?>, <?= $hasPassword ? 'true' : 'false' ?>)"
|
||||
title="Modifier le mot de passe">
|
||||
🔑
|
||||
<button type="button" class="admin-icon-btn admin-icon-btn--key" title="Modifier le mot de passe"
|
||||
onclick="openPasswordDialog(<?= $link['id'] ?>, <?= $hasPassword ? 'true' : 'false' ?>)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M216.57,39.43A80,80,0,0,0,83.91,120.78L28.69,176A15.86,15.86,0,0,0,24,187.31V216a16,16,0,0,0,16,16H72a8,8,0,0,0,8-8V208H96a8,8,0,0,0,8-8V184h16a8,8,0,0,0,5.66-2.34l9.56-9.57A79.73,79.73,0,0,0,160,176h.1A80,80,0,0,0,216.57,39.43ZM224,98.1c-1.09,34.09-29.75,61.86-63.89,61.9H160a63.7,63.7,0,0,1-23.65-4.51,8,8,0,0,0-8.84,1.68L116.69,168H96a8,8,0,0,0-8,8v16H72a8,8,0,0,0-8,8v16H40V187.31l58.83-58.82a8,8,0,0,0,1.68-8.84A63.72,63.72,0,0,1,96,95.92c0-34.14,27.81-62.8,61.9-63.89A64,64,0,0,1,224,98.1ZM192,76a12,12,0,1,1-12-12A12,12,0,0,1,192,76Z"></path></svg>
|
||||
</button>
|
||||
<form method="post" action="actions/acces-etudiante.php" class="publish-form"
|
||||
id="delete-link-form-<?= $link['id'] ?>">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="id" value="<?= $link['id'] ?>">
|
||||
<button type="button" class="btn btn--sm btn--red admin-btn-delete" title="Supprimer"
|
||||
<button type="button" class="admin-icon-btn admin-icon-btn--delete" title="Supprimer"
|
||||
onclick="openDeleteLinkDialog(<?= $link['id'] ?>)">
|
||||
🗑
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -73,48 +73,46 @@
|
||||
<span style="display:inline-block;padding:var(--space-3xs) var(--space-2xs);border-radius:3px;font-size:var(--step--2);font-weight:500;letter-spacing:0.04em;background:var(--error-muted-bg);color:var(--error);"><?= $statusLabel ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= $hasLinkPassword ? '🔒 Oui' : 'Non' ?></td>
|
||||
<td><?= $hasLinkPassword ? 'Oui' : 'Non' ?></td>
|
||||
<td style="text-align:center;"><?= intval($link['usage_count']) ?></td>
|
||||
<td><?= $expires ?></td>
|
||||
<td><?= $created ?></td>
|
||||
<td>
|
||||
<td class="admin-actions-col">
|
||||
<div class="admin-actions">
|
||||
<a href="/partage/<?= urlencode($link['slug']) ?>" target="_blank" rel="noopener"
|
||||
class="btn btn--sm btn--green admin-btn-visit" title="Visiter le formulaire">
|
||||
👁 Visiter
|
||||
class="admin-icon-btn admin-icon-btn--view" title="Visiter le formulaire">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M232.4,114.49,88.32,26.35a16,16,0,0,0-16.2-.3A15.86,15.86,0,0,0,64,39.87V216.13A15.94,15.94,0,0,0,80,232a16.07,16.07,0,0,0,8.36-2.35L232.4,141.51a15.81,15.81,0,0,0,0-27ZM80,215.94V40l143.83,88Z"></path></svg>
|
||||
</a>
|
||||
<button type="button" class="btn btn--sm btn--blue admin-btn-view"
|
||||
onclick="copyUrl(<?= $link['id'] ?>)" title="Copier l'URL">
|
||||
Copier
|
||||
<button type="button" class="admin-icon-btn admin-icon-btn--copy" title="Copier l'URL"
|
||||
onclick="copyUrl(<?= $link['id'] ?>)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32ZM160,208H48V96H160Zm48-48H176V88a8,8,0,0,0-8-8H96V48H208Z"></path></svg>
|
||||
</button>
|
||||
<form method="post" action="actions/acces-etudiante.php" class="publish-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="action" value="toggle">
|
||||
<input type="hidden" name="id" value="<?= $link['id'] ?>">
|
||||
<button type="submit"
|
||||
class="btn btn--sm <?= $link['is_active'] ? 'btn--muted admin-btn-unpublish' : 'btn--green admin-btn-publish' ?>"
|
||||
class="admin-icon-btn <?= $link['is_active'] ? 'admin-icon-btn--unpublish' : 'admin-icon-btn--publish' ?>"
|
||||
title="<?= $link['is_active'] ? 'Désactiver' : 'Activer' ?>">
|
||||
<?= $link['is_active'] ? '⏸' : '▶' ?>
|
||||
<?php if ($link['is_active']): ?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M200,32H160a16,16,0,0,0-16,16V208a16,16,0,0,0,16,16h40a16,16,0,0,0,16-16V48A16,16,0,0,0,200,32Zm0,176H160V48h40ZM96,32H56A16,16,0,0,0,40,48V208a16,16,0,0,0,16,16H96a16,16,0,0,0,16-16V48A16,16,0,0,0,96,32Zm0,176H56V48H96Z"></path></svg>
|
||||
<?php else: ?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M232.4,114.49,88.32,26.35a16,16,0,0,0-16.2-.3A15.86,15.86,0,0,0,64,39.87V216.13A15.94,15.94,0,0,0,80,232a16.07,16.07,0,0,0,8.36-2.35L232.4,141.51a15.81,15.81,0,0,0,0-27ZM80,215.94V40l143.83,88Z"></path></svg>
|
||||
<?php endif; ?>
|
||||
</button>
|
||||
</form>
|
||||
<button type="button" class="btn btn--sm btn--yellow admin-btn-edit"
|
||||
onclick="openEditLinkDialog(<?= $link['id'] ?>, <?= htmlspecialchars(json_encode($linkName), ENT_QUOTES) ?>, <?= htmlspecialchars(json_encode($linkExpiresVal), ENT_QUOTES) ?>, <?= htmlspecialchars(json_encode($linkLockedYear), ENT_QUOTES) ?>)"
|
||||
title="Modifier le lien">
|
||||
✏️
|
||||
</button>
|
||||
<button type="button" class="btn btn--sm btn--yellow admin-btn-edit"
|
||||
onclick="openPasswordDialog(<?= $link['id'] ?>, <?= $hasLinkPassword ? 'true' : 'false' ?>)"
|
||||
title="Modifier le mot de passe">
|
||||
🔑
|
||||
<button type="button" class="admin-icon-btn admin-icon-btn--key" title="Modifier le mot de passe"
|
||||
onclick="openPasswordDialog(<?= $link['id'] ?>, <?= $hasLinkPassword ? 'true' : 'false' ?>)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M216.57,39.43A80,80,0,0,0,83.91,120.78L28.69,176A15.86,15.86,0,0,0,24,187.31V216a16,16,0,0,0,16,16H72a8,8,0,0,0,8-8V208H96a8,8,0,0,0,8-8V184h16a8,8,0,0,0,5.66-2.34l9.56-9.57A79.73,79.73,0,0,0,160,176h.1A80,80,0,0,0,216.57,39.43ZM224,98.1c-1.09,34.09-29.75,61.86-63.89,61.9H160a63.7,63.7,0,0,1-23.65-4.51,8,8,0,0,0-8.84,1.68L116.69,168H96a8,8,0,0,0-8,8v16H72a8,8,0,0,0-8,8v16H40V187.31l58.83-58.82a8,8,0,0,0,1.68-8.84A63.72,63.72,0,0,1,96,95.92c0-34.14,27.81-62.8,61.9-63.89A64,64,0,0,1,224,98.1ZM192,76a12,12,0,1,1-12-12A12,12,0,0,1,192,76Z"></path></svg>
|
||||
</button>
|
||||
<form method="post" action="actions/acces-etudiante.php" class="publish-form"
|
||||
id="archive-link-form-<?= $link['id'] ?>">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="action" value="archive">
|
||||
<input type="hidden" name="id" value="<?= $link['id'] ?>">
|
||||
<button type="button" class="btn btn--sm btn--red admin-btn-delete" title="Archiver"
|
||||
<button type="button" class="admin-icon-btn admin-icon-btn--archive" title="Archiver"
|
||||
onclick="openArchiveLinkDialog(<?= $link['id'] ?>)">
|
||||
🗄
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M224,48H32A16,16,0,0,0,16,64V88a16,16,0,0,0,16,16v88a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V104a16,16,0,0,0,16-16V64A16,16,0,0,0,224,48ZM208,192H48V104H208ZM224,88H32V64H224V88ZM96,136a8,8,0,0,1,8-8h48a8,8,0,0,1,0,16H104A8,8,0,0,1,96,136Z"></path></svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<main id="main-content">
|
||||
<h1>Ajouter un TFE</h1>
|
||||
<h1><a href="/admin/" class="admin-back-btn" title="Retour à la liste"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm48-88a8,8,0,0,1-8,8H107.31l18.35,18.34a8,8,0,0,1-11.32,11.32l-32-32a8,8,0,0,1,0-11.32l32-32a8,8,0,0,1,11.32,11.32L107.31,120H168A8,8,0,0,1,176,128Z"></path></svg></a> Ajouter un TFE</h1>
|
||||
|
||||
<?php
|
||||
// ── Variables for the shared form partial ─────────────────────────────────
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<main id="main-content">
|
||||
<h1>Éditer : <?= htmlspecialchars($editTitle) ?></h1>
|
||||
<h1><a href="/admin/contenus.php" class="admin-back-btn" title="Retour"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm48-88a8,8,0,0,1-8,8H107.31l18.35,18.34a8,8,0,0,1-11.32,11.32l-32-32a8,8,0,0,1,0-11.32l32-32a8,8,0,0,1,11.32,11.32L107.31,120H168A8,8,0,0,1,176,128Z"></path></svg></a> Éditer : <?= htmlspecialchars($editTitle) ?></h1>
|
||||
|
||||
<?php if ($editType === 'about_page'): ?>
|
||||
|
||||
|
||||
@@ -30,7 +30,9 @@
|
||||
<td><?= htmlspecialchars($p['updated_at'] ?? '—') ?></td>
|
||||
<td>
|
||||
<a href="/admin/contenus-edit.php?slug=<?= urlencode($p['slug']) ?>"
|
||||
class="btn btn--primary btn--sm">Éditer</a>
|
||||
class="admin-icon-btn admin-icon-btn--edit" title="Éditer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M248,92.68a15.86,15.86,0,0,0-4.69-11.31L174.63,12.68a16,16,0,0,0-22.63,0L123.57,41.11l-58,21.77A16.06,16.06,0,0,0,55.35,75.23L32.11,214.68A8,8,0,0,0,40,224a8.4,8.4,0,0,0,1.32-.11l139.44-23.24a16,16,0,0,0,12.35-10.17l21.77-58L243.31,104A15.87,15.87,0,0,0,248,92.68Zm-69.87,92.19L63.32,204l47.37-47.37a28,28,0,1,0-11.32-11.32L52,192.7,71.13,77.86,126,57.29,198.7,130ZM112,132a12,12,0,1,1,12,12A12,12,0,0,1,112,132Zm96-15.32L139.31,48l24-24L232,92.68Z"></path></svg>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<main id="main-content">
|
||||
<h1>Modifier un TFE</h1>
|
||||
<h1><a href="/admin/" class="admin-back-btn" title="Retour à la liste"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm48-88a8,8,0,0,1-8,8H107.31l18.35,18.34a8,8,0,0,1-11.32,11.32l-32-32a8,8,0,0,1,0-11.32l32-32a8,8,0,0,1,11.32,11.32L107.31,120H168A8,8,0,0,1,176,128Z"></path></svg></a> Modifier un TFE</h1>
|
||||
|
||||
<?php
|
||||
// ── Build a unified old() callable for the entire edit form ────────────────
|
||||
|
||||
137
app/templates/admin/index-table.php
Normal file
137
app/templates/admin/index-table.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
$sortParams = array_filter([
|
||||
'search' => $searchQuery,
|
||||
'year' => $yearFilter ?: '',
|
||||
'orientation' => $orientationFilter ?: '',
|
||||
'ap' => $apFilter ?: '',
|
||||
]);
|
||||
|
||||
$sortLink = function(string $col) use ($sortCol, $sortDir, $sortParams): string {
|
||||
$params = $sortParams;
|
||||
$params['sort'] = $col;
|
||||
$params['dir'] = ($sortCol === $col && $sortDir === 'desc') ? 'asc' : 'desc';
|
||||
return '/admin/?' . http_build_query($params);
|
||||
};
|
||||
|
||||
$sortArrow = function(string $col) use ($sortCol, $sortDir): string {
|
||||
if ($sortCol !== $col) return '';
|
||||
return $sortDir === 'asc' ? ' ↑' : ' ↓';
|
||||
};
|
||||
?>
|
||||
|
||||
<div id="admin-table-wrap">
|
||||
|
||||
<form id="bulk-form" method="post" action="actions/publish.php">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" id="bulk-action-input" name="action" value="">
|
||||
<input type="hidden" name="bulk" value="1">
|
||||
|
||||
<!-- Meta bar: always visible, shows either count or bulk actions -->
|
||||
<div id="bulk-meta-bar" class="admin-bulk-meta">
|
||||
<div class="admin-bulk-meta__default">
|
||||
<strong><?= $totalCount ?> TFE</strong>
|
||||
</div>
|
||||
<div id="bulk-actions" class="admin-bulk-meta__actions" style="display:none">
|
||||
<strong><span id="selected-count">0</span> TFE(s) sélectionné(s)</strong>
|
||||
<div class="admin-bulk-btns">
|
||||
<button type="button" class="btn btn--sm btn--green admin-btn-publish" onclick="confirmBulk('publish')">Publier</button>
|
||||
<button type="button" class="btn btn--sm btn--muted admin-btn-unpublish" onclick="confirmBulk('unpublish')">Dépublier</button>
|
||||
<button type="button" class="btn btn--sm btn--red admin-btn-delete" onclick="confirmBulk('delete')">Supprimer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="admin-list-meta" style="display:none"><?= $totalCount ?> TFE</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"><input type="checkbox" onchange="toggleAll(this)"></th>
|
||||
<th scope="col"><a href="<?= $sortLink('identifier') ?>" class="admin-sort-link" hx-get="<?= htmlspecialchars($sortLink('identifier')) ?>" hx-target="#admin-table-wrap" hx-swap="innerHTML">ID<?= $sortArrow('identifier') ?></a></th>
|
||||
<th scope="col"><a href="<?= $sortLink('title') ?>" class="admin-sort-link" hx-get="<?= htmlspecialchars($sortLink('title')) ?>" hx-target="#admin-table-wrap" hx-swap="innerHTML">Titre<?= $sortArrow('title') ?></a></th>
|
||||
<th scope="col">Auteur(s)</th>
|
||||
<th scope="col"><a href="<?= $sortLink('year') ?>" class="admin-sort-link" hx-get="<?= htmlspecialchars($sortLink('year')) ?>" hx-target="#admin-table-wrap" hx-swap="innerHTML">Année<?= $sortArrow('year') ?></a></th>
|
||||
<th scope="col"><a href="<?= $sortLink('orientation') ?>" class="admin-sort-link" hx-get="<?= htmlspecialchars($sortLink('orientation')) ?>" hx-target="#admin-table-wrap" hx-swap="innerHTML">Orientation<?= $sortArrow('orientation') ?></a></th>
|
||||
<th scope="col"><a href="<?= $sortLink('ap_program') ?>" class="admin-sort-link" hx-get="<?= htmlspecialchars($sortLink('ap_program')) ?>" hx-target="#admin-table-wrap" hx-swap="innerHTML">AP<?= $sortArrow('ap_program') ?></a></th>
|
||||
<th scope="col"><a href="<?= $sortLink('is_published') ?>" class="admin-sort-link" hx-get="<?= htmlspecialchars($sortLink('is_published')) ?>" hx-target="#admin-table-wrap" hx-swap="innerHTML">Publié<?= $sortArrow('is_published') ?></a></th>
|
||||
<th scope="col">Accès</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($theses)): ?>
|
||||
<tr>
|
||||
<td colspan="10" class="admin-empty">Aucun TFE trouvé.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($theses as $thesis): ?>
|
||||
<tr class="admin-table-row" onclick="event.stopPropagation(); window.location='/admin/recapitulatif.php?id=<?= $thesis['id'] ?>'" style="cursor:pointer">
|
||||
<td><input type="checkbox" name="selected_theses[]" value="<?= $thesis['id'] ?>"></td>
|
||||
<td class="admin-table-id"><?= htmlspecialchars($thesis['identifier'] ?? $thesis['id']) ?></td>
|
||||
<td>
|
||||
<div class="thesis-title"><?= htmlspecialchars($thesis['title']) ?></div>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($thesis['authors'] ?? 'N/A') ?></td>
|
||||
<td><?= $thesis['year'] ?></td>
|
||||
<td class="admin-na"><?= htmlspecialchars($thesis['orientation'] ?? 'N/A') ?></td>
|
||||
<td class="admin-na admin-ap-col"><?= htmlspecialchars($thesis['ap_program'] ?? 'N/A') ?></td>
|
||||
<td>
|
||||
<?php $badgeType = 'publish'; $badgeValue = $thesis['is_published']; include APP_ROOT . '/templates/partials/status-badge.php'; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (!empty($thesis['access_type'])): ?>
|
||||
<?php $badgeType = 'access'; $badgeValue = $thesis['access_type']; include APP_ROOT . '/templates/partials/status-badge.php'; ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="admin-actions-col">
|
||||
<div class="admin-actions">
|
||||
<a href="/admin/edit.php?id=<?= $thesis['id'] ?>" class="admin-icon-btn admin-icon-btn--edit" title="Éditer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M248,92.68a15.86,15.86,0,0,0-4.69-11.31L174.63,12.68a16,16,0,0,0-22.63,0L123.57,41.11l-58,21.77A16.06,16.06,0,0,0,55.35,75.23L32.11,214.68A8,8,0,0,0,40,224a8.4,8.4,0,0,0,1.32-.11l139.44-23.24a16,16,0,0,0,12.35-10.17l21.77-58L243.31,104A15.87,15.87,0,0,0,248,92.68Zm-69.87,92.19L63.32,204l47.37-47.37a28,28,0,1,0-11.32-11.32L52,192.7,71.13,77.86,126,57.29,198.7,130ZM112,132a12,12,0,1,1,12,12A12,12,0,0,1,112,132Zm96-15.32L139.31,48l24-24L232,92.68Z"></path></svg>
|
||||
</a>
|
||||
<form method="post" action="actions/publish.php" class="publish-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="thesis_id" value="<?= $thesis['id'] ?>">
|
||||
<?php if ($thesis['is_published']): ?>
|
||||
<input type="hidden" name="action" value="unpublish">
|
||||
<button type="submit" class="admin-icon-btn admin-icon-btn--unpublish" title="Dépublier">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M53.92,34.62A8,8,0,1,0,42.08,45.38L61.32,66.55C25,88.84,9.38,123.2,8.69,124.76a8,8,0,0,0,0,6.5c.35.79,8.82,19.57,27.65,38.4C61.43,194.74,93.12,208,128,208a127.11,127.11,0,0,0,52.07-10.83l22,24.21a8,8,0,1,0,11.84-10.76Zm47.33,75.84,41.67,45.85a32,32,0,0,1-41.67-45.85ZM128,192c-30.78,0-57.67-11.19-79.93-33.25A133.16,133.16,0,0,1,25,128c4.69-8.79,19.66-33.39,47.35-49.38l18,19.75a48,48,0,0,0,63.66,70l14.73,16.2A112,112,0,0,1,128,192Zm6-95.43a8,8,0,0,1,3-15.72,48.16,48.16,0,0,1,38.77,42.64,8,8,0,0,1-7.22,8.71,6.39,6.39,0,0,1-.75,0,8,8,0,0,1-8-7.26A32.09,32.09,0,0,0,134,96.57Zm113.28,34.69c-.42.94-10.55,23.37-33.36,43.8a8,8,0,1,1-10.67-11.92A132.77,132.77,0,0,0,231.05,128a133.15,133.15,0,0,0-23.12-30.77C185.67,75.19,158.78,64,128,64a118.37,118.37,0,0,0-19.36,1.57A8,8,0,1,1,106,49.79,134,134,0,0,1,128,48c34.88,0,66.57,13.26,91.66,38.35,18.83,18.83,27.3,37.62,27.65,38.41A8,8,0,0,1,247.31,131.26Z"></path></svg>
|
||||
</button>
|
||||
<?php else: ?>
|
||||
<input type="hidden" name="action" value="publish">
|
||||
<button type="submit" class="admin-icon-btn admin-icon-btn--publish" title="Publier">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M247.31,124.76c-.35-.79-8.82-19.58-27.65-38.41C194.57,61.26,162.88,48,128,48S61.43,61.26,36.34,86.35C17.51,105.18,9,124,8.69,124.76a8,8,0,0,0,0,6.5c.35.79,8.82,19.57,27.65,38.4C61.43,194.74,93.12,208,128,208s66.57-13.26,91.66-38.34c18.83-18.83,27.3-37.61,27.65-38.4A8,8,0,0,0,247.31,124.76ZM128,192c-30.78,0-57.67-11.19-79.93-33.25A133.47,133.47,0,0,1,25,128,133.33,133.33,0,0,1,48.07,97.25C70.33,75.19,97.22,64,128,64s57.67,11.19,79.93,33.25A133.46,133.46,0,0,1,231.05,128C223.84,141.46,192.43,192,128,192Zm0-112a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160Z"></path></svg>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
<form method="post" action="actions/delete.php" id="delete-form-<?= $thesis['id'] ?>" class="publish-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="thesis_id" value="<?= $thesis['id'] ?>">
|
||||
<button type="button" class="admin-icon-btn admin-icon-btn--delete" title="Supprimer"
|
||||
onclick="confirmDelete(<?= $thesis['id'] ?>, <?= htmlspecialchars(json_encode($thesis['title']), ENT_QUOTES) ?>)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Bulk actions bar -->
|
||||
<div id="bulk-actions" class="admin-bulk-actions" role="toolbar" aria-label="Actions groupées" style="display:none">
|
||||
<strong><span id="selected-count">0</span> TFE(s) sélectionné(s)</strong>
|
||||
<div class="admin-bulk-btns">
|
||||
<button type="button" class="btn btn--sm btn--green admin-btn-publish" onclick="confirmBulk('publish')">Publier</button>
|
||||
<button type="button" class="btn btn--sm btn--muted admin-btn-unpublish" onclick="confirmBulk('unpublish')">Dépublier</button>
|
||||
<button type="button" class="btn btn--sm btn--red admin-btn-delete" onclick="confirmBulk('delete')">Supprimer</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('input[name="selected_theses[]"]').forEach(cb => cb.addEventListener('change', updateBulk));
|
||||
</script>
|
||||
</div>
|
||||
@@ -1,282 +1,82 @@
|
||||
<script>
|
||||
function toggleAll(src) {
|
||||
document.querySelectorAll('input[name="selected_theses[]"]').forEach(cb => cb.checked = src.checked);
|
||||
updateBulk();
|
||||
}
|
||||
function updateBulk() {
|
||||
const checked = document.querySelectorAll('input[name="selected_theses[]"]:checked');
|
||||
const bulk = document.getElementById('bulk-actions');
|
||||
document.getElementById('selected-count').textContent = checked.length;
|
||||
bulk.style.display = checked.length > 0 ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
// Pending bulk action state
|
||||
let _pendingBulkAction = null;
|
||||
|
||||
function bulkAction(action) {
|
||||
const checked = document.querySelectorAll('input[name="selected_theses[]"]:checked');
|
||||
if (!checked.length) {
|
||||
document.getElementById('no-selection-dialog').showModal();
|
||||
return;
|
||||
}
|
||||
_pendingBulkAction = action;
|
||||
let word, endpoint;
|
||||
if (action === 'publish') { word = 'publier'; endpoint = 'actions/publish.php'; }
|
||||
else if (action === 'unpublish') { word = 'dépublier'; endpoint = 'actions/publish.php'; }
|
||||
else if (action === 'delete') { word = 'supprimer'; endpoint = 'actions/delete.php'; }
|
||||
else return;
|
||||
if (action === 'delete') {
|
||||
document.getElementById('bulk-delete-count').textContent = checked.length;
|
||||
document.getElementById('bulk-delete-dialog').showModal();
|
||||
} else {
|
||||
document.getElementById('bulk-confirm-word').textContent = word.charAt(0).toUpperCase() + word.slice(1);
|
||||
document.getElementById('bulk-confirm-count').textContent = checked.length;
|
||||
document.getElementById('bulk-confirm-dialog').showModal();
|
||||
}
|
||||
}
|
||||
|
||||
function _executeBulkAction() {
|
||||
const action = _pendingBulkAction;
|
||||
if (!action) return;
|
||||
let endpoint;
|
||||
if (action === 'publish' || action === 'unpublish') { endpoint = 'actions/publish.php'; }
|
||||
else if (action === 'delete') { endpoint = 'actions/delete.php'; }
|
||||
else return;
|
||||
const checked = document.querySelectorAll('input[name="selected_theses[]"]:checked');
|
||||
document.getElementById('bulk-action-input').value = action;
|
||||
document.getElementById('bulk-form').action = endpoint;
|
||||
const container = document.getElementById('bulk-checkboxes');
|
||||
container.innerHTML = '';
|
||||
checked.forEach(cb => {
|
||||
const inp = document.createElement('input');
|
||||
inp.type = 'hidden'; inp.name = 'selected_theses[]'; inp.value = cb.value;
|
||||
container.appendChild(inp);
|
||||
});
|
||||
document.getElementById('bulk-form').submit();
|
||||
}
|
||||
|
||||
// Pending single-delete state
|
||||
let _pendingDeleteId = null;
|
||||
|
||||
function deleteThesis(id, title) {
|
||||
_pendingDeleteId = id;
|
||||
document.getElementById('delete-thesis-title').textContent = title;
|
||||
document.getElementById('delete-thesis-dialog').showModal();
|
||||
}
|
||||
|
||||
function _executeDeleteThesis() {
|
||||
const form = document.getElementById('delete-form-' + _pendingDeleteId);
|
||||
if (form) form.submit();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('input[name="selected_theses[]"]').forEach(cb => cb.addEventListener('change', updateBulk));
|
||||
});
|
||||
function toggleAll(src){document.querySelectorAll('input[name="selected_theses[]"]').forEach(cb=>cb.checked=src.checked);updateBulk();}
|
||||
function updateBulk(){const n=document.querySelectorAll('input[name="selected_theses[]"]:checked').length;const b=document.getElementById('bulk-actions');document.getElementById('selected-count').textContent=n;b.style.display=n>0?'flex':'none';}
|
||||
function confirmBulk(act){const n=document.querySelectorAll('input[name="selected_theses[]"]:checked').length;if(!n){document.getElementById('no-selection-dialog').showModal();return;}document.getElementById('bulk-action-input').value=act;if(act==='delete'){document.getElementById('bulk-delete-count').textContent=n;document.getElementById('bulk-delete-dialog').showModal();}else{document.getElementById('bulk-confirm-word').textContent=act==='publish'?'Publier':'Dépublier';document.getElementById('bulk-confirm-count').textContent=n;document.getElementById('bulk-confirm-dialog').showModal();}}
|
||||
function execBulk(){const a=document.getElementById('bulk-action-input').value;const f=document.getElementById('bulk-form');f.action = a==='delete' ? 'actions/delete.php' : 'actions/publish.php';f.submit();}
|
||||
function confirmDelete(id,title){document.getElementById('delete-thesis-title').textContent=title;document.getElementById('delete-thesis-dialog').showModal();document.getElementById('delete-dialog-confirm').onclick=function(){document.getElementById('delete-form-'+id).submit();};}
|
||||
document.addEventListener('DOMContentLoaded',()=>{document.querySelectorAll('input[name="selected_theses[]"]').forEach(cb=>cb.addEventListener('change',updateBulk));});
|
||||
document.addEventListener('htmx:afterSwap',()=>{document.querySelectorAll('input[name="selected_theses[]"]').forEach(cb=>cb.addEventListener('change',updateBulk));updateBulk();});
|
||||
</script>
|
||||
|
||||
<main id="main-content">
|
||||
<main id="main-content" class="admin-main--list">
|
||||
<!-- Title + filters + stats + import all in one toolbar row -->
|
||||
<div class="admin-list-toolbar">
|
||||
<h1>Liste des TFE</h1>
|
||||
|
||||
<dl class="admin-stats">
|
||||
<div class="admin-stat">
|
||||
<dt class="admin-stat__label">Total</dt>
|
||||
<dd class="admin-stat__number"><?= $stats['total'] ?></dd>
|
||||
<div class="admin-list-toolbar admin-list-toolbar--list">
|
||||
<div class="admin-toolbar-top">
|
||||
<div class="admin-toolbar-title-row">
|
||||
<h1>Liste des TFE</h1>
|
||||
<div class="admin-stats">
|
||||
<fieldset class="admin-stat">
|
||||
<legend class="admin-stat__label">Total</legend>
|
||||
<span class="admin-stat__number"><?= $stats['total'] ?></span>
|
||||
</fieldset>
|
||||
<fieldset class="admin-stat admin-stat--pub">
|
||||
<legend class="admin-stat__label">Publiés</legend>
|
||||
<span class="admin-stat__number"><?= $stats['published'] ?></span>
|
||||
</fieldset>
|
||||
<fieldset class="admin-stat admin-stat--pend">
|
||||
<legend class="admin-stat__label">Attente</legend>
|
||||
<span class="admin-stat__number"><?= $stats['pending'] ?></span>
|
||||
</fieldset>
|
||||
</div>
|
||||
</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">Attente</dt>
|
||||
<dd class="admin-stat__number"><?= $stats['pending'] ?></dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<form class="admin-filters" method="get" action="/admin/">
|
||||
<div class="admin-filters__search">
|
||||
<input type="text" name="search" placeholder="Titre, auteur..."
|
||||
value="<?= htmlspecialchars($searchQuery) ?>">
|
||||
<button type="submit" class="btn btn--primary admin-filters-btn">Filtrer</button>
|
||||
<?php if ($searchQuery || $yearFilter || $orientationFilter || $apFilter): ?>
|
||||
<button type="button" class="btn btn--secondary admin-filters-reset"
|
||||
onclick="window.location='/admin/'">✕ Réinitialiser</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="admin-filters__dropdowns">
|
||||
<select name="year">
|
||||
<option value="">Année</option>
|
||||
<?php foreach ($years as $y): ?>
|
||||
<option value="<?= $y ?>" <?= $yearFilter == $y ? 'selected' : '' ?>><?= $y ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<select name="orientation">
|
||||
<option value="">Orientation</option>
|
||||
<?php foreach ($orientations as $o): ?>
|
||||
<option value="<?= $o['id'] ?>" <?= $orientationFilter == $o['id'] ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($o['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<select name="ap">
|
||||
<option value="">AP</option>
|
||||
<?php foreach ($apPrograms as $ap): ?>
|
||||
<option value="<?= $ap['id'] ?>" <?= $apFilter == $ap['id'] ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($ap['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="admin-list-toolbar__right">
|
||||
<div class="admin-btn-group">
|
||||
<a href="/admin/add.php" class="btn btn--primary btn--sm">+ Ajouter un TFE</a>
|
||||
<a href="/admin/tags.php" class="btn btn--primary btn--sm">Mots-clés</a>
|
||||
</div>
|
||||
<div class="admin-btn-group">
|
||||
<button type="button" class="btn btn--primary btn--sm" id="import-dialog-btn"
|
||||
onclick="document.getElementById('import-dialog').showModal()">
|
||||
Importer un CSV
|
||||
Importer
|
||||
</button>
|
||||
<button type="button" class="btn btn--primary btn--sm" id="export-dialog-btn"
|
||||
onclick="document.getElementById('export-dialog').showModal()">
|
||||
Exporter
|
||||
</button>
|
||||
<a href="/admin/actions/export-csv.php" class="btn btn--primary btn--sm">
|
||||
Exporter CSV
|
||||
</a>
|
||||
<a href="/admin/actions/export-files.php" class="btn btn--primary btn--sm">
|
||||
Exporter fichiers
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="admin-filters" method="get" action="/admin/">
|
||||
<input type="text" name="search" placeholder="Titre, auteur..."
|
||||
value="<?= htmlspecialchars($searchQuery) ?>">
|
||||
<select name="year">
|
||||
<option value="">Année</option>
|
||||
<?php foreach ($years as $y): ?>
|
||||
<option value="<?= $y ?>" <?= $yearFilter == $y ? 'selected' : '' ?>><?= $y ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<select name="orientation">
|
||||
<option value="">Orientation</option>
|
||||
<?php foreach ($orientations as $o): ?>
|
||||
<option value="<?= $o['id'] ?>" <?= $orientationFilter == $o['id'] ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($o['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<select name="ap">
|
||||
<option value="">AP</option>
|
||||
<?php foreach ($apPrograms as $ap): ?>
|
||||
<option value="<?= $ap['id'] ?>" <?= $apFilter == $ap['id'] ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($ap['code'] ?? $ap['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="submit" class="btn btn--primary btn--sm admin-filters-btn">Filtrer</button>
|
||||
<?php if ($searchQuery || $yearFilter || $orientationFilter || $apFilter): ?>
|
||||
<button type="button" class="btn btn--secondary btn--sm admin-filters-reset"
|
||||
onclick="window.location='/admin/'">✕ Réinitialiser</button>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Bulk actions bar -->
|
||||
<div id="bulk-actions" class="admin-bulk-actions" role="toolbar" aria-label="Actions groupées">
|
||||
<strong><span id="selected-count">0</span> TFE(s) sélectionné(s)</strong>
|
||||
<div class="admin-bulk-btns">
|
||||
<button type="button" class="btn btn--sm btn--green admin-btn-publish" onclick="bulkAction('publish')">Publier</button>
|
||||
<button type="button" class="btn btn--sm btn--muted admin-btn-unpublish" onclick="bulkAction('unpublish')">Dépublier</button>
|
||||
<button type="button" class="btn btn--sm btn--red admin-btn-delete" onclick="bulkAction('delete')">Supprimer</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="bulk-form" method="post" action="actions/publish.php">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" id="bulk-action-input" name="action" value="">
|
||||
<input type="hidden" name="bulk" value="1">
|
||||
<div id="bulk-checkboxes"></div>
|
||||
</form>
|
||||
|
||||
<!-- Table -->
|
||||
<?php if (empty($theses)): ?>
|
||||
<p class="admin-empty">Aucun TFE trouvé.</p>
|
||||
<?php else: ?>
|
||||
<p class="admin-list-meta">
|
||||
<?php
|
||||
$from = $offset + 1;
|
||||
$to = min($offset + $perPage, $totalCount);
|
||||
if ($totalPages > 1) {
|
||||
echo "{$from}-{$to} sur {$totalCount} TFE";
|
||||
} else {
|
||||
echo "$totalCount TFE";
|
||||
}
|
||||
?>
|
||||
</p>
|
||||
<?php
|
||||
$sortParams = array_filter([
|
||||
'search' => $searchQuery,
|
||||
'year' => $yearFilter ?: '',
|
||||
'orientation' => $orientationFilter ?: '',
|
||||
'ap' => $apFilter ?: '',
|
||||
]);
|
||||
|
||||
$sortLink = function(string $col) use ($sortCol, $sortDir, $sortParams): string {
|
||||
$params = $sortParams;
|
||||
$params['sort'] = $col;
|
||||
$params['dir'] = ($sortCol === $col && $sortDir === 'desc') ? 'asc' : 'desc';
|
||||
return '/admin/?' . http_build_query($params);
|
||||
};
|
||||
|
||||
$sortArrow = function(string $col) use ($sortCol, $sortDir): string {
|
||||
if ($sortCol !== $col) return '';
|
||||
return $sortDir === 'asc' ? ' ↑' : ' ↓';
|
||||
};
|
||||
?>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"><input type="checkbox" onchange="toggleAll(this)"></th>
|
||||
<th scope="col"><a href="<?= $sortLink('identifier') ?>" class="admin-sort-link">ID<?= $sortArrow('identifier') ?></a></th>
|
||||
<th scope="col"><a href="<?= $sortLink('title') ?>" class="admin-sort-link">Titre<?= $sortArrow('title') ?></a></th>
|
||||
<th scope="col">Auteur(s)</th>
|
||||
<th scope="col"><a href="<?= $sortLink('year') ?>" class="admin-sort-link">Année<?= $sortArrow('year') ?></a></th>
|
||||
<th scope="col"><a href="<?= $sortLink('orientation') ?>" class="admin-sort-link">Orientation<?= $sortArrow('orientation') ?></a></th>
|
||||
<th scope="col"><a href="<?= $sortLink('ap_program') ?>" class="admin-sort-link">AP<?= $sortArrow('ap_program') ?></a></th>
|
||||
<th scope="col"><a href="<?= $sortLink('is_published') ?>" class="admin-sort-link">Statut<?= $sortArrow('is_published') ?></a></th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($theses as $thesis): ?>
|
||||
<tr>
|
||||
<td><input type="checkbox" name="selected_theses[]" value="<?= $thesis['id'] ?>"></td>
|
||||
<td class="admin-table-id"><?= htmlspecialchars($thesis['identifier'] ?? $thesis['id']) ?></td>
|
||||
<td>
|
||||
<div class="thesis-title"><?= htmlspecialchars($thesis['title']) ?></div>
|
||||
<?php if ($thesis['subtitle']): ?>
|
||||
<div class="thesis-subtitle"><?= htmlspecialchars($thesis['subtitle']) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($thesis['authors'] ?? 'N/A') ?></td>
|
||||
<td><?= $thesis['year'] ?></td>
|
||||
<td><?= htmlspecialchars($thesis['orientation'] ?? 'N/A') ?></td>
|
||||
<td><?= htmlspecialchars($thesis['ap_program'] ?? 'N/A') ?></td>
|
||||
<td>
|
||||
<?php $badgeType = 'publish'; $badgeValue = $thesis['is_published']; include APP_ROOT . '/templates/partials/status-badge.php'; ?>
|
||||
<?php if (!empty($thesis['access_type'])): ?>
|
||||
<br><?php $badgeType = 'access'; $badgeValue = $thesis['access_type']; include APP_ROOT . '/templates/partials/status-badge.php'; ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<div class="admin-actions">
|
||||
<a href="/admin/recapitulatif.php?id=<?= $thesis['id'] ?>" class="btn btn--sm btn--blue admin-btn-view">Voir</a>
|
||||
<a href="/admin/edit.php?id=<?= $thesis['id'] ?>" class="btn btn--sm btn--yellow admin-btn-edit">Éditer</a>
|
||||
<form method="post" action="actions/publish.php" class="publish-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="thesis_id" value="<?= $thesis['id'] ?>">
|
||||
<?php if ($thesis['is_published']): ?>
|
||||
<input type="hidden" name="action" value="unpublish">
|
||||
<button type="submit" class="btn btn--sm btn--muted admin-btn-unpublish">Dépublier</button>
|
||||
<?php else: ?>
|
||||
<input type="hidden" name="action" value="publish">
|
||||
<button type="submit" class="btn btn--sm btn--green admin-btn-publish">Publier</button>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
<form method="post" action="actions/delete.php" id="delete-form-<?= $thesis['id'] ?>" class="publish-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="thesis_id" value="<?= $thesis['id'] ?>">
|
||||
<button type="button" class="btn btn--sm btn--red admin-btn-delete"
|
||||
onclick="deleteThesis(<?= $thesis['id'] ?>, <?= htmlspecialchars(json_encode($thesis['title']), ENT_QUOTES) ?>)">Supprimer</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$baseParams = array_filter([
|
||||
'search' => $searchQuery,
|
||||
'year' => $yearFilter ?: '',
|
||||
'orientation' => $orientationFilter ?: '',
|
||||
'ap' => $apFilter ?: '',
|
||||
'sort' => $sortCol,
|
||||
'dir' => $sortDir,
|
||||
]);
|
||||
include APP_ROOT . '/templates/partials/pagination.php';
|
||||
?>
|
||||
<?php include APP_ROOT . '/templates/admin/index-table.php'; ?>
|
||||
</main>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════
|
||||
@@ -309,7 +109,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<p><span id="bulk-confirm-word"></span> <span id="bulk-confirm-count"></span> TFE(s) ?</p>
|
||||
</div>
|
||||
<div class="admin-dialog__footer">
|
||||
<button type="button" class="btn btn--primary" onclick="this.closest('dialog').close(); _executeBulkAction()">Confirmer</button>
|
||||
<button type="button" class="btn btn--primary" onclick="this.closest('dialog').close(); execBulk()">Confirmer</button>
|
||||
<button type="button" class="btn btn--secondary" onclick="this.closest('dialog').close()">Annuler</button>
|
||||
</div>
|
||||
</dialog>
|
||||
@@ -325,7 +125,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<p>Supprimer définitivement <strong><span id="bulk-delete-count"></span> TFE(s)</strong> ? Cette action est irréversible.</p>
|
||||
</div>
|
||||
<div class="admin-dialog__footer">
|
||||
<button type="button" class="btn btn--danger" onclick="this.closest('dialog').close(); _executeBulkAction()">Supprimer</button>
|
||||
<button type="button" class="btn btn--danger" onclick="this.closest('dialog').close(); execBulk()">Supprimer</button>
|
||||
<button type="button" class="btn btn--secondary" onclick="this.closest('dialog').close()">Annuler</button>
|
||||
</div>
|
||||
</dialog>
|
||||
@@ -341,7 +141,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<p>Supprimer « <strong id="delete-thesis-title"></strong> » ? Cette action est irréversible.</p>
|
||||
</div>
|
||||
<div class="admin-dialog__footer">
|
||||
<button type="button" class="btn btn--danger" onclick="this.closest('dialog').close(); _executeDeleteThesis()">Supprimer</button>
|
||||
<button type="button" class="btn btn--danger" id="delete-dialog-confirm" onclick="this.closest('dialog').close()">Supprimer</button>
|
||||
<button type="button" class="btn btn--secondary" onclick="this.closest('dialog').close()">Annuler</button>
|
||||
</div>
|
||||
</dialog>
|
||||
@@ -407,6 +207,43 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<?php endif; ?>
|
||||
</dialog>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════
|
||||
EXPORT DIALOG
|
||||
══════════════════════════════════════════════════════════════ -->
|
||||
<dialog id="export-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="export-dialog-title">
|
||||
<div class="admin-dialog__header">
|
||||
<h2 id="export-dialog-title">Exporter</h2>
|
||||
<button type="button" class="admin-dialog__close" aria-label="Fermer"
|
||||
onclick="document.getElementById('export-dialog').close()">✕</button>
|
||||
</div>
|
||||
|
||||
<form method="get" action="/admin/actions/export.php" class="admin-form">
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-checkbox-label">
|
||||
<input type="checkbox" name="csv" value="1" checked>
|
||||
CSV (tableau des métadonnées)
|
||||
</label>
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-checkbox-label">
|
||||
<input type="checkbox" name="files" value="1" checked>
|
||||
Fichiers (archive ZIP)
|
||||
</label>
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-checkbox-label">
|
||||
<input type="checkbox" name="db" value="1">
|
||||
Base de données (SQLite)
|
||||
</label>
|
||||
</div>
|
||||
<div class="admin-form-footer">
|
||||
<button type="submit" class="btn btn--primary">Exporter</button>
|
||||
<button type="button" class="btn btn--secondary"
|
||||
onclick="document.getElementById('export-dialog').close()">Annuler</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<?php if ($importMessage || !empty($importErrors)): ?>
|
||||
<script>document.getElementById('import-dialog').showModal();</script>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -32,17 +32,6 @@
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Export database -->
|
||||
<fieldset class="param-export-zone">
|
||||
<legend>Exporter la base de données</legend>
|
||||
<p>Télécharger une copie complète de la base de données SQLite.
|
||||
Cela inclut tous les TFE, auteurs, jury, mots-clés, paramètres, etc.</p>
|
||||
<button type="button" class="btn btn--primary"
|
||||
onclick="document.getElementById('export-db-dialog').showModal()">
|
||||
Exporter la base de données
|
||||
</button>
|
||||
</fieldset>
|
||||
|
||||
<!-- Danger zone: delete all TFE → now inside maintenance -->
|
||||
<fieldset class="param-danger-zone">
|
||||
<legend>Supprimer tous les TFE</legend>
|
||||
@@ -591,26 +580,6 @@
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════════
|
||||
EXPORT DATABASE DIALOG
|
||||
═══════════════════════════════════════════════════════════════ -->
|
||||
<dialog id="export-db-dialog" class="admin-dialog" aria-labelledby="export-db-dialog-title">
|
||||
<div class="admin-dialog__header">
|
||||
<h2 id="export-db-dialog-title">Exporter la base de données</h2>
|
||||
<button type="button" class="admin-dialog__close" aria-label="Fermer"
|
||||
onclick="document.getElementById('export-db-dialog').close()">✕</button>
|
||||
</div>
|
||||
|
||||
<p>Télécharger une copie complète de la base de données SQLite.
|
||||
Cela inclut tous les TFE, auteurs, jury, mots-clés, paramètres, etc.</p>
|
||||
|
||||
<div class="admin-form-footer">
|
||||
<a href="/admin/actions/export-db.php" class="btn btn--primary">Exporter la base de données</a>
|
||||
<button type="button" class="btn btn--secondary"
|
||||
onclick="document.getElementById('export-db-dialog').close()">Annuler</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
<?php else: ?>
|
||||
<!-- ═══════════════════ ADMIN MODE: Recap page ═══════════════════ -->
|
||||
<h1>Récapitulatif TFE</h1>
|
||||
<h1><a href="/admin/" class="admin-back-btn" title="Retour à la liste"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm48-88a8,8,0,0,1-8,8H107.31l18.35,18.34a8,8,0,0,1-11.32,11.32l-32-32a8,8,0,0,1,0-11.32l32-32a8,8,0,0,1,11.32,11.32L107.31,120H168A8,8,0,0,1,176,128Z"></path></svg></a> Récapitulatif TFE</h1>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<p class="toast" role="alert" data-type="error">⚠ <?= htmlspecialchars($error) ?></p>
|
||||
|
||||
@@ -23,9 +23,16 @@ function _submitPendingTagForm() {
|
||||
}
|
||||
</script>
|
||||
|
||||
<main id="main-content">
|
||||
<h1>Mots-clés (<?= count($tags) ?>)</h1>
|
||||
<main id="main-content" class="admin-main--list">
|
||||
<div class="admin-list-toolbar admin-list-toolbar--list" style="margin-bottom:var(--space-s)">
|
||||
<div class="admin-toolbar-top">
|
||||
<div class="admin-toolbar-title-row">
|
||||
<h1><a href="/admin/" class="admin-back-btn" title="Retour à la liste"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm48-88a8,8,0,0,1-8,8H107.31l18.35,18.34a8,8,0,0,1-11.32,11.32l-32-32a8,8,0,0,1,0-11.32l32-32a8,8,0,0,1,11.32,11.32L107.31,120H168A8,8,0,0,1,176,128Z"></path></svg></a> Mots-clés (<?= count($tags) ?>)</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="admin-table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -47,7 +54,9 @@ function _submitPendingTagForm() {
|
||||
<input type="hidden" name="tag_id" value="<?= (int)$tag['id'] ?>">
|
||||
<input class="admin-input--inline" type="text" name="new_name"
|
||||
value="<?= htmlspecialchars($tag['name']) ?>" required>
|
||||
<button type="submit" class="btn btn--primary btn--sm">Renommer</button>
|
||||
<button type="submit" class="admin-icon-btn admin-icon-btn--edit" title="Renommer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M248,92.68a15.86,15.86,0,0,0-4.69-11.31L174.63,12.68a16,16,0,0,0-22.63,0L123.57,41.11l-58,21.77A16.06,16.06,0,0,0,55.35,75.23L32.11,214.68A8,8,0,0,0,40,224a8.4,8.4,0,0,0,1.32-.11l139.44-23.24a16,16,0,0,0,12.35-10.17l21.77-58L243.31,104A15.87,15.87,0,0,0,248,92.68Zm-69.87,92.19L63.32,204l47.37-47.37a28,28,0,1,0-11.32-11.32L52,192.7,71.13,77.86,126,57.29,198.7,130ZM112,132a12,12,0,1,1,12,12A12,12,0,0,1,112,132Zm96-15.32L139.31,48l24-24L232,92.68Z"></path></svg>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Merge into another tag -->
|
||||
@@ -63,9 +72,9 @@ function _submitPendingTagForm() {
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="button" class="btn btn--sm btn--warning"
|
||||
<button type="button" class="admin-icon-btn admin-icon-btn--merge" title="Fusionner"
|
||||
onclick="return confirmMergeTag(this)">
|
||||
Fusionner
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32ZM160,208H48V96H160Zm48-48H176V88a8,8,0,0,0-8-8H96V48H208Z"></path></svg>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -74,9 +83,9 @@ function _submitPendingTagForm() {
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="tag_id" value="<?= (int)$tag['id'] ?>">
|
||||
<button type="button" class="btn btn--sm btn--danger"
|
||||
<button type="button" class="admin-icon-btn admin-icon-btn--delete" title="Supprimer"
|
||||
onclick="confirmDeleteTag(this, <?= htmlspecialchars(json_encode($tag['name']), ENT_QUOTES) ?>)">
|
||||
Supprimer
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
@@ -84,6 +93,7 @@ function _submitPendingTagForm() {
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Merge tag confirm -->
|
||||
|
||||
@@ -113,6 +113,14 @@ function renderEntries(array $entries): string
|
||||
<a class="apropos-entry" target="_blank" href='https://typotheque.genderfluid.space/fr/fontes/bbb-dm-sans'><b>BBB DM Sans</b> - Camille Circlude, Eugénie Bidaut, Mariel Nils, Bérénice Bouin</a>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="apropos-credit-row">
|
||||
<dt>Iconographie</dt>
|
||||
<dd>
|
||||
<a class="apropos-entry" target="_blank" href="https://phosphoricons.com/">Phosphor Icons</a> —
|
||||
<a class="apropos-entry" target="_blank" href="https://mit-license.org/">MIT</a>,
|
||||
par Helena Zhang et Tobias Fried
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user