![]() 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, useEffect } from 'react';
import { useSession } from 'next-auth/react';
import {
CheckCircle,
Clock,
AlertTriangle,
Calendar,
TrendingUp,
Target,
Award,
Users,
FileText,
MessageSquare,
BarChart3,
Zap,
Flag,
Star,
ArrowRight,
ArrowLeft,
Play,
Pause,
Square,
Edit
} from 'lucide-react';
import { format } from 'date-fns';
import toast from 'react-hot-toast';
interface CaseMilestone {
id: string;
title: string;
description?: string;
status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'BLOCKED';
dueDate?: string;
completedDate?: string;
assignedTo?: string;
priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
type: 'RESEARCH' | 'FILING' | 'HEARING' | 'SETTLEMENT' | 'TRIAL' | 'APPEAL' | 'CLOSURE';
dependencies?: string[];
estimatedHours?: number;
actualHours?: number;
notes?: string;
}
interface CaseProgress {
caseId: string;
overallProgress: number;
currentPhase: string;
estimatedCompletion: string;
totalMilestones: number;
completedMilestones: number;
overdueMilestones: number;
nextMilestone?: CaseMilestone;
recentActivity: Array<{
id: string;
type: 'MILESTONE_COMPLETED' | 'MILESTONE_STARTED' | 'TASK_COMPLETED' | 'DOCUMENT_ADDED' | 'UPDATE_POSTED';
title: string;
description?: string;
timestamp: string;
user: {
id: string;
name: string;
role: string;
};
}>;
milestones: CaseMilestone[];
}
interface CaseProgressTrackerProps {
caseId: string;
caseTitle: string;
onProgressUpdate?: () => void;
}
const CaseProgressTracker: React.FC<CaseProgressTrackerProps> = ({
caseId,
caseTitle,
onProgressUpdate
}) => {
const { data: session } = useSession();
const [progress, setProgress] = useState<CaseProgress | null>(null);
const [loading, setLoading] = useState(true);
const [showMilestoneForm, setShowMilestoneForm] = useState(false);
const [selectedMilestone, setSelectedMilestone] = useState<CaseMilestone | null>(null);
const [filterType, setFilterType] = useState<string>('all');
const [filterStatus, setFilterStatus] = useState<string>('all');
// Form state
const [formData, setFormData] = useState({
title: '',
description: '',
type: 'RESEARCH' as const,
priority: 'MEDIUM' as const,
dueDate: '',
estimatedHours: '',
assignedTo: '',
notes: ''
});
useEffect(() => {
if (caseId) {
fetchProgress();
}
}, [caseId]);
const fetchProgress = async () => {
try {
const response = await fetch(`/api/cases/${caseId}/progress`);
if (response.ok) {
const data = await response.json();
setProgress(data);
}
} catch (error) {
console.error('Error fetching progress:', error);
toast.error('Failed to load progress data');
} finally {
setLoading(false);
}
};
const handleCreateMilestone = async (e: React.FormEvent) => {
e.preventDefault();
try {
const response = await fetch(`/api/cases/${caseId}/milestones`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...formData,
estimatedHours: formData.estimatedHours ? parseFloat(formData.estimatedHours) : null,
}),
});
if (response.ok) {
toast.success('Milestone created successfully');
setShowMilestoneForm(false);
setFormData({
title: '',
description: '',
type: 'RESEARCH',
priority: 'MEDIUM',
dueDate: '',
estimatedHours: '',
assignedTo: '',
notes: ''
});
fetchProgress();
onProgressUpdate?.();
} else {
const error = await response.json();
toast.error(error.message || 'Failed to create milestone');
}
} catch (error) {
console.error('Error creating milestone:', error);
toast.error('Failed to create milestone');
}
};
const handleUpdateMilestone = async (milestoneId: string, updates: Partial<CaseMilestone>) => {
try {
const response = await fetch(`/api/cases/${caseId}/milestones/${milestoneId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(updates),
});
if (response.ok) {
toast.success('Milestone updated successfully');
fetchProgress();
onProgressUpdate?.();
} else {
const error = await response.json();
toast.error(error.message || 'Failed to update milestone');
}
} catch (error) {
console.error('Error updating milestone:', error);
toast.error('Failed to update milestone');
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'COMPLETED': return 'bg-green-100 text-green-800 border-green-200';
case 'IN_PROGRESS': return 'bg-blue-100 text-blue-800 border-blue-200';
case 'PENDING': return 'bg-yellow-100 text-yellow-800 border-yellow-200';
case 'BLOCKED': return 'bg-red-100 text-red-800 border-red-200';
default: return 'bg-gray-100 text-gray-800 border-gray-200';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'COMPLETED': return <CheckCircle className="h-4 w-4 text-green-500" />;
case 'IN_PROGRESS': return <Play className="h-4 w-4 text-blue-500" />;
case 'PENDING': return <Clock className="h-4 w-4 text-yellow-500" />;
case 'BLOCKED': return <Pause className="h-4 w-4 text-red-500" />;
default: return <Clock className="h-4 w-4 text-gray-400" />;
}
};
const getTypeIcon = (type: string) => {
switch (type) {
case 'RESEARCH': return <FileText className="h-4 w-4" />;
case 'FILING': return <FileText className="h-4 w-4" />;
case 'HEARING': return <Users className="h-4 w-4" />;
case 'SETTLEMENT': return <MessageSquare className="h-4 w-4" />;
case 'TRIAL': return <Award className="h-4 w-4" />;
case 'APPEAL': return <TrendingUp className="h-4 w-4" />;
case 'CLOSURE': return <CheckCircle className="h-4 w-4" />;
default: return <Target className="h-4 w-4" />;
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'CRITICAL': return 'bg-red-100 text-red-800 border-red-200';
case 'HIGH': return 'bg-orange-100 text-orange-800 border-orange-200';
case 'MEDIUM': return 'bg-blue-100 text-blue-800 border-blue-200';
case 'LOW': return 'bg-gray-100 text-gray-800 border-gray-200';
default: return 'bg-gray-100 text-gray-800 border-gray-200';
}
};
const getPhaseColor = (phase: string) => {
switch (phase.toLowerCase()) {
case 'initiation': return 'bg-blue-100 text-blue-800';
case 'discovery': return 'bg-purple-100 text-purple-800';
case 'pre-trial': return 'bg-orange-100 text-orange-800';
case 'trial': return 'bg-red-100 text-red-800';
case 'post-trial': return 'bg-green-100 text-green-800';
case 'appeal': return 'bg-indigo-100 text-indigo-800';
case 'closure': return 'bg-gray-100 text-gray-800';
default: return 'bg-blue-100 text-blue-800';
}
};
const filteredMilestones = progress?.milestones.filter(milestone => {
if (filterType !== 'all' && milestone.type !== filterType) return false;
if (filterStatus !== 'all' && milestone.status !== filterStatus) return false;
return true;
}) || [];
if (loading) {
return (
<div className="bg-white rounded-lg shadow-sm border p-6">
<div className="animate-pulse space-y-4">
<div className="h-6 bg-gray-200 rounded w-1/3"></div>
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
<div className="space-y-2">
{[1, 2, 3].map(i => (
<div key={i} className="h-16 bg-gray-200 rounded"></div>
))}
</div>
</div>
</div>
);
}
if (!progress) {
return (
<div className="bg-white rounded-lg shadow-sm border p-6">
<div className="text-center py-8 text-gray-500">
<Target className="h-12 w-12 mx-auto mb-4 text-gray-300" />
<p className="text-lg font-medium">No progress data available</p>
<p className="text-sm">Progress tracking will appear here once milestones are created</p>
</div>
</div>
);
}
return (
<div className="bg-white rounded-lg shadow-sm border">
{/* Header */}
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-lg font-semibold text-gray-900 flex items-center">
<TrendingUp className="h-5 w-5 mr-2 text-blue-600" />
Case Progress
</h2>
<p className="text-sm text-gray-600">Track case milestones and overall progress</p>
</div>
<button
onClick={() => setShowMilestoneForm(true)}
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<Target className="h-4 w-4 mr-2" />
Add Milestone
</button>
</div>
{/* Progress Overview */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div className="text-center">
<div className="text-2xl font-bold text-blue-600">{progress.overallProgress}%</div>
<div className="text-sm text-gray-600">Overall Progress</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-green-600">{progress.completedMilestones}</div>
<div className="text-sm text-gray-600">Completed</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-yellow-600">{progress.totalMilestones - progress.completedMilestones}</div>
<div className="text-sm text-gray-600">Remaining</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-red-600">{progress.overdueMilestones}</div>
<div className="text-sm text-gray-600">Overdue</div>
</div>
</div>
{/* Progress Bar */}
<div className="mb-4">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700">Progress</span>
<span className="text-sm text-gray-500">{progress.overallProgress}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-gradient-to-r from-blue-500 to-green-500 h-2 rounded-full transition-all duration-300"
style={{ width: `${progress.overallProgress}%` }}
></div>
</div>
</div>
{/* Current Phase */}
<div className="flex items-center justify-between">
<div>
<span className="text-sm text-gray-600">Current Phase:</span>
<span className={`ml-2 px-2 py-1 rounded-full text-xs font-medium ${getPhaseColor(progress.currentPhase)}`}>
{progress.currentPhase}
</span>
</div>
<div className="text-right">
<span className="text-sm text-gray-600">Estimated Completion:</span>
<div className="text-sm font-medium text-gray-900">
{format(new Date(progress.estimatedCompletion), 'MMM d, yyyy')}
</div>
</div>
</div>
</div>
{/* Filters */}
<div className="p-4 border-b border-gray-200 bg-gray-50">
<div className="flex flex-wrap items-center gap-4">
<select
value={filterType}
onChange={(e) => setFilterType(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">All Types</option>
<option value="RESEARCH">Research</option>
<option value="FILING">Filing</option>
<option value="HEARING">Hearing</option>
<option value="SETTLEMENT">Settlement</option>
<option value="TRIAL">Trial</option>
<option value="APPEAL">Appeal</option>
<option value="CLOSURE">Closure</option>
</select>
<select
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">All Status</option>
<option value="PENDING">Pending</option>
<option value="IN_PROGRESS">In Progress</option>
<option value="COMPLETED">Completed</option>
<option value="BLOCKED">Blocked</option>
</select>
</div>
</div>
{/* Milestones */}
<div className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Milestones</h3>
{filteredMilestones.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<Target className="h-12 w-12 mx-auto mb-4 text-gray-300" />
<p className="text-lg font-medium">No milestones found</p>
<p className="text-sm">Create your first milestone to track progress</p>
</div>
) : (
<div className="space-y-4">
{filteredMilestones.map((milestone, index) => (
<div
key={milestone.id}
className="border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow"
>
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-2">
{getStatusIcon(milestone.status)}
<div className="flex items-center gap-2">
{getTypeIcon(milestone.type)}
<h4 className="font-medium text-gray-900">{milestone.title}</h4>
</div>
<span className={`text-xs px-2 py-1 rounded-full border ${getStatusColor(milestone.status)}`}>
{milestone.status.replace('_', ' ')}
</span>
<span className={`text-xs px-2 py-1 rounded-full border ${getPriorityColor(milestone.priority)}`}>
{milestone.priority}
</span>
</div>
{milestone.description && (
<p className="text-sm text-gray-600 mb-3">{milestone.description}</p>
)}
<div className="flex items-center gap-4 text-sm text-gray-500">
{milestone.dueDate && (
<div className="flex items-center gap-1">
<Calendar className="h-4 w-4" />
<span className={new Date(milestone.dueDate) < new Date() && milestone.status !== 'COMPLETED' ? 'text-red-600 font-medium' : ''}>
{format(new Date(milestone.dueDate), 'MMM d, yyyy')}
</span>
</div>
)}
{milestone.estimatedHours && (
<div className="flex items-center gap-1">
<Clock className="h-4 w-4" />
<span>{milestone.estimatedHours}h estimated</span>
</div>
)}
{milestone.completedDate && (
<div className="flex items-center gap-1">
<CheckCircle className="h-4 w-4 text-green-500" />
<span>Completed {format(new Date(milestone.completedDate), 'MMM d, yyyy')}</span>
</div>
)}
</div>
</div>
<div className="flex items-center gap-2 ml-4">
{milestone.status === 'PENDING' && (
<button
onClick={() => handleUpdateMilestone(milestone.id, { status: 'IN_PROGRESS' })}
className="p-2 text-blue-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
title="Start milestone"
>
<Play className="h-4 w-4" />
</button>
)}
{milestone.status === 'IN_PROGRESS' && (
<button
onClick={() => handleUpdateMilestone(milestone.id, { status: 'COMPLETED', completedDate: new Date().toISOString() })}
className="p-2 text-green-400 hover:text-green-600 hover:bg-green-50 rounded-lg transition-colors"
title="Mark as completed"
>
<CheckCircle className="h-4 w-4" />
</button>
)}
<button
onClick={() => setSelectedMilestone(milestone)}
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
title="Edit milestone"
>
<Edit className="h-4 w-4" />
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
{/* Recent Activity */}
{progress.recentActivity.length > 0 && (
<div className="p-6 border-t border-gray-200">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Recent Activity</h3>
<div className="space-y-3">
{progress.recentActivity.slice(0, 5).map((activity) => (
<div key={activity.id} className="flex items-start gap-3">
<div className="w-2 h-2 bg-blue-500 rounded-full mt-2"></div>
<div className="flex-1 min-w-0">
<p className="text-sm text-gray-900">{activity.title}</p>
{activity.description && (
<p className="text-xs text-gray-600 mt-1">{activity.description}</p>
)}
<div className="flex items-center gap-2 mt-1 text-xs text-gray-500">
<span>{activity.user.name}</span>
<span>•</span>
<span>{format(new Date(activity.timestamp), 'MMM d, h:mm a')}</span>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Create Milestone Modal */}
{showMilestoneForm && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-lg shadow-xl w-full max-w-2xl max-h-[90vh] overflow-y-auto">
<div className="p-6 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900">Create New Milestone</h3>
<p className="text-sm text-gray-600 mt-1">Add a new milestone to track case progress</p>
</div>
<form onSubmit={handleCreateMilestone} className="p-6 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-2">
Milestone Title *
</label>
<input
type="text"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-2">
Description
</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Type *
</label>
<select
value={formData.type}
onChange={(e) => setFormData({ ...formData, type: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required
>
<option value="RESEARCH">Research</option>
<option value="FILING">Filing</option>
<option value="HEARING">Hearing</option>
<option value="SETTLEMENT">Settlement</option>
<option value="TRIAL">Trial</option>
<option value="APPEAL">Appeal</option>
<option value="CLOSURE">Closure</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Priority
</label>
<select
value={formData.priority}
onChange={(e) => setFormData({ ...formData, priority: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="LOW">Low</option>
<option value="MEDIUM">Medium</option>
<option value="HIGH">High</option>
<option value="CRITICAL">Critical</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Due Date
</label>
<input
type="date"
value={formData.dueDate}
onChange={(e) => setFormData({ ...formData, dueDate: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Estimated Hours
</label>
<input
type="number"
step="0.5"
value={formData.estimatedHours}
onChange={(e) => setFormData({ ...formData, estimatedHours: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-2">
Notes
</label>
<textarea
value={formData.notes}
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
rows={2}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
</div>
<div className="flex items-center justify-end gap-3 pt-4 border-t border-gray-200">
<button
type="button"
onClick={() => setShowMilestoneForm(false)}
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Create Milestone
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
};
export default CaseProgressTracker;