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>
263 lines
9.2 KiB
TypeScript
263 lines
9.2 KiB
TypeScript
import { FillOpenDemandModal } from "~/components/allocations/FillOpenDemandModal.js";
|
|
import { AllocationPopover } from "./AllocationPopover.js";
|
|
import { BatchAssignPopover } from "./BatchAssignPopover.js";
|
|
import { DemandPopover } from "./DemandPopover.js";
|
|
import { InlineAllocationEditor } from "./InlineAllocationEditor.js";
|
|
import { KeyboardShortcutOverlay } from "./KeyboardShortcutOverlay.js";
|
|
import { NewAllocationPopover } from "./NewAllocationPopover.js";
|
|
import { ProjectPanel } from "./ProjectPanel.js";
|
|
import { ResourceHoverCard } from "./ResourceHoverCard.js";
|
|
import type { TimelineDemandEntry, TimelineAssignmentEntry } from "./TimelineContext.js";
|
|
import type { OpenDemandAssignment } from "./TimelineProjectPanel.js";
|
|
import type { useTimelineDrag } from "~/hooks/useTimelineDrag.js";
|
|
|
|
interface TimelinePopoversProps {
|
|
isSelfServiceTimeline: boolean;
|
|
hasActivePointerOverlay: boolean;
|
|
popover: {
|
|
allocationId: string;
|
|
projectId: string;
|
|
allocation?: TimelineAssignmentEntry | null;
|
|
x: number;
|
|
y: number;
|
|
contextDate?: Date;
|
|
} | null;
|
|
setPopover: React.Dispatch<React.SetStateAction<TimelinePopoversProps["popover"]>>;
|
|
demandPopover: { demand: TimelineDemandEntry; x: number; y: number } | null;
|
|
setDemandPopover: React.Dispatch<React.SetStateAction<TimelinePopoversProps["demandPopover"]>>;
|
|
newAllocPopover: {
|
|
resourceId: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
suggestedProjectId: string | null;
|
|
anchorX: number;
|
|
anchorY: number;
|
|
selectionResourceId: string;
|
|
selectionStart: Date;
|
|
selectionEnd: Date;
|
|
} | null;
|
|
setNewAllocPopover: React.Dispatch<
|
|
React.SetStateAction<TimelinePopoversProps["newAllocPopover"]>
|
|
>;
|
|
enrichedSuggestedProjectId: string | null;
|
|
openPanelProjectId: string | null;
|
|
setOpenPanelProjectId: React.Dispatch<React.SetStateAction<string | null>>;
|
|
openDemandToAssign: OpenDemandAssignment | null;
|
|
setOpenDemandToAssign: React.Dispatch<React.SetStateAction<OpenDemandAssignment | null>>;
|
|
openDemandsByProject: Map<string, TimelineDemandEntry[]>;
|
|
scrollContainerRef: React.RefObject<HTMLDivElement | null>;
|
|
multiSelectState: ReturnType<typeof useTimelineDrag>["multiSelectState"];
|
|
clearMultiSelect: ReturnType<typeof useTimelineDrag>["clearMultiSelect"];
|
|
handleBatchDelete: () => void;
|
|
handleShowBatchAssign: () => void;
|
|
isDeleting: boolean;
|
|
showBatchAssign: boolean;
|
|
setShowBatchAssign: React.Dispatch<React.SetStateAction<boolean>>;
|
|
resourceHover: { resourceId: string; anchorEl: HTMLElement } | null;
|
|
setResourceHover: React.Dispatch<React.SetStateAction<TimelinePopoversProps["resourceHover"]>>;
|
|
inlineEditTarget: {
|
|
allocationId: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
hoursPerDay: number;
|
|
barRect: DOMRect;
|
|
} | null;
|
|
setInlineEditTarget: React.Dispatch<
|
|
React.SetStateAction<TimelinePopoversProps["inlineEditTarget"]>
|
|
>;
|
|
showShortcuts: boolean;
|
|
setShowShortcuts: React.Dispatch<React.SetStateAction<boolean>>;
|
|
}
|
|
|
|
function buildDemandAssignment(d: TimelineDemandEntry): OpenDemandAssignment {
|
|
return {
|
|
id: d.id,
|
|
projectId: d.projectId,
|
|
roleId: d.roleId,
|
|
role: d.role,
|
|
headcount: d.requestedHeadcount,
|
|
startDate: new Date(d.startDate),
|
|
endDate: new Date(d.endDate),
|
|
hoursPerDay: d.hoursPerDay,
|
|
...(d.roleEntity !== undefined ? { roleEntity: d.roleEntity } : {}),
|
|
...(d.project !== undefined ? { project: d.project } : {}),
|
|
};
|
|
}
|
|
|
|
export function TimelinePopovers({
|
|
isSelfServiceTimeline,
|
|
hasActivePointerOverlay,
|
|
popover,
|
|
setPopover,
|
|
demandPopover,
|
|
setDemandPopover,
|
|
newAllocPopover,
|
|
setNewAllocPopover,
|
|
enrichedSuggestedProjectId,
|
|
openPanelProjectId,
|
|
setOpenPanelProjectId,
|
|
openDemandToAssign,
|
|
setOpenDemandToAssign,
|
|
openDemandsByProject,
|
|
scrollContainerRef,
|
|
multiSelectState,
|
|
clearMultiSelect,
|
|
handleBatchDelete,
|
|
handleShowBatchAssign,
|
|
isDeleting,
|
|
showBatchAssign,
|
|
setShowBatchAssign,
|
|
resourceHover,
|
|
setResourceHover,
|
|
inlineEditTarget,
|
|
setInlineEditTarget,
|
|
showShortcuts,
|
|
setShowShortcuts,
|
|
}: TimelinePopoversProps) {
|
|
return (
|
|
<>
|
|
{/* Allocation / Demand popover (click path) */}
|
|
{!isSelfServiceTimeline &&
|
|
!hasActivePointerOverlay &&
|
|
popover &&
|
|
(() => {
|
|
const clickedDemand = openDemandsByProject
|
|
.get(popover.projectId)
|
|
?.find((d) => d.id === popover.allocationId);
|
|
if (clickedDemand) {
|
|
return (
|
|
<DemandPopover
|
|
demand={clickedDemand}
|
|
onClose={() => setPopover(null)}
|
|
onOpenPanel={(pid) => {
|
|
setPopover(null);
|
|
setOpenPanelProjectId(pid);
|
|
}}
|
|
onFillDemand={(d) => {
|
|
setPopover(null);
|
|
setOpenDemandToAssign(buildDemandAssignment(d));
|
|
}}
|
|
anchorX={popover.x}
|
|
anchorY={popover.y}
|
|
ignoreScrollContainers={[scrollContainerRef]}
|
|
/>
|
|
);
|
|
}
|
|
return (
|
|
<AllocationPopover
|
|
allocationId={popover.allocationId}
|
|
projectId={popover.projectId}
|
|
initialAllocation={popover.allocation ?? null}
|
|
onClose={() => setPopover(null)}
|
|
onOpenPanel={(pid) => {
|
|
setPopover(null);
|
|
setOpenPanelProjectId(pid);
|
|
}}
|
|
anchorX={popover.x}
|
|
anchorY={popover.y}
|
|
ignoreScrollContainers={[scrollContainerRef]}
|
|
{...(popover.contextDate ? { contextDate: popover.contextDate } : {})}
|
|
/>
|
|
);
|
|
})()}
|
|
|
|
{/* Demand popover (context menu path) */}
|
|
{!isSelfServiceTimeline && !hasActivePointerOverlay && demandPopover && (
|
|
<DemandPopover
|
|
demand={demandPopover.demand}
|
|
onClose={() => setDemandPopover(null)}
|
|
onOpenPanel={(pid) => {
|
|
setDemandPopover(null);
|
|
setOpenPanelProjectId(pid);
|
|
}}
|
|
onFillDemand={(d) => {
|
|
setDemandPopover(null);
|
|
setOpenDemandToAssign(buildDemandAssignment(d));
|
|
}}
|
|
anchorX={demandPopover.x}
|
|
anchorY={demandPopover.y}
|
|
ignoreScrollContainers={[scrollContainerRef]}
|
|
/>
|
|
)}
|
|
|
|
{/* New allocation popover */}
|
|
{!isSelfServiceTimeline && newAllocPopover && (
|
|
<NewAllocationPopover
|
|
resourceId={newAllocPopover.resourceId}
|
|
startDate={newAllocPopover.startDate}
|
|
endDate={newAllocPopover.endDate}
|
|
suggestedProjectId={enrichedSuggestedProjectId}
|
|
anchorX={newAllocPopover.anchorX}
|
|
anchorY={newAllocPopover.anchorY}
|
|
onClose={() => setNewAllocPopover(null)}
|
|
onCreated={() => setNewAllocPopover(null)}
|
|
ignoreScrollContainers={[scrollContainerRef]}
|
|
/>
|
|
)}
|
|
|
|
{/* Project side panel */}
|
|
{!isSelfServiceTimeline && openPanelProjectId && (
|
|
<ProjectPanel projectId={openPanelProjectId} onClose={() => setOpenPanelProjectId(null)} />
|
|
)}
|
|
|
|
{/* Open-demand assignment modal */}
|
|
{!isSelfServiceTimeline && openDemandToAssign && (
|
|
<FillOpenDemandModal
|
|
allocation={openDemandToAssign}
|
|
onClose={() => setOpenDemandToAssign(null)}
|
|
onSuccess={() => setOpenDemandToAssign(null)}
|
|
/>
|
|
)}
|
|
|
|
{/* Multi-select floating action bar + batch assign */}
|
|
{showBatchAssign && multiSelectState.dateRange && (
|
|
<BatchAssignPopover
|
|
resourceIds={multiSelectState.selectedResourceIds}
|
|
startDate={multiSelectState.dateRange.start}
|
|
endDate={multiSelectState.dateRange.end}
|
|
onClose={() => setShowBatchAssign(false)}
|
|
onCreated={() => {
|
|
setShowBatchAssign(false);
|
|
clearMultiSelect();
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* Resource hover card */}
|
|
{!hasActivePointerOverlay && resourceHover && (
|
|
<ResourceHoverCard
|
|
resourceId={resourceHover.resourceId}
|
|
anchorEl={resourceHover.anchorEl}
|
|
onClose={() => setResourceHover(null)}
|
|
/>
|
|
)}
|
|
|
|
{/* Inline allocation editor */}
|
|
{inlineEditTarget && (
|
|
<InlineAllocationEditor
|
|
allocationId={inlineEditTarget.allocationId}
|
|
initialStartDate={inlineEditTarget.startDate}
|
|
initialEndDate={inlineEditTarget.endDate}
|
|
initialHoursPerDay={inlineEditTarget.hoursPerDay}
|
|
barRect={inlineEditTarget.barRect}
|
|
onClose={() => setInlineEditTarget(null)}
|
|
onSaved={() => setInlineEditTarget(null)}
|
|
/>
|
|
)}
|
|
|
|
{/* Keyboard shortcut overlay */}
|
|
{showShortcuts && <KeyboardShortcutOverlay onClose={() => setShowShortcuts(false)} />}
|
|
|
|
{/* Keyboard shortcut hint button */}
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowShortcuts((prev) => !prev)}
|
|
title="Keyboard shortcuts (?)"
|
|
className="fixed bottom-6 right-6 z-40 rounded-full w-8 h-8 flex items-center justify-center bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 shadow text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 text-sm font-medium"
|
|
>
|
|
?
|
|
</button>
|
|
</>
|
|
);
|
|
}
|