feat: timeline multi-select, demand popover, resource hover card, merged tooltips, dark mode fixes
Major timeline enhancements: - Right-click drag multi-selection with floating action bar (batch delete/assign) - DemandPopover for demand strip details (replaces broken "Loading" modal) - ResourceHoverCard on name hover showing skills, rates, role, chapter - Merged heatmap+vacation tooltips into unified TimelineTooltip component - Fixed overbooking blink animation (date normalization, z-index ordering) - Fixed dark mode sticky column bleed-through in project view - System roles admin page, notification task management, performance review docs Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -5,16 +5,27 @@ import type { AllocationMovedSnapshot } from "./useTimelineDrag.js";
|
||||
|
||||
export type { AllocationMovedSnapshot };
|
||||
|
||||
const MAX_HISTORY = 20;
|
||||
/** 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<AllocationMovedSnapshot[]>([]);
|
||||
const future = useRef<AllocationMovedSnapshot[]>([]);
|
||||
const past = useRef<HistoryEntry[]>([]);
|
||||
const future = useRef<HistoryEntry[]>([]);
|
||||
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
// 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: () => {
|
||||
void utils.timeline.getEntries.invalidate();
|
||||
@@ -24,12 +35,35 @@ export function useAllocationHistory() {
|
||||
},
|
||||
});
|
||||
|
||||
const batchShiftMutation = trpc.timeline.batchShiftAllocations.useMutation({
|
||||
onSuccess: () => {
|
||||
void utils.timeline.getEntries.invalidate();
|
||||
void utils.timeline.getEntriesView.invalidate();
|
||||
void utils.timeline.getProjectContext.invalidate();
|
||||
void utils.timeline.getBudgetStatus.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const push = useCallback((snapshot: AllocationMovedSnapshot) => {
|
||||
past.current = [...past.current.slice(-MAX_HISTORY + 1), snapshot];
|
||||
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 invalidateAll = useCallback(() => {
|
||||
void utils.timeline.getEntries.invalidate();
|
||||
void utils.timeline.getEntriesView.invalidate();
|
||||
void utils.timeline.getProjectContext.invalidate();
|
||||
void utils.timeline.getBudgetStatus.invalidate();
|
||||
}, [utils]);
|
||||
|
||||
const undo = useCallback(async () => {
|
||||
const last = past.current[past.current.length - 1];
|
||||
@@ -38,12 +72,25 @@ export function useAllocationHistory() {
|
||||
future.current = [last, ...future.current];
|
||||
setCanUndo(past.current.length > 0);
|
||||
setCanRedo(true);
|
||||
await updateMutation.mutateAsync({
|
||||
allocationId: last.mutationAllocationId,
|
||||
startDate: last.before.startDate,
|
||||
endDate: last.before.endDate,
|
||||
});
|
||||
}, [updateMutation]);
|
||||
|
||||
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];
|
||||
@@ -52,12 +99,22 @@ export function useAllocationHistory() {
|
||||
past.current = [...past.current, next];
|
||||
setCanUndo(true);
|
||||
setCanRedo(future.current.length > 0);
|
||||
await updateMutation.mutateAsync({
|
||||
allocationId: next.mutationAllocationId,
|
||||
startDate: next.after.startDate,
|
||||
endDate: next.after.endDate,
|
||||
});
|
||||
}, [updateMutation]);
|
||||
|
||||
return { push, undo, redo, canUndo, canRedo };
|
||||
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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user