import { useState, useMemo, useCallback, useEffect } from 'react'; import { formatDateTime } from '../../utils/format'; import { forumApi, settingsApi } from '../../utils/api'; import type { ForumCategory, ForumReply, ForumThread } from '../../types'; export default function IntranetModeration() { const [categories, setCategories] = useState([]); const [threads, setThreads] = useState([]); const [replies, setReplies] = useState([]); const [selectedThreadId, setSelectedThreadId] = useState(null); const [createTitle, setCreateTitle] = useState(''); const [createContent, setCreateContent] = useState(''); const [createCategoryId, setCreateCategoryId] = useState(''); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isCategoryModalOpen, setIsCategoryModalOpen] = useState(false); const [editingCategoryId, setEditingCategoryId] = useState(null); const [categoryName, setCategoryName] = useState(''); const [categoryDescription, setCategoryDescription] = useState(''); const [categoryIcon, setCategoryIcon] = useState('📁'); const [search, setSearch] = useState(''); const [activeTab, setActiveTab] = useState<'threads' | 'replies' | 'categories'>('threads'); const [isEnabled, setIsEnabled] = useState(true); const [toggling, setToggling] = useState(false); const [loading, setLoading] = useState(true); const [creating, setCreating] = useState(false); const [savingCategory, setSavingCategory] = useState(false); const [error, setError] = useState(''); const loadModerationData = useCallback(async () => { setLoading(true); setError(''); try { const [cats, threadRes] = await Promise.all([ forumApi.getCategories(), forumApi.getThreads({ limit: 200 }), ]); const loadedThreads = threadRes.data; setCategories(cats); setThreads(loadedThreads); const detailed = await Promise.all( loadedThreads.map((thread) => forumApi.getThread(thread.id).catch(() => null)) ); const allReplies = detailed .filter((thread): thread is ForumThread & { replies: ForumReply[] } => Boolean(thread)) .flatMap((thread) => thread.replies); setReplies(allReplies); } catch { setError('Failed to load moderation data.'); } finally { setLoading(false); } }, []); useEffect(() => { settingsApi.get().then((s) => setIsEnabled(s.forumEnabled)).catch(() => {}); }, []); useEffect(() => { void loadModerationData(); }, [loadModerationData]); useEffect(() => { if (categories.length === 0) { setCreateCategoryId(''); return; } const exists = categories.some((category) => category.id === createCategoryId); if (!exists) { setCreateCategoryId(categories[0].id); } }, [categories, createCategoryId]); const handleToggle = useCallback((enabled: boolean) => { setToggling(true); settingsApi.update({ forumEnabled: enabled }) .then(() => setIsEnabled(enabled)) .catch(() => {}) .finally(() => setToggling(false)); }, []); const filteredThreads = useMemo(() => { if (!search.trim()) return threads; const q = search.toLowerCase(); return threads.filter((t) => t.title.toLowerCase().includes(q) || t.authorName.toLowerCase().includes(q)); }, [threads, search]); const selectedThreadReplies = useMemo(() => { if (!selectedThreadId) return []; return replies.filter((r) => r.threadId === selectedThreadId); }, [replies, selectedThreadId]); const deleteThread = useCallback((id: string) => { forumApi.deleteThread(id) .then(() => { setThreads((prev) => prev.filter((t) => t.id !== id)); setReplies((prev) => prev.filter((r) => r.threadId !== id)); if (selectedThreadId === id) setSelectedThreadId(null); }) .catch(() => { setError('Failed to delete thread.'); }); }, [selectedThreadId]); const togglePin = useCallback((id: string) => { const thread = threads.find((t) => t.id === id); if (!thread) return; forumApi.updateThread(id, { isPinned: !thread.isPinned }) .then((updated) => { setThreads((prev) => prev.map((t) => (t.id === id ? updated : t))); }) .catch(() => { setError('Failed to update pin state.'); }); }, [threads]); const toggleLock = useCallback((id: string) => { const thread = threads.find((t) => t.id === id); if (!thread) return; forumApi.updateThread(id, { isLocked: !thread.isLocked }) .then((updated) => { setThreads((prev) => prev.map((t) => (t.id === id ? updated : t))); }) .catch(() => { setError('Failed to update lock state.'); }); }, [threads]); const deleteReply = useCallback((id: string) => { const removedReply = replies.find((r) => r.id === id); forumApi.deleteReply(id) .then(() => { setReplies((prev) => prev.filter((r) => r.id !== id)); setThreads((prev) => prev.map((t) => { if (!removedReply || removedReply.threadId !== t.id) return t; return { ...t, replyCount: Math.max(0, t.replyCount - 1) }; })); }) .catch(() => { setError('Failed to delete reply.'); }); }, [replies]); const createThread = useCallback(() => { const title = createTitle.trim(); const content = createContent.trim(); if (!title || !content || !createCategoryId) { setError('Title, category and content are required.'); return; } setCreating(true); setError(''); forumApi.createThread({ title, content, categoryId: createCategoryId, }) .then((thread) => { setThreads((prev) => [thread, ...prev]); setCategories((prev) => prev.map((cat) => ( cat.id === createCategoryId ? { ...cat, threadCount: cat.threadCount + 1 } : cat ))); setCreateTitle(''); setCreateContent(''); setIsCreateModalOpen(false); }) .catch(() => { setError('Failed to create thread.'); }) .finally(() => { setCreating(false); }); }, [createCategoryId, createContent, createTitle]); const openCreateCategoryModal = useCallback(() => { setEditingCategoryId(null); setCategoryName(''); setCategoryDescription(''); setCategoryIcon('📁'); setIsCategoryModalOpen(true); }, []); const openEditCategoryModal = useCallback((category: ForumCategory) => { setEditingCategoryId(category.id); setCategoryName(category.name); setCategoryDescription(category.description); setCategoryIcon(category.icon || '📁'); setIsCategoryModalOpen(true); }, []); const saveCategory = useCallback(() => { const name = categoryName.trim(); const description = categoryDescription.trim(); const icon = categoryIcon.trim() || '📁'; if (!name || !description) { setError('Category name and description are required.'); return; } setSavingCategory(true); setError(''); const action = editingCategoryId ? forumApi.updateCategory(editingCategoryId, { name, description, icon }) : forumApi.createCategory({ name, description, icon }); action .then(() => loadModerationData()) .then(() => setIsCategoryModalOpen(false)) .catch(() => { setError(editingCategoryId ? 'Failed to update category.' : 'Failed to create category.'); }) .finally(() => { setSavingCategory(false); }); }, [categoryDescription, categoryIcon, categoryName, editingCategoryId, loadModerationData]); const removeCategory = useCallback((id: string) => { const confirmed = window.confirm('Delete this category? This can fail if it still has threads.'); if (!confirmed) return; setError(''); forumApi.deleteCategory(id) .then(() => loadModerationData()) .catch(() => { setError('Failed to delete category. Remove or move threads first.'); }); }, [loadModerationData]); const recentReplies = useMemo(() => { return [...replies].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()).slice(0, 20); }, [replies]); return (
{!isEnabled ? (
INTRANET / MODERATION

FUNCTIONALITY DISABLED

Forum Moderation feature is currently disabled

) : (
{loading && (
Loading moderation data...
)} {error && (
{error}
)}
INTRANET / MODERATION

FORUM MODERATION

{threads.length} threads — {replies.length} replies

{/* Tabs */}
{(['threads', 'replies', 'categories'] as const).map((tab) => ( ))}
{activeTab === 'threads' && (
{/* Thread list */}
{categories.length === 0 && (
No categories available.
)}
setSearch(e.target.value)} style={{ marginBottom: '1rem', maxWidth: '300px' }} />
{filteredThreads.map((thread) => (
{thread.isPinned && Pinned} {thread.isLocked && Locked}
{thread.title}
by {thread.authorName} — {thread.categoryName} — {thread.replyCount} replies
))} {filteredThreads.length === 0 && (
No threads found.
)}
{/* Thread replies panel */} {selectedThreadId && (
REPLIES ({selectedThreadReplies.length})
{selectedThreadReplies.length === 0 ? (
No replies.
) : (
{selectedThreadReplies.map((reply) => (
{reply.authorName}
{reply.content.slice(0, 150)}{reply.content.length > 150 ? '...' : ''}
))}
)}
)}
)} {activeTab === 'replies' && (
{recentReplies.map((reply) => { const thread = threads.find((t) => t.id === reply.threadId); return (
by {reply.authorName} {thread && <> in {thread.title}} {' '}— {formatDateTime(reply.createdAt)}
{reply.content}
); })} {recentReplies.length === 0 && (
No replies found.
)}
)} {activeTab === 'categories' && (
{categories.map((category) => (
{category.icon} {category.name}
{category.threadCount} threads
{category.description}
))} {categories.length === 0 && (
No categories found.
)}
)} {isCreateModalOpen && (
CREATE THREAD
setCreateTitle(e.target.value)} />