@arkipay/booking-widget 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,1686 @@
1
+ "use client";
2
+ import { BotIdClient } from 'botid/client';
3
+ import { createContext, useMemo, useState, useCallback, useEffect, useContext, useReducer } from 'react';
4
+ import { jsxs, jsx } from 'react/jsx-runtime';
5
+ import { Temporal } from '@js-temporal/polyfill';
6
+
7
+ // src/provider.tsx
8
+ var BookingContext = createContext(null);
9
+ function useBookingContext() {
10
+ const ctx = useContext(BookingContext);
11
+ if (!ctx) {
12
+ throw new Error(`useBookingContext must be used within BookingProvider`);
13
+ }
14
+ return ctx;
15
+ }
16
+
17
+ // src/constants.ts
18
+ var ANY_PROVIDER_ID = `any`;
19
+
20
+ // src/utils/fields.ts
21
+ var emptyCustomerValues = () => ({
22
+ firstName: ``,
23
+ lastName: ``,
24
+ email: ``,
25
+ phone: ``,
26
+ address: ``,
27
+ city: ``,
28
+ state: ``,
29
+ zip: ``,
30
+ notes: ``,
31
+ customField1: ``,
32
+ customField2: ``,
33
+ customField3: ``,
34
+ customField4: ``,
35
+ customField5: ``,
36
+ timezone: typeof Intl !== `undefined` ? Intl.DateTimeFormat().resolvedOptions().timeZone : `UTC`
37
+ });
38
+ var FIELD_KEY_TO_VALUE = {
39
+ first_name: `firstName`,
40
+ last_name: `lastName`,
41
+ email: `email`,
42
+ phone: `phone`,
43
+ address: `address`,
44
+ city: `city`,
45
+ state: `state`,
46
+ zip: `zip`,
47
+ notes: `notes`,
48
+ custom_1: `customField1`,
49
+ custom_2: `customField2`,
50
+ custom_3: `customField3`,
51
+ custom_4: `customField4`,
52
+ custom_5: `customField5`
53
+ };
54
+ var FIELD_KEY_TO_LABEL = {
55
+ first_name: `fields.first_name`,
56
+ last_name: `fields.last_name`,
57
+ email: `fields.email`,
58
+ phone: `fields.phone`,
59
+ address: `fields.address`,
60
+ city: `fields.city`,
61
+ state: `fields.state`,
62
+ zip: `fields.zip`,
63
+ notes: `fields.notes`,
64
+ custom_1: `fields.custom_1`,
65
+ custom_2: `fields.custom_2`,
66
+ custom_3: `fields.custom_3`,
67
+ custom_4: `fields.custom_4`,
68
+ custom_5: `fields.custom_5`
69
+ };
70
+ function visibleFormFields(fields) {
71
+ return [...fields].filter((f) => f.display).sort((a, b) => a.sortOrder - b.sortOrder);
72
+ }
73
+ function customerValueKey(fieldKey) {
74
+ return FIELD_KEY_TO_VALUE[fieldKey];
75
+ }
76
+ function fieldLabelKey(fieldKey) {
77
+ return FIELD_KEY_TO_LABEL[fieldKey];
78
+ }
79
+ function validateCustomerFields(fields, values) {
80
+ const errors = {};
81
+ for (const field of fields) {
82
+ if (!field.display || !field.required) {
83
+ continue;
84
+ }
85
+ const key = customerValueKey(field.fieldKey);
86
+ if (!key) {
87
+ continue;
88
+ }
89
+ const value = values[key];
90
+ if (typeof value !== `string` || value.trim() === ``) {
91
+ errors[field.fieldKey] = `required`;
92
+ }
93
+ }
94
+ if (fields.some((f) => f.fieldKey === `email` && f.display) && values.email.trim()) {
95
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email.trim())) {
96
+ errors.email = `invalid`;
97
+ }
98
+ }
99
+ return errors;
100
+ }
101
+ function customerValuesToAppointmentBody(values) {
102
+ return {
103
+ firstName: values.firstName.trim(),
104
+ lastName: values.lastName.trim(),
105
+ email: values.email.trim(),
106
+ phone: values.phone.trim() || null,
107
+ address: values.address.trim() || null,
108
+ city: values.city.trim() || null,
109
+ state: values.state.trim() || null,
110
+ zip: values.zip.trim() || null,
111
+ notes: values.notes.trim() || null,
112
+ customField1: values.customField1.trim() || null,
113
+ customField2: values.customField2.trim() || null,
114
+ customField3: values.customField3.trim() || null,
115
+ customField4: values.customField4.trim() || null,
116
+ customField5: values.customField5.trim() || null,
117
+ customerTimezone: values.timezone || null
118
+ };
119
+ }
120
+ function needsLegalStep(legal) {
121
+ return legal.displayCookieNotice || legal.displayTerms || legal.displayPrivacy;
122
+ }
123
+
124
+ // src/hooks/use-booking.ts
125
+ var DEFAULT_TIME_DISPLAY_MODE = `business`;
126
+ function withStep(timeDisplayMode, step) {
127
+ return { timeDisplayMode, ...step };
128
+ }
129
+ function retainMode(state, step) {
130
+ return withStep(state.timeDisplayMode, step);
131
+ }
132
+ function createInitialBookingState(prefill) {
133
+ if (prefill?.serviceId && prefill.providerId) {
134
+ return withStep(DEFAULT_TIME_DISPLAY_MODE, {
135
+ step: `date`,
136
+ serviceId: prefill.serviceId,
137
+ providerId: prefill.providerId
138
+ });
139
+ }
140
+ if (prefill?.serviceId) {
141
+ return withStep(DEFAULT_TIME_DISPLAY_MODE, { step: `provider`, serviceId: prefill.serviceId });
142
+ }
143
+ return withStep(DEFAULT_TIME_DISPLAY_MODE, { step: `service` });
144
+ }
145
+ function bookingReducer(state, action, tenantConfig) {
146
+ switch (action.type) {
147
+ case `prefill`: {
148
+ return createInitialBookingState({
149
+ serviceId: action.serviceId,
150
+ providerId: action.providerId
151
+ });
152
+ }
153
+ case `set_time_display_mode`:
154
+ return { ...state, timeDisplayMode: action.mode };
155
+ case `select_service`:
156
+ return retainMode(state, { step: `provider`, serviceId: action.serviceId });
157
+ case `select_provider`:
158
+ if (state.step !== `provider`) {
159
+ return state;
160
+ }
161
+ return retainMode(state, {
162
+ step: `date`,
163
+ serviceId: state.serviceId,
164
+ providerId: action.providerId
165
+ });
166
+ case `select_date`:
167
+ if (state.step !== `date`) {
168
+ return state;
169
+ }
170
+ return retainMode(state, {
171
+ step: `time`,
172
+ serviceId: state.serviceId,
173
+ providerId: state.providerId,
174
+ date: action.date
175
+ });
176
+ case `select_slot`:
177
+ if (state.step !== `time`) {
178
+ return state;
179
+ }
180
+ return retainMode(state, {
181
+ step: `customer`,
182
+ serviceId: state.serviceId,
183
+ providerId: state.providerId,
184
+ date: state.date,
185
+ slot: action.slot
186
+ });
187
+ case `set_customer`:
188
+ if (state.step !== `customer`) {
189
+ return state;
190
+ }
191
+ if (tenantConfig && needsLegalStep(tenantConfig.legal)) {
192
+ return retainMode(state, {
193
+ step: `legal`,
194
+ serviceId: state.serviceId,
195
+ providerId: state.providerId,
196
+ date: state.date,
197
+ slot: state.slot,
198
+ customer: action.customer
199
+ });
200
+ }
201
+ return retainMode(state, {
202
+ step: `confirm`,
203
+ serviceId: state.serviceId,
204
+ providerId: state.providerId,
205
+ date: state.date,
206
+ slot: state.slot,
207
+ customer: action.customer,
208
+ consents: {}
209
+ });
210
+ case `set_consents`:
211
+ if (state.step !== `legal`) {
212
+ return state;
213
+ }
214
+ return retainMode(state, {
215
+ step: `confirm`,
216
+ serviceId: state.serviceId,
217
+ providerId: state.providerId,
218
+ date: state.date,
219
+ slot: state.slot,
220
+ customer: state.customer,
221
+ consents: action.consents
222
+ });
223
+ case `booked`:
224
+ return retainMode(state, { step: `booked`, appointment: action.appointment });
225
+ case `back`:
226
+ return backStep(state);
227
+ default:
228
+ return state;
229
+ }
230
+ }
231
+ function backStep(state) {
232
+ switch (state.step) {
233
+ case `provider`:
234
+ return retainMode(state, { step: `service` });
235
+ case `date`:
236
+ return retainMode(state, { step: `provider`, serviceId: state.serviceId });
237
+ case `time`:
238
+ return retainMode(state, {
239
+ step: `date`,
240
+ serviceId: state.serviceId,
241
+ providerId: state.providerId
242
+ });
243
+ case `customer`:
244
+ return retainMode(state, {
245
+ step: `time`,
246
+ serviceId: state.serviceId,
247
+ providerId: state.providerId,
248
+ date: state.date
249
+ });
250
+ case `legal`:
251
+ return retainMode(state, {
252
+ step: `customer`,
253
+ serviceId: state.serviceId,
254
+ providerId: state.providerId,
255
+ date: state.date,
256
+ slot: state.slot
257
+ });
258
+ case `confirm`:
259
+ return retainMode(state, {
260
+ step: `customer`,
261
+ serviceId: state.serviceId,
262
+ providerId: state.providerId,
263
+ date: state.date,
264
+ slot: state.slot
265
+ });
266
+ default:
267
+ return state;
268
+ }
269
+ }
270
+ function useBookingReducer(initial, tenantConfig) {
271
+ const [state, dispatch] = useReducer(
272
+ (current, action) => bookingReducer(current, action, tenantConfig),
273
+ void 0,
274
+ () => createInitialBookingState(initial)
275
+ );
276
+ return useMemo(() => ({ state, dispatch }), [state, dispatch]);
277
+ }
278
+ function useBooking() {
279
+ const { bookingState, dispatch, tenantConfig, onBooked } = useBookingContext();
280
+ return {
281
+ state: bookingState,
282
+ dispatch,
283
+ tenantConfig,
284
+ onBooked,
285
+ timeDisplayMode: bookingState.timeDisplayMode
286
+ };
287
+ }
288
+
289
+ // src/i18n/en.ts
290
+ var enCatalog = {
291
+ steps: {
292
+ service: `Choose a service`,
293
+ provider: `Choose a provider`,
294
+ date: `Choose a date`,
295
+ time: `Choose a time`,
296
+ customer: `Your details`,
297
+ legal: `Legal information`,
298
+ confirm: `Confirm your booking`,
299
+ booked: `Booking confirmed`
300
+ },
301
+ actions: {
302
+ back: `Back`,
303
+ continue: `Continue`,
304
+ confirm: `Confirm booking`,
305
+ submitting: `Booking\u2026`
306
+ },
307
+ service: {
308
+ empty: `No services are available right now.`,
309
+ durationMinutes: `{minutes} min`,
310
+ price: `{amount}`
311
+ },
312
+ provider: {
313
+ empty: `No providers are available for this service.`,
314
+ any: `Any available provider`
315
+ },
316
+ date: {
317
+ unavailable: `Unavailable`,
318
+ minHint: `Earliest booking is tomorrow.`
319
+ },
320
+ time: {
321
+ empty: `No time slots are available on this date.`,
322
+ timezone: `All times in {timezone}`,
323
+ timezoneHint: `Times shown in {timezone}`,
324
+ mismatchBanner: `This business uses {businessTimezone}. You are in {customerTimezone}.`,
325
+ showMyTime: `Show in my time`,
326
+ showBusinessTime: `Show business time`,
327
+ yourTimeLabel: `Your time ({timezone})`,
328
+ businessTimeLabel: `Business time ({timezone})`
329
+ },
330
+ customer: {
331
+ required: `Required`,
332
+ invalidEmail: `Enter a valid email address.`
333
+ },
334
+ legal: {
335
+ cookie: `Cookie notice`,
336
+ terms: `Terms & conditions`,
337
+ privacy: `Privacy policy`,
338
+ acceptCookie: `I accept the cookie notice`,
339
+ acceptTerms: `I accept the terms & conditions`,
340
+ acceptPrivacy: `I accept the privacy policy`,
341
+ readMore: `Read`
342
+ },
343
+ confirm: {
344
+ summary: `Review your appointment`,
345
+ service: `Service`,
346
+ provider: `Provider`,
347
+ when: `When`,
348
+ customer: `Customer`,
349
+ error: `We could not complete your booking. Please try again.`,
350
+ conflict: `That time slot is no longer available. Please choose another time.`
351
+ },
352
+ booked: {
353
+ reference: `Reference`,
354
+ message: `Your appointment has been booked.`
355
+ },
356
+ disabled: {
357
+ title: `Booking unavailable`
358
+ },
359
+ fields: {
360
+ first_name: `First name`,
361
+ last_name: `Last name`,
362
+ email: `Email`,
363
+ phone: `Phone`,
364
+ address: `Address`,
365
+ city: `City`,
366
+ state: `State / province`,
367
+ zip: `Postal code`,
368
+ notes: `Notes`,
369
+ custom_1: `Additional information`,
370
+ custom_2: `Additional information (2)`,
371
+ custom_3: `Additional information (3)`,
372
+ custom_4: `Additional information (4)`,
373
+ custom_5: `Additional information (5)`
374
+ }
375
+ };
376
+
377
+ // src/i18n/create-translator.ts
378
+ function getNestedValue(obj, path) {
379
+ const parts = path.split(`.`);
380
+ let current = obj;
381
+ for (const part of parts) {
382
+ if (current === null || typeof current !== `object` || !(part in current)) {
383
+ return void 0;
384
+ }
385
+ current = current[part];
386
+ }
387
+ return typeof current === `string` ? current : void 0;
388
+ }
389
+ function interpolate(template, params) {
390
+ if (!params) {
391
+ return template;
392
+ }
393
+ return template.replace(/\{(\w+)\}/g, (_, key) => {
394
+ const value = params[key];
395
+ return value === void 0 ? `{${key}}` : String(value);
396
+ });
397
+ }
398
+ function createTranslator(locale, tenantDefaultLanguage) {
399
+ return (key, params) => {
400
+ const template = getNestedValue(enCatalog, key);
401
+ if (!template) {
402
+ return key;
403
+ }
404
+ return interpolate(template, params);
405
+ };
406
+ }
407
+
408
+ // ../api-contracts/src/idempotency.ts
409
+ var MUTATION_METHODS = /* @__PURE__ */ new Set([`POST`, `PUT`, `PATCH`, `DELETE`]);
410
+ function createIdempotencyKey() {
411
+ return crypto.randomUUID();
412
+ }
413
+ function clientMutationIdempotencyHeaders(method) {
414
+ const normalizedMethod = method.toUpperCase();
415
+ if (!MUTATION_METHODS.has(normalizedMethod)) {
416
+ return {};
417
+ }
418
+ return { "Idempotency-Key": createIdempotencyKey() };
419
+ }
420
+
421
+ // src/lib/fetch-booking-client.ts
422
+ var DEFAULT_API_URL = `https://api.easyappts.com`;
423
+ var BookingApiError = class extends Error {
424
+ constructor(status, code, message) {
425
+ super(message);
426
+ this.status = status;
427
+ this.code = code;
428
+ this.name = `BookingApiError`;
429
+ }
430
+ status;
431
+ code;
432
+ };
433
+ var BookingClient = class {
434
+ baseUrl;
435
+ publishableKey;
436
+ origin;
437
+ constructor(opts) {
438
+ if (!opts.publishableKey.startsWith(`pk_`)) {
439
+ throw new Error(`BookingClient requires a publishable key (pk_*).`);
440
+ }
441
+ this.publishableKey = opts.publishableKey;
442
+ this.baseUrl = (opts.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, ``);
443
+ this.origin = opts.origin;
444
+ }
445
+ async getTenantConfig() {
446
+ return this.getJson(`/v1/public/tenant-config`);
447
+ }
448
+ async listServices() {
449
+ const body = await this.getJson(`/v1/public/services`);
450
+ return body.data;
451
+ }
452
+ async listProviders(params) {
453
+ const query = params?.serviceId ? `?serviceId=${encodeURIComponent(params.serviceId)}` : ``;
454
+ const body = await this.getJson(
455
+ `/v1/public/providers${query}`
456
+ );
457
+ return body.data;
458
+ }
459
+ async getAvailability(params) {
460
+ const query = new URLSearchParams({
461
+ providerId: params.providerId,
462
+ serviceId: params.serviceId,
463
+ date: params.date
464
+ });
465
+ const body = await this.getJson(
466
+ `/v1/public/availabilities?${query.toString()}`
467
+ );
468
+ return body.data;
469
+ }
470
+ async getUnavailableDates(params) {
471
+ const query = new URLSearchParams({
472
+ providerId: params.providerId,
473
+ serviceId: params.serviceId,
474
+ month: params.month
475
+ });
476
+ const body = await this.getJson(
477
+ `/v1/public/unavailable-dates?${query.toString()}`
478
+ );
479
+ return body.data;
480
+ }
481
+ async createAppointment(body) {
482
+ return this.postJson(`/v1/public/appointments`, body);
483
+ }
484
+ headers() {
485
+ const headers = {
486
+ Authorization: `Bearer ${this.publishableKey}`,
487
+ Accept: `application/json`
488
+ };
489
+ if (this.origin) {
490
+ headers.Origin = this.origin;
491
+ }
492
+ return headers;
493
+ }
494
+ async getJson(path) {
495
+ const res = await fetch(`${this.baseUrl}${path}`, { headers: this.headers() });
496
+ return this.parseJson(res);
497
+ }
498
+ async postJson(path, body) {
499
+ const res = await fetch(`${this.baseUrl}${path}`, {
500
+ method: `POST`,
501
+ headers: {
502
+ ...this.headers(),
503
+ "Content-Type": `application/json`,
504
+ ...clientMutationIdempotencyHeaders(`POST`)
505
+ },
506
+ body: JSON.stringify(body)
507
+ });
508
+ return this.parseJson(res);
509
+ }
510
+ async parseJson(res) {
511
+ const payload = await res.json().catch(() => null);
512
+ if (!res.ok) {
513
+ const err = payload !== null && typeof payload === `object` && `error` in payload && payload.error !== null && typeof payload.error === `object` ? payload.error : null;
514
+ const code = err !== null && `code` in err && typeof err.code === `string` ? err.code : `unknown`;
515
+ const message = err !== null && `message` in err && typeof err.message === `string` ? err.message : res.statusText;
516
+ throw new BookingApiError(res.status, code, message);
517
+ }
518
+ return payload;
519
+ }
520
+ };
521
+
522
+ // src/lib/create-booking-client.ts
523
+ function createBookingClient(opts) {
524
+ return new BookingClient(opts);
525
+ }
526
+
527
+ // src/utils/booking-timezone.ts
528
+ function getCustomerTimezone() {
529
+ if (typeof Intl === `undefined`) {
530
+ return `UTC`;
531
+ }
532
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || `UTC`;
533
+ }
534
+ function resolveBusinessTimezone(params) {
535
+ const anyId = params.anyProviderId ?? ANY_PROVIDER_ID;
536
+ if (params.providerId === anyId) {
537
+ return params.tenantDefaultTimezone;
538
+ }
539
+ const provider = params.providers.find((p) => p.id === params.providerId);
540
+ return provider?.timezone ?? params.tenantDefaultTimezone;
541
+ }
542
+ function allowsTimeDisplayToggle(bookingTimeDisplay) {
543
+ return bookingTimeDisplay === `customer`;
544
+ }
545
+ function resolveDisplayTimezone(params) {
546
+ const setting = params.bookingTimeDisplay ?? `business`;
547
+ const mode = params.timeDisplayMode ?? `business`;
548
+ if (setting === `business`) {
549
+ return params.businessTimezone;
550
+ }
551
+ if (setting === `customer_only`) {
552
+ return params.customerTimezone;
553
+ }
554
+ return mode === `customer` ? params.customerTimezone : params.businessTimezone;
555
+ }
556
+ function shouldShowSecondaryTimezone(params) {
557
+ if (!params.zonesDiffer) {
558
+ return false;
559
+ }
560
+ const setting = params.bookingTimeDisplay ?? `business`;
561
+ return setting === `customer` || setting === `customer_only`;
562
+ }
563
+ var DEFAULT_API_URL2 = `https://api.easyappts.com`;
564
+ function normalizeColorScheme(value) {
565
+ if (value === `dark` || value === `light` || value === `system`) {
566
+ return value;
567
+ }
568
+ return void 0;
569
+ }
570
+ function readUrlPrefill() {
571
+ if (typeof window === `undefined`) {
572
+ return {};
573
+ }
574
+ const params = new URLSearchParams(window.location.search);
575
+ const result = {};
576
+ const serviceId = params.get(`service`);
577
+ const providerId = params.get(`provider`);
578
+ if (serviceId) {
579
+ result.serviceId = serviceId;
580
+ }
581
+ if (providerId) {
582
+ result.providerId = providerId;
583
+ }
584
+ return result;
585
+ }
586
+ function BookingProvider({
587
+ publishableKey,
588
+ apiUrl = DEFAULT_API_URL2,
589
+ locale,
590
+ theme = {},
591
+ onBooked,
592
+ initialServiceId,
593
+ initialProviderId,
594
+ origin,
595
+ className,
596
+ style,
597
+ botProtection = true,
598
+ children
599
+ }) {
600
+ const resolvedApiUrl = apiUrl.replace(/\/$/, ``);
601
+ const client = useMemo(
602
+ () => createBookingClient({ publishableKey, apiUrl: resolvedApiUrl, origin }),
603
+ [publishableKey, resolvedApiUrl, origin]
604
+ );
605
+ const [tenantConfig, setTenantConfig] = useState(null);
606
+ const [tenantConfigLoading, setTenantConfigLoading] = useState(true);
607
+ const [tenantConfigError, setTenantConfigError] = useState(null);
608
+ const prefill = useMemo(() => {
609
+ const fromUrl = readUrlPrefill();
610
+ return {
611
+ serviceId: initialServiceId ?? fromUrl.serviceId,
612
+ providerId: initialProviderId ?? fromUrl.providerId
613
+ };
614
+ }, [initialProviderId, initialServiceId]);
615
+ const { state: bookingState, dispatch } = useBookingReducer(prefill, tenantConfig);
616
+ const customerTimezone = useMemo(() => getCustomerTimezone(), []);
617
+ const setTimeDisplayMode = useCallback(
618
+ (mode) => {
619
+ dispatch({ type: `set_time_display_mode`, mode });
620
+ },
621
+ [dispatch]
622
+ );
623
+ useEffect(() => {
624
+ if (prefill.serviceId || prefill.providerId) {
625
+ dispatch({
626
+ type: `prefill`,
627
+ serviceId: prefill.serviceId,
628
+ providerId: prefill.providerId
629
+ });
630
+ }
631
+ }, [dispatch, prefill.providerId, prefill.serviceId]);
632
+ useEffect(() => {
633
+ let cancelled = false;
634
+ setTenantConfigLoading(true);
635
+ setTenantConfigError(null);
636
+ void client.getTenantConfig().then((config) => {
637
+ if (!cancelled) {
638
+ setTenantConfig(config);
639
+ }
640
+ }).catch((err) => {
641
+ if (!cancelled) {
642
+ setTenantConfigError(err instanceof Error ? err.message : `Failed to load settings`);
643
+ }
644
+ }).finally(() => {
645
+ if (!cancelled) {
646
+ setTenantConfigLoading(false);
647
+ }
648
+ });
649
+ return () => {
650
+ cancelled = true;
651
+ };
652
+ }, [client]);
653
+ const resolvedLocale = locale ?? tenantConfig?.localization.defaultLanguage ?? `en`;
654
+ const t = useMemo(
655
+ () => createTranslator(resolvedLocale, tenantConfig?.localization.defaultLanguage),
656
+ [resolvedLocale, tenantConfig?.localization.defaultLanguage]
657
+ );
658
+ const cssVars = useMemo(() => {
659
+ const vars = {};
660
+ if (tenantConfig?.business.brandColor) {
661
+ vars[`--ea-brand-fallback`] = tenantConfig.business.brandColor;
662
+ }
663
+ if (theme.brandColor) vars[`--ea-brand`] = theme.brandColor;
664
+ if (theme.onBrandColor) vars[`--ea-on-brand`] = theme.onBrandColor;
665
+ if (theme.textColor) vars[`--ea-text`] = theme.textColor;
666
+ if (theme.mutedColor) vars[`--ea-muted`] = theme.mutedColor;
667
+ if (theme.borderColor) vars[`--ea-border`] = theme.borderColor;
668
+ if (theme.surfaceColor) vars[`--ea-surface`] = theme.surfaceColor;
669
+ if (theme.errorColor) vars[`--ea-error`] = theme.errorColor;
670
+ if (theme.borderRadius) vars[`--ea-radius`] = theme.borderRadius;
671
+ if (theme.fontFamily) vars[`--ea-font`] = theme.fontFamily;
672
+ if (theme.maxWidth) vars[`--ea-max-width`] = theme.maxWidth;
673
+ return vars;
674
+ }, [
675
+ tenantConfig?.business.brandColor,
676
+ theme.brandColor,
677
+ theme.onBrandColor,
678
+ theme.textColor,
679
+ theme.mutedColor,
680
+ theme.borderColor,
681
+ theme.surfaceColor,
682
+ theme.errorColor,
683
+ theme.borderRadius,
684
+ theme.fontFamily,
685
+ theme.maxWidth
686
+ ]);
687
+ const colorScheme = theme.colorScheme ?? normalizeColorScheme(tenantConfig?.business.theme);
688
+ const rootStyle = useMemo(
689
+ () => ({ ...cssVars, ...style }),
690
+ [cssVars, style]
691
+ );
692
+ const rootClassName = className ? `ea-booking-widget ${className}` : `ea-booking-widget`;
693
+ const value = useMemo(
694
+ () => ({
695
+ client,
696
+ apiUrl: resolvedApiUrl,
697
+ tenantConfig,
698
+ tenantConfigLoading,
699
+ tenantConfigError,
700
+ locale: resolvedLocale,
701
+ theme,
702
+ t,
703
+ bookingState,
704
+ dispatch,
705
+ customerTimezone,
706
+ timeDisplayMode: bookingState.timeDisplayMode,
707
+ setTimeDisplayMode,
708
+ onBooked
709
+ }),
710
+ [
711
+ bookingState,
712
+ client,
713
+ customerTimezone,
714
+ dispatch,
715
+ onBooked,
716
+ setTimeDisplayMode,
717
+ resolvedApiUrl,
718
+ resolvedLocale,
719
+ t,
720
+ tenantConfig,
721
+ tenantConfigError,
722
+ tenantConfigLoading,
723
+ theme
724
+ ]
725
+ );
726
+ return /* @__PURE__ */ jsxs(BookingContext.Provider, { value, children: [
727
+ botProtection ? /* @__PURE__ */ jsx(BotIdClient, { protect: [{ path: `/v1/public/appointments`, method: `POST` }] }) : null,
728
+ /* @__PURE__ */ jsx("div", { className: rootClassName, "data-theme": colorScheme, style: rootStyle, children })
729
+ ] });
730
+ }
731
+
732
+ // src/hooks/use-tenant-config.ts
733
+ function useTenantConfig() {
734
+ const { tenantConfig, tenantConfigLoading, tenantConfigError } = useBookingContext();
735
+ return { tenantConfig, loading: tenantConfigLoading, error: tenantConfigError };
736
+ }
737
+ function useProviders(serviceId) {
738
+ const { client } = useBookingContext();
739
+ const [data, setData] = useState([]);
740
+ const [loading, setLoading] = useState(false);
741
+ const [error, setError] = useState(null);
742
+ const reload = useCallback(async () => {
743
+ if (!serviceId) {
744
+ setData([]);
745
+ return;
746
+ }
747
+ setLoading(true);
748
+ setError(null);
749
+ try {
750
+ const providers = await client.listProviders({ serviceId });
751
+ setData(providers);
752
+ } catch (err) {
753
+ setError(err instanceof Error ? err.message : `Failed to load providers`);
754
+ } finally {
755
+ setLoading(false);
756
+ }
757
+ }, [client, serviceId]);
758
+ useEffect(() => {
759
+ void reload();
760
+ }, [reload]);
761
+ return { providers: data, loading, error, reload };
762
+ }
763
+
764
+ // src/hooks/use-booking-timezones.ts
765
+ function useBookingTimezones(providerId, serviceId) {
766
+ const { customerTimezone, bookingState } = useBookingContext();
767
+ const { tenantConfig } = useTenantConfig();
768
+ const { providers } = useProviders(serviceId);
769
+ const bookingTimeDisplay = tenantConfig?.booking.bookingTimeDisplay;
770
+ const businessTimezone = useMemo(() => {
771
+ if (!tenantConfig) {
772
+ return `UTC`;
773
+ }
774
+ if (providerId === void 0) {
775
+ return tenantConfig.localization.defaultTimezone;
776
+ }
777
+ return resolveBusinessTimezone({
778
+ providerId,
779
+ providers,
780
+ tenantDefaultTimezone: tenantConfig.localization.defaultTimezone
781
+ });
782
+ }, [providerId, providers, tenantConfig]);
783
+ const displayTimezone = useMemo(
784
+ () => resolveDisplayTimezone({
785
+ businessTimezone,
786
+ customerTimezone,
787
+ bookingTimeDisplay,
788
+ timeDisplayMode: bookingState.timeDisplayMode
789
+ }),
790
+ [
791
+ bookingState.timeDisplayMode,
792
+ bookingTimeDisplay,
793
+ businessTimezone,
794
+ customerTimezone
795
+ ]
796
+ );
797
+ const zonesDiffer = customerTimezone !== businessTimezone;
798
+ const secondaryTimezone = useMemo(() => {
799
+ if (!shouldShowSecondaryTimezone({
800
+ zonesDiffer,
801
+ bookingTimeDisplay
802
+ })) {
803
+ return null;
804
+ }
805
+ return displayTimezone === businessTimezone ? customerTimezone : businessTimezone;
806
+ }, [bookingTimeDisplay, businessTimezone, customerTimezone, displayTimezone, zonesDiffer]);
807
+ return {
808
+ businessTimezone,
809
+ customerTimezone,
810
+ displayTimezone,
811
+ secondaryTimezone,
812
+ zonesDiffer,
813
+ bookingTimeDisplay,
814
+ timeDisplayMode: bookingState.timeDisplayMode
815
+ };
816
+ }
817
+ function todayInZone(timeZone) {
818
+ return Temporal.Now.zonedDateTimeISO(timeZone).toPlainDate();
819
+ }
820
+ function minBookableFromToday(today) {
821
+ return today.add({ days: 1 });
822
+ }
823
+ function minBookableDate(timeZone) {
824
+ return minBookableFromToday(todayInZone(timeZone));
825
+ }
826
+ function maxBookableFromToday(today, futureBookingLimitDays) {
827
+ return today.add({ days: futureBookingLimitDays });
828
+ }
829
+ function maxBookableDate(futureBookingLimitDays, timeZone) {
830
+ return maxBookableFromToday(todayInZone(timeZone), futureBookingLimitDays);
831
+ }
832
+ function formatCalendarDate(date) {
833
+ return date.toString();
834
+ }
835
+ function startOfMonth(date) {
836
+ return date.with({ day: 1 });
837
+ }
838
+ function formatMonthKey(date) {
839
+ const y = date.year;
840
+ const m = String(date.month).padStart(2, `0`);
841
+ return `${y}-${m}`;
842
+ }
843
+ function buildMonthDays(month, firstWeekday) {
844
+ const start = startOfMonth(month);
845
+ const jsDay = start.dayOfWeek % 7;
846
+ const offset = firstWeekday === `monday` ? (jsDay + 6) % 7 : firstWeekday === `saturday` ? (jsDay + 1) % 7 : jsDay;
847
+ const cells = Array.from({ length: offset }, () => null);
848
+ let cursor = start;
849
+ while (cursor.month === start.month) {
850
+ cells.push(cursor);
851
+ cursor = cursor.add({ days: 1 });
852
+ }
853
+ while (cells.length % 7 !== 0) {
854
+ cells.push(null);
855
+ }
856
+ return cells;
857
+ }
858
+ function isDateInRange(value, min, max) {
859
+ return value >= min && value <= max;
860
+ }
861
+ function formatSlotTime(iso, locale, timeZone) {
862
+ return new Intl.DateTimeFormat(locale, {
863
+ hour: `numeric`,
864
+ minute: `2-digit`,
865
+ timeZone
866
+ }).format(new Date(iso));
867
+ }
868
+ function formatSlotDate(iso, locale, timeZone) {
869
+ return new Intl.DateTimeFormat(locale, {
870
+ weekday: `long`,
871
+ year: `numeric`,
872
+ month: `long`,
873
+ day: `numeric`,
874
+ timeZone
875
+ }).format(new Date(iso));
876
+ }
877
+ function formatSlotDateTimeDual(iso, locale, primaryTz, secondaryTz) {
878
+ const primary = `${formatSlotDate(iso, locale, primaryTz)} ${formatSlotTime(iso, locale, primaryTz)}`;
879
+ if (!secondaryTz || secondaryTz === primaryTz) {
880
+ return primary;
881
+ }
882
+ return `${primary} (${formatSlotTime(iso, locale, secondaryTz)})`;
883
+ }
884
+ function SlotTimeDisplay({
885
+ iso,
886
+ displayTimezone,
887
+ secondaryTimezone,
888
+ customerTimezone,
889
+ variant = `time`
890
+ }) {
891
+ const { locale, t } = useBookingContext();
892
+ const primary = variant === `datetime` ? `${formatSlotDate(iso, locale, displayTimezone)} ${formatSlotTime(iso, locale, displayTimezone)}` : formatSlotTime(iso, locale, displayTimezone);
893
+ if (!secondaryTimezone) {
894
+ return /* @__PURE__ */ jsx("span", { children: primary });
895
+ }
896
+ const secondaryLabel = secondaryTimezone === customerTimezone ? t(`time.yourTimeLabel`, { timezone: secondaryTimezone }) : t(`time.businessTimeLabel`, { timezone: secondaryTimezone });
897
+ return /* @__PURE__ */ jsxs("span", { className: "ea-slot-time-display", children: [
898
+ /* @__PURE__ */ jsx("span", { children: primary }),
899
+ /* @__PURE__ */ jsxs("span", { className: "ea-time-secondary", children: [
900
+ secondaryLabel,
901
+ ": ",
902
+ formatSlotTime(iso, locale, secondaryTimezone)
903
+ ] })
904
+ ] });
905
+ }
906
+ function TimezoneMismatchBanner({
907
+ businessTimezone,
908
+ customerTimezone,
909
+ timeDisplayMode,
910
+ bookingTimeDisplay
911
+ }) {
912
+ const { t, setTimeDisplayMode } = useBookingContext();
913
+ if (customerTimezone === businessTimezone) {
914
+ return null;
915
+ }
916
+ const showToggle = allowsTimeDisplayToggle(bookingTimeDisplay);
917
+ const nextMode = timeDisplayMode === `business` ? `customer` : `business`;
918
+ return /* @__PURE__ */ jsxs("div", { className: "ea-timezone-banner", role: "note", children: [
919
+ /* @__PURE__ */ jsx("p", { className: "ea-timezone-banner-text", children: t(`time.mismatchBanner`, { businessTimezone, customerTimezone }) }),
920
+ showToggle ? /* @__PURE__ */ jsx(
921
+ "button",
922
+ {
923
+ type: "button",
924
+ className: "ea-time-toggle ea-link",
925
+ onClick: () => setTimeDisplayMode(nextMode),
926
+ children: timeDisplayMode === `business` ? t(`time.showMyTime`) : t(`time.showBusinessTime`)
927
+ }
928
+ ) : null
929
+ ] });
930
+ }
931
+ function StepTitle({
932
+ children,
933
+ id
934
+ }) {
935
+ return /* @__PURE__ */ jsx("h2", { id, className: "ea-step-title", children });
936
+ }
937
+ function Button({
938
+ variant = `primary`,
939
+ ...props
940
+ }) {
941
+ return /* @__PURE__ */ jsx(
942
+ "button",
943
+ {
944
+ type: "button",
945
+ className: variant === `primary` ? `ea-btn ea-btn-primary` : `ea-btn ea-btn-secondary`,
946
+ ...props
947
+ }
948
+ );
949
+ }
950
+ function Field({
951
+ label,
952
+ error,
953
+ children
954
+ }) {
955
+ return /* @__PURE__ */ jsxs("label", { className: "ea-field", children: [
956
+ /* @__PURE__ */ jsx("span", { className: "ea-field-label", children: label }),
957
+ children,
958
+ error ? /* @__PURE__ */ jsx("span", { className: "ea-field-error", children: error }) : null
959
+ ] });
960
+ }
961
+ function TextInput(props) {
962
+ return /* @__PURE__ */ jsx("input", { className: "ea-input", ...props });
963
+ }
964
+ function TextArea(props) {
965
+ return /* @__PURE__ */ jsx("textarea", { className: "ea-input ea-textarea", ...props });
966
+ }
967
+ function LoadingState({ label }) {
968
+ return /* @__PURE__ */ jsx("p", { className: "ea-muted", children: label });
969
+ }
970
+ function ErrorState({ message }) {
971
+ return /* @__PURE__ */ jsx("p", { className: "ea-error", role: "alert", children: message });
972
+ }
973
+ function BookedSummary() {
974
+ const { t } = useBookingContext();
975
+ const { state } = useBooking();
976
+ const appointment = state.step === `booked` ? state.appointment : void 0;
977
+ const {
978
+ businessTimezone,
979
+ customerTimezone,
980
+ displayTimezone,
981
+ secondaryTimezone,
982
+ timeDisplayMode,
983
+ bookingTimeDisplay
984
+ } = useBookingTimezones(appointment?.providerId, appointment?.serviceId);
985
+ if (state.step !== `booked`) {
986
+ return null;
987
+ }
988
+ return /* @__PURE__ */ jsxs("section", { className: "ea-step ea-booked", "aria-labelledby": "ea-step-booked", children: [
989
+ /* @__PURE__ */ jsx(StepTitle, { id: "ea-step-booked", children: t(`steps.booked`) }),
990
+ /* @__PURE__ */ jsx("p", { children: t(`booked.message`) }),
991
+ /* @__PURE__ */ jsxs("p", { className: "ea-booked-ref", children: [
992
+ t(`booked.reference`),
993
+ ": ",
994
+ /* @__PURE__ */ jsx("strong", { children: state.appointment.hash })
995
+ ] }),
996
+ /* @__PURE__ */ jsx(
997
+ TimezoneMismatchBanner,
998
+ {
999
+ businessTimezone,
1000
+ customerTimezone,
1001
+ timeDisplayMode,
1002
+ bookingTimeDisplay
1003
+ }
1004
+ ),
1005
+ /* @__PURE__ */ jsx("p", { className: "ea-muted", children: /* @__PURE__ */ jsx(
1006
+ SlotTimeDisplay,
1007
+ {
1008
+ iso: state.appointment.startAt,
1009
+ displayTimezone,
1010
+ secondaryTimezone,
1011
+ customerTimezone,
1012
+ variant: `datetime`
1013
+ }
1014
+ ) })
1015
+ ] });
1016
+ }
1017
+ function useServices() {
1018
+ const { client } = useBookingContext();
1019
+ const [data, setData] = useState([]);
1020
+ const [loading, setLoading] = useState(true);
1021
+ const [error, setError] = useState(null);
1022
+ const reload = useCallback(async () => {
1023
+ setLoading(true);
1024
+ setError(null);
1025
+ try {
1026
+ const services = await client.listServices();
1027
+ setData(services);
1028
+ } catch (err) {
1029
+ setError(err instanceof Error ? err.message : `Failed to load services`);
1030
+ } finally {
1031
+ setLoading(false);
1032
+ }
1033
+ }, [client]);
1034
+ useEffect(() => {
1035
+ void reload();
1036
+ }, [reload]);
1037
+ return { services: data, loading, error, reload };
1038
+ }
1039
+ async function resolveProviderForAny(client, providerIds, serviceId, date, startAt) {
1040
+ for (const providerId of providerIds) {
1041
+ const slots = await client.getAvailability({ providerId, serviceId, date });
1042
+ if (slots.some((slot) => slot.startAt === startAt)) {
1043
+ return providerId;
1044
+ }
1045
+ }
1046
+ return null;
1047
+ }
1048
+ function ConfirmStep() {
1049
+ const { t, dispatch, client, onBooked } = useBookingContext();
1050
+ const { state } = useBooking();
1051
+ const { tenantConfig } = useTenantConfig();
1052
+ const { services } = useServices();
1053
+ const serviceId = state.step === `confirm` ? state.serviceId : void 0;
1054
+ const { providers } = useProviders(serviceId);
1055
+ const providerId = state.step === `confirm` ? state.providerId : void 0;
1056
+ const {
1057
+ businessTimezone,
1058
+ customerTimezone,
1059
+ displayTimezone,
1060
+ secondaryTimezone,
1061
+ timeDisplayMode,
1062
+ bookingTimeDisplay
1063
+ } = useBookingTimezones(providerId, serviceId);
1064
+ const [submitting, setSubmitting] = useState(false);
1065
+ const [error, setError] = useState(null);
1066
+ if (state.step !== `confirm` || !tenantConfig) {
1067
+ return null;
1068
+ }
1069
+ const confirm = state;
1070
+ const service = services.find((s) => s.id === confirm.serviceId);
1071
+ const provider = confirm.providerId === ANY_PROVIDER_ID ? null : providers.find((p) => p.id === confirm.providerId);
1072
+ async function handleConfirm() {
1073
+ setSubmitting(true);
1074
+ setError(null);
1075
+ try {
1076
+ let providerId2;
1077
+ if (confirm.providerId === ANY_PROVIDER_ID) {
1078
+ const eligible = providers.filter((p) => service?.providerIds.includes(p.id) ?? true).map((p) => p.id);
1079
+ const resolved = await resolveProviderForAny(
1080
+ client,
1081
+ eligible,
1082
+ confirm.serviceId,
1083
+ confirm.date,
1084
+ confirm.slot.startAt
1085
+ );
1086
+ if (!resolved) {
1087
+ setError(t(`confirm.conflict`));
1088
+ return;
1089
+ }
1090
+ providerId2 = resolved;
1091
+ } else {
1092
+ providerId2 = confirm.providerId;
1093
+ }
1094
+ const appointment = await client.createAppointment({
1095
+ providerId: providerId2,
1096
+ serviceId: confirm.serviceId,
1097
+ startAt: confirm.slot.startAt,
1098
+ ...customerValuesToAppointmentBody(confirm.customer),
1099
+ consents: confirm.consents
1100
+ });
1101
+ dispatch({ type: `booked`, appointment });
1102
+ onBooked?.(appointment);
1103
+ } catch (err) {
1104
+ if (err instanceof BookingApiError && err.status === 409) {
1105
+ setError(t(`confirm.conflict`));
1106
+ } else {
1107
+ setError(t(`confirm.error`));
1108
+ }
1109
+ } finally {
1110
+ setSubmitting(false);
1111
+ }
1112
+ }
1113
+ return /* @__PURE__ */ jsxs("section", { className: "ea-step", "aria-labelledby": "ea-step-confirm", children: [
1114
+ /* @__PURE__ */ jsx(StepTitle, { id: "ea-step-confirm", children: t(`steps.confirm`) }),
1115
+ /* @__PURE__ */ jsx("p", { className: "ea-muted", children: t(`confirm.summary`) }),
1116
+ /* @__PURE__ */ jsx(
1117
+ TimezoneMismatchBanner,
1118
+ {
1119
+ businessTimezone,
1120
+ customerTimezone,
1121
+ timeDisplayMode,
1122
+ bookingTimeDisplay
1123
+ }
1124
+ ),
1125
+ /* @__PURE__ */ jsxs("dl", { className: "ea-summary", children: [
1126
+ /* @__PURE__ */ jsxs("div", { children: [
1127
+ /* @__PURE__ */ jsx("dt", { children: t(`confirm.service`) }),
1128
+ /* @__PURE__ */ jsx("dd", { children: service?.name ?? confirm.serviceId })
1129
+ ] }),
1130
+ /* @__PURE__ */ jsxs("div", { children: [
1131
+ /* @__PURE__ */ jsx("dt", { children: t(`confirm.provider`) }),
1132
+ /* @__PURE__ */ jsx("dd", { children: confirm.providerId === ANY_PROVIDER_ID ? t(`provider.any`) : provider?.displayName ?? confirm.providerId })
1133
+ ] }),
1134
+ /* @__PURE__ */ jsxs("div", { children: [
1135
+ /* @__PURE__ */ jsx("dt", { children: t(`confirm.when`) }),
1136
+ /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx(
1137
+ SlotTimeDisplay,
1138
+ {
1139
+ iso: confirm.slot.startAt,
1140
+ displayTimezone,
1141
+ secondaryTimezone,
1142
+ customerTimezone,
1143
+ variant: `datetime`
1144
+ }
1145
+ ) })
1146
+ ] }),
1147
+ /* @__PURE__ */ jsxs("div", { children: [
1148
+ /* @__PURE__ */ jsx("dt", { children: t(`confirm.customer`) }),
1149
+ /* @__PURE__ */ jsxs("dd", { children: [
1150
+ confirm.customer.firstName,
1151
+ " ",
1152
+ confirm.customer.lastName,
1153
+ " \u2014 ",
1154
+ confirm.customer.email
1155
+ ] })
1156
+ ] })
1157
+ ] }),
1158
+ error ? /* @__PURE__ */ jsx(ErrorState, { message: error }) : null,
1159
+ /* @__PURE__ */ jsxs("div", { className: "ea-step-actions", children: [
1160
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => dispatch({ type: `back` }), disabled: submitting, children: t(`actions.back`) }),
1161
+ /* @__PURE__ */ jsx(Button, { onClick: () => void handleConfirm(), disabled: submitting, children: submitting ? t(`actions.submitting`) : t(`actions.confirm`) })
1162
+ ] })
1163
+ ] });
1164
+ }
1165
+ function CustomerForm() {
1166
+ const { t, dispatch } = useBookingContext();
1167
+ const { state } = useBooking();
1168
+ const { tenantConfig } = useTenantConfig();
1169
+ const [values, setValues] = useState(emptyCustomerValues);
1170
+ const [errors, setErrors] = useState({});
1171
+ if (state.step !== `customer` || !tenantConfig) {
1172
+ return null;
1173
+ }
1174
+ const fields = visibleFormFields(tenantConfig.formFields);
1175
+ function setField(fieldKey, value) {
1176
+ const key = customerValueKey(fieldKey);
1177
+ if (!key) {
1178
+ return;
1179
+ }
1180
+ setValues((current) => ({ ...current, [key]: value }));
1181
+ }
1182
+ function handleSubmit() {
1183
+ const nextErrors = validateCustomerFields(tenantConfig.formFields, values);
1184
+ setErrors(nextErrors);
1185
+ if (Object.keys(nextErrors).length > 0) {
1186
+ return;
1187
+ }
1188
+ dispatch({ type: `set_customer`, customer: values });
1189
+ }
1190
+ return /* @__PURE__ */ jsxs("section", { className: "ea-step", "aria-labelledby": "ea-step-customer", children: [
1191
+ /* @__PURE__ */ jsx(StepTitle, { id: "ea-step-customer", children: t(`steps.customer`) }),
1192
+ /* @__PURE__ */ jsxs(
1193
+ "form",
1194
+ {
1195
+ className: "ea-form",
1196
+ onSubmit: (event) => {
1197
+ event.preventDefault();
1198
+ handleSubmit();
1199
+ },
1200
+ children: [
1201
+ fields.map((field) => {
1202
+ const valueKey = customerValueKey(field.fieldKey);
1203
+ if (!valueKey) {
1204
+ return null;
1205
+ }
1206
+ const label = field.label ?? (fieldLabelKey(field.fieldKey) ? t(fieldLabelKey(field.fieldKey)) : field.fieldKey);
1207
+ const value = values[valueKey];
1208
+ const error = errors[field.fieldKey];
1209
+ const isNotes = field.fieldKey === `notes`;
1210
+ return /* @__PURE__ */ jsx(
1211
+ Field,
1212
+ {
1213
+ label: `${label}${field.required ? ` *` : ``}`,
1214
+ error: error === `required` ? t(`customer.required`) : error === `invalid` ? t(`customer.invalidEmail`) : void 0,
1215
+ children: isNotes ? /* @__PURE__ */ jsx(
1216
+ TextArea,
1217
+ {
1218
+ value,
1219
+ onChange: (e) => setField(field.fieldKey, e.target.value),
1220
+ rows: 3
1221
+ }
1222
+ ) : /* @__PURE__ */ jsx(
1223
+ TextInput,
1224
+ {
1225
+ type: field.fieldKey === `email` ? `email` : `text`,
1226
+ value,
1227
+ onChange: (e) => setField(field.fieldKey, e.target.value),
1228
+ autoComplete: field.fieldKey === `email` ? `email` : void 0
1229
+ }
1230
+ )
1231
+ },
1232
+ field.fieldKey
1233
+ );
1234
+ }),
1235
+ /* @__PURE__ */ jsxs("div", { className: "ea-step-actions", children: [
1236
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", type: "button", onClick: () => dispatch({ type: `back` }), children: t(`actions.back`) }),
1237
+ /* @__PURE__ */ jsx(Button, { type: "submit", children: t(`actions.continue`) })
1238
+ ] })
1239
+ ]
1240
+ }
1241
+ )
1242
+ ] });
1243
+ }
1244
+ function useUnavailableDates(params) {
1245
+ const { client } = useBookingContext();
1246
+ const [dates, setDates] = useState([]);
1247
+ const [loading, setLoading] = useState(false);
1248
+ const [error, setError] = useState(null);
1249
+ const reload = useCallback(async () => {
1250
+ if (!params.serviceId || !params.month) {
1251
+ setDates([]);
1252
+ return;
1253
+ }
1254
+ setLoading(true);
1255
+ setError(null);
1256
+ try {
1257
+ const providerIds = params.providerId === ANY_PROVIDER_ID ? params.providerIdsForAny ?? [] : params.providerId ? [params.providerId] : [];
1258
+ if (providerIds.length === 0) {
1259
+ setDates([]);
1260
+ return;
1261
+ }
1262
+ const lists = await Promise.all(
1263
+ providerIds.map(
1264
+ (providerId) => client.getUnavailableDates({
1265
+ providerId,
1266
+ serviceId: params.serviceId,
1267
+ month: params.month
1268
+ })
1269
+ )
1270
+ );
1271
+ const merged = /* @__PURE__ */ new Set();
1272
+ for (const list of lists) {
1273
+ for (const date of list) {
1274
+ merged.add(date);
1275
+ }
1276
+ }
1277
+ setDates([...merged]);
1278
+ } catch (err) {
1279
+ setError(err instanceof Error ? err.message : `Failed to load unavailable dates`);
1280
+ } finally {
1281
+ setLoading(false);
1282
+ }
1283
+ }, [client, params.month, params.providerId, params.providerIdsForAny, params.serviceId]);
1284
+ useEffect(() => {
1285
+ void reload();
1286
+ }, [reload]);
1287
+ return { unavailableDates: dates, loading, error, reload };
1288
+ }
1289
+ function DatePicker() {
1290
+ const { t, dispatch } = useBookingContext();
1291
+ const { state } = useBooking();
1292
+ const { tenantConfig } = useTenantConfig();
1293
+ const [viewMonth, setViewMonth] = useState(() => startOfMonth(minBookableDate(`UTC`)));
1294
+ const serviceId = state.step === `date` ? state.serviceId : void 0;
1295
+ const providerId = state.step === `date` ? state.providerId : void 0;
1296
+ const { providers } = useProviders(serviceId);
1297
+ const providerIdsForAny = useMemo(
1298
+ () => providers.map((p) => p.id),
1299
+ [providers]
1300
+ );
1301
+ const { businessTimezone, customerTimezone, timeDisplayMode, bookingTimeDisplay } = useBookingTimezones(providerId, serviceId);
1302
+ const month = formatMonthKey(viewMonth);
1303
+ const { unavailableDates, loading, error } = useUnavailableDates({
1304
+ serviceId,
1305
+ providerId,
1306
+ month,
1307
+ providerIdsForAny
1308
+ });
1309
+ if (state.step !== `date` || !tenantConfig) {
1310
+ return null;
1311
+ }
1312
+ const minDate = formatCalendarDate(minBookableDate(businessTimezone));
1313
+ const maxDate = formatCalendarDate(
1314
+ maxBookableDate(tenantConfig.booking.futureBookingLimitDays, businessTimezone)
1315
+ );
1316
+ const unavailable = new Set(unavailableDates);
1317
+ const days = buildMonthDays(viewMonth, tenantConfig.localization.firstWeekday);
1318
+ return /* @__PURE__ */ jsxs("section", { className: "ea-step", "aria-labelledby": "ea-step-date", children: [
1319
+ /* @__PURE__ */ jsx(StepTitle, { id: "ea-step-date", children: t(`steps.date`) }),
1320
+ /* @__PURE__ */ jsx(
1321
+ TimezoneMismatchBanner,
1322
+ {
1323
+ businessTimezone,
1324
+ customerTimezone,
1325
+ timeDisplayMode,
1326
+ bookingTimeDisplay
1327
+ }
1328
+ ),
1329
+ /* @__PURE__ */ jsx("p", { className: "ea-muted", children: t(`date.minHint`) }),
1330
+ /* @__PURE__ */ jsxs("div", { className: "ea-calendar-nav", children: [
1331
+ /* @__PURE__ */ jsx(
1332
+ Button,
1333
+ {
1334
+ variant: "secondary",
1335
+ onClick: () => setViewMonth((m) => startOfMonth(m.subtract({ months: 1 }))),
1336
+ "aria-label": "Previous month",
1337
+ children: "\u2039"
1338
+ }
1339
+ ),
1340
+ /* @__PURE__ */ jsx("span", { className: "ea-calendar-month", children: month }),
1341
+ /* @__PURE__ */ jsx(
1342
+ Button,
1343
+ {
1344
+ variant: "secondary",
1345
+ onClick: () => setViewMonth((m) => startOfMonth(m.add({ months: 1 }))),
1346
+ "aria-label": "Next month",
1347
+ children: "\u203A"
1348
+ }
1349
+ )
1350
+ ] }),
1351
+ loading ? /* @__PURE__ */ jsx(LoadingState, { label: "\u2026" }) : null,
1352
+ error ? /* @__PURE__ */ jsx(ErrorState, { message: error }) : null,
1353
+ /* @__PURE__ */ jsx("div", { className: "ea-calendar-grid", role: "grid", children: days.map((day, index) => {
1354
+ if (!day) {
1355
+ return /* @__PURE__ */ jsx("span", { className: "ea-calendar-pad" }, `pad-${index}`);
1356
+ }
1357
+ const value = formatCalendarDate(day);
1358
+ const inRange = isDateInRange(value, minDate, maxDate);
1359
+ const isUnavailable = unavailable.has(value) || !inRange;
1360
+ return /* @__PURE__ */ jsx(
1361
+ "button",
1362
+ {
1363
+ type: "button",
1364
+ className: isUnavailable ? `ea-calendar-day ea-calendar-day-disabled` : `ea-calendar-day`,
1365
+ disabled: isUnavailable,
1366
+ onClick: () => dispatch({ type: `select_date`, date: value }),
1367
+ "aria-label": value,
1368
+ children: day.day
1369
+ },
1370
+ value
1371
+ );
1372
+ }) }),
1373
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => dispatch({ type: `back` }), children: t(`actions.back`) })
1374
+ ] });
1375
+ }
1376
+ function DisabledBooking() {
1377
+ const { t } = useBookingContext();
1378
+ const { tenantConfig } = useTenantConfig();
1379
+ if (!tenantConfig?.booking.disableBooking) {
1380
+ return null;
1381
+ }
1382
+ return /* @__PURE__ */ jsxs("section", { className: "ea-step ea-disabled", "aria-labelledby": "ea-disabled-title", children: [
1383
+ /* @__PURE__ */ jsx(StepTitle, { id: "ea-disabled-title", children: t(`disabled.title`) }),
1384
+ /* @__PURE__ */ jsx("p", { children: tenantConfig.booking.disableBookingMessage ?? t(`disabled.title`) })
1385
+ ] });
1386
+ }
1387
+ function LegalGate() {
1388
+ const { t, dispatch } = useBookingContext();
1389
+ const { state } = useBooking();
1390
+ const { tenantConfig } = useTenantConfig();
1391
+ const [consents, setConsents] = useState({});
1392
+ const [activeModal, setActiveModal] = useState(null);
1393
+ if (state.step !== `legal` || !tenantConfig) {
1394
+ return null;
1395
+ }
1396
+ const { legal } = tenantConfig;
1397
+ function toggle(key) {
1398
+ setConsents((current) => ({ ...current, [key]: !current[key] }));
1399
+ }
1400
+ function handleContinue() {
1401
+ dispatch({ type: `set_consents`, consents });
1402
+ }
1403
+ const modalContent = activeModal === `cookie` ? legal.cookieNoticeContent : activeModal === `terms` ? legal.termsContent : activeModal === `privacy` ? legal.privacyContent : null;
1404
+ return /* @__PURE__ */ jsxs("section", { className: "ea-step", "aria-labelledby": "ea-step-legal", children: [
1405
+ /* @__PURE__ */ jsx(StepTitle, { id: "ea-step-legal", children: t(`steps.legal`) }),
1406
+ /* @__PURE__ */ jsxs("div", { className: "ea-legal-list", children: [
1407
+ legal.displayCookieNotice ? /* @__PURE__ */ jsxs(Field, { label: "", children: [
1408
+ /* @__PURE__ */ jsxs("label", { className: "ea-checkbox", children: [
1409
+ /* @__PURE__ */ jsx(
1410
+ "input",
1411
+ {
1412
+ type: "checkbox",
1413
+ checked: consents.cookie === true,
1414
+ onChange: () => toggle(`cookie`)
1415
+ }
1416
+ ),
1417
+ /* @__PURE__ */ jsx("span", { children: t(`legal.acceptCookie`) })
1418
+ ] }),
1419
+ legal.cookieNoticeContent ? /* @__PURE__ */ jsx("button", { type: "button", className: "ea-link", onClick: () => setActiveModal(`cookie`), children: t(`legal.readMore`) }) : null
1420
+ ] }) : null,
1421
+ legal.displayTerms ? /* @__PURE__ */ jsxs(Field, { label: "", children: [
1422
+ /* @__PURE__ */ jsxs("label", { className: "ea-checkbox", children: [
1423
+ /* @__PURE__ */ jsx(
1424
+ "input",
1425
+ {
1426
+ type: "checkbox",
1427
+ checked: consents.terms === true,
1428
+ onChange: () => toggle(`terms`)
1429
+ }
1430
+ ),
1431
+ /* @__PURE__ */ jsx("span", { children: t(`legal.acceptTerms`) })
1432
+ ] }),
1433
+ legal.termsContent ? /* @__PURE__ */ jsx("button", { type: "button", className: "ea-link", onClick: () => setActiveModal(`terms`), children: t(`legal.readMore`) }) : null
1434
+ ] }) : null,
1435
+ legal.displayPrivacy ? /* @__PURE__ */ jsxs(Field, { label: "", children: [
1436
+ /* @__PURE__ */ jsxs("label", { className: "ea-checkbox", children: [
1437
+ /* @__PURE__ */ jsx(
1438
+ "input",
1439
+ {
1440
+ type: "checkbox",
1441
+ checked: consents.privacy === true,
1442
+ onChange: () => toggle(`privacy`)
1443
+ }
1444
+ ),
1445
+ /* @__PURE__ */ jsx("span", { children: t(`legal.acceptPrivacy`) })
1446
+ ] }),
1447
+ legal.privacyContent ? /* @__PURE__ */ jsx("button", { type: "button", className: "ea-link", onClick: () => setActiveModal(`privacy`), children: t(`legal.readMore`) }) : null
1448
+ ] }) : null
1449
+ ] }),
1450
+ activeModal && modalContent ? /* @__PURE__ */ jsxs("dialog", { open: true, className: "ea-modal", children: [
1451
+ /* @__PURE__ */ jsx("div", { className: "ea-modal-body", children: modalContent }),
1452
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => setActiveModal(null), children: t(`actions.back`) })
1453
+ ] }) : null,
1454
+ /* @__PURE__ */ jsxs("div", { className: "ea-step-actions", children: [
1455
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => dispatch({ type: `back` }), children: t(`actions.back`) }),
1456
+ /* @__PURE__ */ jsx(Button, { onClick: handleContinue, children: t(`actions.continue`) })
1457
+ ] })
1458
+ ] });
1459
+ }
1460
+ function ProviderPicker() {
1461
+ const { t, dispatch } = useBookingContext();
1462
+ const { state } = useBooking();
1463
+ const { tenantConfig } = useTenantConfig();
1464
+ const serviceId = state.step === `provider` ? state.serviceId : void 0;
1465
+ const { providers, loading, error } = useProviders(serviceId);
1466
+ if (state.step !== `provider`) {
1467
+ return null;
1468
+ }
1469
+ const showAny = tenantConfig?.booking.displayAnyProvider ?? false;
1470
+ return /* @__PURE__ */ jsxs("section", { className: "ea-step", "aria-labelledby": "ea-step-provider", children: [
1471
+ /* @__PURE__ */ jsx(StepTitle, { id: "ea-step-provider", children: t(`steps.provider`) }),
1472
+ loading ? /* @__PURE__ */ jsx(LoadingState, { label: "\u2026" }) : null,
1473
+ error ? /* @__PURE__ */ jsx(ErrorState, { message: error }) : null,
1474
+ /* @__PURE__ */ jsxs("ul", { className: "ea-card-list", children: [
1475
+ showAny ? /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
1476
+ "button",
1477
+ {
1478
+ type: "button",
1479
+ className: "ea-card",
1480
+ onClick: () => dispatch({ type: `select_provider`, providerId: ANY_PROVIDER_ID }),
1481
+ children: /* @__PURE__ */ jsx("span", { className: "ea-card-title", children: t(`provider.any`) })
1482
+ }
1483
+ ) }) : null,
1484
+ providers.map((provider) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
1485
+ "button",
1486
+ {
1487
+ type: "button",
1488
+ className: "ea-card",
1489
+ onClick: () => dispatch({ type: `select_provider`, providerId: provider.id }),
1490
+ children: /* @__PURE__ */ jsx("span", { className: "ea-card-title", children: provider.displayName })
1491
+ }
1492
+ ) }, provider.id))
1493
+ ] }),
1494
+ !loading && providers.length === 0 && !showAny ? /* @__PURE__ */ jsx("p", { className: "ea-muted", children: t(`provider.empty`) }) : null,
1495
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => dispatch({ type: `back` }), children: t(`actions.back`) })
1496
+ ] });
1497
+ }
1498
+ function ServicePicker() {
1499
+ const { t, dispatch } = useBookingContext();
1500
+ const { state } = useBooking();
1501
+ const { services, loading, error } = useServices();
1502
+ if (state.step !== `service`) {
1503
+ return null;
1504
+ }
1505
+ return /* @__PURE__ */ jsxs("section", { className: "ea-step", "aria-labelledby": "ea-step-service", children: [
1506
+ /* @__PURE__ */ jsx(StepTitle, { id: "ea-step-service", children: t(`steps.service`) }),
1507
+ loading ? /* @__PURE__ */ jsx(LoadingState, { label: "\u2026" }) : null,
1508
+ error ? /* @__PURE__ */ jsx(ErrorState, { message: error }) : null,
1509
+ !loading && !error && services.length === 0 ? /* @__PURE__ */ jsx("p", { className: "ea-muted", children: t(`service.empty`) }) : null,
1510
+ /* @__PURE__ */ jsx("ul", { className: "ea-card-list", children: services.map((service) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
1511
+ "button",
1512
+ {
1513
+ type: "button",
1514
+ className: "ea-card",
1515
+ onClick: () => dispatch({ type: `select_service`, serviceId: service.id }),
1516
+ children: [
1517
+ /* @__PURE__ */ jsx("span", { className: "ea-card-title", children: service.name }),
1518
+ service.description ? /* @__PURE__ */ jsx("span", { className: "ea-card-desc", children: service.description }) : null,
1519
+ /* @__PURE__ */ jsx("span", { className: "ea-card-meta", children: t(`service.durationMinutes`, { minutes: service.durationMinutes }) })
1520
+ ]
1521
+ }
1522
+ ) }, service.id)) }),
1523
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", disabled: true, children: t(`actions.back`) })
1524
+ ] });
1525
+ }
1526
+ function useAvailability(params) {
1527
+ const { client } = useBookingContext();
1528
+ const [slots, setSlots] = useState([]);
1529
+ const [loading, setLoading] = useState(false);
1530
+ const [error, setError] = useState(null);
1531
+ const reload = useCallback(async () => {
1532
+ if (!params.serviceId || !params.date) {
1533
+ setSlots([]);
1534
+ return;
1535
+ }
1536
+ setLoading(true);
1537
+ setError(null);
1538
+ try {
1539
+ if (params.providerId && params.providerId !== ANY_PROVIDER_ID) {
1540
+ const data = await client.getAvailability({
1541
+ providerId: params.providerId,
1542
+ serviceId: params.serviceId,
1543
+ date: params.date
1544
+ });
1545
+ setSlots(data);
1546
+ return;
1547
+ }
1548
+ const ids = params.providerIdsForAny ?? [];
1549
+ if (ids.length === 0) {
1550
+ setSlots([]);
1551
+ return;
1552
+ }
1553
+ const results = await Promise.all(
1554
+ ids.map(
1555
+ (providerId) => client.getAvailability({
1556
+ providerId,
1557
+ serviceId: params.serviceId,
1558
+ date: params.date
1559
+ })
1560
+ )
1561
+ );
1562
+ const merged = /* @__PURE__ */ new Map();
1563
+ for (const list of results) {
1564
+ for (const slot of list) {
1565
+ merged.set(slot.startAt, slot);
1566
+ }
1567
+ }
1568
+ setSlots(
1569
+ [...merged.values()].sort((a, b) => a.startAt.localeCompare(b.startAt))
1570
+ );
1571
+ } catch (err) {
1572
+ setError(err instanceof Error ? err.message : `Failed to load availability`);
1573
+ } finally {
1574
+ setLoading(false);
1575
+ }
1576
+ }, [client, params.date, params.providerId, params.providerIdsForAny, params.serviceId]);
1577
+ useEffect(() => {
1578
+ void reload();
1579
+ }, [reload]);
1580
+ return { slots, loading, error, reload };
1581
+ }
1582
+ function TimeSlotPicker() {
1583
+ const { t, dispatch } = useBookingContext();
1584
+ const { state } = useBooking();
1585
+ const serviceId = state.step === `time` ? state.serviceId : void 0;
1586
+ const providerId = state.step === `time` ? state.providerId : void 0;
1587
+ const date = state.step === `time` ? state.date : void 0;
1588
+ const { providers } = useProviders(serviceId);
1589
+ const providerIdsForAny = useMemo(() => providers.map((p) => p.id), [providers]);
1590
+ const {
1591
+ businessTimezone,
1592
+ customerTimezone,
1593
+ displayTimezone,
1594
+ secondaryTimezone,
1595
+ timeDisplayMode,
1596
+ bookingTimeDisplay
1597
+ } = useBookingTimezones(providerId, serviceId);
1598
+ const { slots, loading, error } = useAvailability({
1599
+ serviceId,
1600
+ providerId,
1601
+ date,
1602
+ providerIdsForAny
1603
+ });
1604
+ const [selectedStartAt, setSelectedStartAt] = useState(null);
1605
+ useEffect(() => {
1606
+ if (slots.length > 0) {
1607
+ setSelectedStartAt(slots[0].startAt);
1608
+ } else {
1609
+ setSelectedStartAt(null);
1610
+ }
1611
+ }, [slots]);
1612
+ if (state.step !== `time`) {
1613
+ return null;
1614
+ }
1615
+ const selected = slots.find((s) => s.startAt === selectedStartAt) ?? null;
1616
+ return /* @__PURE__ */ jsxs("section", { className: "ea-step", "aria-labelledby": "ea-step-time", children: [
1617
+ /* @__PURE__ */ jsx(StepTitle, { id: "ea-step-time", children: t(`steps.time`) }),
1618
+ /* @__PURE__ */ jsx(
1619
+ TimezoneMismatchBanner,
1620
+ {
1621
+ businessTimezone,
1622
+ customerTimezone,
1623
+ timeDisplayMode,
1624
+ bookingTimeDisplay
1625
+ }
1626
+ ),
1627
+ /* @__PURE__ */ jsx("p", { className: "ea-muted", children: t(`time.timezoneHint`, { timezone: displayTimezone }) }),
1628
+ loading ? /* @__PURE__ */ jsx(LoadingState, { label: "\u2026" }) : null,
1629
+ error ? /* @__PURE__ */ jsx(ErrorState, { message: error }) : null,
1630
+ !loading && slots.length === 0 ? /* @__PURE__ */ jsx("p", { className: "ea-muted", children: t(`time.empty`) }) : null,
1631
+ /* @__PURE__ */ jsx("ul", { className: "ea-slot-list", children: slots.map((slot) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
1632
+ "button",
1633
+ {
1634
+ type: "button",
1635
+ className: slot.startAt === selectedStartAt ? `ea-slot ea-slot-selected` : `ea-slot`,
1636
+ onClick: () => setSelectedStartAt(slot.startAt),
1637
+ children: /* @__PURE__ */ jsx(
1638
+ SlotTimeDisplay,
1639
+ {
1640
+ iso: slot.startAt,
1641
+ displayTimezone,
1642
+ secondaryTimezone,
1643
+ customerTimezone
1644
+ }
1645
+ )
1646
+ }
1647
+ ) }, slot.startAt)) }),
1648
+ /* @__PURE__ */ jsxs("div", { className: "ea-step-actions", children: [
1649
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => dispatch({ type: `back` }), children: t(`actions.back`) }),
1650
+ /* @__PURE__ */ jsx(
1651
+ Button,
1652
+ {
1653
+ disabled: !selected,
1654
+ onClick: () => selected && dispatch({ type: `select_slot`, slot: selected }),
1655
+ children: t(`actions.continue`)
1656
+ }
1657
+ )
1658
+ ] })
1659
+ ] });
1660
+ }
1661
+ function BookingWidget() {
1662
+ const { tenantConfig, tenantConfigLoading, tenantConfigError } = useBookingContext();
1663
+ if (tenantConfigLoading) {
1664
+ return /* @__PURE__ */ jsx("div", { className: "ea-booking-widget", children: /* @__PURE__ */ jsx(LoadingState, { label: "\u2026" }) });
1665
+ }
1666
+ if (tenantConfigError) {
1667
+ return /* @__PURE__ */ jsx("div", { className: "ea-booking-widget", children: /* @__PURE__ */ jsx(ErrorState, { message: tenantConfigError }) });
1668
+ }
1669
+ if (tenantConfig?.booking.disableBooking) {
1670
+ return /* @__PURE__ */ jsx("div", { className: "ea-booking-widget", children: /* @__PURE__ */ jsx(DisabledBooking, {}) });
1671
+ }
1672
+ return /* @__PURE__ */ jsx("div", { className: "ea-booking-widget", children: /* @__PURE__ */ jsxs("div", { className: "ea-booking-flow", children: [
1673
+ /* @__PURE__ */ jsx(ServicePicker, {}),
1674
+ /* @__PURE__ */ jsx(ProviderPicker, {}),
1675
+ /* @__PURE__ */ jsx(DatePicker, {}),
1676
+ /* @__PURE__ */ jsx(TimeSlotPicker, {}),
1677
+ /* @__PURE__ */ jsx(CustomerForm, {}),
1678
+ /* @__PURE__ */ jsx(LegalGate, {}),
1679
+ /* @__PURE__ */ jsx(ConfirmStep, {}),
1680
+ /* @__PURE__ */ jsx(BookedSummary, {})
1681
+ ] }) });
1682
+ }
1683
+
1684
+ export { ANY_PROVIDER_ID, BookedSummary, BookingApiError, BookingProvider, BookingWidget, ConfirmStep, CustomerForm, DatePicker, DisabledBooking as DisableBookingMessage, DisabledBooking, LegalGate, ProviderPicker, ServicePicker, TimeSlotPicker, allowsTimeDisplayToggle, bookingReducer, createBookingClient, createInitialBookingState, createTranslator, enCatalog, formatCalendarDate, formatSlotDateTimeDual, getCustomerTimezone, maxBookableDate, minBookableDate, resolveBusinessTimezone, resolveDisplayTimezone, shouldShowSecondaryTimezone, useAvailability, useBooking, useProviders, useServices, useTenantConfig, useUnavailableDates };
1685
+ //# sourceMappingURL=index.js.map
1686
+ //# sourceMappingURL=index.js.map