pmtool / src /components /todo /TodoItem.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
16.1 kB
import React, { useState, useEffect } from "react";
import { Todo } from "@/types";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Pencil,
Trash2,
CheckCircle2,
User,
Briefcase,
CalendarClock,
Clock,
ChevronRight,
CircleCheck,
Circle,
CheckCircle
} from "lucide-react";
import { format, isAfter, isPast, formatDistance } from "date-fns";
import { employeeService, projectService } from "@/lib/api";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { Progress } from "@/components/ui/progress";
import { cn } from "@/lib/utils";
type TodoItemProps = {
todo: Todo;
onEdit?: (todo: Todo) => void;
onDelete?: (id: number) => void;
onStatusChange: (id: number, status: Todo['status']) => void;
};
const getPriorityColor = (priority: Todo['priority']) => {
switch (priority) {
case 'Low':
return {
bg: 'bg-white',
border: 'border-l-4 border-emerald-400',
text: 'text-emerald-700',
badge: 'bg-emerald-50 text-emerald-700 border border-emerald-200'
};
case 'Medium':
return {
bg: 'bg-white',
border: 'border-l-4 border-amber-400',
text: 'text-amber-700',
badge: 'bg-amber-50 text-amber-700 border border-amber-200'
};
case 'High':
return {
bg: 'bg-white',
border: 'border-l-4 border-rose-500',
text: 'text-rose-700',
badge: 'bg-rose-50 text-rose-700 border border-rose-200'
};
default:
return {
bg: 'bg-white',
border: 'border-l-4 border-slate-400',
text: 'text-slate-700',
badge: 'bg-slate-50 text-slate-700 border border-slate-200'
};
}
};
const getStatusColor = (status: Todo['status']) => {
switch (status) {
case 'Pending':
return {
bg: 'bg-slate-50',
text: 'text-slate-700',
badge: 'bg-white text-slate-700 border border-slate-200',
icon: <Clock className="h-3.5 w-3.5 mr-1 text-slate-500" />
};
case 'In Progress':
return {
bg: 'bg-blue-50',
text: 'text-blue-700',
badge: 'bg-white text-blue-700 border border-blue-200',
icon: <Progress className="h-1.5 w-14 mr-1" value={50} />
};
case 'Completed':
return {
bg: 'bg-green-50',
text: 'text-green-700',
badge: 'bg-white text-green-700 border border-green-200',
icon: <CircleCheck className="h-3.5 w-3.5 mr-1 text-green-500" />
};
default:
return {
bg: 'bg-white',
text: 'text-slate-700',
badge: 'bg-white text-slate-700 border border-slate-200',
icon: null
};
}
};
const getStatusProgress = (status: Todo['status']): number => {
switch (status) {
case 'Pending': return 0;
case 'In Progress': return 50;
case 'Completed': return 100;
default: return 0;
}
};
export function TodoItem({ todo, onEdit, onDelete, onStatusChange }: TodoItemProps) {
const [assignee, setAssignee] = useState<{ name: string; email: string } | null>(null);
const [project, setProject] = useState<{ name: string } | null>(null);
const [isLoadingAssignee, setIsLoadingAssignee] = useState(false);
const [isLoadingProject, setIsLoadingProject] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const priorityStyles = getPriorityColor(todo.priority);
const statusStyles = getStatusColor(todo.status);
const dueDate = new Date(todo.dueDate);
const isPastDue = isPast(dueDate) && todo.status !== 'Completed';
const statusProgress = getStatusProgress(todo.status);
const isCompleted = todo.status === 'Completed';
useEffect(() => {
const fetchAssignee = async () => {
if (!todo.assigneeId) return;
setIsLoadingAssignee(true);
try {
const response = await employeeService.getById(todo.assigneeId);
if (response && response.id) {
setAssignee({
name: `${response.firstName} ${response.lastName}`,
email: response.email
});
}
} catch (error) {
console.error("Failed to fetch assignee details", error);
} finally {
setIsLoadingAssignee(false);
}
};
fetchAssignee();
}, [todo.assigneeId]);
useEffect(() => {
const fetchProject = async () => {
if (!todo.projectId) return;
setIsLoadingProject(true);
try {
const response = await projectService.getById(todo.projectId);
if (response && response.id) {
setProject({
name: response.projectName
});
}
} catch (error) {
console.error("Failed to fetch project details", error);
} finally {
setIsLoadingProject(false);
}
};
fetchProject();
}, [todo.projectId]);
const handleStatusChange = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
let newStatus: Todo['status'] = 'Pending';
switch (todo.status) {
case 'Pending':
newStatus = 'In Progress';
break;
case 'In Progress':
newStatus = 'Completed';
break;
case 'Completed':
newStatus = 'Pending';
break;
}
onStatusChange(todo.id, newStatus);
};
const toggleComplete = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
const newStatus = isCompleted ? 'Pending' : 'Completed';
onStatusChange(todo.id, newStatus);
};
const getInitials = (name: string) => {
return name.split(' ')
.map(part => part.charAt(0))
.join('')
.toUpperCase();
};
const toggleExpand = () => {
setIsExpanded(!isExpanded);
};
return (
<Card
className={cn(
"transition-all duration-200 hover:shadow-lg",
priorityStyles.bg,
priorityStyles.border,
"border border-slate-200 rounded-xl",
isCompleted && "opacity-90 bg-slate-50",
"shadow-sm overflow-hidden"
)}
>
<div className="cursor-pointer group">
<CardHeader className="p-3 sm:p-4 pb-2">
{/* Mobile-first layout */}
<div className="flex items-start gap-3">
{/* Checkbox */}
<div
className="mt-1 flex-shrink-0 relative"
onClick={toggleComplete}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div className={cn(
"size-5 rounded-full flex items-center justify-center transition-colors",
isCompleted ?
"text-blue-600 bg-blue-50 border border-blue-300" :
"text-gray-400 border border-gray-300 bg-white",
isHovered && !isCompleted && "border-blue-500 text-blue-500",
isHovered && isCompleted && "bg-blue-100"
)}>
{isCompleted ? (
<CheckCircle className="size-5 fill-blue-600 text-white" />
) : (
<Circle className="size-5" />
)}
</div>
</div>
{/* Main content */}
<div className="flex-1 min-w-0" onClick={toggleExpand}>
{/* Title and Priority - Mobile responsive */}
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2 mb-2">
<h3 className={cn(
"text-sm sm:text-base font-medium flex-1",
priorityStyles.text,
isCompleted && "line-through text-gray-500"
)}>
{todo.title}
</h3>
<Badge className={cn(
"font-normal text-xs rounded-full px-2 py-0.5 self-start sm:self-center",
priorityStyles.badge
)}>
{todo.priority}
</Badge>
</div>
{/* Description */}
<p className={cn(
"text-xs sm:text-sm text-gray-500 line-clamp-2 mb-2",
isCompleted && "line-through"
)}>
{todo.description}
</p>
{/* Status and Date - Mobile responsive */}
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Badge className={cn(
"font-normal flex items-center rounded-full px-2 py-1 text-xs w-fit",
statusStyles.badge
)}>
{statusStyles.icon}
<span className="hidden sm:inline">{todo.status}</span>
<span className="sm:hidden">
{todo.status === 'In Progress' ? 'In Prog.' : todo.status}
</span>
</Badge>
</TooltipTrigger>
<TooltipContent>
<p>Status: {todo.status}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<div className={cn(
"flex items-center text-xs",
isPastDue ? "text-rose-600" : "text-gray-500"
)}>
<CalendarClock className="h-3 w-3 mr-1" />
<span className="hidden sm:inline">{format(dueDate, 'MMM dd, yyyy')}</span>
<span className="sm:hidden">{format(dueDate, 'MMM dd')}</span>
{isPastDue && (
<span className="ml-1 font-medium text-rose-600">
<span className="hidden sm:inline">(Overdue)</span>
<span className="sm:hidden">!</span>
</span>
)}
</div>
</div>
</div>
</div>
</CardHeader>
{/* Progress bar */}
<div className="px-3 sm:px-4 pb-2" onClick={toggleExpand}>
<Progress
value={statusProgress}
className={cn(
"h-1.5",
todo.status === 'Pending' ? "bg-slate-100" :
todo.status === 'In Progress' ? "bg-blue-100" : "bg-green-100"
)}
/>
</div>
</div>
{isExpanded && (
<CardContent className="p-3 sm:p-4 pt-2 bg-slate-50 border-t border-slate-200">
{/* Full description */}
<div className="text-xs sm:text-sm text-gray-600 mb-4">
{todo.description}
</div>
{/* Details grid - Mobile responsive */}
<div className="grid grid-cols-1 gap-3 text-xs sm:text-sm mb-4">
<div className="flex items-center text-gray-600">
<CalendarClock className="h-4 w-4 mr-2 text-gray-500 flex-shrink-0" />
<span className="font-medium mr-1">Due:</span>
<span>{format(dueDate, 'PPP')}</span>
</div>
{todo.assigneeId && (
<div className="flex items-center text-gray-600">
<User className="h-4 w-4 mr-2 text-gray-500 flex-shrink-0" />
<span className="font-medium mr-1">Assigned to:</span>
{isLoadingAssignee ? (
<span className="italic">Loading...</span>
) : assignee ? (
<div className="flex items-center">
<Avatar className="h-5 w-5 mr-2">
<AvatarFallback className="text-[10px] bg-blue-100 text-blue-700">
{getInitials(assignee.name)}
</AvatarFallback>
</Avatar>
<span className="font-medium">{assignee.name}</span>
</div>
) : (
<span>Employee {todo.assigneeId}</span>
)}
</div>
)}
{todo.projectId && (
<div className="flex items-center text-gray-600">
<Briefcase className="h-4 w-4 mr-2 text-gray-500 flex-shrink-0" />
<span className="font-medium mr-1">Project:</span>
{isLoadingProject ? (
<span className="italic">Loading...</span>
) : project ? (
<span className="font-medium">{project.name}</span>
) : (
<span>Project {todo.projectId}</span>
)}
</div>
)}
<div className="flex items-center text-gray-600">
<Clock className="h-4 w-4 mr-2 text-gray-500 flex-shrink-0" />
<span className="font-medium mr-1">Created:</span>
<span>{format(new Date(todo.createdAt || Date.now()), 'MMM dd, yyyy')}</span>
</div>
</div>
{/* Action buttons - Mobile responsive */}
<div className="flex flex-col sm:flex-row gap-2 sm:justify-between sm:items-center pt-3 border-t border-slate-200">
<div className="flex flex-wrap gap-2">
{onEdit && (
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation();
onEdit(todo);
}}
className="bg-white hover:bg-gray-50 border-gray-200 text-gray-700 flex-1 sm:flex-none"
>
<Pencil className="h-3.5 w-3.5 mr-1" />
<span className="hidden sm:inline">Edit</span>
<span className="sm:hidden">Edit</span>
</Button>
)}
{onDelete && (
<Button
size="sm"
variant="outline"
className="text-rose-600 hover:text-rose-700 hover:bg-rose-50 bg-white border-rose-200 flex-1 sm:flex-none"
onClick={(e) => {
e.stopPropagation();
onDelete(todo.id);
}}
>
<Trash2 className="h-3.5 w-3.5 mr-1" />
<span className="hidden sm:inline">Delete</span>
<span className="sm:hidden">Delete</span>
</Button>
)}
</div>
<Button
size="sm"
variant="outline"
className={cn(
"bg-white border w-full sm:w-auto",
todo.status === 'Pending' ? "text-blue-600 hover:text-blue-700 hover:bg-blue-50 border-blue-200" :
todo.status === 'In Progress' ? "text-blue-600 hover:text-blue-700 hover:bg-blue-50 border-blue-200" :
"text-green-600 hover:text-green-700 hover:bg-green-50 border-green-200"
)}
onClick={handleStatusChange}
>
<CheckCircle2 className="h-3.5 w-3.5 mr-1" />
{todo.status === 'Pending' ? 'Start Task' :
todo.status === 'In Progress' ? 'Mark Complete' : 'Reopen Task'}
</Button>
</div>
</CardContent>
)}
</Card>
);
}