![]() Server : Apache/2 System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64 User : gositeme ( 1004) PHP Version : 8.2.29 Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname Directory : /home/gositeme/backups/lavocat.quebec/backup-20250730-021618/src/pages/admin/ |
import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import { useSession } from 'next-auth/react';
import dynamic from 'next/dynamic';
import type { Registration } from '@prisma/client';
import { format } from 'date-fns';
import Link from 'next/link';
import RegistrationForm from '@/components/RegistrationForm';
import { motion, AnimatePresence } from 'framer-motion';
import { getFacilityName } from '@/utils/facilities';
import { canAccessAdmin } from '@/lib/auth-utils';
import { useMemo } from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, BarChart, Bar, LabelList } from 'recharts';
import jsPDF from 'jspdf';
import 'jspdf-autotable';
const DATE_RANGES = [
{ value: '24h', label: 'Last 24h' },
{ value: '7d', label: 'Last 7 days' },
{ value: '30d', label: 'Last 30 days' },
{ value: '90d', label: 'Last 90 days' },
{ value: 'custom', label: 'Custom Range' }
];
const USER_TYPES = [
{ value: 'all', label: 'All Users' },
{ value: 'admin', label: 'Admin' },
{ value: 'lawyer', label: 'Lawyer' },
{ value: 'client', label: 'Client' },
{ value: 'judge', label: 'Judge' },
{ value: 'jurist', label: 'Jurist' },
{ value: 'mediator', label: 'Mediator' },
{ value: 'business', label: 'Business' },
{ value: 'guest', label: 'Guest' }
];
const DEVICE_TYPES = [
{ value: 'all', label: 'All Devices' },
{ value: 'desktop', label: 'Desktop' },
{ value: 'mobile', label: 'Mobile' },
{ value: 'tablet', label: 'Tablet' }
];
const SEARCH_TYPES = [
{ value: 'all', label: 'All Types' },
{ value: 'users', label: 'Users' },
{ value: 'cases', label: 'Cases' },
{ value: 'documents', label: 'Documents' },
{ value: 'businesses', label: 'Businesses' }
];
const REGIONS = [
{ value: 'all', label: 'All Regions' },
{ value: 'quebec', label: 'Quebec' },
{ value: 'ontario', label: 'Ontario' },
{ value: 'british_columbia', label: 'British Columbia' },
{ value: 'alberta', label: 'Alberta' },
{ value: 'federal', label: 'Federal' },
{ value: 'international', label: 'International' }
];
const PrivateChat = dynamic(() => import('@/components/Chat/PrivateChat'), { ssr: false });
// Dynamically import LayoutWithSidebar with no SSR
const LayoutWithSidebar = dynamic(() => import('@/components/LayoutWithSidebar'), {
ssr: false
});
interface RegistrationWithDetails extends Registration {
address: {
street: string;
city: string;
state: string;
postalCode: string;
country: string;
} | null;
detaineeInfo: {
name: string;
facility: string;
inmateId: string;
incarcerationDate: string;
expectedReleaseDate?: string | null;
} | null;
documents?: { id: string; name: string; url: string }[];
}
const translations = {
en: {
dashboard: 'Admin Dashboard',
manage: 'Manage class action applications here.',
stats: {
total: 'Total Applications',
pending: 'Pending',
approved: 'Approved',
rejected: 'Rejected',
},
manageUsers: 'Manage Users',
newApplication: 'Create New Application',
noApplications: 'No applications found.',
searchPlaceholder: 'Search by name or email...',
allStatuses: 'All Statuses',
status: {
all: 'All Statuses',
pending: 'Pending',
approved: 'Approved',
rejected: 'Rejected',
},
bulk: {
approve: 'Approve',
reject: 'Reject',
delete: 'Delete',
},
table: {
date: 'Date',
name: 'Name',
detainee: 'Detainee',
status: 'Status',
actions: 'Actions',
},
confirmDelete: 'Are you sure you want to delete this application?',
errorDelete: 'Error deleting application',
errorLoad: 'Error loading applications',
errorRefresh: 'Error refreshing registrations',
document: {
add: 'Add Document',
uploading: 'Uploading...',
uploadError: 'Error uploading document',
delete: 'Delete Document',
deleteConfirm: 'Are you sure you want to delete this document?',
deleteError: 'Error deleting document',
documents: 'Documents',
},
modal: {
newApplication: 'New Application',
close: 'Close',
},
fileTooLarge: 'File is too large (max 100MB)',
unsupportedFile: 'Unsupported file type',
approve: 'Approve',
reject: 'Reject',
delete: 'Delete',
actions: 'Actions',
loading: 'Loading...',
restore: {
title: 'Restore Backup',
description: 'Upload a backup file to restore the system to a previous state.',
button: 'Restore Backup',
upload: 'Upload Backup File',
success: 'Restore completed successfully',
error: 'Restore failed',
close: 'Close',
loading: 'Restoring...',
},
},
fr: {
dashboard: 'Tableau de Bord Administrateur',
manage: 'Gérez les demandes d\'action collective ici.',
stats: {
total: 'Total des demandes',
pending: 'En attente',
approved: 'Approuvées',
rejected: 'Rejetées',
},
manageUsers: 'Gérer les utilisateurs',
newApplication: 'Créer une nouvelle demande',
noApplications: 'Aucune demande trouvée.',
searchPlaceholder: 'Rechercher par nom ou email...',
allStatuses: 'Tous les statuts',
status: {
all: 'Tous les statuts',
pending: 'En attente',
approved: 'Approuvé',
rejected: 'Rejeté',
},
bulk: {
approve: 'Approuver',
reject: 'Rejeter',
delete: 'Supprimer',
},
table: {
date: 'Date',
name: 'Nom',
detainee: 'Détenu',
status: 'Statut',
actions: 'Actions',
},
confirmDelete: 'Êtes-vous sûr de vouloir supprimer cette demande ?',
errorDelete: 'Erreur lors de la suppression de la demande',
errorLoad: 'Erreur lors du chargement des demandes',
errorRefresh: 'Erreur lors du rafraîchissement des demandes',
document: {
add: 'Ajouter un document',
uploading: 'Téléchargement...',
uploadError: 'Erreur lors du téléchargement du document',
delete: 'Supprimer le document',
deleteConfirm: 'Êtes-vous sûr de vouloir supprimer ce document ?',
deleteError: 'Erreur lors de la suppression du document',
documents: 'Documents',
},
modal: {
newApplication: 'Nouvelle demande',
close: 'Fermer',
},
fileTooLarge: 'Le fichier est trop volumineux (max 100MB)',
unsupportedFile: 'Type de fichier non supporté',
approve: 'Approuver',
reject: 'Rejeter',
delete: 'Supprimer',
actions: 'Actions',
loading: 'Chargement...',
restore: {
title: 'Restaurer la Sauvegarde',
description: 'Téléchargez un fichier de sauvegarde pour restaurer le système à un état précédent.',
button: 'Restaurer la Sauvegarde',
upload: 'Télécharger le Fichier de Sauvegarde',
success: 'Restauration terminée avec succès',
error: 'Restauration échouée',
close: 'Fermer',
loading: 'Restauration...',
},
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'APPROVED':
case 'COMPLETED':
case 'PAYMENT_RECEIVED':
return 'bg-green-100 text-green-800';
case 'REJECTED':
case 'DOCUMENTS_EXPIRED':
case 'INFORMATION_MISMATCH':
case 'ESCALATED':
return 'bg-red-100 text-red-800';
case 'PENDING':
case 'ADDITIONAL_INFO_NEEDED':
case 'PENDING_PAYMENT':
return 'bg-yellow-100 text-yellow-800';
case 'MISSING_DOCUMENTS':
case 'DOCUMENTS_INCOMPLETE':
return 'bg-orange-100 text-orange-800';
case 'DOCUMENTS_UNDER_REVIEW':
case 'PENDING_LAWYER_APPROVAL':
case 'PENDING_FACILITY_APPROVAL':
return 'bg-blue-100 text-blue-800';
case 'VERIFICATION_IN_PROGRESS':
case 'FINAL_REVIEW':
return 'bg-purple-100 text-purple-800';
case 'ON_HOLD':
return 'bg-gray-100 text-gray-800';
case 'WebAd':
return 'bg-pink-100 text-pink-800';
default:
return 'bg-yellow-100 text-yellow-800';
}
};
function exportCSV(data: any[], headers: string[], filename: string) {
const csvRows = [headers.join(',')];
for (const row of data) {
csvRows.push(headers.map(h => JSON.stringify(row[h] ?? '')).join(','));
}
const blob = new Blob([csvRows.join('\n')], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
window.URL.revokeObjectURL(url);
}
function exportExcel(data: any[], headers: string[], filename: string) {
// Simple Excel export using CSV format (Excel opens CSV)
exportCSV(data, headers, filename.endsWith('.csv') ? filename : filename + '.csv');
}
function exportPDF(title: string, data: any[], headers: string[], filename: string) {
const doc = new jsPDF();
doc.text(title, 14, 16);
(doc as any).autoTable({
head: [headers],
body: data.map(row => headers.map(h => row[h] ?? '')),
startY: 22,
styles: { fontSize: 9 },
headStyles: { fillColor: [99, 102, 241] }
});
doc.save(filename);
}
export default function AdminDashboard() {
const { data: session, status } = useSession();
const router = useRouter();
const { locale } = router;
const [registrations, setRegistrations] = useState<RegistrationWithDetails[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [search, setSearch] = useState('');
const [statusFilter, setStatusFilter] = useState('ALL');
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [bulkActionLoading, setBulkActionLoading] = useState(false);
const [stats, setStats] = useState({
total: 0,
pending: 0,
approved: 0,
rejected: 0,
});
const [isNewApplicationModalOpen, setIsNewApplicationModalOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState<string | null>(null);
const [isUpdatingStatus, setIsUpdatingStatus] = useState<string | null>(null);
const [isUploading, setIsUploading] = useState(false);
const [uploadError, setUploadError] = useState('');
const [previewDoc, setPreviewDoc] = useState<{ url: string; name: string } | null>(null);
const [isRestoreModalOpen, setIsRestoreModalOpen] = useState(false);
const [restoreFile, setRestoreFile] = useState<File | null>(null);
const [isRestoring, setIsRestoring] = useState(false);
const [restoreError, setRestoreError] = useState<string | null>(null);
const [restoreSuccess, setRestoreSuccess] = useState(false);
const [openChatId, setOpenChatId] = useState<string | null>(null);
const [showPrivateChat, setShowPrivateChat] = useState<{ open: boolean; registrationId: string | null }>({ open: false, registrationId: null });
const t = translations[locale as 'en' | 'fr'] || translations.en;
const [isMobile, setIsMobile] = useState(false);
const [searchAnalytics, setSearchAnalytics] = useState<any>(null);
const [searchAnalyticsLoading, setSearchAnalyticsLoading] = useState(true);
const [searchAnalyticsError, setSearchAnalyticsError] = useState('');
const [analyticsDateRange, setAnalyticsDateRange] = useState('7d');
const [analyticsUserType, setAnalyticsUserType] = useState('all');
const [customStartDate, setCustomStartDate] = useState('');
const [customEndDate, setCustomEndDate] = useState('');
const [analyticsDeviceType, setAnalyticsDeviceType] = useState('all');
const [analyticsSearchType, setAnalyticsSearchType] = useState('all');
const [analyticsRegion, setAnalyticsRegion] = useState('all');
const [analyticsOrganization, setAnalyticsOrganization] = useState('all');
// Mobile detection
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 768);
};
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
const getStatusLabel = (status: string) => {
const statusMap: { [key: string]: { en: string; fr: string } } = {
PENDING: { en: 'Pending', fr: 'En attente' },
MISSING_DOCUMENTS: { en: 'Missing Documents', fr: 'Documents manquants' },
DOCUMENTS_UNDER_REVIEW: { en: 'Documents Under Review', fr: 'Documents en cours d\'examen' },
ADDITIONAL_INFO_NEEDED: { en: 'Additional Info Needed', fr: 'Informations supplémentaires requises' },
VERIFICATION_IN_PROGRESS: { en: 'Verification In Progress', fr: 'Vérification en cours' },
LAWYER_VERIFICATION: { en: 'Lawyer Verification', fr: 'Vérification par l\'avocat' },
FACILITY_VERIFICATION: { en: 'Facility Verification', fr: 'Vérification par l\'établissement' },
DOCUMENTS_EXPIRED: { en: 'Documents Expired', fr: 'Documents expirés' },
DOCUMENTS_INCOMPLETE: { en: 'Documents Incomplete', fr: 'Documents incomplets' },
INFORMATION_MISMATCH: { en: 'Information Mismatch', fr: 'Incohérence d\'informations' },
PENDING_PAYMENT: { en: 'Pending Payment', fr: 'Paiement en attente' },
PAYMENT_RECEIVED: { en: 'Payment Received', fr: 'Paiement reçu' },
PENDING_LAWYER_APPROVAL: { en: 'Pending Lawyer Approval', fr: 'En attente d\'approbation par l\'avocat' },
PENDING_FACILITY_APPROVAL: { en: 'Pending Facility Approval', fr: 'En attente d\'approbation par l\'établissement' },
ON_HOLD: { en: 'On Hold', fr: 'En pause' },
ESCALATED: { en: 'Escalated', fr: 'Escaladé' },
FINAL_REVIEW: { en: 'Final Review', fr: 'Examen final' },
APPROVED: { en: 'Approved', fr: 'Approuvée' },
REJECTED: { en: 'Rejected', fr: 'Rejetée' },
COMPLETED: { en: 'Completed', fr: 'Terminé' },
WebAd: { en: 'Web Ad', fr: 'Web Ad' }
};
return statusMap[status]?.[locale as 'en' | 'fr'] || status;
};
const StatusIcon = ({ status }: { status: string }) => {
const iconMap: { [key: string]: JSX.Element } = {
APPROVED: <span className="text-green-500 text-2xl">✔️</span>,
REJECTED: <span className="text-red-500 text-2xl">❌</span>,
PENDING: <span className="text-yellow-500 text-2xl">⏳</span>,
DOCUMENTS_UNDER_REVIEW: <span className="text-blue-500 text-2xl">📄</span>,
MISSING_DOCUMENTS: <span className="text-orange-500 text-2xl">📄</span>,
PENDING_FACILITY_APPROVAL: <span className="text-blue-400 text-2xl">🏢</span>,
// ...add more as needed
};
return iconMap[status] || <span className="text-gray-400 text-2xl">📝</span>;
};
const handleViewAndNavigate = (id: string) => {
router.push(`/admin/registrations/${id}`);
};
const handleDeleteApplication = async (id: string) => {
if (!window.confirm(t.confirmDelete)) {
return;
}
setIsDeleting(id);
try {
const response = await fetch(`/api/admin/registrations/${id}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error(t.errorDelete);
}
setRegistrations(prev => {
const updated = prev.filter(reg => reg.id !== id);
setStats({
total: updated.length,
pending: updated.filter(reg => reg.status === 'PENDING').length,
approved: updated.filter(reg => reg.status === 'APPROVED').length,
rejected: updated.filter(reg => reg.status === 'REJECTED').length,
});
return updated;
});
} catch (err) {
console.error('Error deleting application:', err);
setError(t.errorDelete);
} finally {
setIsDeleting(null);
}
};
useEffect(() => {
if (status === 'loading') return;
if (!session) {
router.push('/auth/login');
return;
}
if (!canAccessAdmin(session)) {
console.log('User role:', session.user.role, 'redirecting to user dashboard');
router.push('/user/dashboard');
return;
}
}, [session, status, router]);
useEffect(() => {
const fetchData = async () => {
console.log('Admin Dashboard - Status:', status);
console.log('Admin Dashboard - Session:', session);
console.log('Admin Dashboard - User Role:', session?.user?.role);
if (status !== 'authenticated' || !canAccessAdmin(session)) {
console.log('Admin Dashboard - Not authenticated or not admin, returning');
return;
}
try {
console.log('Admin Dashboard - Fetching registrations...');
const response = await fetch('/api/admin/registrations');
console.log('Admin Dashboard - Response status:', response.status);
if (!response.ok) {
const errorData = await response.json();
console.log('Admin Dashboard - Error data:', errorData);
throw new Error(errorData.message || 'Failed to fetch registrations');
}
const data = await response.json();
setRegistrations(data);
// Calculate stats
const stats = {
total: data.length,
pending: data.filter((reg: RegistrationWithDetails) => reg.status === 'PENDING').length,
approved: data.filter((reg: RegistrationWithDetails) => reg.status === 'APPROVED').length,
rejected: data.filter((reg: RegistrationWithDetails) => reg.status === 'REJECTED').length,
};
setStats(stats);
} catch (err: any) {
setError(err.message || 'Error loading data');
console.error('Error:', err);
} finally {
setLoading(false);
}
};
fetchData();
}, [status, session]);
useEffect(() => {
// Fetch search analytics
const fetchAnalytics = async () => {
setSearchAnalyticsLoading(true);
try {
const params = new URLSearchParams({
type: 'overview',
userType: analyticsUserType,
deviceType: analyticsDeviceType,
searchType: analyticsSearchType,
region: analyticsRegion,
organization: analyticsOrganization
});
if (analyticsDateRange === 'custom' && customStartDate && customEndDate) {
params.set('startDate', customStartDate);
params.set('endDate', customEndDate);
} else {
params.set('period', analyticsDateRange);
}
const res = await fetch(`/api/search/analytics?${params}`);
if (!res.ok) throw new Error('Failed to fetch search analytics');
const data = await res.json();
setSearchAnalytics(data);
} catch (err: any) {
setSearchAnalyticsError(err.message || 'Error loading analytics');
} finally {
setSearchAnalyticsLoading(false);
}
};
if (analyticsDateRange !== 'custom' || (customStartDate && customEndDate)) {
fetchAnalytics();
}
}, [analyticsDateRange, analyticsUserType, analyticsDeviceType, analyticsSearchType, analyticsRegion, analyticsOrganization, customStartDate, customEndDate]);
// Filtering and search
const filtered = registrations.filter((r) => {
const matchesSearch =
r.firstName.toLowerCase().includes(search.toLowerCase()) ||
r.lastName.toLowerCase().includes(search.toLowerCase()) ||
r.email.toLowerCase().includes(search.toLowerCase());
const matchesStatus =
statusFilter === 'ALL' || r.status === statusFilter;
return matchesSearch && matchesStatus;
});
// Bulk actions
const handleBulkAction = async (action: 'APPROVED' | 'REJECTED' | 'DELETE') => {
setBulkActionLoading(true);
try {
if (action === 'DELETE') {
await Promise.all(selectedIds.map(async (id) => {
await fetch(`/api/admin/registrations/${id}`, { method: 'DELETE' });
}));
setRegistrations((prev) => {
const updated = prev.filter((r) => !selectedIds.includes(r.id));
setStats({
total: updated.length,
pending: updated.filter((r) => r.status === 'PENDING').length,
approved: updated.filter((r) => r.status === 'APPROVED').length,
rejected: updated.filter((r) => r.status === 'REJECTED').length,
});
return updated;
});
} else {
await Promise.all(selectedIds.map(async (id) => {
await fetch(`/api/admin/registrations/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: action }),
});
}));
setRegistrations((prev) =>
prev.map((r) =>
selectedIds.includes(r.id) ? { ...r, status: action } : r
)
);
}
setSelectedIds([]);
// Update stats
setStats({
total: registrations.length,
pending: registrations.filter((r) => r.status === 'PENDING').length,
approved: registrations.filter((r) => r.status === 'APPROVED').length,
rejected: registrations.filter((r) => r.status === 'REJECTED').length,
});
} catch (err) {
setError('Bulk action failed');
} finally {
setBulkActionLoading(false);
}
};
const toggleSelect = (id: string) => {
setSelectedIds((prev) =>
prev.includes(id) ? prev.filter((sid) => sid !== id) : [...prev, id]
);
};
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>, registrationId: string) => {
const file = event.target.files?.[0];
if (!file) return;
// Validate file size (max 100MB)
if (file.size > 100 * 1024 * 1024) {
setUploadError(t.fileTooLarge);
return;
}
// Validate file type
const allowedTypes = [
'application/pdf',
'image/jpeg',
'image/png',
'video/mp4',
'video/mpeg',
'video/x-mpeg',
'video/x-mpeg4',
'video/webm',
'video/quicktime', // mov
'video/x-msvideo', // avi
'video/x-m4v', // m4v
'video/3gpp', // 3gp
'video/x-flv', // flv
'video/x-matroska', // mkv
'video/x-ms-wmv' // wmv
];
if (!allowedTypes.includes(file.type)) {
setUploadError(t.unsupportedFile);
return;
}
setIsUploading(true);
setUploadError('');
try {
const formData = new FormData();
formData.append('file', file);
// For new registrations, we'll just preview the file
if (registrationId === 'new') {
const objectUrl = URL.createObjectURL(file);
setPreviewDoc({ url: objectUrl, name: file.name });
return;
}
const uploadResponse = await fetch(`/api/admin/registrations/${registrationId}/documents`, {
method: 'POST',
body: formData,
});
const contentType = uploadResponse.headers.get('content-type');
let document;
if (contentType && contentType.includes('application/json')) {
document = await uploadResponse.json();
} else {
const text = await uploadResponse.text();
throw new Error(text || t.errorLoad);
}
setRegistrations(prev => prev.map(reg => {
if (reg.id === registrationId) {
return {
...reg,
documents: [...(reg.documents || []), document],
};
}
return reg;
}));
} catch (err) {
setUploadError(err instanceof Error ? err.message : t.errorLoad);
console.error(err);
} finally {
setIsUploading(false);
}
};
const handleDeleteDocument = async (registrationId: string, documentId: string) => {
if (!registrationId || !documentId) return;
setIsDeleting(documentId);
try {
const response = await fetch(`/api/admin/registrations/${registrationId}/documents/${documentId}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error(t.errorDelete);
}
setRegistrations(prev => prev.map(reg => {
if (reg.id === registrationId) {
return {
...reg,
documents: reg.documents?.filter(doc => doc.id !== documentId) || [],
};
}
return reg;
}));
} catch (error) {
console.error('Error deleting document:', error);
setError(t.errorDelete);
} finally {
setIsDeleting(null);
}
};
const handleRestore = async () => {
if (!restoreFile) return;
setIsRestoring(true);
setRestoreError(null);
setRestoreSuccess(false);
const formData = new FormData();
formData.append('file', restoreFile);
try {
const response = await fetch('/api/admin/restore', {
method: 'POST',
body: formData,
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Restore failed');
}
setRestoreSuccess(true);
// Refresh the page after successful restore
setTimeout(() => {
window.location.reload();
}, 2000);
} catch (error: any) {
setRestoreError(error?.message || 'An unexpected error occurred');
} finally {
setIsRestoring(false);
}
};
const handleStatusUpdate = async (registrationId: string, newStatus: string) => {
setIsUpdatingStatus(registrationId);
try {
const response = await fetch(`/api/admin/registrations/${registrationId}/status`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
status: newStatus,
}),
});
if (!response.ok) {
throw new Error('Failed to update status');
}
// Update the registration in the list
setRegistrations(prev => prev.map(reg =>
reg.id === registrationId
? { ...reg, status: newStatus, updatedAt: new Date() }
: reg
));
// Update stats
setStats(prev => ({
total: prev.total,
pending: registrations.filter(r => r.id !== registrationId && r.status === 'PENDING').length + (newStatus === 'PENDING' ? 1 : 0),
approved: registrations.filter(r => r.id !== registrationId && r.status === 'APPROVED').length + (newStatus === 'APPROVED' ? 1 : 0),
rejected: registrations.filter(r => r.id !== registrationId && r.status === 'REJECTED').length + (newStatus === 'REJECTED' ? 1 : 0),
}));
} catch (err) {
console.error('Error updating status:', err);
setError('Failed to update status');
} finally {
setIsUpdatingStatus(null);
}
};
if (status === 'loading' || loading) {
return (
<LayoutWithSidebar>
<div className={`flex justify-center items-center ${isMobile ? 'min-h-64' : 'min-h-96'}`}>
<div className="text-center">
<div className={`animate-spin rounded-full border-4 border-blue-500 border-t-transparent ${isMobile ? 'w-12 h-12' : 'w-16 h-16'} mx-auto mb-4`}></div>
<p className={`text-gray-600 ${isMobile ? 'text-sm' : ''}`}>Loading admin panel...</p>
</div>
</div>
</LayoutWithSidebar>
);
}
if (!session || !canAccessAdmin(session)) {
return null;
}
const adminLinks = [
{
title: 'Dashboard',
description: 'View comprehensive admin dashboard with statistics and management tools',
href: '/admin/dashboard',
icon: '📊',
color: 'from-blue-500 to-blue-600'
},
{
title: 'Case Management',
description: 'Create and manage legal cases, view applications, and assign lawyers',
href: '/admin/case-management',
icon: '⚖️',
color: 'from-blue-600 to-purple-600'
},
{
title: 'User Management',
description: 'Manage user accounts, permissions, and access levels',
href: '/admin/users',
icon: '👥',
color: 'from-green-500 to-green-600'
},
{
title: 'Public Notifications',
description: 'Manage banners, toasts, and engagement prompts for public visitors',
href: '/admin/notifications',
icon: '📢',
color: 'from-red-500 to-red-600'
},
{
title: 'Applications',
description: 'Review and manage legal case applications from clients',
href: '/admin/applications',
icon: '📋',
color: 'from-purple-500 to-purple-600'
},
{
title: 'Registrations',
description: 'View and manage user registrations across all cases',
href: '/admin/registrations',
icon: '📝',
color: 'from-indigo-500 to-indigo-600'
},
{
title: 'System Settings',
description: 'Configure system settings and options',
href: '/admin/options',
icon: '⚙️',
color: 'from-gray-500 to-gray-600'
},
{
title: 'Admin Management',
description: 'Grant or revoke admin privileges to users',
href: '/admin/make-admin',
icon: '👑',
color: 'from-yellow-500 to-yellow-600'
}
];
return (
<LayoutWithSidebar>
<div className={`mx-auto ${isMobile ? 'px-2 py-4 max-w-full' : 'max-w-6xl px-4 py-8'}`}>
{/* Header */}
<div className={`text-center mb-8 bg-gradient-to-r from-blue-600 to-purple-600 text-white rounded-xl shadow-xl ${isMobile ? 'p-6' : 'p-8'}`}>
<h1 className={`font-bold mb-2 ${isMobile ? 'text-2xl' : 'text-4xl'}`}>Admin Panel</h1>
<p className={`${isMobile ? 'text-sm' : 'text-lg'}`}>
Welcome back, {session.user.name}! Manage the multi-case legal platform from here.
</p>
</div>
{/* Quick Stats */}
<div className={`grid gap-4 mb-8 ${isMobile ? 'grid-cols-2' : 'grid-cols-2 md:grid-cols-4'}`}>
<div className={`bg-white rounded-lg shadow-md border-l-4 border-blue-500 ${isMobile ? 'p-4' : 'p-6'}`}>
<div className={`text-blue-600 mb-2 ${isMobile ? 'text-2xl' : 'text-3xl'}`}>👥</div>
<div className={`font-bold text-gray-800 ${isMobile ? 'text-lg' : 'text-2xl'}`}>Users</div>
<div className={`text-gray-600 ${isMobile ? 'text-xs' : 'text-sm'}`}>Total registered</div>
</div>
<div className={`bg-white rounded-lg shadow-md border-l-4 border-green-500 ${isMobile ? 'p-4' : 'p-6'}`}>
<div className={`text-green-600 mb-2 ${isMobile ? 'text-2xl' : 'text-3xl'}`}>📋</div>
<div className={`font-bold text-gray-800 ${isMobile ? 'text-lg' : 'text-2xl'}`}>Apps</div>
<div className={`text-gray-600 ${isMobile ? 'text-xs' : 'text-sm'}`}>Applications</div>
</div>
<div className={`bg-white rounded-lg shadow-md border-l-4 border-purple-500 ${isMobile ? 'p-4' : 'p-6'}`}>
<div className={`text-purple-600 mb-2 ${isMobile ? 'text-2xl' : 'text-3xl'}`}>📝</div>
<div className={`font-bold text-gray-800 ${isMobile ? 'text-lg' : 'text-2xl'}`}>Regs</div>
<div className={`text-gray-600 ${isMobile ? 'text-xs' : 'text-sm'}`}>Registrations</div>
</div>
<div className={`bg-white rounded-lg shadow-md border-l-4 border-yellow-500 ${isMobile ? 'p-4' : 'p-6'}`}>
<div className={`text-yellow-600 mb-2 ${isMobile ? 'text-2xl' : 'text-3xl'}`}>👑</div>
<div className={`font-bold text-gray-800 ${isMobile ? 'text-lg' : 'text-2xl'}`}>Admins</div>
<div className={`text-gray-600 ${isMobile ? 'text-xs' : 'text-sm'}`}>Admin users</div>
</div>
</div>
{/* Search Analytics Section */}
<div className={`mb-8 bg-white rounded-xl shadow-xl border border-gray-200 ${isMobile ? 'p-6' : 'p-8'}`}>
<h2 className={`font-bold text-gray-800 mb-4 ${isMobile ? 'text-xl' : 'text-2xl'}`}>Search Analytics</h2>
{/* Filters */}
<div className="mb-4 flex flex-wrap gap-4 items-center">
<label className="text-sm font-medium text-gray-700">Date Range:
<select
className="ml-2 px-2 py-1 border rounded"
value={analyticsDateRange}
onChange={e => setAnalyticsDateRange(e.target.value)}
>
{DATE_RANGES.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</label>
{analyticsDateRange === 'custom' && (
<>
<label className="text-sm font-medium text-gray-700">From:
<input
type="date"
className="ml-2 px-2 py-1 border rounded"
value={customStartDate}
onChange={e => setCustomStartDate(e.target.value)}
max={customEndDate || undefined}
/>
</label>
<label className="text-sm font-medium text-gray-700">To:
<input
type="date"
className="ml-2 px-2 py-1 border rounded"
value={customEndDate}
onChange={e => setCustomEndDate(e.target.value)}
min={customStartDate || undefined}
/>
</label>
</>
)}
<label className="text-sm font-medium text-gray-700">User Type:
<select
className="ml-2 px-2 py-1 border rounded"
value={analyticsUserType}
onChange={e => setAnalyticsUserType(e.target.value)}
>
{USER_TYPES.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</label>
<label className="text-sm font-medium text-gray-700">Device:
<select
className="ml-2 px-2 py-1 border rounded"
value={analyticsDeviceType}
onChange={e => setAnalyticsDeviceType(e.target.value)}
>
{DEVICE_TYPES.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</label>
<label className="text-sm font-medium text-gray-700">Search Type:
<select
className="ml-2 px-2 py-1 border rounded"
value={analyticsSearchType}
onChange={e => setAnalyticsSearchType(e.target.value)}
>
{SEARCH_TYPES.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</label>
<label className="text-sm font-medium text-gray-700">Region:
<select
className="ml-2 px-2 py-1 border rounded"
value={analyticsRegion}
onChange={e => setAnalyticsRegion(e.target.value)}
>
{REGIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</label>
<label className="text-sm font-medium text-gray-700">Organization:
<input
type="text"
className="ml-2 px-2 py-1 border rounded"
placeholder="All organizations"
value={analyticsOrganization === 'all' ? '' : analyticsOrganization}
onChange={e => setAnalyticsOrganization(e.target.value || 'all')}
/>
</label>
</div>
{/* Export Buttons */}
<div className="mb-4 flex flex-wrap gap-2">
<button
className="bg-blue-600 text-white px-3 py-1 rounded hover:bg-blue-700 text-xs font-semibold"
onClick={() => searchAnalytics && exportCSV(searchAnalytics.searchTrends, ['date', 'searches', 'unique_users'], 'search_trends.csv')}
>
Export Trends (CSV)
</button>
<button
className="bg-green-600 text-white px-3 py-1 rounded hover:bg-green-700 text-xs font-semibold"
onClick={() => searchAnalytics && exportExcel(searchAnalytics.popularQueries, ['query', 'count'], 'top_queries.csv')}
>
Export Top Queries (Excel)
</button>
<button
className="bg-purple-600 text-white px-3 py-1 rounded hover:bg-purple-700 text-xs font-semibold"
onClick={() => searchAnalytics && exportPDF('Search Trends', searchAnalytics.searchTrends, ['date', 'searches', 'unique_users'], 'search_trends.pdf')}
>
Export Trends (PDF)
</button>
<button
className="bg-yellow-600 text-white px-3 py-1 rounded hover:bg-yellow-700 text-xs font-semibold"
onClick={() => searchAnalytics && exportPDF('Top Queries', searchAnalytics.popularQueries, ['query', 'count'], 'top_queries.pdf')}
>
Export Top Queries (PDF)
</button>
</div>
{searchAnalyticsLoading ? (
<div className="text-gray-500">Loading search analytics...</div>
) : searchAnalyticsError ? (
<div className="text-red-500">{searchAnalyticsError}</div>
) : searchAnalytics ? (
<>
<div className={`grid gap-4 mb-6 ${isMobile ? 'grid-cols-2' : 'grid-cols-4'}`}>
<div className="bg-blue-50 rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-blue-700">{searchAnalytics.totalSearches}</div>
<div className="text-xs text-gray-600 mt-1">Total Searches</div>
</div>
<div className="bg-green-50 rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-green-700">{searchAnalytics.uniqueUsers}</div>
<div className="text-xs text-gray-600 mt-1">Unique Users</div>
</div>
<div className="bg-purple-50 rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-purple-700">{searchAnalytics.averageSearchTime.toFixed(0)} ms</div>
<div className="text-xs text-gray-600 mt-1">Avg. Search Time</div>
</div>
<div className="bg-yellow-50 rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-yellow-700">{searchAnalytics.clickThroughRate.toFixed(1)}%</div>
<div className="text-xs text-gray-600 mt-1">Click-Through Rate</div>
</div>
</div>
{/* Charts Row */}
<div className={`grid gap-8 mb-8 ${isMobile ? 'grid-cols-1' : 'grid-cols-2'}`}>
{/* Line Chart: Trends */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-700 mb-2">Search Trends (Last 30 Days)</h3>
<ResponsiveContainer width="100%" height={260}>
<LineChart data={searchAnalytics.searchTrends} margin={{ top: 10, right: 20, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" tick={{ fontSize: 12 }} />
<YAxis tick={{ fontSize: 12 }} />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="searches" stroke="#2563eb" name="Searches" strokeWidth={2} />
<Line type="monotone" dataKey="unique_users" stroke="#16a34a" name="Unique Users" strokeWidth={2} />
</LineChart>
</ResponsiveContainer>
</div>
{/* Bar Chart: Top Queries */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-700 mb-2">Top Queries</h3>
<ResponsiveContainer width="100%" height={260}>
<BarChart data={searchAnalytics.popularQueries} layout="vertical" margin={{ top: 10, right: 20, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis type="number" tick={{ fontSize: 12 }} allowDecimals={false} />
<YAxis dataKey="query" type="category" tick={{ fontSize: 12 }} width={120} />
<Tooltip />
<Bar dataKey="count" fill="#6366f1" name="Count">
<LabelList dataKey="count" position="right" />
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
</div>
{/* Fallback Tables */}
<div className="mb-6">
<h3 className="font-semibold text-gray-700 mb-2">Top Queries (Table)</h3>
<div className="overflow-x-auto">
<table className="min-w-full text-sm border">
<thead>
<tr className="bg-gray-100">
<th className="px-3 py-2 text-left">Query</th>
<th className="px-3 py-2 text-left">Count</th>
</tr>
</thead>
<tbody>
{searchAnalytics.popularQueries.map((q: any, i: number) => (
<tr key={q.query} className={i % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
<td className="px-3 py-2 font-mono">{q.query}</td>
<td className="px-3 py-2">{q.count}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div>
<h3 className="font-semibold text-gray-700 mb-2">Search Trends (Table)</h3>
<div className="overflow-x-auto">
<table className="min-w-full text-xs border">
<thead>
<tr className="bg-gray-100">
<th className="px-2 py-1 text-left">Date</th>
<th className="px-2 py-1 text-left">Searches</th>
<th className="px-2 py-1 text-left">Unique Users</th>
</tr>
</thead>
<tbody>
{searchAnalytics.searchTrends.map((trend: any, i: number) => (
<tr key={trend.date} className={i % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
<td className="px-2 py-1 font-mono">{trend.date}</td>
<td className="px-2 py-1">{trend.searches}</td>
<td className="px-2 py-1">{trend.unique_users}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</>
) : null}
</div>
{/* Admin Links Grid */}
<div className={`grid gap-6 ${isMobile ? 'grid-cols-1' : 'md:grid-cols-2 lg:grid-cols-3'}`}>
{adminLinks.map((link, index) => (
<div
key={link.href}
className={`bg-gradient-to-br ${link.color} text-white rounded-xl shadow-xl hover:shadow-2xl transform hover:scale-105 transition-all duration-200 cursor-pointer ${isMobile ? 'p-6' : 'p-8'}`}
onClick={() => router.push(link.href)}
>
<div className={`text-center mb-4 ${isMobile ? 'text-4xl' : 'text-5xl'}`}>{link.icon}</div>
<h2 className={`font-bold text-center mb-3 ${isMobile ? 'text-lg' : 'text-xl'}`}>{link.title}</h2>
<p className={`text-center leading-relaxed opacity-90 ${isMobile ? 'text-sm' : ''}`}>{link.description}</p>
<div className="text-center mt-4">
<div className={`inline-block bg-white/20 rounded-lg font-semibold hover:bg-white/30 transition-all duration-200 ${isMobile ? 'px-4 py-2 text-sm' : 'px-6 py-3'}`}>
Access {link.title}
</div>
</div>
</div>
))}
</div>
{/* Recent Activity */}
<div className={`mt-8 bg-white rounded-xl shadow-xl border border-gray-200 ${isMobile ? 'p-6' : 'p-8'}`}>
<h2 className={`font-bold text-gray-800 mb-4 ${isMobile ? 'text-xl' : 'text-2xl'}`}>Recent Activity</h2>
<div className="space-y-4">
<div className={`flex items-center space-x-4 border-l-4 border-indigo-500 ${isMobile ? 'pl-3 py-2' : 'pl-4 py-3'} bg-indigo-50 rounded-r-lg`}>
<div className={`text-indigo-600 ${isMobile ? 'text-lg' : 'text-xl'}`}>⚖️</div>
<div className="flex-1">
<div className={`font-semibold text-gray-800 ${isMobile ? 'text-sm' : ''}`}>Bordeaux Class Action Case Active</div>
<div className={`text-gray-600 ${isMobile ? 'text-xs' : 'text-sm'}`}>Case management system operational</div>
</div>
</div>
<div className={`flex items-center space-x-4 border-l-4 border-blue-500 ${isMobile ? 'pl-3 py-2' : 'pl-4 py-3'} bg-blue-50 rounded-r-lg`}>
<div className={`text-blue-600 ${isMobile ? 'text-lg' : 'text-xl'}`}>👥</div>
<div className="flex-1">
<div className={`font-semibold text-gray-800 ${isMobile ? 'text-sm' : ''}`}>ADW Law Firm Configured</div>
<div className={`text-gray-600 ${isMobile ? 'text-xs' : 'text-sm'}`}>4 lawyers + Justin Wee (ADMIN) ready</div>
</div>
</div>
<div className={`flex items-center space-x-4 border-l-4 border-purple-500 ${isMobile ? 'pl-3 py-2' : 'pl-4 py-3'} bg-purple-50 rounded-r-lg`}>
<div className={`text-purple-600 ${isMobile ? 'text-lg' : 'text-xl'}`}>🤖</div>
<div className="flex-1">
<div className={`font-semibold text-gray-800 ${isMobile ? 'text-sm' : ''}`}>AI Assignment System Ready</div>
<div className={`text-gray-600 ${isMobile ? 'text-xs' : 'text-sm'}`}>Smart case matching enabled</div>
</div>
</div>
</div>
</div>
{/* Quick Actions */}
<div className={`mt-8 text-center space-y-4 ${isMobile ? '' : 'space-y-0 space-x-4 flex flex-wrap justify-center'}`}>
<button
onClick={() => router.push('/admin/case-management')}
className={`bg-indigo-600 text-white rounded-lg font-semibold shadow-lg hover:bg-indigo-700 hover:scale-105 transition-all duration-200 ${isMobile ? 'w-full px-6 py-3' : 'px-8 py-4'}`}
>
⚖️ Manage Cases
</button>
<button
onClick={() => router.push('/admin/applications')}
className={`bg-blue-600 text-white rounded-lg font-semibold shadow-lg hover:bg-blue-700 hover:scale-105 transition-all duration-200 ${isMobile ? 'w-full px-6 py-3' : 'px-8 py-4'}`}
>
Review Applications
</button>
<button
onClick={() => router.push('/admin/users')}
className={`bg-green-600 text-white rounded-lg font-semibold shadow-lg hover:bg-green-700 hover:scale-105 transition-all duration-200 ${isMobile ? 'w-full px-6 py-3' : 'px-8 py-4'}`}
>
Manage Users
</button>
<button
onClick={() => router.push('/admin/notifications')}
className={`bg-red-600 text-white rounded-lg font-semibold shadow-lg hover:bg-red-700 hover:scale-105 transition-all duration-200 ${isMobile ? 'w-full px-6 py-3' : 'px-8 py-4'}`}
>
📢 Public Notifications
</button>
<button
onClick={() => router.push('/admin/dashboard')}
className={`bg-purple-600 text-white rounded-lg font-semibold shadow-lg hover:bg-purple-700 hover:scale-105 transition-all duration-200 ${isMobile ? 'w-full px-6 py-3' : 'px-8 py-4'}`}
>
View Dashboard
</button>
</div>
</div>
</LayoutWithSidebar>
);
}