diff --git a/src/pages/AdminDashboard.jsx b/src/pages/AdminDashboard.jsx index c0f572a..e879125 100644 --- a/src/pages/AdminDashboard.jsx +++ b/src/pages/AdminDashboard.jsx @@ -1,251 +1,902 @@ import { useState, useEffect, useCallback } from 'react' -import { motion } from 'framer-motion' -import { Shield, RefreshCw, Clock, CheckCircle, XCircle, AlertTriangle, Inbox } from 'lucide-react' -import MetricsCards from '@/components/admin/MetricsCards' -import ModerationTable from '@/components/admin/ModerationTable' -import BulkActions from '@/components/admin/BulkActions' -import Modal from '@/components/ui/Modal' -import Button from '@/components/ui/Button' -import { useAdmin } from '@/hooks/useAdmin' -import { useAnswers } from '@/hooks/useAnswers' +import { motion, AnimatePresence } from 'framer-motion' +import { + Shield, RefreshCw, Clock, CheckCircle, XCircle, AlertTriangle, Inbox, + Settings, BarChart2, FileText, Ban, Plus, Trash2, Eye, Info, Check, + AlertOctagon, HelpCircle, ArrowUpRight, Search, ShieldAlert, UserMinus, ShieldCheck +} from 'lucide-react' +import { useAuth } from '@/hooks/useAuth' import { useToast } from '@/components/ui/Toast' - -const filterTabs = [ - { id: 'all', label: 'All', icon: Inbox }, - { id: 'pending', label: 'Pending', icon: Clock }, - { id: 'verified', label: 'Verified', icon: CheckCircle }, - { id: 'rejected', label: 'Rejected', icon: XCircle }, - { id: 'spam', label: 'Spam', icon: AlertTriangle }, +import { spamApi } from '@/lib/spamApi' +import Button from '@/components/ui/Button' +import Modal from '@/components/ui/Modal' +import Input from '@/components/ui/Input' +const tabs = [ + { id: 'queue', label: 'Moderation Queue', icon: Inbox }, + { id: 'rules', label: 'Rule Management', icon: Settings }, + { id: 'analytics', label: 'Analytics Console', icon: BarChart2 }, + { id: 'audit', label: 'Audit Logs', icon: FileText }, ] - export default function AdminDashboard() { - const { metrics, allAnswers, loading, fetchMetrics, fetchAllAnswers, bulkVerify, bulkDelete, bulkMarkSpam } = useAdmin() - const { verifyAnswer, rejectAnswer, markSpam, deleteAnswer } = useAnswers() + const { user, isAdmin } = useAuth() const { showToast } = useToast() - const [activeFilter, setActiveFilter] = useState('all') - const [selectedIds, setSelectedIds] = useState([]) - const [adminNoteModal, setAdminNoteModal] = useState({ open: false, answerId: null, action: null }) + + const [activeTab, setActiveTab] = useState('queue') + const [loading, setLoading] = useState(true) + + // Metrics & Data States + const [metrics, setMetrics] = useState({ + totalScanned: 0, + safeContent: 0, + suspiciousContent: 0, + spamBlocked: 0, + criticalSpam: 0, + moderatorActions: 0, + accuracyRate: 98.6 + }) + const [queue, setQueue] = useState([]) + const [rules, setRules] = useState([]) + const [keywords, setKeywords] = useState([]) + const [blacklist, setBlacklist] = useState([]) + const [whitelist, setWhitelist] = useState([]) + const [auditLogs, setAuditLogs] = useState([]) + const [analyticsData, setAnalyticsData] = useState(null) + const [settings, setSettings] = useState({ threshold_needs_review: 30, threshold_spam: 60, threshold_critical: 100 }) + // Filters & Interactivity + const [queueFilter, setQueueFilter] = useState({ status: 'pending', search: '', riskLevel: 'all' }) + const [ruleManagerTab, setRuleManagerTab] = useState('keywords') + const [newItemText, setNewItemText] = useState('') + const [selectedQueueItem, setSelectedQueueItem] = useState(null) const [adminNote, setAdminNote] = useState('') - + const [showConfirmModal, setShowConfirmModal] = useState({ open: false, type: '', target: null }) + const loadAllData = useCallback(async () => { + setLoading(true) + try { + const [ + fetchedMetrics, + fetchedQueue, + fetchedRules, + fetchedKeywords, + fetchedBlacklist, + fetchedWhitelist, + fetchedAuditLogs, + fetchedAnalytics, + fetchedSettings + ] = await Promise.all([ + spamApi.getModerationMetrics(), + spamApi.getModerationQueue(), + spamApi.getRules(), + spamApi.getKeywords(), + spamApi.getBlacklist(), + spamApi.getWhitelist(), + spamApi.getAuditLogs(), + spamApi.getAnalyticsData(), + spamApi.getSettings() + ]) + setMetrics(fetchedMetrics) + setQueue(fetchedQueue) + setRules(fetchedRules) + setKeywords(fetchedKeywords) + setBlacklist(fetchedBlacklist) + setWhitelist(fetchedWhitelist) + setAuditLogs(fetchedAuditLogs) + setAnalyticsData(fetchedAnalytics) + setSettings(fetchedSettings) + } catch (err) { + console.error('Error loading admin dashboard data:', err) + showToast('Error loading moderation data', 'error') + } finally { + setLoading(false) + } + }, [showToast]) useEffect(() => { - fetchMetrics() - fetchAllAnswers(activeFilter) - }, [fetchMetrics, fetchAllAnswers, activeFilter]) - + if (isAdmin) { + loadAllData() + } + }, [isAdmin, loadAllData]) const handleRefresh = () => { - fetchMetrics() - fetchAllAnswers(activeFilter) - setSelectedIds([]) + loadAllData() + showToast('Data refreshed successfully', 'success') } - - const handleVerify = useCallback((answerId) => { - setAdminNoteModal({ open: true, answerId, action: 'verify' }) - }, []) - - const handleReject = useCallback((answerId) => { - setAdminNoteModal({ open: true, answerId, action: 'reject' }) - }, []) - - const handleNoteSubmit = async () => { + // QUEUE ACTIONS + const handleQueueAction = async (itemId, action, note = '') => { try { - if (adminNoteModal.action === 'verify') { - await verifyAnswer(adminNoteModal.answerId, adminNote) - showToast('Answer verified!', 'success') - } else if (adminNoteModal.action === 'reject') { - await rejectAnswer(adminNoteModal.answerId, adminNote) - showToast('Answer rejected', 'info') + const modId = user?.id || 'simulated_moderator' + if (action === 'approve') { + await spamApi.approveQueueItem(itemId, modId, note) + showToast('Submission approved and published', 'success') + } else if (action === 'reject') { + await spamApi.rejectQueueItem(itemId, modId, note) + showToast('Submission rejected', 'info') + } else if (action === 'escalate') { + await spamApi.escalateQueueItem(itemId, modId, note) + showToast('Escalated to upper administrator', 'info') + } else if (action === 'ban') { + const item = queue.find(q => q.id === itemId) + if (item && item.user_id) { + await spamApi.banUser(item.user_id, modId, note || 'Spam campaign violation') + await spamApi.rejectQueueItem(itemId, modId, 'Account suspended due to spamming') + showToast('User restricted and content rejected', 'error') + } else { + showToast('No user linked to ban', 'warning') + } } - setAdminNoteModal({ open: false, answerId: null, action: null }) + setSelectedQueueItem(null) setAdminNote('') - handleRefresh() + loadAllData() } catch (err) { - showToast(`Failed to ${adminNoteModal.action}`, 'error') + showToast(`Failed to perform action: ${err.message}`, 'error') } } - - const handleSpam = async (answerId) => { + // RULE UPDATES + const handleRuleToggle = async (ruleId, currentVal) => { try { - await markSpam(answerId) - showToast('Marked as spam', 'info') - handleRefresh() + await spamApi.updateRule(ruleId, { is_enabled: !currentVal }) + showToast('Rule status updated', 'success') + loadAllData() } catch (err) { - showToast('Failed to mark spam', 'error') + showToast('Failed to update rule status', 'error') } } - - const handleDelete = async (answerId) => { + const handleRuleWeightChange = async (ruleId, newWeight) => { + const weight = parseInt(newWeight, 10) + if (isNaN(weight) || weight < 0 || weight > 100) return try { - await deleteAnswer(answerId) - showToast('Answer deleted', 'info') - handleRefresh() + await spamApi.updateRule(ruleId, { weight }) + showToast('Rule weight updated', 'success') + loadAllData() } catch (err) { - showToast('Failed to delete', 'error') + showToast('Failed to update weight', 'error') } } - - const toggleSelect = (id) => { - setSelectedIds(prev => - prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id] - ) - } - - const selectAll = () => { - if (selectedIds.length === allAnswers.length) { - setSelectedIds([]) - } else { - setSelectedIds(allAnswers.map(a => a.id)) - } - } - - const handleBulkVerify = async () => { + // KEYWORD/LIST MANAGERS + const handleAddListItem = async () => { + if (!newItemText.trim()) return try { - await bulkVerify(selectedIds) - showToast(`${selectedIds.length} answers verified!`, 'success') - setSelectedIds([]) - handleRefresh() + const uId = user?.id || 'admin' + if (ruleManagerTab === 'keywords') { + await spamApi.addKeyword(newItemText, uId) + showToast('Promotional keyword added', 'success') + } else if (ruleManagerTab === 'blacklist') { + await spamApi.addBlacklist(newItemText, uId) + showToast('Domain added to blacklist', 'success') + } else if (ruleManagerTab === 'whitelist') { + await spamApi.addWhitelist(newItemText, uId) + showToast('Domain added to whitelist', 'success') + } + setNewItemText('') + loadAllData() } catch (err) { - showToast('Bulk verify failed', 'error') + showToast(err.message, 'error') } } - - const handleBulkDelete = async () => { + const handleDeleteListItem = async (id) => { try { - await bulkDelete(selectedIds) - showToast(`${selectedIds.length} answers deleted`, 'info') - setSelectedIds([]) - handleRefresh() + if (ruleManagerTab === 'keywords') { + await spamApi.deleteKeyword(id) + showToast('Keyword removed', 'info') + } else if (ruleManagerTab === 'blacklist') { + await spamApi.deleteBlacklist(id) + showToast('Blacklist domain removed', 'info') + } else if (ruleManagerTab === 'whitelist') { + await spamApi.deleteWhitelist(id) + showToast('Whitelist domain removed', 'info') + } + loadAllData() } catch (err) { - showToast('Bulk delete failed', 'error') + showToast('Failed to delete item', 'error') } } - - const handleBulkSpam = async () => { + const handleThresholdChange = async (key, val) => { + const intVal = parseInt(val, 10) + if (isNaN(intVal) || intVal < 0 || intVal > 100) return try { - await bulkMarkSpam(selectedIds) - showToast(`${selectedIds.length} answers marked as spam`, 'info') - setSelectedIds([]) - handleRefresh() + const updated = await spamApi.updateSettings({ [key]: intVal }) + setSettings(updated) + showToast('Thresholds updated successfully', 'success') + loadAllData() } catch (err) { - showToast('Bulk spam failed', 'error') + showToast('Failed to update threshold', 'error') } } - + if (!isAdmin) { + return ( +
+
+ +
+

Access Restricted

+

+ You must be logged in as an administrator to view the spam detection & moderation console. +

+
+ ) + } + // Filtered Queue + const filteredQueue = queue.filter(item => { + const statusMatch = queueFilter.status === 'all' || item.status === queueFilter.status + const riskMatch = queueFilter.riskLevel === 'all' || item.risk_level === queueFilter.riskLevel + const searchMatch = !queueFilter.search.trim() || + item.content_body.toLowerCase().includes(queueFilter.search.toLowerCase()) || + (item.users?.name || '').toLowerCase().includes(queueFilter.search.toLowerCase()) + return statusMatch && riskMatch && searchMatch + }) return ( - - {/* Header */} -
-
-
- -
-
-

Moderation Dashboard

-

Review and manage community content

+ {/* Upper Title Block */} +
+
+
+ + Security Hardened +
+

Enterprise Moderation Console

+

+ Automated spam filter adjustments, explainable scoring metrics, and audit tracking. +

+
+
+
-
- - {/* Metrics */} -
- + {/* Metrics Cards Dashboard Grid */} +
+ {[ + { label: 'Total Analyzed', val: metrics.totalScanned, icon: Shield, col: 'indigo' }, + { label: 'Spam Blocked', val: metrics.spamBlocked + metrics.criticalSpam, icon: AlertOctagon, col: 'red' }, + { label: 'Pending Moderation', val: queue.filter(q => q.status === 'pending').length, icon: Clock, col: 'amber' }, + { label: 'Filter Accuracy', val: `${metrics.accuracyRate}%`, icon: CheckCircle, col: 'emerald' }, + ].map((c, i) => { + const Icon = c.icon + return ( + +
+ {c.label} + {c.val} +
+
+ +
+
+ ) + })}
- - {/* Filter Tabs */} -
- {filterTabs.map((tab) => { + {/* Primary Tab Navigation */} +
+ {tabs.map(tab => { const Icon = tab.icon - const count = tab.id === 'all' ? allAnswers.length : - tab.id === 'pending' ? metrics.pendingReviews : - tab.id === 'verified' ? metrics.verifiedAnswers : - tab.id === 'rejected' ? metrics.flaggedContent : - metrics.spamRemoved - + const isActive = activeTab === tab.id return ( ) })}
- - {/* Bulk Actions */} - - - {/* Moderation Table */} -
- + {/* Tab Contents */} +
+ {loading ? ( +
+ +
+ ) : ( + + + {/* TAB 1: MODERATION QUEUE */} + {activeTab === 'queue' && ( +
+ {/* Search and Filters */} +
+
+ + setQueueFilter(prev => ({ ...prev, search: e.target.value }))} + className="w-full pl-10 pr-4 py-2.5 rounded-xl border border-slate-200/60 dark:border-slate-700/60 bg-white/80 dark:bg-slate-800/80 outline-none focus:ring-2 focus:ring-indigo-500 transition" + /> +
+
+ + +
+
+ {/* List / Table */} +
+ {filteredQueue.length === 0 ? ( +
+ +

Queue Empty

+

No items matching the selected filters were found.

+
+ ) : ( +
+ + + + + + + + + + + + {filteredQueue.map(item => { + const score = item.spam_score + const isCritical = item.risk_level === 'CRITICAL' + const isHigh = item.risk_level === 'HIGH' + const riskColor = isCritical ? 'red' : isHigh ? 'orange' : 'amber' + return ( + + + + + + + + ) + })} + +
Submission DetailsTypeScore / RiskStatusActions
+
+
+ {item.users?.name?.[0] || '?'} +
+
+
{item.users?.name || 'Anonymous User'}
+
{item.content_body}
+
+
+
+ + {item.content_type} + + +
+ + {item.risk_level} + + {score}/100 +
+
+ + {item.status} + + + +
+
+ )} +
+
+ )} + {/* TAB 2: RULE MANAGEMENT */} + {activeTab === 'rules' && ( +
+ {/* Left Column: Heuristic Rules Configuration (8 Cols) */} +
+
+

Heuristic Scanners

+

Configure weighting and activation states for the 15 scanning engines.

+
+
+ {rules.map(rule => ( +
+
+ handleRuleToggle(rule.id, rule.is_enabled)} + className="w-4.5 h-4.5 text-indigo-600 border-slate-300 rounded focus:ring-indigo-500 cursor-pointer" + /> +
+ + {rule.name} + + ID: {rule.id} +
+
+
+ Weight: + handleRuleWeightChange(rule.id, e.target.value)} + disabled={!rule.is_enabled} + className="w-14 px-2 py-1 text-center rounded border border-slate-200 dark:border-slate-700 bg-white/50 dark:bg-slate-900/50 outline-none text-sm font-semibold focus:ring-1 focus:ring-indigo-500 disabled:opacity-50" + /> +
+
+ ))} +
+
+ {/* Right Column: Custom Libraries/Blacklists (5 Cols) */} +
+
+ {/* Sub Tabs */} +
+ {[ + { id: 'keywords', label: 'Keywords' }, + { id: 'blacklist', label: 'Blacklist' }, + { id: 'whitelist', label: 'Whitelist' }, + ].map(sub => ( + + ))} +
+ {/* Add Item form */} +
+ setNewItemText(e.target.value)} + className="flex-1 px-3 py-2 text-xs rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 outline-none focus:ring-1 focus:ring-indigo-500" + /> + +
+ {/* Scrollable List */} +
+ {(() => { + const items = ruleManagerTab === 'keywords' ? keywords : + ruleManagerTab === 'blacklist' ? blacklist : whitelist + if (items.length === 0) { + return
No entries listed.
+ } + return items.map(item => ( +
+ + {ruleManagerTab === 'keywords' ? item.keyword : item.domain} + + +
+ )) + })()} +
+
+ {/* Scoring Threshold Settings Card */} +
+
+

Scoring Thresholds

+

Set the sensitivity thresholds for content categorization.

+
+ +
+
+ + handleThresholdChange('threshold_needs_review', e.target.value)} + className="w-16 px-2 py-1 text-center rounded border border-slate-200 dark:border-slate-700 bg-white/50 dark:bg-slate-900/50 outline-none text-sm font-semibold focus:ring-1 focus:ring-indigo-500" + /> +
+
+ + handleThresholdChange('threshold_spam', e.target.value)} + className="w-16 px-2 py-1 text-center rounded border border-slate-200 dark:border-slate-700 bg-white/50 dark:bg-slate-900/50 outline-none text-sm font-semibold focus:ring-1 focus:ring-indigo-500" + /> +
+
+ + handleThresholdChange('threshold_critical', e.target.value)} + className="w-16 px-2 py-1 text-center rounded border border-slate-200 dark:border-slate-700 bg-white/50 dark:bg-slate-900/50 outline-none text-sm font-semibold focus:ring-1 focus:ring-indigo-500" + /> +
+
+
+
+
+ )} + {/* TAB 3: ANALYTICS CONSOLE */} + {activeTab === 'analytics' && analyticsData && ( +
+ {/* Daily Spam Volume & Rules Grid */} +
+ {/* SVG Volume Area Chart (7 Cols) */} +
+
+

Spam Activity Trends

+

Visual logs of daily safe, suspicious, and blocked content over past 7 days.

+
+ {/* Area Chart Drawing */} +
+ + {/* Grid Lines */} + + + + {/* Generate Paths */} + {(() => { + const data = analyticsData.dailyVolume + const maxVal = Math.max(...data.map(d => d.safe + d.spam + d.suspicious), 10) + + const getCoords = (type) => { + return data.map((d, i) => { + const x = (i / 6) * 500 + const y = 200 - ((d[type] / maxVal) * 160 + 20) + return `${x},${y}` + }).join(' ') + } + return ( + <> + {/* Safe Area */} + + {/* Spam Area */} + + {/* Suspicious Area */} + + + ) + })()} + + {/* Legend */} +
+ Safe + Suspicious + Spam Blocked +
+
+
+ {/* Top Triggered Rules Horizontal Bar Chart (5 Cols) */} +
+
+

Top Triggered Scanners

+

Heuristics triggered most frequently during filtering.

+
+
+ {analyticsData.detectionBreakdown.length === 0 ? ( +
No rule triggers logged yet.
+ ) : ( + analyticsData.detectionBreakdown.map((rule, idx) => ( +
+
+ {rule.name} + {rule.count} scans +
+
+ r.count), 1)) * 100, 100)}%` }} + className="h-full bg-indigo-500 rounded-full" + transition={{ duration: 0.5, delay: idx * 0.1 }} + /> +
+
+ )) + )} +
+
+
+ {/* User Risk Ranking (12 Cols) */} +
+
+

Top User Risk Profiles

+

Flagged contributors ranked by spam-to-submission ratio.

+
+ {analyticsData.userRiskScores.length === 0 ? ( +
No user risk scores registered yet.
+ ) : ( +
+ + + + + + + + + + + {analyticsData.userRiskScores.map((u, i) => ( + + + + + + + ))} + +
UserSpam RatioRisk ScoreRisk Assessment
+
{u.name}
+
{u.email}
+
{u.spamRatio}{u.riskScore}% + + {u.riskLevel} + +
+
+ )} +
+
+ )} + {/* TAB 4: SYSTEM AUDIT LOGS */} + {activeTab === 'audit' && ( +
+ {auditLogs.length === 0 ? ( +
+ +

No Logs Found

+

Audit trail logs will display here as actions are performed.

+
+ ) : ( +
+ + + + + + + + + + + {auditLogs.map(log => ( + + + + + + + ))} + +
ExecutorActionDetailsTimestamp
+ {log.users?.name || 'Administrator'} + + + {log.action} + + + {JSON.stringify(log.details)} + + {new Date(log.created_at).toLocaleString()} +
+
+ )} +
+ )} +
+
+ )}
- - {/* Admin Note Modal */} + {/* DETAIL MODAL FOR QUEUE REVIEW (EXPLAINABLE SPAM REPORT) */} { setAdminNoteModal({ open: false, answerId: null, action: null }); setAdminNote('') }} - title={adminNoteModal.action === 'verify' ? 'Verify Answer' : 'Reject Answer'} - size="sm" + isOpen={!!selectedQueueItem} + onClose={() => { setSelectedQueueItem(null); setAdminNote('') }} + title="Spam Investigation Center" + size="lg" > -
-
- -