feat(planning): ship holiday-aware planning and assistant upgrades
This commit is contained in:
@@ -188,8 +188,10 @@ function TimelineProjectPanelInner({
|
||||
} | null>(null);
|
||||
const heatmapTooltipRef = useRef<HTMLDivElement | null>(null);
|
||||
const vacationTooltipRef = useRef<HTMLDivElement | null>(null);
|
||||
const demandTooltipRef = useRef<HTMLDivElement | null>(null);
|
||||
const heatmapTooltipPosRef = useRef({ left: 0, top: 0 });
|
||||
const vacationTooltipPosRef = useRef({ left: 0, top: 0 });
|
||||
const demandTooltipPosRef = useRef({ left: 0, top: 0 });
|
||||
|
||||
const [heatmapHover, setHeatmapHover] = useState<{
|
||||
date: Date;
|
||||
@@ -206,6 +208,22 @@ function TimelineProjectPanelInner({
|
||||
approvedBy?: { name?: string | null; email: string } | null;
|
||||
approvedAt?: Date | string | null;
|
||||
}>(null);
|
||||
const [demandHover, setDemandHover] = useState<null | {
|
||||
roleName: string;
|
||||
roleColor: string;
|
||||
projectName: string;
|
||||
projectShortCode?: string | null;
|
||||
requestedHeadcount: number;
|
||||
unfilledHeadcount: number;
|
||||
startDate: Date | string;
|
||||
endDate: Date | string;
|
||||
hoursPerDay: number;
|
||||
totalHours: number;
|
||||
percentage?: number;
|
||||
status?: string;
|
||||
totalCostCents?: number;
|
||||
dailyCostCents?: number;
|
||||
}>(null);
|
||||
|
||||
const { resourceHeatmapById, resourceTotalHoursById } = useMemo(() => {
|
||||
const dateIndexByTime = new Map<number, number>();
|
||||
@@ -472,6 +490,7 @@ function TimelineProjectPanelInner({
|
||||
vacationHoverRafRef.current = requestAnimationFrame(() => {
|
||||
vacationHoverRafRef.current = null;
|
||||
const date = xToDate(clientX, rect);
|
||||
date.setHours(0, 0, 0, 0);
|
||||
const time = date.getTime();
|
||||
const resourceVacations = vacationsByResource.get(resourceId) ?? [];
|
||||
const hit =
|
||||
@@ -507,18 +526,58 @@ function TimelineProjectPanelInner({
|
||||
|
||||
const shouldClearHeatmap = lastHeatmapDayRef.current !== -1;
|
||||
const shouldClearVacation = hoveredVacationKeyRef.current !== null;
|
||||
const shouldClearDemand = demandHover !== null;
|
||||
|
||||
lastHeatmapDayRef.current = -1;
|
||||
lastHeatmapResourceRef.current = null;
|
||||
hoveredVacationKeyRef.current = null;
|
||||
|
||||
if (shouldClearHeatmap || shouldClearVacation) {
|
||||
if (shouldClearHeatmap || shouldClearVacation || shouldClearDemand) {
|
||||
startTransition(() => {
|
||||
if (shouldClearHeatmap) setHeatmapHover(null);
|
||||
if (shouldClearVacation) setVacationHover(null);
|
||||
if (shouldClearDemand) setDemandHover(null);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
}, [demandHover]);
|
||||
|
||||
const handleDemandHoverMove = useCallback(
|
||||
(e: React.MouseEvent, demand: TimelineDemandEntry) => {
|
||||
demandTooltipPosRef.current = { left: e.clientX + 16, top: e.clientY - 36 };
|
||||
if (demandTooltipRef.current) {
|
||||
demandTooltipRef.current.style.left = `${demandTooltipPosRef.current.left}px`;
|
||||
demandTooltipRef.current.style.top = `${demandTooltipPosRef.current.top}px`;
|
||||
}
|
||||
|
||||
const startDate = new Date(demand.startDate);
|
||||
const endDate = new Date(demand.endDate);
|
||||
const days = Math.max(1, Math.round((endDate.getTime() - startDate.getTime()) / 86_400_000) + 1);
|
||||
|
||||
startTransition(() => {
|
||||
setDemandHover({
|
||||
roleName: demand.roleEntity?.name ?? demand.role ?? "Open demand",
|
||||
roleColor: demand.roleEntity?.color ?? "#f59e0b",
|
||||
projectName: demand.project.name,
|
||||
projectShortCode: demand.project.shortCode,
|
||||
requestedHeadcount: demand.requestedHeadcount,
|
||||
unfilledHeadcount: demand.unfilledHeadcount,
|
||||
startDate: demand.startDate,
|
||||
endDate: demand.endDate,
|
||||
hoursPerDay: demand.hoursPerDay,
|
||||
totalHours: demand.hoursPerDay * days,
|
||||
percentage: demand.percentage,
|
||||
status: demand.status,
|
||||
...(demand.dailyCostCents > 0
|
||||
? {
|
||||
totalCostCents: demand.dailyCostCents * days,
|
||||
dailyCostCents: demand.dailyCostCents,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
@@ -672,6 +731,8 @@ function TimelineProjectPanelInner({
|
||||
onAllocMouseDown,
|
||||
onAllocTouchStart,
|
||||
onAllocationContextMenu,
|
||||
handleDemandHoverMove,
|
||||
clearHoverTooltips,
|
||||
multiSelectState,
|
||||
allocDragState,
|
||||
)
|
||||
@@ -699,6 +760,9 @@ function TimelineProjectPanelInner({
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-testid="timeline-project-resource-row-canvas"
|
||||
data-project-id={row.project.id}
|
||||
data-resource-id={row.resource.id}
|
||||
className="relative overflow-hidden touch-none"
|
||||
style={{
|
||||
width: totalCanvasWidth,
|
||||
@@ -792,8 +856,11 @@ function TimelineProjectPanelInner({
|
||||
heatmapTooltipPos={heatmapTooltipPosRef.current}
|
||||
vacationTooltipRef={vacationTooltipRef}
|
||||
vacationTooltipPos={vacationTooltipPosRef.current}
|
||||
demandTooltipRef={demandTooltipRef}
|
||||
demandTooltipPos={demandTooltipPosRef.current}
|
||||
heatmapHover={heatmapHover}
|
||||
vacationHover={vacationHover}
|
||||
demandHover={demandHover}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -852,6 +919,8 @@ function renderOpenDemandRow(
|
||||
anchorX: number,
|
||||
anchorY: number,
|
||||
) => void,
|
||||
onDemandHoverMove: (e: React.MouseEvent, demand: TimelineDemandEntry) => void,
|
||||
onClearHoverTooltips: () => void,
|
||||
multiSelectState: MultiSelectState,
|
||||
allocDragState: AllocDragState,
|
||||
) {
|
||||
@@ -889,6 +958,7 @@ function renderOpenDemandRow(
|
||||
<div
|
||||
className="relative overflow-hidden bg-amber-50 touch-none dark:bg-slate-950"
|
||||
style={{ width: totalCanvasWidth, height: rowHeight }}
|
||||
onMouseLeave={onClearHoverTooltips}
|
||||
>
|
||||
{rowGridLines}
|
||||
<div className="pointer-events-none absolute inset-x-0 inset-y-0 border-y border-dashed border-amber-200/70 dark:border-amber-800/80" />
|
||||
@@ -962,7 +1032,6 @@ function renderOpenDemandRow(
|
||||
: "hover:ring-2 hover:ring-amber-400 hover:ring-offset-1",
|
||||
multiSelectState.selectedAllocationIds.includes(alloc.id) && "ring-2 ring-sky-500 ring-offset-1 z-20",
|
||||
)}
|
||||
title={`${roleName}${headcount > 1 ? ` x${headcount}` : ""} · ${alloc.hoursPerDay}h/day · ${formatDateLong(allocStart)} – ${formatDateLong(allocEnd)}`}
|
||||
style={{
|
||||
left: left + 2,
|
||||
width: width - 4,
|
||||
@@ -986,6 +1055,7 @@ function renderOpenDemandRow(
|
||||
e.clientY,
|
||||
);
|
||||
}}
|
||||
onMouseMove={(e) => onDemandHoverMove(e, alloc)}
|
||||
>
|
||||
{/* Left resize handle */}
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user