fix(frontend): dynamic time range in schedule week view

Compute time range dynamically from working hours, bookings, and blockers instead of hardcoded 8-18. Fix gridPxFromTime to use SLOT_HEIGHT instead of HOUR_HEIGHT for accurate pixel positioning.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-06-22 12:55:56 +01:00
co-authored by Sisyphus
parent c4f1535ad4
commit 1187577864
+45 -13
View File
@@ -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)}
<div
class="flex items-start justify-end border-b border-gray-100 pt-0.5 pr-1.5 text-right text-xs text-gray-400"
style="height: {HOUR_HEIGHT}px;"
style="height: {SLOT_HEIGHT}px;"
>
{formatHour(h)}
</div>
@@ -628,7 +660,7 @@
{/if}
<!-- Post-closing gray stripe -->
{#if closePx < timeRange.hours.length * HOUR_HEIGHT}
{#if closePx < timeRange.hours.length * SLOT_HEIGHT}
<div
class="stripe-bg absolute"
style="left:{colLeft}; top:{closePx}px; width:{colWidth}; bottom:0;"
@@ -657,7 +689,7 @@
{@const colLeft = `calc(${todayIdx} * (100% / 7))`}
{@const colWidth = `calc(100% / 7)`}
{#if nowPx >= 0 && nowPx <= timeRange.hours.length * HOUR_HEIGHT && todayIdx !== -1}
{#if nowPx >= 0 && nowPx <= timeRange.hours.length * SLOT_HEIGHT && todayIdx !== -1}
<!-- Dot -->
<div
class="absolute h-3 w-3 rounded-full bg-blue-500"