refactor(web): decompose TimelineView, ReportBuilder, and ResourceModal into focused components
Extract overlay/popover JSX from TimelineView (1268→1037 lines) into TimelineDragOverlays and TimelinePopovers. Extract ResourceMonthConfigSection from ReportBuilder (1132→1018 lines). Extract ResourceSkillsEditor and ResourceOrgClassification from ResourceModal (1035→714 lines). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
import { MILLISECONDS_PER_DAY } from "@capakraken/shared";
|
||||
import type { useTimelineDrag } from "~/hooks/useTimelineDrag.js";
|
||||
import { formatDateShort } from "~/lib/format.js";
|
||||
import { ShiftPreviewTooltip } from "./ShiftPreviewTooltip.js";
|
||||
|
||||
interface TimelineDragOverlaysProps {
|
||||
dragState: ReturnType<typeof useTimelineDrag>["dragState"];
|
||||
allocDragState: ReturnType<typeof useTimelineDrag>["allocDragState"];
|
||||
rangeState: ReturnType<typeof useTimelineDrag>["rangeState"];
|
||||
multiSelectState: ReturnType<typeof useTimelineDrag>["multiSelectState"];
|
||||
shiftPreview: ReturnType<typeof useTimelineDrag>["shiftPreview"];
|
||||
isPreviewLoading: boolean;
|
||||
isApplying: boolean;
|
||||
isAllocSaving: boolean;
|
||||
mousePosRef: React.RefObject<{ x: number; y: number }>;
|
||||
dragTooltipRef: React.RefObject<HTMLDivElement | null>;
|
||||
allocTooltipRef: React.RefObject<HTMLDivElement | null>;
|
||||
rangeHintRef: React.RefObject<HTMLDivElement | null>;
|
||||
multiDragTooltipRef: React.RefObject<HTMLDivElement | null>;
|
||||
today: Date;
|
||||
}
|
||||
|
||||
export function TimelineDragOverlays({
|
||||
dragState,
|
||||
allocDragState,
|
||||
rangeState,
|
||||
multiSelectState,
|
||||
shiftPreview,
|
||||
isPreviewLoading,
|
||||
isApplying,
|
||||
isAllocSaving,
|
||||
mousePosRef,
|
||||
dragTooltipRef,
|
||||
allocTooltipRef,
|
||||
rangeHintRef,
|
||||
multiDragTooltipRef,
|
||||
today,
|
||||
}: TimelineDragOverlaysProps) {
|
||||
return (
|
||||
<>
|
||||
{/* Multi-select rectangle overlay */}
|
||||
{multiSelectState.isSelecting && (
|
||||
<div
|
||||
className="fixed border-2 border-sky-500 bg-sky-500/10 pointer-events-none z-30 rounded"
|
||||
style={{
|
||||
left: Math.min(multiSelectState.startX, multiSelectState.currentX),
|
||||
top: Math.min(multiSelectState.startY, multiSelectState.currentY),
|
||||
width: Math.abs(multiSelectState.currentX - multiSelectState.startX),
|
||||
height: Math.abs(multiSelectState.currentY - multiSelectState.startY),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Saving indicators */}
|
||||
{(isApplying || isAllocSaving) && (
|
||||
<div className="pointer-events-none absolute inset-0 z-50 flex items-center justify-center rounded-2xl bg-white/50 dark:bg-gray-950/50">
|
||||
<div className="app-surface px-5 py-3 text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||
{isApplying ? "Applying shift…" : "Saving…"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Drag preview tooltip */}
|
||||
{dragState.isDragging && dragState.daysDelta !== 0 && (
|
||||
<div
|
||||
ref={dragTooltipRef}
|
||||
className="fixed z-50 pointer-events-none"
|
||||
style={{ left: mousePosRef.current.x + 12, top: mousePosRef.current.y - 8 }}
|
||||
>
|
||||
<ShiftPreviewTooltip
|
||||
preview={
|
||||
shiftPreview ?? {
|
||||
valid: true,
|
||||
deltaCents: 0,
|
||||
wouldExceedBudget: false,
|
||||
budgetUtilizationAfter: 0,
|
||||
conflictCount: 0,
|
||||
errors: [],
|
||||
warnings: [],
|
||||
}
|
||||
}
|
||||
projectName={dragState.projectName ?? ""}
|
||||
newStartDate={dragState.currentStartDate ?? today}
|
||||
newEndDate={dragState.currentEndDate ?? today}
|
||||
isLoading={isPreviewLoading}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Alloc drag tooltip */}
|
||||
{allocDragState.isActive &&
|
||||
allocDragState.daysDelta !== 0 &&
|
||||
allocDragState.currentStartDate &&
|
||||
allocDragState.currentEndDate && (
|
||||
<div
|
||||
ref={allocTooltipRef}
|
||||
className="fixed z-40 bg-gray-800 text-white text-xs px-2.5 py-1.5 rounded-lg pointer-events-none shadow-lg space-y-0.5"
|
||||
style={{ left: mousePosRef.current.x + 14, top: mousePosRef.current.y - 36 }}
|
||||
>
|
||||
<div className="font-semibold">{allocDragState.projectName}</div>
|
||||
<div className="opacity-80">
|
||||
{formatDateShort(allocDragState.currentStartDate)}
|
||||
{" – "}
|
||||
{formatDateShort(allocDragState.currentEndDate)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Range-select hint */}
|
||||
{rangeState.isSelecting && rangeState.startDate && rangeState.currentDate && (
|
||||
<div
|
||||
ref={rangeHintRef}
|
||||
className="fixed z-40 bg-brand-700 text-white text-xs px-2 py-1 rounded-lg pointer-events-none shadow"
|
||||
style={{ left: mousePosRef.current.x + 12, top: mousePosRef.current.y - 28 }}
|
||||
>
|
||||
{(() => {
|
||||
const end = rangeState.currentDate;
|
||||
const [s, e] =
|
||||
rangeState.startDate <= end
|
||||
? [rangeState.startDate, end]
|
||||
: [end, rangeState.startDate];
|
||||
const days = Math.round((e.getTime() - s.getTime()) / MILLISECONDS_PER_DAY) + 1;
|
||||
return `${days} day${days !== 1 ? "s" : ""}`;
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Multi-drag tooltip */}
|
||||
{multiSelectState.isMultiDragging && multiSelectState.multiDragDaysDelta !== 0 && (
|
||||
<div
|
||||
ref={multiDragTooltipRef}
|
||||
className="fixed z-50 bg-sky-700 text-white text-xs px-2.5 py-1.5 rounded-lg pointer-events-none shadow-lg font-medium"
|
||||
style={{ left: mousePosRef.current.x + 14, top: mousePosRef.current.y - 36 }}
|
||||
>
|
||||
{multiSelectState.multiDragMode === "resize-start"
|
||||
? "Start "
|
||||
: multiSelectState.multiDragMode === "resize-end"
|
||||
? "End "
|
||||
: ""}
|
||||
{multiSelectState.multiDragDaysDelta > 0 ? "+" : ""}
|
||||
{multiSelectState.multiDragDaysDelta}d ({multiSelectState.selectedAllocationIds.length}{" "}
|
||||
allocations)
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user