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
@@ -19,8 +19,11 @@
generateAvailableTimeSlots,
generateGroupedTimeSlots,
formatTime,
formatLocalDateTime,
calculateEndTime,
getDayWithOrdinal,
getLondonTodayCalendarDate,
parseWallClockDate,
type DayHours,
type DayAvailability
} from '$lib/utils/timeSlots';
@@ -35,13 +38,7 @@
let { open = $bindable(), booking, onSaved }: Props = $props();
let saving = $state(false);
let placeholder = $state<CalendarDate>(
new CalendarDate(
new SvelteDate().getFullYear(),
new SvelteDate().getMonth() + 1,
new SvelteDate().getDate()
)
);
let placeholder = $state<CalendarDate>(getLondonTodayCalendarDate());
let selectedDate = $state<CalendarDate | undefined>(undefined);
let selectedTime = $state<string | null>(null);
let workingHours = $state<Record<string, DayHours> | null>(null);
@@ -51,17 +48,17 @@
let hoursMonthGeneration = $state(0);
let userNavigatedCalendar = $state(false);
let workingHoursCache: Record<string, Record<string, any>> = {};
let availableHoursCache: Record<string, Record<string, any>> = {};
let workingHoursCache: Record<string, Record<string, DayHours>> = {};
let availableHoursCache: Record<string, Record<string, DayAvailability>> = {};
let loadingMonths: Record<string, boolean> = {};
let loadingMonthKeys = new SvelteSet<string>();
let initialLoadDone = $state(false);
let rescheduleAutoSelectDone = $state(false);
const today = new SvelteDate();
const minDate = new CalendarDate(today.getFullYear(), today.getMonth() + 1, today.getDate());
const maxDate = new SvelteDate();
maxDate.setMonth(today.getMonth() + 6);
const today = getLondonTodayCalendarDate();
const minDate = today;
const maxDate = new SvelteDate(today.year, today.month - 1, today.day);
maxDate.setMonth(today.month - 1 + 6);
const maxCalendarDate = new CalendarDate(
maxDate.getFullYear(),
maxDate.getMonth() + 1,
@@ -300,11 +297,7 @@
loadingMonths = {};
loadingMonthKeys = new SvelteSet<string>();
rescheduleAutoSelectDone = false;
placeholder = new CalendarDate(
new SvelteDate().getFullYear(),
new SvelteDate().getMonth() + 1,
new SvelteDate().getDate()
);
placeholder = getLondonTodayCalendarDate();
}
async function reschedule() {
@@ -315,7 +308,7 @@
const localDate = selectedDate.toDate(getLocalTimeZone());
const [hours, minutes] = selectedTime.split(':').map(Number);
localDate.setHours(hours, minutes, 0, 0);
const newStartTime = localDate.toISOString();
const newStartTime = formatLocalDateTime(localDate);
if (new Date(newStartTime).getTime() <= Date.now()) {
toast.error('Start time must be in the future');
return;
@@ -365,8 +358,8 @@
}
const key = `${mYear}-${String(mMonth).padStart(2, '0')}`;
if (!(key in workingHoursCache)) {
workingHoursCache[key] = null as unknown as Record<string, any>;
availableHoursCache[key] = null as unknown as Record<string, any>;
workingHoursCache[key] = null as unknown as Record<string, DayHours>;
availableHoursCache[key] = null as unknown as Record<string, DayAvailability>;
loadingMonths[key] = true;
}
}
@@ -397,7 +390,7 @@
!rescheduleAutoSelectDone
) {
rescheduleAutoSelectDone = true;
const now = new SvelteDate();
const now = new SvelteDate(getLondonTodayCalendarDate().toString() + 'T00:00:00');
const maxDateJs = new SvelteDate(
maxCalendarDate.year,
maxCalendarDate.month - 1,
@@ -410,7 +403,7 @@
for (let i = 1; i <= daysToCheck; i++) {
const checkDate = new SvelteDate(now);
checkDate.setDate(now.getDate() + i);
const dateStr = checkDate.toISOString().split('T')[0];
const dateStr = checkDate.toLocaleDateString('en-CA', { timeZone: 'Europe/London' });
const calDate = new CalendarDate(
checkDate.getFullYear(),
checkDate.getMonth() + 1,
@@ -484,12 +477,12 @@
<Card.Root class="mb-4">
<Card.Content class="pt-4">
<div class="text-sm font-medium">
Current: {new SvelteDate(booking.start_time).toLocaleDateString('en-GB', {
Current: {parseWallClockDate(booking.start_time).toLocaleDateString('en-GB', {
weekday: 'short',
day: 'numeric',
month: 'short',
year: 'numeric'
})} at {new SvelteDate(booking.start_time).toLocaleTimeString('en-GB', {
})} at {parseWallClockDate(booking.start_time).toLocaleTimeString('en-GB', {
hour: 'numeric',
minute: '2-digit',
hour12: true