![]() 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/client/ |
'use client';
import React, { useState, useEffect, useRef } from 'react';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import { isSuperAdmin } from '@/lib/auth-utils';
import DashboardModal from '@/components/DashboardModal';
import { useDashboardModal } from '@/hooks/useDashboardModal';
import {
FileText,
DollarSign,
MessageSquare,
Calendar,
AlertCircle,
CheckCircle,
Clock,
Download,
Upload,
Eye,
Edit,
Send,
Bell,
Settings,
User,
Shield,
TrendingUp,
Activity,
Folder,
Phone,
Mail,
Video,
MapPin,
Star,
MoreVertical,
Plus,
Trash2,
ArrowLeft
} from 'lucide-react';
import { format } from 'date-fns';
import CaseWidget from '@/components/CaseWidget';
import PaymentWidget from '@/components/payments/PaymentWidget';
import LayoutWithSidebar from '../../components/LayoutWithSidebar';
// Utility functions
const getStatusColor = (status: string) => {
switch (status.toLowerCase()) {
case 'active': return 'text-green-600 bg-green-100';
case 'pending': return 'text-yellow-600 bg-yellow-100';
case 'completed': return 'text-blue-600 bg-blue-100';
case 'cancelled': return 'text-red-600 bg-red-100';
default: return 'text-gray-600 bg-gray-100';
}
};
const getPriorityColor = (priority: string) => {
switch (priority.toLowerCase()) {
case 'high': return 'text-red-600 bg-red-100';
case 'medium': return 'text-yellow-600 bg-yellow-100';
case 'low': return 'text-green-600 bg-green-100';
default: return 'text-gray-600 bg-gray-100';
}
};
interface Case {
id: string;
caseNumber: string;
title: string;
status: string;
priority: string;
createdAt: string;
updatedAt: string;
description?: string;
lawyer: {
id: string;
name: string;
email: string;
profile?: {
phone?: string;
avatar?: string;
};
};
documents: Document[];
payments: Payment[];
updates: CaseUpdate[];
}
interface Document {
id: string;
title: string;
type: string;
filePath: string;
fileSize: number;
uploadedAt: string;
uploadedBy: string;
status: string;
}
interface Payment {
id: string;
amount: number;
status: string;
dueDate: string;
paidAt?: string;
description: string;
type: string;
}
interface CaseUpdate {
id: string;
title: string;
content: string;
type: string;
createdAt: string;
createdBy: string;
}
interface Message {
id: string;
content: string;
senderId: string;
senderName: string;
timestamp: string;
isRead: boolean;
}
const ClientDashboard: React.FC = () => {
const { data: session, status } = useSession();
const router = useRouter();
const [cases, setCases] = useState<Case[]>([]);
const [selectedCase, setSelectedCase] = useState<Case | null>(null);
const [messages, setMessages] = useState<Message[]>([]);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState<'overview' | 'cases' | 'documents' | 'payments' | 'messages' | 'applications'>('overview');
const [showUploadModal, setShowUploadModal] = useState(false);
const [showMessageModal, setShowMessageModal] = useState(false);
const [showCaseManagementModal, setShowCaseManagementModal] = useState(false);
const { isModalOpen, currentRoute, modalTitle, openModal, closeModal } = useDashboardModal();
const [showNotifications, setShowNotifications] = useState(false);
const notificationRef = useRef<HTMLButtonElement>(null);
const [searchTerm, setSearchTerm] = useState('');
const [registrations, setRegistrations] = useState<any[]>([]);
useEffect(() => {
if (status === 'loading') return;
if (!session?.user) {
router.push('/auth/login');
return;
}
// Check if user is a client, user, or super admin
if (!['CLIENT', 'USER'].includes(session.user.role) && !isSuperAdmin(session)) {
router.push('/dashboard');
return;
}
fetchClientData();
}, [session, status]);
const fetchClientData = async () => {
try {
setLoading(true);
// Fetch cases
const casesResponse = await fetch('/api/client/cases');
if (casesResponse.ok) {
const casesData = await casesResponse.json();
setCases(casesData.cases || []);
if (casesData.cases?.length > 0) {
setSelectedCase(casesData.cases[0]);
}
}
// Fetch messages
const messagesResponse = await fetch('/api/client/messages');
if (messagesResponse.ok) {
const messagesData = await messagesResponse.json();
setMessages(messagesData.messages || []);
}
// Fetch registrations
const registrationsResponse = await fetch('/api/user/registrations');
if (registrationsResponse.ok) {
const registrationsData = await registrationsResponse.json();
setRegistrations(registrationsData.registrations || []);
}
} catch (error) {
console.error('Error fetching client data:', error);
} finally {
setLoading(false);
}
};
const handleDocumentUpload = async (file: File, caseId: string) => {
try {
const formData = new FormData();
formData.append('file', file);
formData.append('caseId', caseId);
formData.append('title', file.name);
const response = await fetch('/api/client/documents/upload', {
method: 'POST',
body: formData
});
if (response.ok) {
fetchClientData();
setShowUploadModal(false);
}
} catch (error) {
console.error('Error uploading document:', error);
}
};
const handleSendMessage = async (content: string, recipientId: string) => {
try {
const response = await fetch('/api/client/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content,
recipientId,
caseId: selectedCase?.id
})
});
if (response.ok) {
fetchClientData();
setShowMessageModal(false);
}
} catch (error) {
console.error('Error sending message:', error);
}
};
if (loading) {
return (
<LayoutWithSidebar>
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
</LayoutWithSidebar>
);
}
return (
<LayoutWithSidebar>
<div className="min-h-screen bg-gray-50 p-6">
<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 items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Client Dashboard</h1>
<p className="text-gray-600 mt-2">Welcome back, {session?.user?.name}</p>
</div>
<div className="flex items-center gap-4 relative">
<button
className="p-2 text-gray-600 hover:bg-gray-100 rounded-full relative"
onClick={() => setShowNotifications((prev) => !prev)}
ref={notificationRef}
>
<Bell className="w-5 h-5" />
<span className="absolute top-0 right-0 block h-2 w-2 rounded-full bg-red-500 border-2 border-white"></span>
</button>
{showNotifications && (
<div className="absolute right-0 mt-2 w-72 bg-white border border-gray-200 rounded-lg shadow-lg z-50 animate-fadeIn">
<div className="p-4 border-b font-semibold text-gray-900">Notifications</div>
<ul className="max-h-60 overflow-y-auto">
<li className="px-4 py-2 text-gray-700 hover:bg-gray-50 cursor-pointer">Your application was approved!</li>
<li className="px-4 py-2 text-gray-700 hover:bg-gray-50 cursor-pointer">New message from your lawyer</li>
<li className="px-4 py-2 text-gray-700 hover:bg-gray-50 cursor-pointer">Payment received for Case #1234</li>
</ul>
<div className="p-2 text-center text-blue-600 hover:underline cursor-pointer">View all notifications</div>
</div>
)}
<button className="p-2 text-gray-600 hover:bg-gray-100 rounded-full">
<Settings className="w-5 h-5" />
</button>
<div className="flex items-center gap-2 ml-4">
<div className="w-10 h-10 rounded-full bg-blue-200 flex items-center justify-center text-xl font-bold text-blue-700">
{session?.user?.name?.[0] || <User className="w-6 h-6" />}
</div>
<div className="text-left">
<div className="font-semibold text-gray-900">{session?.user?.name || 'User'}</div>
<div className="text-xs text-gray-500">Welcome back!</div>
</div>
</div>
</div>
</div>
</div>
{/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center">
<div className="p-2 bg-blue-100 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">Active Cases</p>
<p className="text-2xl font-bold text-gray-900">{cases.filter(c => c.status === 'ACTIVE').length}</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center">
<div className="p-2 bg-green-100 rounded-lg">
<DollarSign className="w-6 h-6 text-green-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Total Paid</p>
<p className="text-2xl font-bold text-gray-900">
${cases.reduce((total, c) =>
total + (c.payments ? c.payments.filter(p => p.status === 'PAID').reduce((sum, p) => sum + p.amount, 0) : 0), 0
).toLocaleString()}
</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center">
<div className="p-2 bg-yellow-100 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">Pending Payments</p>
<p className="text-2xl font-bold text-gray-900">
${cases.reduce((total, c) =>
total + (c.payments ? c.payments.filter(p => p.status === 'PENDING').reduce((sum, p) => sum + p.amount, 0) : 0), 0
).toLocaleString()}
</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center">
<div className="p-2 bg-purple-100 rounded-lg">
<MessageSquare className="w-6 h-6 text-purple-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Unread Messages</p>
<p className="text-2xl font-bold text-gray-900">{messages.filter(m => !m.isRead).length}</p>
</div>
</div>
</div>
</div>
{/* Quick Actions */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-8">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Quick Actions</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<button
onClick={() => setShowCaseManagementModal(true)}
className="flex items-center p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
>
<FileText className="h-6 w-6 text-blue-600 mr-3" />
<div className="text-left">
<div className="font-medium text-gray-900">Case Management</div>
<div className="text-sm text-gray-500">View all cases</div>
</div>
</button>
<button
onClick={() => openModal('/hire/new-case', 'Create New Case')}
className="flex items-center p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
>
<Plus className="h-6 w-6 text-green-600 mr-3" />
<div className="text-left">
<div className="font-medium text-gray-900">Create New Case</div>
<div className="text-sm text-gray-500">Start legal process</div>
</div>
</button>
<button
onClick={() => openModal('/client/messages', 'Messages')}
className="flex items-center p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
>
<MessageSquare className="h-6 w-6 text-purple-600 mr-3" />
<div className="text-left">
<div className="font-medium text-gray-900">Messages</div>
<div className="text-sm text-gray-500">View communications</div>
</div>
</button>
<button
onClick={() => openModal('/client/applications', 'My Applications')}
className="flex items-center p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
>
<FileText className="h-6 w-6 text-cyan-600 mr-3" />
<div className="text-left">
<div className="font-medium text-gray-900">My Applications</div>
<div className="text-sm text-gray-500">View your submitted applications</div>
</div>
</button>
</div>
</div>
{/* Main Content */}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
{/* Sidebar */}
<div className="lg:col-span-1">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Navigation</h3>
<nav className="space-y-2">
<button
onClick={() => setActiveTab('overview')}
className={`w-full flex items-center px-3 py-2 rounded-md text-sm font-medium ${
activeTab === 'overview'
? 'bg-blue-100 text-blue-700'
: 'text-gray-600 hover:bg-gray-50'
}`}
>
<Activity className="w-4 h-4 mr-3" />
Overview
</button>
<button
onClick={() => setActiveTab('cases')}
className={`w-full flex items-center px-3 py-2 rounded-md text-sm font-medium ${
activeTab === 'cases'
? 'bg-blue-100 text-blue-700'
: 'text-gray-600 hover:bg-gray-50'
}`}
>
<FileText className="w-4 h-4 mr-3" />
My Cases
</button>
<button
onClick={() => setActiveTab('documents')}
className={`w-full flex items-center px-3 py-2 rounded-md text-sm font-medium ${
activeTab === 'documents'
? 'bg-blue-100 text-blue-700'
: 'text-gray-600 hover:bg-gray-50'
}`}
>
<Folder className="w-4 h-4 mr-3" />
Documents
</button>
<button
onClick={() => setActiveTab('payments')}
className={`w-full flex items-center px-3 py-2 rounded-md text-sm font-medium ${
activeTab === 'payments'
? 'bg-blue-100 text-blue-700'
: 'text-gray-600 hover:bg-gray-50'
}`}
>
<DollarSign className="w-4 h-4 mr-3" />
Payments
</button>
<button
onClick={() => setActiveTab('messages')}
className={`w-full flex items-center px-3 py-2 rounded-md text-sm font-medium ${
activeTab === 'messages'
? 'bg-blue-100 text-blue-700'
: 'text-gray-600 hover:bg-gray-50'
}`}
>
<MessageSquare className="w-4 h-4 mr-3" />
Messages
</button>
<button
onClick={() => setActiveTab('applications')}
className={`w-full flex items-center px-3 py-2 rounded-md text-sm font-medium ${
activeTab === 'applications'
? 'bg-blue-100 text-blue-700'
: 'text-gray-600 hover:bg-gray-50'
}`}
>
<FileText className="w-4 h-4 mr-3" />
Applications
</button>
</nav>
{/* Lawyer Contact */}
{selectedCase && selectedCase.lawyer && (
<div className="mt-8 pt-6 border-t border-gray-200">
<h4 className="text-sm font-medium text-gray-900 mb-3">Your Lawyer</h4>
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
<User className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-sm font-medium text-gray-900">
{selectedCase.lawyer.name}
</p>
<p className="text-xs text-gray-500">{selectedCase.lawyer.email}</p>
</div>
</div>
<div className="mt-3 space-y-2">
<button className="w-full flex items-center justify-center px-3 py-2 text-sm text-blue-600 hover:bg-blue-50 rounded-md">
<Phone className="w-4 h-4 mr-2" />
Call
</button>
<button className="w-full flex items-center justify-center px-3 py-2 text-sm text-blue-600 hover:bg-blue-50 rounded-md">
<Mail className="w-4 h-4 mr-2" />
Email
</button>
<button className="w-full flex items-center justify-center px-3 py-2 text-sm text-blue-600 hover:bg-blue-50 rounded-md">
<Video className="w-4 h-4 mr-2" />
Video Call
</button>
</div>
</div>
)}
</div>
</div>
{/* Main Content Area */}
<div className="lg:col-span-3">
{/* Search Bar */}
{(activeTab === 'cases' || activeTab === 'documents' || activeTab === 'applications') && (
<div className="mb-4 flex items-center">
<input
type="text"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder={`Search ${activeTab.charAt(0).toUpperCase() + activeTab.slice(1)}`}
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
</div>
)}
{/* Tab Content */}
<div className="mt-6">
{activeTab === 'overview' && <OverviewTab cases={cases} selectedCase={selectedCase} registrations={registrations} session={session} />}
{activeTab === 'cases' && <CasesTab cases={cases} selectedCase={selectedCase} onSelectCase={setSelectedCase} searchTerm={searchTerm} />}
{activeTab === 'documents' && <DocumentsTab cases={cases} selectedCase={selectedCase} onUpload={() => setShowUploadModal(true)} searchTerm={searchTerm} />}
{activeTab === 'payments' && <PaymentsTab cases={cases} selectedCase={selectedCase} />}
{activeTab === 'messages' && <MessagesTab messages={messages} selectedCase={selectedCase} onSendMessage={() => setShowMessageModal(true)} />}
{activeTab === 'applications' && <ApplicationsTab searchTerm={searchTerm} />}
</div>
{/* Modals */}
{showUploadModal && (
<UploadModal
onUpload={handleDocumentUpload}
onClose={() => setShowUploadModal(false)}
caseId={selectedCase?.id}
/>
)}
{showMessageModal && (
<MessageModal
onSend={handleSendMessage}
onClose={() => setShowMessageModal(false)}
recipientId={selectedCase?.lawyer?.id}
/>
)}
{showCaseManagementModal && (
<CaseManagementModal
cases={cases}
onClose={() => setShowCaseManagementModal(false)}
onUpload={handleDocumentUpload}
onMessage={(case_) => { setSelectedCase(case_); setShowMessageModal(true); }}
/>
)}
{/* Dashboard Modal */}
<DashboardModal
isOpen={isModalOpen}
onClose={closeModal}
route={currentRoute}
title={modalTitle}
/>
</div>
</div>
</div>
</div>
</LayoutWithSidebar>
);
};
// Tab Components
const OverviewTab: React.FC<{ cases: Case[], selectedCase: Case | null, registrations?: any[], session?: any }> = ({ cases, selectedCase, registrations = [], session }) => {
// Gather recent updates from cases
const caseUpdates = cases.flatMap(c => (c.updates || []).map(u => ({
type: 'case-update',
title: u.title,
description: u.content,
date: u.createdAt,
icon: <Activity className="w-5 h-5 text-blue-500" />
})));
// Recent document uploads
const documentEvents = cases.flatMap(c => (c.documents || []).map(d => ({
type: 'document',
title: d.title,
description: `Document uploaded: ${d.title}`,
date: d.uploadedAt,
icon: <FileText className="w-5 h-5 text-green-500" />
})));
// Recent payments
const paymentEvents = cases.flatMap(c => (c.payments || []).map(p => ({
type: 'payment',
title: p.description,
description: `Payment ${p.status.toLowerCase()}: $${p.amount}`,
date: p.paidAt || p.dueDate,
icon: <DollarSign className="w-5 h-5 text-yellow-500" />
})));
// Recent application status changes
const applicationEvents = (registrations || []).map(reg => ({
type: 'application',
title: `${reg.firstName} ${reg.lastName}`,
description: `Application status: ${reg.status}`,
date: reg.updatedAt || reg.createdAt,
icon: <User className="w-5 h-5 text-purple-500" />
}));
// Combine and sort all events
const allEvents = [...caseUpdates, ...documentEvents, ...paymentEvents, ...applicationEvents]
.filter(e => !!e.date)
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
.slice(0, 8);
return (
<div className="space-y-6">
{/* Welcome Section */}
<div className="bg-gradient-to-r from-blue-600 to-blue-700 rounded-lg shadow-sm p-6 text-white">
<h2 className="text-2xl font-bold mb-2">Welcome to Your Legal Dashboard</h2>
<p className="text-blue-100">
Manage your cases, track progress, and stay connected with your legal team.
</p>
</div>
{/* Unified Case Management Widget */}
<CaseWidget
title="My Cases"
showMyCases={true}
showAssignedCases={false}
showPublicCases={false}
maxCases={5}
showCreateButton={true}
showFilters={true}
className="mb-6"
/>
{/* Payment Widget */}
<PaymentWidget
userId={session?.user?.id || ''}
userRole={session?.user?.role || 'CLIENT'}
compact={true}
/>
{/* Expanded Recent Activity */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Recent Activity</h2>
{allEvents.length === 0 ? (
<div className="text-gray-500">No recent activity.</div>
) : (
allEvents.map((event, idx) => (
<div key={idx} className="flex items-start space-x-3 py-3 border-b border-gray-100 last:border-b-0">
<div>{event.icon}</div>
<div className="flex-1">
<p className="text-sm font-medium text-gray-900">{event.title}</p>
<p className="text-sm text-gray-600">{event.description}</p>
<p className="text-xs text-gray-500 mt-1">
{format(new Date(event.date), 'MMM dd, yyyy')}
</p>
</div>
</div>
))
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Upcoming Deadlines</h3>
<div className="space-y-3">
{cases.flatMap(c => c.documents || []).filter(d => d.status === 'PENDING')
.slice(0, 3)
.map((doc) => (
<div key={doc.id} className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-900">{doc.title}</p>
<p className="text-xs text-gray-500">{doc.type}</p>
</div>
<span className="text-xs text-red-600 bg-red-100 px-2 py-1 rounded-full">
Due Soon
</span>
</div>
))}
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Recent Documents</h3>
<div className="space-y-3">
{cases.flatMap(c => c.documents || []).sort((a, b) => new Date(b.uploadedAt).getTime() - new Date(a.uploadedAt).getTime())
.slice(0, 3)
.map((doc) => (
<div key={doc.id} className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<FileText className="w-4 h-4 text-gray-400" />
<div>
<p className="text-sm font-medium text-gray-900">{doc.title}</p>
<p className="text-xs text-gray-500">
{format(new Date(doc.uploadedAt), 'MMM dd, yyyy')}
</p>
</div>
</div>
<button className="text-blue-600 hover:text-blue-700">
<Download className="w-4 h-4" />
</button>
</div>
))}
</div>
</div>
</div>
</div>
);
};
const CasesTab: React.FC<{
cases: Case[],
selectedCase: Case | null,
onSelectCase: (case_: Case) => void,
searchTerm: string
}> = ({ cases, selectedCase, onSelectCase, searchTerm }) => (
<div className="space-y-6">
{/* Case Management Header */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900">Case Management</h2>
<p className="text-gray-600 mt-1">Manage all your legal cases in one place</p>
</div>
<div className="flex items-center gap-3">
<span className="bg-blue-100 text-blue-800 text-sm font-medium px-3 py-1 rounded-full">
{cases.length} Total Cases
</span>
</div>
</div>
</div>
{/* Unified Case Management Widget */}
<CaseWidget
title="All My Cases"
showMyCases={true}
showAssignedCases={false}
showPublicCases={false}
maxCases={10}
showCreateButton={true}
showFilters={true}
className="mb-6"
/>
{/* Case Statistics */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<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-4">
<p className="text-sm font-medium text-gray-600">Active Cases</p>
<p className="text-2xl font-bold text-gray-900">
{cases.filter(c => c.status === 'active').length}
</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center">
<div className="p-2 bg-yellow-100 rounded-lg">
<Clock className="h-6 w-6 text-yellow-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Pending Cases</p>
<p className="text-2xl font-bold text-gray-900">
{cases.filter(c => c.status === 'pending').length}
</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center">
<div className="p-2 bg-blue-100 rounded-lg">
<FileText className="h-6 w-6 text-blue-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Total Documents</p>
<p className="text-2xl font-bold text-gray-900">
{cases.reduce((total, c) => total + (c.documents?.length || 0), 0)}
</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center">
<div className="p-2 bg-purple-100 rounded-lg">
<MessageSquare className="h-6 w-6 text-purple-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Recent Updates</p>
<p className="text-2xl font-bold text-gray-900">
{cases.reduce((total, c) => total + (c.updates?.length || 0), 0)}
</p>
</div>
</div>
</div>
</div>
</div>
);
const DocumentsTab: React.FC<{
cases: Case[],
selectedCase: Case | null,
onUpload: () => void,
searchTerm: string
}> = ({ cases, selectedCase, onUpload, searchTerm }) => (
<div className="bg-white rounded-lg shadow-sm border border-gray-200">
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-gray-900">Documents</h2>
<button
onClick={onUpload}
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
<Upload className="w-4 h-4 mr-2" />
Upload Document
</button>
</div>
</div>
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{cases.flatMap(c => c.documents || []).filter(doc => doc.title.toLowerCase().includes(searchTerm.toLowerCase())).map((doc) => (
<div key={doc.id} className="border border-gray-200 rounded-lg p-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<h3 className="text-sm font-medium text-gray-900">{doc.title}</h3>
<p className="text-xs text-gray-500 mt-1">{doc.type}</p>
<p className="text-xs text-gray-500">
{format(new Date(doc.uploadedAt), 'MMM dd, yyyy')}
</p>
</div>
<div className="flex items-center space-x-2">
<button className="text-blue-600 hover:text-blue-700">
<Eye className="w-4 h-4" />
</button>
<button className="text-blue-600 hover:text-blue-700">
<Download className="w-4 h-4" />
</button>
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
const PaymentsTab: React.FC<{ cases: Case[], selectedCase: Case | null }> = ({ cases }) => (
<div className="bg-white rounded-lg shadow-sm border border-gray-200">
<div className="p-6 border-b border-gray-200">
<h2 className="text-xl font-semibold text-gray-900">Payment History</h2>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Description
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Amount
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Due Date
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{cases.flatMap(c => c.payments || []).map((payment) => (
<tr key={payment.id}>
<td className="px-6 py-4 whitespace-nowrap">
<div>
<div className="text-sm font-medium text-gray-900">{payment.description}</div>
<div className="text-sm text-gray-500">{payment.type}</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
${payment.amount.toLocaleString()}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 py-1 text-xs font-medium rounded-full ${
payment.status === 'PAID' ? 'text-green-600 bg-green-100' :
payment.status === 'PENDING' ? 'text-yellow-600 bg-yellow-100' :
'text-red-600 bg-red-100'
}`}>
{payment.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{format(new Date(payment.dueDate), 'MMM dd, yyyy')}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
{payment.status === 'PENDING' && (
<button className="text-blue-600 hover:text-blue-900">
Pay Now
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
const MessagesTab: React.FC<{
messages: Message[],
selectedCase: Case | null,
onSendMessage: () => void
}> = ({ messages, onSendMessage }) => (
<div className="bg-white rounded-lg shadow-sm border border-gray-200">
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-gray-900">Messages</h2>
<button
onClick={onSendMessage}
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
<Send className="w-4 h-4 mr-2" />
New Message
</button>
</div>
</div>
<div className="p-6">
<div className="space-y-4">
{messages.map((message) => (
<div key={message.id} className={`flex ${message.senderId === 'client' ? 'justify-end' : 'justify-start'}`}>
<div className={`max-w-xs lg:max-w-md px-4 py-2 rounded-lg ${
message.senderId === 'client'
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-900'
}`}>
<p className="text-sm">{message.content}</p>
<p className={`text-xs mt-1 ${
message.senderId === 'client' ? 'text-blue-100' : 'text-gray-500'
}`}>
{format(new Date(message.timestamp), 'MMM dd, HH:mm')}
</p>
</div>
</div>
))}
</div>
</div>
</div>
);
// Modal Components
const UploadModal: React.FC<{
onUpload: (file: File, caseId: string) => void;
onClose: () => void;
caseId?: string;
}> = ({ onUpload, onClose, caseId }) => {
const [file, setFile] = useState<File | null>(null);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (file && caseId) {
onUpload(file, caseId);
}
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
<div className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Upload Document</h3>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Select File
</label>
<input
type="file"
onChange={(e) => setFile(e.target.files?.[0] || null)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
</div>
<div className="flex items-center justify-end gap-4 pt-4">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-gray-600 hover:text-gray-800"
>
Cancel
</button>
<button
type="submit"
disabled={!file}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
Upload
</button>
</div>
</form>
</div>
</div>
</div>
);
};
const MessageModal: React.FC<{
onSend: (content: string, recipientId: string) => void;
onClose: () => void;
recipientId?: string;
}> = ({ onSend, onClose, recipientId }) => {
const [content, setContent] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (content && recipientId) {
onSend(content, recipientId);
}
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
<div className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Send Message</h3>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Message
</label>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
rows={4}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Type your message..."
required
/>
</div>
<div className="flex items-center justify-end gap-4 pt-4">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-gray-600 hover:text-gray-800"
>
Cancel
</button>
<button
type="submit"
disabled={!content}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
Send
</button>
</div>
</form>
</div>
</div>
</div>
);
};
// Applications Tab Component
const ApplicationsTab: React.FC<{ searchTerm: string }> = ({ searchTerm }) => {
const [loading, setLoading] = useState(true);
const [registrations, setRegistrations] = useState<any[]>([]);
useEffect(() => {
fetchRegistrations();
}, []);
const fetchRegistrations = async () => {
try {
setLoading(true);
const response = await fetch('/api/user/registrations');
if (response.ok) {
const data = await response.json();
setRegistrations(data.registrations || []);
}
} catch (error) {
console.error('Error fetching registrations:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center p-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
);
}
return (
<div className="space-y-6">
{/* Applications Header */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900">Application Management</h2>
<p className="text-gray-600 mt-1">Track and manage your legal applications</p>
</div>
</div>
</div>
{/* Applications List */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200">
<div className="p-6 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900">Your Applications</h3>
</div>
{registrations.length === 0 ? (
<div className="p-8 text-center">
<FileText className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No Applications Yet</h3>
<p className="text-gray-600 mb-4">You haven't submitted any applications yet.</p>
<button
onClick={() => window.location.href = '/join'}
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
>
Submit New Application
</button>
</div>
) : (
<div className="divide-y divide-gray-200">
{registrations.filter(reg => reg.firstName.toLowerCase().includes(searchTerm.toLowerCase()) || reg.lastName.toLowerCase().includes(searchTerm.toLowerCase())).map((registration) => (
<div key={registration.id} className="p-6 hover:bg-gray-50 transition-colors">
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h4 className="text-lg font-medium text-gray-900">
{registration.firstName} {registration.lastName}
</h4>
<span className={`px-2 py-1 text-xs font-medium rounded-full ${
registration.status === 'APPROVED' ? 'bg-green-100 text-green-800' :
registration.status === 'REJECTED' ? 'bg-red-100 text-red-800' :
registration.status === 'PENDING' ? 'bg-yellow-100 text-yellow-800' :
'bg-gray-100 text-gray-800'
}`}>
{registration.status}
</span>
</div>
<p className="text-sm text-gray-600 mb-2">{registration.email}</p>
{registration.detaineeInfo && (
<div className="text-sm text-gray-500">
<p>Detainee: {registration.detaineeInfo.name}</p>
<p>Facility: {registration.detaineeInfo.facility}</p>
</div>
)}
<p className="text-xs text-gray-400 mt-2">
Submitted: {new Date(registration.createdAt).toLocaleDateString()}
</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => window.location.href = `/user/applications/${registration.id}`}
className="text-blue-600 hover:text-blue-700 text-sm font-medium"
>
View Details
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
};
const CaseManagementModal: React.FC<{
cases: Case[];
onClose: () => void;
onUpload: (file: File, caseId: string) => void;
onMessage: (case_: Case) => void;
}> = ({ cases, onClose, onUpload, onMessage }) => {
const [selectedCase, setSelectedCase] = useState<Case | null>(null);
const [showUpload, setShowUpload] = useState(false);
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm transition-opacity duration-300 animate-fadeIn">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-2xl p-0 relative overflow-hidden border border-gray-200 animate-slideUp">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-100 bg-gradient-to-r from-blue-50 to-blue-100">
<div className="flex items-center gap-2">
<FileText className="w-6 h-6 text-blue-600" />
<h2 className="text-xl font-bold text-gray-900">My Cases</h2>
</div>
<button
className="text-gray-400 hover:text-gray-600 transition-colors text-2xl font-bold"
onClick={onClose}
aria-label="Close"
>
×
</button>
</div>
{/* Content */}
<div className="px-6 py-6 max-h-[70vh] overflow-y-auto">
{!selectedCase ? (
<div className="space-y-4">
{cases.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12">
<FileText className="w-12 h-12 text-gray-300 mb-2" />
<p className="text-gray-500">No cases found.</p>
</div>
) : (
cases.map((case_) => (
<div key={case_.id} className="flex items-center justify-between bg-white border border-gray-100 rounded-xl p-4 shadow-sm hover:shadow-md transition-shadow group">
<div className="flex items-center gap-4 min-w-0">
<div className="w-12 h-12 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 font-bold text-lg">
{case_.lawyer?.name ? case_.lawyer.name[0] : <User className="w-6 h-6" />}
</div>
<div className="min-w-0">
<div className="font-semibold text-gray-900 truncate">{case_.title}</div>
<div className="flex items-center gap-2 mt-1">
<span className="text-xs text-gray-500 font-mono">#{case_.caseNumber}</span>
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
case_.status === 'active' ? 'bg-green-100 text-green-700' :
case_.status === 'pending' ? 'bg-yellow-100 text-yellow-700' :
case_.status === 'closed' ? 'bg-gray-100 text-gray-700' :
'bg-blue-100 text-blue-700'
}`}>{case_.status}</span>
</div>
</div>
</div>
<div className="flex gap-2 flex-shrink-0">
<button
className="flex items-center gap-1 px-3 py-1 text-xs font-medium bg-blue-50 text-blue-700 rounded-lg hover:bg-blue-100 transition-colors shadow-sm"
onClick={() => setSelectedCase(case_)}
>
<Eye className="w-4 h-4" /> View
</button>
<button
className="flex items-center gap-1 px-3 py-1 text-xs font-medium bg-green-50 text-green-700 rounded-lg hover:bg-green-100 transition-colors shadow-sm"
onClick={() => { setSelectedCase(case_); setShowUpload(true); }}
>
<Upload className="w-4 h-4" /> Upload
</button>
<button
className="flex items-center gap-1 px-3 py-1 text-xs font-medium bg-purple-50 text-purple-700 rounded-lg hover:bg-purple-100 transition-colors shadow-sm"
onClick={() => onMessage(case_)}
>
<MessageSquare className="w-4 h-4" /> Message
</button>
</div>
</div>
))
)}
</div>
) : (
<div className="relative">
<button className="mb-4 text-blue-600 hover:underline flex items-center gap-1" onClick={() => setSelectedCase(null)}>
<ArrowLeft className="w-4 h-4" /> Back to list
</button>
<div className="bg-gray-50 rounded-xl p-6 shadow-inner mb-4">
<div className="flex items-center gap-4 mb-4">
<div className="w-14 h-14 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 font-bold text-2xl">
{selectedCase.lawyer?.name ? selectedCase.lawyer.name[0] : <User className="w-7 h-7" />}
</div>
<div>
<div className="font-bold text-lg text-gray-900">{selectedCase.title}</div>
<div className="text-xs text-gray-500 font-mono">Case #{selectedCase.caseNumber}</div>
<div className="flex items-center gap-2 mt-1">
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
selectedCase.status === 'active' ? 'bg-green-100 text-green-700' :
selectedCase.status === 'pending' ? 'bg-yellow-100 text-yellow-700' :
selectedCase.status === 'closed' ? 'bg-gray-100 text-gray-700' :
'bg-blue-100 text-blue-700'
}`}>{selectedCase.status}</span>
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
selectedCase.priority === 'high' ? 'bg-red-100 text-red-700' :
selectedCase.priority === 'medium' ? 'bg-yellow-100 text-yellow-700' :
selectedCase.priority === 'low' ? 'bg-green-100 text-green-700' :
'bg-gray-100 text-gray-700'
}`}>{selectedCase.priority} priority</span>
</div>
</div>
</div>
<div className="mb-2 text-sm text-gray-700"><span className="font-semibold">Lawyer:</span> {selectedCase.lawyer?.name || 'N/A'}</div>
<div className="mb-2 text-sm text-gray-700"><span className="font-semibold">Description:</span> {selectedCase.description}</div>
<div className="mb-2 text-sm text-gray-700"><span className="font-semibold">Created:</span> {new Date(selectedCase.createdAt).toLocaleDateString()}</div>
<div className="mb-2 text-sm text-gray-700"><span className="font-semibold">Last Updated:</span> {new Date(selectedCase.updatedAt).toLocaleDateString()}</div>
</div>
<div className="flex gap-2 mb-2">
<button
className="flex items-center gap-1 px-4 py-2 text-xs font-medium bg-green-50 text-green-700 rounded-lg hover:bg-green-100 transition-colors shadow-sm"
onClick={() => setShowUpload(true)}
>
<Upload className="w-4 h-4" /> Upload Document
</button>
<button
className="flex items-center gap-1 px-4 py-2 text-xs font-medium bg-purple-50 text-purple-700 rounded-lg hover:bg-purple-100 transition-colors shadow-sm"
onClick={() => onMessage(selectedCase)}
>
<MessageSquare className="w-4 h-4" /> Message Lawyer
</button>
</div>
{showUpload && (
<div className="mt-4">
<UploadModal
onUpload={(file, caseId) => { onUpload(file, caseId); setShowUpload(false); }}
onClose={() => setShowUpload(false)}
caseId={selectedCase.id}
/>
</div>
)}
</div>
)}
</div>
</div>
</div>
);
};
export default ClientDashboard;