mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 19:19:19 +02:00
admin/index.php: add server-side pagination (25/page)
- Add Database::getThesesListCount(array $filters) — runs the same WHERE clauses as getThesesList() but with COUNT(DISTINCT t.id); used to compute total pages without loading all rows. - Extend Database::getThesesList() with $limit/$offset parameters; when $limit > 0 appends LIMIT/OFFSET and re-binds positional params individually to avoid the PDO mixed-style restriction. - Fix getThesesList() SELECT: add LEFT JOIN access_types + at.name as access_type — the column was referenced in the template but never fetched. - Wire admin/index.php: read ?page=, compute $totalPages/$offset, pass $perPage=25 + $offset to getThesesList(); include pagination.php partial below the table with filter-preserving $baseParams. - Add result-count line (<p class="admin-list-meta">) showing "X–Y sur Z TFE" when multiple pages exist. - Add .admin-body .pagination-wrap / .pagination-btn / .pagination-info styles to admin.css (scoped to .admin-body to avoid colliding with public pages).
This commit is contained in:
2
TODO.md
2
TODO.md
@@ -11,6 +11,8 @@ Pending tasks have been split into topic files under [`todo/`](todo/README.md):
|
||||
|
||||
## Recently completed (this session)
|
||||
|
||||
- [x] `admin/index.php` — server-side pagination (25/page); `Database::getThesesListCount()` added; `getThesesList()` extended with `$limit`/`$offset`; `access_type` JOIN added to query (was missing); result-count meta line added; `.pagination-wrap` + `.pagination-btn` + `.pagination-info` styles added to `admin.css`
|
||||
|
||||
- [x] `checkbox-list.php` — replaced `<div class="admin-checkbox-list">` with `<fieldset class="admin-checkbox-group"><legend class="sr-only">…</legend><ul>` (WCAG 1.3.1 fix)
|
||||
- [x] `admin.css` — replaced `.admin-checkbox-list` with `.admin-body fieldset.admin-checkbox-group > ul` semantic selectors; added `span.admin-row-label` as visible label column counterpart
|
||||
- [x] `login.php` — wrapped content in `<main id="main-content">` landmark
|
||||
|
||||
@@ -21,7 +21,14 @@ try {
|
||||
if ($yearFilter) $filters['year'] = $yearFilter;
|
||||
if ($orientationFilter) $filters['orientation'] = $orientationFilter;
|
||||
|
||||
$theses = $db->getThesesList($filters);
|
||||
$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);
|
||||
$stats = $db->getThesesStats();
|
||||
$years = $db->getAllYears();
|
||||
$orientations = $db->getAllOrientations();
|
||||
@@ -74,14 +81,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<aside role="status" class="admin-maintenance-bar <?= $maintenanceOn ? 'admin-maintenance-bar--active' : '' ?>" aria-label="Statut du site">
|
||||
<?php if ($maintenanceOn): ?>
|
||||
<span>⚠ Mode maintenance <strong>activé</strong> — le site public est inaccessible.</span>
|
||||
<form method="post" action="actions/maintenance.php" style="display:inline;">
|
||||
<form method="post" action="actions/maintenance.php">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="action" value="disable_maintenance">
|
||||
<button type="submit" class="admin-btn admin-btn--sm">Désactiver la maintenance</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<span>Site public : <strong>en ligne</strong></span>
|
||||
<form method="post" action="actions/maintenance.php" style="display:inline;">
|
||||
<form method="post" action="actions/maintenance.php">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="action" value="enable_maintenance">
|
||||
<button type="submit" class="admin-btn admin-btn--sm admin-btn--warning"
|
||||
@@ -150,8 +157,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
<!-- Table -->
|
||||
<?php if (empty($theses)): ?>
|
||||
<p style="color:var(--text-secondary);padding:1rem 0;">Aucun TFE trouvé.</p>
|
||||
<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>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -170,7 +188,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<?php foreach ($theses as $thesis): ?>
|
||||
<tr>
|
||||
<td><input type="checkbox" name="selected_theses[]" value="<?= $thesis['id'] ?>"></td>
|
||||
<td style="color:var(--text-secondary);font-size:.8rem;"><?= htmlspecialchars($thesis['identifier'] ?? $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']): ?>
|
||||
@@ -210,6 +228,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$baseParams = array_filter([
|
||||
'search' => $searchQuery,
|
||||
'year' => $yearFilter ?: '',
|
||||
'orientation' => $orientationFilter ?: '',
|
||||
]);
|
||||
include APP_ROOT . '/templates/partials/pagination.php';
|
||||
?>
|
||||
</main>
|
||||
|
||||
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>
|
||||
|
||||
@@ -343,6 +343,29 @@
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.admin-maintenance-bar form {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* Result-count line above the thesis table */
|
||||
.admin-list-meta {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Empty-state message below the thesis table */
|
||||
.admin-empty {
|
||||
color: var(--text-secondary);
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
/* Identifier column in the thesis table */
|
||||
.admin-table-id {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* ── Filters bar ────────────────────────────────────────────────────────── */
|
||||
.admin-filters {
|
||||
display: flex;
|
||||
@@ -808,3 +831,52 @@
|
||||
color: var(--text-secondary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ── Pagination (admin list) ─────────────────────────────────────────────── */
|
||||
.admin-body .pagination-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1.5rem 0 0.5rem;
|
||||
}
|
||||
|
||||
.admin-body .pagination-wrap ul {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.admin-body .pagination-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 2.75rem;
|
||||
min-height: 2.75rem;
|
||||
padding: 0 0.6rem;
|
||||
border: 1px solid var(--border-secondary);
|
||||
border-radius: 3px;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.9rem;
|
||||
text-decoration: none;
|
||||
transition: border-color 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.admin-body .pagination-btn:hover:not(.disabled) {
|
||||
border-color: var(--admin-purple);
|
||||
color: var(--admin-purple);
|
||||
}
|
||||
|
||||
.admin-body .pagination-btn.disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.admin-body .pagination-info {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
@@ -524,14 +524,12 @@ class Database {
|
||||
* @param array $filters
|
||||
* @return array
|
||||
*/
|
||||
public function getThesesList(array $filters = []): array {
|
||||
$sql = "SELECT
|
||||
t.id, t.identifier, t.title, t.subtitle, t.year,
|
||||
o.name as orientation,
|
||||
ap.name as ap_program,
|
||||
GROUP_CONCAT(DISTINCT a.name) as authors,
|
||||
t.submitted_at,
|
||||
t.is_published
|
||||
/**
|
||||
* Count theses matching the given admin filters (no LIMIT).
|
||||
* Used alongside getThesesList() to calculate total pages.
|
||||
*/
|
||||
public function getThesesListCount(array $filters = []): int {
|
||||
$sql = "SELECT COUNT(DISTINCT t.id)
|
||||
FROM theses t
|
||||
LEFT JOIN orientations o ON t.orientation_id = o.id
|
||||
LEFT JOIN ap_programs ap ON t.ap_program_id = ap.id
|
||||
@@ -559,10 +557,69 @@ class Database {
|
||||
$params[] = intval($filters['orientation']);
|
||||
}
|
||||
|
||||
$sql .= " GROUP BY t.id ORDER BY t.year DESC, t.submitted_at DESC";
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
return (int) $stmt->fetchColumn();
|
||||
}
|
||||
|
||||
public function getThesesList(array $filters = [], int $limit = 0, int $offset = 0): array {
|
||||
$sql = "SELECT
|
||||
t.id, t.identifier, t.title, t.subtitle, t.year,
|
||||
o.name as orientation,
|
||||
ap.name as ap_program,
|
||||
GROUP_CONCAT(DISTINCT a.name) as authors,
|
||||
t.submitted_at,
|
||||
t.is_published,
|
||||
at.name as access_type
|
||||
FROM theses t
|
||||
LEFT JOIN orientations o ON t.orientation_id = o.id
|
||||
LEFT JOIN ap_programs ap ON t.ap_program_id = ap.id
|
||||
LEFT JOIN thesis_authors ta ON t.id = ta.thesis_id
|
||||
LEFT JOIN authors a ON ta.author_id = a.id
|
||||
LEFT JOIN access_types at ON t.access_type_id = at.id
|
||||
WHERE 1=1";
|
||||
|
||||
$params = [];
|
||||
|
||||
if (!empty($filters['search'])) {
|
||||
$sql .= " AND (t.title LIKE ? OR t.subtitle LIKE ? OR a.name LIKE ?)";
|
||||
$searchParam = '%' . $filters['search'] . '%';
|
||||
$params[] = $searchParam;
|
||||
$params[] = $searchParam;
|
||||
$params[] = $searchParam;
|
||||
}
|
||||
|
||||
if (!empty($filters['year'])) {
|
||||
$sql .= " AND t.year = ?";
|
||||
$params[] = intval($filters['year']);
|
||||
}
|
||||
|
||||
if (!empty($filters['orientation'])) {
|
||||
$sql .= " AND t.orientation_id = ?";
|
||||
$params[] = intval($filters['orientation']);
|
||||
}
|
||||
|
||||
$sql .= " GROUP BY t.id ORDER BY t.year DESC, t.submitted_at DESC";
|
||||
|
||||
if ($limit > 0) {
|
||||
$sql .= " LIMIT :limit OFFSET :offset";
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
|
||||
// Bind named params only when LIMIT is used (mix of ? and : is not allowed).
|
||||
if ($limit > 0) {
|
||||
// Re-bind all positional params by index.
|
||||
foreach ($params as $i => $val) {
|
||||
$stmt->bindValue($i + 1, $val);
|
||||
}
|
||||
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
} else {
|
||||
$stmt->execute($params);
|
||||
}
|
||||
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
|
||||
- [ ] **`admin.css`**: Replace `.admin-main` with `.admin-body main`
|
||||
- [ ] **`admin.css`**: Replace `.admin-page-title` with `.admin-body main > h1`
|
||||
- [ ] **`admin.css`**: Replace `.admin-alert` / `.admin-alert--error` / `.admin-alert--success` with `[role="alert"]` / `data-type="error|success"` attribute
|
||||
- [x] **`admin.css`**: Replace `.admin-alert` / `.admin-alert--error` / `.admin-alert--success` with `[role="alert"]` / `data-type="error|success"` attribute
|
||||
- [ ] **`admin.css`**: Replace `.admin-form-row` with `.admin-body form > div` or CSS grid on `<form>` children
|
||||
- [ ] **`admin.css`**: Replace `.admin-label` with `.admin-body form label`
|
||||
- [ ] **`admin.css`**: Replace `.admin-input` / `.admin-select` / `.admin-textarea` with native element selectors
|
||||
- [ ] **`admin.css`**: Replace `.admin-hint` with `.admin-body form small`
|
||||
- [x] **`admin.css`**: Replace `.admin-hint` with `.admin-body form small`
|
||||
- [ ] **`admin.css`**: Replace `.admin-table` with `.admin-body table`
|
||||
- [ ] **`admin.css`**: Replace `.admin-fieldset` / `.admin-fieldset-legend` with `.admin-body fieldset` / `.admin-body legend`
|
||||
- [ ] **`main.css`**: Replace `.card__caption` with `.home-body .cards-container li p` or `li > a > p`
|
||||
- [ ] **`main.css`**: Replace `.card__media` with `.home-body figure`
|
||||
- [x] **`main.css`**: Replace `.card__caption` with `.home-body .cards-container li p` or `li > a > p`
|
||||
- [x] **`main.css`**: Replace `.card__media` with `.home-body figure`
|
||||
- [ ] **`tfe.css`**: Replace `.tfe-meta-list` selectors with `article dl`, `article dt`, `article dd`
|
||||
- [ ] **`tfe.css`**: Replace `.tfe-media-block` with `aside figure`
|
||||
- [ ] **`tfe.css`**: Replace `.tfe-file-caption` with `aside figcaption`
|
||||
@@ -22,10 +22,10 @@
|
||||
## Template HTML changes to match
|
||||
|
||||
- [ ] In all admin templates, replace `<p class="admin-hint">` with `<small>` elements
|
||||
- [ ] In `tfe.php`, remove `class="tfe-meta-list"` — target via `article dl`
|
||||
- [ ] In `tfe.php`, remove `class="tfe-media-block"` — target via `aside figure`
|
||||
- [ ] In `tfe.php`, remove `class="tfe-file-caption"` — target via `aside figcaption`
|
||||
- [ ] In `index.php`, remove `class="card__caption"` — target via `li > a > p`
|
||||
- [x] In `tfe.php`, remove `class="tfe-meta-list"` — target via `article dl`
|
||||
- [x] In `tfe.php`, remove `class="tfe-media-block"` — target via `aside figure`
|
||||
- [x] In `tfe.php`, remove `class="tfe-file-caption"` — target via `aside figcaption`
|
||||
- [x] In `index.php`, remove `class="card__caption"` — target via `li > a > p`
|
||||
|
||||
## Scattered inline styles in templates
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
- [x] **`index.php`**: Replace `<div class="admin-maintenance-bar">` with `<aside role="status">` or `<p role="status">`
|
||||
- [x] **`index.php`**: Add `role="toolbar" aria-label="Actions groupées"` to `<div class="admin-bulk-actions">`
|
||||
- [x] **`index.php`**: Add `scope="col"` to all `<th>` cells in the admin table
|
||||
- [ ] **`index.php`**: Add non-colour indicator + `aria-label="Statut : …"` to status badge `<span>` elements
|
||||
- [x] **`index.php`**: Add non-colour indicator + `aria-label="Statut : …"` to status badge `<span>` elements (via `status-badge.php` partial)
|
||||
- [x] **`tags.php`**: Add `scope="col"` to `<th>` cells
|
||||
- [x] **`tags.php`**: Move inline `style="margin-top:.35rem;"` on forms → `.admin-inline-form + .admin-inline-form` selector
|
||||
- [x] **`thanks.php`**: Replace `<div class="admin-thesis-info">` with `<section>` + `<h2>` heading; CSS targets `main > section`
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
## Shared UI partials — `templates/partials/`
|
||||
|
||||
- [ ] **`pagination.php`** — pagination nav is duplicated between `index.php` and `search.php`; unify into one partial accepting `$page`, `$totalPages`, `$baseParams[]`
|
||||
- [x] **`pagination.php`** — partial created and used in both `search.php` and (now) `admin/index.php`; `admin/index.php` also gained proper server-side pagination (25/page) with filter-aware `$baseParams`
|
||||
- [ ] **`status-badge.php`** — published/pending badge + access badge pattern repeated in `index.php` admin table rows; extract into a partial
|
||||
- [ ] **`admin-alert.php`** — rename/merge `flash-messages.php` to also handle the 3 different legacy flash key patterns (`$_SESSION['error']`, `$_SESSION['admin_error']`, `$_SESSION['edit_error']`, etc.) that pages still consume manually instead of via `App::consumeFlash()`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user