import React, { useState, useEffect } from "react"; import { Todo, TodoCreateRequest, TodoUpdateRequest } from "@/types"; import { TodoItem } from "./TodoItem"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import CustomDialog from "@/components/CustomDialog"; import { TodoForm } from "./TodoForm"; import { Plus, Search, SlidersHorizontal, Calendar, Check, ChevronDown, CalendarIcon, X, ListIcon, Kanban, ChevronRight, CheckCircle, Filter } from "lucide-react"; import { todoService } from "@/services/todoService"; import { useToast } from "@/components/ui/use-toast"; import { useAuth } from "@/lib/auth-context"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; import { Calendar as CalendarComponent } from "@/components/ui/calendar"; import { format, isAfter, isBefore, isSameDay, isWithinInterval, startOfDay, endOfDay, addDays, subDays, isPast } from "date-fns"; import { Loader2 } from "lucide-react"; import { ClipboardX } from "lucide-react"; const sortOptions = [ { value: "title-asc", label: "Title (A-Z)" }, { value: "title-desc", label: "Title (Z-A)" }, { value: "dueDate-asc", label: "Due Date (Ascending)" }, { value: "dueDate-desc", label: "Due Date (Descending)" }, { value: "priority-desc", label: "Priority (High to Low)" }, { value: "priority-asc", label: "Priority (Low to High)" }, ]; const dateFilterOptions = [ { value: "all", label: "All Dates" }, { value: "today", label: "Due Today" }, { value: "tomorrow", label: "Due Tomorrow" }, { value: "thisWeek", label: "Due This Week" }, { value: "overdue", label: "Overdue" }, { value: "custom", label: "Custom Range" }, ]; // Custom Date Filter Dropdown Component function CustomDateFilterDropdown({ onChange, value }) { const [open, setOpen] = useState(false); const [selectedOption, setSelectedOption] = useState(dateFilterOptions.find(opt => opt.value === value) || dateFilterOptions[0]); const [startDate, setStartDate] = useState(undefined); const [endDate, setEndDate] = useState(undefined); const [showCalendar, setShowCalendar] = useState(false); const dropdownRef = React.useRef(null); // Handle outside clicks to close the dropdown useEffect(() => { if (!open) return; const handleOutsideClick = (e: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { setOpen(false); setShowCalendar(false); } }; // Use mousedown instead of click to ensure this fires before any other click handlers document.addEventListener('mousedown', handleOutsideClick); return () => document.removeEventListener('mousedown', handleOutsideClick); }, [open]); const handleButtonClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); setOpen(!open); }; const handleOptionClick = (option: typeof dateFilterOptions[0], e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); setSelectedOption(option); if (option.value !== 'custom') { onChange({ type: option.value, startDate: undefined, endDate: undefined }); setStartDate(undefined); setEndDate(undefined); setShowCalendar(false); setOpen(false); } else { setShowCalendar(true); } }; const handleDateSelect = (date: Date | undefined) => { if (!date) return; if (!startDate) { setStartDate(date); } else if (!endDate && isAfter(date, startDate)) { setEndDate(date); // After selecting both dates, apply the filter but don't close automatically onChange({ type: 'custom', startDate, endDate: date }); } else { // Reset and start over if second date is before first setStartDate(date); setEndDate(undefined); } }; const handleClearFilter = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); setSelectedOption(dateFilterOptions[0]); setStartDate(undefined); setEndDate(undefined); onChange({ type: 'all', startDate: undefined, endDate: undefined }); setOpen(false); }; let displayText = selectedOption.label; if (selectedOption.value === 'custom' && startDate) { displayText = endDate ? `${format(startDate, 'MMM d')} - ${format(endDate, 'MMM d')}` : `From ${format(startDate, 'MMM d')}`; } return (
{displayText}
{selectedOption.value !== 'all' && ( )}
{open && (
e.stopPropagation()} > {!showCalendar ? (
{dateFilterOptions.map((option) => (
handleOptionClick(option, e)} > {option.label} {selectedOption.value === option.value && }
))}
) : (
{!startDate ? 'Select start date' : !endDate ? 'Select end date' : 'Date range selected'}
{startDate && !endDate && (
Start: {format(startDate, 'PPP')}
)} {startDate && endDate && (
)}
)}
)}
); } // Custom Filter Dropdown Component function CustomFilterDropdown({ statusFilter, priorityFilter, setStatusFilter, setPriorityFilter, clearAllFilters, activeFiltersCount }) { const [open, setOpen] = useState(false); const dropdownRef = React.useRef(null); // Handle outside clicks to close the dropdown useEffect(() => { if (!open) return; const handleOutsideClick = (e: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { setOpen(false); } }; // Use mousedown instead of click to ensure this fires before any other click handlers document.addEventListener('mousedown', handleOutsideClick); return () => document.removeEventListener('mousedown', handleOutsideClick); }, [open]); const handleButtonClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); setOpen(!open); }; return (
{open && (
e.stopPropagation()} >

Filter Todos

Select filters to narrow down your todo list

Status

Priority

{activeFiltersCount > 0 && ( )}
)}
); } // Custom Sort By Dropdown Component function CustomSortDropdown({ value, onChange }) { const [open, setOpen] = useState(false); const [selectedOption, setSelectedOption] = useState(sortOptions.find(opt => opt.value === value) || sortOptions[0]); const dropdownRef = React.useRef(null); // Handle outside clicks to close the dropdown useEffect(() => { if (!open) return; const handleOutsideClick = (e: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { setOpen(false); } }; // Use mousedown instead of click to ensure this fires before any other click handlers document.addEventListener('mousedown', handleOutsideClick); return () => document.removeEventListener('mousedown', handleOutsideClick); }, [open]); const handleButtonClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); setOpen(!open); }; const handleOptionClick = (option: typeof sortOptions[0], e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); setSelectedOption(option); onChange(option.value); setOpen(false); }; return (
{selectedOption.label}
{open && (
e.stopPropagation()} >
{sortOptions.map((option) => (
handleOptionClick(option, e)} > {option.label} {selectedOption.value === option.value && }
))}
)}
); } type TodoListProps = { projectId?: number; assigneeId?: number; }; export function TodoList({ projectId, assigneeId }: TodoListProps) { const { userData } = useAuth(); const [todos, setTodos] = useState([]); const [filteredTodos, setFilteredTodos] = useState([]); const [isLoading, setIsLoading] = useState(false); const [searchTerm, setSearchTerm] = useState(""); const [sortBy, setSortBy] = useState("dueDate-asc"); const [statusFilter, setStatusFilter] = useState("All"); const [priorityFilter, setPriorityFilter] = useState("All"); const [dateFilter, setDateFilter] = useState({ type: 'all', startDate: undefined, endDate: undefined }); const [isFormOpen, setIsFormOpen] = useState(false); const [editingTodo, setEditingTodo] = useState(undefined); const [viewMode, setViewMode] = useState<'list' | 'kanban'>('list'); const [showCompleted, setShowCompleted] = useState(false); const [showFilters, setShowFilters] = useState(false); const { toast } = useToast(); // Define roles that can add/edit todos const isAdmin = userData?.roleId === 21; // Admin // const canManageTodos = isAdmin || // userData?.roleName === 'Project Manager' || // userData?.roleName === 'Tech Lead'; const canManageTodos = true; // Fetch todos based on props and user role const fetchTodos = async () => { setIsLoading(true); try { let response; // If projectId is provided, fetch todos for that project if (projectId) { response = await todoService.getTodosByProject(projectId); } // If assigneeId is manually provided via props, use that else if (assigneeId) { response = await todoService.getTodosByAssignee(assigneeId); } // If user is not admin and no specific assigneeId is provided, show user's own todos else if (!isAdmin && userData?.employeeId) { response = await todoService.getTodosByAssignee(userData.employeeId); } // Admin with no filters gets all todos else { response = await todoService.getAllTodos(); } if (response.success) { setTodos(response.data); } else { toast({ title: "Error", description: response.message || "Failed to fetch todos", variant: "destructive", }); } } catch (error) { toast({ title: "Error", description: "An unexpected error occurred", variant: "destructive", }); console.error("Error fetching todos:", error); } finally { setIsLoading(false); } }; useEffect(() => { fetchTodos(); }, [projectId, assigneeId, userData?.employeeId]); // Filter and sort todos useEffect(() => { let result = [...todos]; // Apply search filter if (searchTerm) { const term = searchTerm.toLowerCase(); result = result.filter( (todo) => todo.title.toLowerCase().includes(term) || todo.description.toLowerCase().includes(term) ); } // Apply status filter if (statusFilter !== "All") { result = result.filter((todo) => todo.status === statusFilter); } // Apply priority filter if (priorityFilter !== "All") { result = result.filter((todo) => todo.priority === priorityFilter); } // Apply date filter if (dateFilter.type !== 'all') { const today = startOfDay(new Date()); const tomorrow = startOfDay(addDays(new Date(), 1)); const nextWeek = startOfDay(addDays(new Date(), 7)); result = result.filter((todo) => { const dueDate = startOfDay(new Date(todo.dueDate)); switch (dateFilter.type) { case 'today': return isSameDay(dueDate, today); case 'tomorrow': return isSameDay(dueDate, tomorrow); case 'thisWeek': return isAfter(dueDate, today) && isBefore(dueDate, nextWeek); case 'overdue': return isBefore(dueDate, today); case 'custom': if (dateFilter.startDate && dateFilter.endDate) { return isWithinInterval(dueDate, { start: startOfDay(dateFilter.startDate), end: endOfDay(dateFilter.endDate) }); } return true; default: return true; } }); } // Apply sorting const [sortField, sortDirection] = sortBy.split("-"); result.sort((a, b) => { if (sortField === "dueDate") { const dateA = new Date(a.dueDate).getTime(); const dateB = new Date(b.dueDate).getTime(); return sortDirection === "asc" ? dateA - dateB : dateB - dateA; } else if (sortField === "priority") { const priorityOrder = { Low: 1, Medium: 2, High: 3 }; const priorityA = priorityOrder[a.priority as keyof typeof priorityOrder]; const priorityB = priorityOrder[b.priority as keyof typeof priorityOrder]; return sortDirection === "asc" ? priorityA - priorityB : priorityB - priorityA; } else if (sortField === "title") { return sortDirection === "asc" ? a.title.localeCompare(b.title) : b.title.localeCompare(a.title); } return 0; }); setFilteredTodos(result); }, [todos, searchTerm, sortBy, statusFilter, priorityFilter, dateFilter]); // Handle todo creation const handleCreateTodo = async (data: TodoCreateRequest) => { setIsLoading(true); try { const response = await todoService.createTodo(data); if (response.success) { setTodos((prev) => [...prev, response.data]); setIsFormOpen(false); toast({ title: "Success", description: "Todo created successfully", }); } else { toast({ title: "Error", description: response.message || "Failed to create todo", variant: "destructive", }); } } catch (error) { toast({ title: "Error", description: "An unexpected error occurred", variant: "destructive", }); } finally { setIsLoading(false); } }; // Handle todo update const handleUpdateTodo = async (data: TodoUpdateRequest) => { setIsLoading(true); try { const response = await todoService.updateTodo(data); if (response.success) { setTodos((prev) => prev.map((todo) => (todo.id === data.id ? response.data : todo)) ); setIsFormOpen(false); setEditingTodo(undefined); toast({ title: "Success", description: "Todo updated successfully", }); } else { toast({ title: "Error", description: response.message || "Failed to update todo", variant: "destructive", }); } } catch (error) { toast({ title: "Error", description: "An unexpected error occurred", variant: "destructive", }); } finally { setIsLoading(false); } }; // Handle todo deletion const handleDeleteTodo = async (id: number) => { if (window.confirm("Are you sure you want to delete this todo?")) { setIsLoading(true); try { const response = await todoService.deleteTodo(id); if (response.success) { setTodos((prev) => prev.filter((todo) => todo.id !== id)); toast({ title: "Success", description: "Todo deleted successfully", }); } else { toast({ title: "Error", description: response.message || "Failed to delete todo", variant: "destructive", }); } } catch (error) { toast({ title: "Error", description: "An unexpected error occurred", variant: "destructive", }); } finally { setIsLoading(false); } } }; // Handle todo status change const handleStatusChange = async (id: number, status: Todo["status"]) => { try { setIsLoading(true); const todo = todos.find((t) => t.id === id); if (!todo) { toast({ title: "Error", description: "Todo not found", variant: "destructive", }); return; } const updatedTodo: TodoUpdateRequest = { ...todo, status, }; const response = await todoService.updateTodo(updatedTodo); if (response.success) { setTodos((prev) => prev.map((t) => (t.id === id ? { ...t, status } : t)) ); toast({ title: "Success", description: `Todo status updated to ${status}`, }); } else { toast({ title: "Error", description: response.message || "Failed to update todo status", variant: "destructive", }); } } catch (error) { toast({ title: "Error", description: "An unexpected error occurred while updating status", variant: "destructive", }); console.error("Error updating todo status:", error); } finally { setIsLoading(false); } }; // Open form for editing const handleEditTodo = (todo: Todo) => { setEditingTodo(todo); setIsFormOpen(true); }; // Close form and reset state const handleCloseForm = () => { setIsFormOpen(false); setEditingTodo(undefined); }; // Handle form submission const handleFormSubmit = (data: TodoCreateRequest | TodoUpdateRequest) => { if ("id" in data) { handleUpdateTodo(data as TodoUpdateRequest); } else { handleCreateTodo(data as TodoCreateRequest); } }; // Generate active filters count for display const getActiveFiltersCount = () => { let count = 0; if (statusFilter !== "All") count++; if (priorityFilter !== "All") count++; if (dateFilter.type !== "all") count++; return count; }; // Toggle completed tasks visibility const toggleCompletedVisibility = () => { setShowCompleted(!showCompleted); }; const activeFiltersCount = getActiveFiltersCount(); // Group todos by completion status const activeTodos = filteredTodos.filter(todo => todo.status !== 'Completed'); const completedTodos = filteredTodos.filter(todo => todo.status === 'Completed'); // Group todos by status for kanban view const kanbanTodos = { 'Pending': filteredTodos.filter(todo => todo.status === 'Pending'), 'In Progress': filteredTodos.filter(todo => todo.status === 'In Progress'), 'Completed': filteredTodos.filter(todo => todo.status === 'Completed') }; // Generate summary statistics const stats = { total: filteredTodos.length, pending: filteredTodos.filter(todo => todo.status === 'Pending').length, inProgress: filteredTodos.filter(todo => todo.status === 'In Progress').length, completed: filteredTodos.filter(todo => todo.status === 'Completed').length, high: filteredTodos.filter(todo => todo.priority === 'High').length, overdue: filteredTodos.filter(todo => isPast(new Date(todo.dueDate)) && todo.status !== 'Completed' ).length }; // Handle clearing all filters const clearAllFilters = () => { setStatusFilter("All"); setPriorityFilter("All"); setDateFilter({ type: 'all', startDate: undefined, endDate: undefined }); }; return (
{/* Header Section - Mobile Responsive */}

{projectId ? "Project Todos" : assigneeId ? (userData?.employeeId === assigneeId ? "My Todos" : "Assignee Todos") : isAdmin ? "All Todos" : "My Todos"}

{/* Stats Badges - Mobile Responsive */}
Total: {stats.total} Pending: {stats.pending} In Progress: {stats.inProgress} Completed: {stats.completed} {stats.overdue > 0 && ( Overdue: {stats.overdue} )}
{canManageTodos && ( )}
{/* Search and Filters - Mobile Responsive */}
{/* Always visible: Search Bar and Filter Toggle */}
{/* Search Bar with Mobile Filter Toggle */}
setSearchTerm(e.target.value)} />
{/* Mobile Filter Toggle Button */}
{/* Desktop: Always visible filters */}
{/* View Mode Toggle */}
{/* Filter Controls */}
{/* Mobile: Collapsible filters */} {showFilters && (
{/* View Mode Toggle */}
{/* Filter Controls */}
)}
{isLoading && !todos.length ? (

Loading todos...

) : filteredTodos.length === 0 ? (

No todos found

{searchTerm || statusFilter !== "All" || priorityFilter !== "All" || dateFilter.type !== "all" ? "Try adjusting your filters" : "Create a new todo to get started"}

{canManageTodos && ( )}
) : viewMode === 'list' ? (
{/* Active Todos Section */} {activeTodos.length > 0 && (
{activeTodos.map((todo) => ( ))}
)} {/* Completed Todos Section - Collapsible */} {completedTodos.length > 0 && (
{showCompleted && (
{completedTodos.map((todo) => ( ))}
)}
)}
) : (
{/* Mobile: Stacked columns, Desktop: Grid layout */}
{(['Pending', 'In Progress', 'Completed'] as Todo['status'][]).map(status => (
{/* Column header */}
{status}
{kanbanTodos[status].length}
{/* Column content */}
{kanbanTodos[status].length === 0 ? (

No {status.toLowerCase()} todos

{status === 'Pending' ? 'Tasks will appear here when created' : status === 'In Progress' ? 'Tasks will appear here when started' : 'Tasks will appear here when completed'}

) : ( kanbanTodos[status].map(todo => (
)) )}
))}
)}
); }