@asksable/site-connector 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -7
- package/dist/analytics.d.ts +18 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +94 -0
- package/dist/analytics.js.map +1 -0
- package/dist/booking-widget.d.ts.map +1 -1
- package/dist/booking-widget.js +226 -53
- package/dist/booking-widget.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/provider.d.ts +4 -0
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +70 -2
- package/dist/provider.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -2
package/dist/booking-widget.js
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { useEffect, useMemo, useRef, useState, } from 'react';
|
|
3
3
|
import { createPortal } from 'react-dom';
|
|
4
4
|
import { loadStripe } from '@stripe/stripe-js';
|
|
5
|
-
import { Elements, PaymentElement, useElements, useStripe } from '@stripe/react-stripe-js';
|
|
5
|
+
import { Elements, PaymentElement, useElements, useStripe, } from '@stripe/react-stripe-js';
|
|
6
6
|
import { Skeleton } from 'boneyard-js/react';
|
|
7
7
|
// Side-effect: registers all named bones with the boneyard runtime.
|
|
8
8
|
// Regenerate by running `npx boneyard-js build` against a host site
|
|
9
9
|
// that mounts <BookingWidgetPanel /> in its loaded state.
|
|
10
10
|
import './bones/registry.js';
|
|
11
11
|
import { BookingWidgetPlaceholder } from './booking-widget-placeholder.js';
|
|
12
|
-
import { useSableSiteClient, useSableSiteConfig, useTranslation } from './provider.js';
|
|
13
|
-
import { DEFAULT_LOCALE, localeToIntl, pickLocaleField } from './translations.js';
|
|
12
|
+
import { useSableSiteAnalytics, useSableSiteClient, useSableSiteConfig, useTranslation, } from './provider.js';
|
|
13
|
+
import { DEFAULT_LOCALE, localeToIntl, pickLocaleField, } from './translations.js';
|
|
14
14
|
const stripePromises = new Map();
|
|
15
15
|
function getStripePromise(publishableKey, connectAccountId) {
|
|
16
16
|
const key = `${publishableKey}:${connectAccountId}`;
|
|
17
17
|
const cached = stripePromises.get(key);
|
|
18
18
|
if (cached)
|
|
19
19
|
return cached;
|
|
20
|
-
const promise = loadStripe(publishableKey, {
|
|
20
|
+
const promise = loadStripe(publishableKey, {
|
|
21
|
+
stripeAccount: connectAccountId,
|
|
22
|
+
});
|
|
21
23
|
stripePromises.set(key, promise);
|
|
22
24
|
return promise;
|
|
23
25
|
}
|
|
@@ -52,6 +54,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
52
54
|
const client = useSableSiteClient();
|
|
53
55
|
const { siteSlug } = useSableSiteConfig();
|
|
54
56
|
const { t, locale } = useTranslation();
|
|
57
|
+
const analytics = useSableSiteAnalytics();
|
|
55
58
|
const intlLocale = localeToIntl(locale);
|
|
56
59
|
const [setup, setSetup] = useState(null);
|
|
57
60
|
const [isSetupLoading, setIsSetupLoading] = useState(true);
|
|
@@ -104,7 +107,11 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
104
107
|
// Dev-only: force viewState to 'details' when previewing a payment
|
|
105
108
|
// variant, since the payment panel renders inside the details form.
|
|
106
109
|
useEffect(() => {
|
|
107
|
-
|
|
110
|
+
analytics.capture('sable_booking_widget_viewed', { mode });
|
|
111
|
+
}, [analytics, mode]);
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (__devForceState === 'payment-full' ||
|
|
114
|
+
__devForceState === 'payment-deposit') {
|
|
108
115
|
setViewState('details');
|
|
109
116
|
}
|
|
110
117
|
}, [__devForceState]);
|
|
@@ -177,7 +184,9 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
177
184
|
})
|
|
178
185
|
.catch((nextError) => {
|
|
179
186
|
if (!cancelled) {
|
|
180
|
-
setError(nextError instanceof Error
|
|
187
|
+
setError(nextError instanceof Error
|
|
188
|
+
? nextError.message
|
|
189
|
+
: 'Unable to load booking setup.');
|
|
181
190
|
}
|
|
182
191
|
})
|
|
183
192
|
.finally(() => {
|
|
@@ -189,9 +198,12 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
189
198
|
cancelled = true;
|
|
190
199
|
};
|
|
191
200
|
}, [client, siteSlug]);
|
|
192
|
-
const selectedService = useMemo(() => setup?.services.find((service) => service._id === selectedServiceId) ??
|
|
201
|
+
const selectedService = useMemo(() => setup?.services.find((service) => service._id === selectedServiceId) ??
|
|
202
|
+
null, [selectedServiceId, setup]);
|
|
193
203
|
const selectedServiceRequiresSlot = selectedService?.publicBookingMode !== 'async';
|
|
194
|
-
const selectedServicePath = selectedServiceRequiresSlot
|
|
204
|
+
const selectedServicePath = selectedServiceRequiresSlot
|
|
205
|
+
? 'scheduled'
|
|
206
|
+
: 'async';
|
|
195
207
|
const selectedServiceHasIntakeForm = hasIntakeFormSections(selectedService);
|
|
196
208
|
const isIntakeComplete = useMemo(() => !selectedService?.intakeForm ||
|
|
197
209
|
areRequiredIntakeFieldsComplete(selectedService.intakeForm, intakeResponses, selectedServicePath), [intakeResponses, selectedService?.intakeForm, selectedServicePath]);
|
|
@@ -217,7 +229,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
217
229
|
const uncategorizedKey = '__uncategorized__';
|
|
218
230
|
for (const service of visibleServices) {
|
|
219
231
|
const category = service.categoryId != null
|
|
220
|
-
? categoriesById.get(service.categoryId) ?? null
|
|
232
|
+
? (categoriesById.get(service.categoryId) ?? null)
|
|
221
233
|
: null;
|
|
222
234
|
const key = category?._id ?? uncategorizedKey;
|
|
223
235
|
const existing = groups.get(key);
|
|
@@ -286,7 +298,8 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
286
298
|
// wiped on the first render before the staff list arrives.
|
|
287
299
|
if (availableStaff.length === 0)
|
|
288
300
|
return;
|
|
289
|
-
if (selectedStaffId &&
|
|
301
|
+
if (selectedStaffId &&
|
|
302
|
+
!availableStaff.some((staffMember) => staffMember._id === selectedStaffId)) {
|
|
290
303
|
setSelectedStaffId(null);
|
|
291
304
|
}
|
|
292
305
|
}, [availableStaff, selectedStaffId]);
|
|
@@ -351,7 +364,9 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
351
364
|
if (!cancelled) {
|
|
352
365
|
setAvailabilityByDate(new Map());
|
|
353
366
|
setSelectedDate(null);
|
|
354
|
-
setError(nextError instanceof Error
|
|
367
|
+
setError(nextError instanceof Error
|
|
368
|
+
? nextError.message
|
|
369
|
+
: t('calendarLoadError'));
|
|
355
370
|
}
|
|
356
371
|
})
|
|
357
372
|
.finally(() => {
|
|
@@ -362,7 +377,15 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
362
377
|
return () => {
|
|
363
378
|
cancelled = true;
|
|
364
379
|
};
|
|
365
|
-
}, [
|
|
380
|
+
}, [
|
|
381
|
+
calendarMonth,
|
|
382
|
+
client,
|
|
383
|
+
selectedDate,
|
|
384
|
+
selectedService?.publicBookingMode,
|
|
385
|
+
selectedServiceId,
|
|
386
|
+
selectedStaffId,
|
|
387
|
+
siteSlug,
|
|
388
|
+
]);
|
|
366
389
|
useEffect(() => {
|
|
367
390
|
if (!selectedServiceId || selectedService?.publicBookingMode === 'async') {
|
|
368
391
|
return;
|
|
@@ -375,7 +398,14 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
375
398
|
calendarMonth: nextMonth,
|
|
376
399
|
cacheRef: availabilityCacheRef,
|
|
377
400
|
});
|
|
378
|
-
}, [
|
|
401
|
+
}, [
|
|
402
|
+
calendarMonth,
|
|
403
|
+
client,
|
|
404
|
+
selectedService?.publicBookingMode,
|
|
405
|
+
selectedServiceId,
|
|
406
|
+
selectedStaffId,
|
|
407
|
+
siteSlug,
|
|
408
|
+
]);
|
|
379
409
|
useEffect(() => {
|
|
380
410
|
setSelectedSlot(null);
|
|
381
411
|
setPendingSlotKey(null);
|
|
@@ -394,7 +424,12 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
394
424
|
else {
|
|
395
425
|
setViewState('slots');
|
|
396
426
|
}
|
|
397
|
-
}, [
|
|
427
|
+
}, [
|
|
428
|
+
selectedDate,
|
|
429
|
+
selectedService?.publicBookingMode,
|
|
430
|
+
selectedServiceId,
|
|
431
|
+
selectedStaffId,
|
|
432
|
+
]);
|
|
398
433
|
useEffect(() => {
|
|
399
434
|
setIntakeResponses({});
|
|
400
435
|
setQuote(null);
|
|
@@ -424,7 +459,9 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
424
459
|
})
|
|
425
460
|
.catch((nextError) => {
|
|
426
461
|
if (!cancelled) {
|
|
427
|
-
setError(nextError instanceof Error
|
|
462
|
+
setError(nextError instanceof Error
|
|
463
|
+
? nextError.message
|
|
464
|
+
: 'Unable to calculate price.');
|
|
428
465
|
}
|
|
429
466
|
})
|
|
430
467
|
.finally(() => {
|
|
@@ -436,7 +473,14 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
436
473
|
cancelled = true;
|
|
437
474
|
window.clearTimeout(handle);
|
|
438
475
|
};
|
|
439
|
-
}, [
|
|
476
|
+
}, [
|
|
477
|
+
client,
|
|
478
|
+
intakeResponses,
|
|
479
|
+
isIntakeComplete,
|
|
480
|
+
selectedService?.pricingMode,
|
|
481
|
+
selectedServiceId,
|
|
482
|
+
siteSlug,
|
|
483
|
+
]);
|
|
440
484
|
useEffect(() => {
|
|
441
485
|
return () => {
|
|
442
486
|
if (!holdId) {
|
|
@@ -576,9 +620,15 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
576
620
|
}
|
|
577
621
|
return set;
|
|
578
622
|
}, [availabilityByDate, selectedDate]);
|
|
579
|
-
const selectedDateSlots = selectedDate
|
|
580
|
-
|
|
581
|
-
|
|
623
|
+
const selectedDateSlots = selectedDate
|
|
624
|
+
? (filteredAvailabilityByDate.get(selectedDate) ?? [])
|
|
625
|
+
: [];
|
|
626
|
+
const selectedHeldStaff = availableStaff.find((staffMember) => staffMember._id === heldStaffId) ??
|
|
627
|
+
selectedStaff ??
|
|
628
|
+
null;
|
|
629
|
+
const holdSecondsRemaining = holdExpiresAt !== null
|
|
630
|
+
? Math.max(0, Math.floor((holdExpiresAt - holdNow) / 1000))
|
|
631
|
+
: null;
|
|
582
632
|
const monthOptions = useMemo(() => getMonthOptions(calendarMonth, 4, intlLocale), [calendarMonth, intlLocale]);
|
|
583
633
|
const calendarDays = useMemo(() => buildCalendarDays(calendarMonth), [calendarMonth]);
|
|
584
634
|
// Mobile 4-step gates. canAdvanceStepN = "the user can move PAST
|
|
@@ -623,11 +673,15 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
623
673
|
const slotKey = `${slot.time}-${slot.endTime}`;
|
|
624
674
|
const isPending = pendingSlotKey === slotKey;
|
|
625
675
|
const isActive = isPending ||
|
|
626
|
-
(selectedSlot?.time === slot.time &&
|
|
627
|
-
|
|
676
|
+
(selectedSlot?.time === slot.time &&
|
|
677
|
+
selectedSlot?.endTime === slot.endTime);
|
|
678
|
+
return (_jsx("button", { type: "button", className: `bw-slot${isActive ? ' is-active' : ''}${isPending ? ' is-pending' : ''}`, style: { '--bw-slot-i': index }, onClick: () => void handleSlotSelect(slot), disabled: isSubmitting, children: isPending
|
|
679
|
+
? t('slotSecuring')
|
|
680
|
+
: formatTimeLabel(slot.time, intlLocale) }, slotKey));
|
|
628
681
|
}) })) : (_jsx("p", { className: "bw-no-slots", children: t('slotsEmptyForDate') }))) : null }, slotsAreaKey));
|
|
629
682
|
const canSubmit = Boolean(selectedService &&
|
|
630
|
-
(!selectedServiceRequiresSlot ||
|
|
683
|
+
(!selectedServiceRequiresSlot ||
|
|
684
|
+
(selectedDate && selectedSlot && holdId)) &&
|
|
631
685
|
isIntakeComplete &&
|
|
632
686
|
(selectedService.pricingMode !== 'calculated' || quote) &&
|
|
633
687
|
customerName.trim() &&
|
|
@@ -668,7 +722,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
668
722
|
return null;
|
|
669
723
|
const visibleBlockers = submitBlockers.slice(0, 5);
|
|
670
724
|
const hiddenCount = submitBlockers.length - visibleBlockers.length;
|
|
671
|
-
return (_jsxs("div", { className: "bw-submit-help", role: "status", "aria-live": "polite", children: [_jsx("span", { className: "bw-submit-help-title", children: isQuoteLoading ? t('helpCalculating') : t('helpComplete') }), !isQuoteLoading && visibleBlockers.length > 0 ? (_jsxs("ul", { children: [visibleBlockers.map((blocker) => (_jsx("li", { children: blocker }, blocker))), hiddenCount > 0 ? _jsx("li", { children: t('helpMoreFields', { count: hiddenCount }) }) : null] })) : null] }));
|
|
725
|
+
return (_jsxs("div", { className: "bw-submit-help", role: "status", "aria-live": "polite", children: [_jsx("span", { className: "bw-submit-help-title", children: isQuoteLoading ? t('helpCalculating') : t('helpComplete') }), !isQuoteLoading && visibleBlockers.length > 0 ? (_jsxs("ul", { children: [visibleBlockers.map((blocker) => (_jsx("li", { children: blocker }, blocker))), hiddenCount > 0 ? (_jsx("li", { children: t('helpMoreFields', { count: hiddenCount }) })) : null] })) : null] }));
|
|
672
726
|
}
|
|
673
727
|
function renderContactFields(idSuffix) {
|
|
674
728
|
const nameId = `bw-name${idSuffix}`;
|
|
@@ -689,6 +743,12 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
689
743
|
return (_jsxs(_Fragment, { children: [contactFields, intakeFields] }));
|
|
690
744
|
}
|
|
691
745
|
function handleServiceSelect(service) {
|
|
746
|
+
analytics.capture('sable_booking_service_selected', {
|
|
747
|
+
service_id: service._id,
|
|
748
|
+
service_name: pickLocaleField(service, 'name', locale) ?? service.name,
|
|
749
|
+
booking_mode: service.publicBookingMode ?? 'scheduled',
|
|
750
|
+
requires_payment: service.requiresPayment === true,
|
|
751
|
+
});
|
|
692
752
|
if (holdId) {
|
|
693
753
|
void client
|
|
694
754
|
.releasePublicBookingHold({ siteSlug, holdId, sessionToken })
|
|
@@ -810,7 +870,8 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
810
870
|
async function handleConfirmBooking() {
|
|
811
871
|
if (!selectedServiceId ||
|
|
812
872
|
!selectedService ||
|
|
813
|
-
(selectedServiceRequiresSlot &&
|
|
873
|
+
(selectedServiceRequiresSlot &&
|
|
874
|
+
(!selectedDate || !selectedSlot || !holdId))) {
|
|
814
875
|
return;
|
|
815
876
|
}
|
|
816
877
|
setIsSubmitting(true);
|
|
@@ -832,6 +893,11 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
832
893
|
try {
|
|
833
894
|
await onRescheduleSubmit({ newStartTime, newEndTime });
|
|
834
895
|
setSuccess(t('successRescheduleConfirmed'));
|
|
896
|
+
analytics.capture('sable_booking_rescheduled', {
|
|
897
|
+
service_id: selectedServiceId,
|
|
898
|
+
service_name: pickLocaleField(selectedService, 'name', locale) ??
|
|
899
|
+
selectedService.name,
|
|
900
|
+
});
|
|
835
901
|
setMobileStep(4);
|
|
836
902
|
}
|
|
837
903
|
catch (nextError) {
|
|
@@ -849,11 +915,16 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
849
915
|
siteSlug,
|
|
850
916
|
serviceId: selectedServiceId,
|
|
851
917
|
staffMemberId: selectedServiceRequiresSlot
|
|
852
|
-
? heldStaffId ??
|
|
918
|
+
? (heldStaffId ??
|
|
919
|
+
selectedStaffId ??
|
|
920
|
+
selectedSlot?.availableStaffIds[0])
|
|
853
921
|
: undefined,
|
|
854
922
|
startTime: newStartTime,
|
|
855
923
|
endTime: newEndTime,
|
|
856
|
-
timezone: selectedHeldStaff?.timezone ??
|
|
924
|
+
timezone: selectedHeldStaff?.timezone ??
|
|
925
|
+
availableStaff[0]?.timezone ??
|
|
926
|
+
setup?.workspaceTimezone ??
|
|
927
|
+
'UTC',
|
|
857
928
|
deliveryMethod: selectedService.deliveryMethods[0] ?? 'in-person',
|
|
858
929
|
customerName: customerName.trim(),
|
|
859
930
|
customerEmail: customerEmail.trim(),
|
|
@@ -862,15 +933,34 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
862
933
|
intakeResponses,
|
|
863
934
|
quotedTotalCents: quote?.totalCents,
|
|
864
935
|
bookingHoldId: selectedServiceRequiresSlot ? holdId : undefined,
|
|
865
|
-
bookingSessionToken: selectedServiceRequiresSlot
|
|
936
|
+
bookingSessionToken: selectedServiceRequiresSlot
|
|
937
|
+
? sessionToken
|
|
938
|
+
: undefined,
|
|
866
939
|
});
|
|
867
940
|
if (created.payment) {
|
|
941
|
+
analytics.capture('sable_booking_payment_started', {
|
|
942
|
+
appointment_id: created.appointmentId,
|
|
943
|
+
service_id: selectedServiceId,
|
|
944
|
+
service_name: pickLocaleField(selectedService, 'name', locale) ??
|
|
945
|
+
selectedService.name,
|
|
946
|
+
amount_cents: created.payment.amountCents,
|
|
947
|
+
currency: created.payment.currency,
|
|
948
|
+
});
|
|
868
949
|
setPayment(created.payment);
|
|
869
950
|
setPaymentAppointmentId(created.appointmentId);
|
|
870
951
|
setError(null);
|
|
871
952
|
return;
|
|
872
953
|
}
|
|
873
954
|
setSuccess(t('successBookingConfirmed'));
|
|
955
|
+
analytics.capture('sable_booking_created', {
|
|
956
|
+
appointment_id: created.appointmentId,
|
|
957
|
+
service_id: selectedServiceId,
|
|
958
|
+
service_name: pickLocaleField(selectedService, 'name', locale) ??
|
|
959
|
+
selectedService.name,
|
|
960
|
+
booking_mode: selectedService.publicBookingMode ?? 'scheduled',
|
|
961
|
+
requires_payment: selectedService.requiresPayment === true,
|
|
962
|
+
quoted_total_cents: quote?.totalCents ?? null,
|
|
963
|
+
});
|
|
874
964
|
setSelectedSlot(null);
|
|
875
965
|
setPendingSlotKey(null);
|
|
876
966
|
setHoldId(null);
|
|
@@ -887,7 +977,9 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
887
977
|
setMobileStep(4);
|
|
888
978
|
}
|
|
889
979
|
catch (nextError) {
|
|
890
|
-
setError(nextError instanceof Error
|
|
980
|
+
setError(nextError instanceof Error
|
|
981
|
+
? nextError.message
|
|
982
|
+
: t('errorConfirmFailed'));
|
|
891
983
|
}
|
|
892
984
|
finally {
|
|
893
985
|
setIsSubmitting(false);
|
|
@@ -909,22 +1001,30 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
909
1001
|
}, children: t('cancelledCta') })] }) }));
|
|
910
1002
|
}
|
|
911
1003
|
if (success || forcedState === 'success') {
|
|
912
|
-
return (_jsx("section", { className: "bw", children: _jsxs("div", { className: "bw-done", children: [_jsx("div", { className: "bw-done-icon", children: _jsx(CheckIcon, {}) }), _jsx("h3", { className: "bw-done-title", children: isReschedule
|
|
1004
|
+
return (_jsx("section", { className: "bw", children: _jsxs("div", { className: "bw-done", children: [_jsx("div", { className: "bw-done-icon", children: _jsx(CheckIcon, {}) }), _jsx("h3", { className: "bw-done-title", children: isReschedule
|
|
1005
|
+
? t('successTitleReschedule')
|
|
1006
|
+
: t('successTitleCreate') }), _jsx("p", { className: "bw-done-text", children: isReschedule ? t('successBodyReschedule') : t('successBodyCreate') }), isReschedule ? null : (_jsx("button", { type: "button", className: "bw-btn-primary", onClick: () => {
|
|
913
1007
|
window.location.assign(successRedirectHref);
|
|
914
1008
|
}, children: t('successCtaBackHome') }))] }) }));
|
|
915
1009
|
}
|
|
916
|
-
return (_jsx(Skeleton, { name: "booking-widget", loading: false, children: _jsxs("section", { className: "bw", "data-view-state": viewState, ref: widgetRootRef, children: [mobileHeader ? _jsx("div", { className: "bw-mobile-header", children: mobileHeader }) : null, _jsxs("div", { className: "bw-header", children: [_jsxs("div", { className: "bw-header-row", children: [_jsx("h2", { className: "bw-title bw-title--desktop", children: title ||
|
|
1010
|
+
return (_jsx(Skeleton, { name: "booking-widget", loading: false, children: _jsxs("section", { className: "bw", "data-view-state": viewState, ref: widgetRootRef, children: [mobileHeader ? (_jsx("div", { className: "bw-mobile-header", children: mobileHeader })) : null, _jsxs("div", { className: "bw-header", children: [_jsxs("div", { className: "bw-header-row", children: [_jsx("h2", { className: "bw-title bw-title--desktop", children: title ||
|
|
917
1011
|
(isReschedule
|
|
918
1012
|
? rescheduleContext?.customerName
|
|
919
|
-
? t('rescheduleTitleNamed', {
|
|
1013
|
+
? t('rescheduleTitleNamed', {
|
|
1014
|
+
name: rescheduleContext.customerName,
|
|
1015
|
+
})
|
|
920
1016
|
: t('rescheduleTitle')
|
|
921
1017
|
: t('pageTitle')) }), _jsx("h2", { className: "bw-title bw-title--mobile", children: isReschedule
|
|
922
1018
|
? rescheduleContext?.customerName
|
|
923
|
-
? t('rescheduleTitleNamed', {
|
|
1019
|
+
? t('rescheduleTitleNamed', {
|
|
1020
|
+
name: rescheduleContext.customerName,
|
|
1021
|
+
})
|
|
924
1022
|
: t('rescheduleTitle')
|
|
925
1023
|
: mobileStepTitle(mobileStep, t) }), mobileStep > 1 ? (_jsx("div", { className: "bw-progress", "aria-hidden": "true", children: (selectedServiceRequiresSlot
|
|
926
1024
|
? MOBILE_PROGRESS_STEPS_SCHEDULED
|
|
927
|
-
: MOBILE_PROGRESS_STEPS_ASYNC).map((step) => (_jsx("span", { className: `bw-dot${mobileStep >= step ? ' is-filled' : ''}` }, step))) })) : null] }), description ? _jsx("p", { className: "bw-description", children: description }) : null] }), _jsxs("div", { className: "bw-content", children: [_jsx("div", { className: "bw-body", "data-mobile-step": mobileStep, children: _jsxs("div", { className: "bw-body-inner", children: [_jsxs("div", { className: "bw-col bw-col--left", children: [_jsxs("div", { className: "bw-step-1", children: [_jsx("div", { className: "bw-service-picker", children: selectedService && !isEditingService ? (_jsxs("div", { className: "bw-svc-selected", children: [_jsxs("div", { className: "bw-svc-selected-header", children: [_jsx("label", { className: "bw-label", children: t('selectedService') }), _jsx("button", { type: "button", className: "bw-svc-change", onClick: () => setIsEditingService(true), children: t('btnChange') })] }), _jsxs("div", { className: "bw-svc-row bw-svc-row--readonly", children: [selectedService.image ? (_jsxs("span", { className: "bw-svc-image is-checked", children: [_jsx("img", { src: selectedService.image.url, alt: selectedService.image.alt ??
|
|
1025
|
+
: MOBILE_PROGRESS_STEPS_ASYNC).map((step) => (_jsx("span", { className: `bw-dot${mobileStep >= step ? ' is-filled' : ''}` }, step))) })) : null] }), description ? _jsx("p", { className: "bw-description", children: description }) : null] }), _jsxs("div", { className: "bw-content", children: [_jsx("div", { className: "bw-body", "data-mobile-step": mobileStep, children: _jsxs("div", { className: "bw-body-inner", children: [_jsxs("div", { className: "bw-col bw-col--left", children: [_jsxs("div", { className: "bw-step-1", children: [_jsx("div", { className: "bw-service-picker", children: selectedService && !isEditingService ? (_jsxs("div", { className: "bw-svc-selected", children: [_jsxs("div", { className: "bw-svc-selected-header", children: [_jsx("label", { className: "bw-label", children: t('selectedService') }), _jsx("button", { type: "button", className: "bw-svc-change", onClick: () => setIsEditingService(true), children: t('btnChange') })] }), _jsxs("div", { className: "bw-svc-row bw-svc-row--readonly", children: [selectedService.image ? (_jsxs("span", { className: "bw-svc-image is-checked", children: [_jsx("img", { src: selectedService.image.url, alt: selectedService.image.alt ??
|
|
1026
|
+
pickLocaleField(selectedService, 'name', locale) ??
|
|
1027
|
+
selectedService.name, loading: "lazy" }), _jsx("span", { className: "bw-svc-image-badge", children: _jsx(CheckIcon, {}) })] })) : (_jsx("span", { className: "bw-check is-checked", children: _jsx(CheckIcon, {}) })), _jsxs("span", { className: "bw-svc-info", children: [_jsx("span", { className: "bw-svc-name", children: pickLocaleField(selectedService, 'name', locale) ?? selectedService.name }), (pickLocaleField(selectedService, 'description', locale) ?? selectedService.description) ? (_jsx("span", { className: "bw-svc-desc", children: pickLocaleField(selectedService, 'description', locale) ?? selectedService.description })) : null, _jsxs("span", { className: "bw-svc-meta-row", children: [_jsx("span", { className: "bw-svc-meta", children: formatDuration(selectedService.durationMinutes) }), _jsx("span", { className: "bw-svc-meta-dot", "aria-hidden": "true", children: "\u00B7" }), _jsx("span", { className: "bw-svc-price", children: formatServicePrice(selectedService, undefined, intlLocale) })] })] })] })] })) : (_jsxs(_Fragment, { children: [_jsx("label", { className: "bw-label", children: t('selectService') }), _jsx("div", { className: "bw-svc-scroll is-open", children: _jsx("div", { className: "bw-svc-scroll-wrap", children: _jsx("div", { className: "bw-svc-scroll-inner", children: serviceGroups.map((group) => (_jsxs("div", { className: "bw-svc-group", children: [group.categoryName ? (_jsx("div", { className: "bw-svc-category", children: group.categoryName })) : null, group.services.map((service) => {
|
|
928
1028
|
const isActive = selectedServiceId === service._id;
|
|
929
1029
|
return (_jsxs("button", { type: "button", className: "bw-svc-row", onMouseEnter: () => {
|
|
930
1030
|
void prefetchAvailability({
|
|
@@ -944,8 +1044,12 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
944
1044
|
});
|
|
945
1045
|
}, onClick: () => {
|
|
946
1046
|
handleServiceSelect(service);
|
|
947
|
-
}, children: [service.image ? (_jsxs("span", { className: `bw-svc-image${isActive ? ' is-checked' : ''}`, children: [_jsx("img", { src: service.image.url, alt: service.image.alt ??
|
|
948
|
-
|
|
1047
|
+
}, children: [service.image ? (_jsxs("span", { className: `bw-svc-image${isActive ? ' is-checked' : ''}`, children: [_jsx("img", { src: service.image.url, alt: service.image.alt ??
|
|
1048
|
+
pickLocaleField(service, 'name', locale) ??
|
|
1049
|
+
service.name, loading: "lazy" }), isActive ? (_jsx("span", { className: "bw-svc-image-badge", children: _jsx(CheckIcon, {}) })) : null] })) : (_jsx("span", { className: `bw-check${isActive ? ' is-checked' : ''}`, children: isActive ? _jsx(CheckIcon, {}) : null })), _jsxs("span", { className: "bw-svc-info", children: [_jsx("span", { className: "bw-svc-name", children: pickLocaleField(service, 'name', locale) ?? service.name }), (pickLocaleField(service, 'description', locale) ?? service.description) ? (_jsx("span", { className: "bw-svc-desc", children: pickLocaleField(service, 'description', locale) ?? service.description })) : null, _jsxs("span", { className: "bw-svc-meta-row", children: [_jsx("span", { className: "bw-svc-meta", children: formatDuration(service.durationMinutes) }), _jsx("span", { className: "bw-svc-meta-dot", "aria-hidden": "true", children: "\u00B7" }), _jsx("span", { className: "bw-svc-price", children: formatServicePrice(service, undefined, intlLocale) })] })] })] }, service._id));
|
|
1050
|
+
})] }, group.key))) }) }) })] })) }), selectedService &&
|
|
1051
|
+
selectedServiceRequiresSlot &&
|
|
1052
|
+
!isEditingService ? (_jsxs("div", { className: "bw-provider-section", children: [_jsx("div", { className: "bw-section-divider" }), _jsx("label", { className: "bw-label", children: t('selectProvider') }), providerRow] })) : null] }), _jsxs("div", { className: "bw-step-2", children: [_jsx("p", { className: "bw-step-2-heading", children: t('chooseYourService') }), _jsx("div", { className: "bw-step-2-divider" }), serviceGroups.map((group) => (_jsxs("div", { className: "bw-svc-group", children: [group.categoryName ? (_jsx("div", { className: "bw-svc-category", children: group.categoryName })) : null, group.services.map((service) => {
|
|
949
1053
|
const isActive = selectedServiceId === service._id;
|
|
950
1054
|
return (_jsxs("div", { children: [_jsxs("button", { type: "button", className: "bw-svc-row bw-svc-row--full", onMouseEnter: () => {
|
|
951
1055
|
void prefetchAvailability({
|
|
@@ -965,12 +1069,16 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
965
1069
|
});
|
|
966
1070
|
}, onClick: () => {
|
|
967
1071
|
handleServiceSelect(service);
|
|
968
|
-
}, children: [service.image ? (_jsxs("span", { className: `bw-svc-image bw-svc-image--lg${isActive ? ' is-checked' : ''}`, children: [_jsx("img", { src: service.image.url, alt: service.image.alt ??
|
|
1072
|
+
}, children: [service.image ? (_jsxs("span", { className: `bw-svc-image bw-svc-image--lg${isActive ? ' is-checked' : ''}`, children: [_jsx("img", { src: service.image.url, alt: service.image.alt ??
|
|
1073
|
+
pickLocaleField(service, 'name', locale) ??
|
|
1074
|
+
service.name, loading: "lazy" }), isActive ? (_jsx("span", { className: "bw-svc-image-badge", children: _jsx(CheckIcon, {}) })) : null] })) : (_jsx("span", { className: `bw-check bw-check--lg${isActive ? ' is-checked' : ''}`, children: isActive ? _jsx(CheckIcon, {}) : null })), _jsxs("span", { className: "bw-svc-info", children: [_jsx("span", { className: "bw-svc-name", children: pickLocaleField(service, 'name', locale) ??
|
|
1075
|
+
service.name }), (pickLocaleField(service, 'description', locale) ?? service.description) ? (_jsx("span", { className: "bw-svc-desc", children: pickLocaleField(service, 'description', locale) ?? service.description })) : null, _jsxs("span", { className: "bw-svc-meta-row", children: [_jsx("span", { className: "bw-svc-meta", children: formatDuration(service.durationMinutes) }), _jsx("span", { className: "bw-svc-meta-dot", "aria-hidden": "true", children: "\u00B7" }), _jsx("span", { className: "bw-svc-price", children: formatServicePrice(service, undefined, intlLocale) })] })] })] }), _jsx("div", { className: "bw-row-divider" })] }, service._id));
|
|
969
1076
|
})] }, group.key)))] })] }), _jsxs("div", { className: "bw-col bw-col--center", children: [_jsxs("div", { className: "bw-cal-card", ref: calCardRef, children: [_jsxs("div", { className: "bw-cal-header", children: [_jsxs("div", { className: "bw-month-dropdown", children: [_jsxs("button", { type: "button", className: "bw-month-btn", onClick: () => setMonthOpen((current) => !current), children: [_jsx("span", { children: formatMonthLabel(calendarMonth, intlLocale) }), _jsx(ChevronDownIcon, {})] }), monthOpen ? (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "bw-month-overlay", "aria-label": t('closeMonthPicker'), onClick: () => setMonthOpen(false) }), _jsx("div", { className: "bw-month-list", children: monthOptions.map((option) => (_jsx("button", { type: "button", className: `bw-month-option${option.value === optionCurrentValue(calendarMonth) ? ' is-active' : ''}`, onClick: () => {
|
|
970
1077
|
setCalendarMonth(option.date);
|
|
971
1078
|
setMonthOpen(false);
|
|
972
1079
|
}, children: option.label }, option.value))) })] })) : null] }), _jsxs("div", { className: "bw-cal-navs", children: [_jsx("button", { type: "button", className: "bw-cal-nav", onMouseEnter: () => {
|
|
973
|
-
if (!sameMonth(calendarMonth, startOfMonth(new Date())) &&
|
|
1080
|
+
if (!sameMonth(calendarMonth, startOfMonth(new Date())) &&
|
|
1081
|
+
selectedServiceId) {
|
|
974
1082
|
void prefetchAvailability({
|
|
975
1083
|
client,
|
|
976
1084
|
siteSlug,
|
|
@@ -980,7 +1088,8 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
980
1088
|
});
|
|
981
1089
|
}
|
|
982
1090
|
}, onClick: () => setCalendarMonth((current) => addMonths(current, -1)), disabled: sameMonth(calendarMonth, startOfMonth(new Date())), "aria-label": t('prevMonth'), children: _jsx(ChevronLeftIcon, {}) }), _jsx("button", { type: "button", className: "bw-cal-nav", onMouseEnter: () => {
|
|
983
|
-
if (!sameMonth(calendarMonth, addMonths(startOfMonth(new Date()), 3)) &&
|
|
1091
|
+
if (!sameMonth(calendarMonth, addMonths(startOfMonth(new Date()), 3)) &&
|
|
1092
|
+
selectedServiceId) {
|
|
984
1093
|
void prefetchAvailability({
|
|
985
1094
|
client,
|
|
986
1095
|
siteSlug,
|
|
@@ -1010,22 +1119,53 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1010
1119
|
isSelected ? 'is-selected' : '',
|
|
1011
1120
|
isAvailable && !isPast ? 'is-available' : '',
|
|
1012
1121
|
isPast ? 'is-disabled' : '',
|
|
1013
|
-
!isAvailable && isCurrentMonth && !isPast
|
|
1014
|
-
|
|
1122
|
+
!isAvailable && isCurrentMonth && !isPast
|
|
1123
|
+
? 'is-blocked'
|
|
1124
|
+
: '',
|
|
1125
|
+
dateKey === formatDateKey(startOfDay(new Date()))
|
|
1126
|
+
? 'is-today'
|
|
1127
|
+
: '',
|
|
1015
1128
|
]
|
|
1016
1129
|
.filter(Boolean)
|
|
1017
1130
|
.join(' ');
|
|
1018
1131
|
return (_jsx("button", { type: "button", className: className, disabled: !isCurrentMonth || isPast || !isAvailable, onClick: () => {
|
|
1019
1132
|
setSelectedDate(dateKey);
|
|
1020
1133
|
setSelectedSlot(null);
|
|
1021
|
-
setMobileStep((current) =>
|
|
1134
|
+
setMobileStep((current) => current < 2 ? 2 : current);
|
|
1022
1135
|
}, children: day.getDate() }, dateKey));
|
|
1023
|
-
}) }), !selectedService ? (_jsx("p", { className: "bw-cal-prompt", children: t('calendarSelectServicePrompt') })) : null, _jsxs("div", { className: "bw-timezone", children: [_jsx(GlobeIcon, {}), _jsx("span", { children: selectedHeldStaff?.timezone ??
|
|
1136
|
+
}) }), !selectedService ? (_jsx("p", { className: "bw-cal-prompt", children: t('calendarSelectServicePrompt') })) : null, _jsxs("div", { className: "bw-timezone", children: [_jsx(GlobeIcon, {}), _jsx("span", { children: selectedHeldStaff?.timezone ??
|
|
1137
|
+
selectedStaff?.timezone ??
|
|
1138
|
+
setup?.workspaceTimezone ??
|
|
1139
|
+
setup?.staff?.[0]?.timezone ??
|
|
1140
|
+
'UTC' })] })] }), selectedService && !isEditingService ? (_jsxs("div", { className: "bw-mobile-card bw-provider-section--mobile", children: [_jsx("label", { className: "bw-label", children: t('selectProvider') }), providerRow] })) : null, selectedService && selectedDate ? (_jsxs("div", { className: "bw-mobile-card bw-slots-mobile", children: [_jsx("label", { className: "bw-label", children: t('selectTimePrompt') }), slotsArea] })) : null] }), _jsx("div", { className: "bw-col bw-col--review", "aria-hidden": mobileStep !== 4, children: selectedService ? (_jsxs("div", { className: "bw-summary bw-summary--review", children: [_jsx("span", { className: "bw-summary-title", children: isReschedule
|
|
1141
|
+
? t('summaryTitleReschedule')
|
|
1142
|
+
: t('summaryTitleCreate') }), holdSecondsRemaining !== null ? (_jsx("div", { className: "bw-summary-rows", children: _jsxs("div", { className: "bw-summary-row bw-summary-row--hold", "aria-live": "polite", children: [_jsx("span", { children: t('summaryReservationHold') }), _jsx("span", { className: "bw-summary-val bw-summary-val--hold", children: formatHoldCountdown(holdSecondsRemaining) })] }) })) : null, _jsxs("div", { className: "bw-summary-group", children: [_jsx("span", { className: "bw-summary-subhead", children: t('reviewBookingDetails') }), _jsxs("div", { className: "bw-summary-rows", children: [isReschedule && rescheduleContext ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryFormerTime') }), _jsx("span", { className: "bw-summary-val", style: {
|
|
1143
|
+
...summaryValStyle,
|
|
1144
|
+
textDecoration: 'line-through',
|
|
1145
|
+
opacity: 0.55,
|
|
1146
|
+
}, children: formatFormerSlot(rescheduleContext.formerStartTime, rescheduleContext.formerEndTime, intlLocale) })] })) : null, serviceChanged && formerService ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryFormerService') }), _jsx("span", { className: "bw-summary-val", style: {
|
|
1147
|
+
...summaryValStyle,
|
|
1148
|
+
textDecoration: 'line-through',
|
|
1149
|
+
opacity: 0.55,
|
|
1150
|
+
}, children: formerService.name })] })) : null, staffChanged ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryFormerStaff') }), _jsx("span", { className: "bw-summary-val", style: {
|
|
1151
|
+
...summaryValStyle,
|
|
1152
|
+
textDecoration: 'line-through',
|
|
1153
|
+
opacity: 0.55,
|
|
1154
|
+
}, children: formerStaffName ?? t('providerAny') })] })) : null, _jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryService') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: pickLocaleField(selectedService, 'name', locale) ??
|
|
1155
|
+
selectedService.name })] }), _jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryProvider') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: selectedHeldStaff?.name ??
|
|
1156
|
+
selectedStaff?.name ??
|
|
1157
|
+
t('providerAny') })] }), selectedDate ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryDate') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: formatReadableDate(selectedDate, intlLocale) })] })) : null, selectedSlot ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryTime') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: formatTimeLabel(selectedSlot.time, intlLocale) })] })) : null, _jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryDuration') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: formatDuration(selectedService.durationMinutes) })] })] })] }), _jsxs("div", { className: "bw-summary-group", children: [_jsx("span", { className: "bw-summary-subhead", children: t('reviewYourDetails') }), _jsxs("div", { className: "bw-summary-rows", children: [_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('contactFullName') }), _jsx("span", { className: "bw-summary-val", children: customerName.trim() || t('reviewNotProvided') })] }), _jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('contactEmail') }), _jsx("span", { className: "bw-summary-val", children: customerEmail.trim() || t('reviewNotProvided') })] }), _jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('contactPhone') }), _jsx("span", { className: "bw-summary-val", children: customerPhone.trim() || t('reviewNotProvided') })] })] })] }), (() => {
|
|
1024
1158
|
const intakeRows = buildIntakeReviewRows(selectedService.intakeForm, intakeResponses, selectedServicePath, locale, t);
|
|
1025
1159
|
if (intakeRows.length === 0)
|
|
1026
1160
|
return null;
|
|
1027
1161
|
return (_jsxs("div", { className: "bw-summary-group", children: [_jsx("span", { className: "bw-summary-subhead", children: t('reviewAdditionalInfo') }), _jsx("div", { className: "bw-summary-rows", children: intakeRows.map((row) => (_jsxs("div", { className: `bw-summary-row${row.multiline ? ' bw-summary-row--stack' : ''}`, children: [_jsx("span", { children: row.label }), _jsx("span", { className: "bw-summary-val", children: row.value })] }, row.key))) })] }));
|
|
1028
|
-
})(), customerNotes.trim() ? (_jsxs("div", { className: "bw-summary-group", children: [_jsx("span", { className: "bw-summary-subhead", children: isReschedule
|
|
1162
|
+
})(), customerNotes.trim() ? (_jsxs("div", { className: "bw-summary-group", children: [_jsx("span", { className: "bw-summary-subhead", children: isReschedule
|
|
1163
|
+
? t('notesLabelReschedule')
|
|
1164
|
+
: t('reviewNotesSection') }), _jsx("p", { className: "bw-summary-notes", children: customerNotes.trim() })] })) : null, _jsx("div", { className: "bw-summary-rows bw-summary-rows--total", children: _jsxs("div", { className: "bw-summary-row bw-summary-total", children: [_jsx("span", { children: isQuoteLoading
|
|
1165
|
+
? t('summaryCalculating')
|
|
1166
|
+
: t('summaryEstimatedTotal') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: formatServicePrice(selectedService, quote, intlLocale) })] }) })] })) : null }), _jsx("div", { className: "bw-col bw-col--right", children: _jsx("div", { className: "bw-right-scroll", children: _jsx("div", { className: "bw-right-inner", children: _jsxs("div", { className: "bw-right-stage", children: [_jsxs("div", { className: `bw-pane bw-pane--slots${viewState === 'slots' ? ' is-active' : ' is-hidden'}`, "aria-hidden": viewState !== 'slots', children: [_jsxs("div", { className: "bw-slots-heading bw-slots-heading--sticky", children: [_jsx("span", { children: t('slotsHeading') }), selectedDateSlots.length > 0 ? (_jsx("span", { className: "bw-slots-count", children: selectedDateSlots.length })) : null] }), _jsx("div", { className: "bw-slots-desktop", children: slotsArea })] }), _jsxs("div", { className: `bw-pane bw-pane--details${viewState === 'details' ? ' is-active' : ' is-hidden'}`, "aria-hidden": viewState !== 'details', children: [selectedServiceRequiresSlot ? (_jsxs("button", { type: "button", className: "bw-back-btn", onClick: () => void handleBackToSlots(), children: [_jsx(ArrowLeftIcon, {}), _jsx("span", { children: t('backToDifferentTime') })] })) : (_jsxs("button", { type: "button", className: "bw-back-btn", onClick: handleBackToServicePicker, children: [_jsx(ArrowLeftIcon, {}), _jsx("span", { children: t('backToDifferentService') })] })), _jsxs("div", { className: "bw-form", children: [serviceWarning ? (_jsx(ServiceWarningCallout, { warning: serviceWarning })) : null, renderIntakeAndContact('bw-intake', ''), renderNotesField('bw-notes', t('notesLabel'), t('notesPlaceholder')), selectedService ? (_jsxs("div", { className: "bw-summary", children: [_jsx("span", { className: "bw-summary-title", children: isReschedule
|
|
1167
|
+
? t('summaryTitleReschedule')
|
|
1168
|
+
: t('summaryTitleCreate') }), _jsxs("div", { className: "bw-summary-rows", children: [holdSecondsRemaining !== null ? (_jsxs("div", { className: "bw-summary-row bw-summary-row--hold", "aria-live": "polite", children: [_jsx("span", { children: t('summaryReservationHold') }), _jsx("span", { className: "bw-summary-val bw-summary-val--hold", children: formatHoldCountdown(holdSecondsRemaining) })] })) : null, isReschedule && rescheduleContext ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryFormerTime') }), _jsx("span", { className: "bw-summary-val", style: {
|
|
1029
1169
|
...summaryValStyle,
|
|
1030
1170
|
textDecoration: 'line-through',
|
|
1031
1171
|
opacity: 0.55,
|
|
@@ -1037,7 +1177,11 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1037
1177
|
...summaryValStyle,
|
|
1038
1178
|
textDecoration: 'line-through',
|
|
1039
1179
|
opacity: 0.55,
|
|
1040
|
-
}, children: formerStaffName ?? t('providerAny') })] })) : null, _jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryService') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: pickLocaleField(selectedService, 'name', locale) ?? selectedService.name })] }), _jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryProvider') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: selectedHeldStaff?.name ??
|
|
1180
|
+
}, children: formerStaffName ?? t('providerAny') })] })) : null, _jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryService') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: pickLocaleField(selectedService, 'name', locale) ?? selectedService.name })] }), _jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryProvider') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: selectedHeldStaff?.name ??
|
|
1181
|
+
selectedStaff?.name ??
|
|
1182
|
+
t('providerAny') })] }), selectedDate ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryDate') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: formatReadableDate(selectedDate, intlLocale) })] })) : null, selectedSlot ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryTime') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: formatTimeLabel(selectedSlot.time, intlLocale) })] })) : null, _jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryDuration') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: formatDuration(selectedService.durationMinutes) })] }), _jsxs("div", { className: "bw-summary-row bw-summary-total", children: [_jsx("span", { children: isQuoteLoading
|
|
1183
|
+
? t('summaryCalculating')
|
|
1184
|
+
: t('summaryEstimatedTotal') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: formatServicePrice(selectedService, quote, intlLocale) })] })] })] })) : null, error ? (_jsx("div", { className: "bw-error", children: error })) : null, renderSubmitHelp(), payment ? (_jsx(StripePaymentBlock, { payment: payment, appointmentId: paymentAppointmentId, showInlineButton: false, actionTarget: mobilePaymentActionTarget, onSuccess: () => {
|
|
1041
1185
|
setSuccess(t('successPaymentReceived'));
|
|
1042
1186
|
setPayment(null);
|
|
1043
1187
|
}, onError: setError })) : (_jsxs("button", { type: "button", className: `bw-confirm-btn${!canSubmit || isQuoteLoading ? ' is-disabled' : ''}`, "aria-disabled": !canSubmit || isQuoteLoading, onClick: handleConfirmAttempt, children: [_jsx("span", { children: isSubmitting
|
|
@@ -1048,7 +1192,23 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1048
1192
|
? t('btnConfirmReschedule')
|
|
1049
1193
|
: selectedServiceRequiresPayment
|
|
1050
1194
|
? t('btnContinueToPayment')
|
|
1051
|
-
: t('btnConfirmBooking') }), !isSubmitting ? _jsx(ArrowRightIcon, {}) : null] }))] })] })] }) }) }) })] }) }), _jsxs("div", { className: "bw-details-view", children: [_jsx("div", { className: "bw-details-summary", children: selectedService ? (_jsxs(_Fragment, { children: [selectedServiceRequiresSlot ? (_jsxs("button", { type: "button", className: "bw-back-btn bw-back-btn--details", onClick: () => void handleBackToSlots(), children: [_jsx(ArrowLeftIcon, {}), _jsx("span", { children: t('backToDifferentTime') })] })) : (_jsxs("button", { type: "button", className: "bw-back-btn bw-back-btn--details", onClick: handleBackToServicePicker, children: [_jsx(ArrowLeftIcon, {}), _jsx("span", { children: t('backToDifferentService') })] })), _jsx("h3", { className: "bw-details-service", children: pickLocaleField(selectedService, 'name', locale) ??
|
|
1195
|
+
: t('btnConfirmBooking') }), !isSubmitting ? _jsx(ArrowRightIcon, {}) : null] }))] })] })] }) }) }) })] }) }), _jsxs("div", { className: "bw-details-view", children: [_jsx("div", { className: "bw-details-summary", children: selectedService ? (_jsxs(_Fragment, { children: [selectedServiceRequiresSlot ? (_jsxs("button", { type: "button", className: "bw-back-btn bw-back-btn--details", onClick: () => void handleBackToSlots(), children: [_jsx(ArrowLeftIcon, {}), _jsx("span", { children: t('backToDifferentTime') })] })) : (_jsxs("button", { type: "button", className: "bw-back-btn bw-back-btn--details", onClick: handleBackToServicePicker, children: [_jsx(ArrowLeftIcon, {}), _jsx("span", { children: t('backToDifferentService') })] })), _jsx("h3", { className: "bw-details-service", children: pickLocaleField(selectedService, 'name', locale) ??
|
|
1196
|
+
selectedService.name }), (pickLocaleField(selectedService, 'description', locale) ??
|
|
1197
|
+
selectedService.description) ? (_jsx("p", { className: "bw-details-desc", children: pickLocaleField(selectedService, 'description', locale) ?? selectedService.description })) : null, _jsxs("div", { className: "bw-details-meta", children: [isReschedule && rescheduleContext ? (_jsxs("div", { className: "bw-details-meta-row bw-details-meta-row--strike", children: [_jsx("span", { className: "bw-details-meta-icon", children: _jsx(CalendarIcon, {}) }), _jsxs("div", { className: "bw-details-meta-text", children: [_jsx("span", { className: "bw-details-meta-label", children: t('summaryFormerTime') }), _jsx("span", { className: "bw-details-meta-value", children: formatFormerSlot(rescheduleContext.formerStartTime, rescheduleContext.formerEndTime, intlLocale) })] })] })) : null, selectedDate && selectedSlot ? (_jsxs("div", { className: "bw-details-meta-row", children: [_jsx("span", { className: "bw-details-meta-icon", children: _jsx(CalendarIcon, {}) }), _jsxs("div", { className: "bw-details-meta-text", children: [_jsx("span", { className: "bw-details-meta-label", children: isReschedule
|
|
1198
|
+
? t('summaryNewTime')
|
|
1199
|
+
: t('summaryWhen') }), _jsxs("span", { className: "bw-details-meta-value", children: [formatReadableDate(selectedDate, intlLocale), _jsx("br", {}), formatTimeLabel(selectedSlot.time, intlLocale), " \u2013", ' ', formatTimeLabel(selectedSlot.endTime, intlLocale)] })] })] })) : null, _jsxs("div", { className: "bw-details-meta-row", children: [_jsx("span", { className: "bw-details-meta-icon", children: _jsx(ClockIcon, {}) }), _jsx("span", { className: "bw-details-meta-value", children: formatDuration(selectedService.durationMinutes) })] }), _jsxs("div", { className: "bw-details-meta-row", children: [_jsx("span", { className: "bw-details-meta-icon", children: _jsx(UserIcon, {}) }), _jsx("span", { className: "bw-details-meta-value", children: selectedHeldStaff?.name ??
|
|
1200
|
+
selectedStaff?.name ??
|
|
1201
|
+
t('providerAny') })] }), _jsxs("div", { className: "bw-details-meta-row", children: [_jsx("span", { className: "bw-details-meta-icon", children: _jsx(GlobeIcon, {}) }), _jsx("span", { className: "bw-details-meta-value", children: selectedHeldStaff?.timezone ??
|
|
1202
|
+
selectedStaff?.timezone ??
|
|
1203
|
+
setup?.workspaceTimezone ??
|
|
1204
|
+
setup?.staff?.[0]?.timezone ??
|
|
1205
|
+
'UTC' })] }), _jsxs("div", { className: "bw-details-meta-row", children: [_jsx("span", { className: "bw-details-meta-icon", "aria-hidden": "true", children: "$" }), _jsxs("div", { className: "bw-details-meta-text", children: [_jsx("span", { className: "bw-details-meta-label", children: isQuoteLoading
|
|
1206
|
+
? t('summaryCalculating')
|
|
1207
|
+
: t('summaryEstimatedTotal') }), _jsx("span", { className: "bw-details-meta-value", children: formatServicePrice(selectedService, quote, intlLocale) })] })] }), holdSecondsRemaining !== null ? (_jsxs("div", { className: "bw-details-meta-row bw-details-meta-row--hold", "aria-live": "polite", children: [_jsx("span", { className: "bw-details-meta-icon", children: _jsx(ClockIcon, {}) }), _jsxs("div", { className: "bw-details-meta-text", children: [_jsx("span", { className: "bw-details-meta-label", children: t('summaryReservationHold') }), _jsx("span", { className: "bw-details-meta-value bw-details-hold", children: formatHoldCountdown(holdSecondsRemaining) })] })] })) : null] })] })) : null }), _jsx("div", { className: "bw-details-form", children: _jsxs("div", { className: "bw-form bw-form--details", children: [serviceWarning ? (_jsx(ServiceWarningCallout, { warning: serviceWarning })) : null, renderIntakeAndContact('bw-intake-d', '-d'), renderNotesField('bw-notes-d', isReschedule ? t('notesLabelReschedule') : t('notesLabel'), isReschedule
|
|
1208
|
+
? t('notesPlaceholderReschedule')
|
|
1209
|
+
: t('notesPlaceholder')), error ? _jsx("div", { className: "bw-error", children: error }) : null, renderSubmitHelp(), !payment &&
|
|
1210
|
+
(forcedState === 'payment-full' ||
|
|
1211
|
+
forcedState === 'payment-deposit') ? (_jsx(PaymentPanel, { service: selectedService, mode: forcedState === 'payment-full' ? 'full' : 'deposit' })) : null, payment ? (_jsx(StripePaymentBlock, { payment: payment, appointmentId: paymentAppointmentId, onSuccess: () => {
|
|
1052
1212
|
setSuccess(t('successPaymentReceived'));
|
|
1053
1213
|
setPayment(null);
|
|
1054
1214
|
}, onError: setError })) : (_jsxs("button", { type: "button", className: `bw-confirm-btn${!canSubmit || isQuoteLoading ? ' is-disabled' : ''}`, "aria-disabled": !canSubmit || isQuoteLoading, onClick: handleConfirmAttempt, children: [_jsx("span", { children: isSubmitting
|
|
@@ -1056,7 +1216,9 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1056
1216
|
? t('btnRescheduling')
|
|
1057
1217
|
: t('btnBooking')
|
|
1058
1218
|
: forcedState === 'payment-full'
|
|
1059
|
-
? t('btnPayAndConfirm', {
|
|
1219
|
+
? t('btnPayAndConfirm', {
|
|
1220
|
+
price: formatPrice(selectedService, intlLocale),
|
|
1221
|
+
})
|
|
1060
1222
|
: forcedState === 'payment-deposit'
|
|
1061
1223
|
? t('btnPayDepositAndConfirm')
|
|
1062
1224
|
: isReschedule
|
|
@@ -1156,7 +1318,8 @@ function areRequiredIntakeFieldsComplete(form, responses, mode) {
|
|
|
1156
1318
|
}
|
|
1157
1319
|
function getSubmitBlockers({ selectedService, selectedServiceRequiresSlot, selectedDate, selectedSlot, holdId, isIntakeComplete, intakeResponses, selectedServicePath, quote, customerName, customerEmail, t, locale = DEFAULT_LOCALE, }) {
|
|
1158
1320
|
const blockers = [];
|
|
1159
|
-
if (selectedServiceRequiresSlot &&
|
|
1321
|
+
if (selectedServiceRequiresSlot &&
|
|
1322
|
+
(!selectedDate || !selectedSlot || !holdId)) {
|
|
1160
1323
|
blockers.push(t('blockerDateTime'));
|
|
1161
1324
|
}
|
|
1162
1325
|
if (!customerName.trim())
|
|
@@ -1284,7 +1447,9 @@ function buildIntakeReviewRows(form, responses, servicePath, locale, t) {
|
|
|
1284
1447
|
continue;
|
|
1285
1448
|
for (const field of section.fields) {
|
|
1286
1449
|
if (field.type === 'repeatable-group') {
|
|
1287
|
-
const items = Array.isArray(responses[field.id])
|
|
1450
|
+
const items = Array.isArray(responses[field.id])
|
|
1451
|
+
? responses[field.id]
|
|
1452
|
+
: [];
|
|
1288
1453
|
items.forEach((item, index) => {
|
|
1289
1454
|
const record = item && typeof item === 'object' && !Array.isArray(item)
|
|
1290
1455
|
? item
|
|
@@ -1489,7 +1654,9 @@ function IntakeField({ field, value, onChange, idPrefix, }) {
|
|
|
1489
1654
|
if (field.type === 'repeatable-group') {
|
|
1490
1655
|
const items = Array.isArray(value) && value.length > 0 ? value : [{}];
|
|
1491
1656
|
return (_jsxs("div", { className: "bw-field bw-field--wide bw-repeatable", children: [_jsx("label", { children: label }), items.map((item, index) => {
|
|
1492
|
-
const record = item && typeof item === 'object' && !Array.isArray(item)
|
|
1657
|
+
const record = item && typeof item === 'object' && !Array.isArray(item)
|
|
1658
|
+
? item
|
|
1659
|
+
: {};
|
|
1493
1660
|
return (_jsxs("div", { className: "bw-repeatable-item", children: [_jsxs("div", { className: "bw-repeatable-head", children: [_jsx("span", { children: t('repeatItemLabel', { index: index + 1 }) }), items.length > 1 ? (_jsx("button", { type: "button", className: "bw-link-btn", onClick: () => onChange(items.filter((_, itemIndex) => itemIndex !== index)), children: t('repeatRemove') })) : null] }), _jsx("div", { className: "bw-form-fields", children: (field.fields ?? []).map((child) => (_jsx(IntakeField, { field: child, value: record[child.id], idPrefix: `${id}-${index}`, onChange: (nextValue) => {
|
|
1494
1661
|
const nextItems = [...items];
|
|
1495
1662
|
nextItems[index] = { ...record, [child.id]: nextValue };
|
|
@@ -1513,7 +1680,11 @@ function IntakeField({ field, value, onChange, idPrefix, }) {
|
|
|
1513
1680
|
if (field.type === 'textarea') {
|
|
1514
1681
|
return (_jsxs("div", { className: "bw-field bw-field--wide", children: [_jsxs("label", { htmlFor: id, children: [label, field.required ? _jsx("span", { className: "bw-required", children: " *" }) : null] }), _jsx("textarea", { id: id, rows: 3, required: field.required, value: stringValue, placeholder: field.placeholder, onChange: (event) => onChange(event.target.value) }), field.helpText ? _jsx("p", { className: "bw-help", children: field.helpText }) : null] }));
|
|
1515
1682
|
}
|
|
1516
|
-
return (_jsxs("div", { className: "bw-field", children: [_jsxs("label", { htmlFor: id, children: [label, field.required ? _jsx("span", { className: "bw-required", children: " *" }) : null] }), _jsx("input", { id: id, type: field.type === 'email'
|
|
1683
|
+
return (_jsxs("div", { className: "bw-field", children: [_jsxs("label", { htmlFor: id, children: [label, field.required ? _jsx("span", { className: "bw-required", children: " *" }) : null] }), _jsx("input", { id: id, type: field.type === 'email'
|
|
1684
|
+
? 'email'
|
|
1685
|
+
: field.type === 'number'
|
|
1686
|
+
? 'number'
|
|
1687
|
+
: 'text', min: field.min, max: field.max, required: field.required, value: stringValue, placeholder: field.placeholder, onChange: (event) => {
|
|
1517
1688
|
onChange(field.type === 'number'
|
|
1518
1689
|
? event.target.value === ''
|
|
1519
1690
|
? ''
|
|
@@ -1744,7 +1915,8 @@ function buildCalendarDays(month) {
|
|
|
1744
1915
|
});
|
|
1745
1916
|
}
|
|
1746
1917
|
function sameMonth(left, right) {
|
|
1747
|
-
return left.getFullYear() === right.getFullYear() &&
|
|
1918
|
+
return (left.getFullYear() === right.getFullYear() &&
|
|
1919
|
+
left.getMonth() === right.getMonth());
|
|
1748
1920
|
}
|
|
1749
1921
|
function getMonthOptions(currentMonth, total, locale = 'en-US') {
|
|
1750
1922
|
const start = startOfMonth(new Date());
|
|
@@ -1763,7 +1935,8 @@ function optionCurrentValue(date) {
|
|
|
1763
1935
|
}
|
|
1764
1936
|
function isDateInMonth(dateKey, month) {
|
|
1765
1937
|
const date = new Date(`${dateKey}T00:00:00`);
|
|
1766
|
-
return date.getMonth() === month.getMonth() &&
|
|
1938
|
+
return (date.getMonth() === month.getMonth() &&
|
|
1939
|
+
date.getFullYear() === month.getFullYear());
|
|
1767
1940
|
}
|
|
1768
1941
|
function findFirstAvailableDate(availabilityByDate, month) {
|
|
1769
1942
|
const dates = [...availabilityByDate.entries()]
|