e7b74f13bd
- Extract shared render helpers (vacation blocks, range overlay, overbooking blink) into renderHelpers.tsx - Centralize status badge styles and vacation color maps into status-styles.ts - Extract dragMath.ts utility from useTimelineDrag for reuse - Split useInvalidatePlanningViews into useInvalidateTimeline (4 queries) + useInvalidatePlanningViews (8 queries) - Adopt findUniqueOrThrow() and Prisma select constants across API routers - Add shared fmtEur() helper for API-side money formatting - Wrap TimelineResourcePanel and TimelineProjectPanel with React.memo - Fix pre-existing TS2589 deep type errors in TeamCalendar and VacationModal - 38 files changed, reducing ~400 lines of duplicated code Co-Authored-By: claude-flow <ruv@ruv.net>
105 lines
3.7 KiB
TypeScript
105 lines
3.7 KiB
TypeScript
"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<HistoryEntry[]>([]);
|
|
const future = useRef<HistoryEntry[]>([]);
|
|
|
|
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 };
|
|
}
|