"use client"; import { useCallback, useRef, useState } from "react"; import { trpc } from "~/lib/trpc/client.js"; import { useInvalidateTimeline } from "./useInvalidatePlanningViews.js"; import type { AllocationMovedSnapshot } from "./useTimelineDrag.js"; export type { AllocationMovedSnapshot }; /** A single allocation move or a batch shift of multiple allocations */ export type HistoryEntry = | { type: "single"; snapshot: AllocationMovedSnapshot } | { type: "batch"; allocationIds: string[]; daysDelta: number; mode: "move" | "resize-start" | "resize-end" }; const DEFAULT_MAX_HISTORY = 50; export function useAllocationHistory() { const [canUndo, setCanUndo] = useState(false); const [canRedo, setCanRedo] = useState(false); const past = useRef([]); const future = useRef([]); const invalidateTimeline = useInvalidateTimeline(); // Configurable max steps from system settings const { data: settings } = trpc.settings.getSystemSettings.useQuery(undefined, { staleTime: 60_000, }); const maxHistory = settings?.timelineUndoMaxSteps ?? DEFAULT_MAX_HISTORY; const updateMutation = trpc.timeline.updateAllocationInline.useMutation({ onSuccess: invalidateTimeline, }); const batchShiftMutation = trpc.timeline.batchShiftAllocations.useMutation({ onSuccess: invalidateTimeline, }); const push = useCallback((snapshot: AllocationMovedSnapshot) => { past.current = [...past.current.slice(-(maxHistory - 1)), { type: "single", snapshot }]; future.current = []; setCanUndo(true); setCanRedo(false); }, [maxHistory]); const pushBatch = useCallback((allocationIds: string[], daysDelta: number, mode: "move" | "resize-start" | "resize-end" = "move") => { past.current = [...past.current.slice(-(maxHistory - 1)), { type: "batch", allocationIds, daysDelta, mode }]; future.current = []; setCanUndo(true); setCanRedo(false); }, [maxHistory]); const undo = useCallback(async () => { const last = past.current[past.current.length - 1]; if (!last) return; past.current = past.current.slice(0, -1); future.current = [last, ...future.current]; setCanUndo(past.current.length > 0); setCanRedo(true); if (last.type === "single") { await updateMutation.mutateAsync({ allocationId: last.snapshot.mutationAllocationId, startDate: last.snapshot.before.startDate, endDate: last.snapshot.before.endDate, }); } else { // Batch: reverse the shift (for resize modes, reverse means shifting the same edge back) const reverseMode = last.mode === "resize-start" ? "resize-start" : last.mode === "resize-end" ? "resize-end" : "move"; await batchShiftMutation.mutateAsync({ allocationIds: last.allocationIds, daysDelta: -last.daysDelta, mode: reverseMode, }); } }, [updateMutation, batchShiftMutation]); const redo = useCallback(async () => { const next = future.current[0]; if (!next) return; future.current = future.current.slice(1); past.current = [...past.current, next]; setCanUndo(true); setCanRedo(future.current.length > 0); if (next.type === "single") { await updateMutation.mutateAsync({ allocationId: next.snapshot.mutationAllocationId, startDate: next.snapshot.after.startDate, endDate: next.snapshot.after.endDate, }); } else { // Batch: re-apply the shift await batchShiftMutation.mutateAsync({ allocationIds: next.allocationIds, daysDelta: next.daysDelta, mode: next.mode, }); } }, [updateMutation, batchShiftMutation]); return { push, pushBatch, undo, redo, canUndo, canRedo }; }