feat: Add user reports management dashboard with status filtering, counts, and review functionality including admin notes.

This commit is contained in:
pradeepkumar
2026-03-19 05:20:10 +05:30
parent 9036e9f32f
commit b27cdaeb27
3 changed files with 318 additions and 0 deletions

View File

@@ -0,0 +1,253 @@
'use client';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { userReportsService, type UserReport, type ReportCounts } from '@/services/user-reports.service';
import { getErrorMessage } from '@/services';
function getUserName(user: UserReport['reporter']): string {
const profile = user.userProfile || user.agentProfile;
if (profile?.firstName || profile?.lastName) {
return [profile.firstName, profile.lastName].filter(Boolean).join(' ');
}
return user.email;
}
const STATUS_COLORS: Record<string, string> = {
PENDING: 'bg-yellow-100 text-yellow-800',
REVIEWED: 'bg-blue-100 text-blue-800',
RESOLVED: 'bg-green-100 text-green-800',
DISMISSED: 'bg-gray-100 text-gray-800',
};
export default function ReportsPage() {
const router = useRouter();
const [reports, setReports] = useState<UserReport[]>([]);
const [counts, setCounts] = useState<ReportCounts | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('');
const [selectedReport, setSelectedReport] = useState<UserReport | null>(null);
const [adminNotes, setAdminNotes] = useState('');
const [updatingId, setUpdatingId] = useState<string | null>(null);
const fetchData = async () => {
try {
setLoading(true);
setError('');
const [reportsList, reportCounts] = await Promise.all([
userReportsService.getAllReports(statusFilter || undefined),
userReportsService.getReportCounts(),
]);
setReports(reportsList);
setCounts(reportCounts);
} catch (err) {
const msg = getErrorMessage(err);
setError(msg);
if (msg.includes('Unauthorized')) router.push('/login');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, [statusFilter]);
const handleUpdateStatus = async (reportId: string, status: string) => {
try {
setUpdatingId(reportId);
const updated = await userReportsService.updateReport(reportId, status, adminNotes || undefined);
setReports((prev) => prev.map((r) => (r.id === reportId ? updated : r)));
setSelectedReport(null);
setAdminNotes('');
// Refresh counts
const reportCounts = await userReportsService.getReportCounts();
setCounts(reportCounts);
} catch (err) {
setError(getErrorMessage(err));
} finally {
setUpdatingId(null);
}
};
return (
<div>
<h1 className="text-2xl font-bold text-gray-900 mb-6">User Reports</h1>
{/* Status Counts */}
{counts && (
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
{[
{ label: 'Total', value: counts.total, color: 'bg-gray-50' },
{ label: 'Pending', value: counts.pending, color: 'bg-yellow-50' },
{ label: 'Reviewed', value: counts.reviewed, color: 'bg-blue-50' },
{ label: 'Resolved', value: counts.resolved, color: 'bg-green-50' },
{ label: 'Dismissed', value: counts.dismissed, color: 'bg-gray-50' },
].map((stat) => (
<div key={stat.label} className={`${stat.color} rounded-lg p-4 border border-gray-200`}>
<p className="text-sm text-gray-600">{stat.label}</p>
<p className="text-2xl font-bold text-gray-900">{stat.value}</p>
</div>
))}
</div>
)}
{/* Filter */}
<div className="mb-4 flex items-center gap-3">
<label className="text-sm font-medium text-gray-700">Filter by status:</label>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white"
>
<option value="">All</option>
<option value="PENDING">Pending</option>
<option value="REVIEWED">Reviewed</option>
<option value="RESOLVED">Resolved</option>
<option value="DISMISSED">Dismissed</option>
</select>
</div>
{error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-lg mb-4">
<p className="text-red-700 text-sm">{error}</p>
</div>
)}
{loading ? (
<div className="flex justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
) : reports.length === 0 ? (
<div className="text-center py-12 bg-white rounded-lg border border-gray-200">
<p className="text-gray-500">No reports found</p>
</div>
) : (
<div className="bg-white rounded-lg shadow overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Reporter</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Reported User</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Reason</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Date</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{reports.map((report) => (
<tr key={report.id} className="hover:bg-gray-50">
<td className="px-4 py-3">
<p className="text-sm font-medium text-gray-900">{getUserName(report.reporter)}</p>
<p className="text-xs text-gray-500">{report.reporter.email}</p>
</td>
<td className="px-4 py-3">
<p className="text-sm font-medium text-gray-900">{getUserName(report.reportedUser)}</p>
<p className="text-xs text-gray-500">{report.reportedUser.email}</p>
</td>
<td className="px-4 py-3">
<p className="text-sm text-gray-900">{report.reason}</p>
{report.description && (
<p className="text-xs text-gray-500 mt-1 truncate max-w-[200px]">{report.description}</p>
)}
</td>
<td className="px-4 py-3">
<span className={`px-2 py-1 text-xs font-semibold rounded-full ${STATUS_COLORS[report.status]}`}>
{report.status}
</span>
</td>
<td className="px-4 py-3 text-sm text-gray-500">
{new Date(report.createdAt).toLocaleDateString()}
</td>
<td className="px-4 py-3">
<button
onClick={() => {
setSelectedReport(report);
setAdminNotes(report.adminNotes || '');
}}
className="text-sm text-blue-600 hover:text-blue-800 font-medium"
>
Review
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Review Modal */}
{selectedReport && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl max-w-lg w-full mx-4">
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900">Review Report</h3>
</div>
<div className="px-6 py-4 space-y-4">
<div>
<p className="text-sm font-medium text-gray-700">Reporter</p>
<p className="text-sm text-gray-900">{getUserName(selectedReport.reporter)} ({selectedReport.reporter.email})</p>
</div>
<div>
<p className="text-sm font-medium text-gray-700">Reported User</p>
<p className="text-sm text-gray-900">{getUserName(selectedReport.reportedUser)} ({selectedReport.reportedUser.email})</p>
</div>
<div>
<p className="text-sm font-medium text-gray-700">Reason</p>
<p className="text-sm text-gray-900">{selectedReport.reason}</p>
</div>
{selectedReport.description && (
<div>
<p className="text-sm font-medium text-gray-700">Description</p>
<p className="text-sm text-gray-900">{selectedReport.description}</p>
</div>
)}
<div>
<label className="text-sm font-medium text-gray-700 block mb-1">Admin Notes</label>
<textarea
value={adminNotes}
onChange={(e) => setAdminNotes(e.target.value)}
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm text-gray-900 bg-white placeholder:text-gray-500"
placeholder="Add notes about this report..."
/>
</div>
</div>
<div className="px-6 py-4 border-t border-gray-200 flex flex-wrap gap-2 justify-end">
<button
onClick={() => { setSelectedReport(null); setAdminNotes(''); }}
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors text-sm"
>
Cancel
</button>
<button
onClick={() => handleUpdateStatus(selectedReport.id, 'DISMISSED')}
disabled={updatingId === selectedReport.id}
className="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors text-sm disabled:opacity-50"
>
Dismiss
</button>
<button
onClick={() => handleUpdateStatus(selectedReport.id, 'REVIEWED')}
disabled={updatingId === selectedReport.id}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm disabled:opacity-50"
>
Mark Reviewed
</button>
<button
onClick={() => handleUpdateStatus(selectedReport.id, 'RESOLVED')}
disabled={updatingId === selectedReport.id}
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors text-sm disabled:opacity-50"
>
Resolve
</button>
</div>
</div>
</div>
)}
</div>
);
}