import { useEffect, useMemo, useState } from 'react' import { Button, ButtonGroup, Col, Container, Form, Row, Tab, Tabs } from 'react-bootstrap' import { useTranslation } from 'react-i18next' import { createForum, deleteForum, listAllForums, reorderForums, updateForum } from '../api/client' export default function Acp({ isAdmin }) { const { t } = useTranslation() const [forums, setForums] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [selectedId, setSelectedId] = useState(null) const [draggingId, setDraggingId] = useState(null) const [overId, setOverId] = useState(null) const [showForm, setShowForm] = useState(false) const [createType, setCreateType] = useState(null) const [collapsed, setCollapsed] = useState(() => new Set()) const [form, setForm] = useState({ name: '', description: '', type: 'category', parentId: '', }) const refreshForums = async () => { setLoading(true) setError('') try { const data = await listAllForums() setForums(data) } catch (err) { setError(err.message) } finally { setLoading(false) } } useEffect(() => { if (isAdmin) { refreshForums() } }, [isAdmin]) const getParentId = (forum) => { if (!forum.parent) return null if (typeof forum.parent === 'string') { return forum.parent.split('/').pop() } return forum.parent.id ?? null } const forumTree = useMemo(() => { const map = new Map() const roots = [] forums.forEach((forum) => { map.set(String(forum.id), { ...forum, children: [] }) }) forums.forEach((forum) => { const parentId = getParentId(forum) const node = map.get(String(forum.id)) if (parentId && map.has(String(parentId))) { map.get(String(parentId)).children.push(node) } else { roots.push(node) } }) const sortNodes = (nodes) => { nodes.sort((a, b) => { if (a.position !== b.position) return a.position - b.position return a.name.localeCompare(b.name) }) nodes.forEach((node) => sortNodes(node.children)) } sortNodes(roots) return { roots, map } }, [forums]) const categoryOptions = useMemo( () => forums.filter((forum) => forum.type === 'category'), [forums] ) const handleSelectForum = (forum) => { const parentId = typeof forum.parent === 'string' ? forum.parent.split('/').pop() : forum.parent?.id ?? '' setSelectedId(String(forum.id)) setShowForm(true) setCreateType(null) setForm({ name: forum.name || '', description: forum.description || '', type: forum.type || 'category', parentId: parentId ? String(parentId) : '', }) } const handleReset = () => { setSelectedId(null) setShowForm(false) setCreateType(null) setForm({ name: '', description: '', type: 'category', parentId: '', }) } const isExpanded = (forumId) => { const key = String(forumId) return !collapsed.has(key) } const toggleExpanded = (forumId) => { const key = String(forumId) setCollapsed((prev) => { const next = new Set(prev) if (next.has(key)) { next.delete(key) } else { next.add(key) } return next }) } const handleCollapseAll = () => { const ids = forums .filter((forum) => forum.type === 'category') .map((forum) => String(forum.id)) setCollapsed(new Set(ids)) } const handleExpandAll = () => { setCollapsed(new Set()) } const handleStartCreate = (type) => { const current = selectedId setSelectedId(null) setShowForm(true) setCreateType(type) const parentFromSelection = current ? forums.find((forum) => String(forum.id) === String(current)) : null const parentId = parentFromSelection?.type === 'category' ? String(parentFromSelection.id) : '' setForm({ name: '', description: '', type, parentId, }) } const handleSubmit = async (event) => { event.preventDefault() setError('') const trimmedName = form.name.trim() if (!trimmedName) { setError(t('acp.forums_name_required')) return } try { if (selectedId) { await updateForum(selectedId, { name: trimmedName, description: form.description, type: form.type, parentId: form.parentId || null, }) } else { await createForum({ name: trimmedName, description: form.description, type: form.type, parentId: form.parentId || null, }) } handleReset() refreshForums() } catch (err) { setError(err.message) } } const handleDelete = async (forumId) => { setError('') if (!confirm(t('acp.forums_confirm_delete'))) { return } try { await deleteForum(forumId) if (selectedId === String(forumId)) { handleReset() } refreshForums() } catch (err) { setError(err.message) } } const handleDragStart = (event, forumId) => { event.dataTransfer.effectAllowed = 'move' event.dataTransfer.setData('text/plain', String(forumId)) setDraggingId(String(forumId)) } const handleDragEnd = () => { setDraggingId(null) setOverId(null) } const applyLocalOrder = (parentId, orderedIds) => { setForums((prev) => prev.map((forum) => { const pid = getParentId(forum) if (String(pid ?? '') !== String(parentId ?? '')) { return forum } const newIndex = orderedIds.indexOf(String(forum.id)) return newIndex === -1 ? forum : { ...forum, position: newIndex + 1 } }) ) } const handleDragOver = (event, targetId, parentId) => { event.preventDefault() event.dataTransfer.dropEffect = 'move' if (!draggingId || String(draggingId) === String(targetId)) { return } const draggedForum = forums.find((forum) => String(forum.id) === String(draggingId)) if (!draggedForum) { return } const draggedParentId = getParentId(draggedForum) if (String(draggedParentId ?? '') !== String(parentId ?? '')) { return } const siblings = forums.filter((forum) => { const pid = getParentId(forum) return String(pid ?? '') === String(parentId ?? '') }) const ordered = siblings .slice() .sort((a, b) => { if (a.position !== b.position) return a.position - b.position return a.name.localeCompare(b.name) }) .map((forum) => String(forum.id)) const fromIndex = ordered.indexOf(String(draggingId)) const toIndex = ordered.indexOf(String(targetId)) if (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex) { return } ordered.splice(toIndex, 0, ordered.splice(fromIndex, 1)[0]) setOverId(String(targetId)) applyLocalOrder(parentId, ordered) } const handleDragEnter = (forumId) => { if (draggingId && String(forumId) !== String(draggingId)) { setOverId(String(forumId)) } } const handleDragLeave = (event, forumId) => { if (event.currentTarget.contains(event.relatedTarget)) { return } if (overId === String(forumId)) { setOverId(null) } } const handleDrop = async (event, targetId, parentId) => { event.preventDefault() const draggedId = event.dataTransfer.getData('text/plain') if (!draggedId || String(draggedId) === String(targetId)) { setDraggingId(null) setOverId(null) return } const siblings = forums.filter((forum) => { const pid = getParentId(forum) return String(pid ?? '') === String(parentId ?? '') }) const ordered = siblings .slice() .sort((a, b) => { if (a.position !== b.position) return a.position - b.position return a.name.localeCompare(b.name) }) .map((forum) => String(forum.id)) const fromIndex = ordered.indexOf(String(draggedId)) const toIndex = ordered.indexOf(String(targetId)) if (fromIndex === -1 || toIndex === -1) { return } ordered.splice(toIndex, 0, ordered.splice(fromIndex, 1)[0]) try { await reorderForums(parentId, ordered) const updated = forums.map((forum) => { const pid = getParentId(forum) if (String(pid ?? '') !== String(parentId ?? '')) { return forum } const newIndex = ordered.indexOf(String(forum.id)) return newIndex === -1 ? forum : { ...forum, position: newIndex + 1 } }) setForums(updated) } catch (err) { setError(err.message) } finally { setDraggingId(null) setOverId(null) } } const renderTree = (nodes, depth = 0) => nodes.map((node) => (
{t('acp.no_access')}
{t('acp.general_hint')}
{t('acp.forums_hint')}
{error &&{error}
}{t('acp.loading')}
} {!loading && forumTree.roots.length === 0 && ({t('acp.forums_empty')}
)} {forumTree.roots.length > 0 && ({t('acp.forums_form_hint')}
{t('acp.forums_form_empty_hint')}
{t('acp.users_hint')}