mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 19:19:19 +02:00
Separate admin views from controllers — move HTML to templates/admin/
All admin pages refactored to thin controllers + pure view templates, mirroring the public-page pattern: Controllers (public/admin/*.php): auth, data loading, include template Views (templates/admin/*.php): pure HTML/PHP output Fragment partials (templates/admin/partials/): toast, system-log-panel, system-nginx-config-panel Pages migrated: login, tags, contenus, contenus-edit, account, acces-etudiante, thanks, add, edit, parametres, system, index Fragment endpoints refactored: system-fragment.php, toast-fragment.php Skipped (pure redirects): logout, logs, status, import
This commit is contained in:
2
TODO.md
2
TODO.md
@@ -2,3 +2,5 @@
|
||||
|
||||
- [x] Fix broken `flash-messages.php` include in admin footer
|
||||
- [x] Make `.repertoire-col` columns scrollable instead of `.search-main`
|
||||
- [x] Replace JS toast system with pure HTMX toast fragment (top-right, CSS-only auto-fade)
|
||||
- [x] Separate admin views from controllers: move all HTML to `templates/admin/*.php`, fragments to `templates/admin/partials/`
|
||||
|
||||
@@ -13,193 +13,7 @@ $baseUrl = $protocol . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost');
|
||||
$pageTitle = 'Accès étudiant·e';
|
||||
$isAdmin = true;
|
||||
$bodyClass = 'admin-body';
|
||||
?>
|
||||
<?php require_once APP_ROOT . '/templates/head.php'; ?>
|
||||
<?php include APP_ROOT . '/templates/header.php'; ?>
|
||||
|
||||
<main id="main-content">
|
||||
|
||||
|
||||
<div class="admin-list-toolbar">
|
||||
<h1>Accès étudiant·e</h1>
|
||||
<div class="admin-list-toolbar__right">
|
||||
<button type="button" class="admin-btn admin-btn--sm" id="open-create-dialog">
|
||||
+ Créer un lien
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (empty($links)): ?>
|
||||
<p class="admin-empty">Aucun lien d'accès créé. Cliquez sur « Créer un lien » pour générer un lien partageable.</p>
|
||||
<?php else: ?>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Lien</th>
|
||||
<th scope="col">Statut</th>
|
||||
<th scope="col">Mot de passe</th>
|
||||
<th scope="col">Utilisations</th>
|
||||
<th scope="col">Expiration</th>
|
||||
<th scope="col">Créé le</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($links as $link): ?>
|
||||
<?php
|
||||
$isExpired = $link['expires_at'] !== null && strtotime($link['expires_at']) < time();
|
||||
$isActive = (bool)$link['is_active'] && !$isExpired;
|
||||
$statusLabel = $isExpired ? 'Expiré' : ($link['is_active'] ? 'Actif' : 'Désactivé');
|
||||
if ($isExpired) {
|
||||
$statusClass = 'status-badge status-pending';
|
||||
} elseif ($link['is_active']) {
|
||||
$statusClass = 'status-badge status-published';
|
||||
} else {
|
||||
$statusClass = 'status-badge';
|
||||
$statusClass .= ' style="background:var(--error-muted-bg);color:var(--error);"';
|
||||
}
|
||||
$fullUrl = $baseUrl . '/partage/' . htmlspecialchars($link['slug']);
|
||||
$created = date('d/m/Y H:i', strtotime($link['created_at']));
|
||||
$expires = $link['expires_at'] ? date('d/m/Y', strtotime($link['expires_at'])) : '—';
|
||||
$hasPassword = !empty($link['password_hash']);
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<code style="font-size:var(--step--2);color:var(--text-secondary);"><?= htmlspecialchars($link['slug']) ?></code>
|
||||
<input type="hidden" id="url-<?= $link['id'] ?>" value="<?= $fullUrl ?>">
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($isExpired): ?>
|
||||
<span class="status-badge status-pending"><?= $statusLabel ?></span>
|
||||
<?php elseif ($link['is_active']): ?>
|
||||
<span class="status-badge status-published"><?= $statusLabel ?></span>
|
||||
<?php else: ?>
|
||||
<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 style="text-align:center;"><?= intval($link['usage_count']) ?></td>
|
||||
<td><?= $expires ?></td>
|
||||
<td><?= $created ?></td>
|
||||
<td>
|
||||
<div class="admin-actions">
|
||||
<a href="/partage/<?= urlencode($link['slug']) ?>" target="_blank" rel="noopener"
|
||||
class="admin-btn-sm admin-btn-visit" title="Visiter le formulaire">
|
||||
👁 Visiter
|
||||
</a>
|
||||
<button type="button" class="admin-btn-sm admin-btn-view"
|
||||
onclick="copyUrl(<?= $link['id'] ?>)" title="Copier l'URL">
|
||||
Copier
|
||||
</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="admin-btn-sm <?= $link['is_active'] ? 'admin-btn-unpublish' : 'admin-btn-publish' ?>"
|
||||
title="<?= $link['is_active'] ? 'Désactiver' : 'Activer' ?>">
|
||||
<?= $link['is_active'] ? '⏸' : '▶' ?>
|
||||
</button>
|
||||
</form>
|
||||
<button type="button" class="admin-btn-sm admin-btn-edit"
|
||||
onclick="openPasswordDialog(<?= $link['id'] ?>, <?= $hasPassword ? 'true' : 'false' ?>)"
|
||||
title="Modifier le mot de passe">
|
||||
🔑
|
||||
</button>
|
||||
<form method="post" action="actions/acces-etudiante.php" class="publish-form"
|
||||
onsubmit="return confirm('Supprimer ce lien ? Les soumissions via ce lien seront bloquées.')">
|
||||
<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="submit" class="admin-btn-sm admin-btn-delete" title="Supprimer">
|
||||
🗑
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
|
||||
<!-- ═══════════════════════ CREATE DIALOG ═══════════════════════ -->
|
||||
<dialog id="create-dialog" class="admin-dialog" aria-labelledby="create-dialog-title">
|
||||
<div class="admin-dialog__header">
|
||||
<h2 id="create-dialog-title">Créer un lien d'accès</h2>
|
||||
<button type="button" class="admin-dialog__close" aria-label="Fermer"
|
||||
onclick="document.getElementById('create-dialog').close()">✕</button>
|
||||
</div>
|
||||
<form method="post" action="actions/acces-etudiante.php" class="admin-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="action" value="create">
|
||||
<div>
|
||||
<label for="create-password">Mot de passe (optionnel)</label>
|
||||
<input type="password" id="create-password" name="password" autocomplete="new-password">
|
||||
<small>Laissez vide pour un lien sans mot de passe.</small>
|
||||
</div>
|
||||
<div>
|
||||
<label for="create-expires">Expiration (optionnel)</label>
|
||||
<input type="datetime-local" id="create-expires" name="expires_at">
|
||||
<small>Laissez vide pour qu'il n'expire jamais.</small>
|
||||
</div>
|
||||
<div class="admin-form-footer">
|
||||
<button type="submit" class="admin-btn">Créer le lien</button>
|
||||
<button type="button" class="admin-btn-secondary"
|
||||
onclick="document.getElementById('create-dialog').close()">Annuler</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<!-- ═══════════════════════ PASSWORD DIALOG ═══════════════════════ -->
|
||||
<dialog id="password-dialog" class="admin-dialog" aria-labelledby="password-dialog-title">
|
||||
<div class="admin-dialog__header">
|
||||
<h2 id="password-dialog-title">Mot de passe</h2>
|
||||
<button type="button" class="admin-dialog__close" aria-label="Fermer"
|
||||
onclick="document.getElementById('password-dialog').close()">✕</button>
|
||||
</div>
|
||||
<form method="post" action="actions/acces-etudiante.php" class="admin-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="action" value="set_password">
|
||||
<input type="hidden" name="id" id="password-link-id" value="">
|
||||
<div>
|
||||
<label for="password-input">Nouveau mot de passe</label>
|
||||
<input type="password" id="password-input" name="password" autocomplete="new-password">
|
||||
<small>Laissez vide pour supprimer le mot de passe.</small>
|
||||
<p id="password-current-info" style="font-size:var(--step--2);color:var(--text-secondary);margin-top:var(--space-2xs);"></p>
|
||||
</div>
|
||||
<div class="admin-form-footer">
|
||||
<button type="submit" class="admin-btn">Enregistrer</button>
|
||||
<button type="button" class="admin-btn-secondary"
|
||||
onclick="document.getElementById('password-dialog').close()">Annuler</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
document.getElementById('open-create-dialog').addEventListener('click', () => {
|
||||
document.getElementById('create-dialog').showModal();
|
||||
});
|
||||
|
||||
function copyUrl(id) {
|
||||
const input = document.getElementById('url-' + id);
|
||||
navigator.clipboard.writeText(input.value).then(() => {
|
||||
const btn = event.target.closest('button');
|
||||
const orig = btn.textContent;
|
||||
btn.textContent = '✓ Copié';
|
||||
setTimeout(() => { btn.textContent = orig; }, 1200);
|
||||
});
|
||||
}
|
||||
|
||||
function openPasswordDialog(id, hasPassword) {
|
||||
document.getElementById('password-link-id').value = id;
|
||||
const info = document.getElementById('password-current-info');
|
||||
info.textContent = hasPassword
|
||||
? 'Un mot de passe est actuellement configuré. Entrez-en un nouveau ou laissez vide pour le supprimer.'
|
||||
: 'Aucun mot de passe configuré.';
|
||||
document.getElementById('password-dialog').showModal();
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>
|
||||
require_once APP_ROOT . '/templates/head.php';
|
||||
include APP_ROOT . '/templates/header.php';
|
||||
include APP_ROOT . '/templates/admin/acces-etudiante.php';
|
||||
require_once APP_ROOT . '/templates/admin/footer.php';
|
||||
|
||||
@@ -5,104 +5,14 @@ AdminAuth::requireLogin();
|
||||
|
||||
$pageTitle = "Compte administrateur";
|
||||
|
||||
$hasPassword = AdminAuth::hasPassword();
|
||||
|
||||
// Flash messages are consumed by the flash-messages partial below.
|
||||
$hasPassword = AdminAuth::hasPassword();
|
||||
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
?>
|
||||
<?php $isAdmin = true; $bodyClass = 'admin-body'; require_once APP_ROOT . '/templates/head.php'; ?>
|
||||
<?php include APP_ROOT . '/templates/header.php'; ?>
|
||||
|
||||
<main id="main-content">
|
||||
<h1>Compte administrateur</h1>
|
||||
|
||||
|
||||
|
||||
<!-- Status info -->
|
||||
<dl class="admin-account-status">
|
||||
<div class="admin-account-status__row">
|
||||
<dt class="admin-account-status__label">Authentification PHP</dt>
|
||||
<dd><?php $badgeType = 'ok'; $badgeValue = $hasPassword; $badgeOkLabel = 'Active'; $badgeWarnLabel = 'Non configurée'; include APP_ROOT . '/templates/partials/status-badge.php'; ?></dd>
|
||||
</div>
|
||||
<div class="admin-account-status__row">
|
||||
<dt class="admin-account-status__label">Stockage</dt>
|
||||
<dd>
|
||||
<code class="admin-account-status__code">site_settings (DB)</code>
|
||||
<?php $badgeType = 'ok'; $badgeValue = $hasPassword; $badgeOkLabel = 'Présent'; $badgeWarnLabel = 'Absent'; include APP_ROOT . '/templates/partials/status-badge.php'; ?>
|
||||
</dd>
|
||||
</div>
|
||||
<?php if (!$hasPassword): ?>
|
||||
<p class="admin-account-status__note">
|
||||
Aucun mot de passe PHP configuré. Le formulaire ci-dessous stockera
|
||||
un hash bcrypt dans la base de données.
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</dl>
|
||||
|
||||
<!-- Password change form -->
|
||||
<h2 class="admin-section-title"><?= $hasPassword ? 'Changer le mot de passe' : 'Définir le mot de passe' ?></h2>
|
||||
|
||||
<form method="post" action="/admin/actions/account.php" class="admin-form" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
|
||||
<?php if ($hasPassword): ?>
|
||||
<div>
|
||||
<label for="current_password">Mot de passe actuel</label>
|
||||
<div>
|
||||
<input type="password" id="current_password"
|
||||
name="current_password" required autocomplete="current-password">
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div>
|
||||
<label for="new_password">Nouveau mot de passe</label>
|
||||
<div>
|
||||
<input type="password" id="new_password"
|
||||
name="new_password" required autocomplete="new-password"
|
||||
minlength="12">
|
||||
<small>Minimum 12 caractères.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="confirm_password">Confirmer le mot de passe</label>
|
||||
<div>
|
||||
<input type="password" id="confirm_password"
|
||||
name="confirm_password" required autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-form-footer">
|
||||
<button type="submit" class="admin-btn">
|
||||
<?= $hasPassword ? 'Mettre à jour le mot de passe' : 'Définir le mot de passe' ?>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php if ($hasPassword): ?>
|
||||
<!-- Danger zone: remove password -->
|
||||
<h2 class="admin-section-title admin-section-title--danger">Zone de danger</h2>
|
||||
<div class="admin-danger-zone">
|
||||
<p class="admin-danger-zone__description">
|
||||
<strong>Supprimer la configuration du mot de passe PHP</strong><br>
|
||||
<small>
|
||||
Supprime le hash de la base de données. L'accès admin
|
||||
dépendra uniquement de l'authentification nginx Basic Auth si elle est configurée.
|
||||
</small>
|
||||
</p>
|
||||
<form method="post" action="/admin/actions/account.php"
|
||||
onsubmit="return confirm('Supprimer le mot de passe PHP ? L\'accès admin ne sera protégé que par nginx Basic Auth.')">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="action" value="remove_credentials">
|
||||
<input type="hidden" name="current_password_remove" id="current_password_remove" value="">
|
||||
<button type="submit" class="admin-btn admin-btn--danger">Supprimer</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
|
||||
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>
|
||||
$isAdmin = true; $bodyClass = 'admin-body';
|
||||
require_once APP_ROOT . '/templates/head.php';
|
||||
include APP_ROOT . '/templates/header.php';
|
||||
include APP_ROOT . '/templates/admin/account.php';
|
||||
require_once APP_ROOT . '/templates/admin/footer.php';
|
||||
|
||||
@@ -23,9 +23,6 @@ $formData = $_SESSION['form_data'] ?? [];
|
||||
unset($_SESSION['form_data']);
|
||||
$autofocusField = App::consumeAutofocus();
|
||||
|
||||
/**
|
||||
* Merge autofocus into the $attrs array for a given field.
|
||||
*/
|
||||
function withAutofocus(string $fieldName, array $attrs = []): array {
|
||||
global $autofocusField;
|
||||
if ($autofocusField === $fieldName) {
|
||||
@@ -38,130 +35,17 @@ function old($key, $default = "") {
|
||||
global $formData;
|
||||
return isset($formData[$key]) ? htmlspecialchars($formData[$key]) : $default;
|
||||
}
|
||||
|
||||
function wasSelected($key, $value) {
|
||||
global $formData;
|
||||
if (!isset($formData[$key])) return false;
|
||||
if (is_array($formData[$key])) return in_array($value, $formData[$key]);
|
||||
return $formData[$key] == $value;
|
||||
}
|
||||
?>
|
||||
<?php
|
||||
|
||||
$isAdmin = true;
|
||||
$bodyClass = 'admin-body';
|
||||
require_once APP_ROOT . '/templates/head.php';
|
||||
include APP_ROOT . '/templates/header.php';
|
||||
?>
|
||||
|
||||
<main id="main-content">
|
||||
<div class="thesis-add-header">
|
||||
<h1>Ajouter un TFE</h1>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<p class="required-note"><span class="asterisk">*</span> Champs obligatoires</p>
|
||||
<form action="actions/formulaire.php" method="post" enctype="multipart/form-data" class="admin-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION["csrf_token"]) ?>">
|
||||
|
||||
<!-- ═══════════════════ Informations du TFE ═══════════════════ -->
|
||||
<fieldset>
|
||||
<legend>Informations du TFE</legend>
|
||||
|
||||
<?php $name = 'titre'; $label = 'Titre :'; $value = old('titre'); $required = true; $attrs = withAutofocus('titre'); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
<?php $name = 'subtitle'; $label = 'Sous-titre (si applicable) :'; $value = old('subtitle'); $required = false; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
<?php $name = 'auteurice'; $label = 'Auteur·ice(s) :'; $value = old('auteurice'); $required = true; $attrs = withAutofocus('auteurice', ['autocomplete' => 'name']); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
<?php $name = 'mail'; $label = 'Contact(s) (optionnel) [mail/site/insta/etc.] :'; $value = old('mail'); $attrs = ['autocomplete' => 'email']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-checkbox-label">
|
||||
<input type="checkbox" name="contact_public" value="1"
|
||||
<?= isset($formData['contact_public']) ? 'checked' : '' ?>>
|
||||
Je veux que mon contact soit accessible à toustes depuis la plateforme xamxam
|
||||
</label>
|
||||
<small>Si cette case est cochée, votre contact apparaîtra sur la page publique de votre TFE.</small>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="synopsis">Synopsis :</label>
|
||||
<textarea id="synopsis" name="synopsis"
|
||||
rows="7" required
|
||||
<?= $autofocusField === 'synopsis' ? 'autofocus' : '' ?>><?= old('synopsis') ?></textarea>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- ═══════════════════ Composition du jury ═══════════════════ -->
|
||||
<?php require APP_ROOT . '/templates/partials/form/jury-fieldset.php'; ?>
|
||||
|
||||
<!-- ═══════════════════ Cadre académique ═══════════════════ -->
|
||||
<fieldset>
|
||||
<legend>Cadre académique</legend>
|
||||
|
||||
<?php
|
||||
$name = 'année'; $label = 'Année :'; $value = old('année'); $required = true;
|
||||
$type = 'number';
|
||||
$placeholder = date('Y');
|
||||
$attrs = withAutofocus('année', ['min' => 2000, 'max' => date('Y') + 1]);
|
||||
include APP_ROOT . '/templates/partials/form/text-field.php';
|
||||
?>
|
||||
|
||||
<?php $name = 'orientation'; $label = 'Orientation :'; $options = $orientations; $selected = $formData['orientation'] ?? ''; $required = true; $placeholder = ''; $attrs = withAutofocus('orientation'); include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
|
||||
<?php $name = 'ap'; $label = 'Atelier pluridisciplinaire :'; $options = $apPrograms; $selected = $formData['ap'] ?? ''; $required = true; $placeholder = ''; $attrs = withAutofocus('ap'); include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
|
||||
<?php $name = 'finality'; $label = 'Finalité du master :'; $options = $finalityTypes; $selected = $formData['finality'] ?? ''; $required = true; $placeholder = ''; $attrs = withAutofocus('finality'); include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
|
||||
|
||||
<?php $name = 'languages'; $label = 'Langue(s) :'; $options = $languages; $checked = $formData['languages'] ?? []; $required = true; include APP_ROOT . '/templates/partials/form/checkbox-list.php'; ?>
|
||||
<?php $name = 'formats'; $label = 'Format(s) :'; $options = $formatTypes; $checked = $formData['formats'] ?? []; include APP_ROOT . '/templates/partials/form/checkbox-list.php'; ?>
|
||||
|
||||
<?php $name = 'tag'; $label = 'Mots-clés :'; $value = old('tag'); $placeholder = 'sociologie, anthropologie, ...'; $hint = 'Séparez par des virgules. Max 10 mots-clés.'; $attrs = withAutofocus('tag'); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
</fieldset>
|
||||
|
||||
<!-- ═══════════════════ Fichiers ═══════════════════ -->
|
||||
<fieldset>
|
||||
<legend>Fichiers</legend>
|
||||
|
||||
<?php $name = 'couverture'; $label = 'Image de couverture :'; $accept = 'image/jpeg,image/png'; $hint = 'JPG, PNG. Taille max : 10 MB.'; include APP_ROOT . '/templates/partials/form/file-field.php'; ?>
|
||||
<?php $name = 'banner'; $label = 'Image bannière (accueil) :'; $accept = 'image/jpeg,image/png,image/webp'; $hint = 'JPG, PNG ou WEBP. Format paysage recommandé (4:1). Max 5 MB.'; include APP_ROOT . '/templates/partials/form/file-field.php'; ?>
|
||||
<?php $name = 'files'; $label = 'Fichiers du TFE :'; $accept = '.pdf,.jpg,.jpeg,.png,.mp4,.zip,.vtt'; $hint = 'PDF, JPG, PNG, MP4, ZIP. Max 50 MB par fichier. Pour les vidéos, un fichier .vtt de sous-titres peut être joint (il sera associé automatiquement à la vidéo correspondante).'; $multiple = true; include APP_ROOT . '/templates/partials/form/file-field.php'; ?>
|
||||
</fieldset>
|
||||
|
||||
<!-- ═══════════════════ Métadonnées complémentaires ═══════════════════ -->
|
||||
<fieldset>
|
||||
<legend>Métadonnées complémentaires</legend>
|
||||
|
||||
<?php $name = 'license_id'; $label = 'Licence :'; $options = $licenseTypes; $selected = $formData['license_id'] ?? ''; $placeholder = '— Inconnue —'; include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
|
||||
|
||||
<?php $name = 'duration_info'; $label = 'Durée / Taille :'; $value = old('duration_info'); $placeholder = 'Ex : 84 pages'; $hint = 'Durée (minutes) ou nombre de pages.'; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
|
||||
<?php $name = 'lien'; $label = 'Lien (site / ressource) :'; $value = old('lien'); $type = 'url'; $placeholder = 'https://...'; $attrs = withAutofocus('lien'); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
|
||||
<?php
|
||||
$accessOptions = array_map(function($at) {
|
||||
return ['id' => $at['id'], 'name' => $at['name']];
|
||||
}, $enabledAccessTypes);
|
||||
$defaultAccessType = 2;
|
||||
$selectedAccessType = isset($formData['access_type_id'])
|
||||
? (int)$formData['access_type_id']
|
||||
: $defaultAccessType;
|
||||
$name = 'access_type_id';
|
||||
$label = 'Visibilité / Accès :';
|
||||
$options = $accessOptions;
|
||||
$selected = $selectedAccessType;
|
||||
$placeholder = null;
|
||||
$required = true;
|
||||
$attrs = [];
|
||||
include APP_ROOT . '/templates/partials/form/select-field.php';
|
||||
?>
|
||||
</fieldset>
|
||||
|
||||
<!-- ═══════════════════ E-mail de confirmation ═══════════ -->
|
||||
<fieldset>
|
||||
<legend>E-mail de confirmation</legend>
|
||||
<?php $name = 'confirmation_email'; $label = 'Adresse e-mail * :'; $value = old('confirmation_email'); $type = 'email'; $required = true; $placeholder = 'ton.email@exemple.be'; $hint = 'Nécessaire pour recevoir le récapitulatif de ta soumission.'; $attrs = withAutofocus('confirmation_email'); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
</fieldset>
|
||||
|
||||
<div class="form-footer">
|
||||
<button type="submit" name="go">Soumettre</button>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>
|
||||
include APP_ROOT . '/templates/admin/add.php';
|
||||
require_once APP_ROOT . '/templates/admin/footer.php';
|
||||
|
||||
@@ -15,7 +15,6 @@ $allowedApropos = ["contacts", "credits"];
|
||||
$pageSlug = $_GET["slug"] ?? "";
|
||||
$aproposKey = $_GET["apropos"] ?? "";
|
||||
|
||||
// Exactly one target must be specified
|
||||
if ($pageSlug && !in_array($pageSlug, $allowedPageSlugs)) {
|
||||
$pageSlug = "";
|
||||
}
|
||||
@@ -62,171 +61,15 @@ var editor = new OT(document.getElementById('editor'), {
|
||||
onChange: function(value) { hidden.value = value; }
|
||||
});
|
||||
JS;
|
||||
$aproposEditorJs = null;
|
||||
if ($editType === 'apropos' && in_array($aproposKey, ['contacts', 'credits'])) {
|
||||
// Rich textarea for JSON arrays rendered as structured form
|
||||
$aproposEditorJs = <<<'JS'
|
||||
// Auto-format JSON in the hidden field for display purposes
|
||||
JS;
|
||||
}
|
||||
|
||||
$initialContent = '';
|
||||
if ($editType === 'page') {
|
||||
$initialContent = $page["content"] ?? "";
|
||||
} else {
|
||||
// For apropos, show structured form
|
||||
}
|
||||
?>
|
||||
<?php
|
||||
|
||||
$isAdmin = true;
|
||||
$bodyClass = "admin-body";
|
||||
require_once APP_ROOT . "/templates/head.php";
|
||||
?>
|
||||
<?php include APP_ROOT . "/templates/header.php"; ?>
|
||||
|
||||
<main id="main-content">
|
||||
<h1>Éditer : <?= htmlspecialchars($editTitle) ?></h1>
|
||||
|
||||
<?php if ($editType === 'page'): ?>
|
||||
<form action="/admin/actions/page.php" method="post" class="admin-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION["csrf_token"]) ?>">
|
||||
<input type="hidden" name="slug" value="<?= htmlspecialchars($pageSlug) ?>">
|
||||
|
||||
<label for="editor">Contenu (Markdown) :</label>
|
||||
<input type="hidden" id="content" name="content"
|
||||
value="<?= htmlspecialchars($initialContent) ?>">
|
||||
<div id="editor"></div>
|
||||
|
||||
<div class="admin-form-footer">
|
||||
<button type="submit" class="admin-btn">Enregistrer</button>
|
||||
<a href="/admin/contenus.php" class="admin-btn-secondary admin-cancel-link">Annuler</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php else: ?>
|
||||
<?php
|
||||
$groups = is_array($value) ? $value : [];
|
||||
?>
|
||||
<form action="/admin/actions/apropos.php" method="post" class="admin-form" id="apropos-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION["csrf_token"]) ?>">
|
||||
<input type="hidden" name="apropos_key" value="<?= htmlspecialchars($aproposKey) ?>">
|
||||
|
||||
<?php foreach ($groups as $gi => $group): ?>
|
||||
<fieldset class="apropos-group">
|
||||
<legend><?= htmlspecialchars($aproposKey === 'contacts' ? 'Contact' : 'Crédit') ?> <?= $gi + 1 ?></legend>
|
||||
<?php if ($aproposKey === 'contacts'): ?>
|
||||
<label for="group_<?= $gi ?>_role">Rôle :</label>
|
||||
<input type="text" id="group_<?= $gi ?>_role"
|
||||
name="groups[<?= $gi ?>][role]"
|
||||
value="<?= htmlspecialchars($group['role'] ?? '') ?>">
|
||||
<?php else: ?>
|
||||
<label for="group_<?= $gi ?>_label">Label :</label>
|
||||
<input type="text" id="group_<?= $gi ?>_label"
|
||||
name="groups[<?= $gi ?>][label]"
|
||||
value="<?= htmlspecialchars($group['label'] ?? '') ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<?php $entries = is_array($group['entries'] ?? null) ? $group['entries'] : []; ?>
|
||||
<?php foreach ($entries as $ei => $entry): ?>
|
||||
<div class="apropos-entry">
|
||||
<label for="entry_<?= $gi ?>_<?= $ei ?>_text"><?= $aproposKey === 'contacts' ? 'Nom' : 'Texte' ?> :</label>
|
||||
<input type="text" id="entry_<?= $gi ?>_<?= $ei ?>_text"
|
||||
name="groups[<?= $gi ?>][entries][<?= $ei ?>][text]"
|
||||
value="<?= htmlspecialchars($entry['text'] ?? '') ?>">
|
||||
<?php if ($aproposKey === 'contacts'): ?>
|
||||
<label for="entry_<?= $gi ?>_<?= $ei ?>_email">Email :</label>
|
||||
<input type="email" id="entry_<?= $gi ?>_<?= $ei ?>_email"
|
||||
name="groups[<?= $gi ?>][entries][<?= $ei ?>][email]"
|
||||
value="<?= htmlspecialchars($entry['email'] ?? '') ?>">
|
||||
<?php endif; ?>
|
||||
<label for="entry_<?= $gi ?>_<?= $ei ?>_url">Lien (optionnel) :</label>
|
||||
<input type="url" id="entry_<?= $gi ?>_<?= $ei ?>_url"
|
||||
name="groups[<?= $gi ?>][entries][<?= $ei ?>][url]"
|
||||
value="<?= htmlspecialchars($entry['url'] ?? '') ?>">
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<button type="button" class="admin-btn admin-btn--sm add-entry-btn" data-group="<?= $gi ?>">+ Ajouter une entrée</button>
|
||||
</fieldset>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<button type="button" class="admin-btn" id="add-group-btn">+ Ajouter un <?= $aproposKey === 'contacts' ? 'contact' : 'groupe de crédit' ?></button>
|
||||
|
||||
<div class="admin-form-footer">
|
||||
<button type="submit" class="admin-btn">Enregistrer</button>
|
||||
<a href="/admin/contenus.php" class="admin-btn-secondary admin-cancel-link">Annuler</a>
|
||||
</div>
|
||||
|
||||
<template id="entry-template-<?= $aproposKey ?>">
|
||||
<div class="apropos-entry">
|
||||
<label>Entrée :</label>
|
||||
<input type="text" name="groups[{{gi}}][entries][{{ei}}][text]">
|
||||
<?php if ($aproposKey === 'contacts'): ?>
|
||||
<label>Email :</label>
|
||||
<input type="email" name="groups[{{gi}}][entries][{{ei}}][email]">
|
||||
<?php endif; ?>
|
||||
<label>Lien (optionnel) :</label>
|
||||
<input type="url" name="groups[{{gi}}][entries][{{ei}}][url]">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="group-template-<?= $aproposKey ?>">
|
||||
<fieldset class="apropos-group">
|
||||
<legend><?= htmlspecialchars($aproposKey === 'contacts' ? 'Contact' : 'Crédit') ?> {{gi}}</legend>
|
||||
<?php if ($aproposKey === 'contacts'): ?>
|
||||
<label>Rôle :</label>
|
||||
<input type="text" name="groups[{{gi}}][role]">
|
||||
<?php else: ?>
|
||||
<label>Label :</label>
|
||||
<input type="text" name="groups[{{gi}}][label]">
|
||||
<?php endif; ?>
|
||||
<button type="button" class="admin-btn admin-btn--sm add-entry-btn" data-group="{{gi}}">+ Ajouter une entrée</button>
|
||||
</fieldset>
|
||||
</template>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const aproposKey = '<?= $aproposKey ?>';
|
||||
let groupCount = <?= count($groups) ?>;
|
||||
const entryTpl = document.getElementById('entry-template-' + aproposKey).innerHTML;
|
||||
const groupTpl = document.getElementById('group-template-' + aproposKey).innerHTML;
|
||||
|
||||
// Add entry to a group
|
||||
document.querySelectorAll('.add-entry-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const gi = parseInt(this.dataset.group);
|
||||
const fieldset = this.closest('fieldset');
|
||||
const entryCount = fieldset.querySelectorAll('.apropos-entry').length;
|
||||
const html = entryTpl.replaceAll('{{gi}}', gi).replaceAll('{{ei}}', entryCount);
|
||||
this.insertAdjacentHTML('beforebegin', html);
|
||||
});
|
||||
});
|
||||
|
||||
// Add new group
|
||||
document.getElementById('add-group-btn').addEventListener('click', function() {
|
||||
groupCount++;
|
||||
const html = groupTpl.replaceAll('{{gi}}', groupCount);
|
||||
this.insertAdjacentHTML('beforebegin', html);
|
||||
|
||||
// Re-bind add-entry buttons for the new group
|
||||
const newGroup = this.previousElementSibling;
|
||||
if (newGroup && newGroup.classList.contains('apropos-group')) {
|
||||
const btn = newGroup.querySelector('.add-entry-btn');
|
||||
if (btn) {
|
||||
btn.dataset.group = groupCount;
|
||||
btn.addEventListener('click', function() {
|
||||
const gi = parseInt(this.dataset.group);
|
||||
const fieldset = this.closest('fieldset');
|
||||
const entryCount = fieldset.querySelectorAll('.apropos-entry').length;
|
||||
const html = entryTpl.replaceAll('{{gi}}', gi).replaceAll('{{ei}}', entryCount);
|
||||
this.insertAdjacentHTML('beforebegin', html);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
<?php require_once APP_ROOT . "/templates/admin/footer.php"; ?>
|
||||
include APP_ROOT . "/templates/header.php";
|
||||
include APP_ROOT . '/templates/admin/contenus-edit.php';
|
||||
require_once APP_ROOT . "/templates/admin/footer.php";
|
||||
|
||||
@@ -14,72 +14,9 @@ try {
|
||||
error_log("Error loading contenus: " . $e->getMessage());
|
||||
die("Erreur lors du chargement des contenus.");
|
||||
}
|
||||
?>
|
||||
<?php $isAdmin = true; $bodyClass = 'admin-body'; require_once APP_ROOT . '/templates/head.php'; ?>
|
||||
<?php include APP_ROOT . '/templates/header.php'; ?>
|
||||
|
||||
<main id="main-content">
|
||||
<h1>Contenus</h1>
|
||||
|
||||
|
||||
|
||||
<h2>Pages statiques</h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Slug</th>
|
||||
<th scope="col">Titre</th>
|
||||
<th scope="col">Mis à jour</th>
|
||||
<th scope="col">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($pages as $p): ?>
|
||||
<tr>
|
||||
<td><code><?= htmlspecialchars($p['slug']) ?></code></td>
|
||||
<td><?= htmlspecialchars($p['title']) ?></td>
|
||||
<td><?= htmlspecialchars($p['updated_at'] ?? '—') ?></td>
|
||||
<td>
|
||||
<a href="/admin/contenus-edit.php?slug=<?= urlencode($p['slug']) ?>"
|
||||
class="admin-btn admin-btn--sm">Éditer</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2 style="margin-top:2rem;">À propos</h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Clé</th>
|
||||
<th scope="col">Type</th>
|
||||
<th scope="col">Mis à jour</th>
|
||||
<th scope="col">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($aproposKeys as $a): ?>
|
||||
<?php
|
||||
$typeLabel = match($a['key']) {
|
||||
'contacts' => 'Contacts',
|
||||
'credits' => 'Crédits',
|
||||
};
|
||||
?>
|
||||
<tr>
|
||||
<td><code><?= htmlspecialchars($a['key']) ?></code></td>
|
||||
<td><?= htmlspecialchars($typeLabel) ?></td>
|
||||
<td><?= htmlspecialchars($a['updated_at'] ?? '—') ?></td>
|
||||
<td>
|
||||
<a href="/admin/contenus-edit.php?apropos=<?= urlencode($a['key']) ?>"
|
||||
class="admin-btn admin-btn--sm">Éditer</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
|
||||
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>
|
||||
$isAdmin = true; $bodyClass = 'admin-body';
|
||||
require_once APP_ROOT . '/templates/head.php';
|
||||
include APP_ROOT . '/templates/header.php';
|
||||
include APP_ROOT . '/templates/admin/contenus.php';
|
||||
require_once APP_ROOT . '/templates/admin/footer.php';
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<?php
|
||||
// Bootstrap application
|
||||
require_once __DIR__ . "/../../bootstrap.php";
|
||||
require_once __DIR__ . '/../../src/AdminAuth.php';
|
||||
|
||||
// PHP-level auth guard (defence-in-depth behind nginx Basic Auth)
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
// Generate CSRF token
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
@@ -19,154 +15,19 @@ if ($thesisId <= 0) {
|
||||
die("ID invalide");
|
||||
}
|
||||
|
||||
// WCAG 3.3.1 — consume the autofocus hint stored by the edit action on
|
||||
// validation failure.
|
||||
$autofocusField = App::consumeAutofocus();
|
||||
|
||||
try {
|
||||
$ctrl = ThesisEditController::create();
|
||||
$view = $ctrl->load($thesisId);
|
||||
extract($view); // thesis, currentLanguages, currentFormats, jury, lookup tables, pageTitle …
|
||||
extract($view);
|
||||
} catch (Exception $e) {
|
||||
error_log("Error loading edit page: " . $e->getMessage());
|
||||
die("Erreur lors du chargement: " . $e->getMessage());
|
||||
}
|
||||
?>
|
||||
<?php $isAdmin = true; $bodyClass = 'admin-body'; require_once APP_ROOT . '/templates/head.php'; ?>
|
||||
<?php include APP_ROOT . '/templates/header.php'; ?>
|
||||
|
||||
<main id="main-content">
|
||||
<h1>Modifier un TFE</h1>
|
||||
|
||||
|
||||
|
||||
<form method="post" action="/admin/actions/edit.php" class="admin-form" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="thesis_id" value="<?= $thesisId ?>">
|
||||
|
||||
<?php $name = 'auteurice'; $label = 'Auteur·ice(s) :'; $value = htmlspecialchars($thesis['authors']); $required = true; $attrs = array_merge(['autocomplete' => 'name'], $autofocusField === 'auteurice' ? ['autofocus' => true] : []); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
<?php $name = 'mail'; $label = 'Contact :'; $value = htmlspecialchars($currentAuthorEmail ?? ''); $attrs = ['autocomplete' => 'email']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
|
||||
<!-- Contact visibility -->
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-checkbox-label">
|
||||
<input type="checkbox" name="contact_public" value="1"
|
||||
<?= !empty($currentAuthorShowContact) ? 'checked' : '' ?>>
|
||||
Je veux que mon contact soit accessible à toustes depuis la plateforme xamxam
|
||||
</label>
|
||||
<small>Si cette case est cochée, le contact apparaît sur la page publique du TFE.</small>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$name = 'année'; $label = 'Année :'; $value = htmlspecialchars((string)$thesis['year']); $required = true;
|
||||
$type = 'number';
|
||||
$attrs = $autofocusField === 'année' ? ['autofocus' => true] : [];
|
||||
include APP_ROOT . '/templates/partials/form/text-field.php';
|
||||
?>
|
||||
|
||||
<?php $name = 'orientation'; $label = 'Orientation :'; $options = $orientations; $selected = $thesis['orientation']; $required = true; $placeholder = null; include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
|
||||
|
||||
<?php $name = 'ap'; $label = 'Atelier pluridisciplinaire :'; $options = $apPrograms; $selected = $thesis['ap_program']; $required = true; $placeholder = null; include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
|
||||
|
||||
<?php $name = 'finality'; $label = 'Finalité du master :'; $options = $finalityTypes; $selected = $thesis['finality_type']; $required = true; $placeholder = null; include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
|
||||
|
||||
<!-- Composition du jury -->
|
||||
<?php
|
||||
$juryPresident = null;
|
||||
$juryPromoteur = null;
|
||||
$juryPromoteurExt = 0;
|
||||
$juryLecteurs = [];
|
||||
foreach ($jury as $jm) {
|
||||
if ($jm['role'] === 'president') {
|
||||
$juryPresident = $jm['name'];
|
||||
} elseif ($jm['role'] === 'promoteur') {
|
||||
$juryPromoteur = $jm['name'];
|
||||
$juryPromoteurExt = (int)$jm['is_external'];
|
||||
} elseif ($jm['role'] === 'lecteur') {
|
||||
$juryLecteurs[] = $jm;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<?php require APP_ROOT . '/templates/partials/form/jury-fieldset.php'; ?>
|
||||
|
||||
<?php
|
||||
// Access type select: options need 'id'+'name'; description appended inline
|
||||
$accessOptions = array_map(function($at) {
|
||||
$label = $at['name'];
|
||||
if (!empty($at['description'])) {
|
||||
$label .= ' — ' . $at['description'];
|
||||
}
|
||||
return ['id' => $at['id'], 'name' => $label];
|
||||
}, $accessTypes);
|
||||
$name = 'access_type_id'; $label = 'Visibilité / Accès :'; $options = $accessOptions; $selected = $currentAccessTypeId; $placeholder = '- Non défini -';
|
||||
include APP_ROOT . '/templates/partials/form/select-field.php';
|
||||
?>
|
||||
|
||||
<!-- Context note (textarea — no text-field partial for textarea) -->
|
||||
<div>
|
||||
<label for="context_note">Note contextuelle :</label>
|
||||
<div>
|
||||
<textarea id="context_note" name="context_note"
|
||||
rows="4" maxlength="1500"><?= htmlspecialchars($currentContextNote ?? '') ?></textarea>
|
||||
<small>Visible publiquement pour les TFE Interne ou Interdit. Max 1 500 caractères.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php $name = 'license_id'; $label = 'Licence :'; $options = $licenseTypes; $selected = $currentLicenseId; $placeholder = '- Inconnue -'; include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
|
||||
|
||||
<?php $name = 'titre'; $label = 'Titre :'; $value = htmlspecialchars($thesis['title']); $required = true; $attrs = $autofocusField === 'titre' ? ['autofocus' => true] : []; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
<?php $name = 'subtitle'; $label = 'Sous-titre :'; $value = htmlspecialchars($thesis['subtitle'] ?? ''); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
|
||||
<!-- Synopsis (textarea — not covered by text-field partial) -->
|
||||
<div>
|
||||
<label for="synopsis">Synopsis :</label>
|
||||
<textarea id="synopsis" name="synopsis" rows="7" required
|
||||
<?= $autofocusField === 'synopsis' ? 'autofocus' : '' ?>><?= htmlspecialchars($thesis['synopsis'] ?? '') ?></textarea>
|
||||
</div>
|
||||
|
||||
<?php $name = 'languages'; $label = 'Langue(s) :'; $options = $languages; $checked = $currentLanguages; include APP_ROOT . '/templates/partials/form/checkbox-list.php'; ?>
|
||||
|
||||
<?php $name = 'formats'; $label = 'Format(s) :'; $options = $formatTypes; $checked = $currentFormats; include APP_ROOT . '/templates/partials/form/checkbox-list.php'; ?>
|
||||
|
||||
<?php $name = 'tag'; $label = 'Mots-clés :'; $value = htmlspecialchars($thesis['keywords'] ?? ''); $hint = 'Séparer par des virgules. Max 10.'; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
|
||||
<?php $name = 'duration_info'; $label = 'Durée / Taille :'; $value = htmlspecialchars($thesis['file_size_info'] ?? ''); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
|
||||
<?php $name = 'lien'; $label = 'Lien externe :'; $value = htmlspecialchars($thesis['baiu_link'] ?? ''); $type = 'url'; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
|
||||
<!-- Image bannière (custom: includes current banner preview + remove checkbox) -->
|
||||
<div>
|
||||
<label>Image bannière (accueil) :</label>
|
||||
<div>
|
||||
<?php if (!empty($thesis['banner_path'])): ?>
|
||||
<div class="admin-banner-preview">
|
||||
<img src="/media.php?path=<?= urlencode($thesis['banner_path']) ?>"
|
||||
alt="Bannière actuelle">
|
||||
<label class="admin-checkbox-label">
|
||||
<input type="checkbox" name="remove_banner" value="1"> Supprimer la bannière
|
||||
</label>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<input type="file" name="banner" accept="image/jpeg,image/png,image/webp">
|
||||
<small>JPG, PNG ou WEBP. Format paysage recommandé (4:1). Max 5 MB.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Publication toggle -->
|
||||
<div>
|
||||
<label>Publication :</label>
|
||||
<label class="admin-checkbox-label">
|
||||
<input type="checkbox" name="is_published" value="1"
|
||||
<?= $thesis['is_published'] ? 'checked' : '' ?>>
|
||||
Publier ce TFE sur le site public
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="admin-form-footer">
|
||||
<button type="submit" class="admin-btn">Enregistrer</button>
|
||||
<a href="/admin/thanks.php?id=<?= $thesisId ?>" class="admin-btn-secondary admin-cancel-link">Annuler</a>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>
|
||||
$isAdmin = true; $bodyClass = 'admin-body';
|
||||
require_once APP_ROOT . '/templates/head.php';
|
||||
include APP_ROOT . '/templates/header.php';
|
||||
include APP_ROOT . '/templates/admin/edit.php';
|
||||
require_once APP_ROOT . '/templates/admin/footer.php';
|
||||
|
||||
@@ -323,311 +323,10 @@ try {
|
||||
error_log("Error loading theses list: " . $e->getMessage());
|
||||
die("Erreur lors du chargement de la liste.");
|
||||
}
|
||||
?>
|
||||
<?php $isAdmin = true; $bodyClass = 'admin-body'; require_once APP_ROOT . '/templates/head.php'; ?>
|
||||
<?php include APP_ROOT . '/templates/header.php'; ?>
|
||||
|
||||
<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';
|
||||
}
|
||||
function bulkAction(action) {
|
||||
const checked = document.querySelectorAll('input[name="selected_theses[]"]:checked');
|
||||
if (!checked.length) { alert('Sélectionnez au moins un TFE.'); 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 => {
|
||||
const inp = document.createElement('input');
|
||||
inp.type = 'hidden'; inp.name = 'selected_theses[]'; inp.value = cb.value;
|
||||
container.appendChild(inp);
|
||||
});
|
||||
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));
|
||||
});
|
||||
</script>
|
||||
$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';
|
||||
|
||||
<main id="main-content">
|
||||
<!-- Title + filters + stats + import all in one toolbar row -->
|
||||
<div class="admin-list-toolbar">
|
||||
<h1>Liste des TFE</h1>
|
||||
|
||||
<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['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="submit" class="admin-filters-btn">Filtrer</button>
|
||||
<?php if ($searchQuery || $yearFilter || $orientationFilter || $apFilter): ?>
|
||||
<button type="button" class="admin-filters-reset"
|
||||
onclick="window.location='/admin/'">✕ Réinitialiser</button>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
|
||||
<div class="admin-list-toolbar__right">
|
||||
<dl class="admin-stats">
|
||||
<div class="admin-stat">
|
||||
<dt class="admin-stat__label">Total</dt>
|
||||
<dd class="admin-stat__number"><?= $stats['total'] ?></dd>
|
||||
</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>
|
||||
<a href="/admin/add.php" class="admin-btn admin-btn--sm">Ajouter un TFE</a>
|
||||
<button type="button" class="admin-btn admin-btn--sm" id="import-dialog-btn"
|
||||
onclick="document.getElementById('import-dialog').showModal()">
|
||||
Importer un CSV
|
||||
</button>
|
||||
<a href="/admin/actions/export-csv.php" class="admin-btn admin-btn--sm">
|
||||
Exporter CSV
|
||||
</a>
|
||||
</div>
|
||||
</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="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>
|
||||
|
||||
<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/thanks.php?id=<?= $thesis['id'] ?>" class="admin-btn-sm admin-btn-view">Voir</a>
|
||||
<a href="/admin/edit.php?id=<?= $thesis['id'] ?>" class="admin-btn-sm 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="admin-btn-sm admin-btn-unpublish"
|
||||
onclick="return confirm('Retirer de la publication ?')">Dépublier</button>
|
||||
<?php else: ?>
|
||||
<input type="hidden" name="action" value="publish">
|
||||
<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>
|
||||
<?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';
|
||||
?>
|
||||
</main>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════
|
||||
IMPORT DIALOG
|
||||
══════════════════════════════════════════════════════════════ -->
|
||||
<dialog id="import-dialog" class="admin-dialog" aria-labelledby="import-dialog-title">
|
||||
<div class="admin-dialog__header">
|
||||
<h2 id="import-dialog-title">Importer une liste de TFE</h2>
|
||||
<button type="button" class="admin-dialog__close" aria-label="Fermer"
|
||||
onclick="document.getElementById('import-dialog').close()">✕</button>
|
||||
</div>
|
||||
|
||||
<?php if ($importMessage || !empty($importErrors)): ?>
|
||||
<div class="admin-import-status-card">
|
||||
<?php if (!empty($importErrors)): ?>
|
||||
<div class="toast admin-import-status-card__errors" role="alert" data-type="error">
|
||||
<strong>⚠ Erreurs :</strong>
|
||||
<ul class="admin-error-list">
|
||||
<?php foreach ($importErrors as $err): ?>
|
||||
<li><?= htmlspecialchars($err) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($importMessage): ?>
|
||||
<p class="toast admin-import-status-card__success" role="status" data-type="success">✓ <?= htmlspecialchars($importMessage) ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" class="admin-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
|
||||
<div>
|
||||
<label for="csv_file">Fichier CSV</label>
|
||||
<div class="admin-file-input">
|
||||
<input type="file" id="csv_file" name="csv_file" accept=".csv" required>
|
||||
<small class="admin-file-hint">
|
||||
Colonnes : Identifiant, Titre, Sous-titre, Auteur·ice(s), Contact, Promoteur·ice(s), Format, Année, AP, Orientation, Finalité, Mots-clés, Synopsis, Contexte, Remarques, Langue, Autorisation, License, taille, Points sur 20, lien BAIU<br>
|
||||
Quatre premières lignes ignorées — Séparateur : virgule — UTF-8
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-form-footer">
|
||||
<button type="submit" class="admin-btn">Importer</button>
|
||||
<button type="button" class="admin-btn-secondary"
|
||||
onclick="document.getElementById('import-dialog').close()">Annuler</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php if (!empty($importResults)): ?>
|
||||
<details class="admin-import-log-details">
|
||||
<summary>Logs d'importation (<?= count($importResults) ?> entrées)</summary>
|
||||
<ul class="admin-import-log">
|
||||
<?php foreach ($importResults as $r): ?>
|
||||
<li class="admin-import-log__item admin-import-log__item--<?= $r['type'] ?>"><?= htmlspecialchars($r['msg']) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
</dialog>
|
||||
|
||||
<?php if ($importMessage || !empty($importErrors)): ?>
|
||||
<script>document.getElementById('import-dialog').showModal();</script>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>
|
||||
|
||||
@@ -22,28 +22,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
|
||||
$pageTitle = 'Connexion';
|
||||
?>
|
||||
<?php $isAdmin = true; $bodyClass = 'admin-body'; require_once APP_ROOT . '/templates/head.php'; ?>
|
||||
<?php include APP_ROOT . '/templates/header.php'; ?>
|
||||
|
||||
<main id="main-content">
|
||||
<div class="admin-login-wrap">
|
||||
<div class="admin-login-box">
|
||||
<h2>Administration</h2>
|
||||
<?php if ($error): ?>
|
||||
<p class="toast" role="alert" data-type="error">⚠ <?= htmlspecialchars($error) ?></p>
|
||||
<?php endif; ?>
|
||||
<form method="post" action="/admin/login.php" class="admin-form">
|
||||
<div>
|
||||
<label for="password">Mot de passe</label>
|
||||
<input type="password" id="password" name="password" required autofocus>
|
||||
</div>
|
||||
<div class="admin-form-footer">
|
||||
<button type="submit" class="admin-btn">Se connecter</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>
|
||||
$isAdmin = true; $bodyClass = 'admin-body';
|
||||
require_once APP_ROOT . '/templates/head.php';
|
||||
include APP_ROOT . '/templates/header.php';
|
||||
include APP_ROOT . '/templates/admin/login.php';
|
||||
require_once APP_ROOT . '/templates/admin/footer.php';
|
||||
|
||||
@@ -5,313 +5,23 @@ AdminAuth::requireLogin();
|
||||
|
||||
$pageTitle = "Paramètres";
|
||||
|
||||
$hasPassword = AdminAuth::hasPassword();
|
||||
$maintenanceOn = file_exists(APP_ROOT . '/storage/maintenance.flag');
|
||||
$hasPassword = AdminAuth::hasPassword();
|
||||
$maintenanceOn = file_exists(APP_ROOT . '/storage/maintenance.flag');
|
||||
|
||||
require_once APP_ROOT . '/src/Database.php';
|
||||
require_once APP_ROOT . '/src/SmtpRelay.php';
|
||||
$db = new Database();
|
||||
$siteSettings = $db->getAllSettings();
|
||||
$stats = $db->getThesesStats();
|
||||
$smtpSettings = SmtpRelay::getSettings($db);
|
||||
$siteSettings = $db->getAllSettings();
|
||||
$stats = $db->getThesesStats();
|
||||
$smtpSettings = SmtpRelay::getSettings($db);
|
||||
$smtpConfigured = SmtpRelay::isConfigured($db);
|
||||
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
?>
|
||||
<?php $isAdmin = true; $bodyClass = 'admin-body'; require_once APP_ROOT . '/templates/head.php'; ?>
|
||||
<?php include APP_ROOT . '/templates/header.php'; ?>
|
||||
|
||||
<main id="main-content">
|
||||
<h1>Paramètres</h1>
|
||||
|
||||
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════
|
||||
MAINTENANCE
|
||||
══════════════════════════════════════════════════════════════ -->
|
||||
<section aria-labelledby="settings-maintenance-title">
|
||||
<h2 id="settings-maintenance-title">Maintenance</h2>
|
||||
|
||||
<div class="param-maintenance-row">
|
||||
<?php if ($maintenanceOn): ?>
|
||||
<p>
|
||||
<strong>⚠ Mode maintenance activé</strong> — le site public est inaccessible.
|
||||
</p>
|
||||
<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">
|
||||
<input type="hidden" name="redirect" value="/admin/parametres.php">
|
||||
<button type="submit">Désactiver la maintenance</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<p>Site public : <strong>en ligne</strong></p>
|
||||
<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">
|
||||
<input type="hidden" name="redirect" value="/admin/parametres.php">
|
||||
<button type="submit" class="param-btn-warning"
|
||||
onclick="return confirm('Mettre le site en maintenance ? Les visiteurs verront une page 503.')">
|
||||
Activer la maintenance
|
||||
</button>
|
||||
</form>
|
||||
<?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="param-btn-export"
|
||||
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>
|
||||
<p>
|
||||
Supprime définitivement tous les TFE de la base de données, y compris auteurs,
|
||||
promoteurs, tags, fichiers associés. Cette action est <strong>irréversible</strong>.
|
||||
</p>
|
||||
<form method="post" action="actions/delete.php"
|
||||
onsubmit="return confirm('⚠ Supprimer définitivement TOUS les TFE ? Cette action est IRRÉVERSIBLE.')">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="delete_all" value="1">
|
||||
<button type="submit" class="param-btn-danger">Supprimer tous les TFE (<?= $stats['total'] ?? '?' ?>)</button>
|
||||
</form>
|
||||
</fieldset>
|
||||
</section>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════
|
||||
FORMULAIRE
|
||||
══════════════════════════════════════════════════════════════ -->
|
||||
<section aria-labelledby="settings-formulaire-title">
|
||||
<h2 id="settings-formulaire-title">Formulaire</h2>
|
||||
<p>Options de visibilité disponibles dans le formulaire d'ajout de TFE.</p>
|
||||
<p class="param-note">L'option <strong>Libre</strong> ne sera activée qu'à partir de l'année académique prochaine.</p>
|
||||
|
||||
<form method="post" action="actions/settings.php" class="param-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="section" value="formulaire">
|
||||
|
||||
<fieldset>
|
||||
<legend>Types d'accès</legend>
|
||||
|
||||
<label class="param-checkbox">
|
||||
<input type="checkbox" name="access_type_interdit_enabled" value="1"
|
||||
<?= ($siteSettings['access_type_interdit_enabled'] ?? '1') === '1' ? 'checked' : '' ?>>
|
||||
<span>
|
||||
<strong>Interdit</strong><br>
|
||||
<small>TFE non disponible en physique ni sur le site</small>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="param-checkbox">
|
||||
<input type="checkbox" name="access_type_interne_enabled" value="1"
|
||||
<?= ($siteSettings['access_type_interne_enabled'] ?? '1') === '1' ? 'checked' : '' ?>>
|
||||
<span>
|
||||
<strong>Interne</strong><br>
|
||||
<small>TFE accessible uniquement sur place en physique</small>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="param-checkbox param-checkbox--disabled">
|
||||
<input type="checkbox" name="access_type_libre_enabled" value="1"
|
||||
<?= ($siteSettings['access_type_libre_enabled'] ?? '0') === '1' ? 'checked' : '' ?>>
|
||||
<span>
|
||||
<strong>Libre</strong><br>
|
||||
<small>Libre accès — disponible à partir de l'année académique prochaine</small>
|
||||
</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<button type="submit">Enregistrer</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════
|
||||
RELAY SMTP
|
||||
══════════════════════════════════════════════════════════════ -->
|
||||
<section aria-labelledby="settings-smtp-title">
|
||||
<h2 id="settings-smtp-title">Relay SMTP</h2>
|
||||
<p>
|
||||
Identifiants du serveur SMTP utilisé pour l'envoi d'e-mails
|
||||
(notifications, partage de TFE, etc.).
|
||||
</p>
|
||||
<div class="param-smtp-status">
|
||||
<?php if ($smtpConfigured): ?>
|
||||
<span class="param-badge-ok">✓ Configuré</span>
|
||||
<span><?= htmlspecialchars($smtpSettings['host']) ?>:<?= (int)$smtpSettings['port'] ?> (<?= htmlspecialchars($smtpSettings['encryption']) ?>)</span>
|
||||
<?php else: ?>
|
||||
<span class="param-badge-warn">✗ Non configuré</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<form method="post" action="actions/settings.php" class="param-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="section" value="smtp">
|
||||
|
||||
<div class="param-grid">
|
||||
<div>
|
||||
<label for="smtp_host">Hôte SMTP</label>
|
||||
<input type="text" id="smtp_host" name="smtp_host"
|
||||
value="<?= htmlspecialchars($smtpSettings['host']) ?>"
|
||||
placeholder="smtp.example.com">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="smtp_port">Port</label>
|
||||
<input type="number" id="smtp_port" name="smtp_port"
|
||||
value="<?= (int)$smtpSettings['port'] ?>"
|
||||
min="1" max="65535">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="smtp_encryption">Chiffrement</label>
|
||||
<select id="smtp_encryption" name="smtp_encryption">
|
||||
<option value="tls" <?= $smtpSettings['encryption'] === 'tls' ? 'selected' : '' ?>>TLS (STARTTLS)</option>
|
||||
<option value="ssl" <?= $smtpSettings['encryption'] === 'ssl' ? 'selected' : '' ?>>SSL (SMTPS)</option>
|
||||
<option value="none" <?= $smtpSettings['encryption'] === 'none' ? 'selected' : '' ?>>Aucun</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="smtp_username">Nom d'utilisateur</label>
|
||||
<input type="text" id="smtp_username" name="smtp_username"
|
||||
value="<?= htmlspecialchars($smtpSettings['username']) ?>">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="smtp_password">Mot de passe</label>
|
||||
<input type="password" id="smtp_password" name="smtp_password"
|
||||
value="<?= htmlspecialchars($smtpSettings['password']) ?>"
|
||||
autocomplete="new-password"
|
||||
placeholder="Laissez vide pour ne pas modifier">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<fieldset class="param-fieldset-inline">
|
||||
<legend>Expéditeur par défaut</legend>
|
||||
<div class="param-grid">
|
||||
<div>
|
||||
<label for="smtp_from_email">Adresse e-mail</label>
|
||||
<input type="email" id="smtp_from_email" name="smtp_from_email"
|
||||
value="<?= htmlspecialchars($smtpSettings['from_email']) ?>"
|
||||
placeholder="noreply@example.com">
|
||||
</div>
|
||||
<div>
|
||||
<label for="smtp_from_name">Nom d'expéditeur</label>
|
||||
<input type="text" id="smtp_from_name" name="smtp_from_name"
|
||||
value="<?= htmlspecialchars($smtpSettings['from_name']) ?>">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<button type="submit">Enregistrer</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════
|
||||
COMPTE ADMINISTRATEUR
|
||||
══════════════════════════════════════════════════════════════ -->
|
||||
<section aria-labelledby="settings-account-title">
|
||||
<h2 id="settings-account-title">Compte administrateur</h2>
|
||||
|
||||
<dl class="param-account-status">
|
||||
<div>
|
||||
<dt>Authentification PHP</dt>
|
||||
<dd><?php $badgeType = 'ok'; $badgeValue = $hasPassword; $badgeOkLabel = 'Active'; $badgeWarnLabel = 'Non configurée'; include APP_ROOT . '/templates/partials/status-badge.php'; ?></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Stockage du hash</dt>
|
||||
<dd>
|
||||
<code>site_settings (DB)</code>
|
||||
<?php $badgeType = 'ok'; $badgeValue = $hasPassword; $badgeOkLabel = 'Présent'; $badgeWarnLabel = 'Absent'; include APP_ROOT . '/templates/partials/status-badge.php'; ?>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<?php if (!$hasPassword): ?>
|
||||
<p class="param-note">
|
||||
Aucun mot de passe PHP configuré. Le formulaire ci-dessous stockera
|
||||
un hash bcrypt dans la base de données.
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="/admin/actions/account.php" class="param-form" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="redirect" value="/admin/parametres.php">
|
||||
|
||||
<?php if ($hasPassword): ?>
|
||||
<div>
|
||||
<label for="current_password">Mot de passe actuel</label>
|
||||
<input type="password" id="current_password"
|
||||
name="current_password" required autocomplete="current-password">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div>
|
||||
<label for="new_password">Nouveau mot de passe</label>
|
||||
<input type="password" id="new_password"
|
||||
name="new_password" required autocomplete="new-password"
|
||||
minlength="12">
|
||||
<small>Minimum 12 caractères.</small>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="confirm_password">Confirmer le mot de passe</label>
|
||||
<input type="password" id="confirm_password"
|
||||
name="confirm_password" required autocomplete="new-password">
|
||||
</div>
|
||||
|
||||
<button type="submit">
|
||||
<?= $hasPassword ? 'Mettre à jour le mot de passe' : 'Définir le mot de passe' ?>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Danger zone: remove credentials -->
|
||||
<?php if ($hasPassword): ?>
|
||||
<fieldset class="param-danger-zone">
|
||||
<legend>Supprimer la configuration du mot de passe PHP</legend>
|
||||
<p>
|
||||
Supprime le hash de la base de données. L'accès admin
|
||||
dépendra uniquement de l'authentification nginx Basic Auth si elle est configurée.
|
||||
</p>
|
||||
<form method="post" action="/admin/actions/account.php"
|
||||
onsubmit="return confirm('Supprimer le mot de passe PHP ? L\'accès admin ne sera protégé que par nginx Basic Auth.')">>
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="action" value="remove_credentials">
|
||||
<input type="hidden" name="redirect" value="/admin/parametres.php">
|
||||
<input type="hidden" name="current_password_remove" value="">
|
||||
<button type="submit" class="param-btn-danger">Supprimer le mot de passe</button>
|
||||
</form>
|
||||
</fieldset>
|
||||
<?php endif; ?>
|
||||
</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="admin-btn">Exporter la base de données</a>
|
||||
<button type="button" class="admin-btn-secondary"
|
||||
onclick="document.getElementById('export-db-dialog').close()">Annuler</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</main>
|
||||
|
||||
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>
|
||||
$isAdmin = true; $bodyClass = 'admin-body';
|
||||
require_once APP_ROOT . '/templates/head.php';
|
||||
include APP_ROOT . '/templates/header.php';
|
||||
include APP_ROOT . '/templates/admin/parametres.php';
|
||||
require_once APP_ROOT . '/templates/admin/footer.php';
|
||||
|
||||
@@ -23,14 +23,14 @@ if (!AdminAuth::isAuthenticated()) {
|
||||
}
|
||||
|
||||
// ── Validate inputs ────────────────────────────────────────────────────────
|
||||
$tab = $_GET['tab'] ?? 'nginx_access';
|
||||
if ($tab !== 'nginx_config' && !array_key_exists($tab, SystemController::LOG_FILES)) {
|
||||
$tab = 'nginx_access';
|
||||
$activeTab = $_GET['tab'] ?? 'nginx_access';
|
||||
if ($activeTab !== 'nginx_config' && !array_key_exists($activeTab, SystemController::LOG_FILES)) {
|
||||
$activeTab = 'nginx_access';
|
||||
}
|
||||
|
||||
$n = isset($_GET['n']) ? (int) $_GET['n'] : 100;
|
||||
if (!in_array($n, SystemController::ALLOWED_LINES, true)) {
|
||||
$n = 100;
|
||||
$selectedN = isset($_GET['n']) ? (int) $_GET['n'] : 100;
|
||||
if (!in_array($selectedN, SystemController::ALLOWED_LINES, true)) {
|
||||
$selectedN = 100;
|
||||
}
|
||||
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
@@ -42,105 +42,19 @@ $_cache = new SystemCache($_db->getPDO());
|
||||
$_controller = new SystemController($_db, $_cache);
|
||||
|
||||
// ── Render ─────────────────────────────────────────────────────────────────
|
||||
if ($tab === 'nginx_config') {
|
||||
$data = $_controller->getNginxConfigData();
|
||||
$lines = $data['lines'];
|
||||
$source = $data['source'];
|
||||
$meta = $data['meta'];
|
||||
$error = $data['error'];
|
||||
|
||||
if ($meta): ?>
|
||||
<div class="log-meta">
|
||||
<span data-label="Fichier"><?= htmlspecialchars($meta['path']) ?></span>
|
||||
<span data-label="Taille"><?= $meta['size'] ?></span>
|
||||
<span data-label="Modifié"><?= $meta['mtime'] ?></span>
|
||||
<?php if ($source === 'live'): ?>
|
||||
<span class="nginx-source-badge nginx-source-badge--live">● Config déployée</span>
|
||||
<?php else: ?>
|
||||
<span class="nginx-source-badge nginx-source-badge--local">⚠ Référence locale (config live inaccessible)</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif;
|
||||
|
||||
if ($error !== null): ?>
|
||||
<div class="log-unavailable">
|
||||
<strong>Configuration nginx non disponible</strong>
|
||||
<div class="log-unavail-path"><?= htmlspecialchars($error) ?></div>
|
||||
<?php if (php_sapi_name() === 'cli-server'): ?>
|
||||
<div class="log-unavail-dev">
|
||||
En développement, <code>/etc/nginx/sites-available/posterg</code> n'existe pas.
|
||||
La config de référence se trouve dans <code>nginx/posterg.conf</code>.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php elseif (empty($lines)): ?>
|
||||
<div class="log-empty">Le fichier de configuration est vide.</div>
|
||||
<?php else: ?>
|
||||
<div class="log-output" id="log-output" role="region" aria-label="Configuration nginx">
|
||||
<button class="log-copy-btn" id="log-copy-btn" type="button" title="Copier la configuration"
|
||||
onclick="copyLogContent(this);return false">Copier</button>
|
||||
<?php foreach ($lines as $i => $line): ?>
|
||||
<span class="log-line <?= SystemController::nginxLineClass($line) ?>"
|
||||
data-n="<?= $i + 1 ?>"><?= htmlspecialchars($line, ENT_QUOTES | ENT_SUBSTITUTE) ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif;
|
||||
if ($activeTab === 'nginx_config') {
|
||||
$nginxData = $_controller->getNginxConfigData();
|
||||
$nginxConfigLines = $nginxData['lines'];
|
||||
$nginxConfigSource = $nginxData['source'];
|
||||
$nginxConfigMeta = $nginxData['meta'];
|
||||
$nginxConfigError = $nginxData['error'];
|
||||
|
||||
include APP_ROOT . '/templates/admin/partials/system-nginx-config-panel.php';
|
||||
} else {
|
||||
// ── Log tab ────────────────────────────────────────────────────────────
|
||||
$data = $_controller->getLogData($tab, $n);
|
||||
$logLines = $data['lines'];
|
||||
$logError = $data['error'];
|
||||
$logMeta = $data['meta'];
|
||||
?>
|
||||
<div class="log-toolbar">
|
||||
<form id="lines-form" hx-get="/admin/system-fragment.php"
|
||||
hx-target="#sys-tab-panel"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#sys-tab-panel"
|
||||
hx-trigger="change"
|
||||
hx-vals='{"tab":"<?= htmlspecialchars($tab) ?>"}'>
|
||||
<label for="lines-select">Afficher</label>
|
||||
<select id="lines-select" name="n" aria-label="Nombre de lignes">
|
||||
<?php foreach (SystemController::ALLOWED_LINES as $opt): ?>
|
||||
<option value="<?= $opt ?>" <?= $opt === $n ? 'selected' : '' ?>><?= $opt ?> dernières lignes</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</form>
|
||||
<?php if ($logLines !== null && count($logLines) > 0): ?>
|
||||
<span class="log-count-badge"><?= count($logLines) ?> ligne(s)</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
$logData = $_controller->getLogData($activeTab, $selectedN);
|
||||
$logLines = $logData['lines'];
|
||||
$logError = $logData['error'];
|
||||
$logFileMeta = $logData['meta'];
|
||||
|
||||
<?php if ($logMeta): ?>
|
||||
<div class="log-meta">
|
||||
<span data-label="Fichier"><?= htmlspecialchars($logMeta['path']) ?></span>
|
||||
<span data-label="Taille"><?= $logMeta['size'] ?></span>
|
||||
<span data-label="Modifié"><?= $logMeta['mtime'] ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($logError !== null): ?>
|
||||
<div class="log-unavailable">
|
||||
<strong>Journaux non disponibles</strong>
|
||||
<div class="log-unavail-path"><?= $logError ?></div>
|
||||
<?php if (php_sapi_name() === 'cli-server'): ?>
|
||||
<div class="log-unavail-dev">
|
||||
En environnement de développement, les logs nginx ne sont pas disponibles.
|
||||
Cette page est pleinement fonctionnelle sur le serveur de production.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php elseif (empty($logLines)): ?>
|
||||
<div class="log-empty">Le fichier journal est vide.</div>
|
||||
<?php else: ?>
|
||||
<div class="log-output" id="log-output" role="log" aria-live="off" aria-label="Contenu du journal">
|
||||
<button class="log-copy-btn" id="log-copy-btn" type="button" title="Copier le contenu"
|
||||
onclick="copyLogContent(this);return false">Copier</button>
|
||||
<?php foreach ($logLines as $i => $line): ?>
|
||||
<span class="log-line <?= SystemController::logLineClass($line) ?>"
|
||||
data-n="<?= count($logLines) - $i ?>"><?= htmlspecialchars($line, ENT_QUOTES | ENT_SUBSTITUTE) ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif;
|
||||
include APP_ROOT . '/templates/admin/partials/system-log-panel.php';
|
||||
}
|
||||
|
||||
@@ -12,18 +12,17 @@ $_db = new Database();
|
||||
$_cache = new SystemCache($_db->getPDO());
|
||||
$_controller = new SystemController($_db, $_cache);
|
||||
|
||||
// ?refresh=1 force-busts all cached sections
|
||||
if (isset($_GET['refresh']) && $_GET['refresh'] === '1') {
|
||||
$_controller->invalidateAll();
|
||||
}
|
||||
|
||||
// ── Status / PHP / Disk data ──────────────────────────────────────────────────
|
||||
$statusData = $_controller->getStatusData();
|
||||
$checks = $statusData['checks'];
|
||||
$statusCached = $statusData['cached'];
|
||||
$statusData = $_controller->getStatusData();
|
||||
$checks = $statusData['checks'];
|
||||
$statusCached = $statusData['cached'];
|
||||
$statusCacheAge = $statusData['cacheAge'];
|
||||
|
||||
$phpInfo = $_controller->getPhpInfo();
|
||||
$phpInfo = $_controller->getPhpInfo();
|
||||
$diskInfo = $_controller->getDiskInfo();
|
||||
|
||||
$diskTotal = $diskInfo['total'];
|
||||
@@ -35,7 +34,7 @@ $diskColor = SystemController::diskColor($diskPct);
|
||||
// ── Active tab + line count ───────────────────────────────────────────────────
|
||||
$activeTab = $_GET['tab'] ?? 'nginx_access';
|
||||
if ($activeTab === 'status') {
|
||||
$activeTab = 'nginx_access'; // legacy redirect
|
||||
$activeTab = 'nginx_access';
|
||||
} elseif ($activeTab !== 'nginx_config' && !array_key_exists($activeTab, SystemController::LOG_FILES)) {
|
||||
$activeTab = 'nginx_access';
|
||||
}
|
||||
@@ -46,9 +45,9 @@ if (!in_array($selectedN, SystemController::ALLOWED_LINES, true)) {
|
||||
}
|
||||
|
||||
// ── Tab content data ──────────────────────────────────────────────────────────
|
||||
$logLines = null;
|
||||
$logError = null;
|
||||
$logFileMeta = null;
|
||||
$logLines = null;
|
||||
$logError = null;
|
||||
$logFileMeta = null;
|
||||
|
||||
$nginxConfigLines = null;
|
||||
$nginxConfigSource = null;
|
||||
@@ -70,289 +69,11 @@ if ($activeTab === 'nginx_config') {
|
||||
|
||||
$isAdmin = true; $bodyClass = 'admin-body';
|
||||
$extraCss = ['/assets/css/system.css'];
|
||||
// HTMX loaded once in footer; status collapse + copy via inline JS
|
||||
require_once APP_ROOT . '/templates/head.php';
|
||||
|
||||
// Restore collapsed state from cookie
|
||||
$collapsed = $_COOKIE['sys_collapsed'] ?? null;
|
||||
$statusInitiallyCollapsed = $collapsed === '1';
|
||||
?>
|
||||
<?php include APP_ROOT . '/templates/header.php'; ?>
|
||||
|
||||
|
||||
|
||||
<main id="main-content">
|
||||
<h1>Système</h1>
|
||||
|
||||
<p class="sys-refresh-note">
|
||||
Affiché le <?= date('d/m/Y à H:i:s') ?> —
|
||||
<a href="?tab=<?= htmlspecialchars($activeTab) ?>&n=<?= $selectedN ?>">Rafraîchir</a> —
|
||||
<a href="?tab=<?= htmlspecialchars($activeTab) ?>&n=<?= $selectedN ?>&refresh=1">Forcer actualisation</a>
|
||||
</p>
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════════════════
|
||||
STATUS SECTION — always visible above tabs
|
||||
════════════════════════════════════════════════════════════════════ -->
|
||||
<section class="sys-status-section" aria-label="Statut du système">
|
||||
<div class="sys-status-header">
|
||||
<h2 class="srv-section-title srv-section-title--compact">Statut
|
||||
<?php if ($statusCached && $statusCacheAge !== null): ?>
|
||||
<span class="sys-cache-badge sys-cache-badge--hit" title="Données en cache">
|
||||
⚡ Cache — il y a <?= $statusCacheAge ?>s
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<span class="sys-cache-badge sys-cache-badge--miss" title="Données fraîches">
|
||||
⟳ Actualisé
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</h2>
|
||||
<button id="sys-status-toggle" class="sys-status-toggle"
|
||||
aria-expanded="<?= $statusInitiallyCollapsed ? 'false' : 'true' ?>" aria-controls="sys-status-body"
|
||||
type="button"
|
||||
onclick="var b=document.getElementById('sys-status-body');var c=b.hidden;b.hidden=!c;this.setAttribute('aria-expanded',c);this.textContent=c?'▲ Réduire':'▼ Développer';document.cookie='sys_collapsed='+(!c)+';path=/;max-age=31536000';return false">
|
||||
<?= $statusInitiallyCollapsed ? '▼ Développer' : '▲ Réduire' ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="sys-status-body"<?= $statusInitiallyCollapsed ? ' hidden' : '' ?>>
|
||||
<div class="srv-grid">
|
||||
<?php foreach ($checks as $check): ?>
|
||||
<?php $st = $check['status'] ?? 'unknown'; ?>
|
||||
<div class="srv-card">
|
||||
<div class="srv-card__header">
|
||||
<span class="srv-card__name"><?= htmlspecialchars($check['label']) ?></span>
|
||||
<span class="<?= SystemController::statusClass($st) ?>"><?= SystemController::statusLabel($st) ?></span>
|
||||
</div>
|
||||
<?php if (!empty($check['detail'])): ?>
|
||||
<div class="srv-card__detail"><?= htmlspecialchars($check['detail']) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="sys-status-meta">
|
||||
<div>
|
||||
<h3 class="srv-section-title srv-section-title--sub">Environnement PHP</h3>
|
||||
<div class="php-grid php-grid--flush">
|
||||
<?php foreach ($phpInfo as $key => $val): ?>
|
||||
<div class="php-item">
|
||||
<div class="php-item__key"><?= htmlspecialchars($key) ?></div>
|
||||
<div class="php-item__val"><?= htmlspecialchars($val) ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="srv-section-title srv-section-title--sub">Espace disque</h3>
|
||||
<div class="disk-bar-wrap">
|
||||
<div class="disk-bar" style="--disk-pct:<?= $diskPct ?>%;--disk-color:<?= $diskColor ?>"></div>
|
||||
</div>
|
||||
<div class="disk-stats">
|
||||
<span><?= SystemController::humanBytes($diskUsed) ?> utilisé (<?= $diskPct ?>%)</span>
|
||||
<span><?= SystemController::humanBytes($diskFree) ?> libre / <?= SystemController::humanBytes($diskTotal) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── Tab bar ─────────────────────────────────────────────────────── -->
|
||||
<nav class="sys-tabs" aria-label="Journaux et configuration">
|
||||
<?php foreach (SystemController::LOG_FILES as $key => $def): ?>
|
||||
<a href="?tab=<?= htmlspecialchars($key) ?>&n=<?= $selectedN ?>"
|
||||
class="sys-tab <?= $activeTab === $key ? 'active' : '' ?>"
|
||||
hx-get="/admin/system-fragment.php?tab=<?= htmlspecialchars($key) ?>&n=<?= $selectedN ?>"
|
||||
hx-target="#sys-tab-panel"
|
||||
hx-push-url="?tab=<?= htmlspecialchars($key) ?>&n=<?= $selectedN ?>"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#sys-tab-panel"
|
||||
data-tab="<?= htmlspecialchars($key) ?>"
|
||||
<?= $activeTab === $key ? 'aria-current="page"' : '' ?>>
|
||||
<?= htmlspecialchars($def['label']) ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
<a href="?tab=nginx_config"
|
||||
class="sys-tab <?= $activeTab === 'nginx_config' ? 'active' : '' ?>"
|
||||
hx-get="/admin/system-fragment.php?tab=nginx_config"
|
||||
hx-target="#sys-tab-panel"
|
||||
hx-push-url="?tab=nginx_config"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#sys-tab-panel"
|
||||
data-tab="nginx_config"
|
||||
<?= $activeTab === 'nginx_config' ? 'aria-current="page"' : '' ?>>nginx — config</a>
|
||||
</nav>
|
||||
|
||||
<!-- Tab panel — content swapped by HTMX -->
|
||||
<div id="sys-tab-panel">
|
||||
|
||||
<?php if ($activeTab === 'nginx_config'): ?>
|
||||
<!-- ════════════════════════════════════════════════════════════════════
|
||||
NGINX CONFIG PANEL
|
||||
════════════════════════════════════════════════════════════════════ -->
|
||||
|
||||
<?php if ($nginxConfigMeta): ?>
|
||||
<div class="log-meta">
|
||||
<span data-label="Fichier"><?= htmlspecialchars($nginxConfigMeta['path']) ?></span>
|
||||
<span data-label="Taille"><?= $nginxConfigMeta['size'] ?></span>
|
||||
<span data-label="Modifié"><?= $nginxConfigMeta['mtime'] ?></span>
|
||||
<?php if ($nginxConfigSource === 'live'): ?>
|
||||
<span class="nginx-source-badge nginx-source-badge--live">● Config déployée</span>
|
||||
<?php else: ?>
|
||||
<span class="nginx-source-badge nginx-source-badge--local">⚠ Référence locale (config live inaccessible)</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($nginxConfigError !== null): ?>
|
||||
<div class="log-unavailable">
|
||||
<strong>Configuration nginx non disponible</strong>
|
||||
<div class="log-unavail-path"><?= htmlspecialchars($nginxConfigError) ?></div>
|
||||
<?php if (php_sapi_name() === 'cli-server'): ?>
|
||||
<div class="log-unavail-dev">
|
||||
En développement, <code>/etc/nginx/sites-available/posterg</code> n'existe pas.
|
||||
La config de référence se trouve dans <code>nginx/posterg.conf</code>.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php elseif (empty($nginxConfigLines)): ?>
|
||||
<div class="log-empty">Le fichier de configuration est vide.</div>
|
||||
|
||||
<?php else: ?>
|
||||
<div class="log-output" id="log-output" role="region" aria-label="Configuration nginx">
|
||||
<button class="log-copy-btn" id="log-copy-btn" type="button" title="Copier la configuration"
|
||||
onclick="copyLogContent(this);return false">
|
||||
Copier
|
||||
</button>
|
||||
<?php foreach ($nginxConfigLines as $i => $line): ?>
|
||||
<span class="log-line <?= SystemController::nginxLineClass($line) ?>"
|
||||
data-n="<?= $i + 1 ?>"><?= htmlspecialchars($line, ENT_QUOTES | ENT_SUBSTITUTE) ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php else: ?>
|
||||
<!-- ════════════════════════════════════════════════════════════════════
|
||||
LOG PANEL
|
||||
════════════════════════════════════════════════════════════════════ -->
|
||||
|
||||
<!-- Lines selector (submits via JS on change; no button needed) -->
|
||||
<div class="log-toolbar">
|
||||
<label for="lines-select">Afficher</label>
|
||||
<form id="lines-form" hx-get="/admin/system-fragment.php"
|
||||
hx-target="#sys-tab-panel"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#sys-tab-panel"
|
||||
hx-trigger="change"
|
||||
hx-vals='{"tab":"<?= htmlspecialchars($activeTab) ?>"}'>
|
||||
<label for="lines-select">Afficher</label>
|
||||
<select id="lines-select" name="n" aria-label="Nombre de lignes">
|
||||
<?php foreach (SystemController::ALLOWED_LINES as $n): ?>
|
||||
<option value="<?= $n ?>" <?= $n === $selectedN ? 'selected' : '' ?>>
|
||||
<?= $n ?> dernières lignes
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</form>
|
||||
<?php if ($logLines !== null && count($logLines) > 0): ?>
|
||||
<span class="log-count-badge"><?= count($logLines) ?> ligne(s)</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- File metadata -->
|
||||
<?php if ($logFileMeta): ?>
|
||||
<div class="log-meta">
|
||||
<span data-label="Fichier"><?= htmlspecialchars(SystemController::LOG_FILES[$activeTab]['path']) ?></span>
|
||||
<span data-label="Taille"><?= $logFileMeta['size'] ?></span>
|
||||
<span data-label="Modifié"><?= $logFileMeta['mtime'] ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Log output -->
|
||||
<?php if ($logError !== null): ?>
|
||||
<div class="log-unavailable">
|
||||
<strong>Journaux non disponibles</strong>
|
||||
<div class="log-unavail-path"><?= $logError ?></div>
|
||||
<?php if (php_sapi_name() === 'cli-server'): ?>
|
||||
<div class="log-unavail-dev">
|
||||
En environnement de développement, les logs nginx ne sont pas disponibles.
|
||||
Cette page est pleinement fonctionnelle sur le serveur de production.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php elseif (empty($logLines)): ?>
|
||||
<div class="log-empty">Le fichier journal est vide.</div>
|
||||
|
||||
<?php else: ?>
|
||||
<div class="log-output" id="log-output" role="log" aria-live="off" aria-label="Contenu du journal">
|
||||
<button class="log-copy-btn" id="log-copy-btn" type="button" title="Copier le contenu"
|
||||
onclick="copyLogContent(this);return false">
|
||||
Copier
|
||||
</button>
|
||||
<?php foreach ($logLines as $i => $line): ?>
|
||||
<span class="log-line <?= SystemController::logLineClass($line) ?>"
|
||||
data-n="<?= count($logLines) - $i ?>"><?= htmlspecialchars($line, ENT_QUOTES | ENT_SUBSTITUTE) ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
</div><!-- #sys-tab-panel -->
|
||||
|
||||
</main>
|
||||
|
||||
<script>
|
||||
function copyLogContent(btn) {
|
||||
var logOut = document.querySelector('#log-output');
|
||||
if (!logOut) return;
|
||||
var text = Array.from(logOut.querySelectorAll('.log-line'))
|
||||
.map(function(el){ return el.textContent; }).join('\n');
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(function(){
|
||||
btn.textContent = '\u2713 Copi\u00e9';
|
||||
btn.classList.add('copied');
|
||||
setTimeout(function(){ btn.textContent = 'Copier'; btn.classList.remove('copied'); }, 2000);
|
||||
});
|
||||
} else {
|
||||
fallbackCopy(text, btn);
|
||||
}
|
||||
}
|
||||
function fallbackCopy(text, btn) {
|
||||
var ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
ta.style.cssText = 'position:fixed;opacity:0';
|
||||
document.body.appendChild(ta); ta.select();
|
||||
try { document.execCommand('copy'); btn.textContent = '\u2713 Copi\u00e9'; btn.classList.add('copied');
|
||||
setTimeout(function(){ btn.textContent = 'Copier'; btn.classList.remove('copied'); }, 2000);
|
||||
} catch(e) {}
|
||||
document.body.removeChild(ta);
|
||||
}
|
||||
// Update active tab class after each HTMX swap on #sys-tab-panel
|
||||
document.body.addEventListener('htmx:afterSwap', function(evt) {
|
||||
if (evt.detail.target && evt.detail.target.id === 'sys-tab-panel') {
|
||||
var rc = evt.detail.requestConfig;
|
||||
var tab = null;
|
||||
// Tab clicks carry ?tab=… in the path
|
||||
var qIdx = rc.path.indexOf('?');
|
||||
if (qIdx !== -1) {
|
||||
tab = new URLSearchParams(rc.path.substring(qIdx + 1)).get('tab');
|
||||
}
|
||||
// Line-count form sends tab via hx-vals in parameters
|
||||
if (!tab && rc.parameters && rc.parameters.tab) {
|
||||
tab = rc.parameters.tab;
|
||||
}
|
||||
if (tab) {
|
||||
document.querySelectorAll('.sys-tabs .sys-tab').forEach(function(a) {
|
||||
var isActive = a.getAttribute('data-tab') === tab;
|
||||
a.classList.toggle('active', isActive);
|
||||
if (isActive) a.setAttribute('aria-current', 'page');
|
||||
else a.removeAttribute('aria-current');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>
|
||||
include APP_ROOT . '/templates/header.php';
|
||||
include APP_ROOT . '/templates/admin/system.php';
|
||||
require_once APP_ROOT . '/templates/admin/footer.php';
|
||||
|
||||
@@ -18,74 +18,8 @@ try {
|
||||
die("Erreur : " . htmlspecialchars($e->getMessage()));
|
||||
}
|
||||
|
||||
// Flash messages are consumed by the flash-messages partial below.
|
||||
?>
|
||||
<?php $isAdmin = true; $bodyClass = 'admin-body'; require_once APP_ROOT . '/templates/head.php'; ?>
|
||||
<?php include APP_ROOT . '/templates/header.php'; ?>
|
||||
|
||||
<main id="main-content">
|
||||
<h1>Mots-clés (<?= count($tags) ?>)</h1>
|
||||
|
||||
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Nom</th>
|
||||
<th scope="col">TFE associés</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($tags as $tag): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($tag['name']) ?></td>
|
||||
<td class="admin-tags-count"><?= (int)$tag['thesis_count'] ?></td>
|
||||
<td>
|
||||
<!-- Rename -->
|
||||
<form method="post" action="actions/tag.php" class="admin-inline-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="action" value="rename">
|
||||
<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="admin-btn admin-btn--sm">Renommer</button>
|
||||
</form>
|
||||
|
||||
<!-- Merge into another tag -->
|
||||
<form method="post" action="actions/tag.php" class="admin-inline-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="action" value="merge">
|
||||
<input type="hidden" name="source_id" value="<?= (int)$tag['id'] ?>">
|
||||
<select name="target_id" class="admin-select--inline" required>
|
||||
<option value="">— Fusionner dans… —</option>
|
||||
<?php foreach ($tags as $other): ?>
|
||||
<?php if ($other['id'] !== $tag['id']): ?>
|
||||
<option value="<?= (int)$other['id'] ?>"><?= htmlspecialchars($other['name']) ?></option>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="submit" class="admin-btn admin-btn--sm admin-btn--warning"
|
||||
onclick="return confirm('Fusionner ce tag dans la cible ? Le tag source sera supprimé.')">
|
||||
Fusionner
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Delete -->
|
||||
<form method="post" action="actions/tag.php" class="admin-inline-form">
|
||||
<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="submit" class="admin-btn admin-btn--sm admin-btn--danger"
|
||||
onclick="return confirm('Supprimer le tag « <?= htmlspecialchars(addslashes($tag['name'])) ?> » ? Cette action est irréversible.')">
|
||||
Supprimer
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
|
||||
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>
|
||||
$isAdmin = true; $bodyClass = 'admin-body';
|
||||
require_once APP_ROOT . '/templates/head.php';
|
||||
include APP_ROOT . '/templates/header.php';
|
||||
include APP_ROOT . '/templates/admin/tags.php';
|
||||
require_once APP_ROOT . '/templates/admin/footer.php';
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<?php
|
||||
// Bootstrap application
|
||||
require_once __DIR__ . "/../../bootstrap.php";
|
||||
require_once __DIR__ . '/../../src/AdminAuth.php';
|
||||
|
||||
// Configure error reporting
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('log_errors', 1);
|
||||
ini_set('error_log', 'error.log');
|
||||
@@ -15,7 +13,6 @@ if (!$studentMode) {
|
||||
|
||||
require_once __DIR__ . '/../../src/Database.php';
|
||||
|
||||
// Security: Validate thesis ID parameter
|
||||
$thesisId = null;
|
||||
$thesis = null;
|
||||
$files = [];
|
||||
@@ -27,8 +24,6 @@ if (isset($_GET['id'])) {
|
||||
if ($thesisId !== false && $thesisId > 0) {
|
||||
try {
|
||||
$db = new Database();
|
||||
|
||||
// Get thesis data
|
||||
$thesis = $db->getThesis($thesisId);
|
||||
|
||||
if (!$thesis) {
|
||||
@@ -48,7 +43,6 @@ if (isset($_GET['id'])) {
|
||||
$error = "Aucun identifiant spécifié.";
|
||||
}
|
||||
|
||||
// Helper function to format file size
|
||||
function formatFileSize($bytes) {
|
||||
if ($bytes >= 1073741824) {
|
||||
return number_format($bytes / 1073741824, 2) . ' GB';
|
||||
@@ -61,135 +55,12 @@ function formatFileSize($bytes) {
|
||||
}
|
||||
}
|
||||
|
||||
// Set page title for header
|
||||
$pageTitle = "Récapitulatif TFE";
|
||||
?>
|
||||
<?php
|
||||
$isAdmin = true;
|
||||
if ($studentMode) {
|
||||
$bodyClass = 'admin-body student-body';
|
||||
require_once APP_ROOT . '/templates/head.php';
|
||||
} else {
|
||||
$bodyClass = 'admin-body';
|
||||
require_once APP_ROOT . '/templates/head.php';
|
||||
$bodyClass = $studentMode ? 'admin-body student-body' : 'admin-body';
|
||||
require_once APP_ROOT . '/templates/head.php';
|
||||
if (!$studentMode) {
|
||||
include APP_ROOT . '/templates/header.php';
|
||||
}
|
||||
?>
|
||||
|
||||
<main id="main-content">
|
||||
<?php if ($studentMode): ?>
|
||||
<!-- ═══════════════════ STUDENT MODE: Thank you page ═══════════════════ -->
|
||||
<div class="thanks-student-page">
|
||||
<?php if ($error): ?>
|
||||
<div class="thanks-error">
|
||||
<h1>⚠ Oups…</h1>
|
||||
<p><?= htmlspecialchars($error) ?></p>
|
||||
<a href="/admin/add.php?mode=student" class="btn-new-form">← Retour au formulaire</a>
|
||||
</div>
|
||||
|
||||
<?php elseif ($thesis): ?>
|
||||
<div class="thanks-success">
|
||||
<h1>Merci 🎉</h1>
|
||||
<p class="thanks-message">
|
||||
Ton TFE <strong><?= htmlspecialchars($thesis['title']) ?></strong> a bien été soumis.
|
||||
</p>
|
||||
<a href="/admin/add.php?mode=student" class="btn-new-form">+ Ajouter un nouveau TFE</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="thanks-error">
|
||||
<h1>Erreur</h1>
|
||||
<p>Aucune donnée à afficher.</p>
|
||||
<a href="/admin/add.php?mode=student" class="btn-new-form">← Retour au formulaire</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
<!-- ═══════════════════ ADMIN MODE: Recap page ═══════════════════ -->
|
||||
<h1>Récapitulatif TFE</h1>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<p class="toast" role="alert" data-type="error">⚠ <?= htmlspecialchars($error) ?></p>
|
||||
<p><a href="/admin/add.php" class="admin-btn-secondary">Retour au formulaire</a></p>
|
||||
|
||||
<?php elseif ($thesis): ?>
|
||||
<section>
|
||||
<h2>Informations de base</h2>
|
||||
<dl>
|
||||
<dt>Identifiant</dt><dd><?= htmlspecialchars($thesis['identifier']) ?></dd>
|
||||
<dt>Titre</dt><dd><?= htmlspecialchars($thesis['title']) ?></dd>
|
||||
<?php if ($thesis['subtitle']): ?>
|
||||
<dt>Sous-titre</dt><dd><?= htmlspecialchars($thesis['subtitle']) ?></dd>
|
||||
<?php endif; ?>
|
||||
<dt>Auteur·ice(s)</dt><dd><?= htmlspecialchars($thesis['authors']) ?></dd>
|
||||
<dt>Année</dt><dd><?= htmlspecialchars($thesis['year']) ?></dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Détails académiques</h2>
|
||||
<dl>
|
||||
<dt>Orientation</dt><dd><?= htmlspecialchars($thesis['orientation'] ?? '–') ?></dd>
|
||||
<dt>Atelier pratique</dt><dd><?= htmlspecialchars($thesis['ap_program'] ?? '–') ?></dd>
|
||||
<dt>Finalité</dt><dd><?= htmlspecialchars($thesis['finality_type'] ?? '–') ?></dd>
|
||||
<?php if ($thesis['supervisors']): ?>
|
||||
<dt>Promoteur·ice(s)</dt><dd><?= htmlspecialchars($thesis['supervisors']) ?></dd>
|
||||
<?php endif; ?>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Contenu</h2>
|
||||
<dl>
|
||||
<?php if ($thesis['languages']): ?>
|
||||
<dt>Langue(s)</dt><dd><?= htmlspecialchars($thesis['languages']) ?></dd>
|
||||
<?php endif; ?>
|
||||
<?php if ($thesis['formats']): ?>
|
||||
<dt>Format(s)</dt><dd><?= htmlspecialchars($thesis['formats']) ?></dd>
|
||||
<?php endif; ?>
|
||||
<?php if ($thesis['keywords']): ?>
|
||||
<dt>Mots-clés</dt><dd><?= htmlspecialchars($thesis['keywords']) ?></dd>
|
||||
<?php endif; ?>
|
||||
<?php if ($thesis['file_size_info']): ?>
|
||||
<dt>Durée / Taille</dt><dd><?= htmlspecialchars($thesis['file_size_info']) ?></dd>
|
||||
<?php endif; ?>
|
||||
<?php if ($thesis['baiu_link']): ?>
|
||||
<dt>Lien</dt><dd><a href="<?= htmlspecialchars($thesis['baiu_link']) ?>" target="_blank" rel="noopener"><?= htmlspecialchars($thesis['baiu_link']) ?></a></dd>
|
||||
<?php endif; ?>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<?php if (!empty($files)): ?>
|
||||
<section>
|
||||
<h2>Fichiers</h2>
|
||||
<table>
|
||||
<thead><tr><th scope="col">Type</th><th scope="col">Fichier</th><th scope="col">Taille</th><th scope="col">Date</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($files as $f): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($f['file_type']) ?></td>
|
||||
<td><?= htmlspecialchars($f['file_name']) ?></td>
|
||||
<td><?= formatFileSize($f['file_size']) ?></td>
|
||||
<td><?= date('d/m/Y H:i', strtotime($f['uploaded_at'])) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="admin-action-bar">
|
||||
<a href="/admin/edit.php?id=<?= $thesisId ?>" class="admin-btn">Modifier</a>
|
||||
<a href="/admin/add.php" class="admin-btn-secondary">Ajouter un autre TFE</a>
|
||||
<a href="/admin/" class="admin-btn-secondary">Retour à la liste</a>
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
<p class="admin-muted">Aucune donnée à afficher.</p>
|
||||
<p><a href="/admin/add.php" class="admin-btn-secondary">Retour au formulaire</a></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
|
||||
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>
|
||||
include APP_ROOT . '/templates/admin/thanks.php';
|
||||
require_once APP_ROOT . '/templates/admin/footer.php';
|
||||
|
||||
@@ -17,16 +17,5 @@ if (!$flash['error'] && !$flash['success']) {
|
||||
http_response_code(204);
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
<?php if ($flash['error']): ?>
|
||||
<p class="toast toast--error" role="alert">
|
||||
<span class="toast__icon" aria-hidden="true">⚠</span>
|
||||
<?= htmlspecialchars($flash['error']) ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<?php if ($flash['success']): ?>
|
||||
<p class="toast toast--success" role="status">
|
||||
<span class="toast__icon" aria-hidden="true">✓</span>
|
||||
<?= htmlspecialchars($flash['success']) ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
include APP_ROOT . '/templates/admin/partials/toast.php';
|
||||
|
||||
@@ -335,18 +335,17 @@ label:has(+ div > input:required)::after {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
/* ── Toast messages (bottom-center floating) ─────────────────────────── */
|
||||
#toast-container {
|
||||
/* ── Toast messages (top-right, CSS-only auto-fade) ─────────────────── */
|
||||
#toast-region {
|
||||
position: fixed;
|
||||
bottom: var(--space-l);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: var(--space-l);
|
||||
right: var(--space-l);
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
pointer-events: none;
|
||||
max-width: calc(100vw - 2 * var(--space-l));
|
||||
max-width: min(480px, calc(100vw - 2 * var(--space-l)));
|
||||
}
|
||||
|
||||
.toast {
|
||||
@@ -356,35 +355,32 @@ label:has(+ div > input:required)::after {
|
||||
border-left: 3px solid;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
pointer-events: auto;
|
||||
animation: toast-enter 0.35s ease-out forwards;
|
||||
max-width: 480px;
|
||||
backdrop-filter: blur(6px);
|
||||
/* enter then fade out — total visible ~4.35 s */
|
||||
animation: toast-enter 0.35s ease-out,
|
||||
toast-exit 0.4s ease-in 4s forwards;
|
||||
}
|
||||
|
||||
.toast[data-type="error"] {
|
||||
.toast--error {
|
||||
background: var(--accent-muted);
|
||||
border-color: var(--error);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.toast[data-type="success"] {
|
||||
.toast--success {
|
||||
background: var(--success-muted-bg);
|
||||
border-color: var(--success);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.toast-exit {
|
||||
animation: toast-exit 0.3s ease-in forwards;
|
||||
}
|
||||
|
||||
@keyframes toast-enter {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
from { opacity: 0; transform: translateY(-12px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes toast-exit {
|
||||
from { opacity: 1; transform: translateY(0); }
|
||||
to { opacity: 0; transform: translateY(20px); height: 0; padding: 0; margin: 0; overflow: hidden; }
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; pointer-events: none; }
|
||||
}
|
||||
|
||||
/* ── Stats cards ────────────────────────────────────────────────────────── */
|
||||
|
||||
Binary file not shown.
183
app/templates/admin/acces-etudiante.php
Normal file
183
app/templates/admin/acces-etudiante.php
Normal file
@@ -0,0 +1,183 @@
|
||||
<main id="main-content">
|
||||
|
||||
<div class="admin-list-toolbar">
|
||||
<h1>Accès étudiant·e</h1>
|
||||
<div class="admin-list-toolbar__right">
|
||||
<button type="button" class="admin-btn admin-btn--sm" id="open-create-dialog">
|
||||
+ Créer un lien
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (empty($links)): ?>
|
||||
<p class="admin-empty">Aucun lien d'accès créé. Cliquez sur « Créer un lien » pour générer un lien partageable.</p>
|
||||
<?php else: ?>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Lien</th>
|
||||
<th scope="col">Statut</th>
|
||||
<th scope="col">Mot de passe</th>
|
||||
<th scope="col">Utilisations</th>
|
||||
<th scope="col">Expiration</th>
|
||||
<th scope="col">Créé le</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($links as $link): ?>
|
||||
<?php
|
||||
$isExpired = $link['expires_at'] !== null && strtotime($link['expires_at']) < time();
|
||||
$isActive = (bool)$link['is_active'] && !$isExpired;
|
||||
$statusLabel = $isExpired ? 'Expiré' : ($link['is_active'] ? 'Actif' : 'Désactivé');
|
||||
if ($isExpired) {
|
||||
$statusClass = 'status-badge status-pending';
|
||||
} elseif ($link['is_active']) {
|
||||
$statusClass = 'status-badge status-published';
|
||||
} else {
|
||||
$statusClass = 'status-badge';
|
||||
$statusClass .= ' style="background:var(--error-muted-bg);color:var(--error);"';
|
||||
}
|
||||
$fullUrl = $baseUrl . '/partage/' . htmlspecialchars($link['slug']);
|
||||
$created = date('d/m/Y H:i', strtotime($link['created_at']));
|
||||
$expires = $link['expires_at'] ? date('d/m/Y', strtotime($link['expires_at'])) : '—';
|
||||
$hasPassword = !empty($link['password_hash']);
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<code style="font-size:var(--step--2);color:var(--text-secondary);"><?= htmlspecialchars($link['slug']) ?></code>
|
||||
<input type="hidden" id="url-<?= $link['id'] ?>" value="<?= $fullUrl ?>">
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($isExpired): ?>
|
||||
<span class="status-badge status-pending"><?= $statusLabel ?></span>
|
||||
<?php elseif ($link['is_active']): ?>
|
||||
<span class="status-badge status-published"><?= $statusLabel ?></span>
|
||||
<?php else: ?>
|
||||
<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 style="text-align:center;"><?= intval($link['usage_count']) ?></td>
|
||||
<td><?= $expires ?></td>
|
||||
<td><?= $created ?></td>
|
||||
<td>
|
||||
<div class="admin-actions">
|
||||
<a href="/partage/<?= urlencode($link['slug']) ?>" target="_blank" rel="noopener"
|
||||
class="admin-btn-sm admin-btn-visit" title="Visiter le formulaire">
|
||||
👁 Visiter
|
||||
</a>
|
||||
<button type="button" class="admin-btn-sm admin-btn-view"
|
||||
onclick="copyUrl(<?= $link['id'] ?>)" title="Copier l'URL">
|
||||
Copier
|
||||
</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="admin-btn-sm <?= $link['is_active'] ? 'admin-btn-unpublish' : 'admin-btn-publish' ?>"
|
||||
title="<?= $link['is_active'] ? 'Désactiver' : 'Activer' ?>">
|
||||
<?= $link['is_active'] ? '⏸' : '▶' ?>
|
||||
</button>
|
||||
</form>
|
||||
<button type="button" class="admin-btn-sm admin-btn-edit"
|
||||
onclick="openPasswordDialog(<?= $link['id'] ?>, <?= $hasPassword ? 'true' : 'false' ?>)"
|
||||
title="Modifier le mot de passe">
|
||||
🔑
|
||||
</button>
|
||||
<form method="post" action="actions/acces-etudiante.php" class="publish-form"
|
||||
onsubmit="return confirm('Supprimer ce lien ? Les soumissions via ce lien seront bloquées.')">
|
||||
<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="submit" class="admin-btn-sm admin-btn-delete" title="Supprimer">
|
||||
🗑
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
|
||||
<!-- ═══════════════════════ CREATE DIALOG ═══════════════════════ -->
|
||||
<dialog id="create-dialog" class="admin-dialog" aria-labelledby="create-dialog-title">
|
||||
<div class="admin-dialog__header">
|
||||
<h2 id="create-dialog-title">Créer un lien d'accès</h2>
|
||||
<button type="button" class="admin-dialog__close" aria-label="Fermer"
|
||||
onclick="document.getElementById('create-dialog').close()">✕</button>
|
||||
</div>
|
||||
<form method="post" action="actions/acces-etudiante.php" class="admin-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="action" value="create">
|
||||
<div>
|
||||
<label for="create-password">Mot de passe (optionnel)</label>
|
||||
<input type="password" id="create-password" name="password" autocomplete="new-password">
|
||||
<small>Laissez vide pour un lien sans mot de passe.</small>
|
||||
</div>
|
||||
<div>
|
||||
<label for="create-expires">Expiration (optionnel)</label>
|
||||
<input type="datetime-local" id="create-expires" name="expires_at">
|
||||
<small>Laissez vide pour qu'il n'expire jamais.</small>
|
||||
</div>
|
||||
<div class="admin-form-footer">
|
||||
<button type="submit" class="admin-btn">Créer le lien</button>
|
||||
<button type="button" class="admin-btn-secondary"
|
||||
onclick="document.getElementById('create-dialog').close()">Annuler</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<!-- ═══════════════════════ PASSWORD DIALOG ═══════════════════════ -->
|
||||
<dialog id="password-dialog" class="admin-dialog" aria-labelledby="password-dialog-title">
|
||||
<div class="admin-dialog__header">
|
||||
<h2 id="password-dialog-title">Mot de passe</h2>
|
||||
<button type="button" class="admin-dialog__close" aria-label="Fermer"
|
||||
onclick="document.getElementById('password-dialog').close()">✕</button>
|
||||
</div>
|
||||
<form method="post" action="actions/acces-etudiante.php" class="admin-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="action" value="set_password">
|
||||
<input type="hidden" name="id" id="password-link-id" value="">
|
||||
<div>
|
||||
<label for="password-input">Nouveau mot de passe</label>
|
||||
<input type="password" id="password-input" name="password" autocomplete="new-password">
|
||||
<small>Laissez vide pour supprimer le mot de passe.</small>
|
||||
<p id="password-current-info" style="font-size:var(--step--2);color:var(--text-secondary);margin-top:var(--space-2xs);"></p>
|
||||
</div>
|
||||
<div class="admin-form-footer">
|
||||
<button type="submit" class="admin-btn">Enregistrer</button>
|
||||
<button type="button" class="admin-btn-secondary"
|
||||
onclick="document.getElementById('password-dialog').close()">Annuler</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
document.getElementById('open-create-dialog').addEventListener('click', () => {
|
||||
document.getElementById('create-dialog').showModal();
|
||||
});
|
||||
|
||||
function copyUrl(id) {
|
||||
const input = document.getElementById('url-' + id);
|
||||
navigator.clipboard.writeText(input.value).then(() => {
|
||||
const btn = event.target.closest('button');
|
||||
const orig = btn.textContent;
|
||||
btn.textContent = '✓ Copié';
|
||||
setTimeout(() => { btn.textContent = orig; }, 1200);
|
||||
});
|
||||
}
|
||||
|
||||
function openPasswordDialog(id, hasPassword) {
|
||||
document.getElementById('password-link-id').value = id;
|
||||
const info = document.getElementById('password-current-info');
|
||||
info.textContent = hasPassword
|
||||
? 'Un mot de passe est actuellement configuré. Entrez-en un nouveau ou laissez vide pour le supprimer.'
|
||||
: 'Aucun mot de passe configuré.';
|
||||
document.getElementById('password-dialog').showModal();
|
||||
}
|
||||
</script>
|
||||
86
app/templates/admin/account.php
Normal file
86
app/templates/admin/account.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<main id="main-content">
|
||||
<h1>Compte administrateur</h1>
|
||||
|
||||
<!-- Status info -->
|
||||
<dl class="admin-account-status">
|
||||
<div class="admin-account-status__row">
|
||||
<dt class="admin-account-status__label">Authentification PHP</dt>
|
||||
<dd><?php $badgeType = 'ok'; $badgeValue = $hasPassword; $badgeOkLabel = 'Active'; $badgeWarnLabel = 'Non configurée'; include APP_ROOT . '/templates/partials/status-badge.php'; ?></dd>
|
||||
</div>
|
||||
<div class="admin-account-status__row">
|
||||
<dt class="admin-account-status__label">Stockage</dt>
|
||||
<dd>
|
||||
<code class="admin-account-status__code">site_settings (DB)</code>
|
||||
<?php $badgeType = 'ok'; $badgeValue = $hasPassword; $badgeOkLabel = 'Présent'; $badgeWarnLabel = 'Absent'; include APP_ROOT . '/templates/partials/status-badge.php'; ?>
|
||||
</dd>
|
||||
</div>
|
||||
<?php if (!$hasPassword): ?>
|
||||
<p class="admin-account-status__note">
|
||||
Aucun mot de passe PHP configuré. Le formulaire ci-dessous stockera
|
||||
un hash bcrypt dans la base de données.
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</dl>
|
||||
|
||||
<!-- Password change form -->
|
||||
<h2 class="admin-section-title"><?= $hasPassword ? 'Changer le mot de passe' : 'Définir le mot de passe' ?></h2>
|
||||
|
||||
<form method="post" action="/admin/actions/account.php" class="admin-form" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
|
||||
<?php if ($hasPassword): ?>
|
||||
<div>
|
||||
<label for="current_password">Mot de passe actuel</label>
|
||||
<div>
|
||||
<input type="password" id="current_password"
|
||||
name="current_password" required autocomplete="current-password">
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div>
|
||||
<label for="new_password">Nouveau mot de passe</label>
|
||||
<div>
|
||||
<input type="password" id="new_password"
|
||||
name="new_password" required autocomplete="new-password"
|
||||
minlength="12">
|
||||
<small>Minimum 12 caractères.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="confirm_password">Confirmer le mot de passe</label>
|
||||
<div>
|
||||
<input type="password" id="confirm_password"
|
||||
name="confirm_password" required autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-form-footer">
|
||||
<button type="submit" class="admin-btn">
|
||||
<?= $hasPassword ? 'Mettre à jour le mot de passe' : 'Définir le mot de passe' ?>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php if ($hasPassword): ?>
|
||||
<!-- Danger zone: remove password -->
|
||||
<h2 class="admin-section-title admin-section-title--danger">Zone de danger</h2>
|
||||
<div class="admin-danger-zone">
|
||||
<p class="admin-danger-zone__description">
|
||||
<strong>Supprimer la configuration du mot de passe PHP</strong><br>
|
||||
<small>
|
||||
Supprime le hash de la base de données. L'accès admin
|
||||
dépendra uniquement de l'authentification nginx Basic Auth si elle est configurée.
|
||||
</small>
|
||||
</p>
|
||||
<form method="post" action="/admin/actions/account.php"
|
||||
onsubmit="return confirm('Supprimer le mot de passe PHP ? L\'accès admin ne sera protégé que par nginx Basic Auth.')">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="action" value="remove_credentials">
|
||||
<input type="hidden" name="current_password_remove" id="current_password_remove" value="">
|
||||
<button type="submit" class="admin-btn admin-btn--danger">Supprimer</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
109
app/templates/admin/add.php
Normal file
109
app/templates/admin/add.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<main id="main-content">
|
||||
<div class="thesis-add-header">
|
||||
<h1>Ajouter un TFE</h1>
|
||||
</div>
|
||||
|
||||
<p class="required-note"><span class="asterisk">*</span> Champs obligatoires</p>
|
||||
<form action="actions/formulaire.php" method="post" enctype="multipart/form-data" class="admin-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION["csrf_token"]) ?>">
|
||||
|
||||
<!-- ═══════════════════ Informations du TFE ═══════════════════ -->
|
||||
<fieldset>
|
||||
<legend>Informations du TFE</legend>
|
||||
|
||||
<?php $name = 'titre'; $label = 'Titre :'; $value = old('titre'); $required = true; $attrs = withAutofocus('titre'); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
<?php $name = 'subtitle'; $label = 'Sous-titre (si applicable) :'; $value = old('subtitle'); $required = false; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
<?php $name = 'auteurice'; $label = 'Auteur·ice(s) :'; $value = old('auteurice'); $required = true; $attrs = withAutofocus('auteurice', ['autocomplete' => 'name']); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
<?php $name = 'mail'; $label = 'Contact(s) (optionnel) [mail/site/insta/etc.] :'; $value = old('mail'); $attrs = ['autocomplete' => 'email']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-checkbox-label">
|
||||
<input type="checkbox" name="contact_public" value="1"
|
||||
<?= isset($formData['contact_public']) ? 'checked' : '' ?>>
|
||||
Je veux que mon contact soit accessible à toustes depuis la plateforme xamxam
|
||||
</label>
|
||||
<small>Si cette case est cochée, votre contact apparaîtra sur la page publique de votre TFE.</small>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="synopsis">Synopsis :</label>
|
||||
<textarea id="synopsis" name="synopsis"
|
||||
rows="7" required
|
||||
<?= $autofocusField === 'synopsis' ? 'autofocus' : '' ?>><?= old('synopsis') ?></textarea>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- ═══════════════════ Composition du jury ═══════════════════ -->
|
||||
<?php require APP_ROOT . '/templates/partials/form/jury-fieldset.php'; ?>
|
||||
|
||||
<!-- ═══════════════════ Cadre académique ═══════════════════ -->
|
||||
<fieldset>
|
||||
<legend>Cadre académique</legend>
|
||||
|
||||
<?php
|
||||
$name = 'année'; $label = 'Année :'; $value = old('année'); $required = true;
|
||||
$type = 'number';
|
||||
$placeholder = date('Y');
|
||||
$attrs = withAutofocus('année', ['min' => 2000, 'max' => date('Y') + 1]);
|
||||
include APP_ROOT . '/templates/partials/form/text-field.php';
|
||||
?>
|
||||
|
||||
<?php $name = 'orientation'; $label = 'Orientation :'; $options = $orientations; $selected = $formData['orientation'] ?? ''; $required = true; $placeholder = ''; $attrs = withAutofocus('orientation'); include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
|
||||
<?php $name = 'ap'; $label = 'Atelier pluridisciplinaire :'; $options = $apPrograms; $selected = $formData['ap'] ?? ''; $required = true; $placeholder = ''; $attrs = withAutofocus('ap'); include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
|
||||
<?php $name = 'finality'; $label = 'Finalité du master :'; $options = $finalityTypes; $selected = $formData['finality'] ?? ''; $required = true; $placeholder = ''; $attrs = withAutofocus('finality'); include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
|
||||
|
||||
<?php $name = 'languages'; $label = 'Langue(s) :'; $options = $languages; $checked = $formData['languages'] ?? []; $required = true; include APP_ROOT . '/templates/partials/form/checkbox-list.php'; ?>
|
||||
<?php $name = 'formats'; $label = 'Format(s) :'; $options = $formatTypes; $checked = $formData['formats'] ?? []; include APP_ROOT . '/templates/partials/form/checkbox-list.php'; ?>
|
||||
|
||||
<?php $name = 'tag'; $label = 'Mots-clés :'; $value = old('tag'); $placeholder = 'sociologie, anthropologie, ...'; $hint = 'Séparez par des virgules. Max 10 mots-clés.'; $attrs = withAutofocus('tag'); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
</fieldset>
|
||||
|
||||
<!-- ═══════════════════ Fichiers ═══════════════════ -->
|
||||
<fieldset>
|
||||
<legend>Fichiers</legend>
|
||||
|
||||
<?php $name = 'couverture'; $label = 'Image de couverture :'; $accept = 'image/jpeg,image/png'; $hint = 'JPG, PNG. Taille max : 10 MB.'; include APP_ROOT . '/templates/partials/form/file-field.php'; ?>
|
||||
<?php $name = 'banner'; $label = 'Image bannière (accueil) :'; $accept = 'image/jpeg,image/png,image/webp'; $hint = 'JPG, PNG ou WEBP. Format paysage recommandé (4:1). Max 5 MB.'; include APP_ROOT . '/templates/partials/form/file-field.php'; ?>
|
||||
<?php $name = 'files'; $label = 'Fichiers du TFE :'; $accept = '.pdf,.jpg,.jpeg,.png,.mp4,.zip,.vtt'; $hint = 'PDF, JPG, PNG, MP4, ZIP. Max 50 MB par fichier. Pour les vidéos, un fichier .vtt de sous-titres peut être joint (il sera associé automatiquement à la vidéo correspondante).'; $multiple = true; include APP_ROOT . '/templates/partials/form/file-field.php'; ?>
|
||||
</fieldset>
|
||||
|
||||
<!-- ═══════════════════ Métadonnées complémentaires ═══════════════════ -->
|
||||
<fieldset>
|
||||
<legend>Métadonnées complémentaires</legend>
|
||||
|
||||
<?php $name = 'license_id'; $label = 'Licence :'; $options = $licenseTypes; $selected = $formData['license_id'] ?? ''; $placeholder = '— Inconnue —'; include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
|
||||
|
||||
<?php $name = 'duration_info'; $label = 'Durée / Taille :'; $value = old('duration_info'); $placeholder = 'Ex : 84 pages'; $hint = 'Durée (minutes) ou nombre de pages.'; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
|
||||
<?php $name = 'lien'; $label = 'Lien (site / ressource) :'; $value = old('lien'); $type = 'url'; $placeholder = 'https://...'; $attrs = withAutofocus('lien'); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
|
||||
<?php
|
||||
$accessOptions = array_map(function($at) {
|
||||
return ['id' => $at['id'], 'name' => $at['name']];
|
||||
}, $enabledAccessTypes);
|
||||
$defaultAccessType = 2;
|
||||
$selectedAccessType = isset($formData['access_type_id'])
|
||||
? (int)$formData['access_type_id']
|
||||
: $defaultAccessType;
|
||||
$name = 'access_type_id';
|
||||
$label = 'Visibilité / Accès :';
|
||||
$options = $accessOptions;
|
||||
$selected = $selectedAccessType;
|
||||
$placeholder = null;
|
||||
$required = true;
|
||||
$attrs = [];
|
||||
include APP_ROOT . '/templates/partials/form/select-field.php';
|
||||
?>
|
||||
</fieldset>
|
||||
|
||||
<!-- ═══════════════════ E-mail de confirmation ═══════════ -->
|
||||
<fieldset>
|
||||
<legend>E-mail de confirmation</legend>
|
||||
<?php $name = 'confirmation_email'; $label = 'Adresse e-mail * :'; $value = old('confirmation_email'); $type = 'email'; $required = true; $placeholder = 'ton.email@exemple.be'; $hint = 'Nécessaire pour recevoir le récapitulatif de ta soumission.'; $attrs = withAutofocus('confirmation_email'); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
</fieldset>
|
||||
|
||||
<div class="form-footer">
|
||||
<button type="submit" name="go">Soumettre</button>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
145
app/templates/admin/contenus-edit.php
Normal file
145
app/templates/admin/contenus-edit.php
Normal file
@@ -0,0 +1,145 @@
|
||||
<main id="main-content">
|
||||
<h1>Éditer : <?= htmlspecialchars($editTitle) ?></h1>
|
||||
|
||||
<?php if ($editType === 'page'): ?>
|
||||
<form action="/admin/actions/page.php" method="post" class="admin-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION["csrf_token"]) ?>">
|
||||
<input type="hidden" name="slug" value="<?= htmlspecialchars($pageSlug) ?>">
|
||||
|
||||
<label for="editor">Contenu (Markdown) :</label>
|
||||
<input type="hidden" id="content" name="content"
|
||||
value="<?= htmlspecialchars($initialContent) ?>">
|
||||
<div id="editor"></div>
|
||||
|
||||
<div class="admin-form-footer">
|
||||
<button type="submit" class="admin-btn">Enregistrer</button>
|
||||
<a href="/admin/contenus.php" class="admin-btn-secondary admin-cancel-link">Annuler</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php else: ?>
|
||||
<?php
|
||||
$groups = is_array($value) ? $value : [];
|
||||
?>
|
||||
<form action="/admin/actions/apropos.php" method="post" class="admin-form" id="apropos-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION["csrf_token"]) ?>">
|
||||
<input type="hidden" name="apropos_key" value="<?= htmlspecialchars($aproposKey) ?>">
|
||||
|
||||
<?php foreach ($groups as $gi => $group): ?>
|
||||
<fieldset class="apropos-group">
|
||||
<legend><?= htmlspecialchars($aproposKey === 'contacts' ? 'Contact' : 'Crédit') ?> <?= $gi + 1 ?></legend>
|
||||
<?php if ($aproposKey === 'contacts'): ?>
|
||||
<label for="group_<?= $gi ?>_role">Rôle :</label>
|
||||
<input type="text" id="group_<?= $gi ?>_role"
|
||||
name="groups[<?= $gi ?>][role]"
|
||||
value="<?= htmlspecialchars($group['role'] ?? '') ?>">
|
||||
<?php else: ?>
|
||||
<label for="group_<?= $gi ?>_label">Label :</label>
|
||||
<input type="text" id="group_<?= $gi ?>_label"
|
||||
name="groups[<?= $gi ?>][label]"
|
||||
value="<?= htmlspecialchars($group['label'] ?? '') ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<?php $entries = is_array($group['entries'] ?? null) ? $group['entries'] : []; ?>
|
||||
<?php foreach ($entries as $ei => $entry): ?>
|
||||
<div class="apropos-entry">
|
||||
<label for="entry_<?= $gi ?>_<?= $ei ?>_text"><?= $aproposKey === 'contacts' ? 'Nom' : 'Texte' ?> :</label>
|
||||
<input type="text" id="entry_<?= $gi ?>_<?= $ei ?>_text"
|
||||
name="groups[<?= $gi ?>][entries][<?= $ei ?>][text]"
|
||||
value="<?= htmlspecialchars($entry['text'] ?? '') ?>">
|
||||
<?php if ($aproposKey === 'contacts'): ?>
|
||||
<label for="entry_<?= $gi ?>_<?= $ei ?>_email">Email :</label>
|
||||
<input type="email" id="entry_<?= $gi ?>_<?= $ei ?>_email"
|
||||
name="groups[<?= $gi ?>][entries][<?= $ei ?>][email]"
|
||||
value="<?= htmlspecialchars($entry['email'] ?? '') ?>">
|
||||
<?php endif; ?>
|
||||
<label for="entry_<?= $gi ?>_<?= $ei ?>_url">Lien (optionnel) :</label>
|
||||
<input type="url" id="entry_<?= $gi ?>_<?= $ei ?>_url"
|
||||
name="groups[<?= $gi ?>][entries][<?= $ei ?>][url]"
|
||||
value="<?= htmlspecialchars($entry['url'] ?? '') ?>">
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<button type="button" class="admin-btn admin-btn--sm add-entry-btn" data-group="<?= $gi ?>">+ Ajouter une entrée</button>
|
||||
</fieldset>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<button type="button" class="admin-btn" id="add-group-btn">+ Ajouter un <?= $aproposKey === 'contacts' ? 'contact' : 'groupe de crédit' ?></button>
|
||||
|
||||
<div class="admin-form-footer">
|
||||
<button type="submit" class="admin-btn">Enregistrer</button>
|
||||
<a href="/admin/contenus.php" class="admin-btn-secondary admin-cancel-link">Annuler</a>
|
||||
</div>
|
||||
|
||||
<template id="entry-template-<?= $aproposKey ?>">
|
||||
<div class="apropos-entry">
|
||||
<label>Entrée :</label>
|
||||
<input type="text" name="groups[{{gi}}][entries][{{ei}}][text]">
|
||||
<?php if ($aproposKey === 'contacts'): ?>
|
||||
<label>Email :</label>
|
||||
<input type="email" name="groups[{{gi}}][entries][{{ei}}][email]">
|
||||
<?php endif; ?>
|
||||
<label>Lien (optionnel) :</label>
|
||||
<input type="url" name="groups[{{gi}}][entries][{{ei}}][url]">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="group-template-<?= $aproposKey ?>">
|
||||
<fieldset class="apropos-group">
|
||||
<legend><?= htmlspecialchars($aproposKey === 'contacts' ? 'Contact' : 'Crédit') ?> {{gi}}</legend>
|
||||
<?php if ($aproposKey === 'contacts'): ?>
|
||||
<label>Rôle :</label>
|
||||
<input type="text" name="groups[{{gi}}][role]">
|
||||
<?php else: ?>
|
||||
<label>Label :</label>
|
||||
<input type="text" name="groups[{{gi}}][label]">
|
||||
<?php endif; ?>
|
||||
<button type="button" class="admin-btn admin-btn--sm add-entry-btn" data-group="{{gi}}">+ Ajouter une entrée</button>
|
||||
</fieldset>
|
||||
</template>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const aproposKey = '<?= $aproposKey ?>';
|
||||
let groupCount = <?= count($groups) ?>;
|
||||
const entryTpl = document.getElementById('entry-template-' + aproposKey).innerHTML;
|
||||
const groupTpl = document.getElementById('group-template-' + aproposKey).innerHTML;
|
||||
|
||||
// Add entry to a group
|
||||
document.querySelectorAll('.add-entry-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const gi = parseInt(this.dataset.group);
|
||||
const fieldset = this.closest('fieldset');
|
||||
const entryCount = fieldset.querySelectorAll('.apropos-entry').length;
|
||||
const html = entryTpl.replaceAll('{{gi}}', gi).replaceAll('{{ei}}', entryCount);
|
||||
this.insertAdjacentHTML('beforebegin', html);
|
||||
});
|
||||
});
|
||||
|
||||
// Add new group
|
||||
document.getElementById('add-group-btn').addEventListener('click', function() {
|
||||
groupCount++;
|
||||
const html = groupTpl.replaceAll('{{gi}}', groupCount);
|
||||
this.insertAdjacentHTML('beforebegin', html);
|
||||
|
||||
// Re-bind add-entry buttons for the new group
|
||||
const newGroup = this.previousElementSibling;
|
||||
if (newGroup && newGroup.classList.contains('apropos-group')) {
|
||||
const btn = newGroup.querySelector('.add-entry-btn');
|
||||
if (btn) {
|
||||
btn.dataset.group = groupCount;
|
||||
btn.addEventListener('click', function() {
|
||||
const gi = parseInt(this.dataset.group);
|
||||
const fieldset = this.closest('fieldset');
|
||||
const entryCount = fieldset.querySelectorAll('.apropos-entry').length;
|
||||
const html = entryTpl.replaceAll('{{gi}}', gi).replaceAll('{{ei}}', entryCount);
|
||||
this.insertAdjacentHTML('beforebegin', html);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
61
app/templates/admin/contenus.php
Normal file
61
app/templates/admin/contenus.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<main id="main-content">
|
||||
<h1>Contenus</h1>
|
||||
|
||||
<h2>Pages statiques</h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Slug</th>
|
||||
<th scope="col">Titre</th>
|
||||
<th scope="col">Mis à jour</th>
|
||||
<th scope="col">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($pages as $p): ?>
|
||||
<tr>
|
||||
<td><code><?= htmlspecialchars($p['slug']) ?></code></td>
|
||||
<td><?= htmlspecialchars($p['title']) ?></td>
|
||||
<td><?= htmlspecialchars($p['updated_at'] ?? '—') ?></td>
|
||||
<td>
|
||||
<a href="/admin/contenus-edit.php?slug=<?= urlencode($p['slug']) ?>"
|
||||
class="admin-btn admin-btn--sm">Éditer</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2 style="margin-top:2rem;">À propos</h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Clé</th>
|
||||
<th scope="col">Type</th>
|
||||
<th scope="col">Mis à jour</th>
|
||||
<th scope="col">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($aproposKeys as $a): ?>
|
||||
<?php
|
||||
$typeLabel = match($a['key']) {
|
||||
'contacts' => 'Contacts',
|
||||
'credits' => 'Crédits',
|
||||
};
|
||||
?>
|
||||
<tr>
|
||||
<td><code><?= htmlspecialchars($a['key']) ?></code></td>
|
||||
<td><?= htmlspecialchars($typeLabel) ?></td>
|
||||
<td><?= htmlspecialchars($a['updated_at'] ?? '—') ?></td>
|
||||
<td>
|
||||
<a href="/admin/contenus-edit.php?apropos=<?= urlencode($a['key']) ?>"
|
||||
class="admin-btn admin-btn--sm">Éditer</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
131
app/templates/admin/edit.php
Normal file
131
app/templates/admin/edit.php
Normal file
@@ -0,0 +1,131 @@
|
||||
<main id="main-content">
|
||||
<h1>Modifier un TFE</h1>
|
||||
|
||||
<form method="post" action="/admin/actions/edit.php" class="admin-form" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="thesis_id" value="<?= $thesisId ?>">
|
||||
|
||||
<?php $name = 'auteurice'; $label = 'Auteur·ice(s) :'; $value = htmlspecialchars($thesis['authors']); $required = true; $attrs = array_merge(['autocomplete' => 'name'], $autofocusField === 'auteurice' ? ['autofocus' => true] : []); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
<?php $name = 'mail'; $label = 'Contact :'; $value = htmlspecialchars($currentAuthorEmail ?? ''); $attrs = ['autocomplete' => 'email']; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
|
||||
<!-- Contact visibility -->
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-checkbox-label">
|
||||
<input type="checkbox" name="contact_public" value="1"
|
||||
<?= !empty($currentAuthorShowContact) ? 'checked' : '' ?>>
|
||||
Je veux que mon contact soit accessible à toustes depuis la plateforme xamxam
|
||||
</label>
|
||||
<small>Si cette case est cochée, le contact apparaît sur la page publique du TFE.</small>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$name = 'année'; $label = 'Année :'; $value = htmlspecialchars((string)$thesis['year']); $required = true;
|
||||
$type = 'number';
|
||||
$attrs = $autofocusField === 'année' ? ['autofocus' => true] : [];
|
||||
include APP_ROOT . '/templates/partials/form/text-field.php';
|
||||
?>
|
||||
|
||||
<?php $name = 'orientation'; $label = 'Orientation :'; $options = $orientations; $selected = $thesis['orientation']; $required = true; $placeholder = null; include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
|
||||
|
||||
<?php $name = 'ap'; $label = 'Atelier pluridisciplinaire :'; $options = $apPrograms; $selected = $thesis['ap_program']; $required = true; $placeholder = null; include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
|
||||
|
||||
<?php $name = 'finality'; $label = 'Finalité du master :'; $options = $finalityTypes; $selected = $thesis['finality_type']; $required = true; $placeholder = null; include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
|
||||
|
||||
<!-- Composition du jury -->
|
||||
<?php
|
||||
$juryPresident = null;
|
||||
$juryPromoteur = null;
|
||||
$juryPromoteurExt = 0;
|
||||
$juryLecteurs = [];
|
||||
foreach ($jury as $jm) {
|
||||
if ($jm['role'] === 'president') {
|
||||
$juryPresident = $jm['name'];
|
||||
} elseif ($jm['role'] === 'promoteur') {
|
||||
$juryPromoteur = $jm['name'];
|
||||
$juryPromoteurExt = (int)$jm['is_external'];
|
||||
} elseif ($jm['role'] === 'lecteur') {
|
||||
$juryLecteurs[] = $jm;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<?php require APP_ROOT . '/templates/partials/form/jury-fieldset.php'; ?>
|
||||
|
||||
<?php
|
||||
// Access type select: options need 'id'+'name'; description appended inline
|
||||
$accessOptions = array_map(function($at) {
|
||||
$label = $at['name'];
|
||||
if (!empty($at['description'])) {
|
||||
$label .= ' — ' . $at['description'];
|
||||
}
|
||||
return ['id' => $at['id'], 'name' => $label];
|
||||
}, $accessTypes);
|
||||
$name = 'access_type_id'; $label = 'Visibilité / Accès :'; $options = $accessOptions; $selected = $currentAccessTypeId; $placeholder = '- Non défini -';
|
||||
include APP_ROOT . '/templates/partials/form/select-field.php';
|
||||
?>
|
||||
|
||||
<!-- Context note (textarea — no text-field partial for textarea) -->
|
||||
<div>
|
||||
<label for="context_note">Note contextuelle :</label>
|
||||
<div>
|
||||
<textarea id="context_note" name="context_note"
|
||||
rows="4" maxlength="1500"><?= htmlspecialchars($currentContextNote ?? '') ?></textarea>
|
||||
<small>Visible publiquement pour les TFE Interne ou Interdit. Max 1 500 caractères.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php $name = 'license_id'; $label = 'Licence :'; $options = $licenseTypes; $selected = $currentLicenseId; $placeholder = '- Inconnue -'; include APP_ROOT . '/templates/partials/form/select-field.php'; ?>
|
||||
|
||||
<?php $name = 'titre'; $label = 'Titre :'; $value = htmlspecialchars($thesis['title']); $required = true; $attrs = $autofocusField === 'titre' ? ['autofocus' => true] : []; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
<?php $name = 'subtitle'; $label = 'Sous-titre :'; $value = htmlspecialchars($thesis['subtitle'] ?? ''); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
|
||||
<!-- Synopsis (textarea — not covered by text-field partial) -->
|
||||
<div>
|
||||
<label for="synopsis">Synopsis :</label>
|
||||
<textarea id="synopsis" name="synopsis" rows="7" required
|
||||
<?= $autofocusField === 'synopsis' ? 'autofocus' : '' ?>><?= htmlspecialchars($thesis['synopsis'] ?? '') ?></textarea>
|
||||
</div>
|
||||
|
||||
<?php $name = 'languages'; $label = 'Langue(s) :'; $options = $languages; $checked = $currentLanguages; include APP_ROOT . '/templates/partials/form/checkbox-list.php'; ?>
|
||||
|
||||
<?php $name = 'formats'; $label = 'Format(s) :'; $options = $formatTypes; $checked = $currentFormats; include APP_ROOT . '/templates/partials/form/checkbox-list.php'; ?>
|
||||
|
||||
<?php $name = 'tag'; $label = 'Mots-clés :'; $value = htmlspecialchars($thesis['keywords'] ?? ''); $hint = 'Séparer par des virgules. Max 10.'; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
|
||||
<?php $name = 'duration_info'; $label = 'Durée / Taille :'; $value = htmlspecialchars($thesis['file_size_info'] ?? ''); include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
|
||||
<?php $name = 'lien'; $label = 'Lien externe :'; $value = htmlspecialchars($thesis['baiu_link'] ?? ''); $type = 'url'; include APP_ROOT . '/templates/partials/form/text-field.php'; ?>
|
||||
|
||||
<!-- Image bannière (custom: includes current banner preview + remove checkbox) -->
|
||||
<div>
|
||||
<label>Image bannière (accueil) :</label>
|
||||
<div>
|
||||
<?php if (!empty($thesis['banner_path'])): ?>
|
||||
<div class="admin-banner-preview">
|
||||
<img src="/media.php?path=<?= urlencode($thesis['banner_path']) ?>"
|
||||
alt="Bannière actuelle">
|
||||
<label class="admin-checkbox-label">
|
||||
<input type="checkbox" name="remove_banner" value="1"> Supprimer la bannière
|
||||
</label>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<input type="file" name="banner" accept="image/jpeg,image/png,image/webp">
|
||||
<small>JPG, PNG ou WEBP. Format paysage recommandé (4:1). Max 5 MB.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Publication toggle -->
|
||||
<div>
|
||||
<label>Publication :</label>
|
||||
<label class="admin-checkbox-label">
|
||||
<input type="checkbox" name="is_published" value="1"
|
||||
<?= $thesis['is_published'] ? 'checked' : '' ?>>
|
||||
Publier ce TFE sur le site public
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="admin-form-footer">
|
||||
<button type="submit" class="admin-btn">Enregistrer</button>
|
||||
<a href="/admin/thanks.php?id=<?= $thesisId ?>" class="admin-btn-secondary admin-cancel-link">Annuler</a>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
@@ -1,5 +1,11 @@
|
||||
<!-- Fixed toast container (bottom-center, always visible) -->
|
||||
<div id="toast-container" aria-live="polite"></div>
|
||||
<!-- Toast region — polled by HTMX after page load -->
|
||||
<aside id="toast-region"
|
||||
aria-live="polite"
|
||||
hx-get="/admin/toast-fragment.php"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML"
|
||||
hx-target="#toast-region">
|
||||
</aside>
|
||||
|
||||
<?php foreach ($extraJs ?? [] as $js): ?>
|
||||
<script src="<?= App::assetV($js) ?>"></script>
|
||||
@@ -8,30 +14,5 @@
|
||||
<script><?= $extraJsInline ?></script>
|
||||
<?php endif; ?>
|
||||
<script src="/assets/js/htmx.min.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
var box = document.getElementById('toast-container');
|
||||
if (!box) return;
|
||||
|
||||
// 1) Flash messages from hidden #admin-toasts
|
||||
var src = document.getElementById('admin-toasts');
|
||||
if (src) {
|
||||
src.querySelectorAll('.toast').forEach(function (t) { box.appendChild(t); });
|
||||
src.remove();
|
||||
}
|
||||
|
||||
// 2) Orphaned .toast elements rendered inline by pages (login, thanks, etc.)
|
||||
document.querySelectorAll('.toast:not(#toast-container .toast)')
|
||||
.forEach(function (t) { box.appendChild(t); });
|
||||
|
||||
// Auto-dismiss every toast after 4 seconds
|
||||
box.querySelectorAll('.toast').forEach(function (toast, i) {
|
||||
setTimeout(function () {
|
||||
toast.classList.add('toast-exit');
|
||||
toast.addEventListener('animationend', function () { toast.remove(); });
|
||||
}, 4000 + (i * 200));
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
302
app/templates/admin/index.php
Normal file
302
app/templates/admin/index.php
Normal file
@@ -0,0 +1,302 @@
|
||||
<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';
|
||||
}
|
||||
function bulkAction(action) {
|
||||
const checked = document.querySelectorAll('input[name="selected_theses[]"]:checked');
|
||||
if (!checked.length) { alert('Sélectionnez au moins un TFE.'); 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 => {
|
||||
const inp = document.createElement('input');
|
||||
inp.type = 'hidden'; inp.name = 'selected_theses[]'; inp.value = cb.value;
|
||||
container.appendChild(inp);
|
||||
});
|
||||
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));
|
||||
});
|
||||
</script>
|
||||
|
||||
<main id="main-content">
|
||||
<!-- Title + filters + stats + import all in one toolbar row -->
|
||||
<div class="admin-list-toolbar">
|
||||
<h1>Liste des TFE</h1>
|
||||
|
||||
<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['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="submit" class="admin-filters-btn">Filtrer</button>
|
||||
<?php if ($searchQuery || $yearFilter || $orientationFilter || $apFilter): ?>
|
||||
<button type="button" class="admin-filters-reset"
|
||||
onclick="window.location='/admin/'">✕ Réinitialiser</button>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
|
||||
<div class="admin-list-toolbar__right">
|
||||
<dl class="admin-stats">
|
||||
<div class="admin-stat">
|
||||
<dt class="admin-stat__label">Total</dt>
|
||||
<dd class="admin-stat__number"><?= $stats['total'] ?></dd>
|
||||
</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>
|
||||
<a href="/admin/add.php" class="admin-btn admin-btn--sm">Ajouter un TFE</a>
|
||||
<button type="button" class="admin-btn admin-btn--sm" id="import-dialog-btn"
|
||||
onclick="document.getElementById('import-dialog').showModal()">
|
||||
Importer un CSV
|
||||
</button>
|
||||
<a href="/admin/actions/export-csv.php" class="admin-btn admin-btn--sm">
|
||||
Exporter CSV
|
||||
</a>
|
||||
</div>
|
||||
</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="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>
|
||||
|
||||
<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/thanks.php?id=<?= $thesis['id'] ?>" class="admin-btn-sm admin-btn-view">Voir</a>
|
||||
<a href="/admin/edit.php?id=<?= $thesis['id'] ?>" class="admin-btn-sm 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="admin-btn-sm admin-btn-unpublish"
|
||||
onclick="return confirm('Retirer de la publication ?')">Dépublier</button>
|
||||
<?php else: ?>
|
||||
<input type="hidden" name="action" value="publish">
|
||||
<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>
|
||||
<?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';
|
||||
?>
|
||||
</main>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════
|
||||
IMPORT DIALOG
|
||||
══════════════════════════════════════════════════════════════ -->
|
||||
<dialog id="import-dialog" class="admin-dialog" aria-labelledby="import-dialog-title">
|
||||
<div class="admin-dialog__header">
|
||||
<h2 id="import-dialog-title">Importer une liste de TFE</h2>
|
||||
<button type="button" class="admin-dialog__close" aria-label="Fermer"
|
||||
onclick="document.getElementById('import-dialog').close()">✕</button>
|
||||
</div>
|
||||
|
||||
<?php if ($importMessage || !empty($importErrors)): ?>
|
||||
<div class="admin-import-status-card">
|
||||
<?php if (!empty($importErrors)): ?>
|
||||
<div class="toast admin-import-status-card__errors" role="alert" data-type="error">
|
||||
<strong>⚠ Erreurs :</strong>
|
||||
<ul class="admin-error-list">
|
||||
<?php foreach ($importErrors as $err): ?>
|
||||
<li><?= htmlspecialchars($err) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($importMessage): ?>
|
||||
<p class="toast admin-import-status-card__success" role="status" data-type="success">✓ <?= htmlspecialchars($importMessage) ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" class="admin-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
|
||||
<div>
|
||||
<label for="csv_file">Fichier CSV</label>
|
||||
<div class="admin-file-input">
|
||||
<input type="file" id="csv_file" name="csv_file" accept=".csv" required>
|
||||
<small class="admin-file-hint">
|
||||
Colonnes : Identifiant, Titre, Sous-titre, Auteur·ice(s), Contact, Promoteur·ice(s), Format, Année, AP, Orientation, Finalité, Mots-clés, Synopsis, Contexte, Remarques, Langue, Autorisation, License, taille, Points sur 20, lien BAIU<br>
|
||||
Quatre premières lignes ignorées — Séparateur : virgule — UTF-8
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-form-footer">
|
||||
<button type="submit" class="admin-btn">Importer</button>
|
||||
<button type="button" class="admin-btn-secondary"
|
||||
onclick="document.getElementById('import-dialog').close()">Annuler</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php if (!empty($importResults)): ?>
|
||||
<details class="admin-import-log-details">
|
||||
<summary>Logs d'importation (<?= count($importResults) ?> entrées)</summary>
|
||||
<ul class="admin-import-log">
|
||||
<?php foreach ($importResults as $r): ?>
|
||||
<li class="admin-import-log__item admin-import-log__item--<?= $r['type'] ?>"><?= htmlspecialchars($r['msg']) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
</dialog>
|
||||
|
||||
<?php if ($importMessage || !empty($importErrors)): ?>
|
||||
<script>document.getElementById('import-dialog').showModal();</script>
|
||||
<?php endif; ?>
|
||||
19
app/templates/admin/login.php
Normal file
19
app/templates/admin/login.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<main id="main-content">
|
||||
<div class="admin-login-wrap">
|
||||
<div class="admin-login-box">
|
||||
<h2>Administration</h2>
|
||||
<?php if ($error): ?>
|
||||
<p class="toast" role="alert" data-type="error">⚠ <?= htmlspecialchars($error) ?></p>
|
||||
<?php endif; ?>
|
||||
<form method="post" action="/admin/login.php" class="admin-form">
|
||||
<div>
|
||||
<label for="password">Mot de passe</label>
|
||||
<input type="password" id="password" name="password" required autofocus>
|
||||
</div>
|
||||
<div class="admin-form-footer">
|
||||
<button type="submit" class="admin-btn">Se connecter</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
288
app/templates/admin/parametres.php
Normal file
288
app/templates/admin/parametres.php
Normal file
@@ -0,0 +1,288 @@
|
||||
<main id="main-content">
|
||||
<h1>Paramètres</h1>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════
|
||||
MAINTENANCE
|
||||
══════════════════════════════════════════════════════════════ -->
|
||||
<section aria-labelledby="settings-maintenance-title">
|
||||
<h2 id="settings-maintenance-title">Maintenance</h2>
|
||||
|
||||
<div class="param-maintenance-row">
|
||||
<?php if ($maintenanceOn): ?>
|
||||
<p>
|
||||
<strong>⚠ Mode maintenance activé</strong> — le site public est inaccessible.
|
||||
</p>
|
||||
<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">
|
||||
<input type="hidden" name="redirect" value="/admin/parametres.php">
|
||||
<button type="submit">Désactiver la maintenance</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<p>Site public : <strong>en ligne</strong></p>
|
||||
<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">
|
||||
<input type="hidden" name="redirect" value="/admin/parametres.php">
|
||||
<button type="submit" class="param-btn-warning"
|
||||
onclick="return confirm('Mettre le site en maintenance ? Les visiteurs verront une page 503.')">
|
||||
Activer la maintenance
|
||||
</button>
|
||||
</form>
|
||||
<?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="param-btn-export"
|
||||
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>
|
||||
<p>
|
||||
Supprime définitivement tous les TFE de la base de données, y compris auteurs,
|
||||
promoteurs, tags, fichiers associés. Cette action est <strong>irréversible</strong>.
|
||||
</p>
|
||||
<form method="post" action="actions/delete.php"
|
||||
onsubmit="return confirm('⚠ Supprimer définitivement TOUS les TFE ? Cette action est IRRÉVERSIBLE.')">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="delete_all" value="1">
|
||||
<button type="submit" class="param-btn-danger">Supprimer tous les TFE (<?= $stats['total'] ?? '?' ?>)</button>
|
||||
</form>
|
||||
</fieldset>
|
||||
</section>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════
|
||||
FORMULAIRE
|
||||
══════════════════════════════════════════════════════════════ -->
|
||||
<section aria-labelledby="settings-formulaire-title">
|
||||
<h2 id="settings-formulaire-title">Formulaire</h2>
|
||||
<p>Options de visibilité disponibles dans le formulaire d'ajout de TFE.</p>
|
||||
<p class="param-note">L'option <strong>Libre</strong> ne sera activée qu'à partir de l'année académique prochaine.</p>
|
||||
|
||||
<form method="post" action="actions/settings.php" class="param-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="section" value="formulaire">
|
||||
|
||||
<fieldset>
|
||||
<legend>Types d'accès</legend>
|
||||
|
||||
<label class="param-checkbox">
|
||||
<input type="checkbox" name="access_type_interdit_enabled" value="1"
|
||||
<?= ($siteSettings['access_type_interdit_enabled'] ?? '1') === '1' ? 'checked' : '' ?>>
|
||||
<span>
|
||||
<strong>Interdit</strong><br>
|
||||
<small>TFE non disponible en physique ni sur le site</small>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="param-checkbox">
|
||||
<input type="checkbox" name="access_type_interne_enabled" value="1"
|
||||
<?= ($siteSettings['access_type_interne_enabled'] ?? '1') === '1' ? 'checked' : '' ?>>
|
||||
<span>
|
||||
<strong>Interne</strong><br>
|
||||
<small>TFE accessible uniquement sur place en physique</small>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="param-checkbox param-checkbox--disabled">
|
||||
<input type="checkbox" name="access_type_libre_enabled" value="1"
|
||||
<?= ($siteSettings['access_type_libre_enabled'] ?? '0') === '1' ? 'checked' : '' ?>>
|
||||
<span>
|
||||
<strong>Libre</strong><br>
|
||||
<small>Libre accès — disponible à partir de l'année académique prochaine</small>
|
||||
</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<button type="submit">Enregistrer</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════
|
||||
RELAY SMTP
|
||||
══════════════════════════════════════════════════════════════ -->
|
||||
<section aria-labelledby="settings-smtp-title">
|
||||
<h2 id="settings-smtp-title">Relay SMTP</h2>
|
||||
<p>
|
||||
Identifiants du serveur SMTP utilisé pour l'envoi d'e-mails
|
||||
(notifications, partage de TFE, etc.).
|
||||
</p>
|
||||
<div class="param-smtp-status">
|
||||
<?php if ($smtpConfigured): ?>
|
||||
<span class="param-badge-ok">✓ Configuré</span>
|
||||
<span><?= htmlspecialchars($smtpSettings['host']) ?>:<?= (int)$smtpSettings['port'] ?> (<?= htmlspecialchars($smtpSettings['encryption']) ?>)</span>
|
||||
<?php else: ?>
|
||||
<span class="param-badge-warn">✗ Non configuré</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<form method="post" action="actions/settings.php" class="param-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="section" value="smtp">
|
||||
|
||||
<div class="param-grid">
|
||||
<div>
|
||||
<label for="smtp_host">Hôte SMTP</label>
|
||||
<input type="text" id="smtp_host" name="smtp_host"
|
||||
value="<?= htmlspecialchars($smtpSettings['host']) ?>"
|
||||
placeholder="smtp.example.com">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="smtp_port">Port</label>
|
||||
<input type="number" id="smtp_port" name="smtp_port"
|
||||
value="<?= (int)$smtpSettings['port'] ?>"
|
||||
min="1" max="65535">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="smtp_encryption">Chiffrement</label>
|
||||
<select id="smtp_encryption" name="smtp_encryption">
|
||||
<option value="tls" <?= $smtpSettings['encryption'] === 'tls' ? 'selected' : '' ?>>TLS (STARTTLS)</option>
|
||||
<option value="ssl" <?= $smtpSettings['encryption'] === 'ssl' ? 'selected' : '' ?>>SSL (SMTPS)</option>
|
||||
<option value="none" <?= $smtpSettings['encryption'] === 'none' ? 'selected' : '' ?>>Aucun</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="smtp_username">Nom d'utilisateur</label>
|
||||
<input type="text" id="smtp_username" name="smtp_username"
|
||||
value="<?= htmlspecialchars($smtpSettings['username']) ?>">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="smtp_password">Mot de passe</label>
|
||||
<input type="password" id="smtp_password" name="smtp_password"
|
||||
value="<?= htmlspecialchars($smtpSettings['password']) ?>"
|
||||
autocomplete="new-password"
|
||||
placeholder="Laissez vide pour ne pas modifier">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<fieldset class="param-fieldset-inline">
|
||||
<legend>Expéditeur par défaut</legend>
|
||||
<div class="param-grid">
|
||||
<div>
|
||||
<label for="smtp_from_email">Adresse e-mail</label>
|
||||
<input type="email" id="smtp_from_email" name="smtp_from_email"
|
||||
value="<?= htmlspecialchars($smtpSettings['from_email']) ?>"
|
||||
placeholder="noreply@example.com">
|
||||
</div>
|
||||
<div>
|
||||
<label for="smtp_from_name">Nom d'expéditeur</label>
|
||||
<input type="text" id="smtp_from_name" name="smtp_from_name"
|
||||
value="<?= htmlspecialchars($smtpSettings['from_name']) ?>">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<button type="submit">Enregistrer</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════
|
||||
COMPTE ADMINISTRATEUR
|
||||
══════════════════════════════════════════════════════════════ -->
|
||||
<section aria-labelledby="settings-account-title">
|
||||
<h2 id="settings-account-title">Compte administrateur</h2>
|
||||
|
||||
<dl class="param-account-status">
|
||||
<div>
|
||||
<dt>Authentification PHP</dt>
|
||||
<dd><?php $badgeType = 'ok'; $badgeValue = $hasPassword; $badgeOkLabel = 'Active'; $badgeWarnLabel = 'Non configurée'; include APP_ROOT . '/templates/partials/status-badge.php'; ?></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Stockage du hash</dt>
|
||||
<dd>
|
||||
<code>site_settings (DB)</code>
|
||||
<?php $badgeType = 'ok'; $badgeValue = $hasPassword; $badgeOkLabel = 'Présent'; $badgeWarnLabel = 'Absent'; include APP_ROOT . '/templates/partials/status-badge.php'; ?>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<?php if (!$hasPassword): ?>
|
||||
<p class="param-note">
|
||||
Aucun mot de passe PHP configuré. Le formulaire ci-dessous stockera
|
||||
un hash bcrypt dans la base de données.
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="/admin/actions/account.php" class="param-form" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="redirect" value="/admin/parametres.php">
|
||||
|
||||
<?php if ($hasPassword): ?>
|
||||
<div>
|
||||
<label for="current_password">Mot de passe actuel</label>
|
||||
<input type="password" id="current_password"
|
||||
name="current_password" required autocomplete="current-password">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div>
|
||||
<label for="new_password">Nouveau mot de passe</label>
|
||||
<input type="password" id="new_password"
|
||||
name="new_password" required autocomplete="new-password"
|
||||
minlength="12">
|
||||
<small>Minimum 12 caractères.</small>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="confirm_password">Confirmer le mot de passe</label>
|
||||
<input type="password" id="confirm_password"
|
||||
name="confirm_password" required autocomplete="new-password">
|
||||
</div>
|
||||
|
||||
<button type="submit">
|
||||
<?= $hasPassword ? 'Mettre à jour le mot de passe' : 'Définir le mot de passe' ?>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Danger zone: remove credentials -->
|
||||
<?php if ($hasPassword): ?>
|
||||
<fieldset class="param-danger-zone">
|
||||
<legend>Supprimer la configuration du mot de passe PHP</legend>
|
||||
<p>
|
||||
Supprime le hash de la base de données. L'accès admin
|
||||
dépendra uniquement de l'authentification nginx Basic Auth si elle est configurée.
|
||||
</p>
|
||||
<form method="post" action="/admin/actions/account.php"
|
||||
onsubmit="return confirm('Supprimer le mot de passe PHP ? L\'accès admin ne sera protégé que par nginx Basic Auth.')">>
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="action" value="remove_credentials">
|
||||
<input type="hidden" name="redirect" value="/admin/parametres.php">
|
||||
<input type="hidden" name="current_password_remove" value="">
|
||||
<button type="submit" class="param-btn-danger">Supprimer le mot de passe</button>
|
||||
</form>
|
||||
</fieldset>
|
||||
<?php endif; ?>
|
||||
</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="admin-btn">Exporter la base de données</a>
|
||||
<button type="button" class="admin-btn-secondary"
|
||||
onclick="document.getElementById('export-db-dialog').close()">Annuler</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</main>
|
||||
54
app/templates/admin/partials/system-log-panel.php
Normal file
54
app/templates/admin/partials/system-log-panel.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<div class="log-toolbar">
|
||||
<form id="lines-form" hx-get="/admin/system-fragment.php"
|
||||
hx-target="#sys-tab-panel"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#sys-tab-panel"
|
||||
hx-trigger="change"
|
||||
hx-vals='{"tab":"<?= htmlspecialchars($activeTab) ?>"}'>
|
||||
<label for="lines-select">Afficher</label>
|
||||
<select id="lines-select" name="n" aria-label="Nombre de lignes">
|
||||
<?php foreach (SystemController::ALLOWED_LINES as $opt): ?>
|
||||
<option value="<?= $opt ?>" <?= $opt === $selectedN ? 'selected' : '' ?>><?= $opt ?> dernières lignes</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</form>
|
||||
<?php if ($logLines !== null && count($logLines) > 0): ?>
|
||||
<span class="log-count-badge"><?= count($logLines) ?> ligne(s)</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($logFileMeta): ?>
|
||||
<div class="log-meta">
|
||||
<span data-label="Fichier"><?= htmlspecialchars(SystemController::LOG_FILES[$activeTab]['path']) ?></span>
|
||||
<span data-label="Taille"><?= $logFileMeta['size'] ?></span>
|
||||
<span data-label="Modifié"><?= $logFileMeta['mtime'] ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($logError !== null): ?>
|
||||
<div class="log-unavailable">
|
||||
<strong>Journaux non disponibles</strong>
|
||||
<div class="log-unavail-path"><?= $logError ?></div>
|
||||
<?php if (php_sapi_name() === 'cli-server'): ?>
|
||||
<div class="log-unavail-dev">
|
||||
En environnement de développement, les logs nginx ne sont pas disponibles.
|
||||
Cette page est pleinement fonctionnelle sur le serveur de production.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php elseif (empty($logLines)): ?>
|
||||
<div class="log-empty">Le fichier journal est vide.</div>
|
||||
|
||||
<?php else: ?>
|
||||
<div class="log-output" id="log-output" role="log" aria-live="off" aria-label="Contenu du journal">
|
||||
<button class="log-copy-btn" id="log-copy-btn" type="button" title="Copier le contenu"
|
||||
onclick="copyLogContent(this);return false">
|
||||
Copier
|
||||
</button>
|
||||
<?php foreach ($logLines as $i => $line): ?>
|
||||
<span class="log-line <?= SystemController::logLineClass($line) ?>"
|
||||
data-n="<?= count($logLines) - $i ?>"><?= htmlspecialchars($line, ENT_QUOTES | ENT_SUBSTITUTE) ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
40
app/templates/admin/partials/system-nginx-config-panel.php
Normal file
40
app/templates/admin/partials/system-nginx-config-panel.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php if ($nginxConfigMeta): ?>
|
||||
<div class="log-meta">
|
||||
<span data-label="Fichier"><?= htmlspecialchars($nginxConfigMeta['path']) ?></span>
|
||||
<span data-label="Taille"><?= $nginxConfigMeta['size'] ?></span>
|
||||
<span data-label="Modifié"><?= $nginxConfigMeta['mtime'] ?></span>
|
||||
<?php if ($nginxConfigSource === 'live'): ?>
|
||||
<span class="nginx-source-badge nginx-source-badge--live">● Config déployée</span>
|
||||
<?php else: ?>
|
||||
<span class="nginx-source-badge nginx-source-badge--local">⚠ Référence locale (config live inaccessible)</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($nginxConfigError !== null): ?>
|
||||
<div class="log-unavailable">
|
||||
<strong>Configuration nginx non disponible</strong>
|
||||
<div class="log-unavail-path"><?= htmlspecialchars($nginxConfigError) ?></div>
|
||||
<?php if (php_sapi_name() === 'cli-server'): ?>
|
||||
<div class="log-unavail-dev">
|
||||
En développement, <code>/etc/nginx/sites-available/posterg</code> n'existe pas.
|
||||
La config de référence se trouve dans <code>nginx/posterg.conf</code>.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php elseif (empty($nginxConfigLines)): ?>
|
||||
<div class="log-empty">Le fichier de configuration est vide.</div>
|
||||
|
||||
<?php else: ?>
|
||||
<div class="log-output" id="log-output" role="region" aria-label="Configuration nginx">
|
||||
<button class="log-copy-btn" id="log-copy-btn" type="button" title="Copier la configuration"
|
||||
onclick="copyLogContent(this);return false">
|
||||
Copier
|
||||
</button>
|
||||
<?php foreach ($nginxConfigLines as $i => $line): ?>
|
||||
<span class="log-line <?= SystemController::nginxLineClass($line) ?>"
|
||||
data-n="<?= $i + 1 ?>"><?= htmlspecialchars($line, ENT_QUOTES | ENT_SUBSTITUTE) ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
12
app/templates/admin/partials/toast.php
Normal file
12
app/templates/admin/partials/toast.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php if ($flash['error']): ?>
|
||||
<p class="toast toast--error" role="alert">
|
||||
<span class="toast__icon" aria-hidden="true">⚠</span>
|
||||
<?= htmlspecialchars($flash['error']) ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<?php if ($flash['success']): ?>
|
||||
<p class="toast toast--success" role="status">
|
||||
<span class="toast__icon" aria-hidden="true">✓</span>
|
||||
<?= htmlspecialchars($flash['success']) ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
165
app/templates/admin/system.php
Normal file
165
app/templates/admin/system.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<main id="main-content">
|
||||
<h1>Système</h1>
|
||||
|
||||
<p class="sys-refresh-note">
|
||||
Affiché le <?= date('d/m/Y à H:i:s') ?> —
|
||||
<a href="?tab=<?= htmlspecialchars($activeTab) ?>&n=<?= $selectedN ?>">Rafraîchir</a> —
|
||||
<a href="?tab=<?= htmlspecialchars($activeTab) ?>&n=<?= $selectedN ?>&refresh=1">Forcer actualisation</a>
|
||||
</p>
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════════════════
|
||||
STATUS SECTION — always visible above tabs
|
||||
════════════════════════════════════════════════════════════════════ -->
|
||||
<section class="sys-status-section" aria-label="Statut du système">
|
||||
<div class="sys-status-header">
|
||||
<h2 class="srv-section-title srv-section-title--compact">Statut
|
||||
<?php if ($statusCached && $statusCacheAge !== null): ?>
|
||||
<span class="sys-cache-badge sys-cache-badge--hit" title="Données en cache">
|
||||
⚡ Cache — il y a <?= $statusCacheAge ?>s
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<span class="sys-cache-badge sys-cache-badge--miss" title="Données fraîches">
|
||||
⟳ Actualisé
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</h2>
|
||||
<button id="sys-status-toggle" class="sys-status-toggle"
|
||||
aria-expanded="<?= $statusInitiallyCollapsed ? 'false' : 'true' ?>" aria-controls="sys-status-body"
|
||||
type="button"
|
||||
onclick="var b=document.getElementById('sys-status-body');var c=b.hidden;b.hidden=!c;this.setAttribute('aria-expanded',c);this.textContent=c?'▲ Réduire':'▼ Développer';document.cookie='sys_collapsed='+(!c)+';path=/;max-age=31536000';return false">
|
||||
<?= $statusInitiallyCollapsed ? '▼ Développer' : '▲ Réduire' ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="sys-status-body"<?= $statusInitiallyCollapsed ? ' hidden' : '' ?>>
|
||||
<div class="srv-grid">
|
||||
<?php foreach ($checks as $check): ?>
|
||||
<?php $st = $check['status'] ?? 'unknown'; ?>
|
||||
<div class="srv-card">
|
||||
<div class="srv-card__header">
|
||||
<span class="srv-card__name"><?= htmlspecialchars($check['label']) ?></span>
|
||||
<span class="<?= SystemController::statusClass($st) ?>"><?= SystemController::statusLabel($st) ?></span>
|
||||
</div>
|
||||
<?php if (!empty($check['detail'])): ?>
|
||||
<div class="srv-card__detail"><?= htmlspecialchars($check['detail']) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="sys-status-meta">
|
||||
<div>
|
||||
<h3 class="srv-section-title srv-section-title--sub">Environnement PHP</h3>
|
||||
<div class="php-grid php-grid--flush">
|
||||
<?php foreach ($phpInfo as $key => $val): ?>
|
||||
<div class="php-item">
|
||||
<div class="php-item__key"><?= htmlspecialchars($key) ?></div>
|
||||
<div class="php-item__val"><?= htmlspecialchars($val) ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="srv-section-title srv-section-title--sub">Espace disque</h3>
|
||||
<div class="disk-bar-wrap">
|
||||
<div class="disk-bar" style="--disk-pct:<?= $diskPct ?>%;--disk-color:<?= $diskColor ?>"></div>
|
||||
</div>
|
||||
<div class="disk-stats">
|
||||
<span><?= SystemController::humanBytes($diskUsed) ?> utilisé (<?= $diskPct ?>%)</span>
|
||||
<span><?= SystemController::humanBytes($diskFree) ?> libre / <?= SystemController::humanBytes($diskTotal) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── Tab bar ─────────────────────────────────────────────────────── -->
|
||||
<nav class="sys-tabs" aria-label="Journaux et configuration">
|
||||
<?php foreach (SystemController::LOG_FILES as $key => $def): ?>
|
||||
<a href="?tab=<?= htmlspecialchars($key) ?>&n=<?= $selectedN ?>"
|
||||
class="sys-tab <?= $activeTab === $key ? 'active' : '' ?>"
|
||||
hx-get="/admin/system-fragment.php?tab=<?= htmlspecialchars($key) ?>&n=<?= $selectedN ?>"
|
||||
hx-target="#sys-tab-panel"
|
||||
hx-push-url="?tab=<?= htmlspecialchars($key) ?>&n=<?= $selectedN ?>"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#sys-tab-panel"
|
||||
data-tab="<?= htmlspecialchars($key) ?>"
|
||||
<?= $activeTab === $key ? 'aria-current="page"' : '' ?>>
|
||||
<?= htmlspecialchars($def['label']) ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
<a href="?tab=nginx_config"
|
||||
class="sys-tab <?= $activeTab === 'nginx_config' ? 'active' : '' ?>"
|
||||
hx-get="/admin/system-fragment.php?tab=nginx_config"
|
||||
hx-target="#sys-tab-panel"
|
||||
hx-push-url="?tab=nginx_config"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#sys-tab-panel"
|
||||
data-tab="nginx_config"
|
||||
<?= $activeTab === 'nginx_config' ? 'aria-current="page"' : '' ?>>nginx — config</a>
|
||||
</nav>
|
||||
|
||||
<!-- Tab panel — content swapped by HTMX -->
|
||||
<div id="sys-tab-panel">
|
||||
|
||||
<?php if ($activeTab === 'nginx_config'): ?>
|
||||
<?php include APP_ROOT . '/templates/admin/partials/system-nginx-config-panel.php'; ?>
|
||||
<?php else: ?>
|
||||
<?php include APP_ROOT . '/templates/admin/partials/system-log-panel.php'; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
</div><!-- #sys-tab-panel -->
|
||||
|
||||
</main>
|
||||
|
||||
<script>
|
||||
function copyLogContent(btn) {
|
||||
var logOut = document.querySelector('#log-output');
|
||||
if (!logOut) return;
|
||||
var text = Array.from(logOut.querySelectorAll('.log-line'))
|
||||
.map(function(el){ return el.textContent; }).join('\n');
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(function(){
|
||||
btn.textContent = '\u2713 Copi\u00e9';
|
||||
btn.classList.add('copied');
|
||||
setTimeout(function(){ btn.textContent = 'Copier'; btn.classList.remove('copied'); }, 2000);
|
||||
});
|
||||
} else {
|
||||
fallbackCopy(text, btn);
|
||||
}
|
||||
}
|
||||
function fallbackCopy(text, btn) {
|
||||
var ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
ta.style.cssText = 'position:fixed;opacity:0';
|
||||
document.body.appendChild(ta); ta.select();
|
||||
try { document.execCommand('copy'); btn.textContent = '\u2713 Copi\u00e9'; btn.classList.add('copied');
|
||||
setTimeout(function(){ btn.textContent = 'Copier'; btn.classList.remove('copied'); }, 2000);
|
||||
} catch(e) {}
|
||||
document.body.removeChild(ta);
|
||||
}
|
||||
// Update active tab class after each HTMX swap on #sys-tab-panel
|
||||
document.body.addEventListener('htmx:afterSwap', function(evt) {
|
||||
if (evt.detail.target && evt.detail.target.id === 'sys-tab-panel') {
|
||||
var rc = evt.detail.requestConfig;
|
||||
var tab = null;
|
||||
// Tab clicks carry ?tab=… in the path
|
||||
var qIdx = rc.path.indexOf('?');
|
||||
if (qIdx !== -1) {
|
||||
tab = new URLSearchParams(rc.path.substring(qIdx + 1)).get('tab');
|
||||
}
|
||||
// Line-count form sends tab via hx-vals in parameters
|
||||
if (!tab && rc.parameters && rc.parameters.tab) {
|
||||
tab = rc.parameters.tab;
|
||||
}
|
||||
if (tab) {
|
||||
document.querySelectorAll('.sys-tabs .sys-tab').forEach(function(a) {
|
||||
var isActive = a.getAttribute('data-tab') === tab;
|
||||
a.classList.toggle('active', isActive);
|
||||
if (isActive) a.setAttribute('aria-current', 'page');
|
||||
else a.removeAttribute('aria-current');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
62
app/templates/admin/tags.php
Normal file
62
app/templates/admin/tags.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<main id="main-content">
|
||||
<h1>Mots-clés (<?= count($tags) ?>)</h1>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Nom</th>
|
||||
<th scope="col">TFE associés</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($tags as $tag): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($tag['name']) ?></td>
|
||||
<td class="admin-tags-count"><?= (int)$tag['thesis_count'] ?></td>
|
||||
<td>
|
||||
<!-- Rename -->
|
||||
<form method="post" action="actions/tag.php" class="admin-inline-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="action" value="rename">
|
||||
<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="admin-btn admin-btn--sm">Renommer</button>
|
||||
</form>
|
||||
|
||||
<!-- Merge into another tag -->
|
||||
<form method="post" action="actions/tag.php" class="admin-inline-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="action" value="merge">
|
||||
<input type="hidden" name="source_id" value="<?= (int)$tag['id'] ?>">
|
||||
<select name="target_id" class="admin-select--inline" required>
|
||||
<option value="">— Fusionner dans… —</option>
|
||||
<?php foreach ($tags as $other): ?>
|
||||
<?php if ($other['id'] !== $tag['id']): ?>
|
||||
<option value="<?= (int)$other['id'] ?>"><?= htmlspecialchars($other['name']) ?></option>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="submit" class="admin-btn admin-btn--sm admin-btn--warning"
|
||||
onclick="return confirm('Fusionner ce tag dans la cible ? Le tag source sera supprimé.')">
|
||||
Fusionner
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Delete -->
|
||||
<form method="post" action="actions/tag.php" class="admin-inline-form">
|
||||
<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="submit" class="admin-btn admin-btn--sm admin-btn--danger"
|
||||
onclick="return confirm('Supprimer le tag « <?= htmlspecialchars(addslashes($tag['name'])) ?> » ? Cette action est irréversible.')">
|
||||
Supprimer
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
115
app/templates/admin/thanks.php
Normal file
115
app/templates/admin/thanks.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<main id="main-content">
|
||||
<?php if ($studentMode): ?>
|
||||
<!-- ═══════════════════ STUDENT MODE: Thank you page ═══════════════════ -->
|
||||
<div class="thanks-student-page">
|
||||
<?php if ($error): ?>
|
||||
<div class="thanks-error">
|
||||
<h1>⚠ Oups…</h1>
|
||||
<p><?= htmlspecialchars($error) ?></p>
|
||||
<a href="/admin/add.php?mode=student" class="btn-new-form">← Retour au formulaire</a>
|
||||
</div>
|
||||
|
||||
<?php elseif ($thesis): ?>
|
||||
<div class="thanks-success">
|
||||
<h1>Merci 🎉</h1>
|
||||
<p class="thanks-message">
|
||||
Ton TFE <strong><?= htmlspecialchars($thesis['title']) ?></strong> a bien été soumis.
|
||||
</p>
|
||||
<a href="/admin/add.php?mode=student" class="btn-new-form">+ Ajouter un nouveau TFE</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="thanks-error">
|
||||
<h1>Erreur</h1>
|
||||
<p>Aucune donnée à afficher.</p>
|
||||
<a href="/admin/add.php?mode=student" class="btn-new-form">← Retour au formulaire</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
<!-- ═══════════════════ ADMIN MODE: Recap page ═══════════════════ -->
|
||||
<h1>Récapitulatif TFE</h1>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<p class="toast" role="alert" data-type="error">⚠ <?= htmlspecialchars($error) ?></p>
|
||||
<p><a href="/admin/add.php" class="admin-btn-secondary">Retour au formulaire</a></p>
|
||||
|
||||
<?php elseif ($thesis): ?>
|
||||
<section>
|
||||
<h2>Informations de base</h2>
|
||||
<dl>
|
||||
<dt>Identifiant</dt><dd><?= htmlspecialchars($thesis['identifier']) ?></dd>
|
||||
<dt>Titre</dt><dd><?= htmlspecialchars($thesis['title']) ?></dd>
|
||||
<?php if ($thesis['subtitle']): ?>
|
||||
<dt>Sous-titre</dt><dd><?= htmlspecialchars($thesis['subtitle']) ?></dd>
|
||||
<?php endif; ?>
|
||||
<dt>Auteur·ice(s)</dt><dd><?= htmlspecialchars($thesis['authors']) ?></dd>
|
||||
<dt>Année</dt><dd><?= htmlspecialchars($thesis['year']) ?></dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Détails académiques</h2>
|
||||
<dl>
|
||||
<dt>Orientation</dt><dd><?= htmlspecialchars($thesis['orientation'] ?? '–') ?></dd>
|
||||
<dt>Atelier pratique</dt><dd><?= htmlspecialchars($thesis['ap_program'] ?? '–') ?></dd>
|
||||
<dt>Finalité</dt><dd><?= htmlspecialchars($thesis['finality_type'] ?? '–') ?></dd>
|
||||
<?php if ($thesis['supervisors']): ?>
|
||||
<dt>Promoteur·ice(s)</dt><dd><?= htmlspecialchars($thesis['supervisors']) ?></dd>
|
||||
<?php endif; ?>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Contenu</h2>
|
||||
<dl>
|
||||
<?php if ($thesis['languages']): ?>
|
||||
<dt>Langue(s)</dt><dd><?= htmlspecialchars($thesis['languages']) ?></dd>
|
||||
<?php endif; ?>
|
||||
<?php if ($thesis['formats']): ?>
|
||||
<dt>Format(s)</dt><dd><?= htmlspecialchars($thesis['formats']) ?></dd>
|
||||
<?php endif; ?>
|
||||
<?php if ($thesis['keywords']): ?>
|
||||
<dt>Mots-clés</dt><dd><?= htmlspecialchars($thesis['keywords']) ?></dd>
|
||||
<?php endif; ?>
|
||||
<?php if ($thesis['file_size_info']): ?>
|
||||
<dt>Durée / Taille</dt><dd><?= htmlspecialchars($thesis['file_size_info']) ?></dd>
|
||||
<?php endif; ?>
|
||||
<?php if ($thesis['baiu_link']): ?>
|
||||
<dt>Lien</dt><dd><a href="<?= htmlspecialchars($thesis['baiu_link']) ?>" target="_blank" rel="noopener"><?= htmlspecialchars($thesis['baiu_link']) ?></a></dd>
|
||||
<?php endif; ?>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<?php if (!empty($files)): ?>
|
||||
<section>
|
||||
<h2>Fichiers</h2>
|
||||
<table>
|
||||
<thead><tr><th scope="col">Type</th><th scope="col">Fichier</th><th scope="col">Taille</th><th scope="col">Date</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($files as $f): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($f['file_type']) ?></td>
|
||||
<td><?= htmlspecialchars($f['file_name']) ?></td>
|
||||
<td><?= formatFileSize($f['file_size']) ?></td>
|
||||
<td><?= date('d/m/Y H:i', strtotime($f['uploaded_at'])) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="admin-action-bar">
|
||||
<a href="/admin/edit.php?id=<?= $thesisId ?>" class="admin-btn">Modifier</a>
|
||||
<a href="/admin/add.php" class="admin-btn-secondary">Ajouter un autre TFE</a>
|
||||
<a href="/admin/" class="admin-btn-secondary">Retour à la liste</a>
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
<p class="admin-muted">Aucune donnée à afficher.</p>
|
||||
<p><a href="/admin/add.php" class="admin-btn-secondary">Retour au formulaire</a></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Shared flash-message partial for the admin section.
|
||||
*
|
||||
* Consumes all flash variants via App::consumeFlash(), then renders toast markup.
|
||||
* Toast JS in the admin footer auto-removes them after 4 seconds.
|
||||
*
|
||||
* Usage: include once per request in the admin footer.
|
||||
*/
|
||||
$_flash = App::consumeFlash();
|
||||
?>
|
||||
<?php if ($_flash['error'] || $_flash['success']): ?>
|
||||
<div id="admin-toasts" style="display:none">
|
||||
<?php if ($_flash['error']): ?>
|
||||
<div class="toast" role="alert" data-type="error">⚠ <?= htmlspecialchars($_flash['error']) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($_flash['success']): ?>
|
||||
<div class="toast" role="status" data-type="success">✓ <?= htmlspecialchars($_flash['success']) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
Reference in New Issue
Block a user