- {mode === "edit" && canManageUsers && resource && (
- confirmDelete ? (
+ {mode === "edit" &&
+ canManageUsers &&
+ resource &&
+ (confirmDelete ? (
- Permanently delete this resource?
+
+ Permanently delete this resource?
+
void hardDeleteMutation.mutateAsync({ id: resource.id })}
@@ -1010,8 +741,7 @@ export function ResourceModal({ mode, resource, onClose, onSuccess }: ResourceMo
>
Delete Resource
- )
- )}
+ ))}
void;
+ countryOptions: CountryOption[];
+ orgUnitOptions: OrgUnitOption[];
+ clientOptions: ClientOption[];
+ managementGroupOptions: ManagementGroupOption[];
+ inputClass: string;
+ labelClass: string;
+ sectionHeaderClass: string;
+}
+
+export function ResourceOrgClassification({
+ form,
+ onSetField,
+ countryOptions,
+ orgUnitOptions,
+ clientOptions,
+ managementGroupOptions,
+ inputClass,
+ labelClass,
+ sectionHeaderClass,
+}: ResourceOrgClassificationProps) {
+ const selectedCountry = countryOptions.find((c) => c.id === form.countryId);
+ const metroCities = selectedCountry?.metroCities ?? [];
+ const selectedGroup = managementGroupOptions.find((g) => g.id === form.managementLevelGroupId);
+ const mgmtLevels = selectedGroup?.levels ?? [];
+
+ return (
+ <>
+ {/* Postal Code & Federal State */}
+
+
+
+ Postal Code (PLZ){" "}
+ (optional)
+
+
+ {
+ const plz = e.target.value;
+ onSetField("postalCode", plz);
+ if (/^\d{5}$/.test(plz)) {
+ const inferred = inferStateFromPostalCode(plz);
+ if (inferred && !form.federalState) {
+ onSetField("federalState", inferred);
+ }
+ }
+ }}
+ />
+
+
+
+ Federal State{" "}
+ (optional)
+
+
+ onSetField("federalState", e.target.value)}
+ >
+ — Not specified —
+ {Object.entries(GERMAN_FEDERAL_STATES).map(([abbr, name]) => (
+
+ {name} ({abbr})
+
+ ))}
+
+
+
+
+ {/* Section: Organization & Classification */}
+ Organization & Classification
+
+
+
+
+
+
+ Country
+
+ {
+ onSetField("countryId", e.target.value);
+ onSetField("metroCityId", "");
+ }}
+ >
+ — Not specified —
+ {countryOptions.map((c) => (
+
+ {c.name}
+
+ ))}
+
+
+
+
+ Metro City
+
+ onSetField("metroCityId", e.target.value)}
+ disabled={!form.countryId}
+ >
+ — Not specified —
+ {metroCities.map((c) => (
+
+ {c.name}
+
+ ))}
+
+
+
+
+
+
+
+ Org Unit (L7 Team)
+
+ onSetField("orgUnitId", e.target.value)}
+ >
+ — Not specified —
+ {orgUnitOptions
+ .filter((u) => u.level === 7 && u.isActive)
+ .map((u) => (
+
+ {u.name}
+
+ ))}
+
+
+
+
+ Client Unit
+
+ onSetField("clientUnitId", e.target.value)}
+ >
+ — Not specified —
+ {clientOptions.map((c) => (
+
+ {c.name}
+
+ ))}
+
+
+
+
+
+
+
+ Management Level Group
+
+
+ {
+ onSetField("managementLevelGroupId", e.target.value);
+ onSetField("managementLevelId", "");
+ }}
+ >
+ — Not specified —
+ {managementGroupOptions.map((g) => (
+
+ {g.name}
+
+ ))}
+
+
+
+
+ Management Level
+
+
+ onSetField("managementLevelId", e.target.value)}
+ disabled={!form.managementLevelGroupId}
+ >
+ — Not specified —
+ {mgmtLevels.map((l) => (
+
+ {l.name}
+
+ ))}
+
+
+
+
+
+
+
+ Resource Type
+
+
+ onSetField("resourceType", e.target.value)}
+ >
+ {Object.values(ResourceType).map((t) => (
+
+ {t.charAt(0) + t.slice(1).toLowerCase()}
+
+ ))}
+
+
+
+
+ onSetField("chgResponsibility", e.target.checked)}
+ className="rounded border-gray-300 text-brand-600 focus:ring-brand-500"
+ />
+ Chg Responsibility
+
+
+
+
+ onSetField("rolledOff", e.target.checked)}
+ className="rounded border-gray-300 text-brand-600 focus:ring-brand-500"
+ />
+ Rolled Off
+
+
+
+
+ onSetField("departed", e.target.checked)}
+ className="rounded border-gray-300 text-brand-600 focus:ring-brand-500"
+ />
+ Departed
+
+
+
+ >
+ );
+}
diff --git a/apps/web/src/components/resources/ResourceSkillsEditor.tsx b/apps/web/src/components/resources/ResourceSkillsEditor.tsx
new file mode 100644
index 0000000..2811805
--- /dev/null
+++ b/apps/web/src/components/resources/ResourceSkillsEditor.tsx
@@ -0,0 +1,152 @@
+interface SkillRow {
+ skill: string;
+ proficiency: 1 | 2 | 3 | 4 | 5;
+ yearsExperience: string;
+ category: string;
+ certified: boolean;
+ isMainSkill: boolean;
+}
+
+const proficiencyLabels: Record = {
+ 1: "1 \u2013 Beginner",
+ 2: "2 \u2013 Elementary",
+ 3: "3 \u2013 Intermediate",
+ 4: "4 \u2013 Advanced",
+ 5: "5 \u2013 Expert",
+};
+
+interface ResourceSkillsEditorProps {
+ skills: SkillRow[];
+ onSetSkillField: (index: number, key: keyof SkillRow, value: string | number | boolean) => void;
+ onAddSkill: () => void;
+ onRemoveSkill: (index: number) => void;
+ inputClass: string;
+ labelClass: string;
+}
+
+export function ResourceSkillsEditor({
+ skills,
+ onSetSkillField,
+ onAddSkill,
+ onRemoveSkill,
+ inputClass,
+ labelClass,
+}: ResourceSkillsEditorProps) {
+ return (
+
+ {skills.map((skillRow, idx) => {
+ const mainSkillCount = skills.filter((s) => s.isMainSkill).length;
+ const canToggleMain = skillRow.isMainSkill || mainSkillCount < 2;
+ return (
+
+
+
+
+ Skill
+
+ onSetSkillField(idx, "skill", e.target.value)}
+ />
+
+
+
+ Proficiency
+
+
+ onSetSkillField(
+ idx,
+ "proficiency",
+ parseInt(e.target.value, 10) as 1 | 2 | 3 | 4 | 5,
+ )
+ }
+ >
+ {[1, 2, 3, 4, 5].map((p) => (
+
+ {proficiencyLabels[p]}
+
+ ))}
+
+
+
+
+ Years
+
+ onSetSkillField(idx, "yearsExperience", e.target.value)}
+ />
+
+
+
+ \u2605 Main
+
+ onSetSkillField(idx, "isMainSkill", e.target.checked)}
+ className="rounded border-gray-300 disabled:opacity-40"
+ />
+
+
+
onRemoveSkill(idx)}
+ className="px-2 py-2 text-red-400 hover:text-red-600 transition-colors"
+ aria-label={`Remove skill ${idx + 1}`}
+ >
+
+
+
+
+
+
+
+ );
+ })}
+
+
+
+
+
+ Add skill
+
+
+ );
+}
diff --git a/apps/web/src/components/timeline/TimelineDragOverlays.tsx b/apps/web/src/components/timeline/TimelineDragOverlays.tsx
new file mode 100644
index 0000000..b99d654
--- /dev/null
+++ b/apps/web/src/components/timeline/TimelineDragOverlays.tsx
@@ -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["dragState"];
+ allocDragState: ReturnType["allocDragState"];
+ rangeState: ReturnType["rangeState"];
+ multiSelectState: ReturnType["multiSelectState"];
+ shiftPreview: ReturnType["shiftPreview"];
+ isPreviewLoading: boolean;
+ isApplying: boolean;
+ isAllocSaving: boolean;
+ mousePosRef: React.RefObject<{ x: number; y: number }>;
+ dragTooltipRef: React.RefObject;
+ allocTooltipRef: React.RefObject;
+ rangeHintRef: React.RefObject;
+ multiDragTooltipRef: React.RefObject;
+ 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 && (
+
+ )}
+
+ {/* Saving indicators */}
+ {(isApplying || isAllocSaving) && (
+
+
+ {isApplying ? "Applying shift…" : "Saving…"}
+
+
+ )}
+
+ {/* Drag preview tooltip */}
+ {dragState.isDragging && dragState.daysDelta !== 0 && (
+
+
+
+ )}
+
+ {/* Alloc drag tooltip */}
+ {allocDragState.isActive &&
+ allocDragState.daysDelta !== 0 &&
+ allocDragState.currentStartDate &&
+ allocDragState.currentEndDate && (
+
+
{allocDragState.projectName}
+
+ {formatDateShort(allocDragState.currentStartDate)}
+ {" – "}
+ {formatDateShort(allocDragState.currentEndDate)}
+
+
+ )}
+
+ {/* Range-select hint */}
+ {rangeState.isSelecting && rangeState.startDate && rangeState.currentDate && (
+
+ {(() => {
+ 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" : ""}`;
+ })()}
+
+ )}
+
+ {/* Multi-drag tooltip */}
+ {multiSelectState.isMultiDragging && multiSelectState.multiDragDaysDelta !== 0 && (
+
+ {multiSelectState.multiDragMode === "resize-start"
+ ? "Start "
+ : multiSelectState.multiDragMode === "resize-end"
+ ? "End "
+ : ""}
+ {multiSelectState.multiDragDaysDelta > 0 ? "+" : ""}
+ {multiSelectState.multiDragDaysDelta}d ({multiSelectState.selectedAllocationIds.length}{" "}
+ allocations)
+
+ )}
+ >
+ );
+}
diff --git a/apps/web/src/components/timeline/TimelinePopovers.tsx b/apps/web/src/components/timeline/TimelinePopovers.tsx
new file mode 100644
index 0000000..6460385
--- /dev/null
+++ b/apps/web/src/components/timeline/TimelinePopovers.tsx
@@ -0,0 +1,262 @@
+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>;
+ demandPopover: { demand: TimelineDemandEntry; x: number; y: number } | null;
+ setDemandPopover: React.Dispatch>;
+ 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
+ >;
+ enrichedSuggestedProjectId: string | null;
+ openPanelProjectId: string | null;
+ setOpenPanelProjectId: React.Dispatch>;
+ openDemandToAssign: OpenDemandAssignment | null;
+ setOpenDemandToAssign: React.Dispatch>;
+ openDemandsByProject: Map;
+ scrollContainerRef: React.RefObject;
+ multiSelectState: ReturnType["multiSelectState"];
+ clearMultiSelect: ReturnType["clearMultiSelect"];
+ handleBatchDelete: () => void;
+ handleShowBatchAssign: () => void;
+ isDeleting: boolean;
+ showBatchAssign: boolean;
+ setShowBatchAssign: React.Dispatch>;
+ resourceHover: { resourceId: string; anchorEl: HTMLElement } | null;
+ setResourceHover: React.Dispatch>;
+ inlineEditTarget: {
+ allocationId: string;
+ startDate: Date;
+ endDate: Date;
+ hoursPerDay: number;
+ barRect: DOMRect;
+ } | null;
+ setInlineEditTarget: React.Dispatch<
+ React.SetStateAction
+ >;
+ showShortcuts: boolean;
+ setShowShortcuts: React.Dispatch>;
+}
+
+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 (
+ setPopover(null)}
+ onOpenPanel={(pid) => {
+ setPopover(null);
+ setOpenPanelProjectId(pid);
+ }}
+ onFillDemand={(d) => {
+ setPopover(null);
+ setOpenDemandToAssign(buildDemandAssignment(d));
+ }}
+ anchorX={popover.x}
+ anchorY={popover.y}
+ ignoreScrollContainers={[scrollContainerRef]}
+ />
+ );
+ }
+ return (
+ 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 && (
+ 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 && (
+ setNewAllocPopover(null)}
+ onCreated={() => setNewAllocPopover(null)}
+ ignoreScrollContainers={[scrollContainerRef]}
+ />
+ )}
+
+ {/* Project side panel */}
+ {!isSelfServiceTimeline && openPanelProjectId && (
+ setOpenPanelProjectId(null)} />
+ )}
+
+ {/* Open-demand assignment modal */}
+ {!isSelfServiceTimeline && openDemandToAssign && (
+ setOpenDemandToAssign(null)}
+ onSuccess={() => setOpenDemandToAssign(null)}
+ />
+ )}
+
+ {/* Multi-select floating action bar + batch assign */}
+ {showBatchAssign && multiSelectState.dateRange && (
+ setShowBatchAssign(false)}
+ onCreated={() => {
+ setShowBatchAssign(false);
+ clearMultiSelect();
+ }}
+ />
+ )}
+
+ {/* Resource hover card */}
+ {!hasActivePointerOverlay && resourceHover && (
+ setResourceHover(null)}
+ />
+ )}
+
+ {/* Inline allocation editor */}
+ {inlineEditTarget && (
+ setInlineEditTarget(null)}
+ onSaved={() => setInlineEditTarget(null)}
+ />
+ )}
+
+ {/* Keyboard shortcut overlay */}
+ {showShortcuts && setShowShortcuts(false)} />}
+
+ {/* Keyboard shortcut hint button */}
+ 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"
+ >
+ ?
+
+ >
+ );
+}
diff --git a/apps/web/src/components/timeline/TimelineView.tsx b/apps/web/src/components/timeline/TimelineView.tsx
index 3542ab1..d515a11 100644
--- a/apps/web/src/components/timeline/TimelineView.tsx
+++ b/apps/web/src/components/timeline/TimelineView.tsx
@@ -1,6 +1,5 @@
"use client";
-import { MILLISECONDS_PER_DAY } from "@capakraken/shared";
import { clsx } from "clsx";
import { useSession } from "next-auth/react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -11,21 +10,14 @@ import { useTimelineLayout } from "~/hooks/useTimelineLayout.js";
import { trpc } from "~/lib/trpc/client.js";
import { useInvalidatePlanningViews } from "~/hooks/useInvalidatePlanningViews.js";
import { getPlanningEntryMutationId } from "~/lib/planningEntryIds.js";
-import { FillOpenDemandModal } from "~/components/allocations/FillOpenDemandModal.js";
-import { AllocationPopover } from "./AllocationPopover.js";
-import { DemandPopover } from "./DemandPopover.js";
-import { ResourceHoverCard } from "./ResourceHoverCard.js";
import type { TimelineDemandEntry } from "./TimelineContext.js";
-import { BatchAssignPopover } from "./BatchAssignPopover.js";
import { FloatingActionBar } from "./FloatingActionBar.js";
-import { NewAllocationPopover } from "./NewAllocationPopover.js";
-import { ProjectPanel } from "./ProjectPanel.js";
-import { ShiftPreviewTooltip } from "./ShiftPreviewTooltip.js";
+import { TimelineDragOverlays } from "./TimelineDragOverlays.js";
import { TimelineHeader } from "./TimelineHeader.js";
+import { TimelinePopovers } from "./TimelinePopovers.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,
useTimelineData,
@@ -984,228 +976,23 @@ function TimelineViewContent({
)}
- {/* Multi-select rectangle overlay */}
- {multiSelectState.isSelecting && (
-
- )}
+
- {/* Saving indicators */}
- {(isApplying || isAllocSaving) && (
-
-
- {isApplying ? "Applying shift…" : "Saving…"}
-
-
- )}
-
- {/* Drag preview tooltip */}
- {dragState.isDragging && dragState.daysDelta !== 0 && (
-
-
-
- )}
-
- {/* Alloc drag tooltip */}
- {allocDragState.isActive &&
- allocDragState.daysDelta !== 0 &&
- allocDragState.currentStartDate &&
- allocDragState.currentEndDate && (
-
-
{allocDragState.projectName}
-
- {formatDateShort(allocDragState.currentStartDate)}
- {" – "}
- {formatDateShort(allocDragState.currentEndDate)}
-
-
- )}
-
- {/* Range-select hint */}
- {rangeState.isSelecting && rangeState.startDate && rangeState.currentDate && (
-
- {(() => {
- 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" : ""}`;
- })()}
-
- )}
-
- {/* Multi-drag tooltip */}
- {multiSelectState.isMultiDragging && multiSelectState.multiDragDaysDelta !== 0 && (
-
- {multiSelectState.multiDragMode === "resize-start"
- ? "Start "
- : multiSelectState.multiDragMode === "resize-end"
- ? "End "
- : ""}
- {multiSelectState.multiDragDaysDelta > 0 ? "+" : ""}
- {multiSelectState.multiDragDaysDelta}d ({multiSelectState.selectedAllocationIds.length}{" "}
- allocations)
-
- )}
-
- {/* 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) {
- return (
-
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 (
- 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 && (
- setDemandPopover(null)}
- onOpenPanel={(pid) => {
- setDemandPopover(null);
- setOpenPanelProjectId(pid);
- }}
- onFillDemand={(d) => {
- setDemandPopover(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={demandPopover.x}
- anchorY={demandPopover.y}
- ignoreScrollContainers={[scrollContainerRef]}
- />
- )}
-
- {/* New allocation popover */}
- {!isSelfServiceTimeline && newAllocPopover && (
- setNewAllocPopover(null)}
- onCreated={() => setNewAllocPopover(null)}
- ignoreScrollContainers={[scrollContainerRef]}
- />
- )}
-
- {/* Project side panel */}
- {!isSelfServiceTimeline && openPanelProjectId && (
- setOpenPanelProjectId(null)} />
- )}
-
- {/* Open-demand assignment modal */}
- {!isSelfServiceTimeline && openDemandToAssign && (
- setOpenDemandToAssign(null)}
- onSuccess={() => setOpenDemandToAssign(null)}
- />
- )}
-
- {/* Multi-select floating action bar */}
- {/* Batch assign popover */}
- {showBatchAssign && multiSelectState.dateRange && (
- setShowBatchAssign(false)}
- onCreated={() => {
- setShowBatchAssign(false);
- clearMultiSelect();
- }}
- />
- )}
-
- {/* Resource hover card */}
- {!hasActivePointerOverlay && resourceHover && (
- setResourceHover(null)}
- />
- )}
-
- {/* Inline allocation editor */}
- {inlineEditTarget && (
- setInlineEditTarget(null)}
- onSaved={() => setInlineEditTarget(null)}
- />
- )}
-
- {/* Keyboard shortcut overlay */}
- {showShortcuts && setShowShortcuts(false)} />}
-
- {/* Keyboard shortcut hint button */}
- 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"
- >
- ?
-
+