import React, { useState, useEffect, useMemo } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { RootState } from '@/app/store'; import { ITaskListGroup } from '@/types/tasks/taskList.types'; import { IProjectTask } from '@/types/project/projectTasksViewModel.types'; import '../EnhancedKanbanBoard.css'; import '../EnhancedKanbanGroup.css'; import '../EnhancedKanbanTaskCard.css'; import ImprovedTaskFilters from '../../task-management/improved-task-filters'; import Card from 'antd/es/card'; import Spin from 'antd/es/spin'; import Empty from 'antd/es/empty'; import { reorderGroups, reorderEnhancedKanbanGroups, reorderTasks, reorderEnhancedKanbanTasks, fetchEnhancedKanbanLabels, fetchEnhancedKanbanGroups, fetchEnhancedKanbanTaskAssignees } from '@/features/enhanced-kanban/enhanced-kanban.slice'; import { fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice'; import { useAppSelector } from '@/hooks/useAppSelector'; // Minimal task card for prototype (reuse your styles) const TaskCard: React.FC<{ task: IProjectTask; onTaskDragStart: (e: React.DragEvent, taskId: string, groupId: string) => void; onTaskDragOver: (e: React.DragEvent, groupId: string, taskIdx: number) => void; onTaskDrop: (e: React.DragEvent, groupId: string, taskIdx: number) => void; groupId: string; isDropIndicator: boolean; idx: number; }> = ({ task, onTaskDragStart, onTaskDragOver, onTaskDrop, groupId, isDropIndicator, idx }) => { const themeMode = useSelector((state: RootState) => state.themeReducer.mode); const background = themeMode === 'dark' ? '#23272f' : '#fff'; const color = themeMode === 'dark' ? '#fff' : '#23272f'; return ( <> {isDropIndicator && (
)}
onTaskDragStart(e, task.id!, groupId)} onDragOver={e => onTaskDragOver(e, groupId, idx)} onDrop={e => onTaskDrop(e, groupId, idx)} style={{ background, color }} >
{task.name}
{task.task_key}
{task.assignees?.map(a => a.name).join(', ')}
); }; // Minimal group column for prototype const KanbanGroup: React.FC<{ group: ITaskListGroup; onGroupDragStart: (e: React.DragEvent, groupId: string) => void; onGroupDragOver: (e: React.DragEvent) => void; onGroupDrop: (e: React.DragEvent, groupId: string) => void; onTaskDragStart: (e: React.DragEvent, taskId: string, groupId: string) => void; onTaskDragOver: (e: React.DragEvent, groupId: string, taskIdx: number) => void; onTaskDrop: (e: React.DragEvent, groupId: string, taskIdx: number) => void; hoveredTaskIdx: number | null; hoveredGroupId: string | null; }> = ({ group, onGroupDragStart, onGroupDragOver, onGroupDrop, onTaskDragStart, onTaskDragOver, onTaskDrop, hoveredTaskIdx, hoveredGroupId }) => { const themeMode = useAppSelector(state => state.themeReducer.mode); const headerBackgroundColor = useMemo(() => { if (themeMode === 'dark') { return group.color_code_dark || group.color_code || '#1e1e1e'; } return group.color_code || '#f5f5f5'; }, [themeMode, group.color_code, group.color_code_dark]); return (
onGroupDragStart(e, group.id)} onDragOver={onGroupDragOver} onDrop={e => onGroupDrop(e, group.id)} >

{group.name}

{group.tasks.length}
onTaskDragOver(e, group.id, 0)} // onDrop={e => onTaskDrop(e, group.id, 0)} > {/* Drop indicator at the top of the group */} {hoveredGroupId === group.id && hoveredTaskIdx === 0 && (
)} {group.tasks.map((task, idx) => ( ))} {/* Drop indicator at the end of the group */} {hoveredGroupId === group.id && hoveredTaskIdx === group.tasks.length && (
)}
) }; const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ projectId }) => { const dispatch = useDispatch(); const { taskGroups, loadingGroups, error, } = useSelector((state: RootState) => state.enhancedKanbanReducer); const [draggedGroupId, setDraggedGroupId] = useState(null); const [draggedTaskId, setDraggedTaskId] = useState(null); const [draggedTaskGroupId, setDraggedTaskGroupId] = useState(null); const [hoveredGroupId, setHoveredGroupId] = useState(null); const [hoveredTaskIdx, setHoveredTaskIdx] = useState(null); const [dragType, setDragType] = useState<'group' | 'task' | null>(null); const { statusCategories, status: existingStatuses } = useAppSelector((state) => state.taskStatusReducer); 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]); // Reset drag state if taskGroups changes (e.g., real-time update) useEffect(() => { setDraggedGroupId(null); setDraggedTaskId(null); setDraggedTaskGroupId(null); setHoveredGroupId(null); setHoveredTaskIdx(null); setDragType(null); }, [taskGroups]); // Group drag handlers const handleGroupDragStart = (e: React.DragEvent, groupId: string) => { setDraggedGroupId(groupId); setDragType('group'); e.dataTransfer.effectAllowed = 'move'; }; const handleGroupDragOver = (e: React.DragEvent) => { if (dragType !== 'group') return; e.preventDefault(); }; const handleGroupDrop = (e: React.DragEvent, targetGroupId: string) => { if (dragType !== 'group') return; e.preventDefault(); if (!draggedGroupId || draggedGroupId === targetGroupId) return; // Calculate new order and dispatch const fromIdx = taskGroups.findIndex(g => g.id === draggedGroupId); const toIdx = taskGroups.findIndex(g => g.id === targetGroupId); if (fromIdx === -1 || toIdx === -1) return; const reorderedGroups = [...taskGroups]; const [moved] = reorderedGroups.splice(fromIdx, 1); reorderedGroups.splice(toIdx, 0, moved); dispatch(reorderGroups({ fromIndex: fromIdx, toIndex: toIdx, reorderedGroups })); dispatch(reorderEnhancedKanbanGroups({ fromIndex: fromIdx, toIndex: toIdx, reorderedGroups }) as any); setDraggedGroupId(null); setDragType(null); }; // Task drag handlers const handleTaskDragStart = (e: React.DragEvent, taskId: string, groupId: string) => { setDraggedTaskId(taskId); setDraggedTaskGroupId(groupId); setDragType('task'); e.dataTransfer.effectAllowed = 'move'; }; const handleTaskDragOver = (e: React.DragEvent, groupId: string, taskIdx: number) => { if (dragType !== 'task') return; e.preventDefault(); if (draggedTaskId) { setHoveredGroupId(groupId); setHoveredTaskIdx(taskIdx); } }; const handleTaskDrop = (e: React.DragEvent, targetGroupId: string, targetTaskIdx: number) => { if (dragType !== 'task') return; e.preventDefault(); if (!draggedTaskId || !draggedTaskGroupId || hoveredGroupId === null || hoveredTaskIdx === null) return; // Calculate new order and dispatch const sourceGroup = taskGroups.find(g => g.id === draggedTaskGroupId); const targetGroup = taskGroups.find(g => g.id === targetGroupId); if (!sourceGroup || !targetGroup) return; const taskIdx = sourceGroup.tasks.findIndex(t => t.id === draggedTaskId); if (taskIdx === -1) return; const movedTask = sourceGroup.tasks[taskIdx]; let insertIdx = hoveredTaskIdx; // Handle same group reordering if (sourceGroup.id === targetGroup.id) { // Create a single updated array for the same group const updatedTasks = [...sourceGroup.tasks]; updatedTasks.splice(taskIdx, 1); // Remove from original position // Adjust insert index if moving forward in the same array if (taskIdx < insertIdx) { insertIdx--; } if (insertIdx < 0) insertIdx = 0; if (insertIdx > updatedTasks.length) insertIdx = updatedTasks.length; updatedTasks.splice(insertIdx, 0, movedTask); // Insert at new position dispatch(reorderTasks({ activeGroupId: sourceGroup.id, overGroupId: targetGroup.id, fromIndex: taskIdx, toIndex: insertIdx, task: movedTask, updatedSourceTasks: updatedTasks, updatedTargetTasks: updatedTasks, })); dispatch(reorderEnhancedKanbanTasks({ activeGroupId: sourceGroup.id, overGroupId: targetGroup.id, fromIndex: taskIdx, toIndex: insertIdx, task: movedTask, updatedSourceTasks: updatedTasks, updatedTargetTasks: updatedTasks, }) as any); } else { // Handle cross-group reordering const updatedSourceTasks = [...sourceGroup.tasks]; updatedSourceTasks.splice(taskIdx, 1); const updatedTargetTasks = [...targetGroup.tasks]; if (insertIdx < 0) insertIdx = 0; if (insertIdx > updatedTargetTasks.length) insertIdx = updatedTargetTasks.length; updatedTargetTasks.splice(insertIdx, 0, movedTask); dispatch(reorderTasks({ activeGroupId: sourceGroup.id, overGroupId: targetGroup.id, fromIndex: taskIdx, toIndex: insertIdx, task: movedTask, updatedSourceTasks, updatedTargetTasks, })); dispatch(reorderEnhancedKanbanTasks({ activeGroupId: sourceGroup.id, overGroupId: targetGroup.id, fromIndex: taskIdx, toIndex: insertIdx, task: movedTask, updatedSourceTasks, updatedTargetTasks, }) as any); } setDraggedTaskId(null); setDraggedTaskGroupId(null); setHoveredGroupId(null); setHoveredTaskIdx(null); setDragType(null); }; if (error) { return ( ); } return ( <>
Loading filters...
}>
{loadingGroups ? (
) : taskGroups.length === 0 ? ( ) : (
{taskGroups.map(group => ( ))}
)}
); }; export default EnhancedKanbanBoardNativeDnD;