@asksable/site-connector 0.6.2 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/booking-widget.d.ts +13 -1
- package/dist/booking-widget.d.ts.map +1 -1
- package/dist/booking-widget.js +335 -54
- package/dist/booking-widget.js.map +1 -1
- package/dist/styles.css +107 -0
- package/dist/translations.d.ts +24 -0
- package/dist/translations.d.ts.map +1 -1
- package/dist/translations.js +24 -0
- package/dist/translations.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/booking-widget.js
CHANGED
|
@@ -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 ||
|
|
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
|
|
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
|
|
718
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
855
|
-
endTime:
|
|
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
|
-
(
|
|
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
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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:
|
|
964
|
-
? (
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
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
|
-
|
|
971
|
-
|
|
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:
|
|
1105
|
+
customerNotes: bookingNotes,
|
|
979
1106
|
intakeResponses,
|
|
980
1107
|
quotedTotalCents: displayQuote?.totalCents,
|
|
981
|
-
bookingHoldId: selectedServiceRequiresSlot
|
|
982
|
-
|
|
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 <
|
|
1288
|
+
const isPast = dateKey < todayDateKey;
|
|
1159
1289
|
const slots = filteredAvailabilityByDate.get(dateKey) ?? [];
|
|
1160
|
-
const
|
|
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:
|
|
1183
|
-
|
|
1184
|
-
|
|
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,
|
|
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:
|
|
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,
|
|
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 &&
|
|
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", {}),
|
|
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:
|
|
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,
|
|
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
|
}
|