import React, { useState, useMemo } from 'react'; import { ChevronRightIcon, ChevronDownIcon, ArrowPathIcon, } from '@heroicons/react/24/outline'; import { useTranslation } from 'react-i18next'; import TaskItem from './TaskItem'; import { Project } from '../../entities/Project'; import { Task } from '../../entities/Task'; import { GroupedTasks } from '../../utils/tasksService'; interface GroupedTaskListProps { tasks: Task[]; groupedTasks?: GroupedTasks | null; groupBy?: 'none' | 'project'; onTaskUpdate: (task: Task) => Promise; onTaskCompletionToggle?: (task: Task) => void; onTaskCreate?: (task: Task) => void; onTaskDelete: (taskUid: string) => void; projects: Project[]; hideProjectName?: boolean; onToggleToday?: (taskId: number, task?: Task) => Promise; showCompletedTasks?: boolean; searchQuery?: string; } interface TaskGroup { template: Task; instances: Task[]; } const GroupedTaskList: React.FC = ({ tasks, groupedTasks, groupBy = 'none', onTaskUpdate, onTaskCompletionToggle, onTaskDelete, projects, hideProjectName = false, onToggleToday, showCompletedTasks = false, searchQuery = '', }) => { const { t } = useTranslation(); const [expandedRecurringGroups, setExpandedRecurringGroups] = useState< Set >(new Set()); // If we have day-based groupedTasks from API, use those instead of recurring groups const shouldUseDayGrouping = groupedTasks && Object.keys(groupedTasks).length > 0; // Group tasks by recurring template (legacy behavior) const { recurringGroups, standaloneTask } = useMemo(() => { if (shouldUseDayGrouping) { // For day grouping, we don't need recurring groups return { recurringGroups: [], standaloneTask: [] }; } // Filter tasks based on completion status const filteredTasks = showCompletedTasks ? tasks.filter((task) => { // Show only completed tasks (done=2 or archived=3) const isCompleted = task.status === 'done' || task.status === 'archived' || task.status === 2 || task.status === 3; return isCompleted; }) : tasks.filter((task) => { // Show only non-completed tasks const isCompleted = task.status === 'done' || task.status === 'archived' || task.status === 2 || task.status === 3; return !isCompleted; }); const groups = new Map(); const standalone: Task[] = []; filteredTasks.forEach((task) => { if (task.recurring_parent_id) { // This is a recurring instance const parentId = task.recurring_parent_id; if (!groups.has(parentId)) { // Find the template task in the current results let template = filteredTasks.find((t) => t.id === parentId); // If template not found in results, create a placeholder using the instance data if (!template) { // Create a virtual template task based on the instance template = { ...task, id: parentId, recurring_parent_id: null, // This makes it the template due_date: null, // Templates don't have specific due dates name: task.name, // Keep the same name isVirtualTemplate: true, // Flag to identify virtual templates } as Task & { isVirtualTemplate?: boolean }; } groups.set(parentId, { template, instances: [] }); } const group = groups.get(parentId); if (group) { group.instances.push(task); } } else if ( task.recurrence_type && task.recurrence_type !== 'none' ) { // This is a recurring template - check if it has instances const instances = filteredTasks.filter( (t) => t.recurring_parent_id === task.id ); if (instances.length > 0) { groups.set(task.id!, { template: task, instances }); } else { // Template without instances, show as standalone standalone.push(task); } } else { // Regular task standalone.push(task); } }); return { recurringGroups: Array.from(groups.values()), standaloneTask: standalone, }; }, [tasks, showCompletedTasks, shouldUseDayGrouping]); // Filter grouped tasks for completed status and search query const filteredGroupedTasks = useMemo(() => { if (!shouldUseDayGrouping || !groupedTasks) return {}; const filtered: GroupedTasks = {}; Object.entries(groupedTasks).forEach(([groupName, groupTasks]) => { // Filter by completion status let filteredTasks = showCompletedTasks ? groupTasks.filter((task) => { // Show only completed tasks const isCompleted = task.status === 'done' || task.status === 'archived' || task.status === 2 || task.status === 3; return isCompleted; }) : groupTasks.filter((task) => { // Show only non-completed tasks const isCompleted = task.status === 'done' || task.status === 'archived' || task.status === 2 || task.status === 3; return !isCompleted; }); // Apply search filter if search query provided if (searchQuery.trim()) { const query = searchQuery.toLowerCase(); filteredTasks = filteredTasks.filter( (task) => task.name.toLowerCase().includes(query) || task.note?.toLowerCase().includes(query) ); } if (filteredTasks.length > 0) { filtered[groupName] = filteredTasks; } }); return filtered; }, [groupedTasks, showCompletedTasks, shouldUseDayGrouping, searchQuery]); // Group tasks by project when requested (only applies to standalone view) const groupedByProject = useMemo(() => { if (groupBy !== 'project') return null; // Apply completion filter const filtered = showCompletedTasks ? tasks.filter((task) => { const isCompleted = task.status === 'done' || task.status === 'archived' || task.status === 2 || task.status === 3; return isCompleted; }) : tasks.filter((task) => { const isCompleted = task.status === 'done' || task.status === 'archived' || task.status === 2 || task.status === 3; return !isCompleted; }); // Apply search const filteredBySearch = searchQuery.trim() ? filtered.filter((task) => (task.name || '') .toLowerCase() .includes(searchQuery.toLowerCase()) ) : filtered; const byProject = new Map(); filteredBySearch.forEach((task) => { const key = task.project_id || 'no_project'; const arr = byProject.get(key) || []; arr.push(task); byProject.set(key, arr); }); return Array.from(byProject.entries()).map( ([projectId, projectTasks]) => ({ projectId, tasks: projectTasks, }) ); }, [groupBy, tasks, showCompletedTasks, searchQuery]); const toggleRecurringGroup = (templateId: number) => { setExpandedRecurringGroups((prev) => { const newSet = new Set(prev); if (newSet.has(templateId)) { newSet.delete(templateId); } else { newSet.add(templateId); } return newSet; }); }; const formatRecurrence = (recurrenceType: string) => { switch (recurrenceType) { case 'daily': return t('recurrence.daily', 'Daily'); case 'weekly': return t('recurrence.weekly', 'Weekly'); case 'monthly': return t('recurrence.monthly', 'Monthly'); default: return t('recurrence.recurring', 'Recurring'); } }; // Render day-based grouping if available if (shouldUseDayGrouping) { return (
{Object.keys(filteredGroupedTasks).length === 0 ? (

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

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

) : ( /* Responsive board layout */
{/* Mobile: Stack vertically, Desktop: Horizontal board */}
{Object.entries(filteredGroupedTasks).map( ([groupName, dayTasks]) => { return (
{/* Day column header */}

{groupName}

{/* Day column tasks */}
{dayTasks.map((task) => (
))} {/* Empty state for columns with no tasks */} {dayTasks.length === 0 && (

No tasks scheduled

)}
); } )}
)}
); } // Legacy: Render recurring task grouping return (
{/* Standalone tasks */} {groupBy === 'project' && groupedByProject ? groupedByProject.map( ({ projectId, tasks: projectTasks }, index) => { const projectName = projects.find((p) => p.id === projectId)?.name || (projectId === 'no_project' ? t('tasks.noProject', 'No project') : t( 'tasks.unknownProject', 'Unknown project' )); return (
0 ? 'pt-4' : ''}`} >
{projectName} {projectTasks.length}{' '} {t('tasks.tasks', 'tasks')}
{projectTasks.map((task) => (
))}
); } ) : standaloneTask.map((task) => (
))} {/* Grouped recurring tasks */} {recurringGroups.map((group) => { const isVirtualTemplate = (group.template as any) .isVirtualTemplate; const isExpanded = expandedRecurringGroups.has(group.template.id!) || isVirtualTemplate; // Auto-expand virtual templates return (
{/* Show template only if it's not virtual */} {!isVirtualTemplate && (
{/* Recurring instances count and expand button */} {group.instances.length > 0 && ( )}
)} {/* For virtual templates, show a simple header */} {isVirtualTemplate && group.instances.length > 0 && (
{group.template.name} -{' '} {formatRecurrence( group.template.recurrence_type! )} {group.instances.length} upcoming
)} {/* Expanded instances */} {isExpanded && group.instances.length > 0 && (
{formatRecurrence( group.template.recurrence_type! )}{' '} instances
{group.instances .sort( (a, b) => new Date( a.due_date || '' ).getTime() - new Date(b.due_date || '').getTime() ) .map((instance) => (
))}
)}
); })} {standaloneTask.length === 0 && recurringGroups.length === 0 && (

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

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

)}
); }; export default GroupedTaskList;