272 lines
17 KiB
JavaScript
272 lines
17 KiB
JavaScript
import React, { useState } from 'react';
|
|
import { Head, router } from '@inertiajs/react';
|
|
import AdminLayout from '@/Layouts/AdminLayout';
|
|
import {
|
|
ShieldAlert,
|
|
AlertTriangle,
|
|
CheckCircle,
|
|
XCircle,
|
|
Filter,
|
|
Flag,
|
|
Eye,
|
|
Plus,
|
|
UserX,
|
|
Settings,
|
|
FileText,
|
|
MessageSquare
|
|
} from 'lucide-react';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
|
|
export default function SafetyHub({ reports, rules, moderation_queue }) {
|
|
const [selectedReport, setSelectedReport] = useState(null);
|
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
const [resolutionAction, setResolutionAction] = useState('warn');
|
|
const [adminNotes, setAdminNotes] = useState('');
|
|
|
|
const handleResolve = (e) => {
|
|
e.preventDefault();
|
|
router.post(`/admin/safety/reports/${selectedReport.id}/resolve`, {
|
|
action: resolutionAction,
|
|
admin_notes: adminNotes
|
|
}, {
|
|
onSuccess: () => {
|
|
setIsDialogOpen(false);
|
|
}
|
|
});
|
|
};
|
|
|
|
const handleModQueue = (id, action) => {
|
|
router.post(`/admin/safety/reports/${id}/resolve`, { action }, {
|
|
onSuccess: () => {
|
|
setIsDialogOpen(false);
|
|
}
|
|
});
|
|
};
|
|
|
|
return (
|
|
<AdminLayout title="Safety & Moderation Center">
|
|
<Head title="Safety & Moderation" />
|
|
|
|
<div className="font-sans max-w-7xl mx-auto space-y-8">
|
|
|
|
{/* Header Row */}
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-xl font-bold text-gray-900 tracking-tight">Trust & Safety Escalation Center</h1>
|
|
<p className="text-xs text-gray-500 mt-0.5">Manage user abuse reports, content moderation queues, and customize automatic fraud filters.</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<button className="inline-flex items-center px-4 py-2.5 bg-white border border-gray-200 text-[#0F6E56] rounded-xl text-xs font-bold hover:bg-gray-50 transition-colors shadow-sm space-x-2 uppercase tracking-wider">
|
|
<Settings className="w-4 h-4" />
|
|
<span>System Rule Rules</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
|
|
{/* Column 1 & 2: Abuse Reports & Content Queue */}
|
|
<div className="lg:col-span-2 space-y-8">
|
|
|
|
{/* Section 1: Abuse Reports */}
|
|
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
|
|
<div className="p-5 border-b border-slate-100 flex items-center justify-between bg-slate-50/20">
|
|
<div>
|
|
<h3 className="text-sm font-black text-slate-800 uppercase tracking-widest">Active Incident Reports</h3>
|
|
<p className="text-[10px] text-slate-400 mt-0.5">Escalated complaints filed by employers or workers</p>
|
|
</div>
|
|
<Badge className="bg-red-50 text-red-700 border-none font-bold uppercase tracking-wider text-[8px]">{reports?.length || 0} Open Tickets</Badge>
|
|
</div>
|
|
|
|
<div className="divide-y divide-slate-100">
|
|
{reports.map((report) => (
|
|
<div key={report.id} className="p-5 hover:bg-slate-50/50 transition-colors flex items-start justify-between gap-4 text-xs font-bold text-slate-700">
|
|
<div className="space-y-2 flex-1">
|
|
<div className="flex items-center space-x-2">
|
|
<Badge className={`border-none text-[8px] font-black uppercase ${
|
|
report.severity === 'High' ? 'bg-red-100 text-red-700' :
|
|
report.severity === 'Medium' ? 'bg-amber-100 text-amber-700' : 'bg-slate-100 text-slate-700'
|
|
}`}>
|
|
{report.severity} Severity
|
|
</Badge>
|
|
<span className="text-[10px] text-slate-400 font-mono">{report.id}</span>
|
|
<span className="text-slate-300">•</span>
|
|
<span className="text-slate-400">Reporter: {report.reporter}</span>
|
|
</div>
|
|
<div>
|
|
<h4 className="text-sm font-black text-slate-900">{report.reason}</h4>
|
|
<p className="text-slate-500 font-medium mt-1 leading-relaxed">{report.description}</p>
|
|
</div>
|
|
<div className="text-[10px] text-slate-400 font-semibold uppercase">Reported User: <span className="text-slate-900 font-bold">{report.reported_user}</span></div>
|
|
</div>
|
|
<div className="flex flex-col items-end gap-2">
|
|
<span className="text-[9px] text-slate-400 font-mono">{report.created_at}</span>
|
|
<button
|
|
onClick={() => { setSelectedReport(report); setIsDialogOpen(true); }}
|
|
className="px-3.5 py-1.5 bg-slate-900 hover:bg-slate-800 text-white rounded-lg font-black text-[9px] uppercase tracking-wider"
|
|
>
|
|
Investigate
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Section 2: Content Moderation Queue */}
|
|
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
|
|
<div className="p-5 border-b border-slate-100 bg-slate-50/20">
|
|
<h3 className="text-sm font-black text-slate-800 uppercase tracking-widest">Profile Content Vetting Queue</h3>
|
|
<p className="text-[10px] text-slate-400 mt-0.5">Bio updates or pictures flagged for manual review</p>
|
|
</div>
|
|
|
|
<div className="divide-y divide-slate-100">
|
|
{moderation_queue.map((item) => (
|
|
<div key={item.id} className="p-5 hover:bg-slate-50/30 transition-colors grid grid-cols-1 sm:grid-cols-3 gap-4 text-xs font-bold text-slate-700">
|
|
<div>
|
|
<span className="text-[9px] text-slate-400 uppercase tracking-widest block font-mono">{item.id} • {item.type}</span>
|
|
<span className="text-sm font-black text-slate-900 block mt-1">{item.user}</span>
|
|
<Badge className="mt-2 bg-amber-50 text-amber-700 border-none font-bold uppercase tracking-wider text-[8px]">{item.flag}</Badge>
|
|
</div>
|
|
<div className="sm:col-span-2 flex flex-col justify-between items-end gap-3">
|
|
<div className="bg-slate-50 p-3 rounded-xl border border-slate-100 text-slate-600 font-medium w-full text-[11px] leading-relaxed">
|
|
{item.content.startsWith('http') ? (
|
|
<img src={item.content} alt="Flagged asset" className="h-16 rounded border" />
|
|
) : item.content}
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => handleModQueue(item.id, 'approve')}
|
|
className="px-3.5 py-1.5 bg-emerald-600 hover:bg-emerald-500 text-white rounded-lg text-[9px] font-black uppercase tracking-wider"
|
|
>
|
|
Approve Content
|
|
</button>
|
|
<button
|
|
onClick={() => handleModQueue(item.id, 'reject')}
|
|
className="px-3.5 py-1.5 bg-red-600 hover:bg-red-500 text-white rounded-lg text-[9px] font-black uppercase tracking-wider"
|
|
>
|
|
Reject / Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{/* Column 3: Auto-Flag Detection Rules */}
|
|
<div className="space-y-6">
|
|
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-sm space-y-4">
|
|
<div>
|
|
<h3 className="text-sm font-black text-slate-800 uppercase tracking-widest">Auto-Detection Rules</h3>
|
|
<p className="text-[10px] text-slate-400 mt-0.5">Parameters triggering system automated flags</p>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{rules.map((rule) => (
|
|
<div key={rule.id} className="p-3 bg-slate-50 rounded-xl border border-slate-100 text-xs font-bold text-slate-700 space-y-1">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-slate-800 font-black">{rule.name}</span>
|
|
<Badge className={`border-none text-[8px] font-black uppercase ${
|
|
rule.status === 'Active' ? 'bg-emerald-50 text-emerald-700' : 'bg-slate-100 text-slate-400'
|
|
}`}>{rule.status}</Badge>
|
|
</div>
|
|
<p className="text-[10px] text-slate-400 font-semibold">{rule.trigger}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<button className="w-full py-3 bg-[#0F6E56] text-white rounded-xl text-[10px] font-black uppercase tracking-widest shadow-md hover:bg-[#085041] flex items-center justify-center gap-1.5">
|
|
<Plus className="w-3.5 h-3.5" />
|
|
<span>Create Custom Rule</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Keyword Abuse escalating info */}
|
|
<div className="bg-amber-50/50 p-5 border border-amber-200 rounded-2xl shadow-sm space-y-3">
|
|
<span className="text-[10px] font-black text-amber-800 uppercase tracking-widest flex items-center gap-1">
|
|
<ShieldAlert className="w-4 h-4" />
|
|
<span>Direct Chat Abuse Triggered Logs</span>
|
|
</span>
|
|
<p className="text-xs text-slate-600 font-semibold leading-relaxed">System triggered 14 keyword warning notices to employers for sharing email/phone details in chats prior to hiring offer completions today.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Investigation Dialog Modal */}
|
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
|
<DialogContent className="max-w-xl bg-white p-6 rounded-[24px] border-none shadow-2xl font-sans">
|
|
<DialogHeader className="mb-4">
|
|
<DialogTitle className="text-lg font-black text-slate-800 flex items-center gap-1">
|
|
<ShieldAlert className="w-5 h-5 text-red-600" />
|
|
<span>Incident Investigation: <span className="font-mono text-slate-400">{selectedReport?.id}</span></span>
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<form onSubmit={handleResolve} className="space-y-4 text-xs font-bold text-slate-700">
|
|
<div className="space-y-1">
|
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Reason / Incident Subject</label>
|
|
<div className="p-3 bg-slate-50 border rounded-xl font-black text-slate-800">{selectedReport?.reason}</div>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Report Description</label>
|
|
<p className="p-3 bg-slate-50 border rounded-xl font-medium text-slate-600 leading-relaxed">{selectedReport?.description}</p>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Select Resolution Action</label>
|
|
<select
|
|
className="w-full bg-slate-50 border rounded-xl px-3 py-2.5 font-bold outline-none cursor-pointer focus:bg-white"
|
|
value={resolutionAction}
|
|
onChange={e => setResolutionAction(e.target.value)}
|
|
>
|
|
<option value="warn">Issue Official Warning Notice</option>
|
|
<option value="suspend">Suspend Reported Account temporarily</option>
|
|
<option value="ban">Permanently Ban reported user profile</option>
|
|
<option value="dismiss">Dismiss Report as False / Unverified</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Internal Compliance Action Notes</label>
|
|
<textarea
|
|
rows="3"
|
|
placeholder="State findings of direct message logs audits or phone outreach validation..."
|
|
className="w-full bg-white border rounded-xl p-3 font-semibold focus:ring-2 focus:ring-[#0F6E56]/10 outline-none"
|
|
value={adminNotes}
|
|
onChange={e => setAdminNotes(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex gap-2 pt-2 justify-end">
|
|
<button
|
|
type="submit"
|
|
className="px-5 py-2.5 bg-slate-900 hover:bg-slate-800 text-white rounded-xl text-[10px] font-black uppercase tracking-widest"
|
|
>
|
|
Submit Findings
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsDialogOpen(false)}
|
|
className="px-4 py-2.5 bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-xl text-[10px] font-black uppercase tracking-widest"
|
|
>
|
|
Close
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</AdminLayout>
|
|
);
|
|
}
|