mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 19:19:19 +02:00
Add delete/batch-delete and sortable columns to admin list
- Database: add deleteThesis() and bulkDeleteTheses() methods with file cleanup - Database: add SORT_MAP + buildOrderBy() for safe column sorting - Database: getThesesList() now respects sort/dir filter params - New action: actions/delete.php (single + batch delete with CSRF) - Admin index: delete button per row with confirmation dialog - Admin index: batch 'Supprimer' button in bulk actions bar - Admin index: sortable column headers (ID, Titre, Année, Orientation, AP, Statut) - Admin index: sort state preserved in pagination links - CSS: admin-btn-delete (red muted), admin-sort-link styles
This commit is contained in:
55
public/admin/actions/delete.php
Normal file
55
public/admin/actions/delete.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../config/bootstrap.php';
|
||||
require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
require_once __DIR__ . '/../../../src/Database.php';
|
||||
|
||||
// CSRF validation
|
||||
if (!isset($_POST['csrf_token'], $_SESSION['csrf_token'])
|
||||
|| !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
|
||||
App::flash('error', 'Erreur de sécurité : token invalide.');
|
||||
header('Location: ../index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$isBulk = !empty($_POST['bulk']);
|
||||
|
||||
try {
|
||||
$db = new Database();
|
||||
|
||||
if ($isBulk) {
|
||||
$ids = array_filter(array_map('intval', $_POST['selected_theses'] ?? []), fn($id) => $id > 0);
|
||||
|
||||
if (empty($ids)) {
|
||||
App::flash('error', 'Aucun TFE sélectionné.');
|
||||
header('Location: ../index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$db->bulkDeleteTheses($ids);
|
||||
$count = count($ids);
|
||||
App::flash('success', "$count TFE(s) supprimé(s) avec succès.");
|
||||
|
||||
} else {
|
||||
$thesisId = filter_var($_POST['thesis_id'] ?? '', FILTER_VALIDATE_INT);
|
||||
|
||||
if (!$thesisId || $thesisId <= 0) {
|
||||
App::flash('error', 'ID invalide.');
|
||||
header('Location: ../index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$db->deleteThesis($thesisId);
|
||||
App::flash('success', 'TFE supprimé avec succès.');
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log('delete.php error: ' . $e->getMessage());
|
||||
App::flash('error', 'Erreur lors de la suppression : ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
header('Location: ../index.php');
|
||||
exit;
|
||||
@@ -222,11 +222,16 @@ try {
|
||||
$orientationFilter = isset($_GET['orientation']) ? intval($_GET['orientation']) : null;
|
||||
$apFilter = isset($_GET['ap']) ? intval($_GET['ap']) : null;
|
||||
|
||||
$sortCol = isset($_GET['sort']) ? trim($_GET['sort']) : 'submitted_at';
|
||||
$sortDir = isset($_GET['dir']) ? trim($_GET['dir']) : 'desc';
|
||||
|
||||
$filters = [];
|
||||
if ($searchQuery) $filters['search'] = $searchQuery;
|
||||
if ($yearFilter) $filters['year'] = $yearFilter;
|
||||
if ($orientationFilter) $filters['orientation'] = $orientationFilter;
|
||||
if ($apFilter) $filters['ap'] = $apFilter;
|
||||
$filters['sort'] = $sortCol;
|
||||
$filters['dir'] = $sortDir;
|
||||
|
||||
$perPage = 25;
|
||||
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
|
||||
@@ -262,9 +267,18 @@ function updateBulk() {
|
||||
function bulkAction(action) {
|
||||
const checked = document.querySelectorAll('input[name="selected_theses[]"]:checked');
|
||||
if (!checked.length) { alert('Sélectionnez au moins un TFE.'); return; }
|
||||
const word = action === 'publish' ? 'publier' : 'dépublier';
|
||||
if (!confirm(`${word.charAt(0).toUpperCase()+word.slice(1)} ${checked.length} TFE(s) ?`)) return;
|
||||
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') {
|
||||
if (!confirm(`Supprimer définitivement ${checked.length} TFE(s) ? Cette action est irréversible.`)) return;
|
||||
} else {
|
||||
if (!confirm(`${word.charAt(0).toUpperCase()+word.slice(1)} ${checked.length} TFE(s) ?`)) return;
|
||||
}
|
||||
document.getElementById('bulk-action-input').value = action;
|
||||
document.getElementById('bulk-form').action = endpoint;
|
||||
const container = document.getElementById('bulk-checkboxes');
|
||||
container.innerHTML = '';
|
||||
checked.forEach(cb => {
|
||||
@@ -274,6 +288,11 @@ function bulkAction(action) {
|
||||
});
|
||||
document.getElementById('bulk-form').submit();
|
||||
}
|
||||
function deleteThesis(id, title) {
|
||||
if (!confirm(`Supprimer « ${title} » ?\nCette action est irréversible.`)) return;
|
||||
const form = document.getElementById('delete-form-' + id);
|
||||
if (form) form.submit();
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('input[name="selected_theses[]"]').forEach(cb => cb.addEventListener('change', updateBulk));
|
||||
});
|
||||
@@ -346,6 +365,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<div class="admin-bulk-btns">
|
||||
<button type="button" class="admin-btn-sm admin-btn-publish" onclick="bulkAction('publish')">Publier</button>
|
||||
<button type="button" class="admin-btn-sm admin-btn-unpublish" onclick="bulkAction('unpublish')">Dépublier</button>
|
||||
<button type="button" class="admin-btn-sm admin-btn-delete" onclick="bulkAction('delete')">Supprimer</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -371,17 +391,37 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
?>
|
||||
</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">ID</th>
|
||||
<th scope="col">Titre</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">Année</th>
|
||||
<th scope="col">Orientation</th>
|
||||
<th scope="col">AP</th>
|
||||
<th scope="col">Statut</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>
|
||||
@@ -422,6 +462,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<button type="submit" class="admin-btn-sm 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="admin-btn-sm admin-btn-delete"
|
||||
onclick="deleteThesis(<?= $thesis['id'] ?>, <?= htmlspecialchars(json_encode($thesis['title']), ENT_QUOTES) ?>)">Supprimer</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -436,6 +482,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
'year' => $yearFilter ?: '',
|
||||
'orientation' => $orientationFilter ?: '',
|
||||
'ap' => $apFilter ?: '',
|
||||
'sort' => $sortCol,
|
||||
'dir' => $sortDir,
|
||||
]);
|
||||
include APP_ROOT . '/templates/partials/pagination.php';
|
||||
?>
|
||||
|
||||
@@ -498,6 +498,20 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Sortable column headers */
|
||||
.admin-sort-link {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.admin-sort-link:hover {
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.admin-body table td {
|
||||
padding: var(--space-2xs) var(--space-xs);
|
||||
border-bottom: 1px solid var(--border-primary);
|
||||
@@ -621,6 +635,15 @@
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.admin-btn-delete {
|
||||
background: var(--error-muted-bg);
|
||||
color: var(--error);
|
||||
border-color: var(--error-muted-border, var(--border-primary));
|
||||
}
|
||||
.admin-btn-delete:hover {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
.publish-form {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
|
||||
Reference in New Issue
Block a user