2026-06-11 19:56:20 +05:30

503 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useTaskList } from '../hooks/useProject';
import ListPagination from '../components/ListPagination';
import LinkField from '../components/LinkField';
import { buildDateRangeFilters } from '../utils/listFilterUtils';
import {
FaTasks, FaPlus, FaSearch, FaFilter, FaSync, FaEye, FaChevronDown, FaChevronUp, FaTimes, FaFileExport, FaEdit, FaCopy, FaCheckSquare, FaSquare,
} from 'react-icons/fa';
import { useListPageSelection } from '../hooks/useListPageSelection';
import type { Task } from '../services/projectService';
import DynamicExportModal from '../components/DynamicExportModal';
import { fetchAllRowsForExport } from '../utils/frappeListExport';
const getStatusStyle = (s: string) => {
switch (s?.toLowerCase()) {
case 'open': return 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300';
case 'working': return 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300';
case 'completed': return 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300';
case 'cancelled': return 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400';
case 'overdue': return 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300';
default: return 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400';
}
};
const getPriorityStyle = (p: string) => {
switch (p?.toLowerCase()) {
case 'high': return 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300';
case 'medium': return 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300';
case 'low': return 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300';
default: return 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400';
}
};
const TaskList: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const pageSize = 20;
const currentPage = useMemo(() => {
const p = parseInt(searchParams.get('page') || '1', 10);
return Number.isNaN(p) || p < 1 ? 1 : p;
}, [searchParams]);
const setCurrentPage = useCallback((v: number | ((p: number) => number)) => {
const next = typeof v === 'function' ? v(currentPage) : v;
setSearchParams(prev => { const n = new URLSearchParams(prev); n.set('page', String(next)); return n; });
}, [currentPage, setSearchParams]);
const [isFilterExpanded, setIsFilterExpanded] = useState(false);
const [statusFilter, setStatusFilter] = useState(searchParams.get('status') || '');
const [priorityFilter, setPriorityFilter] = useState(searchParams.get('priority') || '');
const [projectFilter, setProjectFilter] = useState(searchParams.get('project') || '');
const [searchQuery, setSearchQuery] = useState(searchParams.get('q') || '');
const [dateFilterBy, setDateFilterBy] = useState<'' | 'creation' | 'modified'>(
(searchParams.get('date_filter_by') as '' | 'creation' | 'modified') || ''
);
const [dateStart, setDateStart] = useState(searchParams.get('date_start') || '');
const [dateEnd, setDateEnd] = useState(searchParams.get('date_end') || '');
const [sortBy, setSortBy] = useState(searchParams.get('sort_by') || 'creation desc');
const [showExportModal, setShowExportModal] = useState(false);
const didInitUrlSync = useRef(false);
const skipInitialSearchUrlSync = useRef(true);
const searchDebounceRef = useRef<number | null>(null);
const setSearchParamsRef = useRef(setSearchParams);
useEffect(() => {
setSearchParamsRef.current = setSearchParams;
}, [setSearchParams]);
const apiFilters = useMemo(() => {
const f: Record<string, any> = {};
if (statusFilter) f.status = statusFilter;
if (priorityFilter) f.priority = priorityFilter;
if (projectFilter) f.project = projectFilter;
if (searchQuery) f.subject = ['like', `%${searchQuery}%`];
Object.assign(f, buildDateRangeFilters(dateFilterBy, dateStart, dateEnd));
return f;
}, [statusFilter, priorityFilter, projectFilter, searchQuery, dateFilterBy, dateStart, dateEnd]);
const { tasks, loading, error, totalCount, refetch } = useTaskList({
filters: apiFilters,
limit_start: (currentPage - 1) * pageSize,
limit_page_length: pageSize,
order_by: sortBy,
});
const selectionResetKey = useMemo(
() => `${currentPage}|${sortBy}|${JSON.stringify(apiFilters)}`,
[currentPage, sortBy, apiFilters],
);
const {
selectedRows,
toggleRow,
toggleAllOnPage,
allOnPageSelected,
someOnPageSelected,
} = useListPageSelection(tasks, selectionResetKey);
const fetchAllForExport = useCallback(
() => fetchAllRowsForExport({ doctype: 'Task', filters: apiFilters, orderBy: sortBy }),
[apiFilters, sortBy],
);
const totalPages = Math.ceil(totalCount / pageSize);
const formatDate = (d: string) => d ? new Date(d).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) : '-';
const clearFilters = () => {
setStatusFilter(''); setPriorityFilter(''); setProjectFilter('');
setSearchQuery(''); setDateFilterBy(''); setDateStart(''); setDateEnd('');
setSortBy('creation desc');
setSearchParams(prev => {
const n = new URLSearchParams(prev);
['status','priority','project','q','date_filter_by','date_start','date_end','sort_by'].forEach(k => n.delete(k));
n.set('page','1'); return n;
});
};
// Auto-sync filters (no "Apply Filters" button for Project Management pages)
useEffect(() => {
if (!didInitUrlSync.current) {
didInitUrlSync.current = true;
return;
}
setSearchParamsRef.current(prev => {
const n = new URLSearchParams(prev);
statusFilter ? n.set('status', statusFilter) : n.delete('status');
priorityFilter ? n.set('priority', priorityFilter) : n.delete('priority');
projectFilter ? n.set('project', projectFilter) : n.delete('project');
dateFilterBy ? n.set('date_filter_by', dateFilterBy) : n.delete('date_filter_by');
dateStart ? n.set('date_start', dateStart) : n.delete('date_start');
dateEnd ? n.set('date_end', dateEnd) : n.delete('date_end');
sortBy !== 'creation desc' ? n.set('sort_by', sortBy) : n.delete('sort_by');
n.set('page', '1');
return n;
});
}, [statusFilter, priorityFilter, projectFilter, dateFilterBy, dateStart, dateEnd, sortBy]);
useEffect(() => {
if (!didInitUrlSync.current) return;
if (skipInitialSearchUrlSync.current) {
skipInitialSearchUrlSync.current = false;
return;
}
if (searchDebounceRef.current) window.clearTimeout(searchDebounceRef.current);
searchDebounceRef.current = window.setTimeout(() => {
setSearchParamsRef.current(prev => {
const n = new URLSearchParams(prev);
searchQuery ? n.set('q', searchQuery) : n.delete('q');
n.set('page', '1');
return n;
});
}, 450);
return () => {
if (searchDebounceRef.current) window.clearTimeout(searchDebounceRef.current);
};
}, [searchQuery]);
const hasActiveFilters = !!(statusFilter || priorityFilter || projectFilter || searchQuery || (dateFilterBy && (dateStart || dateEnd)));
const handleEdit = (taskName: string) => navigate(`/projects/tasks/${encodeURIComponent(taskName)}?edit=1`);
const handleDuplicate = (taskName: string) => navigate(`/projects/tasks/new?duplicate=${encodeURIComponent(taskName)}`);
return (
<div className="p-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
<div className="flex items-center gap-3">
<button
onClick={() => navigate('/projects')}
className="text-sm text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400"
>
{t('projects.moduleTitle')}
</button>
<span className="text-gray-400">/</span>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
<FaTasks className="text-indigo-500" /> {t('projects.tasksDoctype')}
</h1>
</div>
<div className="flex flex-wrap items-center gap-2">
<button
type="button"
onClick={() => setShowExportModal(true)}
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 shadow transition-all disabled:opacity-50"
disabled={totalCount === 0 && selectedRows.size === 0}
>
<FaFileExport /> <span className="font-medium">{t('listPages.export')}</span>
{selectedRows.size > 0 && (
<span className="bg-white/25 px-1.5 py-0.5 rounded text-xs font-bold">{selectedRows.size}</span>
)}
</button>
<button
onClick={() => navigate('/projects/tasks/new')}
className="flex items-center gap-2 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
>
<FaPlus /> {t('projects.newTask')}
</button>
</div>
</div>
<DynamicExportModal
isOpen={showExportModal}
onClose={() => setShowExportModal(false)}
doctype="Task"
selectedCount={selectedRows.size}
pageCount={tasks.length}
totalCount={totalCount}
pageData={tasks}
selectedRows={selectedRows}
rowKey="name"
onFetchAll={fetchAllForExport}
fileNamePrefix="tasks"
/>
{/* ── Filter Panel ── */}
<div className="isolate bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 mb-6">
{/* Header */}
<div className="bg-gradient-to-r from-blue-500 to-blue-600 dark:from-blue-600 dark:to-blue-700 px-4 py-2.5 rounded-t-lg">
<div className="flex items-center justify-between gap-4">
{/* Left: toggle + title + count */}
<div className="flex items-center gap-3 flex-shrink-0">
<button
onClick={() => setIsFilterExpanded(v => !v)}
className="text-white hover:bg-white/20 p-1.5 rounded-lg transition-all"
>
{isFilterExpanded ? <FaChevronUp size={12} /> : <FaChevronDown size={12} />}
</button>
<div className="flex items-center gap-2">
<FaFilter className="text-white" size={13} />
<span className="text-white font-semibold text-sm">Filters</span>
</div>
{hasActiveFilters && (
<span className="bg-white text-blue-600 px-2 py-0.5 rounded-full text-xs font-bold">
{[searchQuery, statusFilter, priorityFilter, projectFilter, dateFilterBy && dateStart].filter(Boolean).length}
</span>
)}
</div>
{/* Center: active filter pills */}
{hasActiveFilters && (
<div className="flex-1 overflow-x-auto mx-2">
<div className="flex items-center gap-2 py-0.5">
{searchQuery && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-white/90 text-indigo-700 rounded-full text-[10px] font-medium whitespace-nowrap">
<span className="font-semibold">Subject:</span> {searchQuery}
<button onClick={() => setSearchQuery('')}><FaTimes className="text-[9px] hover:text-red-500" /></button>
</span>
)}
{statusFilter && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-white/90 text-indigo-700 rounded-full text-[10px] font-medium whitespace-nowrap">
<span className="font-semibold">Status:</span> {statusFilter}
<button onClick={() => setStatusFilter('')}><FaTimes className="text-[9px] hover:text-red-500" /></button>
</span>
)}
{priorityFilter && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-white/90 text-indigo-700 rounded-full text-[10px] font-medium whitespace-nowrap">
<span className="font-semibold">Priority:</span> {priorityFilter}
<button onClick={() => setPriorityFilter('')}><FaTimes className="text-[9px] hover:text-red-500" /></button>
</span>
)}
{projectFilter && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-white/90 text-indigo-700 rounded-full text-[10px] font-medium whitespace-nowrap">
<span className="font-semibold">Project:</span> {projectFilter}
<button onClick={() => setProjectFilter('')}><FaTimes className="text-[9px] hover:text-red-500" /></button>
</span>
)}
{dateFilterBy && dateStart && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-white/90 text-indigo-700 rounded-full text-[10px] font-medium whitespace-nowrap">
<span className="font-semibold">{dateFilterBy === 'creation' ? 'Created' : 'Modified'}:</span> {dateStart}{dateEnd ? ` ${dateEnd}` : ''}
<button onClick={() => { setDateFilterBy(''); setDateStart(''); setDateEnd(''); }}><FaTimes className="text-[9px] hover:text-red-500" /></button>
</span>
)}
</div>
</div>
)}
{/* Right: clear + refresh */}
<div className="flex items-center gap-2 flex-shrink-0">
{hasActiveFilters && (
<button onClick={clearFilters} className="text-white/80 hover:text-white text-xs underline whitespace-nowrap">
Clear all
</button>
)}
<button onClick={() => refetch()} className="text-white hover:bg-white/20 p-1.5 rounded-lg transition-all" title="Refresh">
<FaSync size={12} className={loading ? 'animate-spin' : ''} />
</button>
</div>
</div>
</div>
{/* Expanded filter body */}
{isFilterExpanded && (
<div className="p-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Search */}
<div>
<label className="block text-[10px] font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wide mb-1">Search</label>
<div className="relative">
<FaSearch className="absolute left-2.5 top-1/2 -translate-y-1/2 text-gray-400 text-xs" />
<input
type="text" value={searchQuery} onChange={e => setSearchQuery(e.target.value)}
onKeyDown={e => e.key === 'Enter' && e.preventDefault()}
placeholder="Search by task subject…"
className="w-full pl-8 pr-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-400 focus:outline-none"
/>
</div>
</div>
{/* Status */}
<div>
<label className="block text-[10px] font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wide mb-1">Status</label>
<select value={statusFilter} onChange={e => setStatusFilter(e.target.value)} className="w-full px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-400 focus:outline-none">
<option value="">All Status</option>
<option value="Open">Open</option>
<option value="Working">Working</option>
<option value="Completed">Completed</option>
<option value="Cancelled">Cancelled</option>
</select>
</div>
{/* Priority */}
<div>
<label className="block text-[10px] font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wide mb-1">Priority</label>
<select value={priorityFilter} onChange={e => setPriorityFilter(e.target.value)} className="w-full px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-400 focus:outline-none">
<option value="">All Priority</option>
<option value="High">High</option>
<option value="Medium">Medium</option>
<option value="Low">Low</option>
</select>
</div>
{/* Project */}
<div>
<label className="block text-[10px] font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wide mb-1">Project</label>
<LinkField
label="Project"
hideLabel
value={projectFilter}
onChange={setProjectFilter}
doctype="Project"
placeholder="Filter by project…"
compact
/>
</div>
{/* Date filter by */}
<div>
<label className="block text-[10px] font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wide mb-1">Date Filter By</label>
<select value={dateFilterBy} onChange={e => setDateFilterBy(e.target.value as '' | 'creation' | 'modified')} className="w-full px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-400 focus:outline-none">
<option value="">None</option>
<option value="creation">Created</option>
<option value="modified">Modified</option>
</select>
</div>
{dateFilterBy && (
<>
<div>
<label className="block text-[10px] font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wide mb-1">From</label>
<input type="date" value={dateStart} onChange={e => setDateStart(e.target.value)} className="w-full px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-400 focus:outline-none" />
</div>
<div>
<label className="block text-[10px] font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wide mb-1">To</label>
<input type="date" value={dateEnd} onChange={e => setDateEnd(e.target.value)} className="w-full px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-400 focus:outline-none" />
</div>
</>
)}
{/* Sort by */}
<div>
<label className="block text-[10px] font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wide mb-1">Sort By</label>
<select value={sortBy} onChange={e => setSortBy(e.target.value)} className="w-full px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-400 focus:outline-none">
<option value="creation desc">Created (newest)</option>
<option value="creation asc">Created (oldest)</option>
<option value="exp_end_date asc">Due date (soonest)</option>
<option value="priority desc">Priority (high first)</option>
</select>
</div>
</div>
</div>
)}
</div>
{error && (
<div className="mb-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg text-red-700 dark:text-red-300 text-sm">{error}</div>
)}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
{loading ? (
<div className="p-12 text-center text-gray-500 dark:text-gray-400">{t('common.loading')}</div>
) : tasks.length === 0 ? (
<div className="p-12 text-center">
<FaTasks className="text-4xl text-gray-300 dark:text-gray-600 mx-auto mb-3" />
<p className="text-gray-500 dark:text-gray-400">{t('projects.noTasks')}</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-100 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
<tr>
<th className="w-10 px-4 py-3 text-left">
<button
type="button"
onClick={toggleAllOnPage}
className="text-gray-500 dark:text-gray-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
title={allOnPageSelected ? 'Deselect all' : 'Select all'}
aria-label="Select all on page"
>
{allOnPageSelected
? <FaCheckSquare className="text-indigo-600 dark:text-indigo-400" size={18} />
: someOnPageSelected
? (
<div className="relative inline-block">
<FaSquare size={18} />
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-2 h-0.5 bg-current" />
</div>
</div>
)
: <FaSquare size={18} />}
</button>
</th>
{[t('projects.taskColumn'), t('projects.project'), t('commonFields.status'), t('commonFields.priority'), t('projects.assignedTo'), t('projects.dueDate'), 'Exp. Time', ''].map(h => (
<th key={h} className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">{h}</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{tasks.map((task: Task) => (
<tr
key={task.name}
className={`hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer ${selectedRows.has(task.name) ? 'bg-indigo-50 dark:bg-indigo-900/20' : ''}`}
onClick={() => navigate(`/projects/tasks/${task.name}`)}
>
<td className="w-10 px-4 py-3" onClick={e => e.stopPropagation()}>
<button
type="button"
onClick={() => toggleRow(task.name)}
className="text-gray-500 dark:text-gray-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
aria-label={`Select ${task.name}`}
>
{selectedRows.has(task.name)
? <FaCheckSquare className="text-indigo-600 dark:text-indigo-400" size={18} />
: <FaSquare size={18} />}
</button>
</td>
<td className="px-6 py-4">
<div className="font-medium text-gray-900 dark:text-white">{task.subject || task.name}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">{task.name}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{task.project ? (
<button
onClick={e => { e.stopPropagation(); navigate(`/projects/list/${task.project}`); }}
className="text-sm text-indigo-600 dark:text-indigo-400 hover:underline"
>
{task.project}
</button>
) : <span className="text-gray-400 text-sm">-</span>}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 py-0.5 rounded text-xs font-medium ${getStatusStyle(task.status || '')}`}>{task.status || '-'}</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 py-0.5 rounded text-xs font-medium ${getPriorityStyle(task.priority || '')}`}>{task.priority || '-'}</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
{task._assign ? (JSON.parse(task._assign)[0] || '-') : '-'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">{formatDate(task.exp_end_date || '')}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">{task.expected_time ? `${task.expected_time}h` : '-'}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium" onClick={e => e.stopPropagation()}>
<div className="flex items-center gap-1">
<button onClick={() => navigate(`/projects/tasks/${task.name}`)} className="text-indigo-600 dark:text-indigo-400 hover:text-indigo-900 dark:hover:text-indigo-300 p-2 hover:bg-indigo-50 dark:hover:bg-indigo-900/30 rounded transition-colors" title="View">
<FaEye />
</button>
<button onClick={() => handleEdit(task.name)} className="text-green-600 dark:text-green-400 hover:text-green-900 dark:hover:text-green-300 p-2 hover:bg-green-50 dark:hover:bg-green-900/30 rounded transition-colors" title="Edit">
<FaEdit />
</button>
<button onClick={() => handleDuplicate(task.name)} className="text-purple-600 dark:text-purple-400 hover:text-purple-900 dark:hover:text-purple-300 p-2 hover:bg-purple-50 dark:hover:bg-purple-900/30 rounded transition-colors" title="Duplicate">
<FaCopy />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{totalPages > 1 && (
<div className="border-t border-gray-200 dark:border-gray-700 px-4 py-3">
<ListPagination currentPage={currentPage} totalPages={totalPages} totalCount={totalCount} pageSize={pageSize} onPageChange={setCurrentPage} />
</div>
)}
</div>
</div>
);
};
export default TaskList;