feat(admin): cleanup page — remove 'Fichiers temporaires' level, promote sections to h2 TOC entries

This commit is contained in:
Pontoporeia
2026-09-18 16:26:49 +02:00
parent 1ed69a2c1a
commit 3f352b0d26
18 changed files with 650 additions and 252 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ the XAMXAM TFE database.
| `add.php` | Add new thesis form |
| `edit.php` | Existing thesis form |
| `recapitulatif.php` | Post-submission recap |
| `cleanup.php` | Orphaned-draft / storage cleanup |
| `cleanup.php` | Cleanup page (Corbeille — restore/delete trashed files) |
| `system.php` | System dashboard (logs, SMTP/PeerTube status) |
| `contenus.php` | Editable content (pages, contacts) |
| `contenus-edit.php` | Edit a content page |
@@ -13,34 +13,22 @@ AdminAuth::requireLogin();
require_once __DIR__ . '/_cleanup-stats-data.php';
$d = getCleanupStats();
$fpStale = $d['filepond_stale_count'] ?? 0;
$fpActive = $d['filepond_active_count'] ?? 0;
$trStale = $d['trash_stale_count'] ?? 0;
$trActive = $d['trash_active_count'] ?? 0;
// Abandoned FilePond uploads are purged automatically by a cron job, so the
// cleanup page only surfaces the manually-managed trash (Corbeille).
$hasTrash = (($d['trash_stale_count'] ?? 0) + ($d['trash_active_count'] ?? 0)) > 0;
$totalStale = $fpStale + $trStale;
$totalFiles = $fpStale + $fpActive + $trStale + $trActive;
$staleSize = ($d['filepond_stale_size'] ?? 0) + ($d['trash_stale_size'] ?? 0);
$staleHuman = '';
if ($staleSize >= 1073741824) { $staleHuman = number_format($staleSize / 1073741824, 1) . ' GB'; }
elseif ($staleSize >= 1048576) { $staleHuman = number_format($staleSize / 1048576, 1) . ' MB'; }
elseif ($staleSize >= 1024) { $staleHuman = number_format($staleSize / 1024, 0) . ' KB'; }
elseif ($staleSize > 0) { $staleHuman = $staleSize . ' B'; }
$fpMeta = $fpStale . ' dossier' . ($fpStale > 1 ? 's' : '');
if ($fpStale > 0) {
$fpMeta .= ' · ' . ($d['filepond_stale_human'] ?? '');
}
$trMeta = ($trStale + $trActive) . ' fichier' . (($trStale + $trActive) > 1 ? 's' : '');
$trTotalSize = ($d['trash_stale_size'] ?? 0) + ($d['trash_active_size'] ?? 0);
$trTotalHuman = humanBytes($trTotalSize);
if (($trStale + $trActive) > 0) {
$trMeta .= ' · ' . $trTotalHuman;
$trMeta = '';
if ($hasTrash) {
$trCount = ($d['trash_stale_count'] ?? 0) + ($d['trash_active_count'] ?? 0);
$trMeta = $trCount . ' fichier' . ($trCount > 1 ? 's' : '');
$trTotalSize = ($d['trash_stale_size'] ?? 0) + ($d['trash_active_size'] ?? 0);
if ($trTotalSize > 0) {
$trMeta .= ' · ' . humanBytes($trTotalSize);
}
}
?>
<?php if ($totalStale === 0 && $totalFiles === 0): ?>
<p style="margin:0;color:var(--accent-green)">✓ Aucun fichier temporaire.</p>
<?php if (!$hasTrash): ?>
<p style="margin:0;color:var(--accent-green)">✓ Corbeille vide — aucun fichier à nettoyer.</p>
<?php return; endif; ?>
<!-- Bulk actions bar -->
@@ -78,35 +66,6 @@ if (($trStale + $trActive) > 0) {
<div id="cleanup-bulk-restore-checkboxes"></div>
</form>
<?php if ($fpStale > 0): ?>
<h3 id="tmp-filepond-heading">Téléversements abandonnés <span class="n-meta"><?= htmlspecialchars($fpMeta) ?></span></h3>
<table class="n-table" aria-labelledby="tmp-filepond-heading">
<thead><tr><th width="1%"><input type="checkbox" onchange="cleanupToggleAll(this, 'filepond')" title="Tout sélectionner"></th><th>Nom</th><th>Taille</th><th>Âge</th><th width="1%"></th></tr></thead>
<tbody>
<?php foreach ($d['filepond_stale_files'] as $f): ?>
<tr>
<td><input type="checkbox" name="filepond_dirs[]" value="<?= htmlspecialchars($f['name']) ?>" data-cleanup-group="filepond" onchange="cleanupUpdateBulk()"></td>
<td><strong><?= htmlspecialchars($f['name']) ?></strong></td>
<td style="white-space:nowrap"><?= htmlspecialchars($f['human']) ?></td>
<td style="white-space:nowrap">~<?= (int)$f['age_minutes'] ?> min</td>
<td style="white-space:nowrap">
<button type="button" class="btn btn--sm btn--danger" style="font-size:0.85em;padding:2px var(--space-xs)"
hx-post="/admin/actions/cleanup-tmp.php"
hx-confirm="Supprimer définitivement ce téléversement abandonné ?"
hx-vals='{"csrf_token":"<?= htmlspecialchars($_SESSION['csrf_token']) ?>","filepond_dir":"<?= htmlspecialchars($f['name']) ?>"}'
hx-target="#tmp-cleanup-stats-wrapper"
hx-swap="innerHTML"
hx-indicator="#tmp-cleanup-stats-wrapper">
<?= icon('trash') ?>
Supprimer
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<?php
// ── Merge stale + active trash files into a single list for display ─────
// stale = orphaned (no sidecar, or too old); active = restorable (has sidecar)
@@ -126,7 +85,10 @@ foreach (($d['trash_stale_files'] ?? []) as $f) {
}
$hasTrash = !empty($allTrashFiles);
if ($hasTrash): ?>
<h3 id="tmp-trash-heading">Corbeille <span class="n-meta"><?= htmlspecialchars($trMeta) ?></span></h3>
<section aria-labelledby="tmp-trash-heading">
<h2 id="tmp-trash-heading">Corbeille</h2>
<p class="cleanup-section-desc">Fichiers retirés d'un TFE. Ceux marqués « Restaurable » peuvent être remis dans le TFE d'origine&nbsp;; les autres sont orphelins (plus aucun TFE lié). La suppression est définitive et sans retour.</p>
<p class="cleanup-stat-card"><?= htmlspecialchars($trMeta) ?></p>
<table class="n-table" aria-labelledby="tmp-trash-heading">
<thead><tr><th width="1%"><input type="checkbox" onchange="cleanupToggleAll(this, 'trash')" title="Tout sélectionner"></th><th>Nom</th><th>Taille</th><th>Âge</th><th>Statut</th><th width="1%"></th></tr></thead>
<tbody>
@@ -147,6 +109,7 @@ if ($hasTrash): ?>
<?php if ($f['_restorable']): ?>
<button type="button" class="btn btn--sm"
style="font-size:0.85em;padding:2px var(--space-xs);margin-right:4px"
title="Restaurer ce fichier vers le TFE associé"
hx-post="/admin/actions/restore-trash.php"
hx-confirm="Restaurer ce fichier vers le TFE associé ?"
hx-vals='{"csrf_token":"<?= htmlspecialchars($_SESSION['csrf_token']) ?>","trash_file":"<?= htmlspecialchars($f['name']) ?>"}'
@@ -158,6 +121,7 @@ if ($hasTrash): ?>
<?php endif; ?>
<button type="button" class="btn btn--sm btn--danger"
style="font-size:0.85em;padding:2px var(--space-xs)"
title="Supprimer définitivement ce fichier de la corbeille (irréversible)"
hx-post="/admin/actions/cleanup-tmp.php"
hx-confirm="Supprimer définitivement ce fichier de la corbeille ?"
hx-vals='{"csrf_token":"<?= htmlspecialchars($_SESSION['csrf_token']) ?>","trash_file":"<?= htmlspecialchars($f['name']) ?>"}'
@@ -172,10 +136,6 @@ if ($hasTrash): ?>
<?php endforeach; ?>
</tbody>
</table>
</section>
<?php endif; ?>
<?php if ($fpActive > 0): ?>
<p style="margin:var(--space-sm) 0 0 0;font-size:0.85em;color:var(--text-secondary)">Conservés :
<?= $fpActive . ' téléversement(s) actif(s) (' . htmlspecialchars($d['filepond_active_human']) . ')' ?>
</p>
<?php endif; ?>
@@ -22,8 +22,9 @@ if (!($d['configured'] ?? false)): ?>
<?php return; endif; ?>
<?php if (($d['orphan_count'] ?? 0) > 0): ?>
<h3 id="peertube-orphans-heading">Vidéos orphelines <span class="n-meta"><?= (int)$d['orphan_count'] ?> vidéos orphelines</span></h3>
<table class="n-table" aria-labelledby="peertube-orphans-heading">
<p class="cleanup-section-desc">Vidéos toujours présentes sur l'instance PeerTube mais plus rattachées à aucun TFE du site. Supprimer une vidéo est définitif et sans retour.</p>
<p class="cleanup-stat-card"><?= (int)$d['orphan_count'] ?> vidéo<?= ($d['orphan_count'] > 1 ? 's' : '') ?> orpheline<?= ($d['orphan_count'] > 1 ? 's' : '') ?></p>
<table class="n-table" aria-labelledby="cleanup-peertube-title">
<thead><tr><th>Nom</th><th>Date</th><th width="1%"></th></tr></thead>
<tbody>
<?php foreach ($d['orphans'] as $v): ?>
@@ -35,6 +36,7 @@ if (!($d['configured'] ?? false)): ?>
<td style="white-space:nowrap"><?= htmlspecialchars(substr($v['createdAt'] ?? '', 0, 10)) ?></td>
<td style="white-space:nowrap">
<button type="button" class="btn btn--sm btn--danger" style="font-size:0.85em;padding:2px var(--space-xs)"
title="Supprimer définitivement cette vidéo de PeerTube (irréversible)"
hx-post="/admin/actions/peertube-delete.php"
hx-confirm="Supprimer définitivement cette vidéo de PeerTube ?"
hx-vals='{"csrf_token":"<?= htmlspecialchars($_SESSION['csrf_token']) ?>","uuid":"<?= htmlspecialchars($v['uuid']) ?>"}'
+1 -1
View File
@@ -7,7 +7,7 @@ if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$pageTitle = 'Nettoyer les fichiers temporaires';
$pageTitle = 'Nettoyage';
$isAdmin = true;
$bodyClass = 'admin-body';
+165 -103
View File
@@ -1,5 +1,6 @@
<?php
require_once __DIR__ . "/../../bootstrap.php";
require_once __DIR__ . '/../../bootstrap.php';
require_once __DIR__ . '/../../src/AdminAuth.php';
AdminAuth::requireLogin();
@@ -7,7 +8,7 @@ if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$pageTitle = "Liste des TFE";
$pageTitle = 'Liste des TFE';
require_once __DIR__ . '/../../src/Database.php';
// ── CSV Import (inline, submitted to this same page) ─────────────────────────
@@ -18,7 +19,7 @@ $importDone = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
$importErrors[] = "Erreur de sécurité : token invalide.";
$importErrors[] = 'Erreur de sécurité : token invalide.';
} else {
$importedCount = 0;
$skippedCount = 0;
@@ -27,11 +28,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
$importPdo = $importDb->getPDO();
if ($_FILES['csv_file']['error'] !== UPLOAD_ERR_OK) {
throw new Exception("Erreur lors du téléversement du fichier.");
throw new Exception('Erreur lors du téléversement du fichier.');
}
$handle = fopen($_FILES['csv_file']['tmp_name'], 'r');
if (!$handle) throw new Exception("Impossible d'ouvrir le fichier CSV.");
if (!$handle) {
throw new Exception("Impossible d'ouvrir le fichier CSV.");
}
// Scan up to 8 rows looking for a header row with known column names.
// Build colIdx[name] → position map; fall back to positional if header not found.
@@ -50,40 +53,66 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
$knownHeaders[] = 'license';
for ($scan = 0; $scan < 8; $scan++) {
$hrow = fgetcsv($handle, 0, ',', '"', '');
if ($hrow === false) break;
if ($hrow === false) {
break;
}
$headerRowNum++;
$normRow = array_map(fn($s) => strtolower(trim((string)$s)), $hrow);
$normRow = array_map(fn ($s) => strtolower(trim((string)$s)), $hrow);
$hits = 0;
$map = [];
$used = [];
foreach ($knownHeaders as $h) {
foreach ($normRow as $pos => $cell) {
if (isset($used[$pos])) continue;
if (isset($used[$pos])) {
continue;
}
// Exact match
if ($cell === $h) { $hits++; $map[$h] = $pos; $used[$pos] = true; break; }
if ($cell === $h) {
$hits++;
$map[$h] = $pos;
$used[$pos] = true;
break;
}
// Licence/License cross-match
if (($h === 'licence' && $cell === 'license') || ($h === 'license' && $cell === 'licence'))
{ $hits++; $map[$h] = $pos; $used[$pos] = true; break; }
if (($h === 'licence' && $cell === 'license') || ($h === 'license' && $cell === 'licence')) {
$hits++;
$map[$h] = $pos;
$used[$pos] = true;
break;
}
// Prefix match (for compound headers like "contact.visible")
$hlen = strlen($h);
if ($hlen >= 4 && str_starts_with($cell, $h)) {
// Avoid short prefixes matching unrelated words
if ($hlen >= 5 || $cell === $h) { $hits++; $map[$h] = $pos; $used[$pos] = true; break; }
if ($hlen >= 5 || $cell === $h) {
$hits++;
$map[$h] = $pos;
$used[$pos] = true;
break;
}
}
// Substring match for short distinguishers (ulb, externe)
if ($hlen >= 3 && $hlen <= 7 && str_contains($cell, $h)) {
$hits++; $map[$h] = $pos; $used[$pos] = true; break;
$hits++;
$map[$h] = $pos;
$used[$pos] = true;
break;
}
}
}
// Require at least 11 known headers to trust the row.
if ($hits >= 11) { $colIdx = $map; break; }
if ($hits >= 11) {
$colIdx = $map;
break;
}
}
// If no header row found, rewind and fall back to positional (skip 4 rows).
if ($colIdx === null) {
rewind($handle);
$headerRowNum = 4;
for ($i = 0; $i < 4; $i++) fgetcsv($handle);
for ($i = 0; $i < 4; $i++) {
fgetcsv($handle);
}
} else {
// Consume blank/instruction/template rows between header and data.
// Stops when a row has a non-empty identifiant column that is not a
@@ -92,7 +121,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
$peekRow = null;
while (true) {
$peek = fgetcsv($handle, 0, ',', '"', '');
if ($peek === false) break;
if ($peek === false) {
break;
}
$headerRowNum++;
$val = trim((string)($peek[$idPos] ?? ''));
if ($val === '' || str_starts_with(strtolower($val), 'column')
@@ -117,28 +148,35 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
// When header was found: use mapped column.
// When header was found but key is missing: use positional fallback.
// When no header found: use positional fallback.
$cell = function(array $row, string $name) use ($colIdx, $fallbackPositions): string {
$cell = function (array $row, string $name) use ($colIdx, $fallbackPositions): string {
if ($colIdx !== null) {
$pos = $colIdx[$name] ?? null;
if ($pos === null) {
// Try licence/license cross-lookup
if ($name === 'license') $pos = $colIdx['licence'] ?? null;
elseif ($name === 'licence') $pos = $colIdx['license'] ?? null;
if ($name === 'license') {
$pos = $colIdx['licence'] ?? null;
} elseif ($name === 'licence') {
$pos = $colIdx['license'] ?? null;
}
}
// Fall back to positional if header didn't have this key
if ($pos === null) {
$pos = $fallbackPositions[$name] ?? null;
}
if ($pos === null) return '';
if ($pos === null) {
return '';
}
} else {
$pos = $fallbackPositions[$name] ?? null;
if ($pos === null) return '';
if ($pos === null) {
return '';
}
}
return isset($row[$pos]) ? trim((string)$row[$pos]) : '';
};
// Helper: parse "Oui"/"Non"/1/0 strings to int
$parseBool = function(string $raw): int {
$parseBool = function (string $raw): int {
$raw = strtolower(trim($raw));
return ($raw === 'oui' || $raw === '1' || $raw === 'yes' || $raw === 'true') ? 1 : 0;
};
@@ -149,9 +187,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
// Europe/Brussels timezone and convert to UTC for storage, matching
// the rest of the DB (SQLite CURRENT_TIMESTAMP) and the db_datetime()
// display helper which converts UTC → Brussels on read.
$parseBulkDate = function(string $raw): ?string {
$parseBulkDate = function (string $raw): ?string {
$raw = trim($raw);
if ($raw === '') return null;
if ($raw === '') {
return null;
}
// Try a set of formats (d/m/Y first — the CSV convention).
// Date-only formats (no time component) are anchored to midnight
@@ -190,11 +230,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
// Code → canonical name (legacy short-code CSV format)
$orientationCodeMap = [
'SC'=>'Sculpture','VI'=>'Vidéographie','CA'=>"Cinéma d'animation",
'IP'=>'Installation-Performance','PE'=>'Peinture','PH'=>'Photographie',
'DE'=>'Dessin','AN'=>'Arts Numériques','GR'=>'Graphisme',
'TY'=>'Typographie','DN'=>'Design Numérique','IL'=>'Illustration',
'BD'=>'Bande-Dessinée','SE'=>'Sérigraphie','GV'=>'Gravure',
'SC' => 'Sculpture','VI' => 'Vidéographie','CA' => "Cinéma d'animation",
'IP' => 'Installation-Performance','PE' => 'Peinture','PH' => 'Photographie',
'DE' => 'Dessin','AN' => 'Arts Numériques','GR' => 'Graphisme',
'TY' => 'Typographie','DN' => 'Design Numérique','IL' => 'Illustration',
'BD' => 'Bande-Dessinée','SE' => 'Sérigraphie','GV' => 'Gravure',
];
// Alias map: normalise known variant spellings → canonical DB name.
@@ -210,9 +250,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
];
// Resolve an orientation string (code or full name) → canonical DB name.
$resolveOrientation = function(string $raw) use ($orientationCodeMap, $orientationAliases, $importPdo): ?int {
$resolveOrientation = function (string $raw) use ($orientationCodeMap, $orientationAliases, $importPdo): ?int {
$raw = trim($raw);
if ($raw === '') return null;
if ($raw === '') {
return null;
}
// 1. Try legacy short code
if (isset($orientationCodeMap[$raw])) {
@@ -226,13 +268,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
}
// 3. Exact DB match
$s = $importPdo->prepare("SELECT id FROM orientations WHERE name = ?");
$s = $importPdo->prepare('SELECT id FROM orientations WHERE name = ?');
$s->execute([$raw]);
$r = $s->fetch();
if ($r) return (int)$r['id'];
if ($r) {
return (int)$r['id'];
}
// 4. Case-insensitive DB match
$s = $importPdo->prepare("SELECT id FROM orientations WHERE LOWER(name) = LOWER(?)");
$s = $importPdo->prepare('SELECT id FROM orientations WHERE LOWER(name) = LOWER(?)');
$s->execute([$raw]);
$r = $s->fetch();
return $r ? (int)$r['id'] : null;
@@ -257,9 +301,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
];
// Resolve an AP string (code or full name) → ap_program id.
$resolveAP = function(string $raw) use ($apAliases, $importPdo): ?int {
$resolveAP = function (string $raw) use ($apAliases, $importPdo): ?int {
$raw = trim($raw);
if ($raw === '') return null;
if ($raw === '') {
return null;
}
// 1. Try alias map (lowercase key) → canonical code
$key = strtolower($raw);
@@ -268,19 +314,23 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
}
// 2. Match by code
$s = $importPdo->prepare("SELECT id FROM ap_programs WHERE code = ?");
$s = $importPdo->prepare('SELECT id FROM ap_programs WHERE code = ?');
$s->execute([$raw]);
$r = $s->fetch();
if ($r) return (int)$r['id'];
if ($r) {
return (int)$r['id'];
}
// 3. Code match (legacy)
$s = $importPdo->prepare("SELECT id FROM ap_programs WHERE code = ?");
$s = $importPdo->prepare('SELECT id FROM ap_programs WHERE code = ?');
$s->execute([$raw]);
$r = $s->fetch();
if ($r) return (int)$r['id'];
if ($r) {
return (int)$r['id'];
}
// 4. Case-insensitive name match
$s = $importPdo->prepare("SELECT id FROM ap_programs WHERE LOWER(name) = LOWER(?)");
$s = $importPdo->prepare('SELECT id FROM ap_programs WHERE LOWER(name) = LOWER(?)');
$s->execute([$raw]);
$r = $s->fetch();
return $r ? (int)$r['id'] : null;
@@ -294,10 +344,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
$usePeek = false;
} else {
$row = fgetcsv($handle, 0, ',', '"', '');
if ($row === false) break;
if ($row === false) {
break;
}
}
$lineNumber++;
if (empty($row[0]) && empty($row[1])) continue;
if (empty($row[0]) && empty($row[1])) {
continue;
}
try {
$importDb->beginTransaction();
@@ -355,10 +409,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
if ($title === '' || $year === 0) {
$missing = [];
if ($title === '') $missing[] = 'titre';
if ($year === 0) $missing[] = 'année';
throw new Exception("Champ(s) requis manquant(s) : " . implode(', ', $missing)
. " (id=\"" . ($identifier ?: '?') . "\", titre=\"" . substr($title, 0, 80) . "\")");
if ($title === '') {
$missing[] = 'titre';
}
if ($year === 0) {
$missing[] = 'année';
}
throw new Exception('Champ(s) requis manquant(s) : ' . implode(', ', $missing)
. ' (id="' . ($identifier ?: '?') . '", titre="' . substr($title, 0, 80) . '")');
}
$orientationId = $resolveOrientation($orientationCode);
@@ -366,26 +424,30 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
$finalityId = null;
if (!empty($finalityName)) {
$s = $importPdo->prepare("SELECT id FROM finality_types WHERE name = ?");
$s = $importPdo->prepare('SELECT id FROM finality_types WHERE name = ?');
$s->execute([$finalityName]);
$r = $s->fetch(); $finalityId = $r ? $r['id'] : null;
$r = $s->fetch();
$finalityId = $r ? $r['id'] : null;
}
$accessTypeId = null;
if (!empty($access)) {
$s = $importPdo->prepare("SELECT id FROM access_types WHERE name = ?");
$s = $importPdo->prepare('SELECT id FROM access_types WHERE name = ?');
$s->execute([ucfirst(strtolower($access))]);
$r = $s->fetch(); $accessTypeId = $r ? $r['id'] : null;
$r = $s->fetch();
$accessTypeId = $r ? $r['id'] : null;
}
if ($accessTypeId === null) {
$accessTypeId = 1;
}
if ($accessTypeId === null) $accessTypeId = 1;
if (!empty($identifier)) {
$s = $importPdo->prepare("SELECT id FROM theses WHERE identifier = ?");
$s = $importPdo->prepare('SELECT id FROM theses WHERE identifier = ?');
$s->execute([$identifier]);
if ($s->fetch()) {
$importDb->rollback();
$skippedCount++;
$importResults[] = ['type'=>'skip', 'msg'=>"Ligne $lineNumber: identifiant \"$identifier\" déjà présent, ignoré."];
$importResults[] = ['type' => 'skip', 'msg' => "Ligne $lineNumber: identifiant \"$identifier\" déjà présent, ignoré."];
continue;
}
}
@@ -393,7 +455,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
// Resolve license name → license_id
$licenseId = null;
if (!empty($license)) {
$ls = $importPdo->prepare("SELECT id FROM license_types WHERE LOWER(name) = LOWER(?)");
$ls = $importPdo->prepare('SELECT id FROM license_types WHERE LOWER(name) = LOWER(?)');
$ls->execute([trim($license)]);
$lr = $ls->fetch();
$licenseId = $lr ? (int)$lr['id'] : null;
@@ -418,13 +480,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
");
$s->execute([
!empty($identifier) ? $identifier : null, $title,
!empty($subtitle) ? $subtitle : null, $year,
!empty($subtitle) ? $subtitle : null, $year,
$orientationId, $apProgramId, $finalityId,
!empty($synopsis) ? $synopsis : null,
!empty($context) ? $context : null,
!empty($remarks) ? $remarks : null,
!empty($synopsis) ? $synopsis : null,
!empty($context) ? $context : null,
!empty($remarks) ? $remarks : null,
$juryPoints,
!empty($baiuLink) ? $baiuLink : null,
!empty($baiuLink) ? $baiuLink : null,
$accessTypeId,
$licenseId,
!empty($licenseCustom) ? $licenseCustom : null,
@@ -444,7 +506,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
foreach (array_map('trim', explode(',', $authorsRaw)) as $idx => $name) {
if ($name) {
$aId = $importDb->findOrCreateAuthor($name, $idx === 0 ? $contact : null);
$s = $importPdo->prepare("INSERT INTO thesis_authors (thesis_id, author_id, author_order) VALUES (?,?,?)");
$s = $importPdo->prepare('INSERT INTO thesis_authors (thesis_id, author_id, author_order) VALUES (?,?,?)');
$s->execute([$thesisId, $aId, $idx + 1]);
}
}
@@ -457,7 +519,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
if ($name) {
$juryOrder++;
$sId = $importDb->findOrCreateSupervisor($name);
$stmt = $importPdo->prepare("INSERT INTO thesis_supervisors (thesis_id, supervisor_id, role, is_external, is_ulb, supervisor_order) VALUES (?,?,?,?,?,?)");
$stmt = $importPdo->prepare('INSERT INTO thesis_supervisors (thesis_id, supervisor_id, role, is_external, is_ulb, supervisor_order) VALUES (?,?,?,?,?,?)');
$stmt->execute([$thesisId, $sId, 'promoteur', 0, 0, $juryOrder]);
}
}
@@ -468,7 +530,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
if ($name) {
$juryOrder++;
$sId = $importDb->findOrCreateSupervisor($name);
$stmt = $importPdo->prepare("INSERT INTO thesis_supervisors (thesis_id, supervisor_id, role, is_external, is_ulb, supervisor_order) VALUES (?,?,?,?,?,?)");
$stmt = $importPdo->prepare('INSERT INTO thesis_supervisors (thesis_id, supervisor_id, role, is_external, is_ulb, supervisor_order) VALUES (?,?,?,?,?,?)');
$stmt->execute([$thesisId, $sId, 'lecteur', 0, 0, $juryOrder]);
}
}
@@ -479,7 +541,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
if ($name) {
$juryOrder++;
$sId = $importDb->findOrCreateSupervisor($name);
$stmt = $importPdo->prepare("INSERT INTO thesis_supervisors (thesis_id, supervisor_id, role, is_external, is_ulb, supervisor_order) VALUES (?,?,?,?,?,?)");
$stmt = $importPdo->prepare('INSERT INTO thesis_supervisors (thesis_id, supervisor_id, role, is_external, is_ulb, supervisor_order) VALUES (?,?,?,?,?,?)');
$stmt->execute([$thesisId, $sId, 'lecteur', 1, 0, $juryOrder]);
}
}
@@ -490,19 +552,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
if ($name) {
$juryOrder++;
$sId = $importDb->findOrCreateSupervisor($name);
$stmt = $importPdo->prepare("INSERT INTO thesis_supervisors (thesis_id, supervisor_id, role, is_external, is_ulb, supervisor_order) VALUES (?,?,?,?,?,?)");
$stmt = $importPdo->prepare('INSERT INTO thesis_supervisors (thesis_id, supervisor_id, role, is_external, is_ulb, supervisor_order) VALUES (?,?,?,?,?,?)');
$stmt->execute([$thesisId, $sId, 'promoteur', 1, 1, $juryOrder]);
}
}
}
if (!empty($keywordsRaw)) {
$normalizeTag = fn(string $t): string => strtolower(trim(preg_replace('/\s+/', ' ', $t)));
$normalizeTag = fn (string $t): string => strtolower(trim(preg_replace('/\s+/', ' ', $t)));
$tags = array_values(array_unique(array_map($normalizeTag, explode(',', $keywordsRaw))));
$tags = array_filter($tags, fn($t) => $t !== '');
$tags = array_filter($tags, fn ($t) => $t !== '');
foreach (array_slice($tags, 0, 10) as $kw) {
$tId = $importDb->findOrCreateTag($kw);
if ($tId) {
$s = $importPdo->prepare("INSERT INTO thesis_tags (thesis_id, tag_id) VALUES (?,?)");
$s = $importPdo->prepare('INSERT INTO thesis_tags (thesis_id, tag_id) VALUES (?,?)');
$s->execute([$thesisId, $tId]);
}
}
@@ -510,28 +572,30 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
if (!empty($languageRaw)) {
foreach (array_map('trim', explode(',', $languageRaw)) as $langName) {
$langName = strtolower($langName);
if ($langName === '') continue;
if ($langName === '') {
continue;
}
// Lookup case-insensitively; insert if missing (stored lowercase).
$s = $importPdo->prepare("SELECT id FROM languages WHERE LOWER(name) = LOWER(?) AND deleted_at IS NULL");
$s = $importPdo->prepare('SELECT id FROM languages WHERE LOWER(name) = LOWER(?) AND deleted_at IS NULL');
$s->execute([$langName]);
$r = $s->fetch();
$langId = $r ? (int)$r['id'] : null;
if ($langId === null) {
$importPdo->prepare("INSERT INTO languages (name) VALUES (?)")->execute([$langName]);
$importPdo->prepare('INSERT INTO languages (name) VALUES (?)')->execute([$langName]);
$langId = (int)$importPdo->lastInsertId();
}
$s2 = $importPdo->prepare("INSERT INTO thesis_languages (thesis_id, language_id) VALUES (?,?)");
$s2 = $importPdo->prepare('INSERT INTO thesis_languages (thesis_id, language_id) VALUES (?,?)');
$s2->execute([$thesisId, $langId]);
}
}
if (!empty($formatsRaw)) {
foreach (array_map('trim', explode(',', $formatsRaw)) as $fmt) {
if ($fmt) {
$s = $importPdo->prepare("SELECT id FROM format_types WHERE name = ?");
$s = $importPdo->prepare('SELECT id FROM format_types WHERE name = ?');
$s->execute([ucfirst(strtolower($fmt))]);
$r = $s->fetch();
if ($r) {
$s2 = $importPdo->prepare("INSERT INTO thesis_formats (thesis_id, format_id) VALUES (?,?)");
$s2 = $importPdo->prepare('INSERT INTO thesis_formats (thesis_id, format_id) VALUES (?,?)');
$s2->execute([$thesisId, $r['id']]);
}
}
@@ -540,12 +604,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
$importDb->commit();
$importedCount++;
$importResults[] = ['type'=>'ok', 'msg'=>"\"$title\" (ID: $thesisId)"];
$importResults[] = ['type' => 'ok', 'msg' => "\"$title\" (ID: $thesisId)"];
} catch (Exception $e) {
$importDb->rollback();
$skippedCount++;
$importResults[] = ['type'=>'error', 'msg'=>"Ligne $lineNumber: " . $e->getMessage()];
$importResults[] = ['type' => 'error', 'msg' => "Ligne $lineNumber: " . $e->getMessage()];
error_log("Import error on line $lineNumber: " . $e->getMessage());
}
}
@@ -554,7 +618,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
$importDone = true;
} catch (Exception $e) {
$importErrors[] = $e->getMessage();
error_log("CSV import error: " . $e->getMessage());
error_log('CSV import error: ' . $e->getMessage());
}
}
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
@@ -562,19 +626,27 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
try {
$db = new Database();
$searchQuery = isset($_GET['search']) ? trim($_GET['search']) : '';
$yearFilter = isset($_GET['year']) ? intval($_GET['year']) : null;
$searchQuery = isset($_GET['search']) ? trim($_GET['search']) : '';
$yearFilter = isset($_GET['year']) ? intval($_GET['year']) : null;
$orientationFilter = isset($_GET['orientation']) ? intval($_GET['orientation']) : null;
$apFilter = isset($_GET['ap']) ? intval($_GET['ap']) : 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';
$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;
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;
@@ -589,21 +661,11 @@ try {
$trashedTheses = ($tab === 'trash') ? $db->getTrashedTheses() : [];
// ── Tmp file stats ───────────────────────────────────────────────────
$filepondDir = STORAGE_ROOT . '/tmp/filepond';
// Abandoned FilePond uploads are purged automatically by cron, so the badge
// only reflects the manually-managed trash.
$trashDir = STORAGE_ROOT . '/tmp/_trash';
$tmpFilepondCount = 0;
$tmpTrashCount = 0;
$tmpTotalCount = 0;
if (is_dir($filepondDir)) {
$items = @scandir($filepondDir);
if ($items !== false) {
foreach ($items as $item) {
if ($item !== '.' && $item !== '..' && $item !== '.gitkeep') {
$tmpFilepondCount++;
}
}
}
}
if (is_dir($trashDir)) {
$items = @scandir($trashDir);
if ($items !== false) {
@@ -614,14 +676,15 @@ try {
}
}
}
$tmpTotalCount = $tmpFilepondCount + $tmpTrashCount;
$tmpTotalCount = $tmpTrashCount;
} catch (Exception $e) {
error_log("Error loading theses list: " . $e->getMessage());
die("Erreur lors du chargement de la liste.");
error_log('Error loading theses list: ' . $e->getMessage());
die('Erreur lors du chargement de la liste.');
}
$isHtmx = ($_SERVER['HTTP_HX_REQUEST'] ?? '') === 'true';
$isAdmin = true; $bodyClass = 'admin-body';
$isAdmin = true;
$bodyClass = 'admin-body';
if ($isHtmx) {
if ($tab === 'trash') {
include APP_ROOT . '/templates/admin/index-trash.php';
@@ -640,4 +703,3 @@ if ($isHtmx) {
}
require_once APP_ROOT . '/templates/admin/footer.php';
}