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 ? `