From ad0ad253ad97225db850ace16ac47fd7b31ab4d2 Mon Sep 17 00:00:00 2001 From: Stephen Adamson Date: Thu, 25 Jun 2026 13:26:26 +0100 Subject: [PATCH] style: apply prettier formatting to frontend --- frontend/src/app.css | 4 +- .../account/EditRequestModal.svelte | 63 +- .../account/UserBookingModal.svelte | 275 +-- .../lib/components/admin/ApprovalModal.svelte | 47 +- .../admin/BookingCreateModal.svelte | 16 +- .../lib/components/admin/BookingModal.svelte | 197 ++- .../lib/components/admin/BookingsCard.svelte | 8 +- .../components/admin/BusinessSettings.svelte | 116 +- .../admin/CustomServicesManagement.svelte | 133 +- .../admin/DiscountsManagement.svelte | 6 +- .../components/admin/EditBookingModal.svelte | 20 +- .../components/admin/EditRequestModal.svelte | 228 +-- .../admin/GiftCardsManagement.svelte | 633 ++++--- .../lib/components/admin/ImageUpload.svelte | 111 +- .../components/admin/RescheduleModal.svelte | 55 +- .../admin/ServicesManagement.svelte | 10 +- .../lib/components/admin/TillPurchases.svelte | 96 +- .../lib/components/admin/TimeBlockers.svelte | 16 +- .../src/lib/components/admin/UserModal.svelte | 50 +- .../src/lib/components/admin/UsersCard.svelte | 4 +- .../lib/components/admin/WalkInBooking.svelte | 4 +- .../components/admin/WalkInCreateModal.svelte | 130 +- .../lib/components/booking/BookingFlow.svelte | 1478 +++++++++-------- .../booking/SelectedTimeSummary.svelte | 12 +- .../components/booking/TimeSlotPicker.svelte | 2 +- .../src/lib/components/layout/Calendar.svelte | 2 +- .../lib/components/payments/CardInput.svelte | 2 +- .../components/payments/PaymentModal.svelte | 176 +- .../payments/TillPaymentModal.svelte | 96 +- .../payments/UserPaymentModal.svelte | 165 +- .../today/CurrentAppointment.svelte | 80 +- .../components/today/PendingApprovals.svelte | 16 +- .../lib/components/today/TodayCalendar.svelte | 33 +- .../src/lib/components/ui/CharCounter.svelte | 46 +- .../src/lib/components/ui/ImageVariant.svelte | 14 +- .../src/lib/components/ui/StatusBadge.svelte | 4 +- .../ui/dialog/dialog-content.svelte | 6 +- .../lib/components/ui/form/form-button.svelte | 2 +- frontend/src/lib/components/ui/map/Map.svelte | 102 +- .../src/lib/components/ui/map/MapArc.svelte | 82 +- .../components/ui/map/MapClusterLayer.svelte | 130 +- .../lib/components/ui/map/MapControls.svelte | 68 +- .../lib/components/ui/map/MapMarker.svelte | 78 +- .../src/lib/components/ui/map/MapPopup.svelte | 44 +- .../src/lib/components/ui/map/MapRoute.svelte | 76 +- .../components/ui/map/MarkerContent.svelte | 12 +- .../lib/components/ui/map/MarkerLabel.svelte | 16 +- .../lib/components/ui/map/MarkerPopup.svelte | 30 +- .../components/ui/map/MarkerTooltip.svelte | 32 +- frontend/src/lib/components/ui/map/index.ts | 26 +- frontend/src/lib/components/ui/map/theme.ts | 4 +- .../ui/phone-input/PhoneInput.svelte | 14 +- .../lib/components/ui/policyPopover.svelte | 4 +- frontend/src/lib/constants/policy.ts | 18 +- frontend/src/lib/hooks/use-map.svelte.ts | 8 +- frontend/src/lib/stores/savedCards.svelte.ts | 90 +- frontend/src/lib/utils/format.ts | 10 +- frontend/src/lib/utils/phone.ts | 44 +- frontend/src/lib/utils/timeSlots.ts | 37 +- frontend/src/routes/account/+page.svelte | 286 ++-- frontend/src/routes/admin/+page.svelte | 20 +- .../routes/admin/notifications/+page.svelte | 27 +- .../src/routes/admin/schedule/+page.svelte | 59 +- frontend/src/routes/book/canvas.md | 385 +++-- .../booking-confirmed/[id]/+page.svelte | 4 +- .../routes/cancellation-policy/+page.svelte | 30 +- frontend/src/routes/contact/+page.svelte | 55 +- frontend/src/routes/demo/+page.svelte | 4 +- frontend/src/routes/gdpr/+page.svelte | 91 +- frontend/src/routes/login/+page.svelte | 90 +- frontend/src/routes/portfolio/+page.svelte | 146 +- frontend/src/routes/prices/+page.svelte | 14 +- frontend/src/routes/schedule/+page.svelte | 41 +- frontend/src/routes/today/+page.svelte | 26 +- 74 files changed, 3927 insertions(+), 2632 deletions(-) diff --git a/frontend/src/app.css b/frontend/src/app.css index 3fafd65..d71b0b9 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -2,7 +2,7 @@ @import 'tw-animate-css'; -@import "shadcn-svelte/tailwind.css"; +@import 'shadcn-svelte/tailwind.css'; @custom-variant dark (&:is(.dark *)); @@ -122,7 +122,7 @@ @apply bg-background text-foreground; } .maplibregl-popup-content { - @apply bg-transparent! shadow-none! p-0! rounded-none!; + @apply rounded-none! bg-transparent! p-0! shadow-none!; } .maplibregl-popup-tip { @apply hidden!; diff --git a/frontend/src/lib/components/account/EditRequestModal.svelte b/frontend/src/lib/components/account/EditRequestModal.svelte index 1043dcd..48a07c0 100644 --- a/frontend/src/lib/components/account/EditRequestModal.svelte +++ b/frontend/src/lib/components/account/EditRequestModal.svelte @@ -9,7 +9,13 @@ import * as Label from '$lib/components/ui/label'; import DatePicker from '$lib/components/booking/DatePicker.svelte'; import PolicyPopover from '$lib/components/ui/policyPopover.svelte'; - import type { Booking, BookingDiscount, Service, WorkingHoursDay, AvailableHoursDay } from '$lib/types/booking'; + import type { + Booking, + BookingDiscount, + Service, + WorkingHoursDay, + AvailableHoursDay + } from '$lib/types/booking'; import { extractBookedSlots, getLunchProtectionForSlots, @@ -70,7 +76,11 @@ // ─── Date constants ───────────────────────────────────── const todayCalendarDate = getLondonTodayCalendarDate(); const minDate = todayCalendarDate; - const maxDate = new SvelteDate(todayCalendarDate.year, todayCalendarDate.month - 1, todayCalendarDate.day); + const maxDate = new SvelteDate( + todayCalendarDate.year, + todayCalendarDate.month - 1, + todayCalendarDate.day + ); maxDate.setMonth(todayCalendarDate.month - 1 + 6); const maxCalendarDate = new CalendarDate( maxDate.getFullYear(), @@ -82,9 +92,7 @@ let hoursUntilAppointment = $derived( (new SvelteDate(booking.start_time).getTime() - new SvelteDate().getTime()) / (1000 * 60 * 60) ); - let hasPayments = $derived( - (booking.amount_paid ?? 0) > 0 - ); + let hasPayments = $derived((booking.amount_paid ?? 0) > 0); let noticePeriodBlocked = $derived( hasPayments ? hoursUntilAppointment < 72 : hoursUntilAppointment < 24 ); @@ -218,7 +226,12 @@ if (isToday) { const now = new Date(); - const londonTime = now.toLocaleTimeString('en-GB', { timeZone: 'Europe/London', hour: '2-digit', minute: '2-digit', hour12: false }); + const londonTime = now.toLocaleTimeString('en-GB', { + timeZone: 'Europe/London', + hour: '2-digit', + minute: '2-digit', + hour12: false + }); const [londonHours, londonMinutes] = londonTime.split(':').map(Number); const currentMin = londonHours * 60 + londonMinutes; startMin = Math.max(startMin, Math.ceil((currentMin + 60) / 15) * 15); @@ -268,7 +281,12 @@ const isToday = date.compare(todayCal) === 0; if (isToday) { const now = new Date(); - const londonTime = now.toLocaleTimeString('en-GB', { timeZone: 'Europe/London', hour: '2-digit', minute: '2-digit', hour12: false }); + const londonTime = now.toLocaleTimeString('en-GB', { + timeZone: 'Europe/London', + hour: '2-digit', + minute: '2-digit', + hour12: false + }); const [londonHours, londonMinutes] = londonTime.split(':').map(Number); const currentMin = londonHours * 60 + londonMinutes; startMin = Math.max(startMin, Math.ceil((currentMin + 60) / 15) * 15); @@ -466,11 +484,7 @@ } const tomorrowCal = getLondonTodayCalendarDate(); - newDate = new CalendarDate( - tomorrowCal.year, - tomorrowCal.month, - tomorrowCal.day + 1 - ); + newDate = new CalendarDate(tomorrowCal.year, tomorrowCal.month, tomorrowCal.day + 1); if (!userNavigatedCalendar) { placeholderDate = new CalendarDate(tomorrowCal.year, tomorrowCal.month, 1); } @@ -774,8 +788,13 @@

Cannot Reschedule Online

{noticeBlockedMessage}

- Contact us to discuss options, - or + Contact us to discuss + options, or + and rebook — note that cancellation fees may apply based on our {#snippet trigger()} @@ -788,8 +807,8 @@

Rescheduling Within 24h

- Rescheduling within 24h counts as a no-show towards your deposit obligations. Two no-shows - within 6 months will require deposits on future bookings. + Rescheduling within 24h counts as a no-show towards your deposit obligations. Two + no-shows within 6 months will require deposits on future bookings. {#snippet trigger()} Full policy @@ -800,14 +819,12 @@ {/if} {#if booking.discounts && booking.discounts.length > 0} -

+

Discounts Applied to This Booking

- 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. + 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.

{/if} @@ -914,7 +931,7 @@
{:else}
{newDate.toDate(getLocalTimeZone()).toLocaleDateString('en-GB', { diff --git a/frontend/src/lib/components/account/UserBookingModal.svelte b/frontend/src/lib/components/account/UserBookingModal.svelte index d2ac66a..2c47a89 100644 --- a/frontend/src/lib/components/account/UserBookingModal.svelte +++ b/frontend/src/lib/components/account/UserBookingModal.svelte @@ -1,18 +1,18 @@ - + Approve Booking @@ -413,7 +415,11 @@ import { parseWallClockDate } from '$lib/utils/timeSlots'; {#if oldest?.id === ob.id} {/if} - {formatUserName(ob.user?.full_name || 'Unknown', ob.user?.previous_first_name, ob.user?.previous_last_name)} + {formatUserName( + ob.user?.full_name || 'Unknown', + ob.user?.previous_first_name, + ob.user?.previous_last_name + )}
{parseWallClockDate(ob.start_time).toLocaleDateString('en-GB', { @@ -497,7 +503,13 @@ import { parseWallClockDate } from '$lib/utils/timeSlots';
-
{formatUserName(booking.user?.full_name || '—', booking.user?.previous_first_name, booking.user?.previous_last_name)}
+
+ {formatUserName( + booking.user?.full_name || '—', + booking.user?.previous_first_name, + booking.user?.previous_last_name + )} +
@@ -517,9 +529,8 @@ import { parseWallClockDate } from '$lib/utils/timeSlots';
Phone
{#if booking.user?.phone} - {booking.user.phone}{booking.user.phone} {:else}
@@ -627,7 +638,8 @@ import { parseWallClockDate } from '$lib/utils/timeSlots'; min="1" step="1" value={serviceOverrides[service.service_id].duration} - oninput={(e) => handleDurationInput(service.service_id, e.currentTarget.value)} + oninput={(e) => + handleDurationInput(service.service_id, e.currentTarget.value)} class="no-spin w-full" placeholder={service.duration_minutes?.toString() || '60'} /> @@ -653,7 +665,11 @@ import { parseWallClockDate } from '$lib/utils/timeSlots';

Applied Discounts

{#each booking.discounts as d}

- - {d.discount_source === 'loyalty' ? 'Loyalty Stamp Card' : d.discount_source === 'referral' ? 'Referral Discount' : d.campaign_name || 'Promo Campaign'} + - {d.discount_source === 'loyalty' + ? 'Loyalty Stamp Card' + : d.discount_source === 'referral' + ? 'Referral Discount' + : d.campaign_name || 'Promo Campaign'} ({d.discount_percent}% off): -£{d.discount_amount.toFixed(2)}

{/each} @@ -704,4 +720,3 @@ import { parseWallClockDate } from '$lib/utils/timeSlots'; - diff --git a/frontend/src/lib/components/admin/BookingCreateModal.svelte b/frontend/src/lib/components/admin/BookingCreateModal.svelte index 88ec8a5..df5636b 100644 --- a/frontend/src/lib/components/admin/BookingCreateModal.svelte +++ b/frontend/src/lib/components/admin/BookingCreateModal.svelte @@ -676,9 +676,7 @@ const startTimeISO = formatLocalDateTime(localDate); const serviceIds = selectedServices.filter((s) => !s.is_custom).map((s) => s.id); - const customServiceIds = selectedServices - .filter((s) => s.is_custom) - .map((s) => s.id); + const customServiceIds = selectedServices.filter((s) => s.is_custom).map((s) => s.id); // Build service overrides payload const overrides = []; @@ -1007,7 +1005,13 @@ start_time: string; service_ids: string[]; custom_service_ids: string[]; - service_overrides: Array<{ service_id: string; override_price: number | null; override_duration_minutes: number | null }> | undefined; + service_overrides: + | Array<{ + service_id: string; + override_price: number | null; + override_duration_minutes: number | null; + }> + | undefined; notes: string | null; out_of_hours: boolean; } = { @@ -1657,7 +1661,9 @@ > - Slot reserved until {parseWallClockDate(reservationExpiresAt.toISOString()).toLocaleTimeString([], { + Slot reserved until {parseWallClockDate( + reservationExpiresAt.toISOString() + ).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} diff --git a/frontend/src/lib/components/admin/BookingModal.svelte b/frontend/src/lib/components/admin/BookingModal.svelte index af80177..262f2c8 100644 --- a/frontend/src/lib/components/admin/BookingModal.svelte +++ b/frontend/src/lib/components/admin/BookingModal.svelte @@ -45,11 +45,14 @@ let balanceDue = $derived(selectedBooking ? computeBalanceDue(selectedBooking) : 0); let hoursUntilAppt = $derived( selectedBooking - ? (new SvelteDate(selectedBooking.start_time).getTime() - new SvelteDate().getTime()) / (1000 * 60 * 60) + ? (new SvelteDate(selectedBooking.start_time).getTime() - new SvelteDate().getTime()) / + (1000 * 60 * 60) : Infinity ); let protectedDeposit = $derived( - selectedBooking ? Math.min(totalPaid, selectedBooking.total_amount * POLICY.PROTECTED_DEPOSIT_MAX_PCT) : 0 + selectedBooking + ? Math.min(totalPaid, selectedBooking.total_amount * POLICY.PROTECTED_DEPOSIT_MAX_PCT) + : 0 ); let estimatedRefund = $derived( forgiveFeesCancel @@ -216,14 +219,21 @@ } } - function getPaymentName(payment: Payment, index: number, payments: Payment[] | undefined, discounts: BookingDiscount[] | undefined): string { + function getPaymentName( + payment: Payment, + index: number, + payments: Payment[] | undefined, + discounts: BookingDiscount[] | undefined + ): string { if (payment.payment_method === 'online_square') return 'Online Card'; if (payment.payment_method === 'in_person_card') return 'Card Machine'; if (payment.payment_method === 'cash') return 'Cash'; if (payment.payment_method === 'giftcard') return 'Gift Card'; if (payment.payment_method === 'discount') { - const discountPaymentsBefore = (payments ?? []).slice(0, index).filter(p => p.payment_method === 'discount').length; - const discountList = (discounts ?? []).filter(d => d.discount_amount > 0.01); + const discountPaymentsBefore = (payments ?? []) + .slice(0, index) + .filter((p) => p.payment_method === 'discount').length; + const discountList = (discounts ?? []).filter((d) => d.discount_amount > 0.01); if (discountList[discountPaymentsBefore]) { const d = discountList[discountPaymentsBefore]; if (d.discount_source === 'loyalty') return 'Loyalty Stamp Card (10% Off)'; @@ -232,7 +242,9 @@ } return 'Discount'; } - return (payment.payment_method as string).replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); + return (payment.payment_method as string) + .replace(/_/g, ' ') + .replace(/\b\w/g, (c) => c.toUpperCase()); } $effect(() => { @@ -323,8 +335,17 @@ - - + + Out-of-hours @@ -418,12 +439,22 @@ {#if selectedBooking.user?.profile_pic_url} {formatUserName(selectedBooking.user.full_name, {/if}
-
{formatUserName(selectedBooking.user?.full_name || '—', selectedBooking.user?.previous_first_name, selectedBooking.user?.previous_last_name)}
+
+ {formatUserName( + selectedBooking.user?.full_name || '—', + selectedBooking.user?.previous_first_name, + selectedBooking.user?.previous_last_name + )} +
{#if selectedBooking.user?.date_of_birth}
{calculateAge(selectedBooking.user.date_of_birth)} years old @@ -541,7 +572,9 @@
{#if selectedBooking.discounts && selectedBooking.discounts.length > 0} -
+
{#each selectedBooking.discounts as d}
@@ -562,14 +595,22 @@
Net Total - £{(selectedBooking.total_amount - selectedBooking.discounts.reduce((sum, d) => sum + d.discount_amount, 0)).toFixed(2)} + £{( + selectedBooking.total_amount - + selectedBooking.discounts.reduce((sum, d) => sum + d.discount_amount, 0) + ).toFixed(2)}
{/if}
Amount Paid (Card/Cash) - £{(selectedBooking.payments ?? []).filter(p => p.payment_method !== 'discount' && p.status === 'completed').reduce((sum, p) => sum + p.amount, 0).toFixed(2)} + £{(selectedBooking.payments ?? []) + .filter((p) => p.payment_method !== 'discount' && p.status === 'completed') + .reduce((sum, p) => sum + p.amount, 0) + .toFixed(2)}
@@ -583,9 +624,7 @@
Balance Due £{balanceDue.toFixed(2)} @@ -606,7 +645,12 @@
- {getPaymentName(payment, index, selectedBooking.payments, selectedBooking.discounts)} + {getPaymentName( + payment, + index, + selectedBooking.payments, + selectedBooking.discounts + )}
Net: £{payment.net_amount?.toFixed(2) || '0.00'}
- VAT ({payment.vat_rate || 0}%): £{payment.vat_amount?.toFixed( - 2 - ) || '0.00'} + VAT ({payment.vat_rate || 0}%): £{payment.vat_amount?.toFixed(2) || + '0.00'}
{/if} @@ -668,7 +711,9 @@
Refund - + {refund.status}
@@ -751,71 +796,131 @@ {@const apptHours = Math.round(hoursUntilAppt)} {#if totalPaid > 0}

- This booking has payments totalling £{totalPaid.toFixed(2)}. + This booking has payments totalling £{totalPaid.toFixed(2)}.

{:else}

Are you sure you want to cancel this booking?

{/if} {#if totalPaid > 0} -
+

- {apptHours > POLICY.FULL_REFUND_THRESHOLD_HOURS ? `Full Refund — Over ${POLICY.FULL_REFUND_THRESHOLD_HOURS}h notice` : apptHours >= POLICY.PARTIAL_REFUND_THRESHOLD_HOURS ? `Partial Refund — ${apptHours}h notice` : `No Refund — Under ${POLICY.PARTIAL_REFUND_THRESHOLD_HOURS}h notice`} + {apptHours > POLICY.FULL_REFUND_THRESHOLD_HOURS + ? `Full Refund — Over ${POLICY.FULL_REFUND_THRESHOLD_HOURS}h notice` + : apptHours >= POLICY.PARTIAL_REFUND_THRESHOLD_HOURS + ? `Partial Refund — ${apptHours}h notice` + : `No Refund — Under ${POLICY.PARTIAL_REFUND_THRESHOLD_HOURS}h notice`}

- {apptHours > POLICY.FULL_REFUND_THRESHOLD_HOURS ? `You've given over ${POLICY.FULL_REFUND_THRESHOLD_HOURS} hours' notice. £${totalPaid.toFixed(2)} will be refunded in full.` : apptHours >= POLICY.PARTIAL_REFUND_THRESHOLD_HOURS ? `You've paid £${totalPaid.toFixed(2)}. Up to ${POLICY.PROTECTED_DEPOSIT_MAX_PCT * 100}% of the total (£${(selectedBooking.total_amount * POLICY.PROTECTED_DEPOSIT_MAX_PCT).toFixed(2)}) counts as protected deposit. £${protectedDeposit.toFixed(2)} will be retained and £${estimatedRefund.toFixed(2)} will be refunded.` : `Under ${POLICY.PARTIAL_REFUND_THRESHOLD_HOURS}h notice. The full £${totalPaid.toFixed(2)} is retained to cover the lost slot.`} + {apptHours > POLICY.FULL_REFUND_THRESHOLD_HOURS + ? `You've given over ${POLICY.FULL_REFUND_THRESHOLD_HOURS} hours' notice. £${totalPaid.toFixed(2)} will be refunded in full.` + : apptHours >= POLICY.PARTIAL_REFUND_THRESHOLD_HOURS + ? `You've paid £${totalPaid.toFixed(2)}. Up to ${POLICY.PROTECTED_DEPOSIT_MAX_PCT * 100}% of the total (£${(selectedBooking.total_amount * POLICY.PROTECTED_DEPOSIT_MAX_PCT).toFixed(2)}) counts as protected deposit. £${protectedDeposit.toFixed(2)} will be retained and £${estimatedRefund.toFixed(2)} will be refunded.` + : `Under ${POLICY.PARTIAL_REFUND_THRESHOLD_HOURS}h notice. The full £${totalPaid.toFixed(2)} is retained to cover the lost slot.`}

What this means

- {apptHours > 72 ? "Over 72 hours' notice means no deposit protection applies — a full refund is given regardless." : apptHours >= 24 ? "Between 24–72 hours' notice, up to 50% of the total (" + '£' + (selectedBooking.total_amount * 0.5).toFixed(2) + ") is treated as a protected deposit to cover the lost slot. The remaining balance above that is refunded." : "Under 24 hours' notice, the full amount paid (" + '£' + totalPaid.toFixed(2) + ") is retained. This also counts as a no-show toward deposit obligations."} + {apptHours > 72 + ? "Over 72 hours' notice means no deposit protection applies — a full refund is given regardless." + : apptHours >= 24 + ? "Between 24–72 hours' notice, up to 50% of the total (" + + '£' + + (selectedBooking.total_amount * 0.5).toFixed(2) + + ') is treated as a protected deposit to cover the lost slot. The remaining balance above that is refunded.' + : "Under 24 hours' notice, the full amount paid (" + + '£' + + totalPaid.toFixed(2) + + ') is retained. This also counts as a no-show toward deposit obligations.'}

-
{:else if apptHours < 24}
-

No-Show Warning — {apptHours}h before appointment

-

This booking has no payments but is under 24 hours' notice. Cancelling counts as a no-show toward deposit obligations.

+

+ No-Show Warning — {apptHours}h before appointment +

+

+ This booking has no payments but is under 24 hours' notice. Cancelling counts as a + no-show toward deposit obligations. +

What this means -

"This cancellation will count as a no-show toward your booking history. Two no-shows within 6 months would require a deposit on future bookings to secure your appointment."

+

+ "This cancellation will count as a no-show toward your booking history. Two + no-shows within 6 months would require a deposit on future bookings to secure your + appointment." +

-
{:else} -
+

Clean Cancellation

This booking has no payments. The booking will be removed cleanly.

diff --git a/frontend/src/lib/components/admin/BookingsCard.svelte b/frontend/src/lib/components/admin/BookingsCard.svelte index fcc7fc5..08bad37 100644 --- a/frontend/src/lib/components/admin/BookingsCard.svelte +++ b/frontend/src/lib/components/admin/BookingsCard.svelte @@ -284,7 +284,13 @@ {/if} {/if} - • {formatUserName(b.user?.full_name || 'Unknown User', b.user?.previous_first_name, b.user?.previous_last_name)} + • {formatUserName( + b.user?.full_name || 'Unknown User', + b.user?.previous_first_name, + b.user?.previous_last_name + )} - {formatServices(b.services)} diff --git a/frontend/src/lib/components/admin/BusinessSettings.svelte b/frontend/src/lib/components/admin/BusinessSettings.svelte index e9b0630..98afc76 100644 --- a/frontend/src/lib/components/admin/BusinessSettings.svelte +++ b/frontend/src/lib/components/admin/BusinessSettings.svelte @@ -121,7 +121,8 @@ const trimmed = String(value ?? '').trim(); if (!trimmed) return ''; // UK VAT numbers: GB + 9 digits (standard) or GB + 12 digits (branch) - if (trimmed.length !== 11 && trimmed.length !== 14) return 'Must be GB followed by 9 or 12 digits'; + if (trimmed.length !== 11 && trimmed.length !== 14) + return 'Must be GB followed by 9 or 12 digits'; if (!trimmed.startsWith('GB')) return 'Must start with GB'; const digits = trimmed.slice(2); if (!/^\d+$/.test(digits)) return 'Must contain only digits after GB'; @@ -156,13 +157,16 @@ function validateField(field: string, inputValue?: unknown) { const val = inputValue ?? form[field as keyof typeof form]; if (field === 'business_name') formErrors.business_name = validateBusinessName(val); - else if (field === 'business_address') formErrors.business_address = validateBusinessAddress(val); + else if (field === 'business_address') + formErrors.business_address = validateBusinessAddress(val); else if (field === 'business_phone') formErrors.business_phone = validatePhone(val); else if (field === 'business_email') formErrors.business_email = validateEmail(val); else if (field === 'website_url') formErrors.website_url = validateWebsiteUrl(val); - else if (field === 'vat_registration_number') formErrors.vat_registration_number = validateVatNumber(val); + else if (field === 'vat_registration_number') + formErrors.vat_registration_number = validateVatNumber(val); else if (field === 'default_vat_rate') formErrors.default_vat_rate = validateVatRate(val); - else if (field === 'gift_card_expiry_months') formErrors.gift_card_expiry_months = validateGiftCardExpiry(val); + else if (field === 'gift_card_expiry_months') + formErrors.gift_card_expiry_months = validateGiftCardExpiry(val); else if (field === 'voucher_type') formErrors.voucher_type = validateVoucherType(val); } @@ -192,7 +196,9 @@ let normalisedEmail = form.business_email?.trim() ?? null; if (normalisedEmail) normalisedEmail = normalizeEmail(normalisedEmail); - const normalisedPhone = form.business_phone ? toE164UK(form.business_phone) ?? normalisePhoneInput(form.business_phone) : null; + const normalisedPhone = form.business_phone + ? (toE164UK(form.business_phone) ?? normalisePhoneInput(form.business_phone)) + : null; const normalisedForm = { ...form, @@ -205,9 +211,16 @@ }; const fields: (keyof BusinessSettings)[] = [ - 'business_name', 'business_address', 'business_phone', 'business_email', - 'vat_registration_number', 'is_vat_registered', 'default_vat_rate', - 'website_url', 'gift_card_expiry_months', 'voucher_type' + 'business_name', + 'business_address', + 'business_phone', + 'business_email', + 'vat_registration_number', + 'is_vat_registered', + 'default_vat_rate', + 'website_url', + 'gift_card_expiry_months', + 'voucher_type' ]; for (const field of fields) { @@ -235,7 +248,7 @@ if (res.ok) { const wasVatEnabled = settings?.is_vat_registered; - const updated = await res.json() as BusinessSettings; + const updated = (await res.json()) as BusinessSettings; settings = updated; if (!wasVatEnabled && updated.is_vat_registered) { toast.success('VAT enabled — past payments and till sales updated retroactively'); @@ -268,9 +281,7 @@ Business details, VAT configuration, and gift card defaults.
- +
@@ -292,7 +303,9 @@
-

Business Information

+

+ Business Information +

Business Name @@ -332,15 +345,17 @@
-

VAT & Currency

+

+ VAT & Currency +

VAT Registered

{settings.is_vat_registered ? 'Registered' : 'Not Registered'} @@ -363,7 +378,9 @@

-

Gift Cards

+

+ Gift Cards +

Expiry Period @@ -373,9 +390,10 @@ Voucher Type

{settings.voucher_type === 'MPV' ? 'MPV (0% VAT)' : 'SPV (20% VAT)'} @@ -390,7 +408,7 @@ - + Edit Business Settings @@ -398,10 +416,20 @@ -

{ e.preventDefault(); saveSettings(); }}> + { + e.preventDefault(); + saveSettings(); + }} + >
-

Business Information

+

+ Business Information +

Business Name @@ -493,7 +521,11 @@
-

VAT & Currency

+

+ VAT & Currency +

@@ -502,18 +534,26 @@ Enable if your business is registered for UK VAT

- { - form.is_vat_registered = v; - if (!v) { - form.vat_registration_number = null; - form.default_vat_rate = 20.00; - formErrors.vat_registration_number = ''; - formErrors.default_vat_rate = ''; - } - }} /> + { + form.is_vat_registered = v; + if (!v) { + form.vat_registration_number = null; + form.default_vat_rate = 20.0; + formErrors.vat_registration_number = ''; + formErrors.default_vat_rate = ''; + } + }} + />
-
+
VAT Registration Number
-

Gift Card Configuration

+

+ Gift Card Configuration +

@@ -579,7 +623,7 @@ Voucher Type { if (e.key === 'Enter') { page = 1; fetchServices(); } }} + onkeydown={(e) => { + if (e.key === 'Enter') { + page = 1; + fetchServices(); + } + }} class="max-w-sm" /> - +
@@ -278,7 +314,9 @@ {service.name} {#if service.description} -
{service.description}
+
+ {service.description} +
{:else} {/if} @@ -286,14 +324,24 @@ £{service.price.toFixed(2)} {service.duration_minutes} min - {service.usage_count}×{service.last_used_at ? ` (last: ${new Date(service.last_used_at).toLocaleDateString('en-GB', { timeZone: 'Europe/London' })})` : ''} + {service.usage_count}×{service.last_used_at + ? ` (last: ${new Date(service.last_used_at).toLocaleDateString('en-GB', { timeZone: 'Europe/London' })})` + : ''}
- -
@@ -320,8 +368,18 @@
Duration: {service.duration_minutes} min
- - + +
@@ -334,8 +392,24 @@ Page {page} of {Math.ceil(total / perPage)} ({total} total)
- - + +
{/if} @@ -408,11 +482,17 @@ id="cs-duration" bind:value={newService.duration_minutes} onchange={() => validateField('duration_minutes')} - class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 {serviceErrors.duration_minutes ? 'border-red-500' : ''}" + class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none {serviceErrors.duration_minutes + ? 'border-red-500' + : ''}" > {#each durationOptions as mins (mins)} - + {/each} {#if serviceErrors.duration_minutes} @@ -443,7 +523,14 @@
- + diff --git a/frontend/src/lib/components/admin/DiscountsManagement.svelte b/frontend/src/lib/components/admin/DiscountsManagement.svelte index c7e3342..16bb53c 100644 --- a/frontend/src/lib/components/admin/DiscountsManagement.svelte +++ b/frontend/src/lib/components/admin/DiscountsManagement.svelte @@ -166,11 +166,7 @@ if (form.campaign_type === 'time_based') { if (!form.start_date) errors.start_date = 'Required'; if (!form.end_date) errors.end_date = 'Required'; - if ( - form.start_date && - form.end_date && - new Date(form.end_date) < new Date(form.start_date) - ) { + if (form.start_date && form.end_date && new Date(form.end_date) < new Date(form.start_date)) { errors.end_date = 'Must be after start'; } } diff --git a/frontend/src/lib/components/admin/EditBookingModal.svelte b/frontend/src/lib/components/admin/EditBookingModal.svelte index d1ecc16..7f8779e 100644 --- a/frontend/src/lib/components/admin/EditBookingModal.svelte +++ b/frontend/src/lib/components/admin/EditBookingModal.svelte @@ -1,10 +1,10 @@ @@ -58,38 +60,74 @@
- - - - -
{#if showGiftCardInput}
- £ + £ { if (e.key === 'Enter') addGiftCard(); }} + onkeydown={(e) => { + if (e.key === 'Enter') addGiftCard(); + }} />
- +
{:else} - {/if} @@ -100,16 +138,20 @@
{#if cart.length === 0} -

Tap items above to add them to the sale.

+

+ Tap items above to add them to the sale. +

{:else}
{#each cart as item (item.id)}
{item.label} - {formatCurrency(item.price)} each + {formatCurrency(item.price)} each
-
+
- {formatCurrency(item.price * item.qty)} + {formatCurrency(item.price * item.qty)} @@ -153,7 +203,9 @@ -

Payment flow and backend integration coming soon.

+

+ Payment flow and backend integration coming soon. +

{/if}
diff --git a/frontend/src/lib/components/admin/TimeBlockers.svelte b/frontend/src/lib/components/admin/TimeBlockers.svelte index 0cdc497..b1a9f34 100644 --- a/frontend/src/lib/components/admin/TimeBlockers.svelte +++ b/frontend/src/lib/components/admin/TimeBlockers.svelte @@ -719,7 +719,9 @@ size="sm" class="flex-1 text-xs" onclick={() => { - newStartDate = new Date().toLocaleDateString('en-CA', { timeZone: 'Europe/London' }); + newStartDate = new Date().toLocaleDateString('en-CA', { + timeZone: 'Europe/London' + }); onStartDateChange(); }} > @@ -731,7 +733,9 @@ size="sm" class="flex-1 text-xs" onclick={() => { - newStartDate = new Date(Date.now() + 86400000).toLocaleDateString('en-CA', { timeZone: 'Europe/London' }); + newStartDate = new Date(Date.now() + 86400000).toLocaleDateString('en-CA', { + timeZone: 'Europe/London' + }); onStartDateChange(); }} > @@ -860,7 +864,13 @@ {#each overlappingBookings as booking (booking.id)}
-
{formatUserName(booking.user?.full_name || 'Unknown', booking.user?.previous_first_name, booking.user?.previous_last_name)}
+
+ {formatUserName( + booking.user?.full_name || 'Unknown', + booking.user?.previous_first_name, + booking.user?.previous_last_name + )} +
{parseWallClockDate(booking.start_time).toLocaleTimeString('en-GB', { hour: 'numeric', diff --git a/frontend/src/lib/components/admin/UserModal.svelte b/frontend/src/lib/components/admin/UserModal.svelte index 5012557..556ca71 100644 --- a/frontend/src/lib/components/admin/UserModal.svelte +++ b/frontend/src/lib/components/admin/UserModal.svelte @@ -1,12 +1,12 @@ @@ -1538,7 +1560,6 @@
{#if !authReady} -
{#each [1, 2, 3, 4] as _}
@@ -1562,723 +1583,762 @@
- {:else} + - + {#if currentStep === 0} + + + Welcome + Log in for the best booking experience + + +

+ Guest checkout does not receive loyalty stamps or seasonal discounts. +

+
+ + +
+
+
+ {/if} - {#if currentStep === 0} - - - Welcome - Log in for the best booking experience - - -

- Guest checkout does not receive loyalty stamps or seasonal discounts. -

-
- - -
-
-
- {/if} + + {#if currentStep === 1} + + + Choose Your Services + Select one or more treatments for your appointment + + + + {#if authStore.isAuthenticated && userDepositsRequired > 0 && hasActiveBooking} +
+
+
+ + + +
+
+

One Booking at a Time

+

+ 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 contact us. +

+
+
+
+ {/if} + + - - {#if currentStep === 1} - - - Choose Your Services - Select one or more treatments for your appointment - - - - {#if authStore.isAuthenticated && userDepositsRequired > 0 && hasActiveBooking}
-
-
+

+ Need something different? Select the closest service and add a note, or contact us for + a bespoke treatment. +

+ + Arrange a custom booking → + +
+ + {#if selectedServices.length > 0} +
+

Selected Services

+
+ {#each selectedServices as service (service.id)} +
+ {service.name} + {service.duration_minutes} mins • £{service.price} +
+ {/each} + +
+ Estimated Duration: + {formattedTotalDuration} +
+
+ Total Cost: + £{getTotalPrice()}{#if vatRegistered} + incl. VAT{/if} +
+
+
+ {/if} + + + + + + {/if} + + + {#if currentStep === 2} + + + Choose Date & Time + + {selectedServices.map((s) => s.name).join(', ')} • {formattedTotalDuration} total • £{getTotalPrice()} + + + + + + {#if loadingWorkingHours} +
+

Loading available dates...

+
+ {:else} + { + selectedDate = newDate; + selectedTime = null; + }} + onPlaceholderChange={(newPlaceholder) => { + placeholder = newPlaceholder; + userNavigatedCalendar = true; + }} + /> + {/if} + + {#if loadingAvailableHours} +
+

Loading times...

+
+ {:else} + { + selectTimeWithValidation(time); + }} + /> + {/if} +
+
+
+ + +
+ {#if selectedDate && selectedTime} + Appointment for + + {selectedDate.toDate(getLocalTimeZone()).toLocaleDateString('en-US', { + weekday: 'long', + day: 'numeric', + month: 'short' + })} + +
at {selectedTime} + {:else} + Select a date and time + {/if} +
+ + + +
+ + + +
+
+
+ {/if} + + + {#if currentStep === 3} +
+

Almost There

+ {#if !authStore.isAuthenticated} +

Just a couple more details

+ {/if} +
+ + + Your Details + Please confirm your contact information + + + {#if reservationExpired} +
+

Reservation expired — please go back and select a new time

+
+ {:else} +
+

+ Your slot will be held for {reservationCountdown} — complete your booking before time + expires +

+
+ {/if} + + + + {#if !authStore.isAuthenticated} +

+ You are checking out as a guest, so you will miss out on a loyalty stamp and any + possible seasonal discounts. Please login for full membership benefits. +

+ +
+
+ + +
+
+ + +
+
+ + { + emailSuggestion = null; + debouncedEmailCheck(); + }} + onerrorchange={(err) => { + emailError = err; + }} + onblur={() => { + debouncedEmailCheck(); + }} + /> + {#if emailError} + {emailError} + {/if} + {#if emailChecking} + Checking... + {/if} +
+
+ + { + if (!err) debouncedEmailCheck(); + }} + /> +
+
+ + {#if emailSuggestion === 'login'} +

+ This email belongs to a registered user. Please + log in + instead to access your bookings and rewards. +

+ {:else if emailSuggestion === 'check'} +

+ This email might belong to an existing account. Please double-check or + log in. +

+ {/if} + {/if} + +
+ +