Files
CapaKraken/apps/web/src/hooks/useAllocationHistory.ts
T
Hartmut e7b74f13bd refactor: consolidate duplicated code across web and API packages
- 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>
2026-03-19 00:10:08 +01:00

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 };
}