'use client' import { differenceInDays, format, isWeekend, isToday } from 'date-fns' import type { GanttTask, TimelineItem } from './types' import { getHealthColor } from './lib/progress' import { ROW_HEIGHT, ZOOM_LEVELS, INDENT_WIDTH } from './hooks/useGanttState' import type { UseGanttStateReturn } from './hooks/useGanttState' export interface GanttTimelineViewProps { // Layout props infoColumnWidth: number infoColumnLabel: string showCategory: boolean showStatus: boolean showFullscreen: boolean // State from useGanttState zoomIndex: UseGanttStateReturn['zoomIndex'] setZoomIndex: UseGanttStateReturn['setZoomIndex'] collapsed: UseGanttStateReturn['collapsed'] mounted: UseGanttStateReturn['mounted'] dragState: UseGanttStateReturn['dragState'] focusedRowIndex: UseGanttStateReturn['focusedRowIndex'] dayWidth: UseGanttStateReturn['dayWidth'] tasks: UseGanttStateReturn['tasks'] startDate: UseGanttStateReturn['startDate'] days: UseGanttStateReturn['days'] months: UseGanttStateReturn['months'] timeHeaderUnits: UseGanttStateReturn['timeHeaderUnits'] dependencyPaths: UseGanttStateReturn['dependencyPaths'] timelineWidth: UseGanttStateReturn['timelineWidth'] bodyHeight: UseGanttStateReturn['bodyHeight'] isFullscreen: UseGanttStateReturn['isFullscreen'] categoryColors: UseGanttStateReturn['categoryColors'] onDateChange: UseGanttStateReturn['onDateChange'] // Refs bodyScrollRef: UseGanttStateReturn['bodyScrollRef'] headerTimelineRef: UseGanttStateReturn['headerTimelineRef'] infoColumnRef: UseGanttStateReturn['infoColumnRef'] // Handlers toggleFullscreen: UseGanttStateReturn['toggleFullscreen'] handleItemClick: UseGanttStateReturn['handleItemClick'] toggleCollapse: UseGanttStateReturn['toggleCollapse'] handleBodyScroll: UseGanttStateReturn['handleBodyScroll'] getBarPosition: UseGanttStateReturn['getBarPosition'] handleDragStart: UseGanttStateReturn['handleDragStart'] } export function GanttTimelineView({ infoColumnWidth, infoColumnLabel, showCategory, showStatus, showFullscreen, zoomIndex, setZoomIndex, collapsed, mounted, dragState, focusedRowIndex, dayWidth, tasks, startDate, days, months, timeHeaderUnits, dependencyPaths, timelineWidth, bodyHeight, isFullscreen, categoryColors, onDateChange, bodyScrollRef, headerTimelineRef, infoColumnRef, toggleFullscreen, handleItemClick, toggleCollapse, handleBodyScroll, getBarPosition, handleDragStart, }: GanttTimelineViewProps) { return ( <> {/* Header row */}
{infoColumnLabel}
setZoomIndex(parseInt(e.target.value))} className="gantt-zoom-slider" aria-label={`Zoom level: ${dayWidth}px per day`} /> {showFullscreen && ( )}
{months.map((m, i) => (
{m.label}
))}
{timeHeaderUnits.mode === 'weeks' ? ( timeHeaderUnits.units.map((unit, i) => (
{unit.width > 40 ? unit.label : ''}
)) ) : ( timeHeaderUnits.units.map((unit, i) => (
{unit.label}
)) )}
{/* Scrollable body */}
{tasks.map((task, rowIdx) => (
{task.hasChildren ? ( ) : ()} handleItemClick(task.item)} title={task.item.title} style={{ cursor: 'pointer' }}> {task.item.title} {showCategory && task.item.category && ( {task.item.category} )} {showStatus && ( {task.item.status.replace(/_/g, ' ')} )}
))}
{days.map((day, i) => (
))} {tasks.map((_, i) => (
))} {dependencyPaths.length > 0 && ( )} {tasks.map((task, rowIndex) => { const isDragging = dragState?.taskId === task.item.id const pos = getBarPosition(task, rowIndex, true) const barColor = getHealthColor(task.healthStatus) const dates = isDragging ? { start: dragState!.currentStart, end: dragState!.currentEnd } : { start: task.start, end: task.end } return (
!dragState && handleItemClick(task.item)} onMouseDown={(e) => handleDragStart(e, task, 'move')} title={`${task.item.title}\n${format(dates.start, 'MMM d, yyyy')} \u2013 ${format(dates.end, 'MMM d, yyyy')}\nProgress: ${task.progress}%`} > {onDateChange && ( <>
{ e.stopPropagation(); handleDragStart(e, task, 'resize-start') }} />
{ e.stopPropagation(); handleDragStart(e, task, 'resize-end') }} /> )} {task.timeProgress > 0 && task.timeProgress < 100 && (
)} {task.progress > 0 && (
task.timeProgress + 10 ? 'ahead' : ''}`} style={{ width: `${task.progress}%` }} /> )} {pos.width > 80 ? task.item.title.replace(/^\[[^\]]+\]\s*/, '') : ''} {isDragging && (
{format(dates.start, 'MMM d')} \u2013 {format(dates.end, 'MMM d')}
)}
) })} {mounted && (() => { const todayOffset = differenceInDays(new Date(), startDate) if (todayOffset >= 0 && todayOffset < days.length) return
return null })()}
On Track
At Risk
Blocked
Not Started
Today
) }