![]() 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/lawyer/ |
import { useEffect, useState } from 'react';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import LayoutWithSidebar from '@/components/LayoutWithSidebar';
import { canAccessLawyer } from '@/lib/auth-utils';
import { format } from 'date-fns';
import Link from 'next/link';
import toast from 'react-hot-toast';
import dynamic from 'next/dynamic';
const PrivateChat = dynamic(() => import('@/components/Chat/PrivateChat'), { ssr: false });
interface Registration {
id: string;
firstName: string;
lastName: string;
email: string;
phone: string;
status: string;
createdAt: string;
updatedAt: string;
userId: string;
detaineeInfo: {
name: string;
facility: string;
inmateId: string;
} | null;
legalCase: {
id: string;
title: string;
caseNumber: string;
logoUrl?: string;
leadLawyer: {
id: string;
name: string;
email: string;
};
} | null;
user: {
id: string;
name: string;
email: string;
} | null;
creator: {
id: string;
name: string;
email: string;
} | null;
documents: {
id: string;
name: string;
type: string;
}[];
}
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';
}
};
const getStatusLabel = (status: string) => {
const statusMap: { [key: string]: string } = {
PENDING: 'Pending',
APPROVED: 'Approved',
REJECTED: 'Rejected',
MISSING_DOCUMENTS: 'Missing Documents',
DOCUMENTS_UNDER_REVIEW: 'Documents Under Review',
ADDITIONAL_INFO_NEEDED: 'Additional Info Needed',
VERIFICATION_IN_PROGRESS: 'Verification in Progress',
LAWYER_VERIFICATION: 'Lawyer Verification',
FACILITY_VERIFICATION: 'Facility Verification',
DOCUMENTS_EXPIRED: 'Documents Expired',
DOCUMENTS_INCOMPLETE: 'Documents Incomplete',
INFORMATION_MISMATCH: 'Information Mismatch',
PENDING_PAYMENT: 'Pending Payment',
PAYMENT_RECEIVED: 'Payment Received',
PENDING_LAWYER_APPROVAL: 'Pending Lawyer Approval',
PENDING_FACILITY_APPROVAL: 'Pending Facility Approval',
ON_HOLD: 'On Hold',
ESCALATED: 'Escalated',
FINAL_REVIEW: 'Final Review',
COMPLETED: 'Completed',
WebAd: 'Web Advertisement'
};
return statusMap[status] || status;
};
// Status icon component
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>,
WebAd: <span className="text-pink-500 text-2xl">π</span>,
};
return iconMap[status] || <span className="text-gray-400 text-2xl">π</span>;
};
const LawyerApplications = () => {
const { data: session, status } = useSession();
const router = useRouter();
const [registrations, setRegistrations] = useState<Registration[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('all');
const [caseFilter, setCaseFilter] = useState('all');
const [sortBy, setSortBy] = useState('createdAt');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
const [showPrivateChat, setShowPrivateChat] = useState<{ open: boolean; registrationId: string | null }>({
open: false,
registrationId: null,
});
useEffect(() => {
if (status === 'loading') return;
if (status === 'unauthenticated' || (status === 'authenticated' && !canAccessLawyer(session))) {
router.push('/auth/login');
return;
}
fetchApplications();
}, [session, status, router]);
const fetchApplications = async () => {
try {
setLoading(true);
const response = await fetch('/api/lawyer/applications');
if (!response.ok) {
throw new Error('Failed to fetch applications');
}
const data = await response.json();
setRegistrations(data);
} catch (err) {
setError('Failed to load applications');
console.error('Error fetching applications:', err);
} finally {
setLoading(false);
}
};
const handleStatusUpdate = async (registrationId: string, newStatus: string) => {
try {
const response = await fetch(`/api/lawyer/applications/${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 } : reg
));
toast.success('Status updated successfully');
} catch (err) {
toast.error('Failed to update status');
console.error('Error updating status:', err);
}
};
// Filter and sort registrations
const filteredRegistrations = registrations
.filter(reg => {
const matchesSearch =
reg.firstName.toLowerCase().includes(searchTerm.toLowerCase()) ||
reg.lastName.toLowerCase().includes(searchTerm.toLowerCase()) ||
reg.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
reg.detaineeInfo?.name.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || reg.status === statusFilter;
const matchesCase = caseFilter === 'all' || reg.legalCase?.id === caseFilter;
return matchesSearch && matchesStatus && matchesCase;
})
.sort((a, b) => {
let aValue: any, bValue: any;
switch (sortBy) {
case 'name':
aValue = `${a.firstName} ${a.lastName}`;
bValue = `${b.firstName} ${b.lastName}`;
break;
case 'email':
aValue = a.email;
bValue = b.email;
break;
case 'status':
aValue = a.status;
bValue = b.status;
break;
case 'createdAt':
default:
aValue = new Date(a.createdAt);
bValue = new Date(b.createdAt);
break;
}
if (sortOrder === 'asc') {
return aValue > bValue ? 1 : -1;
} else {
return aValue < bValue ? 1 : -1;
}
});
// Get unique cases and statuses for filters
const uniqueCases = Array.from(new Set(registrations.map(reg => reg.legalCase?.id).filter(Boolean)));
const uniqueStatuses = Array.from(new Set(registrations.map(reg => reg.status)));
if (status === 'loading' || loading) {
return (
<LayoutWithSidebar>
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading applications...</p>
</div>
</div>
</LayoutWithSidebar>
);
}
return (
<LayoutWithSidebar>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Header */}
<div className="mb-8">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold text-gray-900">My Applications</h1>
<p className="mt-2 text-gray-600">Manage applications for cases where you are the lead lawyer</p>
</div>
<div className="flex gap-2">
<Link
href="/hire/new-case"
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition-colors font-semibold shadow"
>
+ New Application
</Link>
<Link
href="/lawyer/dashboard"
className="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg transition-colors"
>
Back to Dashboard
</Link>
</div>
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold text-gray-900">Total Applications</h3>
<p className="text-3xl font-bold text-blue-600">{registrations.length}</p>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold text-gray-900">Pending Review</h3>
<p className="text-3xl font-bold text-yellow-600">
{registrations.filter(r => r.status === 'PENDING').length}
</p>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold text-gray-900">Approved</h3>
<p className="text-3xl font-bold text-green-600">
{registrations.filter(r => r.status === 'APPROVED').length}
</p>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold text-gray-900">Active Cases</h3>
<p className="text-3xl font-bold text-purple-600">
{uniqueCases.length}
</p>
</div>
</div>
{/* Filters */}
<div className="bg-white rounded-lg shadow p-6 mb-8">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Filters</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Search */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Search</label>
<input
type="text"
placeholder="Search by name, email, or detainee..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{/* Status Filter */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Status</label>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="all">All Statuses</option>
{uniqueStatuses.map(status => (
<option key={status} value={status}>{getStatusLabel(status)}</option>
))}
</select>
</div>
{/* Sort By */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Sort By</label>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="createdAt">Date Created</option>
<option value="name">Name</option>
<option value="email">Email</option>
<option value="status">Status</option>
</select>
</div>
{/* Sort Order */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Order</label>
<select
value={sortOrder}
onChange={(e) => setSortOrder(e.target.value as 'asc' | 'desc')}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="desc">Newest First</option>
<option value="asc">Oldest First</option>
</select>
</div>
</div>
</div>
{/* Applications Cards */}
<div className="bg-white rounded-lg shadow p-6">
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-semibold text-gray-900">
Applications ({filteredRegistrations.length})
</h2>
</div>
{error && (
<div className="p-4 bg-red-50 border-b border-red-200 mb-6">
<p className="text-red-600">{error}</p>
</div>
)}
{filteredRegistrations.length === 0 ? (
<div className="text-center py-12">
<div className="bg-gray-100 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">No applications found</h3>
<p className="text-gray-500">Try adjusting your filters to see more results.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{filteredRegistrations.map(registration => (
<div key={registration.id} className="bg-white rounded-2xl shadow-xl flex flex-col border-2 p-6 min-h-[400px] border-primary/10">
{/* Status Icon & Badge */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center">
<StatusIcon status={registration.status} />
<span className={`ml-3 px-3 py-1 rounded-full text-sm font-bold ${getStatusColor(registration.status)}`}>
{getStatusLabel(registration.status)}
</span>
</div>
{registration.status === 'WebAd' && (
<span className="px-2 py-1 rounded-full bg-pink-200 text-pink-800 text-xs font-bold">
Web Ad
</span>
)}
</div>
{/* Main Info */}
<div className="mb-3">
<div className="font-bold text-xl">{registration.firstName} {registration.lastName}</div>
<div className="text-sm text-gray-500">{registration.email}</div>
<div className="text-sm text-gray-500">{registration.phone}</div>
</div>
{/* Detainee Info */}
<div className="mb-3">
<div className="text-sm">
<span className="font-semibold">Detainee:</span> {registration.detaineeInfo?.name || '-'}
</div>
<div className="text-sm text-gray-600">
{registration.detaineeInfo?.facility || '-'}
</div>
{registration.detaineeInfo?.inmateId && (
<div className="text-sm text-gray-600">ID: {registration.detaineeInfo.inmateId}</div>
)}
</div>
{/* Case Info */}
{registration.legalCase && (
<div className="mb-3">
<div className="flex items-center gap-2 mb-2">
{registration.legalCase.logoUrl && (
<img
src={registration.legalCase.logoUrl}
alt={`${registration.legalCase.title} Logo`}
className="w-8 h-8 rounded object-cover border border-gray-200"
/>
)}
<div className="text-sm">
<span className="font-semibold">Case:</span> {registration.legalCase.title}
</div>
</div>
<div className="text-sm text-gray-600">
{registration.legalCase.caseNumber}
</div>
</div>
)}
{/* Documents */}
{registration.documents && registration.documents.length > 0 && (
<div className="mb-4">
<h4 className="text-sm font-semibold mb-2">Documents ({registration.documents.length})</h4>
<div className="space-y-1">
{registration.documents.slice(0, 3).map((doc) => (
<div key={doc.id} className="flex items-center text-blue-600 text-sm">
<svg className="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
{doc.name}
</div>
))}
{registration.documents.length > 3 && (
<div className="text-xs text-gray-500">+{registration.documents.length - 3} more</div>
)}
</div>
</div>
)}
{/* Status Dropdown */}
<div className="mt-4 bg-gray-50 p-3 rounded-lg border-2 border-dashed">
<label className="block text-sm font-semibold text-gray-700 mb-2">Quick Status Update</label>
<select
value={registration.status}
onChange={(e) => handleStatusUpdate(registration.id, e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-primary bg-white font-medium"
>
<option value="PENDING">β³ Pending</option>
<option value="APPROVED">β
Approved</option>
<option value="REJECTED">β Rejected</option>
<option value="MISSING_DOCUMENTS">π Missing Documents</option>
<option value="DOCUMENTS_UNDER_REVIEW">π Documents Under Review</option>
<option value="ADDITIONAL_INFO_NEEDED">βΉοΈ Additional Info Needed</option>
<option value="VERIFICATION_IN_PROGRESS">π Verification in Progress</option>
<option value="LAWYER_VERIFICATION">π¨βπΌ Lawyer Verification</option>
<option value="FACILITY_VERIFICATION">π’ Facility Verification</option>
<option value="DOCUMENTS_EXPIRED">β° Documents Expired</option>
<option value="DOCUMENTS_INCOMPLETE">π Documents Incomplete</option>
<option value="INFORMATION_MISMATCH">β οΈ Information Mismatch</option>
<option value="PENDING_PAYMENT">π° Pending Payment</option>
<option value="PAYMENT_RECEIVED">π³ Payment Received</option>
<option value="PENDING_LAWYER_APPROVAL">π¨βπΌ Pending Lawyer Approval</option>
<option value="PENDING_FACILITY_APPROVAL">π’ Pending Facility Approval</option>
<option value="ON_HOLD">βΈοΈ On Hold</option>
<option value="ESCALATED">π¨ Escalated</option>
<option value="FINAL_REVIEW">π Final Review</option>
<option value="COMPLETED">β
Completed</option>
<option value="WebAd">π Web Advertisement</option>
</select>
</div>
{/* Actions */}
<div className="mt-auto pt-4 grid grid-cols-3 gap-2">
<Link
href={`/lawyer/registrations/${registration.id}`}
className="font-semibold text-sm px-4 py-2.5 rounded-lg flex items-center justify-center transition-all duration-200 shadow-md hover:shadow-lg transform hover:scale-105 text-white"
style={{
background: `linear-gradient(135deg, #8b5cf6, #7c3aed)`,
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = `linear-gradient(135deg, #7c3aed, #8b5cf6)`;
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = `linear-gradient(135deg, #8b5cf6, #7c3aed)`;
}}
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
View
</Link>
<button
onClick={() => setShowPrivateChat({ open: true, registrationId: registration.id })}
className="font-semibold text-sm px-4 py-2.5 rounded-lg flex items-center justify-center transition-all duration-200 shadow-md hover:shadow-lg transform hover:scale-105 text-white"
style={{
background: `linear-gradient(135deg, #3b82f6, #2563eb)`,
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = `linear-gradient(135deg, #2563eb, #3b82f6)`;
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = `linear-gradient(135deg, #3b82f6, #2563eb)`;
}}
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
Chat
</button>
<Link
href={`/user/registrations/${registration.id}`}
className="bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 text-white font-semibold text-sm px-4 py-2.5 rounded-lg flex items-center justify-center transition-all duration-200 shadow-md hover:shadow-lg transform hover:scale-105"
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Edit
</Link>
</div>
</div>
))}
</div>
)}
</div>
{/* Private Chat Modal */}
{showPrivateChat.open && showPrivateChat.registrationId && (
<PrivateChat
registrationId={showPrivateChat.registrationId}
onClose={() => setShowPrivateChat({ open: false, registrationId: null })}
/>
)}
</div>
</LayoutWithSidebar>
);
};
export default LawyerApplications;