Files
photoBooth/frontend/templates/admin/gallery.html
T
admin 4592f20be3
🚀 Deploy — JH Photomaton / 🔍 Vérification (push) Has been cancelled
🚀 Deploy — JH Photomaton / 🍓 Deploy sur le Pi (push) Has been cancelled
fix: mtime index dans processed_full, print 404, galerie full-width
2026-07-17 16:29:06 +02:00

742 lines
33 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Galerie admin — JH Photomaton{% endblock %}
{% block nav_links %}
<a href="/admin">Dashboard</a>
<a href="/admin/gallery" class="active">Galerie</a>
<a href="/admin/actions">Actions</a>
<a href="/admin/print">Impression</a>
<a href="/admin/settings">Réglages</a>
<a href="/admin/events">Événements</a>
<a href="/admin/logout">Déconnexion</a>
{% endblock %}
{% block head %}
<style>
/* ── Layout ───────────────────────────────────────────────────────────────── */
.gallery-wrap { max-width: 100%; margin: 0; padding: 1rem 1.5rem; }
/* ── 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(200px, 1fr)); gap: .85rem; }
.photo-card {
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 { border-color: #e07b00; }
.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;
display: flex; align-items: center; gap: .25rem;
pointer-events: none;
}
.print-badge.printing { background: rgba(25,108,176,.9); }
.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);
display: flex; align-items: flex-end; justify-content: center;
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;
}
.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: .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; }
/* ── Lightbox ─────────────────────────────────────────────────────────────── */
.lightbox {
display: none; position: fixed; inset: 0; z-index: 1000;
background: rgba(0,0,0,.92); backdrop-filter: blur(8px);
align-items: center; justify-content: center; padding: 1.5rem;
gap: 1rem;
}
.lightbox.open { display: flex; }
.lb-nav-btn {
flex-shrink: 0; width: 44px; height: 44px; border-radius: 50%;
border: 1px solid rgba(255,255,255,.2); background: rgba(255,255,255,.08);
color: #fff; font-size: 1.3rem; cursor: pointer; display: flex;
align-items: center; justify-content: center; transition: background .15s;
user-select: none;
}
.lb-nav-btn:hover:not(:disabled) { background: rgba(255,255,255,.18); }
.lb-nav-btn:disabled { opacity: .2; cursor: default; }
.lb-modal {
background: var(--surface); border: 1px solid var(--border);
border-radius: 16px; overflow: hidden;
max-width: min(1100px, calc(95vw - 110px)); width: 100%;
display: flex; flex-direction: column; max-height: 95vh;
}
.lb-counter {
font-size: .75rem; color: var(--text-muted); white-space: nowrap;
}
.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: 72vh; 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; align-items: center; gap: .75rem; padding: .25rem 0;
border-bottom: 1px solid rgba(255,255,255,.05); font-size: .82rem;
}
.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; }
/* 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; }
.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 %}
{% block content %}
<div class="gallery-wrap">
<div class="gallery-header">
<div class="gallery-title">🖼 Galerie — Administration</div>
<div class="gallery-controls">
<span class="text-sm text-muted" id="photo-count">Chargement…</span>
<button class="pg-btn" onclick="resetFilters()">✕ Réinitialiser</button>
<button class="pg-btn" onclick="loadPhotos(1)"></button>
</div>
</div>
<!-- ── 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 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>
<button class="pg-btn" id="next-btn" onclick="changePage(1)">Suiv. →</button>
</div>
<div class="photo-grid" id="photo-grid">
<div style="color:var(--text-muted);grid-column:1/-1;text-align:center;padding:2rem">⏳ Chargement…</div>
</div>
<div class="pagination" style="margin-top:.5rem">
<button class="pg-btn" id="prev-btn2" onclick="changePage(-1)" disabled>← Préc.</button>
<span class="pg-info">Page <span id="page-cur2">1</span> / <span id="page-total2">1</span></span>
<button class="pg-btn" id="next-btn2" onclick="changePage(1)">Suiv. →</button>
</div>
</div>
<!-- ── Lightbox ─────────────────────────────────────────────────────────────── -->
<div class="lightbox" id="lightbox" onclick="lbBackdropClose(event)">
<button class="lb-nav-btn" id="lb-prev" onclick="lbNavigate(-1);event.stopPropagation()"></button>
<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>
<span class="lb-counter" id="lb-counter"></span>
<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>
<button class="lb-nav-btn" id="lb-next" onclick="lbNavigate(1);event.stopPropagation()"></button>
</div>
{% endblock %}
{% block scripts %}
<script>
let currentPage = 1;
let totalPages = 1;
let allPhotos = [];
let displayedPhotos = []; // photos actuellement dans la grille (après filtre statut)
let currentIndex = -1; // index dans displayedPhotos
let currentPhotoId = null;
let currentPhotoData = null;
// 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
// ════════════════════════════════════════════════════════════════════════════
async function loadPhotos(page = 1) {
const grid = document.getElementById('photo-grid');
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?${_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();
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 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) {
displayedPhotos = photos || [];
const grid = document.getElementById('photo-grid');
if (!displayedPhotos.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;
}
// On utilise l'index (pas le JSON sérialisé) pour éviter les problèmes de quotes
grid.innerHTML = displayedPhotos.map((p, idx) => {
const pid = p.photo_id || p.id || '';
const hasPending = p.print_pending > 0;
const hasPrinting = p.print_printing > 0;
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' : ''}" id="card-${pid}"
onclick="openLightboxIdx(${idx})">
<img src="${p.thumb_url}" loading="lazy" alt="">
${badge}${dateLabel}
<div class="card-overlay">
${hasPending
? `<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>
</div>
</div>`;
}).join('');
}
// ════════════════════════════════════════════════════════════════════════════
// Lightbox
// ════════════════════════════════════════════════════════════════════════════
function openLightboxIdx(idx) {
if (idx < 0 || idx >= displayedPhotos.length) return;
currentIndex = idx;
const photo = displayedPhotos[idx];
currentPhotoData = photo;
currentPhotoId = photo.photo_id || photo.id || '';
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;
document.getElementById('lb-counter').textContent = `${idx + 1} / ${displayedPhotos.length}`;
// Boutons nav
document.getElementById('lb-prev').disabled = idx <= 0;
document.getElementById('lb-next').disabled = idx >= displayedPhotos.length - 1;
_updateLbPrintPanel(photo);
document.getElementById('lightbox').classList.add('open');
}
function lbNavigate(dir) {
openLightboxIdx(currentIndex + dir);
}
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));
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="${r.status === 'printing' ? 's-printing' : 's-pending'}">
${r.status === 'printing' ? '🔵 En cours' : '⏳ En attente'}
</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>`
: ''}
</div>`).join('');
} else {
panel.classList.remove('visible');
btnCancel.style.display = 'none';
list.innerHTML = '';
}
}
function closeLightbox() {
document.getElementById('lightbox').classList.remove('open');
document.getElementById('lb-img').src = '';
currentPhotoId = currentPhotoData = null;
currentIndex = -1;
}
function lbBackdropClose(e) {
if (e.target === document.getElementById('lightbox')) closeLightbox();
}
// Clavier : Échap + flèches
document.addEventListener('keydown', e => {
const lb = document.getElementById('lightbox');
if (!lb.classList.contains('open')) return;
if (e.key === 'Escape') { closeLightbox(); }
else if (e.key === 'ArrowLeft') { lbNavigate(-1); }
else if (e.key === 'ArrowRight') { lbNavigate(1); }
});
// Swipe tactile
let _touchX = 0;
document.getElementById('lightbox').addEventListener('touchstart', e => {
_touchX = e.touches[0].clientX;
}, { passive: true });
document.getElementById('lightbox').addEventListener('touchend', e => {
const dx = e.changedTouches[0].clientX - _touchX;
if (Math.abs(dx) > 50) lbNavigate(dx < 0 ? 1 : -1);
}, { passive: true });
// ════════════════════════════════════════════════════════════════════════════
// Actions impression
// ════════════════════════════════════════════════════════════════════════════
function getLbCopies() { return parseInt(document.getElementById('lb-copies').value) || 1; }
async function lbAddToQueue() {
const copies = getLbCopies();
try {
await api('POST', `/admin/api/gallery/print/${currentPhotoId}?copies=${copies}&immediate=false`);
showToast(`📋 Ajouté à la file (${copies} copie${copies > 1 ? 's' : ''})`, 'info');
await _refreshPhotoData(currentPhotoId);
} catch(e) { showToast('Erreur : ' + e.message, 'error'); }
}
async function lbPrintNow() {
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`);
showToast(r.success ? `✅ Imprimé sur ${r.printer}` : `❌ ${r.error || 'Erreur'}`, r.success ? 'success' : 'error');
await _refreshPhotoData(currentPhotoId);
} catch(e) { showToast('Erreur : ' + e.message, 'error'); }
}
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', 'error'); }
}
async function cancelOneEntry(entryId) {
try {
await api('POST', `/api/print/cancel/${entryId}`);
showToast('Demande annulée', 'info');
await _refreshPhotoData(currentPhotoId);
} catch(e) { showToast('Erreur', 'error'); }
}
async function lbDelete() {
if (!confirm('Supprimer cette photo définitivement ?')) return;
try {
const r = await api('DELETE', `/admin/api/gallery/${currentPhotoId}`);
if (r.ok) {
showToast('🗑 Photo supprimée', 'success');
closeLightbox();
loadPhotos(currentPage);
} else showToast('❌ Erreur suppression', 'error');
} catch(e) { showToast('Erreur', 'error'); }
}
async function quickQueue(pid) {
try {
await api('POST', `/admin/api/gallery/print/${pid}?copies=1&immediate=false`);
showToast('📋 Ajouté à la file', 'info');
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} annulée(s)`, 'info');
await _refreshPhotoData(pid);
} catch(e) { showToast('Erreur', 'error'); }
}
async function deletePhoto(pid) {
if (!confirm('Supprimer cette photo ?')) return;
try {
const r = await api('DELETE', `/admin/api/gallery/${pid}`);
if (r.ok) { showToast('🗑 Supprimée', 'success'); loadPhotos(currentPage); }
} catch(e) { showToast('Erreur', 'error'); }
}
// ════════════════════════════════════════════════════════════════════════════
// Helpers
// ════════════════════════════════════════════════════════════════════════════
async function _refreshPhotoData(pid) {
try {
const data = await api('GET', `/admin/api/gallery/photos?${_buildParams(currentPage)}`);
allPhotos = data.photos || [];
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;
// Si lightbox ouverte sur cette photo, mettre à jour le panel impression
if (currentPhotoId === pid) {
const newIdx = displayedPhotos.findIndex(p => (p.photo_id || p.id) === pid);
if (newIdx >= 0) {
currentIndex = newIdx;
currentPhotoData = displayedPhotos[newIdx];
_updateLbPrintPanel(currentPhotoData);
document.getElementById('lb-prev').disabled = newIdx <= 0;
document.getElementById('lb-next').disabled = newIdx >= displayedPhotos.length - 1;
document.getElementById('lb-counter').textContent = `${newIdx + 1} / ${displayedPhotos.length}`;
}
}
} catch(e) {}
}
function updatePagination() {
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); }
// ════════════════════════════════════════════════════════════════════════════
// 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 === 'print_request' && msg.photo_id) {
_refreshPhotoData(msg.photo_id);
showToast('🖨 Demande d\'impression reçue', 'info');
}
if (msg.type === 'print_result') {
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);
}
}
// 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 n = status[pid] || 0;
if (p.print_pending !== n) { p.print_pending = n; changed = true; }
});
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) {}
}, 30000);
loadEventOptions();
loadPhotos(1);
</script>
{% endblock %}