refactor(gantt): restructure Gantt components and update styles

- Deleted obsolete Gantt components including GanttChart, GanttTaskList, GanttTimeline, GanttToolbar, and their associated styles.
- Renamed ProjectViewGantt component for consistency in naming conventions.
- Updated CSS for task list scrollbar to hide the scrollbar and improve visual aesthetics.
- Introduced new styles for consistent row heights, improved hover states, and enhanced button interactions.
- Added functionality for phase management and task animations to enhance user experience.
This commit is contained in:
Chamika J
2025-08-05 12:21:08 +05:30
parent 9c4293e7a9
commit d33a7db253
9 changed files with 732 additions and 353 deletions

View File

@@ -0,0 +1,276 @@
import React, { useState, useCallback, useRef, useMemo } from 'react';
import { Spin, message } from '@/shared/antd-imports';
import { useParams } from 'react-router-dom';
import GanttTimeline from './components/gantt-timeline/GanttTimeline';
import GanttTaskList from './components/gantt-task-list/GanttTaskList';
import GanttChart from './components/gantt-chart/GanttChart';
import GanttToolbar from './components/gantt-toolbar/GanttToolbar';
import ManagePhaseModal from '@components/task-management/ManagePhaseModal';
import { GanttProvider } from './context/gantt-context';
import { GanttViewMode } from './types/gantt-types';
import {
useGetRoadmapTasksQuery,
useGetProjectPhasesQuery,
transformToGanttTasks,
transformToGanttPhases,
} from './services/gantt-api.service';
import { TimelineUtils } from './utils/timeline-calculator';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import {
setShowTaskDrawer,
setSelectedTaskId,
setTaskFormViewModel,
} from '@features/task-drawer/task-drawer.slice';
import { DEFAULT_TASK_NAME } from '@/shared/constants';
import './gantt-styles.css';
const ProjectViewGantt: React.FC = React.memo(() => {
const { projectId } = useParams<{ projectId: string }>();
const dispatch = useAppDispatch();
const [viewMode, setViewMode] = useState<GanttViewMode>('month');
const [showPhaseModal, setShowPhaseModal] = useState(false);
const [expandedTasks, setExpandedTasks] = useState<Set<string>>(new Set());
const timelineRef = useRef<HTMLDivElement>(null);
const chartRef = useRef<HTMLDivElement>(null);
const taskListRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
// RTK Query hooks
const {
data: tasksResponse,
error: tasksError,
isLoading: tasksLoading,
refetch: refetchTasks,
} = useGetRoadmapTasksQuery({ projectId: projectId || '' }, { skip: !projectId });
const {
data: phasesResponse,
error: phasesError,
isLoading: phasesLoading,
refetch: refetchPhases,
} = useGetProjectPhasesQuery({ projectId: projectId || '' }, { skip: !projectId });
// Transform API data to component format
const tasks = useMemo(() => {
if (tasksResponse?.body && phasesResponse?.body) {
const transformedTasks = transformToGanttTasks(tasksResponse.body, phasesResponse.body);
const result: any[] = [];
transformedTasks.forEach(task => {
// Always show phase milestones
if (task.type === 'milestone' || task.is_milestone) {
result.push(task);
// If this phase is expanded, show its children tasks
const phaseId = task.id === 'phase-unmapped' ? 'unmapped' : task.phase_id;
if (expandedTasks.has(phaseId) && task.children) {
task.children.forEach((child: any) => {
result.push({
...child,
phase_id: task.phase_id // Ensure child has correct phase_id
});
});
}
}
});
return result;
}
return [];
}, [tasksResponse, phasesResponse, expandedTasks]);
const phases = useMemo(() => {
if (phasesResponse?.body) {
return transformToGanttPhases(phasesResponse.body);
}
return [];
}, [phasesResponse]);
// Calculate date range based on tasks
const dateRange = useMemo(() => {
if (tasks.length > 0) {
return TimelineUtils.getSmartDateRange(tasks, viewMode);
}
return { start: new Date(), end: new Date() };
}, [tasks, viewMode]);
const loading = tasksLoading || phasesLoading;
const handleViewModeChange = useCallback((mode: GanttViewMode) => {
setViewMode(mode);
}, []);
const handleChartScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
const target = e.target as HTMLDivElement;
// Sync horizontal scroll with timeline
if (timelineRef.current) {
timelineRef.current.scrollLeft = target.scrollLeft;
}
// Sync vertical scroll with task list
if (taskListRef.current) {
taskListRef.current.scrollTop = target.scrollTop;
}
}, []);
const handleTaskListScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
const target = e.target as HTMLDivElement;
// Sync vertical scroll with chart
if (chartRef.current) {
chartRef.current.scrollTop = target.scrollTop;
}
}, []);
const handleRefresh = useCallback(() => {
refetchTasks();
refetchPhases();
}, [refetchTasks, refetchPhases]);
const handleCreatePhase = useCallback(() => {
setShowPhaseModal(true);
}, []);
const handleCreateTask = useCallback(
(phaseId?: string) => {
// Create a new task using the task drawer
const newTaskViewModel = {
id: null,
name: DEFAULT_TASK_NAME,
project_id: projectId,
phase_id: phaseId || null,
// Add other default properties as needed
};
dispatch(setSelectedTaskId(null));
dispatch(setTaskFormViewModel(newTaskViewModel));
dispatch(setShowTaskDrawer(true));
},
[dispatch, projectId]
);
const handleTaskClick = useCallback(
(taskId: string) => {
// Open existing task in the task drawer
dispatch(setSelectedTaskId(taskId));
dispatch(setTaskFormViewModel(null)); // Clear form view model for existing task
dispatch(setShowTaskDrawer(true));
},
[dispatch]
);
const handleClosePhaseModal = useCallback(() => {
setShowPhaseModal(false);
}, []);
const handlePhaseReorder = useCallback((oldIndex: number, newIndex: number) => {
// TODO: Implement phase reordering API call
console.log('Reorder phases:', { oldIndex, newIndex });
message.info('Phase reordering will be implemented with the backend API');
}, []);
const handleCreateQuickTask = useCallback(
(taskName: string, phaseId?: string) => {
// Refresh the Gantt data after task creation
refetchTasks();
message.success(`Task "${taskName}" created successfully!`);
},
[refetchTasks]
);
// Handle errors
if (tasksError || phasesError) {
message.error('Failed to load Gantt chart data');
}
if (loading) {
return (
<div className="flex justify-center items-center h-full w-full">
<Spin size="large" />
</div>
);
}
return (
<GanttProvider
value={{
tasks,
phases,
viewMode,
projectId: projectId || '',
dateRange,
onRefresh: handleRefresh,
}}
>
<div
className="flex flex-col h-full w-full bg-gray-50 dark:bg-gray-900"
style={{ height: 'calc(100vh - 64px)' }}
>
<GanttToolbar
viewMode={viewMode}
onViewModeChange={handleViewModeChange}
dateRange={dateRange}
onCreatePhase={handleCreatePhase}
onCreateTask={handleCreateTask}
/>
<div className="flex flex-1 overflow-hidden border border-gray-200 dark:border-gray-700 rounded-md bg-white dark:bg-gray-800">
<div className="relative flex w-full h-full">
{/* Fixed Task List - positioned absolutely to avoid scrollbar interference */}
<div className="absolute left-0 top-0 bottom-0 z-20 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700">
<GanttTaskList
tasks={tasks}
projectId={projectId || ''}
viewMode={viewMode}
onTaskClick={handleTaskClick}
onCreateTask={handleCreateTask}
onCreateQuickTask={handleCreateQuickTask}
onPhaseReorder={handlePhaseReorder}
ref={taskListRef}
onScroll={handleTaskListScroll}
expandedTasks={expandedTasks}
onExpandedTasksChange={setExpandedTasks}
/>
</div>
{/* Scrollable Timeline and Chart - with left margin for task list */}
<div
className="flex-1 flex flex-col overflow-hidden gantt-timeline-container"
style={{ marginLeft: '444px' }}
ref={containerRef}
>
<GanttTimeline
viewMode={viewMode}
ref={timelineRef}
containerRef={containerRef}
dateRange={dateRange}
/>
<GanttChart
tasks={tasks}
viewMode={viewMode}
ref={chartRef}
onScroll={handleChartScroll}
containerRef={containerRef}
dateRange={dateRange}
phases={phases}
expandedTasks={expandedTasks}
/>
</div>
</div>
</div>
</div>
{/* Phase Management Modal */}
<ManagePhaseModal
open={showPhaseModal}
onClose={handleClosePhaseModal}
projectId={projectId}
/>
</GanttProvider>
);
});
ProjectViewGantt.displayName = 'ProjectViewGantt';
export default ProjectViewGantt;