import React, { useEffect, useState, useMemo } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { Card, Spin, Empty } from 'antd'; import { DndContext, DragOverlay, DragStartEvent, DragEndEvent, DragOverEvent, KeyboardSensor, PointerSensor, useSensor, useSensors, UniqueIdentifier, getFirstCollision, pointerWithin, rectIntersection, } from '@dnd-kit/core'; import { SortableContext, horizontalListSortingStrategy, } from '@dnd-kit/sortable'; import { RootState } from '@/app/store'; import { fetchEnhancedKanbanGroups, reorderEnhancedKanbanTasks, reorderEnhancedKanbanGroups, setDragState, reorderTasks, reorderGroups, fetchEnhancedKanbanTaskAssignees, fetchEnhancedKanbanLabels, } from '@/features/enhanced-kanban/enhanced-kanban.slice'; import EnhancedKanbanGroup from './EnhancedKanbanGroup'; import './EnhancedKanbanBoard.css'; import { useSocket } from '@/socket/socketContext'; import { useAppSelector } from '@/hooks/useAppSelector'; import { SocketEvents } from '@/shared/socket-events'; import logger from '@/utils/errorLogger'; import { statusApiService } from '@/api/taskAttributes/status/status.api.service'; import { ITaskStatusCreateRequest } from '@/types/tasks/task-status-create-request'; import alertService from '@/services/alerts/alertService'; import { IGroupBy } from '@/features/enhanced-kanban/enhanced-kanban.slice'; import EnhancedKanbanCreateSection from './EnhancedKanbanCreateSection'; import ImprovedTaskFilters from '../task-management/improved-task-filters'; import { fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice'; import { useFilterDataLoader } from '@/hooks/useFilterDataLoader'; import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers'; import { useAuthService } from '@/hooks/useAuth'; // Import the TaskListFilters component const TaskListFilters = React.lazy(() => import('@/pages/projects/projectView/taskList/task-list-filters/task-list-filters')); interface EnhancedKanbanBoardProps { projectId: string; className?: string; } const EnhancedKanbanBoard: React.FC = ({ projectId, className = '' }) => { const dispatch = useDispatch(); const { taskGroups, loadingGroups, error, dragState, performanceMetrics } = useSelector((state: RootState) => state.enhancedKanbanReducer); const { socket } = useSocket(); const authService = useAuthService(); const teamId = authService.getCurrentSession()?.team_id; const groupBy = useSelector((state: RootState) => state.enhancedKanbanReducer.groupBy); const project = useAppSelector((state: RootState) => state.projectReducer.project); const { statusCategories, status: existingStatuses } = useAppSelector((state) => state.taskStatusReducer); const themeMode = useAppSelector(state => state.themeReducer.mode); // Load filter data useFilterDataLoader(); // Set up socket event handlers for real-time updates useTaskSocketHandlers(); // Local state for drag overlay const [activeTask, setActiveTask] = useState(null); const [activeGroup, setActiveGroup] = useState(null); const [overId, setOverId] = useState(null); // Sensors for drag and drop const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8, }, }), useSensor(KeyboardSensor) ); useEffect(() => { if (projectId) { dispatch(fetchEnhancedKanbanGroups(projectId) as any); // Load filter data for enhanced kanban dispatch(fetchEnhancedKanbanTaskAssignees(projectId) as any); dispatch(fetchEnhancedKanbanLabels(projectId) as any); } if (!statusCategories.length) { dispatch(fetchStatusesCategories() as any); } }, [dispatch, projectId]); // Get all task IDs for sortable context const allTaskIds = useMemo(() => taskGroups.flatMap(group => group.tasks.map(task => task.id!)), [taskGroups] ); const allGroupIds = useMemo(() => taskGroups.map(group => group.id), [taskGroups] ); // Enhanced collision detection const collisionDetectionStrategy = (args: any) => { // First, let's see if we're colliding with any droppable areas const pointerIntersections = pointerWithin(args); const intersections = pointerIntersections.length > 0 ? pointerIntersections : rectIntersection(args); let overId = getFirstCollision(intersections, 'id'); if (overId) { // Check if we're over a task or a group const overGroup = taskGroups.find(g => g.id === overId); if (overGroup) { // We're over a group, check if there are tasks in it if (overGroup.tasks.length > 0) { // Find the closest task within this group const taskIntersections = pointerWithin({ ...args, droppableContainers: args.droppableContainers.filter( (container: any) => container.data.current?.type === 'task' ), }); if (taskIntersections.length > 0) { overId = taskIntersections[0].id; } } } } return overId ? [{ id: overId }] : []; }; const handleDragStart = (event: DragStartEvent) => { const { active } = event; const activeId = active.id as string; const activeData = active.data.current; // Check if dragging a group or a task if (activeData?.type === 'group') { // Dragging a group const foundGroup = taskGroups.find(g => g.id === activeId); setActiveGroup(foundGroup); setActiveTask(null); dispatch(setDragState({ activeTaskId: null, activeGroupId: activeId, isDragging: true, })); } else { // Dragging a task let foundTask = null; let foundGroup = null; for (const group of taskGroups) { const task = group.tasks.find(t => t.id === activeId); if (task) { foundTask = task; foundGroup = group; break; } } setActiveTask(foundTask); setActiveGroup(null); dispatch(setDragState({ activeTaskId: activeId, activeGroupId: foundGroup?.id || null, isDragging: true, })); } }; const handleDragOver = (event: DragOverEvent) => { const { active, over } = event; if (!over) { setOverId(null); dispatch(setDragState({ overId: null })); return; } const activeId = active.id as string; const overId = over.id as string; setOverId(overId); // Update over ID in Redux dispatch(setDragState({ overId })); }; const handleDragEnd = async (event: DragEndEvent) => { const { active, over } = event; const activeData = active.data.current; // Reset local state setActiveTask(null); setActiveGroup(null); setOverId(null); // Reset Redux drag state dispatch(setDragState({ activeTaskId: null, activeGroupId: null, overId: null, isDragging: false, })); if (!over) return; const activeId = active.id as string; const overId = over.id as string; // Handle group (column) reordering if (activeData?.type === 'group') { // Don't allow reordering if groupBy is phases if (groupBy === IGroupBy.PHASE) { return; } const fromIndex = taskGroups.findIndex(g => g.id === activeId); const toIndex = taskGroups.findIndex(g => g.id === overId); if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) { // Create new array with reordered groups const reorderedGroups = [...taskGroups]; const [movedGroup] = reorderedGroups.splice(fromIndex, 1); reorderedGroups.splice(toIndex, 0, movedGroup); // Synchronous UI update for immediate feedback dispatch(reorderGroups({ fromIndex, toIndex, reorderedGroups })); dispatch(reorderEnhancedKanbanGroups({ fromIndex, toIndex, reorderedGroups }) as any); // Prepare column order for API const columnOrder = reorderedGroups.map(group => group.id); // Call API to update status order try { const requestBody: ITaskStatusCreateRequest = { status_order: columnOrder }; const response = await statusApiService.updateStatusOrder(requestBody, projectId); if (!response.done) { // Revert the change if API call fails const revertedGroups = [...reorderedGroups]; const [movedBackGroup] = revertedGroups.splice(toIndex, 1); revertedGroups.splice(fromIndex, 0, movedBackGroup); dispatch(reorderGroups({ fromIndex: toIndex, toIndex: fromIndex, reorderedGroups: revertedGroups })); alertService.error('Failed to update column order', 'Please try again'); } } catch (error) { // Revert the change if API call fails const revertedGroups = [...reorderedGroups]; const [movedBackGroup] = revertedGroups.splice(toIndex, 1); revertedGroups.splice(fromIndex, 0, movedBackGroup); dispatch(reorderGroups({ fromIndex: toIndex, toIndex: fromIndex, reorderedGroups: revertedGroups })); alertService.error('Failed to update column order', 'Please try again'); logger.error('Failed to update column order', error); } } return; } // Handle task reordering (within or between groups) let sourceGroup = null; let targetGroup = null; let sourceIndex = -1; let targetIndex = -1; // Find source group and index for (const group of taskGroups) { const taskIndex = group.tasks.findIndex(t => t.id === activeId); if (taskIndex !== -1) { sourceGroup = group; sourceIndex = taskIndex; break; } } // Find target group and index for (const group of taskGroups) { const taskIndex = group.tasks.findIndex(t => t.id === overId); if (taskIndex !== -1) { targetGroup = group; targetIndex = taskIndex; break; } } // If dropping on a group (not a task) if (!targetGroup) { targetGroup = taskGroups.find(g => g.id === overId); if (targetGroup) { targetIndex = targetGroup.tasks.length; // Add to end of group } } if (!sourceGroup || !targetGroup || sourceIndex === -1) return; // Don't do anything if dropping in the same position if (sourceGroup.id === targetGroup.id && sourceIndex === targetIndex) return; // Create updated task arrays const updatedSourceTasks = [...sourceGroup.tasks]; const [movedTask] = updatedSourceTasks.splice(sourceIndex, 1); let updatedTargetTasks: any[]; if (sourceGroup.id === targetGroup.id) { // Moving within the same group updatedTargetTasks = updatedSourceTasks; updatedTargetTasks.splice(targetIndex, 0, movedTask); } else { // Moving between different groups updatedTargetTasks = [...targetGroup.tasks]; updatedTargetTasks.splice(targetIndex, 0, movedTask); } // Synchronous UI update dispatch(reorderTasks({ activeGroupId: sourceGroup.id, overGroupId: targetGroup.id, fromIndex: sourceIndex, toIndex: targetIndex, task: movedTask, updatedSourceTasks, updatedTargetTasks, })); dispatch(reorderEnhancedKanbanTasks({ activeGroupId: sourceGroup.id, overGroupId: targetGroup.id, fromIndex: sourceIndex, toIndex: targetIndex, task: movedTask, updatedSourceTasks, updatedTargetTasks, }) as any); // --- Socket emit for task sort order --- if (socket && projectId && movedTask) { // Find sort_order for from and to const fromSortOrder = movedTask.sort_order; let toSortOrder = -1; let toLastIndex = false; if (targetIndex === targetGroup.tasks.length) { // Dropping at the end toSortOrder = -1; toLastIndex = true; } else if (targetGroup.tasks[targetIndex]) { toSortOrder = typeof targetGroup.tasks[targetIndex].sort_order === 'number' ? targetGroup.tasks[targetIndex].sort_order! : -1; toLastIndex = false; } else if (targetGroup.tasks.length > 0) { const lastSortOrder = targetGroup.tasks[targetGroup.tasks.length - 1].sort_order; toSortOrder = typeof lastSortOrder === 'number' ? lastSortOrder! : -1; toLastIndex = false; } const body = { project_id: projectId, from_index: fromSortOrder, to_index: toSortOrder, to_last_index: toLastIndex, from_group: sourceGroup.id, to_group: targetGroup.id, group_by: groupBy || 'status', task: movedTask, team_id: teamId || project?.team_id || '', }; socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), body); } }; if (error) { return ( ); } return ( <> {/* Task Filters */}
Loading filters...
}>
{/* Performance Monitor - only show for large datasets */} {/* {performanceMetrics.totalTasks > 100 && } */} {loadingGroups ? (
) : taskGroups.length === 0 ? ( ) : (
{taskGroups.map(group => ( ))}
{activeTask && (
{activeTask.name}
)} {activeGroup && (

{activeGroup.name}

({activeGroup.tasks.length})
)}
)}
); }; export default EnhancedKanbanBoard;