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.
This commit is contained in:
2026-08-22 00:34:49 +01:00
parent 6470a3f6c9
commit 0323313921
3 changed files with 44 additions and 12 deletions
@@ -86,6 +86,11 @@
] ]
}); });
// =============== Conflict Resolution State ===============
let overlappingBookings = $state<OverlappingBooking[]>([]);
let checkingOverlap = $state(false);
let hasOverlap = $derived(overlappingBookings.length > 0);
const isFormValid = $derived( const isFormValid = $derived(
exceptionDraft.name.trim() !== '' && exceptionDraft.name.trim() !== '' &&
exceptionDraft.weekStarts.length > 0 && exceptionDraft.weekStarts.length > 0 &&
@@ -120,11 +125,6 @@
const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
// =============== Conflict Resolution State ===============
let overlappingBookings = $state<OverlappingBooking[]>([]);
let checkingOverlap = $state(false);
let hasOverlap = $derived(overlappingBookings.length > 0);
async function checkConflictingBookings() { async function checkConflictingBookings() {
if (exceptionDraft.weekStarts.length === 0) { if (exceptionDraft.weekStarts.length === 0) {
overlappingBookings = []; overlappingBookings = [];
@@ -241,8 +241,8 @@
}) => ({ }) => ({
id: h.id, id: h.id,
weekday: h.weekday, weekday: h.weekday,
start_time: h.startTime, start_time: h.startTime,
end_time: h.endTime, end_time: h.endTime,
is_open: h.isOpen is_open: h.isOpen
}) })
) || [] ) || []
@@ -266,6 +266,13 @@
return; 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; savingHours = true;
const loadingToast = toast.loading('Creating exception group...'); const loadingToast = toast.loading('Creating exception group...');
@@ -298,7 +305,9 @@
await fetchExceptionGroups(); await fetchExceptionGroups();
} else { } else {
const text = await response.text(); 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) { } catch (err) {
console.error('Error creating exception group:', err); console.error('Error creating exception group:', err);
@@ -329,7 +338,9 @@
await fetchExceptionGroups(); await fetchExceptionGroups();
} else { } else {
const text = await response.text(); 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) { } catch (err) {
console.error('Error deleting exception group:', err); console.error('Error deleting exception group:', err);
@@ -697,7 +708,14 @@
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" /> <circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
/>
<path <path
class="opacity-75" class="opacity-75"
fill="currentColor" fill="currentColor"
@@ -729,6 +747,9 @@
? '' ? ''
: 's'} with the proposed hours : 's'} with the proposed hours
</span> </span>
<Button variant="outline" size="sm" class="h-6 text-xs ml-auto" onclick={checkConflictingBookings}>
Refresh
</Button>
</div> </div>
<div class="space-y-3"> <div class="space-y-3">
{#each overlappingBookings as booking (booking.id)} {#each overlappingBookings as booking (booking.id)}
@@ -395,6 +395,14 @@
const end = new Date(endIso); const end = new Date(endIso);
const durationMinutes = Math.round((end.getTime() - start.getTime()) / 60000); 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; creating = true;
const loadingToast = toast.loading('Creating time blocker...'); const loadingToast = toast.loading('Creating time blocker...');
@@ -864,6 +872,9 @@
? '' ? ''
: 's'} with this slot : 's'} with this slot
</span> </span>
<Button variant="outline" size="sm" class="h-6 text-xs ml-auto" onclick={checkOverlappingBookings}>
Refresh
</Button>
</div> </div>
<div class="space-y-3"> <div class="space-y-3">
{#each overlappingBookings as booking (booking.id)} {#each overlappingBookings as booking (booking.id)}
+2 -2
View File
@@ -256,7 +256,7 @@
const isFormComplete = $derived( const isFormComplete = $derived(
isLogin isLogin
? formData.email.trim().length > 0 && formData.password.length > 0 ? formData.email.trim().length > 0 && formData.password.length > 0
: (formData.firstName.trim() && : formData.firstName.trim() &&
formData.lastName.trim() && formData.lastName.trim() &&
formData.phone && formData.phone &&
formData.dateOfBirth && formData.dateOfBirth &&
@@ -270,7 +270,7 @@
agreedToPolicy && agreedToPolicy &&
!validationErrors.email && !validationErrors.email &&
!validationErrors.phone && !validationErrors.phone &&
!validationErrors.dateOfBirth) !validationErrors.dateOfBirth
); );
</script> </script>