import { useEffect, useRef, useState, useCallback } from 'react' import Gantt, { GanttTask, ViewMode } from 'frappe-gantt' import api from '../services/api' import { dependenciesApi, TaskDependency, DependencyType } from '../services/dependencies' interface Task { id: string project_id: string title: string description: string | null priority: string status_id: string | null status_name: string | null status_color: string | null assignee_id: string | null assignee_name: string | null due_date: string | null start_date: string | null time_estimate: number | null subtask_count: number progress?: number } interface TaskStatus { id: string name: string color: string is_done: boolean } interface GanttChartProps { projectId: string tasks: Task[] statuses: TaskStatus[] onTaskClick: (task: Task) => void onTaskUpdate: () => void } // Priority colors for custom styling const priorityClasses: Record = { urgent: 'gantt-bar-urgent', high: 'gantt-bar-high', medium: 'gantt-bar-medium', low: 'gantt-bar-low', } export function GanttChart({ projectId, tasks, statuses, onTaskClick, onTaskUpdate, }: GanttChartProps) { const ganttRef = useRef(null) const ganttInstance = useRef(null) const [viewMode, setViewMode] = useState('Week') const [dependencies, setDependencies] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) // Dependency management state const [showDependencyModal, setShowDependencyModal] = useState(false) const [selectedTaskForDependency, setSelectedTaskForDependency] = useState(null) const [selectedPredecessor, setSelectedPredecessor] = useState('') const [selectedDependencyType, setSelectedDependencyType] = useState('FS') const [dependencyError, setDependencyError] = useState(null) // Task data mapping for quick lookup const taskMap = useRef>(new Map()) // Load dependencies useEffect(() => { loadDependencies() }, [projectId]) const loadDependencies = async () => { try { const deps = await dependenciesApi.getProjectDependencies(projectId) setDependencies(deps) } catch (err) { console.error('Failed to load dependencies:', err) } } // Transform tasks to Gantt format const transformTasksToGantt = useCallback((): GanttTask[] => { const today = new Date() const defaultEndDate = new Date(today) defaultEndDate.setDate(defaultEndDate.getDate() + 7) // Update task map taskMap.current.clear() tasks.forEach((task) => taskMap.current.set(task.id, task)) // Build dependency string map (task_id -> comma-separated predecessor ids) const dependencyMap = new Map() dependencies.forEach((dep) => { const existing = dependencyMap.get(dep.successor_id) || [] existing.push(dep.predecessor_id) dependencyMap.set(dep.successor_id, existing) }) return tasks .filter((task) => task.start_date || task.due_date) // Only tasks with dates .map((task) => { // Determine dates let startDate: Date let endDate: Date if (task.start_date && task.due_date) { startDate = new Date(task.start_date) endDate = new Date(task.due_date) } else if (task.start_date) { startDate = new Date(task.start_date) endDate = new Date(startDate) endDate.setDate(endDate.getDate() + 7) // Default 7 days duration } else if (task.due_date) { endDate = new Date(task.due_date) startDate = new Date(endDate) startDate.setDate(startDate.getDate() - 7) // Default start 7 days before } else { startDate = today endDate = defaultEndDate } // Calculate progress based on status let progress = task.progress ?? 0 if (task.status_id) { const status = statuses.find((s) => s.id === task.status_id) if (status?.is_done) { progress = 100 } } // Get dependencies for this task const taskDeps = dependencyMap.get(task.id) const dependencyString = taskDeps ? taskDeps.join(', ') : '' // Custom class based on priority const customClass = priorityClasses[task.priority] || priorityClasses.medium return { id: task.id, name: task.title, start: startDate, end: endDate, progress, dependencies: dependencyString, custom_class: customClass, // Store original task data for reference _task: task, } }) }, [tasks, statuses, dependencies]) // Initialize and update Gantt chart useEffect(() => { if (!ganttRef.current) return const ganttTasks = transformTasksToGantt() if (ganttTasks.length === 0) { // Clear the gantt if no tasks ganttInstance.current = null return } // Create or update Gantt instance if (!ganttInstance.current) { // Clear container first ganttRef.current.innerHTML = '' ganttInstance.current = new Gantt(ganttRef.current, ganttTasks, { header_height: 50, column_width: 30, step: 24, view_modes: ['Day', 'Week', 'Month'], bar_height: 24, bar_corner_radius: 3, arrow_curve: 5, padding: 18, view_mode: viewMode, date_format: 'YYYY-MM-DD', language: 'en', custom_popup_html: (task: GanttTask) => { const originalTask = taskMap.current.get(task.id) if (!originalTask) return '' const assignee = originalTask.assignee_name || 'Unassigned' const statusName = originalTask.status_name || 'No Status' const priority = originalTask.priority.charAt(0).toUpperCase() + originalTask.priority.slice(1) return `

${task.name}

Assignee: ${assignee}
Status: ${statusName}
Priority: ${priority}
Progress: ${task.progress}%
` }, on_click: (task: GanttTask) => { const originalTask = taskMap.current.get(task.id) if (originalTask) { onTaskClick(originalTask) } }, on_date_change: async (task: GanttTask, start: Date, end: Date) => { await handleDateChange(task.id, start, end) }, on_progress_change: async (task: GanttTask, progress: number) => { await handleProgressChange(task.id, progress) }, }) } else { // Refresh existing instance ganttInstance.current.refresh(ganttTasks) } }, [tasks, dependencies, transformTasksToGantt, onTaskClick]) // Update view mode useEffect(() => { if (ganttInstance.current) { ganttInstance.current.change_view_mode(viewMode) } }, [viewMode]) // Handle date change (drag/resize) const handleDateChange = async (taskId: string, start: Date, end: Date) => { setError(null) setLoading(true) // Format dates const startDate = start.toISOString().split('T')[0] const dueDate = end.toISOString().split('T')[0] try { await api.patch(`/tasks/${taskId}`, { start_date: startDate, due_date: dueDate, }) onTaskUpdate() } catch (err: unknown) { console.error('Failed to update task dates:', err) const error = err as { response?: { data?: { detail?: string } } } const errorMessage = error.response?.data?.detail || 'Failed to update task dates' setError(errorMessage) // Refresh to rollback visual changes onTaskUpdate() } finally { setLoading(false) } } // Handle progress change const handleProgressChange = async (taskId: string, progress: number) => { // Progress changes could update task status in the future // For now, just log it console.log(`Task ${taskId} progress changed to ${progress}%`) } // Add dependency const handleAddDependency = async () => { if (!selectedTaskForDependency || !selectedPredecessor) return setDependencyError(null) try { await dependenciesApi.addDependency(selectedTaskForDependency.id, { predecessor_id: selectedPredecessor, dependency_type: selectedDependencyType, }) await loadDependencies() setShowDependencyModal(false) setSelectedTaskForDependency(null) setSelectedPredecessor('') setSelectedDependencyType('FS') } catch (err: unknown) { console.error('Failed to add dependency:', err) const error = err as { response?: { data?: { detail?: string } } } const errorMessage = error.response?.data?.detail || 'Failed to add dependency' setDependencyError(errorMessage) } } // Remove dependency const handleRemoveDependency = async (dependencyId: string) => { try { await dependenciesApi.removeDependency(dependencyId) await loadDependencies() } catch (err) { console.error('Failed to remove dependency:', err) setError('Failed to remove dependency') } } // Open dependency modal for a task const openDependencyModal = (task: Task) => { setSelectedTaskForDependency(task) setSelectedPredecessor('') setDependencyError(null) setShowDependencyModal(true) } // Get available predecessors (tasks that can be added as dependencies) const getAvailablePredecessors = () => { if (!selectedTaskForDependency) return [] // Get existing predecessor IDs for selected task const existingPredecessorIds = new Set( dependencies .filter((d) => d.successor_id === selectedTaskForDependency.id) .map((d) => d.predecessor_id) ) // Filter out the selected task itself and already added predecessors return tasks.filter( (t) => t.id !== selectedTaskForDependency.id && !existingPredecessorIds.has(t.id) && (t.start_date || t.due_date) // Only tasks with dates ) } // Get current dependencies for selected task const getCurrentDependencies = () => { if (!selectedTaskForDependency) return [] return dependencies.filter((d) => d.successor_id === selectedTaskForDependency.id) } // Check if there are tasks with dates const tasksWithDates = tasks.filter((t) => t.start_date || t.due_date) return (
{/* Toolbar */}
Zoom: {(['Day', 'Week', 'Month'] as ViewMode[]).map((mode) => ( ))}
{loading && Saving...} {error && {error}}
{/* Gantt Chart */} {tasksWithDates.length > 0 ? (
) : (

No tasks with dates to display.

Add start dates and due dates to your tasks to see them on the Gantt chart.

)} {/* Legend and Actions */}
Priority:
Urgent
High
Medium
Low
Click a task to view details. Drag to change dates.
{/* Dependencies Section */} {tasksWithDates.length > 0 && (

Task Dependencies

Select a task below to manage its dependencies.

{tasksWithDates.map((task) => { const taskDeps = dependencies.filter((d) => d.successor_id === task.id) return (
{task.title} {taskDeps.length > 0 && ( Depends on: {taskDeps.map((d) => { const predecessor = tasks.find((t) => t.id === d.predecessor_id) return predecessor?.title || 'Unknown' }).join(', ')} )}
) })}
)} {/* Dependency Modal */} {showDependencyModal && selectedTaskForDependency && (

Manage Dependencies for "{selectedTaskForDependency.title}"

{dependencyError && (
{dependencyError}
)} {/* Current Dependencies */}

Current Dependencies

{getCurrentDependencies().length === 0 ? (

No dependencies yet

) : (
    {getCurrentDependencies().map((dep) => { const predecessor = tasks.find((t) => t.id === dep.predecessor_id) return (
  • Depends on: {predecessor?.title || 'Unknown'} ({dep.dependency_type})
  • ) })}
)}
{/* Add New Dependency */}

Add Dependency

{getAvailablePredecessors().length === 0 ? (

No available tasks to add as dependencies

) : (
)}
)} {/* Custom CSS for Gantt */}
) } // Custom CSS styles for Gantt (including base frappe-gantt styles) const ganttStyles = ` /* Base Frappe Gantt CSS */ :root{--g-arrow-color: #1f2937;--g-bar-color: #fff;--g-bar-border: #fff;--g-tick-color-thick: #ededed;--g-tick-color: #f3f3f3;--g-actions-background: #f3f3f3;--g-border-color: #ebeff2;--g-text-muted: #7c7c7c;--g-text-light: #fff;--g-text-dark: #171717;--g-progress-color: #dbdbdb;--g-handle-color: #37352f;--g-weekend-label-color: #dcdce4;--g-expected-progress: #c4c4e9;--g-header-background: #fff;--g-row-color: #fdfdfd;--g-row-border-color: #c7c7c7;--g-today-highlight: #37352f;--g-popup-actions: #ebeff2;--g-weekend-highlight-color: #f7f7f7}.gantt-container{line-height:14.5px;position:relative;overflow:auto;font-size:12px;height:var(--gv-grid-height);width:100%;border-radius:8px}.gantt-container .popup-wrapper{position:absolute;top:0;left:0;background:#fff;box-shadow:0 10px 24px -3px #0003;padding:10px;border-radius:5px;width:max-content;z-index:1000}.gantt-container .popup-wrapper .title{margin-bottom:2px;color:var(--g-text-dark);font-size:.85rem;font-weight:650;line-height:15px}.gantt-container .popup-wrapper .subtitle{color:var(--g-text-dark);font-size:.8rem;margin-bottom:5px}.gantt-container .popup-wrapper .details{color:var(--g-text-muted);font-size:.7rem}.gantt-container .popup-wrapper .actions{margin-top:10px;margin-left:3px}.gantt-container .popup-wrapper .action-btn{border:none;padding:5px 8px;background-color:var(--g-popup-actions);border-right:1px solid var(--g-text-light)}.gantt-container .popup-wrapper .action-btn:hover{background-color:brightness(97%)}.gantt-container .popup-wrapper .action-btn:first-child{border-top-left-radius:4px;border-bottom-left-radius:4px}.gantt-container .popup-wrapper .action-btn:last-child{border-right:none;border-top-right-radius:4px;border-bottom-right-radius:4px}.gantt-container .grid-header{height:calc(var(--gv-lower-header-height) + var(--gv-upper-header-height) + 10px);background-color:var(--g-header-background);position:sticky;top:0;left:0;border-bottom:1px solid var(--g-row-border-color);z-index:1000}.gantt-container .lower-text,.gantt-container .upper-text{text-anchor:middle}.gantt-container .upper-header{height:var(--gv-upper-header-height)}.gantt-container .lower-header{height:var(--gv-lower-header-height)}.gantt-container .lower-text{font-size:12px;position:absolute;width:calc(var(--gv-column-width) * .8);height:calc(var(--gv-lower-header-height) * .8);margin:0 calc(var(--gv-column-width) * .1);align-content:center;text-align:center;color:var(--g-text-muted)}.gantt-container .upper-text{position:absolute;width:fit-content;font-weight:500;font-size:14px;color:var(--g-text-dark);height:calc(var(--gv-lower-header-height) * .66)}.gantt-container .current-upper{position:sticky;left:0!important;padding-left:17px;background:#fff}.gantt-container .side-header{position:sticky;top:0;right:0;float:right;z-index:1000;line-height:20px;font-weight:400;width:max-content;margin-left:auto;padding-right:10px;padding-top:10px;background:var(--g-header-background);display:flex}.gantt-container .side-header *{transition-property:background-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;background-color:var(--g-actions-background);border-radius:.5rem;border:none;padding:5px 8px;color:var(--g-text-dark);font-size:14px;letter-spacing:.02em;font-weight:420;box-sizing:content-box;margin-right:5px}.gantt-container .side-header *:last-child{margin-right:0}.gantt-container .side-header *:hover{filter:brightness(97.5%)}.gantt-container .side-header select{width:60px;padding-top:2px;padding-bottom:2px}.gantt-container .side-header select:focus{outline:none}.gantt-container .date-range-highlight{background-color:var(--g-progress-color);border-radius:12px;height:calc(var(--gv-lower-header-height) - 6px);top:calc(var(--gv-upper-header-height) + 5px);position:absolute}.gantt-container .current-highlight{position:absolute;background:var(--g-today-highlight);width:1px;z-index:999}.gantt-container .current-ball-highlight{position:absolute;background:var(--g-today-highlight);z-index:1001;border-radius:50%}.gantt-container .current-date-highlight{background:var(--g-today-highlight);color:var(--g-text-light);border-radius:5px}.gantt-container .holiday-label{position:absolute;top:0;left:0;opacity:0;z-index:1000;background:--g-weekend-label-color;border-radius:5px;padding:2px 5px}.gantt-container .holiday-label.show{opacity:100}.gantt-container .extras{position:sticky;left:0}.gantt-container .extras .adjust{position:absolute;left:8px;top:calc(var(--gv-grid-height) - 60px);background-color:#000000b3;color:#fff;border:none;padding:8px;border-radius:3px}.gantt-container .hide{display:none}.gantt{user-select:none;-webkit-user-select:none;position:absolute}.gantt .grid-background{fill:none}.gantt .grid-row{fill:var(--g-row-color)}.gantt .row-line{stroke:var(--g-border-color)}.gantt .tick{stroke:var(--g-tick-color);stroke-width:.4}.gantt .tick.thick{stroke:var(--g-tick-color-thick);stroke-width:.7}.gantt .arrow{fill:none;stroke:var(--g-arrow-color);stroke-width:1.5}.gantt .bar-wrapper .bar{fill:var(--g-bar-color);stroke:var(--g-bar-border);stroke-width:0;transition:stroke-width .3s ease}.gantt .bar-progress{fill:var(--g-progress-color);border-radius:4px}.gantt .bar-expected-progress{fill:var(--g-expected-progress)}.gantt .bar-invalid{fill:transparent;stroke:var(--g-bar-border);stroke-width:1;stroke-dasharray:5}:is(.gantt .bar-invalid)~.bar-label{fill:var(--g-text-light)}.gantt .bar-label{fill:var(--g-text-dark);dominant-baseline:central;font-family:Helvetica;font-size:13px;font-weight:400}.gantt .bar-label.big{fill:var(--g-text-dark);text-anchor:start}.gantt .handle{fill:var(--g-handle-color);opacity:0;transition:opacity .3s ease}.gantt .handle.active,.gantt .handle.visible{cursor:ew-resize;opacity:1}.gantt .handle.progress{fill:var(--g-text-muted)}.gantt .bar-wrapper{cursor:pointer}.gantt .bar-wrapper .bar{outline:1px solid var(--g-row-border-color);border-radius:3px}.gantt .bar-wrapper:hover .bar{transition:transform .3s ease}.gantt .bar-wrapper:hover .date-range-highlight{display:block} /* Override Frappe Gantt styles to match app theme */ .gantt .bar-wrapper .bar { fill: #0066cc; stroke: #0066cc; stroke-width: 0; } .gantt .bar-wrapper .bar-progress { fill: #004c99; } .gantt .bar-wrapper .bar-label { fill: white; font-size: 12px; font-weight: 500; } /* Priority-based bar colors */ .gantt .bar-wrapper.gantt-bar-urgent .bar { fill: #f44336; stroke: #d32f2f; } .gantt .bar-wrapper.gantt-bar-urgent .bar-progress { fill: #c62828; } .gantt .bar-wrapper.gantt-bar-high .bar { fill: #ff9800; stroke: #f57c00; } .gantt .bar-wrapper.gantt-bar-high .bar-progress { fill: #ef6c00; } .gantt .bar-wrapper.gantt-bar-medium .bar { fill: #0066cc; stroke: #0052a3; } .gantt .bar-wrapper.gantt-bar-medium .bar-progress { fill: #004080; } .gantt .bar-wrapper.gantt-bar-low .bar { fill: #808080; stroke: #666666; } .gantt .bar-wrapper.gantt-bar-low .bar-progress { fill: #5a5a5a; } /* Arrow (dependency) styling */ .gantt .arrow { stroke: #666; stroke-width: 2; } /* Grid styling */ .gantt .grid-row { fill: transparent; } .gantt .grid-row:nth-child(even) { fill: #f9f9f9; } .gantt .row-line { stroke: #e0e0e0; } .gantt .tick { stroke: #e0e0e0; } .gantt .today-highlight { fill: rgba(0, 102, 204, 0.1); } /* Header styling */ .gantt .lower-text, .gantt .upper-text { fill: #333; font-size: 12px; } /* Popup styling */ .gantt-popup { padding: 10px; min-width: 180px; } .gantt-popup-title { margin: 0 0 8px 0; font-size: 14px; font-weight: 600; color: #333; } .gantt-popup-info { display: flex; flex-direction: column; gap: 4px; } .gantt-popup-row { display: flex; gap: 8px; font-size: 12px; } .gantt-popup-label { color: #666; min-width: 60px; } .gantt-popup-value { color: #333; font-weight: 500; } /* Popup container override */ .gantt .popup-wrapper { background: white; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); border: 1px solid #e0e0e0; } ` const styles: Record = { container: { display: 'flex', flexDirection: 'column', gap: '16px', }, toolbar: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 16px', backgroundColor: '#f9f9f9', borderRadius: '8px', border: '1px solid #eee', }, viewModeButtons: { display: 'flex', alignItems: 'center', gap: '8px', }, toolbarLabel: { fontSize: '14px', fontWeight: 500, color: '#666', marginRight: '4px', }, viewModeButton: { padding: '6px 16px', backgroundColor: 'white', border: '1px solid #ddd', borderRadius: '4px', cursor: 'pointer', fontSize: '13px', color: '#666', transition: 'all 0.2s', }, viewModeButtonActive: { backgroundColor: '#0066cc', borderColor: '#0066cc', color: 'white', }, loadingIndicator: { fontSize: '13px', color: '#666', }, errorIndicator: { fontSize: '13px', color: '#f44336', backgroundColor: '#ffebee', padding: '4px 12px', borderRadius: '4px', }, ganttWrapper: { backgroundColor: 'white', borderRadius: '8px', boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)', overflow: 'auto', }, ganttContainer: { minHeight: '300px', }, emptyState: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '64px 24px', backgroundColor: 'white', borderRadius: '8px', boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)', }, emptyText: { fontSize: '16px', color: '#333', margin: '0 0 8px 0', }, emptyHint: { fontSize: '14px', color: '#666', margin: 0, }, footer: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 16px', backgroundColor: '#f9f9f9', borderRadius: '8px', fontSize: '13px', }, legend: { display: 'flex', alignItems: 'center', gap: '16px', }, legendTitle: { color: '#666', fontWeight: 500, }, legendItem: { display: 'flex', alignItems: 'center', gap: '6px', color: '#666', }, legendDot: { width: '12px', height: '12px', borderRadius: '3px', }, footerActions: {}, footerHint: { color: '#888', fontStyle: 'italic', }, dependenciesSection: { padding: '16px', backgroundColor: 'white', borderRadius: '8px', boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)', }, dependenciesTitle: { margin: '0 0 8px 0', fontSize: '16px', fontWeight: 600, color: '#333', }, dependenciesHint: { margin: '0 0 16px 0', fontSize: '13px', color: '#666', }, taskList: { display: 'flex', flexDirection: 'column', gap: '8px', }, taskItem: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px', backgroundColor: '#f9f9f9', borderRadius: '6px', border: '1px solid #eee', }, taskItemInfo: { display: 'flex', flexDirection: 'column', gap: '4px', }, taskItemTitle: { fontSize: '14px', fontWeight: 500, color: '#333', }, taskItemDeps: { fontSize: '12px', color: '#666', }, manageDepsButton: { padding: '6px 12px', backgroundColor: 'white', border: '1px solid #ddd', borderRadius: '4px', cursor: 'pointer', fontSize: '12px', color: '#666', }, modalOverlay: { position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0, 0, 0, 0.5)', display: 'flex', justifyContent: 'center', alignItems: 'center', zIndex: 1000, }, modal: { backgroundColor: 'white', padding: '24px', borderRadius: '8px', width: '500px', maxWidth: '90%', maxHeight: '80vh', overflowY: 'auto', }, modalTitle: { margin: '0 0 16px 0', fontSize: '18px', fontWeight: 600, color: '#333', }, modalError: { padding: '10px 14px', backgroundColor: '#ffebee', color: '#c62828', borderRadius: '4px', fontSize: '13px', marginBottom: '16px', }, modalSection: { marginBottom: '20px', }, modalSectionTitle: { margin: '0 0 10px 0', fontSize: '14px', fontWeight: 500, color: '#333', }, modalEmptyText: { fontSize: '13px', color: '#666', fontStyle: 'italic', }, dependencyList: { margin: 0, padding: 0, listStyle: 'none', }, dependencyItem: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '10px 12px', backgroundColor: '#f5f5f5', borderRadius: '4px', marginBottom: '8px', fontSize: '13px', }, depType: { color: '#888', marginLeft: '6px', fontSize: '12px', }, removeDependencyButton: { padding: '4px 10px', backgroundColor: '#ffebee', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px', color: '#c62828', }, addDependencyForm: { display: 'flex', gap: '12px', }, modalSelect: { flex: 1, padding: '10px', border: '1px solid #ddd', borderRadius: '4px', fontSize: '14px', }, dependencyTypeSelect: { padding: '10px', border: '1px solid #ddd', borderRadius: '4px', fontSize: '14px', minWidth: '180px', }, addDependencyButton: { padding: '10px 20px', backgroundColor: '#0066cc', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '14px', }, buttonDisabled: { backgroundColor: '#ccc', cursor: 'not-allowed', }, modalActions: { display: 'flex', justifyContent: 'flex-end', marginTop: '24px', }, closeButton: { padding: '10px 24px', backgroundColor: '#f5f5f5', border: '1px solid #ddd', borderRadius: '4px', cursor: 'pointer', fontSize: '14px', }, } export default GanttChart