setHoverState('task')}
+ onMouseLeave={() => setHoverState(null)}
+ onMouseDown={(e) => handleMouseDown(e, 'move')}
+ >
+ {/* Progress bar */}
+
+
+ {/* Task content */}
+
+
+ {task.name}
+
+
+ {/* Duration display for smaller tasks */}
+ {taskPosition.width < 100 && (
+
+ {getDaysBetween(task.startDate, task.endDate)}d
+
+ )}
+
+
+ {/* Resize handles */}
+ {enableResize && !readOnly && hoverState === 'task' && (
+ <>
+ {/* Left resize handle */}
+
handleMouseDown(e, 'resize-start')}
+ onMouseEnter={() => setHoverState('resize-start')}
+ />
+
+ {/* Right resize handle */}
+
handleMouseDown(e, 'resize-end')}
+ onMouseEnter={() => setHoverState('resize-end')}
+ />
+ >
+ )}
+
+ {/* Progress handle */}
+ {enableProgressEdit && !readOnly && hoverState === 'task' && (
+
handleMouseDown(e, 'progress')}
+ onMouseEnter={() => setHoverState('progress')}
+ />
+ )}
+
+ {/* Task type indicator */}
+ {task.type === 'milestone' && (
+
+ )}
+
+ );
+ };
+
+ return renderTaskBar();
+};
+
+// Helper functions
+function getDefaultTaskColor(status: GanttTask['status']): string {
+ switch (status) {
+ case 'completed': return '#52c41a';
+ case 'in-progress': return '#1890ff';
+ case 'overdue': return '#ff4d4f';
+ case 'on-hold': return '#faad14';
+ default: return '#d9d9d9';
+ }
+}
+
+function darkenColor(color: string, amount: number): string {
+ // Simple color darkening - in a real app, use a proper color manipulation library
+ return color;
+}
+
+function lightenColor(color: string, amount: number): string {
+ // Simple color lightening - in a real app, use a proper color manipulation library
+ return color;
+}
+
+function adjustColorForDarkMode(color: string): string {
+ // Adjust color for dark mode - in a real app, use a proper color manipulation library
+ return color;
+}
+
+export default DraggableTaskBar;
\ No newline at end of file
diff --git a/worklenz-frontend/src/components/advanced-gantt/GanttGrid.tsx b/worklenz-frontend/src/components/advanced-gantt/GanttGrid.tsx
new file mode 100644
index 00000000..0d5aaed5
--- /dev/null
+++ b/worklenz-frontend/src/components/advanced-gantt/GanttGrid.tsx
@@ -0,0 +1,492 @@
+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 && (
+
{
+ e.stopPropagation();
+ onTaskExpand?.(task.id);
+ }}
+ className="p-0.5 hover:bg-gray-200 dark:hover:bg-gray-600 rounded"
+ >
+ {task.isExpanded ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+ {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}
+
+ ) : (
+ Unassigned
+ );
+
+ case 'progress':
+ return (
+
+ );
+
+ 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 (
+ setEditValue(e.target.value)}
+ onKeyDown={handleKeyDown}
+ onBlur={handleBlur}
+ className="w-full px-1 py-0.5 border rounded text-sm"
+ autoFocus
+ >
+ {column.editorOptions?.map((option: any) => (
+
+ {option.label}
+
+ ))}
+
+ );
+
+ 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;
\ No newline at end of file
diff --git a/worklenz-frontend/src/components/advanced-gantt/TimelineMarkers.tsx b/worklenz-frontend/src/components/advanced-gantt/TimelineMarkers.tsx
new file mode 100644
index 00000000..4dd3a9ad
--- /dev/null
+++ b/worklenz-frontend/src/components/advanced-gantt/TimelineMarkers.tsx
@@ -0,0 +1,295 @@
+import React, { useMemo } from 'react';
+import { Holiday, TimelineConfig } from '../../types/advanced-gantt.types';
+import { useAppSelector } from '../../hooks/useAppSelector';
+import { themeWiseColor } from '../../utils/themeWiseColor';
+import { useDateCalculations } from '../../utils/gantt-performance';
+
+interface TimelineMarkersProps {
+ startDate: Date;
+ endDate: Date;
+ dayWidth: number;
+ containerHeight: number;
+ timelineConfig: TimelineConfig;
+ holidays?: Holiday[];
+ showWeekends?: boolean;
+ showHolidays?: boolean;
+ showToday?: boolean;
+ className?: string;
+}
+
+const TimelineMarkers: React.FC = ({
+ startDate,
+ endDate,
+ dayWidth,
+ containerHeight,
+ timelineConfig,
+ holidays = [],
+ showWeekends = true,
+ showHolidays = true,
+ showToday = true,
+ className = '',
+}) => {
+ const themeMode = useAppSelector(state => state.themeReducer.mode);
+ const { getDaysBetween, isWeekend, isWorkingDay } = useDateCalculations();
+
+ // Generate all dates in the timeline
+ const timelineDates = useMemo(() => {
+ const dates: Date[] = [];
+ const totalDays = getDaysBetween(startDate, endDate);
+
+ for (let i = 0; i <= totalDays; i++) {
+ const date = new Date(startDate);
+ date.setDate(date.getDate() + i);
+ dates.push(date);
+ }
+
+ return dates;
+ }, [startDate, endDate, getDaysBetween]);
+
+ // Theme-aware colors
+ const colors = useMemo(() => ({
+ weekend: themeWiseColor('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)', themeMode),
+ nonWorkingDay: themeWiseColor('rgba(0, 0, 0, 0.03)', 'rgba(255, 255, 255, 0.03)', themeMode),
+ holiday: themeWiseColor('rgba(255, 107, 107, 0.1)', 'rgba(255, 107, 107, 0.15)', themeMode),
+ today: themeWiseColor('rgba(24, 144, 255, 0.15)', 'rgba(64, 169, 255, 0.2)', themeMode),
+ todayLine: themeWiseColor('#1890ff', '#40a9ff', themeMode),
+ holidayBorder: themeWiseColor('#ff6b6b', '#ff8787', themeMode),
+ }), [themeMode]);
+
+ // Check if a date is a holiday
+ const isHoliday = (date: Date): Holiday | undefined => {
+ return holidays.find(holiday => {
+ if (holiday.recurring) {
+ return holiday.date.getMonth() === date.getMonth() &&
+ holiday.date.getDate() === date.getDate();
+ }
+ return holiday.date.toDateString() === date.toDateString();
+ });
+ };
+
+ // Check if date is today
+ const isToday = (date: Date): boolean => {
+ const today = new Date();
+ return date.toDateString() === today.toDateString();
+ };
+
+ // Render weekend markers
+ const renderWeekendMarkers = () => {
+ if (!showWeekends) return null;
+
+ return timelineDates.map((date, index) => {
+ if (!isWeekend(date)) return null;
+
+ return (
+
+ );
+ });
+ };
+
+ // Render non-working day markers
+ const renderNonWorkingDayMarkers = () => {
+ return timelineDates.map((date, index) => {
+ if (isWorkingDay(date, timelineConfig.workingDays)) return null;
+
+ return (
+
+ );
+ });
+ };
+
+ // Render holiday markers
+ const renderHolidayMarkers = () => {
+ if (!showHolidays) return null;
+
+ return timelineDates.map((date, index) => {
+ const holiday = isHoliday(date);
+ if (!holiday) return null;
+
+ const holidayColor = holiday.color || colors.holiday;
+
+ return (
+
+ {/* Holiday tooltip */}
+
+
{holiday.name}
+
{date.toLocaleDateString()}
+
+
+
+ {/* Holiday icon */}
+
+
+ );
+ });
+ };
+
+ // Render today marker
+ const renderTodayMarker = () => {
+ if (!showToday) return null;
+
+ const todayIndex = timelineDates.findIndex(date => isToday(date));
+ if (todayIndex === -1) return null;
+
+ return (
+
+ {/* Today line */}
+
+
+ {/* Today label */}
+
+ Today
+
+
+ );
+ };
+
+ // Render time period markers (quarters, months, etc.)
+ const renderTimePeriodMarkers = () => {
+ const markers: React.ReactNode[] = [];
+ const currentDate = new Date(startDate);
+ currentDate.setDate(1); // Start of month
+
+ while (currentDate <= endDate) {
+ const daysSinceStart = getDaysBetween(startDate, currentDate);
+ const isQuarterStart = currentDate.getMonth() % 3 === 0 && currentDate.getDate() === 1;
+ const isYearStart = currentDate.getMonth() === 0 && currentDate.getDate() === 1;
+
+ if (isYearStart) {
+ markers.push(
+
+
+ {currentDate.getFullYear()}
+
+
+ );
+ } else if (isQuarterStart) {
+ markers.push(
+
+
+ Q{Math.floor(currentDate.getMonth() / 3) + 1}
+
+
+ );
+ }
+
+ // Move to next month
+ currentDate.setMonth(currentDate.getMonth() + 1);
+ }
+
+ return markers;
+ };
+
+ return (
+
+ {renderNonWorkingDayMarkers()}
+ {renderWeekendMarkers()}
+ {renderHolidayMarkers()}
+ {renderTodayMarker()}
+ {renderTimePeriodMarkers()}
+
+ );
+};
+
+// Holiday presets for common countries
+export const holidayPresets = {
+ US: [
+ { date: new Date(2024, 0, 1), name: "New Year's Day", type: 'national' as const, recurring: true },
+ { date: new Date(2024, 0, 15), name: "Martin Luther King Jr. Day", type: 'national' as const, recurring: true },
+ { date: new Date(2024, 1, 19), name: "Presidents' Day", type: 'national' as const, recurring: true },
+ { date: new Date(2024, 4, 27), name: "Memorial Day", type: 'national' as const, recurring: true },
+ { date: new Date(2024, 5, 19), name: "Juneteenth", type: 'national' as const, recurring: true },
+ { date: new Date(2024, 6, 4), name: "Independence Day", type: 'national' as const, recurring: true },
+ { date: new Date(2024, 8, 2), name: "Labor Day", type: 'national' as const, recurring: true },
+ { date: new Date(2024, 9, 14), name: "Columbus Day", type: 'national' as const, recurring: true },
+ { date: new Date(2024, 10, 11), name: "Veterans Day", type: 'national' as const, recurring: true },
+ { date: new Date(2024, 10, 28), name: "Thanksgiving", type: 'national' as const, recurring: true },
+ { date: new Date(2024, 11, 25), name: "Christmas Day", type: 'national' as const, recurring: true },
+ ],
+
+ UK: [
+ { date: new Date(2024, 0, 1), name: "New Year's Day", type: 'national' as const, recurring: true },
+ { date: new Date(2024, 2, 29), name: "Good Friday", type: 'religious' as const, recurring: false },
+ { date: new Date(2024, 3, 1), name: "Easter Monday", type: 'religious' as const, recurring: false },
+ { date: new Date(2024, 4, 6), name: "Early May Bank Holiday", type: 'national' as const, recurring: true },
+ { date: new Date(2024, 4, 27), name: "Spring Bank Holiday", type: 'national' as const, recurring: true },
+ { date: new Date(2024, 7, 26), name: "Summer Bank Holiday", type: 'national' as const, recurring: true },
+ { date: new Date(2024, 11, 25), name: "Christmas Day", type: 'religious' as const, recurring: true },
+ { date: new Date(2024, 11, 26), name: "Boxing Day", type: 'national' as const, recurring: true },
+ ],
+};
+
+// Working day presets
+export const workingDayPresets = {
+ standard: [1, 2, 3, 4, 5], // Monday to Friday
+ middle_east: [0, 1, 2, 3, 4], // Sunday to Thursday
+ six_day: [1, 2, 3, 4, 5, 6], // Monday to Saturday
+ four_day: [1, 2, 3, 4], // Monday to Thursday
+};
+
+export default TimelineMarkers;
\ No newline at end of file
diff --git a/worklenz-frontend/src/components/advanced-gantt/VirtualScrollContainer.tsx b/worklenz-frontend/src/components/advanced-gantt/VirtualScrollContainer.tsx
new file mode 100644
index 00000000..0e855bc3
--- /dev/null
+++ b/worklenz-frontend/src/components/advanced-gantt/VirtualScrollContainer.tsx
@@ -0,0 +1,372 @@
+import React, { useRef, useEffect, useState, useCallback, ReactNode } from 'react';
+import { useThrottle, usePerformanceMonitoring } from '../../utils/gantt-performance';
+import { useAppSelector } from '../../hooks/useAppSelector';
+
+interface VirtualScrollContainerProps {
+ items: any[];
+ itemHeight: number;
+ containerHeight: number;
+ containerWidth?: number;
+ overscan?: number;
+ horizontal?: boolean;
+ children: (item: any, index: number, style: React.CSSProperties) => ReactNode;
+ onScroll?: (scrollLeft: number, scrollTop: number) => void;
+ className?: string;
+ style?: React.CSSProperties;
+}
+
+const VirtualScrollContainer: React.FC = ({
+ items,
+ itemHeight,
+ containerHeight,
+ containerWidth = 0,
+ overscan = 5,
+ horizontal = false,
+ children,
+ onScroll,
+ className = '',
+ style = {},
+}) => {
+ const containerRef = useRef(null);
+ const [scrollTop, setScrollTop] = useState(0);
+ const [scrollLeft, setScrollLeft] = useState(0);
+ const { startMeasure, endMeasure, recordMetric } = usePerformanceMonitoring();
+ const themeMode = useAppSelector(state => state.themeReducer.mode);
+
+ // Calculate visible range
+ const totalHeight = items.length * itemHeight;
+ const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
+ const endIndex = Math.min(
+ items.length - 1,
+ Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan
+ );
+ const visibleItems = items.slice(startIndex, endIndex + 1);
+ const offsetY = startIndex * itemHeight;
+
+ // Throttled scroll handler
+ const throttledScrollHandler = useThrottle(
+ useCallback((event: Event) => {
+ const target = event.target as HTMLDivElement;
+ const newScrollTop = target.scrollTop;
+ const newScrollLeft = target.scrollLeft;
+
+ setScrollTop(newScrollTop);
+ setScrollLeft(newScrollLeft);
+ onScroll?.(newScrollLeft, newScrollTop);
+ }, [onScroll]),
+ 16 // ~60fps
+ );
+
+ useEffect(() => {
+ const container = containerRef.current;
+ if (!container) return;
+
+ container.addEventListener('scroll', throttledScrollHandler, { passive: true });
+
+ return () => {
+ container.removeEventListener('scroll', throttledScrollHandler);
+ };
+ }, [throttledScrollHandler]);
+
+ // Performance monitoring
+ useEffect(() => {
+ startMeasure('virtualScroll');
+ recordMetric('visibleTaskCount', visibleItems.length);
+ recordMetric('taskCount', items.length);
+ endMeasure('virtualScroll');
+ }, [visibleItems.length, items.length, startMeasure, endMeasure, recordMetric]);
+
+ const renderVisibleItems = () => {
+ return visibleItems.map((item, virtualIndex) => {
+ const actualIndex = startIndex + virtualIndex;
+ const itemStyle: React.CSSProperties = {
+ position: 'absolute',
+ top: horizontal ? 0 : actualIndex * itemHeight,
+ left: horizontal ? actualIndex * itemHeight : 0,
+ height: horizontal ? '100%' : itemHeight,
+ width: horizontal ? itemHeight : '100%',
+ transform: horizontal ? 'none' : `translateY(${offsetY}px)`,
+ };
+
+ return (
+
+ {children(item, actualIndex, itemStyle)}
+
+ );
+ });
+ };
+
+ return (
+
+ {/* Spacer to maintain scroll height */}
+
+ {/* Visible items container */}
+
+ {renderVisibleItems()}
+
+
+
+ );
+};
+
+// Grid virtual scrolling component for both rows and columns
+interface VirtualGridProps {
+ data: any[][];
+ rowHeight: number;
+ columnWidth: number | number[];
+ containerHeight: number;
+ containerWidth: number;
+ overscan?: number;
+ children: (item: any, rowIndex: number, colIndex: number, style: React.CSSProperties) => ReactNode;
+ onScroll?: (scrollLeft: number, scrollTop: number) => void;
+ className?: string;
+}
+
+export const VirtualGrid: React.FC = ({
+ data,
+ rowHeight,
+ columnWidth,
+ containerHeight,
+ containerWidth,
+ overscan = 3,
+ children,
+ onScroll,
+ className = '',
+}) => {
+ const containerRef = useRef(null);
+ const [scrollTop, setScrollTop] = useState(0);
+ const [scrollLeft, setScrollLeft] = useState(0);
+
+ const rowCount = data.length;
+ const colCount = data[0]?.length || 0;
+
+ // Calculate column positions for variable width columns
+ const columnWidths = Array.isArray(columnWidth) ? columnWidth : new Array(colCount).fill(columnWidth);
+ const columnPositions = columnWidths.reduce((acc, width, index) => {
+ acc[index] = index === 0 ? 0 : acc[index - 1] + columnWidths[index - 1];
+ return acc;
+ }, {} as Record);
+
+ const totalWidth = columnWidths.reduce((sum, width) => sum + width, 0);
+ const totalHeight = rowCount * rowHeight;
+
+ // Calculate visible ranges
+ const startRowIndex = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
+ const endRowIndex = Math.min(rowCount - 1, Math.ceil((scrollTop + containerHeight) / rowHeight) + overscan);
+
+ const startColIndex = Math.max(0, findColumnIndex(scrollLeft) - overscan);
+ const endColIndex = Math.min(colCount - 1, findColumnIndex(scrollLeft + containerWidth) + overscan);
+
+ function findColumnIndex(position: number): number {
+ for (let i = 0; i < colCount; i++) {
+ if (columnPositions[i] <= position && position < columnPositions[i] + columnWidths[i]) {
+ return i;
+ }
+ }
+ return colCount - 1;
+ }
+
+ const throttledScrollHandler = useThrottle(
+ useCallback((event: Event) => {
+ const target = event.target as HTMLDivElement;
+ const newScrollTop = target.scrollTop;
+ const newScrollLeft = target.scrollLeft;
+
+ setScrollTop(newScrollTop);
+ setScrollLeft(newScrollLeft);
+ onScroll?.(newScrollLeft, newScrollTop);
+ }, [onScroll]),
+ 16
+ );
+
+ useEffect(() => {
+ const container = containerRef.current;
+ if (!container) return;
+
+ container.addEventListener('scroll', throttledScrollHandler, { passive: true });
+
+ return () => {
+ container.removeEventListener('scroll', throttledScrollHandler);
+ };
+ }, [throttledScrollHandler]);
+
+ const renderVisibleCells = () => {
+ const cells: ReactNode[] = [];
+
+ for (let rowIndex = startRowIndex; rowIndex <= endRowIndex; rowIndex++) {
+ for (let colIndex = startColIndex; colIndex <= endColIndex; colIndex++) {
+ const item = data[rowIndex]?.[colIndex];
+ if (!item) continue;
+
+ const cellStyle: React.CSSProperties = {
+ position: 'absolute',
+ top: rowIndex * rowHeight,
+ left: columnPositions[colIndex],
+ height: rowHeight,
+ width: columnWidths[colIndex],
+ };
+
+ cells.push(
+
+ {children(item, rowIndex, colIndex, cellStyle)}
+
+ );
+ }
+ }
+
+ return cells;
+ };
+
+ return (
+
+
+ {renderVisibleCells()}
+
+
+ );
+};
+
+// Timeline virtual scrolling component
+interface VirtualTimelineProps {
+ startDate: Date;
+ endDate: Date;
+ dayWidth: number;
+ containerWidth: number;
+ containerHeight: number;
+ overscan?: number;
+ children: (date: Date, index: number, style: React.CSSProperties) => ReactNode;
+ onScroll?: (scrollLeft: number) => void;
+ className?: string;
+}
+
+export const VirtualTimeline: React.FC = ({
+ startDate,
+ endDate,
+ dayWidth,
+ containerWidth,
+ containerHeight,
+ overscan = 10,
+ children,
+ onScroll,
+ className = '',
+}) => {
+ const containerRef = useRef(null);
+ const [scrollLeft, setScrollLeft] = useState(0);
+
+ const totalDays = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
+ const totalWidth = totalDays * dayWidth;
+
+ const startDayIndex = Math.max(0, Math.floor(scrollLeft / dayWidth) - overscan);
+ const endDayIndex = Math.min(totalDays - 1, Math.ceil((scrollLeft + containerWidth) / dayWidth) + overscan);
+
+ const throttledScrollHandler = useThrottle(
+ useCallback((event: Event) => {
+ const target = event.target as HTMLDivElement;
+ const newScrollLeft = target.scrollLeft;
+ setScrollLeft(newScrollLeft);
+ onScroll?.(newScrollLeft);
+ }, [onScroll]),
+ 16
+ );
+
+ useEffect(() => {
+ const container = containerRef.current;
+ if (!container) return;
+
+ container.addEventListener('scroll', throttledScrollHandler, { passive: true });
+
+ return () => {
+ container.removeEventListener('scroll', throttledScrollHandler);
+ };
+ }, [throttledScrollHandler]);
+
+ const renderVisibleDays = () => {
+ const days: ReactNode[] = [];
+
+ for (let dayIndex = startDayIndex; dayIndex <= endDayIndex; dayIndex++) {
+ const date = new Date(startDate);
+ date.setDate(date.getDate() + dayIndex);
+
+ const dayStyle: React.CSSProperties = {
+ position: 'absolute',
+ left: dayIndex * dayWidth,
+ top: 0,
+ width: dayWidth,
+ height: '100%',
+ };
+
+ days.push(
+
+ {children(date, dayIndex, dayStyle)}
+
+ );
+ }
+
+ return days;
+ };
+
+ return (
+
+
+ {renderVisibleDays()}
+
+
+ );
+};
+
+export default VirtualScrollContainer;
\ No newline at end of file
diff --git a/worklenz-frontend/src/components/advanced-gantt/index.ts b/worklenz-frontend/src/components/advanced-gantt/index.ts
new file mode 100644
index 00000000..7d27b5bc
--- /dev/null
+++ b/worklenz-frontend/src/components/advanced-gantt/index.ts
@@ -0,0 +1,17 @@
+// Main Components
+export { default as AdvancedGanttChart } from './AdvancedGanttChart';
+export { default as AdvancedGanttDemo } from './AdvancedGanttDemo';
+
+// Core Components
+export { default as GanttGrid } from './GanttGrid';
+export { default as DraggableTaskBar } from './DraggableTaskBar';
+export { default as TimelineMarkers, holidayPresets, workingDayPresets } from './TimelineMarkers';
+
+// Utility Components
+export { default as VirtualScrollContainer, VirtualGrid, VirtualTimeline } from './VirtualScrollContainer';
+
+// Types
+export * from '../../types/advanced-gantt.types';
+
+// Performance Utilities
+export * from '../../utils/gantt-performance';
\ No newline at end of file
diff --git a/worklenz-frontend/src/components/project-roadmap-gantt/PhaseModal.tsx b/worklenz-frontend/src/components/project-roadmap-gantt/PhaseModal.tsx
new file mode 100644
index 00000000..c921d2f6
--- /dev/null
+++ b/worklenz-frontend/src/components/project-roadmap-gantt/PhaseModal.tsx
@@ -0,0 +1,406 @@
+import React, { useState } from 'react';
+import {
+ Modal,
+ Tabs,
+ Progress,
+ Tag,
+ List,
+ Avatar,
+ Badge,
+ Space,
+ Button,
+ Statistic,
+ Row,
+ Col,
+ Timeline,
+ Input,
+ Form,
+ DatePicker,
+ Select
+} from 'antd';
+import {
+ CalendarOutlined,
+ TeamOutlined,
+ CheckCircleOutlined,
+ ClockCircleOutlined,
+ FlagOutlined,
+ ExclamationCircleOutlined,
+ EditOutlined,
+ SaveOutlined,
+ CloseOutlined
+} from '@ant-design/icons';
+import { PhaseModalData, ProjectPhase, PhaseTask, PhaseMilestone } from '../../types/project-roadmap.types';
+import { useAppSelector } from '../../hooks/useAppSelector';
+import { themeWiseColor } from '../../utils/themeWiseColor';
+import dayjs from 'dayjs';
+
+const { TabPane } = Tabs;
+const { TextArea } = Input;
+
+interface PhaseModalProps {
+ visible: boolean;
+ phase: PhaseModalData | null;
+ onClose: () => void;
+ onUpdate?: (updates: Partial) => void;
+}
+
+const PhaseModal: React.FC = ({
+ visible,
+ phase,
+ onClose,
+ onUpdate,
+}) => {
+ const [isEditing, setIsEditing] = useState(false);
+ const [form] = Form.useForm();
+
+ // Theme support
+ const themeMode = useAppSelector(state => state.themeReducer.mode);
+ const isDarkMode = themeMode === 'dark';
+
+ if (!phase) return null;
+
+ const handleEdit = () => {
+ setIsEditing(true);
+ form.setFieldsValue({
+ name: phase.name,
+ description: phase.description,
+ startDate: dayjs(phase.startDate),
+ endDate: dayjs(phase.endDate),
+ status: phase.status,
+ });
+ };
+
+ const handleSave = async () => {
+ try {
+ const values = await form.validateFields();
+ const updates: Partial = {
+ name: values.name,
+ description: values.description,
+ startDate: values.startDate.toDate(),
+ endDate: values.endDate.toDate(),
+ status: values.status,
+ };
+
+ onUpdate?.(updates);
+ setIsEditing(false);
+ } catch (error) {
+ console.error('Validation failed:', error);
+ }
+ };
+
+ const handleCancel = () => {
+ setIsEditing(false);
+ form.resetFields();
+ };
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'completed': return 'success';
+ case 'in-progress': return 'processing';
+ case 'on-hold': return 'warning';
+ default: return 'default';
+ }
+ };
+
+ const getPriorityColor = (priority: string) => {
+ switch (priority) {
+ case 'high': return 'red';
+ case 'medium': return 'orange';
+ case 'low': return 'green';
+ default: return 'default';
+ }
+ };
+
+ const getTaskStatusIcon = (status: string) => {
+ switch (status) {
+ case 'done': return ;
+ case 'in-progress': return ;
+ default: return ;
+ }
+ };
+
+ return (
+
+
+
+ {isEditing ? (
+
+
+
+ ) : (
+
+ {phase.name}
+
+ )}
+
+
+ {isEditing ? (
+ <>
+ }
+ onClick={handleSave}
+ size="small"
+ >
+ Save
+
+ }
+ onClick={handleCancel}
+ size="small"
+ className="dark:border-gray-600 dark:text-gray-300"
+ >
+ Cancel
+
+ >
+ ) : (
+ }
+ onClick={handleEdit}
+ size="small"
+ className="dark:text-gray-300 dark:hover:bg-gray-700"
+ >
+ Edit
+
+ )}
+
+
+ }
+ open={visible}
+ onCancel={onClose}
+ width={800}
+ footer={null}
+ className="dark:bg-gray-800"
+ >
+
+ }
+ description={
+
+
{task.description}
+
+
+
+
+ {task.startDate.toLocaleDateString()} - {task.endDate.toLocaleDateString()}
+
+
+ {task.assigneeName && (
+
+
+ {task.assigneeName}
+
+ )}
+
+
+ }
+ />
+
+ )}
+ />
+
+
+
+
+ {phase.milestones.map((milestone: PhaseMilestone) => (
+ : }
+ >
+
+
+
{milestone.name}
+ {milestone.criticalPath && (
+
Critical Path
+ )}
+ {milestone.description && (
+
+ {milestone.description}
+
+ )}
+
+
+
+
+ {milestone.dueDate.toLocaleDateString()}
+
+
+
+
+
+ ))}
+
+
+
+
+ (
+
+ {member.charAt(0).toUpperCase()}}
+ title={member}
+ description={
+
+ {phase.tasks.filter(task => task.assigneeName === member).length} tasks assigned
+
+ }
+ />
+
+ )}
+ />
+
+
+
+
+ );
+};
+
+export default PhaseModal;
\ No newline at end of file
diff --git a/worklenz-frontend/src/components/project-roadmap-gantt/ProjectRoadmapGantt.tsx b/worklenz-frontend/src/components/project-roadmap-gantt/ProjectRoadmapGantt.tsx
new file mode 100644
index 00000000..be640bc3
--- /dev/null
+++ b/worklenz-frontend/src/components/project-roadmap-gantt/ProjectRoadmapGantt.tsx
@@ -0,0 +1,333 @@
+import React, { useState, useMemo } from 'react';
+import { Gantt, Task, ViewMode } from 'gantt-task-react';
+import { Button, Space, Badge } from 'antd';
+import { CalendarOutlined, TeamOutlined, CheckCircleOutlined } from '@ant-design/icons';
+import { ProjectPhase, ProjectRoadmap, GanttViewOptions, PhaseModalData } from '../../types/project-roadmap.types';
+import PhaseModal from './PhaseModal';
+import { useAppSelector } from '../../hooks/useAppSelector';
+import { themeWiseColor } from '../../utils/themeWiseColor';
+import 'gantt-task-react/dist/index.css';
+import './gantt-theme.css';
+
+interface ProjectRoadmapGanttProps {
+ roadmap: ProjectRoadmap;
+ viewOptions?: Partial
;
+ onPhaseUpdate?: (phaseId: string, updates: Partial) => void;
+ onTaskUpdate?: (phaseId: string, taskId: string, updates: any) => void;
+}
+
+const ProjectRoadmapGantt: React.FC = ({
+ roadmap,
+ viewOptions = {},
+ onPhaseUpdate,
+ onTaskUpdate,
+}) => {
+ const [selectedPhase, setSelectedPhase] = useState(null);
+ const [isModalVisible, setIsModalVisible] = useState(false);
+ const [viewMode, setViewMode] = useState(ViewMode.Month);
+
+ // Theme support
+ const themeMode = useAppSelector(state => state.themeReducer.mode);
+ const isDarkMode = themeMode === 'dark';
+
+ const defaultViewOptions: GanttViewOptions = {
+ viewMode: 'month',
+ showTasks: true,
+ showMilestones: true,
+ groupByPhase: true,
+ ...viewOptions,
+ };
+
+ // Theme-aware colors
+ const ganttColors = useMemo(() => {
+ return {
+ background: themeWiseColor('#ffffff', '#1f2937', themeMode),
+ surface: themeWiseColor('#f8f9fa', '#374151', themeMode),
+ border: themeWiseColor('#e5e7eb', '#4b5563', themeMode),
+ taskBar: themeWiseColor('#3b82f6', '#60a5fa', themeMode),
+ taskBarHover: themeWiseColor('#2563eb', '#93c5fd', themeMode),
+ progressBar: themeWiseColor('#10b981', '#34d399', themeMode),
+ milestone: themeWiseColor('#f59e0b', '#fbbf24', themeMode),
+ criticalPath: themeWiseColor('#ef4444', '#f87171', themeMode),
+ text: {
+ primary: themeWiseColor('#111827', '#f9fafb', themeMode),
+ secondary: themeWiseColor('#6b7280', '#d1d5db', themeMode),
+ },
+ grid: themeWiseColor('#f3f4f6', '#4b5563', themeMode),
+ today: themeWiseColor('rgba(59, 130, 246, 0.1)', 'rgba(96, 165, 250, 0.2)', themeMode),
+ };
+ }, [themeMode]);
+
+ // Convert phases to Gantt tasks
+ const ganttTasks = useMemo(() => {
+ const tasks: Task[] = [];
+
+ roadmap.phases.forEach((phase, phaseIndex) => {
+ // Add phase as main task with theme-aware colors
+ const phaseTask: Task = {
+ id: phase.id,
+ name: phase.name,
+ start: phase.startDate,
+ end: phase.endDate,
+ progress: phase.progress,
+ type: 'project',
+ styles: {
+ progressColor: themeWiseColor(phase.color, phase.color, themeMode),
+ progressSelectedColor: themeWiseColor(phase.color, phase.color, themeMode),
+ backgroundColor: themeWiseColor(`${phase.color}20`, `${phase.color}30`, themeMode),
+ },
+ };
+ tasks.push(phaseTask);
+
+ // Add phase tasks if enabled
+ if (defaultViewOptions.showTasks) {
+ phase.tasks.forEach((task) => {
+ const ganttTask: Task = {
+ id: task.id,
+ name: task.name,
+ start: task.startDate,
+ end: task.endDate,
+ progress: task.progress,
+ type: 'task',
+ project: phase.id,
+ dependencies: task.dependencies,
+ styles: {
+ progressColor: ganttColors.taskBar,
+ progressSelectedColor: ganttColors.taskBarHover,
+ backgroundColor: themeWiseColor('rgba(59, 130, 246, 0.1)', 'rgba(96, 165, 250, 0.2)', themeMode),
+ },
+ };
+ tasks.push(ganttTask);
+ });
+ }
+
+ // Add milestones if enabled
+ if (defaultViewOptions.showMilestones) {
+ phase.milestones.forEach((milestone) => {
+ const milestoneTask: Task = {
+ id: milestone.id,
+ name: milestone.name,
+ start: milestone.dueDate,
+ end: milestone.dueDate,
+ progress: milestone.isCompleted ? 100 : 0,
+ type: 'milestone',
+ project: phase.id,
+ styles: {
+ progressColor: milestone.criticalPath ? ganttColors.criticalPath : ganttColors.progressBar,
+ progressSelectedColor: milestone.criticalPath ? ganttColors.criticalPath : ganttColors.progressBar,
+ backgroundColor: milestone.criticalPath ?
+ themeWiseColor('rgba(239, 68, 68, 0.1)', 'rgba(248, 113, 113, 0.2)', themeMode) :
+ themeWiseColor('rgba(16, 185, 129, 0.1)', 'rgba(52, 211, 153, 0.2)', themeMode),
+ },
+ };
+ tasks.push(milestoneTask);
+ });
+ }
+ });
+
+ return tasks;
+ }, [roadmap.phases, defaultViewOptions, ganttColors, themeMode]);
+
+ const handlePhaseClick = (phase: ProjectPhase) => {
+ const taskCount = phase.tasks.length;
+ const completedTaskCount = phase.tasks.filter(task => task.status === 'done').length;
+ const milestoneCount = phase.milestones.length;
+ const completedMilestoneCount = phase.milestones.filter(m => m.isCompleted).length;
+ const teamMembers = [...new Set(phase.tasks.map(task => task.assigneeName).filter(Boolean))];
+
+ const phaseModalData: PhaseModalData = {
+ ...phase,
+ taskCount,
+ completedTaskCount,
+ milestoneCount,
+ completedMilestoneCount,
+ teamMembers,
+ };
+
+ setSelectedPhase(phaseModalData);
+ setIsModalVisible(true);
+ };
+
+ const handleTaskClick = (task: Task) => {
+ // Find the phase this task belongs to
+ const phase = roadmap.phases.find(p =>
+ p.tasks.some(t => t.id === task.id) || p.milestones.some(m => m.id === task.id)
+ );
+
+ if (phase) {
+ handlePhaseClick(phase);
+ }
+ };
+
+ const handleDateChange = (task: Task) => {
+ const phase = roadmap.phases.find(p => p.id === task.id);
+ if (phase && onPhaseUpdate) {
+ onPhaseUpdate(phase.id, {
+ startDate: task.start,
+ endDate: task.end,
+ });
+ } else if (onTaskUpdate) {
+ const parentPhase = roadmap.phases.find(p =>
+ p.tasks.some(t => t.id === task.id)
+ );
+ if (parentPhase) {
+ onTaskUpdate(parentPhase.id, task.id, {
+ startDate: task.start,
+ endDate: task.end,
+ });
+ }
+ }
+ };
+
+ const handleProgressChange = (task: Task) => {
+ const phase = roadmap.phases.find(p => p.id === task.id);
+ if (phase && onPhaseUpdate) {
+ onPhaseUpdate(phase.id, { progress: task.progress });
+ } else if (onTaskUpdate) {
+ const parentPhase = roadmap.phases.find(p =>
+ p.tasks.some(t => t.id === task.id)
+ );
+ if (parentPhase) {
+ onTaskUpdate(parentPhase.id, task.id, { progress: task.progress });
+ }
+ }
+ };
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'completed': return '#52c41a';
+ case 'in-progress': return '#1890ff';
+ case 'on-hold': return '#faad14';
+ default: return '#d9d9d9';
+ }
+ };
+
+ const columnWidth = viewMode === ViewMode.Year ? 350 :
+ viewMode === ViewMode.Month ? 300 :
+ viewMode === ViewMode.Week ? 250 : 60;
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+ {roadmap.name}
+
+ {roadmap.description && (
+
+ {roadmap.description}
+
+ )}
+
+
+ setViewMode(ViewMode.Week)}
+ className="dark:border-gray-600 dark:text-gray-300"
+ >
+ Week
+
+ setViewMode(ViewMode.Month)}
+ className="dark:border-gray-600 dark:text-gray-300"
+ >
+ Month
+
+ setViewMode(ViewMode.Year)}
+ className="dark:border-gray-600 dark:text-gray-300"
+ >
+ Year
+
+
+
+
+ {/* Phase Overview */}
+
+ {roadmap.phases.map((phase) => (
+
handlePhaseClick(phase)}
+ >
+
+
+ {phase.name}
+
+ }
+ />
+
+
+
+
+ {phase.startDate.toLocaleDateString()} - {phase.endDate.toLocaleDateString()}
+
+
+
+ {phase.tasks.length} tasks
+
+
+
+ {phase.progress}% complete
+
+
+
+ ))}
+
+
+
+
+ {/* Gantt Chart */}
+
+
+ {/* Phase Modal */}
+
setIsModalVisible(false)}
+ onUpdate={(updates) => {
+ if (selectedPhase && onPhaseUpdate) {
+ onPhaseUpdate(selectedPhase.id, updates);
+ }
+ }}
+ />
+
+ );
+};
+
+export default ProjectRoadmapGantt;
\ No newline at end of file
diff --git a/worklenz-frontend/src/components/project-roadmap-gantt/RoadmapDemo.tsx b/worklenz-frontend/src/components/project-roadmap-gantt/RoadmapDemo.tsx
new file mode 100644
index 00000000..7d5d31e3
--- /dev/null
+++ b/worklenz-frontend/src/components/project-roadmap-gantt/RoadmapDemo.tsx
@@ -0,0 +1,112 @@
+import React, { useState } from 'react';
+import { Button, Space, message } from 'antd';
+import ProjectRoadmapGantt from './ProjectRoadmapGantt';
+import { sampleProjectRoadmap } from './sample-data';
+import { ProjectPhase, ProjectRoadmap } from '../../types/project-roadmap.types';
+import { useAppSelector } from '../../hooks/useAppSelector';
+
+const RoadmapDemo: React.FC = () => {
+ const [roadmap, setRoadmap] = useState(sampleProjectRoadmap);
+ const themeMode = useAppSelector(state => state.themeReducer.mode);
+
+ const handlePhaseUpdate = (phaseId: string, updates: Partial) => {
+ setRoadmap(prevRoadmap => ({
+ ...prevRoadmap,
+ phases: prevRoadmap.phases.map(phase =>
+ phase.id === phaseId
+ ? { ...phase, ...updates }
+ : phase
+ )
+ }));
+
+ message.success('Phase updated successfully!');
+ };
+
+ const handleTaskUpdate = (phaseId: string, taskId: string, updates: any) => {
+ setRoadmap(prevRoadmap => ({
+ ...prevRoadmap,
+ phases: prevRoadmap.phases.map(phase =>
+ phase.id === phaseId
+ ? {
+ ...phase,
+ tasks: phase.tasks.map(task =>
+ task.id === taskId
+ ? { ...task, ...updates }
+ : task
+ )
+ }
+ : phase
+ )
+ }));
+
+ message.success('Task updated successfully!');
+ };
+
+ const resetToSampleData = () => {
+ setRoadmap(sampleProjectRoadmap);
+ message.info('Roadmap reset to sample data');
+ };
+
+ return (
+
+
+
+
+
+
+ Project Roadmap Gantt Chart Demo
+
+
+ Interactive Gantt chart showing project phases as milestones/epics.
+ Click on any phase card or Gantt bar to view detailed information in a modal.
+
+
+
+
+ Reset to Sample Data
+
+
+
+
+
+
+
+
+
+
+
+ Features Demonstrated:
+
+
+ • Phase-based Grouping: Projects organized by phases (Planning, Development, Testing, Deployment)
+ • Interactive Phase Cards: Click on phase cards for detailed view
+ • Gantt Chart Visualization: Timeline view with tasks, milestones, and dependencies
+ • Modal Details: Comprehensive phase information with tasks, milestones, and team members
+ • Progress Tracking: Visual progress indicators and completion statistics
+ • Multiple View Modes: Week, Month, and Year timeline views
+ • Task Management: Task assignments, priorities, and status tracking
+ • Milestone Tracking: Critical path milestones and completion status
+ • Team Overview: Team member assignments and workload distribution
+ • Editable Fields: In-modal editing for phase attributes (name, description, dates, status)
+ • Theme Support: Automatic light/dark theme adaptation with consistent styling
+
+
+
+
+ );
+};
+
+export default RoadmapDemo;
\ No newline at end of file
diff --git a/worklenz-frontend/src/components/project-roadmap-gantt/gantt-theme.css b/worklenz-frontend/src/components/project-roadmap-gantt/gantt-theme.css
new file mode 100644
index 00000000..86b85428
--- /dev/null
+++ b/worklenz-frontend/src/components/project-roadmap-gantt/gantt-theme.css
@@ -0,0 +1,221 @@
+/* Custom Gantt Chart Theme Overrides */
+
+/* Light theme styles */
+.gantt-container {
+ font-family: inherit;
+}
+
+.gantt-container .gantt-header {
+ background-color: var(--gantt-background, #ffffff);
+ border-bottom: 1px solid var(--gantt-border, #e5e7eb);
+ color: var(--gantt-text, #111827);
+}
+
+.gantt-container .gantt-task-list-wrapper {
+ background-color: var(--gantt-background, #ffffff);
+ border-right: 1px solid var(--gantt-border, #e5e7eb);
+}
+
+.gantt-container .gantt-task-list-table {
+ background-color: var(--gantt-background, #ffffff);
+}
+
+.gantt-container .gantt-task-list-table-row {
+ color: var(--gantt-text, #111827);
+ border-bottom: 1px solid var(--gantt-grid, #f3f4f6);
+}
+
+.gantt-container .gantt-task-list-table-row:hover {
+ background-color: var(--gantt-grid, #f3f4f6);
+}
+
+.gantt-container .gantt-gantt {
+ background-color: var(--gantt-background, #ffffff);
+}
+
+.gantt-container .gantt-vertical-container {
+ background-color: var(--gantt-background, #ffffff);
+}
+
+.gantt-container .gantt-horizontal-container {
+ background-color: var(--gantt-background, #ffffff);
+}
+
+.gantt-container .gantt-calendar {
+ background-color: var(--gantt-background, #ffffff);
+ border-bottom: 1px solid var(--gantt-border, #e5e7eb);
+}
+
+.gantt-container .gantt-calendar-row {
+ color: var(--gantt-text, #111827);
+ background-color: var(--gantt-background, #ffffff);
+ border-bottom: 1px solid var(--gantt-grid, #f3f4f6);
+}
+
+.gantt-container .gantt-grid-body {
+ background-color: var(--gantt-background, #ffffff);
+}
+
+.gantt-container .gantt-grid-row {
+ border-bottom: 1px solid var(--gantt-grid, #f3f4f6);
+}
+
+.gantt-container .gantt-grid-row-line {
+ border-left: 1px solid var(--gantt-grid, #f3f4f6);
+}
+
+/* Dark theme overrides */
+html.dark .gantt-container .gantt-header {
+ background-color: #1f2937;
+ border-bottom-color: #4b5563;
+ color: #f9fafb;
+}
+
+html.dark .gantt-container .gantt-task-list-wrapper {
+ background-color: #1f2937;
+ border-right-color: #4b5563;
+}
+
+html.dark .gantt-container .gantt-task-list-table {
+ background-color: #1f2937;
+}
+
+html.dark .gantt-container .gantt-task-list-table-row {
+ color: #f9fafb;
+ border-bottom-color: #4b5563;
+}
+
+html.dark .gantt-container .gantt-task-list-table-row:hover {
+ background-color: #374151;
+}
+
+html.dark .gantt-container .gantt-gantt {
+ background-color: #1f2937;
+}
+
+html.dark .gantt-container .gantt-vertical-container {
+ background-color: #1f2937;
+}
+
+html.dark .gantt-container .gantt-horizontal-container {
+ background-color: #1f2937;
+}
+
+html.dark .gantt-container .gantt-calendar {
+ background-color: #1f2937;
+ border-bottom-color: #4b5563;
+}
+
+html.dark .gantt-container .gantt-calendar-row {
+ color: #f9fafb;
+ background-color: #1f2937;
+ border-bottom-color: #4b5563;
+}
+
+html.dark .gantt-container .gantt-grid-body {
+ background-color: #1f2937;
+}
+
+html.dark .gantt-container .gantt-grid-row {
+ border-bottom-color: #4b5563;
+}
+
+html.dark .gantt-container .gantt-grid-row-line {
+ border-left-color: #4b5563;
+}
+
+/* Tooltip theming */
+.gantt-container .gantt-tooltip {
+ background-color: var(--gantt-background, #ffffff);
+ border: 1px solid var(--gantt-border, #e5e7eb);
+ color: var(--gantt-text, #111827);
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
+}
+
+html.dark .gantt-container .gantt-tooltip {
+ background-color: #374151;
+ border-color: #4b5563;
+ color: #f9fafb;
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
+}
+
+/* Task bar styling improvements */
+.gantt-container .gantt-task-progress {
+ border-radius: 2px;
+}
+
+.gantt-container .gantt-task-background {
+ border-radius: 4px;
+ border: 1px solid var(--gantt-border, #e5e7eb);
+}
+
+html.dark .gantt-container .gantt-task-background {
+ border-color: #4b5563;
+}
+
+/* Milestone styling */
+.gantt-container .gantt-milestone {
+ filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
+}
+
+html.dark .gantt-container .gantt-milestone {
+ filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3));
+}
+
+/* Selection and hover states */
+.gantt-container .gantt-task:hover .gantt-task-background {
+ opacity: 0.9;
+ transform: translateY(-1px);
+ transition: all 0.2s ease;
+}
+
+.gantt-container .gantt-task-selected .gantt-task-background {
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3);
+}
+
+html.dark .gantt-container .gantt-task-selected .gantt-task-background {
+ box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.3);
+}
+
+/* Responsive improvements */
+@media (max-width: 768px) {
+ .gantt-container {
+ font-size: 14px;
+ }
+
+ .gantt-container .gantt-task-list-wrapper {
+ min-width: 200px;
+ }
+}
+
+/* Scrollbar theming */
+.gantt-container ::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+.gantt-container ::-webkit-scrollbar-track {
+ background: var(--gantt-grid, #f3f4f6);
+ border-radius: 4px;
+}
+
+.gantt-container ::-webkit-scrollbar-thumb {
+ background: var(--gantt-border, #e5e7eb);
+ border-radius: 4px;
+}
+
+.gantt-container ::-webkit-scrollbar-thumb:hover {
+ background: #9ca3af;
+}
+
+html.dark .gantt-container ::-webkit-scrollbar-track {
+ background: #4b5563;
+}
+
+html.dark .gantt-container ::-webkit-scrollbar-thumb {
+ background: #6b7280;
+}
+
+html.dark .gantt-container ::-webkit-scrollbar-thumb:hover {
+ background: #9ca3af;
+}
\ No newline at end of file
diff --git a/worklenz-frontend/src/components/project-roadmap-gantt/index.ts b/worklenz-frontend/src/components/project-roadmap-gantt/index.ts
new file mode 100644
index 00000000..05cd1886
--- /dev/null
+++ b/worklenz-frontend/src/components/project-roadmap-gantt/index.ts
@@ -0,0 +1,5 @@
+export { default as ProjectRoadmapGantt } from './ProjectRoadmapGantt';
+export { default as PhaseModal } from './PhaseModal';
+export { default as RoadmapDemo } from './RoadmapDemo';
+export { sampleProjectRoadmap } from './sample-data';
+export * from '../../types/project-roadmap.types';
\ No newline at end of file
diff --git a/worklenz-frontend/src/components/project-roadmap-gantt/sample-data.ts b/worklenz-frontend/src/components/project-roadmap-gantt/sample-data.ts
new file mode 100644
index 00000000..f3c7b91f
--- /dev/null
+++ b/worklenz-frontend/src/components/project-roadmap-gantt/sample-data.ts
@@ -0,0 +1,317 @@
+import { ProjectRoadmap, ProjectPhase, PhaseTask, PhaseMilestone } from '../../types/project-roadmap.types';
+
+// Sample tasks for Planning Phase
+const planningTasks: PhaseTask[] = [
+ {
+ id: 'task-planning-1',
+ name: 'Project Requirements Analysis',
+ description: 'Gather and analyze project requirements from stakeholders',
+ startDate: new Date(2024, 11, 1), // December 1, 2024
+ endDate: new Date(2024, 11, 5),
+ progress: 100,
+ assigneeId: 'user-1',
+ assigneeName: 'Alice Johnson',
+ priority: 'high',
+ status: 'done',
+ },
+ {
+ id: 'task-planning-2',
+ name: 'Technical Architecture Design',
+ description: 'Design the technical architecture and system components',
+ startDate: new Date(2024, 11, 6),
+ endDate: new Date(2024, 11, 12),
+ progress: 75,
+ assigneeId: 'user-2',
+ assigneeName: 'Bob Smith',
+ priority: 'high',
+ status: 'in-progress',
+ },
+ {
+ id: 'task-planning-3',
+ name: 'Resource Allocation Planning',
+ description: 'Plan and allocate team resources for the project',
+ startDate: new Date(2024, 11, 8),
+ endDate: new Date(2024, 11, 15),
+ progress: 50,
+ assigneeId: 'user-3',
+ assigneeName: 'Carol Davis',
+ priority: 'medium',
+ status: 'in-progress',
+ dependencies: ['task-planning-1'],
+ }
+];
+
+// Sample milestones for Planning Phase
+const planningMilestones: PhaseMilestone[] = [
+ {
+ id: 'milestone-planning-1',
+ name: 'Requirements Approved',
+ description: 'All project requirements have been reviewed and approved by stakeholders',
+ dueDate: new Date(2024, 11, 5),
+ isCompleted: true,
+ criticalPath: true,
+ },
+ {
+ id: 'milestone-planning-2',
+ name: 'Architecture Review Complete',
+ description: 'Technical architecture has been reviewed and approved',
+ dueDate: new Date(2024, 11, 15),
+ isCompleted: false,
+ criticalPath: true,
+ }
+];
+
+// Sample tasks for Development Phase
+const developmentTasks: PhaseTask[] = [
+ {
+ id: 'task-dev-1',
+ name: 'Frontend Component Development',
+ description: 'Develop core frontend components using React',
+ startDate: new Date(2024, 11, 16),
+ endDate: new Date(2025, 0, 31), // January 31, 2025
+ progress: 30,
+ assigneeId: 'user-4',
+ assigneeName: 'David Wilson',
+ priority: 'high',
+ status: 'in-progress',
+ dependencies: ['task-planning-2'],
+ },
+ {
+ id: 'task-dev-2',
+ name: 'Backend API Development',
+ description: 'Develop REST APIs and database models',
+ startDate: new Date(2024, 11, 16),
+ endDate: new Date(2025, 0, 25),
+ progress: 45,
+ assigneeId: 'user-5',
+ assigneeName: 'Eva Brown',
+ priority: 'high',
+ status: 'in-progress',
+ dependencies: ['task-planning-2'],
+ },
+ {
+ id: 'task-dev-3',
+ name: 'Database Setup and Migration',
+ description: 'Set up production database and create migration scripts',
+ startDate: new Date(2024, 11, 20),
+ endDate: new Date(2025, 0, 15),
+ progress: 80,
+ assigneeId: 'user-6',
+ assigneeName: 'Frank Miller',
+ priority: 'medium',
+ status: 'in-progress',
+ }
+];
+
+// Sample milestones for Development Phase
+const developmentMilestones: PhaseMilestone[] = [
+ {
+ id: 'milestone-dev-1',
+ name: 'Core Components Complete',
+ description: 'All core frontend components have been developed and tested',
+ dueDate: new Date(2025, 0, 20),
+ isCompleted: false,
+ criticalPath: false,
+ },
+ {
+ id: 'milestone-dev-2',
+ name: 'API Development Complete',
+ description: 'All backend APIs are developed and documented',
+ dueDate: new Date(2025, 0, 25),
+ isCompleted: false,
+ criticalPath: true,
+ }
+];
+
+// Sample tasks for Testing Phase
+const testingTasks: PhaseTask[] = [
+ {
+ id: 'task-test-1',
+ name: 'Unit Testing Implementation',
+ description: 'Write and execute comprehensive unit tests',
+ startDate: new Date(2025, 1, 1), // February 1, 2025
+ endDate: new Date(2025, 1, 15),
+ progress: 0,
+ assigneeId: 'user-7',
+ assigneeName: 'Grace Lee',
+ priority: 'high',
+ status: 'todo',
+ dependencies: ['task-dev-1', 'task-dev-2'],
+ },
+ {
+ id: 'task-test-2',
+ name: 'Integration Testing',
+ description: 'Perform integration testing between frontend and backend',
+ startDate: new Date(2025, 1, 10),
+ endDate: new Date(2025, 1, 25),
+ progress: 0,
+ assigneeId: 'user-8',
+ assigneeName: 'Henry Clark',
+ priority: 'high',
+ status: 'todo',
+ dependencies: ['task-test-1'],
+ },
+ {
+ id: 'task-test-3',
+ name: 'User Acceptance Testing',
+ description: 'Conduct user acceptance testing with stakeholders',
+ startDate: new Date(2025, 1, 20),
+ endDate: new Date(2025, 2, 5), // March 5, 2025
+ progress: 0,
+ assigneeId: 'user-9',
+ assigneeName: 'Ivy Taylor',
+ priority: 'medium',
+ status: 'todo',
+ dependencies: ['task-test-2'],
+ }
+];
+
+// Sample milestones for Testing Phase
+const testingMilestones: PhaseMilestone[] = [
+ {
+ id: 'milestone-test-1',
+ name: 'All Tests Passing',
+ description: 'All unit and integration tests are passing',
+ dueDate: new Date(2025, 1, 25),
+ isCompleted: false,
+ criticalPath: true,
+ },
+ {
+ id: 'milestone-test-2',
+ name: 'UAT Sign-off',
+ description: 'User acceptance testing completed and signed off',
+ dueDate: new Date(2025, 2, 5),
+ isCompleted: false,
+ criticalPath: true,
+ }
+];
+
+// Sample tasks for Deployment Phase
+const deploymentTasks: PhaseTask[] = [
+ {
+ id: 'task-deploy-1',
+ name: 'Production Environment Setup',
+ description: 'Configure and set up production environment',
+ startDate: new Date(2025, 2, 6), // March 6, 2025
+ endDate: new Date(2025, 2, 12),
+ progress: 0,
+ assigneeId: 'user-10',
+ assigneeName: 'Jack Anderson',
+ priority: 'high',
+ status: 'todo',
+ dependencies: ['task-test-3'],
+ },
+ {
+ id: 'task-deploy-2',
+ name: 'Application Deployment',
+ description: 'Deploy application to production environment',
+ startDate: new Date(2025, 2, 13),
+ endDate: new Date(2025, 2, 15),
+ progress: 0,
+ assigneeId: 'user-11',
+ assigneeName: 'Kelly White',
+ priority: 'high',
+ status: 'todo',
+ dependencies: ['task-deploy-1'],
+ },
+ {
+ id: 'task-deploy-3',
+ name: 'Post-Deployment Monitoring',
+ description: 'Monitor application performance post-deployment',
+ startDate: new Date(2025, 2, 16),
+ endDate: new Date(2025, 2, 20),
+ progress: 0,
+ assigneeId: 'user-12',
+ assigneeName: 'Liam Garcia',
+ priority: 'medium',
+ status: 'todo',
+ dependencies: ['task-deploy-2'],
+ }
+];
+
+// Sample milestones for Deployment Phase
+const deploymentMilestones: PhaseMilestone[] = [
+ {
+ id: 'milestone-deploy-1',
+ name: 'Production Go-Live',
+ description: 'Application is live in production environment',
+ dueDate: new Date(2025, 2, 15),
+ isCompleted: false,
+ criticalPath: true,
+ },
+ {
+ id: 'milestone-deploy-2',
+ name: 'Project Handover',
+ description: 'Project handed over to maintenance team',
+ dueDate: new Date(2025, 2, 20),
+ isCompleted: false,
+ criticalPath: false,
+ }
+];
+
+// Sample project phases
+const samplePhases: ProjectPhase[] = [
+ {
+ id: 'phase-planning',
+ name: 'Planning & Analysis',
+ description: 'Initial project planning, requirements gathering, and technical analysis',
+ startDate: new Date(2024, 11, 1),
+ endDate: new Date(2024, 11, 15),
+ progress: 75,
+ color: '#1890ff',
+ status: 'in-progress',
+ tasks: planningTasks,
+ milestones: planningMilestones,
+ },
+ {
+ id: 'phase-development',
+ name: 'Development',
+ description: 'Core development of frontend, backend, and database components',
+ startDate: new Date(2024, 11, 16),
+ endDate: new Date(2025, 0, 31),
+ progress: 40,
+ color: '#52c41a',
+ status: 'in-progress',
+ tasks: developmentTasks,
+ milestones: developmentMilestones,
+ },
+ {
+ id: 'phase-testing',
+ name: 'Testing & QA',
+ description: 'Comprehensive testing including unit, integration, and user acceptance testing',
+ startDate: new Date(2025, 1, 1),
+ endDate: new Date(2025, 2, 5),
+ progress: 0,
+ color: '#faad14',
+ status: 'not-started',
+ tasks: testingTasks,
+ milestones: testingMilestones,
+ },
+ {
+ id: 'phase-deployment',
+ name: 'Deployment & Launch',
+ description: 'Production deployment and project launch activities',
+ startDate: new Date(2025, 2, 6),
+ endDate: new Date(2025, 2, 20),
+ progress: 0,
+ color: '#722ed1',
+ status: 'not-started',
+ tasks: deploymentTasks,
+ milestones: deploymentMilestones,
+ }
+];
+
+// Sample project roadmap
+export const sampleProjectRoadmap: ProjectRoadmap = {
+ id: 'roadmap-sample-project',
+ projectId: 'project-web-platform',
+ name: 'Web Platform Development Roadmap',
+ description: 'Comprehensive roadmap for developing a new web-based platform with modern technologies and agile methodologies',
+ startDate: new Date(2024, 11, 1),
+ endDate: new Date(2025, 2, 20),
+ phases: samplePhases,
+ createdAt: new Date(2024, 10, 15),
+ updatedAt: new Date(2024, 11, 1),
+};
+
+export default sampleProjectRoadmap;
\ No newline at end of file
diff --git a/worklenz-frontend/src/pages/GanttDemoPage.tsx b/worklenz-frontend/src/pages/GanttDemoPage.tsx
new file mode 100644
index 00000000..28cadf70
--- /dev/null
+++ b/worklenz-frontend/src/pages/GanttDemoPage.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import { AdvancedGanttDemo } from '../components/advanced-gantt';
+
+const GanttDemoPage: React.FC = () => {
+ return ;
+};
+
+export default GanttDemoPage;
\ No newline at end of file
diff --git a/worklenz-frontend/src/pages/projects/ProjectGanttView.tsx b/worklenz-frontend/src/pages/projects/ProjectGanttView.tsx
new file mode 100644
index 00000000..0835c2f9
--- /dev/null
+++ b/worklenz-frontend/src/pages/projects/ProjectGanttView.tsx
@@ -0,0 +1,63 @@
+import React, { useMemo } from 'react';
+import { useParams } from 'react-router-dom';
+import { AdvancedGanttChart } from '../../components/advanced-gantt';
+import { useAppSelector } from '../../hooks/useAppSelector';
+import { GanttTask } from '../../types/advanced-gantt.types';
+
+const ProjectGanttView: React.FC = () => {
+ const { projectId } = useParams<{ projectId: string }>();
+
+ // Get tasks from your Redux store (adjust based on your actual state structure)
+ const tasks = useAppSelector(state => state.tasksReducer?.tasks || []);
+
+ // Transform your tasks to GanttTask format
+ const ganttTasks = useMemo((): GanttTask[] => {
+ return tasks.map(task => ({
+ id: task.id,
+ name: task.name,
+ startDate: task.start_date ? new Date(task.start_date) : new Date(),
+ endDate: task.end_date ? new Date(task.end_date) : new Date(),
+ progress: task.progress || 0,
+ type: 'task',
+ status: task.status || 'not-started',
+ priority: task.priority || 'medium',
+ assignee: task.assignee ? {
+ id: task.assignee.id,
+ name: task.assignee.name,
+ avatar: task.assignee.avatar,
+ } : undefined,
+ parent: task.parent_task_id,
+ level: task.level || 0,
+ // Map other fields as needed
+ }));
+ }, [tasks]);
+
+ const handleTaskUpdate = (taskId: string, updates: Partial) => {
+ // Implement your task update logic here
+ console.log('Update task:', taskId, updates);
+ // Dispatch Redux action to update task
+ };
+
+ const handleTaskMove = (taskId: string, newDates: { start: Date; end: Date }) => {
+ // Implement your task move logic here
+ console.log('Move task:', taskId, newDates);
+ // Dispatch Redux action to update task dates
+ };
+
+ return (
+
+ );
+};
+
+export default ProjectGanttView;
\ No newline at end of file
diff --git a/worklenz-frontend/src/types/advanced-gantt.types.ts b/worklenz-frontend/src/types/advanced-gantt.types.ts
new file mode 100644
index 00000000..e080df40
--- /dev/null
+++ b/worklenz-frontend/src/types/advanced-gantt.types.ts
@@ -0,0 +1,307 @@
+import { ReactNode } from 'react';
+
+// Core Task Interface
+export interface GanttTask {
+ id: string;
+ name: string;
+ startDate: Date;
+ endDate: Date;
+ progress: number;
+ duration?: number; // in days
+ parent?: string;
+ type: 'task' | 'milestone' | 'project';
+ status: 'not-started' | 'in-progress' | 'completed' | 'on-hold' | 'overdue';
+ priority: 'low' | 'medium' | 'high' | 'critical';
+ assignee?: {
+ id: string;
+ name: string;
+ avatar?: string;
+ };
+ dependencies?: string[];
+ description?: string;
+ tags?: string[];
+ color?: string;
+ isCollapsed?: boolean;
+ level?: number; // for hierarchical display
+ hasChildren?: boolean;
+ isExpanded?: boolean;
+}
+
+// Column Configuration
+export interface ColumnConfig {
+ field: keyof GanttTask | string;
+ title: string;
+ width: number;
+ minWidth?: number;
+ maxWidth?: number;
+ resizable: boolean;
+ sortable: boolean;
+ fixed: boolean;
+ align?: 'left' | 'center' | 'right';
+ renderer?: (value: any, task: GanttTask) => ReactNode;
+ editor?: 'text' | 'date' | 'select' | 'number' | 'progress';
+ editorOptions?: any;
+}
+
+// Timeline Configuration
+export interface TimelineConfig {
+ topTier: {
+ unit: 'year' | 'month' | 'week' | 'day';
+ format: string;
+ height?: number;
+ };
+ bottomTier: {
+ unit: 'month' | 'week' | 'day' | 'hour';
+ format: string;
+ height?: number;
+ };
+ showWeekends: boolean;
+ showNonWorkingDays: boolean;
+ holidays: Holiday[];
+ workingDays: number[]; // 0-6, Sunday-Saturday
+ workingHours: {
+ start: number; // 0-23
+ end: number; // 0-23
+ };
+ minDate?: Date;
+ maxDate?: Date;
+ dayWidth: number; // pixels per day
+}
+
+// Holiday Interface
+export interface Holiday {
+ date: Date;
+ name: string;
+ type: 'national' | 'company' | 'religious' | 'custom';
+ recurring?: boolean;
+ color?: string;
+}
+
+// Virtual Scrolling Configuration
+export interface VirtualScrollConfig {
+ enableRowVirtualization: boolean;
+ enableTimelineVirtualization: boolean;
+ bufferSize: number;
+ itemHeight: number;
+ overscan?: number;
+}
+
+// Drag and Drop State
+export interface DragState {
+ isDragging: boolean;
+ dragType: 'move' | 'resize-start' | 'resize-end' | 'progress' | 'link';
+ taskId: string;
+ initialPosition: { x: number; y: number };
+ currentPosition?: { x: number; y: number };
+ initialDates: { start: Date; end: Date };
+ initialProgress?: number;
+ snapToGrid?: boolean;
+ constraints?: {
+ minDate?: Date;
+ maxDate?: Date;
+ minDuration?: number;
+ maxDuration?: number;
+ };
+}
+
+// Zoom Levels
+export interface ZoomLevel {
+ name: string;
+ dayWidth: number;
+ topTier: TimelineConfig['topTier'];
+ bottomTier: TimelineConfig['bottomTier'];
+ scale: number;
+}
+
+// Selection State
+export interface SelectionState {
+ selectedTasks: string[];
+ selectedRows: number[];
+ selectionRange?: {
+ start: { row: number; col: number };
+ end: { row: number; col: number };
+ };
+ focusedTask?: string;
+}
+
+// Gantt View State
+export interface GanttViewState {
+ zoomLevel: number;
+ scrollPosition: { x: number; y: number };
+ viewportSize: { width: number; height: number };
+ splitterPosition: number; // percentage for grid/timeline split
+ showCriticalPath: boolean;
+ showBaseline: boolean;
+ showProgress: boolean;
+ showDependencies: boolean;
+ autoSchedule: boolean;
+ readOnly: boolean;
+}
+
+// Performance Metrics
+export interface PerformanceMetrics {
+ renderTime: number;
+ taskCount: number;
+ visibleTaskCount: number;
+ memoryUsage?: number;
+ fps?: number;
+}
+
+// Event Handlers
+export type TaskEventHandler = (task: GanttTask, event: MouseEvent | TouchEvent) => T;
+export type DragEventHandler = (taskId: string, newDates: { start: Date; end: Date }) => void;
+export type ResizeEventHandler = (taskId: string, newDates: { start: Date; end: Date }) => void;
+export type ProgressEventHandler = (taskId: string, progress: number) => void;
+export type SelectionEventHandler = (selectedTasks: string[]) => void;
+export type ColumnResizeHandler = (columnField: string, newWidth: number) => void;
+
+// Gantt Actions (for useReducer)
+export type GanttAction =
+ | { type: 'SET_TASKS'; payload: GanttTask[] }
+ | { type: 'UPDATE_TASK'; payload: { id: string; updates: Partial } }
+ | { type: 'ADD_TASK'; payload: GanttTask }
+ | { type: 'DELETE_TASK'; payload: string }
+ | { type: 'SET_SELECTION'; payload: string[] }
+ | { type: 'SET_DRAG_STATE'; payload: DragState | null }
+ | { type: 'SET_ZOOM_LEVEL'; payload: number }
+ | { type: 'SET_SCROLL_POSITION'; payload: { x: number; y: number } }
+ | { type: 'SET_SPLITTER_POSITION'; payload: number }
+ | { type: 'TOGGLE_TASK_EXPANSION'; payload: string }
+ | { type: 'SET_VIEW_STATE'; payload: Partial }
+ | { type: 'UPDATE_COLUMN_WIDTH'; payload: { field: string; width: number } };
+
+// Main Gantt State
+export interface GanttState {
+ tasks: GanttTask[];
+ columns: ColumnConfig[];
+ timelineConfig: TimelineConfig;
+ virtualScrollConfig: VirtualScrollConfig;
+ dragState: DragState | null;
+ selectionState: SelectionState;
+ viewState: GanttViewState;
+ zoomLevels: ZoomLevel[];
+ performanceMetrics: PerformanceMetrics;
+}
+
+// Gantt Chart Props
+export interface AdvancedGanttProps {
+ // Data
+ tasks: GanttTask[];
+ columns?: ColumnConfig[];
+
+ // Configuration
+ timelineConfig?: Partial;
+ virtualScrollConfig?: Partial;
+ zoomLevels?: ZoomLevel[];
+
+ // Initial State
+ initialViewState?: Partial;
+ initialSelection?: string[];
+
+ // Event Handlers
+ onTaskUpdate?: (taskId: string, updates: Partial) => void;
+ onTaskCreate?: (task: Omit) => void;
+ onTaskDelete?: (taskId: string) => void;
+ onTaskMove?: DragEventHandler;
+ onTaskResize?: ResizeEventHandler;
+ onProgressChange?: ProgressEventHandler;
+ onSelectionChange?: SelectionEventHandler;
+ onColumnResize?: ColumnResizeHandler;
+ onDependencyCreate?: (fromTaskId: string, toTaskId: string) => void;
+ onDependencyDelete?: (fromTaskId: string, toTaskId: string) => void;
+
+ // UI Customization
+ className?: string;
+ style?: React.CSSProperties;
+ theme?: 'light' | 'dark' | 'auto';
+ locale?: string;
+
+ // Feature Flags
+ enableDragDrop?: boolean;
+ enableResize?: boolean;
+ enableProgressEdit?: boolean;
+ enableInlineEdit?: boolean;
+ enableContextMenu?: boolean;
+ enableTooltips?: boolean;
+ enableExport?: boolean;
+ enablePrint?: boolean;
+
+ // Performance Options
+ enableVirtualScrolling?: boolean;
+ enableDebouncing?: boolean;
+ debounceDelay?: number;
+ maxVisibleTasks?: number;
+}
+
+// Context Menu Options
+export interface ContextMenuOption {
+ id: string;
+ label: string;
+ icon?: ReactNode;
+ disabled?: boolean;
+ separator?: boolean;
+ children?: ContextMenuOption[];
+ onClick?: (task?: GanttTask) => void;
+}
+
+// Export Options
+export interface ExportOptions {
+ format: 'png' | 'pdf' | 'svg' | 'json' | 'csv' | 'xlsx';
+ includeColumns?: string[];
+ dateRange?: { start: Date; end: Date };
+ filename?: string;
+ paperSize?: 'A4' | 'A3' | 'Letter' | 'Legal';
+ orientation?: 'portrait' | 'landscape';
+ scale?: number;
+}
+
+// Filter and Search
+export interface FilterConfig {
+ field: string;
+ operator: 'equals' | 'contains' | 'startsWith' | 'endsWith' | 'greaterThan' | 'lessThan' | 'between';
+ value: any;
+ logic?: 'and' | 'or';
+}
+
+export interface SearchConfig {
+ query: string;
+ fields: string[];
+ caseSensitive?: boolean;
+ wholeWord?: boolean;
+ regex?: boolean;
+}
+
+// Baseline and Critical Path
+export interface TaskBaseline {
+ taskId: string;
+ baselineStart: Date;
+ baselineEnd: Date;
+ baselineDuration: number;
+ baselineProgress: number;
+ variance?: number; // days
+}
+
+export interface CriticalPath {
+ taskIds: string[];
+ totalDuration: number;
+ slack: number; // days of buffer
+}
+
+// Undo/Redo
+export interface HistoryState {
+ past: GanttState[];
+ present: GanttState;
+ future: GanttState[];
+ maxHistorySize: number;
+}
+
+// Keyboard Shortcuts
+export interface KeyboardShortcut {
+ key: string;
+ ctrl?: boolean;
+ shift?: boolean;
+ alt?: boolean;
+ action: string;
+ description: string;
+ handler: (event: KeyboardEvent) => void;
+}
\ No newline at end of file
diff --git a/worklenz-frontend/src/types/project-roadmap.types.ts b/worklenz-frontend/src/types/project-roadmap.types.ts
new file mode 100644
index 00000000..a444942b
--- /dev/null
+++ b/worklenz-frontend/src/types/project-roadmap.types.ts
@@ -0,0 +1,62 @@
+export interface ProjectPhase {
+ id: string;
+ name: string;
+ description: string;
+ startDate: Date;
+ endDate: Date;
+ progress: number;
+ color: string;
+ status: 'not-started' | 'in-progress' | 'completed' | 'on-hold';
+ tasks: PhaseTask[];
+ milestones: PhaseMilestone[];
+}
+
+export interface PhaseTask {
+ id: string;
+ name: string;
+ description?: string;
+ startDate: Date;
+ endDate: Date;
+ progress: number;
+ assigneeId?: string;
+ assigneeName?: string;
+ priority: 'low' | 'medium' | 'high';
+ status: 'todo' | 'in-progress' | 'done';
+ dependencies?: string[];
+}
+
+export interface PhaseMilestone {
+ id: string;
+ name: string;
+ description?: string;
+ dueDate: Date;
+ isCompleted: boolean;
+ criticalPath: boolean;
+}
+
+export interface ProjectRoadmap {
+ id: string;
+ projectId: string;
+ name: string;
+ description?: string;
+ startDate: Date;
+ endDate: Date;
+ phases: ProjectPhase[];
+ createdAt: Date;
+ updatedAt: Date;
+}
+
+export interface GanttViewOptions {
+ viewMode: 'day' | 'week' | 'month' | 'year';
+ showTasks: boolean;
+ showMilestones: boolean;
+ groupByPhase: boolean;
+}
+
+export interface PhaseModalData extends ProjectPhase {
+ taskCount: number;
+ completedTaskCount: number;
+ milestoneCount: number;
+ completedMilestoneCount: number;
+ teamMembers: string[];
+}
\ No newline at end of file
diff --git a/worklenz-frontend/src/utils/gantt-performance.ts b/worklenz-frontend/src/utils/gantt-performance.ts
new file mode 100644
index 00000000..5116c186
--- /dev/null
+++ b/worklenz-frontend/src/utils/gantt-performance.ts
@@ -0,0 +1,408 @@
+import { useMemo, useCallback, useRef, useEffect } from 'react';
+import { GanttTask, PerformanceMetrics } from '../types/advanced-gantt.types';
+
+// Debounce utility for drag operations
+export function useDebounce any>(
+ callback: T,
+ delay: number
+): T {
+ const timeoutRef = useRef();
+
+ return useCallback((...args: Parameters) => {
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current);
+ }
+
+ timeoutRef.current = setTimeout(() => {
+ callback(...args);
+ }, delay);
+ }, [callback, delay]) as T;
+}
+
+// Throttle utility for scroll events
+export function useThrottle any>(
+ callback: T,
+ delay: number
+): T {
+ const lastCall = useRef(0);
+
+ return useCallback((...args: Parameters) => {
+ const now = Date.now();
+ if (now - lastCall.current >= delay) {
+ lastCall.current = now;
+ callback(...args);
+ }
+ }, [callback, delay]) as T;
+}
+
+// Memoized task calculations
+export const useTaskCalculations = (tasks: GanttTask[]) => {
+ return useMemo(() => {
+ const taskMap = new Map();
+ const parentChildMap = new Map();
+ const dependencyMap = new Map();
+
+ // Build maps for efficient lookups
+ tasks.forEach(task => {
+ taskMap.set(task.id, task);
+
+ if (task.parent) {
+ if (!parentChildMap.has(task.parent)) {
+ parentChildMap.set(task.parent, []);
+ }
+ parentChildMap.get(task.parent)!.push(task.id);
+ }
+
+ if (task.dependencies) {
+ dependencyMap.set(task.id, task.dependencies);
+ }
+ });
+
+ return {
+ taskMap,
+ parentChildMap,
+ dependencyMap,
+ totalTasks: tasks.length,
+ projectTasks: tasks.filter(t => t.type === 'project'),
+ milestones: tasks.filter(t => t.type === 'milestone'),
+ regularTasks: tasks.filter(t => t.type === 'task'),
+ };
+ }, [tasks]);
+};
+
+// Virtual scrolling calculations
+export interface VirtualScrollData {
+ startIndex: number;
+ endIndex: number;
+ visibleItems: GanttTask[];
+ totalHeight: number;
+ offsetY: number;
+}
+
+export const useVirtualScrolling = (
+ tasks: GanttTask[],
+ containerHeight: number,
+ itemHeight: number,
+ scrollTop: number,
+ overscan: number = 5
+): VirtualScrollData => {
+ return useMemo(() => {
+ const totalHeight = tasks.length * itemHeight;
+ const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
+ const endIndex = Math.min(
+ tasks.length - 1,
+ Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan
+ );
+ const visibleItems = tasks.slice(startIndex, endIndex + 1);
+ const offsetY = startIndex * itemHeight;
+
+ return {
+ startIndex,
+ endIndex,
+ visibleItems,
+ totalHeight,
+ offsetY,
+ };
+ }, [tasks, containerHeight, itemHeight, scrollTop, overscan]);
+};
+
+// Timeline virtual scrolling
+export interface TimelineVirtualData {
+ startDate: Date;
+ endDate: Date;
+ visibleDays: Date[];
+ totalWidth: number;
+ offsetX: number;
+}
+
+export const useTimelineVirtualScrolling = (
+ projectStartDate: Date,
+ projectEndDate: Date,
+ dayWidth: number,
+ containerWidth: number,
+ scrollLeft: number,
+ overscan: number = 10
+): TimelineVirtualData => {
+ return useMemo(() => {
+ const totalDays = Math.ceil((projectEndDate.getTime() - projectStartDate.getTime()) / (1000 * 60 * 60 * 24));
+ const totalWidth = totalDays * dayWidth;
+
+ const startDayIndex = Math.max(0, Math.floor(scrollLeft / dayWidth) - overscan);
+ const endDayIndex = Math.min(
+ totalDays - 1,
+ Math.ceil((scrollLeft + containerWidth) / dayWidth) + overscan
+ );
+
+ const visibleDays: Date[] = [];
+ for (let i = startDayIndex; i <= endDayIndex; i++) {
+ const date = new Date(projectStartDate);
+ date.setDate(date.getDate() + i);
+ visibleDays.push(date);
+ }
+
+ const offsetX = startDayIndex * dayWidth;
+ const startDate = new Date(projectStartDate);
+ startDate.setDate(startDate.getDate() + startDayIndex);
+
+ const endDate = new Date(projectStartDate);
+ endDate.setDate(endDate.getDate() + endDayIndex);
+
+ return {
+ startDate,
+ endDate,
+ visibleDays,
+ totalWidth,
+ offsetX,
+ };
+ }, [projectStartDate, projectEndDate, dayWidth, containerWidth, scrollLeft, overscan]);
+};
+
+// Performance monitoring hook
+export const usePerformanceMonitoring = (): {
+ metrics: PerformanceMetrics;
+ startMeasure: (name: string) => void;
+ endMeasure: (name: string) => void;
+ recordMetric: (name: string, value: number) => void;
+} => {
+ const metricsRef = useRef({
+ renderTime: 0,
+ taskCount: 0,
+ visibleTaskCount: 0,
+ });
+
+ const measurementsRef = useRef>(new Map());
+
+ const startMeasure = useCallback((name: string) => {
+ measurementsRef.current.set(name, performance.now());
+ }, []);
+
+ const endMeasure = useCallback((name: string) => {
+ const startTime = measurementsRef.current.get(name);
+ if (startTime) {
+ const duration = performance.now() - startTime;
+ measurementsRef.current.delete(name);
+
+ if (name === 'render') {
+ metricsRef.current.renderTime = duration;
+ }
+ }
+ }, []);
+
+ const recordMetric = useCallback((name: string, value: number) => {
+ switch (name) {
+ case 'taskCount':
+ metricsRef.current.taskCount = value;
+ break;
+ case 'visibleTaskCount':
+ metricsRef.current.visibleTaskCount = value;
+ break;
+ case 'memoryUsage':
+ metricsRef.current.memoryUsage = value;
+ break;
+ case 'fps':
+ metricsRef.current.fps = value;
+ break;
+ }
+ }, []);
+
+ return {
+ metrics: metricsRef.current,
+ startMeasure,
+ endMeasure,
+ recordMetric,
+ };
+};
+
+// Intersection Observer for lazy loading
+export const useIntersectionObserver = (
+ callback: (entries: IntersectionObserverEntry[]) => void,
+ options?: IntersectionObserverInit
+) => {
+ const targetRef = useRef(null);
+ const observerRef = useRef();
+
+ useEffect(() => {
+ if (!targetRef.current) return;
+
+ observerRef.current = new IntersectionObserver(callback, {
+ root: null,
+ rootMargin: '100px',
+ threshold: 0.1,
+ ...options,
+ });
+
+ observerRef.current.observe(targetRef.current);
+
+ return () => {
+ if (observerRef.current) {
+ observerRef.current.disconnect();
+ }
+ };
+ }, [callback, options]);
+
+ return targetRef;
+};
+
+// Date calculations optimization
+export const useDateCalculations = () => {
+ return useMemo(() => {
+ const cache = new Map();
+
+ const getDaysBetween = (start: Date, end: Date): number => {
+ const key = `${start.getTime()}-${end.getTime()}`;
+ if (cache.has(key)) {
+ return cache.get(key)!;
+ }
+
+ const days = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
+ cache.set(key, days);
+ return days;
+ };
+
+ const addDays = (date: Date, days: number): Date => {
+ const result = new Date(date);
+ result.setDate(result.getDate() + days);
+ return result;
+ };
+
+ const isWeekend = (date: Date): boolean => {
+ const day = date.getDay();
+ return day === 0 || day === 6; // Sunday or Saturday
+ };
+
+ const isWorkingDay = (date: Date, workingDays: number[]): boolean => {
+ return workingDays.includes(date.getDay());
+ };
+
+ const getWorkingDaysBetween = (start: Date, end: Date, workingDays: number[]): number => {
+ let count = 0;
+ const current = new Date(start);
+
+ while (current <= end) {
+ if (isWorkingDay(current, workingDays)) {
+ count++;
+ }
+ current.setDate(current.getDate() + 1);
+ }
+
+ return count;
+ };
+
+ return {
+ getDaysBetween,
+ addDays,
+ isWeekend,
+ isWorkingDay,
+ getWorkingDaysBetween,
+ clearCache: () => cache.clear(),
+ };
+ }, []);
+};
+
+// Task position calculations
+export const useTaskPositions = (
+ tasks: GanttTask[],
+ timelineStart: Date,
+ dayWidth: number
+) => {
+ return useMemo(() => {
+ const positions = new Map();
+
+ tasks.forEach((task, index) => {
+ const startDays = Math.floor((task.startDate.getTime() - timelineStart.getTime()) / (1000 * 60 * 60 * 24));
+ const endDays = Math.floor((task.endDate.getTime() - timelineStart.getTime()) / (1000 * 60 * 60 * 24));
+
+ positions.set(task.id, {
+ x: startDays * dayWidth,
+ width: Math.max(1, (endDays - startDays) * dayWidth),
+ y: index * 40, // Assuming 40px row height
+ });
+ });
+
+ return positions;
+ }, [tasks, timelineStart, dayWidth]);
+};
+
+// Memory management utilities
+export const useMemoryManagement = () => {
+ const cleanupFunctions = useRef void>>([]);
+
+ const addCleanup = useCallback((cleanup: () => void) => {
+ cleanupFunctions.current.push(cleanup);
+ }, []);
+
+ const runCleanup = useCallback(() => {
+ cleanupFunctions.current.forEach(cleanup => cleanup());
+ cleanupFunctions.current = [];
+ }, []);
+
+ useEffect(() => {
+ return runCleanup;
+ }, [runCleanup]);
+
+ return { addCleanup, runCleanup };
+};
+
+// Batch update utility for multiple task changes
+export const useBatchUpdates = (
+ updateFunction: (updates: T[]) => void,
+ delay: number = 100
+) => {
+ const batchRef = useRef([]);
+ const timeoutRef = useRef();
+
+ const addUpdate = useCallback((update: T) => {
+ batchRef.current.push(update);
+
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current);
+ }
+
+ timeoutRef.current = setTimeout(() => {
+ if (batchRef.current.length > 0) {
+ updateFunction([...batchRef.current]);
+ batchRef.current = [];
+ }
+ }, delay);
+ }, [updateFunction, delay]);
+
+ const flushUpdates = useCallback(() => {
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current);
+ }
+
+ if (batchRef.current.length > 0) {
+ updateFunction([...batchRef.current]);
+ batchRef.current = [];
+ }
+ }, [updateFunction]);
+
+ return { addUpdate, flushUpdates };
+};
+
+// FPS monitoring
+export const useFPSMonitoring = () => {
+ const fpsRef = useRef(0);
+ const frameCountRef = useRef(0);
+ const lastTimeRef = useRef(performance.now());
+
+ const measureFPS = useCallback(() => {
+ frameCountRef.current++;
+ const now = performance.now();
+
+ if (now - lastTimeRef.current >= 1000) {
+ fpsRef.current = Math.round((frameCountRef.current * 1000) / (now - lastTimeRef.current));
+ frameCountRef.current = 0;
+ lastTimeRef.current = now;
+ }
+
+ requestAnimationFrame(measureFPS);
+ }, []);
+
+ useEffect(() => {
+ const rafId = requestAnimationFrame(measureFPS);
+ return () => cancelAnimationFrame(rafId);
+ }, [measureFPS]);
+
+ return fpsRef.current;
+};
\ No newline at end of file