refactor(frontend): timezone-safe date handling with London-aware utilities

Introduce getLondonTodayCalendarDate(), parseWallClockDate(), and formatLocalDateTime() for reliable Europe/London timezone handling. Replace ad-hoc SvelteDate/new Date() usage with these utilities across all components and stores.

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-24 23:43:58 +01:00
co-authored by Sisyphus
parent e4b9003439
commit 4ac7768070
32 changed files with 618 additions and 515 deletions
@@ -15,6 +15,7 @@
getLunchProtectionForSlots,
timeToMinutes
} from '$lib/lunchProtection';
import { formatLocalDateTime, getLondonTodayCalendarDate } from '$lib/utils/timeSlots';
import ClockIcon from '@lucide/svelte/icons/clock';
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
@@ -67,10 +68,10 @@
let userNavigatedCalendar = $state(false);
let editRequestAutoSelectDone = $state(false);
// ─── Date constants ─────────────────────────────────────
const today = new Date();
const minDate = new CalendarDate(today.getFullYear(), today.getMonth() + 1, today.getDate());
const maxDate = new Date();
maxDate.setMonth(today.getMonth() + 6);
const todayCalendarDate = getLondonTodayCalendarDate();
const minDate = todayCalendarDate;
const maxDate = new SvelteDate(todayCalendarDate.year, todayCalendarDate.month - 1, todayCalendarDate.day);
maxDate.setMonth(todayCalendarDate.month - 1 + 6);
const maxCalendarDate = new CalendarDate(
maxDate.getFullYear(),
maxDate.getMonth() + 1,
@@ -204,8 +205,7 @@
if (!dayWH || !dayWH.isOpen || !dayAH || !dayAH.slots) return [];
const slots: string[] = [];
const now = new SvelteDate();
const todayCal = new CalendarDate(now.getFullYear(), now.getMonth() + 1, now.getDate());
const todayCal = getLondonTodayCalendarDate();
const isToday = date.compare(todayCal) === 0;
for (const slot of dayAH.slots) {
@@ -217,7 +217,10 @@
const endMin = eh * 60 + em;
if (isToday) {
const currentMin = now.getHours() * 60 + now.getMinutes();
const now = new Date();
const londonTime = now.toLocaleTimeString('en-GB', { timeZone: 'Europe/London', hour: '2-digit', minute: '2-digit', hour12: false });
const [londonHours, londonMinutes] = londonTime.split(':').map(Number);
const currentMin = londonHours * 60 + londonMinutes;
startMin = Math.max(startMin, Math.ceil((currentMin + 60) / 15) * 15);
}
@@ -261,11 +264,13 @@
let startMin = sh * 60 + sm;
const endMin = eh * 60 + em;
const now = new SvelteDate();
const todayCal = new CalendarDate(now.getFullYear(), now.getMonth() + 1, now.getDate());
const todayCal = getLondonTodayCalendarDate();
const isToday = date.compare(todayCal) === 0;
if (isToday) {
const currentMin = now.getHours() * 60 + now.getMinutes();
const now = new Date();
const londonTime = now.toLocaleTimeString('en-GB', { timeZone: 'Europe/London', hour: '2-digit', minute: '2-digit', hour12: false });
const [londonHours, londonMinutes] = londonTime.split(':').map(Number);
const currentMin = londonHours * 60 + londonMinutes;
startMin = Math.max(startMin, Math.ceil((currentMin + 60) / 15) * 15);
}
@@ -324,10 +329,6 @@
function isDateUnavailable(date: DateValue): boolean {
const d = date as CalendarDate;
const jsDate = d.toDate(getLocalTimeZone());
const now = new SvelteDate();
const todayStart = new SvelteDate(now.getFullYear(), now.getMonth(), now.getDate());
if (jsDate < todayStart) return true;
if (d.compare(minDate) < 0 || d.compare(maxCalendarDate) > 0) return true;
if (!workingHours) return true;
const dateStr = d.toString();
@@ -431,7 +432,7 @@
return;
editRequestAutoSelectDone = true;
const currentDate = new SvelteDate();
const currentDate = new SvelteDate(getLondonTodayCalendarDate().toString() + 'T00:00:00');
const maxDateJs = new SvelteDate(
maxCalendarDate.year,
maxCalendarDate.month - 1,
@@ -446,7 +447,7 @@
for (let i = 0; i <= daysToCheck; i++) {
const nextDate = new SvelteDate(currentDate);
nextDate.setDate(currentDate.getDate() + i);
const dateStr = nextDate.toISOString().split('T')[0];
const dateStr = nextDate.toLocaleDateString('en-CA', { timeZone: 'Europe/London' });
if (workingHours[dateStr]?.isOpen) {
const calDate = new CalendarDate(
@@ -464,11 +465,14 @@
}
}
const tomorrow = new SvelteDate();
tomorrow.setDate(tomorrow.getDate() + 1);
newDate = new CalendarDate(tomorrow.getFullYear(), tomorrow.getMonth() + 1, tomorrow.getDate());
const tomorrowCal = getLondonTodayCalendarDate();
newDate = new CalendarDate(
tomorrowCal.year,
tomorrowCal.month,
tomorrowCal.day + 1
);
if (!userNavigatedCalendar) {
placeholderDate = new CalendarDate(tomorrow.getFullYear(), tomorrow.getMonth() + 1, 1);
placeholderDate = new CalendarDate(tomorrowCal.year, tomorrowCal.month, 1);
}
});
@@ -612,7 +616,7 @@
if (!workingHours || !availableHours) return 0;
const bookingDate = new SvelteDate(booking.start_time);
const dateStr = `${bookingDate.getFullYear()}-${String(bookingDate.getMonth() + 1).padStart(2, '0')}-${String(bookingDate.getDate()).padStart(2, '0')}`;
const dateStr = bookingDate.toLocaleDateString('en-CA', { timeZone: 'Europe/London' });
const dayWH = workingHours[dateStr];
const dayAH = availableHours[dateStr];
@@ -687,7 +691,7 @@
const [hours, minutes] = newTime.split(':').map(Number);
const bookingDate = newDate!.toDate(getLocalTimeZone());
bookingDate.setHours(hours || 0, minutes || 0, 0, 0);
body.new_start_time = bookingDate.toISOString();
body.new_start_time = formatLocalDateTime(bookingDate);
}
if (editMode === 'services' || editMode === 'both-time') {