+ {/* Header */}
+
+
{t('tasksStepTitle')}
-
-
- {t('tasksStepLabel')} "{projectName}". {t('maxTasks')}
-
- }
- >
- (
-
-
-
updateTask(task.id, e.target.value)}
- onPressEnter={handleKeyPress}
- ref={setInputRef(index)}
- />
-
}
- disabled={tasks.length === 1}
- onClick={() => removeTask(task.id)}
- />
+
+ {t('tasksStepDescription', { projectName })}
+
+
+
+
+ {/* Tasks List */}
+
+
+ {tasks.map((task, index) => (
+
+
+
+ {task.value.trim() ? : index + 1}
+
+
+
+ updateTask(task.id, e.target.value)}
+ onKeyPress={e => handleKeyPress(e, index)}
+ onFocus={() => setFocusedIndex(index)}
+ onBlur={() => setFocusedIndex(null)}
+ ref={(el) => { inputRefs.current[index] = el as any; }}
+ className="text-base border-0 shadow-none task-input"
+ style={{ backgroundColor: 'transparent', color: token?.colorText }}
+ />
+
+
+ {tasks.length > 1 &&
} onClick={() => removeTask(task.id)} className="text-gray-400 hover:text-red-500" style={{ color: token?.colorTextTertiary }} />}
-
- )}
- />
- }
- onClick={addTask}
- disabled={tasks.length == 5}
- style={{ marginTop: '16px' }}
- >
- {t('tasksStepAddAnother')}
-
-
-
-
+
+ ))}
+
+
+ {tasks.length < 5 && (
+
} onClick={addTask} className="w-full mt-4 h-12 text-base" style={{ borderColor: token?.colorBorder, color: token?.colorTextSecondary }}>{t('addAnotherTask', { current: tasks.length, max: 5 })}
+ )}
+
+
+
);
-};
+};
\ No newline at end of file
diff --git a/worklenz-frontend/src/components/advanced-gantt/AdvancedGanttChart.tsx b/worklenz-frontend/src/components/advanced-gantt/AdvancedGanttChart.tsx
new file mode 100644
index 00000000..b1858c92
--- /dev/null
+++ b/worklenz-frontend/src/components/advanced-gantt/AdvancedGanttChart.tsx
@@ -0,0 +1,612 @@
+import React, { useReducer, useMemo, useCallback, useRef, useEffect, useState } from 'react';
+import {
+ GanttTask,
+ ColumnConfig,
+ TimelineConfig,
+ VirtualScrollConfig,
+ ZoomLevel,
+ GanttState,
+ GanttAction,
+ AdvancedGanttProps,
+ SelectionState,
+ GanttViewState,
+ DragState
+} from '../../types/advanced-gantt.types';
+import GanttGrid from './GanttGrid';
+import DraggableTaskBar from './DraggableTaskBar';
+import TimelineMarkers, { holidayPresets, workingDayPresets } from './TimelineMarkers';
+import VirtualScrollContainer, { VirtualTimeline } from './VirtualScrollContainer';
+import {
+ usePerformanceMonitoring,
+ useTaskCalculations,
+ useDateCalculations,
+ useDebounce,
+ useThrottle
+} from '../../utils/gantt-performance';
+import { useAppSelector } from '../../hooks/useAppSelector';
+import { themeWiseColor } from '../../utils/themeWiseColor';
+
+// Default configurations
+const defaultColumns: ColumnConfig[] = [
+ {
+ field: 'name',
+ title: 'Task Name',
+ width: 250,
+ minWidth: 150,
+ resizable: true,
+ sortable: true,
+ fixed: true,
+ editor: 'text'
+ },
+ {
+ field: 'startDate',
+ title: 'Start Date',
+ width: 120,
+ minWidth: 100,
+ resizable: true,
+ sortable: true,
+ fixed: true,
+ editor: 'date'
+ },
+ {
+ field: 'endDate',
+ title: 'End Date',
+ width: 120,
+ minWidth: 100,
+ resizable: true,
+ sortable: true,
+ fixed: true,
+ editor: 'date'
+ },
+ {
+ field: 'duration',
+ title: 'Duration',
+ width: 80,
+ minWidth: 60,
+ resizable: true,
+ sortable: false,
+ fixed: true
+ },
+ {
+ field: 'progress',
+ title: 'Progress',
+ width: 100,
+ minWidth: 80,
+ resizable: true,
+ sortable: true,
+ fixed: true,
+ editor: 'number'
+ },
+];
+
+const defaultTimelineConfig: TimelineConfig = {
+ topTier: { unit: 'month', format: 'MMM yyyy', height: 30 },
+ bottomTier: { unit: 'day', format: 'dd', height: 25 },
+ showWeekends: true,
+ showNonWorkingDays: true,
+ holidays: holidayPresets.US,
+ workingDays: workingDayPresets.standard,
+ workingHours: { start: 9, end: 17 },
+ dayWidth: 30,
+};
+
+const defaultVirtualScrollConfig: VirtualScrollConfig = {
+ enableRowVirtualization: true,
+ enableTimelineVirtualization: true,
+ bufferSize: 10,
+ itemHeight: 40,
+ overscan: 5,
+};
+
+const defaultZoomLevels: ZoomLevel[] = [
+ {
+ name: 'Year',
+ dayWidth: 2,
+ scale: 0.1,
+ topTier: { unit: 'year', format: 'yyyy' },
+ bottomTier: { unit: 'month', format: 'MMM' }
+ },
+ {
+ name: 'Month',
+ dayWidth: 8,
+ scale: 0.5,
+ topTier: { unit: 'month', format: 'MMM yyyy' },
+ bottomTier: { unit: 'week', format: 'w' }
+ },
+ {
+ name: 'Week',
+ dayWidth: 25,
+ scale: 1,
+ topTier: { unit: 'week', format: 'MMM dd' },
+ bottomTier: { unit: 'day', format: 'dd' }
+ },
+ {
+ name: 'Day',
+ dayWidth: 50,
+ scale: 2,
+ topTier: { unit: 'day', format: 'MMM dd' },
+ bottomTier: { unit: 'hour', format: 'HH' }
+ },
+];
+
+// Gantt state reducer
+function ganttReducer(state: GanttState, action: GanttAction): GanttState {
+ switch (action.type) {
+ case 'SET_TASKS':
+ return { ...state, tasks: action.payload };
+
+ case 'UPDATE_TASK':
+ return {
+ ...state,
+ tasks: state.tasks.map(task =>
+ task.id === action.payload.id
+ ? { ...task, ...action.payload.updates }
+ : task
+ ),
+ };
+
+ case 'ADD_TASK':
+ return { ...state, tasks: [...state.tasks, action.payload] };
+
+ case 'DELETE_TASK':
+ return {
+ ...state,
+ tasks: state.tasks.filter(task => task.id !== action.payload),
+ };
+
+ case 'SET_SELECTION':
+ return {
+ ...state,
+ selectionState: { ...state.selectionState, selectedTasks: action.payload },
+ };
+
+ case 'SET_DRAG_STATE':
+ return { ...state, dragState: action.payload };
+
+ case 'SET_ZOOM_LEVEL':
+ const newZoomLevel = Math.max(0, Math.min(state.zoomLevels.length - 1, action.payload));
+ return {
+ ...state,
+ viewState: { ...state.viewState, zoomLevel: newZoomLevel },
+ timelineConfig: {
+ ...state.timelineConfig,
+ dayWidth: state.zoomLevels[newZoomLevel].dayWidth,
+ topTier: state.zoomLevels[newZoomLevel].topTier,
+ bottomTier: state.zoomLevels[newZoomLevel].bottomTier,
+ },
+ };
+
+ case 'SET_SCROLL_POSITION':
+ return {
+ ...state,
+ viewState: { ...state.viewState, scrollPosition: action.payload },
+ };
+
+ case 'SET_SPLITTER_POSITION':
+ return {
+ ...state,
+ viewState: { ...state.viewState, splitterPosition: action.payload },
+ };
+
+ case 'TOGGLE_TASK_EXPANSION':
+ return {
+ ...state,
+ tasks: state.tasks.map(task =>
+ task.id === action.payload
+ ? { ...task, isExpanded: !task.isExpanded }
+ : task
+ ),
+ };
+
+ case 'SET_VIEW_STATE':
+ return {
+ ...state,
+ viewState: { ...state.viewState, ...action.payload },
+ };
+
+ case 'UPDATE_COLUMN_WIDTH':
+ return {
+ ...state,
+ columns: state.columns.map(col =>
+ col.field === action.payload.field
+ ? { ...col, width: action.payload.width }
+ : col
+ ),
+ };
+
+ default:
+ return state;
+ }
+}
+
+const AdvancedGanttChart: React.FC
= ({
+ tasks: initialTasks,
+ columns = defaultColumns,
+ timelineConfig = {},
+ virtualScrollConfig = {},
+ zoomLevels = defaultZoomLevels,
+ initialViewState = {},
+ initialSelection = [],
+ onTaskUpdate,
+ onTaskCreate,
+ onTaskDelete,
+ onTaskMove,
+ onTaskResize,
+ onProgressChange,
+ onSelectionChange,
+ onColumnResize,
+ onDependencyCreate,
+ onDependencyDelete,
+ className = '',
+ style = {},
+ theme = 'auto',
+ enableDragDrop = true,
+ enableResize = true,
+ enableProgressEdit = true,
+ enableInlineEdit = true,
+ enableVirtualScrolling = true,
+ enableDebouncing = true,
+ debounceDelay = 300,
+ maxVisibleTasks = 1000,
+}) => {
+ const containerRef = useRef(null);
+ const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
+ const themeMode = useAppSelector(state => state.themeReducer.mode);
+ const { startMeasure, endMeasure, metrics } = usePerformanceMonitoring();
+ const { getDaysBetween } = useDateCalculations();
+
+ // Initialize state
+ const initialState: GanttState = {
+ tasks: initialTasks,
+ columns,
+ timelineConfig: { ...defaultTimelineConfig, ...timelineConfig },
+ virtualScrollConfig: { ...defaultVirtualScrollConfig, ...virtualScrollConfig },
+ dragState: null,
+ selectionState: {
+ selectedTasks: initialSelection,
+ selectedRows: [],
+ focusedTask: undefined,
+ },
+ viewState: {
+ zoomLevel: 2, // Week view by default
+ scrollPosition: { x: 0, y: 0 },
+ viewportSize: { width: 0, height: 0 },
+ splitterPosition: 40, // 40% for grid, 60% for timeline
+ showCriticalPath: false,
+ showBaseline: false,
+ showProgress: true,
+ showDependencies: true,
+ autoSchedule: false,
+ readOnly: false,
+ ...initialViewState,
+ },
+ zoomLevels,
+ performanceMetrics: {
+ renderTime: 0,
+ taskCount: initialTasks.length,
+ visibleTaskCount: 0,
+ },
+ };
+
+ const [state, dispatch] = useReducer(ganttReducer, initialState);
+ const { taskMap, parentChildMap, totalTasks } = useTaskCalculations(state.tasks);
+
+ // Calculate project timeline bounds
+ const projectBounds = useMemo(() => {
+ if (state.tasks.length === 0) {
+ const today = new Date();
+ return {
+ start: new Date(today.getFullYear(), today.getMonth(), 1),
+ end: new Date(today.getFullYear(), today.getMonth() + 3, 0),
+ };
+ }
+
+ const startDates = state.tasks.map(task => task.startDate);
+ const endDates = state.tasks.map(task => task.endDate);
+ const minStart = new Date(Math.min(...startDates.map(d => d.getTime())));
+ const maxEnd = new Date(Math.max(...endDates.map(d => d.getTime())));
+
+ // Add some padding
+ minStart.setDate(minStart.getDate() - 7);
+ maxEnd.setDate(maxEnd.getDate() + 7);
+
+ return { start: minStart, end: maxEnd };
+ }, [state.tasks]);
+
+ // Debounced event handlers
+ const debouncedTaskUpdate = useDebounce(
+ useCallback((taskId: string, updates: Partial) => {
+ dispatch({ type: 'UPDATE_TASK', payload: { id: taskId, updates } });
+ onTaskUpdate?.(taskId, updates);
+ }, [onTaskUpdate]),
+ enableDebouncing ? debounceDelay : 0
+ );
+
+ const debouncedTaskMove = useDebounce(
+ useCallback((taskId: string, newDates: { start: Date; end: Date }) => {
+ dispatch({ type: 'UPDATE_TASK', payload: {
+ id: taskId,
+ updates: { startDate: newDates.start, endDate: newDates.end }
+ }});
+ onTaskMove?.(taskId, newDates);
+ }, [onTaskMove]),
+ enableDebouncing ? debounceDelay : 0
+ );
+
+ const debouncedProgressChange = useDebounce(
+ useCallback((taskId: string, progress: number) => {
+ dispatch({ type: 'UPDATE_TASK', payload: { id: taskId, updates: { progress } }});
+ onProgressChange?.(taskId, progress);
+ }, [onProgressChange]),
+ enableDebouncing ? debounceDelay : 0
+ );
+
+ // Throttled scroll handler
+ const throttledScrollHandler = useThrottle(
+ useCallback((scrollLeft: number, scrollTop: number) => {
+ dispatch({ type: 'SET_SCROLL_POSITION', payload: { x: scrollLeft, y: scrollTop } });
+ }, []),
+ 16 // 60fps
+ );
+
+ // Container size observer
+ useEffect(() => {
+ const observer = new ResizeObserver((entries) => {
+ for (const entry of entries) {
+ const { width, height } = entry.contentRect;
+ setContainerSize({ width, height });
+ dispatch({
+ type: 'SET_VIEW_STATE',
+ payload: { viewportSize: { width, height } }
+ });
+ }
+ });
+
+ if (containerRef.current) {
+ observer.observe(containerRef.current);
+ }
+
+ return () => observer.disconnect();
+ }, []);
+
+ // Calculate grid and timeline dimensions
+ const gridWidth = useMemo(() => {
+ return Math.floor(containerSize.width * (state.viewState.splitterPosition / 100));
+ }, [containerSize.width, state.viewState.splitterPosition]);
+
+ const timelineWidth = useMemo(() => {
+ return containerSize.width - gridWidth;
+ }, [containerSize.width, gridWidth]);
+
+ // Handle zoom changes
+ const handleZoomChange = useCallback((direction: 'in' | 'out') => {
+ const currentZoom = state.viewState.zoomLevel;
+ const newZoom = direction === 'in'
+ ? Math.min(state.zoomLevels.length - 1, currentZoom + 1)
+ : Math.max(0, currentZoom - 1);
+
+ dispatch({ type: 'SET_ZOOM_LEVEL', payload: newZoom });
+ }, [state.viewState.zoomLevel, state.zoomLevels.length]);
+
+ // Theme-aware colors
+ const colors = useMemo(() => ({
+ background: themeWiseColor('#ffffff', '#1f2937', themeMode),
+ border: themeWiseColor('#e5e7eb', '#4b5563', themeMode),
+ timelineBackground: themeWiseColor('#f8f9fa', '#374151', themeMode),
+ }), [themeMode]);
+
+ // Render timeline header
+ const renderTimelineHeader = () => {
+ const currentZoom = state.zoomLevels[state.viewState.zoomLevel];
+ const totalDays = getDaysBetween(projectBounds.start, projectBounds.end);
+ const totalWidth = totalDays * state.timelineConfig.dayWidth;
+
+ return (
+
+
+ {(date, index, style) => (
+
+
+ {formatDateForUnit(date, currentZoom.topTier.unit)}
+
+
+ {formatDateForUnit(date, currentZoom.bottomTier.unit)}
+
+
+ )}
+
+
+ );
+ };
+
+ // Render timeline content
+ const renderTimelineContent = () => {
+ const headerHeight = (state.zoomLevels[state.viewState.zoomLevel].topTier.height || 30) +
+ (state.zoomLevels[state.viewState.zoomLevel].bottomTier.height || 25);
+ const contentHeight = containerSize.height - headerHeight;
+
+ return (
+
+ {/* Timeline markers (weekends, holidays, etc.) */}
+
+
+ {/* Task bars */}
+
+ {(task, index, style) => (
+
+ )}
+
+
+ );
+ };
+
+ // Render toolbar
+ const renderToolbar = () => (
+
+
+
+
+ {state.zoomLevels[state.viewState.zoomLevel].name}
+
+
+
+
+
+ Tasks: {state.tasks.length}
+ •
+ Render: {Math.round(metrics.renderTime)}ms
+
+
+ );
+
+ // Performance monitoring
+ useEffect(() => {
+ startMeasure('render');
+ return () => endMeasure('render');
+ });
+
+ return (
+
+ {/* Toolbar */}
+ {renderToolbar()}
+
+ {/* Main content */}
+
+ {/* Grid */}
+
+ {
+ // Handle task selection
+ const newSelection = { ...state.selectionState, selectedTasks: [task.id] };
+ dispatch({ type: 'SET_SELECTION', payload: [task.id] });
+ onSelectionChange?.(newSelection);
+ }}
+ onTaskExpand={(taskId) => {
+ dispatch({ type: 'TOGGLE_TASK_EXPANSION', payload: taskId });
+ }}
+ onColumnResize={(field, width) => {
+ dispatch({ type: 'UPDATE_COLUMN_WIDTH', payload: { field, width } });
+ onColumnResize?.(field, width);
+ }}
+ onTaskUpdate={debouncedTaskUpdate}
+ />
+
+
+ {/* Timeline */}
+
+ {renderTimelineHeader()}
+ {renderTimelineContent()}
+
+
+
+ );
+};
+
+// Helper function to format dates based on unit
+function formatDateForUnit(date: Date, unit: string): string {
+ switch (unit) {
+ case 'year':
+ return date.getFullYear().toString();
+ case 'month':
+ return date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
+ case 'week':
+ return `W${getWeekNumber(date)}`;
+ case 'day':
+ return date.getDate().toString();
+ case 'hour':
+ return date.getHours().toString().padStart(2, '0');
+ default:
+ return '';
+ }
+}
+
+function getWeekNumber(date: Date): number {
+ const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
+ const dayNum = d.getUTCDay() || 7;
+ d.setUTCDate(d.getUTCDate() + 4 - dayNum);
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
+ return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
+}
+
+export default AdvancedGanttChart;
\ No newline at end of file
diff --git a/worklenz-frontend/src/components/advanced-gantt/AdvancedGanttDemo.tsx b/worklenz-frontend/src/components/advanced-gantt/AdvancedGanttDemo.tsx
new file mode 100644
index 00000000..64b10de8
--- /dev/null
+++ b/worklenz-frontend/src/components/advanced-gantt/AdvancedGanttDemo.tsx
@@ -0,0 +1,668 @@
+import React, { useState, useMemo } from 'react';
+import { Button, Space, message, Card } from 'antd';
+import AdvancedGanttChart from './AdvancedGanttChart';
+import { GanttTask, ColumnConfig } from '../../types/advanced-gantt.types';
+import { useAppSelector } from '../../hooks/useAppSelector';
+import { holidayPresets, workingDayPresets } from './TimelineMarkers';
+
+// Enhanced sample data with more realistic project structure
+const generateSampleTasks = (): GanttTask[] => {
+ const baseDate = new Date(2024, 11, 1); // December 1, 2024
+
+ return [
+ // Project Phase 1: Planning & Design
+ {
+ id: 'project-1',
+ name: '🚀 Web Platform Development',
+ startDate: new Date(2024, 11, 1),
+ endDate: new Date(2025, 2, 31),
+ progress: 45,
+ type: 'project',
+ status: 'in-progress',
+ priority: 'high',
+ color: '#1890ff',
+ hasChildren: true,
+ isExpanded: true,
+ level: 0,
+ },
+ {
+ id: 'planning-phase',
+ name: '📋 Planning & Analysis Phase',
+ startDate: new Date(2024, 11, 1),
+ endDate: new Date(2024, 11, 20),
+ progress: 85,
+ type: 'project',
+ status: 'in-progress',
+ priority: 'high',
+ parent: 'project-1',
+ color: '#52c41a',
+ hasChildren: true,
+ isExpanded: true,
+ level: 1,
+ },
+ {
+ id: 'requirements-analysis',
+ name: 'Requirements Gathering & Analysis',
+ startDate: new Date(2024, 11, 1),
+ endDate: new Date(2024, 11, 8),
+ progress: 100,
+ type: 'task',
+ status: 'completed',
+ priority: 'high',
+ parent: 'planning-phase',
+ assignee: {
+ id: 'user-1',
+ name: 'Alice Johnson',
+ avatar: 'https://ui-avatars.com/api/?name=Alice+Johnson&background=1890ff&color=fff',
+ },
+ tags: ['research', 'documentation'],
+ level: 2,
+ },
+ {
+ id: 'technical-architecture',
+ name: 'Technical Architecture Design',
+ startDate: new Date(2024, 11, 8),
+ endDate: new Date(2024, 11, 18),
+ progress: 75,
+ type: 'task',
+ status: 'in-progress',
+ priority: 'high',
+ parent: 'planning-phase',
+ assignee: {
+ id: 'user-2',
+ name: 'Bob Smith',
+ avatar: 'https://ui-avatars.com/api/?name=Bob+Smith&background=52c41a&color=fff',
+ },
+ dependencies: ['requirements-analysis'],
+ tags: ['architecture', 'design'],
+ level: 2,
+ },
+ {
+ id: 'ui-ux-design',
+ name: 'UI/UX Design & Prototyping',
+ startDate: new Date(2024, 11, 10),
+ endDate: new Date(2024, 11, 20),
+ progress: 60,
+ type: 'task',
+ status: 'in-progress',
+ priority: 'medium',
+ parent: 'planning-phase',
+ assignee: {
+ id: 'user-3',
+ name: 'Carol Davis',
+ avatar: 'https://ui-avatars.com/api/?name=Carol+Davis&background=faad14&color=fff',
+ },
+ dependencies: ['requirements-analysis'],
+ tags: ['design', 'prototype'],
+ level: 2,
+ },
+ {
+ id: 'milestone-planning-complete',
+ name: '🎯 Planning Phase Complete',
+ startDate: new Date(2024, 11, 20),
+ endDate: new Date(2024, 11, 20),
+ progress: 0,
+ type: 'milestone',
+ status: 'not-started',
+ priority: 'critical',
+ parent: 'planning-phase',
+ dependencies: ['technical-architecture', 'ui-ux-design'],
+ level: 2,
+ },
+
+ // Development Phase
+ {
+ id: 'development-phase',
+ name: '⚡ Development Phase',
+ startDate: new Date(2024, 11, 21),
+ endDate: new Date(2025, 1, 28),
+ progress: 35,
+ type: 'project',
+ status: 'in-progress',
+ priority: 'high',
+ parent: 'project-1',
+ color: '#722ed1',
+ hasChildren: true,
+ isExpanded: true,
+ level: 1,
+ },
+ {
+ id: 'backend-development',
+ name: 'Backend API Development',
+ startDate: new Date(2024, 11, 21),
+ endDate: new Date(2025, 1, 15),
+ progress: 45,
+ type: 'task',
+ status: 'in-progress',
+ priority: 'high',
+ parent: 'development-phase',
+ assignee: {
+ id: 'user-4',
+ name: 'David Wilson',
+ avatar: 'https://ui-avatars.com/api/?name=David+Wilson&background=722ed1&color=fff',
+ },
+ dependencies: ['milestone-planning-complete'],
+ tags: ['backend', 'api'],
+ level: 2,
+ },
+ {
+ id: 'frontend-development',
+ name: 'Frontend React Application',
+ startDate: new Date(2025, 0, 5),
+ endDate: new Date(2025, 1, 25),
+ progress: 25,
+ type: 'task',
+ status: 'in-progress',
+ priority: 'high',
+ parent: 'development-phase',
+ assignee: {
+ id: 'user-5',
+ name: 'Eva Brown',
+ avatar: 'https://ui-avatars.com/api/?name=Eva+Brown&background=ff7a45&color=fff',
+ },
+ dependencies: ['backend-development'],
+ tags: ['frontend', 'react'],
+ level: 2,
+ },
+ {
+ id: 'database-setup',
+ name: 'Database Schema & Migration',
+ startDate: new Date(2024, 11, 21),
+ endDate: new Date(2025, 0, 10),
+ progress: 80,
+ type: 'task',
+ status: 'in-progress',
+ priority: 'medium',
+ parent: 'development-phase',
+ assignee: {
+ id: 'user-6',
+ name: 'Frank Miller',
+ avatar: 'https://ui-avatars.com/api/?name=Frank+Miller&background=13c2c2&color=fff',
+ },
+ dependencies: ['milestone-planning-complete'],
+ tags: ['database', 'migration'],
+ level: 2,
+ },
+
+ // Testing Phase
+ {
+ id: 'testing-phase',
+ name: '🧪 Testing & QA Phase',
+ startDate: new Date(2025, 2, 1),
+ endDate: new Date(2025, 2, 20),
+ progress: 0,
+ type: 'project',
+ status: 'not-started',
+ priority: 'high',
+ parent: 'project-1',
+ color: '#fa8c16',
+ hasChildren: true,
+ isExpanded: false,
+ level: 1,
+ },
+ {
+ id: 'unit-testing',
+ name: 'Unit Testing Implementation',
+ startDate: new Date(2025, 2, 1),
+ endDate: new Date(2025, 2, 10),
+ progress: 0,
+ type: 'task',
+ status: 'not-started',
+ priority: 'high',
+ parent: 'testing-phase',
+ assignee: {
+ id: 'user-7',
+ name: 'Grace Lee',
+ avatar: 'https://ui-avatars.com/api/?name=Grace+Lee&background=fa8c16&color=fff',
+ },
+ dependencies: ['frontend-development'],
+ tags: ['testing', 'unit'],
+ level: 2,
+ },
+ {
+ id: 'integration-testing',
+ name: 'Integration & E2E Testing',
+ startDate: new Date(2025, 2, 8),
+ endDate: new Date(2025, 2, 18),
+ progress: 0,
+ type: 'task',
+ status: 'not-started',
+ priority: 'high',
+ parent: 'testing-phase',
+ assignee: {
+ id: 'user-8',
+ name: 'Henry Clark',
+ avatar: 'https://ui-avatars.com/api/?name=Henry+Clark&background=eb2f96&color=fff',
+ },
+ dependencies: ['unit-testing'],
+ tags: ['testing', 'integration'],
+ level: 2,
+ },
+ {
+ id: 'milestone-beta-ready',
+ name: '🎯 Beta Release Ready',
+ startDate: new Date(2025, 2, 20),
+ endDate: new Date(2025, 2, 20),
+ progress: 0,
+ type: 'milestone',
+ status: 'not-started',
+ priority: 'critical',
+ parent: 'testing-phase',
+ dependencies: ['integration-testing'],
+ level: 2,
+ },
+
+ // Deployment Phase
+ {
+ id: 'deployment-phase',
+ name: '🚀 Deployment & Launch',
+ startDate: new Date(2025, 2, 21),
+ endDate: new Date(2025, 2, 31),
+ progress: 0,
+ type: 'project',
+ status: 'not-started',
+ priority: 'critical',
+ parent: 'project-1',
+ color: '#f5222d',
+ hasChildren: true,
+ isExpanded: false,
+ level: 1,
+ },
+ {
+ id: 'production-deployment',
+ name: 'Production Environment Setup',
+ startDate: new Date(2025, 2, 21),
+ endDate: new Date(2025, 2, 25),
+ progress: 0,
+ type: 'task',
+ status: 'not-started',
+ priority: 'critical',
+ parent: 'deployment-phase',
+ assignee: {
+ id: 'user-9',
+ name: 'Ivy Taylor',
+ avatar: 'https://ui-avatars.com/api/?name=Ivy+Taylor&background=f5222d&color=fff',
+ },
+ dependencies: ['milestone-beta-ready'],
+ tags: ['deployment', 'production'],
+ level: 2,
+ },
+ {
+ id: 'go-live',
+ name: 'Go Live & Monitoring',
+ startDate: new Date(2025, 2, 26),
+ endDate: new Date(2025, 2, 31),
+ progress: 0,
+ type: 'task',
+ status: 'not-started',
+ priority: 'critical',
+ parent: 'deployment-phase',
+ assignee: {
+ id: 'user-10',
+ name: 'Jack Anderson',
+ avatar: 'https://ui-avatars.com/api/?name=Jack+Anderson&background=2f54eb&color=fff',
+ },
+ dependencies: ['production-deployment'],
+ tags: ['launch', 'monitoring'],
+ level: 2,
+ },
+ {
+ id: 'milestone-project-complete',
+ name: '🎉 Project Launch Complete',
+ startDate: new Date(2025, 2, 31),
+ endDate: new Date(2025, 2, 31),
+ progress: 0,
+ type: 'milestone',
+ status: 'not-started',
+ priority: 'critical',
+ parent: 'deployment-phase',
+ dependencies: ['go-live'],
+ level: 2,
+ },
+ ];
+};
+
+// Enhanced column configuration
+const sampleColumns: ColumnConfig[] = [
+ {
+ field: 'name',
+ title: 'Task / Phase Name',
+ width: 300,
+ minWidth: 200,
+ resizable: true,
+ sortable: true,
+ fixed: true,
+ editor: 'text'
+ },
+ {
+ field: 'assignee',
+ title: 'Assignee',
+ width: 150,
+ minWidth: 120,
+ resizable: true,
+ sortable: true,
+ fixed: true
+ },
+ {
+ field: 'startDate',
+ title: 'Start Date',
+ width: 120,
+ minWidth: 100,
+ resizable: true,
+ sortable: true,
+ fixed: true,
+ editor: 'date'
+ },
+ {
+ field: 'endDate',
+ title: 'End Date',
+ width: 120,
+ minWidth: 100,
+ resizable: true,
+ sortable: true,
+ fixed: true,
+ editor: 'date'
+ },
+ {
+ field: 'duration',
+ title: 'Duration',
+ width: 80,
+ minWidth: 60,
+ resizable: true,
+ sortable: false,
+ fixed: true,
+ align: 'center'
+ },
+ {
+ field: 'progress',
+ title: 'Progress',
+ width: 120,
+ minWidth: 100,
+ resizable: true,
+ sortable: true,
+ fixed: true,
+ editor: 'number'
+ },
+ {
+ field: 'status',
+ title: 'Status',
+ width: 100,
+ minWidth: 80,
+ resizable: true,
+ sortable: true,
+ fixed: true,
+ editor: 'select',
+ editorOptions: [
+ { value: 'not-started', label: 'Not Started' },
+ { value: 'in-progress', label: 'In Progress' },
+ { value: 'completed', label: 'Completed' },
+ { value: 'on-hold', label: 'On Hold' },
+ { value: 'overdue', label: 'Overdue' },
+ ]
+ },
+ {
+ field: 'priority',
+ title: 'Priority',
+ width: 100,
+ minWidth: 80,
+ resizable: true,
+ sortable: true,
+ fixed: true,
+ editor: 'select',
+ editorOptions: [
+ { value: 'low', label: 'Low' },
+ { value: 'medium', label: 'Medium' },
+ { value: 'high', label: 'High' },
+ { value: 'critical', label: 'Critical' },
+ ]
+ },
+];
+
+const AdvancedGanttDemo: React.FC = () => {
+ const [tasks, setTasks] = useState(generateSampleTasks());
+ const [selectedTasks, setSelectedTasks] = useState([]);
+ const themeMode = useAppSelector(state => state.themeReducer.mode);
+
+ const handleTaskUpdate = (taskId: string, updates: Partial) => {
+ setTasks(prevTasks =>
+ prevTasks.map(task =>
+ task.id === taskId ? { ...task, ...updates } : task
+ )
+ );
+ message.success(`Task "${tasks.find(t => t.id === taskId)?.name}" updated`);
+ };
+
+ const handleTaskMove = (taskId: string, newDates: { start: Date; end: Date }) => {
+ setTasks(prevTasks =>
+ prevTasks.map(task =>
+ task.id === taskId
+ ? { ...task, startDate: newDates.start, endDate: newDates.end }
+ : task
+ )
+ );
+ message.info(`Task moved: ${newDates.start.toLocaleDateString()} - ${newDates.end.toLocaleDateString()}`);
+ };
+
+ const handleProgressChange = (taskId: string, progress: number) => {
+ setTasks(prevTasks =>
+ prevTasks.map(task =>
+ task.id === taskId ? { ...task, progress } : task
+ )
+ );
+ message.info(`Progress updated: ${Math.round(progress)}%`);
+ };
+
+ const handleSelectionChange = (selection: any) => {
+ setSelectedTasks(selection.selectedTasks);
+ };
+
+ const resetToSampleData = () => {
+ setTasks(generateSampleTasks());
+ setSelectedTasks([]);
+ message.info('Gantt chart reset to sample data');
+ };
+
+ const addSampleTask = () => {
+ const newTask: GanttTask = {
+ id: `task-${Date.now()}`,
+ name: `New Task ${tasks.length + 1}`,
+ startDate: new Date(),
+ endDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // +7 days
+ progress: 0,
+ type: 'task',
+ status: 'not-started',
+ priority: 'medium',
+ level: 0,
+ };
+ setTasks(prev => [...prev, newTask]);
+ message.success('New task added');
+ };
+
+ const deleteSelectedTasks = () => {
+ if (selectedTasks.length === 0) {
+ message.warning('No tasks selected');
+ return;
+ }
+
+ setTasks(prev => prev.filter(task => !selectedTasks.includes(task.id)));
+ setSelectedTasks([]);
+ message.success(`${selectedTasks.length} task(s) deleted`);
+ };
+
+ const taskStats = useMemo(() => {
+ const total = tasks.length;
+ const completed = tasks.filter(t => t.status === 'completed').length;
+ const inProgress = tasks.filter(t => t.status === 'in-progress').length;
+ const overdue = tasks.filter(t => t.status === 'overdue').length;
+ const avgProgress = tasks.reduce((sum, t) => sum + t.progress, 0) / total;
+
+ return { total, completed, inProgress, overdue, avgProgress };
+ }, [tasks]);
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+ 🚀 Advanced Gantt Chart Demo
+
+
+ Professional Gantt chart with draggable tasks, virtual scrolling, holiday markers,
+ and performance optimizations for modern project management.
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Project Statistics */}
+
+
+
Total Tasks
+
{taskStats.total}
+
+
+
Completed
+
{taskStats.completed}
+
+
+
In Progress
+
{taskStats.inProgress}
+
+
+
Avg Progress
+
+ {Math.round(taskStats.avgProgress)}%
+
+
+
+
+
+
+ {/* Gantt Chart */}
+
+
+ {/* Feature List */}
+
+
+
+ ✨ Advanced Features Demonstrated
+
+
+
+
Performance & UX
+
+ - • Virtual scrolling for 1000+ tasks
+ - • Smooth 60fps drag & drop
+ - • Debounced updates
+ - • Memory-optimized rendering
+ - • Responsive design
+
+
+
+
Gantt Features
+
+ - • Draggable task bars
+ - • Resizable task duration
+ - • Progress editing
+ - • Multi-level hierarchy
+ - • Task dependencies
+
+
+
+
Timeline & Markers
+
+ - • Weekend & holiday markers
+ - • Working day indicators
+ - • Today line
+ - • Multi-tier timeline
+ - • Zoom levels (Year/Month/Week/Day)
+
+
+
+
Grid Features
+
+ - • Fixed columns layout
+ - • Inline editing
+ - • Column resizing
+ - • Multi-select
+ - • Hierarchical tree view
+
+
+
+
UI/UX
+
+ - • Dark/Light theme support
+ - • Tailwind CSS styling
+ - • Consistent with Worklenz
+ - • Accessibility features
+ - • Mobile responsive
+
+
+
+
Architecture
+
+ - • Modern React patterns
+ - • TypeScript safety
+ - • Optimized performance
+ - • Enterprise features
+ - • Best practices 2025
+
+
+
+
+
+
+ );
+};
+
+export default AdvancedGanttDemo;
\ No newline at end of file
diff --git a/worklenz-frontend/src/components/advanced-gantt/DraggableTaskBar.tsx b/worklenz-frontend/src/components/advanced-gantt/DraggableTaskBar.tsx
new file mode 100644
index 00000000..f8d930ca
--- /dev/null
+++ b/worklenz-frontend/src/components/advanced-gantt/DraggableTaskBar.tsx
@@ -0,0 +1,304 @@
+import React, { useState, useRef, useCallback, useMemo } from 'react';
+import { GanttTask, DragState } from '../../types/advanced-gantt.types';
+import { useAppSelector } from '../../hooks/useAppSelector';
+import { themeWiseColor } from '../../utils/themeWiseColor';
+import { useDateCalculations } from '../../utils/gantt-performance';
+
+interface DraggableTaskBarProps {
+ task: GanttTask;
+ timelineStart: Date;
+ dayWidth: number;
+ rowHeight: number;
+ index: number;
+ onTaskMove?: (taskId: string, newDates: { start: Date; end: Date }) => void;
+ onTaskResize?: (taskId: string, newDates: { start: Date; end: Date }) => void;
+ onProgressChange?: (taskId: string, progress: number) => void;
+ onTaskClick?: (task: GanttTask) => void;
+ onTaskDoubleClick?: (task: GanttTask) => void;
+ enableDragDrop?: boolean;
+ enableResize?: boolean;
+ enableProgressEdit?: boolean;
+ readOnly?: boolean;
+}
+
+const DraggableTaskBar: React.FC = ({
+ task,
+ timelineStart,
+ dayWidth,
+ rowHeight,
+ index,
+ onTaskMove,
+ onTaskResize,
+ onProgressChange,
+ onTaskClick,
+ onTaskDoubleClick,
+ enableDragDrop = true,
+ enableResize = true,
+ enableProgressEdit = true,
+ readOnly = false,
+}) => {
+ const [dragState, setDragState] = useState(null);
+ const [hoverState, setHoverState] = useState(null);
+ const taskBarRef = useRef(null);
+ const themeMode = useAppSelector(state => state.themeReducer.mode);
+ const { getDaysBetween, addDays } = useDateCalculations();
+
+ // Calculate task position and dimensions
+ const taskPosition = useMemo(() => {
+ const startDays = getDaysBetween(timelineStart, task.startDate);
+ const duration = getDaysBetween(task.startDate, task.endDate);
+
+ return {
+ x: startDays * dayWidth,
+ width: Math.max(dayWidth * 0.5, duration * dayWidth),
+ y: index * rowHeight + 8, // 8px padding
+ height: rowHeight - 16, // 16px total padding
+ };
+ }, [task.startDate, task.endDate, timelineStart, dayWidth, rowHeight, index, getDaysBetween]);
+
+ // Theme-aware colors
+ const colors = useMemo(() => {
+ const baseColor = task.color || getDefaultTaskColor(task.status);
+ return {
+ background: themeWiseColor(baseColor, adjustColorForDarkMode(baseColor), themeMode),
+ border: themeWiseColor(darkenColor(baseColor, 0.2), lightenColor(baseColor, 0.2), themeMode),
+ progress: themeWiseColor('#52c41a', '#34d399', themeMode),
+ text: themeWiseColor('#ffffff', '#f9fafb', themeMode),
+ hover: themeWiseColor(lightenColor(baseColor, 0.1), darkenColor(baseColor, 0.1), themeMode),
+ };
+ }, [task.color, task.status, themeMode]);
+
+ // Mouse event handlers
+ const handleMouseDown = useCallback((e: React.MouseEvent, dragType: DragState['dragType']) => {
+ if (readOnly || !enableDragDrop) return;
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ const rect = taskBarRef.current?.getBoundingClientRect();
+ if (!rect) return;
+
+ setDragState({
+ isDragging: true,
+ dragType,
+ taskId: task.id,
+ initialPosition: { x: e.clientX, y: e.clientY },
+ currentPosition: { x: e.clientX, y: e.clientY },
+ initialDates: { start: task.startDate, end: task.endDate },
+ initialProgress: task.progress,
+ snapToGrid: true,
+ });
+
+ // Add global mouse event listeners
+ const handleMouseMove = (moveEvent: MouseEvent) => {
+ handleMouseMove_Internal(moveEvent, dragType);
+ };
+
+ const handleMouseUp = () => {
+ handleMouseUp_Internal();
+ document.removeEventListener('mousemove', handleMouseMove);
+ document.removeEventListener('mouseup', handleMouseUp);
+ };
+
+ document.addEventListener('mousemove', handleMouseMove);
+ document.addEventListener('mouseup', handleMouseUp);
+ }, [readOnly, enableDragDrop, task]);
+
+ const handleMouseMove_Internal = useCallback((e: MouseEvent, dragType: DragState['dragType']) => {
+ if (!dragState) return;
+
+ const deltaX = e.clientX - dragState.initialPosition.x;
+ const deltaDays = Math.round(deltaX / dayWidth);
+
+ let newStartDate = task.startDate;
+ let newEndDate = task.endDate;
+
+ switch (dragType) {
+ case 'move':
+ newStartDate = addDays(dragState.initialDates.start, deltaDays);
+ newEndDate = addDays(dragState.initialDates.end, deltaDays);
+ break;
+
+ case 'resize-start':
+ newStartDate = addDays(dragState.initialDates.start, deltaDays);
+ // Ensure minimum duration
+ if (newStartDate >= newEndDate) {
+ newStartDate = addDays(newEndDate, -1);
+ }
+ break;
+
+ case 'resize-end':
+ newEndDate = addDays(dragState.initialDates.end, deltaDays);
+ // Ensure minimum duration
+ if (newEndDate <= newStartDate) {
+ newEndDate = addDays(newStartDate, 1);
+ }
+ break;
+
+ case 'progress':
+ if (enableProgressEdit) {
+ const progressDelta = deltaX / taskPosition.width;
+ const newProgress = Math.max(0, Math.min(100, (dragState.initialProgress || 0) + progressDelta * 100));
+ onProgressChange?.(task.id, newProgress);
+ }
+ return;
+ }
+
+ // Update drag state
+ setDragState(prev => prev ? {
+ ...prev,
+ currentPosition: { x: e.clientX, y: e.clientY },
+ } : null);
+
+ // Call appropriate handler
+ if (dragType === 'move') {
+ onTaskMove?.(task.id, { start: newStartDate, end: newEndDate });
+ } else if (dragType.startsWith('resize')) {
+ onTaskResize?.(task.id, { start: newStartDate, end: newEndDate });
+ }
+ }, [dragState, dayWidth, task, taskPosition.width, enableProgressEdit, onTaskMove, onTaskResize, onProgressChange, addDays]);
+
+ const handleMouseUp_Internal = useCallback(() => {
+ setDragState(null);
+ }, []);
+
+ const handleClick = useCallback((e: React.MouseEvent) => {
+ e.stopPropagation();
+ onTaskClick?.(task);
+ }, [task, onTaskClick]);
+
+ const handleDoubleClick = useCallback((e: React.MouseEvent) => {
+ e.stopPropagation();
+ onTaskDoubleClick?.(task);
+ }, [task, onTaskDoubleClick]);
+
+ // Render task bar with handles
+ const renderTaskBar = () => {
+ const isSelected = false; // TODO: Get from selection state
+ const isDragging = dragState?.isDragging || false;
+
+ return (
+ 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 && (
+
+ )}
+
+ {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 (
+
+ );
+
+ 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/common/invite-team-members/invite-team-members.tsx b/worklenz-frontend/src/components/common/invite-team-members/invite-team-members.tsx
index ad52e7db..ab4ff36a 100644
--- a/worklenz-frontend/src/components/common/invite-team-members/invite-team-members.tsx
+++ b/worklenz-frontend/src/components/common/invite-team-members/invite-team-members.tsx
@@ -1,14 +1,4 @@
-import {
- AutoComplete,
- Button,
- Drawer,
- Flex,
- Form,
- message,
- Select,
- Spin,
- Typography,
-} from '@/shared/antd-imports';
+import { AutoComplete, Button, Drawer, Flex, Form, message, Modal, Select, Spin, Typography } from '@/shared/antd-imports';
import { useAppSelector } from '@/hooks/useAppSelector';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import {
@@ -21,6 +11,7 @@ import { jobTitlesApiService } from '@/api/settings/job-titles/job-titles.api.se
import { IJobTitle } from '@/types/job.types';
import { teamMembersApiService } from '@/api/team-members/teamMembers.api.service';
import { ITeamMemberCreateRequest } from '@/types/teamMembers/team-member-create-request';
+import { LinkOutlined } from '@ant-design/icons';
interface FormValues {
email: string[];
@@ -97,23 +88,33 @@ const InviteTeamMembers = () => {
};
return (
-
{t('addMemberDrawerTitle')}
}
open={isDrawerOpen}
- onClose={handleClose}
+ onCancel={handleClose}
destroyOnClose
afterOpenChange={visible => visible && handleSearch('')}
width={400}
loading={loading}
footer={
-
-
+
+ {/* }
+ disabled
+ >
+ {t('copyTeamLink')}
+ */}
+
+
+
}
>
@@ -186,7 +187,7 @@ const InviteTeamMembers = () => {
/>
-
+
);
};
diff --git a/worklenz-frontend/src/components/common/template-drawer/template-drawer.tsx b/worklenz-frontend/src/components/common/template-drawer/template-drawer.tsx
index 2250b869..9f2e6031 100644
--- a/worklenz-frontend/src/components/common/template-drawer/template-drawer.tsx
+++ b/worklenz-frontend/src/components/common/template-drawer/template-drawer.tsx
@@ -10,6 +10,7 @@ import {
Image,
Input,
Flex,
+ theme,
} from '@/shared/antd-imports';
import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
@@ -37,13 +38,12 @@ const TemplateDrawer: React.FC = ({
showBothTabs = false,
templateSelected = (templateId: string) => {
if (!templateId) return;
- templateId;
},
selectedTemplateType = (type: 'worklenz' | 'custom') => {
- type;
},
}) => {
const themeMode = useSelector((state: RootState) => state.themeReducer.mode);
+ const { token } = theme.useToken();
const { t } = useTranslation('template-drawer');
const [searchQuery, setSearchQuery] = useState('');
@@ -149,7 +149,12 @@ const TemplateDrawer: React.FC = ({
{phase.name}
@@ -171,7 +176,12 @@ const TemplateDrawer: React.FC = ({
{status.name}
@@ -193,7 +203,12 @@ const TemplateDrawer: React.FC = ({
{priority.name}
@@ -215,7 +230,12 @@ const TemplateDrawer: React.FC = ({
{label.name}
@@ -251,14 +271,24 @@ const TemplateDrawer: React.FC = ({
};
const menuContent = (
-
+
{/* Menu Area */}
-
+