feat: admin gallery - lightbox with icons, filters by date/event/status; fix: event badges - only current slug shows En cours; fix: archive_all_open on new event; fix: mtime_index uses stem for event photo filter
🚀 Deploy — JH Photomaton / 🔍 Vérification (push) Has been cancelled
🚀 Deploy — JH Photomaton / 🍓 Deploy sur le Pi (push) Has been cancelled

This commit is contained in:
2026-07-17 15:59:51 +02:00
parent 223d71883e
commit c89924ea18
6 changed files with 508 additions and 288 deletions
+13 -5
View File
@@ -142,8 +142,13 @@ async function loadCurrentEvent() {
}
}
let _currentSlug = '';
async function loadHistory() {
try {
// Récupère d'abord le slug courant pour l'affichage correct des badges
try { const cur = await api('GET', '/event'); _currentSlug = cur.slug || ''; } catch(e) {}
const d = await api('GET', '/event/history');
document.getElementById('history-loading').style.display = 'none';
const list = document.getElementById('history-list');
@@ -154,13 +159,16 @@ async function loadHistory() {
return;
}
list.innerHTML = d.events.map(ev => `
list.innerHTML = d.events.map(ev => {
// "En cours" uniquement si c'est le slug actif ET sans ended_at
const isActive = !ev.ended_at && ev.slug === _currentSlug;
return `
<div class="event-row">
<div>
<div class="event-name">
${ev.name}
<span class="badge ${ev.ended_at ? 'badge-done' : 'badge-active'}">
${ev.ended_at ? 'Terminé' : 'En cours'}
<span class="badge ${isActive ? 'badge-active' : 'badge-done'}">
${isActive ? 'En cours' : (ev.ended_at ? 'Terminé' : 'Archivé')}
</span>
</div>
<div class="event-meta">
@@ -179,8 +187,8 @@ async function loadHistory() {
<a href="/api/event/${ev.slug}/export" class="btn btn-primary btn-sm" download>
⬇️ ZIP
</a>
</div>
`).join('');
</div>`;
}).join('');
} catch(e) {
document.getElementById('history-loading').textContent = 'Erreur chargement';
}
+429 -267
View File
@@ -13,68 +13,90 @@
{% block head %}
<style>
/* ── Photo grid ───────────────────────────────────────────────────────────── */
/* ── Layout ───────────────────────────────────────────────────────────────── */
.gallery-wrap { max-width: 1300px; margin: 0 auto; padding: 1rem; }
.gallery-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap; }
.gallery-title { font-size: 1.2rem; font-weight: 700; }
.gallery-controls { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; }
.gallery-filters { display: flex; align-items: center; gap: .5rem; margin-bottom: .75rem; flex-wrap: wrap; }
.filter-btn { padding: .3rem .75rem; border-radius: 20px; border: 1px solid var(--border); background: transparent; color: var(--text-muted); cursor: pointer; font-size: .85rem; transition: all .2s; }
.filter-btn.active, .filter-btn:hover { background: var(--primary); border-color: var(--primary); color: #fff; }
.filter-badge { background: rgba(224,123,0,.2); color: #e07b00; padding: .15rem .45rem; border-radius: 10px; font-size: .75rem; font-weight: 700; }
/* ── Barre de filtres ─────────────────────────────────────────────────────── */
.filter-bar {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: .85rem 1.1rem;
margin-bottom: 1rem;
display: flex;
flex-wrap: wrap;
gap: .75rem;
align-items: flex-end;
}
.filter-group { display: flex; flex-direction: column; gap: .3rem; }
.filter-label { font-size: .74rem; color: var(--text-muted); font-weight: 600; text-transform: uppercase; letter-spacing: .04em; }
.filter-chips { display: flex; gap: .35rem; flex-wrap: wrap; }
.chip {
padding: .28rem .7rem; border-radius: 20px; border: 1px solid var(--border);
background: transparent; color: var(--text-muted); cursor: pointer;
font-size: .82rem; transition: all .15s;
}
.chip.active, .chip:hover { background: var(--primary); border-color: var(--primary); color: #fff; }
.chip.danger.active { background: #7a0000; border-color: #7a0000; }
.filter-select {
background: var(--bg); border: 1px solid var(--border); color: var(--text);
border-radius: 8px; padding: .3rem .6rem; font-size: .85rem;
}
.filter-divider { width: 1px; background: var(--border); align-self: stretch; margin: 0 .2rem; }
.filter-summary { font-size: .82rem; color: var(--text-muted); margin-left: auto; align-self: center; }
/* ── Header ───────────────────────────────────────────────────────────────── */
.gallery-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: .75rem; flex-wrap: wrap; }
.gallery-title { font-size: 1.15rem; font-weight: 700; }
.gallery-controls { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; }
.copies-wrap { display: flex; align-items: center; gap: .4rem; font-size: .85rem; color: var(--text-muted); }
.copies-wrap input { width: 52px; background: var(--bg); border: 1px solid var(--border); color: var(--text); border-radius: 6px; padding: .3rem .5rem; text-align: center; }
/* ── Grille photos ────────────────────────────────────────────────────────── */
.photo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: .75rem; }
.photo-card {
position: relative;
aspect-ratio: 3/2;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
background: var(--surface);
border: 2px solid transparent;
position: relative; aspect-ratio: 3/2; border-radius: 8px; overflow: hidden;
cursor: pointer; background: var(--surface); border: 2px solid transparent;
transition: border-color .2s, transform .15s;
}
.photo-card:hover { border-color: var(--primary); transform: scale(1.02); }
.photo-card.has-print-request { border-color: #e07b00; }
.photo-card img { width: 100%; height: 100%; object-fit: cover; display: block; }
.photo-card.has-print { border-color: #e07b00; }
/* Badge demande d'impression */
.print-badge {
position: absolute;
top: 5px; right: 5px;
background: rgba(224,123,0,.9);
color: #fff;
border-radius: 12px;
padding: .15rem .45rem;
font-size: .72rem;
font-weight: 700;
position: absolute; top: 5px; right: 5px;
background: rgba(224,123,0,.9); color: #fff;
border-radius: 12px; padding: .15rem .45rem;
font-size: .72rem; font-weight: 700;
display: flex; align-items: center; gap: .25rem;
backdrop-filter: blur(4px);
pointer-events: none;
}
.print-badge.printing { background: rgba(25,108,176,.9); }
/* Overlay actions au hover */
.date-badge {
position: absolute; bottom: 0; left: 0; right: 0;
background: linear-gradient(transparent, rgba(0,0,0,.65));
color: rgba(255,255,255,.85);
font-size: .68rem; padding: .4rem .4rem .3rem;
pointer-events: none; text-align: right;
}
.card-overlay {
position: absolute; inset: 0;
background: rgba(0,0,0,.55);
position: absolute; inset: 0; background: rgba(0,0,0,.55);
display: flex; align-items: flex-end; justify-content: center;
gap: .4rem; padding: .5rem;
opacity: 0; transition: opacity .2s;
gap: .4rem; padding: .5rem; opacity: 0; transition: opacity .2s;
}
.photo-card:hover .card-overlay { opacity: 1; }
.ov-btn {
padding: .3rem .5rem; border: none; border-radius: 6px;
cursor: pointer; font-size: .8rem; font-weight: 700;
backdrop-filter: blur(4px);
}
.ov-print { background: rgba(224,123,0,.85); color: #fff; }
.ov-cancel { background: rgba(192,0,0,.75); color: #fff; }
.ov-delete { background: rgba(80,80,80,.75); color: #fff; }
.ov-dl { background: rgba(40,40,40,.75); color: #ccc; text-decoration: none; }
.ov-print { background: rgba(224,123,0,.85); color: #fff; }
.ov-cancel { background: rgba(192,0,0,.75); color: #fff; }
.ov-delete { background: rgba(80,80,80,.75); color: #fff; }
.ov-dl { background: rgba(40,40,40,.75); color: #ccc; text-decoration: none; display: inline-block; }
/* Pagination */
.pagination { display: flex; align-items: center; gap: .75rem; justify-content: center; margin: 1rem 0; }
/* ── Pagination ───────────────────────────────────────────────────────────── */
.pagination { display: flex; align-items: center; gap: .75rem; justify-content: center; margin: .75rem 0; }
.pg-btn { padding: .4rem .9rem; border: 1px solid var(--border); background: var(--surface); border-radius: 6px; color: var(--text); cursor: pointer; }
.pg-btn:disabled { opacity: .35; cursor: default; }
.pg-info { color: var(--text-muted); font-size: .9rem; }
@@ -82,45 +104,73 @@
/* ── Lightbox ─────────────────────────────────────────────────────────────── */
.lightbox {
display: none; position: fixed; inset: 0; z-index: 1000;
background: rgba(0,0,0,.88); backdrop-filter: blur(6px);
flex-direction: column; align-items: center; justify-content: center;
gap: 1rem; padding: 1.5rem;
background: rgba(0,0,0,.92); backdrop-filter: blur(8px);
align-items: center; justify-content: center; padding: 1.5rem;
}
.lightbox.open { display: flex; }
.lightbox-img-wrap { position: relative; max-width: 80vw; max-height: 70vh; }
.lightbox-img-wrap img { max-width: 80vw; max-height: 70vh; border-radius: 8px; object-fit: contain; display: block; }
.lb-close { position: absolute; top: -14px; right: -14px; width: 28px; height: 28px; border-radius: 50%; background: rgba(255,255,255,.15); border: none; color: #fff; font-size: 1.1rem; cursor: pointer; display: flex; align-items: center; justify-content: center; }
.lb-info { text-align: center; }
.lb-id { font-size: .78rem; color: var(--text-muted); font-family: monospace; }
/* Bloc demandes en attente dans le lightbox */
.lb-print-status {
background: var(--surface); border-radius: 10px; padding: .85rem 1.25rem;
min-width: min(400px, 80vw); border: 1px solid var(--border);
.lb-modal {
background: var(--surface); border: 1px solid var(--border);
border-radius: 16px; overflow: hidden;
max-width: min(900px, 95vw); width: 100%;
display: flex; flex-direction: column; max-height: 95vh;
}
.lb-print-title { font-size: .85rem; font-weight: 700; margin-bottom: .6rem; display: flex; align-items: center; gap: .5rem; }
.lb-header {
display: flex; align-items: center; justify-content: space-between;
padding: .75rem 1.1rem; border-bottom: 1px solid var(--border);
}
.lb-header-info { display: flex; flex-direction: column; gap: .1rem; }
.lb-date { font-weight: 600; font-size: .95rem; }
.lb-id { font-size: .72rem; color: var(--text-muted); font-family: monospace; }
.lb-close-btn {
width: 32px; height: 32px; border-radius: 50%; background: rgba(255,255,255,.08);
border: 1px solid var(--border); color: var(--text); cursor: pointer;
font-size: 1.1rem; display: flex; align-items: center; justify-content: center;
}
.lb-img-wrap {
flex: 1; overflow: hidden; display: flex; align-items: center; justify-content: center;
background: #111; min-height: 200px;
}
.lb-img-wrap img { max-width: 100%; max-height: 55vh; object-fit: contain; display: block; }
/* Bloc demandes impression */
.lb-print-panel {
padding: .6rem 1.1rem; border-top: 1px solid var(--border);
background: rgba(224,123,0,.06); display: none;
}
.lb-print-panel.visible { display: block; }
.lb-print-title { font-size: .82rem; font-weight: 700; color: #e07b00; margin-bottom: .4rem; }
.lb-queue-entry {
display: flex; justify-content: space-between; align-items: center;
padding: .3rem 0; border-bottom: 1px solid rgba(255,255,255,.06);
font-size: .82rem; gap: .75rem;
display: flex; align-items: center; gap: .75rem; padding: .25rem 0;
border-bottom: 1px solid rgba(255,255,255,.05); font-size: .82rem;
}
.lb-queue-status { padding: .15rem .5rem; border-radius: 10px; font-size: .75rem; font-weight: 700; }
.s-pending { background: rgba(224,123,0,.2); color: #e07b00; }
.s-printing { background: rgba(25,108,176,.2); color: var(--primary); }
.lb-queue-entry:last-child { border-bottom: none; }
.s-pending { background: rgba(224,123,0,.2); color: #e07b00; padding: .15rem .45rem; border-radius: 10px; font-size: .75rem; font-weight: 700; }
.s-printing { background: rgba(25,108,176,.2); color: var(--primary); padding: .15rem .45rem; border-radius: 10px; font-size: .75rem; font-weight: 700; }
.lb-actions { display: flex; gap: .6rem; flex-wrap: wrap; justify-content: center; }
.lb-btn { padding: .5rem 1.1rem; border: none; border-radius: 8px; cursor: pointer; font-size: .9rem; font-weight: 600; transition: opacity .2s; }
.lb-btn:hover { opacity: .82; }
.lb-btn-print { background: var(--primary); color: #fff; }
.lb-btn-now { background: #1a7340; color: #fff; }
.lb-btn-cancel { background: rgba(192,0,0,.25); color: #e05050; border: 1px solid rgba(192,0,0,.3); }
.lb-btn-delete { background: #3d1f1f; color: #e05050; border: 1px solid rgba(192,0,0,.2); }
.lb-btn-dl { background: rgba(255,255,255,.08); color: var(--text); text-decoration: none; display: inline-flex; align-items: center; }
/* Actions lightbox */
.lb-actions {
padding: .85rem 1.1rem; border-top: 1px solid var(--border);
display: flex; gap: .6rem; flex-wrap: wrap; align-items: center;
}
.lb-copies-wrap { display: flex; align-items: center; gap: .4rem; font-size: .85rem; color: var(--text-muted); margin-right: auto; }
.lb-copies-wrap input { width: 52px; background: var(--bg); border: 1px solid var(--border); color: var(--text); border-radius: 6px; padding: .3rem .5rem; text-align: center; }
/* copies input */
.copies-wrap { display: flex; align-items: center; gap: .4rem; font-size: .85rem; color: var(--text-muted); }
.copies-wrap input { width: 52px; background: var(--bg); border: 1px solid var(--border); color: var(--text); border-radius: 6px; padding: .3rem .5rem; text-align: center; }
.lb-action-btn {
display: flex; flex-direction: column; align-items: center; gap: .2rem;
padding: .55rem .85rem; border: none; border-radius: 10px;
cursor: pointer; font-size: .78rem; font-weight: 600; transition: opacity .2s;
text-decoration: none; min-width: 60px;
}
.lb-action-btn:hover { opacity: .82; }
.lb-action-btn .icon { font-size: 1.3rem; line-height: 1; }
.lba-queue { background: rgba(224,123,0,.18); color: #e07b00; border: 1px solid rgba(224,123,0,.3); }
.lba-print { background: rgba(25,108,176,.2); color: var(--primary); border: 1px solid rgba(25,108,176,.3); }
.lba-dl { background: rgba(255,255,255,.07); color: var(--text); border: 1px solid var(--border); }
.lba-cancel { background: rgba(192,0,0,.15); color: #e05050; border: 1px solid rgba(192,0,0,.25); display: none; }
.lba-delete { background: rgba(80,20,20,.4); color: #e05050; border: 1px solid rgba(192,0,0,.2); }
</style>
{% endblock %}
@@ -131,23 +181,52 @@
<div class="gallery-title">🖼 Galerie — Administration</div>
<div class="gallery-controls">
<span class="text-sm text-muted" id="photo-count">Chargement…</span>
<div class="copies-wrap">
<label for="copies-input">Copies :</label>
<input type="number" id="copies-input" min="1" max="3" value="1">
</div>
<button class="pg-btn" onclick="refreshPhotos()"></button>
<button class="pg-btn" onclick="resetFilters()">✕ Réinitialiser</button>
<button class="pg-btn" onclick="loadPhotos(1)"></button>
</div>
</div>
<!-- Filtres -->
<div class="gallery-filters">
<button class="filter-btn active" onclick="setFilter('all', this)">Toutes</button>
<button class="filter-btn" onclick="setFilter('pending', this)">
🖨 À imprimer <span class="filter-badge" id="pending-count">0</span>
</button>
<!-- ── Barre de filtres ──────────────────────────────────────────────────── -->
<div class="filter-bar">
<!-- Filtre statut -->
<div class="filter-group">
<div class="filter-label">Statut</div>
<div class="filter-chips">
<button class="chip active" onclick="setStatusFilter('all', this)">Toutes</button>
<button class="chip danger" onclick="setStatusFilter('pending', this)">
🖨 À imprimer <span id="pending-count" style="font-weight:700">0</span>
</button>
</div>
</div>
<div class="filter-divider"></div>
<!-- Filtre date -->
<div class="filter-group">
<div class="filter-label">Date</div>
<div class="filter-chips">
<button class="chip" onclick="setDateFilter('all', this)">Toutes</button>
<button class="chip" onclick="setDateFilter('today', this)">Aujourd'hui</button>
<button class="chip" onclick="setDateFilter('week', this)">Cette semaine</button>
<button class="chip" onclick="setDateFilter('month', this)">Ce mois</button>
</div>
</div>
<div class="filter-divider"></div>
<!-- Filtre événement -->
<div class="filter-group">
<div class="filter-label">Événement</div>
<select class="filter-select" id="event-filter" onchange="setEventFilter(this.value)">
<option value="">Tous les événements</option>
</select>
</div>
<div class="filter-summary" id="filter-summary"></div>
</div>
<!-- Pagination -->
<!-- Pagination haut -->
<div class="pagination">
<button class="pg-btn" id="prev-btn" onclick="changePage(-1)" disabled>← Préc.</button>
<span class="pg-info">Page <span id="page-cur">1</span> / <span id="page-total">1</span></span>
@@ -167,49 +246,127 @@
</div>
<!-- ── Lightbox ─────────────────────────────────────────────────────────────── -->
<div class="lightbox" id="lightbox">
<div class="lightbox" id="lightbox" onclick="lbBackdropClose(event)">
<div class="lb-modal">
<!-- En-tête -->
<div class="lb-header">
<div class="lb-header-info">
<div class="lb-date" id="lb-date"></div>
<div class="lb-id" id="lb-id"></div>
</div>
<button class="lb-close-btn" onclick="closeLightbox()"></button>
</div>
<!-- Image -->
<div class="lb-img-wrap">
<img id="lb-img" src="" alt="">
</div>
<!-- Demandes impression en attente -->
<div class="lb-print-panel" id="lb-print-panel">
<div class="lb-print-title">🖨 Demandes d'impression en attente</div>
<div id="lb-queue-list"></div>
</div>
<!-- Actions -->
<div class="lb-actions">
<div class="lb-copies-wrap">
<label for="lb-copies">Copies :</label>
<input type="number" id="lb-copies" min="1" max="3" value="1">
</div>
<button class="lb-action-btn lba-queue" onclick="lbAddToQueue()">
<span class="icon">📋</span>File d'attente
</button>
<button class="lb-action-btn lba-print" onclick="lbPrintNow()">
<span class="icon">🖨</span>Imprimer
</button>
<a class="lb-action-btn lba-dl" id="lb-btn-dl" href="#">
<span class="icon"></span>Télécharger
</a>
<button class="lb-action-btn lba-cancel" id="lb-btn-cancel" onclick="lbCancelAll()">
<span class="icon"></span>Annuler
</button>
<button class="lb-action-btn lba-delete" onclick="lbDelete()">
<span class="icon">🗑</span>Supprimer
</button>
</div>
<div class="lightbox-img-wrap">
<img id="lb-img" src="" alt="">
<button class="lb-close" onclick="closeLightbox()"></button>
</div>
<div class="lb-info">
<div class="lb-id" id="lb-id"></div>
</div>
<!-- Bloc demandes d'impression en attente -->
<div class="lb-print-status" id="lb-print-status" style="display:none">
<div class="lb-print-title">🖨 Demandes d'impression en attente</div>
<div id="lb-queue-list"></div>
</div>
<!-- Boutons d'action -->
<div class="lb-actions">
<button class="lb-btn lb-btn-print" id="lb-btn-queue" onclick="lbAddToQueue()">
📋 Ajouter à la file
</button>
<button class="lb-btn lb-btn-now" id="lb-btn-now" onclick="lbPrintNow()">
🖨 Imprimer maintenant
</button>
<button class="lb-btn lb-btn-cancel" id="lb-btn-cancel-all" onclick="lbCancelAll()" style="display:none">
✕ Annuler la demande
</button>
<a class="lb-btn lb-btn-dl" id="lb-btn-dl" download>⬇ Télécharger</a>
<button class="lb-btn lb-btn-delete" onclick="lbDelete()">🗑 Supprimer</button>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let currentPage = 1;
let totalPages = 1;
let currentPhotoId = null;
let currentPage = 1;
let totalPages = 1;
let allPhotos = [];
let currentPhotoId = null;
let currentPhotoData = null;
let allPhotos = [];
let filterMode = 'all';
// Filtres actifs
let statusFilter = 'all';
let dateFilter = 'all';
let eventSlug = '';
// ════════════════════════════════════════════════════════════════════════════
// Filtres
// ════════════════════════════════════════════════════════════════════════════
function setStatusFilter(mode, btn) {
statusFilter = mode;
document.querySelectorAll('.filter-chips .chip').forEach(b => {
if (b.closest('.filter-group') === btn.closest('.filter-group')) b.classList.remove('active');
});
btn.classList.add('active');
loadPhotos(1);
}
function setDateFilter(mode, btn) {
dateFilter = mode;
document.querySelectorAll('.filter-chips .chip').forEach(b => {
if (b.closest('.filter-group') === btn.closest('.filter-group')) b.classList.remove('active');
});
btn.classList.add('active');
loadPhotos(1);
}
function setEventFilter(slug) {
eventSlug = slug;
loadPhotos(1);
}
function resetFilters() {
statusFilter = 'all';
dateFilter = 'all';
eventSlug = '';
document.getElementById('event-filter').value = '';
document.querySelectorAll('.chip').forEach(b => b.classList.remove('active'));
document.querySelector('.chip[onclick*="all"]').classList.add('active');
loadPhotos(1);
}
function _buildParams(page) {
const params = new URLSearchParams({ page, limit: 24 });
if (eventSlug) params.set('event_slug', eventSlug);
if (dateFilter !== 'all') {
const now = Date.now() / 1000;
const sod = new Date(); sod.setHours(0,0,0,0);
const sodTs = sod.getTime() / 1000;
if (dateFilter === 'today') {
params.set('date_from', sodTs);
params.set('date_to', now);
} else if (dateFilter === 'week') {
params.set('date_from', now - 7 * 86400);
params.set('date_to', now);
} else if (dateFilter === 'month') {
params.set('date_from', now - 30 * 86400);
params.set('date_to', now);
}
}
return params.toString();
}
// ════════════════════════════════════════════════════════════════════════════
// Chargement photos
@@ -219,158 +376,154 @@ async function loadPhotos(page = 1) {
grid.innerHTML = '<div style="color:var(--text-muted);grid-column:1/-1;text-align:center;padding:2rem">⏳ Chargement…</div>';
try {
const data = await api('GET', `/admin/api/gallery/photos?page=${page}&limit=24`);
allPhotos = data.photos || [];
currentPage = page;
totalPages = data.pages || 1;
const data = await api('GET', `/admin/api/gallery/photos?${_buildParams(page)}`);
allPhotos = data.photos || [];
currentPage = page;
totalPages = data.pages || 1;
// Filtre statut côté client
let photos = allPhotos;
if (statusFilter === 'pending') {
photos = allPhotos.filter(p => p.print_pending > 0 || p.print_printing > 0);
}
document.getElementById('photo-count').textContent =
`${photos.length} / ${data.total} photo(s)`;
document.getElementById('pending-count').textContent =
allPhotos.filter(p => p.print_pending > 0).length;
updatePagination();
document.getElementById('photo-count').textContent = `${data.total} photo(s)`;
// Compteur de demandes en attente
const pendingTotal = allPhotos.filter(p => p.print_pending > 0).length;
document.getElementById('pending-count').textContent = pendingTotal;
renderGrid();
renderGrid(photos);
updateFilterSummary(data.total);
} catch(e) {
grid.innerHTML = '<div style="color:#e05050;grid-column:1/-1;text-align:center;padding:2rem">❌ Erreur de chargement</div>';
}
}
function renderGrid() {
function updateFilterSummary(total) {
const parts = [];
if (dateFilter !== 'all') parts.push({ today: "Aujourd'hui", week: "Cette semaine", month: "Ce mois" }[dateFilter]);
if (eventSlug) parts.push('Événement : ' + (document.getElementById('event-filter').options[document.getElementById('event-filter').selectedIndex]?.text || eventSlug));
document.getElementById('filter-summary').textContent = parts.length ? `Filtre : ${parts.join(' · ')}${total} photo(s)` : '';
}
// ════════════════════════════════════════════════════════════════════════════
// Rendu grille
// ════════════════════════════════════════════════════════════════════════════
function renderGrid(photos) {
const grid = document.getElementById('photo-grid');
let photos = allPhotos;
if (filterMode === 'pending') {
photos = allPhotos.filter(p => p.print_pending > 0 || p.print_printing > 0);
}
if (!photos.length) {
grid.innerHTML = filterMode === 'pending'
? '<div style="color:var(--text-muted);grid-column:1/-1;text-align:center;padding:2rem">✅ Aucune demande d\'impression en attente</div>'
: '<div style="color:var(--text-muted);grid-column:1/-1;text-align:center;padding:2rem">📷 Aucune photo</div>';
if (!photos || !photos.length) {
grid.innerHTML = `<div style="color:var(--text-muted);grid-column:1/-1;text-align:center;padding:2rem">
📷 Aucune photo pour ces filtres</div>`;
return;
}
grid.innerHTML = photos.map(p => {
const pid = p.photo_id || p.id || p.filename || '';
const hasPending = p.print_pending > 0;
const pid = p.photo_id || p.id || '';
const hasPending = p.print_pending > 0;
const hasPrinting = p.print_printing > 0;
let badge = '';
if (hasPrinting) badge = `<div class="print-badge printing">🔵 Impression…</div>`;
else if (hasPending) badge = `<div class="print-badge">🖨 ${p.print_pending} en attente</div>`;
const badge = hasPrinting
? `<div class="print-badge printing">🔵 Impression…</div>`
: hasPending
? `<div class="print-badge">🖨 ${p.print_pending}</div>`
: '';
const dateLabel = p.date_label
? `<div class="date-badge">${p.date_label}</div>` : '';
return `
<div class="photo-card ${hasPending || hasPrinting ? 'has-print-request' : ''}"
id="card-${pid}"
onclick="openLightbox(${JSON.stringify(p)})">
<div class="photo-card ${hasPending || hasPrinting ? 'has-print' : ''}" id="card-${pid}"
onclick="openLightbox(${JSON.stringify(p).replace(/'/g, '&#39;')})">
<img src="${p.thumb_url}" loading="lazy" alt="">
${badge}
${badge}${dateLabel}
<div class="card-overlay">
${hasPending
? `<button class="ov-btn ov-cancel" onclick="event.stopPropagation();quickCancel('${pid}')">✕ Annuler</button>`
: `<button class="ov-btn ov-print" onclick="event.stopPropagation();quickQueue('${pid}')">🖨 File</button>`
}
? `<button class="ov-btn ov-cancel" onclick="event.stopPropagation();quickCancel('${pid}')">✕</button>`
: `<button class="ov-btn ov-print" onclick="event.stopPropagation();quickQueue('${pid}')">🖨</button>`}
<a class="ov-btn ov-dl" href="${p.download_url || p.full_url}" onclick="event.stopPropagation()">⬇</a>
<button class="ov-btn ov-delete" onclick="event.stopPropagation();deletePhoto('${pid}')">🗑</button>
<a class="ov-btn ov-dl" href="${p.full_url}" download onclick="event.stopPropagation()">⬇</a>
</div>
</div>
`;
</div>`;
}).join('');
}
function setFilter(mode, btn) {
filterMode = mode;
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
renderGrid();
}
// ════════════════════════════════════════════════════════════════════════════
// Lightbox
// ════════════════════════════════════════════════════════════════════════════
function openLightbox(photo) {
currentPhotoData = photo;
currentPhotoId = photo.photo_id || photo.id || photo.filename || '';
currentPhotoId = photo.photo_id || photo.id || '';
document.getElementById('lb-img').src = photo.full_url || photo.thumb_url;
document.getElementById('lb-id').textContent = currentPhotoId;
document.getElementById('lb-btn-dl').href = photo.full_url;
// Mise à jour du bloc demandes
updateLightboxPrintStatus(photo);
document.getElementById('lb-img').src = photo.full_url || photo.thumb_url;
document.getElementById('lb-date').textContent = photo.date_label || '—';
document.getElementById('lb-id').textContent = currentPhotoId;
document.getElementById('lb-btn-dl').href = photo.download_url || photo.full_url;
_updateLbPrintPanel(photo);
document.getElementById('lightbox').classList.add('open');
}
function updateLightboxPrintStatus(photo) {
const statusBlock = document.getElementById('lb-print-status');
const queueList = document.getElementById('lb-queue-list');
const btnCancelAll = document.getElementById('lb-btn-cancel-all');
function _updateLbPrintPanel(photo) {
const panel = document.getElementById('lb-print-panel');
const list = document.getElementById('lb-queue-list');
const btnCancel = document.getElementById('lb-btn-cancel');
const active = (photo.print_requests || []).filter(r => ['pending','printing'].includes(r.status));
const requests = photo.print_requests || [];
const activeRequests = requests.filter(r => r.status === 'pending' || r.status === 'printing');
if (activeRequests.length) {
statusBlock.style.display = 'block';
btnCancelAll.style.display = 'inline-flex';
queueList.innerHTML = activeRequests.map(r => `
if (active.length) {
panel.classList.add('visible');
btnCancel.style.display = 'flex';
list.innerHTML = active.map(r => `
<div class="lb-queue-entry">
<span>${r.copies} copie${r.copies > 1 ? 's' : ''}</span>
<span class="lb-queue-status ${r.status === 'printing' ? 's-printing' : 's-pending'}">
<span class="${r.status === 'printing' ? 's-printing' : 's-pending'}">
${r.status === 'printing' ? '🔵 En cours' : '⏳ En attente'}
</span>
<span style="font-size:.75rem;color:var(--text-muted)">${new Date(r.requested_at * 1000).toLocaleTimeString('fr-FR')}</span>
<span style="font-size:.75rem;color:var(--text-muted);margin-left:auto">
${new Date(r.requested_at * 1000).toLocaleTimeString('fr-FR')}
</span>
${r.status === 'pending'
? `<button class="ov-btn ov-cancel" style="padding:.2rem .5rem;font-size:.75rem"
onclick="cancelOneEntry('${r.id}')">✕</button>`
? `<button class="ov-btn ov-cancel" style="padding:.2rem .5rem;font-size:.75rem" onclick="cancelOneEntry('${r.id}')">✕</button>`
: ''}
</div>
`).join('');
</div>`).join('');
} else {
statusBlock.style.display = 'none';
btnCancelAll.style.display = 'none';
queueList.innerHTML = '';
panel.classList.remove('visible');
btnCancel.style.display = 'none';
list.innerHTML = '';
}
}
function closeLightbox() {
document.getElementById('lightbox').classList.remove('open');
currentPhotoId = null;
currentPhotoData = null;
document.getElementById('lb-img').src = '';
currentPhotoId = currentPhotoData = null;
}
document.getElementById('lightbox').addEventListener('click', e => {
function lbBackdropClose(e) {
if (e.target === document.getElementById('lightbox')) closeLightbox();
});
}
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeLightbox(); });
// ════════════════════════════════════════════════════════════════════════════
// Actions impression
// ════════════════════════════════════════════════════════════════════════════
function getCopies() {
return parseInt(document.getElementById('copies-input').value) || 1;
}
function getLbCopies() { return parseInt(document.getElementById('lb-copies').value) || 1; }
async function lbAddToQueue() {
const copies = getCopies();
const copies = getLbCopies();
try {
const r = await api('POST', `/admin/api/gallery/print/${currentPhotoId}?copies=${copies}&immediate=false`);
await api('POST', `/admin/api/gallery/print/${currentPhotoId}?copies=${copies}&immediate=false`);
showToast(`📋 Ajouté à la file (${copies} copie${copies > 1 ? 's' : ''})`, 'info');
// Mettre à jour la photo locale et le lightbox
await refreshPhotoData(currentPhotoId);
await _refreshPhotoData(currentPhotoId);
} catch(e) { showToast('Erreur : ' + e.message, 'error'); }
}
async function lbPrintNow() {
const copies = getCopies();
if (!confirm(`Imprimer ${copies} copie(s) immédiatement ?`)) return;
const copies = getLbCopies();
if (!confirm(`Imprimer ${copies} copie(s) maintenant ?`)) return;
try {
const r = await api('POST', `/admin/api/gallery/print/${currentPhotoId}?copies=${copies}&immediate=true`);
if (r.success) showToast(`✅ Imprimé sur ${r.printer}`, 'success');
else showToast('❌ ' + (r.error || 'Erreur'), 'error');
await refreshPhotoData(currentPhotoId);
showToast(r.success ? `✅ Imprimé sur ${r.printer}` : `${r.error || 'Erreur'}`, r.success ? 'success' : 'error');
await _refreshPhotoData(currentPhotoId);
} catch(e) { showToast('Erreur : ' + e.message, 'error'); }
}
@@ -378,15 +531,15 @@ async function lbCancelAll() {
try {
const r = await api('DELETE', `/admin/api/gallery/print/${currentPhotoId}`);
showToast(`${r.cancelled} demande(s) annulée(s)`, 'info');
await refreshPhotoData(currentPhotoId);
} catch(e) { showToast('Erreur : ' + e.message, 'error'); }
await _refreshPhotoData(currentPhotoId);
} catch(e) { showToast('Erreur', 'error'); }
}
async function cancelOneEntry(entryId) {
try {
await api('POST', `/api/print/cancel/${entryId}`);
showToast('Demande annulée', 'info');
await refreshPhotoData(currentPhotoId);
await _refreshPhotoData(currentPhotoId);
} catch(e) { showToast('Erreur', 'error'); }
}
@@ -399,24 +552,22 @@ async function lbDelete() {
closeLightbox();
loadPhotos(currentPage);
} else showToast('❌ Erreur suppression', 'error');
} catch(e) { showToast('Erreur : ' + e.message, 'error'); }
} catch(e) { showToast('Erreur', 'error'); }
}
// Actions depuis la grille (sans ouvrir le lightbox)
async function quickQueue(pid) {
const copies = getCopies();
try {
await api('POST', `/admin/api/gallery/print/${pid}?copies=${copies}&immediate=false`);
await api('POST', `/admin/api/gallery/print/${pid}?copies=1&immediate=false`);
showToast('📋 Ajouté à la file', 'info');
await refreshPhotoData(pid);
await _refreshPhotoData(pid);
} catch(e) { showToast('Erreur', 'error'); }
}
async function quickCancel(pid) {
try {
const r = await api('DELETE', `/admin/api/gallery/print/${pid}`);
showToast(`${r.cancelled} demande(s) annulée(s)`, 'info');
await refreshPhotoData(pid);
showToast(`${r.cancelled} annulée(s)`, 'info');
await _refreshPhotoData(pid);
} catch(e) { showToast('Erreur', 'error'); }
}
@@ -431,88 +582,99 @@ async function deletePhoto(pid) {
// ════════════════════════════════════════════════════════════════════════════
// Helpers
// ════════════════════════════════════════════════════════════════════════════
async function refreshPhotoData(pid) {
// Re-fetch le statut des demandes pour cette photo et met à jour l'UI
async function _refreshPhotoData(pid) {
try {
const status = await api('GET', '/admin/api/gallery/print-status');
const count = status[pid] || 0;
// Met à jour dans allPhotos
const idx = allPhotos.findIndex(p => (p.photo_id || p.id) === pid);
if (idx >= 0) {
allPhotos[idx].print_pending = count;
const data = await api('GET', `/admin/api/gallery/photos?${_buildParams(currentPage)}`);
allPhotos = data.photos || [];
const photo = allPhotos.find(p => (p.photo_id || p.id) === pid);
if (photo && currentPhotoId === pid) {
currentPhotoData = photo;
_updateLbPrintPanel(photo);
}
// Si le lightbox est ouvert pour cette photo, re-fetch les détails
if (currentPhotoId === pid) {
const data = await api('GET', `/admin/api/gallery/photos?page=${currentPage}&limit=24`);
const photo = (data.photos || []).find(p => (p.photo_id || p.id) === pid);
if (photo) {
currentPhotoData = photo;
updateLightboxPrintStatus(photo);
}
}
// Re-render la grille (met à jour le badge)
renderGrid();
const pendingTotal = allPhotos.filter(p => p.print_pending > 0).length;
document.getElementById('pending-count').textContent = pendingTotal;
let photos = allPhotos;
if (statusFilter === 'pending') photos = allPhotos.filter(p => p.print_pending > 0 || p.print_printing > 0);
renderGrid(photos);
document.getElementById('pending-count').textContent = allPhotos.filter(p => p.print_pending > 0).length;
} catch(e) {}
}
function updatePagination() {
for (const suffix of ['', '2']) {
const cur = document.getElementById(`page-cur${suffix}`);
const tot = document.getElementById(`page-total${suffix}`);
const prev = document.getElementById(`prev-btn${suffix}`);
const next = document.getElementById(`next-btn${suffix}`);
if (cur) cur.textContent = currentPage;
if (tot) tot.textContent = totalPages;
if (prev) prev.disabled = currentPage <= 1;
if (next) next.disabled = currentPage >= totalPages;
for (const s of ['', '2']) {
const c = document.getElementById(`page-cur${s}`);
const t = document.getElementById(`page-total${s}`);
const p = document.getElementById(`prev-btn${s}`);
const n = document.getElementById(`next-btn${s}`);
if (c) c.textContent = currentPage;
if (t) t.textContent = totalPages;
if (p) p.disabled = currentPage <= 1;
if (n) n.disabled = currentPage >= totalPages;
}
}
function changePage(delta) { loadPhotos(currentPage + delta); }
function refreshPhotos() { loadPhotos(currentPage); }
// ════════════════════════════════════════════════════════════════════════════
// Init — charge les événements pour le filtre
// ════════════════════════════════════════════════════════════════════════════
async function loadEventOptions() {
try {
const data = await api('GET', '/api/event/history');
const events = data.events || data || [];
const sel = document.getElementById('event-filter');
events.forEach(ev => {
const opt = document.createElement('option');
opt.value = ev.slug;
const date = ev.started_at ? new Date(ev.started_at * 1000).toLocaleDateString('fr-FR') : '';
opt.textContent = `${ev.name}${date ? ' — ' + date : ''}`;
sel.appendChild(opt);
});
// Ajouter aussi l'événement en cours s'il n'est pas dans l'historique
try {
const cur = await api('GET', '/api/event');
if (cur.slug && !events.find(e => e.slug === cur.slug)) {
const opt = document.createElement('option');
opt.value = cur.slug;
opt.textContent = `${cur.name} (en cours)`;
sel.insertBefore(opt, sel.children[1]);
}
} catch(e) {}
} catch(e) { console.warn('Impossible de charger les événements:', e); }
}
// ── WebSocket ─────────────────────────────────────────────────────────────────
function onWsMessage(msg) {
if (msg.type === 'photo_deleted') {
loadPhotos(currentPage);
}
if (msg.type === 'photo_deleted') loadPhotos(currentPage);
if (msg.type === 'print_request' && msg.photo_id) {
refreshPhotoData(msg.photo_id);
showToast(`🖨 Demande d'impression reçue`, 'info');
_refreshPhotoData(msg.photo_id);
showToast('🖨 Demande d\'impression reçue', 'info');
}
if (msg.type === 'print_result') {
const ok = msg.result && msg.result.success;
const ok = msg.result?.success;
showToast(ok ? `✅ Impression OK — ${msg.result.printer}` : `${msg.result?.error || 'Erreur'}`, ok ? 'success' : 'error');
if (msg.photo_id) refreshPhotoData(msg.photo_id);
}
if (msg.type === 'print_cancelled_for_photo' && msg.photo_id) {
refreshPhotoData(msg.photo_id);
if (msg.photo_id) _refreshPhotoData(msg.photo_id);
}
}
// Auto-refresh des badges toutes les 20s
// Auto-refresh badges toutes les 30s
setInterval(async () => {
try {
const status = await api('GET', '/admin/api/gallery/print-status');
let changed = false;
allPhotos.forEach(p => {
const pid = p.photo_id || p.id;
const newCount = status[pid] || 0;
if (p.print_pending !== newCount) {
p.print_pending = newCount;
changed = true;
}
const n = status[pid] || 0;
if (p.print_pending !== n) { p.print_pending = n; changed = true; }
});
if (changed) renderGrid();
if (changed) {
let photos = allPhotos;
if (statusFilter === 'pending') photos = allPhotos.filter(p => p.print_pending > 0 || p.print_printing > 0);
renderGrid(photos);
document.getElementById('pending-count').textContent = allPhotos.filter(p => p.print_pending > 0).length;
}
} catch(e) {}
}, 20000);
}, 30000);
loadEventOptions();
loadPhotos(1);
</script>
{% endblock %}