@asksable/site-connector 0.6.2 → 0.6.3

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.
@@ -11,6 +11,29 @@ import './bones/registry.js';
11
11
  import { BookingWidgetPlaceholder } from './booking-widget-placeholder.js';
12
12
  import { useSableSiteAnalytics, useSableSiteClient, useSableSiteConfig, useTranslation, } from './provider.js';
13
13
  import { DEFAULT_LOCALE, localeToIntl, pickLocaleField, } from './translations.js';
14
+ const FLEXIBLE_WINDOW_OPTIONS = [
15
+ {
16
+ id: 'all-day',
17
+ startTime: '09:00',
18
+ endTime: '17:00',
19
+ labelKey: 'flexWindowAllDay',
20
+ descriptionKey: 'flexWindowAllDayDesc',
21
+ },
22
+ {
23
+ id: 'morning',
24
+ startTime: '09:00',
25
+ endTime: '12:00',
26
+ labelKey: 'flexWindowMorning',
27
+ descriptionKey: 'flexWindowMorningDesc',
28
+ },
29
+ {
30
+ id: 'afternoon',
31
+ startTime: '12:00',
32
+ endTime: '17:00',
33
+ labelKey: 'flexWindowAfternoon',
34
+ descriptionKey: 'flexWindowAfternoonDesc',
35
+ },
36
+ ];
14
37
  const stripePromises = new Map();
15
38
  function getStripePromise(publishableKey, connectAccountId) {
16
39
  const key = `${publishableKey}:${connectAccountId}`;
@@ -35,7 +58,7 @@ function findCalculatedServiceMissingIntake(setup) {
35
58
  }
36
59
  const MOBILE_PROGRESS_STEPS_SCHEDULED = [1, 2, 3, 4];
37
60
  const MOBILE_PROGRESS_STEPS_ASYNC = [1, 3, 4];
38
- export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'create', rescheduleContext, onRescheduleSubmit, initialSelection, successRedirectHref = '/', __devForceState, __devForceSuccess, }) {
61
+ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'create', rescheduleContext, onRescheduleSubmit, initialSelection, allowFlexibleScheduling = false, defaultSchedulingPreference = 'exact', successRedirectHref = '/', __devForceState, __devForceSuccess, }) {
39
62
  const forcedState = __devForceState ?? (__devForceSuccess ? 'success' : null);
40
63
  const isReschedule = mode === 'reschedule';
41
64
  // Reschedule summary holds longer values (full former-time
@@ -52,7 +75,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
52
75
  }
53
76
  : undefined;
54
77
  const client = useSableSiteClient();
55
- const { siteSlug } = useSableSiteConfig();
78
+ const { siteSlug, timezone: configuredTimezone } = useSableSiteConfig();
56
79
  const { t, locale } = useTranslation();
57
80
  const analytics = useSableSiteAnalytics();
58
81
  const intlLocale = localeToIntl(locale);
@@ -71,6 +94,8 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
71
94
  const [isAvailabilityLoading, setIsAvailabilityLoading] = useState(false);
72
95
  const [selectedDate, setSelectedDate] = useState(null);
73
96
  const [selectedSlot, setSelectedSlot] = useState(null);
97
+ const [schedulingPreference, setSchedulingPreference] = useState(() => defaultSchedulingPreference === 'flexible' ? 'flexible' : 'exact');
98
+ const [selectedFlexibleWindowId, setSelectedFlexibleWindowId] = useState(() => FLEXIBLE_WINDOW_OPTIONS[0]?.id ?? 'all-day');
74
99
  const [pendingSlotKey, setPendingSlotKey] = useState(null);
75
100
  // Service picker is progressive: full list shows initially, collapses
76
101
  // to a single "selected service" card once a service is picked, then
@@ -119,6 +144,15 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
119
144
  setViewState('details');
120
145
  }
121
146
  }, [__devForceState]);
147
+ useEffect(() => {
148
+ if (!allowFlexibleScheduling) {
149
+ setSchedulingPreference('exact');
150
+ return;
151
+ }
152
+ if (defaultSchedulingPreference === 'flexible') {
153
+ setSchedulingPreference('flexible');
154
+ }
155
+ }, [allowFlexibleScheduling, defaultSchedulingPreference]);
122
156
  const [monthOpen, setMonthOpen] = useState(false);
123
157
  const [holdNow, setHoldNow] = useState(() => Date.now());
124
158
  const [sessionToken] = useState(() => sessionTokenFromBrowser());
@@ -240,6 +274,15 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
240
274
  : quote;
241
275
  const isDisplayQuoteLoading = isQuoteLoading && !displayQuote;
242
276
  const selectedServiceRequiresSlot = selectedService?.publicBookingMode !== 'async';
277
+ const flexibleSchedulingEnabled = allowFlexibleScheduling && selectedServiceRequiresSlot && !isReschedule;
278
+ const effectiveSchedulingPreference = flexibleSchedulingEnabled ? schedulingPreference : 'exact';
279
+ const selectedFlexibleWindow = FLEXIBLE_WINDOW_OPTIONS.find((option) => option.id === selectedFlexibleWindowId) ?? FLEXIBLE_WINDOW_OPTIONS[0];
280
+ const bookingTimezone = resolveBookingTimezone({
281
+ workspaceTimezone: setup?.workspaceTimezone,
282
+ configuredTimezone,
283
+ fallbackStaffTimezone: setup?.staff?.[0]?.timezone,
284
+ });
285
+ const todayDateKey = dateKeyInTimeZone(Date.now(), bookingTimezone);
243
286
  const selectedServicePath = selectedServiceRequiresSlot
244
287
  ? 'scheduled'
245
288
  : 'async';
@@ -349,6 +392,21 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
349
392
  setSelectedDate(null);
350
393
  return;
351
394
  }
395
+ if (effectiveSchedulingPreference === 'flexible') {
396
+ setAvailabilityByDate(new Map());
397
+ setIsAvailabilityLoading(false);
398
+ if (selectedDate &&
399
+ !isFlexibleDateSelectable({
400
+ dateKey: selectedDate,
401
+ service: selectedService,
402
+ businessHours: setup?.businessHours ?? [],
403
+ timezone: bookingTimezone,
404
+ window: selectedFlexibleWindow,
405
+ })) {
406
+ setSelectedDate(null);
407
+ }
408
+ return;
409
+ }
352
410
  let cancelled = false;
353
411
  const monthStart = formatDateKey(calendarMonth);
354
412
  const monthEnd = formatDateKey(endOfMonth(calendarMonth));
@@ -419,14 +477,21 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
419
477
  }, [
420
478
  calendarMonth,
421
479
  client,
480
+ bookingTimezone,
481
+ effectiveSchedulingPreference,
422
482
  selectedDate,
423
483
  selectedService?.publicBookingMode,
424
484
  selectedServiceId,
425
485
  selectedStaffId,
486
+ selectedFlexibleWindow,
487
+ selectedService,
488
+ setup?.businessHours,
426
489
  siteSlug,
427
490
  ]);
428
491
  useEffect(() => {
429
- if (!selectedServiceId || selectedService?.publicBookingMode === 'async') {
492
+ if (!selectedServiceId ||
493
+ selectedService?.publicBookingMode === 'async' ||
494
+ effectiveSchedulingPreference === 'flexible') {
430
495
  return;
431
496
  }
432
497
  const nextMonth = addMonths(calendarMonth, 1);
@@ -440,6 +505,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
440
505
  }, [
441
506
  calendarMonth,
442
507
  client,
508
+ effectiveSchedulingPreference,
443
509
  selectedService?.publicBookingMode,
444
510
  selectedServiceId,
445
511
  selectedStaffId,
@@ -653,6 +719,8 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
653
719
  // date (regardless of who's currently filtered). Drives the
654
720
  // "Unavailable" subtitle + disabled state in the provider dropdown.
655
721
  const staffIdsAvailableOnSelectedDate = useMemo(() => {
722
+ if (effectiveSchedulingPreference === 'flexible')
723
+ return null;
656
724
  if (!selectedDate)
657
725
  return null;
658
726
  const slots = availabilityByDate.get(selectedDate);
@@ -665,7 +733,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
665
733
  }
666
734
  }
667
735
  return set;
668
- }, [availabilityByDate, selectedDate]);
736
+ }, [availabilityByDate, effectiveSchedulingPreference, selectedDate]);
669
737
  const selectedDateSlots = selectedDate
670
738
  ? (filteredAvailabilityByDate.get(selectedDate) ?? [])
671
739
  : [];
@@ -684,7 +752,16 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
684
752
  // valid email, and any required intake fields filled. Step 4 is
685
753
  // the review screen (submit, not advance).
686
754
  const canAdvanceStep1 = Boolean(selectedService);
687
- const canAdvanceStep2 = Boolean(selectedDate && selectedSlot);
755
+ const hasScheduleSelection = !selectedServiceRequiresSlot ||
756
+ (effectiveSchedulingPreference === 'flexible'
757
+ ? Boolean(selectedDate && selectedFlexibleWindow)
758
+ : Boolean(selectedDate && selectedSlot && holdId));
759
+ const selectedTimeLabel = selectedDate && effectiveSchedulingPreference === 'flexible'
760
+ ? formatFlexibleWindowTimeRange(selectedFlexibleWindow, intlLocale)
761
+ : selectedSlot
762
+ ? `${formatTimeLabel(selectedSlot.time, intlLocale)} – ${formatTimeLabel(selectedSlot.endTime, intlLocale)}`
763
+ : null;
764
+ const canAdvanceStep2 = hasScheduleSelection;
688
765
  const canAdvanceStep3 = Boolean(selectedService &&
689
766
  customerName.trim() &&
690
767
  isCustomerEmailValid &&
@@ -714,8 +791,12 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
714
791
  staffIdsAvailableOnSelectedDate.has(staffMember._id);
715
792
  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));
716
793
  })] })) : null;
717
- const slotsAreaKey = `${selectedServiceId ?? 'none'}-${selectedStaffId ?? 'any'}-${selectedDate ?? 'none'}-${isAvailabilityLoading ? 'loading' : 'ready'}`;
718
- const slotsArea = (_jsx("div", { className: "bw-slots-fade", children: !selectedService ? (_jsx("p", { className: "bw-slots-empty", children: t('slotsSelectServicePrompt') })) : isAvailabilityLoading ? (_jsx(AvailabilitySkeleton, {})) : selectedDate ? (selectedDateSlots.length > 0 ? (_jsx("div", { className: "bw-time-slots", children: selectedDateSlots.map((slot, index) => {
794
+ 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;
795
+ const slotsAreaKey = `${selectedServiceId ?? 'none'}-${selectedStaffId ?? 'any'}-${selectedDate ?? 'none'}-${effectiveSchedulingPreference}-${selectedFlexibleWindowId}-${isAvailabilityLoading ? 'loading' : 'ready'}`;
796
+ const slotsArea = (_jsx("div", { className: "bw-slots-fade", children: !selectedService ? (_jsx("p", { className: "bw-slots-empty", children: t('slotsSelectServicePrompt') })) : effectiveSchedulingPreference === 'flexible' ? (selectedDate ? (_jsx("div", { className: "bw-window-options", children: FLEXIBLE_WINDOW_OPTIONS.map((option, index) => {
797
+ const isActive = selectedFlexibleWindowId === option.id;
798
+ 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));
799
+ }) })) : (_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) => {
719
800
  const slotKey = `${slot.time}-${slot.endTime}`;
720
801
  const isPending = pendingSlotKey === slotKey;
721
802
  const isActive = isPending ||
@@ -726,8 +807,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
726
807
  : formatTimeLabel(slot.time, intlLocale) }, slotKey));
727
808
  }) })) : (_jsx("p", { className: "bw-no-slots", children: t('slotsEmptyForDate') }))) : null }, slotsAreaKey));
728
809
  const canSubmit = Boolean(selectedService &&
729
- (!selectedServiceRequiresSlot ||
730
- (selectedDate && selectedSlot && holdId)) &&
810
+ hasScheduleSelection &&
731
811
  isIntakeComplete &&
732
812
  (selectedService.pricingMode !== 'calculated' || quote) &&
733
813
  customerName.trim() &&
@@ -736,9 +816,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
736
816
  ? getSubmitBlockers({
737
817
  selectedService,
738
818
  selectedServiceRequiresSlot,
739
- selectedDate,
740
- selectedSlot,
741
- holdId,
819
+ hasScheduleSelection,
742
820
  isIntakeComplete,
743
821
  intakeResponses,
744
822
  selectedServicePath,
@@ -817,6 +895,35 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
817
895
  setMobileStep(3);
818
896
  }
819
897
  }
898
+ function handleSchedulingPreferenceChange(next) {
899
+ if (next === schedulingPreference)
900
+ return;
901
+ if (holdId) {
902
+ void client
903
+ .releasePublicBookingHold({ siteSlug, holdId, sessionToken })
904
+ .catch(() => undefined);
905
+ }
906
+ setSchedulingPreference(next);
907
+ setSelectedSlot(null);
908
+ setHoldId(null);
909
+ setHoldExpiresAt(null);
910
+ setHeldStaffId(null);
911
+ setPendingSlotKey(null);
912
+ setViewState('slots');
913
+ }
914
+ function handleFlexibleWindowSelect(optionId) {
915
+ if (!selectedDate)
916
+ return;
917
+ setSelectedFlexibleWindowId(optionId);
918
+ setSelectedSlot(null);
919
+ setHoldId(null);
920
+ setHoldExpiresAt(null);
921
+ setHeldStaffId(null);
922
+ setPendingSlotKey(null);
923
+ setError(null);
924
+ setViewState('details');
925
+ setMobileStep((current) => (current < 3 ? 3 : current));
926
+ }
820
927
  async function handleSlotSelect(slot) {
821
928
  if (!selectedServiceId || !selectedDate) {
822
929
  return;
@@ -851,8 +958,8 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
851
958
  siteSlug,
852
959
  serviceId: selectedServiceId,
853
960
  staffMemberId: reservationStaffId,
854
- startTime: Date.parse(`${selectedDate}T${slot.time}:00`),
855
- endTime: Date.parse(`${selectedDate}T${slot.endTime}:00`),
961
+ startTime: zonedDateTimeToTimestamp(selectedDate, slot.time, bookingTimezone),
962
+ endTime: zonedDateTimeToTimestamp(selectedDate, slot.endTime, bookingTimezone),
856
963
  sessionToken,
857
964
  });
858
965
  setSelectedSlot(slot);
@@ -917,17 +1024,37 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
917
1024
  if (!selectedServiceId ||
918
1025
  !selectedService ||
919
1026
  (selectedServiceRequiresSlot &&
920
- (!selectedDate || !selectedSlot || !holdId))) {
1027
+ (effectiveSchedulingPreference === 'flexible'
1028
+ ? !selectedDate || !selectedFlexibleWindow
1029
+ : !selectedDate || !selectedSlot || !holdId))) {
921
1030
  return;
922
1031
  }
923
1032
  setIsSubmitting(true);
924
1033
  setError(null);
925
- const newStartTime = selectedDate && selectedSlot
926
- ? Date.parse(`${selectedDate}T${selectedSlot.time}:00`)
927
- : Date.now();
928
- const newEndTime = selectedDate && selectedSlot
929
- ? Date.parse(`${selectedDate}T${selectedSlot.endTime}:00`)
930
- : newStartTime + selectedService.durationMinutes * 60 * 1000;
1034
+ const isFlexibleRequest = selectedServiceRequiresSlot &&
1035
+ effectiveSchedulingPreference === 'flexible' &&
1036
+ selectedDate != null &&
1037
+ selectedFlexibleWindow != null;
1038
+ const newStartTime = isFlexibleRequest
1039
+ ? zonedDateTimeToTimestamp(selectedDate, selectedFlexibleWindow.startTime, bookingTimezone)
1040
+ : selectedDate && selectedSlot
1041
+ ? zonedDateTimeToTimestamp(selectedDate, selectedSlot.time, bookingTimezone)
1042
+ : Date.now();
1043
+ const newEndTime = isFlexibleRequest
1044
+ ? zonedDateTimeToTimestamp(selectedDate, selectedFlexibleWindow.endTime, bookingTimezone)
1045
+ : selectedDate && selectedSlot
1046
+ ? zonedDateTimeToTimestamp(selectedDate, selectedSlot.endTime, bookingTimezone)
1047
+ : newStartTime + selectedService.durationMinutes * 60 * 1000;
1048
+ const bookingNotes = isFlexibleRequest
1049
+ ? appendFlexibleWindowNote({
1050
+ notes: customerNotes,
1051
+ dateKey: selectedDate,
1052
+ window: selectedFlexibleWindow,
1053
+ locale: intlLocale,
1054
+ timezone: bookingTimezone,
1055
+ t,
1056
+ })
1057
+ : customerNotes.trim() || undefined;
931
1058
  // Reschedule mode bypasses the public-create flow entirely.
932
1059
  // Service / staff / customer are already attached to the
933
1060
  // original appointment, so the host (admin path) or magic-link
@@ -960,26 +1087,28 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
960
1087
  const created = await client.createPublicBooking({
961
1088
  siteSlug,
962
1089
  serviceId: selectedServiceId,
963
- staffMemberId: selectedServiceRequiresSlot
964
- ? (heldStaffId ??
965
- selectedStaffId ??
966
- selectedSlot?.availableStaffIds[0])
967
- : undefined,
1090
+ staffMemberId: isFlexibleRequest
1091
+ ? (selectedStaffId ?? undefined)
1092
+ : selectedServiceRequiresSlot
1093
+ ? (heldStaffId ??
1094
+ selectedStaffId ??
1095
+ selectedSlot?.availableStaffIds[0])
1096
+ : undefined,
968
1097
  startTime: newStartTime,
969
1098
  endTime: newEndTime,
970
- timezone: selectedHeldStaff?.timezone ??
971
- availableStaff[0]?.timezone ??
972
- setup?.workspaceTimezone ??
973
- 'UTC',
1099
+ isAnytime: isFlexibleRequest ? true : undefined,
1100
+ timezone: bookingTimezone,
974
1101
  deliveryMethod: selectedService.deliveryMethods[0] ?? 'in-person',
975
1102
  customerName: customerName.trim(),
976
1103
  customerEmail: customerEmail.trim(),
977
1104
  customerPhone: customerPhone.trim() || undefined,
978
- customerNotes: customerNotes.trim() || undefined,
1105
+ customerNotes: bookingNotes,
979
1106
  intakeResponses,
980
1107
  quotedTotalCents: displayQuote?.totalCents,
981
- bookingHoldId: selectedServiceRequiresSlot ? holdId : undefined,
982
- bookingSessionToken: selectedServiceRequiresSlot
1108
+ bookingHoldId: selectedServiceRequiresSlot && !isFlexibleRequest
1109
+ ? holdId
1110
+ : undefined,
1111
+ bookingSessionToken: selectedServiceRequiresSlot && !isFlexibleRequest
983
1112
  ? sessionToken
984
1113
  : undefined,
985
1114
  });
@@ -1004,6 +1133,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
1004
1133
  service_name: pickLocaleField(selectedService, 'name', locale) ??
1005
1134
  selectedService.name,
1006
1135
  booking_mode: selectedService.publicBookingMode ?? 'scheduled',
1136
+ scheduling_preference: isFlexibleRequest ? 'flexible' : 'exact',
1007
1137
  requires_payment: selectedService.requiresPayment === true,
1008
1138
  quoted_total_cents: displayQuote?.totalCents ?? null,
1009
1139
  });
@@ -1144,7 +1274,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
1144
1274
  cacheRef: availabilityCacheRef,
1145
1275
  });
1146
1276
  }
1147
- }, onClick: () => setCalendarMonth((current) => addMonths(current, 1)), disabled: sameMonth(calendarMonth, addMonths(startOfMonth(new Date()), 3)), "aria-label": t('nextMonth'), children: _jsx(ChevronRightIcon, {}) })] })] }), _jsx("div", { className: "bw-cal-weekdays", children: [
1277
+ }, 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: [
1148
1278
  t('weekdaySun'),
1149
1279
  t('weekdayMon'),
1150
1280
  t('weekdayTue'),
@@ -1155,9 +1285,21 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
1155
1285
  ].map((day) => (_jsx("span", { children: day }, day))) }), _jsx("div", { className: "bw-cal-grid", children: calendarDays.map((day) => {
1156
1286
  const dateKey = formatDateKey(day);
1157
1287
  const isCurrentMonth = day.getMonth() === calendarMonth.getMonth();
1158
- const isPast = dateKey < formatDateKey(startOfDay(new Date()));
1288
+ const isPast = dateKey < todayDateKey;
1159
1289
  const slots = filteredAvailabilityByDate.get(dateKey) ?? [];
1160
- const isAvailable = slots.length > 0;
1290
+ const isExactAvailable = slots.length > 0;
1291
+ const isFlexibleAvailable = effectiveSchedulingPreference === 'flexible' &&
1292
+ selectedService != null &&
1293
+ isFlexibleDateSelectable({
1294
+ dateKey,
1295
+ service: selectedService,
1296
+ businessHours: setup?.businessHours ?? [],
1297
+ timezone: bookingTimezone,
1298
+ window: selectedFlexibleWindow,
1299
+ });
1300
+ const isAvailable = effectiveSchedulingPreference === 'flexible'
1301
+ ? isFlexibleAvailable
1302
+ : isExactAvailable;
1161
1303
  const isSelected = selectedDate === dateKey;
1162
1304
  const className = [
1163
1305
  'bw-cal-day',
@@ -1175,15 +1317,25 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
1175
1317
  .filter(Boolean)
1176
1318
  .join(' ');
1177
1319
  return (_jsx("button", { type: "button", className: className, disabled: !isCurrentMonth || isPast || !isAvailable, onClick: () => {
1320
+ if (holdId) {
1321
+ void client
1322
+ .releasePublicBookingHold({
1323
+ siteSlug,
1324
+ holdId,
1325
+ sessionToken,
1326
+ })
1327
+ .catch(() => undefined);
1328
+ }
1178
1329
  setSelectedDate(dateKey);
1179
1330
  setSelectedSlot(null);
1331
+ setHoldId(null);
1332
+ setHoldExpiresAt(null);
1333
+ setHeldStaffId(null);
1180
1334
  setMobileStep((current) => current < 2 ? 2 : current);
1181
1335
  }, children: day.getDate() }, dateKey));
1182
- }) }), !selectedService ? (_jsx("p", { className: "bw-cal-prompt", children: t('calendarSelectServicePrompt') })) : null, _jsxs("div", { className: "bw-timezone", children: [_jsx(GlobeIcon, {}), _jsx("span", { children: selectedHeldStaff?.timezone ??
1183
- selectedStaff?.timezone ??
1184
- setup?.workspaceTimezone ??
1185
- setup?.staff?.[0]?.timezone ??
1186
- 'UTC' })] })] }), selectedService && !isEditingService ? (_jsxs("div", { className: "bw-mobile-card bw-provider-section--mobile", children: [_jsx("label", { className: "bw-label", children: t('selectProvider') }), providerRow] })) : null, selectedService && selectedDate ? (_jsxs("div", { className: "bw-mobile-card bw-slots-mobile", children: [_jsx("label", { className: "bw-label", children: t('selectTimePrompt') }), slotsArea] })) : null] }), _jsx("div", { className: "bw-col bw-col--review", "aria-hidden": mobileStep !== 4, children: selectedService ? (_jsxs("div", { className: "bw-summary bw-summary--review", children: [_jsx("span", { className: "bw-summary-title", children: isReschedule
1336
+ }) }), !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'
1337
+ ? t('flexWindowHeading')
1338
+ : 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
1187
1339
  ? t('summaryTitleReschedule')
1188
1340
  : t('summaryTitleCreate') }), holdSecondsRemaining !== null ? (_jsx("div", { className: "bw-summary-rows", children: _jsxs("div", { className: "bw-summary-row bw-summary-row--hold", "aria-live": "polite", children: [_jsx("span", { children: t('summaryReservationHold') }), _jsx("span", { className: "bw-summary-val bw-summary-val--hold", children: formatHoldCountdown(holdSecondsRemaining) })] }) })) : null, _jsxs("div", { className: "bw-summary-group", children: [_jsx("span", { className: "bw-summary-subhead", children: t('reviewBookingDetails') }), _jsxs("div", { className: "bw-summary-rows", children: [isReschedule && rescheduleContext ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryFormerTime') }), _jsx("span", { className: "bw-summary-val", style: {
1189
1341
  ...summaryValStyle,
@@ -1200,7 +1352,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
1200
1352
  }, 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) ??
1201
1353
  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 ??
1202
1354
  selectedStaff?.name ??
1203
- t('providerAny') })] }), selectedDate ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryDate') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: formatReadableDate(selectedDate, intlLocale) })] })) : null, selectedSlot ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryTime') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: formatTimeLabel(selectedSlot.time, intlLocale) })] })) : null, _jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryDuration') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: formatDuration(selectedService.durationMinutes) })] })] })] }), _jsxs("div", { className: "bw-summary-group", children: [_jsx("span", { className: "bw-summary-subhead", children: t('reviewYourDetails') }), _jsxs("div", { className: "bw-summary-rows", children: [_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('contactFullName') }), _jsx("span", { className: "bw-summary-val", children: customerName.trim() || t('reviewNotProvided') })] }), _jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('contactEmail') }), _jsx("span", { className: "bw-summary-val", children: customerEmail.trim() || t('reviewNotProvided') })] }), _jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('contactPhone') }), _jsx("span", { className: "bw-summary-val", children: customerPhone.trim() || t('reviewNotProvided') })] })] })] }), (() => {
1355
+ 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') })] })] })] }), (() => {
1204
1356
  const intakeRows = buildIntakeReviewRows(selectedService.intakeForm, intakeResponses, selectedServicePath, locale, t);
1205
1357
  if (intakeRows.length === 0)
1206
1358
  return null;
@@ -1209,7 +1361,9 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
1209
1361
  ? t('notesLabelReschedule')
1210
1362
  : t('reviewNotesSection') }), _jsx("p", { className: "bw-summary-notes", children: customerNotes.trim() })] })) : null, _jsx("div", { className: "bw-summary-rows bw-summary-rows--total", children: _jsxs("div", { className: "bw-summary-row bw-summary-total", children: [_jsx("span", { children: isDisplayQuoteLoading
1211
1363
  ? t('summaryCalculating')
1212
- : 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: t('slotsHeading') }), selectedDateSlots.length > 0 ? (_jsx("span", { className: "bw-slots-count", children: selectedDateSlots.length })) : null] }), _jsx("div", { className: "bw-slots-desktop", children: slotsArea })] }), _jsxs("div", { className: `bw-pane bw-pane--details${viewState === 'details' ? ' is-active' : ' is-hidden'}`, "aria-hidden": viewState !== 'details', children: [selectedServiceRequiresSlot ? (_jsxs("button", { type: "button", className: "bw-back-btn", onClick: () => void handleBackToSlots(), children: [_jsx(ArrowLeftIcon, {}), _jsx("span", { children: t('backToDifferentTime') })] })) : (_jsxs("button", { type: "button", className: "bw-back-btn", onClick: handleBackToServicePicker, children: [_jsx(ArrowLeftIcon, {}), _jsx("span", { children: t('backToDifferentService') })] })), _jsxs("div", { className: "bw-form", children: [serviceWarning ? (_jsx(ServiceWarningCallout, { warning: serviceWarning })) : null, renderIntakeAndContact('bw-intake', ''), renderNotesField('bw-notes', t('notesLabel'), t('notesPlaceholder')), selectedService ? (_jsxs("div", { className: "bw-summary", children: [_jsx("span", { className: "bw-summary-title", children: isReschedule
1364
+ : 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'
1365
+ ? t('flexWindowHeading')
1366
+ : t('slotsHeading') }), effectiveSchedulingPreference === 'flexible' ? (_jsx("span", { className: "bw-slots-count", children: FLEXIBLE_WINDOW_OPTIONS.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', ''), renderNotesField('bw-notes', t('notesLabel'), t('notesPlaceholder')), selectedService ? (_jsxs("div", { className: "bw-summary", children: [_jsx("span", { className: "bw-summary-title", children: isReschedule
1213
1367
  ? t('summaryTitleReschedule')
1214
1368
  : t('summaryTitleCreate') }), _jsxs("div", { className: "bw-summary-rows", children: [holdSecondsRemaining !== null ? (_jsxs("div", { className: "bw-summary-row bw-summary-row--hold", "aria-live": "polite", children: [_jsx("span", { children: t('summaryReservationHold') }), _jsx("span", { className: "bw-summary-val bw-summary-val--hold", children: formatHoldCountdown(holdSecondsRemaining) })] })) : null, isReschedule && rescheduleContext ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryFormerTime') }), _jsx("span", { className: "bw-summary-val", style: {
1215
1369
  ...summaryValStyle,
@@ -1225,7 +1379,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
1225
1379
  opacity: 0.55,
1226
1380
  }, 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 ??
1227
1381
  selectedStaff?.name ??
1228
- t('providerAny') })] }), selectedDate ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryDate') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: formatReadableDate(selectedDate, intlLocale) })] })) : null, selectedSlot ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryTime') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: formatTimeLabel(selectedSlot.time, intlLocale) })] })) : null, _jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryDuration') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: formatDuration(selectedService.durationMinutes) })] }), _jsxs("div", { className: "bw-summary-row bw-summary-total", children: [_jsx("span", { children: isDisplayQuoteLoading
1382
+ 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
1229
1383
  ? t('summaryCalculating')
1230
1384
  : 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: () => {
1231
1385
  setSuccess(t('successPaymentReceived'));
@@ -1240,15 +1394,11 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
1240
1394
  ? t('btnContinueToPayment')
1241
1395
  : t('btnConfirmBooking') }), !isSubmitting ? _jsx(ArrowRightIcon, {}) : null] }))] })] })] }) }) }) })] }) }), _jsxs("div", { className: "bw-details-view", children: [_jsx("div", { className: "bw-details-summary", children: selectedService ? (_jsxs(_Fragment, { children: [selectedServiceRequiresSlot ? (_jsxs("button", { type: "button", className: "bw-back-btn bw-back-btn--details", onClick: () => void handleBackToSlots(), children: [_jsx(ArrowLeftIcon, {}), _jsx("span", { children: t('backToDifferentTime') })] })) : (_jsxs("button", { type: "button", className: "bw-back-btn bw-back-btn--details", onClick: handleBackToServicePicker, children: [_jsx(ArrowLeftIcon, {}), _jsx("span", { children: t('backToDifferentService') })] })), _jsx("h3", { className: "bw-details-service", children: pickLocaleField(selectedService, 'name', locale) ??
1242
1396
  selectedService.name }), (pickLocaleField(selectedService, 'description', locale) ??
1243
- selectedService.description) ? (_jsx("p", { className: "bw-details-desc", children: pickLocaleField(selectedService, 'description', locale) ?? selectedService.description })) : null, _jsxs("div", { className: "bw-details-meta", children: [isReschedule && rescheduleContext ? (_jsxs("div", { className: "bw-details-meta-row bw-details-meta-row--strike", children: [_jsx("span", { className: "bw-details-meta-icon", children: _jsx(CalendarIcon, {}) }), _jsxs("div", { className: "bw-details-meta-text", children: [_jsx("span", { className: "bw-details-meta-label", children: t('summaryFormerTime') }), _jsx("span", { className: "bw-details-meta-value", children: formatFormerSlot(rescheduleContext.formerStartTime, rescheduleContext.formerEndTime, intlLocale) })] })] })) : null, selectedDate && selectedSlot ? (_jsxs("div", { className: "bw-details-meta-row", children: [_jsx("span", { className: "bw-details-meta-icon", children: _jsx(CalendarIcon, {}) }), _jsxs("div", { className: "bw-details-meta-text", children: [_jsx("span", { className: "bw-details-meta-label", children: isReschedule
1397
+ selectedService.description) ? (_jsx("p", { className: "bw-details-desc", children: pickLocaleField(selectedService, 'description', locale) ?? selectedService.description })) : null, _jsxs("div", { className: "bw-details-meta", children: [isReschedule && rescheduleContext ? (_jsxs("div", { className: "bw-details-meta-row bw-details-meta-row--strike", children: [_jsx("span", { className: "bw-details-meta-icon", children: _jsx(CalendarIcon, {}) }), _jsxs("div", { className: "bw-details-meta-text", children: [_jsx("span", { className: "bw-details-meta-label", children: t('summaryFormerTime') }), _jsx("span", { className: "bw-details-meta-value", children: formatFormerSlot(rescheduleContext.formerStartTime, rescheduleContext.formerEndTime, intlLocale) })] })] })) : null, selectedDate && selectedTimeLabel ? (_jsxs("div", { className: "bw-details-meta-row", children: [_jsx("span", { className: "bw-details-meta-icon", children: _jsx(CalendarIcon, {}) }), _jsxs("div", { className: "bw-details-meta-text", children: [_jsx("span", { className: "bw-details-meta-label", children: isReschedule
1244
1398
  ? t('summaryNewTime')
1245
- : t('summaryWhen') }), _jsxs("span", { className: "bw-details-meta-value", children: [formatReadableDate(selectedDate, intlLocale), _jsx("br", {}), formatTimeLabel(selectedSlot.time, intlLocale), " \u2013", ' ', formatTimeLabel(selectedSlot.endTime, intlLocale)] })] })] })) : null, _jsxs("div", { className: "bw-details-meta-row", children: [_jsx("span", { className: "bw-details-meta-icon", children: _jsx(ClockIcon, {}) }), _jsx("span", { className: "bw-details-meta-value", children: formatDuration(selectedService.durationMinutes) })] }), _jsxs("div", { className: "bw-details-meta-row", children: [_jsx("span", { className: "bw-details-meta-icon", children: _jsx(UserIcon, {}) }), _jsx("span", { className: "bw-details-meta-value", children: selectedHeldStaff?.name ??
1399
+ : 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 ??
1246
1400
  selectedStaff?.name ??
1247
- t('providerAny') })] }), _jsxs("div", { className: "bw-details-meta-row", children: [_jsx("span", { className: "bw-details-meta-icon", children: _jsx(GlobeIcon, {}) }), _jsx("span", { className: "bw-details-meta-value", children: selectedHeldStaff?.timezone ??
1248
- selectedStaff?.timezone ??
1249
- setup?.workspaceTimezone ??
1250
- setup?.staff?.[0]?.timezone ??
1251
- 'UTC' })] }), _jsxs("div", { className: "bw-details-meta-row", children: [_jsx("span", { className: "bw-details-meta-icon", "aria-hidden": "true", children: "$" }), _jsxs("div", { className: "bw-details-meta-text", children: [_jsx("span", { className: "bw-details-meta-label", children: isDisplayQuoteLoading
1401
+ t('providerAny') })] }), _jsxs("div", { className: "bw-details-meta-row", children: [_jsx("span", { className: "bw-details-meta-icon", children: _jsx(GlobeIcon, {}) }), _jsx("span", { className: "bw-details-meta-value", children: 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
1252
1402
  ? t('summaryCalculating')
1253
1403
  : 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'), renderNotesField('bw-notes-d', isReschedule ? t('notesLabelReschedule') : t('notesLabel'), isReschedule
1254
1404
  ? t('notesPlaceholderReschedule')
@@ -1362,10 +1512,9 @@ function areRequiredIntakeFieldsComplete(form, responses, mode) {
1362
1512
  return isRequiredIntakeFieldComplete(field, responses[field.id], minItems);
1363
1513
  }));
1364
1514
  }
1365
- function getSubmitBlockers({ selectedService, selectedServiceRequiresSlot, selectedDate, selectedSlot, holdId, isIntakeComplete, intakeResponses, selectedServicePath, quote, customerName, customerEmail, t, locale = DEFAULT_LOCALE, }) {
1515
+ function getSubmitBlockers({ selectedService, selectedServiceRequiresSlot, hasScheduleSelection, isIntakeComplete, intakeResponses, selectedServicePath, quote, customerName, customerEmail, t, locale = DEFAULT_LOCALE, }) {
1366
1516
  const blockers = [];
1367
- if (selectedServiceRequiresSlot &&
1368
- (!selectedDate || !selectedSlot || !holdId)) {
1517
+ if (selectedServiceRequiresSlot && !hasScheduleSelection) {
1369
1518
  blockers.push(t('blockerDateTime'));
1370
1519
  }
1371
1520
  if (!customerName.trim())
@@ -2004,6 +2153,138 @@ function formatTimeLabel(time, locale = 'en-US') {
2004
2153
  minute: '2-digit',
2005
2154
  }).format(date);
2006
2155
  }
2156
+ function formatFlexibleWindowTimeRange(window, locale = 'en-US') {
2157
+ return `${formatTimeLabel(window.startTime, locale)} – ${formatTimeLabel(window.endTime, locale)}`;
2158
+ }
2159
+ function appendFlexibleWindowNote({ notes, dateKey, window, locale, timezone, t, }) {
2160
+ const trimmed = notes.trim();
2161
+ const windowLabel = `${t(window.labelKey)} (${formatFlexibleWindowTimeRange(window, locale)})`;
2162
+ const line = `${t('flexRequestNotePrefix')}: ${formatReadableDate(dateKey, locale)}, ${windowLabel}, ${timezone}`;
2163
+ return trimmed ? `${trimmed}\n\n${line}` : line;
2164
+ }
2165
+ function resolveBookingTimezone({ workspaceTimezone, configuredTimezone, heldStaffTimezone, selectedStaffTimezone, fallbackStaffTimezone, }) {
2166
+ return normalizeWidgetTimezone(workspaceTimezone ??
2167
+ configuredTimezone ??
2168
+ heldStaffTimezone ??
2169
+ selectedStaffTimezone ??
2170
+ fallbackStaffTimezone ??
2171
+ getBrowserTimezone() ??
2172
+ 'UTC');
2173
+ }
2174
+ function normalizeWidgetTimezone(value) {
2175
+ const candidate = value?.trim();
2176
+ if (!candidate)
2177
+ return 'UTC';
2178
+ try {
2179
+ return new Intl.DateTimeFormat('en-US', {
2180
+ timeZone: candidate,
2181
+ }).resolvedOptions().timeZone;
2182
+ }
2183
+ catch {
2184
+ return 'UTC';
2185
+ }
2186
+ }
2187
+ function getBrowserTimezone() {
2188
+ if (typeof Intl === 'undefined')
2189
+ return null;
2190
+ return Intl.DateTimeFormat().resolvedOptions().timeZone ?? null;
2191
+ }
2192
+ const timezoneFormatterCache = new Map();
2193
+ function getTimezoneFormatter(timezone) {
2194
+ const normalized = normalizeWidgetTimezone(timezone);
2195
+ const cached = timezoneFormatterCache.get(normalized);
2196
+ if (cached)
2197
+ return cached;
2198
+ const formatter = new Intl.DateTimeFormat('en-CA', {
2199
+ timeZone: normalized,
2200
+ year: 'numeric',
2201
+ month: '2-digit',
2202
+ day: '2-digit',
2203
+ hour: '2-digit',
2204
+ minute: '2-digit',
2205
+ second: '2-digit',
2206
+ hour12: false,
2207
+ });
2208
+ timezoneFormatterCache.set(normalized, formatter);
2209
+ return formatter;
2210
+ }
2211
+ function parseTimezoneParts(timestamp, timezone) {
2212
+ const parts = getTimezoneFormatter(timezone).formatToParts(new Date(timestamp));
2213
+ const values = {};
2214
+ for (const part of parts) {
2215
+ if (part.type === 'year' ||
2216
+ part.type === 'month' ||
2217
+ part.type === 'day' ||
2218
+ part.type === 'hour' ||
2219
+ part.type === 'minute' ||
2220
+ part.type === 'second') {
2221
+ values[part.type] = Number(part.value);
2222
+ }
2223
+ }
2224
+ return {
2225
+ year: values.year ?? 0,
2226
+ month: values.month ?? 1,
2227
+ day: values.day ?? 1,
2228
+ hour: values.hour ?? 0,
2229
+ minute: values.minute ?? 0,
2230
+ second: values.second ?? 0,
2231
+ };
2232
+ }
2233
+ function timezonePartsToUtcMs(parts) {
2234
+ return Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
2235
+ }
2236
+ function dateKeyInTimeZone(timestamp, timezone) {
2237
+ const parts = parseTimezoneParts(timestamp, timezone);
2238
+ return `${parts.year}-${String(parts.month).padStart(2, '0')}-${String(parts.day).padStart(2, '0')}`;
2239
+ }
2240
+ function parseDateKeyParts(dateKey) {
2241
+ const [year, month, day] = dateKey.split('-').map(Number);
2242
+ return { year, month, day };
2243
+ }
2244
+ function zonedDateTimeToTimestamp(dateKey, time, timezone) {
2245
+ const { year, month, day } = parseDateKeyParts(dateKey);
2246
+ const [hour, minute] = time.split(':').map(Number);
2247
+ const target = {
2248
+ year,
2249
+ month,
2250
+ day,
2251
+ hour,
2252
+ minute,
2253
+ second: 0,
2254
+ };
2255
+ let guess = Date.UTC(year, month - 1, day, hour, minute, 0);
2256
+ for (let index = 0; index < 3; index += 1) {
2257
+ const actual = parseTimezoneParts(guess, timezone);
2258
+ const offset = timezonePartsToUtcMs(actual) - guess;
2259
+ const adjusted = timezonePartsToUtcMs(target) - offset;
2260
+ if (adjusted === guess)
2261
+ return adjusted;
2262
+ guess = adjusted;
2263
+ }
2264
+ return guess;
2265
+ }
2266
+ function isFlexibleDateSelectable({ dateKey, service, businessHours, timezone, window, }) {
2267
+ if (!service)
2268
+ return false;
2269
+ if (!isBusinessOpenOnDate(dateKey, businessHours))
2270
+ return false;
2271
+ const now = Date.now();
2272
+ const windowEnd = zonedDateTimeToTimestamp(dateKey, window.endTime, timezone);
2273
+ const latestAllowedStart = now + (service.maxAdvanceBookingDays ?? 90) * 24 * 60 * 60 * 1000;
2274
+ if (windowEnd < now + (service.minimumNoticeMinutes ?? 0) * 60 * 1000) {
2275
+ return false;
2276
+ }
2277
+ const windowStart = zonedDateTimeToTimestamp(dateKey, window.startTime, timezone);
2278
+ return windowStart <= latestAllowedStart;
2279
+ }
2280
+ function isBusinessOpenOnDate(dateKey, businessHours) {
2281
+ if (businessHours.length === 0)
2282
+ return true;
2283
+ const { year, month, day } = parseDateKeyParts(dateKey);
2284
+ const dayOfWeek = new Date(Date.UTC(year, month - 1, day)).getUTCDay();
2285
+ const entry = businessHours.find((hours) => hours.day === dayOfWeek);
2286
+ return entry?.isOpen === true;
2287
+ }
2007
2288
  function startOfMonth(date) {
2008
2289
  return new Date(date.getFullYear(), date.getMonth(), 1);
2009
2290
  }