|
|
|
@@ -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-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>
|
|
|
|
|
<!-- ── 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>
|
|
|
|
|
|
|
|
|
|
<!-- Pagination -->
|
|
|
|
|
<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>
|
|
|
|
@@ -167,38 +246,54 @@
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- ── Lightbox ─────────────────────────────────────────────────────────────── -->
|
|
|
|
|
<div class="lightbox" id="lightbox">
|
|
|
|
|
<div class="lightbox" id="lightbox" onclick="lbBackdropClose(event)">
|
|
|
|
|
<div class="lb-modal">
|
|
|
|
|
|
|
|
|
|
<div class="lightbox-img-wrap">
|
|
|
|
|
<img id="lb-img" src="" alt="">
|
|
|
|
|
<button class="lb-close" onclick="closeLightbox()">✕</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="lb-info">
|
|
|
|
|
<!-- 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>
|
|
|
|
|
|
|
|
|
|
<!-- Bloc demandes d'impression en attente -->
|
|
|
|
|
<div class="lb-print-status" id="lb-print-status" style="display:none">
|
|
|
|
|
<!-- 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>
|
|
|
|
|
|
|
|
|
|
<!-- Boutons d'action -->
|
|
|
|
|
<!-- Actions -->
|
|
|
|
|
<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 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>
|
|
|
|
|
</div>
|
|
|
|
|
{% endblock %}
|
|
|
|
|
|
|
|
|
@@ -206,10 +301,72 @@
|
|
|
|
|
<script>
|
|
|
|
|
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`);
|
|
|
|
|
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 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, ''')})">
|
|
|
|
|
<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-date').textContent = photo.date_label || '—';
|
|
|
|
|
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-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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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) {
|
|
|
|
|
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;
|
|
|
|
|
updateLightboxPrintStatus(photo);
|
|
|
|
|
_updateLbPrintPanel(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 %}
|
|
|
|
|
|