From 6e901690fd99a365b394636c881387ca6bc084bf Mon Sep 17 00:00:00 2001 From: Stephen Adamson Date: Fri, 29 May 2026 20:23:14 +0100 Subject: [PATCH] chore: apply project-wide formatting and UI library updates Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- frontend/src/app.css | 224 +++++----- frontend/src/app.d.ts | 4 +- .../account/EditRequestModal.svelte | 235 +++++------ .../account/UserBookingModal.svelte | 27 +- .../lib/components/admin/ApprovalModal.svelte | 72 ++-- .../admin/BookingCreateModal.svelte | 152 ++++--- .../lib/components/admin/BookingModal.svelte | 127 +++--- .../lib/components/admin/BookingsCard.svelte | 52 ++- .../components/admin/EditBookingModal.svelte | 86 ++-- .../components/admin/EditRequestModal.svelte | 32 +- .../lib/components/admin/ImageUpload.svelte | 32 +- .../components/admin/PatchTestModal.svelte | 12 +- .../components/admin/RescheduleModal.svelte | 132 ++++-- .../lib/components/admin/TimeBlockers.svelte | 354 ++++++++++------ .../src/lib/components/admin/UserModal.svelte | 147 ++++--- .../lib/components/admin/WalkInBooking.svelte | 15 +- .../components/admin/WalkInCreateModal.svelte | 111 ++--- .../components/admin/WeeklySchedule.svelte | 8 +- .../lib/components/booking/BookingFlow.svelte | 17 +- .../lib/components/booking/DatePicker.svelte | 2 +- .../booking/SelectedTimeSummary.svelte | 33 +- .../lib/components/booking/ServiceCard.svelte | 4 +- .../components/booking/StepIndicator.svelte | 4 +- .../components/booking/TimeSlotList.svelte | 29 +- .../components/payments/PaymentModal.svelte | 175 ++++---- .../payments/UserPaymentModal.svelte | 164 +++++--- .../today/CurrentAppointment.svelte | 61 ++- .../components/today/PendingApprovals.svelte | 6 +- .../lib/components/today/TodayCalendar.svelte | 386 ++++++++++++++---- .../lib/components/today/TodayStats.svelte | 81 +++- .../alert-dialog/alert-dialog-action.svelte | 6 +- .../alert-dialog/alert-dialog-cancel.svelte | 8 +- .../alert-dialog/alert-dialog-content.svelte | 2 +- .../alert-dialog-description.svelte | 6 +- .../alert-dialog/alert-dialog-header.svelte | 6 +- .../alert-dialog/alert-dialog-overlay.svelte | 6 +- .../ui/alert-dialog/alert-dialog-title.svelte | 6 +- .../alert-dialog/alert-dialog-trigger.svelte | 2 +- .../lib/components/ui/alert-dialog/index.ts | 22 +- .../ui/avatar/avatar-fallback.svelte | 2 +- .../src/lib/components/ui/badge/badge.svelte | 29 +- frontend/src/lib/components/ui/badge/index.ts | 4 +- .../ui/calendar/calendar-caption.svelte | 32 +- .../ui/calendar/calendar-cell.svelte | 6 +- .../ui/calendar/calendar-day.svelte | 28 +- .../ui/calendar/calendar-grid-body.svelte | 4 +- .../ui/calendar/calendar-grid-head.svelte | 4 +- .../ui/calendar/calendar-grid-row.svelte | 6 +- .../ui/calendar/calendar-grid.svelte | 6 +- .../ui/calendar/calendar-head-cell.svelte | 6 +- .../ui/calendar/calendar-header.svelte | 6 +- .../ui/calendar/calendar-heading.svelte | 6 +- .../ui/calendar/calendar-month-select.svelte | 10 +- .../ui/calendar/calendar-month.svelte | 6 +- .../ui/calendar/calendar-months.svelte | 6 +- .../ui/calendar/calendar-nav.svelte | 6 +- .../ui/calendar/calendar-next-button.svelte | 12 +- .../ui/calendar/calendar-prev-button.svelte | 12 +- .../ui/calendar/calendar-year-select.svelte | 10 +- .../components/ui/calendar/calendar.svelte | 2 +- .../src/lib/components/ui/calendar/index.ts | 38 +- .../lib/components/ui/card/card-action.svelte | 6 +- .../components/ui/card/card-content.svelte | 6 +- .../ui/card/card-description.svelte | 6 +- .../lib/components/ui/card/card-footer.svelte | 6 +- .../lib/components/ui/card/card-header.svelte | 6 +- .../lib/components/ui/card/card-title.svelte | 6 +- .../src/lib/components/ui/card/card.svelte | 2 +- frontend/src/lib/components/ui/card/index.ts | 16 +- .../components/ui/checkbox/checkbox.svelte | 10 +- .../src/lib/components/ui/checkbox/index.ts | 4 +- .../ui/dialog/dialog-content.svelte | 4 +- .../ui/dialog/dialog-description.svelte | 2 +- .../ui/dialog/dialog-overlay.svelte | 2 +- .../lib/components/ui/form/form-button.svelte | 2 +- .../ui/form/form-description.svelte | 6 +- .../ui/form/form-element-field.svelte | 10 +- .../ui/form/form-field-errors.svelte | 6 +- .../lib/components/ui/form/form-field.svelte | 15 +- .../components/ui/form/form-fieldset.svelte | 8 +- .../lib/components/ui/form/form-label.svelte | 8 +- .../lib/components/ui/form/form-legend.svelte | 6 +- frontend/src/lib/components/ui/form/index.ts | 20 +- .../image-cropper-preview.svelte | 2 +- frontend/src/lib/components/ui/input/index.ts | 4 +- .../src/lib/components/ui/input/input.svelte | 26 +- frontend/src/lib/components/ui/label/index.ts | 4 +- .../src/lib/components/ui/label/label.svelte | 6 +- .../src/lib/components/ui/select/index.ts | 22 +- .../ui/select/select-content.svelte | 12 +- .../ui/select/select-group-heading.svelte | 8 +- .../components/ui/select/select-group.svelte | 2 +- .../components/ui/select/select-item.svelte | 8 +- .../components/ui/select/select-label.svelte | 6 +- .../select/select-scroll-down-button.svelte | 8 +- .../ui/select/select-scroll-up-button.svelte | 8 +- .../ui/select/select-separator.svelte | 8 +- .../ui/select/select-trigger.svelte | 12 +- .../src/lib/components/ui/separator/index.ts | 4 +- .../components/ui/separator/separator.svelte | 8 +- .../src/lib/components/ui/skeleton/index.ts | 4 +- .../components/ui/skeleton/skeleton.svelte | 6 +- .../src/lib/components/ui/sonner/index.ts | 2 +- .../lib/components/ui/sonner/sonner.svelte | 4 +- frontend/src/lib/components/ui/table/index.ts | 18 +- .../lib/components/ui/table/table-body.svelte | 6 +- .../components/ui/table/table-caption.svelte | 6 +- .../lib/components/ui/table/table-cell.svelte | 6 +- .../components/ui/table/table-footer.svelte | 6 +- .../lib/components/ui/table/table-head.svelte | 6 +- .../components/ui/table/table-header.svelte | 6 +- .../lib/components/ui/table/table-row.svelte | 6 +- .../src/lib/components/ui/table/table.svelte | 6 +- .../src/lib/components/ui/textarea/index.ts | 4 +- .../components/ui/textarea/textarea.svelte | 8 +- frontend/src/lib/stores/auth.svelte.ts | 13 +- frontend/src/lib/types/booking.ts | 1 - frontend/src/lib/types/index.ts | 2 +- frontend/src/lib/utils/timeSlots.ts | 47 ++- frontend/src/routes/+layout.svelte | 2 +- frontend/src/routes/account/+page.svelte | 135 +++--- .../src/routes/admin/schedule/+page.svelte | 56 ++- frontend/src/routes/api/[...path]/+server.ts | 74 ++-- .../booking-confirmed/[id]/+page.svelte | 36 +- frontend/src/routes/contact/+page.svelte | 3 +- .../src/routes/notifications/+page.svelte | 11 +- frontend/src/routes/pay-tip/[id]/+page.svelte | 59 +-- frontend/src/routes/portfolio/+page.svelte | 62 +-- frontend/src/routes/schedule/+page.svelte | 3 +- frontend/src/routes/tip/+page.svelte | 12 +- frontend/src/routes/today/+page.svelte | 4 +- 131 files changed, 2437 insertions(+), 1571 deletions(-) diff --git a/frontend/src/app.css b/frontend/src/app.css index 3593a20..0f20cf0 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -1,138 +1,138 @@ -@import "tailwindcss"; +@import 'tailwindcss'; -@import "tw-animate-css"; +@import 'tw-animate-css'; @custom-variant dark (&:is(.dark *)); :root { - --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.129 0.042 264.695); - --card: oklch(1 0 0); - --card-foreground: oklch(0.129 0.042 264.695); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.129 0.042 264.695); - --primary: oklch(0.28 0.04 260); - --primary-foreground: oklch(0.984 0.003 247.858); - --secondary: oklch(0.97 0.01 248); - --secondary-foreground: oklch(0.208 0.042 265.755); - --muted: oklch(0.968 0.007 247.896); - --muted-foreground: oklch(0.554 0.046 257.417); - --accent: oklch(0.968 0.007 247.896); - --accent-foreground: oklch(0.208 0.042 265.755); - --destructive: oklch(0.64 0.21 25); - --border: oklch(0.929 0.013 255.508); - --input: oklch(0.929 0.013 255.508); - --ring: oklch(0.704 0.04 256.788); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.984 0.003 247.858); - --sidebar-foreground: oklch(0.129 0.042 264.695); - --sidebar-primary: oklch(0.208 0.042 265.755); - --sidebar-primary-foreground: oklch(0.984 0.003 247.858); - --sidebar-accent: oklch(0.968 0.007 247.896); - --sidebar-accent-foreground: oklch(0.208 0.042 265.755); - --sidebar-border: oklch(0.929 0.013 255.508); - --sidebar-ring: oklch(0.704 0.04 256.788); + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.129 0.042 264.695); + --card: oklch(1 0 0); + --card-foreground: oklch(0.129 0.042 264.695); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.129 0.042 264.695); + --primary: oklch(0.28 0.04 260); + --primary-foreground: oklch(0.984 0.003 247.858); + --secondary: oklch(0.97 0.01 248); + --secondary-foreground: oklch(0.208 0.042 265.755); + --muted: oklch(0.968 0.007 247.896); + --muted-foreground: oklch(0.554 0.046 257.417); + --accent: oklch(0.968 0.007 247.896); + --accent-foreground: oklch(0.208 0.042 265.755); + --destructive: oklch(0.64 0.21 25); + --border: oklch(0.929 0.013 255.508); + --input: oklch(0.929 0.013 255.508); + --ring: oklch(0.704 0.04 256.788); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.984 0.003 247.858); + --sidebar-foreground: oklch(0.129 0.042 264.695); + --sidebar-primary: oklch(0.208 0.042 265.755); + --sidebar-primary-foreground: oklch(0.984 0.003 247.858); + --sidebar-accent: oklch(0.968 0.007 247.896); + --sidebar-accent-foreground: oklch(0.208 0.042 265.755); + --sidebar-border: oklch(0.929 0.013 255.508); + --sidebar-ring: oklch(0.704 0.04 256.788); } .dark { - --background: oklch(0.129 0.042 264.695); - --foreground: oklch(0.984 0.003 247.858); - --card: oklch(0.208 0.042 265.755); - --card-foreground: oklch(0.984 0.003 247.858); - --popover: oklch(0.208 0.042 265.755); - --popover-foreground: oklch(0.984 0.003 247.858); - --primary: oklch(0.929 0.013 255.508); - --primary-foreground: oklch(0.208 0.042 265.755); - --secondary: oklch(0.279 0.041 260.031); - --secondary-foreground: oklch(0.984 0.003 247.858); - --muted: oklch(0.279 0.041 260.031); - --muted-foreground: oklch(0.704 0.04 256.788); - --accent: oklch(0.279 0.041 260.031); - --accent-foreground: oklch(0.984 0.003 247.858); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.551 0.027 264.364); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.208 0.042 265.755); - --sidebar-foreground: oklch(0.984 0.003 247.858); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.984 0.003 247.858); - --sidebar-accent: oklch(0.279 0.041 260.031); - --sidebar-accent-foreground: oklch(0.984 0.003 247.858); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.551 0.027 264.364); + --background: oklch(0.129 0.042 264.695); + --foreground: oklch(0.984 0.003 247.858); + --card: oklch(0.208 0.042 265.755); + --card-foreground: oklch(0.984 0.003 247.858); + --popover: oklch(0.208 0.042 265.755); + --popover-foreground: oklch(0.984 0.003 247.858); + --primary: oklch(0.929 0.013 255.508); + --primary-foreground: oklch(0.208 0.042 265.755); + --secondary: oklch(0.279 0.041 260.031); + --secondary-foreground: oklch(0.984 0.003 247.858); + --muted: oklch(0.279 0.041 260.031); + --muted-foreground: oklch(0.704 0.04 256.788); + --accent: oklch(0.279 0.041 260.031); + --accent-foreground: oklch(0.984 0.003 247.858); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.551 0.027 264.364); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.208 0.042 265.755); + --sidebar-foreground: oklch(0.984 0.003 247.858); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.984 0.003 247.858); + --sidebar-accent: oklch(0.279 0.041 260.031); + --sidebar-accent-foreground: oklch(0.984 0.003 247.858); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.551 0.027 264.364); } @theme inline { - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); } @layer base { - * { - @apply border-border outline-ring/50; - } + * { + @apply border-border outline-ring/50; + } - body { - @apply bg-background text-foreground; - } + body { + @apply bg-background text-foreground; + } } .calendar-container { - z-index: 49 !important; + z-index: 49 !important; } [data-calendar] { - z-index: 49 !important; + z-index: 49 !important; } .scrollbar-hide { - -ms-overflow-style: none; - scrollbar-width: none; + -ms-overflow-style: none; + scrollbar-width: none; } .scrollbar-hide::-webkit-scrollbar { - display: none; -} \ No newline at end of file + display: none; +} diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts index d6ed927..5dc4eab 100644 --- a/frontend/src/app.d.ts +++ b/frontend/src/app.d.ts @@ -10,5 +10,5 @@ declare global { } } -export { }; -declare module 'swiper/svelte'; \ No newline at end of file +export {}; +declare module 'swiper/svelte'; diff --git a/frontend/src/lib/components/account/EditRequestModal.svelte b/frontend/src/lib/components/account/EditRequestModal.svelte index 2532f7e..85f6334 100644 --- a/frontend/src/lib/components/account/EditRequestModal.svelte +++ b/frontend/src/lib/components/account/EditRequestModal.svelte @@ -9,7 +9,11 @@ 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 { extractBookedSlots, getLunchProtectionForSlots, timeToMinutes } from '$lib/lunchProtection'; + import { + extractBookedSlots, + getLunchProtectionForSlots, + timeToMinutes + } from '$lib/lunchProtection'; import ClockIcon from '@lucide/svelte/icons/clock'; import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw'; import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left'; @@ -94,32 +98,31 @@ ); let canSubmit = $derived( - submitting === false && ( - (editMode === 'time' && !!newDate && newTime.length >= 4) || - (editMode === 'services' && selectedServices.length > 0) || - (editMode === 'both-time' && !!newDate && newTime.length >= 4) - ) + submitting === false && + ((editMode === 'time' && !!newDate && newTime.length >= 4) || + (editMode === 'services' && selectedServices.length > 0) || + (editMode === 'both-time' && !!newDate && newTime.length >= 4)) ); let notesChanged = $derived(notes.trim() !== originalNotes.trim()); let modalTitle = $derived(() => { switch (editMode) { - case 'select': return 'Edit Request'; - case 'time': return 'Change Time'; - case 'services': return 'Change Services'; - case 'both-services': return 'Change Services'; - case 'both-time': return 'Choose Time'; + case 'select': + return 'Edit Request'; + case 'time': + return 'Change Time'; + case 'services': + return 'Change Services'; + case 'both-services': + return 'Change Services'; + case 'both-time': + return 'Choose Time'; } }); const lunchProtection = $derived(() => { - if ( - !newDate || - !workingHours || - !availableHours || - slotDuration === 0 - ) { + if (!newDate || !workingHours || !availableHours || slotDuration === 0) { return new Map(); } @@ -131,11 +134,7 @@ return new Map(); } - const existingBookings = extractBookedSlots( - dayWH.startTime, - dayWH.endTime, - dayAH.slots - ); + const existingBookings = extractBookedSlots(dayWH.startTime, dayWH.endTime, dayAH.slots); return getLunchProtectionForSlots( dayWH.startTime, @@ -316,7 +315,11 @@ const dayAH = availableHours?.[dateStr]; if (dayAH?.slots) { - const existingBookings = extractBookedSlots(dayHours.startTime, dayHours.endTime, dayAH.slots); + const existingBookings = extractBookedSlots( + dayHours.startTime, + dayHours.endTime, + dayAH.slots + ); const lunchProtection = getLunchProtectionForSlots( dayHours.startTime, dayHours.endTime, @@ -390,7 +393,14 @@ // ─── Auto-select ──────────────────────────────────────── $effect(() => { - if (!workingHours || !availableHours || newDate || userNavigatedCalendar || editRequestAutoSelectDone) return; + if ( + !workingHours || + !availableHours || + newDate || + userNavigatedCalendar || + editRequestAutoSelectDone + ) + return; editRequestAutoSelectDone = true; const currentDate = new SvelteDate(); @@ -419,11 +429,7 @@ if (!isDateUnavailable(calDate)) { newDate = calDate; if (!userNavigatedCalendar) { - placeholderDate = new CalendarDate( - nextDate.getFullYear(), - nextDate.getMonth() + 1, - 1 - ); + placeholderDate = new CalendarDate(nextDate.getFullYear(), nextDate.getMonth() + 1, 1); } return; } @@ -432,11 +438,7 @@ const tomorrow = new SvelteDate(); tomorrow.setDate(tomorrow.getDate() + 1); - newDate = new CalendarDate( - tomorrow.getFullYear(), - tomorrow.getMonth() + 1, - tomorrow.getDate() - ); + newDate = new CalendarDate(tomorrow.getFullYear(), tomorrow.getMonth() + 1, tomorrow.getDate()); if (!userNavigatedCalendar) { placeholderDate = new CalendarDate(tomorrow.getFullYear(), tomorrow.getMonth() + 1, 1); } @@ -716,7 +718,9 @@ - +
{modalTitle()} @@ -734,7 +738,9 @@ 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" > -
+
@@ -749,9 +755,13 @@ onclick={() => selectMode('services')} disabled={hasOverrides} 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.' : ''} + title={hasOverrides + ? 'This booking has custom pricing. To change services, please contact the salon.' + : ''} > -
+
@@ -766,11 +776,15 @@ onclick={() => selectMode('both-services')} disabled={hasOverrides} 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.' : ''} + title={hasOverrides + ? 'This booking has custom pricing. To change services, please contact the salon.' + : ''} > -
+
- +
Change Both
@@ -781,11 +795,11 @@ {#if hasOverrides}
- This booking has custom pricing. To change services, please contact the salon. You can still request a time change. + This booking has custom pricing. To change services, please contact the salon. You can + still request a time change.
{/if}
- {:else if editMode === 'time' || editMode === 'both-time'} {#if loadingHours && !workingHours} @@ -799,7 +813,7 @@ placeholder={placeholderDate} minValue={minDate} maxValue={maxCalendarDate} - isDateUnavailable={isDateUnavailable} + {isDateUnavailable} onchange={(d) => { newDate = d; newTime = ''; @@ -819,15 +833,15 @@

Loading times...

{:else} -
+
- {newDate - .toDate(getLocalTimeZone()) - .toLocaleDateString('en-GB', { - weekday: 'long', - day: 'numeric', - month: 'short' - })} + {newDate.toDate(getLocalTimeZone()).toLocaleDateString('en-GB', { + weekday: 'long', + day: 'numeric', + month: 'short' + })}
{#if workingHours && !workingHours[newDate.toString()]?.isOpen}

We're closed on this day

@@ -844,7 +858,9 @@
- {:else if editMode === 'services'} {#if hasOverrides} @@ -897,9 +912,7 @@

Current Services {#if selectedServices.length > 0} - - (tap to remove) - + (tap to remove) {/if}

{#if selectedServices.length === 0} @@ -912,16 +925,18 @@ onclick={() => { selectedServices = selectedServices.filter((s) => s.id !== service.id); }} - class="cursor-pointer rounded-lg border border-input bg-background p-4 text-left transition-colors hover:bg-fuchsia-50 bg-fuchsia-100" + class="cursor-pointer rounded-lg border border-input bg-background bg-fuchsia-100 p-4 text-left transition-colors hover:bg-fuchsia-50" > -
+

{service.name}

{service.description || ''}

-
+
{service.duration_minutes} mins - £{service.price.toFixed(2)} + £{service.price.toFixed(2)}
@@ -942,24 +957,26 @@
{#each fittingServices as service (service.id)} -
- + {/each}
@@ -969,15 +986,15 @@
{/if} {:else} -
+
No remaining time available. Remove a service to free up time for additions.
{/if} {#if selectedServices.length === 0} -

- Select at least one service to continue. -

+

Select at least one service to continue.

{/if}
{/if} @@ -991,7 +1008,8 @@
{#if originalNotes}
- Original: {originalNotes} + Original: + {originalNotes}
{/if}
- {:else if editMode === 'both-services'} {#if hasOverrides} @@ -1022,9 +1039,7 @@

Selected Services {#if selectedServices.length > 0} - - (tap to remove) - + (tap to remove) {/if}

{#if selectedServices.length === 0} @@ -1037,16 +1052,18 @@ onclick={() => { selectedServices = selectedServices.filter((s) => s.id !== service.id); }} - class="cursor-pointer rounded-lg border border-input bg-background p-4 text-left transition-colors hover:bg-fuchsia-50 bg-fuchsia-100" + class="cursor-pointer rounded-lg border border-input bg-background bg-fuchsia-100 p-4 text-left transition-colors hover:bg-fuchsia-50" > -
+

{service.name}

{service.description || ''}

-
+
{service.duration_minutes} mins - £{service.price.toFixed(2)} + £{service.price.toFixed(2)}
@@ -1069,14 +1086,16 @@ }} class="cursor-pointer rounded-lg border border-input bg-background p-4 text-left transition-colors hover:bg-fuchsia-50" > -
+

{service.name}

{service.description || ''}

-
+
{service.duration_minutes} mins - £{service.price.toFixed(2)} + £{service.price.toFixed(2)}
@@ -1086,9 +1105,7 @@ {/if} {#if selectedServices.length === 0} -

- Select at least one service to continue. -

+

Select at least one service to continue.

{/if}
@@ -1101,7 +1118,8 @@
{#if originalNotes}
- Original: {originalNotes} + Original: + {originalNotes}
{/if} (open = false)}> Cancel - {:else if editMode === 'time' || editMode === 'services'} - @@ -1142,13 +1155,8 @@ > {submitting ? 'Submitting...' : 'Submit Request'} - {:else if editMode === 'both-services'} - @@ -1165,13 +1173,8 @@ Next: Choose Time - {:else if editMode === 'both-time'} - diff --git a/frontend/src/lib/components/account/UserBookingModal.svelte b/frontend/src/lib/components/account/UserBookingModal.svelte index cd45271..527fdfc 100644 --- a/frontend/src/lib/components/account/UserBookingModal.svelte +++ b/frontend/src/lib/components/account/UserBookingModal.svelte @@ -45,9 +45,7 @@ selectedBooking && isFutureBooking && ['pending', 'confirmed'].includes(selectedBooking.status) ); - let canEditBooking = $derived( - isCancellable && !hasPayments - ); + let canEditBooking = $derived(isCancellable && !hasPayments); let totalPaid = $derived( selectedBooking?.payments @@ -249,7 +247,9 @@ - +
@@ -500,7 +500,6 @@
{/if} -
{/if} @@ -516,14 +515,16 @@ Cancel Booking {#if canEditBooking} - + {/if} {/if}
diff --git a/frontend/src/lib/components/admin/ApprovalModal.svelte b/frontend/src/lib/components/admin/ApprovalModal.svelte index d49316b..d076835 100644 --- a/frontend/src/lib/components/admin/ApprovalModal.svelte +++ b/frontend/src/lib/components/admin/ApprovalModal.svelte @@ -66,16 +66,20 @@ function getBookingDateTime(): string { if (!booking?.start_time) return ''; const d = new SvelteDate(booking.start_time); - return d.toLocaleDateString('en-GB', { - weekday: 'long', - day: 'numeric', - month: 'long', - year: 'numeric' - }) + ' at ' + d.toLocaleTimeString('en-GB', { - hour: 'numeric', - minute: '2-digit', - hour12: true - }); + return ( + d.toLocaleDateString('en-GB', { + weekday: 'long', + day: 'numeric', + month: 'long', + year: 'numeric' + }) + + ' at ' + + d.toLocaleTimeString('en-GB', { + hour: 'numeric', + minute: '2-digit', + hour12: true + }) + ); } function getTotalCost(): number { @@ -447,36 +451,36 @@
{/if} - -
-

- Customer Contact -

-
-
-
Name
-
{booking.user?.full_name || '—'}
-
-
+ +
+

+ Customer Contact +

+
-
Phone
-
{booking.user?.phone || '—'}
+
Name
+
{booking.user?.full_name || '—'}
-
-
Email
-
{booking.user?.email || '—'}
+
+
+
Phone
+
{booking.user?.phone || '—'}
+
+
+
Email
+
{booking.user?.email || '—'}
+
-
- -
-

- Booking Date & Time -

-
{getBookingDateTime()}
-
+ +
+

+ Booking Date & Time +

+
{getBookingDateTime()}
+
diff --git a/frontend/src/lib/components/admin/BookingCreateModal.svelte b/frontend/src/lib/components/admin/BookingCreateModal.svelte index 127320c..5f26dbc 100644 --- a/frontend/src/lib/components/admin/BookingCreateModal.svelte +++ b/frontend/src/lib/components/admin/BookingCreateModal.svelte @@ -74,7 +74,11 @@ // Step 4: Date & Time let placeholder = $state( - new CalendarDate(new SvelteDate().getFullYear(), new SvelteDate().getMonth() + 1, new SvelteDate().getDate()) + new CalendarDate( + new SvelteDate().getFullYear(), + new SvelteDate().getMonth() + 1, + new SvelteDate().getDate() + ) ); let selectedDate = $state(undefined); let selectedTime = $state(null); @@ -129,7 +133,7 @@ // =============== Lunch Protection =============== const lunchProtection = $derived( - (selectedDate && selectedServices.length > 0) + selectedDate && selectedServices.length > 0 ? buildLunchProtection(selectedDate, workingHours, availableHours, getTotalDuration(), true) : new Map() ); @@ -197,7 +201,16 @@ // Auto-select first available date once data loads (timing-safe, data-driven) let bookingCreateAutoSelectDone = $state(false); $effect(() => { - if (open && currentStep === 4 && workingHours && availableHours && !selectedDate && selectedServices.length > 0 && !userNavigatedCalendar && !bookingCreateAutoSelectDone) { + if ( + open && + currentStep === 4 && + workingHours && + availableHours && + !selectedDate && + selectedServices.length > 0 && + !userNavigatedCalendar && + !bookingCreateAutoSelectDone + ) { bookingCreateAutoSelectDone = true; const now = new SvelteDate(); const maxDateJs = new SvelteDate( @@ -213,7 +226,11 @@ const checkDate = new SvelteDate(now); checkDate.setDate(now.getDate() + i); const dateStr = checkDate.toISOString().split('T')[0]; - const calDate = new CalendarDate(checkDate.getFullYear(), checkDate.getMonth() + 1, checkDate.getDate()); + const calDate = new CalendarDate( + checkDate.getFullYear(), + checkDate.getMonth() + 1, + checkDate.getDate() + ); if (workingHours[dateStr]?.isOpen && !isDateUnavailable(calDate)) { selectedDate = calDate; if (!userNavigatedCalendar) { @@ -342,7 +359,9 @@ const whMap: Record = {}; const ahMap: Record = {}; - whData.forEach((d) => (whMap[d.date] = { isOpen: d.isOpen, startTime: d.startTime, endTime: d.endTime })); + whData.forEach( + (d) => (whMap[d.date] = { isOpen: d.isOpen, startTime: d.startTime, endTime: d.endTime }) + ); ahData.forEach((d) => (ahMap[d.date] = { isOpen: d.isOpen, slots: d.slots })); if (gen !== hoursRangeGeneration) return; // Stale response, discard @@ -435,7 +454,7 @@ } finally { loadingWorkingHours = false; loadingAvailableHours = false; - loadingMonthKeys = new Set([...loadingMonthKeys].filter(k => k !== monthKey)); + loadingMonthKeys = new Set([...loadingMonthKeys].filter((k) => k !== monthKey)); } } @@ -575,7 +594,13 @@ const groupedTimeSlots = $derived( currentStep === 4 && selectedServices.length > 0 && selectedDate - ? generateGroupedTimeSlots(selectedDate, workingHours, availableHours, getTotalDuration(), lunchProtection) + ? generateGroupedTimeSlots( + selectedDate, + workingHours, + availableHours, + getTotalDuration(), + lunchProtection + ) : [] ); @@ -599,8 +624,20 @@ if (duration <= 0) return true; // Build lunch protection specifically for the date being checked - const dayProtection = buildLunchProtection(date as CalendarDate, workingHours, availableHours, duration, true); - const slots = generateAvailableTimeSlots(date as CalendarDate, workingHours, availableHours, duration, dayProtection); + const dayProtection = buildLunchProtection( + date as CalendarDate, + workingHours, + availableHours, + duration, + true + ); + const slots = generateAvailableTimeSlots( + date as CalendarDate, + workingHours, + availableHours, + duration, + dayProtection + ); if (slots.length === 0) return true; return false; @@ -620,32 +657,32 @@ try { let finalUserId = selectedUserId; - if (userType === 'guest') { - const phone = guestPhone.trim() || '+447700900000'; - const createRes = await fetch('/api/users/guest', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${authStore.currentToken}` - }, - body: JSON.stringify({ - firstName: guestName.trim().split(' ')[0] || 'Guest', - lastName: guestName.trim().split(' ').slice(1).join(' ') || 'Customer', - phone: phone, - email: `callin-${Date.now()}@guest.invalid` - }) - }); + if (userType === 'guest') { + const phone = guestPhone.trim() || '+447700900000'; + const createRes = await fetch('/api/users/guest', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${authStore.currentToken}` + }, + body: JSON.stringify({ + firstName: guestName.trim().split(' ')[0] || 'Guest', + lastName: guestName.trim().split(' ').slice(1).join(' ') || 'Customer', + phone: phone, + email: `callin-${Date.now()}@guest.invalid` + }) + }); - if (!createRes.ok) { - toast.error('Failed to create guest user'); - submitting = false; - return; + if (!createRes.ok) { + toast.error('Failed to create guest user'); + submitting = false; + return; + } + + const guestUser = await createRes.json(); + finalUserId = guestUser.id; } - const guestUser = await createRes.json(); - finalUserId = guestUser.id; - } - if (!finalUserId) throw new Error('User ID required'); if (!selectedDate || !selectedTime) throw new Error('Date and time required'); @@ -734,7 +771,9 @@ - + Create Admin Booking @@ -1096,17 +1135,30 @@ {#if reservationId && reservationExpiresAt} -
+
- - + + - Slot reserved until {reservationExpiresAt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + Slot reserved until {reservationExpiresAt.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit' + })}
- + ({reservationCountdown} remaining)
@@ -1123,11 +1175,11 @@ selectedDate = newDate; selectedTime = null; }} - onPlaceholderChange={(newPlaceholder) => { - placeholder = newPlaceholder; - userNavigatedCalendar = true; - fetchHoursForMonth(newPlaceholder); - }} + onPlaceholderChange={(newPlaceholder) => { + placeholder = newPlaceholder; + userNavigatedCalendar = true; + fetchHoursForMonth(newPlaceholder); + }} />
@@ -1139,14 +1191,18 @@
{#if formattedSelectedDate} -
{formattedSelectedDate}
+
+ {formattedSelectedDate} +
{/if} { selectedTime = time; }} + onSelect={(time) => { + selectedTime = time; + }} />
@@ -1177,7 +1233,11 @@ onclick={submitBooking} class="bg-primary text-primary-foreground" > - {isReserving ? 'Reserving Slot...' : submitting ? 'Creating Booking...' : 'Create Booking'} + {isReserving + ? 'Reserving Slot...' + : submitting + ? 'Creating Booking...' + : 'Create Booking'} diff --git a/frontend/src/lib/components/admin/BookingModal.svelte b/frontend/src/lib/components/admin/BookingModal.svelte index b2100e7..ea79c9e 100644 --- a/frontend/src/lib/components/admin/BookingModal.svelte +++ b/frontend/src/lib/components/admin/BookingModal.svelte @@ -25,8 +25,10 @@ // customised at booking time (discounts, extended sessions). Showing base values // misleads admins about what was actually booked. let totalDuration = $derived( - selectedBooking?.services?.reduce((sum, service) => sum + (service.override_duration_minutes ?? service.duration_minutes ?? 0), 0) || + selectedBooking?.services?.reduce( + (sum, service) => sum + (service.override_duration_minutes ?? service.duration_minutes ?? 0), 0 + ) || 0 ); async function fetchBookingDetails() { @@ -135,7 +137,9 @@ - +
@@ -145,7 +149,9 @@ {/if}
{#if selectedBooking} - {@const isConfirmedOrLater = ['confirmed', 'in_progress', 'completed'].includes(selectedBooking.status)} + {@const isConfirmedOrLater = ['confirmed', 'in_progress', 'completed'].includes( + selectedBooking.status + )} {#if selectedBooking.status === 'pending'}
@@ -175,14 +181,14 @@ Pending - - {#if selectedBooking.deposit_required} - - Will Require Deposit - - {/if} + + {#if selectedBooking.deposit_required} + + Will Require Deposit + + {/if}
{:else if isConfirmedOrLater && selectedBooking.deposit_required} @@ -197,14 +203,12 @@ > {selectedBooking.status.replace('_', ' ')} - - {selectedBooking.deposit_paid ? 'Deposit Paid' : 'Deposit Due'} - + + {selectedBooking.deposit_paid ? 'Deposit Paid' : 'Deposit Due'} +
{/if} {/if} @@ -270,8 +274,14 @@
{service.service_description}
{/if}
- {formatDuration(service.override_duration_minutes ?? service.duration_minutes ?? 0)} - £{(service.override_price ?? service.price ?? 0).toFixed(2)} + {formatDuration( + service.override_duration_minutes ?? service.duration_minutes ?? 0 + )} + £{(service.override_price ?? service.price ?? 0).toFixed(2)}
{/each} @@ -296,7 +306,9 @@
{selectedBooking.user?.full_name || '—'}
{#if selectedBooking.user?.date_of_birth} -
{calculateAge(selectedBooking.user.date_of_birth)} years old
+
+ {calculateAge(selectedBooking.user.date_of_birth)} years old +
{/if}
@@ -308,7 +320,8 @@ {selectedBooking.user.email} + >{selectedBooking.user.email} {:else}
{/if} @@ -318,8 +331,8 @@ {#if selectedBooking.user?.phone} {selectedBooking.user.phone} + class="font-medium text-blue-600 hover:underline">{selectedBooking.user.phone} {:else}
{/if} @@ -328,19 +341,26 @@
Referral Code
- + {selectedBooking.user.referral_code} diff --git a/frontend/src/lib/components/admin/BookingsCard.svelte b/frontend/src/lib/components/admin/BookingsCard.svelte index 2f78447..e9de38e 100644 --- a/frontend/src/lib/components/admin/BookingsCard.svelte +++ b/frontend/src/lib/components/admin/BookingsCard.svelte @@ -246,35 +246,33 @@
{formatBookingDateTime(b.start_time)}
-
- - - {b.status} - - {#if b.deposit_required} - {#if b.status === 'pending'} - - Will Require Deposit - - {:else if ['confirmed', 'in_progress', 'completed'].includes(b.status)} - - {b.deposit_paid ? 'Deposit Paid' : 'Deposit Due'} - - {/if} +
+ + + {b.status} + + {#if b.deposit_required} + {#if b.status === 'pending'} + + Will Require Deposit + + {:else if ['confirmed', 'in_progress', 'completed'].includes(b.status)} + + {b.deposit_paid ? 'Deposit Paid' : 'Deposit Due'} + {/if} - • {b.user?.full_name || 'Unknown User'} - - - {formatServices(b.services)} - -
+ {/if} + • {b.user?.full_name || 'Unknown User'} + + - {formatServices(b.services)} +
+
{/each} diff --git a/frontend/src/lib/components/admin/EditBookingModal.svelte b/frontend/src/lib/components/admin/EditBookingModal.svelte index b74eeb5..13087db 100644 --- a/frontend/src/lib/components/admin/EditBookingModal.svelte +++ b/frontend/src/lib/components/admin/EditBookingModal.svelte @@ -1,8 +1,8 @@ - +
@@ -409,9 +413,7 @@ import { formatDuration } from '$lib/utils/format';
-

- Customer -

+

Customer

Name
@@ -419,7 +421,9 @@ import { formatDuration } from '$lib/utils/format';
Email
-
{booking.user?.email || '—'}
+
+ {booking.user?.email || '—'} +
Phone
@@ -465,17 +469,23 @@ import { formatDuration } from '$lib/utils/format';
{#if services.length === 0} -
+
No services added yet. Click "Add Service" to begin.
{:else}
{#each services as service, index (index)} -
+
{service.service_name}
- {formatDuration(service.override_duration_minutes ?? service.duration_minutes ?? 0)} • £{(service.override_price ?? service.price ?? 0).toFixed(2)} + {formatDuration( + service.override_duration_minutes ?? service.duration_minutes ?? 0 + )} • £{(service.override_price ?? service.price ?? 0).toFixed(2)}
{#if service.override_price !== undefined || service.override_duration_minutes !== undefined}
Modified
@@ -523,7 +533,9 @@ import { formatDuration } from '$lib/utils/format'; {/each}
-
+
Total: {formatDuration(totalDuration)} £{totalPrice.toFixed(2)}
@@ -538,10 +550,16 @@ import { formatDuration } from '$lib/utils/format';
{#each booking.payments as payment, index (index)} -
+
- {payment.payment_type === 'deposit' ? 'Deposit' : payment.payment_type === 'full' ? 'Full Payment' : payment.payment_type} + {payment.payment_type === 'deposit' + ? 'Deposit' + : payment.payment_type === 'full' + ? 'Full Payment' + : payment.payment_type} {#if payment.payment_method} via {payment.payment_method} {/if} @@ -550,7 +568,8 @@ import { formatDuration } from '$lib/utils/format'; {payment.status} @@ -562,7 +581,11 @@ import { formatDuration } from '$lib/utils/format'; {/if}
- {new SvelteDate(payment.created_at).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })} + {new SvelteDate(payment.created_at).toLocaleDateString('en-GB', { + day: 'numeric', + month: 'short', + year: 'numeric' + })}
{#if payment.status === 'completed'} @@ -592,9 +615,7 @@ import { formatDuration } from '$lib/utils/format'; class="w-full" /> {#if notes.trim() !== (booking.notes || '')} -
- Notes will be saved -
+
Notes will be saved
{/if}
@@ -724,9 +745,7 @@ import { formatDuration } from '$lib/utils/format'; Add Service - - Select a service to add to this booking. - + Select a service to add to this booking.
@@ -742,7 +761,9 @@ import { formatDuration } from '$lib/utils/format'; {#if loadingServices}
Loading services...
{:else if filteredServices.length === 0} -
+
{#if availableServices.length === 0} No services available. {:else} @@ -778,16 +799,12 @@ import { formatDuration } from '$lib/utils/format'; Process Refund - - Enter the refund amount and reason. - + Enter the refund amount and reason.
- +
£ @@ -819,7 +836,10 @@ import { formatDuration } from '$lib/utils/format'; - diff --git a/frontend/src/lib/components/admin/EditRequestModal.svelte b/frontend/src/lib/components/admin/EditRequestModal.svelte index 4096a08..67f05f8 100644 --- a/frontend/src/lib/components/admin/EditRequestModal.svelte +++ b/frontend/src/lib/components/admin/EditRequestModal.svelte @@ -221,7 +221,10 @@ {formatDateLine1(editRequest.original.start_time)}
- {formatDateLine2(editRequest.original.start_time, getDuration(editRequest.original.services))} + {formatDateLine2( + editRequest.original.start_time, + getDuration(editRequest.original.services) + )}
{#if isTimeChanged()} @@ -231,11 +234,14 @@ {formatDateLine1(editRequest.proposed.start_time!)}
- {formatDateLine2(editRequest.proposed.start_time!, getDuration(editRequest.proposed.services))} + {formatDateLine2( + editRequest.proposed.start_time!, + getDuration(editRequest.proposed.services) + )}
{:else} -
No change
+
No change
{/if}
@@ -252,7 +258,7 @@ {#each editRequest.original.services as service (service.id)} {#if serviceDiff.removed.some((s) => s.id === service.id)}
- +
{service.name} @@ -264,7 +270,7 @@
{:else}
- +
{service.name}
@@ -281,8 +287,10 @@
{#each editRequest.proposed.services as service (service.id)} {#if serviceDiff.added.some((s) => s.id === service.id)} -
- + +
+ +
{service.name}
@@ -292,7 +300,7 @@
{:else}
- +
{service.name}
@@ -322,7 +330,7 @@ {#if editRequest.proposed.notes && editRequest.proposed.notes !== editRequest.original.notes}
{editRequest.proposed.notes}
{:else} -
No change
+
No change
{/if}
@@ -340,11 +348,7 @@
-
- {#if input.length && suggestions.length} -
- {#each suggestions as s, i (s)} - {@const isHighlighted = isMobile ? i === 0 : i === selectedSuggestionIndex} + {#if input.length && suggestions.length}
selectSuggestion(s)} - onkeydown={(e) => (e.key === 'Enter' || e.key === ' ') && selectSuggestion(s)} - role="button" - tabindex="0" + class="absolute top-full right-0 left-0 z-10 mt-1 rounded-md border bg-white shadow-md" > - {s} + {#each suggestions as s, i (s)} + {@const isHighlighted = isMobile ? i === 0 : i === selectedSuggestionIndex} +
selectSuggestion(s)} + onkeydown={(e) => (e.key === 'Enter' || e.key === ' ') && selectSuggestion(s)} + role="button" + tabindex="0" + > + {s} +
+ {/each}
- {/each} -
- {/if} + {/if}
diff --git a/frontend/src/lib/components/admin/PatchTestModal.svelte b/frontend/src/lib/components/admin/PatchTestModal.svelte index b798017..6d1ee7a 100644 --- a/frontend/src/lib/components/admin/PatchTestModal.svelte +++ b/frontend/src/lib/components/admin/PatchTestModal.svelte @@ -82,12 +82,12 @@ const text = await response.text(); toast.error(text || 'Failed to add patch test', { id: loadingToast }); } - } catch (err) { - console.error('Error adding patch test:', err); - toast.error('Network error', { id: loadingToast }); - } finally { - submitting = false; - } + } catch (err) { + console.error('Error adding patch test:', err); + toast.error('Network error', { id: loadingToast }); + } finally { + submitting = false; + } } $effect(() => { diff --git a/frontend/src/lib/components/admin/RescheduleModal.svelte b/frontend/src/lib/components/admin/RescheduleModal.svelte index 43614ce..da56b6c 100644 --- a/frontend/src/lib/components/admin/RescheduleModal.svelte +++ b/frontend/src/lib/components/admin/RescheduleModal.svelte @@ -33,7 +33,11 @@ let saving = $state(false); let placeholder = $state( - new CalendarDate(new SvelteDate().getFullYear(), new SvelteDate().getMonth() + 1, new SvelteDate().getDate()) + new CalendarDate( + new SvelteDate().getFullYear(), + new SvelteDate().getMonth() + 1, + new SvelteDate().getDate() + ) ); let selectedDate = $state(undefined); let selectedTime = $state(null); @@ -55,21 +59,42 @@ const minDate = new CalendarDate(today.getFullYear(), today.getMonth() + 1, today.getDate()); const maxDate = new SvelteDate(); maxDate.setMonth(today.getMonth() + 6); - const maxCalendarDate = new CalendarDate(maxDate.getFullYear(), maxDate.getMonth() + 1, maxDate.getDate()); + const maxCalendarDate = new CalendarDate( + maxDate.getFullYear(), + maxDate.getMonth() + 1, + maxDate.getDate() + ); let bookingDuration = $derived( - booking.services?.reduce((sum, s) => sum + (s.override_duration_minutes ?? s.duration_minutes ?? 0), 0) ?? booking.duration_minutes ?? 0 + booking.services?.reduce( + (sum, s) => sum + (s.override_duration_minutes ?? s.duration_minutes ?? 0), + 0 + ) ?? + booking.duration_minutes ?? + 0 ); const lunchProtection = $derived( - selectedDate ? buildLunchProtection(selectedDate, workingHours, availableHours, bookingDuration, true) : new Map() + selectedDate + ? buildLunchProtection(selectedDate, workingHours, availableHours, bookingDuration, true) + : new Map() ); const groupedTimeSlots = $derived( - selectedDate ? generateGroupedTimeSlots(selectedDate, workingHours, availableHours, bookingDuration, lunchProtection) : [] + selectedDate + ? generateGroupedTimeSlots( + selectedDate, + workingHours, + availableHours, + bookingDuration, + lunchProtection + ) + : [] ); - const formattedSelectedDate = $derived(selectedDate ? getDayWithOrdinal(selectedDate) : undefined); + const formattedSelectedDate = $derived( + selectedDate ? getDayWithOrdinal(selectedDate) : undefined + ); function isDateUnavailable(date: DateValue): boolean { if (!(date instanceof CalendarDate)) return true; @@ -84,8 +109,20 @@ // If booking duration is 0 (data missing), no valid slots can exist — mark unavailable if (bookingDuration <= 0) return true; // Build lunch protection specifically for the date being checked (not the selected date) - const dayProtection = buildLunchProtection(date as CalendarDate, workingHours, availableHours, bookingDuration, true); - const slots = generateAvailableTimeSlots(date as CalendarDate, workingHours, availableHours, bookingDuration, dayProtection); + const dayProtection = buildLunchProtection( + date as CalendarDate, + workingHours, + availableHours, + bookingDuration, + true + ); + const slots = generateAvailableTimeSlots( + date as CalendarDate, + workingHours, + availableHours, + bookingDuration, + dayProtection + ); if (slots.length === 0) return true; return false; } @@ -130,7 +167,9 @@ const whMap: Record = {}; const ahMap: Record = {}; - whData.forEach((d) => (whMap[d.date] = { isOpen: d.isOpen, startTime: d.startTime, endTime: d.endTime })); + whData.forEach( + (d) => (whMap[d.date] = { isOpen: d.isOpen, startTime: d.startTime, endTime: d.endTime }) + ); ahData.forEach((d) => (ahMap[d.date] = { isOpen: d.isOpen, slots: d.slots })); // Cache by month key @@ -191,7 +230,11 @@ try { const startOfMonth = new CalendarDate(date.year, date.month, 1); - const endOfMonth = new CalendarDate(date.year, date.month, date.calendar.getDaysInMonth(date)); + const endOfMonth = new CalendarDate( + date.year, + date.month, + date.calendar.getDaysInMonth(date) + ); const [whRes, ahRes] = await Promise.all([ fetch(`/api/scheduling/working-hours?start=${startOfMonth}&end=${endOfMonth}`, { headers: { Authorization: `Bearer ${authStore.currentToken}` } @@ -206,7 +249,9 @@ if (gen !== hoursMonthGeneration) return; const whMap: Record = {}; const ahMap: Record = {}; - whData.forEach((d) => (whMap[d.date] = { isOpen: d.isOpen, startTime: d.startTime, endTime: d.endTime })); + whData.forEach( + (d) => (whMap[d.date] = { isOpen: d.isOpen, startTime: d.startTime, endTime: d.endTime }) + ); ahData.forEach((d) => (ahMap[d.date] = { isOpen: d.isOpen, slots: d.slots })); workingHoursCache[monthKey] = whMap; @@ -220,7 +265,7 @@ toast.error('Failed to load availability'); } finally { delete loadingMonths[monthKey]; - loadingMonthKeys = new Set([...loadingMonthKeys].filter(k => k !== monthKey)); + loadingMonthKeys = new Set([...loadingMonthKeys].filter((k) => k !== monthKey)); loadingHours = false; } } @@ -237,7 +282,11 @@ loadingMonths = {}; loadingMonthKeys = new Set(); rescheduleAutoSelectDone = false; - placeholder = new CalendarDate(new SvelteDate().getFullYear(), new SvelteDate().getMonth() + 1, new SvelteDate().getDate()); + placeholder = new CalendarDate( + new SvelteDate().getFullYear(), + new SvelteDate().getMonth() + 1, + new SvelteDate().getDate() + ); } async function reschedule() { @@ -318,7 +367,14 @@ // Auto-select first available date once data loads (timing-safe, data-driven) $effect(() => { - if (open && workingHours && availableHours && !selectedDate && !userNavigatedCalendar && !rescheduleAutoSelectDone) { + if ( + open && + workingHours && + availableHours && + !selectedDate && + !userNavigatedCalendar && + !rescheduleAutoSelectDone + ) { rescheduleAutoSelectDone = true; const now = new SvelteDate(); const maxDateJs = new SvelteDate( @@ -334,7 +390,11 @@ const checkDate = new SvelteDate(now); checkDate.setDate(now.getDate() + i); const dateStr = checkDate.toISOString().split('T')[0]; - const calDate = new CalendarDate(checkDate.getFullYear(), checkDate.getMonth() + 1, checkDate.getDate()); + const calDate = new CalendarDate( + checkDate.getFullYear(), + checkDate.getMonth() + 1, + checkDate.getDate() + ); if (workingHours[dateStr]?.isOpen && !isDateUnavailable(calDate)) { selectedDate = calDate; if (!userNavigatedCalendar) { @@ -360,10 +420,21 @@
- 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 })} + 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 +
+ {booking.user?.full_name || 'Unknown'} · {booking.services + ?.map((s) => s.service_name) + .join(', ') || 'No services'} · {bookingDuration} min
@@ -381,10 +452,10 @@ selectedDate = newDate; selectedTime = null; }} - onPlaceholderChange={(newPlaceholder) => { - placeholder = newPlaceholder; - userNavigatedCalendar = true; - }} + onPlaceholderChange={(newPlaceholder) => { + placeholder = newPlaceholder; + userNavigatedCalendar = true; + }} />
@@ -393,14 +464,18 @@
{#if formattedSelectedDate} -
{formattedSelectedDate}
+
+ {formattedSelectedDate} +
{/if} { selectedTime = time; }} + onSelect={(time) => { + selectedTime = time; + }} />
@@ -423,7 +498,14 @@
-
+ {:else if blockers.length === 0} +

No time blockers found.

{: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)} -
-
-
- +
+
+
{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} + {/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} @@ -668,14 +702,37 @@
- -
- +
@@ -684,7 +741,11 @@ {:else if !selectedWorkingHours}

Select a date first

{:else if !selectedWorkingHours.isOpen} -

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

+

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

{:else} (guestPhone = e.currentTarget.value)} - /> -
+
+ + (guestPhone = e.currentTarget.value)} + /> +

Booking as a guest creates a temporary record. Encourage them to sign up for loyalty benefits. diff --git a/frontend/src/lib/components/admin/WeeklySchedule.svelte b/frontend/src/lib/components/admin/WeeklySchedule.svelte index 2430f66..be17e76 100644 --- a/frontend/src/lib/components/admin/WeeklySchedule.svelte +++ b/frontend/src/lib/components/admin/WeeklySchedule.svelte @@ -487,7 +487,9 @@ > {#each Array(24) as _, hour} {#each TIME15 as min (min)} - + {/each} {/each} @@ -500,7 +502,9 @@ > {#each Array(24) as _, hour} {#each TIME15 as min (min)} - + {/each} {/each} diff --git a/frontend/src/lib/components/booking/BookingFlow.svelte b/frontend/src/lib/components/booking/BookingFlow.svelte index 8ebefe0..61b41a6 100644 --- a/frontend/src/lib/components/booking/BookingFlow.svelte +++ b/frontend/src/lib/components/booking/BookingFlow.svelte @@ -1358,7 +1358,12 @@

Professional beauty treatments in a calm and friendly environment

- + {#if currentStep === 0} @@ -1371,12 +1376,14 @@ Guest checkout does not receive loyalty stamps or seasonal discounts.

- - +
diff --git a/frontend/src/lib/components/booking/DatePicker.svelte b/frontend/src/lib/components/booking/DatePicker.svelte index 38c1878..a32025c 100644 --- a/frontend/src/lib/components/booking/DatePicker.svelte +++ b/frontend/src/lib/components/booking/DatePicker.svelte @@ -25,7 +25,7 @@ {#if protection?.showWarning || protection?.isBlocked} -
+
- - + +
-
+
{protection?.isBlocked ? 'Lunch break conflict' : 'Lunch break warning'}
@@ -31,9 +46,15 @@
{/if} -
+
- {#if protection?.isBlocked}Booking conflict{:else if protection?.showWarning}Lunch warning{:else}Time selected{/if} + {#if protection?.isBlocked}Booking conflict{:else if protection?.showWarning}Lunch warning{:else}Time + selected{/if}
{selectedDate} at {selectedTime} diff --git a/frontend/src/lib/components/booking/ServiceCard.svelte b/frontend/src/lib/components/booking/ServiceCard.svelte index 3d1e5d5..b3ba008 100644 --- a/frontend/src/lib/components/booking/ServiceCard.svelte +++ b/frontend/src/lib/components/booking/ServiceCard.svelte @@ -26,7 +26,7 @@ onclick={() => !isGrayedOut && onclick?.()} disabled={isGrayedOut} > -
+

{service.name}

{#if isGrayedOut} @@ -44,7 +44,7 @@

{service.description}

{/if}
-
+
{service.duration_minutes} mins £{service.price}
diff --git a/frontend/src/lib/components/booking/StepIndicator.svelte b/frontend/src/lib/components/booking/StepIndicator.svelte index 14879bc..babae0a 100644 --- a/frontend/src/lib/components/booking/StepIndicator.svelte +++ b/frontend/src/lib/components/booking/StepIndicator.svelte @@ -7,7 +7,9 @@ const totalSteps = steps.length; -
+
{#each steps as step, index (step)} {@const displayNumber = startAt + index} {@const isActive = displayNumber <= currentStep} diff --git a/frontend/src/lib/components/booking/TimeSlotList.svelte b/frontend/src/lib/components/booking/TimeSlotList.svelte index 4be01e6..6233192 100644 --- a/frontend/src/lib/components/booking/TimeSlotList.svelte +++ b/frontend/src/lib/components/booking/TimeSlotList.svelte @@ -27,7 +27,7 @@ title={p.warningMessage || 'Lunch protection'} > {formatTime(slot.startTime)} - - {formatTime(slot.endTime)} + - {formatTime(slot.endTime)} {duration} min {:else} @@ -36,20 +36,33 @@ onclick={() => onSelect(slot.startTime)} class={`w-full hover:bg-fuchsia-50 ${ slot.startTime === selectedTime - ? (p?.showWarning ? 'bg-fuchsia-200 border-amber-500' : 'bg-fuchsia-100') - : (p?.showWarning ? 'border-amber-400 bg-amber-100' : '') + ? p?.showWarning + ? 'border-amber-500 bg-fuchsia-200' + : 'bg-fuchsia-100' + : p?.showWarning + ? 'border-amber-400 bg-amber-100' + : '' }`} title={p?.warningMessage} > {#if p?.showWarning} - - + + {/if} {formatTime(slot.startTime)} - - {formatTime(slot.endTime)} + - {formatTime(slot.endTime)} {duration} min {/if} @@ -60,11 +73,11 @@ disabled > {formatTime(slot.startTime)} - - {formatTime(slot.endTime)} + - {formatTime(slot.endTime)} {/if} {/each}
{:else} -

No available slots for this date

+

No available slots for this date

{/if} diff --git a/frontend/src/lib/components/payments/PaymentModal.svelte b/frontend/src/lib/components/payments/PaymentModal.svelte index c5ef980..859438f 100644 --- a/frontend/src/lib/components/payments/PaymentModal.svelte +++ b/frontend/src/lib/components/payments/PaymentModal.svelte @@ -15,7 +15,17 @@ let { booking, onClose, onComplete }: Props = $props(); - type PaymentStatus = 'idle' | 'selecting' | 'card-processing' | 'card-polling' | 'cash-entering' | 'cash-confirming' | 'gift-entering' | 'gift-confirming' | 'success' | 'error'; + type PaymentStatus = + | 'idle' + | 'selecting' + | 'card-processing' + | 'card-polling' + | 'cash-entering' + | 'cash-confirming' + | 'gift-entering' + | 'gift-confirming' + | 'success' + | 'error'; type PaymentResult = { checkout_id: string; @@ -84,9 +94,9 @@ let tipPercentages = $derived.by(() => { if (subtotal <= 0) return []; return [ - { pct: 10, amount: Math.round(subtotal * 0.10 * 100) / 100 }, + { pct: 10, amount: Math.round(subtotal * 0.1 * 100) / 100 }, { pct: 15, amount: Math.round(subtotal * 0.15 * 100) / 100 }, - { pct: 20, amount: Math.round(subtotal * 0.20 * 100) / 100 }, + { pct: 20, amount: Math.round(subtotal * 0.2 * 100) / 100 } ]; }); @@ -124,9 +134,7 @@ return service.override_price ?? service.price ?? 0; } - let subtotal = $derived( - (booking.services ?? []).reduce((sum, s) => sum + getServicePrice(s), 0) - ); + let subtotal = $derived((booking.services ?? []).reduce((sum, s) => sum + getServicePrice(s), 0)); let tipMultiplier = $derived( selectedTipPercent !== null @@ -415,8 +423,12 @@ }); - !open && handleClose()} onOpenAutoFocus={(e) => e.preventDefault()}> - + !open && handleClose()} + onOpenAutoFocus={(e) => e.preventDefault()} +> + Take Payment @@ -425,10 +437,16 @@
Services
-
+
{#each booking.services ?? [] as service, i (service.service_id ?? `svc-${i}`)}
-
{service.service_name || 'Unknown Service'}
+
+ {service.service_name || 'Unknown Service'} +
£ handlePriceInput(service.service_id, e.currentTarget.value)} /> {#if serviceOverrides[service.service_id] && Math.abs(parseFloat(serviceOverrides[service.service_id].price) - serviceOverrides[service.service_id].originalPrice) > 0.01} @@ -469,10 +489,22 @@
- +
- {:else if status === 'selecting'}
@@ -537,8 +593,8 @@ type="button" class="rounded-lg border py-3 text-center text-sm font-semibold transition-colors hover:bg-fuchsia-50 {selectedTipPercent === tip.pct - ? 'border-input bg-fuchsia-100 text-foreground' - : 'border-input'}" + ? 'border-input bg-fuchsia-100 text-foreground' + : 'border-input'}" onclick={() => selectTipPercent(tip.pct)} >
{tip.pct}%
@@ -547,7 +603,7 @@ {/each}
- £ + £ - - +
- {:else if status === 'card-processing' || status === 'card-polling'}
-
+

Waiting for customer to tap card...

This may take a few moments

- {:else if status === 'cash-entering'}
@@ -599,11 +650,9 @@
- +
- £ + £ {#if changeDue > 0}
- + @@ -637,9 +683,7 @@ {/if}
- +
- {:else if status === 'cash-confirming'}
-
+

Processing cash payment...

- {:else if status === 'gift-entering'}
@@ -664,9 +708,7 @@
- +
- +
- {:else if status === 'gift-confirming'}
-
+

Processing gift card...

- {:else if status === 'error' && error}

{error}

- - + +
- {:else if status === 'success' && paymentResult}
@@ -759,9 +794,7 @@
- +
{/if} diff --git a/frontend/src/lib/components/payments/UserPaymentModal.svelte b/frontend/src/lib/components/payments/UserPaymentModal.svelte index 7a79509..31698b5 100644 --- a/frontend/src/lib/components/payments/UserPaymentModal.svelte +++ b/frontend/src/lib/components/payments/UserPaymentModal.svelte @@ -92,32 +92,31 @@ let expiryParts = $derived(parseExpiryParts(newCardExpiry)); let isExpiryInPast = $derived( - expiryParts !== null && (() => { - const expiryDate = new SvelteDate(expiryParts.year, expiryParts.month); - return expiryDate < new SvelteDate(); - })() + expiryParts !== null && + (() => { + const expiryDate = new SvelteDate(expiryParts.year, expiryParts.month); + return expiryDate < new SvelteDate(); + })() ); - let hasInvalidMonth = $derived( - /^\d{2}\/\d{2}$/.test(newCardExpiry) && expiryParts === null - ); + let hasInvalidMonth = $derived(/^\d{2}\/\d{2}$/.test(newCardExpiry) && expiryParts === null); let cardFormValid = $derived( newCardNumber.replace(/\s/g, '').length >= 13 && - expiryParts !== null && - newCardCVC.length >= 3 && - !isExpiryInPast + expiryParts !== null && + newCardCVC.length >= 3 && + !isExpiryInPast ); let cardSelected = $derived( (selectedPaymentMethod !== null && paymentMethods.length > 0) || - (showNewCardForm && cardFormValid) || - (paymentMethods.length === 0 && cardFormValid) + (showNewCardForm && cardFormValid) || + (paymentMethods.length === 0 && cardFormValid) ); let cardValidationError = $derived( - !cardSelected ? ( - selectedPaymentMethod === null && paymentMethods.length > 0 && !showNewCardForm + !cardSelected + ? selectedPaymentMethod === null && paymentMethods.length > 0 && !showNewCardForm ? 'Please select a card' : newCardNumber.replace(/\s/g, '').length < 13 && newCardNumber.length > 0 ? 'Card number too short' @@ -132,7 +131,7 @@ : paymentMethods.length === 0 && !showNewCardForm && newCardNumber.length === 0 ? 'Please enter card details' : 'Please complete all card fields' - ) : null + : null ); // Partial payment amount (in pounds, user enters) @@ -143,9 +142,7 @@ let pollingInterval: ReturnType | null = null; // Derived values - let depositOutstanding = $derived( - booking.deposit_required && !booking.deposit_paid - ); + let depositOutstanding = $derived(booking.deposit_required && !booking.deposit_paid); let totalPaid = $derived( booking.payments @@ -157,23 +154,21 @@ let isScenarioA = $derived(depositOutstanding); - let isScenarioB = $derived( - !depositOutstanding && totalPaid < booking.total_amount - ); + let isScenarioB = $derived(!depositOutstanding && totalPaid < booking.total_amount); let partialAmountNum = $derived(partialAmount === '' ? 0 : parseFloat(partialAmount)); let partialAmountValid = $derived( paymentType === 'partial' && - partialAmount !== '' && - !isNaN(partialAmountNum) && - partialAmountNum > 0 && - partialAmountNum <= amountRemaining && - /^\d+(\.\d{0,2})?$/.test(partialAmount) + partialAmount !== '' && + !isNaN(partialAmountNum) && + partialAmountNum > 0 && + partialAmountNum <= amountRemaining && + /^\d+(\.\d{0,2})?$/.test(partialAmount) ); let partialValidationError = $derived( - paymentType === 'partial' && !partialAmountValid ? ( - partialAmount === '' + paymentType === 'partial' && !partialAmountValid + ? partialAmount === '' ? 'Enter an amount' : !/^\d+(\.\d{0,2})?$/.test(partialAmount) ? 'Invalid amount format' @@ -182,7 +177,7 @@ : partialAmountNum > amountRemaining ? 'Amount exceeds balance' : 'Invalid amount' - ) : null + : null ); let payButtonDisabled = $derived( @@ -190,9 +185,13 @@ ); let payButtonError = $derived( - status === 'processing' ? null : ( - !cardSelected ? cardValidationError : (paymentType === 'partial' ? partialValidationError : null) - ) + status === 'processing' + ? null + : !cardSelected + ? cardValidationError + : paymentType === 'partial' + ? partialValidationError + : null ); function formatCurrency(pence: number): string { @@ -388,7 +387,7 @@ !open && handleClose()}> - + Make a Payment {#if booking.id} @@ -406,7 +405,11 @@
{service.service_name || 'Unknown Service'} - {service.override_price ? formatCurrency(Math.round(service.override_price * 100)) : service.price ? formatCurrency(Math.round(service.price * 100)) : '-'} + {service.override_price + ? formatCurrency(Math.round(service.override_price * 100)) + : service.price + ? formatCurrency(Math.round(service.price * 100)) + : '-'}
{/each} @@ -417,7 +420,8 @@
Total - {formatCurrency(Math.round(booking.total_amount * 100))} + {formatCurrency(Math.round(booking.total_amount * 100))}
Amount Paid @@ -441,9 +445,13 @@ {#if selectedPaymentMethod} {#each paymentMethods as method (method.id)} {#if method.id === selectedPaymentMethod} -
+
-
+
{method.brand}
@@ -474,7 +482,7 @@ {#if canSaveCards} @@ -490,7 +504,10 @@ {#each paymentMethods as method (method.id)}
{:else if status === 'polling'}
-
+

Processing payment...

This may take a few moments

- +
{:else if status === 'success' && paymentResult} @@ -750,9 +796,7 @@
- +
{/if} diff --git a/frontend/src/lib/components/today/CurrentAppointment.svelte b/frontend/src/lib/components/today/CurrentAppointment.svelte index 284b767..6d99433 100644 --- a/frontend/src/lib/components/today/CurrentAppointment.svelte +++ b/frontend/src/lib/components/today/CurrentAppointment.svelte @@ -89,8 +89,18 @@ if (closingTime) { const [ch, cm] = closingTime.split(':').map(Number); const today = new SvelteDate(); - const closing = new SvelteDate(today.getFullYear(), today.getMonth() + 1, today.getDate(), ch, cm, 0); - const minutesToClosing = Math.max(0, Math.floor((closing.getTime() - endTime.getTime()) / 60000)); + const closing = new SvelteDate( + today.getFullYear(), + today.getMonth() + 1, + today.getDate(), + ch, + cm, + 0 + ); + const minutesToClosing = Math.max( + 0, + Math.floor((closing.getTime() - endTime.getTime()) / 60000) + ); if (!nextAppointment) { // No next appointment — show time until closing @@ -213,7 +223,7 @@ {#if activeAppointment} {#if isInProgress}
- + {#if timeRemaining > 29} {#if freeTimeCapped && freeTimeAfter === 0} - + closing after {:else if freeTimeAfter > 0} - - {freeTimeAfter} min free afterwards{#if freeTimeCapped} (closing after){/if} + + {freeTimeAfter} min free afterwards{#if freeTimeCapped} + (closing after){/if} {/if} {/if}
{:else} - + Starts in {timeRemaining} min {/if} @@ -246,7 +257,7 @@ {#if loading}
- +
@@ -280,23 +291,29 @@
-
+
{#if activeAppointment.user?.profile_pic_url} {activeAppointment.user.full_name} {:else} - {@const initials = activeAppointment.user?.full_name?.split(' ').map(n => n[0]).join('') || '?'} + {@const initials = + activeAppointment.user?.full_name + ?.split(' ') + .map((n) => n[0]) + .join('') || '?'}
{initials}
{/if}
-
{activeAppointment.user?.full_name || 'Guest'}
+
+ {activeAppointment.user?.full_name || 'Guest'} +
{activeAppointment.user?.phone || '—'}
{#if activeAppointment.user}
{/if}
- {formatDuration(service.override_duration_minutes ?? service.duration_minutes ?? 0)} + {formatDuration( + service.override_duration_minutes ?? service.duration_minutes ?? 0 + )}
{/each} @@ -413,7 +434,11 @@ Cancel -
{item.data.services.join(', ')} - + {formatDuration(item.data.duration_minutes)}
-
{:else if item.type === 'blocker'} -
-
+
+
{formatTime(item.data.start_time)}
-
- +
+
-
{item.data.description || 'Untitled'}
- +
+ {item.data.description || 'Untitled'} +
+ {formatDuration(item.data.duration_minutes)}
{:else if item.type === 'lunch'} -
-
+
+
{item.data.startLabel}
-
- +
+ @@ -705,7 +852,12 @@
- +

Defaults to "break" if left empty

@@ -718,7 +870,17 @@ {:else if !workingHours.isOpen}

Closed today

{:else} - { + const p = parseSelectValue(startSelectValue); + startHour = p.hour; + startMinute = p.minute; + startPeriod = p.period; + checkOverlappingBookings(); + }} + > {#each availableStartOptions as opt} {/each} @@ -736,7 +898,17 @@ {:else if availableEndOptions.length === 0}

No available end time after selected start

{:else} - { + const p = parseSelectValue(endSelectValue); + endHour = p.hour; + endMinute = p.minute; + endPeriod = p.period; + checkOverlappingBookings(); + }} + > {#each availableEndOptions as opt} {/each} @@ -746,16 +918,36 @@ {#if checkingOverlap}
- - + + Checking for conflicting bookings…
{:else if hasOverlap}
-
- - +
+ + @@ -765,17 +957,32 @@ {#each overlappingBookings as booking (booking.id)}
-
{booking.user?.full_name || 'Unknown'}
+
{booking.user?.full_name || 'Unknown'}
- {new SvelteDate(booking.start_time).toLocaleTimeString('en-GB', { hour: 'numeric', minute: '2-digit', hour12: true })} + {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 booking.services?.length} + {' · '}{booking.services.join(', ')}{/if}
-
- +
+ {#if booking.user?.id} - + {/if}
@@ -785,7 +992,14 @@ {/if}
- + @@ -797,11 +1011,21 @@ Delete time blocker? - This will remove the "{blockerToDelete?.description}" time blocker. This action cannot be undone. + This will remove the "{blockerToDelete?.description}" time blocker. This action cannot be + undone. - { showDeleteAlert = false; blockerToDelete = null; }}>Cancel - Delete + { + showDeleteAlert = false; + blockerToDelete = null; + }}>Cancel + Delete diff --git a/frontend/src/lib/components/today/TodayStats.svelte b/frontend/src/lib/components/today/TodayStats.svelte index 47a53d2..85577c3 100644 --- a/frontend/src/lib/components/today/TodayStats.svelte +++ b/frontend/src/lib/components/today/TodayStats.svelte @@ -57,9 +57,15 @@ d.setDate(d.getDate() - i); const dateStr = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; - const res = await fetch(`/api/scheduling/working-hours?start=${encodeURIComponent(dateStr)}&end=${encodeURIComponent(dateStr)}`, { - headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${authStore.currentToken}` } - }); + const res = await fetch( + `/api/scheduling/working-hours?start=${encodeURIComponent(dateStr)}&end=${encodeURIComponent(dateStr)}`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${authStore.currentToken}` + } + } + ); if (res.ok) { const data: DayWorkingHours[] = await res.json(); @@ -88,11 +94,20 @@ const dateParam = today; const [apptsRes, whRes] = await Promise.all([ fetch(`/api/admin/today/appointments`, { - headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${authStore.currentToken}` } + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${authStore.currentToken}` + } }), - fetch(`/api/scheduling/working-hours?start=${encodeURIComponent(dateParam)}&end=${encodeURIComponent(dateParam)}`, { - headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${authStore.currentToken}` } - }) + fetch( + `/api/scheduling/working-hours?start=${encodeURIComponent(dateParam)}&end=${encodeURIComponent(dateParam)}`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${authStore.currentToken}` + } + } + ) ]); let appointments: TodayAppointment[] = []; @@ -111,34 +126,60 @@ const total = appointments.length; const completed = appointments.filter((a) => a.status === 'completed').length; const inProgress = appointments.filter((a) => a.status === 'in_progress').length; - const upcoming = appointments.filter((a) => a.status === 'confirmed' || a.status === 'pending').length; - const cancelled = appointments.filter((a) => a.status === 'client_cancelled' || a.status === 'we_cancelled').length; + const upcoming = appointments.filter( + (a) => a.status === 'confirmed' || a.status === 'pending' + ).length; + const cancelled = appointments.filter( + (a) => a.status === 'client_cancelled' || a.status === 'we_cancelled' + ).length; const noShow = appointments.filter((a) => a.status === 'no_show').length; let occupancyPercent = 0; if (workingHours?.isOpen) { - const workingMinutes = timeToMinutes(workingHours.endTime) - timeToMinutes(workingHours.startTime); + const workingMinutes = + timeToMinutes(workingHours.endTime) - timeToMinutes(workingHours.startTime); const bookedMinutes = appointments - .filter((a) => a.status !== 'client_cancelled' && a.status !== 'we_cancelled' && a.status !== 'no_show') + .filter( + (a) => + a.status !== 'client_cancelled' && + a.status !== 'we_cancelled' && + a.status !== 'no_show' + ) .reduce((sum, a) => sum + a.duration_minutes, 0); - occupancyPercent = workingMinutes > 0 ? Math.round((bookedMinutes / workingMinutes) * 100) : 0; + occupancyPercent = + workingMinutes > 0 ? Math.round((bookedMinutes / workingMinutes) * 100) : 0; } const { cutoff, spansClosed } = await findLastWorkingDayClose(); const nowISO = new SvelteDate().toISOString(); let bookingsMade = 0; try { - const bmRes = await fetch(`/api/admin/bookings/by-created-range?start=${encodeURIComponent(cutoff)}&end=${encodeURIComponent(nowISO)}`, { - headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${authStore.currentToken}` } - }); + const bmRes = await fetch( + `/api/admin/bookings/by-created-range?start=${encodeURIComponent(cutoff)}&end=${encodeURIComponent(nowISO)}`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${authStore.currentToken}` + } + } + ); if (bmRes.ok) { const data = await bmRes.json(); bookingsMade = (data.bookings || []).length; } - } catch { - } + } catch {} - const newStats: Stats = { total, completed, inProgress, upcoming, cancelled, noShow, occupancyPercent, bookingsMade, bookingsMadeOverClosed: spansClosed }; + const newStats: Stats = { + total, + completed, + inProgress, + upcoming, + cancelled, + noShow, + occupancyPercent, + bookingsMade, + bookingsMadeOverClosed: spansClosed + }; const newJson = JSON.stringify(newStats); if (newJson !== prevJson) { stats = newStats; @@ -223,7 +264,9 @@ {#if stats.bookingsMade > 0}
- Bookings Made{#if stats.bookingsMadeOverClosed}*{/if} + Bookings Made{#if stats.bookingsMadeOverClosed}*{/if} {stats.bookingsMade}
diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte index a005691..df663a6 100644 --- a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte @@ -1,7 +1,7 @@ diff --git a/frontend/src/lib/components/ui/alert-dialog/index.ts b/frontend/src/lib/components/ui/alert-dialog/index.ts index cc281c5..a4439bc 100644 --- a/frontend/src/lib/components/ui/alert-dialog/index.ts +++ b/frontend/src/lib/components/ui/alert-dialog/index.ts @@ -1,13 +1,13 @@ -import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; -import Trigger from "./alert-dialog-trigger.svelte"; -import Title from "./alert-dialog-title.svelte"; -import Action from "./alert-dialog-action.svelte"; -import Cancel from "./alert-dialog-cancel.svelte"; -import Footer from "./alert-dialog-footer.svelte"; -import Header from "./alert-dialog-header.svelte"; -import Overlay from "./alert-dialog-overlay.svelte"; -import Content from "./alert-dialog-content.svelte"; -import Description from "./alert-dialog-description.svelte"; +import { AlertDialog as AlertDialogPrimitive } from 'bits-ui'; +import Trigger from './alert-dialog-trigger.svelte'; +import Title from './alert-dialog-title.svelte'; +import Action from './alert-dialog-action.svelte'; +import Cancel from './alert-dialog-cancel.svelte'; +import Footer from './alert-dialog-footer.svelte'; +import Header from './alert-dialog-header.svelte'; +import Overlay from './alert-dialog-overlay.svelte'; +import Content from './alert-dialog-content.svelte'; +import Description from './alert-dialog-description.svelte'; const Root = AlertDialogPrimitive.Root; const Portal = AlertDialogPrimitive.Portal; @@ -35,5 +35,5 @@ export { Trigger as AlertDialogTrigger, Overlay as AlertDialogOverlay, Content as AlertDialogContent, - Description as AlertDialogDescription, + Description as AlertDialogDescription }; diff --git a/frontend/src/lib/components/ui/avatar/avatar-fallback.svelte b/frontend/src/lib/components/ui/avatar/avatar-fallback.svelte index b911baf..4a452e9 100644 --- a/frontend/src/lib/components/ui/avatar/avatar-fallback.svelte +++ b/frontend/src/lib/components/ui/avatar/avatar-fallback.svelte @@ -12,6 +12,6 @@ diff --git a/frontend/src/lib/components/ui/badge/badge.svelte b/frontend/src/lib/components/ui/badge/badge.svelte index e3164ba..0acc5eb 100644 --- a/frontend/src/lib/components/ui/badge/badge.svelte +++ b/frontend/src/lib/components/ui/badge/badge.svelte @@ -1,36 +1,35 @@ - import type { ComponentProps } from "svelte"; - import type Calendar from "./calendar.svelte"; - import CalendarMonthSelect from "./calendar-month-select.svelte"; - import CalendarYearSelect from "./calendar-year-select.svelte"; - import { DateFormatter, getLocalTimeZone, type DateValue } from "@internationalized/date"; + import type { ComponentProps } from 'svelte'; + import type Calendar from './calendar.svelte'; + import CalendarMonthSelect from './calendar-month-select.svelte'; + import CalendarYearSelect from './calendar-year-select.svelte'; + import { DateFormatter, getLocalTimeZone, type DateValue } from '@internationalized/date'; let { captionLayout, @@ -14,13 +14,13 @@ month, locale, placeholder = $bindable(), - monthIndex = 0, + monthIndex = 0 }: { - captionLayout: ComponentProps["captionLayout"]; - months: ComponentProps["months"]; - monthFormat: ComponentProps["monthFormat"]; - years: ComponentProps["years"]; - yearFormat: ComponentProps["yearFormat"]; + captionLayout: ComponentProps['captionLayout']; + months: ComponentProps['months']; + monthFormat: ComponentProps['monthFormat']; + years: ComponentProps['years']; + yearFormat: ComponentProps['yearFormat']; month: DateValue; placeholder: DateValue | undefined; locale: string; @@ -29,13 +29,13 @@ function formatYear(date: DateValue) { const dateObj = date.toDate(getLocalTimeZone()); - if (typeof yearFormat === "function") return yearFormat(dateObj.getFullYear()); + if (typeof yearFormat === 'function') return yearFormat(dateObj.getFullYear()); return new DateFormatter(locale, { year: yearFormat }).format(dateObj); } function formatMonth(date: DateValue) { const dateObj = date.toDate(getLocalTimeZone()); - if (typeof monthFormat === "function") return monthFormat(dateObj.getMonth() + 1); + if (typeof monthFormat === 'function') return monthFormat(dateObj.getMonth() + 1); return new DateFormatter(locale, { month: monthFormat }).format(dateObj); } @@ -58,15 +58,15 @@ {/snippet} -{#if captionLayout === "dropdown"} +{#if captionLayout === 'dropdown'} {@render MonthSelect()} {@render YearSelect()} -{:else if captionLayout === "dropdown-months"} +{:else if captionLayout === 'dropdown-months'} {@render MonthSelect()} {#if placeholder} {formatYear(placeholder)} {/if} -{:else if captionLayout === "dropdown-years"} +{:else if captionLayout === 'dropdown-years'} {#if placeholder} {formatMonth(placeholder)} {/if} diff --git a/frontend/src/lib/components/ui/calendar/calendar-cell.svelte b/frontend/src/lib/components/ui/calendar/calendar-cell.svelte index 5f295d6..6eb8149 100644 --- a/frontend/src/lib/components/ui/calendar/calendar-cell.svelte +++ b/frontend/src/lib/components/ui/calendar/calendar-cell.svelte @@ -1,6 +1,6 @@ - + diff --git a/frontend/src/lib/components/ui/calendar/calendar-grid.svelte b/frontend/src/lib/components/ui/calendar/calendar-grid.svelte index e0c8627..bfa49dc 100644 --- a/frontend/src/lib/components/ui/calendar/calendar-grid.svelte +++ b/frontend/src/lib/components/ui/calendar/calendar-grid.svelte @@ -1,6 +1,6 @@ -
+
{@render children?.()}
diff --git a/frontend/src/lib/components/ui/calendar/calendar-months.svelte b/frontend/src/lib/components/ui/calendar/calendar-months.svelte index f717a9d..023cad3 100644 --- a/frontend/src/lib/components/ui/calendar/calendar-months.svelte +++ b/frontend/src/lib/components/ui/calendar/calendar-months.svelte @@ -1,6 +1,6 @@ -
+
{@render children?.()}
diff --git a/frontend/src/lib/components/ui/card/card-description.svelte b/frontend/src/lib/components/ui/card/card-description.svelte index 9b20ac7..b46a1ad 100644 --- a/frontend/src/lib/components/ui/card/card-description.svelte +++ b/frontend/src/lib/components/ui/card/card-description.svelte @@ -1,6 +1,6 @@ diff --git a/frontend/src/lib/components/ui/form/form-description.svelte b/frontend/src/lib/components/ui/form/form-description.svelte index a5f42be..034270a 100644 --- a/frontend/src/lib/components/ui/form/form-description.svelte +++ b/frontend/src/lib/components/ui/form/form-description.svelte @@ -1,6 +1,6 @@ - + diff --git a/frontend/src/lib/components/ui/form/form-label.svelte b/frontend/src/lib/components/ui/form/form-label.svelte index 8749360..6d92ce7 100644 --- a/frontend/src/lib/components/ui/form/form-label.svelte +++ b/frontend/src/lib/components/ui/form/form-label.svelte @@ -1,7 +1,7 @@ -{#if type === "file"} +{#if type === 'file'} - import { Label as LabelPrimitive } from "bits-ui"; - import { cn } from "$lib/utils.js"; + import { Label as LabelPrimitive } from 'bits-ui'; + import { cn } from '$lib/utils.js'; let { ref = $bindable(null), @@ -13,7 +13,7 @@ bind:ref data-slot="label" class={cn( - "flex select-none items-center gap-2 text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50", + 'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50', className )} {...restProps} diff --git a/frontend/src/lib/components/ui/select/index.ts b/frontend/src/lib/components/ui/select/index.ts index 9e8d3e9..bfa73d9 100644 --- a/frontend/src/lib/components/ui/select/index.ts +++ b/frontend/src/lib/components/ui/select/index.ts @@ -1,14 +1,14 @@ -import { Select as SelectPrimitive } from "bits-ui"; +import { Select as SelectPrimitive } from 'bits-ui'; -import Group from "./select-group.svelte"; -import Label from "./select-label.svelte"; -import Item from "./select-item.svelte"; -import Content from "./select-content.svelte"; -import Trigger from "./select-trigger.svelte"; -import Separator from "./select-separator.svelte"; -import ScrollDownButton from "./select-scroll-down-button.svelte"; -import ScrollUpButton from "./select-scroll-up-button.svelte"; -import GroupHeading from "./select-group-heading.svelte"; +import Group from './select-group.svelte'; +import Label from './select-label.svelte'; +import Item from './select-item.svelte'; +import Content from './select-content.svelte'; +import Trigger from './select-trigger.svelte'; +import Separator from './select-separator.svelte'; +import ScrollDownButton from './select-scroll-down-button.svelte'; +import ScrollUpButton from './select-scroll-up-button.svelte'; +import GroupHeading from './select-group-heading.svelte'; const Root = SelectPrimitive.Root; @@ -33,5 +33,5 @@ export { Separator as SelectSeparator, ScrollDownButton as SelectScrollDownButton, ScrollUpButton as SelectScrollUpButton, - GroupHeading as SelectGroupHeading, + GroupHeading as SelectGroupHeading }; diff --git a/frontend/src/lib/components/ui/select/select-content.svelte b/frontend/src/lib/components/ui/select/select-content.svelte index dc16d65..f00fc75 100644 --- a/frontend/src/lib/components/ui/select/select-content.svelte +++ b/frontend/src/lib/components/ui/select/select-content.svelte @@ -1,8 +1,8 @@ diff --git a/frontend/src/lib/components/ui/select/select-item.svelte b/frontend/src/lib/components/ui/select/select-item.svelte index 49dbbd7..d527d4c 100644 --- a/frontend/src/lib/components/ui/select/select-item.svelte +++ b/frontend/src/lib/components/ui/select/select-item.svelte @@ -1,7 +1,7 @@ @@ -19,7 +19,7 @@ data-slot="select-trigger" data-size={size} class={cn( - "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 shadow-xs flex w-fit select-none items-center justify-between gap-2 whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", + "flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none select-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground", className )} {...restProps} diff --git a/frontend/src/lib/components/ui/separator/index.ts b/frontend/src/lib/components/ui/separator/index.ts index 82442d2..768efac 100644 --- a/frontend/src/lib/components/ui/separator/index.ts +++ b/frontend/src/lib/components/ui/separator/index.ts @@ -1,7 +1,7 @@ -import Root from "./separator.svelte"; +import Root from './separator.svelte'; export { Root, // - Root as Separator, + Root as Separator }; diff --git a/frontend/src/lib/components/ui/separator/separator.svelte b/frontend/src/lib/components/ui/separator/separator.svelte index 89b2695..3828612 100644 --- a/frontend/src/lib/components/ui/separator/separator.svelte +++ b/frontend/src/lib/components/ui/separator/separator.svelte @@ -1,11 +1,11 @@ @@ -14,7 +14,7 @@ bind:ref data-slot={dataSlot} class={cn( - "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px", + 'shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px', className )} {...restProps} diff --git a/frontend/src/lib/components/ui/skeleton/index.ts b/frontend/src/lib/components/ui/skeleton/index.ts index 186db21..3120ce1 100644 --- a/frontend/src/lib/components/ui/skeleton/index.ts +++ b/frontend/src/lib/components/ui/skeleton/index.ts @@ -1,7 +1,7 @@ -import Root from "./skeleton.svelte"; +import Root from './skeleton.svelte'; export { Root, // - Root as Skeleton, + Root as Skeleton }; diff --git a/frontend/src/lib/components/ui/skeleton/skeleton.svelte b/frontend/src/lib/components/ui/skeleton/skeleton.svelte index c7e3d26..cdd10e0 100644 --- a/frontend/src/lib/components/ui/skeleton/skeleton.svelte +++ b/frontend/src/lib/components/ui/skeleton/skeleton.svelte @@ -1,6 +1,6 @@ diff --git a/frontend/src/lib/components/ui/table/index.ts b/frontend/src/lib/components/ui/table/index.ts index 14695c8..99239ae 100644 --- a/frontend/src/lib/components/ui/table/index.ts +++ b/frontend/src/lib/components/ui/table/index.ts @@ -1,11 +1,11 @@ -import Root from "./table.svelte"; -import Body from "./table-body.svelte"; -import Caption from "./table-caption.svelte"; -import Cell from "./table-cell.svelte"; -import Footer from "./table-footer.svelte"; -import Head from "./table-head.svelte"; -import Header from "./table-header.svelte"; -import Row from "./table-row.svelte"; +import Root from './table.svelte'; +import Body from './table-body.svelte'; +import Caption from './table-caption.svelte'; +import Cell from './table-cell.svelte'; +import Footer from './table-footer.svelte'; +import Head from './table-head.svelte'; +import Header from './table-header.svelte'; +import Row from './table-row.svelte'; export { Root, @@ -24,5 +24,5 @@ export { Footer as TableFooter, Head as TableHead, Header as TableHeader, - Row as TableRow, + Row as TableRow }; diff --git a/frontend/src/lib/components/ui/table/table-body.svelte b/frontend/src/lib/components/ui/table/table-body.svelte index 29e9687..cf720f4 100644 --- a/frontend/src/lib/components/ui/table/table-body.svelte +++ b/frontend/src/lib/components/ui/table/table-body.svelte @@ -1,6 +1,6 @@ @@ -15,7 +15,7 @@ bind:this={ref} data-slot={dataSlot} class={cn( - "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 field-sizing-content shadow-xs flex min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", + 'flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40', className )} bind:value diff --git a/frontend/src/lib/stores/auth.svelte.ts b/frontend/src/lib/stores/auth.svelte.ts index 423828a..405d7e4 100644 --- a/frontend/src/lib/stores/auth.svelte.ts +++ b/frontend/src/lib/stores/auth.svelte.ts @@ -33,11 +33,14 @@ class AuthStore { if (browser) { this.initializeAuth(); // Check token refresh every hour - setInterval(() => { - if (this.token) { - this.refreshTokenIfNeeded(); - } - }, 60 * 60 * 1000); + setInterval( + () => { + if (this.token) { + this.refreshTokenIfNeeded(); + } + }, + 60 * 60 * 1000 + ); } } diff --git a/frontend/src/lib/types/booking.ts b/frontend/src/lib/types/booking.ts index e8832ad..51a7468 100644 --- a/frontend/src/lib/types/booking.ts +++ b/frontend/src/lib/types/booking.ts @@ -180,4 +180,3 @@ export interface CampaignStats { total_discount_amount: number; booking_count: number; } - diff --git a/frontend/src/lib/types/index.ts b/frontend/src/lib/types/index.ts index 1b2b958..938c96a 100644 --- a/frontend/src/lib/types/index.ts +++ b/frontend/src/lib/types/index.ts @@ -55,4 +55,4 @@ export interface PaymentSummary { export interface CheckoutResponse { checkout_id: string; status: 'PENDING' | 'COMPLETED' | 'FAILED'; -} \ No newline at end of file +} diff --git a/frontend/src/lib/utils/timeSlots.ts b/frontend/src/lib/utils/timeSlots.ts index 52dc216..14b3905 100644 --- a/frontend/src/lib/utils/timeSlots.ts +++ b/frontend/src/lib/utils/timeSlots.ts @@ -1,6 +1,10 @@ import { SvelteDate } from 'svelte/reactivity'; import { CalendarDate } from '@internationalized/date'; -import { extractBookedSlots, getLunchProtectionForSlots, type LunchProtectionResult } from '$lib/lunchProtection'; +import { + extractBookedSlots, + getLunchProtectionForSlots, + type LunchProtectionResult +} from '$lib/lunchProtection'; export interface TimeSlotGroup { type: 'available' | 'unavailable'; @@ -54,14 +58,21 @@ export function normalizeTime(time: string): string { } export function getDayWithOrdinal(date: CalendarDate): string { - const monthName = new SvelteDate(date.year, date.month - 1, date.day).toLocaleDateString('en-GB', { month: 'long' }); + const monthName = new SvelteDate(date.year, date.month - 1, date.day).toLocaleDateString( + 'en-GB', + { month: 'long' } + ); const day = date.day; if (day > 3 && day < 21) return monthName + ' ' + day + 'th'; switch (day % 10) { - case 1: return monthName + ' ' + day + 'st'; - case 2: return monthName + ' ' + day + 'nd'; - case 3: return monthName + ' ' + day + 'rd'; - default: return monthName + ' ' + day + 'th'; + case 1: + return monthName + ' ' + day + 'st'; + case 2: + return monthName + ' ' + day + 'nd'; + case 3: + return monthName + ' ' + day + 'rd'; + default: + return monthName + ' ' + day + 'th'; } } @@ -79,7 +90,14 @@ export function buildLunchProtection( if (!dayWH?.isOpen || !dayAH?.slots) return new Map(); const existingBookings = extractBookedSlots(dayWH.startTime, dayWH.endTime, dayAH.slots); - return getLunchProtectionForSlots(dayWH.startTime, dayWH.endTime, existingBookings, duration, 15, isAdmin); + return getLunchProtectionForSlots( + dayWH.startTime, + dayWH.endTime, + existingBookings, + duration, + 15, + isAdmin + ); } export function generateAvailableTimeSlots( @@ -150,7 +168,13 @@ export function generateGroupedTimeSlots( startTotalMinutes = Math.max(startTotalMinutes, Math.ceil((currentMinutes + 15) / 15) * 15); } - const availableSlots = generateAvailableTimeSlots(date, workingHours, availableHours, duration, protection); + const availableSlots = generateAvailableTimeSlots( + date, + workingHours, + availableHours, + duration, + protection + ); let currentUnavailableStart: string | null = null; let lastAvailableEndTime: string | null = null; @@ -164,7 +188,12 @@ export function generateGroupedTimeSlots( const unavailStart = lastAvailableEndTime || currentUnavailableStart; const groupEnd = calculatePreviousTime(timeStr); if (timeToMinutes(unavailStart) < timeToMinutes(groupEnd)) { - grouped.push({ type: 'unavailable', startTime: unavailStart, endTime: groupEnd, isGrouped: true }); + grouped.push({ + type: 'unavailable', + startTime: unavailStart, + endTime: groupEnd, + isGrouped: true + }); } currentUnavailableStart = null; } diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 3633b67..5976dd8 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -54,7 +54,7 @@
{#if authStore.currentUser?.role === 'unverified_email'} -
+
Please verify your email address to continue. Didn't recieve the email? Check your spam folder, or {#if canSaveCards} - + + + + + Cards + {/if} +
{:else}
{#each savedCards as card (card.id)}
-
+
{card.brand}
@@ -1608,25 +1636,25 @@ {#if canSaveCards} - + + + + + Cards + {/if}