feat(frontend): update booking flow and account components
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -8,7 +8,8 @@
|
||||
import * as Textarea from '$lib/components/ui/textarea';
|
||||
import * as Label from '$lib/components/ui/label';
|
||||
import DatePicker from '$lib/components/booking/DatePicker.svelte';
|
||||
import type { Booking, Service, WorkingHoursDay, AvailableHoursDay } from '$lib/types/booking';
|
||||
import PolicyPopover from '$lib/components/ui/policyPopover.svelte';
|
||||
import type { Booking, BookingDiscount, Service, WorkingHoursDay, AvailableHoursDay } from '$lib/types/booking';
|
||||
import {
|
||||
extractBookedSlots,
|
||||
getLunchProtectionForSlots,
|
||||
@@ -77,6 +78,28 @@
|
||||
);
|
||||
let placeholderDate = $state<CalendarDate>(minDate);
|
||||
|
||||
let hoursUntilAppointment = $derived(
|
||||
(new SvelteDate(booking.start_time).getTime() - new SvelteDate().getTime()) / (1000 * 60 * 60)
|
||||
);
|
||||
let hasPayments = $derived(
|
||||
(booking.amount_paid ?? 0) > 0
|
||||
);
|
||||
let noticePeriodBlocked = $derived(
|
||||
hasPayments ? hoursUntilAppointment < 72 : hoursUntilAppointment < 24
|
||||
);
|
||||
let noticePeriodWarning = $derived(
|
||||
!hasPayments && hoursUntilAppointment < 24 && hoursUntilAppointment >= 0
|
||||
);
|
||||
let noticeBlockedMessage = $derived(
|
||||
hasPayments
|
||||
? 'This booking has payments and is too close to the original appointment to reschedule online.'
|
||||
: 'This booking is too close to the original appointment time to reschedule online.'
|
||||
);
|
||||
|
||||
let discountTotal = $derived(
|
||||
booking.discounts?.reduce((sum, d) => sum + d.discount_amount, 0) ?? 0
|
||||
);
|
||||
|
||||
let bookingTotalDuration = $derived(
|
||||
booking.services?.reduce(
|
||||
(sum, s) => sum + (s.override_duration_minutes ?? s.duration_minutes ?? 0),
|
||||
@@ -110,7 +133,7 @@
|
||||
let modalTitle = $derived(() => {
|
||||
switch (editMode) {
|
||||
case 'select':
|
||||
return 'Edit Request';
|
||||
return 'Edit/Reschedule';
|
||||
case 'time':
|
||||
return 'Change Time';
|
||||
case 'services':
|
||||
@@ -379,9 +402,10 @@
|
||||
}
|
||||
});
|
||||
|
||||
let servicesHoursFetched = $state(false);
|
||||
$effect(() => {
|
||||
if (editMode === 'services') {
|
||||
// Fetch hours for the booking's current date to calculate remaining time
|
||||
if (editMode === 'services' && !servicesHoursFetched) {
|
||||
servicesHoursFetched = true;
|
||||
const bookingDate = new SvelteDate(booking.start_time);
|
||||
const calDate = new CalendarDate(
|
||||
bookingDate.getFullYear(),
|
||||
@@ -390,6 +414,9 @@
|
||||
);
|
||||
fetchHoursForMonth(calDate);
|
||||
}
|
||||
if (editMode !== 'services') {
|
||||
servicesHoursFetched = false;
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Auto-select ────────────────────────────────────────
|
||||
@@ -681,12 +708,16 @@
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
toast.success("Edit request sent — we'll confirm shortly");
|
||||
const noShowWarning = response.headers.get('X-No-Show-Warning');
|
||||
if (noShowWarning) {
|
||||
toast.warning(noShowWarning, { duration: 8000 });
|
||||
}
|
||||
toast.success("Edit/reschedule request sent — we'll confirm shortly");
|
||||
open = false;
|
||||
onSubmitted();
|
||||
} else {
|
||||
const text = await response.text();
|
||||
toast.error(text || 'Failed to submit edit request');
|
||||
toast.error(text || 'Failed to submit edit/reschedule request');
|
||||
}
|
||||
} catch {
|
||||
toast.error('Network error');
|
||||
@@ -734,10 +765,54 @@
|
||||
<div class="flex flex-col gap-3">
|
||||
<p class="text-sm text-gray-600">What would you like to change?</p>
|
||||
|
||||
{#if noticePeriodBlocked}
|
||||
<div class="rounded-md border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800">
|
||||
<p class="font-medium text-amber-900">Cannot Reschedule Online</p>
|
||||
<p class="mt-1">{noticeBlockedMessage}</p>
|
||||
<p class="mt-1">
|
||||
<a href="/contact" target="_blank" class="underline">Contact us</a> to discuss options,
|
||||
or <button type="button" onclick={() => open = false} class="inline underline cursor-pointer">cancel this booking</button>
|
||||
and rebook — note that cancellation fees may apply based on our
|
||||
<PolicyPopover>
|
||||
{#snippet trigger()}
|
||||
<span class="underline">deposit policy</span>
|
||||
{/snippet}
|
||||
</PolicyPopover>.
|
||||
</p>
|
||||
</div>
|
||||
{:else if noticePeriodWarning}
|
||||
<div class="rounded-md border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800">
|
||||
<p class="font-medium text-amber-900">Rescheduling Within 24h</p>
|
||||
<p class="mt-1">
|
||||
Rescheduling within 24h counts as a no-show towards your deposit obligations. Two no-shows
|
||||
within 6 months will require deposits on future bookings.
|
||||
<PolicyPopover>
|
||||
{#snippet trigger()}
|
||||
<span class="underline">Full policy</span>
|
||||
{/snippet}
|
||||
</PolicyPopover>
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if booking.discounts && booking.discounts.length > 0}
|
||||
<div
|
||||
class="rounded-md border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800"
|
||||
>
|
||||
<p class="font-medium text-amber-900">Discounts Applied to This Booking</p>
|
||||
<p class="mt-1">
|
||||
Your booking has £{discountTotal.toFixed(2)} in savings from loyalty stamps or
|
||||
promotional offers. A time change requires admin approval. If denied, you can cancel
|
||||
(standard refund policy applies) and rebook at full price.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={noticePeriodBlocked}
|
||||
onclick={() => selectMode('time')}
|
||||
class="flex w-full items-center gap-4 rounded-lg border border-gray-200 bg-white p-4 text-left transition-colors hover:border-fuchsia-200 hover:bg-fuchsia-50"
|
||||
class="flex w-full items-center gap-4 rounded-lg border border-gray-200 bg-white p-4 text-left transition-colors hover:border-fuchsia-200 hover:bg-fuchsia-50 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<div
|
||||
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-gray-100 text-gray-600"
|
||||
@@ -775,7 +850,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => selectMode('both-services')}
|
||||
disabled={hasOverrides}
|
||||
disabled={hasOverrides || noticePeriodBlocked}
|
||||
class="flex w-full items-center gap-4 rounded-lg border border-gray-200 bg-white p-4 text-left transition-colors hover:border-fuchsia-200 hover:bg-fuchsia-50 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
title={hasOverrides
|
||||
? 'This booking has custom pricing. To change services, please contact the salon.'
|
||||
|
||||
Reference in New Issue
Block a user