fix(web): make invalidation hooks async with Promise.all and fix cross-view staleness

- useInvalidateTimeline and useInvalidatePlanningViews now return
  Promise.all instead of fire-and-forget void calls
- Timeline mutations now use useInvalidatePlanningViews to also
  invalidate allocation list views, preventing stale data
- AllocationsClient sequential awaits replaced with single
  invalidatePlanningViews() call (parallel invalidation)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-11 08:24:33 +02:00
parent f18777c365
commit f3fa902773
11 changed files with 372 additions and 229 deletions
@@ -6,7 +6,7 @@ import { useState } from "react";
import { createPortal } from "react-dom";
import { AllocationStatus } from "@capakraken/shared";
import { trpc } from "~/lib/trpc/client.js";
import { useInvalidateTimeline } from "~/hooks/useInvalidatePlanningViews.js";
import { useInvalidatePlanningViews } from "~/hooks/useInvalidatePlanningViews.js";
import { useViewportPopover } from "~/hooks/useViewportPopover.js";
import { DateInput } from "~/components/ui/DateInput.js";
import { ProjectCombobox } from "~/components/ui/ProjectCombobox.js";
@@ -50,7 +50,7 @@ export function NewAllocationPopover({
ignoreSelectors: ["[data-entity-combobox-overlay='true']"],
...(ignoreScrollContainers ? { ignoreScrollContainers } : {}),
});
const invalidateTimeline = useInvalidateTimeline();
const invalidatePlanningViews = useInvalidatePlanningViews();
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(
suggestedProjectId ?? null,
@@ -62,7 +62,7 @@ export function NewAllocationPopover({
const createMutation = trpc.timeline.quickAssign.useMutation({
onSuccess: () => {
invalidateTimeline();
void invalidatePlanningViews();
onCreated();
onClose();
},
@@ -91,15 +91,24 @@ export function NewAllocationPopover({
>
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 bg-gray-50 dark:bg-gray-900 border-b border-gray-100 dark:border-gray-700">
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">Assign to Project</span>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 text-lg leading-none">&times;</button>
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">
Assign to Project
</span>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 text-lg leading-none"
>
&times;
</button>
</div>
<div className="space-y-3 overflow-y-auto p-4">
{/* Date range */}
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Start</label>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
Start
</label>
<DateInput
value={start}
onChange={setStart}
@@ -107,7 +116,9 @@ export function NewAllocationPopover({
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">End</label>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
End
</label>
<DateInput
value={end}
onChange={setEnd}
@@ -119,7 +130,9 @@ export function NewAllocationPopover({
{/* Project picker */}
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Project</label>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
Project
</label>
<ProjectCombobox
value={selectedProjectId}
onChange={setSelectedProjectId}
@@ -130,7 +143,9 @@ export function NewAllocationPopover({
{/* Role */}
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Role</label>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
Role
</label>
<input
type="text"
value={role}
@@ -141,7 +156,9 @@ export function NewAllocationPopover({
{/* Hours per day */}
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Hours / day</label>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
Hours / day
</label>
<div className="flex items-center gap-2">
<input
type="number"