diff --git a/backend/api/admin_api.py b/backend/api/admin_api.py index 015b76f..4259fb4 100644 --- a/backend/api/admin_api.py +++ b/backend/api/admin_api.py @@ -120,6 +120,15 @@ async def admin_settings(request: Request): }) +@router.get("/admin/events", response_class=HTMLResponse) +async def admin_events(request: Request): + redirect = _require_auth(request) + if redirect: + return redirect + cfg = request.app.state.config + return _templates.TemplateResponse(request, "admin/events.html", {"config": cfg}) + + @router.get("/admin/print", response_class=HTMLResponse) async def admin_print(request: Request): redirect = _require_auth(request) diff --git a/backend/api/event_api.py b/backend/api/event_api.py index 4810981..7186e55 100644 --- a/backend/api/event_api.py +++ b/backend/api/event_api.py @@ -2,10 +2,14 @@ from __future__ import annotations +import io import logging +import zipfile from datetime import datetime +from pathlib import Path from fastapi import APIRouter, Request +from fastapi.responses import StreamingResponse, JSONResponse from pydantic import BaseModel from backend.services.event_service import slugify @@ -81,6 +85,55 @@ async def update_event(request: Request, payload: EventUpdate): } +@router.get("/event/{slug}/export") +async def export_event_zip(request: Request, slug: str): + """Exporte toutes les photos d'un événement dans un fichier ZIP. + + Les photos sont sélectionnées par timestamp de fichier (mtime) entre + started_at et ended_at de l'événement. Pour l'événement en cours, + ended_at = maintenant. + """ + event_svc = request.app.state.event_service + cfg = request.app.state.config + + ev = await event_svc.get_event_by_slug(slug) + if not ev: + return JSONResponse({"error": f"Événement '{slug}' introuvable"}, status_code=404) + + started_at = ev["started_at"] or 0.0 + ended_at = ev["ended_at"] or datetime.now().timestamp() + + media_dir = Path(cfg.photobooth.media_dir) + if not media_dir.exists(): + return JSONResponse({"error": "Dossier média introuvable"}, status_code=500) + + # Sélectionner les photos dans la plage de l'événement + photos = [] + for f in sorted(media_dir.iterdir()): + if f.suffix.lower() not in (".jpg", ".jpeg", ".png"): + continue + mtime = f.stat().st_mtime + if started_at <= mtime <= ended_at: + photos.append(f) + + if not photos: + return JSONResponse({"error": "Aucune photo pour cet événement"}, status_code=404) + + # Créer le ZIP en mémoire + buf = io.BytesIO() + with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: + for photo in photos: + zf.write(photo, photo.name) + buf.seek(0) + + filename = f"{slug}_By_LSDW_{datetime.fromtimestamp(started_at).strftime('%Y-%m-%d')}.zip" + return StreamingResponse( + buf, + media_type="application/zip", + headers={"Content-Disposition": f'attachment; filename="{filename}"'}, + ) + + @router.get("/event/history") async def get_event_history(request: Request): """Retourne tous les événements passés avec leurs statistiques.""" diff --git a/backend/api/gallery.py b/backend/api/gallery.py index 035eebf..2bea5da 100644 --- a/backend/api/gallery.py +++ b/backend/api/gallery.py @@ -34,12 +34,37 @@ async def api_gallery_photos( request: Request, page: int = Query(default=1, ge=1), limit: int = Query(default=24, ge=1, le=100), + event_slug: str = Query(default=None, description="Filtrer par slug d'événement"), ): pb = request.app.state.photobooth_service + cfg = request.app.state.config all_photos = await pb.get_media_collection(limit=500) photos = [p for p in all_photos if _is_image(p)] + # Filtre par événement si demandé + if event_slug: + from pathlib import Path + from datetime import datetime + event_svc = getattr(request.app.state, "event_service", None) + if event_svc: + ev = await event_svc.get_event_by_slug(event_slug) + if ev: + started_at = ev["started_at"] or 0.0 + ended_at = ev["ended_at"] or datetime.now().timestamp() + media_dir = Path(cfg.photobooth.media_dir) + # Construire un index mtime par nom de fichier + mtime_index: dict[str, float] = {} + if media_dir.exists(): + for f in media_dir.iterdir(): + mtime_index[f.name] = f.stat().st_mtime + # Filtrer les photos par mtime + def _in_event(p: dict) -> bool: + pid = _get_id(p) + mtime = mtime_index.get(pid, mtime_index.get(pid + ".jpg", 0.0)) + return started_at <= mtime <= ended_at + photos = [p for p in photos if _in_event(p)] + total = len(photos) start = (page - 1) * limit end = start + limit @@ -56,6 +81,7 @@ async def api_gallery_photos( "total": total, "page": page, "pages": max(1, (total + limit - 1) // limit), + "event_slug": event_slug, } diff --git a/backend/services/event_service.py b/backend/services/event_service.py index 30bc5b9..69422a5 100644 --- a/backend/services/event_service.py +++ b/backend/services/event_service.py @@ -97,6 +97,17 @@ class EventService: ) await self._db.commit() + async def get_event_by_slug(self, slug: str) -> dict | None: + """Retourne un événement complet par son slug.""" + async with self._db.execute( + """SELECT id, name, slug, started_at, ended_at, + photos_taken, print_requests, prints_done, downloads + FROM events WHERE slug = ?""", + (slug,), + ) as cur: + row = await cur.fetchone() + return dict(row) if row else None + async def get_stats(self, slug: str) -> dict: """Retourne les stats pour un slug donné.""" async with self._db.execute( diff --git a/frontend/templates/admin/actions.html b/frontend/templates/admin/actions.html index 36de9a7..0af8258 100644 --- a/frontend/templates/admin/actions.html +++ b/frontend/templates/admin/actions.html @@ -7,6 +7,7 @@ Actions Impression Réglages +Événements Déconnexion {% endblock %} diff --git a/frontend/templates/admin/dashboard.html b/frontend/templates/admin/dashboard.html index 903576d..a599700 100644 --- a/frontend/templates/admin/dashboard.html +++ b/frontend/templates/admin/dashboard.html @@ -7,6 +7,7 @@ Actions Impression Réglages +Événements Déconnexion {% endblock %} @@ -330,3 +331,4 @@ setInterval(loadQueue, 15000); setInterval(loadEvent, 60000); {% endblock %} + \ No newline at end of file diff --git a/frontend/templates/admin/events.html b/frontend/templates/admin/events.html new file mode 100644 index 0000000..3910959 --- /dev/null +++ b/frontend/templates/admin/events.html @@ -0,0 +1,222 @@ +{% extends "base.html" %} +{% block title %}Événements — JH Photomaton{% endblock %} + +{% block nav_links %} +Dashboard +Galerie +Actions +Impression +Réglages +Événements +Déconnexion +{% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} +