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:
@@ -1,10 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { sanitizeText } from '$lib/utils/toast-safe';
|
||||
import { formatDateTime, formatDuration, calculateAge } from '$lib/utils/format';
|
||||
import { formatUserName } from '$lib/utils/nameDisplay';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { sanitizeText } from '$lib/utils/toast-safe';
|
||||
import { formatDateTime, formatDuration, calculateAge } from '$lib/utils/format';
|
||||
import { formatUserName } from '$lib/utils/nameDisplay';
|
||||
import { parseWallClockDate } from '$lib/utils/timeSlots';
|
||||
import * as Modal from '$lib/components/ui/dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
@@ -73,7 +74,7 @@
|
||||
|
||||
function getBookingDateTime(): string {
|
||||
if (!booking?.start_time) return '';
|
||||
const d = new SvelteDate(booking.start_time);
|
||||
const d = parseWallClockDate(booking.start_time);
|
||||
return (
|
||||
d.toLocaleDateString('en-GB', {
|
||||
weekday: 'long',
|
||||
@@ -152,7 +153,7 @@
|
||||
overlappingBookings = data.bookings || [];
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching overlapping bookings:', err);
|
||||
// Silently handle - overlapping bookings couldn't be fetched
|
||||
} finally {
|
||||
loadingOverlaps = false;
|
||||
}
|
||||
@@ -340,7 +341,6 @@
|
||||
toast.error('Failed to confirm: ' + sanitizeText(text), { id: loadingToast });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error confirming booking:', err);
|
||||
toast.error('Network error confirming booking', { id: loadingToast });
|
||||
} finally {
|
||||
submitting = false;
|
||||
@@ -370,7 +370,6 @@
|
||||
toast.error('Failed to decline: ' + sanitizeText(text), { id: loadingToast });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error declining booking:', err);
|
||||
toast.error('Network error declining booking', { id: loadingToast });
|
||||
} finally {
|
||||
submitting = false;
|
||||
@@ -417,7 +416,7 @@
|
||||
{formatUserName(ob.user?.full_name || 'Unknown', ob.user?.previous_first_name, ob.user?.previous_last_name)}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{new SvelteDate(ob.start_time).toLocaleDateString('en-GB', {
|
||||
{parseWallClockDate(ob.start_time).toLocaleDateString('en-GB', {
|
||||
weekday: 'short',
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
|
||||
@@ -40,9 +40,12 @@
|
||||
generateAvailableTimeSlots,
|
||||
generateGroupedTimeSlots,
|
||||
formatTime,
|
||||
formatLocalDateTime,
|
||||
calculateEndTime,
|
||||
timeToMinutes,
|
||||
getDayWithOrdinal,
|
||||
parseWallClockDate,
|
||||
getLondonTodayCalendarDate,
|
||||
type DayHours,
|
||||
type DayAvailability
|
||||
} from '$lib/utils/timeSlots';
|
||||
@@ -173,13 +176,7 @@
|
||||
>({});
|
||||
|
||||
// Step 4: Date & Time
|
||||
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);
|
||||
@@ -196,7 +193,6 @@
|
||||
$effect(() => {
|
||||
if (outOfHours !== prevOutOfHours) {
|
||||
prevOutOfHours = outOfHours;
|
||||
console.log('[DEBUG] Out-of-hours mode TOGGLED', { now: outOfHours });
|
||||
// Save normal hours BEFORE clearing, so we can show which slots are genuinely out-of-hours
|
||||
if (outOfHours && workingHours) {
|
||||
normalWorkingHours = { ...workingHours };
|
||||
@@ -218,10 +214,10 @@
|
||||
});
|
||||
|
||||
// Date Boundaries
|
||||
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,
|
||||
@@ -237,8 +233,8 @@
|
||||
let isReserving = $state(false);
|
||||
|
||||
// =============== Cache ===============
|
||||
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 loadingMonthKeys: Set<string> = new Set();
|
||||
|
||||
// =============== Derived Helpers ===============
|
||||
@@ -399,7 +395,7 @@
|
||||
!bookingCreateAutoSelectDone
|
||||
) {
|
||||
bookingCreateAutoSelectDone = true;
|
||||
const now = new SvelteDate();
|
||||
const now = new SvelteDate(getLondonTodayCalendarDate().toString() + 'T00:00:00');
|
||||
const maxDateJs = new SvelteDate(
|
||||
maxCalendarDate.year,
|
||||
maxCalendarDate.month - 1,
|
||||
@@ -412,7 +408,7 @@
|
||||
for (let i = 0; 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,
|
||||
@@ -467,8 +463,8 @@
|
||||
reservationId = null;
|
||||
reservationExpiresAt = null;
|
||||
reservationCountdown = '';
|
||||
if ((window as any).__bookingCreateCountdownInterval) {
|
||||
clearInterval((window as any).__bookingCreateCountdownInterval);
|
||||
if (window.__bookingCreateCountdownInterval) {
|
||||
clearInterval(window.__bookingCreateCountdownInterval);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,7 +488,6 @@
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch users', err);
|
||||
toast.error('Failed to load users');
|
||||
} finally {
|
||||
loadingUsers = false;
|
||||
@@ -514,7 +509,6 @@
|
||||
services = await response.json();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch services', err);
|
||||
toast.error('Failed to load services');
|
||||
} finally {
|
||||
loadingServices = false;
|
||||
@@ -561,24 +555,8 @@
|
||||
const whData: WorkingHoursDay[] = await whRes.json();
|
||||
const ahData: AvailableHoursDay[] = await ahRes.json();
|
||||
|
||||
console.log('[DEBUG] API response for hours', {
|
||||
outOfHours,
|
||||
start: startStr,
|
||||
end: endStr,
|
||||
mode: outOfHours ? 'out_of_hours' : 'normal',
|
||||
whSample: whData.slice(0, 3).map((d) => ({
|
||||
date: d.date,
|
||||
isOpen: d.isOpen,
|
||||
startTime: d.startTime,
|
||||
endTime: d.endTime
|
||||
})),
|
||||
ahSample: ahData
|
||||
.slice(0, 3)
|
||||
.map((d) => ({ date: d.date, isOpen: d.isOpen, slotsCount: d.slots?.length }))
|
||||
});
|
||||
|
||||
const whMap: Record<string, any> = {};
|
||||
const ahMap: Record<string, any> = {};
|
||||
const whMap: Record<string, DayHours> = {};
|
||||
const ahMap: Record<string, DayAvailability> = {};
|
||||
|
||||
whData.forEach(
|
||||
(d) => (whMap[d.date] = { isOpen: d.isOpen, startTime: d.startTime, endTime: d.endTime })
|
||||
@@ -605,7 +583,6 @@
|
||||
availableHours = { ...availableHours, ...ahMap };
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch hours', err);
|
||||
toast.error('Failed to load availability');
|
||||
} finally {
|
||||
_loadingWorkingHours = false;
|
||||
@@ -660,8 +637,8 @@
|
||||
const whData: WorkingHoursDay[] = await whRes.json();
|
||||
const ahData: AvailableHoursDay[] = await ahRes.json();
|
||||
|
||||
const whMap: Record<string, any> = {};
|
||||
const ahMap: Record<string, any> = {};
|
||||
const whMap: Record<string, DayHours> = {};
|
||||
const ahMap: Record<string, DayAvailability> = {};
|
||||
|
||||
whData.forEach(
|
||||
(d) => (whMap[d.date] = { isOpen: d.isOpen, startTime: d.startTime, endTime: d.endTime })
|
||||
@@ -676,7 +653,6 @@
|
||||
availableHours = { ...availableHours, ...ahMap };
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch hours', err);
|
||||
toast.error('Failed to load availability');
|
||||
} finally {
|
||||
_loadingWorkingHours = false;
|
||||
@@ -697,11 +673,11 @@
|
||||
const localDate = selectedDate.toDate(getLocalTimeZone());
|
||||
const [hours, minutes] = selectedTime.split(':').map(Number);
|
||||
localDate.setHours(hours, minutes, 0, 0);
|
||||
const startTimeISO = localDate.toISOString();
|
||||
const startTimeISO = formatLocalDateTime(localDate);
|
||||
|
||||
const serviceIds = selectedServices.filter((s) => !(s as any).is_custom).map((s) => s.id);
|
||||
const serviceIds = selectedServices.filter((s) => !s.is_custom).map((s) => s.id);
|
||||
const customServiceIds = selectedServices
|
||||
.filter((s) => (s as any).is_custom)
|
||||
.filter((s) => s.is_custom)
|
||||
.map((s) => s.id);
|
||||
|
||||
// Build service overrides payload
|
||||
@@ -716,7 +692,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
const payload: Record<string, any> = {
|
||||
const payload: {
|
||||
user_id: string | null;
|
||||
start_time: string;
|
||||
service_ids: string[];
|
||||
service_overrides: Array<{ service_id: string; override_duration_minutes: number }>;
|
||||
ttl_minutes: number;
|
||||
reservation_type: string;
|
||||
out_of_hours: boolean;
|
||||
custom_service_ids?: string[];
|
||||
} = {
|
||||
user_id: selectedUserId || null,
|
||||
start_time: startTimeISO,
|
||||
service_ids: serviceIds,
|
||||
@@ -760,7 +745,6 @@
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Reservation error:', err);
|
||||
toast.error('Failed to reserve slot');
|
||||
return false;
|
||||
} finally {
|
||||
@@ -770,8 +754,8 @@
|
||||
|
||||
function startCountdown() {
|
||||
// Clear any existing interval
|
||||
if ((window as any).__bookingCreateCountdownInterval) {
|
||||
clearInterval((window as any).__bookingCreateCountdownInterval);
|
||||
if (window.__bookingCreateCountdownInterval) {
|
||||
clearInterval(window.__bookingCreateCountdownInterval);
|
||||
}
|
||||
|
||||
const updateCountdown = () => {
|
||||
@@ -787,8 +771,8 @@
|
||||
reservationCountdown = 'Expired';
|
||||
reservationId = null;
|
||||
reservationExpiresAt = null;
|
||||
if ((window as any).__bookingCreateCountdownInterval) {
|
||||
clearInterval((window as any).__bookingCreateCountdownInterval);
|
||||
if (window.__bookingCreateCountdownInterval) {
|
||||
clearInterval(window.__bookingCreateCountdownInterval);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -799,7 +783,7 @@
|
||||
};
|
||||
|
||||
updateCountdown();
|
||||
(window as any).__bookingCreateCountdownInterval = setInterval(updateCountdown, 1000);
|
||||
window.__bookingCreateCountdownInterval = setInterval(updateCountdown, 1000);
|
||||
}
|
||||
|
||||
// =============== Logic ===============
|
||||
@@ -841,7 +825,7 @@
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const list = data.services || data;
|
||||
customServices = list.map((cs: any) => ({ ...cs, is_custom: true }));
|
||||
customServices = list.map((cs: CustomService) => ({ ...cs, is_custom: true }));
|
||||
}
|
||||
} catch {
|
||||
console.error('Failed to fetch custom services');
|
||||
@@ -916,17 +900,7 @@
|
||||
// Out-of-hours: only check if available hours exist with slots
|
||||
if (outOfHours) {
|
||||
const ahDay = availableHours?.[dateStr];
|
||||
const whDay = workingHours[dateStr];
|
||||
const result = !ahDay?.slots || ahDay.slots.length === 0;
|
||||
console.log('[DEBUG] isDateUnavailable (outOfHours)', {
|
||||
dateStr,
|
||||
result,
|
||||
hasSlots: ahDay?.slots?.length,
|
||||
whIsOpen: whDay?.isOpen,
|
||||
whStart: whDay?.startTime,
|
||||
whEnd: whDay?.endTime
|
||||
});
|
||||
return result;
|
||||
return !ahDay?.slots || ahDay.slots.length === 0;
|
||||
}
|
||||
|
||||
const dayHours = workingHours[dateStr];
|
||||
@@ -1012,7 +986,7 @@
|
||||
const localDate = selectedDate.toDate(getLocalTimeZone());
|
||||
const [hours, minutes] = selectedTime.split(':').map(Number);
|
||||
localDate.setHours(hours, minutes, 0, 0);
|
||||
const dateTimeStr = localDate.toISOString();
|
||||
const dateTimeStr = formatLocalDateTime(localDate);
|
||||
|
||||
const overrides = [];
|
||||
for (const [serviceId, data] of Object.entries(serviceOverrides)) {
|
||||
@@ -1028,11 +1002,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
const payload: Record<string, any> = {
|
||||
const payload: {
|
||||
user_id: string;
|
||||
start_time: string;
|
||||
service_ids: string[];
|
||||
custom_service_ids: string[];
|
||||
service_overrides: Array<{ service_id: string; override_price: number | null; override_duration_minutes: number | null }> | undefined;
|
||||
notes: string | null;
|
||||
out_of_hours: boolean;
|
||||
} = {
|
||||
user_id: finalUserId,
|
||||
start_time: dateTimeStr,
|
||||
service_ids: selectedServices.filter((s) => !(s as any).is_custom).map((s) => s.id),
|
||||
custom_service_ids: selectedServices.filter((s) => (s as any).is_custom).map((s) => s.id),
|
||||
service_ids: selectedServices.filter((s) => !s.is_custom).map((s) => s.id),
|
||||
custom_service_ids: selectedServices.filter((s) => s.is_custom).map((s) => s.id),
|
||||
service_overrides: overrides.length > 0 ? overrides : undefined,
|
||||
notes: notes.trim() || null,
|
||||
out_of_hours: outOfHours
|
||||
@@ -1054,11 +1036,9 @@
|
||||
onBookingCreated?.();
|
||||
} else {
|
||||
const errorText = await res.text();
|
||||
console.error('Booking creation failed:', errorText);
|
||||
toast.error(`Failed to create booking: ${errorText}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Booking submission error:', err);
|
||||
toast.error('An error occurred while creating booking');
|
||||
} finally {
|
||||
submitting = false;
|
||||
@@ -1677,7 +1657,7 @@
|
||||
></path>
|
||||
</svg>
|
||||
<span class="text-sm font-medium text-green-800">
|
||||
Slot reserved until {reservationExpiresAt.toLocaleTimeString([], {
|
||||
Slot reserved until {parseWallClockDate(reservationExpiresAt.toISOString()).toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
|
||||
@@ -118,10 +118,14 @@
|
||||
}
|
||||
|
||||
function validateVatNumber(value: unknown): string {
|
||||
const v = value === null || value === undefined ? '' : String(value);
|
||||
const trimmed = v.trim();
|
||||
const trimmed = String(value ?? '').trim();
|
||||
if (!trimmed) return '';
|
||||
if (trimmed.length > 20) return 'Must be 20 characters or fewer';
|
||||
// UK VAT numbers: GB + 9 digits (standard) or GB + 12 digits (branch)
|
||||
if (trimmed.length !== 11 && trimmed.length !== 14) return 'Must be GB followed by 9 or 12 digits';
|
||||
if (!trimmed.startsWith('GB')) return 'Must start with GB';
|
||||
const digits = trimmed.slice(2);
|
||||
if (!/^\d+$/.test(digits)) return 'Must contain only digits after GB';
|
||||
if (digits.length !== 9 && digits.length !== 12) return 'Must be GB followed by 9 or 12 digits';
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -207,7 +211,7 @@
|
||||
];
|
||||
|
||||
for (const field of fields) {
|
||||
const newVal = normalisedForm[field] as unknown;
|
||||
const newVal = normalisedForm[field];
|
||||
const oldVal = settings[field];
|
||||
if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
|
||||
patch[field] = newVal;
|
||||
|
||||
@@ -286,7 +286,7 @@
|
||||
<td class="py-3 text-right font-medium">£{service.price.toFixed(2)}</td>
|
||||
<td class="py-3 text-right">{service.duration_minutes} min</td>
|
||||
<td class="py-3 text-right">
|
||||
{service.usage_count}×{service.last_used_at ? ` (last: ${new Date(service.last_used_at).toLocaleDateString()})` : ''}
|
||||
{service.usage_count}×{service.last_used_at ? ` (last: ${new Date(service.last_used_at).toLocaleDateString('en-GB', { timeZone: 'Europe/London' })})` : ''}
|
||||
</td>
|
||||
<td class="py-3">
|
||||
<div class="flex justify-center gap-2">
|
||||
|
||||
@@ -147,8 +147,8 @@
|
||||
campaign_type: c.campaign_type,
|
||||
discount_percent: c.discount_percent,
|
||||
scope: c.scope || 'all_bookings',
|
||||
start_date: c.start_date ? new Date(c.start_date).toISOString().slice(0, 10) : '',
|
||||
end_date: c.end_date ? new Date(c.end_date).toISOString().slice(0, 10) : '',
|
||||
start_date: c.start_date ? c.start_date.slice(0, 10) : '',
|
||||
end_date: c.end_date ? c.end_date.slice(0, 10) : '',
|
||||
milestone_type: c.milestone_type || 'per_user_booking_count',
|
||||
milestone_value: c.milestone_value || 0,
|
||||
milestone_unit: c.milestone_unit || 'bookings',
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { formatDuration } from '$lib/utils/format';
|
||||
import { formatUserName } from '$lib/utils/nameDisplay';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { formatDuration } from '$lib/utils/format';
|
||||
import { formatUserName } from '$lib/utils/nameDisplay';
|
||||
import { parseWallClockDate } from '$lib/utils/timeSlots';
|
||||
import * as Modal from '$lib/components/ui/dialog';
|
||||
import * as AlertDialog from '$lib/components/ui/alert-dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
@@ -437,7 +438,7 @@
|
||||
<div class="text-xs text-gray-500">Time</div>
|
||||
<div class="font-medium">
|
||||
{(() => {
|
||||
const date = new SvelteDate(booking.start_time);
|
||||
const date = parseWallClockDate(booking.start_time);
|
||||
return date.toLocaleTimeString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import * as Modal from '$lib/components/ui/dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as AlertDialog from '$lib/components/ui/alert-dialog';
|
||||
import { formatUserName } from '$lib/utils/nameDisplay';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import * as Modal from '$lib/components/ui/dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as AlertDialog from '$lib/components/ui/alert-dialog';
|
||||
import { formatUserName } from '$lib/utils/nameDisplay';
|
||||
import { parseWallClockDate } from '$lib/utils/timeSlots';
|
||||
|
||||
interface ServiceItem {
|
||||
id: string;
|
||||
@@ -55,7 +56,7 @@
|
||||
let showDenyConfirm = $state(false);
|
||||
|
||||
function formatDateLine1(dateTimeString: string): string {
|
||||
const d = new SvelteDate(dateTimeString);
|
||||
const d = parseWallClockDate(dateTimeString);
|
||||
const dateStr = d.toLocaleDateString('en-GB', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
@@ -71,7 +72,7 @@
|
||||
}
|
||||
|
||||
function formatDateLine2(dateTimeString: string, durationMinutes: number): string {
|
||||
const d = new SvelteDate(dateTimeString);
|
||||
const d = parseWallClockDate(dateTimeString);
|
||||
const endMinutes = d.getHours() * 60 + d.getMinutes() + durationMinutes;
|
||||
const endH = Math.floor(endMinutes / 60);
|
||||
const endM = endMinutes % 60;
|
||||
|
||||
@@ -752,7 +752,8 @@
|
||||
return new Date(dateStr).toLocaleDateString('en-GB', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
year: 'numeric',
|
||||
timeZone: 'Europe/London'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -96,12 +96,15 @@
|
||||
}
|
||||
|
||||
function isoDateOf(d: Date) {
|
||||
return d.toISOString().slice(0, 10);
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
|
||||
function addWeeksToException(fromISO: string, toISO: string, dest: string[]) {
|
||||
const from = new SvelteDate(fromISO + 'T00:00:00');
|
||||
const to = new SvelteDate(toISO + 'T00:00:00');
|
||||
const from = new SvelteDate(fromISO + 'T00:00:00Z');
|
||||
const to = new SvelteDate(toISO + 'T00:00:00Z');
|
||||
const first = new SvelteDate(from);
|
||||
const day = first.getDay();
|
||||
const daysToMonday = day === 0 ? -6 : 1 - day;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { CalendarDate } from '@internationalized/date';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { sanitizeText } from '$lib/utils/toast-safe';
|
||||
import { formatDuration } from '$lib/utils/format';
|
||||
import { formatUserName } from '$lib/utils/nameDisplay';
|
||||
import { formatLocalDateTime, parseWallClockDate } from '$lib/utils/timeSlots';
|
||||
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
@@ -115,7 +117,7 @@
|
||||
|
||||
function getWorkingHoursForDate(dateStr: string): WorkingHourRow | null {
|
||||
if (!dateStr || defaultHours.length === 0) return null;
|
||||
const d = new SvelteDate(dateStr + 'T00:00:00');
|
||||
const d = new SvelteDate(dateStr + 'T00:00:00Z');
|
||||
const jsDay = d.getDay();
|
||||
const weekday = jsDay === 0 ? 6 : jsDay - 1;
|
||||
return defaultHours.find((h) => h.weekday === weekday) ?? null;
|
||||
@@ -265,7 +267,12 @@
|
||||
): string | null {
|
||||
if (!date) return null;
|
||||
const t24 = to24h(hour, minute, period);
|
||||
return `${date}T${t24}:00`;
|
||||
const [y, m, d] = date.split('-').map(Number);
|
||||
const cal = new CalendarDate(y, m, d);
|
||||
const [h, min] = t24.split(':').map(Number);
|
||||
const dt = cal.toDate('Europe/London');
|
||||
dt.setHours(h, min, 0, 0);
|
||||
return formatLocalDateTime(dt);
|
||||
}
|
||||
|
||||
let formComplete = $derived.by(() => {
|
||||
@@ -403,7 +410,7 @@
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
start_time: start.toISOString(),
|
||||
start_time: startIso,
|
||||
duration_minutes: 60,
|
||||
description: `RESERVATION:placeholder:${booking.id}`
|
||||
})
|
||||
@@ -422,7 +429,7 @@
|
||||
Authorization: `Bearer ${authStore.currentToken}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
start_time: start.toISOString(),
|
||||
start_time: startIso,
|
||||
duration_minutes: durationMinutes,
|
||||
description: newDescription.trim()
|
||||
})
|
||||
@@ -572,7 +579,7 @@
|
||||
<div class="truncate font-medium text-gray-900">{b.description || 'Untitled'}</div>
|
||||
<div class="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm text-gray-600">
|
||||
<span>
|
||||
{new SvelteDate(b.start_time).toLocaleDateString('en-GB', {
|
||||
{parseWallClockDate(b.start_time).toLocaleDateString('en-GB', {
|
||||
weekday: 'short',
|
||||
day: 'numeric',
|
||||
month: 'short'
|
||||
@@ -580,7 +587,7 @@
|
||||
</span>
|
||||
<span class="text-gray-400">·</span>
|
||||
<span>
|
||||
{new SvelteDate(b.start_time).toLocaleTimeString('en-GB', {
|
||||
{parseWallClockDate(b.start_time).toLocaleTimeString('en-GB', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
@@ -589,7 +596,7 @@
|
||||
<span class="text-gray-400">–</span>
|
||||
<span>
|
||||
{(() => {
|
||||
const end = new SvelteDate(b.start_time);
|
||||
const end = parseWallClockDate(b.start_time);
|
||||
end.setMinutes(end.getMinutes() + b.duration_minutes);
|
||||
return end.toLocaleTimeString('en-GB', {
|
||||
hour: 'numeric',
|
||||
@@ -712,7 +719,7 @@
|
||||
size="sm"
|
||||
class="flex-1 text-xs"
|
||||
onclick={() => {
|
||||
newStartDate = new Date().toISOString().slice(0, 10);
|
||||
newStartDate = new Date().toLocaleDateString('en-CA', { timeZone: 'Europe/London' });
|
||||
onStartDateChange();
|
||||
}}
|
||||
>
|
||||
@@ -724,7 +731,7 @@
|
||||
size="sm"
|
||||
class="flex-1 text-xs"
|
||||
onclick={() => {
|
||||
newStartDate = new Date(Date.now() + 86400000).toISOString().slice(0, 10);
|
||||
newStartDate = new Date(Date.now() + 86400000).toLocaleDateString('en-CA', { timeZone: 'Europe/London' });
|
||||
onStartDateChange();
|
||||
}}
|
||||
>
|
||||
@@ -746,7 +753,7 @@
|
||||
<p class="text-sm text-gray-400">Select a date first</p>
|
||||
{:else if !selectedWorkingHours.isOpen}
|
||||
<p class="text-sm text-red-500">
|
||||
Closed on {new SvelteDate(newStartDate + 'T00:00:00').toLocaleDateString('en-GB', {
|
||||
Closed on {new SvelteDate(newStartDate + 'T00:00:00Z').toLocaleDateString('en-GB', {
|
||||
weekday: 'long'
|
||||
})}
|
||||
</p>
|
||||
@@ -855,7 +862,7 @@
|
||||
<div class="min-w-0">
|
||||
<div class="text-sm font-medium">{formatUserName(booking.user?.full_name || 'Unknown', booking.user?.previous_first_name, booking.user?.previous_last_name)}</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{new SvelteDate(booking.start_time).toLocaleTimeString('en-GB', {
|
||||
{parseWallClockDate(booking.start_time).toLocaleTimeString('en-GB', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import * as Modal from '$lib/components/ui/dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import PatchTestModal from './PatchTestModal.svelte';
|
||||
import { formatUserName } from '$lib/utils/nameDisplay';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { SvelteDate } from 'svelte/reactivity';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import * as Modal from '$lib/components/ui/dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import PatchTestModal from './PatchTestModal.svelte';
|
||||
import { formatUserName } from '$lib/utils/nameDisplay';
|
||||
import { parseWallClockDate } from '$lib/utils/timeSlots';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
@@ -404,7 +405,7 @@
|
||||
<div class="flex-1">
|
||||
<div class="font-medium">
|
||||
{(() => {
|
||||
const date = new SvelteDate(booking.start_time);
|
||||
const date = parseWallClockDate(booking.start_time);
|
||||
const dateStr = date.toLocaleDateString('en-US', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
calculateMiddleWindow,
|
||||
shouldApplyLunchProtection
|
||||
} from '$lib/lunchProtection';
|
||||
import { formatLocalDateTime } from '$lib/utils/timeSlots';
|
||||
|
||||
const RESERVATION_TTL = 15;
|
||||
|
||||
@@ -50,7 +51,7 @@
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch services', err);
|
||||
// Silently handled - services list remains empty
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +144,9 @@
|
||||
|
||||
try {
|
||||
const now = new SvelteDate();
|
||||
const today = new CalendarDate(now.getFullYear(), now.getMonth() + 1, now.getDate());
|
||||
const londonDateStr = new Date().toLocaleDateString('en-CA', { timeZone: 'Europe/London' });
|
||||
const [y, m, d] = londonDateStr.split('-').map(Number);
|
||||
const today = new CalendarDate(y, m, d);
|
||||
|
||||
const response = await fetch(`/api/scheduling/available-hours?start=${today}&end=${today}`, {
|
||||
headers: { Authorization: `Bearer ${authStore.currentToken}` }
|
||||
@@ -234,18 +237,11 @@
|
||||
isReserving = true;
|
||||
|
||||
try {
|
||||
const now = new SvelteDate();
|
||||
const londonDateStr = new Date().toLocaleDateString('en-CA', { timeZone: 'Europe/London' });
|
||||
const [y, m, d] = londonDateStr.split('-').map(Number);
|
||||
const [hours, minutes] = startTime.split(':').map(Number);
|
||||
const start = new SvelteDate(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate(),
|
||||
hours,
|
||||
minutes,
|
||||
0,
|
||||
0
|
||||
);
|
||||
const startTimeISO = start.toISOString();
|
||||
const start = new SvelteDate(y, m - 1, d, hours, minutes, 0, 0);
|
||||
const startTimeISO = formatLocalDateTime(start);
|
||||
|
||||
const response = await fetch('/api/admin/bookings/reserve', {
|
||||
method: 'POST',
|
||||
@@ -290,8 +286,8 @@
|
||||
}
|
||||
|
||||
function startWalkInCountdown() {
|
||||
if ((window as any).__walkInCountdownInterval) {
|
||||
clearInterval((window as any).__walkInCountdownInterval);
|
||||
if (window.__walkInCountdownInterval) {
|
||||
clearInterval(window.__walkInCountdownInterval);
|
||||
}
|
||||
|
||||
const updateCountdown = () => {
|
||||
@@ -308,8 +304,8 @@
|
||||
reservationId = null;
|
||||
reservationExpiresAt = null;
|
||||
toast.error('Slot released — please re-check availability');
|
||||
if ((window as any).__walkInCountdownInterval) {
|
||||
clearInterval((window as any).__walkInCountdownInterval);
|
||||
if (window.__walkInCountdownInterval) {
|
||||
clearInterval(window.__walkInCountdownInterval);
|
||||
}
|
||||
showCreateModal = false;
|
||||
return;
|
||||
@@ -321,7 +317,7 @@
|
||||
};
|
||||
|
||||
updateCountdown();
|
||||
(window as any).__walkInCountdownInterval = setInterval(updateCountdown, 1000);
|
||||
window.__walkInCountdownInterval = setInterval(updateCountdown, 1000);
|
||||
}
|
||||
|
||||
let availableMinutes = $derived.by(() => {
|
||||
@@ -379,8 +375,8 @@
|
||||
reservationCountdown = '';
|
||||
reservedDuration = 0;
|
||||
reservedStartTime = null;
|
||||
if ((window as any).__walkInCountdownInterval) {
|
||||
clearInterval((window as any).__walkInCountdownInterval);
|
||||
if (window.__walkInCountdownInterval) {
|
||||
clearInterval(window.__walkInCountdownInterval);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { getLocalTimeZone } from '@internationalized/date';
|
||||
import { isValidUKPhone, toE164UK } from '$lib/utils/phone';
|
||||
import { formatUserName } from '$lib/utils/nameDisplay';
|
||||
import { formatLocalDateTime } from '$lib/utils/timeSlots';
|
||||
|
||||
// UI Components
|
||||
import * as Modal from '$lib/components/ui/dialog';
|
||||
@@ -213,8 +214,8 @@
|
||||
});
|
||||
|
||||
function startCountdown() {
|
||||
if ((window as any).__walkInModalCountdownInterval) {
|
||||
clearInterval((window as any).__walkInModalCountdownInterval);
|
||||
if (window.__walkInModalCountdownInterval) {
|
||||
clearInterval(window.__walkInModalCountdownInterval);
|
||||
}
|
||||
|
||||
const updateCountdown = () => {
|
||||
@@ -229,8 +230,8 @@
|
||||
if (diff <= 0) {
|
||||
reservationCountdown = 'Expired';
|
||||
isReservationExpired = true;
|
||||
if ((window as any).__walkInModalCountdownInterval) {
|
||||
clearInterval((window as any).__walkInModalCountdownInterval);
|
||||
if (window.__walkInModalCountdownInterval) {
|
||||
clearInterval(window.__walkInModalCountdownInterval);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -241,7 +242,7 @@
|
||||
};
|
||||
|
||||
updateCountdown();
|
||||
(window as any).__walkInModalCountdownInterval = setInterval(updateCountdown, 1000);
|
||||
window.__walkInModalCountdownInterval = setInterval(updateCountdown, 1000);
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
@@ -299,7 +300,6 @@
|
||||
services = await response.json();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch services', err);
|
||||
toast.error('Failed to load services');
|
||||
} finally {
|
||||
loadingServices = false;
|
||||
@@ -461,19 +461,16 @@
|
||||
if (availableStartTime) {
|
||||
// Parse the time from the widget (format: "HH:MM" or "HH:MM:SS")
|
||||
const [hours, minutes] = availableStartTime.split(':').map(Number);
|
||||
const now = new SvelteDate();
|
||||
start = new SvelteDate(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate(),
|
||||
hours,
|
||||
minutes,
|
||||
0,
|
||||
0
|
||||
);
|
||||
const londonDateStr = new Date().toLocaleDateString('en-CA', { timeZone: 'Europe/London' });
|
||||
const [y, m, d] = londonDateStr.split('-').map(Number);
|
||||
start = new SvelteDate(y, m - 1, d, hours, minutes, 0, 0);
|
||||
} else {
|
||||
// Fallback: Calculate immediate start time (rounded to next 15 min)
|
||||
const now = new SvelteDate();
|
||||
const londonDateStr = new Date().toLocaleDateString('en-CA', { timeZone: 'Europe/London' });
|
||||
const londonTimeStr = new Date().toLocaleTimeString('en-GB', { timeZone: 'Europe/London', hour: '2-digit', minute: '2-digit', hour12: false });
|
||||
const [y, m, d] = londonDateStr.split('-').map(Number);
|
||||
const [h, min] = londonTimeStr.split(':').map(Number);
|
||||
const now = new SvelteDate(y, m - 1, d, h, min, 0, 0);
|
||||
start = new SvelteDate(now);
|
||||
const minutes = start.getMinutes();
|
||||
const remainder = 15 - (minutes % 15);
|
||||
@@ -484,7 +481,7 @@
|
||||
start.setMilliseconds(0);
|
||||
}
|
||||
|
||||
const dateTimeStr = start.toISOString();
|
||||
const dateTimeStr = formatLocalDateTime(start);
|
||||
|
||||
const overrides = [];
|
||||
for (const [serviceId, data] of Object.entries(serviceOverrides)) {
|
||||
@@ -500,11 +497,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
const payload: Record<string, any> = {
|
||||
const payload: {
|
||||
user_id: string;
|
||||
start_time: string;
|
||||
service_ids: string[];
|
||||
custom_service_ids: string[];
|
||||
service_overrides: Array<{ service_id: string; override_price: number | null; override_duration_minutes: number | null }> | undefined;
|
||||
notes: string | null;
|
||||
} = {
|
||||
user_id: finalUserId,
|
||||
start_time: dateTimeStr,
|
||||
service_ids: selectedServices.filter(s => !(s as any).is_custom).map((s) => s.id),
|
||||
custom_service_ids: selectedServices.filter(s => (s as any).is_custom).map((s) => s.id),
|
||||
service_ids: selectedServices.filter(s => !s.is_custom).map((s) => s.id),
|
||||
custom_service_ids: selectedServices.filter(s => s.is_custom).map((s) => s.id),
|
||||
service_overrides: overrides.length > 0 ? overrides : undefined,
|
||||
notes: notes.trim() || null
|
||||
};
|
||||
@@ -526,11 +530,9 @@
|
||||
onBookingCreated?.();
|
||||
} else {
|
||||
const errorText = await res.text();
|
||||
console.error('Booking creation failed:', errorText);
|
||||
toast.error(`Failed to create booking: ${errorText}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Booking submission error:', err);
|
||||
toast.error('An error occurred while creating booking');
|
||||
} finally {
|
||||
submitting = false;
|
||||
|
||||
Reference in New Issue
Block a user