![]() 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 React, { useState, useEffect } from 'react';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import LayoutWithSidebar from '@/components/LayoutWithSidebar';
import { toast } from 'react-hot-toast';
import {
User,
Mail,
Phone,
Calendar,
DollarSign,
TrendingUp,
Star,
Search,
Filter,
Plus,
Eye,
MessageSquare,
FileText,
Clock,
CheckCircle,
AlertCircle,
Users,
MapPin,
Building,
RefreshCw,
Edit,
Trash2,
ExternalLink
} from 'lucide-react';
interface Client {
id: string;
name: string;
email: string;
phone?: string;
profilePicture?: string;
organization?: string;
address?: string;
city?: string;
province?: string;
postalCode?: string;
country?: string;
createdAt: string;
updatedAt: string;
relationships: ClientRelationship[];
consultations: Consultation[];
cases: Case[];
}
interface ClientRelationship {
id: string;
relationshipType: string;
startDate: string;
endDate?: string;
isActive: boolean;
totalHoursWorked?: number;
totalFeePaid?: number;
feeStructure?: string;
caseStatus?: string;
outcomeDescription?: string;
settlementAmount?: number;
clientSatisfaction?: number;
wouldRecommend?: boolean;
impactLevel?: string;
clientReview?: string;
lawyerNotes?: string;
publicTestimonial?: string;
isTestimonialPublic: boolean;
createdAt: string;
updatedAt: string;
}
interface Consultation {
id: string;
preferredDate: string;
preferredTime: string;
duration: number;
consultationType: string;
status: string;
hourlyRate?: number;
totalAmount?: number;
createdAt: string;
}
interface Case {
id: string;
title: string;
status: string;
caseType: string;
jurisdiction?: string;
createdAt: string;
updatedAt: string;
}
const LawyerClients: React.FC = () => {
const { data: session, status } = useSession();
const router = useRouter();
const [clients, setClients] = useState<Client[]>([]);
const [loading, setLoading] = useState(true);
const [stats, setStats] = useState({
total: 0,
active: 0,
newThisMonth: 0,
totalRevenue: 0,
averageSatisfaction: 0
});
const [searchTerm, setSearchTerm] = useState('');
const [selectedStatus, setSelectedStatus] = useState('all');
const [selectedClient, setSelectedClient] = useState<Client | null>(null);
const [showDetailsModal, setShowDetailsModal] = useState(false);
const [activeTab, setActiveTab] = useState('overview');
useEffect(() => {
if (status === 'loading') return;
if (!session || !['LAWYER', 'ADMIN', 'SUPERADMIN'].includes(session.user.role)) {
router.push('/');
return;
}
fetchClients();
}, [session, status, router]);
const fetchClients = async () => {
try {
setLoading(true);
// Fetch client relationships for the lawyer
const response = await fetch('/api/lawyer/clients');
if (response.ok) {
const data = await response.json();
setClients(data.clients || []);
calculateStats(data.clients || []);
} else {
toast.error('Failed to load clients');
}
} catch (error) {
console.error('Error fetching clients:', error);
toast.error('Error loading clients');
} finally {
setLoading(false);
}
};
const calculateStats = (clients: Client[]) => {
const now = new Date();
const thisMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const stats = {
total: clients.length,
active: clients.filter(c => c.relationships.some(r => r.isActive)).length,
newThisMonth: clients.filter(c => new Date(c.createdAt) >= thisMonth).length,
totalRevenue: clients.reduce((sum, c) =>
sum + c.relationships.reduce((rSum, r) => rSum + (r.totalFeePaid || 0), 0), 0
),
averageSatisfaction: clients.length > 0 ?
clients.reduce((sum, c) =>
sum + c.relationships.reduce((rSum, r) => rSum + (r.clientSatisfaction || 0), 0), 0
) / clients.length : 0
};
setStats(stats);
};
const filteredClients = clients.filter(client => {
const matchesStatus = selectedStatus === 'all' ||
(selectedStatus === 'active' && client.relationships.some(r => r.isActive)) ||
(selectedStatus === 'inactive' && !client.relationships.some(r => r.isActive));
const matchesSearch = client.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
client.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
client.organization?.toLowerCase().includes(searchTerm.toLowerCase());
return matchesStatus && matchesSearch;
});
const getRelationshipStatusColor = (isActive: boolean) => {
return isActive ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800';
};
const getCaseStatusColor = (status: string) => {
switch (status.toLowerCase()) {
case 'active': return 'bg-green-100 text-green-800';
case 'pending': return 'bg-yellow-100 text-yellow-800';
case 'closed': return 'bg-gray-100 text-gray-800';
default: return 'bg-gray-100 text-gray-800';
}
};
const getConsultationStatusColor = (status: string) => {
switch (status) {
case 'PENDING': return 'bg-yellow-100 text-yellow-800';
case 'CONFIRMED': return 'bg-blue-100 text-blue-800';
case 'COMPLETED': return 'bg-green-100 text-green-800';
case 'CANCELLED': return 'bg-red-100 text-red-800';
default: return 'bg-gray-100 text-gray-800';
}
};
if (status === 'loading') {
return (
<LayoutWithSidebar>
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-teal-600"></div>
</div>
</LayoutWithSidebar>
);
}
if (!session || !['LAWYER', 'ADMIN', 'SUPERADMIN'].includes(session.user.role)) {
router.push('/');
return null;
}
return (
<LayoutWithSidebar>
<div className="max-w-7xl mx-auto px-4 py-8">
{/* Header */}
<div className="mb-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Clients</h1>
<p className="text-gray-600 mt-1">Manage your clients and communications</p>
</div>
<button
onClick={fetchClients}
className="inline-flex items-center px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors"
>
<RefreshCw className="h-4 w-4 mr-2" />
Refresh
</button>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-8">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="flex items-center">
<div className="p-2 bg-teal-100 rounded-lg">
<Users className="h-6 w-6 text-teal-600" />
</div>
<div className="ml-3">
<p className="text-sm font-medium text-gray-600">Total Clients</p>
<p className="text-2xl font-bold text-gray-900">{stats.total}</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="flex items-center">
<div className="p-2 bg-green-100 rounded-lg">
<CheckCircle className="h-6 w-6 text-green-600" />
</div>
<div className="ml-3">
<p className="text-sm font-medium text-gray-600">Active</p>
<p className="text-2xl font-bold text-gray-900">{stats.active}</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="flex items-center">
<div className="p-2 bg-blue-100 rounded-lg">
<TrendingUp className="h-6 w-6 text-blue-600" />
</div>
<div className="ml-3">
<p className="text-sm font-medium text-gray-600">New This Month</p>
<p className="text-2xl font-bold text-gray-900">{stats.newThisMonth}</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="flex items-center">
<div className="p-2 bg-emerald-100 rounded-lg">
<DollarSign className="h-6 w-6 text-emerald-600" />
</div>
<div className="ml-3">
<p className="text-sm font-medium text-gray-600">Total Revenue</p>
<p className="text-2xl font-bold text-gray-900">${stats.totalRevenue.toLocaleString()}</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="flex items-center">
<div className="p-2 bg-yellow-100 rounded-lg">
<Star className="h-6 w-6 text-yellow-600" />
</div>
<div className="ml-3">
<p className="text-sm font-medium text-gray-600">Avg Satisfaction</p>
<p className="text-2xl font-bold text-gray-900">{stats.averageSatisfaction.toFixed(1)}/5</p>
</div>
</div>
</div>
</div>
{/* Filters */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-8">
<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 h-4 w-4" />
<input
type="text"
placeholder="Search clients..."
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-teal-500 focus:border-transparent"
/>
</div>
</div>
<div className="flex gap-2">
<select
value={selectedStatus}
onChange={(e) => setSelectedStatus(e.target.value)}
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-transparent"
>
<option value="all">All Clients</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
</div>
</div>
{/* Clients List */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200">
{loading ? (
<div className="p-8 text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-teal-600 mx-auto"></div>
<p className="mt-2 text-gray-600">Loading clients...</p>
</div>
) : filteredClients.length === 0 ? (
<div className="p-8 text-center">
<Users className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No clients found</h3>
<p className="text-gray-600">
{searchTerm || selectedStatus !== 'all'
? 'Try adjusting your search or filters'
: 'You don\'t have any clients yet'
}
</p>
</div>
) : (
<div className="divide-y divide-gray-200">
{filteredClients.map((client) => (
<div key={client.id} className="p-6 hover:bg-gray-50 transition-colors">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<div className="w-12 h-12 bg-teal-100 rounded-full flex items-center justify-center">
{client.profilePicture ? (
<img
src={client.profilePicture}
alt={client.name}
className="w-12 h-12 rounded-full object-cover"
/>
) : (
<User className="h-6 w-6 text-teal-600" />
)}
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900">
{client.name}
</h3>
{client.organization && (
<p className="text-sm text-gray-600">{client.organization}</p>
)}
</div>
<span className={`px-2 py-1 text-xs font-medium rounded-full ${getRelationshipStatusColor(client.relationships.some(r => r.isActive))}`}>
{client.relationships.some(r => r.isActive) ? 'Active' : 'Inactive'}
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm text-gray-600 mb-3">
<div className="flex items-center gap-2">
<Mail className="h-4 w-4" />
<span>{client.email}</span>
</div>
{client.phone && (
<div className="flex items-center gap-2">
<Phone className="h-4 w-4" />
<span>{client.phone}</span>
</div>
)}
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4" />
<span>Since {new Date(client.createdAt).toLocaleDateString()}</span>
</div>
</div>
{/* Quick Stats */}
<div className="flex items-center gap-4 text-sm text-gray-600">
<div className="flex items-center gap-1">
<FileText className="h-4 w-4" />
<span>{client.cases.length} cases</span>
</div>
<div className="flex items-center gap-1">
<MessageSquare className="h-4 w-4" />
<span>{client.consultations.length} consultations</span>
</div>
<div className="flex items-center gap-1">
<DollarSign className="h-4 w-4" />
<span>${client.relationships.reduce((sum, r) => sum + (r.totalFeePaid || 0), 0).toLocaleString()}</span>
</div>
{client.relationships.some(r => r.clientSatisfaction) && (
<div className="flex items-center gap-1">
<Star className="h-4 w-4" />
<span>
{client.relationships
.filter(r => r.clientSatisfaction)
.reduce((sum, r) => sum + (r.clientSatisfaction || 0), 0) /
client.relationships.filter(r => r.clientSatisfaction).length
}/5
</span>
</div>
)}
</div>
</div>
<div className="flex items-center gap-2 ml-4">
<button
onClick={() => {
setSelectedClient(client);
setShowDetailsModal(true);
}}
className="inline-flex items-center px-3 py-1 text-sm text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-md transition-colors"
>
<Eye className="h-4 w-4 mr-1" />
View
</button>
<button
onClick={() => router.push(`/messages?userId=${client.id}`)}
className="inline-flex items-center px-3 py-1 text-sm text-green-600 hover:text-green-800 hover:bg-green-50 rounded-md transition-colors"
>
<MessageSquare className="h-4 w-4 mr-1" />
Message
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
{/* Client Details Modal */}
{showDetailsModal && selectedClient && (
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div className="bg-white rounded-xl shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold text-gray-900">Client Details</h2>
<button
onClick={() => setShowDetailsModal(false)}
className="text-gray-400 hover:text-gray-600"
>
<AlertCircle className="h-6 w-6" />
</button>
</div>
</div>
<div className="p-6">
{/* Tabs */}
<div className="border-b border-gray-200 mb-6">
<nav className="-mb-px flex space-x-8">
{['overview', 'cases', 'consultations', 'relationships'].map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`py-2 px-1 border-b-2 font-medium text-sm capitalize ${
activeTab === tab
? 'border-teal-500 text-teal-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
{tab}
</button>
))}
</nav>
</div>
{/* Tab Content */}
{activeTab === 'overview' && (
<div className="space-y-6">
{/* Client Information */}
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">Client Information</h3>
<div className="bg-gray-50 rounded-lg p-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<p className="text-sm font-medium text-gray-600">Name</p>
<p className="text-gray-900">{selectedClient.name}</p>
</div>
<div>
<p className="text-sm font-medium text-gray-600">Email</p>
<p className="text-gray-900">{selectedClient.email}</p>
</div>
{selectedClient.phone && (
<div>
<p className="text-sm font-medium text-gray-600">Phone</p>
<p className="text-gray-900">{selectedClient.phone}</p>
</div>
)}
{selectedClient.organization && (
<div>
<p className="text-sm font-medium text-gray-600">Organization</p>
<p className="text-gray-900">{selectedClient.organization}</p>
</div>
)}
{selectedClient.address && (
<div className="md:col-span-2">
<p className="text-sm font-medium text-gray-600">Address</p>
<p className="text-gray-900">{selectedClient.address}</p>
{selectedClient.city && (
<p className="text-gray-900">{selectedClient.city}, {selectedClient.province} {selectedClient.postalCode}</p>
)}
</div>
)}
</div>
</div>
</div>
{/* Quick Stats */}
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">Quick Stats</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-blue-50 rounded-lg p-4 text-center">
<p className="text-2xl font-bold text-blue-600">{selectedClient.cases.length}</p>
<p className="text-sm text-gray-600">Total Cases</p>
</div>
<div className="bg-green-50 rounded-lg p-4 text-center">
<p className="text-2xl font-bold text-green-600">{selectedClient.consultations.length}</p>
<p className="text-sm text-gray-600">Consultations</p>
</div>
<div className="bg-emerald-50 rounded-lg p-4 text-center">
<p className="text-2xl font-bold text-emerald-600">
${selectedClient.relationships.reduce((sum, r) => sum + (r.totalFeePaid || 0), 0).toLocaleString()}
</p>
<p className="text-sm text-gray-600">Total Revenue</p>
</div>
<div className="bg-yellow-50 rounded-lg p-4 text-center">
<p className="text-2xl font-bold text-yellow-600">
{selectedClient.relationships.some(r => r.clientSatisfaction)
? (selectedClient.relationships
.filter(r => r.clientSatisfaction)
.reduce((sum, r) => sum + (r.clientSatisfaction || 0), 0) /
selectedClient.relationships.filter(r => r.clientSatisfaction).length
).toFixed(1)
: 'N/A'
}
</p>
<p className="text-sm text-gray-600">Satisfaction</p>
</div>
</div>
</div>
</div>
)}
{activeTab === 'cases' && (
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">Cases</h3>
{selectedClient.cases.length === 0 ? (
<p className="text-gray-600">No cases found for this client.</p>
) : (
<div className="space-y-3">
{selectedClient.cases.map((case_) => (
<div key={case_.id} className="border rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<h4 className="font-medium text-gray-900">{case_.title}</h4>
<p className="text-sm text-gray-600">{case_.caseType} • {case_.jurisdiction}</p>
</div>
<div className="flex items-center gap-2">
<span className={`px-2 py-1 text-xs font-medium rounded-full ${getCaseStatusColor(case_.status)}`}>
{case_.status}
</span>
<button
onClick={() => router.push(`/public/cases/${case_.id}`)}
className="text-blue-600 hover:text-blue-800"
>
<ExternalLink className="h-4 w-4" />
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
)}
{activeTab === 'consultations' && (
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">Consultations</h3>
{selectedClient.consultations.length === 0 ? (
<p className="text-gray-600">No consultations found for this client.</p>
) : (
<div className="space-y-3">
{selectedClient.consultations.map((consultation) => (
<div key={consultation.id} className="border rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<h4 className="font-medium text-gray-900">
{consultation.consultationType.replace('_', ' ')}
</h4>
<p className="text-sm text-gray-600">
{new Date(consultation.preferredDate).toLocaleDateString()} at {consultation.preferredTime}
</p>
</div>
<div className="flex items-center gap-2">
<span className={`px-2 py-1 text-xs font-medium rounded-full ${getConsultationStatusColor(consultation.status)}`}>
{consultation.status}
</span>
{consultation.totalAmount && (
<span className="text-sm text-gray-600">${consultation.totalAmount}</span>
)}
</div>
</div>
</div>
))}
</div>
)}
</div>
)}
{activeTab === 'relationships' && (
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">Relationships</h3>
{selectedClient.relationships.length === 0 ? (
<p className="text-gray-600">No relationships found for this client.</p>
) : (
<div className="space-y-3">
{selectedClient.relationships.map((relationship) => (
<div key={relationship.id} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<h4 className="font-medium text-gray-900">{relationship.relationshipType}</h4>
<span className={`px-2 py-1 text-xs font-medium rounded-full ${getRelationshipStatusColor(relationship.isActive)}`}>
{relationship.isActive ? 'Active' : 'Inactive'}
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-gray-600">
<div>
<p className="font-medium">Start Date</p>
<p>{new Date(relationship.startDate).toLocaleDateString()}</p>
</div>
{relationship.endDate && (
<div>
<p className="font-medium">End Date</p>
<p>{new Date(relationship.endDate).toLocaleDateString()}</p>
</div>
)}
{relationship.totalHoursWorked && (
<div>
<p className="font-medium">Hours Worked</p>
<p>{relationship.totalHoursWorked}</p>
</div>
)}
{relationship.totalFeePaid && (
<div>
<p className="font-medium">Total Fee Paid</p>
<p>${relationship.totalFeePaid}</p>
</div>
)}
{relationship.clientSatisfaction && (
<div>
<p className="font-medium">Satisfaction</p>
<div className="flex items-center gap-1">
<span>{relationship.clientSatisfaction}/5</span>
<Star className="h-4 w-4 text-yellow-500" />
</div>
</div>
)}
</div>
{relationship.clientReview && (
<div className="mt-3">
<p className="font-medium text-sm text-gray-600">Client Review</p>
<p className="text-sm text-gray-900 italic">"{relationship.clientReview}"</p>
</div>
)}
</div>
))}
</div>
)}
</div>
)}
{/* Actions */}
<div className="flex gap-3 pt-6 border-t border-gray-200 mt-6">
<button
onClick={() => router.push(`/messages?userId=${selectedClient.id}`)}
className="flex-1 bg-teal-600 text-white py-2 px-4 rounded-lg hover:bg-teal-700 transition-colors"
>
<MessageSquare className="h-4 w-4 mr-2 inline" />
Send Message
</button>
<button
onClick={() => router.push(`/hire/consultation?lawyerId=${session.user.id}&clientId=${selectedClient.id}`)}
className="flex-1 bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 transition-colors"
>
<Calendar className="h-4 w-4 mr-2 inline" />
Schedule Consultation
</button>
<button
onClick={() => setShowDetailsModal(false)}
className="flex-1 bg-gray-600 text-white py-2 px-4 rounded-lg hover:bg-gray-700 transition-colors"
>
Close
</button>
</div>
</div>
</div>
</div>
)}
</div>
</LayoutWithSidebar>
);
};
export default LawyerClients;