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:
2026-03-19 00:10:08 +01:00
parent ddec3a927a
commit e7b74f13bd
38 changed files with 637 additions and 652 deletions
+21 -31
View File
@@ -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);