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
@@ -29,6 +29,9 @@
import DatePicker from '$lib/components/booking/DatePicker.svelte';
import TimeSlotPicker from '$lib/components/booking/TimeSlotPicker.svelte';
import ServiceSelector from '$lib/components/booking/ServiceSelector.svelte';
import CardInput from '$lib/components/payments/CardInput.svelte';
import PolicyPopover from '$lib/components/ui/policyPopover.svelte';
import { POLICY } from '$lib/constants/policy';
import UserPaymentModal from '$lib/components/payments/UserPaymentModal.svelte';
import {
extractBookedSlots,
@@ -42,7 +45,8 @@
WorkingHoursDay,
AvailableHoursDay,
BookingService,
BookingStatus
BookingStatus,
Payment
} from '$lib/types/booking';
// =============== State Management ===============
@@ -155,9 +159,23 @@
status: string;
start_time: string;
notes: string;
deposit_required: boolean;
deposit_paid: boolean;
deposit_amount: number;
amount_paid: number;
amount_due: number;
payments: Payment[];
total_amount: number;
duration_minutes: number;
} | null>(null);
let showPayEarlyModal = $state(false);
let discountPreview = $state<{
eligible: boolean;
discounts: Array<{ source: string; name: string; percent: number; amount: number }>;
original_total: number;
discounted_total: number;
} | null>(null);
// =============== Payment Functions ===============
async function fetchUserDepositsRequired() {
@@ -167,7 +185,7 @@
}
try {
const response = await fetch('/api/user', {
const response = await fetch('/api/user/profile', {
headers: authStore.currentToken ? { Authorization: `Bearer ${authStore.currentToken}` } : {}
});
if (response.ok) {
@@ -252,39 +270,91 @@
function calculateDepositRequired(): boolean {
if (!selectedDate || !selectedTime) return false;
// Deposit required if user has deposits_required > 0 AND appointment is within 24 hours
const [hours, minutes] = selectedTime.split(':').map(Number);
const appointmentDate = selectedDate.toDate(getLocalTimeZone());
appointmentDate.setHours(hours, minutes, 0, 0);
const now = new SvelteDate();
const hoursUntilAppointment = (appointmentDate.getTime() - now.getTime()) / (1000 * 60 * 60);
return userDepositsRequired > 0 && hoursUntilAppointment <= 24;
// Deposit required if user has deposits_required > 0, regardless of booking window
return userDepositsRequired > 0;
}
function calculateDepositAmount(): number {
return Math.round(getTotalPrice() * 0.2 * 100) / 100;
}
async function fetchDiscountPreview() {
if (!confirmedBooking?.id) return;
try {
const resp = await fetch(`/api/bookings/${confirmedBooking.id}/discount-preview`, {
headers: authStore.currentToken
? { Authorization: `Bearer ${authStore.currentToken}` }
: undefined
});
if (resp.ok) {
const data = await resp.json();
// Only show time-based (auto-apply) discounts on the confirmation screen
if (data.eligible && data.discounts?.length > 0) {
discountPreview = data;
}
}
} catch (err) {
console.error('Failed to fetch discount preview:', err);
}
}
async function processPayment(amount: number) {
isProcessingPayment = true;
paymentAttempted = false;
try {
// TODO: Integrate Square SDK for actual payment processing
// For now, simulate successful payment after a delay
await new Promise((resolve) => setTimeout(resolve, 1500));
await submitAndProceed();
if (!confirmedBooking) {
toast.error('Booking was not created. Please try again.');
return;
}
const bookingId = confirmedBooking.id;
const amountCents = Math.round(amount * 100);
toast.success('Payment successful!');
depositPaid = true;
showPaymentForm = false;
nextStep();
const body: Record<string, unknown> = {
payment_type: 'deposit',
amount: amountCents,
idempotency_key: crypto.randomUUID?.() ?? Date.now().toString()
};
if (selectedPaymentMethod) {
body.card_id = selectedPaymentMethod;
} else {
const rawNumber = newCardNumber.replace(/\s/g, '');
if (rawNumber.length >= 13) {
body.new_card_token = rawNumber;
}
}
paymentAttempted = true;
const response = await fetch(`/api/bookings/${bookingId}/payment`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(authStore.currentToken ? { Authorization: `Bearer ${authStore.currentToken}` } : {})
},
body: JSON.stringify(body)
});
if (response.ok) {
depositPaid = true;
confirmedBooking.deposit_paid = true;
confirmedBooking.amount_paid = (confirmedBooking.amount_paid || 0) + amount;
confirmedBooking.amount_due = Math.max(0, (confirmedBooking.amount_due || 0) - amount);
toast.success('Payment successful!');
} else {
const text = await response.text();
toast.warning(text || 'Payment failed — you can pay again from your booking details.');
}
} catch (err) {
toast.error('Payment failed. Please try again.');
toast.error('An error occurred. Your booking may still be confirmed — check your appointments.');
} finally {
isProcessingPayment = false;
}
}
let paymentAttempted = $state(false);
function handlePayNow() {
showPaymentForm = true;
if (authStore.isAuthenticated) {
@@ -923,8 +993,13 @@
const minute = minutes % 60;
const timeStr = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
const isAvailable =
availableSlots.includes(timeStr) && !lunchProtectionMap.get(timeStr)?.isBlocked;
const slotDateTime = date.toDate(getLocalTimeZone());
slotDateTime.setHours(hour, minute, 0, 0);
const hoursUntilSlot = (slotDateTime.getTime() - now.getTime()) / (1000 * 60 * 60);
const isBlockedByDepositAdvance = userDepositsRequired > 0 && hoursUntilSlot < POLICY.DEPOSIT_ADVANCE_HOURS;
const isAvailable =
availableSlots.includes(timeStr) && !lunchProtectionMap.get(timeStr)?.isBlocked && !isBlockedByDepositAdvance;
if (isAvailable) {
if (currentUnavailableStart !== null) {
@@ -1032,7 +1107,18 @@
15,
false
);
const validSlots = availableSlots.filter((t) => !lunchProtection.get(t)?.isBlocked);
const now = new SvelteDate();
const validSlots = availableSlots.filter((t) => {
if (lunchProtection.get(t)?.isBlocked) return false;
if (userDepositsRequired > 0) {
const [h, m] = t.split(':').map(Number);
const slotDate = date.toDate(getLocalTimeZone());
slotDate.setHours(h, m, 0, 0);
const hoursUntil = (slotDate.getTime() - now.getTime()) / (1000 * 60 * 60);
if (hoursUntil < POLICY.DEPOSIT_ADVANCE_HOURS) return false;
}
return true;
});
if (validSlots.length === 0) return true;
}
@@ -1334,9 +1420,18 @@
id: booking.id,
status: booking.status,
start_time: booking.start_time,
notes: booking.notes || ''
notes: booking.notes || '',
deposit_required: booking.deposit_required ?? false,
deposit_paid: booking.deposit_paid ?? false,
deposit_amount: booking.deposit_amount ?? 0,
amount_paid: booking.amount_paid ?? 0,
amount_due: booking.amount_due || getTotalPrice(),
payments: booking.payments ?? [],
total_amount: booking.total_amount || getTotalPrice(),
duration_minutes: booking.duration_minutes || getTotalDuration()
};
currentStep = depositRequired ? 5 : 4;
fetchDiscountPreview();
setTimeout(() => {
window.scrollTo({ top: 0, behavior: 'smooth' });
}, 50);
@@ -1397,9 +1492,14 @@
}
// =============== Validation ===============
const canProceedStep1 = $derived(selectedServices.length > 0);
const canProceedStep2 = $derived(!!(selectedDate && selectedTime));
const isBlockedByActiveBooking = $derived(
authStore.isAuthenticated && userDepositsRequired > 0 && hasActiveBooking
);
const canProceedStep1 = $derived(selectedServices.length > 0 && !isBlockedByActiveBooking);
const canProceedStep2 = $derived(!!(selectedDate && selectedTime) && !isBlockedByActiveBooking);
const canProceedStep3 = $derived(
!isBlockedByActiveBooking &&
(authStore.isAuthenticated
? !!(
authStore.currentUser?.firstName &&
@@ -1486,25 +1586,12 @@
</svg>
</div>
<div class="space-y-1 text-sm text-amber-900">
<h4 class="font-semibold text-amber-800">Booking Limit While Deposits Are Owed</h4>
{#if userDepositsRequired === 1}
<p>
You have <span class="font-medium">1 deposit remaining</span>. After this
deposit is paid, you'll be able to book in advance again with no further
deposits required.
</p>
{:else}
<p>
You currently owe <span class="font-medium"
>{userDepositsRequired} deposits</span
>. You can only have <span class="font-medium">1 upcoming booking</span> at a time
while deposits are outstanding.
</p>
<p>
Once your current booking is complete and paid for, you'll be able to book
again.
</p>
{/if}
<p class="font-semibold text-amber-800">One Booking at a Time</p>
<p>
We are currently asking for deposits on upcoming bookings. While this is active,
only one online booking can be made at a time. If you need another appointment
please <a href="/contact" target="_blank" class="underline font-medium">contact us</a>.
</p>
</div>
</div>
</div>
@@ -1779,9 +1866,16 @@
appointment reminders via email and/or SMS.
</p>
<p class="mt-2 text-xs text-gray-500">
<strong>Cancellation Policy:</strong> Free cancellation up to 24 hours before your appointment.
Cancellations within 24 hours may incur a deposit penalty.
<strong>Cancellation Policy:</strong> If paying early a free full refund will be given if cancelled more than 72 hours before your appointment. Between 24-72 hours, up to 50% of the booking total may be retained as a protected deposit. Cancellations within 24 hours are non-refundable and count as a no-show against your account.
</p>
<p class="mt-1 text-xs text-gray-500">
<PolicyPopover>
{#snippet trigger()}
<span class="underline">Read full cancellation policy →</span>
{/snippet}
</PolicyPopover>
</p>
</div>
</Card.Content>
<Card.Footer class="flex justify-between">
@@ -1830,176 +1924,87 @@
showCustomer={true}
/>
{#if !showPaymentForm}
<div class="rounded-lg border border-amber-200 bg-amber-50 p-6">
<h3 class="mb-2 text-lg font-semibold text-amber-800">Deposit Required</h3>
<p class="mb-4 text-amber-700">
Due to your booking being within 24 hours, a deposit is required.
</p>
<div class="flex flex-wrap gap-3">
<Button
onclick={() => {
showPaymentForm = true;
}}
class="bg-primary text-primary-foreground"
>
Pay Deposit Now
</Button>
<Button variant="outline" onclick={nextStep}>Pay at Appointment</Button>
</div>
</div>
{:else}
<div class="rounded-lg border border-gray-200 bg-white p-6">
<h3 class="mb-4 text-xl font-semibold">Pay Deposit</h3>
<div class="rounded-lg border border-gray-200 bg-white p-6">
<h3 class="mb-4 text-xl font-semibold">Pay Deposit</h3>
{#if authStore.isAuthenticated}
{#if paymentMethodsLoading}
<div class="py-4 text-center text-gray-500">Loading payment methods...</div>
{:else if paymentMethods.length > 0}
<div class="mb-6">
<h4 class="mb-3 text-sm font-medium text-gray-700">Saved Cards</h4>
<div class="space-y-3">
{#each paymentMethods as method (method.id)}
<div
class="flex items-center justify-between rounded-lg border border-gray-200 p-3 {selectedPaymentMethod ===
method.id
? 'border-primary bg-primary/5'
: ''}"
>
<div class="flex items-center gap-3">
<div
class="flex h-10 w-14 items-center justify-center rounded bg-gray-100 text-xs font-medium"
>
{method.brand}
</div>
<div class="text-sm">
<span class="font-mono">**** {method.last4}</span>
<span class="ml-2 text-gray-500">
{formatCardExpiry(method.expiry_month, method.expiry_year)}
</span>
</div>
</div>
<Button
size="sm"
variant={selectedPaymentMethod === method.id ? 'default' : 'outline'}
onclick={() => {
selectedPaymentMethod = method.id;
showNewCardForm = false;
}}
{#if authStore.isAuthenticated}
{#if paymentMethodsLoading}
<div class="py-4 text-center text-gray-500">Loading payment methods...</div>
{:else if paymentMethods.length > 0}
<div class="mb-6">
<h4 class="mb-3 text-sm font-medium text-gray-700">Saved Cards</h4>
<div class="space-y-3">
{#each paymentMethods as method (method.id)}
<div
class="flex items-center justify-between rounded-lg border border-gray-200 p-3 {selectedPaymentMethod ===
method.id
? 'border-primary bg-primary/5'
: ''}"
>
<div class="flex items-center gap-3">
<div
class="flex h-10 w-14 items-center justify-center rounded bg-gray-100 text-xs font-medium"
>
{selectedPaymentMethod === method.id ? 'Selected' : 'Use this card'}
</Button>
{method.brand}
</div>
<div class="text-sm">
<span class="font-mono">**** {method.last4}</span>
<span class="ml-2 text-gray-500">
{formatCardExpiry(method.expiry_month, method.expiry_year)}
</span>
</div>
</div>
{/each}
</div>
</div>
{/if}
{#if !showNewCardForm}
<Button
variant="outline"
class="mb-6"
onclick={() => {
showNewCardForm = true;
selectedPaymentMethod = null;
}}
>
+ Add new card
</Button>
{/if}
{/if}
{#if showNewCardForm || !authStore.isAuthenticated}
<div class="mb-6 rounded-lg border border-gray-100 bg-gray-50 p-4">
<h4 class="mb-4 text-sm font-medium text-gray-700">Card Details</h4>
<div class="space-y-4">
<div class="space-y-2">
<Label for="cardNumber">Card Number</Label>
<Input
id="cardNumber"
type="text"
inputmode="numeric"
value={newCardNumber}
oninput={(e) =>
(newCardNumber = formatDepositCardNumber(
(e.target as HTMLInputElement).value
))}
placeholder="1234 5678 9012 3456"
maxlength={19}
/>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="cardExpiry">Expiry (MM/YY)</Label>
<Input
id="cardExpiry"
type="text"
inputmode="numeric"
value={newCardExpiry}
oninput={(e) =>
(newCardExpiry = formatDepositExpiry(
(e.target as HTMLInputElement).value
))}
placeholder="MM/YY"
maxlength={5}
/>
<Button
size="sm"
variant={selectedPaymentMethod === method.id ? 'default' : 'outline'}
onclick={() => {
selectedPaymentMethod = method.id;
showNewCardForm = false;
}}
>
{selectedPaymentMethod === method.id ? 'Selected' : 'Use this card'}
</Button>
</div>
<div class="space-y-2">
<Label for="cardCVC">CVC</Label>
<Input
id="cardCVC"
type="text"
inputmode="numeric"
bind:value={newCardCVC}
placeholder="123"
maxlength={4}
/>
</div>
</div>
{#if authStore.isAuthenticated}
<div class="flex items-center gap-2">
<Checkbox id="saveCard" bind:checked={saveCardForFuture} />
<Label for="saveCard" class="text-sm font-normal">
Save card for next time
</Label>
</div>
{/if}
{/each}
</div>
</div>
{/if}
<div class="flex items-center justify-between border-t pt-4">
{#if !showNewCardForm}
<Button
variant="ghost"
variant="outline"
class="mb-6"
onclick={() => {
showPaymentForm = false;
showNewCardForm = true;
selectedPaymentMethod = null;
showNewCardForm = false;
}}
>
Cancel
+ Add new card
</Button>
<Button
disabled={isProcessingPayment || !depositCardFormValid}
onclick={() => processPayment(calculateDepositAmount())}
class="bg-primary text-primary-foreground"
>
{isProcessingPayment ? 'Processing...' : `Pay £${calculateDepositAmount()}`}
</Button>
</div>
{/if}
{/if}
{#if showNewCardForm || !authStore.isAuthenticated}
<CardInput
bind:cardNumber={newCardNumber}
bind:cardExpiry={newCardExpiry}
bind:cardCVC={newCardCVC}
disabled={isProcessingPayment}
/>
{/if}
<div class="flex items-center justify-between border-t pt-4">
<Button variant="ghost" onclick={prevStep}>Back</Button>
<Button
disabled={isProcessingPayment || !depositCardFormValid}
onclick={() => processPayment(calculateDepositAmount())}
class="bg-primary text-primary-foreground"
>
{isProcessingPayment ? 'Processing...' : `Pay Deposit £${calculateDepositAmount()}`}
</Button>
</div>
{/if}
</div>
</Card.Content>
<Card.Footer class="flex justify-between">
<Button variant="outline" onclick={prevStep}>Back</Button>
<Button
disabled={isSubmitting}
onclick={nextStep}
class="bg-primary text-primary-foreground"
>
{isSubmitting ? 'Processing...' : 'Continue'}
</Button>
</Card.Footer>
</Card.Root>
{/if}
@@ -2021,35 +2026,52 @@
})}
<Card.Root class="border-emerald-200">
<Card.Header class="text-center">
<div
class="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full {isRequested
? 'bg-amber-100'
: 'bg-emerald-100'}"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 {isRequested ? 'text-amber-600' : 'text-emerald-600'}"
viewBox="0 0 20 20"
fill="currentColor"
{#if isProcessingPayment && paymentAttempted}
<Card.Header class="text-center">
<div
class="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-amber-100"
>
<path
fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"
/>
</svg>
</div>
<Card.Title class="text-2xl font-bold"
>{isRequested ? 'Booking Requested' : 'Booking Confirmed'}</Card.Title
>
<Card.Description class="mt-2 text-base">
{isRequested
? "Your booking has been submitted and is awaiting approval. We'll notify you once it's confirmed."
: 'Your appointment has been booked successfully.'}
</Card.Description>
</Card.Header>
<div
class="h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-amber-600"
></div>
</div>
<Card.Title class="text-2xl font-bold">Processing Payment</Card.Title>
<Card.Description class="mt-2 text-base">
Your booking is confirmed. We're processing your payment — this should only take a moment.
</Card.Description>
</Card.Header>
{:else}
<Card.Header class="text-center">
<div
class="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full {isRequested
? 'bg-amber-100'
: 'bg-emerald-100'}"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 {isRequested ? 'text-amber-600' : 'text-emerald-600'}"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"
/>
</svg>
</div>
<Card.Title class="text-2xl font-bold"
>{isRequested ? 'Booking Requested' : 'Booking Confirmed'}</Card.Title
>
<Card.Description class="mt-2 text-base">
{isRequested
? "Your booking has been submitted and is awaiting approval. We'll notify you once it's confirmed."
: 'Your appointment has been booked successfully.'}
</Card.Description>
</Card.Header>
{/if}
<Card.Content class="space-y-6">
{#if !(isProcessingPayment && paymentAttempted)}
<div class="rounded-lg border border-gray-200 bg-gray-50 p-6">
<div class="mb-4 flex items-center justify-between">
<span class="text-sm font-medium text-gray-500">Confirmation Number</span>
@@ -2095,10 +2117,23 @@
</div>
{/each}
<div class="border-t pt-2">
<div class="flex justify-between font-semibold">
<span>Total (estimated)</span>
<span>£{getTotalPrice()}</span>
</div>
{#if discountPreview?.eligible}
{#each discountPreview.discounts as d}
<div class="flex justify-between text-sm text-gray-600">
<span>{d.name}</span>
<span>{d.amount.toFixed(2)}</span>
</div>
{/each}
<div class="flex justify-between font-semibold text-emerald-700">
<span>Estimated Total After Discount</span>
<span>£{discountPreview.discounted_total.toFixed(2)}</span>
</div>
{:else}
<div class="flex justify-between font-semibold">
<span>Total (estimated)</span>
<span>£{getTotalPrice()}</span>
</div>
{/if}
</div>
</div>
</div>
@@ -2114,20 +2149,50 @@
</div>
{/if}
{#if !calculateDepositRequired() && authStore.isAuthenticated && authStore.currentUser?.role !== 'admin' && authStore.currentUser?.role !== 'guest'}
<div class="rounded-lg border border-gray-200 bg-gray-50 p-6 text-center">
<h3 class="mb-2 text-lg font-semibold">Pay on the Day</h3>
<p class="mb-4 text-gray-600">
You can pay when you arrive, or pay ahead of time to speed things up.
</p>
<Button
onclick={() => (showPayEarlyModal = true)}
class="bg-emerald-600 text-white hover:bg-emerald-700"
>
Pay Early
</Button>
</div>
{#if authStore.isAuthenticated && authStore.currentUser?.role !== 'admin' && authStore.currentUser?.role !== 'guest'}
{#if depositPaid}
<div class="rounded-lg border border-emerald-200 bg-emerald-50 p-6 text-center">
<div class="mb-2 inline-flex h-10 w-10 items-center justify-center rounded-full bg-emerald-100">
<svg class="h-5 w-5 text-emerald-600" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>
</div>
<h3 class="text-lg font-semibold text-emerald-800">Deposit Paid</h3>
<p class="mt-1 text-emerald-700">
Your deposit of <strong>£{calculateDepositAmount().toFixed(2)}</strong> has been paid
successfully. See you at your appointment!
</p>
</div>
{:else if depositRequired}
<div class="rounded-lg border border-amber-200 bg-amber-50 p-6 text-center">
<h3 class="mb-2 text-lg font-semibold text-amber-800">Deposit Not Paid</h3>
<p class="mb-4 text-amber-700">
Your booking is confirmed but the deposit of <strong>£{calculateDepositAmount().toFixed(2)}</strong>
was not paid. If the deposit remains unpaid within 24 hours of your appointment,
the slot may be released and the booking could be cancelled or rebooked by someone else.
</p>
<p class="mb-4 text-xs text-amber-600">
<PolicyPopover>
{#snippet trigger()}
<span class="underline">Read our cancellation policy →</span>
{/snippet}
</PolicyPopover>
</p>
<Button onclick={() => (showPayEarlyModal = true)} class="bg-amber-600 text-white hover:bg-amber-700">
Pay Deposit Now
</Button>
</div>
{:else}
<div class="rounded-lg border border-gray-200 bg-gray-50 p-6 text-center">
<h3 class="mb-2 text-lg font-semibold">Pay on the Day</h3>
<p class="mb-4 text-gray-600">
You can pay when you arrive, or pay ahead of time to speed things up.
</p>
<Button onclick={() => (showPayEarlyModal = true)} class="bg-emerald-600 text-white hover:bg-emerald-700">
Pay Early
</Button>
</div>
{/if}
{/if}
{/if}
</Card.Content>
<Card.Footer class="flex justify-center">
<Button
@@ -2153,27 +2218,27 @@
{/if}
{#if showPayEarlyModal && confirmedBooking}
{@const booking = confirmedBooking}
{@const bk = confirmedBooking}
<UserPaymentModal
booking={{
id: booking.id,
status: booking.status as BookingStatus,
start_time: booking.start_time,
notes: booking.notes,
id: bk.id,
status: bk.status as BookingStatus,
start_time: bk.start_time,
notes: bk.notes,
services: selectedServices.map((s) => ({
booking_id: booking.id,
booking_id: bk.id,
service_id: s.id,
service_name: s.name,
price: s.price,
duration_minutes: s.duration_minutes
})) as BookingService[],
total_amount: getTotalPrice(),
amount_paid: 0,
amount_due: getTotalPrice(),
deposit_required: false,
deposit_paid: true,
payments: [],
duration_minutes: getTotalDuration(),
total_amount: bk.total_amount,
amount_paid: bk.amount_paid,
amount_due: bk.amount_due,
deposit_required: bk.deposit_required,
deposit_paid: bk.deposit_paid,
payments: bk.payments,
duration_minutes: bk.duration_minutes,
created_at: new SvelteDate().toISOString(),
updated_at: new SvelteDate().toISOString()
}}
@@ -2181,8 +2246,7 @@
onComplete={() => {
showPayEarlyModal = false;
}}
canSaveCards={authStore.currentUser?.role === 'verified_email' ||
authStore.currentUser?.role === 'affiliate'}
canSaveCards={authStore.isAuthenticated}
/>
{/if}
</div>