/* Fiche établissement plein écran — overlay superposé à la carte. * * L'overlay est simplement affiché/masqué : la carte Leaflet reste montée * dessous, donc « retour à la carte » ne provoque aucun rechargement. * * Contenu servi par l'API PHP/MySQL : * - arborescence globale de dossiers (api/folders.php) ; * - éléments validés de l'établissement (api/items.php) : fichiers, photos, liens ; * - soumission par tout internaute (statut « en attente » jusqu'à validation). */ (function () { 'use strict'; const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ( { '&': '&', '<': '<', '>': '>', '"': '"' }[c] )); const fmtSize = (n) => { if (n == null) return ''; if (n < 1024) return n + ' o'; if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' Ko'; return (n / 1024 / 1024).toFixed(1) + ' Mo'; }; // --- API --- 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; } let foldersCache = null; // arbre global, chargé une fois const getFolders = async () => (foldersCache ??= await apiJson('api/folders.php')); const getItems = (uai) => apiJson(`api/items.php?uai=${encodeURIComponent(uai)}`); // --- État --- let current = null; // établissement affiché let items = []; // éléments validés de l'établissement let selectedFolder = 'all'; let onLocate = null; // Contexte de l'utilisateur connecté (déduit de api/comments.php?uai=). let viewer = { auth: false, can_post: false, can_validate: false, role: null, username: null }; let estComments = null; // dernière réponse des commentaires d'établissement const els = {}; const $ = (id) => document.getElementById(id); function cacheEls() { els.root = $('fiche'); els.back = $('fiche-back'); els.nom = $('fiche-nom'); els.sub = $('fiche-sub'); els.tabs = els.root.querySelectorAll('.fiche-tab'); els.panels = { infos: $('tab-infos'), docs: $('tab-docs'), photos: $('tab-photos'), propose: $('tab-propose'), }; els.infoMain = $('info-main'); // colonne gauche : identité, coordonnées, effectifs, DPE els.infoSide = $('info-side'); // colonne droite : commentaires els.tree = $('folder-tree'); els.items = $('folder-items'); els.photoGrid = $('photo-grid'); els.locate = $('fiche-locate'); els.form = $('propose-form'); els.pFolder = $('p-folder'); els.proposeMsg = $('propose-msg'); } // --- Onglets --- function selectTab(name) { els.tabs.forEach((t) => t.classList.toggle('active', t.dataset.tab === name)); Object.entries(els.panels).forEach(([k, p]) => p.classList.toggle('active', k === name)); } // --- Informations --- const OPTIONS = [ { key: 'rest', label: 'Restauration' }, { key: 'heb', label: 'Internat (hébergement)' }, { key: 'ulis', label: 'ULIS' }, { key: 'segp', label: 'SEGPA' }, ]; function infoRow(label, value) { if (value === null || value === undefined || value === '') return ''; return `
${esc(label)}
${value}
`; } function renderInfos(e) { const badgeCls = e.etat === 'OUVERT' ? 'ouvert' : 'fermer'; const ville = [e.cp, e.com].filter(Boolean).join(' '); const opts = OPTIONS.filter((o) => e[o.key] === 1).map((o) => o.label); let webHtml = ''; if (e.web) { const url = /^https?:/.test(e.web) ? e.web : 'http://' + e.web; webHtml = `${esc(e.web)}`; } const mailHtml = e.mail ? `${esc(e.mail)}` : ''; const telHtml = e.tel ? `${esc(e.tel)}` : ''; const dep = [e.dep, e.depc ? `(${e.depc})` : ''].filter(Boolean).join(' '); els.infoMain.querySelectorAll('.info-grid').forEach((n) => n.remove()); els.infoMain.insertAdjacentHTML('afterbegin', `

Identité

${infoRow('Type', esc(e.type))} ${infoRow('Statut', esc(e.stat))} ${infoRow('État', e.etat ? `${esc(e.etat)}` : '')} ${infoRow('UAI', esc(e.id))}

Coordonnées

${infoRow('Adresse', esc(e.adr))} ${infoRow('Commune', esc(ville))} ${infoRow('Téléphone', telHtml)} ${infoRow('Courriel', mailHtml)} ${infoRow('Site web', webHtml)}

Rattachement

${infoRow('Région', esc(e.reg))} ${infoRow('Académie', esc(e.aca))} ${infoRow('Département', esc(dep))} ${infoRow('Circonscription', esc(e.circ))}

Options & services

${infoRow('Options proposées', opts.length ? esc(opts.join(' · ')) : 'Aucune renseignée')} ${infoRow('Coordonnées GPS', e.lat != null ? `${e.lat}, ${e.lon}` : '')}
`); } // --- Effectifs (écoles 1er degré) : derniers chiffres + évolution --- function sparkline(series, key, color) { // viewBox proportionné ; le SVG conserve son ratio (pas d'étirement). const W = 640, H = 200, padX = 34, padT = 16, padB = 26; const vals = series.map((s) => s[key]); const minV = Math.min(...vals), maxV = Math.max(...vals); const x = (i) => padX + (W - 2 * padX) * (series.length === 1 ? 0.5 : i / (series.length - 1)); const y = (v) => (H - padB) - (H - padT - padB) * (maxV === minV ? 0.5 : (v - minV) / (maxV - minV)); const pts = series.map((s, i) => `${x(i).toFixed(1)},${y(s[key]).toFixed(1)}`); const area = `${padX},${H - padB} ${pts.join(' ')} ${(W - padX).toFixed(1)},${H - padB}`; const last = series.length - 1; const dots = series.map((s, i) => `${s.annee} : ${s[key]}` ).join(''); return ` ${dots} ${series[0].annee} ${series[last].annee} ${maxV} ${minV} `; } const EFF_LABEL = { 'Ecole': 'école (1er degré)', 'Collège': 'collège', 'Lycée': 'lycée', }; // Graphique IPS : courbe de l'établissement + référence départementale (même échelle). function ipsChart(series) { const W = 640, H = 200, padX = 34, padT = 16, padB = 26; const depVals = series.map((s) => s.ips_dep).filter((v) => v != null); const hasDep = depVals.length > 0; const vals = series.map((s) => s.ips).concat(depVals); const minV = Math.min(...vals), maxV = Math.max(...vals); const x = (i) => padX + (W - 2 * padX) * (series.length === 1 ? 0.5 : i / (series.length - 1)); const y = (v) => (H - padB) - (H - padT - padB) * (maxV === minV ? 0.5 : (v - minV) / (maxV - minV)); const poly = (key, color, extra = '') => { const pts = series.map((s, i) => (s[key] != null ? `${x(i).toFixed(1)},${y(s[key]).toFixed(1)}` : null)).filter(Boolean); return ``; }; const dots = (key, color) => series.map((s, i) => s[key] != null ? `${s.annee} : ${s[key]}` : '' ).join(''); return ` ${hasDep ? poly('ips_dep', '#b08968', 'stroke-dasharray="5 4" opacity="0.85"') : ''} ${poly('ips', '#9d4edd')} ${hasDep ? dots('ips_dep', '#b08968') : ''} ${dots('ips', '#9d4edd')} ${series[0].annee} ${series[series.length - 1].annee} ${maxV} ${minV} `; } function ipsCardHtml(ips) { const last = ips[ips.length - 1]; const first = ips[0]; const depTxt = last.ips_dep != null ? ` · département ${last.ips_dep}` : ''; const legend = last.ips_dep != null ? `
ÉtablissementDépartement
` : ''; // Ligne d'évolution seulement s'il y a au moins deux rentrées. let evolHtml = ''; if (ips.length >= 2) { const delta = +(last.ips - first.ips).toFixed(1); const sign = delta > 0 ? '+' : ''; const trend = delta > 0 ? 'up' : delta < 0 ? 'down' : 'flat'; evolHtml = `
${first.annee} → ${last.annee} : ${sign}${delta} pts
`; } return `

IPS — indice de position sociale

${last.ips} (rentrée ${last.annee}${depTxt})
${legend} ${ipsChart(ips)} ${evolHtml}

IPS élevé = milieu social plus favorisé (réf. nationale ≈ 100).

`; } // Carte « Effectifs » de l'établissement (sa propre évolution). function effectifsCardHtml(serie, e) { if (!Array.isArray(serie) || !serie.length) return ''; const last = serie[serie.length - 1]; const first = serie[0]; const delta = last.eleves - first.eleves; const pct = first.eleves ? Math.round(delta / first.eleves * 100) : 0; const sign = delta > 0 ? '+' : ''; const trend = delta > 0 ? 'up' : delta < 0 ? 'down' : 'flat'; const label = EFF_LABEL[e.type] || (e.type ? esc(e.type.toLowerCase()) : ''); const classesHtml = last.classes > 0 ? ` · ${last.classes} classe${last.classes > 1 ? 's' : ''}` : ''; return `

Effectifs${label ? ' — ' + label : ''}

${last.eleves} élèves${classesHtml} (rentrée ${last.annee})
${sparkline(serie, 'eleves', '#1d3557')}
${first.annee} → ${last.annee} : ${sign}${delta} élève${Math.abs(delta) > 1 ? 's' : ''} (${sign}${pct} %)
`; } // Carte « Effectifs cumulés des écoles PUBLIQUES de la commune ». function communeCardHtml(commune, e) { if (!Array.isArray(commune) || !commune.length) return ''; const last = commune[commune.length - 1]; const first = commune[0]; let evol = ''; if (commune.length >= 2) { const delta = last.eleves - first.eleves; const sign = delta > 0 ? '+' : ''; const trend = delta > 0 ? 'up' : delta < 0 ? 'down' : 'flat'; evol = `
${first.annee} → ${last.annee} : ${sign}${delta.toLocaleString('fr')} élève${Math.abs(delta) > 1 ? 's' : ''}
`; } return `

Écoles publiques de la commune${e.com ? ' — ' + esc(e.com) : ''}

${last.eleves.toLocaleString('fr')} élèves (rentrée ${last.annee}, cumul public)
${sparkline(commune, 'eleves', '#2a9d8f')} ${evol}
`; } // Bloc d'une série de population : dernier chiffre + courbe + évolution. function popSeriesHtml(serie, color) { const last = serie[serie.length - 1]; const first = serie[0]; let evol = ''; if (serie.length >= 2) { const delta = last.pop - first.pop; const pct = first.pop ? Math.round(delta / first.pop * 100) : 0; const sign = delta > 0 ? '+' : ''; const trend = delta > 0 ? 'up' : delta < 0 ? 'down' : 'flat'; evol = `
${first.annee} → ${last.annee} : ${sign}${delta.toLocaleString('fr')} hab. (${sign}${pct} %)
`; } return `
${last.pop.toLocaleString('fr')} habitants (${last.annee})
${sparkline(serie, 'pop', color)} ${evol}`; } // Carte « Population » : zone (commune/arrondissement) + ville entière pour les arrondissements. function populationCardHtml(data, e) { const zone = (data && Array.isArray(data.zone)) ? data.zone : []; if (!zone.length) return ''; const estimated = !!(data && data.zone_estimated); const ville = (data && Array.isArray(data.ville)) ? data.ville : []; const estNote = estimated ? '

Estimation : population de la ville × part de l’arrondissement dans les IRIS (recensement 2022).

' : ''; let villeHtml = ''; if (ville.length) { villeHtml = `

Ville entière${data.ville_nom ? ' — ' + esc(data.ville_nom) : ''}

${popSeriesHtml(ville, '#577590')}`; } return `

Population${e.com ? ' — ' + esc(e.com) : ''} ${estimated ? '(estimation)' : '(INSEE)'}

${popSeriesHtml(zone, estimated ? '#9aa7b0' : '#577590')} ${estNote} ${villeHtml}
`; } // Établissement (effectifs · IPS) puis commune (écoles publiques · population), côte à côte. async function renderEffectifs(e) { els.infoMain.querySelectorAll('.eff-ips-row').forEach((n) => n.remove()); const [eff, communeEff, ips, popData] = await Promise.all([ apiJson(`api/effectifs.php?uai=${encodeURIComponent(e.id)}`).catch(() => []), apiJson(`api/effectifs.php?action=commune&uai=${encodeURIComponent(e.id)}`).catch(() => []), apiJson(`api/ips.php?uai=${encodeURIComponent(e.id)}`).catch(() => []), apiJson(`api/population.php?uai=${encodeURIComponent(e.id)}`).catch(() => null), ]); const cards = [ effectifsCardHtml(Array.isArray(eff) ? eff : [], e), (Array.isArray(ips) && ips.length) ? ipsCardHtml(ips) : '', communeCardHtml(Array.isArray(communeEff) ? communeEff : [], e), populationCardHtml(popData, e), ].filter(Boolean); if (!cards.length) return; const row = document.createElement('div'); row.className = 'eff-ips-row'; row.innerHTML = cards.join(''); els.infoMain.appendChild(row); } // --- DPE (ADEME, bâtiments tertiaires) — rattaché par proximité --- const DPE_NOTE = 'Rattaché au(x) bâtiment(s) tertiaire(s) le(s) plus proche(s) ' + '(données ADEME). Le dataset n’a pas d’identifiant école : le lien est ' + 'géographique, la distance indique sa fiabilité.'; function dpeBadge(letter, kind) { const l = String(letter || '').toUpperCase(); const cls = /^[A-G]$/.test(l) ? 'dpe-' + l.toLowerCase() : 'dpe-na'; const k = kind === 'ges' ? 'GES' : 'Énergie'; return `${k}${esc(l || '—')}`; } async function renderDpe(e) { els.infoMain.querySelectorAll('.dpe-block').forEach((n) => n.remove()); let list; try { list = await apiJson(`api/dpe.php?uai=${encodeURIComponent(e.id)}`); } catch { return; } if (!Array.isArray(list) || !list.length) return; // aucun DPE rattaché const rows = list.map((d) => { const meta = []; if (d.conso != null) meta.push(`${d.conso} kWhEP/m²/an`); if (d.ges != null) meta.push(`${d.ges} kg CO₂/m²/an`); if (d.surface != null) meta.push(`${d.surface} m²`); const dat = d.date_dpe ? `établi le ${esc(d.date_dpe)}` : ''; const fin = d.date_fin ? ` · valide jusqu’au ${esc(d.date_fin)}` : ''; return `
  • ${dpeBadge(d.etiquette_dpe, 'dpe')} ${dpeBadge(d.etiquette_ges, 'ges')}
    ${meta.length ? `
    ${meta.join(' · ')}
    ` : ''}
    ${esc(d.adresse || '')} ≈ ${d.distance_m} m
    ${dat || fin ? `
    ${dat}${fin}
    ` : ''}
  • `; }).join(''); const block = document.createElement('section'); block.className = 'info-card dpe-block'; block.innerHTML = `

    Performance énergétique (DPE)

    ${DPE_NOTE}

    `; els.infoMain.appendChild(block); } // --- Commentaires (réservés aux utilisateurs, visibles par groupe) --- const postJson = (url, body) => apiJson(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); function fmtDateTime(s) { const d = new Date(String(s || '').replace(' ', 'T')); return isNaN(d) ? esc(s) : d.toLocaleString('fr-FR', { dateStyle: 'short', timeStyle: 'short' }); } function fmtDate(s) { const d = new Date(String(s || '').replace(' ', 'T')); return isNaN(d) ? '' : d.toLocaleDateString('fr-FR'); } function commentItemHtml(c, canModerate) { const del = c.mine || canModerate ? `` : ''; return `
  • ${esc(c.username)} ${fmtDateTime(c.created_at)} ${del}
    ${esc(c.body)}
  • `; } // Contenu interne d'un fil (connexion / lecture seule / liste + saisie). // « intro » = true pour le fil d'établissement (textes plus explicites). function threadInnerHtml(data, intro) { if (!data.auth) { return ` ${intro ? '

    Les commentaires sont réservés aux utilisateurs. Connectez-vous pour les consulter et en déposer.

    ' : ''}

    Mot de passe oublié ?

    `; } const canModerate = data.role === 'super'; const list = data.comments.map((c) => commentItemHtml(c, canModerate)); const listHtml = list.length ? `` : '

    Aucun commentaire pour le moment.

    '; let form; if (data.can_post) { form = `
    Connecté : ${esc(data.username)}
    `; } else { form = '

    Vous n’appartenez à aucun groupe : lecture seule. Demandez à un administrateur de vous ajouter à un groupe pour commenter.

    '; } return form + listHtml; } // Charge le contexte visiteur + les commentaires d'établissement (une fois par fiche). async function loadViewer(uai) { try { estComments = await apiJson(`api/comments.php?uai=${encodeURIComponent(uai)}`); } catch { estComments = { auth: false, can_post: false, can_validate: false, comments: [] }; } viewer = { auth: !!estComments.auth, can_post: !!estComments.can_post, can_validate: !!estComments.can_validate, role: estComments.role || null, username: estComments.username || null, }; } // Rendu du fil d'établissement, dans la colonne droite de l'onglet Informations. function renderComments() { els.infoSide.querySelectorAll('.comments-block').forEach((n) => n.remove()); if (!estComments) return; const block = document.createElement('section'); block.className = 'info-card comments-block'; block.innerHTML = `

    💬 Commentaires

    ${threadInnerHtml(estComments, true)}
    `; els.infoSide.appendChild(block); } // Portée d'un fil (établissement ou document) → URL d'API. function threadUrl(threadEl) { return threadEl.dataset.item ? `api/comments.php?item_id=${encodeURIComponent(threadEl.dataset.item)}` : `api/comments.php?uai=${encodeURIComponent(threadEl.dataset.uai)}`; } // Recharge un fil donné (sans toucher au reste de la page). async function refreshThread(threadEl) { let data; try { data = await apiJson(threadUrl(threadEl)); } catch (err) { threadEl.innerHTML = `

    Erreur : ${esc(err.message)}

    `; return; } threadEl.innerHTML = threadInnerHtml(data, !threadEl.dataset.item); } // Gestion commune (envoi connexion / commentaire, suppression) sur tout .comment-thread. async function onThreadSubmit(ev) { const loginForm = ev.target.closest('.comment-login'); const commentForm = ev.target.closest('.comment-form'); if (!loginForm && !commentForm) return; ev.preventDefault(); const threadEl = ev.target.closest('.comment-thread'); try { if (loginForm) { const res = await postJson('api/auth.php?action=login', { username: loginForm.querySelector('.cl-user').value, password: loginForm.querySelector('.cl-pass').value, }); if (res.user && res.user.must_change) { threadEl.innerHTML = '

    Vous devez définir un nouveau mot de passe avant de continuer. Changer mon mot de passe, puis rouvrez la fiche.

    '; return; } await loadViewer(current.id); // met à jour droits (édition) + identité renderComments(); renderDocs(); // réaffiche les documents (boutons d'édition) } else { const body = commentForm.querySelector('.cf-body').value.trim(); if (!body) return; await postJson(threadUrl(threadEl), { body }); await refreshThread(threadEl); } } catch (err) { const msg = ev.target.querySelector('.comment-msg'); if (msg) { msg.textContent = 'Erreur : ' + err.message; msg.className = 'comment-msg err'; } } } async function onThreadClick(ev) { const del = ev.target.closest('.comment-del'); if (!del) return; const threadEl = ev.target.closest('.comment-thread'); if (!threadEl || !confirm('Supprimer ce commentaire ?')) return; try { await apiJson(`api/comments.php?id=${del.dataset.id}`, { method: 'DELETE' }); await refreshThread(threadEl); } catch (err) { alert('Erreur : ' + err.message); } } // Interactions d'une carte document : déplier le fil, éditer le libellé/description. async function onDocClick(ev) { const toggle = ev.target.closest('.comment-toggle'); const edit = ev.target.closest('.doc-edit'); const cancel = ev.target.closest('.doc-edit-cancel'); if (toggle) { const card = toggle.closest('.doc-card'); const box = card.querySelector('.doc-thread'); box.hidden = !box.hidden; toggle.classList.toggle('open', !box.hidden); if (!box.hidden && !box.dataset.loaded) { box.dataset.loaded = '1'; box.innerHTML = `

    Chargement…

    `; await refreshThread(box.querySelector('.comment-thread')); } return; } if (edit) { const card = edit.closest('.doc-card'); const box = card.querySelector('.doc-edit-box'); if (!box.hidden) { box.hidden = true; box.innerHTML = ''; return; } const it = items.find((x) => String(x.id) === String(card.dataset.id)); if (!it) return; box.innerHTML = `
    `; box.hidden = false; return; } if (cancel) { const box = cancel.closest('.doc-edit-box'); box.hidden = true; box.innerHTML = ''; } } async function onDocEditSubmit(ev) { const form = ev.target.closest('.doc-edit-form'); if (!form) return false; ev.preventDefault(); const card = form.closest('.doc-card'); const msg = form.querySelector('.comment-msg'); const title = form.querySelector('.de-title').value.trim(); const description = form.querySelector('.de-desc').value.trim(); if (!title) { msg.textContent = 'Le libellé est requis.'; msg.className = 'comment-msg err'; return true; } try { await postJson('api/items.php?action=update', { id: Number(card.dataset.id), title, description }); items = await getItems(current.id); renderDocs(); } catch (err) { msg.textContent = 'Erreur : ' + err.message; msg.className = 'comment-msg err'; } return true; } // --- Arborescence de dossiers --- function childrenOf(parentId) { return foldersCache .filter((f) => f.parent_id === parentId) .sort((a, b) => a.position - b.position || a.name.localeCompare(b.name, 'fr')); } function countInFolder(folderId) { return items.filter((it) => it.folder_id === folderId).length; } function treeNodeHtml(folder) { const kids = childrenOf(folder.id); const n = countInFolder(folder.id); const active = String(selectedFolder) === String(folder.id) ? ' active' : ''; let html = `
  • ` + `📁 ${esc(folder.name)}${n ? ` ${n}` : ''}`; if (kids.length) { html += ''; } return html + '
  • '; } function renderTree() { const noneCount = countInFolder(null); let html = ''; els.tree.innerHTML = html; } // Ligne principale d'un élément (selon son type). function itemMainHtml(it) { if (it.kind === 'photo') { return `
    ${esc(it.title)}
    🖼 ${esc(it.title)}
    `; } if (it.kind === 'link') { return `
    🔗 ${esc(it.title)}
    `; } return `
    📄 ${esc(it.title)} ${fmtSize(it.size)}
    `; } // Carte d'un élément : libellé, date de dépôt, description, édition, commentaires. function docCardHtml(it) { const date = it.submitted_at ? `📅 déposé le ${fmtDate(it.submitted_at)}` : ''; const desc = it.description ? `
    ${esc(it.description)}
    ` : ''; const editBtn = viewer.can_validate ? `` : ''; return `
    ${itemMainHtml(it)} ${desc}
    ${date} ${editBtn}
    `; } function renderItemsPanel() { let list; if (selectedFolder === 'all') list = items; else if (selectedFolder === 'none') list = items.filter((it) => it.folder_id === null); else list = items.filter((it) => String(it.folder_id) === String(selectedFolder)); if (!list.length) { els.items.innerHTML = '

    Aucun élément validé dans ce dossier.

    '; return; } els.items.innerHTML = `
    ${list.map(docCardHtml).join('')}
    `; } function renderDocs() { renderTree(); renderItemsPanel(); } // --- Photothèque (toutes les photos validées) --- function photoFigureHtml(it) { const date = it.submitted_at ? `${fmtDate(it.submitted_at)}` : ''; return `
    ${esc(it.title)}
    ${esc(it.title)} ${date}
    `; } function renderPhotos() { const photos = items.filter((it) => it.kind === 'photo'); if (!photos.length) { els.photoGrid.innerHTML = '

    Aucune photo validée pour le moment.

    '; return; } els.photoGrid.innerHTML = photos.map(photoFigureHtml).join(''); } // --- Lightbox --- function openLightbox(url, caption) { const box = document.createElement('div'); box.className = 'lightbox'; box.innerHTML = `${esc(caption)}${esc(caption)}`; box.addEventListener('click', () => box.remove()); document.body.appendChild(box); } // --- Formulaire de proposition --- function fillFolderSelect() { const opts = ['']; const walk = (parentId, depth) => { childrenOf(parentId).forEach((f) => { opts.push(``); walk(f.id, depth + 1); }); }; walk(null, 0); els.pFolder.innerHTML = opts.join(''); } function syncProposeFields() { const kind = els.form.querySelector('input[name="kind"]:checked').value; els.form.querySelectorAll('[data-when]').forEach((el) => { el.hidden = !el.dataset.when.split(' ').includes(kind); }); } async function submitProposal(ev) { ev.preventDefault(); const kind = els.form.querySelector('input[name="kind"]:checked').value; const fd = new FormData(); fd.append('kind', kind); fd.append('folder_id', els.pFolder.value); fd.append('title', $('p-title').value.trim()); fd.append('description', $('p-desc').value.trim()); fd.append('submitter_name', $('p-name').value.trim()); fd.append('submitter_email', $('p-mail').value.trim()); if (kind === 'link') { const url = $('p-url').value.trim(); if (!/^https?:\/\//i.test(url)) { return showProposeMsg('Veuillez saisir une URL valide (http(s)://…).', false); } fd.append('url', url); } else { const file = $('p-file').files[0]; if (!file) return showProposeMsg('Veuillez choisir un fichier.', false); fd.append('file', file); } try { await apiJson(`api/items.php?uai=${encodeURIComponent(current.id)}`, { method: 'POST', body: fd }); els.form.reset(); syncProposeFields(); showProposeMsg('✅ Merci ! Votre proposition a été enregistrée et sera publiée après validation.', true); } catch (err) { showProposeMsg('Erreur : ' + err.message, false); } } function showProposeMsg(text, ok) { els.proposeMsg.textContent = text; els.proposeMsg.className = 'propose-msg ' + (ok ? 'ok' : 'err'); } // --- Ouverture / fermeture --- async function open(e) { current = e; items = []; selectedFolder = 'all'; els.nom.textContent = e.nom || 'Établissement'; els.sub.textContent = [e.type, e.stat, [e.cp, e.com].filter(Boolean).join(' ')] .filter(Boolean).join(' · '); renderInfos(e); renderEffectifs(e); renderDpe(e); estComments = null; viewer = { auth: false, can_post: false, can_validate: false, role: null, username: null }; showProposeMsg('', true); selectTab('infos'); els.root.hidden = false; document.body.classList.add('fiche-open'); try { await getFolders(); items = await getItems(e.id); } catch (err) { els.items.innerHTML = `

    Erreur de chargement : ${esc(err.message)}

    `; } await loadViewer(e.id); // contexte connecté (droits d'édition) + commentaires d'établissement fillFolderSelect(); syncProposeFields(); renderDocs(); renderPhotos(); renderComments(); } function close() { els.root.hidden = true; document.body.classList.remove('fiche-open'); current = null; // La carte n'est pas touchée : aucun rechargement. } // --- Initialisation (appelée par app.js) --- function init(opts) { onLocate = (opts || {}).onLocate || null; cacheEls(); els.back.addEventListener('click', close); els.tabs.forEach((t) => t.addEventListener('click', () => selectTab(t.dataset.tab))); // Sélection d'un dossier dans l'arbre. els.tree.addEventListener('click', (ev) => { const node = ev.target.closest('.tree-folder'); if (!node) return; selectedFolder = node.dataset.folder === 'all' || node.dataset.folder === 'none' ? node.dataset.folder : Number(node.dataset.folder); renderDocs(); }); // Agrandissement des photos (onglets Documents et Photothèque). const onPhotoClick = (ev) => { const fig = ev.target.closest('.item-photo'); if (fig) openLightbox(fig.dataset.url, fig.dataset.title); }; els.photoGrid.addEventListener('click', onPhotoClick); // Onglet Documents : photo (lightbox), déplier/éditer une carte, et fils de commentaires. els.items.addEventListener('click', (ev) => { if (!current) return; if (ev.target.closest('.item-photo')) { onPhotoClick(ev); return; } if (ev.target.closest('.comment-toggle, .doc-edit, .doc-edit-cancel')) { onDocClick(ev); return; } onThreadClick(ev); // suppression d'un commentaire dans un fil de document }); els.items.addEventListener('submit', (ev) => { if (!current) return; // Routage SYNCHRONE : chaque handler appelle ev.preventDefault() avant tout await. if (ev.target.closest('.doc-edit-form')) onDocEditSubmit(ev); // édition libellé/description else onThreadSubmit(ev); // connexion / commentaire (fil document) }); // Fil de commentaires d'établissement (carte dans l'onglet Informations). els.panels.infos.addEventListener('submit', (ev) => { if (current) onThreadSubmit(ev); }); els.panels.infos.addEventListener('click', (ev) => { if (current) onThreadClick(ev); }); // Formulaire de proposition. els.form.addEventListener('submit', submitProposal); els.form.querySelectorAll('input[name="kind"]').forEach((r) => r.addEventListener('change', syncProposeFields)); els.locate.addEventListener('click', () => { const e = current; close(); if (e && onLocate) onLocate(e); }); document.addEventListener('keydown', (ev) => { if (ev.key === 'Escape' && !els.root.hidden) { const lb = document.querySelector('.lightbox'); if (lb) lb.remove(); else close(); } }); } window.Fiche = { init, open, close }; })();