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>
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { trpc } from "~/lib/trpc/client.js";
|
||||
import { useInvalidateTimeline } from "./useInvalidatePlanningViews.js";
|
||||
import { pixelsToDays, computeDragDates } from "~/components/timeline/dragMath.js";
|
||||
|
||||
// ─── Project-shift drag state ───────────────────────────────────────────────
|
||||
|
||||
@@ -214,6 +216,7 @@ export function useTimelineDrag({
|
||||
onMultiDragCompleteRef.current = onMultiDragComplete;
|
||||
|
||||
const utils = trpc.useUtils();
|
||||
const invalidateTimeline = useInvalidateTimeline();
|
||||
|
||||
// Project-shift preview
|
||||
const { data: previewData, isFetching: isPreviewLoading } = trpc.timeline.previewShift.useQuery(
|
||||
@@ -235,9 +238,7 @@ export function useTimelineDrag({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const applyShiftMutation = (trpc.timeline.applyShift.useMutation as any)({
|
||||
onSuccess: (data: { project: { id: string } }) => {
|
||||
void utils.timeline.getEntries.invalidate();
|
||||
void utils.timeline.getEntriesView.invalidate();
|
||||
void utils.timeline.getBudgetStatus.invalidate();
|
||||
invalidateTimeline();
|
||||
void utils.project.list.invalidate();
|
||||
onShiftApplied?.(data.project.id);
|
||||
},
|
||||
@@ -251,10 +252,7 @@ export function useTimelineDrag({
|
||||
|
||||
const updateAllocMutation = trpc.timeline.updateAllocationInline.useMutation({
|
||||
onSuccess: () => {
|
||||
void utils.timeline.getEntries.invalidate();
|
||||
void utils.timeline.getEntriesView.invalidate();
|
||||
void utils.timeline.getProjectContext.invalidate();
|
||||
void utils.timeline.getBudgetStatus.invalidate();
|
||||
invalidateTimeline();
|
||||
const snap = pendingSnapshotRef.current;
|
||||
if (snap) {
|
||||
onAllocationMovedRef.current?.(snap);
|
||||
@@ -378,7 +376,7 @@ export function useTimelineDrag({
|
||||
|
||||
function handleMultiMove(ev: MouseEvent) {
|
||||
const deltaX = ev.clientX - startMouseX;
|
||||
const daysDelta = Math.round(deltaX / cellWidthRef.current);
|
||||
const daysDelta = pixelsToDays(deltaX, cellWidthRef.current);
|
||||
if (daysDelta === currentDaysDelta) return;
|
||||
currentDaysDelta = daysDelta;
|
||||
|
||||
@@ -432,25 +430,15 @@ export function useTimelineDrag({
|
||||
if (!alloc.isActive || !alloc.originalStartDate || !alloc.originalEndDate) return;
|
||||
|
||||
const deltaX = ev.clientX - alloc.startMouseX;
|
||||
const daysDelta = Math.round(deltaX / cellWidthRef.current);
|
||||
const daysDelta = pixelsToDays(deltaX, cellWidthRef.current);
|
||||
if (daysDelta === alloc.daysDelta) return;
|
||||
|
||||
const newStart = new Date(alloc.originalStartDate);
|
||||
const newEnd = new Date(alloc.originalEndDate);
|
||||
|
||||
if (alloc.mode === "move") {
|
||||
newStart.setDate(newStart.getDate() + daysDelta);
|
||||
newEnd.setDate(newEnd.getDate() + daysDelta);
|
||||
} else if (alloc.mode === "resize-start") {
|
||||
newStart.setDate(newStart.getDate() + daysDelta);
|
||||
// Allow same-day (single day booking), prevent crossing
|
||||
if (newStart > newEnd) newStart.setTime(newEnd.getTime());
|
||||
} else {
|
||||
// resize-end
|
||||
newEnd.setDate(newEnd.getDate() + daysDelta);
|
||||
// Allow same-day (single day booking), prevent crossing
|
||||
if (newEnd < newStart) newEnd.setTime(newStart.getTime());
|
||||
}
|
||||
const { start: newStart, end: newEnd } = computeDragDates(
|
||||
alloc.mode,
|
||||
alloc.originalStartDate,
|
||||
alloc.originalEndDate,
|
||||
daysDelta,
|
||||
);
|
||||
|
||||
const updated: AllocDragState = {
|
||||
...alloc,
|
||||
@@ -545,12 +533,14 @@ export function useTimelineDrag({
|
||||
const drag = dragStateRef.current;
|
||||
if (drag.isDragging && drag.originalStartDate && drag.originalEndDate) {
|
||||
const deltaX = e.clientX - drag.startMouseX;
|
||||
const daysDelta = Math.round(deltaX / cellWidth);
|
||||
const daysDelta = pixelsToDays(deltaX, cellWidth);
|
||||
if (daysDelta !== drag.daysDelta) {
|
||||
const newStart = new Date(drag.originalStartDate);
|
||||
newStart.setDate(newStart.getDate() + daysDelta);
|
||||
const newEnd = new Date(drag.originalEndDate);
|
||||
newEnd.setDate(newEnd.getDate() + daysDelta);
|
||||
const { start: newStart, end: newEnd } = computeDragDates(
|
||||
"move",
|
||||
drag.originalStartDate,
|
||||
drag.originalEndDate,
|
||||
daysDelta,
|
||||
);
|
||||
const updated: DragState = {
|
||||
...drag,
|
||||
currentStartDate: newStart,
|
||||
@@ -567,7 +557,7 @@ export function useTimelineDrag({
|
||||
const range = rangeStateRef.current;
|
||||
if (range.isSelecting && range.startDate) {
|
||||
const deltaX = e.clientX - range.startClientX;
|
||||
const daysDelta = Math.round(deltaX / cellWidth);
|
||||
const daysDelta = pixelsToDays(deltaX, cellWidth);
|
||||
const currentDate = new Date(range.startDate);
|
||||
currentDate.setDate(currentDate.getDate() + daysDelta);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user