/* ============================================================ BAN YARO — Moderations-Panel Nur für Moderatoren und Admins. ============================================================ */ window.Page_moderation = (() => { let _container = null; let _appState = null; let _tab = 'uebersicht'; const TABS = [ { id: 'uebersicht', label: 'Übersicht', icon: 'house-line' }, { id: 'fotos', label: 'Fotos', icon: 'image' }, { id: 'user', label: 'User', icon: 'users' }, { id: 'forum', label: 'Forum', icon: 'chat-circle-dots' }, { id: 'poi-edits', label: 'POI-Edits', icon: 'clock' }, ]; // ------------------------------------------------------------------ async function init(container, appState) { _container = container; _appState = appState; const u = appState.user; const isMod = u?.rolle === 'admin' || u?.rolle === 'moderator' || u?.is_moderator; if (!isMod) { container.innerHTML = _emptyState('shield', 'Kein Zugriff', 'Dieser Bereich ist nur für Moderatoren und Admins.'); return; } _render(); } function refresh() { _renderTab(); } function onDogChange() {} // ------------------------------------------------------------------ // SHELL // ------------------------------------------------------------------ function _render() { _container.innerHTML = `
${TABS.map(t => ` `).join('')}
`; _container.querySelector('#mod-tabs') ?.style.setProperty('--adm-tab-cols', Math.ceil(TABS.length / 2)); _container.querySelectorAll('#mod-tabs .by-tab').forEach(btn => { btn.addEventListener('click', () => { _tab = btn.dataset.tab; _container.querySelectorAll('#mod-tabs .by-tab').forEach(b => b.classList.toggle('active', b.dataset.tab === _tab) ); _renderTab(); }); }); _renderTab(); } async function _renderTab() { const el = _container.querySelector('#mod-content'); if (!el) return; el.innerHTML = `
Lade…
`; try { switch (_tab) { case 'uebersicht': await _renderStats(el); break; case 'fotos': await _renderFotos(el); break; case 'user': await _renderUsers(el); break; case 'forum': await _renderForum(el); break; case 'poi-edits': await _renderPoiEdits(el); break; } } catch (e) { el.innerHTML = _emptyState('warning', 'Fehler', e.message || 'Unbekannter Fehler.'); } } // ------------------------------------------------------------------ // TAB: ÜBERSICHT // ------------------------------------------------------------------ async function _renderStats(el) { const s = await API.get('/moderation/stats'); el.innerHTML = `
${_statCard('warning', 'Offene Meldungen', s.open_reports, s.open_reports > 0 ? 'var(--c-danger)' : 'var(--c-text-muted)')} ${_statCard('image', 'Fotos ausstehend', s.pending_fotos, s.pending_fotos > 0 ? 'var(--c-warning)' : 'var(--c-text-muted)')} ${_statCard('skull', 'Gesperrte User', s.banned_users, s.banned_users > 0 ? '#f59e0b' : 'var(--c-text-muted)')} ${_statCard('storefront', 'Züchter ausstehend', s.pending_zuchter, s.pending_zuchter > 0 ? 'var(--c-warning)' : 'var(--c-text-muted)')} ${_statCard('clock', 'POI-Korrekturen', s.pending_poi_edits ?? 0, (s.pending_poi_edits ?? 0) > 0 ? 'var(--c-warning)' : 'var(--c-text-muted)')}

${UI.icon('info')} Das Moderations-Panel zeigt dir alle ausstehenden Aufgaben auf einen Blick. Verwende die Tabs oben für Details zu Fotos, Usern und Forum-Meldungen.

`; } function _statCard(icon, label, value, color) { return `
${value ?? '—'}
${label}
`; } // ------------------------------------------------------------------ // TAB: FOTOS // ------------------------------------------------------------------ async function _renderFotos(el) { el.innerHTML = `
Lade…
`; el.querySelector('#mod-fotos-refresh').addEventListener('click', () => _loadFotos(el.querySelector('#mod-fotos-list')) ); await _loadFotos(el.querySelector('#mod-fotos-list')); } async function _loadFotos(el) { el.innerHTML = `
Lade…
`; const fotos = await API.get('/moderation/fotos'); if (!fotos.length) { el.innerHTML = _emptyState('check-circle', 'Keine ausstehenden Fotos', 'Alle Foto-Einreichungen wurden bearbeitet.'); return; } el.innerHTML = `
${fotos.map(f => `
${_esc(f.rasse_name || f.rasse_slug)}
von ${_esc(f.user_name)}
${f.rights_confirmed ? `✓ Bildrechte bestätigt` : `⚠ Keine Bestätigung`}
${f.aktuell_foto ? `
Aktuell:
Aktuell ` : `
Noch kein Foto vorhanden
`}
`).join('')}
`; el.querySelectorAll('.mod-foto-approve').forEach(btn => { btn.addEventListener('click', async () => { btn.disabled = true; btn.textContent = '…'; try { await API.patch(`/moderation/fotos/${btn.dataset.id}`, { action: 'approve' }); UI.toast('Foto freigegeben.', 'success'); await _loadFotos(el); } catch (e) { if (e.status === 404) { UI.toast('Bereits bearbeitet — Liste aktualisiert.', 'info'); await _loadFotos(el); } else { UI.toast(e.message, 'danger'); btn.disabled = false; btn.textContent = '✓ Freigeben'; } } }); }); el.querySelectorAll('.mod-foto-reject').forEach(btn => { btn.addEventListener('click', async () => { const reason = prompt('Ablehnungsgrund (optional, wird dem User angezeigt):'); if (reason === null) return; btn.disabled = true; try { await API.patch(`/moderation/fotos/${btn.dataset.id}`, { action: 'reject', reject_reason: reason || 'Foto entspricht nicht den Anforderungen.' }); UI.toast('Einreichung abgelehnt.', 'info'); await _loadFotos(el); } catch (e) { UI.toast.error(e.message); btn.disabled = false; } }); }); } // ------------------------------------------------------------------ // TAB: USER // ------------------------------------------------------------------ async function _renderUsers(el) { el.innerHTML = `
Lade…
`; const load = async () => { const q = el.querySelector('#mod-user-q').value; const banned = el.querySelector('#mod-only-banned').checked ? 1 : 0; const data = await API.get( `/moderation/users?q=${encodeURIComponent(q)}&banned=${banned}` ); _renderUserList(el.querySelector('#mod-user-list'), data.users, data.total, el); }; let timer; el.querySelector('#mod-user-q').addEventListener('input', () => { clearTimeout(timer); timer = setTimeout(load, 350); }); el.querySelector('#mod-only-banned').addEventListener('change', load); await load(); } function _renderUserList(el, users, total, parentEl) { // Moderatoren (non-admins) sehen keine Admin-User — serverseitig bereits // gefiltert, aber zur Sicherheit auch clientseitig nochmal ausfiltern. const isAdmin = _appState?.user?.rolle === 'admin'; const visible = isAdmin ? users : users.filter(u => u.rolle !== 'admin' && !u.is_admin); if (!visible.length) { el.innerHTML = _emptyState('users', 'Keine Nutzer gefunden', ''); return; } el.innerHTML = `
${total} Nutzer gefunden
${visible.map(u => { const isAdminUser = u.rolle === 'admin' || u.is_admin; const canAction = isAdmin && !isAdminUser; return `
${_esc(u.name[0].toUpperCase())}
${_esc(u.name)} ${u.is_banned ? `GESPERRT` : ''}
${_esc(u.email)} · ${_esc(u.rolle)}
${canAction ? (u.is_banned ? `` : ``) : '' }
`}).join('')}
`; el.querySelectorAll('.mod-ban').forEach(btn => { btn.addEventListener('click', () => _banUser(btn.dataset.uid, btn.dataset.name, true, parentEl)); }); el.querySelectorAll('.mod-unban').forEach(btn => { btn.addEventListener('click', () => _banUser(btn.dataset.uid, btn.dataset.name, false, parentEl)); }); } async function _banUser(uid, name, ban, parentEl) { if (ban) { const reason = window.prompt(`${name} sperren — Grund (optional):`); if (reason === null) return; try { await API.patch(`/moderation/users/${uid}`, { is_banned: 1, ban_reason: reason || 'Kein Grund angegeben.' }); UI.toast.success(`${name} gesperrt.`); _renderTab(); } catch (e) { UI.toast.error(e.message); } } else { try { await API.patch(`/moderation/users/${uid}`, { is_banned: 0, ban_reason: null }); UI.toast.success(`Sperre für ${name} aufgehoben.`); _renderTab(); } catch (e) { UI.toast.error(e.message); } } } // ------------------------------------------------------------------ // TAB: FORUM // ------------------------------------------------------------------ async function _renderForum(el) { el.innerHTML = `
Lade…
`; el.querySelector('#mod-forum-refresh').addEventListener('click', () => _loadReports(el.querySelector('#mod-forum-list')) ); await _loadReports(el.querySelector('#mod-forum-list')); } async function _loadReports(el) { el.innerHTML = `
Lade…
`; const reports = await API.get('/moderation/reports'); if (!reports.length) { el.innerHTML = _emptyState('check-circle', 'Keine offenen Meldungen', 'Alles sauber.'); return; } el.innerHTML = `
${reports.map(r => `
${_esc(r.target_type)} #${r.target_id} · Gemeldet von ${_esc(r.melder_name)}
Grund: ${_esc(r.grund)}
${r.content_preview ? `
${_esc(r.content_preview)}
` : ''}
`).join('')}
`; el.querySelectorAll('.mod-resolve-btn').forEach(btn => { btn.addEventListener('click', async () => { btn.disabled = true; try { await API.patch(`/moderation/reports/${btn.dataset.rid}`, {}); UI.toast.success('Meldung als erledigt markiert.'); await _loadReports(el); } catch (e) { UI.toast.error(e.message); btn.disabled = false; } }); }); } // ------------------------------------------------------------------ // HELPERS // ------------------------------------------------------------------ function _emptyState(icon, title, text) { return `

${title}

${text ? `

${text}

` : ''}
`; } // ------------------------------------------------------------------ // TAB: POI-KORREKTUREN // ------------------------------------------------------------------ async function _renderPoiEdits(el) { const edits = await API.get('/moderation/poi-edits'); if (!edits.length) { el.innerHTML = _emptyState('check-circle', 'Alles erledigt', 'Keine ausstehenden POI-Korrekturen.'); return; } const STATUS_LABEL = { pending: 'Ausstehend', approved: 'Genehmigt', rejected: 'Abgelehnt' }; const STATUS_COLOR = { pending: 'var(--c-warning)', approved: 'var(--c-success,#22c55e)', rejected: 'var(--c-danger)' }; el.innerHTML = `
${edits.map(e => `
${_esc(e.poi_name)}
OSM-ID: ${_esc(e.osm_id)} · Feld: ${_esc(e.field)} · von ${_esc(e.einreicher_name)} · ${new Date(e.created_at).toLocaleDateString('de-DE')}
${STATUS_LABEL[e.status] || e.status}
Aktuell
${_esc(e.old_value) || 'leer'}
Vorschlag
${_esc(e.new_value)}
${e.status === 'pending' ? `
` : ''}
`).join('')}
`; el.querySelectorAll('[data-action]').forEach(btn => { btn.addEventListener('click', async () => { const id = parseInt(btn.dataset.id); const action = btn.dataset.action; btn.disabled = true; try { await API.patch(`/moderation/poi-edits/${id}`, { action }); UI.toast.success(action === 'approve' ? 'Übernommen!' : 'Abgelehnt.'); await _renderPoiEdits(el); } catch (err) { UI.toast.error(err.message || 'Fehler.'); btn.disabled = false; } }); }); } function _esc(s) { if (!s) return ''; return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } // ------------------------------------------------------------------ return { init, refresh, onDogChange }; })();