From 897adc7299b6811558235f594f14c041974ac7ce Mon Sep 17 00:00:00 2001 From: Ben Oluoch Date: Thu, 9 Oct 2025 21:59:33 +0300 Subject: [PATCH 1/2] Updated admin panel and environments --- .../src/components/EnhancedAdminPanel.jsx | 2025 ++++------------- frontend/src/services/api.js | 4 + 2 files changed, 451 insertions(+), 1578 deletions(-) diff --git a/frontend/src/components/EnhancedAdminPanel.jsx b/frontend/src/components/EnhancedAdminPanel.jsx index afa40c9..0d2a149 100644 --- a/frontend/src/components/EnhancedAdminPanel.jsx +++ b/frontend/src/components/EnhancedAdminPanel.jsx @@ -1,1653 +1,522 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from "react"; +import { useNavigate } from "react-router-dom"; import { - Users, - MessageSquare, - Flag, - Activity, - TrendingUp, - Eye, - EyeOff, - Trash2, - CheckCircle, - Search, - Filter, - MoreHorizontal, - FileText, - AlertTriangle, + Award, + ChevronRight, Clock, - UserCheck, - UserX, - Edit, + Eye, + Loader2, + MessageSquare, Plus, - Download, - Calendar, -} from 'lucide-react'; -import api, { problemsApi } from '../services/api'; -import { Button } from './ui/button'; -import { Card, CardContent, CardHeader, CardTitle } from './ui/card'; -import { Badge } from './ui/badge'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs'; + Search, + Sparkles, + Tag, + ThumbsUp, + TrendingUp, + Users, +} from "lucide-react"; + +import { adminApi } from "../services/api"; +import { Badge } from "./ui/badge"; +import { Button } from "./ui/button"; import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from './ui/table'; + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "./ui/card"; +import { Input } from "./ui/input"; +import { Progress } from "./ui/progress"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from './ui/select'; -import { Input } from './ui/input'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from './ui/dialog'; -import { Textarea } from './ui/textarea'; -import { Label } from './ui/label'; -import { Separator } from './ui/separator'; -import { Progress } from './ui/progress'; -import { Avatar, AvatarFallback } from './ui/avatar'; - -const TIME_RANGE_LABELS = { - '1d': 'Last 24 hours', - '7d': 'Last 7 days', - '30d': 'Last 30 days', - '90d': 'Last 90 days', -}; - -function getStatusBadgeClasses(status) { - switch (status) { - case 'pending': - return 'border-amber-200 bg-amber-100 text-amber-700'; - case 'resolved': - return 'border-emerald-200 bg-emerald-100 text-emerald-700'; - case 'dismissed': - return 'border-slate-200 bg-slate-100 text-slate-600'; - default: - return 'border-slate-200 bg-slate-100 text-slate-600'; - } +} from "./ui/select"; +import { Separator } from "./ui/separator"; +import { Tabs, TabsList, TabsTrigger } from "./ui/tabs"; + +function formatShortNumber(value) { + const formatter = new Intl.NumberFormat("en", { + notation: "compact", + maximumFractionDigits: 1, + }); + return formatter.format(value || 0); } -function getPriorityBadgeClasses(priority) { - switch (priority) { - case 'high': - return 'border-red-200 bg-red-100 text-red-600'; - case 'medium': - return 'border-amber-200 bg-amber-100 text-amber-700'; - case 'low': - return 'border-emerald-200 bg-emerald-100 text-emerald-700'; - default: - return 'border-slate-200 bg-slate-100 text-slate-600'; - } +function formatPercent(value) { + if (value == null || Number.isNaN(value)) return "0%"; + return `${Math.round(value)}%`; } -function formatDateTime(value) { +function formatDate(value) { + if (!value) return "—"; const date = new Date(value); - if (Number.isNaN(date.getTime())) { - return '-'; - } - return date.toLocaleString('en-US', { - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', + if (Number.isNaN(date.getTime())) return "—"; + return date.toLocaleDateString("en", { + month: "short", + day: "numeric", + year: "numeric", }); } function formatTimeAgo(value) { + if (!value) return "—"; const date = new Date(value); - if (Number.isNaN(date.getTime())) { - return ''; - } - const diffMinutes = Math.round((Date.now() - date.getTime()) / 60000); - if (diffMinutes < 1) { - return 'just now'; - } - if (diffMinutes < 60) { - return `${diffMinutes}m ago`; - } - if (diffMinutes < 1440) { - return `${Math.floor(diffMinutes / 60)}h ago`; - } - return `${Math.floor(diffMinutes / 1440)}d ago`; -} - -function formatCategoryLabel(value) { - if (!value) { - return 'General'; - } - return value.charAt(0).toUpperCase() + value.slice(1); + if (Number.isNaN(date.getTime())) return "—"; + const diff = Date.now() - date.getTime(); + const minutes = diff / 60000; + if (minutes < 1) return "just now"; + if (minutes < 60) return `${Math.floor(minutes)}m ago`; + const hours = minutes / 60; + if (hours < 24) return `${Math.floor(hours)}h ago`; + const days = hours / 24; + if (days < 7) return `${Math.floor(days)}d ago`; + const weeks = days / 7; + if (weeks < 4) return `${Math.floor(weeks)}w ago`; + const months = days / 30; + if (months < 12) return `${Math.floor(months)}mo ago`; + return `${Math.floor(days / 365)}y ago`; } -function safeParseDate(value) { - if (!value) { - return null; - } - const date = new Date(value); - return Number.isNaN(date.getTime()) ? null : date; -} - -function formatDuration(minutes) { - if (!minutes || !Number.isFinite(minutes) || minutes <= 0) { - return '—'; - } - const totalMinutes = Math.round(minutes); - const hours = Math.floor(totalMinutes / 60); - const mins = totalMinutes % 60; - if (hours && mins) { - return `${hours}h ${mins}m`; - } - if (hours) { - return `${hours}h`; - } - return `${mins}m`; -} - -function formatActionLabel(value) { - if (!value) { - return ''; - } - return value - .toString() - .replace(/[_-]+/g, ' ') - .trim() - .replace(/\b\w/g, (char) => char.toUpperCase()); +function normalizeQuestion(question) { + const tags = Array.isArray(question?.tags) ? question.tags : []; + return { + id: question.id, + title: question.title ?? "(untitled)", + description: question.description ?? question.body ?? "", + created_at: question.created_at ?? question.timestamp, + updated_at: question.updated_at ?? question.timestamp, + tags, + problem_type: question.problem_type, + solutions_count: question.solutions_count ?? 0, + follows_count: question.follows_count ?? question.followers_count ?? 0, + view_count: question.view_count ?? question.views ?? 0, + vote_count: question.vote_total ?? question.vote_count ?? 0, + authorName: question.authorName ?? question.author?.name ?? "Unknown", + authorId: question.authorId ?? question.user_id, + is_solved: Boolean(question.is_solved) || (question.solutions_count ?? 0) > 0, + bounty: question.bounty ?? 0, + is_featured: Boolean(question.is_featured), + is_following: Boolean(question.is_following), + }; } -function normalizeText(value) { - if (typeof value === 'string') { - return value.toLowerCase(); - } - if (value === null || value === undefined) { - return ''; - } - return String(value).toLowerCase(); -} +const METRIC_CARD_CONFIG = [ + { + key: "total_questions", + label: "Total Questions", + icon: Users, + accent: "text-emerald-600 bg-emerald-100", + }, + { + key: "answer_rate", + label: "Answer Rate", + icon: TrendingUp, + accent: "text-rose-600 bg-rose-100", + isPercent: true, + showProgress: true, + }, + { + key: "total_views", + label: "Total Views", + icon: Eye, + accent: "text-indigo-600 bg-indigo-100", + }, + { + key: "total_bounty", + label: "Total Bounty", + icon: Sparkles, + accent: "text-amber-600 bg-amber-100", + }, +]; + +const SORT_OPTIONS = [ + { value: "newest", label: "Newest" }, + { value: "popular", label: "Most Popular" }, + { value: "answers", label: "Most Answers" }, + { value: "bounty", label: "Highest Bounty" }, +]; export function EnhancedAdminPanel({ currentUser }) { - const [activeSection, setActiveSection] = useState('overview'); - const [reports, setReports] = useState([]); - const [auditLogs, setAuditLogs] = useState([]); - const [stats, setStats] = useState(null); - const [adminUsers, setAdminUsers] = useState([]); - const [faqs, setFaqs] = useState([]); + const navigate = useNavigate(); + const [dashboard, setDashboard] = useState(null); + const [questions, setQuestions] = useState([]); + const [activeTab, setActiveTab] = useState("all"); + const [searchTerm, setSearchTerm] = useState(""); + const [sortBy, setSortBy] = useState("newest"); const [loading, setLoading] = useState(true); - const [error, setError] = useState(''); - const [selectedTimeRange, setSelectedTimeRange] = useState('7d'); - const [searchTerm, setSearchTerm] = useState(''); - const [selectedReportStatus, setSelectedReportStatus] = useState('all'); - const [showCreateFAQDialog, setShowCreateFAQDialog] = useState(false); - const [newFAQ, setNewFAQ] = useState({ - question: '', - answer: '', - category: 'moderation', - }); - const [trendingTopics, setTrendingTopics] = useState([]); - - useEffect(() => { - let active = true; - - (async () => { - try { - setLoading(true); - setError(''); - - const [statsData, reportsData, auditData, usersData, faqsData] = await Promise.all([ - api.get('/admin/stats').then((res) => res.data), - api.get('/admin/reports').then((res) => res.data), - api.get('/admin/audit').then((res) => res.data), - api - .get('/admin/users', { params: { page: 1, per_page: 10 } }) - .then((res) => res.data?.users ?? []), - api.get('/faqs').then((res) => res.data?.faqs ?? []), - ]); - - if (!active) return; + const [error, setError] = useState(""); - setStats(statsData || null); - const reportsList = Array.isArray(reportsData) - ? reportsData - : reportsData?.reports ?? []; - setReports(reportsList); - setAuditLogs(Array.isArray(auditData) ? auditData : auditData?.logs ?? []); - setAdminUsers(Array.isArray(usersData) ? usersData : []); - setFaqs(Array.isArray(faqsData) ? faqsData : []); - } catch (err) { - if (!active) return; - console.error('Failed to load admin data', err); - setError(err.response?.data?.error || err.message || 'Failed to load admin data'); - } finally { - if (active) { - setLoading(false); - } - } - })(); - - return () => { - active = false; - }; - }, []); + const loadDashboard = async () => { + try { + setLoading(true); + setError(""); + const data = await adminApi.dashboard(); + setDashboard(data); + const list = Array.isArray(data?.questions) ? data.questions : []; + setQuestions(list.map(normalizeQuestion)); + } catch (err) { + console.error("Failed to load admin dashboard", err); + setError(err?.response?.data?.error || err.message || "Failed to load dashboard"); + } finally { + setLoading(false); + } + }; useEffect(() => { - let active = true; - - (async () => { - try { - const data = await problemsApi.list({ page: 1, per_page: 50 }); - if (!active) { - return; - } - - const source = Array.isArray(data?.questions) - ? data.questions - : Array.isArray(data?.items) - ? data.items - : []; - - const tagCounts = new Map(); - source.forEach((item) => { - const tagList = Array.isArray(item?.tags) ? item.tags : []; - tagList.forEach((tag) => { - const value = - typeof tag === 'string' - ? tag.trim().toLowerCase() - : typeof tag?.name === 'string' - ? tag.name.trim().toLowerCase() - : ''; - if (!value) { - return; - } - tagCounts.set(value, (tagCounts.get(value) || 0) + 1); - }); - }); - - const ranked = Array.from(tagCounts.entries()) - .sort((a, b) => b[1] - a[1]) - .slice(0, 5) - .map(([tag, count]) => ({ - tag, - count, - })); - - setTrendingTopics(ranked); - } catch (err) { - if (active) { - console.error('Failed to load trending topics', err); - setTrendingTopics([]); - } - } - })(); - - return () => { - active = false; - }; - }, []); - - const reloadReports = useCallback(async () => { - const data = await api.get('/admin/reports').then((res) => res.data); - const list = Array.isArray(data) ? data : data?.reports ?? []; - setReports(list); - return list; - }, []); - - const reloadAuditLogs = useCallback(async () => { - const data = await api.get('/admin/audit').then((res) => res.data); - const list = Array.isArray(data) ? data : data?.logs ?? []; - setAuditLogs(list); - return list; - }, []); - - const reloadFaqs = useCallback(async () => { - const data = await api.get('/faqs').then((res) => res.data?.faqs ?? []); - const list = Array.isArray(data) ? data : []; - setFaqs(list); - return list; + loadDashboard(); }, []); - const numberFormatter = useMemo(() => new Intl.NumberFormat('en-US'), []); - - const moderationStats = useMemo(() => { - const totalFromStats = (stats?.pendingReports ?? 0) + (stats?.resolvedReports ?? 0); - const totalReports = reports.length || totalFromStats; - const pending = - reports.filter((report) => normalizeText(report?.status) === 'pending').length || - stats?.pendingReports || - 0; - const resolved = - reports.filter((report) => normalizeText(report?.status) === 'resolved').length || - stats?.resolvedReports || - 0; - const dismissed = reports.filter((report) => normalizeText(report?.status) === 'dismissed').length; - const highPriority = reports.filter( - (report) => - String(report.priority).toLowerCase() === 'high' && normalizeText(report?.status) === 'pending' - ).length; - const resolutionRate = totalReports ? Math.round((resolved / totalReports) * 100) : 0; - - return { - totalReports, - pending, - resolved, - dismissed, - highPriority, - resolutionRate, - averageResponseTime: pending ? '—' : '—', + const metrics = dashboard?.metrics ?? {}; + const popularTags = dashboard?.popular_tags ?? []; + const topContributors = dashboard?.top_contributors ?? []; + const recentActivity = dashboard?.recent_activity ?? []; + + const counts = useMemo(() => { + const base = { + all: questions.length, + unanswered: questions.filter((q) => q.solutions_count === 0).length, + bounty: questions.filter((q) => (q.bounty || 0) > 0).length, + following: questions.filter((q) => q.follows_count > 0).length, + featured: questions.filter((q) => q.is_featured).length, }; - }, [reports, stats]); - - const moderationInsights = useMemo(() => { - const now = Date.now(); - const pendingList = reports.filter((report) => normalizeText(report?.status) === 'pending'); - const pendingLast24 = pendingList.filter((report) => { - const date = safeParseDate(report?.timestamp ?? report?.created_at); - return date ? now - date.getTime() <= 24 * 60 * 60 * 1000 : false; - }).length; - const pendingAges = pendingList - .map((report) => { - const date = safeParseDate(report?.timestamp ?? report?.created_at); - return date ? (now - date.getTime()) / 60000 : null; - }) - .filter((value) => value !== null); - const avgPendingMinutes = pendingAges.length - ? pendingAges.reduce((sum, value) => sum + value, 0) / pendingAges.length - : 0; - const resolvedLast7 = auditLogs.filter((log) => { - const action = normalizeText(log?.action); - if (!action || !action.includes('resolve')) { - return false; - } - const date = safeParseDate(log?.timestamp); - return date ? now - date.getTime() <= 7 * 24 * 60 * 60 * 1000 : false; - }).length; - const dismissedLast7 = auditLogs.filter((log) => { - const action = normalizeText(log?.action); - if (!action || !action.includes('dismiss')) { - return false; - } - const date = safeParseDate(log?.timestamp); - return date ? now - date.getTime() <= 7 * 24 * 60 * 60 * 1000 : false; - }).length; - - return [ - { - id: 'mi-queue', - title: 'Pending queue', - value: numberFormatter.format(moderationStats.pending), - change: `${numberFormatter.format(pendingLast24)} new today`, - tone: moderationStats.pending > 0 ? 'attention' : 'positive', - }, - { - id: 'mi-resolution', - title: 'Resolution rate', - value: `${moderationStats.resolutionRate}%`, - change: `${numberFormatter.format(resolvedLast7)} resolved past 7d`, - tone: moderationStats.resolutionRate >= 80 ? 'positive' : 'attention', - }, - { - id: 'mi-age', - title: 'Average pending age', - value: formatDuration(avgPendingMinutes), - change: dismissedLast7 - ? `${numberFormatter.format(dismissedLast7)} dismissed past 7d` - : 'Monitoring queue', - tone: avgPendingMinutes > 240 ? 'attention' : 'positive', - }, - ]; - }, [auditLogs, moderationStats, numberFormatter, reports]); - - const totalQuestions = stats?.totalQuestions ?? 0; - const totalAnswers = stats?.totalAnswers ?? 0; - const totalUsersCount = stats?.totalUsers ?? adminUsers.length; - const answerRate = totalQuestions ? Math.round((totalAnswers / totalQuestions) * 100) : 0; - const unansweredQuestions = Math.max(totalQuestions - totalAnswers, 0); - - const activeContributors = stats?.activeUsers ?? totalUsersCount; - - const statsCards = useMemo( - () => [ - { - id: 'stat-1', - icon: Users, - label: 'Active contributors', - value: numberFormatter.format(activeContributors), - change: 'Active this month', - accentClass: 'bg-emerald-500/10 text-emerald-500', - }, - { - id: 'stat-2', - icon: MessageSquare, - label: 'New questions', - value: numberFormatter.format(totalQuestions), - change: `${answerRate}% answered`, - accentClass: 'bg-sky-500/10 text-sky-600', - }, - { - id: 'stat-3', - icon: Flag, - label: 'Pending reports', - value: numberFormatter.format(moderationStats.pending), - change: `${numberFormatter.format(moderationStats.highPriority)} priority`, - accentClass: 'bg-amber-500/10 text-amber-600', - }, - { - id: 'stat-4', - icon: TrendingUp, - label: 'Resolution rate', - value: `${moderationStats.resolutionRate}%`, - change: 'Target ≥ 90%', - accentClass: 'bg-indigo-500/10 text-indigo-600', - }, - ], - [ - answerRate, - moderationStats.highPriority, - moderationStats.pending, - moderationStats.resolutionRate, - numberFormatter, - totalQuestions, - activeContributors, - ] - ); - - const recentReports = useMemo(() => { - return reports.slice(0, 5).map((report) => { - const status = normalizeText(report?.status) || 'unknown'; - const timestamp = report?.timestamp ?? report?.created_at; - const titleRaw = - report?.targetTitle ?? - report?.target ?? - `${formatCategoryLabel(normalizeText(report?.type) || 'content')} #${report?.target_id ?? ''}`; - const title = typeof titleRaw === 'string' ? titleRaw.trim() : String(titleRaw); - return { - id: report?.id ?? `${title}-${timestamp ?? status}`, - title: title || 'Untitled content', - status, - timestamp, - }; - }); - }, [reports]); - - const complianceChecks = useMemo(() => { - const nowISO = new Date().toISOString(); - const queueStatus = moderationStats.pending > 5 ? 'attention' : 'pass'; - const answerRateStatus = answerRate >= 65 ? 'pass' : 'attention'; - const highPriorityStatus = moderationStats.highPriority > 0 ? 'attention' : 'pass'; - - return [ - { - id: 'check-queue', - label: 'Moderation queue load', - status: queueStatus, - updatedAt: nowISO, - detail: `${numberFormatter.format(moderationStats.pending)} pending`, - }, - { - id: 'check-answers', - label: 'Community answer rate', - status: answerRateStatus, - updatedAt: nowISO, - detail: `${answerRate}% answered`, - }, - { - id: 'check-priority', - label: 'High priority flags', - status: highPriorityStatus, - updatedAt: nowISO, - detail: `${numberFormatter.format(moderationStats.highPriority)} escalated`, - }, - ]; - }, [answerRate, moderationStats, numberFormatter]); + return base; + }, [questions]); + + const filteredQuestions = useMemo(() => { + const term = searchTerm.trim().toLowerCase(); + let data = [...questions]; + + if (term) { + data = data.filter((q) => { + const haystack = [ + q.title, + q.description, + ...(Array.isArray(q.tags) ? q.tags : []), + q.authorName, + ] + .filter(Boolean) + .join(" ") + .toLowerCase(); + return haystack.includes(term); + }); + } - const automationSignals = useMemo(() => { - if (!auditLogs.length) { - return []; + if (activeTab === "unanswered") { + data = data.filter((q) => q.solutions_count === 0); + } else if (activeTab === "bounty") { + data = data.filter((q) => (q.bounty || 0) > 0); + } else if (activeTab === "following") { + data = data.filter((q) => q.follows_count > 0); + } else if (activeTab === "featured") { + data = data.filter((q) => q.is_featured); } - const seen = new Set(); - const entries = []; - auditLogs.forEach((log) => { - const action = log?.action; - if (!action || seen.has(action)) { - return; + data.sort((a, b) => { + if (sortBy === "popular") { + const scoreA = (a.view_count || 0) + (a.vote_count || 0) * 2; + const scoreB = (b.view_count || 0) + (b.vote_count || 0) * 2; + return scoreB - scoreA; } - seen.add(action); - entries.push({ - id: log?.id ?? action, - label: formatActionLabel(action), - status: normalizeText(action).includes('dismiss') ? 'paused' : 'active', - timestamp: log?.timestamp, - }); - }); - - return entries.slice(0, 4); - }, [auditLogs]); - - const filteredReports = useMemo(() => { - const search = searchTerm.trim().toLowerCase(); - return reports.filter((report) => { - const haystack = [ - report?.targetTitle, - report?.reason, - report?.description, - report?.reporterName, - report?.reporterEmail, - report?.type, - ].map(normalizeText); - const matchesSearch = !search || haystack.some((value) => value.includes(search)); - const status = normalizeText(report?.status); - const matchesStatus = selectedReportStatus === 'all' || status === selectedReportStatus; - return matchesSearch && matchesStatus; - }); - }, [reports, searchTerm, selectedReportStatus]); - - const timeRangeLabel = TIME_RANGE_LABELS[selectedTimeRange] ?? 'Last 7 days'; - const flaggedUsers = useMemo(() => { - const grouped = new Map(); - reports.forEach((report) => { - const displayName = report.targetTitle || `${report.type ?? report.target_type} #${report.target_id}`; - if (!grouped.has(displayName)) { - grouped.set(displayName, { - id: displayName, - name: displayName, - reason: report.reason, - reports: 0, - lastSeen: report.timestamp || report.created_at, - }); + if (sortBy === "answers") { + return (b.solutions_count || 0) - (a.solutions_count || 0); } - const entry = grouped.get(displayName); - entry.reports += 1; - entry.reason = report.reason; - entry.lastSeen = report.timestamp || report.created_at || entry.lastSeen; - }); - return Array.from(grouped.values()).slice(0, 5); - }, [reports]); - - const teamMembers = useMemo(() => { - if (!Array.isArray(adminUsers) || !adminUsers.length) { - return []; - } - const now = Date.now(); - return adminUsers.map((user) => { - const joinedAt = safeParseDate(user?.created_at); - const updatedAt = safeParseDate(user?.updated_at); - const isActive = updatedAt ? now - updatedAt.getTime() <= 12 * 60 * 60 * 1000 : false; - return { - id: user.id ?? user.email ?? `user-${Math.random().toString(36).slice(2)}`, - name: user.name || user.email || 'Team member', - roleLabel: formatCategoryLabel(normalizeText(user?.role) || 'member'), - email: user.email, - joinedAt, - lastActive: updatedAt, - status: isActive ? 'Active' : 'Away', - }; - }); - }, [adminUsers]); - - const topReporters = useMemo(() => { - const counts = new Map(); - reports.forEach((report) => { - const reporterId = report.reporterId ?? report.user_id ?? report.userId ?? report.userID; - const key = reporterId ?? report.reporterEmail ?? report.reporterName ?? report.user_id ?? report.id; - const name = report.reporterName || report.reporterEmail || (reporterId ? `User #${reporterId}` : 'Community member'); - if (!counts.has(key)) { - counts.set(key, { - id: key, - name, - reports: 0, - }); + if (sortBy === "bounty") { + return (b.bounty || 0) - (a.bounty || 0); } - const entry = counts.get(key); - entry.reports += 1; + const dateA = new Date(a.created_at || 0).getTime(); + const dateB = new Date(b.created_at || 0).getTime(); + return dateB - dateA; }); - return Array.from(counts.values()) - .sort((a, b) => b.reports - a.reports) - .slice(0, 3); - }, [reports]); - - const navigation = [ - { label: 'Overview', value: 'overview', icon: Activity }, - { label: 'Moderation Hub', value: 'moderation', icon: Flag }, - { label: 'Community', value: 'users', icon: Users }, - ]; - - const handleReportAction = async (reportId, action) => { - try { - const endpoint = - action === 'resolve' ? 'resolve' : action === 'dismiss' || action === 'remove' ? 'dismiss' : null; - if (!endpoint) return; - - await api.post(`/admin/reports/${reportId}/${endpoint}`); - await Promise.all([reloadReports(), reloadAuditLogs()]); - } catch (err) { - console.error('Failed to update report', err); - } - }; - const handleUserAction = (userId, action) => { - console.log(`Admin action: ${action} applied to user ${userId}`); - }; - - const handleCreateFAQ = async (event) => { - event.preventDefault(); - if (!newFAQ.question.trim() || !newFAQ.answer.trim()) { - return; - } - - try { - await api.post('/faqs', { - question: newFAQ.question.trim(), - answer: newFAQ.answer.trim(), - }); - await reloadFaqs(); - setNewFAQ({ question: '', answer: '', category: newFAQ.category }); - setShowCreateFAQDialog(false); - } catch (err) { - console.error('Failed to create FAQ', err); - } - }; - - const greeting = currentUser?.name - ? `Welcome back, ${currentUser.name}` - : 'Welcome back, Admin'; + return data; + }, [questions, searchTerm, activeTab, sortBy]); if (loading) { return ( -
-
- Loading admin data… -
+
+ + Loading admin dashboard…
); } if (error) { return ( -
-
- - - {error} - - -
+
+

Unable to load dashboard

+

{error}

+
); } return ( -
-
-
- - -
- - -
-
+
+
+
+
+
+
+ + Admin Dashboard +
+

+ Welcome back, {currentUser?.name ?? "Administrator"}! 👋 +

+

+ Discover answers, monitor community health, and keep the platform thriving with + real-time insights. +

+
+ +
+
+ +
+ {METRIC_CARD_CONFIG.map((config) => { + const rawValue = metrics[config.key] ?? 0; + const displayValue = config.isPercent + ? formatPercent(rawValue) + : formatShortNumber(rawValue); + return ( + + +
+ + +
-

- Admin Control -

-

{greeting}

-

- Monitor community health, triage urgent reports, and coordinate the moderation team from a single view. +

+ {config.label}

-
-
- - - +

{displayValue}

-
-
-

Pending queue

-

- {numberFormatter.format(moderationStats.pending)} -

-

- {moderationStats.highPriority} high priority items + {config.showProgress && ( +

+ +

+ {formatShortNumber(metrics.answered_questions ?? 0)} answered ·{" "} + {formatShortNumber(metrics.unanswered_questions ?? 0)} open

-
-

Resolution pace

-

- {moderationStats.resolutionRate}% -

-

- Target ≥ 90% -

+ )} + + + ); + })} +
+ +
+ +
+
+ + setSearchTerm(event.target.value)} + /> +
+
+ +
+
+ + + + All ({counts.all}) + + + Unanswered ({counts.unanswered}) + + + Bounty ({counts.bounty}) + + + Following ({counts.following}) + + + Featured ({counts.featured}) + + + +
+ + + {filteredQuestions.length === 0 ? ( + + No questions match the current filters. + + ) : ( + filteredQuestions.map((question) => ( + + +
+ {question.is_solved && ( + Solved + )} + {(question.bounty || 0) > 0 && ( + + {formatShortNumber(question.bounty)} bounty + + )} + + {formatDate(question.created_at)} +
-
-

Response time

-

- {moderationStats.averageResponseTime} -

-

- From first flag to human review +

+

{question.title}

+

+ {question.description}

-
-
- - - -
- {statsCards.map((card) => { - const Icon = card.icon; - return ( - - -
-

- {card.label} -

-

- {card.value} -

-

{card.change}

-
- - - -
-
- ); - })} -
- - -
- - Overview - Moderation - Community - -
- - -
-
- - -
-
- - - Moderation snapshot - - {timeRangeLabel} - - - -
-
-

Items awaiting review

-

- {numberFormatter.format(moderationStats.pending)} -

-
- - {moderationStats.highPriority} high priority - -
-
-
-
-

Resolution rate

-

- {moderationStats.resolutionRate}% -

-
- -
-
-
-

Pending workload

-

- {moderationStats.totalReports - ? `${Math.round((moderationStats.pending / moderationStats.totalReports) * 100)}%` - : '0%'} -

-
- -
-
- -
-
-

Resolved

-

- {numberFormatter.format(moderationStats.resolved)} -

-
-
-

Dismissed

-

- {numberFormatter.format(moderationStats.dismissed)} -

-
-
-

Avg response

-
- - {moderationStats.averageResponseTime} -
-
-
-
-
- - - - Admin activity - - Live feed +
+ {question.tags.slice(0, 6).map((tag) => ( + + {tag} - - - {auditLogs.map((activity) => ( -
-
-
-

- {activity.admin_name || 'Admin'} -

-

- {activity.action} -

-
- - {formatTimeAgo(activity.timestamp)} - -
-

- {activity.reason || activity.target} -

-
- ))} -
- - - - - Trending topics - - - Momentum - - - - {trendingTopics.length ? ( - trendingTopics.map((item) => ( -
- #{item.tag} - - {numberFormatter.format(item.count)} questions - -
- )) - ) : ( -

No topic insights available yet.

- )} -
-
-
- -
- - - Team availability -

Coverage across the moderation roster

-
- - {teamMembers.length ? ( - teamMembers.map((member) => ( -
-
- - - {member.name - .split(' ') - .map((part) => part?.[0]) - .filter(Boolean) - .join('')} - - -
-

{member.name}

-

{member.roleLabel}

-

{member.email}

-
-
-
- - - {member.status} - -

- Last active {formatTimeAgo(member.lastActive) || '—'} -

-

- Joined{' '} - {member.joinedAt - ? member.joinedAt.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - }) - : '—'} -

-
-
- )) - ) : ( -

- No admin teammates found yet. -

- )} -
-
- - - - Compliance checks -

Policy coverage and automation audits

-
- - {complianceChecks.length ? ( - complianceChecks.map((check) => ( -
-
-

{check.label}

-

{check.detail}

-

- Updated {formatTimeAgo(check.updatedAt) || 'recently'} -

-
- {check.status === 'pass' ? ( - - - On track - - ) : ( - - - Attention - - )} -
- )) - ) : ( -

No compliance signals yet.

- )} -
-
- - - - Moderation actions -

Latest admin interventions

-
- - {automationSignals.length ? ( - automationSignals.map((signal) => ( -
-
-

{signal.label}

-

- {formatTimeAgo(signal.timestamp) || 'recently'} -

-
- {signal.status === 'active' ? ( - Active - ) : ( - Paused - )} -
- )) - ) : ( -

No admin activity recorded yet.

- )} -
-
-
-
- - - - - -
- Moderation queue -

Prioritise reports with the most impact

-
- -
- -
-
-
- - setSearchTerm(event.target.value)} - placeholder="Search reports by keyword, reason, or content" - className="pl-9" - /> -
- -
-
- - -
+ ))}
- -
- - - - Content - Reason - Reporter - Priority - Status - Timestamp - Actions - - - - {filteredReports.map((report) => { - const status = normalizeText(report?.status) || 'unknown'; - const priority = normalizeText(report?.priority) || 'medium'; - const targetTitleRaw = report?.targetTitle ?? ''; - const targetTitle = String(targetTitleRaw).trim() || 'Untitled content'; - const reasonRaw = report?.reason ?? report?.description ?? ''; - const reason = String(reasonRaw).trim() || 'No reason provided'; - const reporterLabel = - report?.reporterName || report?.reporterEmail || 'Community member'; - const timestamp = report?.timestamp ?? report?.created_at; - const typeLabel = formatCategoryLabel(normalizeText(report?.type) || 'content'); - const reportId = report?.id; - const rowKey = reportId ?? `${targetTitle}-${timestamp ?? 'pending'}`; - - return ( - - -

- {targetTitle} -

-

- {typeLabel} -

-
- - {reason} - - - {reporterLabel} - - - - {formatCategoryLabel(priority)} - - - - - {formatCategoryLabel(status)} - - - - {formatDateTime(timestamp)} - - -
- {status === 'pending' && reportId ? ( - <> - - - - - ) : ( - - {status === 'unknown' ? '—' : formatTimeAgo(timestamp)} - - )} -
-
-
- ); - })} -
-
+
+ + + {formatShortNumber(question.vote_count)} votes + + + + {formatShortNumber(question.solutions_count)} answers + + + + {formatShortNumber(question.view_count)} views + + + + {question.authorName} +
+ )) + )} + + + + Showing {filteredQuestions.length} of {questions.length} questions + + + + +
-
- - - Moderation insights -

Operational performance signals

-
- - {moderationInsights.map((insight) => ( -
-

{insight.title}

-

- {insight.value} -

-

- {insight.change} -

-
- ))} -
-
- - - - Flagged users -

Accounts needing moderation follow up

-
- - {flaggedUsers.map((user) => ( -
-
-
-

{user.name}

-
- - {user.reason} -
-
- {user.reports} reports - Last seen {formatTimeAgo(user.lastSeen)} -
-
-
- - - -
-
-
- ))} -
-
-
-
- - -
-
- - - Community engagement -

Answer quality and participation metrics

-
- -
-
-

Answer rate

-

{answerRate}%

-
- -
-
-
-

Total questions

-

- {numberFormatter.format(totalQuestions)} -

-
-
-

Total answers

-

- {numberFormatter.format(totalAnswers)} -

-
-
-

Unanswered

-

- {numberFormatter.format(unansweredQuestions)} -

-
-
-
-
- - - - Top reporters -

Community members flagging content the most

-
- - {topReporters.length ? ( - topReporters.map((leader, index) => ( -
-
- #{index + 1} -
-

{leader.name}

-

- {numberFormatter.format(leader.reports)} reports -

-
-
- - {numberFormatter.format(leader.reports)} - -
- )) - ) : ( -

No reports submitted yet.

- )} -
-
-
- -
- - - -
- Knowledge base -

FAQs used by moderators

-
- - - -
- - {faqs.map((faq) => ( -
-
-
-

{faq.question}

-

{faq.answer}

-
-
- - {formatCategoryLabel(faq.category)} - - -
-
-
- ))} -
-
- - - - Create FAQ entry - -
-
- - - setNewFAQ((prev) => ({ ...prev, question: event.target.value })) - } - placeholder="When should we escalate to the on-call lead?" - required - /> -
-
- -