@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.
@@ -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
- : quote;
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 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;
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: 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] }));
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 = selectedStaffId ?? slot.availableStaffIds[0] ?? null;
1100
- if (!reservationStaffId) {
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
- ? (selectedStaffId ?? undefined)
1358
+ ? autoAssignProvider
1359
+ ? undefined
1360
+ : (selectedStaffId ?? undefined)
1257
1361
  : selectedServiceRequiresSlot
1258
- ? (heldStaffId ??
1259
- selectedStaffId ??
1260
- selectedSlot?.availableStaffIds[0])
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: displayQuote?.totalCents,
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 ? (_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) => {
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: "bw-summary-row bw-summary-total", children: [_jsx("span", { children: isDisplayQuoteLoading
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: "bw-summary-row bw-summary-total", children: [_jsx("span", { children: isDisplayQuoteLoading
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: isDisplayQuoteLoading
1574
- ? t('summaryCalculating')
1575
- : t('summaryEstimatedTotal') }), _jsx("span", { className: "bw-details-meta-value", children: formatServicePrice(selectedService, displayQuote, intlLocale) })] })] }), holdSecondsRemaining !== null ? (_jsxs("div", { className: "bw-details-meta-row bw-details-meta-row--hold", "aria-live": "polite", children: [_jsx("span", { className: "bw-details-meta-icon", children: _jsx(ClockIcon, {}) }), _jsxs("div", { className: "bw-details-meta-text", children: [_jsx("span", { className: "bw-details-meta-label", children: t('summaryReservationHold') }), _jsx("span", { className: "bw-details-meta-value bw-details-hold", children: formatHoldCountdown(holdSecondsRemaining) })] })] })) : null] })] })) : null }), _jsx("div", { className: "bw-details-form", children: _jsxs("div", { className: "bw-form bw-form--details", children: [serviceWarning ? (_jsx(ServiceWarningCallout, { warning: serviceWarning })) : null, renderIntakeAndContact('bw-intake-d', '-d'), renderBookingContext(), renderNotesField('bw-notes-d', isReschedule ? t('notesLabelReschedule') : t('notesLabel'), isReschedule
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 quoteFromOverride(service, override) {
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: override.totalCents,
2582
+ subtotalCents,
2385
2583
  discountCents: 0,
2386
2584
  insuranceCents: 0,
2387
2585
  additionalFeesCents: 0,
2388
- totalCents: override.totalCents,
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
- quotedTotalCents: override.totalCents,
2393
- quoteSource: 'host-estimator',
2593
+ taxCents,
2594
+ taxRateName: taxRate?.name,
2595
+ taxRateBps: taxRate?.rateBps,
2596
+ ...(rateSnapshot ?? {}),
2394
2597
  },
2395
- displayLabel: override.label,
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);