/* 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
? `Établissement Dé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 ``;
}
// 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 ? '' : ''}
`;
}
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 = ``;
} else {
form = '';
}
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
`;
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 = '';
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 = ``;
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 += '' + kids.map(treeNodeHtml).join('') + ' ';
}
return html + ' ';
}
function renderTree() {
const noneCount = countInFolder(null);
let html = '';
html += ``
+ `🗂 Tous les éléments ${items.length} `;
html += childrenOf(null).map(treeNodeHtml).join('');
if (noneCount) {
html += ``
+ `📂 Sans dossier ${noneCount} `;
}
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)}
`;
}
if (it.kind === 'link') {
return ``;
}
return ``;
}
// 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 ? `✏️ Éditer ` : '';
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)} ${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)} `;
box.addEventListener('click', () => box.remove());
document.body.appendChild(box);
}
// --- Formulaire de proposition ---
function fillFolderSelect() {
const opts = ['— Aucun dossier — '];
const walk = (parentId, depth) => {
childrenOf(parentId).forEach((f) => {
opts.push(`${' '.repeat(depth)}${esc(f.name)} `);
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 };
})();