![]() 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/components/ |
import React, { useState } from 'react';
import { format } from 'date-fns';
import {
Calendar,
CheckCircle,
Clock,
AlertTriangle,
FileText,
Users,
DollarSign,
Scale,
MapPin,
MessageSquare,
ChevronRight,
ChevronDown,
ExternalLink,
Star,
Award,
Zap
} from 'lucide-react';
interface TimelineEvent {
id: string;
title: string;
description?: string;
date: string;
type: 'milestone' | 'update' | 'deadline' | 'achievement' | 'warning';
status: 'completed' | 'pending' | 'upcoming' | 'overdue';
icon?: React.ReactNode;
metadata?: {
participants?: string[];
location?: string;
amount?: number;
documents?: string[];
};
}
interface CaseTimelineProps {
caseData: any;
className?: string;
}
const getEventIcon = (type: string, status: string) => {
const baseClasses = "h-6 w-6";
switch (type) {
case 'milestone':
return status === 'completed' ?
<CheckCircle className={`${baseClasses} text-green-500`} /> :
<Calendar className={`${baseClasses} text-blue-500`} />;
case 'update':
return <MessageSquare className={`${baseClasses} text-purple-500`} />;
case 'deadline':
return status === 'overdue' ?
<AlertTriangle className={`${baseClasses} text-red-500`} /> :
<Clock className={`${baseClasses} text-orange-500`} />;
case 'achievement':
return <Award className={`${baseClasses} text-yellow-500`} />;
case 'warning':
return <AlertTriangle className={`${baseClasses} text-red-500`} />;
default:
return <FileText className={`${baseClasses} text-gray-500`} />;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'completed': return 'border-green-500 bg-green-50';
case 'pending': return 'border-yellow-500 bg-yellow-50';
case 'upcoming': return 'border-blue-500 bg-blue-50';
case 'overdue': return 'border-red-500 bg-red-50';
default: return 'border-gray-300 bg-gray-50';
}
};
const getStatusText = (status: string) => {
switch (status) {
case 'completed': return 'Completed';
case 'pending': return 'In Progress';
case 'upcoming': return 'Upcoming';
case 'overdue': return 'Overdue';
default: return 'Unknown';
}
};
const CaseTimeline: React.FC<CaseTimelineProps> = ({ caseData, className = '' }) => {
const [expandedEvents, setExpandedEvents] = useState<Set<string>>(new Set());
const [selectedFilter, setSelectedFilter] = useState<string>('all');
// Helper function to safely parse dates
const safeParseDate = (dateString: string | null | undefined): Date | null => {
if (!dateString) return null;
const date = new Date(dateString);
return isNaN(date.getTime()) ? null : date;
};
// Generate timeline events from case data
const generateTimelineEvents = (): TimelineEvent[] => {
const events: TimelineEvent[] = [];
// Validate caseData exists
if (!caseData) return events;
// Case creation
if (caseData.createdAt) {
events.push({
id: 'case-created',
title: 'Case Created',
description: `Case "${caseData.title || 'Untitled Case'}" was created and made public`,
date: caseData.createdAt,
type: 'milestone',
status: 'completed',
metadata: {
participants: [caseData.creator?.name || 'Unknown']
}
});
}
// Application deadline
if (caseData.applicationDeadline) {
const deadlineDate = safeParseDate(caseData.applicationDeadline);
if (deadlineDate) {
const now = new Date();
const status = deadlineDate < now ? 'overdue' : 'upcoming';
events.push({
id: 'application-deadline',
title: 'Application Deadline',
description: 'Last day to submit applications for this case',
date: caseData.applicationDeadline,
type: 'deadline',
status,
metadata: {
participants: ['All Applicants']
}
});
}
}
// Filing date
if (caseData.filingDate) {
const filingDate = safeParseDate(caseData.filingDate);
if (filingDate) {
const now = new Date();
const status = filingDate < now ? 'completed' : 'upcoming';
events.push({
id: 'case-filed',
title: 'Case Filed',
description: 'Legal case was officially filed with the court',
date: caseData.filingDate,
type: 'milestone',
status,
metadata: {
location: caseData.court || 'Court',
participants: [caseData.leadLawyer?.name || 'Lead Lawyer']
}
});
}
}
// Case updates
if (caseData.caseUpdates && Array.isArray(caseData.caseUpdates)) {
caseData.caseUpdates.forEach((update: any, index: number) => {
if (update && update.id && update.createdAt) {
events.push({
id: `update-${update.id}`,
title: update.title || 'Case Update',
description: update.description,
date: update.createdAt,
type: 'update',
status: 'completed',
metadata: {
participants: [update.author?.name || 'Unknown']
}
});
}
});
}
// Expected completion
if (caseData.expectedDuration && caseData.createdAt) {
const createdDate = safeParseDate(caseData.createdAt);
if (createdDate && typeof caseData.expectedDuration === 'number') {
const expectedDate = new Date(createdDate.getTime() + (caseData.expectedDuration * 24 * 60 * 60 * 1000));
const now = new Date();
const status = expectedDate < now ? 'overdue' : 'upcoming';
events.push({
id: 'expected-completion',
title: 'Expected Completion',
description: `Estimated completion date based on ${caseData.expectedDuration} days duration`,
date: expectedDate.toISOString(),
type: 'milestone',
status,
metadata: {
participants: [caseData.leadLawyer?.name || 'Lead Lawyer']
}
});
}
}
// Achievements based on stats
if (caseData._count?.registrations && caseData._count.registrations >= 5) {
events.push({
id: 'high-interest',
title: 'High Interest Achieved',
description: 'Case has received significant interest from the community',
date: new Date().toISOString(),
type: 'achievement',
status: 'completed',
metadata: {
participants: ['Community']
}
});
}
if (caseData._count?.supporters && caseData._count.supporters >= 10) {
events.push({
id: 'community-support',
title: 'Strong Community Support',
description: 'Case has gained strong community backing',
date: new Date().toISOString(),
type: 'achievement',
status: 'completed',
metadata: {
participants: ['Supporters']
}
});
}
// Sort events by date
return events.sort((a, b) => {
const dateA = safeParseDate(a.date);
const dateB = safeParseDate(b.date);
if (!dateA || !dateB) return 0;
return dateA.getTime() - dateB.getTime();
});
};
const timelineEvents = generateTimelineEvents();
const filteredEvents = selectedFilter === 'all'
? timelineEvents
: timelineEvents.filter(event => event.type === selectedFilter);
const toggleEventExpansion = (eventId: string) => {
const newExpanded = new Set(expandedEvents);
if (newExpanded.has(eventId)) {
newExpanded.delete(eventId);
} else {
newExpanded.add(eventId);
}
setExpandedEvents(newExpanded);
};
const getProgressPercentage = () => {
if (timelineEvents.length === 0) return 0;
const completedEvents = timelineEvents.filter(event => event.status === 'completed');
return Math.round((completedEvents.length / timelineEvents.length) * 100);
};
// Safe date formatting
const safeFormatDate = (dateString: string) => {
const date = safeParseDate(dateString);
if (!date) return 'Invalid Date';
try {
return format(date, 'MMM d, yyyy');
} catch {
return 'Invalid Date';
}
};
const safeFormatFullDate = (dateString: string) => {
const date = safeParseDate(dateString);
if (!date) return 'Invalid Date';
try {
return format(date, 'PPP');
} catch {
return 'Invalid Date';
}
};
const safeFormatTime = (dateString: string) => {
const date = safeParseDate(dateString);
if (!date) return 'Invalid Time';
try {
return format(date, 'p');
} catch {
return 'Invalid Time';
}
};
return (
<div className={`bg-white rounded-xl shadow-sm border border-gray-200 p-8 ${className}`}>
{/* Header */}
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg flex items-center justify-center">
<Zap className="h-5 w-5 text-white" />
</div>
<div>
<h2 className="text-2xl font-bold text-gray-900">Case Timeline</h2>
<p className="text-gray-600">Track the progress and key milestones</p>
</div>
</div>
{/* Progress Overview */}
<div className="mb-8">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-medium text-gray-700">Overall Progress</span>
<span className="text-sm font-bold text-blue-600">{getProgressPercentage()}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-3">
<div
className="bg-gradient-to-r from-blue-500 to-purple-600 h-3 rounded-full transition-all duration-1000 ease-out"
style={{ width: `${getProgressPercentage()}%` }}
></div>
</div>
<div className="flex justify-between text-xs text-gray-500 mt-2">
<span>{timelineEvents.filter(e => e.status === 'completed').length} completed</span>
<span>{timelineEvents.filter(e => e.status === 'pending').length} in progress</span>
<span>{timelineEvents.filter(e => e.status === 'upcoming').length} upcoming</span>
</div>
</div>
{/* Filter Tabs */}
<div className="flex flex-wrap gap-2 mb-6">
{[
{ key: 'all', label: 'All Events', count: timelineEvents.length },
{ key: 'milestone', label: 'Milestones', count: timelineEvents.filter(e => e.type === 'milestone').length },
{ key: 'update', label: 'Updates', count: timelineEvents.filter(e => e.type === 'update').length },
{ key: 'deadline', label: 'Deadlines', count: timelineEvents.filter(e => e.type === 'deadline').length },
{ key: 'achievement', label: 'Achievements', count: timelineEvents.filter(e => e.type === 'achievement').length }
].map(filter => (
<button
key={filter.key}
onClick={() => setSelectedFilter(filter.key)}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
selectedFilter === filter.key
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
{filter.label} ({filter.count})
</button>
))}
</div>
{/* Timeline */}
<div className="relative">
{/* Timeline Line */}
<div className="absolute left-6 top-0 bottom-0 w-0.5 bg-gray-200"></div>
<div className="space-y-6">
{filteredEvents.map((event, index) => {
const isExpanded = expandedEvents.has(event.id);
const isLast = index === filteredEvents.length - 1;
return (
<div key={event.id} className="relative">
{/* Timeline Dot */}
<div className={`absolute left-6 w-4 h-4 rounded-full border-2 transform -translate-x-1/2 -translate-y-1/2 ${
event.status === 'completed' ? 'bg-green-500 border-green-500' :
event.status === 'pending' ? 'bg-yellow-500 border-yellow-500' :
event.status === 'overdue' ? 'bg-red-500 border-red-500' :
'bg-blue-500 border-blue-500'
}`}>
{event.status === 'completed' && (
<CheckCircle className="w-4 h-4 text-white" />
)}
</div>
{/* Event Content */}
<div className={`ml-12 p-4 rounded-lg border transition-all duration-200 hover:shadow-md ${
getStatusColor(event.status)
} ${isExpanded ? 'shadow-md' : ''}`}>
<div className="flex items-start justify-between">
<div className="flex items-start gap-3 flex-1">
<div className="flex-shrink-0 mt-1">
{getEventIcon(event.type, event.status)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h3 className="font-semibold text-gray-900">{event.title}</h3>
<span className={`px-2 py-1 text-xs font-medium rounded-full ${
event.status === 'completed' ? 'bg-green-100 text-green-800' :
event.status === 'pending' ? 'bg-yellow-100 text-yellow-800' :
event.status === 'overdue' ? 'bg-red-100 text-red-800' :
'bg-blue-100 text-blue-800'
}`}>
{getStatusText(event.status)}
</span>
</div>
<p className="text-sm text-gray-600 mb-2">
{safeFormatDate(event.date)}
</p>
{event.description && (
<p className="text-gray-700 mb-3">{event.description}</p>
)}
{/* Metadata */}
{event.metadata && (
<div className="space-y-2">
{event.metadata.participants && (
<div className="flex items-center gap-2 text-sm text-gray-600">
<Users className="h-4 w-4" />
<span>{event.metadata.participants.join(', ')}</span>
</div>
)}
{event.metadata.location && (
<div className="flex items-center gap-2 text-sm text-gray-600">
<MapPin className="h-4 w-4" />
<span>{event.metadata.location}</span>
</div>
)}
{event.metadata.amount && (
<div className="flex items-center gap-2 text-sm text-gray-600">
<DollarSign className="h-4 w-4" />
<span>${event.metadata.amount.toLocaleString()}</span>
</div>
)}
</div>
)}
</div>
</div>
{/* Expand/Collapse Button */}
<button
onClick={() => toggleEventExpansion(event.id)}
className="flex-shrink-0 p-1 hover:bg-gray-100 rounded transition-colors"
>
{isExpanded ? (
<ChevronDown className="h-4 w-4 text-gray-500" />
) : (
<ChevronRight className="h-4 w-4 text-gray-500" />
)}
</button>
</div>
{/* Expanded Content */}
{isExpanded && (
<div className="mt-4 pt-4 border-t border-gray-200">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h4 className="font-medium text-gray-900 mb-2">Event Details</h4>
<div className="space-y-2 text-sm">
<div>
<span className="font-medium text-gray-700">Type:</span>
<span className="ml-2 text-gray-600 capitalize">{event.type}</span>
</div>
<div>
<span className="font-medium text-gray-700">Date:</span>
<span className="ml-2 text-gray-600">
{safeFormatFullDate(event.date)}
</span>
</div>
<div>
<span className="font-medium text-gray-700">Time:</span>
<span className="ml-2 text-gray-600">
{safeFormatTime(event.date)}
</span>
</div>
</div>
</div>
{event.metadata && (
<div>
<h4 className="font-medium text-gray-900 mb-2">Additional Info</h4>
<div className="space-y-2 text-sm">
{event.metadata.documents && event.metadata.documents.length > 0 && (
<div>
<span className="font-medium text-gray-700">Documents:</span>
<div className="mt-1 space-y-1">
{event.metadata.documents.map((doc, idx) => (
<div key={idx} className="flex items-center gap-2 text-blue-600 hover:text-blue-800">
<FileText className="h-3 w-3" />
<span className="text-xs">{doc}</span>
</div>
))}
</div>
</div>
)}
</div>
</div>
)}
</div>
</div>
)}
</div>
</div>
);
})}
</div>
{/* End Cap */}
<div className="absolute left-6 bottom-0 w-4 h-4 rounded-full bg-gray-300 transform -translate-x-1/2 translate-y-1/2"></div>
</div>
{/* Empty State */}
{filteredEvents.length === 0 && (
<div className="text-center py-12">
<Clock className="h-12 w-12 text-gray-300 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No events found</h3>
<p className="text-gray-500">Try selecting a different filter or check back later for updates.</p>
</div>
)}
</div>
);
};
export default CaseTimeline;