@asksable/site-connector 0.6.8 → 0.6.10
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/dist/booking-widget.d.ts.map +1 -1
- package/dist/booking-widget.js +152 -31
- package/dist/booking-widget.js.map +1 -1
- package/dist/client.d.ts +2 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +10 -0
- package/dist/client.js.map +1 -1
- package/dist/provider.d.ts +1 -0
- package/dist/provider.d.ts.map +1 -1
- package/dist/styles.css +2 -0
- package/dist/translations.d.ts +2 -0
- package/dist/translations.d.ts.map +1 -1
- package/dist/translations.js +2 -0
- package/dist/translations.js.map +1 -1
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"booking-widget.d.ts","sourceRoot":"","sources":["../src/booking-widget.tsx"],"names":[],"mappings":"AAaA,OAAO,qBAAqB,CAAA;AAc5B,OAAO,KAAK,EAKV,SAAS,EACV,MAAM,OAAO,CAAA;AAcd,KAAK,oBAAoB,GAAG,OAAO,GAAG,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"booking-widget.d.ts","sourceRoot":"","sources":["../src/booking-widget.tsx"],"names":[],"mappings":"AAaA,OAAO,qBAAqB,CAAA;AAc5B,OAAO,KAAK,EAKV,SAAS,EACV,MAAM,OAAO,CAAA;AAcd,KAAK,oBAAoB,GAAG,OAAO,GAAG,UAAU,CAAA;AA4NhD;;;;;GAKG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACrC,aAAa,EAAE,MAAM,CAAA;IACrB;;qBAEiB;IACjB,eAAe,EAAE,MAAM,CAAA;IACvB,aAAa,EAAE,MAAM,CAAA;IACrB;qCACiC;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB;;sBAEkB;IAClB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;yCACqC;IACrC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;oEACgE;IAChE,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B,CAAA;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,sEAAsE;IACtE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACzC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,sEAAsE;IACtE,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B,CAAA;AAED,KAAK,kBAAkB,GAAG;IACxB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,SAAS,CAAA;IACxB;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,QAAQ,GAAG,YAAY,CAAA;IAC9B,iBAAiB,CAAC,EAAE,wBAAwB,CAAA;IAC5C;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,CAAC,IAAI,EAAE;QAC1B,YAAY,EAAE,MAAM,CAAA;QACpB,UAAU,EAAE,MAAM,CAAA;KACnB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACnB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,uBAAuB,CAAA;IAC1C;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAA;IACjC;;;OAGG;IACH,2BAA2B,CAAC,EAAE,oBAAoB,CAAA;IAClD;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B;;;;;;;;;;;;;;OAcG;IACH,eAAe,CAAC,EAAE,SAAS,GAAG,WAAW,GAAG,cAAc,GAAG,iBAAiB,CAAA;IAC9E,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B,CAAA;AAKD,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,WAAW,EACX,YAAY,EACZ,IAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,uBAA+B,EAC/B,2BAAqC,EACrC,mBAAyB,EACzB,eAAe,EACf,iBAAiB,GAClB,EAAE,kBAAkB,2CAs+GpB"}
|
package/dist/booking-widget.js
CHANGED
|
@@ -164,6 +164,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
164
164
|
const [customerEmail, setCustomerEmail] = useState(() => rescheduleContext?.customerEmail ?? '');
|
|
165
165
|
const [customerPhone, setCustomerPhone] = useState(() => rescheduleContext?.customerPhone ?? '');
|
|
166
166
|
const [serviceAddress, setServiceAddress] = useState('');
|
|
167
|
+
const [serviceAddressPlace, setServiceAddressPlace] = useState(null);
|
|
167
168
|
const [isServiceAddressValid, setIsServiceAddressValid] = useState(true);
|
|
168
169
|
const [customerNotes, setCustomerNotes] = useState('');
|
|
169
170
|
const [bookingPhotos, setBookingPhotos] = useState([]);
|
|
@@ -348,7 +349,20 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
348
349
|
}), [displayQuote, intlLocale, isDisplayQuoteLoading, selectedService, t]);
|
|
349
350
|
const totalPriceSummaryRow = priceSummaryRows.find((row) => row.isTotal) ?? priceSummaryRows.at(-1);
|
|
350
351
|
const selectedServiceRequiresSlot = selectedService?.publicBookingMode !== 'async';
|
|
351
|
-
const
|
|
352
|
+
const publicBookingPolicy = setup?.publicBookingPolicy ?? null;
|
|
353
|
+
const autoAssignProvider = publicBookingPolicy?.providerSelection === 'auto';
|
|
354
|
+
const requiresServiceAddressBeforeSlots = publicBookingPolicy?.requiresServiceAddressBeforeSlots === true &&
|
|
355
|
+
!isReschedule;
|
|
356
|
+
const serviceAddressGeo = serviceAddressPlace && isServiceAddressValid
|
|
357
|
+
? serviceAddressPlace
|
|
358
|
+
: null;
|
|
359
|
+
const serviceAddressCacheKey = serviceAddressGeo
|
|
360
|
+
? `${serviceAddressGeo.placeId ?? ''}:${serviceAddressGeo.lat.toFixed(5)}:${serviceAddressGeo.lng.toFixed(5)}`
|
|
361
|
+
: 'none';
|
|
362
|
+
const flexibleSchedulingEnabled = allowFlexibleScheduling &&
|
|
363
|
+
publicBookingPolicy?.allowFlexibleScheduling !== false &&
|
|
364
|
+
selectedServiceRequiresSlot &&
|
|
365
|
+
!isReschedule;
|
|
352
366
|
const effectiveSchedulingPreference = flexibleSchedulingEnabled ? schedulingPreference : 'exact';
|
|
353
367
|
const bookingTimezone = resolveBookingTimezone({
|
|
354
368
|
workspaceTimezone: setup?.workspaceTimezone,
|
|
@@ -484,6 +498,11 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
484
498
|
}
|
|
485
499
|
return setup.staff.filter((staffMember) => selectedService.assignedStaffIds.includes(staffMember._id));
|
|
486
500
|
}, [selectedService, setup]);
|
|
501
|
+
useEffect(() => {
|
|
502
|
+
if (autoAssignProvider && selectedStaffId !== null) {
|
|
503
|
+
setSelectedStaffId(null);
|
|
504
|
+
}
|
|
505
|
+
}, [autoAssignProvider, selectedStaffId]);
|
|
487
506
|
useEffect(() => {
|
|
488
507
|
// Wait for availableStaff to populate before clearing - otherwise
|
|
489
508
|
// a pre-seeded staffMemberId (e.g. from rescheduleContext) gets
|
|
@@ -502,6 +521,13 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
502
521
|
setSelectedDate(null);
|
|
503
522
|
return;
|
|
504
523
|
}
|
|
524
|
+
if (requiresServiceAddressBeforeSlots && !serviceAddressGeo) {
|
|
525
|
+
setAvailabilityByDate(new Map());
|
|
526
|
+
setSelectedDate(null);
|
|
527
|
+
setSelectedSlot(null);
|
|
528
|
+
setIsAvailabilityLoading(false);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
505
531
|
if (effectiveSchedulingPreference === 'flexible') {
|
|
506
532
|
setAvailabilityByDate(new Map());
|
|
507
533
|
setIsAvailabilityLoading(false);
|
|
@@ -536,7 +562,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
536
562
|
// instant (no refetch) and we always know which staff are
|
|
537
563
|
// available on a given date - even when a single provider is
|
|
538
564
|
// selected - so we can disable unavailable rows in the dropdown.
|
|
539
|
-
const requestKey = getAvailabilityCacheKey(siteSlug, selectedServiceId, calendarMonth);
|
|
565
|
+
const requestKey = getAvailabilityCacheKey(siteSlug, selectedServiceId, calendarMonth, serviceAddressCacheKey);
|
|
540
566
|
const cachedAvailability = availabilityCacheRef.current.get(requestKey);
|
|
541
567
|
if (cachedAvailability) {
|
|
542
568
|
setAvailabilityByDate(cachedAvailability);
|
|
@@ -556,6 +582,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
556
582
|
serviceId: selectedServiceId,
|
|
557
583
|
startDate: monthStart,
|
|
558
584
|
endDate: monthEnd,
|
|
585
|
+
serviceAddressGeo,
|
|
559
586
|
})
|
|
560
587
|
.then((result) => {
|
|
561
588
|
if (cancelled) {
|
|
@@ -603,6 +630,9 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
603
630
|
selectedServiceId,
|
|
604
631
|
selectedStaffId,
|
|
605
632
|
selectedService,
|
|
633
|
+
requiresServiceAddressBeforeSlots,
|
|
634
|
+
serviceAddressCacheKey,
|
|
635
|
+
serviceAddressGeo,
|
|
606
636
|
setup?.businessHours,
|
|
607
637
|
siteSlug,
|
|
608
638
|
todayDateKey,
|
|
@@ -614,17 +644,25 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
614
644
|
return;
|
|
615
645
|
}
|
|
616
646
|
const nextMonth = addMonths(calendarMonth, 1);
|
|
647
|
+
if (requiresServiceAddressBeforeSlots && !serviceAddressGeo) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
617
650
|
void prefetchAvailability({
|
|
618
651
|
client,
|
|
619
652
|
siteSlug,
|
|
620
653
|
selectedServiceId,
|
|
621
654
|
calendarMonth: nextMonth,
|
|
622
655
|
cacheRef: availabilityCacheRef,
|
|
656
|
+
serviceAddressGeo,
|
|
657
|
+
serviceAddressCacheKey,
|
|
623
658
|
});
|
|
624
659
|
}, [
|
|
625
660
|
calendarMonth,
|
|
626
661
|
client,
|
|
627
662
|
effectiveSchedulingPreference,
|
|
663
|
+
requiresServiceAddressBeforeSlots,
|
|
664
|
+
serviceAddressCacheKey,
|
|
665
|
+
serviceAddressGeo,
|
|
628
666
|
selectedService?.publicBookingMode,
|
|
629
667
|
selectedServiceId,
|
|
630
668
|
selectedStaffId,
|
|
@@ -653,6 +691,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
653
691
|
selectedService?.publicBookingMode,
|
|
654
692
|
selectedServiceId,
|
|
655
693
|
selectedStaffId,
|
|
694
|
+
serviceAddressCacheKey,
|
|
656
695
|
]);
|
|
657
696
|
useEffect(() => {
|
|
658
697
|
if (previousSelectedServiceIdRef.current === selectedServiceId) {
|
|
@@ -870,7 +909,8 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
870
909
|
// slot to advance. Step 3 is the details form - needs contact name,
|
|
871
910
|
// valid email, and any required intake fields filled. Step 4 is
|
|
872
911
|
// the review screen (submit, not advance).
|
|
873
|
-
const canAdvanceStep1 = Boolean(selectedService
|
|
912
|
+
const canAdvanceStep1 = Boolean(selectedService &&
|
|
913
|
+
(!requiresServiceAddressBeforeSlots || serviceAddressGeo));
|
|
874
914
|
const hasScheduleSelection = !selectedServiceRequiresSlot ||
|
|
875
915
|
(effectiveSchedulingPreference === 'flexible'
|
|
876
916
|
? Boolean(selectedDate && selectedFlexibleWindow)
|
|
@@ -910,15 +950,15 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
910
950
|
// label. The row scrolls horizontally when content exceeds the
|
|
911
951
|
// available width, so it works on desktop AND mobile without a
|
|
912
952
|
// breakpoint-specific layout.
|
|
913
|
-
const providerRow = selectedService && !isEditingService ? (_jsxs("div", { className: "bw-staff-row", role: "listbox", "aria-label": t('chooseProviderAria'), children: [_jsxs("button", { type: "button", role: "option", "aria-selected": selectedStaffId === null, className: `bw-staff-card${selectedStaffId === null ? ' is-active' : ''}`, onClick: () => setSelectedStaffId(null), children: [_jsx("span", { className: "bw-staff-card-avatar", children: _jsx(UserIcon, {}) }), _jsxs("span", { className: "bw-staff-card-info", children: [_jsx("span", { className: "bw-staff-card-name", children: t('providerAny') }), _jsx("span", { className: "bw-staff-card-desc", children: t('providerFirstAvailable') })] })] }), availableStaff.map((staffMember) => {
|
|
953
|
+
const providerRow = selectedService && !isEditingService && !autoAssignProvider ? (_jsxs("div", { className: "bw-staff-row", role: "listbox", "aria-label": t('chooseProviderAria'), children: [_jsxs("button", { type: "button", role: "option", "aria-selected": selectedStaffId === null, className: `bw-staff-card${selectedStaffId === null ? ' is-active' : ''}`, onClick: () => setSelectedStaffId(null), children: [_jsx("span", { className: "bw-staff-card-avatar", children: _jsx(UserIcon, {}) }), _jsxs("span", { className: "bw-staff-card-info", children: [_jsx("span", { className: "bw-staff-card-name", children: t('providerAny') }), _jsx("span", { className: "bw-staff-card-desc", children: t('providerFirstAvailable') })] })] }), availableStaff.map((staffMember) => {
|
|
914
954
|
const isActive = selectedStaffId === staffMember._id;
|
|
915
955
|
const isAvailable = staffIdsAvailableOnSelectedDate === null ||
|
|
916
956
|
staffIdsAvailableOnSelectedDate.has(staffMember._id);
|
|
917
957
|
return (_jsxs("button", { type: "button", role: "option", "aria-selected": isActive, "aria-disabled": !isAvailable, disabled: !isAvailable, className: `bw-staff-card${isActive ? ' is-active' : ''}${!isAvailable ? ' is-disabled' : ''}`, onClick: () => setSelectedStaffId(staffMember._id), children: [_jsx("span", { className: "bw-staff-card-avatar", children: staffMember.image?.url ? (_jsx("img", { src: staffMember.image.url, alt: staffMember.image.alt ?? staffMember.name, className: "bw-staff-avatar-img" })) : (_jsx("span", { className: "bw-staff-initials", children: getInitials(staffMember.name) })) }), _jsxs("span", { className: "bw-staff-card-info", children: [_jsx("span", { className: "bw-staff-card-name", children: staffMember.name }), !isAvailable ? (_jsx("span", { className: "bw-staff-card-desc", children: t('providerUnavailable') })) : null] })] }, staffMember._id));
|
|
918
958
|
})] })) : null;
|
|
919
959
|
const schedulingPreferenceControl = flexibleSchedulingEnabled ? (_jsxs("div", { className: "bw-schedule-mode", role: "radiogroup", "aria-label": t('scheduleModeAria'), children: [_jsx("button", { type: "button", role: "radio", "aria-checked": effectiveSchedulingPreference === 'flexible', className: `bw-schedule-mode-btn${effectiveSchedulingPreference === 'flexible' ? ' is-active' : ''}`, onClick: () => handleSchedulingPreferenceChange('flexible'), children: _jsx("span", { children: t('scheduleModeFlexible') }) }), _jsx("button", { type: "button", role: "radio", "aria-checked": effectiveSchedulingPreference === 'exact', className: `bw-schedule-mode-btn${effectiveSchedulingPreference === 'exact' ? ' is-active' : ''}`, onClick: () => handleSchedulingPreferenceChange('exact'), children: _jsx("span", { children: t('scheduleModeExact') }) })] })) : null;
|
|
920
|
-
const slotsAreaKey = `${selectedServiceId ?? 'none'}-${selectedStaffId ?? 'any'}-${selectedDate ?? 'none'}-${effectiveSchedulingPreference}-${selectedFlexibleWindowId}-${isAvailabilityLoading ? 'loading' : 'ready'}`;
|
|
921
|
-
const slotsArea = (_jsx("div", { className: "bw-slots-fade", children: !selectedService ? (_jsx("p", { className: "bw-slots-empty", children: t('slotsSelectServicePrompt') })) : effectiveSchedulingPreference === 'flexible' ? (selectedDate ? (_jsxs("div", { className: "bw-window-options", children: [selectedDateFlexibleWindows.map((option, index) => {
|
|
960
|
+
const slotsAreaKey = `${selectedServiceId ?? 'none'}-${selectedStaffId ?? 'any'}-${selectedDate ?? 'none'}-${effectiveSchedulingPreference}-${selectedFlexibleWindowId}-${serviceAddressCacheKey}-${isAvailabilityLoading ? 'loading' : 'ready'}`;
|
|
961
|
+
const slotsArea = (_jsx("div", { className: "bw-slots-fade", children: !selectedService ? (_jsx("p", { className: "bw-slots-empty", children: t('slotsSelectServicePrompt') })) : requiresServiceAddressBeforeSlots && !serviceAddressGeo ? (_jsx("p", { className: "bw-slots-empty", children: t('slotsEnterAddressPrompt') })) : effectiveSchedulingPreference === 'flexible' ? (selectedDate ? (_jsxs("div", { className: "bw-window-options", children: [selectedDateFlexibleWindows.map((option, index) => {
|
|
922
962
|
const isActive = selectedFlexibleWindowId === option.id;
|
|
923
963
|
return (_jsxs("button", { type: "button", className: `bw-window-option${isActive ? ' is-active' : ''}`, style: { '--bw-slot-i': index }, onClick: () => handleFlexibleWindowSelect(option.id), children: [_jsxs("span", { className: "bw-window-option-main", children: [_jsx("span", { children: t(option.labelKey) }), _jsx("span", { children: formatFlexibleWindowTimeRange(option, intlLocale) })] }), _jsx("span", { className: "bw-window-option-desc", children: t(option.descriptionKey) })] }, option.id));
|
|
924
964
|
}), selectedDateFlexibleWindows.length === 0 ? (_jsx("p", { className: "bw-no-slots", children: t('slotsEmptyForDate') })) : null] })) : (_jsx("p", { className: "bw-no-slots", children: t('flexPickDateFirst') }))) : isAvailabilityLoading ? (_jsx(AvailabilitySkeleton, {})) : selectedDate ? (selectedDateSlots.length > 0 ? (_jsx("div", { className: "bw-time-slots", children: selectedDateSlots.map((slot, index) => {
|
|
@@ -1019,6 +1059,51 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1019
1059
|
const hiddenCount = submitBlockers.length - visibleBlockers.length;
|
|
1020
1060
|
return (_jsxs("div", { className: "bw-submit-help", role: "status", "aria-live": "polite", children: [_jsx("span", { className: "bw-submit-help-title", children: isDisplayQuoteLoading ? t('helpCalculating') : t('helpComplete') }), !isDisplayQuoteLoading && 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] }));
|
|
1021
1061
|
}
|
|
1062
|
+
function resetAddressDependentSelection() {
|
|
1063
|
+
if (holdId) {
|
|
1064
|
+
void client
|
|
1065
|
+
.releasePublicBookingHold({ siteSlug, holdId, sessionToken })
|
|
1066
|
+
.catch(() => undefined);
|
|
1067
|
+
}
|
|
1068
|
+
setAvailabilityByDate(new Map());
|
|
1069
|
+
setSelectedDate(null);
|
|
1070
|
+
setSelectedSlot(null);
|
|
1071
|
+
setHoldId(null);
|
|
1072
|
+
setHoldExpiresAt(null);
|
|
1073
|
+
setHeldStaffId(null);
|
|
1074
|
+
setPendingSlotKey(null);
|
|
1075
|
+
setError(null);
|
|
1076
|
+
if (selectedService?.publicBookingMode !== 'async') {
|
|
1077
|
+
setViewState('slots');
|
|
1078
|
+
setMobileStep((current) => (current > 2 ? 2 : current));
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
function handleServiceAddressChange(next) {
|
|
1082
|
+
setServiceAddress(next);
|
|
1083
|
+
if (serviceAddressPlace !== null) {
|
|
1084
|
+
setServiceAddressPlace(null);
|
|
1085
|
+
}
|
|
1086
|
+
if (requiresServiceAddressBeforeSlots) {
|
|
1087
|
+
resetAddressDependentSelection();
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
function handleServiceAddressPlaceSelect(place) {
|
|
1091
|
+
setServiceAddressPlace(place);
|
|
1092
|
+
if (requiresServiceAddressBeforeSlots) {
|
|
1093
|
+
resetAddressDependentSelection();
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
function renderServiceAddressGate(idSuffix = '') {
|
|
1097
|
+
if (!selectedService ||
|
|
1098
|
+
!selectedServiceRequiresSlot ||
|
|
1099
|
+
isEditingService ||
|
|
1100
|
+
!requiresServiceAddressBeforeSlots) {
|
|
1101
|
+
return null;
|
|
1102
|
+
}
|
|
1103
|
+
const addressId = `bw-service-address-gate${idSuffix}`;
|
|
1104
|
+
const addressErrorId = `${addressId}-error`;
|
|
1105
|
+
return (_jsxs("div", { className: "bw-service-address-section", children: [_jsx("div", { className: "bw-section-divider" }), _jsxs("div", { className: `bw-field bw-field--wide${showServiceAddressError ? ' is-invalid' : ''}`, children: [_jsxs("label", { htmlFor: addressId, children: [t('contactServiceAddress'), " ", _jsx("span", { className: "bw-required", children: "*" })] }), _jsx(BookingAddressAutocompleteInput, { id: addressId, value: serviceAddress, onChange: handleServiceAddressChange, onPlaceSelect: handleServiceAddressPlaceSelect, onValidityChange: setIsServiceAddressValid, mapsApiKey: setup?.mapsBrowserKey, "aria-invalid": showServiceAddressError, "aria-describedby": showServiceAddressRequiredError ? addressErrorId : undefined, showInvalid: showServiceAddressSelectionError, invalidMessage: t('contactServiceAddressInvalid'), lookupUnavailableMessage: t('contactServiceAddressLookupUnavailable'), placeholder: t('placeholderServiceAddress') }), showServiceAddressRequiredError ? (_jsx("span", { className: "bw-field-error", id: addressErrorId, children: t('contactServiceAddressRequired') })) : null] })] }));
|
|
1106
|
+
}
|
|
1022
1107
|
function renderContactFields(idSuffix) {
|
|
1023
1108
|
const nameId = `bw-name${idSuffix}`;
|
|
1024
1109
|
const emailId = `bw-email${idSuffix}`;
|
|
@@ -1027,7 +1112,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1027
1112
|
const emailErrorId = `${emailId}-error`;
|
|
1028
1113
|
const phoneErrorId = `${phoneId}-error`;
|
|
1029
1114
|
const addressErrorId = `${addressId}-error`;
|
|
1030
|
-
return (_jsxs("div", { className: "bw-form-fields", children: [_jsxs("div", { className: "bw-field", children: [_jsxs("label", { htmlFor: nameId, children: [t('contactFullName'), " ", _jsx("span", { className: "bw-required", children: "*" })] }), _jsx("input", { id: nameId, value: customerName, onChange: (event) => setCustomerName(event.target.value), placeholder: t('placeholderFullName') })] }), _jsxs("div", { className: `bw-field${showCustomerEmailError ? ' is-invalid' : ''}`, children: [_jsxs("label", { htmlFor: emailId, children: [t('contactEmail'), " ", _jsx("span", { className: "bw-required", children: "*" })] }), _jsx("input", { id: emailId, type: "email", required: true, value: customerEmail, onChange: (event) => setCustomerEmail(event.target.value), "aria-invalid": showCustomerEmailError, "aria-describedby": showCustomerEmailError ? emailErrorId : undefined, placeholder: t('placeholderEmail') }), showCustomerEmailError ? (_jsx("span", { className: "bw-field-error", id: emailErrorId, children: t('contactEmailInvalid') })) : null] }), _jsxs("div", { className: `bw-field${showCustomerPhoneError ? ' is-invalid' : ''}`, children: [_jsxs("label", { htmlFor: phoneId, children: [t('contactPhone'), " ", _jsx("span", { className: "bw-required", children: "*" })] }), _jsx("input", { id: phoneId, type: "tel", required: true, value: customerPhone, onChange: (event) => setCustomerPhone(event.target.value), "aria-invalid": showCustomerPhoneError, "aria-describedby": showCustomerPhoneError ? phoneErrorId : undefined, placeholder: t('placeholderPhone') }), showCustomerPhoneError ? (_jsx("span", { className: "bw-field-error", id: phoneErrorId, children: t('contactPhoneRequired') })) : null] }), !isReschedule ? (_jsxs("div", { className: `bw-field bw-field--wide${showServiceAddressError ? ' is-invalid' : ''}`, children: [_jsxs("label", { htmlFor: addressId, children: [t('contactServiceAddress'), ' ', _jsx("span", { className: "bw-required", children: "*" })] }), _jsx(BookingAddressAutocompleteInput, { id: addressId, value: serviceAddress, onChange:
|
|
1115
|
+
return (_jsxs("div", { className: "bw-form-fields", children: [_jsxs("div", { className: "bw-field", children: [_jsxs("label", { htmlFor: nameId, children: [t('contactFullName'), " ", _jsx("span", { className: "bw-required", children: "*" })] }), _jsx("input", { id: nameId, value: customerName, onChange: (event) => setCustomerName(event.target.value), placeholder: t('placeholderFullName') })] }), _jsxs("div", { className: `bw-field${showCustomerEmailError ? ' is-invalid' : ''}`, children: [_jsxs("label", { htmlFor: emailId, children: [t('contactEmail'), " ", _jsx("span", { className: "bw-required", children: "*" })] }), _jsx("input", { id: emailId, type: "email", required: true, value: customerEmail, onChange: (event) => setCustomerEmail(event.target.value), "aria-invalid": showCustomerEmailError, "aria-describedby": showCustomerEmailError ? emailErrorId : undefined, placeholder: t('placeholderEmail') }), showCustomerEmailError ? (_jsx("span", { className: "bw-field-error", id: emailErrorId, children: t('contactEmailInvalid') })) : null] }), _jsxs("div", { className: `bw-field${showCustomerPhoneError ? ' is-invalid' : ''}`, children: [_jsxs("label", { htmlFor: phoneId, children: [t('contactPhone'), " ", _jsx("span", { className: "bw-required", children: "*" })] }), _jsx("input", { id: phoneId, type: "tel", required: true, value: customerPhone, onChange: (event) => setCustomerPhone(event.target.value), "aria-invalid": showCustomerPhoneError, "aria-describedby": showCustomerPhoneError ? phoneErrorId : undefined, placeholder: t('placeholderPhone') }), showCustomerPhoneError ? (_jsx("span", { className: "bw-field-error", id: phoneErrorId, children: t('contactPhoneRequired') })) : null] }), !isReschedule && !requiresServiceAddressBeforeSlots ? (_jsxs("div", { className: `bw-field bw-field--wide${showServiceAddressError ? ' is-invalid' : ''}`, children: [_jsxs("label", { htmlFor: addressId, children: [t('contactServiceAddress'), ' ', _jsx("span", { className: "bw-required", children: "*" })] }), _jsx(BookingAddressAutocompleteInput, { id: addressId, value: serviceAddress, onChange: handleServiceAddressChange, onPlaceSelect: handleServiceAddressPlaceSelect, onValidityChange: setIsServiceAddressValid, mapsApiKey: setup?.mapsBrowserKey, "aria-invalid": showServiceAddressError, "aria-describedby": showServiceAddressRequiredError ? addressErrorId : undefined, showInvalid: showServiceAddressSelectionError, invalidMessage: t('contactServiceAddressInvalid'), lookupUnavailableMessage: t('contactServiceAddressLookupUnavailable'), placeholder: t('placeholderServiceAddress') }), showServiceAddressRequiredError ? (_jsx("span", { className: "bw-field-error", id: addressErrorId, children: t('contactServiceAddressRequired') })) : null] })) : null] }));
|
|
1031
1116
|
}
|
|
1032
1117
|
function renderIntakeFields(idPrefix) {
|
|
1033
1118
|
return (_jsx(IntakeFormFields, { form: selectedService?.intakeForm, responses: intakeResponses, onChange: (fieldId, value) => setIntakeResponses((prev) => ({ ...prev, [fieldId]: value })), idPrefix: idPrefix, mode: selectedServicePath }));
|
|
@@ -1110,8 +1195,10 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1110
1195
|
const previousHoldExpiresAt = holdExpiresAt;
|
|
1111
1196
|
const previousHeldStaffId = heldStaffId;
|
|
1112
1197
|
setPendingSlotKey(nextSlotKey);
|
|
1113
|
-
const reservationStaffId =
|
|
1114
|
-
|
|
1198
|
+
const reservationStaffId = autoAssignProvider
|
|
1199
|
+
? null
|
|
1200
|
+
: (selectedStaffId ?? slot.availableStaffIds[0] ?? null);
|
|
1201
|
+
if (!reservationStaffId && !autoAssignProvider) {
|
|
1115
1202
|
setPendingSlotKey(null);
|
|
1116
1203
|
setError(t('errorNoStaffForSlot'));
|
|
1117
1204
|
return;
|
|
@@ -1131,16 +1218,17 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1131
1218
|
const result = await client.reservePublicBookingHold({
|
|
1132
1219
|
siteSlug,
|
|
1133
1220
|
serviceId: selectedServiceId,
|
|
1134
|
-
staffMemberId: reservationStaffId,
|
|
1221
|
+
staffMemberId: reservationStaffId ?? undefined,
|
|
1135
1222
|
startTime: zonedDateTimeToTimestamp(selectedDate, slot.time, bookingTimezone),
|
|
1136
1223
|
endTime: zonedDateTimeToTimestamp(selectedDate, slot.endTime, bookingTimezone),
|
|
1137
1224
|
sessionToken,
|
|
1225
|
+
serviceAddressGeo,
|
|
1138
1226
|
});
|
|
1139
1227
|
setSelectedSlot(slot);
|
|
1140
1228
|
setPendingSlotKey(null);
|
|
1141
1229
|
setHoldId(result.holdId);
|
|
1142
1230
|
setHoldExpiresAt(result.expiresAt);
|
|
1143
|
-
setHeldStaffId(reservationStaffId);
|
|
1231
|
+
setHeldStaffId(result.staffMemberId ?? reservationStaffId);
|
|
1144
1232
|
setError(null);
|
|
1145
1233
|
// Auto-advance the desktop right pane to the details/form view
|
|
1146
1234
|
// once the hold is reserved. Mobile flow keeps the existing
|
|
@@ -1267,11 +1355,15 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1267
1355
|
siteSlug,
|
|
1268
1356
|
serviceId: selectedServiceId,
|
|
1269
1357
|
staffMemberId: isFlexibleRequest
|
|
1270
|
-
?
|
|
1358
|
+
? autoAssignProvider
|
|
1359
|
+
? undefined
|
|
1360
|
+
: (selectedStaffId ?? undefined)
|
|
1271
1361
|
: selectedServiceRequiresSlot
|
|
1272
|
-
?
|
|
1273
|
-
|
|
1274
|
-
|
|
1362
|
+
? autoAssignProvider
|
|
1363
|
+
? (heldStaffId ?? undefined)
|
|
1364
|
+
: (heldStaffId ??
|
|
1365
|
+
selectedStaffId ??
|
|
1366
|
+
selectedSlot?.availableStaffIds[0])
|
|
1275
1367
|
: undefined,
|
|
1276
1368
|
startTime: newStartTime,
|
|
1277
1369
|
endTime: newEndTime,
|
|
@@ -1283,6 +1375,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1283
1375
|
customerPhone: trimmedCustomerPhone,
|
|
1284
1376
|
customerNotes: bookingNotes,
|
|
1285
1377
|
location: trimmedServiceAddress,
|
|
1378
|
+
serviceAddressGeo,
|
|
1286
1379
|
intakeResponses,
|
|
1287
1380
|
photos: bookingPhotos.map((photo) => ({
|
|
1288
1381
|
filename: photo.filename,
|
|
@@ -1330,6 +1423,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1330
1423
|
setCustomerEmail('');
|
|
1331
1424
|
setCustomerPhone('');
|
|
1332
1425
|
setServiceAddress('');
|
|
1426
|
+
setServiceAddressPlace(null);
|
|
1333
1427
|
setCustomerNotes('');
|
|
1334
1428
|
setBookingPhotos([]);
|
|
1335
1429
|
setPhotoError(null);
|
|
@@ -1410,9 +1504,10 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1410
1504
|
}, children: [service.image ? (_jsxs("span", { className: `bw-svc-image${isActive ? ' is-checked' : ''}`, children: [_jsx("img", { src: service.image.url, alt: service.image.alt ??
|
|
1411
1505
|
pickLocaleField(service, 'name', locale) ??
|
|
1412
1506
|
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));
|
|
1413
|
-
})] }, group.key))) }) }) })] })) }), selectedService &&
|
|
1507
|
+
})] }, group.key))) }) }) })] })) }), renderServiceAddressGate(), selectedService &&
|
|
1414
1508
|
selectedServiceRequiresSlot &&
|
|
1415
|
-
!isEditingService
|
|
1509
|
+
!isEditingService &&
|
|
1510
|
+
!autoAssignProvider ? (_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) => {
|
|
1416
1511
|
const isActive = selectedServiceId === service._id;
|
|
1417
1512
|
return (_jsxs("div", { children: [_jsxs("button", { type: "button", className: "bw-svc-row bw-svc-row--full", onMouseEnter: () => {
|
|
1418
1513
|
void prefetchAvailability({
|
|
@@ -1436,29 +1531,37 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1436
1531
|
pickLocaleField(service, 'name', locale) ??
|
|
1437
1532
|
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) ??
|
|
1438
1533
|
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));
|
|
1439
|
-
})] }, 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: () => {
|
|
1534
|
+
})] }, group.key))), renderServiceAddressGate('-mobile')] })] }), _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: () => {
|
|
1440
1535
|
setCalendarMonth(option.date);
|
|
1441
1536
|
setMonthOpen(false);
|
|
1442
1537
|
}, children: option.label }, option.value))) })] })) : null] }), _jsxs("div", { className: "bw-cal-navs", children: [_jsx("button", { type: "button", className: "bw-cal-nav", onMouseEnter: () => {
|
|
1443
1538
|
if (!sameMonth(calendarMonth, startOfMonth(new Date())) &&
|
|
1444
|
-
selectedServiceId
|
|
1539
|
+
selectedServiceId &&
|
|
1540
|
+
(!requiresServiceAddressBeforeSlots ||
|
|
1541
|
+
serviceAddressGeo)) {
|
|
1445
1542
|
void prefetchAvailability({
|
|
1446
1543
|
client,
|
|
1447
1544
|
siteSlug,
|
|
1448
1545
|
selectedServiceId,
|
|
1449
1546
|
calendarMonth: addMonths(calendarMonth, -1),
|
|
1450
1547
|
cacheRef: availabilityCacheRef,
|
|
1548
|
+
serviceAddressGeo,
|
|
1549
|
+
serviceAddressCacheKey,
|
|
1451
1550
|
});
|
|
1452
1551
|
}
|
|
1453
1552
|
}, 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: () => {
|
|
1454
1553
|
if (!sameMonth(calendarMonth, addMonths(startOfMonth(new Date()), 3)) &&
|
|
1455
|
-
selectedServiceId
|
|
1554
|
+
selectedServiceId &&
|
|
1555
|
+
(!requiresServiceAddressBeforeSlots ||
|
|
1556
|
+
serviceAddressGeo)) {
|
|
1456
1557
|
void prefetchAvailability({
|
|
1457
1558
|
client,
|
|
1458
1559
|
siteSlug,
|
|
1459
1560
|
selectedServiceId,
|
|
1460
1561
|
calendarMonth: addMonths(calendarMonth, 1),
|
|
1461
1562
|
cacheRef: availabilityCacheRef,
|
|
1563
|
+
serviceAddressGeo,
|
|
1564
|
+
serviceAddressCacheKey,
|
|
1462
1565
|
});
|
|
1463
1566
|
}
|
|
1464
1567
|
}, onClick: () => setCalendarMonth((current) => addMonths(current, 1)), disabled: sameMonth(calendarMonth, addMonths(startOfMonth(new Date()), 3)), "aria-label": t('nextMonth'), children: _jsx(ChevronRightIcon, {}) })] })] }), schedulingPreferenceControl, _jsx("div", { className: "bw-cal-weekdays", children: [
|
|
@@ -1519,7 +1622,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1519
1622
|
setHeldStaffId(null);
|
|
1520
1623
|
setMobileStep((current) => current < 2 ? 2 : current);
|
|
1521
1624
|
}, children: day.getDate() }, dateKey));
|
|
1522
|
-
}) }), !selectedService ? (_jsx("p", { className: "bw-cal-prompt", children: t('calendarSelectServicePrompt') })) : null, _jsxs("div", { className: "bw-timezone", children: [_jsx(GlobeIcon, {}), _jsx("span", { children: bookingTimezone })] })] }), 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: effectiveSchedulingPreference === 'flexible'
|
|
1625
|
+
}) }), !selectedService ? (_jsx("p", { className: "bw-cal-prompt", children: t('calendarSelectServicePrompt') })) : null, _jsxs("div", { className: "bw-timezone", children: [_jsx(GlobeIcon, {}), _jsx("span", { children: bookingTimezone })] })] }), selectedService && !isEditingService && !autoAssignProvider ? (_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: effectiveSchedulingPreference === 'flexible'
|
|
1523
1626
|
? t('flexWindowHeading')
|
|
1524
1627
|
: 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
|
|
1525
1628
|
? t('summaryTitleReschedule')
|
|
@@ -1536,9 +1639,9 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1536
1639
|
textDecoration: 'line-through',
|
|
1537
1640
|
opacity: 0.55,
|
|
1538
1641
|
}, 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) ??
|
|
1539
|
-
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 ??
|
|
1642
|
+
selectedService.name })] }), !autoAssignProvider ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryProvider') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: selectedHeldStaff?.name ??
|
|
1540
1643
|
selectedStaff?.name ??
|
|
1541
|
-
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, selectedTimeLabel ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryTime') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: selectedTimeLabel })] })) : 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') })] }), !isReschedule ? (_jsxs("div", { className: "bw-summary-row bw-summary-row--stack", children: [_jsx("span", { children: t('contactServiceAddress') }), _jsx("span", { className: "bw-summary-val", children: serviceAddress.trim() || t('reviewNotProvided') })] })) : null] })] }), (() => {
|
|
1644
|
+
t('providerAny') })] })) : null, 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, selectedTimeLabel ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryTime') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: selectedTimeLabel })] })) : 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') })] }), !isReschedule ? (_jsxs("div", { className: "bw-summary-row bw-summary-row--stack", children: [_jsx("span", { children: t('contactServiceAddress') }), _jsx("span", { className: "bw-summary-val", children: serviceAddress.trim() || t('reviewNotProvided') })] })) : null] })] }), (() => {
|
|
1542
1645
|
const intakeRows = buildIntakeReviewRows(selectedService.intakeForm, intakeResponses, selectedServicePath, locale, t);
|
|
1543
1646
|
if (intakeRows.length === 0)
|
|
1544
1647
|
return null;
|
|
@@ -1561,9 +1664,9 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1561
1664
|
...summaryValStyle,
|
|
1562
1665
|
textDecoration: 'line-through',
|
|
1563
1666
|
opacity: 0.55,
|
|
1564
|
-
}, 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 ??
|
|
1667
|
+
}, 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 })] }), !autoAssignProvider ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryProvider') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: selectedHeldStaff?.name ??
|
|
1565
1668
|
selectedStaff?.name ??
|
|
1566
|
-
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, selectedTimeLabel ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryTime') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: selectedTimeLabel })] })) : 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) })] }), priceSummaryRows.map((row) => (_jsxs("div", { className: `bw-summary-row${row.isTotal ? ' bw-summary-total' : ''}`, children: [_jsx("span", { children: row.label }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: row.value })] }, row.key)))] })] })) : null, error ? (_jsx("div", { className: "bw-error", children: error })) : null, renderSubmitHelp(), payment ? (_jsx(StripePaymentBlock, { payment: payment, appointmentId: paymentAppointmentId, showInlineButton: false, actionTarget: mobilePaymentActionTarget, onSuccess: () => {
|
|
1669
|
+
t('providerAny') })] })) : null, 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, selectedTimeLabel ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryTime') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: selectedTimeLabel })] })) : 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) })] }), priceSummaryRows.map((row) => (_jsxs("div", { className: `bw-summary-row${row.isTotal ? ' bw-summary-total' : ''}`, children: [_jsx("span", { children: row.label }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: row.value })] }, row.key)))] })] })) : null, error ? (_jsx("div", { className: "bw-error", children: error })) : null, renderSubmitHelp(), payment ? (_jsx(StripePaymentBlock, { payment: payment, appointmentId: paymentAppointmentId, showInlineButton: false, actionTarget: mobilePaymentActionTarget, onSuccess: () => {
|
|
1567
1670
|
setSuccess(t('successPaymentReceived'));
|
|
1568
1671
|
setPayment(null);
|
|
1569
1672
|
}, onError: setError })) : (_jsxs("button", { type: "button", className: `bw-confirm-btn${!canSubmit || isDisplayQuoteLoading ? ' is-disabled' : ''}`, "aria-disabled": !canSubmit || isDisplayQuoteLoading, onClick: handleConfirmAttempt, children: [_jsx("span", { children: isSubmitting
|
|
@@ -2175,7 +2278,7 @@ function IntakeField({ field, value, onChange, idPrefix, }) {
|
|
|
2175
2278
|
} }), field.helpText ? _jsx("p", { className: "bw-help", children: field.helpText }) : null] }));
|
|
2176
2279
|
}
|
|
2177
2280
|
const ADDRESS_PREDICTION_DEBOUNCE_MS = 180;
|
|
2178
|
-
function BookingAddressAutocompleteInput({ id, value, onChange, onValidityChange, mapsApiKey, placeholder, ariaInvalid, ariaDescribedBy, showInvalid, invalidMessage, lookupUnavailableMessage, }) {
|
|
2281
|
+
function BookingAddressAutocompleteInput({ id, value, onChange, onPlaceSelect, onValidityChange, mapsApiKey, placeholder, ariaInvalid, ariaDescribedBy, showInvalid, invalidMessage, lookupUnavailableMessage, }) {
|
|
2179
2282
|
const inputRef = useRef(null);
|
|
2180
2283
|
const debounceRef = useRef(null);
|
|
2181
2284
|
const serviceRef = useRef(null);
|
|
@@ -2263,7 +2366,7 @@ function BookingAddressAutocompleteInput({ id, value, onChange, onValidityChange
|
|
|
2263
2366
|
return;
|
|
2264
2367
|
placesService.getDetails({
|
|
2265
2368
|
placeId: prediction.place_id,
|
|
2266
|
-
fields: ['formatted_address', 'place_id'],
|
|
2369
|
+
fields: ['formatted_address', 'place_id', 'geometry'],
|
|
2267
2370
|
sessionToken: sessionTokenRef.current ?? undefined,
|
|
2268
2371
|
}, (place, status) => {
|
|
2269
2372
|
const places = getLoadedGooglePlacesApi();
|
|
@@ -2274,10 +2377,25 @@ function BookingAddressAutocompleteInput({ id, value, onChange, onValidityChange
|
|
|
2274
2377
|
const formatted = place?.formatted_address?.trim();
|
|
2275
2378
|
if (status !== okStatus || !formatted) {
|
|
2276
2379
|
setAcceptedValue('');
|
|
2380
|
+
onPlaceSelect?.(null);
|
|
2277
2381
|
return;
|
|
2278
2382
|
}
|
|
2383
|
+
const lat = place?.geometry?.location?.lat();
|
|
2384
|
+
const lng = place?.geometry?.location?.lng();
|
|
2385
|
+
const hasCoordinates = typeof lat === 'number' &&
|
|
2386
|
+
typeof lng === 'number' &&
|
|
2387
|
+
Number.isFinite(lat) &&
|
|
2388
|
+
Number.isFinite(lng);
|
|
2279
2389
|
setAcceptedValue(formatted);
|
|
2280
2390
|
onChange(formatted);
|
|
2391
|
+
onPlaceSelect?.(hasCoordinates
|
|
2392
|
+
? {
|
|
2393
|
+
formatted,
|
|
2394
|
+
lat,
|
|
2395
|
+
lng,
|
|
2396
|
+
...(place?.place_id ? { placeId: place.place_id } : {}),
|
|
2397
|
+
}
|
|
2398
|
+
: null);
|
|
2281
2399
|
closeDropdown();
|
|
2282
2400
|
inputRef.current?.blur();
|
|
2283
2401
|
});
|
|
@@ -2288,11 +2406,13 @@ function BookingAddressAutocompleteInput({ id, value, onChange, onValidityChange
|
|
|
2288
2406
|
const trimmed = next.trim();
|
|
2289
2407
|
if (!trimmed) {
|
|
2290
2408
|
setAcceptedValue('');
|
|
2409
|
+
onPlaceSelect?.(null);
|
|
2291
2410
|
closeDropdown();
|
|
2292
2411
|
return;
|
|
2293
2412
|
}
|
|
2294
2413
|
if (trimmed !== acceptedValue) {
|
|
2295
2414
|
setAcceptedValue('');
|
|
2415
|
+
onPlaceSelect?.(null);
|
|
2296
2416
|
}
|
|
2297
2417
|
if (!placesReady || loadFailed)
|
|
2298
2418
|
return;
|
|
@@ -2998,8 +3118,8 @@ function findFirstAvailableDate(availabilityByDate, month) {
|
|
|
2998
3118
|
.sort();
|
|
2999
3119
|
return dates[0] ?? null;
|
|
3000
3120
|
}
|
|
3001
|
-
async function prefetchAvailability({ client, siteSlug, selectedServiceId, calendarMonth, cacheRef, }) {
|
|
3002
|
-
const requestKey = getAvailabilityCacheKey(siteSlug, selectedServiceId, calendarMonth);
|
|
3121
|
+
async function prefetchAvailability({ client, siteSlug, selectedServiceId, calendarMonth, cacheRef, serviceAddressGeo, serviceAddressCacheKey = 'none', }) {
|
|
3122
|
+
const requestKey = getAvailabilityCacheKey(siteSlug, selectedServiceId, calendarMonth, serviceAddressCacheKey);
|
|
3003
3123
|
if (cacheRef.current.has(requestKey)) {
|
|
3004
3124
|
return;
|
|
3005
3125
|
}
|
|
@@ -3008,6 +3128,7 @@ async function prefetchAvailability({ client, siteSlug, selectedServiceId, calen
|
|
|
3008
3128
|
serviceId: selectedServiceId,
|
|
3009
3129
|
startDate: formatDateKey(calendarMonth),
|
|
3010
3130
|
endDate: formatDateKey(endOfMonth(calendarMonth)),
|
|
3131
|
+
serviceAddressGeo,
|
|
3011
3132
|
});
|
|
3012
3133
|
const nextMap = new Map();
|
|
3013
3134
|
for (const day of result.dates) {
|
|
@@ -3015,8 +3136,8 @@ async function prefetchAvailability({ client, siteSlug, selectedServiceId, calen
|
|
|
3015
3136
|
}
|
|
3016
3137
|
cacheRef.current.set(requestKey, nextMap);
|
|
3017
3138
|
}
|
|
3018
|
-
function getAvailabilityCacheKey(siteSlug, serviceId, month) {
|
|
3019
|
-
return `${siteSlug}:${serviceId}:${optionCurrentValue(month)}`;
|
|
3139
|
+
function getAvailabilityCacheKey(siteSlug, serviceId, month, serviceAddressCacheKey = 'none') {
|
|
3140
|
+
return `${siteSlug}:${serviceId}:${optionCurrentValue(month)}:${serviceAddressCacheKey}`;
|
|
3020
3141
|
}
|
|
3021
3142
|
function formatHoldCountdown(totalSeconds) {
|
|
3022
3143
|
const minutes = Math.floor(totalSeconds / 60);
|