import React, { useEffect, useState, useRef, useMemo } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import TaskList from './Task/TaskList'; import NewTask from './Task/NewTask'; import SortFilter from './Shared/SortFilter'; import { Task } from '../entities/Task'; import { Project } from '../entities/Project'; import { getTitleAndIcon } from './Task/getTitleAndIcon'; import { getDescription } from './Task/getDescription'; import { createTask, toggleTaskToday } from '../utils/tasksService'; import { useToast } from './Shared/ToastContext'; import { SortOption } from './Shared/SortFilterButton'; import { TagIcon, XMarkIcon, MagnifyingGlassIcon, } from '@heroicons/react/24/solid'; 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 [tasks, setTasks] = useState([]); const [projects, setProjects] = useState([]); 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 dropdownRef = useRef(null); const location = useLocation(); const navigate = useNavigate(); // Filter tasks based on completion status and search query const displayTasks = useMemo(() => { let filteredTasks; // 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 if (taskSearchQuery.trim()) { const query = taskSearchQuery.toLowerCase(); filteredTasks = filteredTasks.filter( (task) => task.name.toLowerCase().includes(query) || task.note?.toLowerCase().includes(query) ); } return filteredTasks; }, [tasks, showCompleted, taskSearchQuery]); const query = new URLSearchParams(location.search); const { title: stateTitle } = location.state || {}; const title = stateTitle || getTitleAndIcon(query, projects, t).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]); 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'); // Fetch all tasks (both completed and non-completed) for client-side filtering const allTasksUrl = new URLSearchParams(location.search); // Add special parameter to get ALL tasks (completed and non-completed) allTasksUrl.set('client_side_filtering', 'true'); const searchParams = allTasksUrl.toString(); const [tasksResponse, projectsResponse] = await Promise.all([ fetch( `/api/tasks?${searchParams}${tagId ? `&tag=${tagId}` : ''}` ), fetch('/api/projects'), ]); if (tasksResponse.ok) { const tasksData = await tasksResponse.json(); setTasks(tasksData.tasks || []); } else { throw new Error('Failed to fetch tasks.'); } if (projectsResponse.ok) { const projectsData = await projectsResponse.json(); setProjects(projectsData?.projects || []); } else { throw new Error('Failed to fetch projects.'); } } catch (error) { setError((error as Error).message); } finally { setLoading(false); } }; fetchData(); }, [location]); // 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) { setTasks((prevTasks) => prevTasks.map((task) => task.id === updatedTask.id ? { ...task, ...updatedTask, // Explicitly preserve subtasks data subtasks: updatedTask.subtasks || updatedTask.Subtasks || task.subtasks || task.Subtasks || [], Subtasks: updatedTask.subtasks || updatedTask.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 ) ); }; 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); const isNewTaskAllowed = () => { return status !== 'done'; }; 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 */}
Show completed
{/* Info/description section with large info icon, collapsible */}

{description}

{/* Search input section, collapsible */}
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 ? ( ) : (

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

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

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