import { useCallback, useEffect, useMemo, useState } from 'react' import { Link, Navigate } from 'react-router-dom' import { useAuth } from '../context/AuthContext' import api from '../utils/api' import AdminPageNav from '../components/AdminPageNav' const VISIBILITY_OPTIONS = [ { value: 'all', label: 'Alle Sichtbarkeiten' }, { value: 'private', label: 'Privat' }, { value: 'club', label: 'Verein' }, { value: 'official', label: 'Offiziell' }, ] const VISIBILITY_LABEL = { private: 'Privat', club: 'Verein', official: 'Offiziell', } const STATUS_LABELS = { draft: 'Entwurf', in_review: 'In Prüfung', approved: 'Freigegeben', archived: 'Archiviert', active: 'Aktiv', legacy_unreviewed: 'Rechte ungeprüft', declared: 'Rechte erklärt', blocked: 'Gesperrt', } const LIFECYCLE_LABELS = { active: 'Aktiv', trash_soft: 'Papierkorb (soft)', trash_hidden: 'Papierkorb (hidden)', } function formatDate(value) { if (!value) return '—' try { return new Date(value).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }) } catch { return String(value) } } function contentLink(item) { const id = item.id switch (item.content_type) { case 'exercise': return `/exercises/${id}` case 'training_module': return `/planning/training-modules/${id}` case 'framework_program': return `/planning/framework-programs/${id}` case 'plan_template': return `/planning/plan-templates/${id}` case 'maturity_model': return '/admin/maturity-models' case 'media_asset': return '/media' default: return null } } function statusOptionsForType(meta, contentType) { const t = meta?.content_types?.find((x) => x.id === contentType) return (t?.status_values || []).map((v) => ({ value: v, label: STATUS_LABELS[v] || v, })) } function EditModal({ open, item, meta, onClose, onSaved }) { const [status, setStatus] = useState('') const [visibility, setVisibility] = useState('') const [lifecycle, setLifecycle] = useState('') const [saving, setSaving] = useState(false) const [error, setError] = useState('') useEffect(() => { if (!item) return setStatus(item.status || '') setVisibility(item.visibility || '') setLifecycle(item.extra_status || 'active') setError('') }, [item]) if (!open || !item) return null const typeMeta = meta?.content_types?.find((x) => x.id === item.content_type) const statusOpts = statusOptionsForType(meta, item.content_type) const submit = async () => { setSaving(true) setError('') try { const body = {} if (typeMeta?.has_status && status && status !== item.status) body.status = status if (typeMeta?.has_visibility && visibility && visibility !== item.visibility) { body.visibility = visibility } if (item.content_type === 'media_asset' && lifecycle && lifecycle !== item.extra_status) { body.lifecycle_state = lifecycle } if (!Object.keys(body).length) { onClose() return } await api.patchAdminUserContentItem(item.content_type, item.id, body) await onSaved() onClose() } catch (e) { setError(e.message || String(e)) } finally { setSaving(false) } } return (

Inhalt bearbeiten

{item.type_label} · #{item.id}

{item.title || '—'}

{typeMeta?.has_status ? (
) : null} {typeMeta?.has_visibility ? (
) : null} {item.content_type === 'media_asset' ? (
) : null} {error ? (

{error}

) : null}
) } export default function AdminUserContentPage() { const { user } = useAuth() const isSuperadmin = user?.role === 'superadmin' const [meta, setMeta] = useState(null) const [userSummary, setUserSummary] = useState([]) const [items, setItems] = useState([]) const [total, setTotal] = useState(0) const [loading, setLoading] = useState(true) const [itemsLoading, setItemsLoading] = useState(false) const [error, setError] = useState('') const [profileId, setProfileId] = useState('') const [contentType, setContentType] = useState('all') const [visibility, setVisibility] = useState('all') const [status, setStatus] = useState('') const [search, setSearch] = useState('') const [offset, setOffset] = useState(0) const limit = 50 const [editItem, setEditItem] = useState(null) const contentTypeOptions = useMemo(() => { const base = [{ value: 'all', label: 'Alle Typen' }] for (const t of meta?.content_types || []) { base.push({ value: t.id, label: t.label }) } return base }, [meta]) const statusFilterOptions = useMemo(() => { if (contentType === 'all') { return [ { value: '', label: 'Beliebiger Status' }, { value: 'draft', label: STATUS_LABELS.draft }, { value: 'in_review', label: STATUS_LABELS.in_review }, { value: 'approved', label: STATUS_LABELS.approved }, { value: 'archived', label: STATUS_LABELS.archived }, { value: 'active', label: STATUS_LABELS.active }, { value: 'legacy_unreviewed', label: STATUS_LABELS.legacy_unreviewed }, ] } return [ { value: '', label: 'Beliebiger Status' }, ...statusOptionsForType(meta, contentType), ] }, [contentType, meta]) const loadSummary = useCallback(async () => { const [m, s] = await Promise.all([api.getAdminUserContentMeta(), api.listAdminUserContentSummary()]) setMeta(m) setUserSummary(Array.isArray(s) ? s : []) }, []) const loadItems = useCallback(async (forcedOffset) => { setItemsLoading(true) try { const params = { content_type: contentType, visibility, limit, offset: forcedOffset ?? offset, } if (profileId) params.profile_id = Number(profileId) if (status) params.status = status if (search.trim()) params.search = search.trim() const res = await api.listAdminUserContentItems(params) setItems(Array.isArray(res?.items) ? res.items : []) setTotal(Number(res?.total) || 0) } finally { setItemsLoading(false) } }, [contentType, visibility, status, search, profileId, offset]) useEffect(() => { if (!isSuperadmin) return let cancelled = false ;(async () => { setLoading(true) setError('') try { await loadSummary() } catch (e) { if (!cancelled) setError(e.message || String(e)) } finally { if (!cancelled) setLoading(false) } })() return () => { cancelled = true } }, [isSuperadmin, loadSummary]) useEffect(() => { if (!isSuperadmin) return let cancelled = false ;(async () => { try { await loadItems() } catch (e) { if (!cancelled) setError(e.message || String(e)) } })() return () => { cancelled = true } }, [isSuperadmin, loadItems]) const applyFilters = () => { setOffset(0) loadItems(0) } const handleDelete = async (item) => { const label = item.title || `${item.type_label} #${item.id}` if ( !confirm( `„${label}" wirklich endgültig löschen?\n\nDiese Aktion kann nicht rückgängig gemacht werden.`, ) ) { return } try { await api.deleteAdminUserContentItem(item.content_type, item.id) await Promise.all([loadItems(), loadSummary()]) } catch (e) { alert(e.message || String(e)) } } if (!isSuperadmin) return return (

Nutzer-Inhalte

Aktivitäten aller Nutzer einsehen — inklusive privater Inhalte. Status setzen oder Inhalte löschen (nur Superadmin).

{error ? (

{error}

) : null} {loading ? (
) : ( <>

Aktivität je Nutzer

{userSummary.length === 0 ? (

Noch keine nutzerangelegten Inhalte.

) : (
{(meta?.content_types || []).map((t) => ( ))} {userSummary.map((u) => ( {(meta?.content_types || []).map((t) => ( ))} ))}
Nutzer Gesamt{t.label}
{u.name || `Profil #${u.id}`}
{u.email || '—'}
{u.total_count} {u.counts_by_type?.[t.id] ?? 0}
)}

Inhalte

setSearch(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && applyFilters()} placeholder="Titel oder Dateiname…" />
{itemsLoading ? (
) : items.length === 0 ? (

Keine Inhalte für die aktuellen Filter.

) : (
{items.map((item) => { const href = contentLink(item) return ( ) })}
Typ Titel Nutzer Sichtbarkeit Status Aktualisiert
{item.type_label}
{href ? ( {item.title || '—'} ) : ( item.title || '—' )}
#{item.id} {item.club_name ? ` · ${item.club_name}` : ''}
{item.profile_name || '—'}
{item.profile_email || (item.profile_id ? `#${item.profile_id}` : '—')}
{item.visibility ? ( {VISIBILITY_LABEL[item.visibility] || item.visibility} ) : ( '—' )} {item.status ? STATUS_LABELS[item.status] || item.status : '—'} {item.extra_status && item.extra_status !== 'active' ? (
{LIFECYCLE_LABELS[item.extra_status] || item.extra_status}
) : null}
{formatDate(item.updated_at)}
)} {total > limit ? (
{total} Einträge · Seite {Math.floor(offset / limit) + 1} von{' '} {Math.ceil(total / limit)}
) : null}
)} setEditItem(null)} onSaved={async () => { await Promise.all([loadItems(), loadSummary()]) }} />
) }