From c32b3ba3457e9e696783228deee83952932314ab Mon Sep 17 00:00:00 2001 From: jbperrin Date: Fri, 17 Jul 2026 16:48:48 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20badges=20imprim=C3=A9=20(=E2=9C=85=20co?= =?UTF-8?q?pies)=20dans=20galeries=20admin+public;=20fix:=20timestamp=5Fto?= =?UTF-8?q?=5Fdate=20dans=20print.html?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/admin_api.py | 8 +++++ backend/api/admin_gallery_api.py | 22 ++++++++------ backend/api/gallery.py | 16 ++++++++++ backend/services/printer_service.py | 42 +++++++++++++++++++++----- frontend/templates/admin/gallery.html | 27 +++++++++++++++-- frontend/templates/admin/print.html | 2 +- frontend/templates/public/gallery.html | 35 ++++++++++++++++----- 7 files changed, 123 insertions(+), 29 deletions(-) diff --git a/backend/api/admin_api.py b/backend/api/admin_api.py index 4259fb4..3b9dea2 100644 --- a/backend/api/admin_api.py +++ b/backend/api/admin_api.py @@ -1,6 +1,7 @@ """Routes du dashboard admin -- authentification requise.""" import logging +from datetime import datetime from pathlib import Path from fastapi import APIRouter, Request, Form @@ -141,6 +142,13 @@ async def admin_print(request: Request): queue = await printer_svc.get_queue() printers = await printer_svc.get_printers_status() + # Formate les dates pour le template Jinja2 + for q in queue: + ts = q.get("requested_at") + q["requested_at_str"] = ( + datetime.fromtimestamp(ts).strftime("%d/%m %H:%M") if ts else "—" + ) + return _templates.TemplateResponse(request, "admin/print.html", { "config": cfg, "queue": queue, diff --git a/backend/api/admin_gallery_api.py b/backend/api/admin_gallery_api.py index 2c24cf7..1707b23 100644 --- a/backend/api/admin_gallery_api.py +++ b/backend/api/admin_gallery_api.py @@ -93,21 +93,25 @@ async def admin_get_photos( p["thumb_url"] = pb.thumbnail_url(pid) p["download_url"] = f"/api/gallery/download/{pid}" - # Croise avec la file d'impression en attente + # Croise avec les stats d'impression (pending, printing, done) try: - pending_map = await printer_svc.get_pending_by_photo_id() + stats_map = await printer_svc.get_print_stats_by_photo_id() for p in page_photos: pid = p.get("photo_id", "") - requests = pending_map.get(pid, []) - p["print_requests"] = requests - p["print_pending"] = len([r for r in requests if r["status"] == "pending"]) - p["print_printing"] = len([r for r in requests if r["status"] == "printing"]) + info = stats_map.get(pid, {}) + p["print_requests"] = info.get("requests", []) + p["print_pending"] = info.get("pending", 0) + p["print_printing"] = info.get("printing", 0) + p["print_done"] = info.get("done", 0) + p["print_done_copies"] = info.get("copies_done", 0) except Exception as e: logger.warning("Impossible de croiser avec print_queue: %s", e) for p in page_photos: - p["print_requests"] = [] - p["print_pending"] = 0 - p["print_printing"] = 0 + p["print_requests"] = [] + p["print_pending"] = 0 + p["print_printing"] = 0 + p["print_done"] = 0 + p["print_done_copies"] = 0 return { "photos": page_photos, diff --git a/backend/api/gallery.py b/backend/api/gallery.py index 01a16a3..a962f5e 100644 --- a/backend/api/gallery.py +++ b/backend/api/gallery.py @@ -74,6 +74,22 @@ async def api_gallery_photos( p["thumb_url"] = pb.thumbnail_url(pid) p["download_url"] = f"/api/gallery/download/{pid}" + # Ajoute les stats d'impression (copies réussies) pour les badges + printer_svc = getattr(request.app.state, "printer_service", None) + if printer_svc: + try: + stats_map = await printer_svc.get_print_stats_by_photo_id() + for p in page_photos: + pid = _get_id(p) + info = stats_map.get(pid, {}) + p["print_done_copies"] = info.get("copies_done", 0) + except Exception: + for p in page_photos: + p["print_done_copies"] = 0 + else: + for p in page_photos: + p["print_done_copies"] = 0 + return { "photos": page_photos, "total": total, diff --git a/backend/services/printer_service.py b/backend/services/printer_service.py index 424ef31..599f25a 100644 --- a/backend/services/printer_service.py +++ b/backend/services/printer_service.py @@ -111,18 +111,44 @@ class PrinterService: return await self.get_queue("pending") async def get_pending_by_photo_id(self) -> dict[str, list]: - """Retourne un dict {photo_id: [entries]} pour toutes les demandes actives. + """Retourne {photo_id: [entries actives]}. Conservé pour compatibilité.""" + stats = await self.get_print_stats_by_photo_id() + return {pid: info["requests"] for pid, info in stats.items() if info["requests"]} - Utilise le champ photo_id (UUID photobooth-app) s'il est renseigné, - sinon fall-back sur le stem du filename. + async def get_print_stats_by_photo_id(self) -> dict[str, dict]: + """Stats complètes d'impression par photo_id. + + Retourne {photo_id: {requests, pending, printing, done, copies_done}}. + 'requests' contient uniquement les entrées pending/printing (pour la lightbox). + 'done' et 'copies_done' comptent les impressions réussies. """ - rows = await self.get_queue("pending") - rows += await self.get_queue("printing") + async with self._db.execute( + "SELECT id, photo_id, filename, status, copies, requested_at, thumb_url " + "FROM print_queue WHERE status IN ('pending','printing','done') " + "ORDER BY requested_at" + ) as cur: + rows = await cur.fetchall() - result: dict[str, list] = {} + result: dict[str, dict] = {} for r in rows: - key = r.get("photo_id") or Path(r["filename"]).stem - result.setdefault(key, []).append(r) + key = r["photo_id"] or Path(r["filename"]).stem + if key not in result: + result[key] = { + "requests": [], + "pending": 0, + "printing": 0, + "done": 0, + "copies_done": 0, + } + if r["status"] in ("pending", "printing"): + result[key]["requests"].append(dict(r)) + if r["status"] == "pending": + result[key]["pending"] += 1 + elif r["status"] == "printing": + result[key]["printing"] += 1 + elif r["status"] == "done": + result[key]["done"] += 1 + result[key]["copies_done"] += r["copies"] or 1 return result async def cancel_by_photo_id(self, photo_id: str) -> int: diff --git a/frontend/templates/admin/gallery.html b/frontend/templates/admin/gallery.html index aaa80de..2f94fbe 100644 --- a/frontend/templates/admin/gallery.html +++ b/frontend/templates/admin/gallery.html @@ -71,6 +71,15 @@ pointer-events: none; } .print-badge.printing { background: rgba(25,108,176,.9); } +.print-badge.done { background: rgba(30,160,70,.9); } +.print-badge.done-secondary { + position: absolute; top: 5px; left: 5px; + background: rgba(30,160,70,.85); color: #fff; + border-radius: 12px; padding: .15rem .45rem; + font-size: .72rem; font-weight: 700; + display: flex; align-items: center; gap: .2rem; + pointer-events: none; +} .date-badge { position: absolute; bottom: 0; left: 0; right: 0; @@ -445,11 +454,23 @@ function renderGrid(photos) { const pid = p.photo_id || p.id || ''; const hasPending = p.print_pending > 0; const hasPrinting = p.print_printing > 0; + const hasDone = (p.print_done || 0) > 0; + const copies = p.print_done_copies || 0; + + // Badge principal (coin haut droite) — priorité : impression en cours > attente > terminé const badge = hasPrinting ? `` : hasPending - ? `` - : ''; + ? `` + : hasDone + ? `` + : ''; + + // Si en attente ET déjà imprimé → badge "imprimé" coin haut gauche aussi + const doneBadgeLeft = (hasPending || hasPrinting) && hasDone + ? `` + : ''; + const dateLabel = p.date_label ? `
${p.date_label}
` : ''; @@ -457,7 +478,7 @@ function renderGrid(photos) {
- ${badge}${dateLabel} + ${badge}${doneBadgeLeft}${dateLabel}
${hasPending ? `` diff --git a/frontend/templates/admin/print.html b/frontend/templates/admin/print.html index 2257879..4bbe5c1 100644 --- a/frontend/templates/admin/print.html +++ b/frontend/templates/admin/print.html @@ -308,7 +308,7 @@ - {{ q.requested_at|int|timestamp_to_date if q.requested_at else '—' }} + {{ q.requested_at_str }} {{ q.printer or '—' }} diff --git a/frontend/templates/public/gallery.html b/frontend/templates/public/gallery.html index cf64b3c..0a0c131 100644 --- a/frontend/templates/public/gallery.html +++ b/frontend/templates/public/gallery.html @@ -5,6 +5,19 @@ 📷 Galerie photos {% endblock %} +{% block head %} + +{% endblock %} + {% block content %}
@@ -71,14 +84,20 @@ async function loadPhotos(page = 1) { return; } - grid.innerHTML = photos.map(p => ` -
- Photo - -
- `).join(''); + grid.innerHTML = photos.map(p => { + const copies = p.print_done_copies || 0; + const printedBadge = copies > 0 + ? `
🖨 ${copies}×
` + : ''; + return ` +
+ Photo + ${printedBadge} + +
`; + }).join(''); } catch(e) { document.getElementById('photo-grid').innerHTML = `