feat: timeline UI overhaul with project/resource panel redesign, quick filters, and API improvements

Redesigned timeline project and resource panels with expanded detail views,
added quick filter toolbar, improved drag handling, and enhanced vacation/entitlement
router logic. Includes e2e test updates and minor API fixes.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-03-15 09:28:59 +01:00
parent fa2019f521
commit a83edb2f9d
23 changed files with 2464 additions and 734 deletions
+108 -60
View File
@@ -154,7 +154,11 @@ export function useTimelineDrag({
cellWidthRef.current = cellWidth;
// Touch disambiguation: track initial touch position to distinguish horizontal drag from vertical scroll
const touchStartRef = useRef<{ x: number; y: number; decided: boolean }>({ x: 0, y: 0, decided: false });
const touchStartRef = useRef<{ x: number; y: number; decided: boolean }>({
x: 0,
y: 0,
decided: false,
});
const onBlockClickRef = useRef(onBlockClick);
onBlockClickRef.current = onBlockClick;
@@ -190,7 +194,11 @@ export function useTimelineDrag({
void utils.project.list.invalidate();
onShiftApplied?.(data.project.id);
},
}) as { isPending: boolean; mutate: (...args: unknown[]) => void; mutateAsync: (...args: unknown[]) => Promise<unknown> };
}) as {
isPending: boolean;
mutate: (...args: unknown[]) => void;
mutateAsync: (...args: unknown[]) => Promise<unknown>;
};
const pendingSnapshotRef = useRef<AllocationMovedSnapshot | null>(null);
@@ -211,12 +219,16 @@ export function useTimelineDrag({
// ── Project-bar drag (shifts all allocations) ──────────────────────────────
const onProjectBarMouseDown = useCallback(
(e: React.MouseEvent, opts: {
projectId: string;
projectName: string;
startDate: Date;
endDate: Date;
}) => {
(
e: React.MouseEvent,
opts: {
projectId: string;
projectName: string;
startDate: Date;
endDate: Date;
},
) => {
if (e.button !== 0) return;
e.preventDefault();
e.stopPropagation();
const state: DragState = {
@@ -241,15 +253,19 @@ export function useTimelineDrag({
// Legacy — kept for backward compat (triggers project shift from allocation block)
const onBlockMouseDown = useCallback(
(e: React.MouseEvent, opts: {
projectId: string;
projectName: string;
allocationId?: string;
startDate: Date;
endDate: Date;
blockLeft: number;
blockWidth: number;
}) => {
(
e: React.MouseEvent,
opts: {
projectId: string;
projectName: string;
allocationId?: string;
startDate: Date;
endDate: Date;
blockLeft: number;
blockWidth: number;
},
) => {
if (e.button !== 0) return;
e.preventDefault();
e.stopPropagation();
const state: DragState = {
@@ -279,16 +295,20 @@ export function useTimelineDrag({
// moving quickly or scrolling into the sticky header area).
const onAllocMouseDown = useCallback(
(e: React.MouseEvent, opts: {
mode: AllocDragMode;
allocationId: string;
mutationAllocationId?: string;
projectId: string;
projectName: string;
resourceId: string | null;
startDate: Date;
endDate: Date;
}) => {
(
e: React.MouseEvent,
opts: {
mode: AllocDragMode;
allocationId: string;
mutationAllocationId?: string;
projectId: string;
projectName: string;
resourceId: string | null;
startDate: Date;
endDate: Date;
},
) => {
if (e.button !== 0) return;
e.preventDefault();
e.stopPropagation();
@@ -389,12 +409,16 @@ export function useTimelineDrag({
// ── Range-select ────────────────────────────────────────────────────────────
const onRowMouseDown = useCallback(
(e: React.MouseEvent, opts: {
resourceId: string;
startDate: Date;
suggestedProjectId?: string;
}) => {
(
e: React.MouseEvent,
opts: {
resourceId: string;
startDate: Date;
suggestedProjectId?: string;
},
) => {
if (dragStateRef.current.isDragging || allocDragRef.current.isActive) return;
if (e.button !== 0) return;
e.preventDefault();
const state: RangeState = {
isSelecting: true,
@@ -424,7 +448,12 @@ export function useTimelineDrag({
newStart.setDate(newStart.getDate() + daysDelta);
const newEnd = new Date(drag.originalEndDate);
newEnd.setDate(newEnd.getDate() + daysDelta);
const updated: DragState = { ...drag, currentStartDate: newStart, currentEndDate: newEnd, daysDelta };
const updated: DragState = {
...drag,
currentStartDate: newStart,
currentEndDate: newEnd,
daysDelta,
};
dragStateRef.current = updated;
setDragState(updated);
}
@@ -488,9 +517,7 @@ export function useTimelineDrag({
if (range.isSelecting && range.resourceId && range.startDate) {
const endDate = range.currentDate ?? range.startDate;
const [startDate, finalEnd] =
range.startDate <= endDate
? [range.startDate, endDate]
: [endDate, range.startDate];
range.startDate <= endDate ? [range.startDate, endDate] : [endDate, range.startDate];
onRangeSelected?.({
resourceId: range.resourceId,
@@ -529,16 +556,23 @@ export function useTimelineDrag({
}
const onProjectBarTouchStart = useCallback(
(e: React.TouchEvent, opts: {
projectId: string;
projectName: string;
startDate: Date;
endDate: Date;
}) => {
(
e: React.TouchEvent,
opts: {
projectId: string;
projectName: string;
startDate: Date;
endDate: Date;
},
) => {
e.preventDefault();
touchStartRef.current = { x: toClientX(e), y: e.touches[0]?.clientY ?? 0, decided: true };
onProjectBarMouseDown(
{ clientX: toClientX(e), preventDefault: () => {}, stopPropagation: () => {} } as unknown as React.MouseEvent,
{
clientX: toClientX(e),
preventDefault: () => {},
stopPropagation: () => {},
} as unknown as React.MouseEvent,
opts,
);
},
@@ -546,20 +580,27 @@ export function useTimelineDrag({
);
const onAllocTouchStart = useCallback(
(e: React.TouchEvent, opts: {
mode: AllocDragMode;
allocationId: string;
mutationAllocationId?: string;
projectId: string;
projectName: string;
resourceId: string | null;
startDate: Date;
endDate: Date;
}) => {
(
e: React.TouchEvent,
opts: {
mode: AllocDragMode;
allocationId: string;
mutationAllocationId?: string;
projectId: string;
projectName: string;
resourceId: string | null;
startDate: Date;
endDate: Date;
},
) => {
e.preventDefault();
touchStartRef.current = { x: toClientX(e), y: e.touches[0]?.clientY ?? 0, decided: true };
onAllocMouseDown(
{ clientX: toClientX(e), preventDefault: () => {}, stopPropagation: () => {} } as unknown as React.MouseEvent,
{
clientX: toClientX(e),
preventDefault: () => {},
stopPropagation: () => {},
} as unknown as React.MouseEvent,
opts,
);
},
@@ -567,15 +608,22 @@ export function useTimelineDrag({
);
const onRowTouchStart = useCallback(
(e: React.TouchEvent, opts: {
resourceId: string;
startDate: Date;
suggestedProjectId?: string;
}) => {
(
e: React.TouchEvent,
opts: {
resourceId: string;
startDate: Date;
suggestedProjectId?: string;
},
) => {
e.preventDefault();
touchStartRef.current = { x: toClientX(e), y: e.touches[0]?.clientY ?? 0, decided: false };
onRowMouseDown(
{ clientX: toClientX(e), preventDefault: () => {}, stopPropagation: () => {} } as unknown as React.MouseEvent,
{
clientX: toClientX(e),
preventDefault: () => {},
stopPropagation: () => {},
} as unknown as React.MouseEvent,
opts,
);
},