![]() 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 React, { useState, useEffect } from 'react';
import { toast } from 'react-hot-toast';
import TeamPrivateChat from './TeamPrivateChat';
interface User {
id: string;
name: string;
email: string;
role: string;
title?: string;
username?: string;
lawFirm?: {
name: string;
shortName?: string;
};
}
interface Registration {
id: string;
firstName: string;
lastName: string;
status: string;
}
interface LegalCase {
id: string;
title: string;
caseNumber: string;
status: string;
priority: string;
jurisdiction: string;
court?: string;
firmName?: string;
leadLawyer?: {
id: string;
name: string;
email: string;
title?: string;
lawFirm?: {
name: string;
shortName?: string;
address: string;
city: string;
province: string;
};
};
_count?: {
caseAssignments: number;
registrations: number;
};
}
interface CaseAssignment {
id: string;
role: 'primary_lawyer' | 'assistant_lawyer' | 'secretary' | 'lead_lawyer' | 'unassigned';
assignedAt: string;
user: User;
registration: Registration;
legalCase?: LegalCase;
}
interface CaseAssignmentDashboardProps {
currentUser: User;
}
const CaseAssignmentDashboard: React.FC<CaseAssignmentDashboardProps> = ({ currentUser }) => {
const [assignments, setAssignments] = useState<CaseAssignment[]>([]);
const [loading, setLoading] = useState(true);
const [selectedCase, setSelectedCase] = useState<string | null>(null);
const [caseTeam, setCaseTeam] = useState<CaseAssignment[]>([]);
const [onlineUsers, setOnlineUsers] = useState<Set<string>>(new Set());
const [privateChatMember, setPrivateChatMember] = useState<CaseAssignment | null>(null);
useEffect(() => {
fetchMyAssignments();
}, []);
// Simulate online status - in real app, this would come from WebSocket/real-time connection
useEffect(() => {
const mockOnlineUsers = new Set([
'cmcb44auw0000vj2gt90q3fph', // Danny (you)
// Add other user IDs that should appear online
]);
setOnlineUsers(mockOnlineUsers);
// Simulate real-time updates
const interval = setInterval(() => {
// Randomly update online status for demo
const allUserIds = caseTeam.map(member => member.user.id);
const randomOnlineUsers = new Set<string>();
randomOnlineUsers.add(currentUser.id); // Current user always online
allUserIds.forEach(userId => {
if (Math.random() > 0.3) { // 70% chance to be online
randomOnlineUsers.add(userId);
}
});
setOnlineUsers(randomOnlineUsers);
}, 10000); // Update every 10 seconds
return () => clearInterval(interval);
}, [caseTeam, currentUser.id]);
const fetchMyAssignments = async () => {
try {
// First try with the current user ID
let response = await fetch(`/api/admin/case-assignments?userId=${currentUser.id}`);
let data: any[] = [];
if (response.ok) {
data = await response.json();
}
// If no assignments found and we have an email, try to find the correct user ID
if (data.length === 0 && currentUser.email) {
try {
// Get the actual user from the database by email
const userResponse = await fetch(`/api/admin/users?email=${encodeURIComponent(currentUser.email)}`);
if (userResponse.ok) {
const userData = await userResponse.json();
const users = userData.users || userData;
const actualUser = Array.isArray(users)
? users.find((u: any) => u.email === currentUser.email)
: users.email === currentUser.email ? users : null;
if (actualUser && actualUser.id !== currentUser.id) {
// Try again with the correct user ID
response = await fetch(`/api/admin/case-assignments?userId=${actualUser.id}`);
if (response.ok) {
data = await response.json();
}
}
}
} catch (emailLookupError) {
console.log('Email lookup failed, continuing with original data');
}
}
// For admin users, also fetch all cases to show them in the dashboard
if (['ADMIN', 'SUPERADMIN'].includes(currentUser.role)) {
try {
const allCasesResponse = await fetch('/api/admin/cases');
if (allCasesResponse.ok) {
const allCasesData = await allCasesResponse.json();
const allCases = allCasesData.cases || [];
// Create placeholder assignments for cases that don't have assignments yet
const unassignedCases = allCases.filter((legalCase: any) =>
!data.some((assignment: any) => assignment.legalCase?.id === legalCase.id)
);
// Add placeholder assignments for unassigned cases
const placeholderAssignments = unassignedCases.map((legalCase: any) => ({
id: `placeholder-${legalCase.id}`,
role: 'unassigned' as any,
assignedAt: legalCase.createdAt || new Date().toISOString(),
user: currentUser,
legalCase: legalCase,
registration: {
id: 'placeholder',
firstName: 'Unassigned',
lastName: 'Case',
status: 'active'
}
}));
data = [...data, ...placeholderAssignments];
}
} catch (allCasesError) {
console.log('Failed to fetch all cases for admin view:', allCasesError);
}
}
setAssignments(data);
} catch (error) {
console.error('Error fetching assignments:', error);
} finally {
setLoading(false);
}
};
const fetchCaseTeam = async (caseId: string) => {
try {
const response = await fetch(`/api/admin/case-assignments?caseId=${caseId}`);
if (response.ok) {
const data = await response.json();
setCaseTeam(data);
}
} catch (error) {
console.error('Error fetching case team:', error);
}
};
const getRoleInfo = (role: string) => {
switch (role) {
case 'lead_lawyer':
return {
label: 'Lead Attorney',
color: 'bg-purple-100 text-purple-800 border-purple-200',
icon: 'βοΈ'
};
case 'primary_lawyer':
return {
label: 'Primary Attorney',
color: 'bg-blue-100 text-blue-800 border-blue-200',
icon: 'ποΈ'
};
case 'assistant_lawyer':
return {
label: 'Assistant Attorney',
color: 'bg-green-100 text-green-800 border-green-200',
icon: 'π€'
};
case 'secretary':
return {
label: 'Secretary',
color: 'bg-yellow-100 text-yellow-800 border-yellow-200',
icon: 'π'
};
case 'unassigned':
return {
label: 'Unassigned Case',
color: 'bg-orange-100 text-orange-800 border-orange-200',
icon: 'π'
};
default:
return {
label: role.replace('_', ' '),
color: 'bg-gray-100 text-gray-800 border-gray-200',
icon: 'π€'
};
}
};
const getStatusColor = (status: string) => {
switch (status.toLowerCase()) {
case 'approved':
return 'bg-green-100 text-green-800';
case 'pending':
return 'bg-yellow-100 text-yellow-800';
case 'rejected':
return 'bg-red-100 text-red-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
const handleProfileClick = (user: User) => {
const profileSlug = user.username || user.name.replace(/\s+/g, '').toLowerCase();
window.open(`/profile/${profileSlug}`, '_blank');
};
const handlePrivateChat = async (userId: string, userName: string) => {
// Find the team member to chat with
const targetMember = caseTeam.find(member => member.user.id === userId);
if (targetMember) {
setPrivateChatMember(targetMember);
} else {
toast.error('Unable to start private chat with this team member');
}
};
const handleGroupChat = (caseId: string) => {
if (caseId) {
window.open(`/group-chat/${caseId}`, '_blank');
} else {
window.open('/group-chat', '_blank');
}
};
const isUserOnline = (userId: string) => onlineUsers.has(userId);
const getOnlineStatus = (userId: string) => {
const isOnline = isUserOnline(userId);
return {
isOnline,
indicator: isOnline ? 'π’' : 'β«',
text: isOnline ? 'Online' : 'Offline',
color: isOnline ? 'text-green-600' : 'text-gray-400'
};
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="bg-white shadow rounded-lg p-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900 flex items-center gap-2">
ποΈ Case Assignment Dashboard
</h1>
<p className="text-gray-600 mt-1">
{currentUser.role === 'SUPERADMIN' ?
`Legal Representative & CEO - ${currentUser.name}` :
`Team workflow management for ${currentUser.name} (${currentUser.role})`
}
</p>
{currentUser.role === 'SUPERADMIN' && (
<p className="text-sm text-blue-600 mt-1 font-medium">
π LibertΓ© MΓͺme En Prison - Case management oversight
</p>
)}
</div>
<div className="text-right">
<div className="text-sm text-gray-500">Active Cases</div>
<div className="text-3xl font-bold text-primary">{assignments.length}</div>
</div>
</div>
</div>
{/* Role Summary */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{[
{ role: 'lead_lawyer', count: assignments.filter(a => a.role === 'lead_lawyer').length, label: 'Lead Attorney' },
{ role: 'primary_lawyer', count: assignments.filter(a => a.role === 'primary_lawyer').length, label: 'Primary Attorney' },
{ role: 'assistant_lawyer', count: assignments.filter(a => a.role === 'assistant_lawyer').length, label: 'Assistant Attorney' },
{ role: 'secretary', count: assignments.filter(a => a.role === 'secretary').length, label: 'Secretary' }
].map(({ role, count, label }) => {
const roleInfo = getRoleInfo(role);
return (
<div key={role} className="bg-white p-4 rounded-lg shadow">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">{label}</p>
<p className="text-2xl font-bold">{count}</p>
</div>
<div className="text-3xl">{roleInfo.icon}</div>
</div>
</div>
);
})}
</div>
{/* Unassigned Cases Summary for Admins */}
{['ADMIN', 'SUPERADMIN'].includes(currentUser.role) && assignments.filter(a => a.role === 'unassigned').length > 0 && (
<div className="bg-orange-50 border border-orange-200 rounded-lg p-4">
<div className="flex items-center gap-3">
<div className="text-2xl">π</div>
<div>
<h3 className="font-semibold text-orange-900">Unassigned Cases</h3>
<p className="text-sm text-orange-700">
{assignments.filter(a => a.role === 'unassigned').length} case(s) need team assignments
</p>
</div>
</div>
</div>
)}
{/* My Assignments */}
<div className="bg-white shadow rounded-lg">
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-lg font-semibold text-gray-900">
{['ADMIN', 'SUPERADMIN'].includes(currentUser.role) ? 'All Cases' : 'My Assigned Cases'}
</h2>
<p className="text-sm text-gray-500">
{['ADMIN', 'SUPERADMIN'].includes(currentUser.role)
? 'Click on a case to view the team or assign members'
: 'Click on a case to view the full team'
}
</p>
</div>
<div className="p-6">
{assignments.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<div className="text-4xl mb-3">π</div>
<p>
{['ADMIN', 'SUPERADMIN'].includes(currentUser.role)
? 'No cases found in the system'
: 'No cases assigned to you yet'
}
</p>
<p className="text-sm mt-2">
{['ADMIN', 'SUPERADMIN'].includes(currentUser.role)
? 'Create cases or check the database for existing cases'
: 'Cases will appear here when assigned by an administrator'
}
</p>
</div>
) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{assignments.map((assignment) => {
const roleInfo = getRoleInfo(assignment.role);
return (
<div
key={assignment.id}
className="border border-gray-200 rounded-lg p-4 hover:shadow-md hover:border-primary transition-all duration-200 cursor-pointer"
onClick={() => {
if (assignment.legalCase?.id) {
setSelectedCase(assignment.legalCase.id);
fetchCaseTeam(assignment.legalCase.id);
}
}}
>
{/* Case Header */}
<div className="mb-4">
<div className="flex items-center justify-between mb-2">
<h3 className="font-semibold text-gray-900 text-base">
{assignment.legalCase?.title || 'Case Assignment'}
</h3>
<span className={`px-2 py-1 text-xs rounded-full ${getStatusColor(assignment.legalCase?.status || 'active')}`}>
{assignment.legalCase?.status || 'Active'}
</span>
</div>
{assignment.legalCase?.caseNumber && (
<p className="text-sm text-blue-600 font-medium mb-1">
π {assignment.legalCase.caseNumber}
</p>
)}
<p className="text-xs text-gray-500">
{assignment.legalCase?.jurisdiction || 'Quebec'} β’ {assignment.legalCase?.court || 'Superior Court'}
{assignment.legalCase?.priority && (
<span className="ml-2 capitalize font-medium text-orange-600">
{assignment.legalCase.priority} Priority
</span>
)}
</p>
</div>
{/* Your Role */}
<div className="mb-3">
<div className={`inline-flex items-center gap-2 px-3 py-2 text-sm rounded-lg border ${roleInfo.color}`}>
<span className="text-base">{roleInfo.icon}</span>
<span className="font-medium">
{assignment.role === 'unassigned' ? 'Status: Unassigned' : `Your Role: ${roleInfo.label}`}
</span>
</div>
</div>
{/* Team Summary */}
<div className="bg-gray-50 rounded-lg p-3 mb-3">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-gray-600 mb-1">Legal Team</p>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-900">
{assignment.legalCase?._count?.caseAssignments || 1} Members
</span>
{assignment.legalCase?.leadLawyer && (
<span className="text-xs text-blue-600">
β’ Lead: {assignment.legalCase.leadLawyer.name.split(' ')[0]}
</span>
)}
</div>
</div>
<div className="text-xs text-gray-500">
π₯ Click to view full team
</div>
</div>
</div>
{/* Special Badge for CEO */}
{currentUser.role === 'SUPERADMIN' && assignment.legalCase?.title?.includes('Bordeaux') && (
<div className="bg-gradient-to-r from-purple-50 to-blue-50 border border-purple-200 rounded-lg p-2">
<p className="text-xs text-purple-700 font-medium flex items-center gap-1">
π’ Legal Representative & CEO - Case Oversight
</p>
</div>
)}
{/* Assignment Date */}
<div className="mt-3 pt-3 border-t border-gray-100">
<p className="text-xs text-gray-500 flex items-center gap-1">
π
{assignment.role === 'unassigned'
? `Case created: ${new Date(assignment.assignedAt).toLocaleDateString()}`
: `Your assignment: ${new Date(assignment.assignedAt).toLocaleDateString()}`
}
</p>
</div>
</div>
);
})}
</div>
)}
</div>
</div>
{/* Case Team Modal */}
{selectedCase && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg w-full max-w-3xl max-h-[80vh] overflow-hidden">
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-xl font-semibold flex items-center gap-2">
π₯ Legal Team
</h3>
{caseTeam.length > 0 && caseTeam[0].legalCase && (
<div className="mt-2">
<p className="text-lg font-medium text-gray-900">
{caseTeam[0].legalCase.title}
</p>
<p className="text-sm text-blue-600">
{caseTeam[0].legalCase.caseNumber} β’ {caseTeam[0].legalCase.jurisdiction}
</p>
</div>
)}
</div>
<div className="flex items-center gap-3">
<button
onClick={() => handleGroupChat(selectedCase)}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center gap-2 text-sm font-medium"
title="Open team group chat"
>
π¬ Team Chat
</button>
<button
onClick={() => setSelectedCase(null)}
className="text-gray-400 hover:text-gray-600 text-2xl hover:bg-gray-100 rounded-full w-10 h-10 flex items-center justify-center transition-colors"
>
β
</button>
</div>
</div>
<div className="max-h-96 overflow-y-auto">
<div className="space-y-4">
{caseTeam.length === 0 ? (
<div className="text-center py-12 text-gray-500">
<div className="text-6xl mb-4">π₯</div>
<p className="text-lg font-medium">No team members assigned</p>
<p className="text-sm mt-2">Use the assignment interface to build your legal team</p>
</div>
) : (
<>
{/* Team Statistics */}
<div className="bg-blue-50 rounded-lg p-4 mb-4">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-center">
{[
{ label: 'Total Members', count: caseTeam.length, icon: 'π₯' },
{ label: 'Lead Attorneys', count: caseTeam.filter(m => m.role === 'lead_lawyer').length, icon: 'βοΈ' },
{ label: 'Primary Attorneys', count: caseTeam.filter(m => m.role === 'primary_lawyer').length, icon: 'ποΈ' },
{ label: 'Support Staff', count: caseTeam.filter(m => ['assistant_lawyer', 'secretary'].includes(m.role)).length, icon: 'π' }
].map((stat) => (
<div key={stat.label}>
<div className="text-2xl mb-1">{stat.icon}</div>
<div className="text-xl font-bold text-blue-900">{stat.count}</div>
<div className="text-xs text-blue-700">{stat.label}</div>
</div>
))}
</div>
</div>
{/* Team Members */}
<div className="space-y-3">
{caseTeam
.sort((a, b) => {
const roleOrder = { 'lead_lawyer': 0, 'primary_lawyer': 1, 'assistant_lawyer': 2, 'secretary': 3 };
return roleOrder[a.role as keyof typeof roleOrder] - roleOrder[b.role as keyof typeof roleOrder];
})
.map((member) => {
const roleInfo = getRoleInfo(member.role);
const isCurrentUser = member.user.id === currentUser.id;
const onlineStatus = getOnlineStatus(member.user.id);
return (
<div
key={member.id}
className={`border rounded-lg transition-all duration-200 ${
isCurrentUser
? 'border-purple-200 bg-purple-50'
: 'border-gray-200 bg-white hover:shadow-md'
}`}
>
<div className="p-4">
<div className="flex items-start justify-between">
{/* Left side - Profile info */}
<div className="flex items-center gap-4 flex-1">
{/* Profile Avatar with online indicator */}
<div className="relative">
<button
onClick={() => handleProfileClick(member.user)}
className={`w-16 h-16 rounded-full flex items-center justify-center text-white font-semibold text-xl transition-transform hover:scale-105 ${
isCurrentUser
? 'bg-gradient-to-r from-purple-500 to-purple-600'
: 'bg-gradient-to-r from-primary to-primary-dark'
} cursor-pointer`}
title={`View ${member.user.name}'s profile`}
>
{member.user.name.charAt(0).toUpperCase()}
</button>
{/* Online status indicator */}
<div
className="absolute -bottom-1 -right-1 w-5 h-5 rounded-full bg-white flex items-center justify-center border-2 border-white"
title={onlineStatus.text}
>
<span className="text-xs">{onlineStatus.indicator}</span>
</div>
</div>
{/* User Information */}
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<button
onClick={() => handleProfileClick(member.user)}
className="font-bold text-gray-900 text-lg hover:text-primary transition-colors cursor-pointer"
title={`View ${member.user.name}'s profile`}
>
{member.user.name}
</button>
{isCurrentUser && (
<span className="text-sm text-purple-600 font-medium px-2 py-1 bg-purple-100 rounded-full">
You
</span>
)}
<span className={`text-xs font-medium ${onlineStatus.color}`}>
{onlineStatus.text}
</span>
</div>
<div className="space-y-1">
<div className="text-sm text-gray-600">{member.user.email}</div>
{member.user.title && (
<div className="text-sm text-blue-600 font-medium">{member.user.title}</div>
)}
{member.user.lawFirm && (
<div className="text-sm text-green-600 font-medium flex items-center gap-1">
π’ {member.user.lawFirm.name}
</div>
)}
<div className="text-xs text-gray-500 flex items-center gap-2">
<span>System Role: <span className="font-medium">{member.user.role}</span></span>
<span>β’</span>
<span>Since {new Date(member.assignedAt).toLocaleDateString()}</span>
</div>
</div>
</div>
</div>
{/* Right side - Role and Actions */}
<div className="flex flex-col items-end gap-3">
{/* Legal Role Badge */}
<span className={`inline-flex items-center gap-2 px-4 py-2 text-sm rounded-lg border font-medium ${roleInfo.color}`}>
<span className="text-lg">{roleInfo.icon}</span>
<span>{roleInfo.label}</span>
</span>
{/* Action Buttons */}
{!isCurrentUser && (
<div className="flex gap-2">
<button
onClick={() => handleProfileClick(member.user)}
className="px-3 py-2 text-xs font-medium text-blue-600 bg-blue-50 border border-blue-200 rounded-lg hover:bg-blue-100 transition-colors flex items-center gap-1"
title={`View ${member.user.name}'s profile`}
>
π€ Profile
</button>
{onlineStatus.isOnline && (
<button
onClick={() => handlePrivateChat(member.user.id, member.user.name)}
className="px-3 py-2 text-xs font-medium text-green-600 bg-green-50 border border-green-200 rounded-lg hover:bg-green-100 transition-colors flex items-center gap-1"
title={`Start private chat with ${member.user.name}`}
>
π¬ Chat
</button>
)}
</div>
)}
{isCurrentUser && (
<button
onClick={() => handleProfileClick(member.user)}
className="px-3 py-2 text-xs font-medium text-purple-600 bg-purple-50 border border-purple-200 rounded-lg hover:bg-purple-100 transition-colors flex items-center gap-1"
>
βοΈ Edit Profile
</button>
)}
</div>
</div>
</div>
</div>
);
})
}
</div>
</>
)}
</div>
</div>
</div>
</div>
</div>
)}
{/* Private Chat Modal */}
{privateChatMember && (
<TeamPrivateChat
member={privateChatMember}
currentUser={currentUser}
onClose={() => setPrivateChatMember(null)}
/>
)}
</div>
);
};
export default CaseAssignmentDashboard;