diff --git a/backend/destinations.py b/backend/destinations.py index 1a1775c..8242561 100644 --- a/backend/destinations.py +++ b/backend/destinations.py @@ -11,7 +11,7 @@ from backend.config import charger_config, mettre_a_jour_config log = logging.getLogger("photobooth.destinations") -def distribuer_photo(chemin_photo: Path, imprimee: bool = False, copies: int = 1): +def distribuer_photo(chemin_photo: Path, imprimee: bool = False, copies: int = 1, format_papier: str | None = None): """Copie la photo vers toutes les destinations activees.""" config = charger_config() dest = config.get("destinations", {}) @@ -43,6 +43,7 @@ def distribuer_photo(chemin_photo: Path, imprimee: bool = False, copies: int = 1 if compteur.get("actif", False): compteur["photos_prises"] = compteur.get("photos_prises", 0) + copies mettre_a_jour_config({"compteur": compteur}) + maj_consommables(config, copies, format_papier) def copier_usb(chemin_photo: Path, chemin_usb: str, sous_dossier: str | None = None): @@ -218,3 +219,65 @@ def compteur_restant() -> dict: def reset_compteur(): """Remet le compteur a zero.""" mettre_a_jour_config({"compteur": {"photos_prises": 0}}) + + +# --- Suivi consommables --- + +TAILLE_RUBAN = { + "15x20": 1.0, + "10x15": 0.667, + "10x15-2up": 1.0, + "15x20-2up": 1.0, +} + +def maj_consommables(config: dict, copies: int, format_papier: str | None): + """Met a jour le suivi consommables (papier, ruban, photos).""" + conso = config.get("consommables", {}) + fmt = format_papier or config.get("impression", {}).get("format", "15x20") + rembobinage = config.get("impression", {}).get("rembobinage_ruban", False) + is_2up = "-2up" in fmt + + feuilles = copies if not is_2up else (copies + 1) // 2 + conso["papier_utilise"] = conso.get("papier_utilise", 0) + feuilles + conso["photos_imprimees"] = conso.get("photos_imprimees", 0) + copies + + if rembobinage: + ruban_par_feuille = TAILLE_RUBAN.get(fmt, 1.0) + else: + ruban_par_feuille = 1.0 + conso["ruban_utilise"] = round(conso.get("ruban_utilise", 0) + feuilles * ruban_par_feuille, 1) + + mettre_a_jour_config({"consommables": conso}) + + +def consommables_etat() -> dict: + """Retourne l'etat des consommables.""" + config = charger_config() + conso = config.get("consommables", {}) + papier_cap = conso.get("papier_capacite", 400) + ruban_cap = conso.get("ruban_capacite", 400) + papier_used = conso.get("papier_utilise", 0) + ruban_used = conso.get("ruban_utilise", 0) + photos = conso.get("photos_imprimees", 0) + return { + "papier_capacite": papier_cap, + "papier_utilise": papier_used, + "papier_restant": max(0, papier_cap - papier_used), + "ruban_capacite": ruban_cap, + "ruban_utilise": ruban_used, + "ruban_restant": max(0, round(ruban_cap - ruban_used, 1)), + "photos_imprimees": photos, + } + + +def reset_consommables(quoi: str): + """Reinitialise papier, ruban ou tout.""" + config = charger_config() + conso = config.get("consommables", {}) + if quoi in ("papier", "tout"): + conso["papier_utilise"] = 0 + if quoi in ("ruban", "tout"): + conso["ruban_utilise"] = 0 + if quoi == "tout": + conso["photos_imprimees"] = 0 + mettre_a_jour_config({"consommables": conso}) diff --git a/backend/main.py b/backend/main.py index 09634e0..c7f5431 100644 --- a/backend/main.py +++ b/backend/main.py @@ -27,7 +27,7 @@ except ImportError: from backend.gallery import lister_photos, compter_photos, supprimer_photo, vider_galerie from backend.effects import appliquer_filtre, appliquer_overlay, chroma_key, lister_overlays, lister_fonds, lister_cadres, FILTRES from backend.collage import creer_strip, creer_collage -from backend.destinations import distribuer_photo, detecter_usb, compteur_restant, reset_compteur, recuperer_booth_password +from backend.destinations import distribuer_photo, detecter_usb, compteur_restant, reset_compteur, recuperer_booth_password, consommables_etat, reset_consommables 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 @@ -903,10 +903,41 @@ async def api_imprimer(donnees: dict): format_papier = donnees.get("format_papier") or None resultat = imprimer(chemin, copies=copies, cadre_override=cadre_override, format_papier=format_papier) if resultat.get("succes"): - distribuer_photo(chemin, imprimee=True, copies=copies) + distribuer_photo(chemin, imprimee=True, copies=copies, format_papier=format_papier) return resultat +# --- API Consommables --- + +@app.get("/api/consommables") +async def api_consommables(): + return consommables_etat() + + +@app.post("/api/consommables/reset") +async def api_reset_consommables(request: Request): + body = await request.json() + quoi = body.get("quoi", "tout") + reset_consommables(quoi) + return consommables_etat() + + +@app.post("/api/consommables/capacite") +async def api_consommables_capacite(request: Request): + body = await request.json() + updates = {} + if "papier_capacite" in body: + updates["papier_capacite"] = int(body["papier_capacite"]) + if "ruban_capacite" in body: + updates["ruban_capacite"] = int(body["ruban_capacite"]) + if updates: + config = charger_config() + conso = config.get("consommables", {}) + conso.update(updates) + mettre_a_jour_config({"consommables": conso}) + return consommables_etat() + + # --- API Booth (galerie live) --- @app.get("/api/booth/info") diff --git a/frontend/css/style.css b/frontend/css/style.css index 1cfa957..c8a517b 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -935,6 +935,42 @@ html, body { } .admin-onglets.cache { display: none; } +.conso-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + margin-bottom: 1.5rem; +} +.conso-card { + background: rgba(255,255,255,0.04); + border-radius: 12px; + padding: 1.2rem; + text-align: center; +} +.conso-icon { font-size: 2rem; margin-bottom: 0.3rem; } +.conso-titre { font-weight: 600; margin-bottom: 0.8rem; font-size: 0.95rem; } +.conso-barre-fond { + height: 12px; + background: rgba(255,255,255,0.08); + border-radius: 6px; + overflow: hidden; + margin-bottom: 0.5rem; +} +.conso-barre { + height: 100%; + border-radius: 6px; + transition: width 0.5s ease, background 0.5s ease; +} +.conso-chiffres { font-size: 0.85rem; color: var(--texte-secondaire); margin-bottom: 0.8rem; } +.conso-gros { font-size: 2.5rem; font-weight: 800; margin: 0.5rem 0; } +.conso-diag { + background: rgba(255,255,255,0.03); + border-radius: 8px; + padding: 1rem; + font-size: 0.85rem; + line-height: 1.6; +} + .video-situation-bloc { background: rgba(255,255,255,0.03); border-radius: 8px; diff --git a/frontend/index.html b/frontend/index.html index f68f66e..376a9b6 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -7,7 +7,7 @@ - + @@ -379,6 +379,7 @@
+ @@ -476,6 +477,48 @@
+ +
+

Suivi consommables

+

Suivez l'usure du papier et du ruban. Reinitalisez quand vous changez une bobine.

+ +
+
+
📄
+
Papier
+
+
-- / -- feuilles
+ +
+
+
🎨
+
Ruban
+
+
-- / -- poses
+ +
+
+
📷
+
Photos sorties
+
--
+
+
+ +

Capacites

+
+ + +
+
+ + +
+ + + +
+
+

Ou sauvegarder les photos ?

@@ -1017,6 +1060,6 @@ - + diff --git a/frontend/js/admin.js b/frontend/js/admin.js index 0620530..0d1845c 100644 --- a/frontend/js/admin.js +++ b/frontend/js/admin.js @@ -1401,6 +1401,67 @@ async function chargerDiagCamera() { } } +// === CONSOMMABLES === + +async function chargerConsommables() { + try { + const data = await apiGet('/api/consommables'); + document.getElementById('conso-papier-restant').textContent = data.papier_restant; + document.getElementById('conso-papier-cap').textContent = data.papier_capacite; + document.getElementById('conso-ruban-restant').textContent = data.ruban_restant; + document.getElementById('conso-ruban-cap').textContent = data.ruban_capacite; + document.getElementById('conso-photos').textContent = data.photos_imprimees; + setValue('conso-cap-papier', data.papier_capacite); + setValue('conso-cap-ruban', data.ruban_capacite); + + const pctPapier = data.papier_capacite > 0 ? Math.max(0, (data.papier_restant / data.papier_capacite) * 100) : 0; + const pctRuban = data.ruban_capacite > 0 ? Math.max(0, (data.ruban_restant / data.ruban_capacite) * 100) : 0; + + const barrePapier = document.getElementById('conso-barre-papier'); + const barreRuban = document.getElementById('conso-barre-ruban'); + barrePapier.style.width = pctPapier + '%'; + barreRuban.style.width = pctRuban + '%'; + barrePapier.style.background = pctPapier < 10 ? '#f44336' : pctPapier < 25 ? '#fb8c00' : '#43a047'; + barreRuban.style.background = pctRuban < 10 ? '#f44336' : pctRuban < 25 ? '#fb8c00' : '#43a047'; + + const diag = document.getElementById('conso-diagnostic'); + const papierUsed = data.papier_utilise; + const rubanUsed = data.ruban_utilise; + let html = 'Diagnostic
'; + html += `Feuilles consommees : ${papierUsed}
`; + html += `Poses ruban consommees : ${rubanUsed}
`; + html += `Photos imprimees : ${data.photos_imprimees}
`; + if (papierUsed > 0) { + const ratio = (rubanUsed / papierUsed).toFixed(2); + html += `
Ratio ruban/papier : ${ratio}`; + if (parseFloat(ratio) <= 0.75) { + html += ' ✅ Rembobinage actif (economie ruban)'; + } else if (parseFloat(ratio) >= 0.95) { + html += ' ⚠ Ratio 1:1 — pas d\'economie ruban'; + } + } + diag.innerHTML = html; + } catch (e) { + console.warn('Erreur chargement consommables:', e); + } +} + +async function resetConsommable(quoi) { + const labels = { papier: 'Reinitialiser le compteur papier ?', ruban: 'Reinitialiser le compteur ruban ?', tout: 'Reinitialiser tous les compteurs ?' }; + if (!confirm(labels[quoi] || 'Reinitialiser ?')) return; + await apiPost('/api/consommables/reset', { quoi }); + chargerConsommables(); + afficherStatut('Consommable reinitialise', 'succes'); +} + +async function sauvegarderCapacites() { + const papier = parseInt(getValue('conso-cap-papier')) || 400; + const ruban = parseInt(getValue('conso-cap-ruban')) || 400; + await apiPost('/api/consommables/capacite', { papier_capacite: papier, ruban_capacite: ruban }); + chargerConsommables(); + afficherStatut('Capacites sauvegardees', 'succes'); +} + // === VIDEOS DE SITUATION === async function chargerVideosAdmin() {