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:
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Shared pure render functions used by both TimelineResourcePanel and TimelineProjectPanel.
|
||||
* Extracted to avoid duplication of identical vacation blocks, range overlay, and overbooking blink logic.
|
||||
*/
|
||||
|
||||
import { clsx } from "clsx";
|
||||
import {
|
||||
VACATION_TIMELINE_COLORS,
|
||||
VACATION_TIMELINE_BORDER,
|
||||
VACATION_TYPE_LABELS_SHORT,
|
||||
} from "~/lib/status-styles.js";
|
||||
import type { RangeState } from "~/hooks/useTimelineDrag.js";
|
||||
import type { TimelineAssignmentEntry } from "./TimelineContext.js";
|
||||
import type { VacationEntry } from "./TimelineContext.js";
|
||||
|
||||
// ─── Shared types ─────────────────────────────────────────────────────────────
|
||||
|
||||
export interface VacationBlockInfo {
|
||||
vacation: VacationEntry;
|
||||
left: number;
|
||||
width: number;
|
||||
}
|
||||
|
||||
// ─── Vacation block overlays ─────────────────────────────────────────────────
|
||||
|
||||
export function renderVacationBlocks(blocks: VacationBlockInfo[], rowHeight: number) {
|
||||
if (blocks.length === 0) return null;
|
||||
|
||||
return blocks.map(({ vacation: v, left, width }) => {
|
||||
const colorClass = VACATION_TIMELINE_COLORS[v.type] ?? "bg-orange-400/40";
|
||||
const borderClass = VACATION_TIMELINE_BORDER[v.type] ?? "border-orange-500";
|
||||
const label = VACATION_TYPE_LABELS_SHORT[v.type] ?? v.type;
|
||||
const isPending = v.status === "PENDING";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`vac-${v.id}`}
|
||||
className={clsx(
|
||||
"absolute z-[5] flex items-end px-1 pb-0.5 overflow-hidden pointer-events-none",
|
||||
colorClass,
|
||||
isPending ? "border-t-2 border-dashed opacity-60" : "border-t-2",
|
||||
isPending ? "" : borderClass,
|
||||
)}
|
||||
style={{ left: left + 1, width: width - 2, top: 0, height: rowHeight }}
|
||||
>
|
||||
{width > 40 && (
|
||||
<span className="text-[9px] font-bold truncate opacity-70 text-gray-700 dark:text-gray-200 pointer-events-none">
|
||||
{isPending ? "\u23F3" : "\uD83C\uDFD6"} {label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Range selection overlay ─────────────────────────────────────────────────
|
||||
|
||||
export function renderRangeOverlay(
|
||||
rangeState: RangeState,
|
||||
resourceId: string,
|
||||
rowHeight: number,
|
||||
toLeft: (d: Date) => number,
|
||||
toWidth: (s: Date, e: Date) => number,
|
||||
CELL_WIDTH: number,
|
||||
) {
|
||||
if (!rangeState.isSelecting || rangeState.resourceId !== resourceId || !rangeState.startDate) {
|
||||
return null;
|
||||
}
|
||||
const end = rangeState.currentDate ?? rangeState.startDate;
|
||||
const [selStart, selEnd] =
|
||||
rangeState.startDate <= end ? [rangeState.startDate, end] : [end, rangeState.startDate];
|
||||
|
||||
const left = toLeft(selStart);
|
||||
const width = Math.max(CELL_WIDTH, toWidth(selStart, selEnd));
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute bg-brand-200/40 border-2 border-brand-400 rounded pointer-events-none z-10"
|
||||
style={{ left, width, top: 4, height: rowHeight - 8 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Overbooking blink overlay ───────────────────────────────────────────────
|
||||
|
||||
export function renderOverbookingBlink(
|
||||
allocs: TimelineAssignmentEntry[],
|
||||
dates: Date[],
|
||||
CELL_WIDTH: number,
|
||||
) {
|
||||
const REF_H = 8;
|
||||
const overbooked: number[] = [];
|
||||
|
||||
for (let i = 0; i < dates.length; i++) {
|
||||
const d = new Date(dates[i]!);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
const t = d.getTime();
|
||||
let totalH = 0;
|
||||
for (const a of allocs) {
|
||||
const s = new Date(a.startDate);
|
||||
s.setHours(0, 0, 0, 0);
|
||||
const e = new Date(a.endDate);
|
||||
e.setHours(0, 0, 0, 0);
|
||||
if (t >= s.getTime() && t <= e.getTime()) totalH += a.hoursPerDay;
|
||||
}
|
||||
if (totalH > REF_H) overbooked.push(i);
|
||||
}
|
||||
|
||||
if (overbooked.length === 0) return null;
|
||||
|
||||
return overbooked.map((i) => (
|
||||
<div
|
||||
key={`ob-${i}`}
|
||||
className="absolute top-0 bottom-0 pointer-events-none z-[15] animate-overbooking-blink"
|
||||
style={{
|
||||
left: i * CELL_WIDTH,
|
||||
width: CELL_WIDTH,
|
||||
}}
|
||||
/>
|
||||
));
|
||||
}
|
||||
Reference in New Issue
Block a user