@asksable/site-connector 0.3.2 → 0.4.1
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 +57 -8
- package/dist/analytics.d.ts +19 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +170 -0
- package/dist/analytics.js.map +1 -0
- package/dist/booking-widget.d.ts +1 -1
- package/dist/booking-widget.d.ts.map +1 -1
- package/dist/booking-widget.js +227 -56
- package/dist/booking-widget.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/provider.d.ts +6 -2
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +74 -4
- package/dist/provider.js.map +1 -1
- package/dist/types.d.ts +18 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -2
package/dist/booking-widget.js
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
import { createPortal } from 'react-dom';
|
|
2
|
+
import { Elements, PaymentElement, useElements, useStripe, } from '@stripe/react-stripe-js';
|
|
4
3
|
import { loadStripe } from '@stripe/stripe-js';
|
|
5
|
-
import { Elements, PaymentElement, useElements, useStripe } from '@stripe/react-stripe-js';
|
|
6
4
|
import { Skeleton } from 'boneyard-js/react';
|
|
5
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
6
|
+
import { createPortal } from 'react-dom';
|
|
7
7
|
// Side-effect: registers all named bones with the boneyard runtime.
|
|
8
8
|
// Regenerate by running `npx boneyard-js build` against a host site
|
|
9
9
|
// that mounts <BookingWidgetPanel /> in its loaded state.
|
|
10
10
|
import './bones/registry.js';
|
|
11
11
|
import { BookingWidgetPlaceholder } from './booking-widget-placeholder.js';
|
|
12
|
-
import { useSableSiteClient, useSableSiteConfig, useTranslation } from './provider.js';
|
|
13
|
-
import { DEFAULT_LOCALE, localeToIntl, pickLocaleField } from './translations.js';
|
|
12
|
+
import { useSableSiteAnalytics, useSableSiteClient, useSableSiteConfig, useTranslation, } from './provider.js';
|
|
13
|
+
import { DEFAULT_LOCALE, localeToIntl, pickLocaleField, } from './translations.js';
|
|
14
14
|
const stripePromises = new Map();
|
|
15
15
|
function getStripePromise(publishableKey, connectAccountId) {
|
|
16
16
|
const key = `${publishableKey}:${connectAccountId}`;
|
|
17
17
|
const cached = stripePromises.get(key);
|
|
18
18
|
if (cached)
|
|
19
19
|
return cached;
|
|
20
|
-
const promise = loadStripe(publishableKey, {
|
|
20
|
+
const promise = loadStripe(publishableKey, {
|
|
21
|
+
stripeAccount: connectAccountId,
|
|
22
|
+
});
|
|
21
23
|
stripePromises.set(key, promise);
|
|
22
24
|
return promise;
|
|
23
25
|
}
|
|
@@ -52,6 +54,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
52
54
|
const client = useSableSiteClient();
|
|
53
55
|
const { siteSlug } = useSableSiteConfig();
|
|
54
56
|
const { t, locale } = useTranslation();
|
|
57
|
+
const analytics = useSableSiteAnalytics();
|
|
55
58
|
const intlLocale = localeToIntl(locale);
|
|
56
59
|
const [setup, setSetup] = useState(null);
|
|
57
60
|
const [isSetupLoading, setIsSetupLoading] = useState(true);
|
|
@@ -104,7 +107,11 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
104
107
|
// Dev-only: force viewState to 'details' when previewing a payment
|
|
105
108
|
// variant, since the payment panel renders inside the details form.
|
|
106
109
|
useEffect(() => {
|
|
107
|
-
|
|
110
|
+
analytics.capture('sable_booking_widget_viewed', { mode });
|
|
111
|
+
}, [analytics, mode]);
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (__devForceState === 'payment-full' ||
|
|
114
|
+
__devForceState === 'payment-deposit') {
|
|
108
115
|
setViewState('details');
|
|
109
116
|
}
|
|
110
117
|
}, [__devForceState]);
|
|
@@ -177,7 +184,9 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
177
184
|
})
|
|
178
185
|
.catch((nextError) => {
|
|
179
186
|
if (!cancelled) {
|
|
180
|
-
setError(nextError instanceof Error
|
|
187
|
+
setError(nextError instanceof Error
|
|
188
|
+
? nextError.message
|
|
189
|
+
: 'Unable to load booking setup.');
|
|
181
190
|
}
|
|
182
191
|
})
|
|
183
192
|
.finally(() => {
|
|
@@ -189,9 +198,12 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
189
198
|
cancelled = true;
|
|
190
199
|
};
|
|
191
200
|
}, [client, siteSlug]);
|
|
192
|
-
const selectedService = useMemo(() => setup?.services.find((service) => service._id === selectedServiceId) ??
|
|
201
|
+
const selectedService = useMemo(() => setup?.services.find((service) => service._id === selectedServiceId) ??
|
|
202
|
+
null, [selectedServiceId, setup]);
|
|
193
203
|
const selectedServiceRequiresSlot = selectedService?.publicBookingMode !== 'async';
|
|
194
|
-
const selectedServicePath = selectedServiceRequiresSlot
|
|
204
|
+
const selectedServicePath = selectedServiceRequiresSlot
|
|
205
|
+
? 'scheduled'
|
|
206
|
+
: 'async';
|
|
195
207
|
const selectedServiceHasIntakeForm = hasIntakeFormSections(selectedService);
|
|
196
208
|
const isIntakeComplete = useMemo(() => !selectedService?.intakeForm ||
|
|
197
209
|
areRequiredIntakeFieldsComplete(selectedService.intakeForm, intakeResponses, selectedServicePath), [intakeResponses, selectedService?.intakeForm, selectedServicePath]);
|
|
@@ -217,7 +229,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
217
229
|
const uncategorizedKey = '__uncategorized__';
|
|
218
230
|
for (const service of visibleServices) {
|
|
219
231
|
const category = service.categoryId != null
|
|
220
|
-
? categoriesById.get(service.categoryId) ?? null
|
|
232
|
+
? (categoriesById.get(service.categoryId) ?? null)
|
|
221
233
|
: null;
|
|
222
234
|
const key = category?._id ?? uncategorizedKey;
|
|
223
235
|
const existing = groups.get(key);
|
|
@@ -286,7 +298,8 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
286
298
|
// wiped on the first render before the staff list arrives.
|
|
287
299
|
if (availableStaff.length === 0)
|
|
288
300
|
return;
|
|
289
|
-
if (selectedStaffId &&
|
|
301
|
+
if (selectedStaffId &&
|
|
302
|
+
!availableStaff.some((staffMember) => staffMember._id === selectedStaffId)) {
|
|
290
303
|
setSelectedStaffId(null);
|
|
291
304
|
}
|
|
292
305
|
}, [availableStaff, selectedStaffId]);
|
|
@@ -351,7 +364,9 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
351
364
|
if (!cancelled) {
|
|
352
365
|
setAvailabilityByDate(new Map());
|
|
353
366
|
setSelectedDate(null);
|
|
354
|
-
setError(nextError instanceof Error
|
|
367
|
+
setError(nextError instanceof Error
|
|
368
|
+
? nextError.message
|
|
369
|
+
: t('calendarLoadError'));
|
|
355
370
|
}
|
|
356
371
|
})
|
|
357
372
|
.finally(() => {
|
|
@@ -362,7 +377,15 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
362
377
|
return () => {
|
|
363
378
|
cancelled = true;
|
|
364
379
|
};
|
|
365
|
-
}, [
|
|
380
|
+
}, [
|
|
381
|
+
calendarMonth,
|
|
382
|
+
client,
|
|
383
|
+
selectedDate,
|
|
384
|
+
selectedService?.publicBookingMode,
|
|
385
|
+
selectedServiceId,
|
|
386
|
+
selectedStaffId,
|
|
387
|
+
siteSlug,
|
|
388
|
+
]);
|
|
366
389
|
useEffect(() => {
|
|
367
390
|
if (!selectedServiceId || selectedService?.publicBookingMode === 'async') {
|
|
368
391
|
return;
|
|
@@ -375,7 +398,14 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
375
398
|
calendarMonth: nextMonth,
|
|
376
399
|
cacheRef: availabilityCacheRef,
|
|
377
400
|
});
|
|
378
|
-
}, [
|
|
401
|
+
}, [
|
|
402
|
+
calendarMonth,
|
|
403
|
+
client,
|
|
404
|
+
selectedService?.publicBookingMode,
|
|
405
|
+
selectedServiceId,
|
|
406
|
+
selectedStaffId,
|
|
407
|
+
siteSlug,
|
|
408
|
+
]);
|
|
379
409
|
useEffect(() => {
|
|
380
410
|
setSelectedSlot(null);
|
|
381
411
|
setPendingSlotKey(null);
|
|
@@ -394,7 +424,12 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
394
424
|
else {
|
|
395
425
|
setViewState('slots');
|
|
396
426
|
}
|
|
397
|
-
}, [
|
|
427
|
+
}, [
|
|
428
|
+
selectedDate,
|
|
429
|
+
selectedService?.publicBookingMode,
|
|
430
|
+
selectedServiceId,
|
|
431
|
+
selectedStaffId,
|
|
432
|
+
]);
|
|
398
433
|
useEffect(() => {
|
|
399
434
|
setIntakeResponses({});
|
|
400
435
|
setQuote(null);
|
|
@@ -424,7 +459,9 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
424
459
|
})
|
|
425
460
|
.catch((nextError) => {
|
|
426
461
|
if (!cancelled) {
|
|
427
|
-
setError(nextError instanceof Error
|
|
462
|
+
setError(nextError instanceof Error
|
|
463
|
+
? nextError.message
|
|
464
|
+
: 'Unable to calculate price.');
|
|
428
465
|
}
|
|
429
466
|
})
|
|
430
467
|
.finally(() => {
|
|
@@ -436,7 +473,14 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
436
473
|
cancelled = true;
|
|
437
474
|
window.clearTimeout(handle);
|
|
438
475
|
};
|
|
439
|
-
}, [
|
|
476
|
+
}, [
|
|
477
|
+
client,
|
|
478
|
+
intakeResponses,
|
|
479
|
+
isIntakeComplete,
|
|
480
|
+
selectedService?.pricingMode,
|
|
481
|
+
selectedServiceId,
|
|
482
|
+
siteSlug,
|
|
483
|
+
]);
|
|
440
484
|
useEffect(() => {
|
|
441
485
|
return () => {
|
|
442
486
|
if (!holdId) {
|
|
@@ -506,7 +550,6 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
506
550
|
}, refreshAt);
|
|
507
551
|
return () => window.clearTimeout(handle);
|
|
508
552
|
// handleHoldExpired is stable enough — it only reads setters
|
|
509
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
510
553
|
}, [client, holdExpiresAt, holdId, sessionToken, siteSlug]);
|
|
511
554
|
// Fires the moment the displayed countdown hits zero. The refresh
|
|
512
555
|
// effect above usually preempts this by extending the hold ~30s
|
|
@@ -519,7 +562,6 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
519
562
|
if (holdNow >= holdExpiresAt) {
|
|
520
563
|
handleHoldExpired();
|
|
521
564
|
}
|
|
522
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
523
565
|
}, [holdExpiresAt, holdNow]);
|
|
524
566
|
function handleHoldExpired() {
|
|
525
567
|
setHoldId(null);
|
|
@@ -576,9 +618,15 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
576
618
|
}
|
|
577
619
|
return set;
|
|
578
620
|
}, [availabilityByDate, selectedDate]);
|
|
579
|
-
const selectedDateSlots = selectedDate
|
|
580
|
-
|
|
581
|
-
|
|
621
|
+
const selectedDateSlots = selectedDate
|
|
622
|
+
? (filteredAvailabilityByDate.get(selectedDate) ?? [])
|
|
623
|
+
: [];
|
|
624
|
+
const selectedHeldStaff = availableStaff.find((staffMember) => staffMember._id === heldStaffId) ??
|
|
625
|
+
selectedStaff ??
|
|
626
|
+
null;
|
|
627
|
+
const holdSecondsRemaining = holdExpiresAt !== null
|
|
628
|
+
? Math.max(0, Math.floor((holdExpiresAt - holdNow) / 1000))
|
|
629
|
+
: null;
|
|
582
630
|
const monthOptions = useMemo(() => getMonthOptions(calendarMonth, 4, intlLocale), [calendarMonth, intlLocale]);
|
|
583
631
|
const calendarDays = useMemo(() => buildCalendarDays(calendarMonth), [calendarMonth]);
|
|
584
632
|
// Mobile 4-step gates. canAdvanceStepN = "the user can move PAST
|
|
@@ -623,11 +671,15 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
623
671
|
const slotKey = `${slot.time}-${slot.endTime}`;
|
|
624
672
|
const isPending = pendingSlotKey === slotKey;
|
|
625
673
|
const isActive = isPending ||
|
|
626
|
-
(selectedSlot?.time === slot.time &&
|
|
627
|
-
|
|
674
|
+
(selectedSlot?.time === slot.time &&
|
|
675
|
+
selectedSlot?.endTime === slot.endTime);
|
|
676
|
+
return (_jsx("button", { type: "button", className: `bw-slot${isActive ? ' is-active' : ''}${isPending ? ' is-pending' : ''}`, style: { '--bw-slot-i': index }, onClick: () => void handleSlotSelect(slot), disabled: isSubmitting, children: isPending
|
|
677
|
+
? t('slotSecuring')
|
|
678
|
+
: formatTimeLabel(slot.time, intlLocale) }, slotKey));
|
|
628
679
|
}) })) : (_jsx("p", { className: "bw-no-slots", children: t('slotsEmptyForDate') }))) : null }, slotsAreaKey));
|
|
629
680
|
const canSubmit = Boolean(selectedService &&
|
|
630
|
-
(!selectedServiceRequiresSlot ||
|
|
681
|
+
(!selectedServiceRequiresSlot ||
|
|
682
|
+
(selectedDate && selectedSlot && holdId)) &&
|
|
631
683
|
isIntakeComplete &&
|
|
632
684
|
(selectedService.pricingMode !== 'calculated' || quote) &&
|
|
633
685
|
customerName.trim() &&
|
|
@@ -668,7 +720,7 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
668
720
|
return null;
|
|
669
721
|
const visibleBlockers = submitBlockers.slice(0, 5);
|
|
670
722
|
const hiddenCount = submitBlockers.length - visibleBlockers.length;
|
|
671
|
-
return (_jsxs("div", { className: "bw-submit-help", role: "status", "aria-live": "polite", children: [_jsx("span", { className: "bw-submit-help-title", children: isQuoteLoading ? t('helpCalculating') : t('helpComplete') }), !isQuoteLoading && visibleBlockers.length > 0 ? (_jsxs("ul", { children: [visibleBlockers.map((blocker) => (_jsx("li", { children: blocker }, blocker))), hiddenCount > 0 ? _jsx("li", { children: t('helpMoreFields', { count: hiddenCount }) }) : null] })) : null] }));
|
|
723
|
+
return (_jsxs("div", { className: "bw-submit-help", role: "status", "aria-live": "polite", children: [_jsx("span", { className: "bw-submit-help-title", children: isQuoteLoading ? t('helpCalculating') : t('helpComplete') }), !isQuoteLoading && visibleBlockers.length > 0 ? (_jsxs("ul", { children: [visibleBlockers.map((blocker) => (_jsx("li", { children: blocker }, blocker))), hiddenCount > 0 ? (_jsx("li", { children: t('helpMoreFields', { count: hiddenCount }) })) : null] })) : null] }));
|
|
672
724
|
}
|
|
673
725
|
function renderContactFields(idSuffix) {
|
|
674
726
|
const nameId = `bw-name${idSuffix}`;
|
|
@@ -689,6 +741,12 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
689
741
|
return (_jsxs(_Fragment, { children: [contactFields, intakeFields] }));
|
|
690
742
|
}
|
|
691
743
|
function handleServiceSelect(service) {
|
|
744
|
+
analytics.capture('sable_booking_service_selected', {
|
|
745
|
+
service_id: service._id,
|
|
746
|
+
service_name: pickLocaleField(service, 'name', locale) ?? service.name,
|
|
747
|
+
booking_mode: service.publicBookingMode ?? 'scheduled',
|
|
748
|
+
requires_payment: service.requiresPayment === true,
|
|
749
|
+
});
|
|
692
750
|
if (holdId) {
|
|
693
751
|
void client
|
|
694
752
|
.releasePublicBookingHold({ siteSlug, holdId, sessionToken })
|
|
@@ -810,7 +868,8 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
810
868
|
async function handleConfirmBooking() {
|
|
811
869
|
if (!selectedServiceId ||
|
|
812
870
|
!selectedService ||
|
|
813
|
-
(selectedServiceRequiresSlot &&
|
|
871
|
+
(selectedServiceRequiresSlot &&
|
|
872
|
+
(!selectedDate || !selectedSlot || !holdId))) {
|
|
814
873
|
return;
|
|
815
874
|
}
|
|
816
875
|
setIsSubmitting(true);
|
|
@@ -832,6 +891,11 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
832
891
|
try {
|
|
833
892
|
await onRescheduleSubmit({ newStartTime, newEndTime });
|
|
834
893
|
setSuccess(t('successRescheduleConfirmed'));
|
|
894
|
+
analytics.capture('sable_booking_rescheduled', {
|
|
895
|
+
service_id: selectedServiceId,
|
|
896
|
+
service_name: pickLocaleField(selectedService, 'name', locale) ??
|
|
897
|
+
selectedService.name,
|
|
898
|
+
});
|
|
835
899
|
setMobileStep(4);
|
|
836
900
|
}
|
|
837
901
|
catch (nextError) {
|
|
@@ -849,11 +913,16 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
849
913
|
siteSlug,
|
|
850
914
|
serviceId: selectedServiceId,
|
|
851
915
|
staffMemberId: selectedServiceRequiresSlot
|
|
852
|
-
? heldStaffId ??
|
|
916
|
+
? (heldStaffId ??
|
|
917
|
+
selectedStaffId ??
|
|
918
|
+
selectedSlot?.availableStaffIds[0])
|
|
853
919
|
: undefined,
|
|
854
920
|
startTime: newStartTime,
|
|
855
921
|
endTime: newEndTime,
|
|
856
|
-
timezone: selectedHeldStaff?.timezone ??
|
|
922
|
+
timezone: selectedHeldStaff?.timezone ??
|
|
923
|
+
availableStaff[0]?.timezone ??
|
|
924
|
+
setup?.workspaceTimezone ??
|
|
925
|
+
'UTC',
|
|
857
926
|
deliveryMethod: selectedService.deliveryMethods[0] ?? 'in-person',
|
|
858
927
|
customerName: customerName.trim(),
|
|
859
928
|
customerEmail: customerEmail.trim(),
|
|
@@ -862,15 +931,34 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
862
931
|
intakeResponses,
|
|
863
932
|
quotedTotalCents: quote?.totalCents,
|
|
864
933
|
bookingHoldId: selectedServiceRequiresSlot ? holdId : undefined,
|
|
865
|
-
bookingSessionToken: selectedServiceRequiresSlot
|
|
934
|
+
bookingSessionToken: selectedServiceRequiresSlot
|
|
935
|
+
? sessionToken
|
|
936
|
+
: undefined,
|
|
866
937
|
});
|
|
867
938
|
if (created.payment) {
|
|
939
|
+
analytics.capture('sable_booking_payment_started', {
|
|
940
|
+
appointment_id: created.appointmentId,
|
|
941
|
+
service_id: selectedServiceId,
|
|
942
|
+
service_name: pickLocaleField(selectedService, 'name', locale) ??
|
|
943
|
+
selectedService.name,
|
|
944
|
+
amount_cents: created.payment.amountCents,
|
|
945
|
+
currency: created.payment.currency,
|
|
946
|
+
});
|
|
868
947
|
setPayment(created.payment);
|
|
869
948
|
setPaymentAppointmentId(created.appointmentId);
|
|
870
949
|
setError(null);
|
|
871
950
|
return;
|
|
872
951
|
}
|
|
873
952
|
setSuccess(t('successBookingConfirmed'));
|
|
953
|
+
analytics.capture('sable_booking_created', {
|
|
954
|
+
appointment_id: created.appointmentId,
|
|
955
|
+
service_id: selectedServiceId,
|
|
956
|
+
service_name: pickLocaleField(selectedService, 'name', locale) ??
|
|
957
|
+
selectedService.name,
|
|
958
|
+
booking_mode: selectedService.publicBookingMode ?? 'scheduled',
|
|
959
|
+
requires_payment: selectedService.requiresPayment === true,
|
|
960
|
+
quoted_total_cents: quote?.totalCents ?? null,
|
|
961
|
+
});
|
|
874
962
|
setSelectedSlot(null);
|
|
875
963
|
setPendingSlotKey(null);
|
|
876
964
|
setHoldId(null);
|
|
@@ -887,7 +975,9 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
887
975
|
setMobileStep(4);
|
|
888
976
|
}
|
|
889
977
|
catch (nextError) {
|
|
890
|
-
setError(nextError instanceof Error
|
|
978
|
+
setError(nextError instanceof Error
|
|
979
|
+
? nextError.message
|
|
980
|
+
: t('errorConfirmFailed'));
|
|
891
981
|
}
|
|
892
982
|
finally {
|
|
893
983
|
setIsSubmitting(false);
|
|
@@ -909,22 +999,30 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
909
999
|
}, children: t('cancelledCta') })] }) }));
|
|
910
1000
|
}
|
|
911
1001
|
if (success || forcedState === 'success') {
|
|
912
|
-
return (_jsx("section", { className: "bw", children: _jsxs("div", { className: "bw-done", children: [_jsx("div", { className: "bw-done-icon", children: _jsx(CheckIcon, {}) }), _jsx("h3", { className: "bw-done-title", children: isReschedule
|
|
1002
|
+
return (_jsx("section", { className: "bw", children: _jsxs("div", { className: "bw-done", children: [_jsx("div", { className: "bw-done-icon", children: _jsx(CheckIcon, {}) }), _jsx("h3", { className: "bw-done-title", children: isReschedule
|
|
1003
|
+
? t('successTitleReschedule')
|
|
1004
|
+
: t('successTitleCreate') }), _jsx("p", { className: "bw-done-text", children: isReschedule ? t('successBodyReschedule') : t('successBodyCreate') }), isReschedule ? null : (_jsx("button", { type: "button", className: "bw-btn-primary", onClick: () => {
|
|
913
1005
|
window.location.assign(successRedirectHref);
|
|
914
1006
|
}, children: t('successCtaBackHome') }))] }) }));
|
|
915
1007
|
}
|
|
916
|
-
return (_jsx(Skeleton, { name: "booking-widget", loading: false, children: _jsxs("section", { className: "bw", "data-view-state": viewState, ref: widgetRootRef, children: [mobileHeader ? _jsx("div", { className: "bw-mobile-header", children: mobileHeader }) : null, _jsxs("div", { className: "bw-header", children: [_jsxs("div", { className: "bw-header-row", children: [_jsx("h2", { className: "bw-title bw-title--desktop", children: title ||
|
|
1008
|
+
return (_jsx(Skeleton, { name: "booking-widget", loading: false, children: _jsxs("section", { className: "bw", "data-view-state": viewState, ref: widgetRootRef, children: [mobileHeader ? (_jsx("div", { className: "bw-mobile-header", children: mobileHeader })) : null, _jsxs("div", { className: "bw-header", children: [_jsxs("div", { className: "bw-header-row", children: [_jsx("h2", { className: "bw-title bw-title--desktop", children: title ||
|
|
917
1009
|
(isReschedule
|
|
918
1010
|
? rescheduleContext?.customerName
|
|
919
|
-
? t('rescheduleTitleNamed', {
|
|
1011
|
+
? t('rescheduleTitleNamed', {
|
|
1012
|
+
name: rescheduleContext.customerName,
|
|
1013
|
+
})
|
|
920
1014
|
: t('rescheduleTitle')
|
|
921
1015
|
: t('pageTitle')) }), _jsx("h2", { className: "bw-title bw-title--mobile", children: isReschedule
|
|
922
1016
|
? rescheduleContext?.customerName
|
|
923
|
-
? t('rescheduleTitleNamed', {
|
|
1017
|
+
? t('rescheduleTitleNamed', {
|
|
1018
|
+
name: rescheduleContext.customerName,
|
|
1019
|
+
})
|
|
924
1020
|
: t('rescheduleTitle')
|
|
925
1021
|
: mobileStepTitle(mobileStep, t) }), mobileStep > 1 ? (_jsx("div", { className: "bw-progress", "aria-hidden": "true", children: (selectedServiceRequiresSlot
|
|
926
1022
|
? MOBILE_PROGRESS_STEPS_SCHEDULED
|
|
927
|
-
: MOBILE_PROGRESS_STEPS_ASYNC).map((step) => (_jsx("span", { className: `bw-dot${mobileStep >= step ? ' is-filled' : ''}` }, step))) })) : null] }), description ? _jsx("p", { className: "bw-description", children: description }) : null] }), _jsxs("div", { className: "bw-content", children: [_jsx("div", { className: "bw-body", "data-mobile-step": mobileStep, children: _jsxs("div", { className: "bw-body-inner", children: [_jsxs("div", { className: "bw-col bw-col--left", children: [_jsxs("div", { className: "bw-step-1", children: [_jsx("div", { className: "bw-service-picker", children: selectedService && !isEditingService ? (_jsxs("div", { className: "bw-svc-selected", children: [_jsxs("div", { className: "bw-svc-selected-header", children: [_jsx("label", { className: "bw-label", children: t('selectedService') }), _jsx("button", { type: "button", className: "bw-svc-change", onClick: () => setIsEditingService(true), children: t('btnChange') })] }), _jsxs("div", { className: "bw-svc-row bw-svc-row--readonly", children: [selectedService.image ? (_jsxs("span", { className: "bw-svc-image is-checked", children: [_jsx("img", { src: selectedService.image.url, alt: selectedService.image.alt ??
|
|
1023
|
+
: MOBILE_PROGRESS_STEPS_ASYNC).map((step) => (_jsx("span", { className: `bw-dot${mobileStep >= step ? ' is-filled' : ''}` }, step))) })) : null] }), description ? _jsx("p", { className: "bw-description", children: description }) : null] }), _jsxs("div", { className: "bw-content", children: [_jsx("div", { className: "bw-body", "data-mobile-step": mobileStep, children: _jsxs("div", { className: "bw-body-inner", children: [_jsxs("div", { className: "bw-col bw-col--left", children: [_jsxs("div", { className: "bw-step-1", children: [_jsx("div", { className: "bw-service-picker", children: selectedService && !isEditingService ? (_jsxs("div", { className: "bw-svc-selected", children: [_jsxs("div", { className: "bw-svc-selected-header", children: [_jsx("label", { className: "bw-label", children: t('selectedService') }), _jsx("button", { type: "button", className: "bw-svc-change", onClick: () => setIsEditingService(true), children: t('btnChange') })] }), _jsxs("div", { className: "bw-svc-row bw-svc-row--readonly", children: [selectedService.image ? (_jsxs("span", { className: "bw-svc-image is-checked", children: [_jsx("img", { src: selectedService.image.url, alt: selectedService.image.alt ??
|
|
1024
|
+
pickLocaleField(selectedService, 'name', locale) ??
|
|
1025
|
+
selectedService.name, loading: "lazy" }), _jsx("span", { className: "bw-svc-image-badge", children: _jsx(CheckIcon, {}) })] })) : (_jsx("span", { className: "bw-check is-checked", children: _jsx(CheckIcon, {}) })), _jsxs("span", { className: "bw-svc-info", children: [_jsx("span", { className: "bw-svc-name", children: pickLocaleField(selectedService, 'name', locale) ?? selectedService.name }), (pickLocaleField(selectedService, 'description', locale) ?? selectedService.description) ? (_jsx("span", { className: "bw-svc-desc", children: pickLocaleField(selectedService, 'description', locale) ?? selectedService.description })) : null, _jsxs("span", { className: "bw-svc-meta-row", children: [_jsx("span", { className: "bw-svc-meta", children: formatDuration(selectedService.durationMinutes) }), _jsx("span", { className: "bw-svc-meta-dot", "aria-hidden": "true", children: "\u00B7" }), _jsx("span", { className: "bw-svc-price", children: formatServicePrice(selectedService, undefined, intlLocale) })] })] })] })] })) : (_jsxs(_Fragment, { children: [_jsx("label", { className: "bw-label", children: t('selectService') }), _jsx("div", { className: "bw-svc-scroll is-open", children: _jsx("div", { className: "bw-svc-scroll-wrap", children: _jsx("div", { className: "bw-svc-scroll-inner", children: serviceGroups.map((group) => (_jsxs("div", { className: "bw-svc-group", children: [group.categoryName ? (_jsx("div", { className: "bw-svc-category", children: group.categoryName })) : null, group.services.map((service) => {
|
|
928
1026
|
const isActive = selectedServiceId === service._id;
|
|
929
1027
|
return (_jsxs("button", { type: "button", className: "bw-svc-row", onMouseEnter: () => {
|
|
930
1028
|
void prefetchAvailability({
|
|
@@ -944,8 +1042,12 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
944
1042
|
});
|
|
945
1043
|
}, onClick: () => {
|
|
946
1044
|
handleServiceSelect(service);
|
|
947
|
-
}, children: [service.image ? (_jsxs("span", { className: `bw-svc-image${isActive ? ' is-checked' : ''}`, children: [_jsx("img", { src: service.image.url, alt: service.image.alt ??
|
|
948
|
-
|
|
1045
|
+
}, children: [service.image ? (_jsxs("span", { className: `bw-svc-image${isActive ? ' is-checked' : ''}`, children: [_jsx("img", { src: service.image.url, alt: service.image.alt ??
|
|
1046
|
+
pickLocaleField(service, 'name', locale) ??
|
|
1047
|
+
service.name, loading: "lazy" }), isActive ? (_jsx("span", { className: "bw-svc-image-badge", children: _jsx(CheckIcon, {}) })) : null] })) : (_jsx("span", { className: `bw-check${isActive ? ' is-checked' : ''}`, children: isActive ? _jsx(CheckIcon, {}) : null })), _jsxs("span", { className: "bw-svc-info", children: [_jsx("span", { className: "bw-svc-name", children: pickLocaleField(service, 'name', locale) ?? service.name }), (pickLocaleField(service, 'description', locale) ?? service.description) ? (_jsx("span", { className: "bw-svc-desc", children: pickLocaleField(service, 'description', locale) ?? service.description })) : null, _jsxs("span", { className: "bw-svc-meta-row", children: [_jsx("span", { className: "bw-svc-meta", children: formatDuration(service.durationMinutes) }), _jsx("span", { className: "bw-svc-meta-dot", "aria-hidden": "true", children: "\u00B7" }), _jsx("span", { className: "bw-svc-price", children: formatServicePrice(service, undefined, intlLocale) })] })] })] }, service._id));
|
|
1048
|
+
})] }, group.key))) }) }) })] })) }), selectedService &&
|
|
1049
|
+
selectedServiceRequiresSlot &&
|
|
1050
|
+
!isEditingService ? (_jsxs("div", { className: "bw-provider-section", children: [_jsx("div", { className: "bw-section-divider" }), _jsx("label", { className: "bw-label", children: t('selectProvider') }), providerRow] })) : null] }), _jsxs("div", { className: "bw-step-2", children: [_jsx("p", { className: "bw-step-2-heading", children: t('chooseYourService') }), _jsx("div", { className: "bw-step-2-divider" }), serviceGroups.map((group) => (_jsxs("div", { className: "bw-svc-group", children: [group.categoryName ? (_jsx("div", { className: "bw-svc-category", children: group.categoryName })) : null, group.services.map((service) => {
|
|
949
1051
|
const isActive = selectedServiceId === service._id;
|
|
950
1052
|
return (_jsxs("div", { children: [_jsxs("button", { type: "button", className: "bw-svc-row bw-svc-row--full", onMouseEnter: () => {
|
|
951
1053
|
void prefetchAvailability({
|
|
@@ -965,12 +1067,16 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
965
1067
|
});
|
|
966
1068
|
}, onClick: () => {
|
|
967
1069
|
handleServiceSelect(service);
|
|
968
|
-
}, children: [service.image ? (_jsxs("span", { className: `bw-svc-image bw-svc-image--lg${isActive ? ' is-checked' : ''}`, children: [_jsx("img", { src: service.image.url, alt: service.image.alt ??
|
|
1070
|
+
}, children: [service.image ? (_jsxs("span", { className: `bw-svc-image bw-svc-image--lg${isActive ? ' is-checked' : ''}`, children: [_jsx("img", { src: service.image.url, alt: service.image.alt ??
|
|
1071
|
+
pickLocaleField(service, 'name', locale) ??
|
|
1072
|
+
service.name, loading: "lazy" }), isActive ? (_jsx("span", { className: "bw-svc-image-badge", children: _jsx(CheckIcon, {}) })) : null] })) : (_jsx("span", { className: `bw-check bw-check--lg${isActive ? ' is-checked' : ''}`, children: isActive ? _jsx(CheckIcon, {}) : null })), _jsxs("span", { className: "bw-svc-info", children: [_jsx("span", { className: "bw-svc-name", children: pickLocaleField(service, 'name', locale) ??
|
|
1073
|
+
service.name }), (pickLocaleField(service, 'description', locale) ?? service.description) ? (_jsx("span", { className: "bw-svc-desc", children: pickLocaleField(service, 'description', locale) ?? service.description })) : null, _jsxs("span", { className: "bw-svc-meta-row", children: [_jsx("span", { className: "bw-svc-meta", children: formatDuration(service.durationMinutes) }), _jsx("span", { className: "bw-svc-meta-dot", "aria-hidden": "true", children: "\u00B7" }), _jsx("span", { className: "bw-svc-price", children: formatServicePrice(service, undefined, intlLocale) })] })] })] }), _jsx("div", { className: "bw-row-divider" })] }, service._id));
|
|
969
1074
|
})] }, group.key)))] })] }), _jsxs("div", { className: "bw-col bw-col--center", children: [_jsxs("div", { className: "bw-cal-card", ref: calCardRef, children: [_jsxs("div", { className: "bw-cal-header", children: [_jsxs("div", { className: "bw-month-dropdown", children: [_jsxs("button", { type: "button", className: "bw-month-btn", onClick: () => setMonthOpen((current) => !current), children: [_jsx("span", { children: formatMonthLabel(calendarMonth, intlLocale) }), _jsx(ChevronDownIcon, {})] }), monthOpen ? (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "bw-month-overlay", "aria-label": t('closeMonthPicker'), onClick: () => setMonthOpen(false) }), _jsx("div", { className: "bw-month-list", children: monthOptions.map((option) => (_jsx("button", { type: "button", className: `bw-month-option${option.value === optionCurrentValue(calendarMonth) ? ' is-active' : ''}`, onClick: () => {
|
|
970
1075
|
setCalendarMonth(option.date);
|
|
971
1076
|
setMonthOpen(false);
|
|
972
1077
|
}, children: option.label }, option.value))) })] })) : null] }), _jsxs("div", { className: "bw-cal-navs", children: [_jsx("button", { type: "button", className: "bw-cal-nav", onMouseEnter: () => {
|
|
973
|
-
if (!sameMonth(calendarMonth, startOfMonth(new Date())) &&
|
|
1078
|
+
if (!sameMonth(calendarMonth, startOfMonth(new Date())) &&
|
|
1079
|
+
selectedServiceId) {
|
|
974
1080
|
void prefetchAvailability({
|
|
975
1081
|
client,
|
|
976
1082
|
siteSlug,
|
|
@@ -980,7 +1086,8 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
980
1086
|
});
|
|
981
1087
|
}
|
|
982
1088
|
}, onClick: () => setCalendarMonth((current) => addMonths(current, -1)), disabled: sameMonth(calendarMonth, startOfMonth(new Date())), "aria-label": t('prevMonth'), children: _jsx(ChevronLeftIcon, {}) }), _jsx("button", { type: "button", className: "bw-cal-nav", onMouseEnter: () => {
|
|
983
|
-
if (!sameMonth(calendarMonth, addMonths(startOfMonth(new Date()), 3)) &&
|
|
1089
|
+
if (!sameMonth(calendarMonth, addMonths(startOfMonth(new Date()), 3)) &&
|
|
1090
|
+
selectedServiceId) {
|
|
984
1091
|
void prefetchAvailability({
|
|
985
1092
|
client,
|
|
986
1093
|
siteSlug,
|
|
@@ -1010,22 +1117,53 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1010
1117
|
isSelected ? 'is-selected' : '',
|
|
1011
1118
|
isAvailable && !isPast ? 'is-available' : '',
|
|
1012
1119
|
isPast ? 'is-disabled' : '',
|
|
1013
|
-
!isAvailable && isCurrentMonth && !isPast
|
|
1014
|
-
|
|
1120
|
+
!isAvailable && isCurrentMonth && !isPast
|
|
1121
|
+
? 'is-blocked'
|
|
1122
|
+
: '',
|
|
1123
|
+
dateKey === formatDateKey(startOfDay(new Date()))
|
|
1124
|
+
? 'is-today'
|
|
1125
|
+
: '',
|
|
1015
1126
|
]
|
|
1016
1127
|
.filter(Boolean)
|
|
1017
1128
|
.join(' ');
|
|
1018
1129
|
return (_jsx("button", { type: "button", className: className, disabled: !isCurrentMonth || isPast || !isAvailable, onClick: () => {
|
|
1019
1130
|
setSelectedDate(dateKey);
|
|
1020
1131
|
setSelectedSlot(null);
|
|
1021
|
-
setMobileStep((current) =>
|
|
1132
|
+
setMobileStep((current) => current < 2 ? 2 : current);
|
|
1022
1133
|
}, children: day.getDate() }, dateKey));
|
|
1023
|
-
}) }), !selectedService ? (_jsx("p", { className: "bw-cal-prompt", children: t('calendarSelectServicePrompt') })) : null, _jsxs("div", { className: "bw-timezone", children: [_jsx(GlobeIcon, {}), _jsx("span", { children: selectedHeldStaff?.timezone ??
|
|
1134
|
+
}) }), !selectedService ? (_jsx("p", { className: "bw-cal-prompt", children: t('calendarSelectServicePrompt') })) : null, _jsxs("div", { className: "bw-timezone", children: [_jsx(GlobeIcon, {}), _jsx("span", { children: selectedHeldStaff?.timezone ??
|
|
1135
|
+
selectedStaff?.timezone ??
|
|
1136
|
+
setup?.workspaceTimezone ??
|
|
1137
|
+
setup?.staff?.[0]?.timezone ??
|
|
1138
|
+
'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
|
|
1139
|
+
? t('summaryTitleReschedule')
|
|
1140
|
+
: 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: {
|
|
1141
|
+
...summaryValStyle,
|
|
1142
|
+
textDecoration: 'line-through',
|
|
1143
|
+
opacity: 0.55,
|
|
1144
|
+
}, children: formatFormerSlot(rescheduleContext.formerStartTime, rescheduleContext.formerEndTime, intlLocale) })] })) : null, serviceChanged && formerService ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryFormerService') }), _jsx("span", { className: "bw-summary-val", style: {
|
|
1145
|
+
...summaryValStyle,
|
|
1146
|
+
textDecoration: 'line-through',
|
|
1147
|
+
opacity: 0.55,
|
|
1148
|
+
}, children: formerService.name })] })) : null, staffChanged ? (_jsxs("div", { className: "bw-summary-row", children: [_jsx("span", { children: t('summaryFormerStaff') }), _jsx("span", { className: "bw-summary-val", style: {
|
|
1149
|
+
...summaryValStyle,
|
|
1150
|
+
textDecoration: 'line-through',
|
|
1151
|
+
opacity: 0.55,
|
|
1152
|
+
}, 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) ??
|
|
1153
|
+
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 ??
|
|
1154
|
+
selectedStaff?.name ??
|
|
1155
|
+
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') })] })] })] }), (() => {
|
|
1024
1156
|
const intakeRows = buildIntakeReviewRows(selectedService.intakeForm, intakeResponses, selectedServicePath, locale, t);
|
|
1025
1157
|
if (intakeRows.length === 0)
|
|
1026
1158
|
return null;
|
|
1027
1159
|
return (_jsxs("div", { className: "bw-summary-group", children: [_jsx("span", { className: "bw-summary-subhead", children: t('reviewAdditionalInfo') }), _jsx("div", { className: "bw-summary-rows", children: intakeRows.map((row) => (_jsxs("div", { className: `bw-summary-row${row.multiline ? ' bw-summary-row--stack' : ''}`, children: [_jsx("span", { children: row.label }), _jsx("span", { className: "bw-summary-val", children: row.value })] }, row.key))) })] }));
|
|
1028
|
-
})(), customerNotes.trim() ? (_jsxs("div", { className: "bw-summary-group", children: [_jsx("span", { className: "bw-summary-subhead", children: isReschedule
|
|
1160
|
+
})(), customerNotes.trim() ? (_jsxs("div", { className: "bw-summary-group", children: [_jsx("span", { className: "bw-summary-subhead", children: isReschedule
|
|
1161
|
+
? t('notesLabelReschedule')
|
|
1162
|
+
: 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: isQuoteLoading
|
|
1163
|
+
? t('summaryCalculating')
|
|
1164
|
+
: t('summaryEstimatedTotal') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: formatServicePrice(selectedService, quote, 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
|
|
1165
|
+
? t('summaryTitleReschedule')
|
|
1166
|
+
: 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: {
|
|
1029
1167
|
...summaryValStyle,
|
|
1030
1168
|
textDecoration: 'line-through',
|
|
1031
1169
|
opacity: 0.55,
|
|
@@ -1037,7 +1175,11 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1037
1175
|
...summaryValStyle,
|
|
1038
1176
|
textDecoration: 'line-through',
|
|
1039
1177
|
opacity: 0.55,
|
|
1040
|
-
}, 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 ??
|
|
1178
|
+
}, 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 ??
|
|
1179
|
+
selectedStaff?.name ??
|
|
1180
|
+
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: isQuoteLoading
|
|
1181
|
+
? t('summaryCalculating')
|
|
1182
|
+
: t('summaryEstimatedTotal') }), _jsx("span", { className: "bw-summary-val", style: summaryValStyle, children: formatServicePrice(selectedService, quote, intlLocale) })] })] })] })) : null, error ? (_jsx("div", { className: "bw-error", children: error })) : null, renderSubmitHelp(), payment ? (_jsx(StripePaymentBlock, { payment: payment, appointmentId: paymentAppointmentId, showInlineButton: false, actionTarget: mobilePaymentActionTarget, onSuccess: () => {
|
|
1041
1183
|
setSuccess(t('successPaymentReceived'));
|
|
1042
1184
|
setPayment(null);
|
|
1043
1185
|
}, onError: setError })) : (_jsxs("button", { type: "button", className: `bw-confirm-btn${!canSubmit || isQuoteLoading ? ' is-disabled' : ''}`, "aria-disabled": !canSubmit || isQuoteLoading, onClick: handleConfirmAttempt, children: [_jsx("span", { children: isSubmitting
|
|
@@ -1048,7 +1190,23 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1048
1190
|
? t('btnConfirmReschedule')
|
|
1049
1191
|
: selectedServiceRequiresPayment
|
|
1050
1192
|
? t('btnContinueToPayment')
|
|
1051
|
-
: 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) ??
|
|
1193
|
+
: 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) ??
|
|
1194
|
+
selectedService.name }), (pickLocaleField(selectedService, 'description', locale) ??
|
|
1195
|
+
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
|
|
1196
|
+
? t('summaryNewTime')
|
|
1197
|
+
: 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 ??
|
|
1198
|
+
selectedStaff?.name ??
|
|
1199
|
+
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 ??
|
|
1200
|
+
selectedStaff?.timezone ??
|
|
1201
|
+
setup?.workspaceTimezone ??
|
|
1202
|
+
setup?.staff?.[0]?.timezone ??
|
|
1203
|
+
'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: isQuoteLoading
|
|
1204
|
+
? t('summaryCalculating')
|
|
1205
|
+
: t('summaryEstimatedTotal') }), _jsx("span", { className: "bw-details-meta-value", children: formatServicePrice(selectedService, quote, 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
|
|
1206
|
+
? t('notesPlaceholderReschedule')
|
|
1207
|
+
: t('notesPlaceholder')), error ? _jsx("div", { className: "bw-error", children: error }) : null, renderSubmitHelp(), !payment &&
|
|
1208
|
+
(forcedState === 'payment-full' ||
|
|
1209
|
+
forcedState === 'payment-deposit') ? (_jsx(PaymentPanel, { service: selectedService, mode: forcedState === 'payment-full' ? 'full' : 'deposit' })) : null, payment ? (_jsx(StripePaymentBlock, { payment: payment, appointmentId: paymentAppointmentId, onSuccess: () => {
|
|
1052
1210
|
setSuccess(t('successPaymentReceived'));
|
|
1053
1211
|
setPayment(null);
|
|
1054
1212
|
}, onError: setError })) : (_jsxs("button", { type: "button", className: `bw-confirm-btn${!canSubmit || isQuoteLoading ? ' is-disabled' : ''}`, "aria-disabled": !canSubmit || isQuoteLoading, onClick: handleConfirmAttempt, children: [_jsx("span", { children: isSubmitting
|
|
@@ -1056,7 +1214,9 @@ export function BookingWidgetPanel({ title, description, mobileHeader, mode = 'c
|
|
|
1056
1214
|
? t('btnRescheduling')
|
|
1057
1215
|
: t('btnBooking')
|
|
1058
1216
|
: forcedState === 'payment-full'
|
|
1059
|
-
? t('btnPayAndConfirm', {
|
|
1217
|
+
? t('btnPayAndConfirm', {
|
|
1218
|
+
price: formatPrice(selectedService, intlLocale),
|
|
1219
|
+
})
|
|
1060
1220
|
: forcedState === 'payment-deposit'
|
|
1061
1221
|
? t('btnPayDepositAndConfirm')
|
|
1062
1222
|
: isReschedule
|
|
@@ -1156,7 +1316,8 @@ function areRequiredIntakeFieldsComplete(form, responses, mode) {
|
|
|
1156
1316
|
}
|
|
1157
1317
|
function getSubmitBlockers({ selectedService, selectedServiceRequiresSlot, selectedDate, selectedSlot, holdId, isIntakeComplete, intakeResponses, selectedServicePath, quote, customerName, customerEmail, t, locale = DEFAULT_LOCALE, }) {
|
|
1158
1318
|
const blockers = [];
|
|
1159
|
-
if (selectedServiceRequiresSlot &&
|
|
1319
|
+
if (selectedServiceRequiresSlot &&
|
|
1320
|
+
(!selectedDate || !selectedSlot || !holdId)) {
|
|
1160
1321
|
blockers.push(t('blockerDateTime'));
|
|
1161
1322
|
}
|
|
1162
1323
|
if (!customerName.trim())
|
|
@@ -1284,7 +1445,9 @@ function buildIntakeReviewRows(form, responses, servicePath, locale, t) {
|
|
|
1284
1445
|
continue;
|
|
1285
1446
|
for (const field of section.fields) {
|
|
1286
1447
|
if (field.type === 'repeatable-group') {
|
|
1287
|
-
const items = Array.isArray(responses[field.id])
|
|
1448
|
+
const items = Array.isArray(responses[field.id])
|
|
1449
|
+
? responses[field.id]
|
|
1450
|
+
: [];
|
|
1288
1451
|
items.forEach((item, index) => {
|
|
1289
1452
|
const record = item && typeof item === 'object' && !Array.isArray(item)
|
|
1290
1453
|
? item
|
|
@@ -1489,7 +1652,9 @@ function IntakeField({ field, value, onChange, idPrefix, }) {
|
|
|
1489
1652
|
if (field.type === 'repeatable-group') {
|
|
1490
1653
|
const items = Array.isArray(value) && value.length > 0 ? value : [{}];
|
|
1491
1654
|
return (_jsxs("div", { className: "bw-field bw-field--wide bw-repeatable", children: [_jsx("label", { children: label }), items.map((item, index) => {
|
|
1492
|
-
const record = item && typeof item === 'object' && !Array.isArray(item)
|
|
1655
|
+
const record = item && typeof item === 'object' && !Array.isArray(item)
|
|
1656
|
+
? item
|
|
1657
|
+
: {};
|
|
1493
1658
|
return (_jsxs("div", { className: "bw-repeatable-item", children: [_jsxs("div", { className: "bw-repeatable-head", children: [_jsx("span", { children: t('repeatItemLabel', { index: index + 1 }) }), items.length > 1 ? (_jsx("button", { type: "button", className: "bw-link-btn", onClick: () => onChange(items.filter((_, itemIndex) => itemIndex !== index)), children: t('repeatRemove') })) : null] }), _jsx("div", { className: "bw-form-fields", children: (field.fields ?? []).map((child) => (_jsx(IntakeField, { field: child, value: record[child.id], idPrefix: `${id}-${index}`, onChange: (nextValue) => {
|
|
1494
1659
|
const nextItems = [...items];
|
|
1495
1660
|
nextItems[index] = { ...record, [child.id]: nextValue };
|
|
@@ -1513,7 +1678,11 @@ function IntakeField({ field, value, onChange, idPrefix, }) {
|
|
|
1513
1678
|
if (field.type === 'textarea') {
|
|
1514
1679
|
return (_jsxs("div", { className: "bw-field bw-field--wide", children: [_jsxs("label", { htmlFor: id, children: [label, field.required ? _jsx("span", { className: "bw-required", children: " *" }) : null] }), _jsx("textarea", { id: id, rows: 3, required: field.required, value: stringValue, placeholder: field.placeholder, onChange: (event) => onChange(event.target.value) }), field.helpText ? _jsx("p", { className: "bw-help", children: field.helpText }) : null] }));
|
|
1515
1680
|
}
|
|
1516
|
-
return (_jsxs("div", { className: "bw-field", children: [_jsxs("label", { htmlFor: id, children: [label, field.required ? _jsx("span", { className: "bw-required", children: " *" }) : null] }), _jsx("input", { id: id, type: field.type === 'email'
|
|
1681
|
+
return (_jsxs("div", { className: "bw-field", children: [_jsxs("label", { htmlFor: id, children: [label, field.required ? _jsx("span", { className: "bw-required", children: " *" }) : null] }), _jsx("input", { id: id, type: field.type === 'email'
|
|
1682
|
+
? 'email'
|
|
1683
|
+
: field.type === 'number'
|
|
1684
|
+
? 'number'
|
|
1685
|
+
: 'text', min: field.min, max: field.max, required: field.required, value: stringValue, placeholder: field.placeholder, onChange: (event) => {
|
|
1517
1686
|
onChange(field.type === 'number'
|
|
1518
1687
|
? event.target.value === ''
|
|
1519
1688
|
? ''
|
|
@@ -1744,7 +1913,8 @@ function buildCalendarDays(month) {
|
|
|
1744
1913
|
});
|
|
1745
1914
|
}
|
|
1746
1915
|
function sameMonth(left, right) {
|
|
1747
|
-
return left.getFullYear() === right.getFullYear() &&
|
|
1916
|
+
return (left.getFullYear() === right.getFullYear() &&
|
|
1917
|
+
left.getMonth() === right.getMonth());
|
|
1748
1918
|
}
|
|
1749
1919
|
function getMonthOptions(currentMonth, total, locale = 'en-US') {
|
|
1750
1920
|
const start = startOfMonth(new Date());
|
|
@@ -1763,7 +1933,8 @@ function optionCurrentValue(date) {
|
|
|
1763
1933
|
}
|
|
1764
1934
|
function isDateInMonth(dateKey, month) {
|
|
1765
1935
|
const date = new Date(`${dateKey}T00:00:00`);
|
|
1766
|
-
return date.getMonth() === month.getMonth() &&
|
|
1936
|
+
return (date.getMonth() === month.getMonth() &&
|
|
1937
|
+
date.getFullYear() === month.getFullYear());
|
|
1767
1938
|
}
|
|
1768
1939
|
function findFirstAvailableDate(availabilityByDate, month) {
|
|
1769
1940
|
const dates = [...availabilityByDate.entries()]
|