Files
photobooth/frontend/js/admin.js
Jules 3aa3068127 Videos didactiques multi-etapes : wizard operateur + admin dynamique
Backend : 8 endpoints API (upload/delete/reorder/toggle/label/stream/creer etape),
stockage data/videos/{situation}/etape_XX.mp4, config.json toggle+labels,
6 situations par defaut, migration auto anciens fichiers flat.
Admin : panneau dynamique avec toggle, etapes reordonnables, labels editables.
Wizard booth : overlay plein ecran, video loop par etape, bouton validation sequentielle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-12 20:41:05 +02:00

1826 lines
64 KiB
JavaScript

/* Module administration - Backoffice complet */
// Mapping toggle ID -> cle config fonctionnalites
const TOGGLES_MAP = {
'tog-photo-simple': 'photo_simple',
'tog-multi-shot': 'multi_shot',
'tog-filtres': 'filtres',
'tog-overlays': 'overlays',
'tog-chroma-key': 'chroma_key',
'tog-impression': 'impression',
'tog-email': 'email',
'tog-qr-code': 'qr_code',
'tog-galerie': 'galerie',
};
async function chargerAdmin() {
config = await apiGet('/api/config');
chargerMateriel();
chargerCompteur();
chargerDestinations();
chargerCadres();
chargerCadresImpression();
chargerAnimations();
chargerFonctionnalites();
chargerEmailAdmin();
chargerFondsAdmin();
chargerGalerieAdmin();
chargerBoothAdmin();
chargerEvenementAdmin();
}
// === MATERIEL ===
async function chargerMateriel() {
const cam = config.camera || {};
const imp = config.impression || {};
// Camera
const statut = await apiGet('/api/camera/statut');
const badge = document.getElementById('admin-camera-statut');
badge.textContent = statut.connectee ? 'Connectee' : 'Deconnectee';
badge.style.background = statut.connectee ? 'rgba(76,175,80,0.2)' : 'rgba(244,67,54,0.2)';
badge.style.color = statut.connectee ? 'var(--succes)' : 'var(--danger)';
// Liste appareils
const selectApp = document.getElementById('admin-appareil');
selectApp.innerHTML = '';
for (const a of statut.appareils) {
const opt = document.createElement('option');
opt.value = a.source;
opt.textContent = `${a.nom} [${a.type}]`;
if (a.source === statut.mode || a.source === cam.appareil) opt.selected = true;
selectApp.appendChild(opt);
}
if (cam.appareil) selectApp.value = cam.appareil;
setValue('admin-car', cam.compte_a_rebours || 3);
document.getElementById('tog-flash-integre').checked = cam.flash_integre !== false;
// Imprimantes
await rafraichirImprimantes();
rafraichirStatutImprimante();
if (imp.imprimante) document.getElementById('admin-imprimante').value = imp.imprimante;
if (imp.format) document.getElementById('admin-format-papier').value = imp.format;
const fmt = (imp.format || '15x20').replace('-2up', '');
const orient = (imp.orientations || {})[fmt] || 'portrait';
document.querySelector(`input[name="admin-orientation"][value="${orient}"]`).checked = true;
setValue('admin-copies-max', imp.copies_max || 5);
document.getElementById('tog-rembobinage-ruban').checked = imp.rembobinage_ruban || false;
}
async function rafraichirImprimantes() {
const imprimantes = await apiGet('/api/imprimantes');
const select = document.getElementById('admin-imprimante');
select.innerHTML = '';
for (const p of imprimantes) {
const opt = document.createElement('option');
opt.value = p.nom;
opt.textContent = `${p.nom} (${p.statut})`;
select.appendChild(opt);
}
}
async function reconnecterCamera() {
const source = getValue('admin-appareil') || null;
const resultat = await apiPost('/api/camera/reconnecter', { source });
chargerMateriel();
}
function _getOrientations() {
const fmt = (getValue('admin-format-papier') || '15x20').replace('-2up', '');
const orient = document.querySelector('input[name="admin-orientation"]:checked')?.value || 'portrait';
// On conserve les orientations des autres formats depuis la config courante
const existing = (config.impression || {}).orientations || {};
return { ...existing, [fmt]: orient };
}
async function couperPapier() {
afficherStatut('Envoi commande coupe...', 'succes');
const r = await apiPost('/api/imprimante/couper', {});
afficherStatut(r.message, r.succes ? 'succes' : 'erreur');
}
async function resetUsb() {
afficherStatut('Réinitialisation USB...', 'succes');
const r = await apiPost('/api/imprimante/reset-usb', {});
afficherStatut(r.message, r.succes ? 'succes' : 'erreur');
}
async function evacuerBourrage() {
const r = await apiPost('/api/imprimante/evacuer', {});
afficherStatut(r.message || 'Imprimante réactivée', r.succes ? 'succes' : 'erreur');
const el = document.getElementById('imprimante-erreur-msg');
const bloc = document.getElementById('imprimante-statut-detail');
if (r.message && r.message !== 'Imprimante réactivée') {
el.textContent = '⚠ ' + r.message;
bloc.style.display = 'block';
} else {
bloc.style.display = 'none';
}
}
async function rafraichirStatutImprimante() {
const r = await apiGet('/api/imprimante/statut-detail').catch(() => null);
if (!r) return;
const el = document.getElementById('imprimante-erreur-msg');
const bloc = document.getElementById('imprimante-statut-detail');
if (r.derniere_erreur) {
let msg = r.derniere_erreur;
if (msg.includes('media does not match')) msg = '⚠ Format papier incorrect — vérifiez que le format configuré correspond à la cassette chargée dans l\'imprimante';
else if (msg.includes('jam')) msg = '⚠ Bourrage papier détecté';
else msg = '⚠ ' + msg.split(']').pop().trim();
el.textContent = msg;
bloc.style.display = 'block';
} else {
bloc.style.display = 'none';
}
}
async function sauvegarderMateriel() {
const flashIntegre = document.getElementById('tog-flash-integre').checked;
await apiPost('/api/config', {
camera: {
appareil: getValue('admin-appareil'),
compte_a_rebours: parseInt(getValue('admin-car')) || 3,
flash_integre: flashIntegre,
},
impression: {
imprimante: getValue('admin-imprimante'),
format: getValue('admin-format-papier'),
copies_max: parseInt(getValue('admin-copies-max')) || 5,
orientations: _getOrientations(),
rembobinage_ruban: document.getElementById('tog-rembobinage-ruban').checked,
},
});
afficherStatut('Materiel sauvegarde', 'succes');
}
// === COMPTEUR ===
async function chargerCompteur() {
const etat = await apiGet('/api/compteur');
document.getElementById('tog-compteur-actif').checked = etat.actif;
document.getElementById('admin-compteur-restant').textContent = etat.restantes;
document.getElementById('admin-compteur-limite-display').textContent = etat.limite;
setValue('admin-compteur-limite', etat.limite);
}
async function sauvegarderCompteur() {
await apiPost('/api/config', {
compteur: {
actif: document.getElementById('tog-compteur-actif').checked,
limite: parseInt(getValue('admin-compteur-limite')) || 400,
},
});
chargerCompteur();
afficherStatut('Compteur sauvegarde', 'succes');
}
async function resetCompteur() {
if (confirm('Remettre le compteur a zero ?')) {
await apiPost('/api/compteur/reset');
chargerCompteur();
afficherStatut('Compteur remis a zero', 'succes');
}
}
// === DESTINATIONS ===
function chargerDestinations() {
const dest = config.destinations || {};
const booth = config.booth || {};
document.getElementById('tog-dest-memoire').checked = dest.memoire_interne !== false;
document.getElementById('tog-dest-usb').checked = dest.cle_usb || false;
document.getElementById('tog-dest-ftp').checked = dest.ftp || false;
document.getElementById('tog-dest-galerie').checked = booth.actif || false;
document.getElementById('tog-dest-email-auto').checked = dest.email_auto || false;
// Sous-configs visibles si actives
toggleSousConfig('tog-dest-usb', 'dest-usb-details');
toggleSousConfig('tog-dest-ftp', 'dest-ftp-details');
toggleSousConfig('tog-dest-galerie', 'dest-galerie-details');
setValue('admin-ftp-host', dest.ftp_host);
setValue('admin-ftp-port', dest.ftp_port || 21);
setValue('admin-ftp-user', dest.ftp_user);
setValue('admin-ftp-pass', dest.ftp_password);
setValue('admin-ftp-chemin', dest.ftp_chemin || '/photobooth');
// Booth fields
setValue('admin-booth-url', booth.url || 'https://booth.copydev.fr');
setValue('admin-booth-apikey', booth.api_key || 'booth-photobooth-2026');
setValue('admin-booth-eventid', booth.event_id || 'default');
// Radio sauvegarder tout / imprimees
if (dest.sauvegarder_tout === false) {
document.getElementById('sauv-imprimees').checked = true;
} else {
document.getElementById('sauv-tout').checked = true;
}
// Listeners pour afficher/cacher sous-configs
['tog-dest-usb', 'tog-dest-ftp', 'tog-dest-galerie'].forEach(id => {
const el = document.getElementById(id);
el.onchange = () => {
const map = { 'tog-dest-usb': 'dest-usb-details', 'tog-dest-ftp': 'dest-ftp-details', 'tog-dest-galerie': 'dest-galerie-details' };
toggleSousConfig(id, map[id]);
};
});
}
function toggleSousConfig(toggleId, detailsId) {
const checked = document.getElementById(toggleId).checked;
const el = document.getElementById(detailsId);
if (checked) el.classList.remove('cache');
else el.classList.add('cache');
}
async function detecterUSB() {
const usbs = await apiGet('/api/usb/detecter');
const select = document.getElementById('admin-chemin-usb');
select.innerHTML = '';
if (usbs.length === 0) {
const opt = document.createElement('option');
opt.value = '';
opt.textContent = 'Aucune cle detectee';
select.appendChild(opt);
} else {
for (const u of usbs) {
const opt = document.createElement('option');
opt.value = u;
opt.textContent = u;
select.appendChild(opt);
}
}
}
async function sauvegarderDestinations() {
await apiPost('/api/config', {
destinations: {
memoire_interne: document.getElementById('tog-dest-memoire').checked,
cle_usb: document.getElementById('tog-dest-usb').checked,
chemin_usb: getValue('admin-chemin-usb') || '/media/usb',
ftp: document.getElementById('tog-dest-ftp').checked,
ftp_host: getValue('admin-ftp-host'),
ftp_port: parseInt(getValue('admin-ftp-port')) || 21,
ftp_user: getValue('admin-ftp-user'),
ftp_password: getValue('admin-ftp-pass'),
ftp_chemin: getValue('admin-ftp-chemin') || '/photobooth',
email_auto: document.getElementById('tog-dest-email-auto').checked,
sauvegarder_tout: document.getElementById('sauv-tout').checked,
},
booth: {
actif: document.getElementById('tog-dest-galerie').checked,
url: getValue('admin-booth-url'),
api_key: getValue('admin-booth-apikey'),
event_id: getValue('admin-booth-eventid'),
},
});
afficherStatut('Destinations sauvegardees', 'succes');
}
// === CADRES IMPRESSION (par format) ===
const FORMATS_IMPRESSION = ['strip', '10x15', '15x20'];
async function chargerCadresImpression() {
for (const fmt of FORMATS_IMPRESSION) {
await chargerCadresFormat(fmt);
}
}
async function chargerCadresFormat(fmt) {
const data = await apiGet(`/api/cadres-impression/${fmt}`);
const liste = document.getElementById(`cadres-liste-${fmt}`);
liste.innerHTML = '';
// Option "Aucun cadre"
const divAucun = _creerItemCadreImpression(fmt, null, data.actif);
liste.appendChild(divAucun);
if (data.disponibles.length === 0) {
const p = document.createElement('p');
p.className = 'texte-secondaire';
p.textContent = 'Aucun cadre importe';
liste.appendChild(p);
return;
}
for (const nom of data.disponibles) {
const div = _creerItemCadreImpression(fmt, nom, data.actif);
liste.appendChild(div);
}
}
function _creerItemCadreImpression(fmt, nom, actif) {
const div = document.createElement('div');
div.className = 'cadre-imp-item';
const rb = document.createElement('input');
rb.type = 'radio';
rb.name = `cadre-imp-${fmt}`;
rb.value = nom || '';
rb.checked = (actif === nom);
rb.onchange = () => activerCadreFormat(fmt, nom);
if (nom) {
const img = document.createElement('img');
img.src = `/assets/cadres/${fmt}/${nom}`;
img.alt = nom;
img.className = 'cadre-preview';
const btnDel = document.createElement('button');
btnDel.className = 'btn-danger btn-icone';
btnDel.title = 'Supprimer';
btnDel.textContent = '✕';
btnDel.onclick = () => supprimerCadreFormat(fmt, nom);
const span = document.createElement('span');
span.textContent = nom.replace('.png', '');
div.appendChild(rb);
div.appendChild(img);
div.appendChild(span);
div.appendChild(btnDel);
} else {
const span = document.createElement('span');
span.textContent = 'Aucun cadre';
div.appendChild(rb);
div.appendChild(span);
}
return div;
}
async function activerCadreFormat(fmt, nom) {
await apiPost(`/api/cadres-impression/${fmt}/actif`, { nom: nom || null });
afficherStatut(`Cadre ${fmt} mis a jour`, 'succes');
}
async function supprimerCadreFormat(fmt, nom) {
await fetch(`/api/cadres-impression/${fmt}/${encodeURIComponent(nom)}`, { method: 'DELETE' });
await chargerCadresFormat(fmt);
afficherStatut('Cadre supprime', 'succes');
}
async function uploaderCadreFormat(fmt) {
const input = document.getElementById(`input-cadre-${fmt}`);
if (!input.files.length) return;
const formData = new FormData();
formData.append('fichier', input.files[0]);
const r = await fetch(`/api/upload/cadre/${fmt}`, { method: 'POST', body: formData });
if (r.ok) {
input.value = '';
await chargerCadresFormat(fmt);
afficherStatut(`Cadre ${fmt} importe`, 'succes');
} else {
afficherStatut('Erreur import cadre', 'erreur');
}
}
// === CADRES OVERLAYS PHOTO ===
async function chargerCadres() {
const data = await apiGet('/api/cadres');
const liste = document.getElementById('liste-cadres');
liste.innerHTML = '';
if (data.tous.length === 0) {
liste.innerHTML = '<p class="texte-secondaire">Aucun cadre importe</p>';
return;
}
for (const nom of data.tous) {
const div = document.createElement('div');
div.className = 'cadre-item';
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.checked = data.actifs.includes(nom);
cb.onchange = () => majCadresActifs();
const img = document.createElement('img');
img.src = `/assets/overlays/${nom}`;
img.alt = nom;
const span = document.createElement('span');
span.textContent = nom.replace('.png', '');
div.appendChild(cb);
div.appendChild(img);
div.appendChild(span);
liste.appendChild(div);
}
}
async function majCadresActifs() {
const items = document.querySelectorAll('.cadre-item input[type="checkbox"]');
const noms = document.querySelectorAll('.cadre-item span');
const actifs = [];
items.forEach((cb, i) => {
if (cb.checked) actifs.push(noms[i].textContent + '.png');
});
await apiPost('/api/cadres', { actifs });
}
async function uploaderCadre() {
const input = document.getElementById('input-cadre');
if (!input.files.length) return;
const formData = new FormData();
formData.append('fichier', input.files[0]);
await fetch('/api/upload/overlay', { method: 'POST', body: formData });
input.value = '';
chargerCadres();
afficherStatut('Cadre importe', 'succes');
}
// === FONDS VERTS ===
async function chargerFondsAdmin() {
const fonds = await apiGet('/api/fonds');
const liste = document.getElementById('liste-fonds');
liste.innerHTML = '';
if (fonds.length === 0) {
liste.innerHTML = '<p class="texte-secondaire">Aucun fond importe</p>';
return;
}
for (const nom of fonds) {
const div = document.createElement('div');
div.className = 'fond-item';
const img = document.createElement('img');
img.src = `/assets/backgrounds/${nom}`;
img.alt = nom;
const btnSuppr = document.createElement('button');
btnSuppr.className = 'btn-supprimer';
btnSuppr.innerHTML = '&#10005;';
btnSuppr.onclick = () => supprimerFond(nom);
div.appendChild(img);
div.appendChild(btnSuppr);
liste.appendChild(div);
}
}
async function uploaderFond() {
const input = document.getElementById('input-fond');
if (!input.files.length) return;
const formData = new FormData();
formData.append('fichier', input.files[0]);
await fetch('/api/upload/fond', { method: 'POST', body: formData });
input.value = '';
chargerFondsAdmin();
afficherStatut('Fond importe', 'succes');
}
async function supprimerFond(nom) {
if (!confirm(`Supprimer le fond "${nom}" ?`)) return;
await fetch(`/api/fonds/${encodeURIComponent(nom)}`, { method: 'DELETE' });
chargerFondsAdmin();
}
// === ANIMATIONS ===
const EMOJI_MAP = { 3: '🤪', 2: '😱', 1: '🔥' };
async function chargerAnimations() {
// Animations CSS internes
const anims = await apiGet('/api/animations');
const cssActives = config.camera?.animations_css_actives || ['classique'];
const liste = document.getElementById('liste-animations');
liste.innerHTML = '';
for (const [id, nom] of Object.entries(anims)) {
const div = document.createElement('div');
div.className = 'anim-item';
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.checked = cssActives.includes(id);
cb.dataset.animId = id;
cb.onchange = () => majAnimationsCss();
const label = document.createElement('span');
label.textContent = nom;
div.appendChild(cb);
div.appendChild(label);
liste.appendChild(div);
}
// Animations custom (video/GIF)
await chargerAnimationsCustom();
}
async function chargerAnimationsCustom() {
const data = await apiGet('/api/animations/custom');
const liste = document.getElementById('liste-animations-custom');
liste.innerHTML = '';
if (data.tous.length === 0) {
liste.innerHTML = '<p class="texte-secondaire">Aucune animation importee</p>';
return;
}
for (const nom of data.tous) {
const div = document.createElement('div');
div.className = 'anim-item';
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.checked = data.actives.includes(nom);
cb.dataset.animNom = nom;
cb.onchange = () => majAnimationsCustom();
// Preview miniature
const ext = nom.split('.').pop().toLowerCase();
let preview;
if (ext === 'gif') {
preview = document.createElement('img');
preview.src = `/assets/animations/${nom}`;
preview.className = 'anim-item-preview';
} else {
preview = document.createElement('video');
preview.src = `/assets/animations/${nom}`;
preview.className = 'anim-item-preview';
preview.muted = true;
preview.preload = 'metadata';
}
const label = document.createElement('span');
label.textContent = nom;
const btnSuppr = document.createElement('button');
btnSuppr.className = 'btn-supprimer';
btnSuppr.innerHTML = '&#10005;';
btnSuppr.onclick = () => supprimerAnimation(nom);
div.appendChild(cb);
div.appendChild(preview);
div.appendChild(label);
div.appendChild(btnSuppr);
liste.appendChild(div);
}
}
async function majAnimationsCss() {
const checkboxes = document.querySelectorAll('#liste-animations .anim-item input[type="checkbox"]');
const actives = [];
checkboxes.forEach(cb => { if (cb.checked) actives.push(cb.dataset.animId); });
// Au moins une doit rester active
if (actives.length === 0) actives.push('classique');
await apiPost('/api/config', { camera: { animations_css_actives: actives } });
}
async function majAnimationsCustom() {
const checkboxes = document.querySelectorAll('#liste-animations-custom .anim-item input[type="checkbox"]');
const actives = [];
checkboxes.forEach(cb => { if (cb.checked) actives.push(cb.dataset.animNom); });
await apiPost('/api/animations/custom', { actives });
}
async function uploaderAnimation() {
const input = document.getElementById('input-animation');
if (!input.files.length) return;
const formData = new FormData();
formData.append('fichier', input.files[0]);
await fetch('/api/upload/animation', { method: 'POST', body: formData });
input.value = '';
chargerAnimationsCustom();
afficherStatut('Animation importee', 'succes');
}
async function supprimerAnimation(nom) {
if (!confirm(`Supprimer l'animation "${nom}" ?`)) return;
await fetch(`/api/animations/custom/${encodeURIComponent(nom)}`, { method: 'DELETE' });
chargerAnimationsCustom();
}
function testerAnimationPreview() {
const chiffre = document.getElementById('preview-anim-chiffre');
const video = document.getElementById('preview-anim-video');
// Verifier s'il y a des animations custom actives
const customActives = [];
document.querySelectorAll('#liste-animations-custom .anim-item input[type="checkbox"]:checked').forEach(cb => {
customActives.push(cb.dataset.animNom);
});
// Verifier les CSS actives
const cssActives = [];
document.querySelectorAll('#liste-animations .anim-item input[type="checkbox"]:checked').forEach(cb => {
cssActives.push(cb.dataset.animId);
});
const toutes = [...cssActives.map(a => ({type: 'css', id: a})), ...customActives.map(a => ({type: 'custom', nom: a}))];
if (toutes.length === 0) return;
const choix = toutes[Math.floor(Math.random() * toutes.length)];
if (choix.type === 'custom') {
// Jouer la video/gif
chiffre.classList.add('cache');
video.classList.remove('cache');
video.src = `/assets/animations/${choix.nom}`;
video.currentTime = 0;
video.play();
video.onended = () => {
video.classList.add('cache');
chiffre.classList.remove('cache');
};
// Pour les GIF, masquer apres 3s
if (choix.nom.endsWith('.gif')) {
setTimeout(() => {
video.classList.add('cache');
chiffre.classList.remove('cache');
}, 3000);
}
} else {
// Animation CSS
video.classList.add('cache');
chiffre.classList.remove('cache');
let count = 3;
function afficher() {
if (choix.id === 'emoji') {
chiffre.textContent = EMOJI_MAP[count] || count;
} else {
chiffre.textContent = count;
}
chiffre.className = 'anim-chiffre';
void chiffre.offsetWidth;
chiffre.classList.add('anim-' + choix.id);
count--;
if (count > 0) setTimeout(afficher, 1000);
}
afficher();
}
}
// === FONCTIONNALITES ===
function chargerFonctionnalites() {
const fonc = config.fonctionnalites || {};
for (const [id, cle] of Object.entries(TOGGLES_MAP)) {
const el = document.getElementById(id);
if (el) el.checked = fonc[cle] !== false;
}
setupToggleListeners();
}
function setupToggleListeners() {
for (const [id, cle] of Object.entries(TOGGLES_MAP)) {
const el = document.getElementById(id);
if (!el) continue;
const nouveau = el.cloneNode(true);
el.parentNode.replaceChild(nouveau, el);
nouveau.addEventListener('change', () => {
apiPost('/api/config', { fonctionnalites: { [cle]: nouveau.checked } });
});
}
}
// === 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');
rafraichirMediaAccueil().then(() => {
const select = document.getElementById('admin-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');
// Lien galerie en ligne
const booth = config.booth || {};
const lienBloc = document.getElementById('lien-galerie-event');
const lienA = document.getElementById('url-galerie-event');
if (booth.actif && booth.url) {
const evtId = booth.event_id || eventEditId || 'default';
const url = booth.url.replace(/\/$/, '') + '/' + evtId;
lienA.href = url;
lienA.textContent = url;
lienBloc.style.display = '';
} else {
lienBloc.style.display = 'none';
}
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) {
document.documentElement.setAttribute('data-theme', theme);
}
async function rafraichirMediaAccueil() {
const data = await apiGet('/api/animations/custom');
const select = document.getElementById('admin-media-accueil');
const valeur = select.value;
select.innerHTML = '<option value="">Aucun (animation par defaut)</option>';
for (const nom of data.tous) {
const opt = document.createElement('option');
opt.value = nom;
opt.textContent = nom;
select.appendChild(opt);
}
if (valeur) select.value = valeur;
}
async function uploaderMediaAccueil() {
const input = document.getElementById('input-media-accueil');
if (!input.files.length) return;
const formData = new FormData();
formData.append('fichier', input.files[0]);
await fetch('/api/upload/animation', { method: 'POST', body: formData });
const nomFichier = input.files[0].name;
input.value = '';
await rafraichirMediaAccueil();
document.getElementById('admin-media-accueil').value = nomFichier;
// Applique automatiquement la video comme media d'accueil de l'evenement en cours
// (sinon il faut la re-selectionner dans le menu ET cliquer sur "Enregistrer" separement)
if (eventEditId) {
await apiPost('/api/config', { evenement: { media_accueil: nomFichier, event_id: eventEditId } });
await fetch(`/api/evenements/${eventEditId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ media_accueil: nomFichier }),
});
appliquerConfig();
}
afficherStatut('Media importe et applique', 'succes');
}
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 = {
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,
formats_actifs,
};
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_CADRE_EVENT = ['10x15', '15x20'];
async function chargerCadresEvent() {
if (!eventEditId) return;
const container = document.getElementById('cadres-event-formats');
container.innerHTML = '';
// Charger cadres du premier format pour afficher le mode + liste
const data = await apiGet(`/api/evenements/${eventEditId}/cadres/15x20`);
const data2 = await apiGet(`/api/evenements/${eventEditId}/cadres/10x15`);
const tous = [...new Set([...(data.disponibles || []), ...(data2.disponibles || [])])];
let cadresHtml = '';
for (const nom of tous) {
const selected = data.cadre === nom ? ' cadre-selectionne' : '';
const src15 = `/data/evenements/${eventEditId}/cadres/15x20/${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>
<button class="btn-danger btn-icone" onclick="event.stopPropagation();supprimerCadreEvent('${nom}')">✕</button>
</div>`;
}
container.innerHTML = `
<div class="champ">
<label>Mode cadre</label>
<select id="mode-cadre-event" onchange="changerModeCadreEvent()">
<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 importe</p>'}</div>
<input type="file" id="input-cadre-event" accept=".png" class="input-fichier">
<button class="btn-secondaire btn-petit" onclick="uploaderCadreEvent()">Importer cadre</button>
`;
}
async function selectionnerCadreEvent(nom) {
if (!eventEditId) return;
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 chargerCadresEvent();
}
async function changerModeCadreEvent() {
if (!eventEditId) return;
const mode = document.getElementById('mode-cadre-event').value;
const ev = await apiGet(`/api/evenements/${eventEditId}`);
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 });
}
afficherStatut(`Mode cadre : ${mode}`, 'succes');
}
async function uploaderCadreEvent() {
if (!eventEditId) return;
const input = document.getElementById('input-cadre-event');
if (!input.files.length) return;
const nomFichier = input.files[0].name;
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 = '';
// Applique automatiquement le cadre importe (sinon il reste inactif tant qu'on ne clique pas sur sa vignette)
const modeSelect = document.getElementById('mode-cadre-event');
if (modeSelect && modeSelect.value === 'aucun') modeSelect.value = 'impose';
await selectionnerCadreEvent(nomFichier);
afficherStatut('Cadre importe et applique (10x15 + 15x20)', 'succes');
}
async function supprimerCadreEvent(nom) {
if (!eventEditId) return;
for (const fmt of FORMATS_CADRE_EVENT) {
await fetch(`/api/evenements/${eventEditId}/cadres/${fmt}/${encodeURIComponent(nom)}`, { method: 'DELETE' }).catch(() => {});
}
await chargerCadresEvent();
afficherStatut('Cadre supprime', 'succes');
}
// === EMAIL ===
function chargerEmailAdmin() {
const email = config.email || {};
setValue('admin-smtp-host', email.smtp_host);
setValue('admin-smtp-port', email.smtp_port || 587);
setValue('admin-smtp-user', email.smtp_user);
setValue('admin-smtp-pass', email.smtp_password);
setValue('admin-expediteur', email.expediteur);
}
async function sauvegarderEmail() {
await apiPost('/api/config', {
email: {
smtp_host: getValue('admin-smtp-host'),
smtp_port: parseInt(getValue('admin-smtp-port')) || 587,
smtp_user: getValue('admin-smtp-user'),
smtp_password: getValue('admin-smtp-pass'),
expediteur: getValue('admin-expediteur'),
},
});
afficherStatut('Email sauvegarde', 'succes');
}
// === GALERIE ===
async function chargerGalerieAdmin() {
const compteur = await apiGet('/api/galerie/compteur');
document.getElementById('admin-nb-photos').textContent = compteur.photos_prises || 0;
document.getElementById('admin-nb-exports').textContent = compteur.exports || 0;
setValue('admin-url-galerie', (config.qr_code || {}).url_galerie);
}
async function sauvegarderGalerie() {
await apiPost('/api/config', {
qr_code: { url_galerie: getValue('admin-url-galerie') },
});
afficherStatut('Configuration sauvegardee', 'succes');
}
function confirmerViderGalerie() {
if (confirm('Supprimer TOUTES les photos ? Cette action est irreversible.')) {
apiPost('/api/galerie/vider').then(() => {
afficherStatut('Galerie videe', 'succes');
chargerGalerieAdmin();
});
}
}
// === SYSTEME ===
function confirmerRedemarrage() {
if (confirm('Redemarrer le systeme ?')) apiPost('/api/systeme/redemarrer');
}
function confirmerExtinction() {
if (confirm('Eteindre le systeme ?')) apiPost('/api/systeme/eteindre');
}
function quitterNavigateur() {
apiPost('/api/systeme/quitter-navigateur');
}
// === Utilitaires ===
function setValue(id, val) {
const el = document.getElementById(id);
if (el) el.value = val || '';
}
function getValue(id) {
const el = document.getElementById(id);
return el ? el.value : '';
}
// === BOOTH (Galerie Live) ===
async function chargerBoothAdmin() {
const booth = config.booth || {};
// Recuperer le code actuel
if (booth.actif && booth.url) {
try {
const info = await apiGet('/api/booth/info');
if (info.password) {
document.getElementById('admin-booth-password').textContent = info.password;
}
} catch (e) {
document.getElementById('admin-booth-password').textContent = '--';
}
}
}
async function sauvegarderBooth() {
await apiPost('/api/config', {
booth: {
actif: document.getElementById('tog-dest-galerie').checked,
url: getValue('admin-booth-url'),
api_key: getValue('admin-booth-apikey'),
event_id: getValue('admin-booth-eventid'),
}
});
afficherStatut('Galerie live sauvegardee', 'succes');
chargerBoothAdmin();
chargerBoothInfo();
}
// === INFOS SYSTEME ===
async function chargerInfosSysteme() {
const contenu = document.getElementById('infos-systeme-contenu');
contenu.innerHTML = '<p class="infos-chargement">Chargement...</p>';
try {
const info = await apiGet('/api/systeme/info');
let html = '';
// Reseau
html += '<div class="infos-section">';
html += '<h4>&#127760; Reseau</h4>';
html += `<div class="infos-ligne"><span>Hostname</span><strong>${info.hostname}</strong></div>`;
if (info.interfaces && info.interfaces.length > 0) {
info.interfaces.forEach(iface => {
iface.ips.forEach(ip => {
html += `<div class="infos-ligne"><span>${iface.nom}</span><strong class="infos-ip">${ip}</strong></div>`;
});
});
} else {
html += '<div class="infos-ligne"><span>Aucune interface</span><strong>—</strong></div>';
}
html += '</div>';
// Systeme
html += '<div class="infos-section">';
html += '<h4>&#128187; Systeme</h4>';
if (info.uptime) html += `<div class="infos-ligne"><span>Uptime</span><strong>${info.uptime}</strong></div>`;
if (info.temperature_cpu !== null && info.temperature_cpu !== undefined) {
const couleur = info.temperature_cpu > 70 ? '#e53935' : info.temperature_cpu > 55 ? '#fb8c00' : '#43a047';
html += `<div class="infos-ligne"><span>Temperature CPU</span><strong style="color:${couleur}">${info.temperature_cpu} °C</strong></div>`;
}
if (info.python) html += `<div class="infos-ligne"><span>Python</span><strong>${info.python}</strong></div>`;
html += '</div>';
// Stockage
if (info.disque && info.disque.total) {
const pct = Math.round((info.disque.utilise / info.disque.total) * 100);
const couleurDisk = pct > 85 ? '#e53935' : pct > 65 ? '#fb8c00' : '#43a047';
html += '<div class="infos-section">';
html += '<h4>&#128190; Stockage</h4>';
html += `<div class="infos-ligne"><span>Total</span><strong>${info.disque.total} Go</strong></div>`;
html += `<div class="infos-ligne"><span>Utilise</span><strong style="color:${couleurDisk}">${info.disque.utilise} Go (${pct}%)</strong></div>`;
html += `<div class="infos-ligne"><span>Libre</span><strong>${info.disque.libre} Go</strong></div>`;
html += '<div class="infos-barre-fond"><div class="infos-barre" style="width:' + pct + '%;background:' + couleurDisk + '"></div></div>';
html += '</div>';
}
// RAM
if (info.ram && info.ram.total) {
const pctRam = Math.round((info.ram.utilise / info.ram.total) * 100);
const couleurRam = pctRam > 85 ? '#e53935' : pctRam > 65 ? '#fb8c00' : '#43a047';
html += '<div class="infos-section">';
html += '<h4>&#129728; Memoire RAM</h4>';
html += `<div class="infos-ligne"><span>Total</span><strong>${info.ram.total} Go</strong></div>`;
html += `<div class="infos-ligne"><span>Utilise</span><strong style="color:${couleurRam}">${info.ram.utilise} Go (${pctRam}%)</strong></div>`;
html += `<div class="infos-ligne"><span>Libre</span><strong>${info.ram.libre} Go</strong></div>`;
html += '<div class="infos-barre-fond"><div class="infos-barre" style="width:' + pctRam + '%;background:' + couleurRam + '"></div></div>';
html += '</div>';
}
// Camera
html += '<div class="infos-section">';
html += '<h4>&#128247; Camera</h4>';
const camOk = info.camera_connectee;
html += `<div class="infos-ligne"><span>Mode</span><strong>${info.camera_mode}</strong></div>`;
html += `<div class="infos-ligne"><span>Statut</span><strong style="color:${camOk ? '#43a047' : '#e53935'}">${camOk ? 'Connectee' : 'Deconnectee'}</strong></div>`;
html += '</div>';
contenu.innerHTML = html;
} catch (e) {
contenu.innerHTML = '<p class="infos-erreur">Impossible de charger les informations systeme.</p>';
}
}
async function imprimerFlyerQR() {
afficherStatut('Generation du flyer QR...', 'succes');
const r = await apiPost('/api/booth/imprimer-qr', {});
if (r.succes) {
afficherStatut('Flyer QR envoye a l\'imprimante !', 'succes');
} else {
afficherStatut(r.erreur || 'Erreur impression flyer', 'erreur');
}
}
async function regenererBoothPassword() {
const booth = config.booth || {};
const url = getValue('admin-booth-url').replace(/\/$/, '');
const apiKey = getValue('admin-booth-apikey');
const eventId = getValue('admin-booth-eventid') || 'default';
try {
const resp = await fetch(`${url}/api/${eventId}/reset-password`, {
method: 'POST',
headers: { 'X-Api-Key': apiKey },
});
const data = await resp.json();
if (data.password) {
document.getElementById('admin-booth-password').textContent = data.password;
afficherStatut('Nouveau code genere : ' + data.password, 'succes');
}
} catch (e) {
afficherStatut('Erreur connexion au serveur booth', 'erreur');
}
}
// === WIFI ===
let _wifiMajActif = false;
let _wifiSsidCible = null;
function _signalBars(signal, actif) {
const seuils = [20, 40, 60, 80];
return seuils.map(s =>
`<div class="bar${signal >= s ? ' actif' : ''}"></div>`
).join('');
}
async function chargerWifi() {
const statut = await apiGet('/api/wifi/status');
const icon = document.getElementById('wifi-statut-icon');
const ssid = document.getElementById('wifi-ssid');
const detail = document.getElementById('wifi-detail');
const bars = document.getElementById('wifi-signal-bars');
if (statut.connecte) {
icon.textContent = '\u{1F4F6}';
ssid.textContent = statut.ssid;
detail.textContent = `IP : ${statut.ip || '—'}`;
bars.innerHTML = _signalBars(statut.signal || 0, true);
} else {
icon.textContent = '\u{1F4F4}';
ssid.textContent = 'Non connecté';
detail.textContent = 'Aucun réseau WiFi';
bars.innerHTML = _signalBars(0, false);
}
await chargerWifiEnregistres();
}
async function scannerWifi() {
const liste = document.getElementById('wifi-liste-reseaux');
const loading = document.getElementById('wifi-scan-loading');
liste.innerHTML = '';
loading.classList.remove('cache');
try {
const reseaux = await apiGet('/api/wifi/scan');
loading.classList.add('cache');
if (reseaux.length === 0) {
liste.innerHTML = '<p style="color:#888;padding:.5rem">Aucun réseau détecté</p>';
return;
}
for (const r of reseaux) {
const div = document.createElement('div');
div.className = 'wifi-reseau' + (r.actif ? ' wifi-actif' : '');
div.onclick = () => onWifiReseauClick(r);
div.innerHTML = `
<div class="wifi-reseau-icons">
${r.securise ? '&#128274;' : '&#128275;'}
</div>
<div class="wifi-reseau-ssid">${_escHtml(r.ssid)}</div>
<div class="wifi-reseau-meta">${r.securite || 'Ouvert'}</div>
${r.actif ? '<span class="wifi-badge-connecte">Connecté</span>' : ''}
${!r.actif && r.enregistre ? '<span class="wifi-badge-enregistre">Enregistré</span>' : ''}
<div class="wifi-signal-bars">${_signalBars(r.signal)}</div>
`;
liste.appendChild(div);
}
} catch (e) {
loading.classList.add('cache');
liste.innerHTML = '<p style="color:#f44;padding:.5rem">Erreur scan WiFi</p>';
}
}
function onWifiReseauClick(reseau) {
if (reseau.actif) return;
if (reseau.enregistre) {
connecterWifiDirect(reseau.ssid);
} else if (!reseau.securise) {
connecterWifiDirect(reseau.ssid);
} else {
afficherPopupWifiMdp(reseau.ssid);
}
}
async function connecterWifiDirect(ssid) {
afficherStatut(`Connexion à ${ssid}...`, 'succes');
const r = await apiPost('/api/wifi/connect', { ssid });
if (r.succes) {
afficherStatut(r.message, 'succes');
await chargerWifi();
await scannerWifi();
} else {
afficherStatut(r.message || 'Échec connexion', 'erreur');
afficherPopupWifiMdp(ssid);
}
}
function afficherPopupWifiMdp(ssid) {
_wifiSsidCible = ssid;
document.getElementById('wifi-mdp-titre').textContent = `WiFi : ${ssid}`;
document.getElementById('wifi-mdp-input').value = '';
document.getElementById('wifi-mdp-erreur').classList.add('cache');
document.getElementById('popup-wifi-mdp').classList.remove('cache');
apiGet(`/api/wifi/password/${encodeURIComponent(ssid)}`).then(data => {
if (data.password) {
document.getElementById('wifi-mdp-input').value = data.password;
}
}).catch(() => {});
}
function fermerPopupWifi() {
document.getElementById('popup-wifi-mdp').classList.add('cache');
_wifiSsidCible = null;
}
let _wifiMaj = false;
function wifiTape(c) {
const input = document.getElementById('wifi-mdp-input');
if (_wifiMaj && c.length === 1 && c.match(/[a-z]/)) c = c.toUpperCase();
input.value += c;
if (_wifiMaj) { _wifiMaj = false; _wifiMajUI(); }
}
function wifiEffacer() {
const input = document.getElementById('wifi-mdp-input');
input.value = input.value.slice(0, -1);
}
function wifiToggleMaj() {
_wifiMaj = !_wifiMaj;
_wifiMajUI();
}
function _wifiMajUI() {
const btn = document.getElementById('wifi-btn-maj');
btn.style.background = _wifiMaj ? 'var(--primaire)' : '';
btn.style.color = _wifiMaj ? '#fff' : '';
['wifi-kb-row1', 'wifi-kb-row2', 'wifi-kb-row3'].forEach(id => {
document.getElementById(id).querySelectorAll('button').forEach(b => {
const c = b.textContent;
if (c.length === 1 && c.match(/[a-zA-Z]/)) {
b.textContent = _wifiMaj ? c.toUpperCase() : c.toLowerCase();
}
});
});
}
async function validerWifiMdp() {
const mdp = document.getElementById('wifi-mdp-input').value;
if (!mdp) return;
const btn = document.getElementById('wifi-btn-connecter');
btn.textContent = 'Connexion...';
btn.disabled = true;
const r = await apiPost('/api/wifi/connect', { ssid: _wifiSsidCible, password: mdp });
btn.textContent = 'Connecter';
btn.disabled = false;
if (r.succes) {
fermerPopupWifi();
afficherStatut(r.message, 'succes');
await chargerWifi();
await scannerWifi();
} else {
const err = document.getElementById('wifi-mdp-erreur');
err.textContent = r.message || 'Échec connexion';
err.classList.remove('cache');
setTimeout(() => err.classList.add('cache'), 4000);
}
}
async function chargerWifiEnregistres() {
const liste = document.getElementById('wifi-liste-enregistres');
const saved = await apiGet('/api/wifi/saved');
liste.innerHTML = '';
if (saved.length === 0) {
liste.innerHTML = '<p style="color:#888;padding:.5rem;font-size:.85rem">Aucun réseau enregistré</p>';
return;
}
for (const s of saved) {
const div = document.createElement('div');
div.className = 'wifi-enregistre-item';
div.innerHTML = `
<span class="wifi-reseau-ssid">${_escHtml(s.ssid)}</span>
${s.mdp_connu ? `<span class="wifi-enregistre-mdp" onclick="afficherMdpWifi('${_escAttr(s.ssid)}', this)" title="Voir le mot de passe">&#128065; mdp</span>` : ''}
<button class="btn-secondaire btn-petit" onclick="connecterWifiDirect('${_escAttr(s.ssid)}')">Connecter</button>
<button class="btn-danger btn-petit" onclick="oublierWifi('${_escAttr(s.ssid)}')">Oublier</button>
`;
liste.appendChild(div);
}
}
async function afficherMdpWifi(ssid, el) {
const data = await apiGet(`/api/wifi/password/${encodeURIComponent(ssid)}`);
if (data.password) {
el.textContent = data.password;
setTimeout(() => { el.innerHTML = '&#128065; mdp'; }, 5000);
}
}
async function oublierWifi(ssid) {
await fetch(`/api/wifi/saved/${encodeURIComponent(ssid)}`, { method: 'DELETE' });
afficherStatut(`${ssid} oublié`, 'succes');
await chargerWifiEnregistres();
}
function _escHtml(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
function _escAttr(s) { return s.replace(/'/g, "\\'").replace(/"/g, '&quot;'); }
async function chargerDiagCamera() {
const el = document.getElementById('diag-camera-contenu');
el.textContent = 'Test en cours...';
try {
const data = await apiGet('/api/camera/debug');
let txt = '';
for (const [k, v] of Object.entries(data)) {
if (k === 'config_widgets_top') {
txt += `${k}: ${Array.isArray(v) ? v.join(', ') : v}\n`;
} else {
txt += `${k}: ${JSON.stringify(v)}\n`;
}
}
el.textContent = txt || 'Aucune donnee';
} catch (e) {
el.textContent = 'ERREUR : ' + e.message;
}
}
// === 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 perdues = data.poses_perdues || 0;
document.getElementById('conso-perdues').textContent = perdues;
const cardPerdues = document.getElementById('conso-card-perdues');
if (perdues > 0) {
document.getElementById('conso-perdues').style.color = '#f44336';
cardPerdues.style.borderLeft = '3px solid #f44336';
} else {
document.getElementById('conso-perdues').style.color = '#43a047';
cardPerdues.style.borderLeft = '3px solid #43a047';
}
const diag = document.getElementById('conso-diagnostic');
const papierUsed = data.papier_utilise;
const rubanUsed = data.ruban_utilise;
const photos = data.photos_imprimees;
let html = '<strong>Diagnostic consommables</strong><br>';
html += `Feuilles papier consommees : ${papierUsed}<br>`;
html += `Poses ruban consommees : ${rubanUsed}<br>`;
html += `Photos imprimees : ${photos}<br>`;
html += `Poses perdues (gaspillees) : ${perdues}<br>`;
if (papierUsed > 0) {
const ratio = (rubanUsed / papierUsed).toFixed(2);
html += `<br>Ratio ruban/papier : <strong>${ratio}</strong><br>`;
if (parseFloat(ratio) <= 0.75) {
html += '&#9989; <strong>Rembobinage efficace</strong> — le ruban est economise';
} else if (parseFloat(ratio) >= 0.95) {
html += '&#9888; <strong>Pas d\'economie ruban</strong> — activez le rembobinage dans Materiel';
} else {
html += '&#128260; Rembobinage partiel';
}
if (perdues > 0) {
const pctPerdu = ((perdues / (rubanUsed + perdues)) * 100).toFixed(0);
html += `<br>&#128308; ${pctPerdu}% du ruban gaspille (${perdues} poses pour rien)`;
}
}
if (data.papier_restant <= 20 || data.ruban_restant <= 20) {
html += '<br><br>&#128680; <strong>Consommable bientot epuise !</strong>';
}
if (data.ruban_restant < data.papier_restant - 10) {
html += '<br>&#9888; Le ruban va s\'epuiser avant le papier';
}
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 DIDACTIQUES (multi-etapes) ===
let _videosData = {};
async function chargerVideosAdmin() {
try {
const data = await apiGet('/api/videos');
_videosData = data.situations || {};
const container = document.getElementById('video-situations-liste');
if (!container) return;
container.innerHTML = '';
for (const [sid, sit] of Object.entries(_videosData)) {
container.appendChild(_creerBlocSituation(sid, sit));
}
} catch (e) {
console.warn('Erreur chargement videos:', e);
}
}
function _creerBlocSituation(sid, sit) {
const bloc = document.createElement('div');
bloc.className = 'video-situation-bloc';
bloc.style.cssText = 'margin-bottom:1rem;padding:0.75rem;background:#1a1a2e;border-radius:8px;border:1px solid #333';
const header = document.createElement('div');
header.style.cssText = 'display:flex;align-items:center;gap:0.75rem;margin-bottom:0.5rem;flex-wrap:wrap';
const toggle = document.createElement('label');
toggle.className = 'toggle';
toggle.style.cssText = 'margin:0;flex-shrink:0';
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.checked = !!sit.actif;
cb.onchange = () => toggleSituation(sid, cb.checked);
const slider = document.createElement('span');
slider.className = 'toggle-slider';
toggle.appendChild(cb);
toggle.appendChild(slider);
const titre = document.createElement('h4');
titre.style.cssText = 'margin:0;flex:1;font-size:0.95rem';
titre.textContent = sit.label || sid;
const nbEtapes = document.createElement('span');
nbEtapes.style.cssText = 'color:#888;font-size:0.8rem';
nbEtapes.textContent = `${(sit.etapes || []).length} etape(s)`;
header.appendChild(toggle);
header.appendChild(titre);
header.appendChild(nbEtapes);
bloc.appendChild(header);
const etapesDiv = document.createElement('div');
etapesDiv.style.cssText = 'margin-left:0.5rem';
(sit.etapes || []).forEach((etape, i) => {
const row = document.createElement('div');
row.style.cssText = 'display:flex;align-items:center;gap:0.5rem;margin-bottom:0.4rem;padding:0.3rem 0.5rem;background:#111;border-radius:6px';
const num = document.createElement('span');
num.style.cssText = 'color:#888;font-size:0.8rem;min-width:1.5rem';
num.textContent = `${i + 1}.`;
const labelEtape = document.createElement('input');
labelEtape.type = 'text';
labelEtape.placeholder = `Etape ${i + 1}`;
labelEtape.value = (sit.labels_etapes && sit.labels_etapes[String(i)]) || '';
labelEtape.style.cssText = 'flex:1;padding:0.2rem 0.4rem;border-radius:4px;border:1px solid #444;background:#1a1a2e;color:#fff;font-size:0.8rem';
labelEtape.onchange = () => labeliserEtape(sid, i, labelEtape.value);
const btnPlay = document.createElement('button');
btnPlay.className = 'btn-secondaire btn-petit';
btnPlay.textContent = '▶';
btnPlay.title = 'Apercu';
btnPlay.onclick = () => previewEtape(sid, i);
const btnUp = document.createElement('button');
btnUp.className = 'btn-secondaire btn-petit';
btnUp.textContent = '↑';
btnUp.disabled = i === 0;
btnUp.onclick = () => reordonnerEtape(sid, i, i - 1);
const btnDown = document.createElement('button');
btnDown.className = 'btn-secondaire btn-petit';
btnDown.textContent = '↓';
btnDown.disabled = i === (sit.etapes || []).length - 1;
btnDown.onclick = () => reordonnerEtape(sid, i, i + 1);
const btnDel = document.createElement('button');
btnDel.className = 'btn-danger btn-petit';
btnDel.textContent = '✕';
btnDel.onclick = () => supprimerEtape(sid, i);
row.appendChild(num);
row.appendChild(labelEtape);
row.appendChild(btnPlay);
row.appendChild(btnUp);
row.appendChild(btnDown);
row.appendChild(btnDel);
etapesDiv.appendChild(row);
});
const ajoutRow = document.createElement('div');
ajoutRow.style.cssText = 'display:flex;align-items:center;gap:0.5rem;margin-top:0.4rem';
const inputFile = document.createElement('input');
inputFile.type = 'file';
inputFile.accept = '.mp4,.webm,.mov';
inputFile.className = 'input-fichier';
inputFile.id = 'input-etape-' + sid;
inputFile.style.cssText = 'flex:1;font-size:0.75rem';
const btnAjout = document.createElement('button');
btnAjout.className = 'btn-secondaire btn-petit';
btnAjout.textContent = '+ Etape';
btnAjout.onclick = () => uploaderEtape(sid);
ajoutRow.appendChild(inputFile);
ajoutRow.appendChild(btnAjout);
etapesDiv.appendChild(ajoutRow);
bloc.appendChild(etapesDiv);
return bloc;
}
async function toggleSituation(sid, actif) {
try {
await fetch(`/api/videos/${sid}/toggle`, {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ actif })
});
} catch (e) {
afficherStatut('Erreur toggle', 'erreur');
}
}
async function uploaderEtape(sid) {
const input = document.getElementById('input-etape-' + sid);
if (!input || !input.files.length) return;
const fd = new FormData();
fd.append('video', input.files[0]);
try {
const resp = await fetch(`/api/videos/${sid}/upload`, { method: 'POST', body: fd });
if (resp.ok) {
input.value = '';
chargerVideosAdmin();
afficherStatut('Etape ajoutee', 'succes');
}
} catch (e) {
afficherStatut('Erreur upload etape', 'erreur');
}
}
async function supprimerEtape(sid, index) {
try {
await fetch(`/api/videos/${sid}/${index}`, { method: 'DELETE' });
chargerVideosAdmin();
afficherStatut('Etape supprimee', 'succes');
} catch (e) {
afficherStatut('Erreur suppression', 'erreur');
}
}
async function reordonnerEtape(sid, de, vers) {
try {
await fetch(`/api/videos/${sid}/reorder`, {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ de, vers })
});
chargerVideosAdmin();
} catch (e) {
afficherStatut('Erreur reorder', 'erreur');
}
}
async function labeliserEtape(sid, index, label) {
try {
await fetch(`/api/videos/${sid}/label`, {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ index, label })
});
} catch (e) {
console.warn('Erreur label etape:', e);
}
}
function previewEtape(sid, index) {
const preview = document.getElementById('video-situation-preview');
if (preview) {
preview.src = `/api/videos/${sid}/${index}/stream`;
preview.load();
preview.play();
}
}
async function creerSituation() {
const input = document.getElementById('nouvelle-situation-label');
const label = (input && input.value || '').trim();
if (!label) return;
const id = label.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '');
try {
await fetch('/api/videos/situation/creer', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ id, label })
});
input.value = '';
chargerVideosAdmin();
afficherStatut('Situation ajoutee', 'succes');
} catch (e) {
afficherStatut('Erreur creation situation', 'erreur');
}
}
// === WIZARD VIDEO DIDACTIQUE ===
let _wizardSituation = null;
let _wizardEtapes = [];
let _wizardIndex = 0;
async function lancerWizard(situationId) {
const data = await apiGet('/api/videos');
const sit = (data.situations || {})[situationId];
if (!sit || !sit.etapes || sit.etapes.length === 0) {
afficherStatut('Aucune etape pour cette situation', 'erreur');
return;
}
_wizardSituation = { id: situationId, ...sit };
_wizardEtapes = sit.etapes;
_wizardIndex = 0;
_afficherEtapeWizard();
document.getElementById('wizard-video').classList.remove('cache');
}
function _afficherEtapeWizard() {
const titre = document.getElementById('wizard-titre');
const info = document.getElementById('wizard-etape-info');
const video = document.getElementById('wizard-video-player');
const label = document.getElementById('wizard-etape-label');
const btn = document.getElementById('wizard-btn-ok');
titre.textContent = _wizardSituation.label || _wizardSituation.id;
info.textContent = `Etape ${_wizardIndex + 1} / ${_wizardEtapes.length}`;
const etapeLabel = (_wizardSituation.labels_etapes || {})[String(_wizardIndex)] || '';
label.textContent = etapeLabel;
video.src = `/api/videos/${_wizardSituation.id}/${_wizardIndex}/stream`;
video.load();
video.play();
if (_wizardIndex === _wizardEtapes.length - 1) {
btn.textContent = 'Terminer ✓';
btn.style.background = '#2196f3';
} else {
btn.textContent = "OK, c'est bon !";
btn.style.background = '#4caf50';
}
}
function wizardEtapeSuivante() {
_wizardIndex++;
if (_wizardIndex >= _wizardEtapes.length) {
fermerWizard();
return;
}
_afficherEtapeWizard();
}
function fermerWizard() {
const overlay = document.getElementById('wizard-video');
const video = document.getElementById('wizard-video-player');
if (video) { video.pause(); video.src = ''; }
overlay.classList.add('cache');
_wizardSituation = null;
_wizardEtapes = [];
_wizardIndex = 0;
}
function lancerWizardDepuisAdmin() {
const actives = Object.entries(_videosData).filter(([_, s]) => s.actif && s.etapes && s.etapes.length > 0);
if (actives.length === 0) {
afficherStatut('Aucune situation active avec des etapes', 'erreur');
return;
}
lancerWizard(actives[0][0]);
}
// === SURPRISE (photo/video avant capture) ===
async function chargerSurpriseConfig() {
const surprise = config.surprise || {};
const tog = document.getElementById('tog-surprise-actif');
if (tog) tog.checked = !!surprise.actif;
const delai = document.getElementById('admin-surprise-delai');
if (delai) delai.value = surprise.delai_ms || 1000;
const typeRadios = document.querySelectorAll('input[name="surprise-type"]');
typeRadios.forEach(r => r.checked = r.value === (surprise.type || 'photo'));
const current = document.getElementById('surprise-current');
if (current) current.textContent = surprise.fichier || 'Aucun media configure';
majSurprisePreview(surprise);
}
function majSurprisePreview(surprise) {
const container = document.getElementById('surprise-preview');
if (!container) return;
if (!surprise || !surprise.fichier) {
container.innerHTML = '<span style="color:#666">Aucun apercu</span>';
return;
}
const url = '/api/surprise/media';
if (surprise.type === 'video') {
container.innerHTML = `<video src="${url}" style="width:100%;max-height:300px;border-radius:8px" controls></video>`;
} else {
container.innerHTML = `<img src="${url}" style="width:100%;max-height:300px;border-radius:8px;object-fit:contain">`;
}
}
async function uploaderSurprise() {
const input = document.getElementById('input-surprise-media');
if (!input || !input.files.length) return;
const fd = new FormData();
fd.append('media', input.files[0]);
try {
const resp = await fetch('/api/surprise/upload', { method: 'POST', body: fd });
if (resp.ok) {
const data = await resp.json();
input.value = '';
config = await apiGet('/api/config');
chargerSurpriseConfig();
afficherStatut('Media surprise importe', 'succes');
}
} catch (e) {
afficherStatut('Erreur upload surprise', 'erreur');
}
}
async function supprimerSurprise() {
try {
await fetch('/api/surprise/media', { method: 'DELETE' });
config = await apiGet('/api/config');
chargerSurpriseConfig();
afficherStatut('Surprise supprimee', 'succes');
} catch (e) {
afficherStatut('Erreur suppression surprise', 'erreur');
}
}
async function sauvegarderSurprise() {
const actif = document.getElementById('tog-surprise-actif')?.checked || false;
const delai = parseInt(document.getElementById('admin-surprise-delai')?.value) || 1000;
const type = document.querySelector('input[name="surprise-type"]:checked')?.value || 'photo';
await apiPost('/api/config', {
surprise: { actif, delai_ms: delai, type }
});
afficherStatut('Surprise sauvegardee', 'succes');
}