diff --git a/backend/destinations.py b/backend/destinations.py index 8670ca3..4108d91 100644 --- a/backend/destinations.py +++ b/backend/destinations.py @@ -25,18 +25,17 @@ def distribuer_photo(chemin_photo: Path, imprimee: bool = False): daemon=True, ).start() - # Verifier si on sauvegarde tout ou seulement les imprimees + sous_dossier = None if not dest.get("sauvegarder_tout", True) and not imprimee: - log.info(f"Photo non imprimee, pas de distribution : {chemin_photo.name}") - return + sous_dossier = "non_imprimees" # Cle USB if dest.get("cle_usb", False): - copier_usb(chemin_photo, dest.get("chemin_usb", "/media/usb")) + copier_usb(chemin_photo, dest.get("chemin_usb", "/media/usb"), sous_dossier) # FTP if dest.get("ftp", False): - envoyer_ftp(chemin_photo, dest) + envoyer_ftp(chemin_photo, dest, sous_dossier) # Incrementer le compteur compteur = config.get("compteur", {}) @@ -45,7 +44,7 @@ def distribuer_photo(chemin_photo: Path, imprimee: bool = False): mettre_a_jour_config({"compteur": compteur}) -def copier_usb(chemin_photo: Path, chemin_usb: str): +def copier_usb(chemin_photo: Path, chemin_usb: str, sous_dossier: str | None = None): """Copie une photo sur la cle USB.""" dossier_usb = Path(chemin_usb) if not dossier_usb.exists(): @@ -53,24 +52,28 @@ def copier_usb(chemin_photo: Path, chemin_usb: str): return False dossier_dest = dossier_usb / "photobooth" - dossier_dest.mkdir(exist_ok=True) + if sous_dossier: + dossier_dest = dossier_dest / sous_dossier + dossier_dest.mkdir(parents=True, exist_ok=True) try: shutil.copy2(chemin_photo, dossier_dest / chemin_photo.name) - log.info(f"Photo copiee sur USB : {chemin_photo.name}") + log.info(f"Photo copiee sur USB : {chemin_photo.name} ({sous_dossier or 'root'})") return True except OSError as e: log.error(f"Erreur copie USB : {e}") return False -def envoyer_ftp(chemin_photo: Path, config_dest: dict): +def envoyer_ftp(chemin_photo: Path, config_dest: dict, sous_dossier: str | None = None): """Envoie une photo par FTP.""" host = config_dest.get("ftp_host", "") port = config_dest.get("ftp_port", 21) user = config_dest.get("ftp_user", "") password = config_dest.get("ftp_password", "") chemin_distant = config_dest.get("ftp_chemin", "/photobooth") + if sous_dossier: + chemin_distant = f"{chemin_distant}/{sous_dossier}" if not host: log.warning("FTP non configure") diff --git a/backend/effects.py b/backend/effects.py index 9a14d04..b230ae1 100644 --- a/backend/effects.py +++ b/backend/effects.py @@ -121,13 +121,24 @@ def chroma_key(chemin_photo: Path, nom_fond: str | None = None, return chemin_export -def appliquer_cadre_impression(img: Image.Image, format_papier: str, nom_cadre: str) -> Image.Image: +def appliquer_cadre_impression(img: Image.Image, format_papier: str, nom_cadre: str, event_id: str | None = None) -> Image.Image: """Composite un cadre PNG par-dessus l'image assemblée.""" - chemin = DOSSIER_CADRES / format_papier / nom_cadre + chemin = None + if event_id: + from backend.evenements import DOSSIER_EVENEMENTS + chemin_event = DOSSIER_EVENEMENTS / event_id / "cadres" / format_papier / nom_cadre + if chemin_event.exists(): + chemin = chemin_event + if chemin is None: + chemin = DOSSIER_CADRES / format_papier / nom_cadre if not chemin.exists(): log.warning(f"Cadre introuvable : {chemin}") return img cadre = Image.open(chemin).convert("RGBA") + img_portrait = img.height > img.width + cadre_portrait = cadre.height > cadre.width + if img_portrait != cadre_portrait: + cadre = cadre.rotate(90, expand=True) if cadre.size != img.size: cadre = cadre.resize(img.size, Image.LANCZOS) base = img.convert("RGBA") diff --git a/backend/evenements.py b/backend/evenements.py new file mode 100644 index 0000000..bc2eca6 --- /dev/null +++ b/backend/evenements.py @@ -0,0 +1,148 @@ +import json +import logging +import shutil +import uuid +from pathlib import Path + +from backend.config import RACINE, DOSSIER_CADRES, FORMATS_CADRES, charger_config, mettre_a_jour_config + +log = logging.getLogger("photobooth.evenements") + +DOSSIER_EVENEMENTS = RACINE / "data" / "evenements" +DOSSIER_EVENEMENTS.mkdir(parents=True, exist_ok=True) + + +def _chemin_event(event_id: str) -> Path: + return DOSSIER_EVENEMENTS / f"{event_id}.json" + + +def _chemin_cadres_event(event_id: str) -> Path: + d = DOSSIER_EVENEMENTS / event_id / "cadres" + d.mkdir(parents=True, exist_ok=True) + return d + + +def lister_evenements() -> list[dict]: + events = [] + config = charger_config() + actif_id = config.get("evenement", {}).get("event_id") + for f in sorted(DOSSIER_EVENEMENTS.glob("*.json")): + try: + data = json.loads(f.read_text("utf-8")) + data["actif"] = (data.get("id") == actif_id) + events.append(data) + except (json.JSONDecodeError, KeyError): + continue + return events + + +def creer_evenement(nom: str, **kwargs) -> dict: + event_id = uuid.uuid4().hex[:8] + event = { + "id": event_id, + "nom": nom, + "theme": kwargs.get("theme", "base"), + "couleur_primaire": kwargs.get("couleur_primaire", "#e91e63"), + "couleur_secondaire": kwargs.get("couleur_secondaire", "#ffffff"), + "media_accueil": kwargs.get("media_accueil"), + "cadres": {}, + } + _chemin_event(event_id).write_text(json.dumps(event, ensure_ascii=False, indent=2), "utf-8") + log.info(f"Evenement cree : {nom} ({event_id})") + return event + + +def obtenir_evenement(event_id: str) -> dict | None: + chemin = _chemin_event(event_id) + if not chemin.exists(): + return None + return json.loads(chemin.read_text("utf-8")) + + +def modifier_evenement(event_id: str, modifications: dict) -> dict | None: + event = obtenir_evenement(event_id) + if not event: + return None + for k, v in modifications.items(): + if k != "id": + event[k] = v + _chemin_event(event_id).write_text(json.dumps(event, ensure_ascii=False, indent=2), "utf-8") + return event + + +def supprimer_evenement(event_id: str) -> bool: + chemin = _chemin_event(event_id) + if not chemin.exists(): + return False + chemin.unlink() + dossier_cadres = DOSSIER_EVENEMENTS / event_id + if dossier_cadres.exists(): + shutil.rmtree(dossier_cadres) + config = charger_config() + if config.get("evenement", {}).get("event_id") == event_id: + mettre_a_jour_config({"evenement": {"event_id": None}}) + log.info(f"Evenement supprime : {event_id}") + return True + + +def activer_evenement(event_id: str) -> dict | None: + event = obtenir_evenement(event_id) + if not event: + return None + mettre_a_jour_config({ + "evenement": { + "event_id": event_id, + "nom": event["nom"], + "theme": event.get("theme", "base"), + "couleur_primaire": event.get("couleur_primaire", "#e91e63"), + "couleur_secondaire": event.get("couleur_secondaire", "#ffffff"), + "media_accueil": event.get("media_accueil"), + } + }) + log.info(f"Evenement active : {event['nom']} ({event_id})") + return event + + +def lister_cadres_event(event_id: str, format_papier: str) -> list[str]: + dossier = _chemin_cadres_event(event_id) / format_papier + if not dossier.exists(): + return [] + return sorted(f.name for f in dossier.iterdir() if f.suffix.lower() == ".png") + + +def config_cadre_event(event_id: str) -> dict: + """Retourne la config cadres de l'evenement : {format: {mode, cadre}} + mode: 'disponible' (choix user), 'impose' (force), 'defaut' (pre-selectionne) + """ + event = obtenir_evenement(event_id) + if not event: + return {} + return event.get("cadres", {}) + + +def set_cadre_event(event_id: str, format_papier: str, mode: str, cadre: str | None) -> bool: + """mode: 'aucun', 'disponible', 'impose', 'defaut'""" + event = obtenir_evenement(event_id) + if not event: + return False + if "cadres" not in event: + event["cadres"] = {} + event["cadres"][format_papier] = {"mode": mode, "cadre": cadre} + _chemin_event(event_id).write_text(json.dumps(event, ensure_ascii=False, indent=2), "utf-8") + return True + + +def cadre_actif_pour_impression(format_papier: str) -> tuple[str | None, str]: + """Retourne (nom_cadre, mode) en tenant compte de l'evenement actif. + mode: 'aucun', 'disponible', 'impose', 'defaut' + """ + config = charger_config() + event_id = config.get("evenement", {}).get("event_id") + if not event_id: + return None, "aucun" + event = obtenir_evenement(event_id) + if not event: + return None, "aucun" + cadres = event.get("cadres", {}) + fmt_conf = cadres.get(format_papier, {}) + return fmt_conf.get("cadre"), fmt_conf.get("mode", "aucun") diff --git a/backend/main.py b/backend/main.py index 4613765..ff8aef5 100644 --- a/backend/main.py +++ b/backend/main.py @@ -30,6 +30,12 @@ from backend.destinations import distribuer_photo, detecter_usb, compteur_restan from backend.printer import lister_imprimantes, imprimer from backend.mailer import envoyer_photo, charger_emails_historique, effacer_emails_historique, ajouter_au_spool, taille_spool, tache_spool_demarrage from backend.qrcode_gen import generer_qr, qr_galerie +from backend.evenements import ( + lister_evenements, creer_evenement, obtenir_evenement, + modifier_evenement, supprimer_evenement, activer_evenement, + lister_cadres_event, set_cadre_event, cadre_actif_pour_impression, +) +from backend.evenements import DOSSIER_EVENEMENTS logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s") log = logging.getLogger("photobooth") @@ -890,6 +896,103 @@ async def api_booth_imprimer_qr(): return {"succes": ok, "chemin": f"/data/exports/flyer_qr_booth.jpg"} +# --- API Evenements --- + +@app.get("/api/evenements") +async def api_lister_evenements(): + return lister_evenements() + + +@app.post("/api/evenements") +async def api_creer_evenement(donnees: dict): + nom = donnees.get("nom", "").strip() + if not nom: + return JSONResponse({"erreur": "Nom requis"}, status_code=400) + event = creer_evenement( + nom, + theme=donnees.get("theme", "base"), + couleur_primaire=donnees.get("couleur_primaire", "#e91e63"), + couleur_secondaire=donnees.get("couleur_secondaire", "#ffffff"), + media_accueil=donnees.get("media_accueil"), + ) + return event + + +@app.get("/api/evenements/{event_id}") +async def api_obtenir_evenement(event_id: str): + event = obtenir_evenement(event_id) + if not event: + return JSONResponse({"erreur": "Evenement introuvable"}, status_code=404) + return event + + +@app.put("/api/evenements/{event_id}") +async def api_modifier_evenement(event_id: str, donnees: dict): + event = modifier_evenement(event_id, donnees) + if not event: + return JSONResponse({"erreur": "Evenement introuvable"}, status_code=404) + return event + + +@app.delete("/api/evenements/{event_id}") +async def api_supprimer_evenement(event_id: str): + ok = supprimer_evenement(event_id) + if not ok: + return JSONResponse({"erreur": "Evenement introuvable"}, status_code=404) + return {"succes": True} + + +@app.post("/api/evenements/{event_id}/activer") +async def api_activer_evenement(event_id: str): + event = activer_evenement(event_id) + if not event: + return JSONResponse({"erreur": "Evenement introuvable"}, status_code=404) + return event + + +@app.get("/api/evenements/{event_id}/cadres/{format_papier}") +async def api_cadres_event(event_id: str, format_papier: str): + cadres = lister_cadres_event(event_id, format_papier) + event = obtenir_evenement(event_id) + conf = (event or {}).get("cadres", {}).get(format_papier, {}) + return { + "disponibles": cadres, + "mode": conf.get("mode", "aucun"), + "cadre": conf.get("cadre"), + } + + +@app.post("/api/evenements/{event_id}/cadres/{format_papier}") +async def api_set_cadre_event(event_id: str, format_papier: str, donnees: dict): + mode = donnees.get("mode", "aucun") + cadre = donnees.get("cadre") + set_cadre_event(event_id, format_papier, mode, cadre) + return {"succes": True} + + +@app.post("/api/evenements/{event_id}/upload-cadre/{format_papier}") +async def api_upload_cadre_event(event_id: str, format_papier: str, fichier: UploadFile = File(...)): + if format_papier not in FORMATS_CADRES: + return JSONResponse({"erreur": "Format inconnu"}, status_code=400) + if not fichier.filename.lower().endswith(".png"): + return JSONResponse({"erreur": "Seuls les PNG sont acceptes"}, status_code=400) + dossier = DOSSIER_EVENEMENTS / event_id / "cadres" / format_papier + dossier.mkdir(parents=True, exist_ok=True) + chemin = dossier / fichier.filename + with open(chemin, "wb") as f: + f.write(await fichier.read()) + return {"nom": fichier.filename, "format": format_papier} + + +@app.delete("/api/evenements/{event_id}/cadres/{format_papier}/{nom}") +async def api_supprimer_cadre_event(event_id: str, format_papier: str, nom: str): + chemin = DOSSIER_EVENEMENTS / event_id / "cadres" / format_papier / nom + if not chemin.exists(): + return JSONResponse({"erreur": "Fichier introuvable"}, status_code=404) + chemin.unlink() + return {"succes": True} + + # --- API Galerie --- @app.get("/api/galerie") @@ -977,7 +1080,16 @@ async def api_cadres_impression_list(format_papier: str): return JSONResponse({"erreur": "Format inconnu"}, status_code=400) config = charger_config() actif = config.get("impression", {}).get("cadres_actifs", {}).get(format_papier) - return {"format": format_papier, "disponibles": lister_cadres(format_papier), "actif": actif} + result = {"format": format_papier, "disponibles": lister_cadres(format_papier), "actif": actif} + event_id = config.get("evenement", {}).get("event_id") + if event_id: + ev_cadres = lister_cadres_event(event_id, format_papier) + ev_cadre, ev_mode = cadre_actif_pour_impression(format_papier) + result["event_cadres"] = ev_cadres + result["event_mode"] = ev_mode + result["event_cadre"] = ev_cadre + result["event_id"] = event_id + return result @app.post("/api/cadres-impression/{format_papier}/actif") diff --git a/backend/printer.py b/backend/printer.py index 806ed18..27b8a27 100644 --- a/backend/printer.py +++ b/backend/printer.py @@ -77,7 +77,7 @@ def _crop_to(img, largeur: int, hauteur: int): def _preparer_image(chemin: Path, largeur: int, hauteur: int, format_papier: str | None = None, cadre: str | None = None, - orientation: str = "paysage") -> Path: + orientation: str = "paysage", event_id: str | None = None) -> Path: """ Pivote si nécessaire, recadre aux dimensions exactes (cover crop), applique le cadre, sauvegarde en temp. @@ -101,7 +101,7 @@ def _preparer_image(chemin: Path, largeur: int, hauteur: int, img = _crop_to(img, largeur, hauteur) if cadre and format_papier: - img = appliquer_cadre_impression(img, format_papier, cadre) + img = appliquer_cadre_impression(img, format_papier, cadre, event_id=event_id) tmp = tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) img.save(tmp.name, "JPEG", quality=95, dpi=(300, 300)) @@ -221,11 +221,20 @@ def imprimer( cadre = cadre_override if cadre_override is not None else conf_imp.get("cadres_actifs", {}).get(fmt_base) orientation = conf_imp.get("orientations", {}).get(fmt_base, "portrait") + event_id = config.get("evenement", {}).get("event_id") + if event_id and cadre_override is None: + from backend.evenements import cadre_actif_pour_impression + ev_cadre, ev_mode = cadre_actif_pour_impression(fmt_base) + if ev_mode == "impose" and ev_cadre: + cadre = ev_cadre + elif ev_mode == "defaut" and ev_cadre and not cadre: + cadre = ev_cadre + if not chemin_photo.exists(): log.error(f"Fichier introuvable : {chemin_photo}") return {"succes": False, "erreur": ERREUR_FICHIER, "message": "Fichier photo introuvable"} - chemin_print = _preparer_image(chemin_photo, largeur, hauteur, fmt_base, cadre, orientation) + chemin_print = _preparer_image(chemin_photo, largeur, hauteur, fmt_base, cadre, orientation, event_id=event_id) tmp_a_supprimer = [chemin_print] if chemin_print != chemin_photo else [] px_page = fmt.get("px_page") diff --git a/frontend/css/style.css b/frontend/css/style.css index 64ed904..4ca3149 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -1332,6 +1332,76 @@ h3 { /* --- Fin cadres impression --- */ +/* --- Evenements admin --- */ +.event-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.6rem 0.8rem; + margin-bottom: 0.4rem; + background: rgba(255,255,255,0.04); + border-radius: 8px; + border: 1px solid rgba(255,255,255,0.08); +} +.event-item.event-actif { + border-color: var(--primaire); + background: rgba(233,30,99,0.08); +} +.event-nom { font-weight: 500; } +.event-actions { display: flex; gap: 0.3rem; align-items: center; } +.badge-actif { + background: var(--primaire); + color: #fff; + padding: 0.2rem 0.6rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; +} +.cadre-event-section { margin-bottom: 1.2rem; } +.cadres-event-grille { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + margin: 0.5rem 0; +} +.cadre-event-item { + position: relative; + width: 90px; + text-align: center; + cursor: pointer; + padding: 0.3rem; + border: 2px solid transparent; + border-radius: 8px; +} +.cadre-event-item.cadre-selectionne { + border-color: var(--primaire); + background: rgba(233,30,99,0.1); +} +.cadre-event-item img { + width: 80px; + height: 80px; + object-fit: contain; + border-radius: 4px; +} +.cadre-event-item span { + display: block; + font-size: 0.7rem; + margin-top: 0.2rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.cadre-event-item .btn-icone { + position: absolute; + top: -4px; + right: -4px; + width: 20px; + height: 20px; + font-size: 0.6rem; + line-height: 20px; + padding: 0; +} + /* --- Choix cadre avant capture --- */ #ecran-cadre-choix { display: flex; diff --git a/frontend/index.html b/frontend/index.html index 15a26c9..2aa235e 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -7,7 +7,7 @@ - + @@ -578,37 +578,51 @@
-
- - +

Evenements

+
+ +
-
- - -
-
- -
- + + -
- - +
+ + +
+
+ +
+ + +
+ + +
+
+ + +
+
+ + +
+ + +

Cadres evenement

+

Cadres specifiques a cet evenement (n'apparaissent pas dans les autres).

+
-
- - -
-
- - -
-
@@ -743,7 +757,7 @@ - - + + diff --git a/frontend/js/admin.js b/frontend/js/admin.js index 7c4a79e..715ecb1 100644 --- a/frontend/js/admin.js +++ b/frontend/js/admin.js @@ -26,6 +26,7 @@ async function chargerAdmin() { chargerFondsAdmin(); chargerGalerieAdmin(); chargerBoothAdmin(); + chargerEvenementAdmin(); } // === MATERIEL === @@ -690,15 +691,91 @@ function setupToggleListeners() { // === EVENEMENT === +let eventEditId = null; + async function chargerEvenementAdmin() { + await chargerListeEvenements(); const event = config.evenement || {}; + if (event.event_id) { + eventEditId = event.event_id; + afficherDetailEvent(event); + } +} + +async function chargerListeEvenements() { + const events = await apiGet('/api/evenements'); + const liste = document.getElementById('liste-evenements'); + liste.innerHTML = ''; + const actifId = (config.evenement || {}).event_id; + + for (const ev of events) { + const div = document.createElement('div'); + div.className = 'event-item' + (ev.id === actifId ? ' event-actif' : ''); + div.innerHTML = ` + ${ev.nom} + + ${ev.id !== actifId ? `` : 'Actif'} + + + `; + liste.appendChild(div); + } + + if (events.length === 0) { + liste.innerHTML = '

Aucun evenement. Creez-en un ci-dessus.

'; + } +} + +async function creerEvenementAdmin() { + const nom = document.getElementById('input-nouveau-event').value.trim(); + if (!nom) return; + const event = await apiPost('/api/evenements', { nom }); + document.getElementById('input-nouveau-event').value = ''; + await activerEventAdmin(event.id); +} + +async function activerEventAdmin(id) { + await apiPost(`/api/evenements/${id}/activer`); + config = await apiGet('/api/config'); + eventEditId = id; + await chargerListeEvenements(); + afficherDetailEvent(config.evenement); + appliquerConfig(); + afficherStatut('Evenement active', 'succes'); +} + +async function editerEventAdmin(id) { + const event = await apiGet(`/api/evenements/${id}`); + if (!event) return; + eventEditId = id; + afficherDetailEvent(event); +} + +function afficherDetailEvent(event) { + document.getElementById('titre-event-detail').classList.remove('cache'); + document.getElementById('detail-evenement').classList.remove('cache'); document.getElementById('admin-theme').value = event.theme || 'base'; setValue('admin-nom-event', event.nom); setValue('admin-couleur-primaire', event.couleur_primaire || '#e91e63'); setValue('admin-couleur-secondaire', event.couleur_secondaire || '#ffffff'); - await rafraichirMediaAccueil(); - const select = document.getElementById('admin-media-accueil'); - if (event.media_accueil) select.value = event.media_accueil; + rafraichirMediaAccueil().then(() => { + const select = document.getElementById('admin-media-accueil'); + if (event.media_accueil) select.value = event.media_accueil; + }); + chargerCadresEvent(); +} + +async function supprimerEventAdmin(id, nom) { + if (!confirm(`Supprimer l'evenement "${nom}" et ses cadres ?`)) return; + await fetch(`/api/evenements/${id}`, { method: 'DELETE' }); + if (eventEditId === id) { + eventEditId = null; + document.getElementById('titre-event-detail').classList.add('cache'); + document.getElementById('detail-evenement').classList.add('cache'); + } + config = await apiGet('/api/config'); + await chargerListeEvenements(); + afficherStatut('Evenement supprime', 'succes'); } function changerTheme(theme) { @@ -732,19 +809,108 @@ async function uploaderMediaAccueil() { } async function sauvegarderEvenement() { - await apiPost('/api/config', { - evenement: { - theme: document.getElementById('admin-theme').value, - nom: getValue('admin-nom-event'), - couleur_primaire: getValue('admin-couleur-primaire'), - couleur_secondaire: getValue('admin-couleur-secondaire'), - media_accueil: getValue('admin-media-accueil') || null, - }, - }); + const donnees = { + theme: document.getElementById('admin-theme').value, + nom: getValue('admin-nom-event'), + couleur_primaire: getValue('admin-couleur-primaire'), + couleur_secondaire: getValue('admin-couleur-secondaire'), + media_accueil: getValue('admin-media-accueil') || null, + }; + + if (eventEditId) { + await apiPost('/api/config', { evenement: { ...donnees, event_id: eventEditId } }); + await fetch(`/api/evenements/${eventEditId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(donnees), + }); + } else { + await apiPost('/api/config', { evenement: donnees }); + } appliquerConfig(); + await chargerListeEvenements(); afficherStatut('Evenement sauvegarde', 'succes'); } +// === CADRES EVENEMENT === + +const FORMATS_EVENT = ['strip', '10x15', '15x20']; + +async function chargerCadresEvent() { + if (!eventEditId) return; + const container = document.getElementById('cadres-event-formats'); + container.innerHTML = ''; + + for (const fmt of FORMATS_EVENT) { + const data = await apiGet(`/api/evenements/${eventEditId}/cadres/${fmt}`); + const section = document.createElement('div'); + section.className = 'cadre-event-section'; + + let cadresHtml = ''; + for (const nom of data.disponibles) { + const selected = data.cadre === nom ? ' cadre-selectionne' : ''; + cadresHtml += `
+ ${nom} + ${nom.replace('.png','')} + +
`; + } + + section.innerHTML = ` +

Format ${fmt}

+
+ + +
+
${cadresHtml || '

Aucun cadre

'}
+ + + `; + container.appendChild(section); + } +} + +async function selectionnerCadreEvent(fmt, nom) { + if (!eventEditId) return; + const mode = document.getElementById(`mode-cadre-event-${fmt}`).value; + await apiPost(`/api/evenements/${eventEditId}/cadres/${fmt}`, { mode, cadre: nom }); + await chargerCadresEvent(); +} + +async function changerModeCadreEvent(fmt) { + if (!eventEditId) return; + const mode = document.getElementById(`mode-cadre-event-${fmt}`).value; + const event = await apiGet(`/api/evenements/${eventEditId}`); + const cadre = (event.cadres || {})[fmt]?.cadre || null; + await apiPost(`/api/evenements/${eventEditId}/cadres/${fmt}`, { mode, cadre }); + afficherStatut(`Mode cadre ${fmt} : ${mode}`, 'succes'); +} + +async function uploaderCadreEvent(fmt) { + if (!eventEditId) return; + const input = document.getElementById(`input-cadre-event-${fmt}`); + if (!input.files.length) return; + + const formData = new FormData(); + formData.append('fichier', input.files[0]); + await fetch(`/api/evenements/${eventEditId}/upload-cadre/${fmt}`, { method: 'POST', body: formData }); + input.value = ''; + await chargerCadresEvent(); + afficherStatut(`Cadre ${fmt} importe`, 'succes'); +} + +async function supprimerCadreEvent(fmt, nom) { + if (!eventEditId) return; + await fetch(`/api/evenements/${eventEditId}/cadres/${fmt}/${encodeURIComponent(nom)}`, { method: 'DELETE' }); + await chargerCadresEvent(); + afficherStatut('Cadre supprime', 'succes'); +} + // === EMAIL === function chargerEmailAdmin() { diff --git a/frontend/js/share.js b/frontend/js/share.js index 9c461ac..8e3bbd5 100644 --- a/frontend/js/share.js +++ b/frontend/js/share.js @@ -35,13 +35,25 @@ function annulerCadreImpression() { async function chargerCadresChoixImpression() { const fmt = (config.impression?.format || '15x20').replace('-2up', ''); const data = await apiGet(`/api/cadres-impression/${fmt}`).catch(() => null); - if (!data || data.disponibles.length === 0) return false; + if (!data) return false; + + // Mode impose : pas de choix, cadre force + if (data.event_mode === 'impose' && data.event_cadre) { + cadreChoisi = data.event_cadre; + return false; + } + + const tousLesGlobaux = data.disponibles || []; + const tousLesEvent = data.event_cadres || []; + if (tousLesGlobaux.length === 0 && tousLesEvent.length === 0) return false; const grille = document.getElementById('grille-cadres-impression'); grille.innerHTML = ''; + const eventCadreDefaut = (data.event_mode === 'defaut') ? data.event_cadre : null; + const divAucun = document.createElement('div'); - divAucun.className = 'cadre-choix-item cadre-choix-aucun cadre-selectionne'; + divAucun.className = 'cadre-choix-item cadre-choix-aucun' + (!eventCadreDefaut ? ' cadre-selectionne' : ''); divAucun.innerHTML = '
Sans cadre'; divAucun.onclick = () => { cadreChoisi = null; @@ -50,7 +62,23 @@ async function chargerCadresChoixImpression() { }; grille.appendChild(divAucun); - for (const nom of data.disponibles) { + // Cadres evenement + for (const nom of tousLesEvent) { + const isDefaut = (nom === eventCadreDefaut); + const div = document.createElement('div'); + div.className = 'cadre-choix-item' + (isDefaut ? ' cadre-selectionne' : ''); + div.innerHTML = `
${nom}
${nom.replace('.png','')}`; + div.onclick = () => { + cadreChoisi = nom; + grille.querySelectorAll('.cadre-choix-item').forEach(el => el.classList.remove('cadre-selectionne')); + div.classList.add('cadre-selectionne'); + }; + grille.appendChild(div); + if (isDefaut) cadreChoisi = nom; + } + + // Cadres globaux + for (const nom of tousLesGlobaux) { const div = document.createElement('div'); div.className = 'cadre-choix-item'; div.innerHTML = `
${nom}
${nom.replace('.png','')}`;