chore: add pre-commit hooks, tighten ESLint, activate Sentry DSN, publish CI coverage (Phase 1)
- Install husky v9 + lint-staged: pre-commit runs eslint --fix and prettier on staged files - Tighten ESLint base config: no-console→error, ban-ts-comment (ts-ignore banned, ts-expect-error with description allowed), reportUnusedDisableDirectives→error - Migrate web app from deprecated `next lint` to `eslint src/` with flat config and react-hooks plugin - Convert all 5 @ts-ignore to @ts-expect-error with descriptions, remove stale disable comments - Add NEXT_PUBLIC_SENTRY_DSN to docker-compose.prod.yml and .env.example - Add coverage artifact upload step to CI test job - Pre-existing violations (102 warnings) downgraded to warn in web config for Phase 2 cleanup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -48,13 +48,21 @@ import { InlineAllocationEditor } from "./InlineAllocationEditor.js";
|
||||
export function TimelineView() {
|
||||
const { data: session, status: sessionStatus } = useSession();
|
||||
const mousePosRef = useRef({ x: 0, y: 0 });
|
||||
const role = sessionStatus === "authenticated"
|
||||
? ((session.user as { role?: string } | undefined)?.role ?? "USER")
|
||||
: null;
|
||||
const role =
|
||||
sessionStatus === "authenticated"
|
||||
? ((session.user as { role?: string } | undefined)?.role ?? "USER")
|
||||
: null;
|
||||
const isSelfServiceTimeline = role === "USER" || role === "VIEWER";
|
||||
const canManageTimeline = !isSelfServiceTimeline;
|
||||
|
||||
const { push: pushHistory, pushBatch: pushBatchHistory, undo, redo, canUndo, canRedo } = useAllocationHistory();
|
||||
const {
|
||||
push: pushHistory,
|
||||
pushBatch: pushBatchHistory,
|
||||
undo,
|
||||
redo,
|
||||
canUndo,
|
||||
canRedo,
|
||||
} = useAllocationHistory();
|
||||
const pushHistoryRef = useRef(pushHistory);
|
||||
pushHistoryRef.current = pushHistory;
|
||||
const pushBatchHistoryRef = useRef(pushBatchHistory);
|
||||
@@ -145,7 +153,7 @@ export function TimelineView() {
|
||||
pushHistoryRef.current(snapshot);
|
||||
},
|
||||
onShiftClickAlloc: (allocationId: string) => {
|
||||
setMultiSelectState(prev => {
|
||||
setMultiSelectState((prev) => {
|
||||
const ids = new Set(prev.selectedAllocationIds);
|
||||
if (ids.has(allocationId)) {
|
||||
ids.delete(allocationId);
|
||||
@@ -169,61 +177,64 @@ export function TimelineView() {
|
||||
const [openPanelProjectId, setOpenPanelProjectId] = useState<string | null>(null);
|
||||
const dragProjectId = dragState.isDragging ? dragState.projectId : null;
|
||||
const contextProjectId = canManageTimeline ? (dragProjectId ?? openPanelProjectId) : null;
|
||||
const { contextResourceIds, contextAllocations } = useProjectDragContext(contextProjectId, canManageTimeline);
|
||||
const { contextResourceIds, contextAllocations } = useProjectDragContext(
|
||||
contextProjectId,
|
||||
canManageTimeline,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SuccessToast
|
||||
show={dragErrorToast !== null}
|
||||
message={dragErrorToast ?? ""}
|
||||
variant="warning"
|
||||
onDone={() => setDragErrorToast(null)}
|
||||
/>
|
||||
<TimelineProvider
|
||||
isDragging={dragState.isDragging}
|
||||
contextAllocations={contextAllocations as TimelineAssignmentEntry[]}
|
||||
>
|
||||
<TimelineViewContent
|
||||
mousePosRef={mousePosRef}
|
||||
cellWidthRef={cellWidthRef}
|
||||
dragState={dragState}
|
||||
allocDragState={allocDragState}
|
||||
rangeState={rangeState}
|
||||
multiSelectState={multiSelectState}
|
||||
setMultiSelectState={setMultiSelectState}
|
||||
optimisticAllocations={optimisticAllocations}
|
||||
reconcileOptimisticAllocations={reconcileOptimisticAllocations}
|
||||
onCanvasRightMouseDown={onCanvasRightMouseDown}
|
||||
clearMultiSelect={clearMultiSelect}
|
||||
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}
|
||||
isSelfServiceTimeline={isSelfServiceTimeline}
|
||||
undo={undo}
|
||||
redo={redo}
|
||||
<SuccessToast
|
||||
show={dragErrorToast !== null}
|
||||
message={dragErrorToast ?? ""}
|
||||
variant="warning"
|
||||
onDone={() => setDragErrorToast(null)}
|
||||
/>
|
||||
</TimelineProvider>
|
||||
<TimelineProvider
|
||||
isDragging={dragState.isDragging}
|
||||
contextAllocations={contextAllocations as TimelineAssignmentEntry[]}
|
||||
>
|
||||
<TimelineViewContent
|
||||
mousePosRef={mousePosRef}
|
||||
cellWidthRef={cellWidthRef}
|
||||
dragState={dragState}
|
||||
allocDragState={allocDragState}
|
||||
rangeState={rangeState}
|
||||
multiSelectState={multiSelectState}
|
||||
setMultiSelectState={setMultiSelectState}
|
||||
optimisticAllocations={optimisticAllocations}
|
||||
reconcileOptimisticAllocations={reconcileOptimisticAllocations}
|
||||
onCanvasRightMouseDown={onCanvasRightMouseDown}
|
||||
clearMultiSelect={clearMultiSelect}
|
||||
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}
|
||||
isSelfServiceTimeline={isSelfServiceTimeline}
|
||||
undo={undo}
|
||||
redo={redo}
|
||||
/>
|
||||
</TimelineProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -278,7 +289,9 @@ function TimelineViewContent({
|
||||
multiSelectState: ReturnType<typeof useTimelineDrag>["multiSelectState"];
|
||||
setMultiSelectState: ReturnType<typeof useTimelineDrag>["setMultiSelectState"];
|
||||
optimisticAllocations: TimelineVisualOverrides;
|
||||
reconcileOptimisticAllocations: ReturnType<typeof useTimelineDrag>["reconcileOptimisticAllocations"];
|
||||
reconcileOptimisticAllocations: ReturnType<
|
||||
typeof useTimelineDrag
|
||||
>["reconcileOptimisticAllocations"];
|
||||
onCanvasRightMouseDown: ReturnType<typeof useTimelineDrag>["onCanvasRightMouseDown"];
|
||||
clearMultiSelect: ReturnType<typeof useTimelineDrag>["clearMultiSelect"];
|
||||
shiftPreview: ReturnType<typeof useTimelineDrag>["shiftPreview"];
|
||||
@@ -410,17 +423,15 @@ function TimelineViewContent({
|
||||
} | null>(null);
|
||||
|
||||
const hasActivePointerOverlay =
|
||||
dragState.isDragging || allocDragState.isActive || rangeState.isSelecting || multiSelectState.isMultiDragging;
|
||||
dragState.isDragging ||
|
||||
allocDragState.isActive ||
|
||||
rangeState.isSelecting ||
|
||||
multiSelectState.isMultiDragging;
|
||||
|
||||
useEffect(() => {
|
||||
if (optimisticAllocations.size === 0) return;
|
||||
reconcileOptimisticAllocations([...visibleAssignments, ...visibleDemands]);
|
||||
}, [
|
||||
optimisticAllocations,
|
||||
reconcileOptimisticAllocations,
|
||||
visibleAssignments,
|
||||
visibleDemands,
|
||||
]);
|
||||
}, [optimisticAllocations, reconcileOptimisticAllocations, visibleAssignments, visibleDemands]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasActivePointerOverlay) return;
|
||||
@@ -473,12 +484,18 @@ function TimelineViewContent({
|
||||
if (!allocs || allocs.length === 0) return null;
|
||||
const projectHours = new Map<string, number>();
|
||||
for (const alloc of allocs) {
|
||||
projectHours.set(alloc.projectId, (projectHours.get(alloc.projectId) ?? 0) + alloc.hoursPerDay);
|
||||
projectHours.set(
|
||||
alloc.projectId,
|
||||
(projectHours.get(alloc.projectId) ?? 0) + alloc.hoursPerDay,
|
||||
);
|
||||
}
|
||||
let maxPid: string | null = null;
|
||||
let maxH = 0;
|
||||
for (const [pid, h] of projectHours) {
|
||||
if (h > maxH) { maxH = h; maxPid = pid; }
|
||||
if (h > maxH) {
|
||||
maxH = h;
|
||||
maxPid = pid;
|
||||
}
|
||||
}
|
||||
return maxPid;
|
||||
}, [newAllocPopover, allocsByResource]);
|
||||
@@ -516,7 +533,7 @@ function TimelineViewContent({
|
||||
const target: EventTarget = multiSelectState.isMultiDragging ? document : el;
|
||||
target.addEventListener("mousemove", handler as EventListener, { passive: true });
|
||||
return () => target.removeEventListener("mousemove", handler as EventListener);
|
||||
}, [hasActivePointerOverlay, isLoading, mousePosRef, multiSelectState.isMultiDragging]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [hasActivePointerOverlay, isLoading, mousePosRef, multiSelectState.isMultiDragging]);
|
||||
|
||||
// ─── Shift+wheel → horizontal scroll ──────────────────────────────────────
|
||||
useEffect(() => {
|
||||
@@ -530,7 +547,7 @@ function TimelineViewContent({
|
||||
};
|
||||
el.addEventListener("wheel", handler, { passive: false });
|
||||
return () => el.removeEventListener("wheel", handler);
|
||||
}, [isLoading]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [isLoading]);
|
||||
|
||||
// ─── Keyboard undo/redo ───────────────────────────────────────────────────
|
||||
useEffect(() => {
|
||||
@@ -555,7 +572,10 @@ function TimelineViewContent({
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.key !== "Escape") return;
|
||||
if (multiSelectState.selectedAllocationIds.length > 0 || multiSelectState.selectedResourceIds.length > 0) {
|
||||
if (
|
||||
multiSelectState.selectedAllocationIds.length > 0 ||
|
||||
multiSelectState.selectedResourceIds.length > 0
|
||||
) {
|
||||
e.preventDefault();
|
||||
clearMultiSelect();
|
||||
return;
|
||||
@@ -579,7 +599,19 @@ function TimelineViewContent({
|
||||
};
|
||||
window.addEventListener("keydown", handler);
|
||||
return () => window.removeEventListener("keydown", handler);
|
||||
}, [demandPopover, popover, newAllocPopover, openDemandToAssign, openPanelProjectId, setPopover, setNewAllocPopover, setOpenPanelProjectId, multiSelectState.selectedAllocationIds.length, multiSelectState.selectedResourceIds.length, clearMultiSelect]);
|
||||
}, [
|
||||
demandPopover,
|
||||
popover,
|
||||
newAllocPopover,
|
||||
openDemandToAssign,
|
||||
openPanelProjectId,
|
||||
setPopover,
|
||||
setNewAllocPopover,
|
||||
setOpenPanelProjectId,
|
||||
multiSelectState.selectedAllocationIds.length,
|
||||
multiSelectState.selectedResourceIds.length,
|
||||
clearMultiSelect,
|
||||
]);
|
||||
|
||||
// ─── Resource hover card — event delegation on label columns ──────────────
|
||||
useEffect(() => {
|
||||
@@ -623,7 +655,11 @@ function TimelineViewContent({
|
||||
if (hasActivePointerOverlay) return;
|
||||
const related = e.relatedTarget as HTMLElement | null;
|
||||
// Don't close if moving into another resource-hover target or the hover card itself
|
||||
if (related?.closest?.("[data-resource-hover-id]") || related?.closest?.("[data-resource-hover-card]")) return;
|
||||
if (
|
||||
related?.closest?.("[data-resource-hover-id]") ||
|
||||
related?.closest?.("[data-resource-hover-card]")
|
||||
)
|
||||
return;
|
||||
|
||||
if (resourceHoverTimerRef.current) {
|
||||
clearTimeout(resourceHoverTimerRef.current);
|
||||
@@ -646,7 +682,7 @@ function TimelineViewContent({
|
||||
resourceHoverTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [resourceHover?.resourceId, isInitialLoading, hasActivePointerOverlay]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [resourceHover?.resourceId, isInitialLoading, hasActivePointerOverlay]);
|
||||
|
||||
// ─── Scroll-left tracking for horizontal virtualization ────────────────────
|
||||
// Updated via RAF so React state only updates after a frame, not on every
|
||||
@@ -655,7 +691,6 @@ function TimelineViewContent({
|
||||
const scrollRafRef = useRef<number | null>(null);
|
||||
const [scrollLeft, setScrollLeft] = useState(0);
|
||||
|
||||
|
||||
// ─── Navigation callbacks for TimelineToolbar ────────────────────────────
|
||||
const handleNavigateBack = useCallback(
|
||||
() => setViewStart((v) => addDays(v, -28)),
|
||||
@@ -669,8 +704,12 @@ function TimelineViewContent({
|
||||
() => setViewStart((v) => addDays(v, 28)),
|
||||
[setViewStart],
|
||||
);
|
||||
const handleUndo = useCallback(() => { void undo(); }, [undo]);
|
||||
const handleRedo = useCallback(() => { void redo(); }, [redo]);
|
||||
const handleUndo = useCallback(() => {
|
||||
void undo();
|
||||
}, [undo]);
|
||||
const handleRedo = useCallback(() => {
|
||||
void redo();
|
||||
}, [redo]);
|
||||
|
||||
// ─── Scroll handler — extends date range and tracks scroll offset ─────────
|
||||
const handleContainerScroll = useCallback(() => {
|
||||
@@ -712,12 +751,14 @@ function TimelineViewContent({
|
||||
setDemandPopover({ demand, x: anchorX, y: anchorY });
|
||||
return;
|
||||
}
|
||||
const allocation = visibleAssignments.find((entry) => (
|
||||
entry.id === info.allocationId
|
||||
|| entry.entityId === info.allocationId
|
||||
|| entry.sourceAllocationId === info.allocationId
|
||||
|| getPlanningEntryMutationId(entry) === info.allocationId
|
||||
)) ?? null;
|
||||
const allocation =
|
||||
visibleAssignments.find(
|
||||
(entry) =>
|
||||
entry.id === info.allocationId ||
|
||||
entry.entityId === info.allocationId ||
|
||||
entry.sourceAllocationId === info.allocationId ||
|
||||
getPlanningEntryMutationId(entry) === info.allocationId,
|
||||
) ?? null;
|
||||
setPopover({
|
||||
allocationId: info.allocationId,
|
||||
projectId: info.projectId,
|
||||
@@ -754,15 +795,33 @@ function TimelineViewContent({
|
||||
// memo() on ResourcePanel/ProjectPanel is not defeated by new fn refs.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const stableNoop = useCallback((..._args: any[]) => undefined, []);
|
||||
const panelOnAllocMouseDown = (isSelfServiceTimeline ? stableNoop : onAllocMouseDown) as typeof onAllocMouseDown;
|
||||
const panelOnAllocTouchStart = (isSelfServiceTimeline ? stableNoop : onAllocTouchStart) as typeof onAllocTouchStart;
|
||||
const panelOnRowMouseDown = (isSelfServiceTimeline ? stableNoop : onRowMouseDown) as typeof onRowMouseDown;
|
||||
const panelOnRowTouchStart = (isSelfServiceTimeline ? stableNoop : onRowTouchStart) as typeof onRowTouchStart;
|
||||
const panelOnAllocationContextMenu = (isSelfServiceTimeline ? stableNoop : openAllocationPopoverAt) as typeof openAllocationPopoverAt;
|
||||
const panelOnProjectBarMouseDown = (isSelfServiceTimeline ? stableNoop : onProjectBarMouseDown) as typeof onProjectBarMouseDown;
|
||||
const panelOnProjectBarTouchStart = (isSelfServiceTimeline ? stableNoop : onProjectBarTouchStart) as typeof onProjectBarTouchStart;
|
||||
const panelOnOpenPanel = (isSelfServiceTimeline ? stableNoop : setOpenPanelProjectId) as typeof setOpenPanelProjectId;
|
||||
const panelOnOpenDemandClick = (isSelfServiceTimeline ? stableNoop : handleOpenDemandClick) as typeof handleOpenDemandClick;
|
||||
const panelOnAllocMouseDown = (
|
||||
isSelfServiceTimeline ? stableNoop : onAllocMouseDown
|
||||
) as typeof onAllocMouseDown;
|
||||
const panelOnAllocTouchStart = (
|
||||
isSelfServiceTimeline ? stableNoop : onAllocTouchStart
|
||||
) as typeof onAllocTouchStart;
|
||||
const panelOnRowMouseDown = (
|
||||
isSelfServiceTimeline ? stableNoop : onRowMouseDown
|
||||
) as typeof onRowMouseDown;
|
||||
const panelOnRowTouchStart = (
|
||||
isSelfServiceTimeline ? stableNoop : onRowTouchStart
|
||||
) as typeof onRowTouchStart;
|
||||
const panelOnAllocationContextMenu = (
|
||||
isSelfServiceTimeline ? stableNoop : openAllocationPopoverAt
|
||||
) as typeof openAllocationPopoverAt;
|
||||
const panelOnProjectBarMouseDown = (
|
||||
isSelfServiceTimeline ? stableNoop : onProjectBarMouseDown
|
||||
) as typeof onProjectBarMouseDown;
|
||||
const panelOnProjectBarTouchStart = (
|
||||
isSelfServiceTimeline ? stableNoop : onProjectBarTouchStart
|
||||
) as typeof onProjectBarTouchStart;
|
||||
const panelOnOpenPanel = (
|
||||
isSelfServiceTimeline ? stableNoop : setOpenPanelProjectId
|
||||
) as typeof setOpenPanelProjectId;
|
||||
const panelOnOpenDemandClick = (
|
||||
isSelfServiceTimeline ? stableNoop : handleOpenDemandClick
|
||||
) as typeof handleOpenDemandClick;
|
||||
|
||||
// ─── Multi-select intersection computation ────────────────────────────────
|
||||
useMultiSelectIntersection({
|
||||
@@ -854,7 +913,10 @@ function TimelineViewContent({
|
||||
}}
|
||||
onTouchEnd={(e) => void onCanvasTouchEnd(e)}
|
||||
className={clsx(
|
||||
(dragState.isDragging || allocDragState.isActive || multiSelectState.isMultiDragging) && "cursor-grabbing select-none",
|
||||
(dragState.isDragging ||
|
||||
allocDragState.isActive ||
|
||||
multiSelectState.isMultiDragging) &&
|
||||
"cursor-grabbing select-none",
|
||||
rangeState.isSelecting && "cursor-crosshair select-none",
|
||||
multiSelectState.isSelecting && "cursor-crosshair select-none",
|
||||
)}
|
||||
@@ -1014,65 +1076,73 @@ function TimelineViewContent({
|
||||
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.multiDragMode === "resize-start"
|
||||
? "Start "
|
||||
: multiSelectState.multiDragMode === "resize-end"
|
||||
? "End "
|
||||
: ""}
|
||||
{multiSelectState.multiDragDaysDelta > 0 ? "+" : ""}
|
||||
{multiSelectState.multiDragDaysDelta}d
|
||||
{" "}
|
||||
({multiSelectState.selectedAllocationIds.length} allocations)
|
||||
{multiSelectState.multiDragDaysDelta}d ({multiSelectState.selectedAllocationIds.length}{" "}
|
||||
allocations)
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Allocation / Demand popover (click path) */}
|
||||
{!isSelfServiceTimeline && !hasActivePointerOverlay && popover && (() => {
|
||||
// Check if clicked allocation is actually a demand
|
||||
const clickedDemand = openDemandsByProject.get(popover.projectId)?.find((d) => d.id === popover.allocationId);
|
||||
if (clickedDemand) {
|
||||
{!isSelfServiceTimeline &&
|
||||
!hasActivePointerOverlay &&
|
||||
popover &&
|
||||
(() => {
|
||||
// Check if clicked allocation is actually a demand
|
||||
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({
|
||||
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 } : {}),
|
||||
});
|
||||
}}
|
||||
anchorX={popover.x}
|
||||
anchorY={popover.y}
|
||||
ignoreScrollContainers={[scrollContainerRef]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<DemandPopover
|
||||
demand={clickedDemand}
|
||||
<AllocationPopover
|
||||
allocationId={popover.allocationId}
|
||||
projectId={popover.projectId}
|
||||
initialAllocation={popover.allocation ?? null}
|
||||
onClose={() => setPopover(null)}
|
||||
onOpenPanel={(pid) => {
|
||||
setPopover(null);
|
||||
setOpenPanelProjectId(pid);
|
||||
}}
|
||||
onFillDemand={(d) => {
|
||||
setPopover(null);
|
||||
setOpenDemandToAssign({
|
||||
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 } : {}),
|
||||
});
|
||||
}}
|
||||
anchorX={popover.x}
|
||||
anchorY={popover.y}
|
||||
ignoreScrollContainers={[scrollContainerRef]}
|
||||
{...(popover.contextDate ? { contextDate: popover.contextDate } : {})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
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 */}
|
||||
{!isSelfServiceTimeline && !hasActivePointerOverlay && demandPopover && (
|
||||
|
||||
Reference in New Issue
Block a user