![]() 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/public_html/src/components/ |
import { useState, useEffect } from 'react';
import { useImpersonation } from '@/hooks/useImpersonation';
import { Search, User, Mail, Calendar, FileText, Gavel, Loader2 } from 'lucide-react';
import Image from 'next/image';
interface User {
id: string;
name: string;
email: string;
role: string;
image: string | null;
createdAt: string;
stats: {
registrations: number;
cases: number;
};
}
interface ImpersonationModalProps {
isOpen: boolean;
onClose: () => void;
}
const getRoleIcon = (role: string) => {
switch (role) {
case 'SUPERADMIN':
return '👑';
case 'ADMIN':
return '⚡';
case 'LAWYER':
return '👨💼';
case 'CLIENT':
return '👤';
case 'JURIST':
return '📚';
case 'JUDGE':
return '⚖️';
case 'MEDIATOR':
return '🤝';
case 'CONSULTANT':
return '💼';
case 'INVESTIGATOR':
return '🔍';
case 'EXPERT_WITNESS':
return '🎓';
case 'SUPPORT_STAFF':
return '🛠️';
case 'STUDENT':
return '🎒';
case 'NOTARY':
return '📜';
default:
return '👤';
}
};
const getRoleColor = (role: string) => {
switch (role) {
case 'SUPERADMIN':
return 'bg-purple-100 text-purple-800';
case 'ADMIN':
return 'bg-red-100 text-red-800';
case 'LAWYER':
return 'bg-blue-100 text-blue-800';
case 'CLIENT':
return 'bg-green-100 text-green-800';
case 'JURIST':
return 'bg-indigo-100 text-indigo-800';
case 'JUDGE':
return 'bg-yellow-100 text-yellow-800';
case 'MEDIATOR':
return 'bg-pink-100 text-pink-800';
case 'CONSULTANT':
return 'bg-orange-100 text-orange-800';
case 'INVESTIGATOR':
return 'bg-teal-100 text-teal-800';
case 'EXPERT_WITNESS':
return 'bg-cyan-100 text-cyan-800';
case 'SUPPORT_STAFF':
return 'bg-gray-100 text-gray-800';
case 'STUDENT':
return 'bg-emerald-100 text-emerald-800';
case 'NOTARY':
return 'bg-amber-100 text-amber-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
export default function ImpersonationModal({ isOpen, onClose }: ImpersonationModalProps) {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [roleFilter, setRoleFilter] = useState('all');
const { impersonateUser } = useImpersonation();
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (isOpen) {
fetchUsers();
}
}, [isOpen]);
const fetchUsers = async () => {
try {
setLoading(true);
const response = await fetch('/api/admin/users');
console.log('API Response status:', response.status);
console.log('API Response ok:', response.ok);
if (!response.ok) {
const errorData = await response.text();
console.error('API Error response:', errorData);
throw new Error(`Failed to fetch users: ${response.status} - ${errorData}`);
}
const data = await response.json();
setUsers(data);
} catch (error) {
console.error('Error fetching users:', error);
setError(error instanceof Error ? error.message : 'Failed to load users');
} finally {
setLoading(false);
}
};
const handleImpersonate = async (user: User) => {
try {
console.log('🔄 Impersonation modal - Starting impersonation for user:', user);
const success = await impersonateUser(user.id, user.name);
if (success) {
console.log('🔄 Impersonation modal - Success, closing modal');
onClose();
} else {
console.error('🔄 Impersonation modal - Failed to impersonate user');
}
} catch (error) {
console.error('🔄 Impersonation modal - Error during impersonation:', error);
// Don't close modal on error, let user try again
}
};
const filteredUsers = users.filter(user => {
const matchesSearch =
user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.role.toLowerCase().includes(searchTerm.toLowerCase());
const matchesRole = roleFilter === 'all' || user.role === roleFilter;
return matchesSearch && matchesRole;
});
const uniqueRoles = Array.from(new Set(users.map(user => user.role))).sort();
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-2xl max-w-4xl w-full mx-4 max-h-[80vh] flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-gray-200">
<div>
<h2 className="text-2xl font-bold text-gray-900">Impersonate User</h2>
<p className="text-gray-600 mt-1">Select a user to impersonate their account</p>
</div>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 transition-colors"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Filters */}
<div className="p-6 border-b border-gray-200 bg-gray-50">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<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="Search users by name, email, or role..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<select
value={roleFilter}
onChange={(e) => setRoleFilter(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">All Roles</option>
{uniqueRoles.map(role => (
<option key={role} value={role}>
{getRoleIcon(role)} {role}
</option>
))}
</select>
</div>
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-6">
{loading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
<span className="ml-2 text-gray-600">Loading users...</span>
</div>
) : filteredUsers.length === 0 ? (
<div className="text-center py-12">
<User className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No users found</h3>
<p className="text-gray-500">Try adjusting your search or filters.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredUsers.map((user) => (
<div
key={user.id}
className="bg-white border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow cursor-pointer"
onClick={() => handleImpersonate(user)}
>
<div className="flex items-start space-x-3">
<div className="flex-shrink-0">
{user.image ? (
<Image
src={user.image}
alt={user.name}
width={48}
height={48}
className="rounded-full"
/>
) : (
<div className="w-12 h-12 bg-gray-200 rounded-full flex items-center justify-center">
<User className="w-6 h-6 text-gray-500" />
</div>
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2 mb-1">
<h3 className="text-sm font-semibold text-gray-900 truncate">
{user.name}
</h3>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getRoleColor(user.role)}`}>
{getRoleIcon(user.role)} {user.role}
</span>
</div>
<div className="flex items-center text-xs text-gray-500 mb-2">
<Mail className="w-3 h-3 mr-1" />
<span className="truncate">{user.email}</span>
</div>
<div className="flex items-center text-xs text-gray-500 mb-2">
<Calendar className="w-3 h-3 mr-1" />
<span>Joined {new Date(user.createdAt).toLocaleDateString()}</span>
</div>
<div className="flex items-center space-x-4 text-xs text-gray-500">
<div className="flex items-center">
<FileText className="w-3 h-3 mr-1" />
<span>{user.stats.registrations} apps</span>
</div>
<div className="flex items-center">
<Gavel className="w-3 h-3 mr-1" />
<span>{user.stats.cases} cases</span>
</div>
</div>
</div>
</div>
<div className="mt-3 pt-3 border-t border-gray-100">
<button
className="w-full bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium py-2 px-3 rounded-md transition-colors"
>
Impersonate
</button>
</div>
</div>
))}
</div>
)}
</div>
{/* Footer */}
<div className="p-6 border-t border-gray-200 bg-gray-50">
<div className="flex justify-between items-center">
<div className="text-sm text-gray-600">
{filteredUsers.length} user{filteredUsers.length !== 1 ? 's' : ''} found
</div>
<button
onClick={onClose}
className="px-4 py-2 bg-gray-300 hover:bg-gray-400 text-gray-800 rounded-md transition-colors"
>
Cancel
</button>
</div>
</div>
</div>
</div>
);
}