import { format } from "date-fns"; import { useRef, useState } from "react"; import { buildAllocationBarSegment, dateStringsFromDayIndices, dayIndexFromPointer, formatAllocationEffort, projectBarStyle, roundToHalfHour, visibleWeekdayCount, type EffortDisplayMode, } from "../../planner/allocationTimeline"; import type { Allocation, Person } from "../../types"; import { AllocationBarMenuTrigger } from "./AllocationBarMenu"; import { AllocationCreatePopover, type AllocationDragDraft } from "./AllocationCreatePopover"; interface AllocationDragTimelineProps { dayDates: Date[]; color: string; fixedPerson?: Person; candidatePeople?: Person[]; transferPeople?: Person[]; allocation?: Allocation; weeklyCapacityHrs?: number; allocationPct?: number; onSave: (draft: AllocationDragDraft) => Promise | Allocation | undefined; onUpdate?: (draft: AllocationDragDraft) => void; onDelete?: (allocation: Allocation) => void; onTransfer?: (personId: number) => void; onStartClone?: () => void; cloneSourceId?: number; onSelectAllToRight?: () => void; onToggleMultiSelect?: () => void; multiSelectActive?: boolean; selected?: boolean; onToggleSelect?: () => void; isSaving?: boolean; effortMode?: EffortDisplayMode; } export function AllocationDragTimeline({ dayDates, color, fixedPerson, candidatePeople = [], allocation, weeklyCapacityHrs, allocationPct, onSave, onUpdate, onDelete, transferPeople = [], onTransfer, onStartClone, cloneSourceId, onSelectAllToRight, onToggleMultiSelect, multiSelectActive = false, selected = false, onToggleSelect, isSaving = false, effortMode = "hours", }: AllocationDragTimelineProps) { const trackRef = useRef(null); const barHostRef = useRef(null); const dragPointerRef = useRef<{ x: number; y: number } | null>(null); const [dragRange, setDragRange] = useState<{ start: number; end: number } | null>(null); const [popover, setPopover] = useState<{ startDate: string; endDate: string; anchor: { top: number; left: number }; allocation?: Allocation; } | null>(null); const totalDays = dayDates.length; const existingSegment = allocation && weeklyCapacityHrs !== undefined && allocationPct !== undefined ? buildAllocationBarSegment(dayDates, allocation.start_date, allocation.end_date) : null; const previewSegment = dragRange && totalDays > 0 ? { startIndex: Math.min(dragRange.start, dragRange.end), endIndex: Math.max(dragRange.start, dragRange.end), } : null; const openPopoverAt = (startDate: string, endDate: string, clientX: number, clientY: number, edit?: Allocation) => { setPopover({ startDate, endDate, anchor: { top: clientY + 8, left: clientX - 180 }, allocation: edit, }); }; const openEditPopover = (event?: React.MouseEvent) => { if (!allocation) return; event?.stopPropagation(); if (multiSelectActive && onToggleSelect) { onToggleSelect(); return; } const rect = event?.currentTarget.getBoundingClientRect() ?? barHostRef.current?.getBoundingClientRect(); if (!rect) return; openPopoverAt( allocation.start_date, allocation.end_date, rect.left + rect.width / 2, rect.bottom, allocation, ); }; const onPointerDown = (event: React.PointerEvent) => { if (!trackRef.current || event.button !== 0) return; if ((event.target as HTMLElement).closest(".allocation-bar-host, .allocation-bar")) return; dragPointerRef.current = { x: event.clientX, y: event.clientY }; const index = dayIndexFromPointer(event.clientX, trackRef.current.getBoundingClientRect(), totalDays); setDragRange({ start: index, end: index }); trackRef.current.setPointerCapture(event.pointerId); }; const onPointerMove = (event: React.PointerEvent) => { if (!dragRange || !trackRef.current) return; const index = dayIndexFromPointer(event.clientX, trackRef.current.getBoundingClientRect(), totalDays); setDragRange((prev) => (prev ? { ...prev, end: index } : null)); }; const onPointerUp = (event: React.PointerEvent) => { if (!trackRef.current?.hasPointerCapture(event.pointerId)) return; trackRef.current.releasePointerCapture(event.pointerId); const pointerStart = dragPointerRef.current; dragPointerRef.current = null; const range = dragRange; setDragRange(null); if (!range) return; const movedPx = pointerStart ? Math.hypot(event.clientX - pointerStart.x, event.clientY - pointerStart.y) : 0; const didDrag = range.start !== range.end || movedPx > 6; if (!didDrag) return; const { start_date, end_date } = dateStringsFromDayIndices(dayDates, range.start, range.end); openPopoverAt(start_date, end_date, event.clientX, event.clientY); }; const pickerPeople = fixedPerson ? [fixedPerson] : candidatePeople; const editingAllocation = popover?.allocation; return ( <>
{dayDates.map((day, index) => (
))} {previewSegment ? (
) : null} {existingSegment && weeklyCapacityHrs !== undefined && allocationPct !== undefined && allocation ? ( (() => { const weekdayCount = visibleWeekdayCount( dayDates, allocation.start_date, allocation.end_date, ); const daySpan = existingSegment.endIndex - existingSegment.startIndex + 1; const widthPct = totalDays > 0 ? (daySpan / totalDays) * 100 : 100; const isCompact = weekdayCount <= 1 || widthPct <= 18; return (
onStartClone?.()} onDelete={onDelete ? () => onDelete(allocation) : undefined} onEdit={() => openEditPopover()} onSelectAllToRight={onSelectAllToRight} onToggleMultiSelect={onToggleMultiSelect} onTransferPerson={onTransfer} transferPeople={transferPeople} />
); })() ) : null}
{popover && pickerPeople.length > 0 ? ( setPopover(null)} onDelete={ editingAllocation && onDelete ? () => { onDelete(editingAllocation); setPopover(null); } : undefined } onSave={async (draft) => { const created = await onSave(draft); if (created) { setPopover({ startDate: created.start_date, endDate: created.end_date, anchor: popover.anchor, allocation: created, }); return; } setPopover(null); }} onUpdate={ editingAllocation && onUpdate ? (draft) => { onUpdate(draft); setPopover(null); } : undefined } people={pickerPeople} startDate={popover.startDate} defaultHoursPerDay={ fixedPerson ? roundToHalfHour(fixedPerson.weekly_capacity_hrs / 5) : undefined } multiSelectActive={multiSelectActive} onClone={ editingAllocation && onStartClone ? () => { onStartClone(); setPopover(null); } : undefined } onSelectAllToRight={ editingAllocation && onSelectAllToRight ? () => { onSelectAllToRight(); setPopover(null); } : undefined } onToggleMultiSelect={ onToggleMultiSelect ? () => { onToggleMultiSelect(); setPopover(null); } : undefined } onTransfer={ editingAllocation && onTransfer ? (personId) => { onTransfer(personId); setPopover(null); } : undefined } transferPeople={transferPeople} /> ) : null} ); }