pmtool / src /components /tasks /MonthlyTaskCalendar.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
14.6 kB
import React, { useState, useMemo } from 'react';
import {
format,
startOfMonth,
endOfMonth,
startOfWeek,
endOfWeek,
eachDayOfInterval,
isSameMonth,
isSameDay,
addMonths,
subMonths,
isToday,
parseISO,
isWithinInterval,
startOfDay,
endOfDay
} from 'date-fns';
import { ChevronLeft, ChevronRight, Calendar as CalendarIcon, CheckCircle2, Circle, Clock, Users } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { Task } from '@/services/tasksApi';
import { Badge } from '@/components/ui/badge';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
interface Employee {
id: number;
firstName: string;
lastName: string;
profileImage?: string;
}
interface MonthlyTaskCalendarProps {
tasks: Task[];
employees?: Employee[];
onSelectDate?: (date: Date) => void;
isLoading?: boolean;
}
export const MonthlyTaskCalendar: React.FC<MonthlyTaskCalendarProps> = ({
tasks,
employees = [], // Default to empty array if not provided
onSelectDate,
isLoading = false
}) => {
const [currentMonth, setCurrentMonth] = useState(new Date());
const nextMonth = () => setCurrentMonth(addMonths(currentMonth, 1));
const prevMonth = () => setCurrentMonth(subMonths(currentMonth, 1));
const goToToday = () => setCurrentMonth(new Date());
const monthStart = startOfMonth(currentMonth);
const monthEnd = endOfMonth(monthStart);
const startDate = startOfWeek(monthStart);
const endDate = endOfWeek(monthEnd);
const calendarDays = eachDayOfInterval({ start: startDate, end: endDate });
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
// Memoize tasks for each day
// Since tasks can span multiple days, we need to check intervals
const getTasksForDate = (date: Date) => {
return tasks.filter(task => {
if (!task.stateDate) return false;
const taskStart = startOfDay(parseISO(task.stateDate));
let taskEnd = task.endDate ? endOfDay(parseISO(task.endDate)) : taskStart;
// Handle invalid date ranges where end date is before start date
if (taskEnd < taskStart) {
taskEnd = taskStart;
}
try {
return isWithinInterval(date, { start: taskStart, end: taskEnd });
} catch (e) {
console.error("Error checking date interval for task:", task.id, e);
return false;
}
});
};
const getPriorityColor = (priority?: string) => {
switch (priority?.toLowerCase()) {
case 'high': return 'bg-red-100 text-red-700 border-red-200';
case 'medium': return 'bg-orange-100 text-orange-700 border-orange-200';
case 'low': return 'bg-green-100 text-green-700 border-green-200';
default: return 'bg-gray-100 text-gray-700 border-gray-200';
}
};
const getStatusColor = (status: string) => {
switch (status) {
case "New": return "bg-blue-500";
case "In Progress": return "bg-orange-500";
case "In Review": return "bg-purple-500";
case "Completed": return "bg-green-500";
case "Closed": return "bg-gray-500";
default: return "bg-slate-500";
}
};
const getEmployeeDetails = (id: number | null) => {
if (!id) return null;
return employees.find(e => e.id === id);
};
if (isLoading) {
return (
<div className="flex justify-center items-center h-96 bg-white rounded-xl shadow-sm border border-gray-100">
<div className="flex flex-col items-center gap-3">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600"></div>
<p className="text-sm text-gray-500">Loading calendar...</p>
</div>
</div>
);
}
return (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden flex flex-col h-full min-h-[600px]">
{/* Calendar Header */}
<div className="flex flex-col sm:flex-row items-center justify-between p-4 border-b border-gray-200 gap-4 bg-white">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-full bg-indigo-50 flex items-center justify-center text-indigo-600">
<CalendarIcon className="h-5 w-5" />
</div>
<div>
<h2 className="text-xl font-bold text-gray-800 leading-none">
{format(currentMonth, 'MMMM yyyy')}
</h2>
<p className="text-sm text-gray-500 mt-1">
Manage tasks schedule
</p>
</div>
</div>
<div className="flex items-center gap-2 bg-gray-50 p-1 rounded-lg border border-gray-100">
<Button variant="ghost" size="icon" onClick={prevMonth} className="h-8 w-8 hover:bg-white hover:shadow-sm">
<ChevronLeft className="h-4 w-4" />
</Button>
<Button variant="ghost" size="sm" onClick={goToToday} className="h-8 text-xs font-medium px-3 hover:bg-white hover:shadow-sm">
Today
</Button>
<Button variant="ghost" size="icon" onClick={nextMonth} className="h-8 w-8 hover:bg-white hover:shadow-sm">
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
{/* Week Days Header */}
<div className="grid grid-cols-7 border-b border-gray-200 bg-gray-50/50">
{weekDays.map(day => (
<div key={day} className="py-3 text-center text-xs font-semibold text-gray-500 uppercase tracking-wider">
{day}
</div>
))}
</div>
{/* Calendar Grid */}
<div className="grid grid-cols-7 flex-1 auto-rows-fr bg-gray-100 gap-px">
{calendarDays.map((day, dayIdx) => {
const dayTasks = getTasksForDate(day);
const isCurrentMonth = isSameMonth(day, currentMonth);
const isDayToday = isToday(day);
return (
<div
key={day.toString()}
className={cn(
"bg-white p-2 min-h-[120px] flex flex-col gap-1 transition-all hover:bg-gray-50/80 relative group",
!isCurrentMonth && "bg-gray-50/30 text-gray-400",
isDayToday && "bg-indigo-50/10 ring-1 ring-inset ring-indigo-500/20"
)}
onClick={() => onSelectDate && onSelectDate(day)}
>
<div className="flex justify-between items-start mb-1">
<span className={cn(
"text-sm font-medium h-7 w-7 flex items-center justify-center rounded-full transition-colors",
isDayToday
? "bg-indigo-600 text-white shadow-sm"
: "text-gray-700 group-hover:bg-gray-200"
)}>
{format(day, 'd')}
</span>
{dayTasks.length > 0 && (
<Badge variant="secondary" className="text-[10px] h-5 px-1.5">
{dayTasks.length}
</Badge>
)}
</div>
<div className="flex-1 flex flex-col gap-1 overflow-y-auto max-h-[140px] custom-scrollbar">
{dayTasks.map((task, idx) => {
const assignee = getEmployeeDetails(task.assignedTo);
return (
<TooltipProvider key={`${task.id}-${dayIdx}`}>
<Tooltip delayDuration={0}>
<TooltipTrigger asChild>
<div className={cn(
"text-[10px] p-1.5 rounded-md border truncate cursor-pointer transition-all shadow-sm transform hover:scale-[1.02]",
"bg-white border-gray-200 hover:border-indigo-300 hover:shadow-md hover:z-10 bg-gradient-to-l from-transparent via-transparent to-transparent flex items-center gap-1.5"
)}>
<div className={cn("w-1.5 h-1.5 rounded-full flex-shrink-0", getStatusColor(task.status))} />
<span className={cn("font-medium truncate flex-1", task.status === 'Completed' && "line-through text-gray-400")}>
{task.title}
</span>
{assignee && (
<div className="h-4 w-4 bg-gray-100 rounded-full flex items-center justify-center text-[8px] flex-shrink-0" title={`${assignee.firstName} ${assignee.lastName}`}>
{assignee.firstName.charAt(0)}{assignee.lastName.charAt(0)}
</div>
)}
</div>
</TooltipTrigger>
<TooltipContent side="right" className="p-0 border-none shadow-xl z-50">
<div className="w-64 bg-white rounded-lg border border-gray-200 overflow-hidden">
<div className={cn("px-3 py-2 border-b border-gray-200 flex justify-between items-center", getPriorityColor(task.type).replace('text-', 'bg-').replace('border-', 'border-transparent text-white'))}>
<span className="font-semibold text-xs text-gray-700">{task.type} Priority</span>
<Badge variant="outline" className="text-[10px] bg-white text-black border-none">
{task.status}
</Badge>
</div>
<div className="p-3">
<h4 className="font-bold text-sm text-gray-800 mb-1 leading-tight">{task.title}</h4>
<p className="text-xs text-gray-500 mb-2">#{task.id}</p>
{assignee && (
<div className="flex items-center gap-2 mb-2">
<div className="h-5 w-5 bg-indigo-100 rounded-full flex items-center justify-center text-[9px] text-indigo-700 font-bold">
{assignee.firstName.charAt(0)}{assignee.lastName.charAt(0)}
</div>
<span className="text-xs text-gray-700">{assignee.firstName} {assignee.lastName}</span>
</div>
)}
<div className="grid grid-cols-2 gap-2 mt-2 pt-2 border-t border-gray-100">
<div className="text-xs">
<span className="text-gray-400 block mb-0.5">Start</span>
<span className="text-gray-700 font-medium">{task.stateDate ? format(parseISO(task.stateDate), 'MMM d') : '-'}</span>
</div>
<div className="text-xs">
<span className="text-gray-400 block mb-0.5">Due</span>
<span className="text-gray-700 font-medium">{task.endDate ? format(parseISO(task.endDate), 'MMM d') : '-'}</span>
</div>
</div>
</div>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
})}
</div>
</div>
);
})}
</div>
</div>
);
};