diff --git a/apps/web/src/components/allocations/AllocationsClient.tsx b/apps/web/src/components/allocations/AllocationsClient.tsx index c72d8e5..8a8dbb4 100644 --- a/apps/web/src/components/allocations/AllocationsClient.tsx +++ b/apps/web/src/components/allocations/AllocationsClient.tsx @@ -24,6 +24,7 @@ import { getPlanningEntryMutationId } from "~/lib/planningEntryIds.js"; import { ALLOCATION_STATUS_BADGE as STATUS_BADGE } from "~/lib/status-styles.js"; import { SuccessToast } from "~/components/ui/SuccessToast.js"; import { EmptyState } from "~/components/ui/EmptyState.js"; +import { BatchDateShiftModal } from "./BatchDateShiftModal.js"; import { collapseAllAllocationGroups, createInitialCollapsedAllocationGroups, @@ -114,6 +115,7 @@ export function AllocationsClient() { const [batchStatusPicker, setBatchStatusPicker] = useState(false); const [confirmBatchStatus, setConfirmBatchStatus] = useState<{ ids: string[]; status: string } | null>(null); const [showStatusToast, setShowStatusToast] = useState(false); + const [showDateShiftModal, setShowDateShiftModal] = useState(false); const selection = useSelection(); const utils = trpc.useUtils(); @@ -175,6 +177,15 @@ export function AllocationsClient() { }, }); + const batchDateShiftMutation = trpc.timeline.batchShiftAllocations.useMutation({ + onSuccess: async () => { + await utils.allocation.list.invalidate(); + await utils.allocation.listView.invalidate(); + selection.clear(); + setShowDateShiftModal(false); + }, + }); + useEffect(() => { selection.clear(); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -1024,6 +1035,11 @@ export function AllocationsClient() { onClick: () => setBatchStatusPicker(true), disabled: batchStatusMutation.isPending, }, + { + label: "Shift Dates…", + onClick: () => setShowDateShiftModal(true), + disabled: batchDateShiftMutation.isPending, + }, { label: `Delete (${selection.count})`, variant: "danger", @@ -1033,6 +1049,18 @@ export function AllocationsClient() { ]} /> + {/* Batch date shift modal */} + {showDateShiftModal && ( + + batchDateShiftMutation.mutate({ allocationIds: selectedMutationIds, daysDelta, mode: "move" }) + } + onClose={() => setShowDateShiftModal(false)} + /> + )} + {/* Modal */} {modalOpen && ( diff --git a/apps/web/src/components/allocations/BatchDateShiftModal.tsx b/apps/web/src/components/allocations/BatchDateShiftModal.tsx new file mode 100644 index 0000000..12ab2e0 --- /dev/null +++ b/apps/web/src/components/allocations/BatchDateShiftModal.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { useState } from "react"; +import { AnimatedModal } from "~/components/ui/AnimatedModal.js"; + +interface BatchDateShiftModalProps { + count: number; + onConfirm: (daysDelta: number) => void; + onClose: () => void; + isPending?: boolean; +} + +export function BatchDateShiftModal({ count, onConfirm, onClose, isPending }: BatchDateShiftModalProps) { + const [days, setDays] = useState(7); + + function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (days !== 0) onConfirm(days); + } + + return ( + +
+

+ Shift Dates +

+

+ Move the start and end dates of {count} allocation{count !== 1 ? "s" : ""} by the specified number of days. + Use a negative number to shift backwards. +

+
+
+ +
+ setDays(Number(e.target.value))} + step={1} + className="w-28 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 px-3 py-1.5 text-sm text-gray-900 dark:text-gray-100 focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500" + autoFocus + /> + + {days > 0 ? `+${days} day${Math.abs(days) !== 1 ? "s" : ""} forward` : days < 0 ? `${days} day${Math.abs(days) !== 1 ? "s" : ""} back` : "no change"} + +
+
+ +
+ + +
+
+
+
+ ); +}