diff --git a/frontend/src/routes/admin/schedule/+page.svelte b/frontend/src/routes/admin/schedule/+page.svelte index d2e13ff..516c859 100644 --- a/frontend/src/routes/admin/schedule/+page.svelte +++ b/frontend/src/routes/admin/schedule/+page.svelte @@ -125,10 +125,11 @@ } // Calculates pixel offset from the top of the grid. - // Adds 1px per hour to account for border heights. + // Each hour row is SLOT_HEIGHT (64px content + 1px border-b), + // so fractional hours are multiplied by SLOT_HEIGHT. function gridPxFromTime(time: string, minStart: number): number { const fractionalHours = timeToMinutes(time) / 60 - minStart; - return fractionalHours * HOUR_HEIGHT + Math.floor(fractionalHours); + return fractionalHours * SLOT_HEIGHT; } function getServiceNames(b: ScheduleBooking): string { @@ -304,6 +305,7 @@ // -- Layout Constants -- const HOUR_HEIGHT = 64; + const SLOT_HEIGHT = HOUR_HEIGHT + 1; // each hour row has 1px border-b const TIME_LABEL_WIDTH = 56; const HEADER_HEIGHT = 48; const dayHeaders = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; @@ -334,19 +336,49 @@ const timeRange = $derived.by(() => { if (!weekStart) - return { minStart: 8, maxEnd: 18, hours: [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18] }; + return { minStart: 8, maxEnd: 18, hours: Array.from({ length: 11 }, (_, i) => i + 8) }; + + // Find earliest start and latest end across ALL data (hours, bookings, blockers) + let earliest = 24, latest = 0; - let minStart = 8, - maxEnd = 18; for (const day of getWeekDays(weekStart)) { - const wh = workingHours.find((w) => w.date === formatDate(day)); + const dateStr = formatDate(day); + const wh = workingHours.find((w) => w.date === dateStr); if (wh?.isOpen) { - const [sh] = wh.startTime.split(':').map(Number); - const [eh] = wh.endTime.split(':').map(Number); - if (sh < minStart) minStart = sh; - if (eh > maxEnd) maxEnd = eh; + const [sh, sm] = wh.startTime.split(':').map(Number); + const sf = sh + (sm || 0) / 60; + if (sf < earliest) earliest = sf; + const [eh, em] = wh.endTime.split(':').map(Number); + const ef = eh + (em || 0) / 60; + if (ef > latest) latest = ef; + } + for (const b of (bookingsByDate.get(dateStr) || [])) { + const d = new SvelteDate(b.start_time); + const sf = d.getHours() + d.getMinutes() / 60; + if (sf < earliest) earliest = sf; + const ef = sf + b.duration_minutes / 60; + if (ef > latest) latest = ef; + } + for (const b of (blockersByDate.get(dateStr) || [])) { + const d = new SvelteDate(b.start_time); + const sf = d.getHours() + d.getMinutes() / 60; + if (sf < earliest) earliest = sf; + const ef = sf + b.duration_minutes / 60; + if (ef > latest) latest = ef; } } + + // No data: fall back to 8-18 + if (earliest >= 24 || latest <= 0) return { minStart: 8, maxEnd: 18, hours: [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18] }; + + // Start: 30min buffer before earliest (to show the pre-booking gap) + // End: floor of latest (the row that CONTAINS the latest item, not the next row) + let minStart = Math.floor(earliest - 0.5); + let maxEnd = Math.floor(latest); + + minStart = Math.max(0, minStart); + maxEnd = Math.min(24, maxEnd); + const hours: number[] = []; for (let h = minStart; h <= maxEnd; h++) hours.push(h); return { minStart, maxEnd, hours }; @@ -479,7 +511,7 @@ {#each timeRange.hours as h (h)}
{formatHour(h)}
@@ -628,7 +660,7 @@ {/if} - {#if closePx < timeRange.hours.length * HOUR_HEIGHT} + {#if closePx < timeRange.hours.length * SLOT_HEIGHT}
= 0 && nowPx <= timeRange.hours.length * HOUR_HEIGHT && todayIdx !== -1} + {#if nowPx >= 0 && nowPx <= timeRange.hours.length * SLOT_HEIGHT && todayIdx !== -1}