import React, { useEffect, useState, useRef, useMemo } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useSidebar } from '../contexts/SidebarContext'; import TaskList from './Task/TaskList'; import GroupedTaskList from './Task/GroupedTaskList'; import NewTask from './Task/NewTask'; import { Task } from '../entities/Task'; import { getTitleAndIcon } from './Task/getTitleAndIcon'; import { getDescription } from './Task/getDescription'; import { createTask, GroupedTasks } from '../utils/tasksService'; import { useStore } from '../store/useStore'; import { useToast } from './Shared/ToastContext'; import { SortOption } from './Shared/SortFilterButton'; import IconSortDropdown from './Shared/IconSortDropdown'; import { TagIcon, XMarkIcon } from '@heroicons/react/24/solid'; import { QueueListIcon, InformationCircleIcon, MagnifyingGlassIcon, CheckIcon, } from '@heroicons/react/24/outline'; import { getApiPath } from '../config/paths'; const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); const getSearchPlaceholder = (language: string): string => { const placeholders: Record = { en: 'Search tasks...', el: 'Αναζήτηση εργασιών...', es: 'Buscar tareas...', de: 'Aufgaben suchen...', jp: 'タスクを検索...', ua: 'Пошук завдань...', }; return placeholders[language] || 'Search tasks...'; }; const Tasks: React.FC = () => { const { t, i18n } = useTranslation(); const { showSuccessToast } = useToast(); const { isSidebarOpen } = useSidebar(); const [tasks, setTasks] = useState([]); const projects = useStore((state: any) => state.projectsStore.projects); const [groupedTasks, setGroupedTasks] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [dropdownOpen, setDropdownOpen] = useState(false); const [orderBy, setOrderBy] = useState('created_at:desc'); const [taskSearchQuery, setTaskSearchQuery] = useState(''); const [isInfoExpanded, setIsInfoExpanded] = useState(false); const [isSearchExpanded, setIsSearchExpanded] = useState(false); const [showCompleted, setShowCompleted] = useState(false); const [isMobile, setIsMobile] = useState(window.innerWidth < 768); const [groupBy, setGroupBy] = useState<'none' | 'project'>('none'); const [offset, setOffset] = useState(0); const [hasMore, setHasMore] = useState(false); const [isLoadingMore, setIsLoadingMore] = useState(false); const [totalCount, setTotalCount] = useState(0); const DEFAULT_LIMIT = 20; const [limit, setLimit] = useState(DEFAULT_LIMIT); const dropdownRef = useRef(null); const location = useLocation(); const navigate = useNavigate(); const query = new URLSearchParams(location.search); const isUpcomingView = query.get('type') === 'upcoming' || location.pathname === '/upcoming'; const status = query.get('status'); const tag = query.get('tag'); useEffect(() => { if (status === 'completed') { setShowCompleted(true); } else if (status === 'active') { setShowCompleted(false); } else if (status === null) { setShowCompleted(true); } }, [status, isUpcomingView]); const displayTasks = useMemo(() => { let filteredTasks: Task[] = tasks; if (status === 'completed') { filteredTasks = filteredTasks.filter((task: Task) => { const isCompleted = task.status === 'done' || task.status === 'archived' || task.status === 2 || task.status === 3; return isCompleted; }); } else if (status === 'active') { filteredTasks = filteredTasks.filter((task: Task) => { const isCompleted = task.status === 'done' || task.status === 'archived' || task.status === 2 || task.status === 3; return !isCompleted; }); } if (taskSearchQuery.trim() && !isUpcomingView) { const queryLower = taskSearchQuery.toLowerCase(); filteredTasks = filteredTasks.filter( (task: Task) => task.name.toLowerCase().includes(queryLower) || task.original_name?.toLowerCase().includes(queryLower) || task.note?.toLowerCase().includes(queryLower) ); } return filteredTasks; }, [tasks, showCompleted, status, taskSearchQuery, isUpcomingView]); if (location.pathname === '/upcoming' && !query.get('type')) { query.set('type', 'upcoming'); } const { title: stateTitle } = location.state || {}; const title = stateTitle || getTitleAndIcon(query, projects, t, location.pathname).title; useEffect(() => { const savedOrderBy = localStorage.getItem('order_by') || 'created_at:desc'; setOrderBy(savedOrderBy); const savedGroupBy = (localStorage.getItem('tasks_group_by') as 'none' | 'project') || 'none'; setGroupBy(savedGroupBy); const params = new URLSearchParams(location.search); if (!params.get('order_by')) { params.set('order_by', savedOrderBy); navigate( { pathname: location.pathname, search: `?${params.toString()}`, }, { replace: true } ); } }, [location.pathname]); useEffect(() => { if (isUpcomingView) { setTaskSearchQuery(''); setIsSearchExpanded(false); } }, [isUpcomingView]); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( dropdownRef.current && !dropdownRef.current.contains(event.target as Node) ) { setDropdownOpen(false); } }; if (dropdownOpen) { document.addEventListener('mousedown', handleClickOutside); } return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [dropdownOpen]); const fetchData = async ( resetPagination = true, options?: { limitOverride?: number; forceOffset?: number; disableHasMore?: boolean; disablePagination?: boolean; } ) => { setLoading(resetPagination); setError(null); try { const tagId = query.get('tag'); const type = query.get('type'); const allTasksUrl = new URLSearchParams(query.toString()); allTasksUrl.set('client_side_filtering', 'true'); if (type === 'upcoming') { allTasksUrl.set('type', 'upcoming'); allTasksUrl.set('groupBy', 'day'); allTasksUrl.set('maxDays', '7'); allTasksUrl.set('sidebarOpen', isSidebarOpen.toString()); allTasksUrl.set('isMobile', isMobile.toString()); } if (!options?.disablePagination && type !== 'upcoming') { const currentOffset = options?.forceOffset !== undefined ? options.forceOffset : resetPagination ? 0 : offset; const limitToUse = options?.limitOverride ?? limit; allTasksUrl.set('limit', limitToUse.toString()); allTasksUrl.set('offset', currentOffset.toString()); } const searchParams = allTasksUrl.toString(); const tasksResponse = await fetch( getApiPath( `tasks?${searchParams}${tagId ? `&tag=${tagId}` : ''}` ) ); if (tasksResponse.ok) { const tasksData = await tasksResponse.json(); if (resetPagination) { setTasks(tasksData.tasks || []); setGroupedTasks(tasksData.groupedTasks || null); if (!options?.disablePagination) { const limitToUse = options?.limitOverride ?? limit; setOffset(limitToUse); } } else { setTasks((prev) => [...prev, ...(tasksData.tasks || [])]); if (tasksData.groupedTasks) { setGroupedTasks((prev) => { if (!prev) return tasksData.groupedTasks; return { ...prev, ...tasksData.groupedTasks, }; }); } if (!options?.disablePagination) { const limitToUse = options?.limitOverride ?? limit; setOffset((prev) => prev + limitToUse); } } setHasMore( options?.disableHasMore || options?.disablePagination || type === 'upcoming' ? false : tasksData.pagination?.hasMore || false ); if (tasksData.pagination) { setTotalCount(tasksData.pagination.total); } else if (options?.disablePagination || type === 'upcoming') { setTotalCount(tasksData.tasks?.length || 0); } } else { throw new Error('Failed to fetch tasks.'); } } catch (error) { setError((error as Error).message); } finally { setLoading(false); setIsLoadingMore(false); } }; const loadMore = async (all: boolean) => { if (isLoadingMore) return; if (!hasMore && !all) return; setIsLoadingMore(true); const shouldDisablePagination = !isUpcomingView && groupBy === 'project'; if (all || shouldDisablePagination) { const newLimit = totalCount > 0 ? totalCount : 10000; await fetchData(true, { limitOverride: newLimit, forceOffset: 0, disableHasMore: true, disablePagination: true, }); setLimit(DEFAULT_LIMIT); setHasMore(false); } else { await fetchData(false); } if (all) { setHasMore(false); } }; useEffect(() => { const shouldDisablePagination = isUpcomingView || groupBy === 'project'; fetchData( true, shouldDisablePagination ? { disablePagination: true, disableHasMore: true, limitOverride: 10000, forceOffset: 0, } : undefined ); }, [location, isSidebarOpen, isMobile, groupBy, isUpcomingView]); useEffect(() => { const handleResize = () => { const newIsMobile = window.innerWidth < 768; if (newIsMobile !== isMobile) { setIsMobile(newIsMobile); } }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, [isMobile]); useEffect(() => { const handleTaskCreated = (event: CustomEvent) => { const newTask = event.detail; if (newTask) { setTasks((prevTasks) => [newTask, ...prevTasks]); } }; window.addEventListener( 'taskCreated', handleTaskCreated as EventListener ); return () => { window.removeEventListener( 'taskCreated', handleTaskCreated as EventListener ); }; }, []); const handleRemoveTag = () => { const params = new URLSearchParams(location.search); params.delete('tag'); navigate({ pathname: location.pathname, search: `?${params.toString()}`, }); }; const handleTaskCreate = async (taskData: Partial) => { try { const newTask = await createTask(taskData as Task); setTasks((prevTasks) => [newTask, ...prevTasks]); const taskLink = ( {t('task.created', 'Task')}{' '} {newTask.name} {' '} {t('task.createdSuccessfully', 'created successfully!')} ); showSuccessToast(taskLink); } catch (error) { console.error('Error creating task:', error); setError('Error creating task.'); throw error; } }; const handleTaskUpdate = async (updatedTask: Task) => { try { const response = await fetch( getApiPath(`task/${updatedTask.uid}`), { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updatedTask), } ); if (response.ok) { const updatedTaskFromServer = await response.json(); setTasks((prevTasks) => prevTasks.map((task) => task.id === updatedTask.id ? { ...task, ...updatedTaskFromServer, subtasks: updatedTaskFromServer.subtasks || task.subtasks || [], } : task ) ); } else { const errorData = await response.json(); console.error('Failed to update task:', errorData.error); setError('Failed to update task.'); } } catch (error) { console.error('Error updating task:', error); setError('Error updating task.'); } }; const handleTaskCompletionToggle = (updatedTask: Task) => { setTasks((prevTasks) => prevTasks.map((task) => task.id === updatedTask.id ? updatedTask : task ) ); if (groupedTasks) { setGroupedTasks((prevGroupedTasks) => { if (!prevGroupedTasks) return null; const newGroupedTasks: GroupedTasks = {}; Object.entries(prevGroupedTasks).forEach( ([groupName, tasks]) => { newGroupedTasks[groupName] = tasks.map((task) => task.id === updatedTask.id ? updatedTask : task ); } ); return newGroupedTasks; }); } }; const handleTaskDelete = async (taskUid: string) => { try { const response = await fetch( getApiPath(`task/${encodeURIComponent(taskUid)}`), { method: 'DELETE', } ); if (response.ok) { setTasks((prevTasks) => prevTasks.filter((task) => task.uid !== taskUid) ); } else { const errorData = await response.json(); console.error('Failed to delete task:', errorData.error); setError('Failed to delete task.'); } } catch (error) { console.error('Error deleting task:', error); setError('Error deleting task.'); } }; const handleSortChange = (order: string) => { setOrderBy(order); localStorage.setItem('order_by', order); const params = new URLSearchParams(location.search); params.set('order_by', order); navigate( { pathname: location.pathname, search: `?${params.toString()}`, }, { replace: true } ); setDropdownOpen(false); }; const sortOptions: SortOption[] = [ { value: 'due_date:asc', label: t('sort.due_date', 'Due Date') }, { value: 'name:asc', label: t('sort.name', 'Name') }, { value: 'priority:desc', label: t('sort.priority', 'Priority') }, { value: 'status:desc', label: t('sort.status', 'Status') }, { value: 'created_at:desc', label: t('sort.created_at', 'Created At') }, ...(status === 'done' ? [ { value: 'completed_at:desc', label: t('sort.completed_at', 'Completed At'), }, ] : []), ]; const description = getDescription(query, projects, t, location.pathname); const isNewTaskAllowed = () => { const type = query.get('type'); return status !== 'done' && type !== 'upcoming'; }; return (
{/* Title row with info button and filters dropdown on the right */}

{title}

{tag && (
)}
{/* Info expand/collapse button, search button, show completed toggle, and sort dropdown */}
{!isUpcomingView && ( )} {!isUpcomingView && (
{t('tasks.groupBy', 'Group by')}
{['none', 'project'].map( (val) => ( ) )}
)}
{t('tasks.show', 'Show')}
{[ { key: 'active', label: t( 'tasks.open', 'Open' ), }, { key: 'all', label: t( 'tasks.all', 'All' ), }, { key: 'completed', label: t( 'tasks.completed', 'Completed' ), }, ].map((opt) => { const isActive = (opt.key === 'all' && status === null) || (opt.key === 'completed' && status === 'completed') || (opt.key === 'active' && status === 'active'); return ( ); })}
{t('tasks.direction', 'Direction')}
{[ { key: 'asc', label: t( 'tasks.ascending', 'Ascending' ), }, { key: 'desc', label: t( 'tasks.descending', 'Descending' ), }, ].map((dir) => { const currentDirection = orderBy.split(':')[1] || 'asc'; const isActive = currentDirection === dir.key; return ( ); })}
} />
{/* Info/description section with large info icon, collapsible */}

{description}

{/* Search input section, collapsible - hidden in upcoming view */} {!isUpcomingView && (
setTaskSearchQuery(e.target.value) } className="w-full bg-transparent border-none focus:ring-0 focus:outline-none dark:text-white" />
)} {loading ? (

{t('common.loading', 'Loading...')}

) : error ? (

{error}

) : ( <> {/* New Task Form */} {isNewTaskAllowed() && (
await handleTaskCreate({ name: taskName, status: 'not_started', }) } />
)} {displayTasks.length > 0 || (groupedTasks && Object.keys(groupedTasks).length > 0) ? ( <> {query.get('type') === 'upcoming' ? ( ) : groupBy === 'project' ? ( ) : ( )} {/* Load more button - hide in upcoming view */} {!isUpcomingView && hasMore && (
)} {/* Pagination info - hide in upcoming view */} {!isUpcomingView && tasks.length > 0 && (
{t( 'tasks.showingItems', 'Showing {{current}} of {{total}} tasks', { current: tasks.length, total: totalCount, } )}
)} ) : (

{t( 'tasks.noTasksAvailable', 'No tasks available.' )}

{t( 'tasks.blankSlateHint', 'Start by creating a new task or changing your filters.' )}

)} )}
); }; export default Tasks;