import React, { memo, useCallback, useState, useRef, useEffect } from 'react'; import { useSelector } from 'react-redux'; import { RootState } from '@/app/store'; import { IProjectTask } from '@/types/project/projectTasksViewModel.types'; import { useAppDispatch } from '@/hooks/useAppDispatch'; import { setSelectedTaskId, setShowTaskDrawer } from '@/features/task-drawer/task-drawer.slice'; import { useTranslation } from 'react-i18next'; import AvatarGroup from '@/components/AvatarGroup'; import LazyAssigneeSelectorWrapper from '@/components/task-management/lazy-assignee-selector'; import { format } from 'date-fns'; import logger from '@/utils/errorLogger'; import { createPortal } from 'react-dom'; import { useSocket } from '@/socket/socketContext'; import { SocketEvents } from '@/shared/socket-events'; import { getUserSession } from '@/utils/session-helper'; import { themeWiseColor } from '@/utils/themeWiseColor'; import { toggleTaskExpansion, fetchBoardSubTasks, deleteTask as deleteKanbanTask, updateEnhancedKanbanSubtask } from '@/features/enhanced-kanban/enhanced-kanban.slice'; import TaskProgressCircle from './TaskProgressCircle'; import { Button, Modal } from 'antd'; import { DeleteOutlined } from '@ant-design/icons'; import { tasksApiService } from '@/api/tasks/tasks.api.service'; // Simple Portal component const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => { const portalRoot = document.getElementById('portal-root') || document.body; return createPortal(children, portalRoot); }; interface TaskCardProps { 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; idx: number; onDragEnd: (e: React.DragEvent) => void; // <-- add this } function getDaysInMonth(year: number, month: number) { return new Date(year, month + 1, 0).getDate(); } function getFirstDayOfWeek(year: number, month: number) { return new Date(year, month, 1).getDay(); } const TaskCard: React.FC = memo(({ task, onTaskDragStart, onTaskDragOver, onTaskDrop, groupId, idx, onDragEnd // <-- add this }) => { const { socket } = useSocket(); const themeMode = useSelector((state: RootState) => state.themeReducer.mode); const { projectId } = useSelector((state: RootState) => state.projectReducer); const background = themeMode === 'dark' ? '#23272f' : '#fff'; const color = themeMode === 'dark' ? '#fff' : '#23272f'; const dispatch = useAppDispatch(); const { t } = useTranslation('kanban-board'); const [showDatePicker, setShowDatePicker] = useState(false); const [selectedDate, setSelectedDate] = useState( task.end_date ? new Date(task.end_date) : null ); const [isUpdating, setIsUpdating] = useState(false); const datePickerRef = useRef(null); const dateButtonRef = useRef(null); const [dropdownPosition, setDropdownPosition] = useState<{ top: number; left: number } | null>(null); const [calendarMonth, setCalendarMonth] = useState(() => { const d = selectedDate || new Date(); return new Date(d.getFullYear(), d.getMonth(), 1); }); const [contextMenu, setContextMenu] = useState<{ visible: boolean; x: number; y: number }>({ visible: false, x: 0, y: 0 }); const contextMenuRef = useRef(null); const [selectedTask, setSelectedTask] = useState(null); useEffect(() => { setSelectedDate(task.end_date ? new Date(task.end_date) : null); }, [task.end_date]); // Close date picker when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (datePickerRef.current && !datePickerRef.current.contains(event.target as Node)) { setShowDatePicker(false); } }; if (showDatePicker) { document.addEventListener('mousedown', handleClickOutside); } return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [showDatePicker]); useEffect(() => { if (showDatePicker && dateButtonRef.current) { const rect = dateButtonRef.current.getBoundingClientRect(); setDropdownPosition({ top: rect.bottom + window.scrollY, left: rect.left + window.scrollX, }); } }, [showDatePicker]); // Hide context menu on click elsewhere useEffect(() => { const handleClick = (e: MouseEvent) => { if (contextMenuRef.current && !contextMenuRef.current.contains(e.target as Node)) { setContextMenu({ ...contextMenu, visible: false }); } }; if (contextMenu.visible) { document.addEventListener('mousedown', handleClick); } return () => { document.removeEventListener('mousedown', handleClick); }; }, [contextMenu]); const handleCardClick = useCallback((e: React.MouseEvent, id: string) => { e.stopPropagation(); dispatch(setSelectedTaskId(id)); dispatch(setShowTaskDrawer(true)); }, [dispatch]); const handleDateClick = useCallback((e: React.MouseEvent) => { e.stopPropagation(); setShowDatePicker(true); }, []); const handleDateChange = useCallback( (date: Date | null) => { if (!task.id || !projectId) return; setIsUpdating(true); try { setSelectedDate(date); socket?.emit( SocketEvents.TASK_END_DATE_CHANGE.toString(), JSON.stringify({ task_id: task.id, end_date: date, parent_task: task.parent_task_id, time_zone: getUserSession()?.timezone_name ? getUserSession()?.timezone_name : Intl.DateTimeFormat().resolvedOptions().timeZone, }) ); } catch (error) { logger.error('Failed to update due date:', error); } finally { setIsUpdating(false); setShowDatePicker(false); } }, [task.id, projectId, socket] ); const handleClearDate = useCallback(() => { handleDateChange(null); }, [handleDateChange]); const handleToday = useCallback(() => { handleDateChange(new Date()); }, [handleDateChange]); const handleTomorrow = useCallback(() => { const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); handleDateChange(tomorrow); }, [handleDateChange]); const handleNextWeek = useCallback(() => { const nextWeek = new Date(); nextWeek.setDate(nextWeek.getDate() + 7); handleDateChange(nextWeek); }, [handleDateChange]); const handleSubTaskExpand = useCallback(() => { if (task && task.id && projectId) { if (task.sub_tasks && task.sub_tasks.length > 0 && task.sub_tasks_count && task.sub_tasks_count > 0) { dispatch(toggleTaskExpansion(task.id)); } else if (task.sub_tasks_count && task.sub_tasks_count > 0) { dispatch(toggleTaskExpansion(task.id)); dispatch(fetchBoardSubTasks({ taskId: task.id, projectId })); } else { dispatch(toggleTaskExpansion(task.id)); } } }, [task, projectId, dispatch]); const handleSubtaskButtonClick = useCallback((e: React.MouseEvent) => { e.stopPropagation(); handleSubTaskExpand(); }, [handleSubTaskExpand]); // Delete logic (similar to task-drawer-header) const handleDeleteTask = async (task: IProjectTask | null) => { if (!task || !task.id) return; Modal.confirm({ title: t('deleteTaskTitle'), content: t('deleteTaskContent'), okText: t('deleteTaskConfirm'), okType: 'danger', cancelText: t('deleteTaskCancel'), centered: true, onOk: async () => { if (!task.id) return; const res = await tasksApiService.deleteTask(task.id); if (res.done) { dispatch(setSelectedTaskId(null)); if (task.is_sub_task) { dispatch(updateEnhancedKanbanSubtask({ sectionId: '', subtask: { id: task.id , parent_task_id: task.parent_task_id || '', manual_progress: false }, mode: 'delete', })); } else { dispatch(deleteKanbanTask(task.id)); } dispatch(setShowTaskDrawer(false)); if (task.parent_task_id) { socket?.emit( SocketEvents.GET_TASK_PROGRESS.toString(), task.parent_task_id ); } } setContextMenu({ visible: false, x: 0, y: 0 }); setSelectedTask(null); }, onCancel: () => { setContextMenu({ visible: false, x: 0, y: 0 }); setSelectedTask(null); }, }); }; // Calendar rendering helpers const year = calendarMonth.getFullYear(); const month = calendarMonth.getMonth(); const daysInMonth = getDaysInMonth(year, month); const firstDayOfWeek = (getFirstDayOfWeek(year, month) + 6) % 7; // Make Monday first const today = new Date(); const weeks: (Date | null)[][] = []; let week: (Date | null)[] = Array(firstDayOfWeek).fill(null); for (let day = 1; day <= daysInMonth; day++) { week.push(new Date(year, month, day)); if (week.length === 7) { weeks.push(week); week = []; } } if (week.length > 0) { while (week.length < 7) week.push(null); weeks.push(week); } const [isDown, setIsDown] = useState(false); return ( <> {/* Context menu for delete */} {contextMenu.visible && (
)}
{/* Progress circle at top right */}
onTaskDragStart(e, task.id!, groupId)} onDragOver={e => { e.preventDefault(); const rect = e.currentTarget.getBoundingClientRect(); const offsetY = e.clientY - rect.top; const isDown = offsetY > rect.height / 2; setIsDown(isDown); onTaskDragOver(e, groupId, isDown ? idx + 1 : idx); }} onDrop={e => onTaskDrop(e, groupId, idx)} onDragEnd={onDragEnd} // <-- add this onClick={e => handleCardClick(e, task.id!)} onContextMenu={e => { e.preventDefault(); setContextMenu({ visible: true, x: e.clientX, y: e.clientY }); setSelectedTask(task); }} >
{task.labels?.map(label => (
{label.name}
))}
{task.name}
{isUpdating ? (
) : ( selectedDate ? format(selectedDate, 'MMM d, yyyy') : t('noDueDate') )}
{/* Custom Calendar Popup */} {showDatePicker && dropdownPosition && (
e.stopPropagation()} >
{calendarMonth.toLocaleString('default', { month: 'long' })} {year}
{['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map(d => (
{d}
))} {weeks.map((week, i) => ( {week.map((date, j) => { const isSelected = date && selectedDate && date.toDateString() === selectedDate.toDateString(); const isToday = date && date.toDateString() === today.toDateString(); return ( ); })} ))}
)}
{(task.sub_tasks_count ?? 0) > 0 && ( )}
{/* Loading state */} {task.sub_tasks_loading && (
)} {/* Loaded subtasks */} {!task.sub_tasks_loading && Array.isArray(task.sub_tasks) && task.sub_tasks.length > 0 && (
    {task.sub_tasks.map(sub => (
  • handleCardClick(e, sub.id!)} className="flex items-center gap-2 px-2 py-1 rounded hover:bg-gray-50 dark:hover:bg-gray-800" onContextMenu={e => { e.preventDefault(); setContextMenu({ visible: true, x: e.clientX, y: e.clientY }); setSelectedTask(sub); }}> {sub.priority_color || sub.priority_color_dark ? ( ) : null} {sub.name} {sub.end_date ? format(new Date(sub.end_date), 'MMM d, yyyy') : ''} {sub.names && sub.names.length > 0 && ( )}
  • ))}
)} {/* Empty state */} {!task.sub_tasks_loading && (!Array.isArray(task.sub_tasks) || task.sub_tasks.length === 0) && (
{t('noSubtasks', 'No subtasks')}
)}
); }); TaskCard.displayName = 'TaskCard'; export default TaskCard;