From 03233139213c18d30aa889f6552f92c2c23308cf Mon Sep 17 00:00:00 2001 From: Stephen Adamson Date: Thu, 30 Jul 2026 08:59:16 +0100 Subject: [PATCH] fix: add conflict re-check guard before saving holiday hours and time blockers Re-check conflicts immediately before finalizing holiday hours and time blocker saves to prevent race conditions. Add Refresh button to conflict banners. Fix prettier formatting in login page. --- .../lib/components/admin/HolidayHours.svelte | 41 ++++++++++++++----- .../lib/components/admin/TimeBlockers.svelte | 11 +++++ frontend/src/routes/login/+page.svelte | 4 +- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/frontend/src/lib/components/admin/HolidayHours.svelte b/frontend/src/lib/components/admin/HolidayHours.svelte index 5c46c36..d751f91 100644 --- a/frontend/src/lib/components/admin/HolidayHours.svelte +++ b/frontend/src/lib/components/admin/HolidayHours.svelte @@ -86,6 +86,11 @@ ] }); + // =============== Conflict Resolution State =============== + let overlappingBookings = $state([]); + let checkingOverlap = $state(false); + let hasOverlap = $derived(overlappingBookings.length > 0); + const isFormValid = $derived( exceptionDraft.name.trim() !== '' && exceptionDraft.weekStarts.length > 0 && @@ -120,11 +125,6 @@ const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; - // =============== Conflict Resolution State =============== - let overlappingBookings = $state([]); - let checkingOverlap = $state(false); - let hasOverlap = $derived(overlappingBookings.length > 0); - async function checkConflictingBookings() { if (exceptionDraft.weekStarts.length === 0) { overlappingBookings = []; @@ -241,8 +241,8 @@ }) => ({ id: h.id, weekday: h.weekday, - start_time: h.startTime, - end_time: h.endTime, + start_time: h.startTime, + end_time: h.endTime, is_open: h.isOpen }) ) || [] @@ -266,6 +266,13 @@ return; } + // Re-check conflicts before finalizing (race condition guard) + await checkConflictingBookings(); + if (overlappingBookings.length > 0) { + toast.error('New conflicting bookings found. Please resolve them first.'); + return; + } + savingHours = true; const loadingToast = toast.loading('Creating exception group...'); @@ -298,7 +305,9 @@ await fetchExceptionGroups(); } else { const text = await response.text(); - toast.error('Failed to create: ' + sanitizeText(extractErrorMessage(text)), { id: loadingToast }); + toast.error('Failed to create: ' + sanitizeText(extractErrorMessage(text)), { + id: loadingToast + }); } } catch (err) { console.error('Error creating exception group:', err); @@ -329,7 +338,9 @@ await fetchExceptionGroups(); } else { const text = await response.text(); - toast.error('Failed to delete: ' + sanitizeText(extractErrorMessage(text)), { id: loadingToast }); + toast.error('Failed to delete: ' + sanitizeText(extractErrorMessage(text)), { + id: loadingToast + }); } } catch (err) { console.error('Error deleting exception group:', err); @@ -697,7 +708,14 @@ fill="none" viewBox="0 0 24 24" > - + +
{#each overlappingBookings as booking (booking.id)} diff --git a/frontend/src/lib/components/admin/TimeBlockers.svelte b/frontend/src/lib/components/admin/TimeBlockers.svelte index 30389f1..2550bdf 100644 --- a/frontend/src/lib/components/admin/TimeBlockers.svelte +++ b/frontend/src/lib/components/admin/TimeBlockers.svelte @@ -395,6 +395,14 @@ const end = new Date(endIso); const durationMinutes = Math.round((end.getTime() - start.getTime()) / 60000); + // Re-check conflicts before finalizing (race condition guard) + await checkOverlappingBookings(); + if (overlappingBookings.length > 0) { + toast.error('New conflicting bookings found. Please resolve them first.'); + creating = false; + return; + } + creating = true; const loadingToast = toast.loading('Creating time blocker...'); @@ -864,6 +872,9 @@ ? '' : 's'} with this slot +
{#each overlappingBookings as booking (booking.id)} diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte index b1e209e..bbdbf57 100644 --- a/frontend/src/routes/login/+page.svelte +++ b/frontend/src/routes/login/+page.svelte @@ -256,7 +256,7 @@ const isFormComplete = $derived( isLogin ? formData.email.trim().length > 0 && formData.password.length > 0 - : (formData.firstName.trim() && + : formData.firstName.trim() && formData.lastName.trim() && formData.phone && formData.dateOfBirth && @@ -270,7 +270,7 @@ agreedToPolicy && !validationErrors.email && !validationErrors.phone && - !validationErrors.dateOfBirth) + !validationErrors.dateOfBirth );