Files
xamxam/app/templates/admin/index.php
Pontoporeia 048a14bc2e Add language-search component for Autre Langue input + active search in lists
Mirrors the mots-clé tag-search system: dropdown suggestions from
existing languages via HTMX, pill display with bin-icon remove buttons,
'Créer' option for new languages. Replaces the plain text input.

- New partial: templates/partials/form/language-search.php
- New fragment: public/partage/language-search-fragment.php
- Admin wrapper: public/admin/language-search-fragment.php
- Updated language-autre-fragment to return just the required asterisk indicator
- Updated both controllers to handle language_autre as array (pill-based)
  with backward-compatible string path
- Updated edit form to compute selectedOtherLanguages from DB
- Registered new route in partage/index.php
- Fix CSV importer: split comma-separated language column into individual entries
- Add htmx active search to admin index, title line-clamp, predefined languages only in checkboxes
- Admin index: filter form now uses htmx triggers (input delay:300ms on search,
  change on selects) to actively search without page reload
- Sort links include hx-push-url for back-button support
- Added loading indicator bar (.admin-search-indicator)
- Title column: line-clamp at 2 lines with overflow hidden, native title attr
  tooltip for full text
- Language checkboxes now show only 3 predefined languages (Français, Anglais,
  Néerlandais); all others go via the Autre langue search component
- Added Database::getPredefinedLanguages() and excluded predefined from
  language-search-fragment suggestions
- Included hidden sort/dir inputs in table-wrap so sort state preserved across
  filter changes
- Fix language-search: block 'Créer' for predefined languages in dropdown
  The 'Créer' option in the language-search dropdown now also checks against the
  predefined set (français, anglais, néerlandais) to avoid offering creation of
  languages that already exist as checkboxes.
2026-05-19 00:08:05 +02:00

259 lines
15 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script>
function toggleAll(src){document.querySelectorAll('input[name="selected_theses[]"]').forEach(cb=>cb.checked=src.checked);updateBulk();}
function updateBulk(){const n=document.querySelectorAll('input[name="selected_theses[]"]:checked').length;const b=document.getElementById('bulk-actions');document.getElementById('selected-count').textContent=n;b.style.display=n>0?'flex':'none';}
function confirmBulk(act){const n=document.querySelectorAll('input[name="selected_theses[]"]:checked').length;if(!n){document.getElementById('no-selection-dialog').showModal();return;}document.getElementById('bulk-action-input').value=act;if(act==='delete'){document.getElementById('bulk-delete-count').textContent=n;document.getElementById('bulk-delete-dialog').showModal();}else{document.getElementById('bulk-confirm-word').textContent=act==='publish'?'Publier':'Dépublier';document.getElementById('bulk-confirm-count').textContent=n;document.getElementById('bulk-confirm-dialog').showModal();}}
function execBulk(){const a=document.getElementById('bulk-action-input').value;const f=document.getElementById('bulk-form');f.action = a==='delete' ? 'actions/delete.php' : 'actions/publish.php';const c=document.getElementById('bulk-checkboxes');c.innerHTML='';document.querySelectorAll('input[name="selected_theses[]"]:checked').forEach(cb=>{const inp=document.createElement('input');inp.type='hidden';inp.name='selected_theses[]';inp.value=cb.value;c.appendChild(inp);});f.submit();}
function confirmDelete(id,title){document.getElementById('delete-thesis-title').textContent=title;document.getElementById('delete-thesis-dialog').showModal();document.getElementById('delete-dialog-confirm').onclick=function(){document.getElementById('delete-form-'+id).submit();};}
document.addEventListener('DOMContentLoaded',()=>{document.querySelectorAll('input[name="selected_theses[]"]').forEach(cb=>cb.addEventListener('change',updateBulk));});
document.addEventListener('htmx:afterSwap',()=>{document.querySelectorAll('input[name="selected_theses[]"]').forEach(cb=>cb.addEventListener('change',updateBulk));updateBulk();});
</script>
<main id="main-content" class="admin-main--list">
<!-- Title + filters + stats + import all in one toolbar row -->
<div class="admin-list-toolbar admin-list-toolbar--list">
<div class="admin-toolbar-top">
<div class="admin-toolbar-title-row">
<h1>Liste des TFE</h1>
<div class="admin-stats">
<fieldset class="admin-stat">
<legend class="admin-stat__label">Total</legend>
<span class="admin-stat__number"><?= $stats['total'] ?></span>
</fieldset>
<fieldset class="admin-stat admin-stat--pub">
<legend class="admin-stat__label">Publiés</legend>
<span class="admin-stat__number"><?= $stats['published'] ?></span>
</fieldset>
<fieldset class="admin-stat admin-stat--pend">
<legend class="admin-stat__label">Attente</legend>
<span class="admin-stat__number"><?= $stats['pending'] ?></span>
</fieldset>
</div>
</div>
<div class="admin-btn-group">
<a href="/admin/add.php" class="btn btn--primary btn--sm">+ Ajouter un TFE</a>
<a href="/admin/tags.php" class="btn btn--primary btn--sm">Mots-clés</a>
<button type="button" class="btn btn--primary btn--sm" id="import-dialog-btn"
onclick="document.getElementById('import-dialog').showModal()">
Importer
</button>
<button type="button" class="btn btn--primary btn--sm" id="export-dialog-btn"
onclick="document.getElementById('export-dialog').showModal()">
Exporter
</button>
</div>
</div>
<form id="admin-filter-form" class="admin-filters" method="get" action="/admin/"
hx-get="/admin/"
hx-trigger="change from:select, input changed delay:300ms from:input[name=search], keyup[key=='Enter'] from:input[name=search]"
hx-target="#admin-table-wrap"
hx-swap="innerHTML"
hx-indicator="#admin-search-indicator"
hx-include="#admin-filter-form, #admin-table-wrap input[name=sort], #admin-table-wrap input[name=dir]"
hx-push-url="true"
hx-sync="#admin-filter-form:replace">
<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['code'] ?? $ap['name']) ?>
</option>
<?php endforeach; ?>
</select>
<?php if ($searchQuery || $yearFilter || $orientationFilter || $apFilter): ?>
<a href="/admin/" class="btn btn--secondary btn--sm admin-filters-reset">&#x2715; Réinitialiser</a>
<?php endif; ?>
</form>
<!-- Loading indicator bar -->
<div id="admin-search-indicator" class="admin-search-indicator"></div>
</div>
<?php include APP_ROOT . '/templates/admin/index-table.php'; ?>
</main>
<!-- ══════════════════════════════════════════════════════════════
CONFIRM DIALOGS (replacing browser alert/confirm)
══════════════════════════════════════════════════════════════ -->
<!-- No-selection alert -->
<dialog id="no-selection-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="no-sel-title">
<div class="admin-dialog__header">
<h2 id="no-sel-title">Aucune sélection</h2>
<button type="button" class="admin-dialog__close" aria-label="Fermer"
onclick="this.closest('dialog').close()">&#x2715;</button>
</div>
<div class="admin-dialog__alert">
<p>Sélectionnez au moins un TFE avant d'effectuer une action groupée.</p>
</div>
<div class="admin-dialog__footer">
<button type="button" class="btn btn--primary" onclick="this.closest('dialog').close()">OK</button>
</div>
</dialog>
<!-- Bulk publish/unpublish confirm -->
<dialog id="bulk-confirm-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="bulk-confirm-title">
<div class="admin-dialog__header">
<h2 id="bulk-confirm-title">Confirmation</h2>
<button type="button" class="admin-dialog__close" aria-label="Fermer"
onclick="this.closest('dialog').close()">&#x2715;</button>
</div>
<div class="admin-dialog__alert">
<p><span id="bulk-confirm-word"></span> <span id="bulk-confirm-count"></span> TFE(s) ?</p>
</div>
<div class="admin-dialog__footer">
<button type="button" class="btn btn--primary" onclick="this.closest('dialog').close(); execBulk()">Confirmer</button>
<button type="button" class="btn btn--secondary" onclick="this.closest('dialog').close()">Annuler</button>
</div>
</dialog>
<!-- Bulk delete confirm -->
<dialog id="bulk-delete-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="bulk-delete-title">
<div class="admin-dialog__header">
<h2 id="bulk-delete-title">Supprimer des TFE</h2>
<button type="button" class="admin-dialog__close" aria-label="Fermer"
onclick="this.closest('dialog').close()">&#x2715;</button>
</div>
<div class="admin-dialog__alert">
<p>Supprimer définitivement <strong><span id="bulk-delete-count"></span> TFE(s)</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(); execBulk()">Supprimer</button>
<button type="button" class="btn btn--secondary" onclick="this.closest('dialog').close()">Annuler</button>
</div>
</dialog>
<!-- Single thesis delete confirm -->
<dialog id="delete-thesis-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="delete-thesis-title-label">
<div class="admin-dialog__header">
<h2 id="delete-thesis-title-label">Supprimer ce TFE</h2>
<button type="button" class="admin-dialog__close" aria-label="Fermer"
onclick="this.closest('dialog').close()">&#x2715;</button>
</div>
<div class="admin-dialog__alert">
<p>Supprimer « <strong id="delete-thesis-title"></strong> » ? Cette action est irréversible.</p>
</div>
<div class="admin-dialog__footer">
<button type="button" class="btn btn--danger" id="delete-dialog-confirm" onclick="this.closest('dialog').close()">Supprimer</button>
<button type="button" class="btn btn--secondary" onclick="this.closest('dialog').close()">Annuler</button>
</div>
</dialog>
<!-- ══════════════════════════════════════════════════════════════
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()">&#x2715;</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="btn btn--primary">Importer</button>
<button type="button" class="btn 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>
<!-- ══════════════════════════════════════════════════════════════
EXPORT DIALOG
══════════════════════════════════════════════════════════════ -->
<dialog id="export-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="export-dialog-title">
<div class="admin-dialog__header">
<h2 id="export-dialog-title">Exporter</h2>
<button type="button" class="admin-dialog__close" aria-label="Fermer"
onclick="document.getElementById('export-dialog').close()">&#x2715;</button>
</div>
<form method="get" action="/admin/actions/export.php" class="admin-form">
<div class="admin-form-group">
<label class="admin-checkbox-label">
<input type="checkbox" name="csv" value="1" checked>
CSV (tableau des métadonnées)
</label>
</div>
<div class="admin-form-group">
<label class="admin-checkbox-label">
<input type="checkbox" name="files" value="1" checked>
Fichiers (archive ZIP)
</label>
</div>
<div class="admin-form-group">
<label class="admin-checkbox-label">
<input type="checkbox" name="db" value="1">
Base de données (SQLite)
</label>
</div>
<div class="admin-form-footer">
<button type="submit" class="btn btn--primary">Exporter</button>
<button type="button" class="btn btn--secondary"
onclick="document.getElementById('export-dialog').close()">Annuler</button>
</div>
</form>
</dialog>
<?php if ($importMessage || !empty($importErrors)): ?>
<script>document.getElementById('import-dialog').showModal();</script>
<?php endif; ?>