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:
2026-06-18 16:27:02 +01:00
co-authored by Sisyphus
parent b23a8b89c4
commit 2ad3c01a80
3 changed files with 541 additions and 296 deletions
@@ -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.'