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 } from '@heroicons/react/24/outline'; 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); 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; // For upcoming view, don't filter by completion status here // Let GroupedTaskList handle it if (isUpcomingView) { filteredTasks = tasks; } else { // First filter by completion status if (showCompleted) { // Show only completed tasks (done=2 or archived=3) filteredTasks = tasks.filter( (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.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()) { const query = taskSearchQuery.toLowerCase(); filteredTasks = filteredTasks.filter( (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]); useEffect(() => { const fetchData = async () => { setLoading(true); 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()); } const searchParams = allTasksUrl.toString(); const tasksResponse = await fetch( `/api/tasks?${searchParams}${tagId ? `&tag=${tagId}` : ''}` ); if (tasksResponse.ok) { const tasksData = await tasksResponse.json(); setTasks(tasksData.tasks || []); setGroupedTasks(tasksData.groupedTasks || null); } 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); } }; fetchData(); }, [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(`/api/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(`/api/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): Promise => { try { await toggleTaskToday(taskId); // 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 = `/api/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(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' ? ( ) : ( ) ) : (

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

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

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