feat(ux): Sprint 1 — quick wins: EmptyState, DateRangePresets, debounce, save feedback, scenarios nav

- EmptyState shared component; replace AllocationsClient inline empty state
- DateRangePresets (this month/quarter/3 months/year) integrated into AllocationModal
- Debounce conflict-check inputs in AllocationModal (400ms) using existing useDebounce
- Dashboard layout save feedback via SuccessToast after DB write completes
- Scenarios nav item in Planning sidebar + /scenarios list page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-09 13:08:19 +02:00
parent a16c41e739
commit 6831e199c6
9 changed files with 272 additions and 35 deletions
@@ -1,6 +1,7 @@
"use client";
import { useState, useEffect, useMemo } from "react";
import { useDebounce } from "~/hooks/useDebounce.js";
import { AnimatedModal } from "~/components/ui/AnimatedModal.js";
import { useInvalidatePlanningViews } from "~/hooks/useInvalidatePlanningViews.js";
import { AllocationStatus } from "@capakraken/shared";
@@ -9,6 +10,7 @@ import { trpc } from "~/lib/trpc/client.js";
import { getPlanningEntryMutationId } from "~/lib/planningEntryIds.js";
import { toDateInputValue } from "~/lib/format.js";
import { DateInput } from "~/components/ui/DateInput.js";
import { DateRangePresets } from "~/components/ui/DateRangePresets.js";
import { RecurrenceEditor } from "./RecurrenceEditor.js";
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
import { ConflictWarningPanel } from "./ConflictWarningPanel.js";
@@ -71,22 +73,28 @@ export function AllocationModal({ allocation, onClose, onSuccess }: AllocationMo
{ enabled: shouldCheckOverlap, staleTime: 30_000 },
);
// Debounce conflict-check inputs so we don't fire on every keystroke/interaction.
const debouncedResourceId = useDebounce(resourceId, 400);
const debouncedStartDate = useDebounce(startDate, 400);
const debouncedEndDate = useDebounce(endDate, 400);
const debouncedHoursPerDay = useDebounce(hoursPerDay, 400);
// Pre-flight conflict check: overbooking + vacation overlap for this resource/period.
const conflictCheckStart = startDate ? new Date(startDate) : null;
const conflictCheckEnd = endDate ? new Date(endDate) : null;
const conflictCheckStart = debouncedStartDate ? new Date(debouncedStartDate) : null;
const conflictCheckEnd = debouncedEndDate ? new Date(debouncedEndDate) : null;
const shouldCheckConflicts =
!isDemandEntry &&
!!resourceId &&
!!debouncedResourceId &&
conflictCheckStart !== null && !isNaN(conflictCheckStart.getTime()) &&
conflictCheckEnd !== null && !isNaN(conflictCheckEnd.getTime()) &&
hoursPerDay > 0;
debouncedHoursPerDay > 0;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { data: conflictResult, isFetching: checkingConflicts } = (trpc.allocation.checkConflicts.useQuery as any)(
{
resourceId,
resourceId: debouncedResourceId,
startDate: conflictCheckStart,
endDate: conflictCheckEnd,
hoursPerDay,
hoursPerDay: debouncedHoursPerDay,
excludeAssignmentId: isEditing && allocation?.id ? allocation.id : undefined,
},
{ enabled: shouldCheckConflicts, staleTime: 15_000 },
@@ -424,10 +432,15 @@ export function AllocationModal({ allocation, onClose, onSuccess }: AllocationMo
</div>
{/* Dates */}
<div className="grid grid-cols-2 gap-4">
<div>
<div className="flex items-center justify-between mb-1">
<span className={labelClass}>Date Range <span className="text-red-500">*</span></span>
<DateRangePresets onSelect={(s, e) => { setStartDate(s); setEndDate(e); }} />
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label htmlFor="modal-start" className={labelClass}>
Start Date <span className="text-red-500">*</span><InfoTooltip content="First day of this allocation period (inclusive)." />
Start Date <InfoTooltip content="First day of this allocation period (inclusive)." />
</label>
<DateInput
id="modal-start"
@@ -439,7 +452,7 @@ export function AllocationModal({ allocation, onClose, onSuccess }: AllocationMo
</div>
<div>
<label htmlFor="modal-end" className={labelClass}>
End Date <span className="text-red-500">*</span><InfoTooltip content="Last day of this allocation period (inclusive)." />
End Date <InfoTooltip content="Last day of this allocation period (inclusive)." />
</label>
<DateInput
id="modal-end"
@@ -450,6 +463,7 @@ export function AllocationModal({ allocation, onClose, onSuccess }: AllocationMo
required
/>
</div>
</div>
</div>
{/* Hours/Day + Status */}