mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 08:09:18 +02:00
387 lines
22 KiB
PHP
387 lines
22 KiB
PHP
<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';document.getElementById('admin-table-wrap').style.setProperty('--sticky-top',n>0?b.offsetHeight+'px':'0px');}
|
||
function getSelectedIds(){return Array.from(document.querySelectorAll('input[name="selected_theses[]"]:checked')).map(cb=>cb.value);}
|
||
function confirmBulk(act){const ids=getSelectedIds();if(!ids.length){document.getElementById('no-selection-dialog').showModal();return;}const n=ids.length;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='';getSelectedIds().forEach(id=>{const inp=document.createElement('input');inp.type='hidden';inp.name='selected_theses[]';inp.value=id;c.appendChild(inp);});f.submit();}
|
||
function confirmExport(){const ids=getSelectedIds();if(!ids.length){document.getElementById('no-selection-dialog').showModal();return;}window.location.href='/admin/actions/export.php?csv=1&ids='+ids.join(',');}
|
||
function confirmExportFiles(){const ids=getSelectedIds();if(!ids.length){document.getElementById('no-selection-dialog').showModal();return;}window.location.href='/admin/actions/export.php?files=1&ids='+ids.join(',');}
|
||
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>
|
||
<?php if ($trashCount > 0): ?>
|
||
<a href="/admin/index.php?tab=trash" class="btn btn--sm <?= $tab === 'trash' ? 'btn--primary' : 'btn--secondary' ?>">
|
||
Corbeille (<?= $trashCount ?>)
|
||
</a>
|
||
<?php endif; ?>
|
||
<?php if ($tmpTotalCount > 0): ?>
|
||
<button type="button" class="btn btn--sm btn--secondary" id="tmp-cleanup-btn"
|
||
onclick="document.getElementById('tmp-cleanup-dialog').showModal(); fetchTmpStats()">
|
||
Nettoyer (<?= $tmpTotalCount ?>)
|
||
</button>
|
||
<?php endif; ?>
|
||
<button type="button" class="btn btn--primary btn--sm" id="import-dialog-btn"
|
||
onclick="document.getElementById('import-dialog').showModal(); window.XamxamInitFilePonds()">
|
||
Importer
|
||
</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="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>
|
||
<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>
|
||
<?php if ($searchQuery || $yearFilter || $orientationFilter || $apFilter): ?>
|
||
<a href="/admin/" class="btn btn--secondary btn--sm admin-filters-reset">✕ 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()">✕</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()">✕</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()">✕</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()">✕</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()">✕</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="admin-import-status-card__success" role="status">✓ <?= htmlspecialchars($importMessage) ?></p>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($importMessage): ?>
|
||
<?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; ?>
|
||
<div class="admin-form-footer">
|
||
<button type="button" class="btn btn--primary"
|
||
onclick="document.getElementById('import-dialog').close(); window.location.href = window.location.pathname">Terminé</button>
|
||
</div>
|
||
<?php else: ?>
|
||
<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"
|
||
class="tfe-file-picker"
|
||
data-queue-type="csv_import"
|
||
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; ?>
|
||
<?php endif; ?>
|
||
</dialog>
|
||
|
||
<!-- ══════════════════════════════════════════════════════════════
|
||
TMP CLEANUP DIALOG
|
||
══════════════════════════════════════════════════════════════ -->
|
||
<dialog id="tmp-cleanup-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="tmp-cleanup-title">
|
||
<div class="admin-dialog__header">
|
||
<h2 id="tmp-cleanup-title">Nettoyer les fichiers temporaires</h2>
|
||
<button type="button" class="admin-dialog__close" aria-label="Fermer"
|
||
onclick="document.getElementById('tmp-cleanup-dialog').close()">✕</button>
|
||
</div>
|
||
<div class="admin-dialog__body">
|
||
<p>Les fichiers temporaires s'accumulent lorsque des téléversements sont abandonnés (formulaire fermé avant envoi).</p>
|
||
<div id="tmp-cleanup-stats" class="admin-dialog__stats">
|
||
Chargement…
|
||
</div>
|
||
<p class="admin-dialog__hint">
|
||
Seuls les fichiers de plus de 2 heures (FilePond) et 30 jours (corbeille) seront supprimés.
|
||
Les téléversements récents sont conservés.
|
||
</p>
|
||
|
||
<div id="tmp-cleanup-result" style="display:none"></div>
|
||
</div>
|
||
<div class="admin-dialog__footer">
|
||
<button type="button" class="btn btn--danger" id="tmp-cleanup-confirm"
|
||
onclick="executeTmpCleanup()">
|
||
Nettoyer
|
||
</button>
|
||
<button type="button" class="btn btn--secondary"
|
||
onclick="document.getElementById('tmp-cleanup-dialog').close()">Annuler</button>
|
||
</div>
|
||
</dialog>
|
||
|
||
<script>
|
||
async function fetchTmpStats() {
|
||
const el = document.getElementById('tmp-cleanup-stats');
|
||
try {
|
||
const resp = await fetch('/admin/actions/cleanup-stats.php');
|
||
const data = await resp.json();
|
||
let html = '';
|
||
const totalStale = data.filepond_stale_count + data.trash_stale_count;
|
||
|
||
if (totalStale === 0) {
|
||
html = '<p style="margin:0;color:var(--accent-green)">✓ Aucun fichier obsolète à nettoyer.</p>';
|
||
if ((data.filepond_active_count || 0) + (data.trash_active_count || 0) > 0) {
|
||
html += '<p style="margin:var(--space-xs) 0 0 0;font-size:0.85em;color:var(--text-secondary)">';
|
||
if (data.filepond_active_count) html += `📁 ${data.filepond_active_count} téléversement(s) actif(s) (session existante) — ${data.filepond_active_human}<br>`;
|
||
if (data.trash_active_count) html += `🗑️ ${data.trash_active_count} fichier(s) récent(s) en corbeille — ${data.trash_active_human}`;
|
||
html += '</p>';
|
||
}
|
||
} else {
|
||
html = `<p style="margin:0 0 var(--space-xs) 0;font-weight:600">⚠️ ${totalStale} élément(s) obsolète(s) à nettoyer :</p>`;
|
||
if (data.filepond_stale_count) {
|
||
html += `<details style="margin:0 0 var(--space-xs) 0;font-size:0.9em" open><summary>📁 <strong>Téléversements abandonnés</strong> : ${data.filepond_stale_count} dossier(s) — ${data.filepond_stale_human} <span style="font-size:0.85em;color:var(--text-secondary)">(session expirée ou >2h)</span></summary>`;
|
||
if (data.filepond_stale_files) {
|
||
html += '<ul style="margin:var(--space-xs) 0 0 var(--space-md);max-height:10em;overflow-y:auto">';
|
||
data.filepond_stale_files.forEach(f => { html += `<li>${f.name} <span style="color:var(--text-secondary)">(${f.human}, ~${Math.round(f.age_minutes)}min)</span></li>`; });
|
||
html += '</ul>';
|
||
}
|
||
html += '</details>';
|
||
}
|
||
if (data.trash_stale_count) {
|
||
html += `<details style="margin:0 0 var(--space-xs) 0;font-size:0.9em" open><summary>🗑️ <strong>Fichiers supprimés orphelins</strong> : ${data.trash_stale_count} fichier(s) — ${data.trash_stale_human} <span style="font-size:0.85em;color:var(--text-secondary)">(référence DB disparue ou >30j)</span></summary>`;
|
||
if (data.trash_stale_files) {
|
||
html += '<ul style="margin:var(--space-xs) 0 0 var(--space-md);max-height:10em;overflow-y:auto">';
|
||
data.trash_stale_files.forEach(f => { html += `<li>${f.name} <span style="color:var(--text-secondary)">(${f.human}, ~${Math.round(f.age_days)}j)</span></li>`; });
|
||
html += '</ul>';
|
||
}
|
||
html += '</details>';
|
||
}
|
||
if (data.filepond_active_count || data.trash_active_count) {
|
||
html += '<p style="margin:var(--space-xs) 0 0 0;font-size:0.85em;color:var(--text-secondary)">Conservés : ';
|
||
if (data.filepond_active_count) html += `${data.filepond_active_count} téléversement(s) actif(s), `;
|
||
if (data.trash_active_count) html += `${data.trash_active_count} fichier(s) récent(s)`;
|
||
html += '</p>';
|
||
}
|
||
}
|
||
el.innerHTML = html;
|
||
|
||
if (totalStale === 0) {
|
||
document.getElementById('tmp-cleanup-confirm').disabled = true;
|
||
document.getElementById('tmp-cleanup-confirm').textContent = 'Rien à nettoyer';
|
||
}
|
||
} catch (e) {
|
||
el.textContent = 'Erreur lors du chargement des statistiques.';
|
||
}
|
||
}
|
||
|
||
async function executeTmpCleanup() {
|
||
const btn = document.getElementById('tmp-cleanup-confirm');
|
||
const result = document.getElementById('tmp-cleanup-result');
|
||
btn.disabled = true;
|
||
btn.textContent = 'Nettoyage…';
|
||
try {
|
||
const resp = await fetch('/admin/actions/cleanup-tmp.php', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||
body: 'csrf_token=' + encodeURIComponent('<?= htmlspecialchars($_SESSION['csrf_token']) ?>')
|
||
});
|
||
const data = await resp.json();
|
||
result.style.display = 'block';
|
||
if (data.success) {
|
||
const total = data.filepond_removed + data.trash_removed;
|
||
if (total === 0) {
|
||
result.className = 'flash-success';
|
||
result.innerHTML = '✓ Aucun fichier obsolète trouvé.';
|
||
} else {
|
||
result.className = 'flash-success';
|
||
let msg = `✓ ${total} élément(s) supprimé(s) : `;
|
||
if (data.filepond_removed) msg += `${data.filepond_removed} téléversement(s) abandonné(s), `;
|
||
if (data.trash_removed) msg += `${data.trash_removed} fichier(s) orphelin(s)`;
|
||
result.innerHTML = msg;
|
||
if (data.details && data.details.length > 0) {
|
||
result.innerHTML += '<details style="margin-top:var(--space-xs);font-size:0.85em"><summary>Détails (' + data.details.length + ')</summary><ul style="margin:var(--space-xs) 0 0 var(--space-md)">' +
|
||
data.details.map(d => '<li>' + d + '</li>').join('') + '</ul></details>';
|
||
}
|
||
}
|
||
setTimeout(() => {
|
||
document.getElementById('tmp-cleanup-dialog').close();
|
||
window.location.reload();
|
||
}, 2000);
|
||
} else {
|
||
result.className = 'flash-error';
|
||
result.textContent = 'Erreur : ' + (data.error || 'inconnue');
|
||
btn.disabled = false;
|
||
btn.textContent = 'Réessayer';
|
||
}
|
||
} catch (e) {
|
||
result.style.display = 'block';
|
||
result.className = 'flash-error';
|
||
result.textContent = 'Erreur réseau.';
|
||
btn.disabled = false;
|
||
btn.textContent = 'Réessayer';
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<?php if ($importMessage || !empty($importErrors)): ?>
|
||
<script>document.getElementById('import-dialog').showModal();</script>
|
||
<?php endif; ?>
|