@asksable/site-connector 0.6.8 → 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.
@@ -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;AA+MhD;;;;;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,2CAq0GpB"}
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,2CAq+GpB"}
@@ -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 flexibleSchedulingEnabled = allowFlexibleScheduling && selectedServiceRequiresSlot && !isReschedule;
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() {
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
+ }
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: setServiceAddress, 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] }));
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 = selectedStaffId ?? slot.availableStaffIds[0] ?? null;
1114
- if (!reservationStaffId) {
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
- ? (selectedStaffId ?? undefined)
1358
+ ? autoAssignProvider
1359
+ ? undefined
1360
+ : (selectedStaffId ?? undefined)
1271
1361
  : selectedServiceRequiresSlot
1272
- ? (heldStaffId ??
1273
- selectedStaffId ??
1274
- selectedSlot?.availableStaffIds[0])
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 ? (_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) => {
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({
@@ -1441,24 +1536,32 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
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);