/* Espace d'administration : connexion, modération, arborescence, admins, import. */ 'use strict'; const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ( { '&': '&', '<': '<', '>': '>', '"': '"' }[c] )); const $ = (id) => document.getElementById(id); async function apiJson(url, opts) { const res = await fetch(url, opts); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || ('HTTP ' + res.status)); return data; } const postJson = (url, body) => apiJson(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; }; let me = null; let folders = []; let allUsers = []; // liste des utilisateurs (pour l'ajout de membres aux groupes) // --- Démarrage / session --- async function boot() { try { me = (await apiJson('api/auth.php?action=me')).user; } catch { me = null; } me ? showAdmin() : showLogin(); } function showLogin() { $('login').hidden = false; $('admin').hidden = true; $('logout').hidden = true; $('who').textContent = ''; } const ROLE_LABEL = { super: 'admin plateforme', perimeter: 'admin périmètre', member: 'commentateur' }; async function showAdmin() { // Changement de mot de passe forcé : on redirige avant tout. if (me.must_change) { location.href = 'account.html?forced=1'; return; } $('login').hidden = true; $('admin').hidden = false; $('logout').hidden = false; $('account-link').hidden = false; $('who').textContent = `${me.username} — ${ROLE_LABEL[me.role] || me.role}`; $('super-only').hidden = me.role !== 'super'; // Commentateur : pas de file de modération ; il commente depuis les fiches. if (me.role === 'member') { $('queue-count').textContent = ''; $('queue').innerHTML = '
Compte commentateur : rendez-vous sur une fiche d’établissement (onglet 💬 Commentaires) pour participer.
'; return; } await loadQueue(); if (me.role === 'super') { await loadFolders(); await loadUsers(); await loadGroups(); } } $('login-form').addEventListener('submit', async (ev) => { ev.preventDefault(); try { me = (await postJson('api/auth.php?action=login', { username: $('l-user').value, password: $('l-pass').value })).user; $('login-msg').textContent = ''; await showAdmin(); } catch (err) { $('login-msg').textContent = err.message; } }); $('logout').addEventListener('click', async () => { await apiJson('api/auth.php?action=logout', { method: 'POST' }); me = null; showLogin(); }); // --- File de validation --- async function loadQueue() { let q; try { q = await apiJson('api/items.php?action=queue'); } catch (err) { $('queue').innerHTML = `${esc(err.message)}
`; return; } $('queue-count').textContent = q.length || ''; if (!q.length) { $('queue').innerHTML = 'Aucun élément en attente. 🎉
'; return; } $('queue').innerHTML = q.map((it) => { const icon = it.kind === 'photo' ? '🖼' : it.kind === 'link' ? '🔗' : '📄'; let preview; if (it.kind === 'photo') preview = `Aucun dossier.
'; } $('folder-form').addEventListener('submit', async (ev) => { ev.preventDefault(); try { await postJson('api/folders.php', { name: $('nf-name').value.trim(), parent_id: $('nf-parent').value || null }); $('nf-name').value = ''; await loadFolders(); } catch (err) { alert('Erreur : ' + err.message); } }); $('folders-admin').addEventListener('click', async (ev) => { const row = ev.target.closest('.fa-row'); if (!row) return; const id = Number(row.dataset.id); const name = row.querySelector('.fa-name').textContent.replace(/^📁\s*/, ''); try { if (ev.target.closest('.fa-rename')) { const nv = prompt('Nouveau nom :', name); if (nv && nv.trim()) { await apiJson('api/folders.php', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, name: nv.trim() }) }); await loadFolders(); } } else if (ev.target.closest('.fa-del')) { if (confirm(`Supprimer « ${name} » et ses sous-dossiers ?\nLes éléments associés seront déplacés vers « Sans dossier ».`)) { await apiJson(`api/folders.php?id=${id}`, { method: 'DELETE' }); await loadFolders(); } } } catch (err) { alert('Erreur : ' + err.message); } }); // --- Administrateurs de périmètre (super-admin) --- async function loadUsers() { const users = await apiJson('api/users.php'); $('users-admin').innerHTML = users.map((u) => { const roleBadge = u.role === 'super' ? 'plateforme' : u.role === 'member' ? 'commentateur' : 'périmètre'; let scopes; if (u.role === 'super') scopes = 'Tous les établissements'; else if (u.role === 'member') scopes = 'Commente via ses groupes (aucune validation)'; else scopes = u.scopes.map((s) => `${esc(s.com)} (${esc(s.depc)}) ` ).join(' ') || 'Aucune commune'; // Périmètre personnel réservé aux admins de périmètre. const scopeEditor = u.role === 'perimeter' ? `Aucun groupe.
'; } $('group-form').addEventListener('submit', async (ev) => { ev.preventDefault(); try { await postJson('api/groups.php', { name: $('ng-name').value.trim(), can_validate: $('ng-validate').checked }); $('ng-name').value = ''; $('ng-validate').checked = false; await loadGroups(); } catch (err) { alert('Erreur : ' + err.message); } }); $('groups-admin').addEventListener('input', (ev) => { if (ev.target.classList.contains('scope-search')) searchCommunes(ev.target); }); $('groups-admin').addEventListener('change', async (ev) => { const card = ev.target.closest('.group-card'); if (!card) return; const gid = Number(card.dataset.gid); try { if (ev.target.classList.contains('g-validate')) { await postJson('api/groups.php?action=toggle', { id: gid, can_validate: ev.target.checked }); await loadGroups(); } else if (ev.target.classList.contains('g-member-add') && ev.target.value) { await postJson('api/groups.php?action=member', { group_id: gid, user_id: Number(ev.target.value) }); await loadGroups(); } } catch (err) { alert('Erreur : ' + err.message); } }); $('groups-admin').addEventListener('click', async (ev) => { const card = ev.target.closest('.group-card'); if (!card) return; const gid = Number(card.dataset.gid); try { if (ev.target.closest('.scope-result')) { const b = ev.target.closest('.scope-result'); await postJson('api/groups.php?action=scope', { group_id: gid, com: b.dataset.com, depc: b.dataset.depc }); await loadGroups(); } else if (ev.target.closest('.chip-x') && ev.target.dataset.member) { await apiJson(`api/groups.php?action=member&group_id=${gid}&user_id=${ev.target.dataset.member}`, { method: 'DELETE' }); await loadGroups(); } else if (ev.target.closest('.chip-x') && ev.target.dataset.scope) { await apiJson(`api/groups.php?action=scope&id=${ev.target.dataset.scope}`, { method: 'DELETE' }); await loadGroups(); } else if (ev.target.closest('.g-del')) { if (confirm('Supprimer ce groupe ? Les membres et le périmètre associés seront retirés.')) { await apiJson(`api/groups.php?id=${gid}`, { method: 'DELETE' }); await loadGroups(); } } } catch (err) { alert('Erreur : ' + err.message); } }); // --- Import opendata (super-admin) --- $('import-btn').addEventListener('click', async () => { if (!confirm('Télécharger et importer la dernière version de l\'opendata ?\nCela peut prendre quelques minutes.')) return; const btn = $('import-btn'); btn.disabled = true; $('import-msg').className = 'propose-msg'; $('import-msg').textContent = '⏳ Téléchargement et import en cours…'; try { const r = await apiJson('api/import.php', { method: 'POST' }); $('import-msg').className = 'propose-msg ok'; $('import-msg').textContent = `✅ Import terminé : ${r.imported} établissements traités, ${r.skipped} ignorés. Total en base : ${r.total}.`; } catch (err) { $('import-msg').className = 'propose-msg err'; $('import-msg').textContent = 'Erreur : ' + err.message; } finally { btn.disabled = false; } }); // --- Import DPE ADEME + rattachement (super-admin) --- $('dpe-form').addEventListener('submit', async (ev) => { ev.preventDefault(); const dep = $('dpe-dep').value.trim(); const radius = Number($('dpe-radius').value) || 80; const scope = dep ? `du département ${dep}` : 'COMPLET (~532 000 DPE)'; if (!confirm(`Importer le dataset DPE ${scope} et rattacher les bâtiments aux écoles (rayon ${radius} m) ?\nCela peut prendre plusieurs minutes.`)) return; const btn = $('dpe-btn'); btn.disabled = true; $('dpe-msg').className = 'propose-msg'; $('dpe-msg').textContent = '⏳ Import et rattachement en cours… (ne fermez pas cette page)'; try { const r = await postJson('api/import_dpe.php', { dep, radius }); $('dpe-msg').className = 'propose-msg ok'; $('dpe-msg').textContent = `✅ Terminé : ${r.imported} DPE importés (${r.geocoded} géolocalisés) · ${r.links} rattachements pour ${r.etabs} établissements (≤ ${r.radius} m).`; } catch (err) { $('dpe-msg').className = 'propose-msg err'; $('dpe-msg').textContent = 'Erreur : ' + err.message; } finally { btn.disabled = false; } }); boot();