/* ============================================================
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
// ------------------------------------------------------------------
function _switchTab(tabId) {
_tab = tabId;
_container.querySelectorAll('#mod-tabs .by-tab').forEach(b =>
b.classList.toggle('active', b.dataset.tab === _tab)
);
_renderTab();
}
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)', 'forum')}
${_statCard('image', 'Fotos ausstehend', s.pending_fotos, s.pending_fotos > 0 ? 'var(--c-warning)' : 'var(--c-text-muted)', 'fotos')}
${_statCard('skull', 'Gesperrte User', s.banned_users, s.banned_users > 0 ? '#f59e0b' : 'var(--c-text-muted)', 'user')}
${_statCard('storefront','Züchter ausstehend',s.pending_zuchter, s.pending_zuchter > 0 ? 'var(--c-warning)' : 'var(--c-text-muted)', 'user')}
${_statCard('clock', 'POI-Korrekturen', s.pending_poi_edits ?? 0,(s.pending_poi_edits ?? 0) > 0 ? 'var(--c-warning)' : 'var(--c-text-muted)', 'poi-edits')}
`;
el.querySelectorAll('.mod-stat-card[data-tab]').forEach(card => {
card.addEventListener('click', () => _switchTab(card.dataset.tab));
});
}
function _statCard(icon, label, value, color, tab) {
const clickable = tab ? `data-tab="${tab}" style="padding:var(--space-4);text-align:center;cursor:pointer;transition:box-shadow .15s,transform .15s" onmouseenter="this.style.boxShadow='var(--shadow-md)';this.style.transform='translateY(-2px)'" onmouseleave="this.style.boxShadow='';this.style.transform=''"` : `style="padding:var(--space-4);text-align:center"`;
return `
${value ?? '—'}
${label}
${tab ? `
${UI.icon('arrow-right')} öffnen
` : ''}
`;
}
// ------------------------------------------------------------------
// 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:
})
` : `
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 };
})();