feat: impression depuis galerie publique (quota session, modal copies)
This commit is contained in:
@@ -148,6 +148,90 @@ async def get_user_print_config(request: Request):
|
||||
}
|
||||
|
||||
|
||||
@router.get("/print/user-print-status")
|
||||
async def get_user_print_status(request: Request):
|
||||
"""Statut d'impression de la session courante (quota restant, config active)."""
|
||||
cfg = request.app.state.config.print
|
||||
count = request.session.get("gallery_print_count", 0)
|
||||
remaining = (cfg.user_print_quota - count) if cfg.user_print_quota > 0 else -1
|
||||
return {
|
||||
"user_print_enabled": cfg.user_print_enabled,
|
||||
"user_print_quota": cfg.user_print_quota,
|
||||
"user_print_max_copies": cfg.user_print_max_copies,
|
||||
"session_print_count": count,
|
||||
"remaining_quota": remaining, # -1 = illimité
|
||||
}
|
||||
|
||||
|
||||
@router.post("/print/request-gallery")
|
||||
async def gallery_print_request(request: Request, body: dict = Body(...)):
|
||||
"""Demande d'impression depuis la galerie publique.
|
||||
Vérifie que l'impression est activée et que le quota session n'est pas dépassé.
|
||||
"""
|
||||
from pathlib import Path as _Path
|
||||
|
||||
cfg_print = request.app.state.config.print
|
||||
|
||||
# ── Activation ────────────────────────────────────────────────────────────
|
||||
if not cfg_print.user_print_enabled:
|
||||
return JSONResponse({"error": "L'impression est désactivée"}, status_code=403)
|
||||
|
||||
photo_id = str(body.get("photo_id", "")).strip()
|
||||
if not photo_id:
|
||||
return JSONResponse({"error": "photo_id requis"}, status_code=400)
|
||||
|
||||
copies = max(1, min(cfg_print.user_print_max_copies, int(body.get("copies", 1))))
|
||||
|
||||
# ── Quota session ─────────────────────────────────────────────────────────
|
||||
if cfg_print.user_print_quota > 0:
|
||||
count = request.session.get("gallery_print_count", 0)
|
||||
if count >= cfg_print.user_print_quota:
|
||||
return JSONResponse({
|
||||
"error": f"Quota atteint ({cfg_print.user_print_quota} impression(s) maximum par session)",
|
||||
"quota_exceeded": True,
|
||||
}, status_code=429)
|
||||
|
||||
# ── Résolution du fichier ─────────────────────────────────────────────────
|
||||
media_dir = _Path(request.app.state.config.photobooth.media_dir)
|
||||
candidates = sorted(media_dir.glob(f"{photo_id}*"))
|
||||
if not candidates:
|
||||
return JSONResponse({"error": "Photo introuvable"}, status_code=404)
|
||||
filepath = str(candidates[0])
|
||||
|
||||
# ── Enqueue ───────────────────────────────────────────────────────────────
|
||||
pb = request.app.state.photobooth_service
|
||||
printer_svc = request.app.state.printer_service
|
||||
ws = request.app.state.ws_manager
|
||||
led = request.app.state.led_service
|
||||
|
||||
thumb_url = pb.thumbnail_url(photo_id)
|
||||
entry = await printer_svc.add_request(filepath, thumb_url, copies)
|
||||
await ws.broadcast({"type": "print_request", "entry": entry})
|
||||
|
||||
# ── Incrémenter compteur session ──────────────────────────────────────────
|
||||
new_count = request.session.get("gallery_print_count", 0) + 1
|
||||
request.session["gallery_print_count"] = new_count
|
||||
remaining = (cfg_print.user_print_quota - new_count) if cfg_print.user_print_quota > 0 else -1
|
||||
|
||||
# ── Stats événement ───────────────────────────────────────────────────────
|
||||
event_svc = getattr(request.app.state, "event_service", None)
|
||||
if event_svc:
|
||||
slug = request.app.state.config.event.slug
|
||||
asyncio.create_task(event_svc.increment(slug, "print_requests"))
|
||||
|
||||
# ── LED si mode direct ────────────────────────────────────────────────────
|
||||
if request.app.state.config.print.mode == "direct":
|
||||
led.play("printing")
|
||||
|
||||
logger.info("Impression galerie: photo_id=%s copies=%d session_count=%d", photo_id, copies, new_count)
|
||||
return {
|
||||
"ok": True,
|
||||
"id": entry["id"],
|
||||
"mode": request.app.state.config.print.mode,
|
||||
"remaining_quota": remaining,
|
||||
}
|
||||
|
||||
|
||||
# ── Gestion avancée des imprimantes CUPS ──────────────────────────────────────
|
||||
|
||||
@router.post("/print/printers/{printer_name}/enable")
|
||||
|
||||
@@ -11,6 +11,59 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
.photo-card { position: relative; }
|
||||
|
||||
/* ── Bouton imprimer sur la card ── */
|
||||
.photo-card-actions { display: flex; gap: .35rem; flex-wrap: wrap; }
|
||||
|
||||
/* ── Bandeau quota ── */
|
||||
#quota-bar {
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: .6rem;
|
||||
background: rgba(25,108,176,.12);
|
||||
border: 1px solid rgba(25,108,176,.3);
|
||||
border-radius: 8px;
|
||||
padding: .45rem .85rem;
|
||||
font-size: .88rem;
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
#quota-bar.quota-warn {
|
||||
background: rgba(224,123,0,.12);
|
||||
border-color: rgba(224,123,0,.35);
|
||||
}
|
||||
#quota-bar.quota-empty {
|
||||
background: rgba(192,0,0,.1);
|
||||
border-color: rgba(192,0,0,.3);
|
||||
}
|
||||
|
||||
/* ── Modal copies ── */
|
||||
.modal-overlay {
|
||||
display: none;
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,.65);
|
||||
z-index: 2000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.modal-overlay.open { display: flex; }
|
||||
.modal-box {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 1.5rem;
|
||||
max-width: 340px;
|
||||
width: 90%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
.modal-title { font-size: 1.05rem; font-weight: 700; }
|
||||
.modal-preview { width: 100%; border-radius: 8px; object-fit: cover; max-height: 180px; }
|
||||
.copies-row { display: flex; align-items: center; gap: .75rem; }
|
||||
.copies-btn { background: var(--bg-muted); border: 1px solid var(--border); color: var(--text);
|
||||
width: 2rem; height: 2rem; border-radius: 6px; cursor: pointer; font-size: 1.1rem;
|
||||
display: flex; align-items: center; justify-content: center; }
|
||||
.copies-val { font-size: 1.2rem; font-weight: 700; min-width: 1.5rem; text-align: center; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -27,6 +80,12 @@
|
||||
Retrouvez vos photos ci-dessous. Scannez le QR code depuis la galerie du photomaton pour télécharger votre photo directement sur votre téléphone.
|
||||
</p>
|
||||
|
||||
<!-- Bandeau quota -->
|
||||
<div id="quota-bar">
|
||||
<span id="quota-icon">🖨</span>
|
||||
<span id="quota-text"></span>
|
||||
</div>
|
||||
|
||||
<!-- Navigation pages -->
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<button class="btn btn-ghost btn-sm" id="prev-btn" onclick="changePage(-1)" disabled>← Préc.</button>
|
||||
@@ -45,16 +104,83 @@
|
||||
<img id="lightbox-img" src="" alt="Photo">
|
||||
<div class="lightbox-actions">
|
||||
<a class="btn btn-primary" id="lb-download">⬇ Télécharger la photo</a>
|
||||
<button class="btn btn-primary" id="lb-print" style="display:none" onclick="openPrintModal()">🖨 Imprimer</button>
|
||||
</div>
|
||||
<div class="text-xs text-muted">Connectez-vous au WiFi Photomaton pour télécharger</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal sélection copies -->
|
||||
<div class="modal-overlay" id="print-modal" onclick="closePrintModal(event)">
|
||||
<div class="modal-box" onclick="event.stopPropagation()">
|
||||
<div class="modal-title">🖨 Demande d'impression</div>
|
||||
<img id="pm-preview" src="" alt="" class="modal-preview">
|
||||
<div id="pm-copies-row" class="copies-row" style="display:none">
|
||||
<span style="flex:1;font-size:.9rem">Nombre de copies</span>
|
||||
<button class="copies-btn" onclick="changeCopies(-1)">−</button>
|
||||
<span class="copies-val" id="pm-copies">1</span>
|
||||
<button class="copies-btn" onclick="changeCopies(1)">+</button>
|
||||
</div>
|
||||
<p class="text-sm text-muted" id="pm-quota-info" style="margin:0"></p>
|
||||
<div class="flex gap-1">
|
||||
<button class="btn btn-primary" onclick="confirmPrint()" id="pm-confirm">🖨 Confirmer</button>
|
||||
<button class="btn btn-ghost" onclick="closePrintModal()">Annuler</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// ── État ──────────────────────────────────────────────────────────────────────
|
||||
let currentPage = 1;
|
||||
let totalPages = 1;
|
||||
let printCfg = { user_print_enabled: false, user_print_quota: 0,
|
||||
user_print_max_copies: 1, remaining_quota: -1 };
|
||||
let currentPrintPhotoId = null;
|
||||
let currentPrintThumb = null;
|
||||
let currentCopies = 1;
|
||||
|
||||
// ── Quota ─────────────────────────────────────────────────────────────────────
|
||||
async function loadPrintStatus() {
|
||||
try {
|
||||
const s = await api('GET', '/api/print/user-print-status');
|
||||
printCfg = s;
|
||||
updateQuotaBar();
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
function updateQuotaBar() {
|
||||
const bar = document.getElementById('quota-bar');
|
||||
const text = document.getElementById('quota-text');
|
||||
const icon = document.getElementById('quota-icon');
|
||||
|
||||
if (!printCfg.user_print_enabled) { bar.style.display = 'none'; return; }
|
||||
|
||||
bar.style.display = 'flex';
|
||||
bar.className = '';
|
||||
|
||||
if (printCfg.user_print_quota === 0) {
|
||||
// Illimité — pas besoin d'afficher le bandeau
|
||||
bar.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const rem = printCfg.remaining_quota;
|
||||
if (rem <= 0) {
|
||||
bar.classList.add('quota-empty');
|
||||
icon.textContent = '🚫';
|
||||
text.textContent = `Vous avez utilisé votre quota d'impression (${printCfg.user_print_quota} max).`;
|
||||
} else if (rem === 1) {
|
||||
bar.classList.add('quota-warn');
|
||||
icon.textContent = '⚠️';
|
||||
text.textContent = `Dernière impression disponible pour cette session.`;
|
||||
} else {
|
||||
icon.textContent = '🖨';
|
||||
text.textContent = `${rem} impression(s) restante(s) sur ${printCfg.user_print_quota}.`;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Photos ────────────────────────────────────────────────────────────────────
|
||||
async function loadPhotos(page = 1) {
|
||||
try {
|
||||
const data = await api('GET', `/api/gallery/photos?page=${page}&limit=24`);
|
||||
@@ -80,17 +206,27 @@ async function loadPhotos(page = 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const canPrint = printCfg.user_print_enabled &&
|
||||
(printCfg.user_print_quota === 0 || printCfg.remaining_quota > 0);
|
||||
|
||||
grid.innerHTML = photos.map(p => {
|
||||
const copies = p.print_done_copies || 0;
|
||||
const printedBadge = copies > 0
|
||||
? `<div class="pub-print-badge">🖨 ${copies}×</div>`
|
||||
: '';
|
||||
const printBtn = printCfg.user_print_enabled
|
||||
? `<button class="btn btn-sm ${canPrint ? 'btn-ghost' : ''}"
|
||||
style="${canPrint ? '' : 'opacity:.4;cursor:not-allowed'}"
|
||||
onclick="event.stopPropagation();${canPrint ? `askPrint('${p.id}','${p.thumb_url}')` : 'showToast(\'Quota atteint\',\'error\')'}"
|
||||
title="Imprimer cette photo">🖨</button>`
|
||||
: '';
|
||||
return `
|
||||
<div class="photo-card" onclick="openLightbox('${p.full_url}', '${p.download_url}')">
|
||||
<div class="photo-card" onclick="openLightbox('${p.full_url}','${p.download_url}','${p.id}','${p.thumb_url}')">
|
||||
<img src="${p.thumb_url}" loading="lazy" alt="Photo" onerror="this.src='${p.full_url}'">
|
||||
${printedBadge}
|
||||
<div class="photo-card-actions">
|
||||
<a class="btn btn-primary btn-sm" href="${p.download_url}" onclick="event.stopPropagation()">⬇ Télécharger</a>
|
||||
${printBtn}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
@@ -105,9 +241,22 @@ async function loadPhotos(page = 1) {
|
||||
|
||||
function changePage(delta) { loadPhotos(currentPage + delta); }
|
||||
|
||||
function openLightbox(fullUrl, downloadUrl) {
|
||||
// ── Lightbox ──────────────────────────────────────────────────────────────────
|
||||
function openLightbox(fullUrl, downloadUrl, photoId, thumbUrl) {
|
||||
document.getElementById('lightbox-img').src = fullUrl;
|
||||
document.getElementById('lb-download').href = downloadUrl;
|
||||
|
||||
const lbPrint = document.getElementById('lb-print');
|
||||
if (printCfg.user_print_enabled) {
|
||||
lbPrint.style.display = '';
|
||||
const canPrint = printCfg.user_print_quota === 0 || printCfg.remaining_quota > 0;
|
||||
lbPrint.disabled = !canPrint;
|
||||
lbPrint.title = canPrint ? 'Demander une impression' : 'Quota atteint';
|
||||
lbPrint.onclick = () => askPrint(photoId, thumbUrl);
|
||||
} else {
|
||||
lbPrint.style.display = 'none';
|
||||
}
|
||||
|
||||
document.getElementById('lightbox').classList.add('open');
|
||||
}
|
||||
|
||||
@@ -116,19 +265,102 @@ function closeLightbox(e) {
|
||||
document.getElementById('lightbox').classList.remove('open');
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelector('.lightbox-close').addEventListener('click', closeLightbox);
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeLightbox({}); });
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') { closeLightbox({}); closePrintModal(); } });
|
||||
|
||||
// ── Modal impression ──────────────────────────────────────────────────────────
|
||||
function askPrint(photoId, thumbUrl) {
|
||||
currentPrintPhotoId = photoId;
|
||||
currentPrintThumb = thumbUrl;
|
||||
currentCopies = 1;
|
||||
|
||||
document.getElementById('pm-preview').src = thumbUrl;
|
||||
document.getElementById('pm-copies').textContent = '1';
|
||||
|
||||
// Afficher le sélecteur de copies seulement si max > 1
|
||||
const copiesRow = document.getElementById('pm-copies-row');
|
||||
copiesRow.style.display = printCfg.user_print_max_copies > 1 ? 'flex' : 'none';
|
||||
|
||||
// Info quota
|
||||
const info = document.getElementById('pm-quota-info');
|
||||
if (printCfg.user_print_quota > 0) {
|
||||
const rem = printCfg.remaining_quota;
|
||||
info.textContent = `Il vous reste ${rem} impression(s) pour cette session.`;
|
||||
} else {
|
||||
info.textContent = '';
|
||||
}
|
||||
|
||||
document.getElementById('print-modal').classList.add('open');
|
||||
}
|
||||
|
||||
function openPrintModal() { /* appelé depuis le bouton lightbox — déjà configuré via askPrint */ }
|
||||
|
||||
function closePrintModal(e) {
|
||||
if (!e || e.target.id === 'print-modal') {
|
||||
document.getElementById('print-modal').classList.remove('open');
|
||||
}
|
||||
}
|
||||
|
||||
function changeCopies(delta) {
|
||||
currentCopies = Math.max(1, Math.min(printCfg.user_print_max_copies, currentCopies + delta));
|
||||
document.getElementById('pm-copies').textContent = currentCopies;
|
||||
}
|
||||
|
||||
async function confirmPrint() {
|
||||
if (!currentPrintPhotoId) return;
|
||||
const btn = document.getElementById('pm-confirm');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⏳ Envoi…';
|
||||
|
||||
try {
|
||||
const r = await api('POST', '/api/print/request-gallery', {
|
||||
photo_id: currentPrintPhotoId,
|
||||
copies: currentCopies,
|
||||
});
|
||||
// Mettre à jour le quota local
|
||||
printCfg.remaining_quota = r.remaining_quota;
|
||||
printCfg.session_print_count = (printCfg.session_print_count || 0) + 1;
|
||||
updateQuotaBar();
|
||||
|
||||
const modeMsg = r.mode === 'direct'
|
||||
? 'L\'impression va démarrer.'
|
||||
: 'Votre demande a bien été transmise à l\'admin.';
|
||||
showToast(`✅ Demande envoyée ! ${modeMsg}`, 'success', 5000);
|
||||
closePrintModal();
|
||||
document.getElementById('lightbox').classList.remove('open');
|
||||
// Recharger les photos pour mettre à jour le badge imprimé
|
||||
setTimeout(() => loadPhotos(currentPage), 1500);
|
||||
} catch(e) {
|
||||
const msg = e.message || 'Erreur';
|
||||
if (msg.includes('429')) {
|
||||
showToast('🚫 Quota atteint — vous ne pouvez plus imprimer pour cette session.', 'error', 6000);
|
||||
printCfg.remaining_quota = 0;
|
||||
updateQuotaBar();
|
||||
} else if (msg.includes('403')) {
|
||||
showToast('L\'impression est désactivée.', 'error');
|
||||
} else {
|
||||
showToast('❌ Erreur : ' + msg, 'error');
|
||||
}
|
||||
closePrintModal();
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '🖨 Confirmer';
|
||||
}
|
||||
}
|
||||
|
||||
// ── WebSocket ─────────────────────────────────────────────────────────────────
|
||||
function onWsMessage(msg) {
|
||||
// Rafraîchit la galerie quand une nouvelle photo est ajoutée
|
||||
if (msg.type === 'photobooth_event' && msg.event === 'finished') {
|
||||
setTimeout(() => loadPhotos(1), 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────────────────
|
||||
(async () => {
|
||||
await loadPrintStatus();
|
||||
loadPhotos(1);
|
||||
// Auto-refresh toutes les 30 secondes
|
||||
setInterval(() => loadPhotos(1), 30000);
|
||||
})();
|
||||
|
||||
setInterval(() => loadPhotos(currentPage), 30000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user