import React, { useMemo, useRef, useState, useCallback } from 'react'; import { GanttTask, ColumnConfig, SelectionState } from '../../types/advanced-gantt.types'; import { useAppSelector } from '../../hooks/useAppSelector'; import { themeWiseColor } from '../../utils/themeWiseColor'; import { ChevronRightIcon, ChevronDownIcon } from '@heroicons/react/24/outline'; import { CalendarIcon, UserIcon, FlagIcon } from '@heroicons/react/24/solid'; interface GanttGridProps { tasks: GanttTask[]; columns: ColumnConfig[]; rowHeight: number; containerHeight: number; selection: SelectionState; enableInlineEdit?: boolean; enableMultiSelect?: boolean; onTaskClick?: (task: GanttTask, event: React.MouseEvent) => void; onTaskDoubleClick?: (task: GanttTask) => void; onTaskExpand?: (taskId: string) => void; onSelectionChange?: (selection: SelectionState) => void; onColumnResize?: (columnField: string, newWidth: number) => void; onTaskUpdate?: (taskId: string, field: string, value: any) => void; className?: string; } const GanttGrid: React.FC = ({ tasks, columns, rowHeight, containerHeight, selection, enableInlineEdit = true, enableMultiSelect = true, onTaskClick, onTaskDoubleClick, onTaskExpand, onSelectionChange, onColumnResize, onTaskUpdate, className = '', }) => { const [editingCell, setEditingCell] = useState<{ taskId: string; field: string } | null>(null); const [columnWidths, setColumnWidths] = useState>( columns.reduce((acc, col) => ({ ...acc, [col.field]: col.width }), {}) ); const gridRef = useRef(null); const themeMode = useAppSelector(state => state.themeReducer.mode); // Theme-aware colors const colors = useMemo(() => ({ background: themeWiseColor('#ffffff', '#1f2937', themeMode), alternateRow: themeWiseColor('#f9fafb', '#374151', themeMode), border: themeWiseColor('#e5e7eb', '#4b5563', themeMode), text: themeWiseColor('#111827', '#f9fafb', themeMode), textSecondary: themeWiseColor('#6b7280', '#d1d5db', themeMode), selected: themeWiseColor('#eff6ff', '#1e3a8a', themeMode), hover: themeWiseColor('#f3f4f6', '#4b5563', themeMode), headerBg: themeWiseColor('#f8f9fa', '#374151', themeMode), }), [themeMode]); // Calculate total grid width const totalWidth = useMemo(() => { return columns.reduce((sum, col) => sum + columnWidths[col.field], 0); }, [columns, columnWidths]); // Handle column resize const handleColumnResize = useCallback((columnField: string, deltaX: number) => { const column = columns.find(col => col.field === columnField); if (!column) return; const currentWidth = columnWidths[columnField]; const newWidth = Math.max(column.minWidth || 60, Math.min(column.maxWidth || 400, currentWidth + deltaX)); setColumnWidths(prev => ({ ...prev, [columnField]: newWidth })); onColumnResize?.(columnField, newWidth); }, [columns, columnWidths, onColumnResize]); // Handle task selection const handleTaskSelection = useCallback((task: GanttTask, event: React.MouseEvent) => { const { ctrlKey, shiftKey } = event; let newSelectedTasks = [...selection.selectedTasks]; if (shiftKey && enableMultiSelect && selection.selectedTasks.length > 0) { // Range selection const lastSelectedIndex = tasks.findIndex(t => t.id === selection.selectedTasks[selection.selectedTasks.length - 1]); const currentIndex = tasks.findIndex(t => t.id === task.id); const [start, end] = [Math.min(lastSelectedIndex, currentIndex), Math.max(lastSelectedIndex, currentIndex)]; newSelectedTasks = tasks.slice(start, end + 1).map(t => t.id); } else if (ctrlKey && enableMultiSelect) { // Multi selection if (newSelectedTasks.includes(task.id)) { newSelectedTasks = newSelectedTasks.filter(id => id !== task.id); } else { newSelectedTasks.push(task.id); } } else { // Single selection newSelectedTasks = [task.id]; } onSelectionChange?.({ ...selection, selectedTasks: newSelectedTasks, focusedTask: task.id, }); onTaskClick?.(task, event); }, [tasks, selection, enableMultiSelect, onSelectionChange, onTaskClick]); // Handle cell editing const handleCellDoubleClick = useCallback((task: GanttTask, column: ColumnConfig) => { if (!enableInlineEdit || !column.editor) return; setEditingCell({ taskId: task.id, field: column.field }); }, [enableInlineEdit]); const handleCellEditComplete = useCallback((value: any) => { if (!editingCell) return; onTaskUpdate?.(editingCell.taskId, editingCell.field, value); setEditingCell(null); }, [editingCell, onTaskUpdate]); // Render cell content const renderCellContent = useCallback((task: GanttTask, column: ColumnConfig) => { const value = task[column.field as keyof GanttTask]; const isEditing = editingCell?.taskId === task.id && editingCell?.field === column.field; if (isEditing) { return renderCellEditor(value, column, handleCellEditComplete); } if (column.renderer) { return column.renderer(value, task); } // Default renderers switch (column.field) { case 'name': return (
{task.hasChildren && ( )}
{getTaskTypeIcon(task.type)} {task.name}
); case 'startDate': case 'endDate': return (
{(value as Date)?.toLocaleDateString() || '-'}
); case 'assignee': return task.assignee ? (
{task.assignee.avatar ? ( {task.assignee.name} ) : ( )} {task.assignee.name}
) : ( Unassigned ); case 'progress': return (
{task.progress}%
); case 'status': return ( {task.status.replace('-', ' ')} ); case 'priority': return (
{task.priority}
); case 'duration': const duration = task.duration || Math.ceil((task.endDate.getTime() - task.startDate.getTime()) / (1000 * 60 * 60 * 24)); return {duration}d; default: return {String(value || '')}; } }, [editingCell, onTaskExpand, handleCellEditComplete]); // Render header const renderHeader = () => (
{columns.map((column, index) => (
{column.title} {/* Resize handle */} {column.resizable && ( handleColumnResize(column.field, deltaX)} className="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-blue-500 opacity-0 group-hover:opacity-100" /> )}
))}
); // Render task rows const renderRows = () => (
{tasks.map((task, rowIndex) => { const isSelected = selection.selectedTasks.includes(task.id); const isFocused = selection.focusedTask === task.id; return (
handleTaskSelection(task, e)} onDoubleClick={() => onTaskDoubleClick?.(task)} > {columns.map((column) => (
handleCellDoubleClick(task, column)} > {renderCellContent(task, column)}
))}
); })}
); return (
{renderHeader()}
{renderRows()}
); }; // Resize handle component interface ResizeHandleProps { onResize: (deltaX: number) => void; className?: string; } const ResizeHandle: React.FC = ({ onResize, className }) => { const [isDragging, setIsDragging] = useState(false); const startXRef = useRef(0); const handleMouseDown = useCallback((e: React.MouseEvent) => { e.preventDefault(); setIsDragging(true); startXRef.current = e.clientX; const handleMouseMove = (moveEvent: MouseEvent) => { const deltaX = moveEvent.clientX - startXRef.current; onResize(deltaX); startXRef.current = moveEvent.clientX; }; const handleMouseUp = () => { setIsDragging(false); document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); }, [onResize]); return (
); }; // Cell editor component const renderCellEditor = (value: any, column: ColumnConfig, onComplete: (value: any) => void) => { const [editValue, setEditValue] = useState(value); const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { onComplete(editValue); } else if (e.key === 'Escape') { onComplete(value); // Cancel editing } }; const handleBlur = () => { onComplete(editValue); }; switch (column.editor) { case 'text': return ( setEditValue(e.target.value)} onKeyDown={handleKeyDown} onBlur={handleBlur} className="w-full px-1 py-0.5 border rounded text-sm" autoFocus /> ); case 'date': return ( setEditValue(new Date(e.target.value))} onKeyDown={handleKeyDown} onBlur={handleBlur} className="w-full px-1 py-0.5 border rounded text-sm" autoFocus /> ); case 'number': return ( setEditValue(parseFloat(e.target.value))} onKeyDown={handleKeyDown} onBlur={handleBlur} className="w-full px-1 py-0.5 border rounded text-sm" autoFocus /> ); case 'select': return ( ); default: return {String(value)}; } }; // Helper functions const getTaskTypeIcon = (type: GanttTask['type']) => { switch (type) { case 'project': return
; case 'milestone': return
; default: return
; } }; const getStatusColor = (status: GanttTask['status']) => { switch (status) { case 'completed': return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'; case 'in-progress': return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'; case 'overdue': return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'; case 'on-hold': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'; default: return 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200'; } }; const getPriorityColor = (priority: GanttTask['priority']) => { switch (priority) { case 'critical': return 'text-red-600'; case 'high': return 'text-orange-500'; case 'medium': return 'text-yellow-500'; case 'low': return 'text-green-500'; default: return 'text-gray-400'; } }; export default GanttGrid;