![]() 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 { useSession } from 'next-auth/react';
import {
Plus,
CheckCircle,
Clock,
AlertTriangle,
User,
Calendar,
Edit,
Trash2,
Filter,
Search,
ChevronDown,
ChevronUp,
FileText,
MessageSquare,
BarChart3,
Target,
Zap,
Award,
Users,
Tag,
X
} from 'lucide-react';
import { format } from 'date-fns';
import toast from 'react-hot-toast';
interface Task {
id: string;
title: string;
description?: string;
caseId: string;
assignedTo: string;
assignedBy: string;
priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'ON_HOLD' | 'CANCELLED';
dueDate?: string;
completedAt?: string;
estimatedHours?: number;
actualHours?: number;
tags?: string;
attachments?: string;
notes?: string;
createdAt: string;
updatedAt: string;
assignedToUser: {
id: string;
name: string;
email: string;
role: string;
profilePicture?: string;
};
assignedByUser: {
id: string;
name: string;
email: string;
role: string;
};
case: {
id: string;
title: string;
caseNumber?: string;
};
}
interface CaseTaskManagerProps {
caseId: string;
caseTitle: string;
onTaskUpdate?: () => void;
}
const CaseTaskManager: React.FC<CaseTaskManagerProps> = ({
caseId,
caseTitle,
onTaskUpdate
}) => {
const { data: session } = useSession();
const [tasks, setTasks] = useState<Task[]>([]);
const [loading, setLoading] = useState(true);
const [showCreateForm, setShowCreateForm] = useState(false);
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
const [filterStatus, setFilterStatus] = useState<string>('all');
const [filterPriority, setFilterPriority] = useState<string>('all');
const [searchTerm, setSearchTerm] = useState('');
const [sortBy, setSortBy] = useState<'dueDate' | 'priority' | 'status' | 'createdAt'>('dueDate');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
// Form state
const [formData, setFormData] = useState({
title: '',
description: '',
assignedTo: '',
priority: 'MEDIUM' as const,
dueDate: '',
estimatedHours: '',
tags: '',
notes: ''
});
useEffect(() => {
if (caseId) {
fetchTasks();
}
}, [caseId]);
const fetchTasks = async () => {
try {
const response = await fetch(`/api/cases/${caseId}/tasks`);
if (response.ok) {
const data = await response.json();
setTasks(data.tasks || []);
}
} catch (error) {
console.error('Error fetching tasks:', error);
toast.error('Failed to load tasks');
} finally {
setLoading(false);
}
};
const handleCreateTask = async (e: React.FormEvent) => {
e.preventDefault();
try {
const response = await fetch(`/api/cases/${caseId}/tasks`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...formData,
estimatedHours: formData.estimatedHours ? parseFloat(formData.estimatedHours) : null,
}),
});
if (response.ok) {
toast.success('Task created successfully');
setShowCreateForm(false);
setFormData({
title: '',
description: '',
assignedTo: '',
priority: 'MEDIUM',
dueDate: '',
estimatedHours: '',
tags: '',
notes: ''
});
fetchTasks();
onTaskUpdate?.();
} else {
const error = await response.json();
toast.error(error.message || 'Failed to create task');
}
} catch (error) {
console.error('Error creating task:', error);
toast.error('Failed to create task');
}
};
const handleUpdateTask = async (taskId: string, updates: Partial<Task>) => {
try {
const response = await fetch(`/api/cases/${caseId}/tasks/${taskId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(updates),
});
if (response.ok) {
toast.success('Task updated successfully');
fetchTasks();
onTaskUpdate?.();
} else {
const error = await response.json();
toast.error(error.message || 'Failed to update task');
}
} catch (error) {
console.error('Error updating task:', error);
toast.error('Failed to update task');
}
};
const handleDeleteTask = async (taskId: string) => {
if (!confirm('Are you sure you want to delete this task?')) return;
try {
const response = await fetch(`/api/cases/${caseId}/tasks/${taskId}`, {
method: 'DELETE',
});
if (response.ok) {
toast.success('Task deleted successfully');
fetchTasks();
onTaskUpdate?.();
} else {
const error = await response.json();
toast.error(error.message || 'Failed to delete task');
}
} catch (error) {
console.error('Error deleting task:', error);
toast.error('Failed to delete task');
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'URGENT': 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 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 'ON_HOLD': return 'bg-gray-100 text-gray-800 border-gray-200';
case 'CANCELLED': 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 <Clock className="h-4 w-4 text-blue-500" />;
case 'PENDING': return <Clock className="h-4 w-4 text-yellow-500" />;
case 'ON_HOLD': return <AlertTriangle className="h-4 w-4 text-gray-500" />;
case 'CANCELLED': return <X className="h-4 w-4 text-red-500" />;
default: return <Clock className="h-4 w-4 text-gray-400" />;
}
};
const filteredTasks = tasks
.filter(task => {
if (filterStatus !== 'all' && task.status !== filterStatus) return false;
if (filterPriority !== 'all' && task.priority !== filterPriority) return false;
if (searchTerm && !task.title.toLowerCase().includes(searchTerm.toLowerCase())) return false;
return true;
})
.sort((a, b) => {
let comparison = 0;
switch (sortBy) {
case 'dueDate':
comparison = (a.dueDate || '9999') > (b.dueDate || '9999') ? 1 : -1;
break;
case 'priority':
const priorityOrder = { 'URGENT': 4, 'HIGH': 3, 'MEDIUM': 2, 'LOW': 1 };
comparison = priorityOrder[a.priority] - priorityOrder[b.priority];
break;
case 'status':
const statusOrder = { 'PENDING': 1, 'IN_PROGRESS': 2, 'ON_HOLD': 3, 'COMPLETED': 4, 'CANCELLED': 5 };
comparison = statusOrder[a.status] - statusOrder[b.status];
break;
case 'createdAt':
comparison = new Date(a.createdAt) > new Date(b.createdAt) ? 1 : -1;
break;
}
return sortOrder === 'asc' ? comparison : -comparison;
});
const taskStats = {
total: tasks.length,
pending: tasks.filter(t => t.status === 'PENDING').length,
inProgress: tasks.filter(t => t.status === 'IN_PROGRESS').length,
completed: tasks.filter(t => t.status === 'COMPLETED').length,
overdue: tasks.filter(t => t.dueDate && new Date(t.dueDate) < new Date() && t.status !== 'COMPLETED').length
};
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>
);
}
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">
<Target className="h-5 w-5 mr-2 text-blue-600" />
Task Management
</h2>
<p className="text-sm text-gray-600">Manage case tasks and track progress</p>
</div>
<button
onClick={() => setShowCreateForm(true)}
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<Plus className="h-4 w-4 mr-2" />
New Task
</button>
</div>
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
{[
{ label: 'Total', value: taskStats.total, color: 'bg-blue-100 text-blue-800' },
{ label: 'Pending', value: taskStats.pending, color: 'bg-yellow-100 text-yellow-800' },
{ label: 'In Progress', value: taskStats.inProgress, color: 'bg-blue-100 text-blue-800' },
{ label: 'Completed', value: taskStats.completed, color: 'bg-green-100 text-green-800' },
{ label: 'Overdue', value: taskStats.overdue, color: 'bg-red-100 text-red-800' }
].map((stat, index) => (
<div key={index} className="text-center">
<div className={`text-lg font-semibold px-3 py-1 rounded-full ${stat.color}`}>
{stat.value}
</div>
<div className="text-xs text-gray-600 mt-1">{stat.label}</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">
<div className="flex-1 min-w-0">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Search tasks..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
</div>
<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="ON_HOLD">On Hold</option>
<option value="CANCELLED">Cancelled</option>
</select>
<select
value={filterPriority}
onChange={(e) => setFilterPriority(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 Priority</option>
<option value="URGENT">Urgent</option>
<option value="HIGH">High</option>
<option value="MEDIUM">Medium</option>
<option value="LOW">Low</option>
</select>
<select
value={`${sortBy}-${sortOrder}`}
onChange={(e) => {
const [field, order] = e.target.value.split('-');
setSortBy(field as any);
setSortOrder(order as any);
}}
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="dueDate-asc">Due Date (Earliest)</option>
<option value="dueDate-desc">Due Date (Latest)</option>
<option value="priority-desc">Priority (High to Low)</option>
<option value="priority-asc">Priority (Low to High)</option>
<option value="status-asc">Status</option>
<option value="createdAt-desc">Created (Newest)</option>
<option value="createdAt-asc">Created (Oldest)</option>
</select>
</div>
</div>
{/* Task List */}
<div className="p-6">
{filteredTasks.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 tasks found</p>
<p className="text-sm">Create your first task to get started</p>
</div>
) : (
<div className="space-y-4">
{filteredTasks.map((task) => (
<div
key={task.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(task.status)}
<h3 className="font-medium text-gray-900 truncate">{task.title}</h3>
<span className={`text-xs px-2 py-1 rounded-full border ${getPriorityColor(task.priority)}`}>
{task.priority}
</span>
<span className={`text-xs px-2 py-1 rounded-full border ${getStatusColor(task.status)}`}>
{task.status.replace('_', ' ')}
</span>
</div>
{task.description && (
<p className="text-sm text-gray-600 mb-3 line-clamp-2">{task.description}</p>
)}
<div className="flex items-center gap-4 text-sm text-gray-500">
<div className="flex items-center gap-1">
<User className="h-4 w-4" />
<span>{task.assignedToUser.name}</span>
</div>
{task.dueDate && (
<div className="flex items-center gap-1">
<Calendar className="h-4 w-4" />
<span className={new Date(task.dueDate) < new Date() && task.status !== 'COMPLETED' ? 'text-red-600 font-medium' : ''}>
{format(new Date(task.dueDate), 'MMM d, yyyy')}
</span>
</div>
)}
{task.estimatedHours && (
<div className="flex items-center gap-1">
<Clock className="h-4 w-4" />
<span>{task.estimatedHours}h estimated</span>
</div>
)}
{task.tags && (
<div className="flex items-center gap-1">
<Tag className="h-4 w-4" />
<span>{task.tags}</span>
</div>
)}
</div>
</div>
<div className="flex items-center gap-2 ml-4">
<button
onClick={() => setSelectedTask(task)}
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
title="Edit task"
>
<Edit className="h-4 w-4" />
</button>
{task.status !== 'COMPLETED' && (
<button
onClick={() => handleUpdateTask(task.id, { status: 'COMPLETED', completedAt: 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={() => handleDeleteTask(task.id)}
className="p-2 text-red-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
title="Delete task"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
{/* Create Task Modal */}
{showCreateForm && (
<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 Task</h3>
<p className="text-sm text-gray-600 mt-1">Add a new task for the case team</p>
</div>
<form onSubmit={handleCreateTask} 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">
Task 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">
Assign To *
</label>
<select
value={formData.assignedTo}
onChange={(e) => setFormData({ ...formData, assignedTo: 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
>
<option value="">Select team member...</option>
{/* This would be populated with case team members */}
<option value="user1">John Doe (Lawyer)</option>
<option value="user2">Jane Smith (Assistant)</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="URGENT">Urgent</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>
<label className="block text-sm font-medium text-gray-700 mb-2">
Tags
</label>
<input
type="text"
value={formData.tags}
onChange={(e) => setFormData({ ...formData, tags: e.target.value })}
placeholder="research, filing, client-meeting"
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={() => setShowCreateForm(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 Task
</button>
</div>
</form>
</div>
</div>
)}
{/* Edit Task Modal */}
{selectedTask && (
<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">Edit Task</h3>
<p className="text-sm text-gray-600 mt-1">Update task details and progress</p>
</div>
<div 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">
Task Title
</label>
<input
type="text"
value={selectedTask.title}
onChange={(e) => setSelectedTask({ ...selectedTask, 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"
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-2">
Description
</label>
<textarea
value={selectedTask.description || ''}
onChange={(e) => setSelectedTask({ ...selectedTask, 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">
Status
</label>
<select
value={selectedTask.status}
onChange={(e) => setSelectedTask({ ...selectedTask, status: 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="PENDING">Pending</option>
<option value="IN_PROGRESS">In Progress</option>
<option value="COMPLETED">Completed</option>
<option value="ON_HOLD">On Hold</option>
<option value="CANCELLED">Cancelled</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Priority
</label>
<select
value={selectedTask.priority}
onChange={(e) => setSelectedTask({ ...selectedTask, 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="URGENT">Urgent</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Due Date
</label>
<input
type="date"
value={selectedTask.dueDate ? selectedTask.dueDate.split('T')[0] : ''}
onChange={(e) => setSelectedTask({ ...selectedTask, 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">
Actual Hours
</label>
<input
type="number"
step="0.5"
value={selectedTask.actualHours || ''}
onChange={(e) => {
const value = e.target.value;
setSelectedTask({
...selectedTask,
actualHours: value === '' ? undefined : parseFloat(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>
<div className="flex items-center justify-end gap-3 pt-4 border-t border-gray-200">
<button
type="button"
onClick={() => setSelectedTask(null)}
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
>
Cancel
</button>
<button
onClick={() => {
handleUpdateTask(selectedTask.id, selectedTask);
setSelectedTask(null);
}}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Update Task
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default CaseTaskManager;