From 77963a13f43bb5472f8397ffc19fe4905687cb91 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 02:33:15 +0000 Subject: [PATCH] Address code-review findings on draft schedule Gantt - Use make_interval(months => $n) instead of parameterized interval multiplication so Postgres can infer the bound param's type (the prior form risked a "could not determine data type of parameter" runtime error) - Fix subject/verb grammar in the coverage-gap warning for a single sport ("1 sport has" vs "N sports have") - Stack overlapping draft windows for the same sport into separate lanes via greedy interval scheduling so bars no longer render on top of one another; row height grows with the number of concurrent windows - Add a test covering overlapping windows rendering as separate bars Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Fd51U9DPMNz4KzBeucR6CK --- app/components/admin/DraftScheduleGantt.tsx | 58 ++++++++++++++++--- .../__tests__/DraftScheduleGantt.test.tsx | 29 ++++++++++ app/models/sports-season.ts | 2 +- app/routes/admin.draft-schedule.tsx | 3 +- 4 files changed, 83 insertions(+), 9 deletions(-) diff --git a/app/components/admin/DraftScheduleGantt.tsx b/app/components/admin/DraftScheduleGantt.tsx index cf062d3..e491fe3 100644 --- a/app/components/admin/DraftScheduleGantt.tsx +++ b/app/components/admin/DraftScheduleGantt.tsx @@ -46,6 +46,40 @@ const STATUS_LABELS: Record = { const clampPct = (n: number) => Math.max(0, Math.min(100, n)); +// Row layout (px). A single-lane row is LANE_HEIGHT + ROW_V_PAD tall; extra +// concurrent windows add one lane each so overlapping bars never stack on top +// of one another. +const LANE_HEIGHT = 30; +const LANE_GAP = 8; +const ROW_V_PAD = 14; + +const rowHeight = (laneCount: number) => Math.max(laneCount, 1) * LANE_HEIGHT + ROW_V_PAD; + +/** + * Greedy interval-scheduling: assign each window to the first lane whose last + * bar ends before this one starts, otherwise open a new lane. Windows arrive + * sorted by draftOn (see findDraftScheduleForHorizon). + */ +function assignLanes( + windows: DraftScheduleWindow[], + start: Date +): { placed: Array<{ window: DraftScheduleWindow; lane: number }>; laneCount: number } { + const laneEnds: number[] = []; + const placed = windows.map((window) => { + const startDay = differenceInCalendarDays(parseISO(window.draftOn), start); + const endDay = differenceInCalendarDays(parseISO(window.draftOff), start); + let lane = laneEnds.findIndex((laneEnd) => laneEnd < startDay); + if (lane === -1) { + lane = laneEnds.length; + laneEnds.push(endDay); + } else { + laneEnds[lane] = endDay; + } + return { window, lane }; + }); + return { placed, laneCount: Math.max(laneEnds.length, 1) }; +} + export function DraftScheduleGantt({ sports, today, months }: DraftScheduleGanttProps) { const start = parseISO(today); const end = addMonths(start, months); @@ -58,6 +92,12 @@ export function DraftScheduleGantt({ sports, today, months }: DraftScheduleGantt .map((date) => ({ date, left: pct(date) })) .filter((m) => m.left >= 0 && m.left <= 100); + // Pre-compute lane assignment + height for each sport row. + const rows = sports.map((sport) => { + const { placed, laneCount } = assignLanes(sport.windows, start); + return { sport, placed, height: rowHeight(laneCount) }; + }); + if (sports.length === 0) { return ( @@ -106,10 +146,11 @@ export function DraftScheduleGantt({ sports, today, months }: DraftScheduleGantt {/* Sport labels column */}