Formats activables par evenement + cadre impose exclut strips

- Toggles formats (10x15, 15x20, strip) par evenement
- Strip desactive masque le bouton pellicule sur l'accueil
- Cadre impose/defaut s'applique uniquement sur 10x15 et 15x20 (pas strip)
- Upload cadre evenement le copie dans les deux formats automatiquement
- Section cadres admin simplifiee (un seul bloc au lieu de 3)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 00:45:47 +02:00
parent 77b1127816
commit 6bbcf85e52
5 changed files with 93 additions and 53 deletions

View File

@@ -45,6 +45,7 @@ def creer_evenement(nom: str, **kwargs) -> dict:
"couleur_primaire": kwargs.get("couleur_primaire", "#e91e63"), "couleur_primaire": kwargs.get("couleur_primaire", "#e91e63"),
"couleur_secondaire": kwargs.get("couleur_secondaire", "#ffffff"), "couleur_secondaire": kwargs.get("couleur_secondaire", "#ffffff"),
"media_accueil": kwargs.get("media_accueil"), "media_accueil": kwargs.get("media_accueil"),
"formats_actifs": kwargs.get("formats_actifs", ["10x15", "15x20", "strip"]),
"cadres": {}, "cadres": {},
} }
_chemin_event(event_id).write_text(json.dumps(event, ensure_ascii=False, indent=2), "utf-8") _chemin_event(event_id).write_text(json.dumps(event, ensure_ascii=False, indent=2), "utf-8")
@@ -97,6 +98,7 @@ def activer_evenement(event_id: str) -> dict | None:
"couleur_primaire": event.get("couleur_primaire", "#e91e63"), "couleur_primaire": event.get("couleur_primaire", "#e91e63"),
"couleur_secondaire": event.get("couleur_secondaire", "#ffffff"), "couleur_secondaire": event.get("couleur_secondaire", "#ffffff"),
"media_accueil": event.get("media_accueil"), "media_accueil": event.get("media_accueil"),
"formats_actifs": event.get("formats_actifs", ["10x15", "15x20", "strip"]),
} }
}) })
log.info(f"Evenement active : {event['nom']} ({event_id})") log.info(f"Evenement active : {event['nom']} ({event_id})")
@@ -135,7 +137,10 @@ def set_cadre_event(event_id: str, format_papier: str, mode: str, cadre: str | N
def cadre_actif_pour_impression(format_papier: str) -> tuple[str | None, str]: def cadre_actif_pour_impression(format_papier: str) -> tuple[str | None, str]:
"""Retourne (nom_cadre, mode) en tenant compte de l'evenement actif. """Retourne (nom_cadre, mode) en tenant compte de l'evenement actif.
mode: 'aucun', 'disponible', 'impose', 'defaut' mode: 'aucun', 'disponible', 'impose', 'defaut'
Strips exclus du cadre impose/defaut.
""" """
if format_papier == "strip":
return None, "aucun"
config = charger_config() config = charger_config()
event_id = config.get("evenement", {}).get("event_id") event_id = config.get("evenement", {}).get("event_id")
if not event_id: if not event_id:

View File

@@ -626,8 +626,16 @@
</div> </div>
<button class="btn-action" onclick="sauvegarderEvenement()">Sauvegarder</button> <button class="btn-action" onclick="sauvegarderEvenement()">Sauvegarder</button>
<h3>Cadres evenement</h3> <h3>Formats d'impression</h3>
<p class="aide">Cadres specifiques a cet evenement (n'apparaissent pas dans les autres).</p> <p class="aide">Formats papier disponibles pour cet evenement.</p>
<div class="toggle-groupe" id="event-formats-toggles">
<label class="toggle"><input type="checkbox" id="tog-event-fmt-10x15"><span class="toggle-slider"></span> 10&times;15 cm</label>
<label class="toggle"><input type="checkbox" id="tog-event-fmt-15x20"><span class="toggle-slider"></span> 15&times;20 cm</label>
<label class="toggle"><input type="checkbox" id="tog-event-fmt-strip"><span class="toggle-slider"></span> Strip / Pellicule</label>
</div>
<h3>Cadre evenement</h3>
<p class="aide">Cadre specifique a cet evenement. Applique sur tous les formats sauf strips.</p>
<div id="cadres-event-formats"></div> <div id="cadres-event-formats"></div>
</div> </div>
</div> </div>
@@ -760,11 +768,11 @@
</div> </div>
<script src="/js/websocket.js?v=3"></script> <script src="/js/websocket.js?v=3"></script>
<script src="/js/app.js?v=5"></script> <script src="/js/app.js?v=6"></script>
<script src="/js/camera.js?v=12"></script> <script src="/js/camera.js?v=13"></script>
<script src="/js/effects.js?v=4"></script> <script src="/js/effects.js?v=4"></script>
<script src="/js/gallery.js?v=3"></script> <script src="/js/gallery.js?v=3"></script>
<script src="/js/share.js?v=7"></script> <script src="/js/share.js?v=7"></script>
<script src="/js/admin.js?v=6"></script> <script src="/js/admin.js?v=7"></script>
</body> </body>
</html> </html>

View File

@@ -762,6 +762,12 @@ function afficherDetailEvent(event) {
const select = document.getElementById('admin-media-accueil'); const select = document.getElementById('admin-media-accueil');
if (event.media_accueil) select.value = event.media_accueil; if (event.media_accueil) select.value = event.media_accueil;
}); });
const fmts = event.formats_actifs || ['10x15', '15x20', 'strip'];
document.getElementById('tog-event-fmt-10x15').checked = fmts.includes('10x15');
document.getElementById('tog-event-fmt-15x20').checked = fmts.includes('15x20');
document.getElementById('tog-event-fmt-strip').checked = fmts.includes('strip');
chargerCadresEvent(); chargerCadresEvent();
} }
@@ -809,12 +815,18 @@ async function uploaderMediaAccueil() {
} }
async function sauvegarderEvenement() { async function sauvegarderEvenement() {
const formats_actifs = [];
if (document.getElementById('tog-event-fmt-10x15').checked) formats_actifs.push('10x15');
if (document.getElementById('tog-event-fmt-15x20').checked) formats_actifs.push('15x20');
if (document.getElementById('tog-event-fmt-strip').checked) formats_actifs.push('strip');
const donnees = { const donnees = {
theme: document.getElementById('admin-theme').value, theme: document.getElementById('admin-theme').value,
nom: getValue('admin-nom-event'), nom: getValue('admin-nom-event'),
couleur_primaire: getValue('admin-couleur-primaire'), couleur_primaire: getValue('admin-couleur-primaire'),
couleur_secondaire: getValue('admin-couleur-secondaire'), couleur_secondaire: getValue('admin-couleur-secondaire'),
media_accueil: getValue('admin-media-accueil') || null, media_accueil: getValue('admin-media-accueil') || null,
formats_actifs,
}; };
if (eventEditId) { if (eventEditId) {
@@ -834,79 +846,88 @@ async function sauvegarderEvenement() {
// === CADRES EVENEMENT === // === CADRES EVENEMENT ===
const FORMATS_EVENT = ['strip', '10x15', '15x20']; const FORMATS_CADRE_EVENT = ['10x15', '15x20'];
async function chargerCadresEvent() { async function chargerCadresEvent() {
if (!eventEditId) return; if (!eventEditId) return;
const container = document.getElementById('cadres-event-formats'); const container = document.getElementById('cadres-event-formats');
container.innerHTML = ''; container.innerHTML = '';
for (const fmt of FORMATS_EVENT) { // Charger cadres du premier format pour afficher le mode + liste
const data = await apiGet(`/api/evenements/${eventEditId}/cadres/${fmt}`); const data = await apiGet(`/api/evenements/${eventEditId}/cadres/15x20`);
const section = document.createElement('div'); const data2 = await apiGet(`/api/evenements/${eventEditId}/cadres/10x15`);
section.className = 'cadre-event-section'; const tous = [...new Set([...(data.disponibles || []), ...(data2.disponibles || [])])];
let cadresHtml = ''; let cadresHtml = '';
for (const nom of data.disponibles) { for (const nom of tous) {
const selected = data.cadre === nom ? ' cadre-selectionne' : ''; const selected = data.cadre === nom ? ' cadre-selectionne' : '';
cadresHtml += `<div class="cadre-event-item${selected}" onclick="selectionnerCadreEvent('${fmt}','${nom}')"> const src15 = `/data/evenements/${eventEditId}/cadres/15x20/${nom}`;
<img src="/data/evenements/${eventEditId}/cadres/${fmt}/${nom}" alt="${nom}"> const src10 = `/data/evenements/${eventEditId}/cadres/10x15/${nom}`;
cadresHtml += `<div class="cadre-event-item${selected}" onclick="selectionnerCadreEvent('${nom}')">
<img src="${src15}" onerror="this.src='${src10}'" alt="${nom}">
<span>${nom.replace('.png','')}</span> <span>${nom.replace('.png','')}</span>
<button class="btn-danger btn-icone" onclick="event.stopPropagation();supprimerCadreEvent('${fmt}','${nom}')">✕</button> <button class="btn-danger btn-icone" onclick="event.stopPropagation();supprimerCadreEvent('${nom}')">✕</button>
</div>`; </div>`;
} }
section.innerHTML = ` container.innerHTML = `
<h4>Format ${fmt}</h4>
<div class="champ"> <div class="champ">
<label>Mode cadre</label> <label>Mode cadre</label>
<select id="mode-cadre-event-${fmt}" onchange="changerModeCadreEvent('${fmt}')"> <select id="mode-cadre-event" onchange="changerModeCadreEvent()">
<option value="aucun" ${data.mode === 'aucun' ? 'selected' : ''}>Aucun</option> <option value="aucun" ${data.mode === 'aucun' ? 'selected' : ''}>Aucun</option>
<option value="disponible" ${data.mode === 'disponible' ? 'selected' : ''}>Disponible (choix utilisateur)</option> <option value="disponible" ${data.mode === 'disponible' ? 'selected' : ''}>Disponible (choix utilisateur)</option>
<option value="defaut" ${data.mode === 'defaut' ? 'selected' : ''}>Par defaut (pre-selectionne)</option> <option value="defaut" ${data.mode === 'defaut' ? 'selected' : ''}>Par defaut (pre-selectionne)</option>
<option value="impose" ${data.mode === 'impose' ? 'selected' : ''}>Impose (toujours applique)</option> <option value="impose" ${data.mode === 'impose' ? 'selected' : ''}>Impose (toujours applique)</option>
</select> </select>
</div> </div>
<div class="cadres-event-grille">${cadresHtml || '<p class="texte-secondaire">Aucun cadre</p>'}</div> <div class="cadres-event-grille">${cadresHtml || '<p class="texte-secondaire">Aucun cadre importe</p>'}</div>
<input type="file" id="input-cadre-event-${fmt}" accept=".png" class="input-fichier"> <input type="file" id="input-cadre-event" accept=".png" class="input-fichier">
<button class="btn-secondaire btn-petit" onclick="uploaderCadreEvent('${fmt}')">Importer cadre ${fmt}</button> <button class="btn-secondaire btn-petit" onclick="uploaderCadreEvent()">Importer cadre</button>
`; `;
container.appendChild(section);
}
} }
async function selectionnerCadreEvent(fmt, nom) { async function selectionnerCadreEvent(nom) {
if (!eventEditId) return; if (!eventEditId) return;
const mode = document.getElementById(`mode-cadre-event-${fmt}`).value; const mode = document.getElementById('mode-cadre-event').value;
for (const fmt of FORMATS_CADRE_EVENT) {
await apiPost(`/api/evenements/${eventEditId}/cadres/${fmt}`, { mode, cadre: nom }); await apiPost(`/api/evenements/${eventEditId}/cadres/${fmt}`, { mode, cadre: nom });
}
await chargerCadresEvent(); await chargerCadresEvent();
} }
async function changerModeCadreEvent(fmt) { async function changerModeCadreEvent() {
if (!eventEditId) return; if (!eventEditId) return;
const mode = document.getElementById(`mode-cadre-event-${fmt}`).value; const mode = document.getElementById('mode-cadre-event').value;
const event = await apiGet(`/api/evenements/${eventEditId}`); const ev = await apiGet(`/api/evenements/${eventEditId}`);
const cadre = (event.cadres || {})[fmt]?.cadre || null; const cadre = (ev.cadres || {})['15x20']?.cadre || (ev.cadres || {})['10x15']?.cadre || null;
for (const fmt of FORMATS_CADRE_EVENT) {
await apiPost(`/api/evenements/${eventEditId}/cadres/${fmt}`, { mode, cadre }); await apiPost(`/api/evenements/${eventEditId}/cadres/${fmt}`, { mode, cadre });
afficherStatut(`Mode cadre ${fmt} : ${mode}`, 'succes'); }
afficherStatut(`Mode cadre : ${mode}`, 'succes');
} }
async function uploaderCadreEvent(fmt) { async function uploaderCadreEvent() {
if (!eventEditId) return; if (!eventEditId) return;
const input = document.getElementById(`input-cadre-event-${fmt}`); const input = document.getElementById('input-cadre-event');
if (!input.files.length) return; if (!input.files.length) return;
const formData = new FormData(); const formData = new FormData();
formData.append('fichier', input.files[0]); formData.append('fichier', input.files[0]);
await fetch(`/api/evenements/${eventEditId}/upload-cadre/${fmt}`, { method: 'POST', body: formData }); for (const fmt of FORMATS_CADRE_EVENT) {
const fd = new FormData();
fd.append('fichier', input.files[0]);
await fetch(`/api/evenements/${eventEditId}/upload-cadre/${fmt}`, { method: 'POST', body: fd });
}
input.value = ''; input.value = '';
await chargerCadresEvent(); await chargerCadresEvent();
afficherStatut(`Cadre ${fmt} importe`, 'succes'); afficherStatut('Cadre importe (10x15 + 15x20)', 'succes');
} }
async function supprimerCadreEvent(fmt, nom) { async function supprimerCadreEvent(nom) {
if (!eventEditId) return; if (!eventEditId) return;
await fetch(`/api/evenements/${eventEditId}/cadres/${fmt}/${encodeURIComponent(nom)}`, { method: 'DELETE' }); for (const fmt of FORMATS_CADRE_EVENT) {
await fetch(`/api/evenements/${eventEditId}/cadres/${fmt}/${encodeURIComponent(nom)}`, { method: 'DELETE' }).catch(() => {});
}
await chargerCadresEvent(); await chargerCadresEvent();
afficherStatut('Cadre supprime', 'succes'); afficherStatut('Cadre supprime', 'succes');
} }

View File

@@ -48,8 +48,10 @@ function appliquerConfig() {
const h1 = document.getElementById('nom-evenement'); const h1 = document.getElementById('nom-evenement');
if (h1) h1.textContent = event.nom || 'Photobooth'; if (h1) h1.textContent = event.nom || 'Photobooth';
// Visibilite des modes // Visibilite des modes (restreint par formats evenement)
toggleVisible('btn-multi', fonc.multi_shot); const fmtsEvent = event.formats_actifs;
const stripDispo = !fmtsEvent || fmtsEvent.includes('strip');
toggleVisible('btn-multi', fonc.multi_shot && stripDispo);
toggleVisible('btn-imprimer', fonc.impression); toggleVisible('btn-imprimer', fonc.impression);
toggleVisible('btn-email', fonc.email); toggleVisible('btn-email', fonc.email);
const boothActif = config.booth?.actif; const boothActif = config.booth?.actif;

View File

@@ -437,6 +437,10 @@ async function majBoutonImprimerCompteur() {
} }
async function chargerCadreImpose() { async function chargerCadreImpose() {
if (modeActuel === 'multi') {
afficherCadreOverlay(null);
return;
}
const fmt = (config.impression?.format || '15x20').replace('-2up', ''); const fmt = (config.impression?.format || '15x20').replace('-2up', '');
const data = await apiGet(`/api/cadres-impression/${fmt}`).catch(() => null); const data = await apiGet(`/api/cadres-impression/${fmt}`).catch(() => null);
if (!data) { afficherCadreOverlay(null); return; } if (!data) { afficherCadreOverlay(null); return; }