diff --git a/app/admin/reports/page.tsx b/app/admin/reports/page.tsx index 6f5594c..8043e00 100644 --- a/app/admin/reports/page.tsx +++ b/app/admin/reports/page.tsx @@ -1,632 +1,889 @@ "use client"; -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { AdminService } from '../../lib/database/AdminService'; -import { supabase } from '../../lib/supabaseClient'; -import { AlertTriangle, Clock, CheckCircle2, XCircle, Eye, Search, Filter, User, FileText, Calendar } from 'lucide-react'; +import { useAuth } from '../../context/AuthContext'; +import { + AlertTriangle, Clock, CheckCircle2, XCircle, Eye, Search, + User, FileText, ShieldAlert, ShieldX, ShieldCheck, Zap, + ChevronDown, BarChart2, +} from 'lucide-react'; import { toast, ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; -import Image from 'next/image'; import AdminLayout from '../../../components/admin/AdminLayout'; - +import { SEVERITY_DESCRIPTIONS, SEVERITY_STRIKE_VALUES, type Severity } from '../../lib/database/StrikeService'; interface ReportData { id: string; type: 'listing' | 'user'; reason: string; + severity: Severity; description?: string; status: 'pending' | 'resolved' | 'dismissed'; created_at: string; reporter_id: string; reported_listing_id?: string; reported_user_id?: string; + listing_user_id?: string; listing_title?: string; reported_user_name?: string; + listing_user_name?: string; reporter_name?: string; + // Populated after fetching strike data + targetUserStrikes?: number; + recommendedAction?: string; +} + +type ActionOption = 'warn' | 'temp_suspend' | 'ban' | 'dismiss'; +type SeverityFilter = 'all' | Severity; +type StatusFilter = 'all' | 'pending' | 'resolved' | 'dismissed'; +type TypeFilter = 'all' | 'listing' | 'user'; + +const SEVERITY_CONFIG: Record = { + low: { + label: 'Low', + color: 'text-yellow-700', + bg: 'bg-yellow-50', + border: 'border-yellow-300', + icon: , + }, + medium: { + label: 'Medium', + color: 'text-orange-700', + bg: 'bg-orange-50', + border: 'border-orange-300', + icon: , + }, + high: { + label: 'High', + color: 'text-red-700', + bg: 'bg-red-50', + border: 'border-red-300', + icon: , + }, +}; + +function getSeverityForReason(reason: string): Severity { + const meta = SEVERITY_DESCRIPTIONS[reason]; + return meta?.severity ?? 'low'; +} + +function getRecommendedAction(strikes: number, severity: Severity): ActionOption { + const newTotal = strikes + SEVERITY_STRIKE_VALUES[severity]; + if (severity === 'high') return newTotal >= 6 ? 'ban' : 'temp_suspend'; + if (newTotal >= 6) return 'ban'; + if (newTotal >= 3) return 'temp_suspend'; + if (newTotal >= 1) return 'warn'; + return 'dismiss'; +} + +function SeverityBadge({ severity }: { severity: Severity }) { + const cfg = SEVERITY_CONFIG[severity]; + return ( + + {cfg.icon} + {cfg.label} + + ); +} + +function StrikeBadge({ count }: { count: number }) { + const color = + count === 0 ? 'bg-gray-100 text-gray-500' : + count <= 2 ? 'bg-yellow-100 text-yellow-700' : + count <= 5 ? 'bg-orange-100 text-orange-700' : + 'bg-red-100 text-red-700'; + return ( + + + {count} strike{count !== 1 ? 's' : ''} + + ); +} + +function RecommendedActionBadge({ action }: { action: ActionOption }) { + const map: Record = { + warn: { label: 'Warn', color: 'bg-blue-100 text-blue-700' }, + temp_suspend: { label: 'Temp Suspend', color: 'bg-orange-100 text-orange-700' }, + ban: { label: 'Permanent Ban', color: 'bg-red-100 text-red-700' }, + dismiss: { label: 'Dismiss', color: 'bg-gray-100 text-gray-500' }, + }; + const { label, color } = map[action]; + return ( + + {label} + + ); +} + +function StatusBadge({ status }: { status: string }) { + switch (status) { + case 'pending': + return ( + + Pending + + ); + case 'resolved': + return ( + + Resolved + + ); + case 'dismissed': + return ( + + Dismissed + + ); + default: + return null; + } +} + +interface ActionModalProps { + report: ReportData; + onClose: () => void; + onActionTaken: () => void; + adminId: string; +} + +function ActionModal({ report, onClose, onActionTaken, adminId }: ActionModalProps) { + const [action, setAction] = useState( + report.recommendedAction as ActionOption ?? 'warn' + ); + const [suspensionDays, setSuspensionDays] = useState(7); + const [notes, setNotes] = useState(''); + const [loading, setLoading] = useState(false); + + const severityCfg = SEVERITY_CONFIG[report.severity]; + const meta = SEVERITY_DESCRIPTIONS[report.reason]; + + async function handleSubmit() { + setLoading(true); + try { + const res = await fetch('/api/admin/take-action', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + reportId: report.id, + reportType: report.type, + adminId, + action, + suspensionDays: action === 'temp_suspend' ? suspensionDays : undefined, + notes: notes.trim() || undefined, + }), + }); + const data = await res.json(); + if (data.success) { + const msgs: Record = { + warn: 'Warning issued. User notified.', + temp_suspend: `Account suspended for ${suspensionDays} days. User notified.`, + ban: 'Account permanently banned. User notified.', + dismiss: 'Report dismissed.', + }; + toast.success(msgs[action]); + onActionTaken(); + onClose(); + } else { + toast.error(data.error ?? 'Failed to take action'); + } + } catch { + toast.error('Network error. Please try again.'); + } finally { + setLoading(false); + } + } + + return ( +
+
+
+ + {/* Header */} +
+

Take Action on Report

+ +
+ + {/* Report summary */} +
+
+ + {report.type === 'listing' ? '📋 Listing' : '👤 User'} Report + + +
+

{report.reason}

+ {meta && ( +

{meta.description}

+ )} + {report.type === 'listing' && report.listing_title && ( +

Listing: {report.listing_title}

+ )} + {report.type === 'user' && report.reported_user_name && ( +

User: {report.reported_user_name}

+ )} + {report.description && ( +

"{report.description}"

+ )} +
+ + {/* Strike context */} +
+ + Current strike total: + + + +{SEVERITY_STRIKE_VALUES[report.severity]} on action + +
+ + {/* Recommended action note */} + {report.recommendedAction && ( +
+ Recommended: + + based on severity + strike history +
+ )} + + {/* Action selector */} +
+ +
+ {[ + { value: 'warn', label: 'Issue Warning', desc: 'Remove content, add 1 strike', color: 'border-blue-300 bg-blue-50 text-blue-800' }, + { value: 'temp_suspend', label: 'Temp Suspension', desc: 'Restrict account temporarily', color: 'border-orange-300 bg-orange-50 text-orange-800' }, + { value: 'ban', label: 'Permanent Ban', desc: 'Remove account permanently', color: 'border-red-300 bg-red-50 text-red-800' }, + { value: 'dismiss', label: 'Dismiss', desc: 'No violation found', color: 'border-gray-300 bg-gray-50 text-gray-600' }, + ].map(opt => ( + + ))} +
+
+ + {/* Suspension days (only shown for temp_suspend) */} + {action === 'temp_suspend' && ( +
+ +
+ {[3, 7, 14, 30].map(d => ( + + ))} +
+
+ )} + + {/* Notes */} +
+ +