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 SortFilter from './Shared/SortFilter'; import { Task } from '../entities/Task'; import { getTitleAndIcon } from './Task/getTitleAndIcon'; import { getDescription } from './Task/getDescription'; import { createTask, toggleTaskToday, GroupedTasks, } from '../utils/tasksService'; import { useStore } from '../store/useStore'; import { useToast } from './Shared/ToastContext'; import { SortOption } from './Shared/SortFilterButton'; import { TagIcon, XMarkIcon, MagnifyingGlassIcon, } from '@heroicons/react/24/solid'; import { InformationCircleIcon, QueueListIcon, } from '@heroicons/react/24/outline'; import { getApiPath } from '../config/paths'; const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); // Helper function to get search placeholder by language 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); // Collapsed by default const [isSearchExpanded, setIsSearchExpanded] = useState(false); // Collapsed by default const [showCompleted, setShowCompleted] = useState(false); // Show completed tasks toggle const [isMobile, setIsMobile] = useState(window.innerWidth < 768); // Pagination state const [offset, setOffset] = useState(0); const [hasMore, setHasMore] = useState(false); const [isLoadingMore, setIsLoadingMore] = useState(false); const [totalCount, setTotalCount] = useState(0); const limit = 20; const dropdownRef = useRef(null); const location = useLocation(); const navigate = useNavigate(); const query = new URLSearchParams(location.search); const isUpcomingView = query.get('type') === 'upcoming'; // Filter tasks based on completion status and search query const displayTasks = useMemo(() => { let filteredTasks: Task[]; // Filter by completion status (applies to all views) if (showCompleted) { // Show only completed tasks (done=2 or archived=3) filteredTasks = tasks.filter( (task: Task) => task.status === 'done' || task.status === 'archived' || task.status === 2 || task.status === 3 ); } else { // Show only non-completed tasks - exclude done(2) and archived(3) filteredTasks = tasks.filter( (task: Task) => task.status !== 'done' && task.status !== 'archived' && task.status !== 2 && task.status !== 3 ); } // Then filter by search query if provided (skip for upcoming view) if (taskSearchQuery.trim() && !isUpcomingView) { const query = taskSearchQuery.toLowerCase(); filteredTasks = filteredTasks.filter( (task: Task) => task.name.toLowerCase().includes(query) || task.original_name?.toLowerCase().includes(query) || task.note?.toLowerCase().includes(query) ); } return filteredTasks; }, [tasks, showCompleted, taskSearchQuery, isUpcomingView]); // Handle the /upcoming route by setting type=upcoming in query params 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; const tag = query.get('tag'); const status = query.get('status'); useEffect(() => { const savedOrderBy = localStorage.getItem('order_by') || 'created_at:desc'; setOrderBy(savedOrderBy); 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]); // Clear search query when switching to upcoming view 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) => { setLoading(resetPagination); setError(null); try { const tagId = query.get('tag'); const type = query.get('type'); // Fetch all tasks (both completed and non-completed) for client-side filtering const allTasksUrl = new URLSearchParams(query.toString()); // Add special parameter to get ALL tasks (completed and non-completed) allTasksUrl.set('client_side_filtering', 'true'); // Add groupBy=day for upcoming tasks if (type === 'upcoming') { allTasksUrl.set('type', 'upcoming'); allTasksUrl.set('groupBy', 'day'); // Always show 7 days (whole week including tomorrow) allTasksUrl.set('maxDays', '7'); allTasksUrl.set('sidebarOpen', isSidebarOpen.toString()); allTasksUrl.set('isMobile', isMobile.toString()); } // Add pagination parameters const currentOffset = resetPagination ? 0 : offset; allTasksUrl.set('limit', limit.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); setOffset(limit); } else { setTasks((prev) => [...prev, ...(tasksData.tasks || [])]); // For grouped tasks, merge them if (tasksData.groupedTasks) { setGroupedTasks((prev) => { if (!prev) return tasksData.groupedTasks; return { ...prev, ...tasksData.groupedTasks, }; }); } setOffset((prev) => prev + limit); } setHasMore(tasksData.pagination?.hasMore || false); if (tasksData.pagination) { setTotalCount(tasksData.pagination.total); } } else { throw new Error('Failed to fetch tasks.'); } // Projects are now loaded by Layout component into global store } catch (error) { setError((error as Error).message); } finally { setLoading(false); setIsLoadingMore(false); } }; const loadMore = async () => { if (!hasMore || isLoadingMore) return; setIsLoadingMore(true); await fetchData(false); }; useEffect(() => { fetchData(true); }, [location, isSidebarOpen, isMobile]); // Handle window resize for mobile detection useEffect(() => { const handleResize = () => { const newIsMobile = window.innerWidth < 768; if (newIsMobile !== isMobile) { setIsMobile(newIsMobile); } }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, [isMobile]); // Listen for task creation from other components (e.g., Layout modal) 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); // Add the new task optimistically to avoid race conditions setTasks((prevTasks) => [newTask, ...prevTasks]); // Show success toast with task link 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; // Re-throw to allow proper error handling } }; const handleTaskUpdate = async (updatedTask: Task) => { try { const response = await fetch(getApiPath(`task/${updatedTask.id}`), { 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, // Explicitly preserve subtasks data subtasks: updatedTaskFromServer.subtasks || updatedTaskFromServer.Subtasks || task.subtasks || task.Subtasks || [], Subtasks: updatedTaskFromServer.subtasks || updatedTaskFromServer.Subtasks || task.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.'); } }; // Handler specifically for task completion toggles (no API call needed, just state update) const handleTaskCompletionToggle = (updatedTask: Task) => { setTasks((prevTasks) => prevTasks.map((task) => task.id === updatedTask.id ? updatedTask : task ) ); // Also update groupedTasks if they exist 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 (taskId: number) => { try { const response = await fetch(getApiPath(`task/${taskId}`), { method: 'DELETE', }); if (response.ok) { setTasks((prevTasks) => prevTasks.filter((task) => task.id !== taskId) ); } 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 handleToggleToday = async ( taskId: number, task?: Task ): Promise => { try { await toggleTaskToday(taskId, task); // Refetch data to ensure consistency with all task relationships const params = new URLSearchParams(location.search); const type = params.get('type') || 'all'; const tag = params.get('tag'); const project = params.get('project'); const priority = params.get('priority'); let apiPath = `tasks?type=${type}&order_by=${orderBy}`; if (tag) apiPath += `&tag=${tag}`; if (project) apiPath += `&project=${project}`; if (priority) apiPath += `&priority=${priority}`; const response = await fetch(getApiPath(apiPath), { credentials: 'include', }); if (response.ok) { const data = await response.json(); setTasks(data.tasks || data); } } catch (error) { console.error('Error toggling task today status:', error); setError('Error toggling task today status.'); } }; 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); }; // Sort options for tasks 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') }, ]; 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 && ( )}
Show completed
{/* 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' ? ( ) : ( )} {/* Load more button */} {hasMore && (
)} {/* Pagination info */} {tasks.length > 0 && (
{t( 'tasks.showingItems', 'Showing {{current}} of {{total}} items', { 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;