feat: admin tag management, maintenance mode, TFE visibility states

Tags admin:
- Database: getAllTagsWithCount(), renameTag(), mergeTag(), deleteTag()
- public/admin/tags.php: table with inline rename/merge/delete forms, CSRF-guarded
- public/admin/actions/tag.php: routes on action=rename|merge|delete
- templates/admin/head.php: 'Mots-clés' nav link
- admin.css: admin-inline-form, admin-btn--sm/warning/danger variants

Maintenance mode:
- config/bootstrap.php: gate on MAINTENANCE_FLAG file; admin/ and maintenance.php exempt
- public/maintenance.php: 503 dark minimal page
- public/admin/actions/maintenance.php: enable/disable toggle
- public/admin/index.php: status bar with toggle button
- admin.css: admin-maintenance-bar styles

TFE Visibility (Libre/Interne/Interdit via existing access_type_id):
- migration 002_add_visibility.sql: seeds access_types if missing
- Database: setVisibility(), bulkSetVisibility(), getAccessTypes()
- public/media.php: blocks thesis files for access_type_id=3
- public/tfe.php: shows access_type, context_note; hides file panel for Interdit
- public/admin/edit.php: access_type_id select + context_note textarea; saves both
- public/admin/index.php: three-state badge (Libre/Interne/Interdit) per row
- public/admin/actions/visibility.php: single + bulk visibility action handler
- admin.css: status-access badge variants
This commit is contained in:
Pontoporeia
2026-03-24 15:35:52 +01:00
parent 0933137540
commit 92e344b757
17 changed files with 661 additions and 35 deletions

View File

@@ -640,6 +640,69 @@ class Database {
return $this->findOrCreateTag((string)$keyword);
}
// ========================================================================
// TAG MANAGEMENT (admin)
// ========================================================================
/**
* Return all tags with a count of associated (published) theses.
*/
public function getAllTagsWithCount(): array {
$stmt = $this->pdo->query("
SELECT tg.id, tg.name,
COUNT(DISTINCT tt.thesis_id) as thesis_count
FROM tags tg
LEFT JOIN thesis_tags tt ON tg.id = tt.tag_id
GROUP BY tg.id
ORDER BY tg.name COLLATE NOCASE
");
return $stmt->fetchAll();
}
/**
* Rename a tag. Throws if the new name already exists.
*/
public function renameTag(int $id, string $newName): void {
$newName = trim($newName);
if ($newName === '') throw new Exception("Le nom du tag ne peut pas être vide.");
// Check uniqueness
$stmt = $this->pdo->prepare("SELECT id FROM tags WHERE name = ? AND id != ?");
$stmt->execute([$newName, $id]);
if ($stmt->fetch()) throw new Exception("Un tag avec ce nom existe déjà.");
$this->pdo->prepare("UPDATE tags SET name = ? WHERE id = ?")->execute([$newName, $id]);
}
/**
* Merge sourceId into targetId: reassign all thesis_tags rows, then delete source.
* Uses INSERT OR IGNORE to avoid PK conflicts.
*/
public function mergeTag(int $sourceId, int $targetId): void {
if ($sourceId === $targetId) throw new Exception("Source et destination identiques.");
$this->pdo->beginTransaction();
try {
// Re-point thesis_tags rows from source → target (skip conflicts)
$this->pdo->prepare("
INSERT OR IGNORE INTO thesis_tags (tag_id, thesis_id)
SELECT ?, thesis_id FROM thesis_tags WHERE tag_id = ?
")->execute([$targetId, $sourceId]);
// Delete the old source rows
$this->pdo->prepare("DELETE FROM thesis_tags WHERE tag_id = ?")->execute([$sourceId]);
// Delete the source tag itself
$this->pdo->prepare("DELETE FROM tags WHERE id = ?")->execute([$sourceId]);
$this->pdo->commit();
} catch (\Throwable $e) {
$this->pdo->rollBack();
throw $e;
}
}
/**
* Delete a tag and all its thesis_tags rows (cascades via FK).
*/
public function deleteTag(int $id): void {
$this->pdo->prepare("DELETE FROM tags WHERE id = ?")->execute([$id]);
}
/**
* Get orientation ID by name
*/
@@ -749,6 +812,44 @@ class Database {
return $this->getLicenseTypes();
}
// ========================================================================
// VISIBILITY METHODS
// ========================================================================
/**
* Set the access_type_id (visibility) for a single thesis.
* @param int $thesisId
* @param int|null $accessTypeId 1=Libre, 2=Interne, 3=Interdit, null=unset
*/
public function setVisibility(int $thesisId, ?int $accessTypeId): void {
$stmt = $this->pdo->prepare(
"UPDATE theses SET access_type_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"
);
$stmt->execute([$accessTypeId, $thesisId]);
}
/**
* Set visibility for multiple theses at once.
* @param int[] $thesisIds
* @param int|null $accessTypeId
*/
public function bulkSetVisibility(array $thesisIds, ?int $accessTypeId): void {
if (empty($thesisIds)) return;
$placeholders = implode(',', array_fill(0, count($thesisIds), '?'));
$params = array_merge([$accessTypeId], $thesisIds);
$this->pdo->prepare(
"UPDATE theses SET access_type_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id IN ($placeholders)"
)->execute($params);
}
/**
* Get all access types (visibility options).
*/
public function getAccessTypes(): array {
$stmt = $this->pdo->query("SELECT * FROM access_types ORDER BY id");
return $stmt->fetchAll();
}
// ========================================================================
// JURY METHODS
// ========================================================================