fix(web): keep segmented timeline allocations actionable

This commit is contained in:
2026-04-01 08:54:15 +02:00
parent 6249f61ce1
commit 4edf3a32ac
5 changed files with 334 additions and 48 deletions
@@ -1,9 +1,10 @@
"use client";
import React from "react";
import { clsx } from "clsx";
import { useEffect, useRef, useState } from "react";
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
import type { AllocationLike, AllocationReadModel, Assignment } from "@capakraken/shared";
import type { AllocationLike, Assignment } from "@capakraken/shared";
import { trpc } from "~/lib/trpc/client.js";
import { useInvalidateTimeline } from "~/hooks/useInvalidatePlanningViews.js";
import { useViewportPopover } from "~/hooks/useViewportPopover.js";
@@ -43,16 +44,19 @@ export function AllocationPopover({
onClose,
});
const { data: allocationView, isLoading } = trpc.allocation.listView.useQuery(
{ projectId },
{ staleTime: 10_000, enabled: !initialAllocation },
) as { data: AllocationReadModel<AllocationLike> | undefined; isLoading: boolean };
const allocation = initialAllocation ?? allocationView?.assignments.find((entry) => (
entry.id === allocationId
|| entry.entityId === allocationId
|| entry.sourceAllocationId === allocationId
|| getPlanningEntryMutationId(entry) === allocationId
)) as AllocationPopoverAssignment | undefined;
const shouldLoadAllocation = !initialAllocation;
const allocationQuery = trpc.allocation.getAssignmentById.useQuery(
{ id: allocationId },
{
staleTime: 10_000,
enabled: shouldLoadAllocation,
retry: false,
},
);
const fetchedAllocation = allocationQuery.data as AllocationPopoverAssignment | undefined;
const allocation = initialAllocation ?? fetchedAllocation;
const isLoading = shouldLoadAllocation && allocationQuery.isLoading;
const allocationError = shouldLoadAllocation ? allocationQuery.error : null;
const [hoursPerDay, setHoursPerDay] = useState<number | null>(null);
const [startDate, setStartDate] = useState<string>("");
@@ -79,6 +83,7 @@ export function AllocationPopover({
const updateMutation = trpc.timeline.updateAllocationInline.useMutation({
onSuccess: () => {
invalidateTimeline();
void utils.allocation.getAssignmentById.invalidate({ id: allocationId });
void utils.allocation.listView.invalidate();
onClose();
},
@@ -87,6 +92,7 @@ export function AllocationPopover({
const carveMutation = trpc.timeline.carveAllocationRange.useMutation({
onSuccess: () => {
invalidateTimeline();
void utils.allocation.getAssignmentById.invalidate({ id: allocationId });
void utils.allocation.listView.invalidate();
onClose();
},
@@ -122,18 +128,48 @@ export function AllocationPopover({
if (isLoading) {
const loadingPopover = (
<div ref={ref} style={style} className="bg-white border border-gray-200 rounded-xl shadow-xl p-4 text-sm text-gray-500">
<div
ref={ref}
style={style}
data-testid="timeline-allocation-popover-loading"
className="bg-white border border-gray-200 rounded-xl shadow-xl p-4 text-sm text-gray-500"
>
Loading...
</div>
);
return typeof document === "undefined" ? loadingPopover : createPortal(loadingPopover, document.body);
}
if (allocationError) {
const errorPopover = (
<div
ref={ref}
style={style}
data-testid="timeline-allocation-popover-error"
className="flex max-w-[300px] flex-col gap-3 rounded-xl border border-red-200 bg-white p-4 shadow-xl"
>
<div className="text-sm font-medium text-gray-800">Allocation unavailable</div>
<p className="text-xs text-gray-500">
The selected booking could not be loaded right now.
</p>
<p className="text-xs text-red-600">{allocationError.message}</p>
<button
onClick={() => { onClose(); onOpenPanel(projectId); }}
className="w-full rounded-lg bg-brand-600 px-3 py-2 text-sm font-medium text-white hover:bg-brand-700"
>
Open Project Panel
</button>
</div>
);
return typeof document === "undefined" ? errorPopover : createPortal(errorPopover, document.body);
}
if (!allocation) {
const missingPopover = (
<div
ref={ref}
style={style}
data-testid="timeline-allocation-popover-unavailable"
className="flex max-w-[300px] flex-col gap-3 rounded-xl border border-gray-200 bg-white p-4 shadow-xl"
>
<div className="text-sm font-medium text-gray-800">Allocation unavailable</div>
@@ -160,6 +196,8 @@ export function AllocationPopover({
<div
ref={ref}
style={style}
data-testid="timeline-allocation-popover"
data-allocation-id={allocationId}
className="flex max-h-[calc(100vh-32px)] flex-col overflow-hidden rounded-xl border border-gray-200 bg-white shadow-xl"
>
{/* Header */}