a83edb2f9d
Redesigned timeline project and resource panels with expanded detail views, added quick filter toolbar, improved drag handling, and enhanced vacation/entitlement router logic. Includes e2e test updates and minor API fixes. Co-Authored-By: claude-flow <ruv@ruv.net>
588 lines
21 KiB
TypeScript
588 lines
21 KiB
TypeScript
"use client";
|
||
|
||
import { clsx } from "clsx";
|
||
import { useEffect, useRef, useState } from "react";
|
||
import { useAllocationHistory } from "~/hooks/useAllocationHistory.js";
|
||
import { useProjectDragContext } from "~/hooks/useProjectDragContext.js";
|
||
import { useTimelineDrag } from "~/hooks/useTimelineDrag.js";
|
||
import { useTimelineLayout } from "~/hooks/useTimelineLayout.js";
|
||
import { FillOpenDemandModal } from "~/components/allocations/FillOpenDemandModal.js";
|
||
import { AllocationPopover } from "./AllocationPopover.js";
|
||
import { NewAllocationPopover } from "./NewAllocationPopover.js";
|
||
import { ProjectPanel } from "./ProjectPanel.js";
|
||
import { ShiftPreviewTooltip } from "./ShiftPreviewTooltip.js";
|
||
import { TimelineHeader } from "./TimelineHeader.js";
|
||
import { TimelineToolbar } from "./TimelineToolbar.js";
|
||
import { addDays } from "./utils.js";
|
||
import { HEADER_DAY_HEIGHT, HEADER_MONTH_HEIGHT, LABEL_WIDTH } from "./timelineConstants.js";
|
||
import { formatDateShort } from "~/lib/format.js";
|
||
import {
|
||
TimelineProvider,
|
||
useTimelineContext,
|
||
type TimelineAssignmentEntry,
|
||
} from "./TimelineContext.js";
|
||
import { TimelineResourcePanel } from "./TimelineResourcePanel.js";
|
||
import { TimelineProjectPanel, type OpenDemandAssignment } from "./TimelineProjectPanel.js";
|
||
|
||
// ─── Entry point ────────────────────────────────────────────────────────────
|
||
// Two-layer mount: the outer shell creates drag state + project context,
|
||
// then wraps children with TimelineProvider. The inner content consumes context.
|
||
|
||
export function TimelineView() {
|
||
const mousePosRef = useRef({ x: 0, y: 0 });
|
||
|
||
const { push: pushHistory, undo, redo, canUndo, canRedo } = useAllocationHistory();
|
||
const pushHistoryRef = useRef(pushHistory);
|
||
pushHistoryRef.current = pushHistory;
|
||
|
||
const [popover, setPopover] = useState<{
|
||
allocationId: string;
|
||
projectId: string;
|
||
x: number;
|
||
y: number;
|
||
} | null>(null);
|
||
const [newAllocPopover, setNewAllocPopover] = useState<{
|
||
resourceId: string;
|
||
startDate: Date;
|
||
endDate: Date;
|
||
suggestedProjectId: string | null;
|
||
anchorX: number;
|
||
anchorY: number;
|
||
} | null>(null);
|
||
|
||
// cellWidth placeholder — the real value comes from useTimelineLayout inside the content.
|
||
// useTimelineDrag only needs cellWidth for pixel→day conversion during drag.
|
||
// We start with 40 (day zoom default) and update via a ref.
|
||
const cellWidthRef = useRef(40);
|
||
|
||
const {
|
||
dragState,
|
||
allocDragState,
|
||
rangeState,
|
||
shiftPreview,
|
||
isPreviewLoading,
|
||
isApplying,
|
||
isAllocSaving,
|
||
onProjectBarMouseDown,
|
||
onAllocMouseDown,
|
||
onRowMouseDown,
|
||
onCanvasMouseMove,
|
||
onCanvasMouseUp,
|
||
onCanvasMouseLeave,
|
||
onProjectBarTouchStart,
|
||
onAllocTouchStart,
|
||
onRowTouchStart,
|
||
onCanvasTouchMove,
|
||
onCanvasTouchEnd,
|
||
} = useTimelineDrag({
|
||
cellWidth: cellWidthRef.current,
|
||
onBlockClick: (info) => {
|
||
setPopover({
|
||
allocationId: info.allocationId,
|
||
projectId: info.projectId,
|
||
x: mousePosRef.current.x,
|
||
y: mousePosRef.current.y,
|
||
});
|
||
},
|
||
onRangeSelected: (info) => {
|
||
setNewAllocPopover({
|
||
resourceId: info.resourceId,
|
||
startDate: info.startDate,
|
||
endDate: info.endDate,
|
||
suggestedProjectId: info.suggestedProjectId,
|
||
anchorX: info.anchorX,
|
||
anchorY: info.anchorY,
|
||
});
|
||
},
|
||
onAllocationMoved: (snapshot) => {
|
||
pushHistoryRef.current(snapshot);
|
||
},
|
||
});
|
||
|
||
const [openPanelProjectId, setOpenPanelProjectId] = useState<string | null>(null);
|
||
const dragProjectId = dragState.isDragging ? dragState.projectId : null;
|
||
const contextProjectId = dragProjectId ?? openPanelProjectId;
|
||
const { contextResourceIds, contextAllocations } = useProjectDragContext(contextProjectId);
|
||
|
||
return (
|
||
<TimelineProvider
|
||
isDragging={dragState.isDragging}
|
||
contextAllocations={contextAllocations as TimelineAssignmentEntry[]}
|
||
>
|
||
<TimelineViewContent
|
||
mousePosRef={mousePosRef}
|
||
cellWidthRef={cellWidthRef}
|
||
dragState={dragState}
|
||
allocDragState={allocDragState}
|
||
rangeState={rangeState}
|
||
shiftPreview={shiftPreview}
|
||
isPreviewLoading={isPreviewLoading}
|
||
isApplying={isApplying}
|
||
isAllocSaving={isAllocSaving}
|
||
onProjectBarMouseDown={onProjectBarMouseDown}
|
||
onAllocMouseDown={onAllocMouseDown}
|
||
onRowMouseDown={onRowMouseDown}
|
||
onCanvasMouseMove={onCanvasMouseMove}
|
||
onCanvasMouseUp={onCanvasMouseUp}
|
||
onCanvasMouseLeave={onCanvasMouseLeave}
|
||
onProjectBarTouchStart={onProjectBarTouchStart}
|
||
onAllocTouchStart={onAllocTouchStart}
|
||
onRowTouchStart={onRowTouchStart}
|
||
onCanvasTouchMove={onCanvasTouchMove}
|
||
onCanvasTouchEnd={onCanvasTouchEnd}
|
||
contextResourceIds={contextResourceIds}
|
||
popover={popover}
|
||
setPopover={setPopover}
|
||
newAllocPopover={newAllocPopover}
|
||
setNewAllocPopover={setNewAllocPopover}
|
||
openPanelProjectId={openPanelProjectId}
|
||
setOpenPanelProjectId={setOpenPanelProjectId}
|
||
canUndo={canUndo}
|
||
canRedo={canRedo}
|
||
undo={undo}
|
||
redo={redo}
|
||
/>
|
||
</TimelineProvider>
|
||
);
|
||
}
|
||
|
||
// ─── Content (inside TimelineProvider — has context access) ─────────────────
|
||
|
||
function TimelineViewContent({
|
||
mousePosRef,
|
||
cellWidthRef,
|
||
dragState,
|
||
allocDragState,
|
||
rangeState,
|
||
shiftPreview,
|
||
isPreviewLoading,
|
||
isApplying,
|
||
isAllocSaving,
|
||
onProjectBarMouseDown,
|
||
onAllocMouseDown,
|
||
onRowMouseDown,
|
||
onCanvasMouseMove,
|
||
onCanvasMouseUp,
|
||
onCanvasMouseLeave,
|
||
onProjectBarTouchStart,
|
||
onAllocTouchStart,
|
||
onRowTouchStart,
|
||
onCanvasTouchMove,
|
||
onCanvasTouchEnd,
|
||
contextResourceIds,
|
||
popover,
|
||
setPopover,
|
||
newAllocPopover,
|
||
setNewAllocPopover,
|
||
openPanelProjectId,
|
||
setOpenPanelProjectId,
|
||
canUndo,
|
||
canRedo,
|
||
undo,
|
||
redo,
|
||
}: {
|
||
mousePosRef: React.RefObject<{ x: number; y: number }>;
|
||
cellWidthRef: React.RefObject<number>;
|
||
dragState: ReturnType<typeof useTimelineDrag>["dragState"];
|
||
allocDragState: ReturnType<typeof useTimelineDrag>["allocDragState"];
|
||
rangeState: ReturnType<typeof useTimelineDrag>["rangeState"];
|
||
shiftPreview: ReturnType<typeof useTimelineDrag>["shiftPreview"];
|
||
isPreviewLoading: boolean;
|
||
isApplying: boolean;
|
||
isAllocSaving: boolean;
|
||
onProjectBarMouseDown: ReturnType<typeof useTimelineDrag>["onProjectBarMouseDown"];
|
||
onAllocMouseDown: ReturnType<typeof useTimelineDrag>["onAllocMouseDown"];
|
||
onRowMouseDown: ReturnType<typeof useTimelineDrag>["onRowMouseDown"];
|
||
onCanvasMouseMove: ReturnType<typeof useTimelineDrag>["onCanvasMouseMove"];
|
||
onCanvasMouseUp: ReturnType<typeof useTimelineDrag>["onCanvasMouseUp"];
|
||
onCanvasMouseLeave: ReturnType<typeof useTimelineDrag>["onCanvasMouseLeave"];
|
||
onProjectBarTouchStart: ReturnType<typeof useTimelineDrag>["onProjectBarTouchStart"];
|
||
onAllocTouchStart: ReturnType<typeof useTimelineDrag>["onAllocTouchStart"];
|
||
onRowTouchStart: ReturnType<typeof useTimelineDrag>["onRowTouchStart"];
|
||
onCanvasTouchMove: ReturnType<typeof useTimelineDrag>["onCanvasTouchMove"];
|
||
onCanvasTouchEnd: ReturnType<typeof useTimelineDrag>["onCanvasTouchEnd"];
|
||
contextResourceIds: string[];
|
||
popover: { allocationId: string; projectId: string; x: number; y: number } | null;
|
||
setPopover: React.Dispatch<React.SetStateAction<typeof popover>>;
|
||
newAllocPopover: {
|
||
resourceId: string;
|
||
startDate: Date;
|
||
endDate: Date;
|
||
suggestedProjectId: string | null;
|
||
anchorX: number;
|
||
anchorY: number;
|
||
} | null;
|
||
setNewAllocPopover: React.Dispatch<React.SetStateAction<typeof newAllocPopover>>;
|
||
openPanelProjectId: string | null;
|
||
setOpenPanelProjectId: React.Dispatch<React.SetStateAction<string | null>>;
|
||
canUndo: boolean;
|
||
canRedo: boolean;
|
||
undo: () => Promise<void>;
|
||
redo: () => Promise<void>;
|
||
}) {
|
||
const ctx = useTimelineContext();
|
||
const {
|
||
resources,
|
||
projectGroups,
|
||
viewStart,
|
||
viewEnd,
|
||
viewDays,
|
||
setViewStart,
|
||
setViewDays,
|
||
filters,
|
||
setFilters,
|
||
filterOpen,
|
||
setFilterOpen,
|
||
viewMode,
|
||
setViewMode,
|
||
today,
|
||
isLoading,
|
||
isInitialLoading,
|
||
totalAllocCount,
|
||
} = ctx;
|
||
|
||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||
const canvasRef = useRef<HTMLDivElement>(null);
|
||
|
||
// Tooltip DOM refs
|
||
const dragTooltipRef = useRef<HTMLDivElement>(null);
|
||
const allocTooltipRef = useRef<HTMLDivElement>(null);
|
||
const rangeHintRef = useRef<HTMLDivElement>(null);
|
||
const [openDemandToAssign, setOpenDemandToAssign] = useState<OpenDemandAssignment | null>(null);
|
||
|
||
const { CELL_WIDTH, dates, totalCanvasWidth, toLeft, toWidth, gridLines, monthGroups, xToDate } =
|
||
useTimelineLayout(viewStart, viewDays, filters.zoom, filters.showWeekends, today);
|
||
const hasActivePointerOverlay =
|
||
dragState.isDragging || allocDragState.isActive || rangeState.isSelecting;
|
||
|
||
function openAllocationPopoverAt(
|
||
info: {
|
||
allocationId: string;
|
||
projectId: string;
|
||
},
|
||
anchorX: number,
|
||
anchorY: number,
|
||
) {
|
||
setPopover({
|
||
allocationId: info.allocationId,
|
||
projectId: info.projectId,
|
||
x: anchorX,
|
||
y: anchorY,
|
||
});
|
||
}
|
||
|
||
// Keep cellWidthRef in sync so the drag hook uses the correct value.
|
||
cellWidthRef.current = CELL_WIDTH;
|
||
|
||
// ─── Native mousemove listener — updates tooltips without React state ─────
|
||
useEffect(() => {
|
||
if (!hasActivePointerOverlay) return;
|
||
const el = canvasRef.current;
|
||
if (!el) return;
|
||
const handler = (e: MouseEvent) => {
|
||
const x = e.clientX;
|
||
const y = e.clientY;
|
||
mousePosRef.current = { x, y };
|
||
if (dragTooltipRef.current) {
|
||
dragTooltipRef.current.style.left = `${x + 12}px`;
|
||
dragTooltipRef.current.style.top = `${y - 8}px`;
|
||
}
|
||
if (allocTooltipRef.current) {
|
||
allocTooltipRef.current.style.left = `${x + 14}px`;
|
||
allocTooltipRef.current.style.top = `${y - 36}px`;
|
||
}
|
||
if (rangeHintRef.current) {
|
||
rangeHintRef.current.style.left = `${x + 12}px`;
|
||
rangeHintRef.current.style.top = `${y - 28}px`;
|
||
}
|
||
};
|
||
el.addEventListener("mousemove", handler, { passive: true });
|
||
return () => el.removeEventListener("mousemove", handler);
|
||
}, [hasActivePointerOverlay, isLoading, mousePosRef]); // eslint-disable-line react-hooks/exhaustive-deps
|
||
|
||
// ─── Shift+wheel → horizontal scroll ──────────────────────────────────────
|
||
useEffect(() => {
|
||
const el = scrollContainerRef.current;
|
||
if (!el) return;
|
||
const handler = (e: WheelEvent) => {
|
||
if (e.shiftKey && e.deltaY !== 0 && e.deltaX === 0) {
|
||
e.preventDefault();
|
||
el.scrollLeft += e.deltaY;
|
||
}
|
||
};
|
||
el.addEventListener("wheel", handler, { passive: false });
|
||
return () => el.removeEventListener("wheel", handler);
|
||
}, [isLoading]); // eslint-disable-line react-hooks/exhaustive-deps
|
||
|
||
// ─── Keyboard undo/redo ───────────────────────────────────────────────────
|
||
useEffect(() => {
|
||
const handler = (e: KeyboardEvent) => {
|
||
const isMac = navigator.platform.toUpperCase().includes("MAC");
|
||
const modKey = isMac ? e.metaKey : e.ctrlKey;
|
||
if (!modKey) return;
|
||
if (e.key === "z" && !e.shiftKey) {
|
||
e.preventDefault();
|
||
void undo();
|
||
}
|
||
if ((e.key === "z" && e.shiftKey) || e.key === "y") {
|
||
e.preventDefault();
|
||
void redo();
|
||
}
|
||
};
|
||
window.addEventListener("keydown", handler);
|
||
return () => window.removeEventListener("keydown", handler);
|
||
}, [undo, redo]);
|
||
|
||
// ─── Lazy-extend date range on scroll ─────────────────────────────────────
|
||
function handleContainerScroll() {
|
||
const el = scrollContainerRef.current;
|
||
if (!el) return;
|
||
const distanceFromRight = el.scrollWidth - el.scrollLeft - el.clientWidth;
|
||
if (distanceFromRight < CELL_WIDTH * 40) {
|
||
setViewDays((d) => d + 120);
|
||
}
|
||
}
|
||
|
||
const handleMouseMove = (e: React.MouseEvent) => {
|
||
if (!hasActivePointerOverlay) return;
|
||
onCanvasMouseMove(e);
|
||
};
|
||
|
||
return (
|
||
<div className="relative flex flex-1 flex-col gap-4 min-h-0">
|
||
{/* Toolbar */}
|
||
<TimelineToolbar
|
||
viewMode={viewMode}
|
||
onViewModeChange={setViewMode}
|
||
filters={filters}
|
||
onFiltersChange={setFilters}
|
||
filterOpen={filterOpen}
|
||
onFilterOpenChange={setFilterOpen}
|
||
resourceCount={resources.length}
|
||
projectCount={projectGroups.length}
|
||
totalAllocCount={totalAllocCount}
|
||
onNavigateBack={() => setViewStart((v) => addDays(v, -28))}
|
||
onNavigateToday={() => setViewStart(addDays(today, -30))}
|
||
onNavigateForward={() => setViewStart((v) => addDays(v, 28))}
|
||
canUndo={canUndo}
|
||
canRedo={canRedo}
|
||
onUndo={() => {
|
||
void undo();
|
||
}}
|
||
onRedo={() => {
|
||
void redo();
|
||
}}
|
||
/>
|
||
|
||
{/* Scrollable canvas */}
|
||
<div
|
||
ref={scrollContainerRef}
|
||
onScroll={handleContainerScroll}
|
||
className="app-surface relative flex-1 overflow-auto"
|
||
>
|
||
{isInitialLoading ? (
|
||
<div className="flex items-center justify-center py-24 text-sm text-gray-500 dark:text-gray-400">
|
||
Loading timeline...
|
||
</div>
|
||
) : (
|
||
<div style={{ minWidth: LABEL_WIDTH + totalCanvasWidth }}>
|
||
<TimelineHeader
|
||
monthGroups={monthGroups}
|
||
dates={dates}
|
||
CELL_WIDTH={CELL_WIDTH}
|
||
LABEL_WIDTH={LABEL_WIDTH}
|
||
HEADER_MONTH_HEIGHT={HEADER_MONTH_HEIGHT}
|
||
HEADER_DAY_HEIGHT={HEADER_DAY_HEIGHT}
|
||
zoom={filters.zoom}
|
||
viewMode={viewMode}
|
||
today={today}
|
||
/>
|
||
|
||
{/* Canvas rows */}
|
||
<div
|
||
ref={canvasRef}
|
||
onMouseMove={handleMouseMove}
|
||
onMouseUp={(e) => void onCanvasMouseUp(e)}
|
||
onMouseLeave={onCanvasMouseLeave}
|
||
onTouchMove={(e) => {
|
||
if (!hasActivePointerOverlay) return;
|
||
onCanvasTouchMove(e);
|
||
}}
|
||
onTouchEnd={(e) => void onCanvasTouchEnd(e)}
|
||
className={clsx(
|
||
(dragState.isDragging || allocDragState.isActive) && "cursor-grabbing select-none",
|
||
rangeState.isSelecting && "cursor-crosshair select-none",
|
||
)}
|
||
>
|
||
{viewMode === "resource" && (
|
||
<TimelineResourcePanel
|
||
scrollContainerRef={scrollContainerRef}
|
||
dragState={dragState}
|
||
allocDragState={allocDragState}
|
||
rangeState={rangeState}
|
||
shiftPreview={shiftPreview}
|
||
contextResourceIds={contextResourceIds}
|
||
onAllocMouseDown={onAllocMouseDown}
|
||
onAllocTouchStart={onAllocTouchStart}
|
||
onRowMouseDown={onRowMouseDown}
|
||
onRowTouchStart={onRowTouchStart}
|
||
onAllocationContextMenu={openAllocationPopoverAt}
|
||
CELL_WIDTH={CELL_WIDTH}
|
||
dates={dates}
|
||
totalCanvasWidth={totalCanvasWidth}
|
||
toLeft={toLeft}
|
||
toWidth={toWidth}
|
||
gridLines={gridLines}
|
||
xToDate={xToDate}
|
||
/>
|
||
)}
|
||
|
||
{viewMode === "project" && (
|
||
<TimelineProjectPanel
|
||
scrollContainerRef={scrollContainerRef}
|
||
dragState={dragState}
|
||
allocDragState={allocDragState}
|
||
rangeState={rangeState}
|
||
onProjectBarMouseDown={onProjectBarMouseDown}
|
||
onProjectBarTouchStart={onProjectBarTouchStart}
|
||
onAllocMouseDown={onAllocMouseDown}
|
||
onAllocTouchStart={onAllocTouchStart}
|
||
onRowMouseDown={onRowMouseDown}
|
||
onRowTouchStart={onRowTouchStart}
|
||
onOpenPanel={setOpenPanelProjectId}
|
||
onOpenDemandClick={setOpenDemandToAssign}
|
||
onAllocationContextMenu={openAllocationPopoverAt}
|
||
CELL_WIDTH={CELL_WIDTH}
|
||
dates={dates}
|
||
totalCanvasWidth={totalCanvasWidth}
|
||
toLeft={toLeft}
|
||
toWidth={toWidth}
|
||
gridLines={gridLines}
|
||
xToDate={xToDate}
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 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()) / 86400000) + 1;
|
||
return `${days} day${days !== 1 ? "s" : ""}`;
|
||
})()}
|
||
</div>
|
||
)}
|
||
|
||
{/* Allocation popover */}
|
||
{popover && (
|
||
<AllocationPopover
|
||
allocationId={popover.allocationId}
|
||
projectId={popover.projectId}
|
||
onClose={() => setPopover(null)}
|
||
onOpenPanel={(pid) => {
|
||
setPopover(null);
|
||
setOpenPanelProjectId(pid);
|
||
}}
|
||
anchorX={popover.x}
|
||
anchorY={popover.y}
|
||
/>
|
||
)}
|
||
|
||
{/* New allocation popover */}
|
||
{newAllocPopover && (
|
||
<NewAllocationPopover
|
||
resourceId={newAllocPopover.resourceId}
|
||
startDate={newAllocPopover.startDate}
|
||
endDate={newAllocPopover.endDate}
|
||
suggestedProjectId={newAllocPopover.suggestedProjectId}
|
||
anchorX={newAllocPopover.anchorX}
|
||
anchorY={newAllocPopover.anchorY}
|
||
onClose={() => setNewAllocPopover(null)}
|
||
onCreated={() => setNewAllocPopover(null)}
|
||
/>
|
||
)}
|
||
|
||
{/* Project side panel */}
|
||
{openPanelProjectId && (
|
||
<ProjectPanel projectId={openPanelProjectId} onClose={() => setOpenPanelProjectId(null)} />
|
||
)}
|
||
|
||
{/* Open-demand assignment modal */}
|
||
{openDemandToAssign && (
|
||
<FillOpenDemandModal
|
||
allocation={openDemandToAssign}
|
||
onClose={() => setOpenDemandToAssign(null)}
|
||
onSuccess={() => setOpenDemandToAssign(null)}
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|