import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react'; import { createPortal } from 'react-dom'; import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppDispatch } from '@/hooks/useAppDispatch'; import { useSocket } from '@/socket/socketContext'; import { SocketEvents } from '@/shared/socket-events'; import { Task } from '@/types/task-management.types'; import { updateTask, selectCurrentGroupingV3, selectGroups, moveTaskBetweenGroups, } from '@/features/task-management/task-management.slice'; interface TaskStatusDropdownProps { task: Task; projectId: string; isDarkMode?: boolean; } const TaskStatusDropdown: React.FC = ({ task, projectId, isDarkMode = false, }) => { const dispatch = useAppDispatch(); const { socket, connected } = useSocket(); const [isOpen, setIsOpen] = useState(false); const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 }); const buttonRef = useRef(null); const dropdownRef = useRef(null); const statusList = useAppSelector(state => state.taskStatusReducer.status); const currentGroupingV3 = useAppSelector(selectCurrentGroupingV3); const groups = useAppSelector(selectGroups); // Find current status details const currentStatus = useMemo(() => { return statusList.find( status => status.name?.toLowerCase() === task.status?.toLowerCase() || status.id === task.status ); }, [statusList, task.status]); // Handle status change const handleStatusChange = useCallback( (statusId: string, statusName: string) => { if (!task.id || !statusId || !connected) return; // Optimistic update: immediately update the task status in Redux for instant feedback const updatedTask = { ...task, status: statusId, updatedAt: new Date().toISOString(), }; dispatch(updateTask(updatedTask)); // Handle group movement if grouping by status if (currentGroupingV3 === 'status' && groups && groups.length > 0) { // Find current group containing the task const currentGroup = groups.find(group => group.taskIds.includes(task.id)); // Find target group based on the new status ID let targetGroup = groups.find(group => group.id === statusId); // If not found by status ID, try matching with group value if (!targetGroup) { targetGroup = groups.find(group => group.groupValue === statusId); } if (currentGroup && targetGroup && currentGroup.id !== targetGroup.id) { // Move task between groups immediately for instant feedback dispatch( moveTaskBetweenGroups({ taskId: task.id, sourceGroupId: currentGroup.id, targetGroupId: targetGroup.id, }) ); } } // Emit socket event for server-side update and real-time sync socket?.emit( SocketEvents.TASK_STATUS_CHANGE.toString(), JSON.stringify({ task_id: task.id, status_id: statusId, parent_task: task.parent_task_id || null, team_id: projectId, }) ); socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.id); setIsOpen(false); }, [task, connected, socket, projectId, dispatch, currentGroupingV3, groups] ); // Calculate dropdown position and handle outside clicks useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (buttonRef.current && buttonRef.current.contains(event.target as Node)) { return; // Don't close if clicking the button } if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { setIsOpen(false); } }; if (isOpen && buttonRef.current) { // Calculate position with better handling of scrollable containers const rect = buttonRef.current.getBoundingClientRect(); const viewportHeight = window.innerHeight; const dropdownHeight = 200; // Estimated dropdown height // Check if dropdown would go below viewport const spaceBelow = viewportHeight - rect.bottom; const shouldShowAbove = spaceBelow < dropdownHeight && rect.top > dropdownHeight; setDropdownPosition({ top: shouldShowAbove ? rect.top - dropdownHeight - 4 : rect.bottom + 4, left: rect.left, }); document.addEventListener('mousedown', handleClickOutside); } return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [isOpen]); // Get status color - enhanced dark mode support const getStatusColor = useCallback( (status: any) => { if (isDarkMode) { return status?.color_code_dark || status?.color_code || '#4b5563'; } return status?.color_code || '#6b7280'; }, [isDarkMode] ); // Status display name - format status names by replacing underscores with spaces const getStatusDisplayName = useCallback((status: string) => { return status .replace(/_/g, ' ') // Replace underscores with spaces .replace(/\b\w/g, char => char.toUpperCase()); // Capitalize first letter of each word }, []); // Format status name for display const formatStatusName = useCallback((name: string) => { if (!name) return name; return name.replace(/_/g, ' '); // Replace underscores with spaces }, []); if (!task.status) return null; return ( <> {/* Status Button - Rounded Pill Design */} {/* Dropdown Menu - Redesigned */} {isOpen && createPortal(
{/* Status Options */}
{statusList.map((status, index) => { const isSelected = status.name?.toLowerCase() === task.status?.toLowerCase() || status.id === task.status; return ( ); })}
, document.body )} {/* CSS Animations - Injected as style tag */} {isOpen && createPortal( , document.head )} ); }; export default TaskStatusDropdown;