mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
415 lines
23 KiB
PHP
415 lines
23 KiB
PHP
<main id="main-content" class="admin-main--toc">
|
||
<?php include APP_ROOT . '/templates/admin/partials/admin-toc.php'; ?>
|
||
|
||
<article>
|
||
<h1>Contenus</h1>
|
||
|
||
<?php
|
||
$flash = App::consumeFlash();
|
||
?>
|
||
<?php if ($flash['success']): ?>
|
||
<div class="flash-success" role="alert"><?= htmlspecialchars($flash['success']) ?></div>
|
||
<?php endif; ?>
|
||
<?php if ($flash['error']): ?>
|
||
<div class="flash-error" role="alert"><?= htmlspecialchars($flash['error']) ?></div>
|
||
<?php endif; ?>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════════════
|
||
PAGES STATIQUES
|
||
═══════════════════════════════════════════════════════════════════ -->
|
||
<section aria-labelledby="static-pages-title">
|
||
<h2 id="static-pages-title">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-icon-btn admin-icon-btn--edit" title="Éditer">
|
||
<?= icon('pencil-note') ?>
|
||
</a>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</section>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════════════
|
||
DONNÉES SECONDAIRES
|
||
═══════════════════════════════════════════════════════════════════ -->
|
||
<section aria-labelledby="donnees-secondaires-title">
|
||
<h2 id="donnees-secondaires-title">Données Secondaires</h2>
|
||
|
||
<fieldset>
|
||
<legend>Langues</legend>
|
||
|
||
<form id="langues-search-form"
|
||
hx-get="/admin/contenus-langues-fragment.php"
|
||
hx-target="#langues-table-wrap"
|
||
hx-swap="innerHTML"
|
||
hx-trigger="input changed delay:200ms from:input[name=q], keyup[key=='Enter'] from:input[name=q]"
|
||
hx-push-url="false"
|
||
style="margin-bottom:var(--space-xs)">
|
||
<input type="text" name="q" placeholder="Rechercher une langue…" style="max-width:300px">
|
||
</form>
|
||
|
||
<div id="langues-table-wrap"
|
||
hx-get="/admin/contenus-langues-fragment.php"
|
||
hx-trigger="load"
|
||
hx-swap="innerHTML"
|
||
style="min-height:50vh;max-height:50vh;overflow-y:auto">
|
||
<div style="padding:var(--space-m); text-align:center; color:var(--text-tertiary)">
|
||
<img alt="Chargement…" class="htmx-indicator" width="24" src="/assets/img/bars.svg" style="opacity:0.5">
|
||
</div>
|
||
</div>
|
||
</fieldset>
|
||
|
||
<p style="margin-top:var(--space-s)"><a href="/admin/tags.php" class="btn btn--sm btn--primary">Gérer les mots-clés (page dédiée)</a></p>
|
||
|
||
<fieldset>
|
||
<legend>Mots-clés</legend>
|
||
|
||
<form id="motscles-search-form"
|
||
hx-get="/admin/contenus-motscles-fragment.php"
|
||
hx-target="#motscles-table-wrap"
|
||
hx-swap="innerHTML"
|
||
hx-trigger="input changed delay:200ms from:input[name=q], keyup[key=='Enter'] from:input[name=q]"
|
||
hx-push-url="false"
|
||
style="margin-bottom:var(--space-xs)">
|
||
<input type="text" name="q" placeholder="Rechercher un mot-clé…" style="max-width:300px">
|
||
</form>
|
||
|
||
<div id="motscles-table-wrap"
|
||
hx-get="/admin/contenus-motscles-fragment.php"
|
||
hx-trigger="load"
|
||
hx-swap="innerHTML"
|
||
style="min-height:50vh;max-height:50vh;overflow-y:auto">
|
||
<div style="padding:var(--space-m); text-align:center; color:var(--text-tertiary)">
|
||
<img alt="Chargement…" class="htmx-indicator" width="24" src="/assets/img/bars.svg" style="opacity:0.5">
|
||
</div>
|
||
</div>
|
||
</fieldset>
|
||
</section>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════════════
|
||
PARAMÈTRES DU FORMULAIRE
|
||
═══════════════════════════════════════════════════════════════════ -->
|
||
<section aria-labelledby="form-settings-title">
|
||
<h2 id="form-settings-title">Paramètres du Formulaire</h2>
|
||
|
||
<fieldset id="fieldset-acces">
|
||
<legend>Degré d'ouverture</legend>
|
||
<p>Options de visibilité disponibles dans le formulaire d'ajout de TFE.</p>
|
||
|
||
<div class="param-form">
|
||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||
<input type="hidden" name="section" value="formulaire_acces">
|
||
|
||
<label class="param-checkbox">
|
||
<input type="checkbox" name="access_type_libre_enabled" value="1"
|
||
<?= ($siteSettings['access_type_libre_enabled'] ?? '0') === '1' ? 'checked' : '' ?>
|
||
hx-post="/admin/actions/settings.php"
|
||
hx-trigger="change"
|
||
hx-target="#acces-response"
|
||
hx-swap="innerHTML"
|
||
hx-include="#fieldset-acces"
|
||
hx-on::before-request="console.log('[acces-libre] sending checked=' + this.checked)"
|
||
hx-on::after-request="console.log('[acces-libre] response received')">
|
||
<span>
|
||
<strong>Libre</strong><br>
|
||
<small>Libre accès — TFE accessible publiquement sur la plateforme et en bibliothèque</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' : '' ?>
|
||
hx-post="/admin/actions/settings.php"
|
||
hx-trigger="change"
|
||
hx-target="#acces-response"
|
||
hx-swap="innerHTML"
|
||
hx-include="#fieldset-acces"
|
||
hx-on::before-request="console.log('[acces-interne] sending checked=' + this.checked)"
|
||
hx-on::after-request="console.log('[acces-interne] response received')">
|
||
<span>
|
||
<strong>Interne</strong><br>
|
||
<small>TFE accessible uniquement sur place en physique</small>
|
||
</span>
|
||
</label>
|
||
|
||
<label class="param-checkbox">
|
||
<input type="checkbox" name="access_type_interdit_enabled" value="1"
|
||
<?= ($siteSettings['access_type_interdit_enabled'] ?? '1') === '1' ? 'checked' : '' ?>
|
||
hx-post="/admin/actions/settings.php"
|
||
hx-trigger="change"
|
||
hx-target="#acces-response"
|
||
hx-swap="innerHTML"
|
||
hx-include="#fieldset-acces"
|
||
hx-on::before-request="console.log('[acces-interdit] sending checked=' + this.checked)"
|
||
hx-on::after-request="console.log('[acces-interdit] response received')">
|
||
<span>
|
||
<strong>Interdit</strong><br>
|
||
<small>TFE non disponible en physique ni sur le site</small>
|
||
</span>
|
||
</label>
|
||
</div>
|
||
|
||
<div id="acces-response" aria-live="polite"></div>
|
||
</fieldset>
|
||
|
||
<fieldset id="fieldset-types">
|
||
<legend>Types de travaux</legend>
|
||
<p>Active ou désactive les types de travaux dans les formulaires et la consultation. Un type désactivé ne peut plus être soumis ni affiché sur le site.</p>
|
||
<p class="param-note">Le type <strong>TFE</strong> est toujours actif et ne peut pas être désactivé.</p>
|
||
|
||
<div class="param-form">
|
||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||
<input type="hidden" name="section" value="objet_types">
|
||
|
||
<label class="param-checkbox param-checkbox--disabled">
|
||
<input type="checkbox" disabled checked>
|
||
<span>
|
||
<strong>TFE</strong><br>
|
||
<small>Travail de fin d'études — toujours actif</small>
|
||
</span>
|
||
</label>
|
||
|
||
<label class="param-checkbox">
|
||
<input type="checkbox" name="objet_these_enabled" value="1"
|
||
<?= ($siteSettings['objet_these_enabled'] ?? '1') === '1' ? 'checked' : '' ?>
|
||
hx-post="/admin/actions/settings.php"
|
||
hx-trigger="change"
|
||
hx-target="#types-response"
|
||
hx-swap="innerHTML"
|
||
hx-include="#fieldset-types"
|
||
hx-on::before-request="console.log('[types-these] sending checked=' + this.checked)"
|
||
hx-on::after-request="console.log('[types-these] response received')">
|
||
<span>
|
||
<strong>Thèse</strong><br>
|
||
<small>Thèses doctorales</small>
|
||
</span>
|
||
</label>
|
||
|
||
<label class="param-checkbox">
|
||
<input type="checkbox" name="objet_frart_enabled" value="1"
|
||
<?= ($siteSettings['objet_frart_enabled'] ?? '1') === '1' ? 'checked' : '' ?>
|
||
hx-post="/admin/actions/settings.php"
|
||
hx-trigger="change"
|
||
hx-target="#types-response"
|
||
hx-swap="innerHTML"
|
||
hx-include="#fieldset-types"
|
||
hx-on::before-request="console.log('[types-frart] sending checked=' + this.checked)"
|
||
hx-on::after-request="console.log('[types-frart] response received')">
|
||
<span>
|
||
<strong>Frart</strong><br>
|
||
<small>Formation de recherche en art</small>
|
||
</span>
|
||
</label>
|
||
</div>
|
||
|
||
<div id="types-response" aria-live="polite"></div>
|
||
</fieldset>
|
||
|
||
<fieldset>
|
||
<legend>Structure du Formulaire</legend>
|
||
<p class="fhb-hint">
|
||
Chaque <strong>bloc d'aide</strong> s'affiche au-dessus de sa section dans le formulaire de soumission.
|
||
Le <strong>bouton rond</strong> active/désactive l'affichage.
|
||
</p>
|
||
<p>
|
||
<a href="/admin/structure-formulaire.php" class="btn btn--primary btn--sm">Gérer la structure du formulaire (page dédiée)</a>
|
||
</p>
|
||
</fieldset>
|
||
</section>
|
||
</article>
|
||
</main>
|
||
|
||
<!-- ══════════════════════════════════════════════════════════════
|
||
CONFIRM DIALOGS FOR LANGUES
|
||
══════════════════════════════════════════════════════════════ -->
|
||
|
||
<dialog id="langues-delete-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="langues-delete-title">
|
||
<div class="admin-dialog__header">
|
||
<h2 id="langues-delete-title">Supprimer la langue</h2>
|
||
<button type="button" class="admin-dialog__close" aria-label="Fermer"
|
||
onclick="this.closest('dialog').close()">✕</button>
|
||
</div>
|
||
<div class="admin-dialog__alert">
|
||
<p>Supprimer « <strong id="langues-delete-name"></strong> » ? Cette action est irréversible.</p>
|
||
</div>
|
||
<div class="admin-dialog__footer">
|
||
<button type="button" class="btn btn--danger" onclick="this.closest('dialog').close(); languesSubmitPending()">Supprimer</button>
|
||
<button type="button" class="btn btn--secondary" onclick="this.closest('dialog').close()">Annuler</button>
|
||
</div>
|
||
</dialog>
|
||
|
||
<dialog id="langues-bulk-merge-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="langues-bulk-merge-title">
|
||
<div class="admin-dialog__header">
|
||
<h2 id="langues-bulk-merge-title">Fusionner des langues</h2>
|
||
<button type="button" class="admin-dialog__close" aria-label="Fermer"
|
||
onclick="this.closest('dialog').close()">✕</button>
|
||
</div>
|
||
<div class="admin-dialog__alert">
|
||
<p>Fusionner <strong><span id="langues-bulk-merge-count"></span> langue(s)</strong> sélectionnée(s) dans :</p>
|
||
<select id="langues-bulk-merge-target-select" class="admin-select--inline" style="margin-top:var(--space-xs); width:100%" required>
|
||
<option value="">— Choisir la destination —</option>
|
||
</select>
|
||
</div>
|
||
<div class="admin-dialog__footer">
|
||
<button type="button" class="btn btn--warning" onclick="languesExecBulkMerge()">Fusionner</button>
|
||
<button type="button" class="btn btn--secondary" onclick="this.closest('dialog').close()">Annuler</button>
|
||
</div>
|
||
</dialog>
|
||
|
||
<dialog id="langues-bulk-delete-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="langues-bulk-delete-title">
|
||
<div class="admin-dialog__header">
|
||
<h2 id="langues-bulk-delete-title">Supprimer des langues</h2>
|
||
<button type="button" class="admin-dialog__close" aria-label="Fermer"
|
||
onclick="this.closest('dialog').close()">✕</button>
|
||
</div>
|
||
<div class="admin-dialog__alert">
|
||
<p>Supprimer définitivement <strong><span id="langues-bulk-delete-count"></span> langue(s)</strong> ? Cette action est irréversible.</p>
|
||
</div>
|
||
<div class="admin-dialog__footer">
|
||
<button type="button" class="btn btn--danger" onclick="languesExecBulkDelete()">Supprimer</button>
|
||
<button type="button" class="btn btn--secondary" onclick="this.closest('dialog').close()">Annuler</button>
|
||
</div>
|
||
</dialog>
|
||
|
||
<script src="<?= App::assetV('/assets/js/app/admin-contenus-langues.js') ?>"></script>
|
||
<script>
|
||
// ── Inline rename via HTMX (needs PHP-generated icon SVGs) ───────────────
|
||
function languesStartRename(id) {
|
||
var cell = document.getElementById('lang-name-' + id);
|
||
var csrf = document.querySelector('input[name="csrf_token"]').value;
|
||
cell.innerHTML = '<form hx-post="/admin/actions/language.php" hx-target="#langues-table-wrap" hx-swap="innerHTML" class="admin-inline-form">'
|
||
+ '<input type="hidden" name="csrf_token" value="' + csrf + '">'
|
||
+ '<input type="hidden" name="action" value="rename">'
|
||
+ '<input type="hidden" name="return" value="/admin/contenus.php">'
|
||
+ '<input type="hidden" name="language_id" value="' + id + '">'
|
||
+ '<input type="text" name="new_name" value="' + cell.getAttribute('data-name') + '" required class="admin-input--inline">'
|
||
+ '<button type="submit" class="admin-icon-btn admin-icon-btn--edit" title="Valider">'
|
||
+ '<?= icon('check-circle') ?>'
|
||
+ '</button>'
|
||
+ '<button type="button" class="admin-icon-btn admin-icon-btn--delete" onclick="languesCancelRename(' + id + ')" title="Annuler">'
|
||
+ '<?= icon('x-close') ?>'
|
||
+ '</button></form>';
|
||
cell.querySelector('input').focus();
|
||
}
|
||
|
||
function languesCancelRename(id) {
|
||
var cell = document.getElementById('lang-name-' + id);
|
||
cell.innerHTML = '<span class="tag-name-cell">' + cell.getAttribute('data-name') + '</span>'
|
||
+ '<button type="button" class="admin-icon-btn admin-icon-btn--edit" title="Renommer" onclick="languesStartRename(' + id + ')">'
|
||
+ '<?= icon('pencil-note') ?>'
|
||
+ '</button>';
|
||
}
|
||
</script>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════════
|
||
CONFIRM DIALOGS FOR MOTS-CLÉS
|
||
══════════════════════════════════════════════════════════════ -->
|
||
|
||
<dialog id="motscles-delete-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="motscles-delete-title">
|
||
<div class="admin-dialog__header">
|
||
<h2 id="motscles-delete-title">Supprimer le mot-clé</h2>
|
||
<button type="button" class="admin-dialog__close" aria-label="Fermer"
|
||
onclick="this.closest('dialog').close()">✕</button>
|
||
</div>
|
||
<div class="admin-dialog__alert">
|
||
<p>Supprimer « <strong id="motscles-delete-name"></strong> » ? Cette action est irréversible.</p>
|
||
</div>
|
||
<div class="admin-dialog__footer">
|
||
<button type="button" class="btn btn--danger" onclick="this.closest('dialog').close(); motsclesSubmitPending()">Supprimer</button>
|
||
<button type="button" class="btn btn--secondary" onclick="this.closest('dialog').close()">Annuler</button>
|
||
</div>
|
||
</dialog>
|
||
|
||
<dialog id="motscles-bulk-merge-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="motscles-bulk-merge-title">
|
||
<div class="admin-dialog__header">
|
||
<h2 id="motscles-bulk-merge-title">Fusionner des mots-clés</h2>
|
||
<button type="button" class="admin-dialog__close" aria-label="Fermer"
|
||
onclick="this.closest('dialog').close()">✕</button>
|
||
</div>
|
||
<div class="admin-dialog__alert">
|
||
<p>Fusionner <strong><span id="motscles-bulk-merge-count"></span> mot(s)-clé(s)</strong> sélectionné(s) dans :</p>
|
||
<select id="motscles-bulk-merge-target-select" class="admin-select--inline" style="margin-top:var(--space-xs); width:100%" required>
|
||
<option value="">— Choisir la destination —</option>
|
||
</select>
|
||
</div>
|
||
<div class="admin-dialog__footer">
|
||
<button type="button" class="btn btn--warning" onclick="motsclesExecBulkMerge()">Fusionner</button>
|
||
<button type="button" class="btn btn--secondary" onclick="this.closest('dialog').close()">Annuler</button>
|
||
</div>
|
||
</dialog>
|
||
|
||
<dialog id="motscles-bulk-delete-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="motscles-bulk-delete-title">
|
||
<div class="admin-dialog__header">
|
||
<h2 id="motscles-bulk-delete-title">Supprimer des mots-clés</h2>
|
||
<button type="button" class="admin-dialog__close" aria-label="Fermer"
|
||
onclick="this.closest('dialog').close()">✕</button>
|
||
</div>
|
||
<div class="admin-dialog__alert">
|
||
<p>Supprimer définitivement <strong><span id="motscles-bulk-delete-count"></span> mot(s)-clé(s)</strong> ? Cette action est irréversible.</p>
|
||
</div>
|
||
<div class="admin-dialog__footer">
|
||
<button type="button" class="btn btn--danger" onclick="motsclesExecBulkDelete()">Supprimer</button>
|
||
<button type="button" class="btn btn--secondary" onclick="this.closest('dialog').close()">Annuler</button>
|
||
</div>
|
||
</dialog>
|
||
|
||
<script src="<?= App::assetV('/assets/js/app/admin-contenus-motscles.js') ?>"></script>
|
||
<script>
|
||
// ── Inline rename via HTMX (needs PHP-generated icon SVGs) ───────────────
|
||
function motsclesStartRename(id) {
|
||
var cell = document.getElementById('motscles-name-' + id);
|
||
var csrf = document.querySelector('input[name="csrf_token"]').value;
|
||
cell.innerHTML = '<form hx-post="/admin/actions/tag.php" hx-target="#motscles-table-wrap" hx-swap="innerHTML" class="admin-inline-form">'
|
||
+ '<input type="hidden" name="csrf_token" value="' + csrf + '">'
|
||
+ '<input type="hidden" name="action" value="rename">'
|
||
+ '<input type="hidden" name="return" value="/admin/contenus.php">'
|
||
+ '<input type="hidden" name="tag_id" value="' + id + '">'
|
||
+ '<input type="text" name="new_name" value="' + cell.getAttribute('data-name') + '" required class="admin-input--inline">'
|
||
+ '<button type="submit" class="admin-icon-btn admin-icon-btn--edit" title="Valider">'
|
||
+ '<?= icon('check-circle') ?>'
|
||
+ '</button>'
|
||
+ '<button type="button" class="admin-icon-btn admin-icon-btn--delete" onclick="motsclesCancelRename(' + id + ')" title="Annuler">'
|
||
+ '<?= icon('x-close') ?>'
|
||
+ '</button></form>';
|
||
cell.querySelector('input').focus();
|
||
}
|
||
|
||
function motsclesCancelRename(id) {
|
||
var cell = document.getElementById('motscles-name-' + id);
|
||
cell.innerHTML = '<span class="tag-name-cell">' + cell.getAttribute('data-name') + '</span>'
|
||
+ '<button type="button" class="admin-icon-btn admin-icon-btn--edit" title="Renommer" onclick="motsclesStartRename(' + id + ')">'
|
||
+ '<?= icon('pencil-note') ?>'
|
||
+ '</button>';
|
||
}
|
||
</script>
|
||
|
||
<script>
|
||
(function () {
|
||
var otScript = document.createElement('script');
|
||
otScript.src = '<?= App::assetV('/assets/js/vendor/overtype.min.js') ?>';
|
||
document.head.appendChild(otScript);
|
||
|
||
var handlerScript = document.createElement('script');
|
||
handlerScript.src = '<?= App::assetV('/assets/js/app/autosave-handler.js') ?>';
|
||
document.head.appendChild(handlerScript);
|
||
})();
|
||
</script>
|