pmtool / src /components /todo /TodoList.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
46.5 kB
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<Date | undefined>(undefined);
const [endDate, setEndDate] = useState<Date | undefined>(undefined);
const [showCalendar, setShowCalendar] = useState(false);
const dropdownRef = React.useRef<HTMLDivElement>(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 (
<div className="relative" ref={dropdownRef}>
<div
className={cn(
"flex h-10 items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background cursor-pointer",
open && "ring-2 ring-ring ring-offset-2"
)}
onClick={handleButtonClick}
>
<div className="flex items-center">
<CalendarIcon className="h-4 w-4 mr-2" />
<span>{displayText}</span>
</div>
<div className="flex items-center">
{selectedOption.value !== 'all' && (
<X
className="h-4 w-4 mr-1 hover:text-destructive"
onClick={handleClearFilter}
/>
)}
<ChevronDown className="h-4 w-4 opacity-50" />
</div>
</div>
{open && (
<div
className="absolute z-[100] mt-1 w-auto min-w-[280px] overflow-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md right-0"
onClick={(e) => e.stopPropagation()}
>
{!showCalendar ? (
<div className="p-1">
{dateFilterOptions.map((option) => (
<div
key={option.value}
className={cn(
"flex cursor-pointer items-center justify-between rounded-sm px-2 py-1.5 hover:bg-accent hover:text-accent-foreground",
selectedOption.value === option.value && "bg-accent text-accent-foreground"
)}
onClick={(e) => handleOptionClick(option, e)}
>
<span>{option.label}</span>
{selectedOption.value === option.value && <Check className="h-4 w-4" />}
</div>
))}
</div>
) : (
<div className="p-3">
<div className="mb-2 text-sm font-medium">
{!startDate ? 'Select start date' : !endDate ? 'Select end date' : 'Date range selected'}
</div>
<CalendarComponent
mode="single"
selected={endDate || startDate}
onSelect={handleDateSelect}
initialFocus
/>
{startDate && !endDate && (
<div className="mt-2 text-sm text-muted-foreground">
Start: {format(startDate, 'PPP')}
</div>
)}
{startDate && endDate && (
<div className="mt-2 flex justify-between">
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setStartDate(undefined);
setEndDate(undefined);
}}
type="button"
>
Reset
</Button>
<Button
size="sm"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onChange({
type: 'custom',
startDate,
endDate
});
setOpen(false);
setShowCalendar(false);
}}
type="button"
>
Apply
</Button>
</div>
)}
</div>
)}
</div>
)}
</div>
);
}
// Custom Filter Dropdown Component
function CustomFilterDropdown({
statusFilter,
priorityFilter,
setStatusFilter,
setPriorityFilter,
clearAllFilters,
activeFiltersCount
}) {
const [open, setOpen] = useState(false);
const dropdownRef = React.useRef<HTMLDivElement>(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 (
<div className="relative" ref={dropdownRef}>
<Button
variant="outline"
size="icon"
className="relative border-blue-200"
onClick={handleButtonClick}
>
<SlidersHorizontal className="h-4 w-4 text-blue-600" />
{activeFiltersCount > 0 && (
<span className="absolute -top-1 -right-1 h-5 w-5 rounded-full bg-blue-600 text-[10px] flex items-center justify-center text-white font-medium">
{activeFiltersCount}
</span>
)}
</Button>
{open && (
<div
className="absolute z-[100] mt-1 w-auto min-w-[280px] overflow-auto rounded-md border bg-popover p-4 text-popover-foreground shadow-md right-0"
onClick={(e) => e.stopPropagation()}
>
<div className="flex flex-col space-y-1.5 mb-3">
<h2 className="text-sm font-semibold text-blue-800">Filter Todos</h2>
<p className="text-xs text-muted-foreground">Select filters to narrow down your todo list</p>
</div>
<div className="grid gap-4">
<div>
<h3 className="text-xs font-medium mb-2 text-gray-600">Status</h3>
<div className="grid grid-cols-2 gap-1">
<Button
type="button"
variant={statusFilter === "All" ? "default" : "outline"}
size="sm"
className={statusFilter === "All" ? "bg-blue-600 hover:bg-blue-700" : ""}
onClick={() => setStatusFilter("All")}
>
All
</Button>
<Button
type="button"
variant={statusFilter === "Pending" ? "default" : "outline"}
size="sm"
className={statusFilter === "Pending" ? "bg-blue-600 hover:bg-blue-700" : ""}
onClick={() => setStatusFilter("Pending")}
>
Pending
</Button>
<Button
type="button"
variant={statusFilter === "In Progress" ? "default" : "outline"}
size="sm"
className={statusFilter === "In Progress" ? "bg-blue-600 hover:bg-blue-700" : ""}
onClick={() => setStatusFilter("In Progress")}
>
In Progress
</Button>
<Button
type="button"
variant={statusFilter === "Completed" ? "default" : "outline"}
size="sm"
className={statusFilter === "Completed" ? "bg-blue-600 hover:bg-blue-700" : ""}
onClick={() => setStatusFilter("Completed")}
>
Completed
</Button>
</div>
</div>
<div>
<h3 className="text-xs font-medium mb-2 text-gray-600">Priority</h3>
<div className="grid grid-cols-2 gap-1">
<Button
type="button"
variant={priorityFilter === "All" ? "default" : "outline"}
size="sm"
className={priorityFilter === "All" ? "bg-blue-600 hover:bg-blue-700" : ""}
onClick={() => setPriorityFilter("All")}
>
All
</Button>
<Button
type="button"
variant={priorityFilter === "Low" ? "default" : "outline"}
size="sm"
className={priorityFilter === "Low" ? "bg-blue-600 hover:bg-blue-700" : ""}
onClick={() => setPriorityFilter("Low")}
>
<Badge className="bg-emerald-100 text-emerald-800 mr-2 border-emerald-200">Low</Badge>
</Button>
<Button
type="button"
variant={priorityFilter === "Medium" ? "default" : "outline"}
size="sm"
className={priorityFilter === "Medium" ? "bg-blue-600 hover:bg-blue-700" : ""}
onClick={() => setPriorityFilter("Medium")}
>
<Badge className="bg-amber-100 text-amber-800 mr-2 border-amber-200">Medium</Badge>
</Button>
<Button
type="button"
variant={priorityFilter === "High" ? "default" : "outline"}
size="sm"
className={priorityFilter === "High" ? "bg-blue-600 hover:bg-blue-700" : ""}
onClick={() => setPriorityFilter("High")}
>
<Badge className="bg-rose-100 text-rose-800 mr-2 border-rose-200">High</Badge>
</Button>
</div>
</div>
{activeFiltersCount > 0 && (
<Button
type="button"
variant="destructive"
size="sm"
onClick={() => {
clearAllFilters();
setOpen(false);
}}
className="mt-2"
>
Clear All Filters
</Button>
)}
</div>
</div>
)}
</div>
);
}
// 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<HTMLDivElement>(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 (
<div className="relative" ref={dropdownRef}>
<div
className={cn(
"flex h-10 items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background cursor-pointer w-[180px]",
open && "ring-2 ring-ring ring-offset-2"
)}
onClick={handleButtonClick}
>
<span>{selectedOption.label}</span>
<ChevronDown className="h-4 w-4 opacity-50" />
</div>
{open && (
<div
className="absolute z-[100] mt-1 w-auto min-w-[220px] overflow-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md right-0"
onClick={(e) => e.stopPropagation()}
>
<div className="p-1">
{sortOptions.map((option) => (
<div
key={option.value}
className={cn(
"flex cursor-pointer items-center justify-between rounded-sm px-2 py-1.5 hover:bg-accent hover:text-accent-foreground",
selectedOption.value === option.value && "bg-accent text-accent-foreground"
)}
onClick={(e) => handleOptionClick(option, e)}
>
<span>{option.label}</span>
{selectedOption.value === option.value && <Check className="h-4 w-4" />}
</div>
))}
</div>
</div>
)}
</div>
);
}
type TodoListProps = {
projectId?: number;
assigneeId?: number;
};
export function TodoList({ projectId, assigneeId }: TodoListProps) {
const { userData } = useAuth();
const [todos, setTodos] = useState<Todo[]>([]);
const [filteredTodos, setFilteredTodos] = useState<Todo[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [sortBy, setSortBy] = useState("dueDate-asc");
const [statusFilter, setStatusFilter] = useState<Todo["status"] | "All">("All");
const [priorityFilter, setPriorityFilter] = useState<Todo["priority"] | "All">("All");
const [dateFilter, setDateFilter] = useState({ type: 'all', startDate: undefined, endDate: undefined });
const [isFormOpen, setIsFormOpen] = useState(false);
const [editingTodo, setEditingTodo] = useState<Todo | undefined>(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 (
<div className="space-y-4 sm:space-y-6">
{/* Header Section - Mobile Responsive */}
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl border border-blue-100 overflow-hidden">
<div className="p-4 sm:p-6">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div className="flex-1">
<h2 className="text-xl sm:text-2xl font-bold tracking-tight text-blue-800 mb-2">
{projectId ? "Project Todos" :
assigneeId ? (userData?.employeeId === assigneeId ? "My Todos" : "Assignee Todos") :
isAdmin ? "All Todos" : "My Todos"}
</h2>
{/* Stats Badges - Mobile Responsive */}
<div className="flex flex-wrap gap-2">
<Badge variant="outline" className="bg-white text-xs sm:text-sm">
Total: {stats.total}
</Badge>
<Badge className="bg-amber-100 text-amber-800 hover:bg-amber-200 border-none text-xs sm:text-sm">
Pending: {stats.pending}
</Badge>
<Badge className="bg-blue-100 text-blue-800 hover:bg-blue-200 border-none text-xs sm:text-sm">
In Progress: {stats.inProgress}
</Badge>
<Badge className="bg-green-100 text-green-800 hover:bg-green-200 border-none text-xs sm:text-sm">
Completed: {stats.completed}
</Badge>
{stats.overdue > 0 && (
<Badge className="bg-rose-100 text-rose-800 hover:bg-rose-200 border-none text-xs sm:text-sm">
Overdue: {stats.overdue}
</Badge>
)}
</div>
</div>
{canManageTodos && (
<Button
onClick={() => setIsFormOpen(true)}
className="bg-blue-600 hover:bg-blue-700 w-full sm:w-auto"
size="sm"
>
<Plus className="h-4 w-4 mr-2" />
<span className="hidden sm:inline">Add Todo</span>
<span className="sm:hidden">Add</span>
</Button>
)}
</div>
</div>
</div>
{/* Search and Filters - Mobile Responsive */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
{/* Always visible: Search Bar and Filter Toggle */}
<div className="p-4 space-y-4">
{/* Search Bar with Mobile Filter Toggle */}
<div className="flex gap-3">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-slate-400" />
<Input
placeholder="Search todos..."
className="pl-10 border-slate-200 focus-visible:ring-blue-500 h-10"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
{/* Mobile Filter Toggle Button */}
<Button
variant="outline"
size="sm"
className="sm:hidden relative"
onClick={() => setShowFilters(!showFilters)}
>
<Filter className="h-4 w-4" />
{activeFiltersCount > 0 && (
<span className="absolute -top-1 -right-1 h-4 w-4 rounded-full bg-blue-600 text-[10px] flex items-center justify-center text-white font-medium">
{activeFiltersCount}
</span>
)}
</Button>
</div>
{/* Desktop: Always visible filters */}
<div className="hidden sm:flex flex-col sm:flex-row gap-3 sm:gap-4">
{/* View Mode Toggle */}
<div className="flex bg-slate-100 rounded-lg p-1 w-full sm:w-auto">
<Button
variant={viewMode === 'list' ? "default" : "ghost"}
size="sm"
className={`flex-1 sm:flex-none rounded-md text-xs sm:text-sm ${
viewMode === 'list' ? "bg-white shadow-sm" : "hover:bg-slate-200"
}`}
onClick={() => setViewMode('list')}
>
<ListIcon className="h-4 w-4 mr-1 sm:mr-2" />
<span className="hidden sm:inline">List</span>
<span className="sm:hidden">List</span>
</Button>
<Button
variant={viewMode === 'kanban' ? "default" : "ghost"}
size="sm"
className={`flex-1 sm:flex-none rounded-md text-xs sm:text-sm ${
viewMode === 'kanban' ? "bg-white shadow-sm" : "hover:bg-slate-200"
}`}
onClick={() => setViewMode('kanban')}
>
<Kanban className="h-4 w-4 mr-1 sm:mr-2" />
<span className="hidden sm:inline">Kanban</span>
<span className="sm:hidden">Board</span>
</Button>
</div>
{/* Filter Controls */}
<div className="flex flex-wrap gap-2 flex-1 justify-end">
<CustomSortDropdown
value={sortBy}
onChange={setSortBy}
/>
<CustomDateFilterDropdown
value={dateFilter.type}
onChange={setDateFilter}
/>
<CustomFilterDropdown
statusFilter={statusFilter}
priorityFilter={priorityFilter}
setStatusFilter={setStatusFilter}
setPriorityFilter={setPriorityFilter}
clearAllFilters={clearAllFilters}
activeFiltersCount={activeFiltersCount}
/>
</div>
</div>
</div>
{/* Mobile: Collapsible filters */}
{showFilters && (
<div className="sm:hidden border-t border-slate-200 bg-slate-50 p-4 space-y-4">
{/* View Mode Toggle */}
<div>
<label className="text-sm font-medium text-slate-700 mb-2 block">View Mode</label>
<div className="flex bg-slate-100 rounded-lg p-1">
<Button
variant={viewMode === 'list' ? "default" : "ghost"}
size="sm"
className={`flex-1 rounded-md text-xs ${
viewMode === 'list' ? "bg-white shadow-sm" : "hover:bg-slate-200"
}`}
onClick={() => setViewMode('list')}
>
<ListIcon className="h-4 w-4 mr-2" />
List
</Button>
<Button
variant={viewMode === 'kanban' ? "default" : "ghost"}
size="sm"
className={`flex-1 rounded-md text-xs ${
viewMode === 'kanban' ? "bg-white shadow-sm" : "hover:bg-slate-200"
}`}
onClick={() => setViewMode('kanban')}
>
<Kanban className="h-4 w-4 mr-2" />
Board
</Button>
</div>
</div>
{/* Filter Controls */}
<div className="space-y-3">
<label className="text-sm font-medium text-slate-700 block">Filters & Sorting</label>
<div className="grid grid-cols-1 gap-3">
<CustomSortDropdown
value={sortBy}
onChange={setSortBy}
/>
<CustomDateFilterDropdown
value={dateFilter.type}
onChange={setDateFilter}
/>
<CustomFilterDropdown
statusFilter={statusFilter}
priorityFilter={priorityFilter}
setStatusFilter={setStatusFilter}
setPriorityFilter={setPriorityFilter}
clearAllFilters={clearAllFilters}
activeFiltersCount={activeFiltersCount}
/>
</div>
</div>
</div>
)}
</div>
{isLoading && !todos.length ? (
<div className="text-center py-12 border rounded-lg bg-gradient-to-r from-slate-50 to-blue-50">
<div className="flex flex-col items-center space-y-4">
<Loader2 className="h-8 w-8 animate-spin text-blue-600" />
<p className="text-lg text-blue-800 font-medium">Loading todos...</p>
</div>
</div>
) : filteredTodos.length === 0 ? (
<div className="text-center py-12 border rounded-lg bg-gradient-to-r from-slate-50 to-blue-50">
<div className="flex flex-col items-center space-y-4">
<div className="h-16 w-16 rounded-full bg-blue-100 flex items-center justify-center">
<ClipboardX className="h-8 w-8 text-blue-600" />
</div>
<div>
<p className="text-lg text-blue-800 font-medium">No todos found</p>
<p className="text-sm text-gray-500 mt-1">
{searchTerm || statusFilter !== "All" || priorityFilter !== "All" || dateFilter.type !== "all"
? "Try adjusting your filters"
: "Create a new todo to get started"}
</p>
</div>
{canManageTodos && (
<Button
variant="outline"
className="mt-4 border-blue-300 text-blue-700 hover:bg-blue-50"
onClick={() => setIsFormOpen(true)}
>
<Plus className="h-4 w-4 mr-2" /> Create a todo
</Button>
)}
</div>
</div>
) : viewMode === 'list' ? (
<div className="space-y-4">
{/* Active Todos Section */}
{activeTodos.length > 0 && (
<div className="space-y-4">
{activeTodos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onEdit={canManageTodos ? handleEditTodo : undefined}
onDelete={canManageTodos ? handleDeleteTodo : undefined}
onStatusChange={handleStatusChange}
/>
))}
</div>
)}
{/* Completed Todos Section - Collapsible */}
{completedTodos.length > 0 && (
<div className="mt-6 border-t border-gray-200 pt-4">
<button
className="flex items-center text-gray-700 hover:text-blue-600 transition-colors mb-4 group w-full"
onClick={toggleCompletedVisibility}
>
<div className="flex items-center">
<ChevronRight
className={cn(
"h-5 w-5 mr-2 transition-transform",
showCompleted && "transform rotate-90"
)}
/>
<CheckCircle className="h-5 w-5 mr-2 text-green-600" />
<span className="font-medium">Completed</span>
<Badge className="ml-2 bg-green-100 text-green-800">{completedTodos.length}</Badge>
</div>
</button>
{showCompleted && (
<div className="space-y-4 pl-4 border-l-2 border-gray-100">
{completedTodos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onEdit={canManageTodos ? handleEditTodo : undefined}
onDelete={canManageTodos ? handleDeleteTodo : undefined}
onStatusChange={handleStatusChange}
/>
))}
</div>
)}
</div>
)}
</div>
) : (
<div className="space-y-4">
{/* Mobile: Stacked columns, Desktop: Grid layout */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 lg:gap-6">
{(['Pending', 'In Progress', 'Completed'] as Todo['status'][]).map(status => (
<div key={status} className="bg-white rounded-xl border border-slate-200 overflow-hidden">
{/* Column header */}
<div className={cn(
"p-3 sm:p-4 font-medium text-sm flex items-center justify-between border-b",
status === 'Pending' ? "bg-slate-50 text-slate-800 border-slate-200" :
status === 'In Progress' ? "bg-blue-50 text-blue-800 border-blue-200" :
"bg-green-50 text-green-800 border-green-200"
)}>
<div className="flex items-center gap-2">
<div className={cn(
"w-3 h-3 rounded-full",
status === 'Pending' ? "bg-slate-400" :
status === 'In Progress' ? "bg-blue-500" :
"bg-green-500"
)} />
<span className="font-semibold">{status}</span>
</div>
<Badge className={cn(
"text-xs font-medium",
status === 'Pending' ? "bg-slate-100 text-slate-700" :
status === 'In Progress' ? "bg-blue-100 text-blue-700" :
"bg-green-100 text-green-700"
)}>
{kanbanTodos[status].length}
</Badge>
</div>
{/* Column content */}
<div className="p-3 sm:p-4 space-y-3 min-h-[200px] max-h-[70vh] overflow-y-auto">
{kanbanTodos[status].length === 0 ? (
<div className="border-2 border-dashed border-slate-200 rounded-lg p-6 text-center">
<div className={cn(
"w-12 h-12 rounded-full mx-auto mb-3 flex items-center justify-center",
status === 'Pending' ? "bg-slate-100" :
status === 'In Progress' ? "bg-blue-100" :
"bg-green-100"
)}>
<div className={cn(
"w-6 h-6 rounded-full",
status === 'Pending' ? "bg-slate-300" :
status === 'In Progress' ? "bg-blue-300" :
"bg-green-300"
)} />
</div>
<p className="text-sm text-slate-500 font-medium">
No {status.toLowerCase()} todos
</p>
<p className="text-xs text-slate-400 mt-1">
{status === 'Pending' ? 'Tasks will appear here when created' :
status === 'In Progress' ? 'Tasks will appear here when started' :
'Tasks will appear here when completed'}
</p>
</div>
) : (
kanbanTodos[status].map(todo => (
<div key={todo.id} className="transform transition-transform hover:scale-[1.02]">
<TodoItem
todo={todo}
onEdit={canManageTodos ? handleEditTodo : undefined}
onDelete={canManageTodos ? handleDeleteTodo : undefined}
onStatusChange={handleStatusChange}
/>
</div>
))
)}
</div>
</div>
))}
</div>
</div>
)}
<CustomDialog
isOpen={isFormOpen}
onClose={handleCloseForm}
title={editingTodo ? "Edit Todo" : "Create New Todo"}
allowClose={!isLoading}
isPending={isLoading}
>
<TodoForm
todo={editingTodo}
onSubmit={handleFormSubmit}
onCancel={handleCloseForm}
isLoading={isLoading}
/>
</CustomDialog>
</div>
);
}