Gestion evenements + cadres par event + fix cadre paysage + dossier non-imprimees

- CRUD evenements : creer, activer, editer, supprimer dans panneau admin
- Cadres specifiques par evenement (aucun/disponible/defaut/impose)
- Fix cadre paysage deformé : rotation auto si orientation ne correspond pas
- Photos non imprimees vont dans sous-dossier "non_imprimees" (USB/FTP)
  au lieu d'etre ignorees ; booth recoit toujours toutes les photos

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 00:16:35 +02:00
parent 545d8cd6ad
commit 9ce8fc0e55
9 changed files with 621 additions and 60 deletions

View File

@@ -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;

View File

@@ -7,7 +7,7 @@
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta name="google" content="notranslate">
<meta http-equiv="Content-Language" content="fr">
<link rel="stylesheet" href="/css/style.css?v=5">
<link rel="stylesheet" href="/css/style.css?v=6">
<link rel="stylesheet" href="/css/themes.css">
</head>
<body>
@@ -578,37 +578,51 @@
<!-- Panneau Evenement -->
<div class="admin-panneau" id="panneau-evenement">
<div class="champ">
<label>Theme visuel</label>
<select id="admin-theme" onchange="changerTheme(this.value)">
<option value="base">Base (Sombre moderne)</option>
<option value="sikapics">SikaPics (Rose elegant)</option>
</select>
<h3>Evenements</h3>
<div class="champ-row" style="margin-bottom:1rem">
<input type="text" id="input-nouveau-event" placeholder="Nom du nouvel evenement" style="flex:1">
<button class="btn-action btn-petit" onclick="creerEvenementAdmin()">Creer</button>
</div>
<div class="champ">
<label>Nom de l'evenement</label>
<input type="text" id="admin-nom-event" placeholder="Mon Evenement">
</div>
<div class="champ">
<label>Video/GIF d'accueil (boucle)</label>
<div class="champ-row">
<select id="admin-media-accueil">
<option value="">Aucun (animation par defaut)</option>
<div id="liste-evenements"></div>
<h3 id="titre-event-detail" class="cache">Configuration evenement</h3>
<div id="detail-evenement" class="cache">
<div class="champ">
<label>Theme visuel</label>
<select id="admin-theme" onchange="changerTheme(this.value)">
<option value="base">Base (Sombre moderne)</option>
<option value="sikapics">SikaPics (Rose elegant)</option>
</select>
<button class="btn-secondaire btn-petit" onclick="rafraichirMediaAccueil()">Rafraichir</button>
</div>
<input type="file" id="input-media-accueil" accept=".gif,.mp4,.webm" class="input-fichier">
<button class="btn-secondaire btn-petit" onclick="uploaderMediaAccueil()">Importer</button>
<div class="champ">
<label>Nom de l'evenement</label>
<input type="text" id="admin-nom-event" placeholder="Mon Evenement">
</div>
<div class="champ">
<label>Video/GIF d'accueil (boucle)</label>
<div class="champ-row">
<select id="admin-media-accueil">
<option value="">Aucun (animation par defaut)</option>
</select>
<button class="btn-secondaire btn-petit" onclick="rafraichirMediaAccueil()">Rafraichir</button>
</div>
<input type="file" id="input-media-accueil" accept=".gif,.mp4,.webm" class="input-fichier">
<button class="btn-secondaire btn-petit" onclick="uploaderMediaAccueil()">Importer</button>
</div>
<div class="champ">
<label>Couleur primaire</label>
<input type="color" id="admin-couleur-primaire" value="#e91e63">
</div>
<div class="champ">
<label>Couleur secondaire</label>
<input type="color" id="admin-couleur-secondaire" value="#ffffff">
</div>
<button class="btn-action" onclick="sauvegarderEvenement()">Sauvegarder</button>
<h3>Cadres evenement</h3>
<p class="aide">Cadres specifiques a cet evenement (n'apparaissent pas dans les autres).</p>
<div id="cadres-event-formats"></div>
</div>
<div class="champ">
<label>Couleur primaire</label>
<input type="color" id="admin-couleur-primaire" value="#e91e63">
</div>
<div class="champ">
<label>Couleur secondaire</label>
<input type="color" id="admin-couleur-secondaire" value="#ffffff">
</div>
<button class="btn-action" onclick="sauvegarderEvenement()">Sauvegarder</button>
</div>
<!-- Panneau Fonctions (toggles + email + booth + galerie + systeme) -->
@@ -743,7 +757,7 @@
<script src="/js/camera.js?v=11"></script>
<script src="/js/effects.js?v=3"></script>
<script src="/js/gallery.js?v=3"></script>
<script src="/js/share.js?v=6"></script>
<script src="/js/admin.js?v=5"></script>
<script src="/js/share.js?v=7"></script>
<script src="/js/admin.js?v=6"></script>
</body>
</html>

View File

@@ -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 = `
<span class="event-nom">${ev.nom}</span>
<span class="event-actions">
${ev.id !== actifId ? `<button class="btn-secondaire btn-petit" onclick="activerEventAdmin('${ev.id}')">Activer</button>` : '<span class="badge-actif">Actif</span>'}
<button class="btn-secondaire btn-petit" onclick="editerEventAdmin('${ev.id}')">Editer</button>
<button class="btn-danger btn-petit" onclick="supprimerEventAdmin('${ev.id}','${ev.nom}')">Suppr</button>
</span>`;
liste.appendChild(div);
}
if (events.length === 0) {
liste.innerHTML = '<p class="texte-secondaire">Aucun evenement. Creez-en un ci-dessus.</p>';
}
}
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 += `<div class="cadre-event-item${selected}" onclick="selectionnerCadreEvent('${fmt}','${nom}')">
<img src="/data/evenements/${eventEditId}/cadres/${fmt}/${nom}" alt="${nom}">
<span>${nom.replace('.png','')}</span>
<button class="btn-danger btn-icone" onclick="event.stopPropagation();supprimerCadreEvent('${fmt}','${nom}')">✕</button>
</div>`;
}
section.innerHTML = `
<h4>Format ${fmt}</h4>
<div class="champ">
<label>Mode cadre</label>
<select id="mode-cadre-event-${fmt}" onchange="changerModeCadreEvent('${fmt}')">
<option value="aucun" ${data.mode === 'aucun' ? 'selected' : ''}>Aucun</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="impose" ${data.mode === 'impose' ? 'selected' : ''}>Impose (toujours applique)</option>
</select>
</div>
<div class="cadres-event-grille">${cadresHtml || '<p class="texte-secondaire">Aucun cadre</p>'}</div>
<input type="file" id="input-cadre-event-${fmt}" accept=".png" class="input-fichier">
<button class="btn-secondaire btn-petit" onclick="uploaderCadreEvent('${fmt}')">Importer cadre ${fmt}</button>
`;
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() {

View File

@@ -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 = '<div class="cadre-choix-vide"></div><span>Sans cadre</span>';
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 = `<div class="cadre-thumb-wrap"><img src="/data/evenements/${data.event_id}/cadres/${fmt}/${nom}" alt="${nom}"></div><span>${nom.replace('.png','')}</span>`;
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 = `<div class="cadre-thumb-wrap"><img src="/assets/cadres/${fmt}/${nom}" alt="${nom}"></div><span>${nom.replace('.png','')}</span>`;