![]() 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/domains/lavocat.ca/private_html/src/pages/admin/ |
import React, { useState, useEffect } from 'react';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import LayoutWithSidebar from '@/components/LayoutWithSidebar';
import {
Eye,
CheckCircle,
XCircle,
Clock,
Download,
FileText,
Search,
Filter,
RefreshCw,
MessageSquare
} from 'lucide-react';
import { format } from 'date-fns';
import { fr } from 'date-fns/locale';
interface ManualVerification {
id: string;
email: string;
name: string;
fileName: string;
filePath: string;
uploadedAt: string;
status: 'pending' | 'approved' | 'rejected';
notes?: string;
}
const ManualVerificationsPage: React.FC = () => {
const { data: session, status } = useSession();
const router = useRouter();
const [verifications, setVerifications] = useState<ManualVerification[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<'all' | 'pending' | 'approved' | 'rejected'>('all');
const [selectedVerification, setSelectedVerification] = useState<ManualVerification | null>(null);
const [showPreview, setShowPreview] = useState(false);
const [notes, setNotes] = useState('');
const [updating, setUpdating] = useState<string | null>(null);
useEffect(() => {
if (status === 'loading') return;
if (!session?.user || !['ADMIN', 'SUPERADMIN'].includes(session.user.role)) {
router.push('/auth/login');
return;
}
fetchVerifications();
}, [session, status, router]);
const fetchVerifications = async () => {
try {
const response = await fetch('/api/admin/manual-verifications');
if (response.ok) {
const data = await response.json();
setVerifications(data.verifications || []);
}
} catch (error) {
console.error('Error fetching verifications:', error);
} finally {
setLoading(false);
}
};
const updateVerificationStatus = async (id: string, status: 'approved' | 'rejected') => {
setUpdating(id);
try {
const response = await fetch('/api/admin/manual-verifications', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, status, notes })
});
if (response.ok) {
setVerifications(prev =>
prev.map(v => v.id === id ? { ...v, status, notes } : v)
);
setSelectedVerification(null);
setNotes('');
}
} catch (error) {
console.error('Error updating verification:', error);
} finally {
setUpdating(null);
}
};
const filteredVerifications = verifications.filter(verification => {
const matchesSearch =
verification.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
verification.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
verification.fileName.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || verification.status === statusFilter;
return matchesSearch && matchesStatus;
});
const getStatusBadge = (status: string) => {
switch (status) {
case 'pending':
return <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800"><Clock className="w-3 h-3 mr-1" />En attente</span>;
case 'approved':
return <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"><CheckCircle className="w-3 h-3 mr-1" />Approuvé</span>;
case 'rejected':
return <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800"><XCircle className="w-3 h-3 mr-1" />Rejeté</span>;
default:
return null;
}
};
if (status === 'loading' || loading) {
return (
<LayoutWithSidebar>
<div className="flex justify-center items-center min-h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-600"></div>
</div>
</LayoutWithSidebar>
);
}
if (!session?.user || !['ADMIN', 'SUPERADMIN'].includes(session.user.role)) {
return null;
}
return (
<LayoutWithSidebar>
<div className="max-w-7xl mx-auto p-6">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white flex items-center">
<FileText className="w-8 h-8 mr-3 text-blue-600" />
Vérifications Manuelles des Avocats
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-2">
Gérez les documents de vérification uploadés par les avocats
</p>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<div className="flex items-center">
<div className="p-2 bg-blue-100 dark:bg-blue-900/20 rounded-lg">
<FileText className="w-6 h-6 text-blue-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Total</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">{verifications.length}</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<div className="flex items-center">
<div className="p-2 bg-yellow-100 dark:bg-yellow-900/20 rounded-lg">
<Clock className="w-6 h-6 text-yellow-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">En attente</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">
{verifications.filter(v => v.status === 'pending').length}
</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<div className="flex items-center">
<div className="p-2 bg-green-100 dark:bg-green-900/20 rounded-lg">
<CheckCircle className="w-6 h-6 text-green-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Approuvés</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">
{verifications.filter(v => v.status === 'approved').length}
</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<div className="flex items-center">
<div className="p-2 bg-red-100 dark:bg-red-900/20 rounded-lg">
<XCircle className="w-6 h-6 text-red-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Rejetés</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">
{verifications.filter(v => v.status === 'rejected').length}
</p>
</div>
</div>
</div>
</div>
{/* Filters and Search */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6 mb-6">
<div className="flex flex-col md:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<input
type="text"
placeholder="Rechercher par nom, email ou fichier..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white"
/>
</div>
</div>
<div className="flex gap-2">
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as any)}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white"
>
<option value="all">Tous les statuts</option>
<option value="pending">En attente</option>
<option value="approved">Approuvés</option>
<option value="rejected">Rejetés</option>
</select>
<button
onClick={fetchVerifications}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center"
>
<RefreshCw className="w-4 h-4 mr-2" />
Actualiser
</button>
</div>
</div>
</div>
{/* Verifications Table */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Avocat
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Document
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Date d'upload
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Statut
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{filteredVerifications.map((verification) => (
<tr key={verification.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
<td className="px-6 py-4 whitespace-nowrap">
<div>
<div className="text-sm font-medium text-gray-900 dark:text-white">
{verification.name}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{verification.email}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900 dark:text-white">
{verification.fileName}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{format(new Date(verification.uploadedAt), 'dd/MM/yyyy HH:mm', { locale: fr })}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getStatusBadge(verification.status)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex items-center space-x-2">
<button
onClick={() => {
setSelectedVerification(verification);
setShowPreview(true);
}}
className="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300"
title="Voir le document"
>
<Eye className="w-4 h-4" />
</button>
<a
href={verification.filePath}
download
className="text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300"
title="Télécharger"
>
<Download className="w-4 h-4" />
</a>
{verification.status === 'pending' && (
<>
<button
onClick={() => updateVerificationStatus(verification.id, 'approved')}
disabled={updating === verification.id}
className="text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300 disabled:opacity-50"
title="Approuver"
>
<CheckCircle className="w-4 h-4" />
</button>
<button
onClick={() => updateVerificationStatus(verification.id, 'rejected')}
disabled={updating === verification.id}
className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 disabled:opacity-50"
title="Rejeter"
>
<XCircle className="w-4 h-4" />
</button>
</>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{filteredVerifications.length === 0 && (
<div className="text-center py-12">
<FileText className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-white">
Aucune vérification trouvée
</h3>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
{searchTerm || statusFilter !== 'all'
? 'Essayez de modifier vos filtres de recherche.'
: 'Aucun document de vérification n\'a été uploadé pour le moment.'
}
</p>
</div>
)}
</div>
{/* Document Preview Modal */}
{showPreview && selectedVerification && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg max-w-4xl w-full mx-4 max-h-[90vh] overflow-hidden">
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-medium text-gray-900 dark:text-white">
Aperçu du document - {selectedVerification.name}
</h3>
<button
onClick={() => {
setShowPreview(false);
setSelectedVerification(null);
}}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<XCircle className="w-6 h-6" />
</button>
</div>
<div className="p-6">
<div className="mb-4">
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Informations</h4>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-500 dark:text-gray-400">Email:</span>
<span className="ml-2 text-gray-900 dark:text-white">{selectedVerification.email}</span>
</div>
<div>
<span className="text-gray-500 dark:text-gray-400">Fichier:</span>
<span className="ml-2 text-gray-900 dark:text-white">{selectedVerification.fileName}</span>
</div>
<div>
<span className="text-gray-500 dark:text-gray-400">Date d'upload:</span>
<span className="ml-2 text-gray-900 dark:text-white">
{format(new Date(selectedVerification.uploadedAt), 'dd/MM/yyyy HH:mm', { locale: fr })}
</span>
</div>
<div>
<span className="text-gray-500 dark:text-gray-400">Statut:</span>
<span className="ml-2">{getStatusBadge(selectedVerification.status)}</span>
</div>
</div>
</div>
{/* Document Preview */}
<div className="mb-4">
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Aperçu</h4>
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 bg-gray-50 dark:bg-gray-700">
<iframe
src={selectedVerification.filePath}
className="w-full h-96 border-0"
title="Document preview"
/>
</div>
</div>
{/* Notes and Actions */}
{selectedVerification.status === 'pending' && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Notes (optionnel)
</label>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
rows={3}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white"
placeholder="Ajoutez des notes sur cette vérification..."
/>
</div>
<div className="flex justify-end space-x-3">
<button
onClick={() => updateVerificationStatus(selectedVerification.id, 'approved')}
disabled={updating === selectedVerification.id}
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 flex items-center"
>
<CheckCircle className="w-4 h-4 mr-2" />
Approuver
</button>
<button
onClick={() => updateVerificationStatus(selectedVerification.id, 'rejected')}
disabled={updating === selectedVerification.id}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50 flex items-center"
>
<XCircle className="w-4 h-4 mr-2" />
Rejeter
</button>
</div>
</div>
)}
</div>
</div>
</div>
)}
</div>
</LayoutWithSidebar>
);
};
export default ManualVerificationsPage;