diff --git a/frontend/src/lib/components/admin/RescheduleModal.svelte b/frontend/src/lib/components/admin/RescheduleModal.svelte new file mode 100644 index 0000000..3838ee8 --- /dev/null +++ b/frontend/src/lib/components/admin/RescheduleModal.svelte @@ -0,0 +1,434 @@ + + + + + + Reschedule Booking + + Move this booking to a new time. This change takes effect immediately. + + + +
+ + +
+ Current: {new SvelteDate(booking.start_time).toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short', year: 'numeric' })} at {new SvelteDate(booking.start_time).toLocaleTimeString('en-GB', { hour: 'numeric', minute: '2-digit', hour12: true })} +
+
+ {booking.user?.full_name || 'Unknown'} · {booking.services?.map((s) => s.service_name).join(', ') || 'No services'} · {bookingDuration} min +
+
+
+ + + +
+ { + selectedDate = newDate; + selectedTime = null; + }} + onPlaceholderChange={(newPlaceholder) => { + placeholder = newPlaceholder; + userNavigatedCalendar = true; + }} + /> +
+ + {#if !loadingHours} + {#if selectedDate} +
+
+ {#if formattedSelectedDate} +
{formattedSelectedDate}
+ {/if} + { selectedTime = time; }} + /> +
+
+ {:else} +
+

Select a date to see available times

+
+ {/if} + {/if} + + {#if selectedDate && selectedTime} + + {/if} +
+ + + + + +
+
diff --git a/frontend/src/lib/components/admin/TimeBlockers.svelte b/frontend/src/lib/components/admin/TimeBlockers.svelte new file mode 100644 index 0000000..aa941d4 --- /dev/null +++ b/frontend/src/lib/components/admin/TimeBlockers.svelte @@ -0,0 +1,857 @@ + + + + +
+
+ Time Blockers + + Manage one-off blocked periods like appointments, breaks, or closures. + +
+ +
+
+ + + {#if loading} +
+ {#each Array(3) as _, i (i)} + + {/each} +
+ {:else} + {#if blockers.length === 0} +

No time blockers found.

+ {:else} +
+ {#each pagedBlockers as b (b.id)} +
+
+ +
+
{b.description || 'Untitled'}
+
+ + {new SvelteDate(b.start_time).toLocaleDateString('en-GB', { + weekday: 'short', + day: 'numeric', + month: 'short' + })} + + · + + {new SvelteDate(b.start_time).toLocaleTimeString('en-GB', { + hour: 'numeric', + minute: '2-digit', + hour12: true + })} + + + + {(() => { + const end = new SvelteDate(b.start_time); + end.setMinutes(end.getMinutes() + b.duration_minutes); + return end.toLocaleTimeString('en-GB', { + hour: 'numeric', + minute: '2-digit', + hour12: true + }); + })()} + + + {formatDuration(b.duration_minutes)} + +
+
+ {formatRelativeTime(b.start_time)} +
+
+
+ +
+ {/each} +
+ + {#if totalPages > 1} +
+

+ {(currentPage - 1) * PAGE_SIZE + 1}–{Math.min(currentPage * PAGE_SIZE, sortedBlockers.length)} of {sortedBlockers.length} +

+
+ + {#each Array(totalPages) as _, i (i)} + + {/each} + +
+
+ {/if} + {/if} + {/if} +
+
+ + + + + Create Time Blocker + + Block off a period of time so no bookings can be made during it. + + + +
+
+ + +
+ + + +
+

Start

+
+
+ +
+ + +
+ +
+
+ + {#if hoursLoading} + + {:else if !selectedWorkingHours} +

Select a date first

+ {:else if !selectedWorkingHours.isOpen} +

Closed on {new SvelteDate(newStartDate + 'T00:00:00').toLocaleDateString('en-GB', { weekday: 'long' })}

+ {:else} + + {/if} +
+
+
+ + + +
+

End Time

+ {#if hoursLoading} + + {:else if !selectedWorkingHours} +

Select a date first

+ {:else if !selectedWorkingHours.isOpen} +

Closed on this day

+ {:else if availableEndOptions.length === 0} +

No available end time after selected start

+ {:else} + + {/if} +
+ + {#if checkingOverlap} +
+ + + + + Checking for conflicting bookings… +
+ {:else if hasOverlap} +
+
+ + + + + + + {overlappingBookings.length} booking{overlappingBookings.length > 1 ? 's' : ''} conflict{overlappingBookings.length > 1 ? '' : 's'} with this slot + +
+
+ {#each overlappingBookings as booking (booking.id)} +
+
+
{booking.user?.full_name || 'Unknown'}
+
+ {new SvelteDate(booking.start_time).toLocaleTimeString('en-GB', { + hour: 'numeric', + minute: '2-digit', + hour12: true + })}} + {' · '} + {formatDuration(booking.duration_minutes)} + {#if booking.services?.length} + {' · '} + {booking.services.join(', ')} + {/if} +
+
+
+ {#if openBookingModal} + + {/if} + {#if openUserModal && booking.user?.id} + + {/if} +
+
+ {/each} +
+
+ {/if} +
+ + + + + +
+
+ + + + + Delete time blocker? + + This will remove the "{blockerToDelete?.description}" time blocker. Bookings may become available + during this period. This action cannot be undone. + + + + { + showDeleteAlert = false; + blockerToDelete = null; + }} + > + Cancel + + + Delete + + + + diff --git a/frontend/src/routes/admin/+page.svelte b/frontend/src/routes/admin/+page.svelte index 2047854..e0f9406 100644 --- a/frontend/src/routes/admin/+page.svelte +++ b/frontend/src/routes/admin/+page.svelte @@ -9,6 +9,7 @@ import UsersCard from '$lib/components/admin/UsersCard.svelte'; import BookingsCard from '$lib/components/admin/BookingsCard.svelte'; import HolidayHours from '$lib/components/admin/HolidayHours.svelte'; + import TimeBlockers from '$lib/components/admin/TimeBlockers.svelte'; import WeeklySchedule from '$lib/components/admin/WeeklySchedule.svelte'; import ServicesManagement from '$lib/components/admin/ServicesManagement.svelte'; import DiscountsManagement from '$lib/components/admin/DiscountsManagement.svelte'; @@ -47,6 +48,7 @@ let showBookingModal = $state(false); let selectedUserId = $state(null); let selectedBookingId = $state(null); + let rescheduleVersion = $state(0); function openUserModal(userId: string) { selectedUserId = userId; @@ -57,6 +59,10 @@ selectedBookingId = bookingId; showBookingModal = true; } + + function handleReschedule() { + rescheduleVersion++; + } {#if pageState === 'loading'} @@ -119,22 +125,24 @@ - +
-
- - +
+
+ + +
+
-
- -
-
- {#each Array(2) as _, i (i)} - +
+ {#each Array(3) as _, i (i)} + {/each}
+ +
@@ -267,6 +275,7 @@
+ @@ -279,6 +288,6 @@ {/if} {#if showBookingModal && selectedBookingId} - + {/if} {/if}