fix(web): keep segmented timeline allocations actionable
This commit is contained in:
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user