@asksable/site-connector 0.6.7 → 0.6.9
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 +251 -47
- 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 +10 -0
- package/dist/translations.d.ts +6 -0
- package/dist/translations.d.ts.map +1 -1
- package/dist/translations.js +6 -0
- package/dist/translations.js.map +1 -1
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
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([]);
|
|
@@ -324,17 +325,44 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
324
325
|
const selectedQuoteOverride = selectedService && initialQuoteOverride
|
|
325
326
|
? getApplicableInitialQuoteOverride(initialSelection, selectedService, initialQuoteOverride)
|
|
326
327
|
: null;
|
|
328
|
+
const defaultTaxRate = setup?.defaultTaxRate ?? null;
|
|
327
329
|
const displayQuote = selectedService && selectedQuoteOverride
|
|
328
|
-
? quoteFromOverride(selectedService, selectedQuoteOverride)
|
|
329
|
-
:
|
|
330
|
+
? quoteFromOverride(selectedService, selectedQuoteOverride, defaultTaxRate)
|
|
331
|
+
: selectedService
|
|
332
|
+
? (quote ??
|
|
333
|
+
(selectedService.pricingMode === 'calculated'
|
|
334
|
+
? null
|
|
335
|
+
: quoteFromService(selectedService, defaultTaxRate)))
|
|
336
|
+
: null;
|
|
330
337
|
const isDisplayQuoteLoading = isQuoteLoading && !displayQuote;
|
|
331
338
|
const bookingContextRows = useMemo(() => buildBookingContextRows({
|
|
332
339
|
intakeResponses,
|
|
333
340
|
quote: displayQuote,
|
|
334
341
|
locale: intlLocale,
|
|
335
342
|
}), [displayQuote, intakeResponses, intlLocale]);
|
|
343
|
+
const priceSummaryRows = useMemo(() => buildPriceSummaryRows({
|
|
344
|
+
service: selectedService,
|
|
345
|
+
quote: displayQuote,
|
|
346
|
+
isLoading: isDisplayQuoteLoading,
|
|
347
|
+
locale: intlLocale,
|
|
348
|
+
t,
|
|
349
|
+
}), [displayQuote, intlLocale, isDisplayQuoteLoading, selectedService, t]);
|
|
350
|
+
const totalPriceSummaryRow = priceSummaryRows.find((row) => row.isTotal) ?? priceSummaryRows.at(-1);
|
|
336
351
|
const selectedServiceRequiresSlot = selectedService?.publicBookingMode !== 'async';
|
|
337
|
-
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;
|
|
338
366
|
const effectiveSchedulingPreference = flexibleSchedulingEnabled ? schedulingPreference : 'exact';
|
|
339
367
|
const bookingTimezone = resolveBookingTimezone({
|
|
340
368
|
workspaceTimezone: setup?.workspaceTimezone,
|
|
@@ -470,6 +498,11 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
470
498
|
}
|
|
471
499
|
return setup.staff.filter((staffMember) => selectedService.assignedStaffIds.includes(staffMember._id));
|
|
472
500
|
}, [selectedService, setup]);
|
|
501
|
+
useEffect(() => {
|
|
502
|
+
if (autoAssignProvider && selectedStaffId !== null) {
|
|
503
|
+
setSelectedStaffId(null);
|
|
504
|
+
}
|
|
505
|
+
}, [autoAssignProvider, selectedStaffId]);
|
|
473
506
|
useEffect(() => {
|
|
474
507
|
// Wait for availableStaff to populate before clearing - otherwise
|
|
475
508
|
// a pre-seeded staffMemberId (e.g. from rescheduleContext) gets
|
|
@@ -488,6 +521,13 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
488
521
|
setSelectedDate(null);
|
|
489
522
|
return;
|
|
490
523
|
}
|
|
524
|
+
if (requiresServiceAddressBeforeSlots && !serviceAddressGeo) {
|
|
525
|
+
setAvailabilityByDate(new Map());
|
|
526
|
+
setSelectedDate(null);
|
|
527
|
+
setSelectedSlot(null);
|
|
528
|
+
setIsAvailabilityLoading(false);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
491
531
|
if (effectiveSchedulingPreference === 'flexible') {
|
|
492
532
|
setAvailabilityByDate(new Map());
|
|
493
533
|
setIsAvailabilityLoading(false);
|
|
@@ -522,7 +562,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
522
562
|
// instant (no refetch) and we always know which staff are
|
|
523
563
|
// available on a given date - even when a single provider is
|
|
524
564
|
// selected - so we can disable unavailable rows in the dropdown.
|
|
525
|
-
const requestKey = getAvailabilityCacheKey(siteSlug, selectedServiceId, calendarMonth);
|
|
565
|
+
const requestKey = getAvailabilityCacheKey(siteSlug, selectedServiceId, calendarMonth, serviceAddressCacheKey);
|
|
526
566
|
const cachedAvailability = availabilityCacheRef.current.get(requestKey);
|
|
527
567
|
if (cachedAvailability) {
|
|
528
568
|
setAvailabilityByDate(cachedAvailability);
|
|
@@ -542,6 +582,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
542
582
|
serviceId: selectedServiceId,
|
|
543
583
|
startDate: monthStart,
|
|
544
584
|
endDate: monthEnd,
|
|
585
|
+
serviceAddressGeo,
|
|
545
586
|
})
|
|
546
587
|
.then((result) => {
|
|
547
588
|
if (cancelled) {
|
|
@@ -589,6 +630,9 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
589
630
|
selectedServiceId,
|
|
590
631
|
selectedStaffId,
|
|
591
632
|
selectedService,
|
|
633
|
+
requiresServiceAddressBeforeSlots,
|
|
634
|
+
serviceAddressCacheKey,
|
|
635
|
+
serviceAddressGeo,
|
|
592
636
|
setup?.businessHours,
|
|
593
637
|
siteSlug,
|
|
594
638
|
todayDateKey,
|
|
@@ -600,17 +644,25 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
600
644
|
return;
|
|
601
645
|
}
|
|
602
646
|
const nextMonth = addMonths(calendarMonth, 1);
|
|
647
|
+
if (requiresServiceAddressBeforeSlots && !serviceAddressGeo) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
603
650
|
void prefetchAvailability({
|
|
604
651
|
client,
|
|
605
652
|
siteSlug,
|
|
606
653
|
selectedServiceId,
|
|
607
654
|
calendarMonth: nextMonth,
|
|
608
655
|
cacheRef: availabilityCacheRef,
|
|
656
|
+
serviceAddressGeo,
|
|
657
|
+
serviceAddressCacheKey,
|
|
609
658
|
});
|
|
610
659
|
}, [
|
|
611
660
|
calendarMonth,
|
|
612
661
|
client,
|
|
613
662
|
effectiveSchedulingPreference,
|
|
663
|
+
requiresServiceAddressBeforeSlots,
|
|
664
|
+
serviceAddressCacheKey,
|
|
665
|
+
serviceAddressGeo,
|
|
614
666
|
selectedService?.publicBookingMode,
|
|
615
667
|
selectedServiceId,
|
|
616
668
|
selectedStaffId,
|
|
@@ -639,6 +691,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
639
691
|
selectedService?.publicBookingMode,
|
|
640
692
|
selectedServiceId,
|
|
641
693
|
selectedStaffId,
|
|
694
|
+
serviceAddressCacheKey,
|
|
642
695
|
]);
|
|
643
696
|
useEffect(() => {
|
|
644
697
|
if (previousSelectedServiceIdRef.current === selectedServiceId) {
|
|
@@ -856,7 +909,8 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
856
909
|
// slot to advance. Step 3 is the details form - needs contact name,
|
|
857
910
|
// valid email, and any required intake fields filled. Step 4 is
|
|
858
911
|
// the review screen (submit, not advance).
|
|
859
|
-
const canAdvanceStep1 = Boolean(selectedService
|
|
912
|
+
const canAdvanceStep1 = Boolean(selectedService &&
|
|
913
|
+
(!requiresServiceAddressBeforeSlots || serviceAddressGeo));
|
|
860
914
|
const hasScheduleSelection = !selectedServiceRequiresSlot ||
|
|
861
915
|
(effectiveSchedulingPreference === 'flexible'
|
|
862
916
|
? Boolean(selectedDate && selectedFlexibleWindow)
|
|
@@ -896,15 +950,15 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
896
950
|
// label. The row scrolls horizontally when content exceeds the
|
|
897
951
|
// available width, so it works on desktop AND mobile without a
|
|
898
952
|
// breakpoint-specific layout.
|
|
899
|
-
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) => {
|
|
900
954
|
const isActive = selectedStaffId === staffMember._id;
|
|
901
955
|
const isAvailable = staffIdsAvailableOnSelectedDate === null ||
|
|
902
956
|
staffIdsAvailableOnSelectedDate.has(staffMember._id);
|
|
903
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));
|
|
904
958
|
})] })) : null;
|
|
905
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;
|
|
906
|
-
const slotsAreaKey = `${selectedServiceId ?? 'none'}-${selectedStaffId ?? 'any'}-${selectedDate ?? 'none'}-${effectiveSchedulingPreference}-${selectedFlexibleWindowId}-${isAvailabilityLoading ? 'loading' : 'ready'}`;
|
|
907
|
-
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) => {
|
|
908
962
|
const isActive = selectedFlexibleWindowId === option.id;
|
|
909
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));
|
|
910
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) => {
|
|
@@ -1005,6 +1059,51 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1005
1059
|
const hiddenCount = submitBlockers.length - visibleBlockers.length;
|
|
1006
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] }));
|
|
1007
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() {
|
|
1097
|
+
if (!selectedService ||
|
|
1098
|
+
!selectedServiceRequiresSlot ||
|
|
1099
|
+
isEditingService ||
|
|
1100
|
+
!requiresServiceAddressBeforeSlots) {
|
|
1101
|
+
return null;
|
|
1102
|
+
}
|
|
1103
|
+
const addressId = 'bw-service-address-gate';
|
|
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
|
+
}
|
|
1008
1107
|
function renderContactFields(idSuffix) {
|
|
1009
1108
|
const nameId = `bw-name${idSuffix}`;
|
|
1010
1109
|
const emailId = `bw-email${idSuffix}`;
|
|
@@ -1013,7 +1112,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1013
1112
|
const emailErrorId = `${emailId}-error`;
|
|
1014
1113
|
const phoneErrorId = `${phoneId}-error`;
|
|
1015
1114
|
const addressErrorId = `${addressId}-error`;
|
|
1016
|
-
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] }));
|
|
1017
1116
|
}
|
|
1018
1117
|
function renderIntakeFields(idPrefix) {
|
|
1019
1118
|
return (_jsx(IntakeFormFields, { form: selectedService?.intakeForm, responses: intakeResponses, onChange: (fieldId, value) => setIntakeResponses((prev) => ({ ...prev, [fieldId]: value })), idPrefix: idPrefix, mode: selectedServicePath }));
|
|
@@ -1096,8 +1195,10 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1096
1195
|
const previousHoldExpiresAt = holdExpiresAt;
|
|
1097
1196
|
const previousHeldStaffId = heldStaffId;
|
|
1098
1197
|
setPendingSlotKey(nextSlotKey);
|
|
1099
|
-
const reservationStaffId =
|
|
1100
|
-
|
|
1198
|
+
const reservationStaffId = autoAssignProvider
|
|
1199
|
+
? null
|
|
1200
|
+
: (selectedStaffId ?? slot.availableStaffIds[0] ?? null);
|
|
1201
|
+
if (!reservationStaffId && !autoAssignProvider) {
|
|
1101
1202
|
setPendingSlotKey(null);
|
|
1102
1203
|
setError(t('errorNoStaffForSlot'));
|
|
1103
1204
|
return;
|
|
@@ -1117,16 +1218,17 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1117
1218
|
const result = await client.reservePublicBookingHold({
|
|
1118
1219
|
siteSlug,
|
|
1119
1220
|
serviceId: selectedServiceId,
|
|
1120
|
-
staffMemberId: reservationStaffId,
|
|
1221
|
+
staffMemberId: reservationStaffId ?? undefined,
|
|
1121
1222
|
startTime: zonedDateTimeToTimestamp(selectedDate, slot.time, bookingTimezone),
|
|
1122
1223
|
endTime: zonedDateTimeToTimestamp(selectedDate, slot.endTime, bookingTimezone),
|
|
1123
1224
|
sessionToken,
|
|
1225
|
+
serviceAddressGeo,
|
|
1124
1226
|
});
|
|
1125
1227
|
setSelectedSlot(slot);
|
|
1126
1228
|
setPendingSlotKey(null);
|
|
1127
1229
|
setHoldId(result.holdId);
|
|
1128
1230
|
setHoldExpiresAt(result.expiresAt);
|
|
1129
|
-
setHeldStaffId(reservationStaffId);
|
|
1231
|
+
setHeldStaffId(result.staffMemberId ?? reservationStaffId);
|
|
1130
1232
|
setError(null);
|
|
1131
1233
|
// Auto-advance the desktop right pane to the details/form view
|
|
1132
1234
|
// once the hold is reserved. Mobile flow keeps the existing
|
|
@@ -1253,11 +1355,15 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1253
1355
|
siteSlug,
|
|
1254
1356
|
serviceId: selectedServiceId,
|
|
1255
1357
|
staffMemberId: isFlexibleRequest
|
|
1256
|
-
?
|
|
1358
|
+
? autoAssignProvider
|
|
1359
|
+
? undefined
|
|
1360
|
+
: (selectedStaffId ?? undefined)
|
|
1257
1361
|
: selectedServiceRequiresSlot
|
|
1258
|
-
?
|
|
1259
|
-
|
|
1260
|
-
|
|
1362
|
+
? autoAssignProvider
|
|
1363
|
+
? (heldStaffId ?? undefined)
|
|
1364
|
+
: (heldStaffId ??
|
|
1365
|
+
selectedStaffId ??
|
|
1366
|
+
selectedSlot?.availableStaffIds[0])
|
|
1261
1367
|
: undefined,
|
|
1262
1368
|
startTime: newStartTime,
|
|
1263
1369
|
endTime: newEndTime,
|
|
@@ -1269,12 +1375,13 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1269
1375
|
customerPhone: trimmedCustomerPhone,
|
|
1270
1376
|
customerNotes: bookingNotes,
|
|
1271
1377
|
location: trimmedServiceAddress,
|
|
1378
|
+
serviceAddressGeo,
|
|
1272
1379
|
intakeResponses,
|
|
1273
1380
|
photos: bookingPhotos.map((photo) => ({
|
|
1274
1381
|
filename: photo.filename,
|
|
1275
1382
|
data: photo.data,
|
|
1276
1383
|
})),
|
|
1277
|
-
quotedTotalCents:
|
|
1384
|
+
quotedTotalCents: selectedQuoteOverride?.totalCents,
|
|
1278
1385
|
bookingHoldId: selectedServiceRequiresSlot && !isFlexibleRequest
|
|
1279
1386
|
? holdId
|
|
1280
1387
|
: undefined,
|
|
@@ -1316,6 +1423,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1316
1423
|
setCustomerEmail('');
|
|
1317
1424
|
setCustomerPhone('');
|
|
1318
1425
|
setServiceAddress('');
|
|
1426
|
+
setServiceAddressPlace(null);
|
|
1319
1427
|
setCustomerNotes('');
|
|
1320
1428
|
setBookingPhotos([]);
|
|
1321
1429
|
setPhotoError(null);
|
|
@@ -1396,9 +1504,10 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1396
1504
|
}, children: [service.image ? (_jsxs("span", { className: `bw-svc-image${isActive ? ' is-checked' : ''}`, children: [_jsx("img", { src: service.image.url, alt: service.image.alt ??
|
|
1397
1505
|
pickLocaleField(service, 'name', locale) ??
|
|
1398
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));
|
|
1399
|
-
})] }, group.key))) }) }) })] })) }), selectedService &&
|
|
1507
|
+
})] }, group.key))) }) }) })] })) }), renderServiceAddressGate(), selectedService &&
|
|
1400
1508
|
selectedServiceRequiresSlot &&
|
|
1401
|
-
!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) => {
|
|
1402
1511
|
const isActive = selectedServiceId === service._id;
|
|
1403
1512
|
return (_jsxs("div", { children: [_jsxs("button", { type: "button", className: "bw-svc-row bw-svc-row--full", onMouseEnter: () => {
|
|
1404
1513
|
void prefetchAvailability({
|
|
@@ -1427,24 +1536,32 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1427
1536
|
setMonthOpen(false);
|
|
1428
1537
|
}, children: option.label }, option.value))) })] })) : null] }), _jsxs("div", { className: "bw-cal-navs", children: [_jsx("button", { type: "button", className: "bw-cal-nav", onMouseEnter: () => {
|
|
1429
1538
|
if (!sameMonth(calendarMonth, startOfMonth(new Date())) &&
|
|
1430
|
-
selectedServiceId
|
|
1539
|
+
selectedServiceId &&
|
|
1540
|
+
(!requiresServiceAddressBeforeSlots ||
|
|
1541
|
+
serviceAddressGeo)) {
|
|
1431
1542
|
void prefetchAvailability({
|
|
1432
1543
|
client,
|
|
1433
1544
|
siteSlug,
|
|
1434
1545
|
selectedServiceId,
|
|
1435
1546
|
calendarMonth: addMonths(calendarMonth, -1),
|
|
1436
1547
|
cacheRef: availabilityCacheRef,
|
|
1548
|
+
serviceAddressGeo,
|
|
1549
|
+
serviceAddressCacheKey,
|
|
1437
1550
|
});
|
|
1438
1551
|
}
|
|
1439
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: () => {
|
|
1440
1553
|
if (!sameMonth(calendarMonth, addMonths(startOfMonth(new Date()), 3)) &&
|
|
1441
|
-
selectedServiceId
|
|
1554
|
+
selectedServiceId &&
|
|
1555
|
+
(!requiresServiceAddressBeforeSlots ||
|
|
1556
|
+
serviceAddressGeo)) {
|
|
1442
1557
|
void prefetchAvailability({
|
|
1443
1558
|
client,
|
|
1444
1559
|
siteSlug,
|
|
1445
1560
|
selectedServiceId,
|
|
1446
1561
|
calendarMonth: addMonths(calendarMonth, 1),
|
|
1447
1562
|
cacheRef: availabilityCacheRef,
|
|
1563
|
+
serviceAddressGeo,
|
|
1564
|
+
serviceAddressCacheKey,
|
|
1448
1565
|
});
|
|
1449
1566
|
}
|
|
1450
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: [
|
|
@@ -1505,7 +1622,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1505
1622
|
setHeldStaffId(null);
|
|
1506
1623
|
setMobileStep((current) => current < 2 ? 2 : current);
|
|
1507
1624
|
}, children: day.getDate() }, dateKey));
|
|
1508
|
-
}) }), !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'
|
|
1509
1626
|
? t('flexWindowHeading')
|
|
1510
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
|
|
1511
1628
|
? t('summaryTitleReschedule')
|
|
@@ -1522,18 +1639,16 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1522
1639
|
textDecoration: 'line-through',
|
|
1523
1640
|
opacity: 0.55,
|
|
1524
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) ??
|
|
1525
|
-
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 ??
|
|
1526
1643
|
selectedStaff?.name ??
|
|
1527
|
-
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] })] }), (() => {
|
|
1528
1645
|
const intakeRows = buildIntakeReviewRows(selectedService.intakeForm, intakeResponses, selectedServicePath, locale, t);
|
|
1529
1646
|
if (intakeRows.length === 0)
|
|
1530
1647
|
return null;
|
|
1531
1648
|
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))) })] }));
|
|
1532
1649
|
})(), bookingContextRows.length > 0 ? (_jsxs("div", { className: "bw-summary-group", children: [_jsx("span", { className: "bw-summary-subhead", children: t('bookingContextTitle') }), _jsx("div", { className: "bw-summary-rows", children: bookingContextRows.map((row) => (_jsxs("div", { className: "bw-summary-row bw-summary-row--stack", children: [_jsx("span", { children: row.label }), _jsx("span", { className: "bw-summary-val", children: row.value })] }, row.key))) })] })) : null, customerNotes.trim() ? (_jsxs("div", { className: "bw-summary-group", children: [_jsx("span", { className: "bw-summary-subhead", children: isReschedule
|
|
1533
1650
|
? t('notesLabelReschedule')
|
|
1534
|
-
: t('reviewNotesSection') }), _jsx("p", { className: "bw-summary-notes", children: customerNotes.trim() })] })) : null, !isReschedule && bookingPhotos.length > 0 ? (_jsxs("div", { className: "bw-summary-group", children: [_jsx("span", { className: "bw-summary-subhead", children: t('photosLabel') }), _jsx("p", { className: "bw-summary-notes", children: t('photosCount', { count: bookingPhotos.length }) })] })) : null, _jsx("div", { className: "bw-summary-rows bw-summary-rows--total", children: _jsxs("div", { className:
|
|
1535
|
-
? t('summaryCalculating')
|
|
1536
|
-
: t('summaryEstimatedTotal') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: formatServicePrice(selectedService, displayQuote, 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: effectiveSchedulingPreference === 'flexible'
|
|
1651
|
+
: t('reviewNotesSection') }), _jsx("p", { className: "bw-summary-notes", children: customerNotes.trim() })] })) : null, !isReschedule && bookingPhotos.length > 0 ? (_jsxs("div", { className: "bw-summary-group", children: [_jsx("span", { className: "bw-summary-subhead", children: t('photosLabel') }), _jsx("p", { className: "bw-summary-notes", children: t('photosCount', { count: bookingPhotos.length }) })] })) : null, _jsx("div", { className: "bw-summary-rows bw-summary-rows--total", children: 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 }), _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: effectiveSchedulingPreference === 'flexible'
|
|
1537
1652
|
? t('flexWindowHeading')
|
|
1538
1653
|
: t('slotsHeading') }), effectiveSchedulingPreference === 'flexible' ? (_jsx("span", { className: "bw-slots-count", children: selectedDateFlexibleWindows.length })) : 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', ''), renderBookingContext(), renderNotesField('bw-notes', t('notesLabel'), t('notesPlaceholder')), renderPhotoField('bw-photos'), selectedService ? (_jsxs("div", { className: "bw-summary", children: [_jsx("span", { className: "bw-summary-title", children: isReschedule
|
|
1539
1654
|
? t('summaryTitleReschedule')
|
|
@@ -1549,11 +1664,9 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1549
1664
|
...summaryValStyle,
|
|
1550
1665
|
textDecoration: 'line-through',
|
|
1551
1666
|
opacity: 0.55,
|
|
1552
|
-
}, 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 ??
|
|
1553
1668
|
selectedStaff?.name ??
|
|
1554
|
-
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:
|
|
1555
|
-
? t('summaryCalculating')
|
|
1556
|
-
: t('summaryEstimatedTotal') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: formatServicePrice(selectedService, displayQuote, intlLocale) })] })] })] })) : 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: () => {
|
|
1557
1670
|
setSuccess(t('successPaymentReceived'));
|
|
1558
1671
|
setPayment(null);
|
|
1559
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
|
|
@@ -1570,9 +1683,12 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1570
1683
|
? t('summaryNewTime')
|
|
1571
1684
|
: t('summaryWhen') }), _jsxs("span", { className: "bw-details-meta-value", children: [formatReadableDate(selectedDate, intlLocale), _jsx("br", {}), selectedTimeLabel] })] })] })) : 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 ??
|
|
1572
1685
|
selectedStaff?.name ??
|
|
1573
|
-
t('providerAny') })] }), !isReschedule && serviceAddress.trim() ? (_jsxs("div", { className: "bw-details-meta-row", children: [_jsx("span", { className: "bw-details-meta-icon", children: _jsx(LocationPinIcon, {}) }), _jsx("span", { className: "bw-details-meta-value", children: serviceAddress.trim() })] })) : null, _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: bookingTimezone })] }), _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:
|
|
1574
|
-
|
|
1575
|
-
|
|
1686
|
+
t('providerAny') })] }), !isReschedule && serviceAddress.trim() ? (_jsxs("div", { className: "bw-details-meta-row", children: [_jsx("span", { className: "bw-details-meta-icon", children: _jsx(LocationPinIcon, {}) }), _jsx("span", { className: "bw-details-meta-value", children: serviceAddress.trim() })] })) : null, _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: bookingTimezone })] }), _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: totalPriceSummaryRow?.label ??
|
|
1687
|
+
t('summaryEstimatedTotal') }), _jsx("span", { className: "bw-details-meta-value", children: totalPriceSummaryRow?.value ??
|
|
1688
|
+
formatServicePrice(selectedService, displayQuote, intlLocale) }), priceSummaryRows.length > 1 ? (_jsx("span", { className: "bw-details-meta-subvalue", children: priceSummaryRows
|
|
1689
|
+
.filter((row) => !row.isTotal)
|
|
1690
|
+
.map((row) => `${row.label}: ${row.value}`)
|
|
1691
|
+
.join(' · ') })) : null] })] }), 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'), renderBookingContext(), renderNotesField('bw-notes-d', isReschedule ? t('notesLabelReschedule') : t('notesLabel'), isReschedule
|
|
1576
1692
|
? t('notesPlaceholderReschedule')
|
|
1577
1693
|
: t('notesPlaceholder')), renderPhotoField('bw-photos-d'), error ? _jsx("div", { className: "bw-error", children: error }) : null, renderSubmitHelp(), !payment &&
|
|
1578
1694
|
(forcedState === 'payment-full' ||
|
|
@@ -2162,7 +2278,7 @@ function IntakeField({ field, value, onChange, idPrefix, }) {
|
|
|
2162
2278
|
} }), field.helpText ? _jsx("p", { className: "bw-help", children: field.helpText }) : null] }));
|
|
2163
2279
|
}
|
|
2164
2280
|
const ADDRESS_PREDICTION_DEBOUNCE_MS = 180;
|
|
2165
|
-
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, }) {
|
|
2166
2282
|
const inputRef = useRef(null);
|
|
2167
2283
|
const debounceRef = useRef(null);
|
|
2168
2284
|
const serviceRef = useRef(null);
|
|
@@ -2250,7 +2366,7 @@ function BookingAddressAutocompleteInput({ id, value, onChange, onValidityChange
|
|
|
2250
2366
|
return;
|
|
2251
2367
|
placesService.getDetails({
|
|
2252
2368
|
placeId: prediction.place_id,
|
|
2253
|
-
fields: ['formatted_address', 'place_id'],
|
|
2369
|
+
fields: ['formatted_address', 'place_id', 'geometry'],
|
|
2254
2370
|
sessionToken: sessionTokenRef.current ?? undefined,
|
|
2255
2371
|
}, (place, status) => {
|
|
2256
2372
|
const places = getLoadedGooglePlacesApi();
|
|
@@ -2261,10 +2377,25 @@ function BookingAddressAutocompleteInput({ id, value, onChange, onValidityChange
|
|
|
2261
2377
|
const formatted = place?.formatted_address?.trim();
|
|
2262
2378
|
if (status !== okStatus || !formatted) {
|
|
2263
2379
|
setAcceptedValue('');
|
|
2380
|
+
onPlaceSelect?.(null);
|
|
2264
2381
|
return;
|
|
2265
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);
|
|
2266
2389
|
setAcceptedValue(formatted);
|
|
2267
2390
|
onChange(formatted);
|
|
2391
|
+
onPlaceSelect?.(hasCoordinates
|
|
2392
|
+
? {
|
|
2393
|
+
formatted,
|
|
2394
|
+
lat,
|
|
2395
|
+
lng,
|
|
2396
|
+
...(place?.place_id ? { placeId: place.place_id } : {}),
|
|
2397
|
+
}
|
|
2398
|
+
: null);
|
|
2268
2399
|
closeDropdown();
|
|
2269
2400
|
inputRef.current?.blur();
|
|
2270
2401
|
});
|
|
@@ -2275,11 +2406,13 @@ function BookingAddressAutocompleteInput({ id, value, onChange, onValidityChange
|
|
|
2275
2406
|
const trimmed = next.trim();
|
|
2276
2407
|
if (!trimmed) {
|
|
2277
2408
|
setAcceptedValue('');
|
|
2409
|
+
onPlaceSelect?.(null);
|
|
2278
2410
|
closeDropdown();
|
|
2279
2411
|
return;
|
|
2280
2412
|
}
|
|
2281
2413
|
if (trimmed !== acceptedValue) {
|
|
2282
2414
|
setAcceptedValue('');
|
|
2415
|
+
onPlaceSelect?.(null);
|
|
2283
2416
|
}
|
|
2284
2417
|
if (!placesReady || loadFailed)
|
|
2285
2418
|
return;
|
|
@@ -2378,21 +2511,91 @@ function formatServicePrice(service, quote, locale = 'en-US') {
|
|
|
2378
2511
|
}
|
|
2379
2512
|
return currencyFormatter(service.currency, locale).format(service.priceCents / 100);
|
|
2380
2513
|
}
|
|
2381
|
-
function
|
|
2514
|
+
function buildPriceSummaryRows({ service, quote, isLoading, locale, t, }) {
|
|
2515
|
+
if (!service)
|
|
2516
|
+
return [];
|
|
2517
|
+
const totalLabel = isLoading
|
|
2518
|
+
? t('summaryCalculating')
|
|
2519
|
+
: t('summaryEstimatedTotal');
|
|
2520
|
+
const totalValue = formatServicePrice(service, quote, locale);
|
|
2521
|
+
if (!quote || (quote.taxCents ?? 0) <= 0) {
|
|
2522
|
+
return [
|
|
2523
|
+
{
|
|
2524
|
+
key: 'total',
|
|
2525
|
+
label: totalLabel,
|
|
2526
|
+
value: totalValue,
|
|
2527
|
+
isTotal: true,
|
|
2528
|
+
},
|
|
2529
|
+
];
|
|
2530
|
+
}
|
|
2531
|
+
const formatter = currencyFormatter(quote.currency, locale);
|
|
2532
|
+
const taxLabel = quote.taxRateBps
|
|
2533
|
+
? `${quote.taxRateName ?? t('summarySalesTax')} (${formatPercentBps(quote.taxRateBps)})`
|
|
2534
|
+
: (quote.taxRateName ?? t('summarySalesTax'));
|
|
2535
|
+
return [
|
|
2536
|
+
{
|
|
2537
|
+
key: 'subtotal',
|
|
2538
|
+
label: t('summarySubtotal'),
|
|
2539
|
+
value: formatter.format(quote.subtotalCents / 100),
|
|
2540
|
+
},
|
|
2541
|
+
{
|
|
2542
|
+
key: 'tax',
|
|
2543
|
+
label: taxLabel,
|
|
2544
|
+
value: formatter.format((quote.taxCents ?? 0) / 100),
|
|
2545
|
+
},
|
|
2546
|
+
{
|
|
2547
|
+
key: 'total',
|
|
2548
|
+
label: totalLabel,
|
|
2549
|
+
value: totalValue,
|
|
2550
|
+
isTotal: true,
|
|
2551
|
+
},
|
|
2552
|
+
];
|
|
2553
|
+
}
|
|
2554
|
+
function formatPercentBps(rateBps) {
|
|
2555
|
+
const percent = rateBps / 100;
|
|
2556
|
+
return `${Number.isInteger(percent) ? percent.toFixed(0) : percent.toFixed(2).replace(/0+$/, '').replace(/\.$/, '')}%`;
|
|
2557
|
+
}
|
|
2558
|
+
function quoteFromOverride(service, override, taxRate) {
|
|
2559
|
+
return buildDisplayQuote({
|
|
2560
|
+
service,
|
|
2561
|
+
subtotalCents: override.totalCents,
|
|
2562
|
+
taxRate,
|
|
2563
|
+
displayLabel: taxRate ? undefined : override.label,
|
|
2564
|
+
rateSnapshot: {
|
|
2565
|
+
quotedTotalCents: override.totalCents,
|
|
2566
|
+
quoteSource: 'host-estimator',
|
|
2567
|
+
},
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
2570
|
+
function quoteFromService(service, taxRate) {
|
|
2571
|
+
return buildDisplayQuote({
|
|
2572
|
+
service,
|
|
2573
|
+
subtotalCents: service.priceCents,
|
|
2574
|
+
taxRate,
|
|
2575
|
+
});
|
|
2576
|
+
}
|
|
2577
|
+
function buildDisplayQuote({ service, subtotalCents, taxRate, displayLabel, rateSnapshot, }) {
|
|
2578
|
+
const taxRateBps = taxRate?.rateBps ?? 0;
|
|
2579
|
+
const taxCents = Math.round((subtotalCents * taxRateBps) / 10_000);
|
|
2382
2580
|
return {
|
|
2383
2581
|
currency: service.currency || 'usd',
|
|
2384
|
-
subtotalCents
|
|
2582
|
+
subtotalCents,
|
|
2385
2583
|
discountCents: 0,
|
|
2386
2584
|
insuranceCents: 0,
|
|
2387
2585
|
additionalFeesCents: 0,
|
|
2388
|
-
|
|
2586
|
+
taxCents,
|
|
2587
|
+
taxRateName: taxRate?.name,
|
|
2588
|
+
taxRateBps: taxRate?.rateBps,
|
|
2589
|
+
totalCents: subtotalCents + taxCents,
|
|
2389
2590
|
rateSnapshot: {
|
|
2390
2591
|
pricingMode: 'fixed',
|
|
2391
2592
|
servicePriceCents: service.priceCents,
|
|
2392
|
-
|
|
2393
|
-
|
|
2593
|
+
taxCents,
|
|
2594
|
+
taxRateName: taxRate?.name,
|
|
2595
|
+
taxRateBps: taxRate?.rateBps,
|
|
2596
|
+
...(rateSnapshot ?? {}),
|
|
2394
2597
|
},
|
|
2395
|
-
displayLabel
|
|
2598
|
+
displayLabel,
|
|
2396
2599
|
};
|
|
2397
2600
|
}
|
|
2398
2601
|
function getInitialQuoteOverride(initialSelection) {
|
|
@@ -2915,8 +3118,8 @@ function findFirstAvailableDate(availabilityByDate, month) {
|
|
|
2915
3118
|
.sort();
|
|
2916
3119
|
return dates[0] ?? null;
|
|
2917
3120
|
}
|
|
2918
|
-
async function prefetchAvailability({ client, siteSlug, selectedServiceId, calendarMonth, cacheRef, }) {
|
|
2919
|
-
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);
|
|
2920
3123
|
if (cacheRef.current.has(requestKey)) {
|
|
2921
3124
|
return;
|
|
2922
3125
|
}
|
|
@@ -2925,6 +3128,7 @@ async function prefetchAvailability({ client, siteSlug, selectedServiceId, calen
|
|
|
2925
3128
|
serviceId: selectedServiceId,
|
|
2926
3129
|
startDate: formatDateKey(calendarMonth),
|
|
2927
3130
|
endDate: formatDateKey(endOfMonth(calendarMonth)),
|
|
3131
|
+
serviceAddressGeo,
|
|
2928
3132
|
});
|
|
2929
3133
|
const nextMap = new Map();
|
|
2930
3134
|
for (const day of result.dates) {
|
|
@@ -2932,8 +3136,8 @@ async function prefetchAvailability({ client, siteSlug, selectedServiceId, calen
|
|
|
2932
3136
|
}
|
|
2933
3137
|
cacheRef.current.set(requestKey, nextMap);
|
|
2934
3138
|
}
|
|
2935
|
-
function getAvailabilityCacheKey(siteSlug, serviceId, month) {
|
|
2936
|
-
return `${siteSlug}:${serviceId}:${optionCurrentValue(month)}`;
|
|
3139
|
+
function getAvailabilityCacheKey(siteSlug, serviceId, month, serviceAddressCacheKey = 'none') {
|
|
3140
|
+
return `${siteSlug}:${serviceId}:${optionCurrentValue(month)}:${serviceAddressCacheKey}`;
|
|
2937
3141
|
}
|
|
2938
3142
|
function formatHoldCountdown(totalSeconds) {
|
|
2939
3143
|
const minutes = Math.floor(totalSeconds / 60);
|