@aglyn/plugins-bookings 1.0.0-beta.221 → 1.0.0-beta.223
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 +3 -2
- package/package.json +11 -11
- package/src/lib/components/book-meeting-action.js +5 -3
- package/src/lib/components/book-meeting-action.js.map +1 -1
- package/src/lib/components/booking.d.ts +33 -0
- package/src/lib/components/booking.js +260 -54
- package/src/lib/components/booking.js.map +1 -1
- package/src/lib/components/bookings-console-page.js +139 -15
- package/src/lib/components/bookings-console-page.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +34 -0
- package/src/lib/declarations.console-server.js +84 -0
- package/src/lib/declarations.console-server.js.map +1 -0
- package/src/lib/model/booking-contact-fields.d.ts +88 -0
- package/src/lib/model/booking-contact-fields.js +130 -0
- package/src/lib/model/booking-contact-fields.js.map +1 -0
- package/src/lib/model/booking-price.d.ts +51 -0
- package/src/lib/model/booking-price.js +61 -0
- package/src/lib/model/booking-price.js.map +1 -0
- package/src/lib/model/booking-record.d.ts +7 -0
- package/src/lib/model/booking-record.js +11 -2
- package/src/lib/model/booking-record.js.map +1 -1
- package/src/lib/model/bookings-record-counts.d.ts +25 -0
- package/src/lib/model/bookings-record-counts.js +34 -0
- package/src/lib/model/bookings-record-counts.js.map +1 -0
- package/src/lib/model/bookings.d.ts +61 -4
- package/src/lib/model/bookings.js +81 -15
- package/src/lib/model/bookings.js.map +1 -1
- package/src/lib/model/index.d.ts +2 -0
- package/src/lib/model/index.js +2 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/plugin.js +14 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/billing-webhook.js +33 -15
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/booking-crm.js +4 -1
- package/src/lib/server/booking-crm.js.map +1 -1
- package/src/lib/server/person-eraser.d.ts +36 -0
- package/src/lib/server/person-eraser.js +48 -0
- package/src/lib/server/person-eraser.js.map +1 -0
- package/src/lib/server/refund.js +2 -2
- package/src/lib/server/refund.js.map +1 -1
- package/src/lib/server.js +97 -36
- package/src/lib/server.js.map +1 -1
- package/src/lib/transfer/bookings-transfer-common.d.ts +68 -0
- package/src/lib/transfer/bookings-transfer-common.js +130 -0
- package/src/lib/transfer/bookings-transfer-common.js.map +1 -0
- package/src/lib/transfer/bookings-transfer.d.ts +86 -0
- package/src/lib/transfer/bookings-transfer.js +613 -0
- package/src/lib/transfer/bookings-transfer.js.map +1 -0
- package/src/lib/transfer/bookings-transfer.server.d.ts +26 -0
- package/src/lib/transfer/bookings-transfer.server.js +31 -0
- package/src/lib/transfer/bookings-transfer.server.js.map +1 -0
|
@@ -31,9 +31,12 @@ import MenuItem from "@mui/material/MenuItem";
|
|
|
31
31
|
import Stack from "@mui/material/Stack";
|
|
32
32
|
import TextField from "@mui/material/TextField";
|
|
33
33
|
import Typography from "@mui/material/Typography";
|
|
34
|
-
import { forwardRef, useCallback, useEffect, useMemo, useState } from "react";
|
|
34
|
+
import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
35
35
|
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
36
36
|
import { BOOKING_RECORD_PARAM, BOOKING_SERVICE_PARAM, formatBookingRecordRef, parseBookingRecordRef } from "../model/booking-record.js";
|
|
37
|
+
import { bookingContactAsks, readBookingContactFields, readBookingPhone } from "../model/booking-contact-fields.js";
|
|
38
|
+
import { bookingPriceDisplay, bookingPriceText } from "../model/booking-price.js";
|
|
39
|
+
import { BOOKING_MAX_DAYS_AHEAD, BOOKING_SLOT_PAGE_DAYS } from "../model/bookings.js";
|
|
37
40
|
import { generatePresetId } from "../utils/generate-preset-id.js";
|
|
38
41
|
import { useBookingPurchaseEvent } from "../utils/use-booking-purchase-event.js";
|
|
39
42
|
// Component ids are persisted in screen documents; never rename.
|
|
@@ -50,13 +53,81 @@ export const ID = 'booking';
|
|
|
50
53
|
if (typeof window === 'undefined') return null;
|
|
51
54
|
return new URLSearchParams(window.location.search).get(key);
|
|
52
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Slots grouped by the VISITOR's local day, in order (AGL-3492).
|
|
58
|
+
*
|
|
59
|
+
* A page of the listing is whole days in the SERVICE's zone, so for a
|
|
60
|
+
* visitor in another zone the last day of a page can be one the page ended
|
|
61
|
+
* part way through. `nextFromMs` is where the page stopped — every open time
|
|
62
|
+
* before it is loaded — so a day that runs past it is held back until the
|
|
63
|
+
* next page completes it, rather than shown with its later times missing.
|
|
64
|
+
*/ export function groupSlotsByLocalDay(slots, nextFromMs) {
|
|
65
|
+
const byDay = new Map();
|
|
66
|
+
for (const slot of slots){
|
|
67
|
+
const at = new Date(slot.startsAtMs);
|
|
68
|
+
const startMs = new Date(at.getFullYear(), at.getMonth(), at.getDate()).getTime();
|
|
69
|
+
const day = byDay.get(startMs);
|
|
70
|
+
if (day) day.push(slot);
|
|
71
|
+
else byDay.set(startMs, [
|
|
72
|
+
slot
|
|
73
|
+
]);
|
|
74
|
+
}
|
|
75
|
+
return [
|
|
76
|
+
...byDay.entries()
|
|
77
|
+
].map(([startMs, daySlots])=>({
|
|
78
|
+
startMs,
|
|
79
|
+
slots: daySlots.sort((a, b)=>a.startsAtMs - b.startsAtMs)
|
|
80
|
+
})).filter((day)=>{
|
|
81
|
+
if (nextFromMs === null) return true;
|
|
82
|
+
const start = new Date(day.startMs);
|
|
83
|
+
const nextDayMs = new Date(start.getFullYear(), start.getMonth(), start.getDate() + 1).getTime();
|
|
84
|
+
return nextDayMs <= nextFromMs;
|
|
85
|
+
}).sort((a, b)=>a.startMs - b.startMs);
|
|
86
|
+
}
|
|
87
|
+
/** A day chip's label: `Mon, Oct 5` in the visitor's locale. */ export function bookingDayLabel(startMs) {
|
|
88
|
+
return new Date(startMs).toLocaleDateString(undefined, {
|
|
89
|
+
weekday: 'short',
|
|
90
|
+
month: 'short',
|
|
91
|
+
day: 'numeric'
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/** A time chip's label: `2:30 PM` in the visitor's locale. */ export function bookingTimeLabel(startsAtMs) {
|
|
95
|
+
return new Date(startsAtMs).toLocaleTimeString([], {
|
|
96
|
+
hour: 'numeric',
|
|
97
|
+
minute: '2-digit'
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* A day's times in the parts of the day a visitor scans for (AGL-3492): a
|
|
102
|
+
* day open 8 to 5 at 15-minute steps holds 33 starts, and one unbroken run
|
|
103
|
+
* of chips that long reads as noise. Empty parts are left out.
|
|
104
|
+
*/ export function partsOfDay(slots) {
|
|
105
|
+
const parts = [
|
|
106
|
+
{
|
|
107
|
+
label: 'Morning',
|
|
108
|
+
slots: []
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
label: 'Afternoon',
|
|
112
|
+
slots: []
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
label: 'Evening',
|
|
116
|
+
slots: []
|
|
117
|
+
}
|
|
118
|
+
];
|
|
119
|
+
for (const slot of slots){
|
|
120
|
+
const hour = new Date(slot.startsAtMs).getHours();
|
|
121
|
+
parts[hour < 12 ? 0 : hour < 17 ? 1 : 2].slots.push(slot);
|
|
122
|
+
}
|
|
123
|
+
return parts.filter((part)=>part.slots.length > 0);
|
|
124
|
+
}
|
|
53
125
|
/**
|
|
54
126
|
* Booking widget (AGL-160): service picker → open-slot picker → contact
|
|
55
127
|
* form, driven by the tenant /api/bookings endpoints (server-validated,
|
|
56
128
|
* double-booking safe). Inert placeholder in the besigner/preview (no
|
|
57
129
|
* SiteContext host id) so editing never creates bookings.
|
|
58
130
|
*/ const Booking = /*#__PURE__*/ forwardRef((props, ref)=>{
|
|
59
|
-
var _slotsByDay_day;
|
|
60
131
|
const { heading, successMessage } = props, rest = _object_without_properties_loose(props, [
|
|
61
132
|
"heading",
|
|
62
133
|
"successMessage"
|
|
@@ -74,10 +145,22 @@ export const ID = 'booking';
|
|
|
74
145
|
useBookingPurchaseEvent(hostId, siteFetch);
|
|
75
146
|
const [services, setServices] = useState(null);
|
|
76
147
|
const [serviceId, setServiceId] = useState('');
|
|
148
|
+
// Every page of the listing loaded so far, in order (AGL-3492).
|
|
77
149
|
const [slots, setSlots] = useState(null);
|
|
150
|
+
// Where the next page starts; `null` once the horizon is loaded.
|
|
151
|
+
const [nextFromMs, setNextFromMs] = useState(null);
|
|
152
|
+
const [horizonDays, setHorizonDays] = useState(BOOKING_MAX_DAYS_AHEAD);
|
|
153
|
+
const [loadingMore, setLoadingMore] = useState(false);
|
|
154
|
+
// The first day of the strip on screen, as an index into the loaded days.
|
|
155
|
+
const [stripStart, setStripStart] = useState(0);
|
|
156
|
+
// The service the loaded pages belong to, so a page that lands after the
|
|
157
|
+
// visitor switched service is dropped rather than mixed into the new one.
|
|
158
|
+
const slotsServiceRef = useRef('');
|
|
78
159
|
const [slotMs, setSlotMs] = useState(null);
|
|
79
160
|
const [name, setName] = useState('');
|
|
80
161
|
const [email, setEmail] = useState('');
|
|
162
|
+
const [phone, setPhone] = useState('');
|
|
163
|
+
const [address, setAddress] = useState('');
|
|
81
164
|
const [marketingConsent, setMarketingConsent] = useState(false);
|
|
82
165
|
const [status, setStatus] = useState('idle');
|
|
83
166
|
const [errorMessage, setErrorMessage] = useState(null);
|
|
@@ -106,42 +189,102 @@ export const ID = 'booking';
|
|
|
106
189
|
}, [
|
|
107
190
|
hostId
|
|
108
191
|
]);
|
|
192
|
+
/**
|
|
193
|
+
* One page of the listing (AGL-3492): the next strip's worth of days from
|
|
194
|
+
* `fromMs`, or from now. Appended, never replacing, so a day split across
|
|
195
|
+
* two pages for a visitor in another zone is whole once both are in.
|
|
196
|
+
*/ const fetchSlotPage = useCallback(async (forService, fromMs)=>{
|
|
197
|
+
const response = await fetch(`/api/bookings/slots?hostId=${encodeURIComponent(hostId != null ? hostId : '')}` + `&serviceId=${encodeURIComponent(forService)}` + (fromMs !== null ? `&from=${encodeURIComponent(String(fromMs))}` : ''));
|
|
198
|
+
const payload = await response.json();
|
|
199
|
+
if (slotsServiceRef.current !== forService) return;
|
|
200
|
+
const page = Array.isArray(payload == null ? void 0 : payload.slots) ? payload.slots : [];
|
|
201
|
+
setSlots((loaded)=>{
|
|
202
|
+
const seen = new Set((loaded != null ? loaded : []).map((slot)=>slot.startsAtMs));
|
|
203
|
+
return [
|
|
204
|
+
...loaded != null ? loaded : [],
|
|
205
|
+
...page.filter((slot)=>!seen.has(slot.startsAtMs))
|
|
206
|
+
];
|
|
207
|
+
});
|
|
208
|
+
setNextFromMs(typeof (payload == null ? void 0 : payload.nextFromMs) === 'number' ? payload.nextFromMs : null);
|
|
209
|
+
if (Number(payload == null ? void 0 : payload.horizonDays) > 0) {
|
|
210
|
+
setHorizonDays(Number(payload.horizonDays));
|
|
211
|
+
}
|
|
212
|
+
}, [
|
|
213
|
+
hostId
|
|
214
|
+
]);
|
|
109
215
|
useEffect(()=>{
|
|
110
|
-
|
|
111
|
-
let active = true;
|
|
216
|
+
slotsServiceRef.current = serviceId;
|
|
112
217
|
setSlots(null);
|
|
218
|
+
setNextFromMs(null);
|
|
219
|
+
setStripStart(0);
|
|
113
220
|
setSlotMs(null);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (
|
|
117
|
-
}).catch(()=>{
|
|
118
|
-
if (active) setSlots([]);
|
|
221
|
+
if (!hostId || !serviceId) return;
|
|
222
|
+
fetchSlotPage(serviceId, null).catch(()=>{
|
|
223
|
+
if (slotsServiceRef.current === serviceId) setSlots([]);
|
|
119
224
|
});
|
|
120
|
-
return ()=>{
|
|
121
|
-
active = false;
|
|
122
|
-
};
|
|
123
225
|
}, [
|
|
124
226
|
hostId,
|
|
125
|
-
serviceId
|
|
227
|
+
serviceId,
|
|
228
|
+
fetchSlotPage
|
|
126
229
|
]);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
230
|
+
const days = useMemo(()=>groupSlotsByLocalDay(slots != null ? slots : [], nextFromMs), [
|
|
231
|
+
slots,
|
|
232
|
+
nextFromMs
|
|
233
|
+
]);
|
|
234
|
+
const visibleDays = days.slice(stripStart, stripStart + BOOKING_SLOT_PAGE_DAYS);
|
|
235
|
+
// The strip asks for the next page itself when the days it is showing run
|
|
236
|
+
// out before the horizon does — after "Later dates", or when a page held
|
|
237
|
+
// no open day this visitor can see whole.
|
|
238
|
+
useEffect(()=>{
|
|
239
|
+
if (slots === null || nextFromMs === null || loadingMore) return;
|
|
240
|
+
if (days.length >= stripStart + BOOKING_SLOT_PAGE_DAYS) return;
|
|
241
|
+
if (stripStart === 0 && days.length > 0) return;
|
|
242
|
+
const forService = serviceId;
|
|
243
|
+
setLoadingMore(true);
|
|
244
|
+
fetchSlotPage(forService, nextFromMs).catch(()=>{
|
|
245
|
+
// Stop asking: a page that failed would otherwise be asked for
|
|
246
|
+
// again on every render.
|
|
247
|
+
if (slotsServiceRef.current === forService) setNextFromMs(null);
|
|
248
|
+
}).finally(()=>setLoadingMore(false));
|
|
136
249
|
}, [
|
|
137
|
-
slots
|
|
250
|
+
slots,
|
|
251
|
+
nextFromMs,
|
|
252
|
+
loadingMore,
|
|
253
|
+
days.length,
|
|
254
|
+
stripStart,
|
|
255
|
+
serviceId,
|
|
256
|
+
fetchSlotPage
|
|
138
257
|
]);
|
|
139
|
-
const [day, setDay] = useState(
|
|
258
|
+
const [day, setDay] = useState(null);
|
|
140
259
|
useEffect(()=>{
|
|
141
|
-
setDay(
|
|
260
|
+
setDay(null);
|
|
142
261
|
}, [
|
|
143
262
|
serviceId
|
|
144
263
|
]);
|
|
264
|
+
const dayTimes = useMemo(()=>{
|
|
265
|
+
var _ref;
|
|
266
|
+
var _days_find;
|
|
267
|
+
return partsOfDay((_ref = (_days_find = days.find((one)=>one.startMs === day)) == null ? void 0 : _days_find.slots) != null ? _ref : []);
|
|
268
|
+
}, [
|
|
269
|
+
days,
|
|
270
|
+
day
|
|
271
|
+
]);
|
|
272
|
+
const hasEarlierDates = stripStart > 0;
|
|
273
|
+
const hasLaterDates = days.length > stripStart + BOOKING_SLOT_PAGE_DAYS || nextFromMs !== null;
|
|
274
|
+
const moveStrip = (toStart)=>{
|
|
275
|
+
setStripStart(Math.max(0, toStart));
|
|
276
|
+
setDay(null);
|
|
277
|
+
setSlotMs(null);
|
|
278
|
+
};
|
|
279
|
+
// What the picked service asks for beyond a name and an email, and
|
|
280
|
+
// whether what was typed answers it (AGL-3493) — the route's own reading,
|
|
281
|
+
// so the button and the server cannot disagree about a required field.
|
|
282
|
+
const asks = bookingContactAsks((services != null ? services : []).find((one)=>one.$id === serviceId));
|
|
283
|
+
const contactCheck = readBookingContactFields(asks, {
|
|
284
|
+
phone,
|
|
285
|
+
address
|
|
286
|
+
});
|
|
287
|
+
const phoneUnreadable = asks.phone !== 'off' && Boolean(phone.trim()) && !readBookingPhone(phone);
|
|
145
288
|
const handleBook = useCallback(async ()=>{
|
|
146
289
|
if (!hostId || !serviceId || !slotMs || status === 'booking') return;
|
|
147
290
|
setStatus('booking');
|
|
@@ -163,7 +306,7 @@ export const ID = 'booking';
|
|
|
163
306
|
startsAtMs: slotMs,
|
|
164
307
|
name,
|
|
165
308
|
email
|
|
166
|
-
}, recordRef ? {
|
|
309
|
+
}, contactCheck.ok ? contactCheck.fields : {}, recordRef ? {
|
|
167
310
|
crmRef: formatBookingRecordRef(recordRef)
|
|
168
311
|
} : {}, marketingConsent ? {
|
|
169
312
|
marketingConsent: true
|
|
@@ -239,6 +382,7 @@ export const ID = 'booking';
|
|
|
239
382
|
slotMs,
|
|
240
383
|
name,
|
|
241
384
|
email,
|
|
385
|
+
contactCheck,
|
|
242
386
|
marketingConsent,
|
|
243
387
|
status,
|
|
244
388
|
siteFetch,
|
|
@@ -309,7 +453,7 @@ export const ID = 'booking';
|
|
|
309
453
|
fullWidth: true,
|
|
310
454
|
children: services.map((service)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
311
455
|
value: service.$id,
|
|
312
|
-
children: `${service.name} · ${service.durationMinutes} min` + (service.priceUsd > 0 ? ` ·
|
|
456
|
+
children: `${service.name} · ${service.durationMinutes} min` + (bookingPriceDisplay(service.priceDisplay) !== 'fixed' || service.priceUsd > 0 ? ` · ${bookingPriceText(service)}` : '')
|
|
313
457
|
}, service.$id))
|
|
314
458
|
}),
|
|
315
459
|
(selected == null ? void 0 : selected.description) ? /*#__PURE__*/ _jsx(Typography, {
|
|
@@ -317,47 +461,86 @@ export const ID = 'booking';
|
|
|
317
461
|
color: "text.secondary",
|
|
318
462
|
children: selected.description
|
|
319
463
|
}) : null,
|
|
320
|
-
serviceId ? slots === null ? /*#__PURE__*/ _jsx(CircularProgress, {
|
|
464
|
+
serviceId ? slots === null || days.length === 0 && nextFromMs !== null ? /*#__PURE__*/ _jsx(CircularProgress, {
|
|
321
465
|
size: 24
|
|
322
|
-
}) :
|
|
466
|
+
}) : days.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
|
|
323
467
|
variant: "body2",
|
|
324
468
|
color: "text.secondary",
|
|
325
|
-
children:
|
|
469
|
+
children: `No open times in the next ${horizonDays} days.`
|
|
326
470
|
}) : /*#__PURE__*/ _jsxs(_Fragment, {
|
|
327
471
|
children: [
|
|
328
|
-
/*#__PURE__*/
|
|
472
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
329
473
|
direction: "row",
|
|
330
474
|
spacing: 1,
|
|
331
475
|
sx: {
|
|
332
476
|
flexWrap: 'wrap',
|
|
333
477
|
rowGap: 1
|
|
334
478
|
},
|
|
335
|
-
children:
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
479
|
+
children: [
|
|
480
|
+
visibleDays.map((one)=>/*#__PURE__*/ _jsx(Chip, {
|
|
481
|
+
label: bookingDayLabel(one.startMs),
|
|
482
|
+
color: day === one.startMs ? 'primary' : 'default',
|
|
483
|
+
variant: day === one.startMs ? 'filled' : 'outlined',
|
|
484
|
+
onClick: ()=>{
|
|
485
|
+
setDay(one.startMs);
|
|
486
|
+
setSlotMs(null);
|
|
487
|
+
}
|
|
488
|
+
}, one.startMs)),
|
|
489
|
+
loadingMore && visibleDays.length < BOOKING_SLOT_PAGE_DAYS ? /*#__PURE__*/ _jsx(CircularProgress, {
|
|
490
|
+
size: 20,
|
|
491
|
+
sx: {
|
|
492
|
+
alignSelf: 'center'
|
|
342
493
|
}
|
|
343
|
-
}
|
|
494
|
+
}) : null
|
|
495
|
+
]
|
|
344
496
|
}),
|
|
345
|
-
|
|
497
|
+
hasEarlierDates || hasLaterDates ? /*#__PURE__*/ _jsxs(Stack, {
|
|
346
498
|
direction: "row",
|
|
347
499
|
spacing: 1,
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
500
|
+
children: [
|
|
501
|
+
hasEarlierDates ? /*#__PURE__*/ _jsx(Button, {
|
|
502
|
+
size: "small",
|
|
503
|
+
onClick: ()=>moveStrip(stripStart - BOOKING_SLOT_PAGE_DAYS),
|
|
504
|
+
children: 'Earlier dates'
|
|
505
|
+
}) : null,
|
|
506
|
+
hasLaterDates ? /*#__PURE__*/ _jsx(Button, {
|
|
507
|
+
size: "small",
|
|
508
|
+
disabled: loadingMore && visibleDays.length < BOOKING_SLOT_PAGE_DAYS,
|
|
509
|
+
// Never past the last day loaded: a strip that was short
|
|
510
|
+
// moves on from where it ended, so no day is stepped over
|
|
511
|
+
// while the next page loads.
|
|
512
|
+
onClick: ()=>moveStrip(Math.min(stripStart + BOOKING_SLOT_PAGE_DAYS, days.length)),
|
|
513
|
+
children: 'Later dates'
|
|
514
|
+
}) : null
|
|
515
|
+
]
|
|
516
|
+
}) : null,
|
|
517
|
+
day !== null ? /*#__PURE__*/ _jsx(Stack, {
|
|
518
|
+
spacing: 1.5,
|
|
519
|
+
children: dayTimes.map((part)=>/*#__PURE__*/ _jsxs(Stack, {
|
|
520
|
+
spacing: 0.75,
|
|
521
|
+
children: [
|
|
522
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
523
|
+
variant: "caption",
|
|
524
|
+
color: "text.secondary",
|
|
525
|
+
children: part.label
|
|
526
|
+
}),
|
|
527
|
+
/*#__PURE__*/ _jsx(Box, {
|
|
528
|
+
role: "group",
|
|
529
|
+
"aria-label": `${part.label} times`,
|
|
530
|
+
sx: {
|
|
531
|
+
display: 'grid',
|
|
532
|
+
gridTemplateColumns: 'repeat(auto-fill, minmax(5.5rem, 1fr))',
|
|
533
|
+
gap: 1
|
|
534
|
+
},
|
|
535
|
+
children: part.slots.map((slot)=>/*#__PURE__*/ _jsx(Chip, {
|
|
536
|
+
label: bookingTimeLabel(slot.startsAtMs),
|
|
537
|
+
color: slotMs === slot.startsAtMs ? 'primary' : 'default',
|
|
538
|
+
variant: slotMs === slot.startsAtMs ? 'filled' : 'outlined',
|
|
539
|
+
onClick: ()=>setSlotMs(slot.startsAtMs)
|
|
540
|
+
}, slot.startsAtMs))
|
|
541
|
+
})
|
|
542
|
+
]
|
|
543
|
+
}, part.label))
|
|
361
544
|
}) : null
|
|
362
545
|
]
|
|
363
546
|
}) : null,
|
|
@@ -379,6 +562,29 @@ export const ID = 'booking';
|
|
|
379
562
|
size: "small",
|
|
380
563
|
fullWidth: true
|
|
381
564
|
}),
|
|
565
|
+
asks.phone !== 'off' ? /*#__PURE__*/ _jsx(TextField, {
|
|
566
|
+
label: "Phone",
|
|
567
|
+
type: "tel",
|
|
568
|
+
autoComplete: "tel",
|
|
569
|
+
required: asks.phone === 'required',
|
|
570
|
+
value: phone,
|
|
571
|
+
onChange: (event)=>setPhone(event.target.value),
|
|
572
|
+
error: phoneUnreadable,
|
|
573
|
+
helperText: phoneUnreadable ? 'Enter a valid phone number' : undefined,
|
|
574
|
+
size: "small",
|
|
575
|
+
fullWidth: true
|
|
576
|
+
}) : null,
|
|
577
|
+
asks.address !== 'off' ? /*#__PURE__*/ _jsx(TextField, {
|
|
578
|
+
label: "Address",
|
|
579
|
+
autoComplete: "street-address",
|
|
580
|
+
required: asks.address === 'required',
|
|
581
|
+
value: address,
|
|
582
|
+
onChange: (event)=>setAddress(event.target.value),
|
|
583
|
+
size: "small",
|
|
584
|
+
fullWidth: true,
|
|
585
|
+
multiline: true,
|
|
586
|
+
minRows: 2
|
|
587
|
+
}) : null,
|
|
382
588
|
/*#__PURE__*/ _jsx(FormControlLabel, {
|
|
383
589
|
control: /*#__PURE__*/ _jsx(Checkbox, {
|
|
384
590
|
size: "small",
|
|
@@ -393,7 +599,7 @@ export const ID = 'booking';
|
|
|
393
599
|
}) : null,
|
|
394
600
|
/*#__PURE__*/ _jsx(Button, {
|
|
395
601
|
variant: "contained",
|
|
396
|
-
disabled: !name.trim() || !email.trim() || status === 'booking',
|
|
602
|
+
disabled: !name.trim() || !email.trim() || !contactCheck.ok || status === 'booking',
|
|
397
603
|
onClick: handleBook,
|
|
398
604
|
sx: {
|
|
399
605
|
alignSelf: 'flex-start'
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/components/booking.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport {\n buildBeginCheckoutParams,\n trackEvent,\n trackEventBeforeNavigation,\n} from '@aglyn/aglyn/app-utils/analytics-events'\nimport { utmTouchField } from '@aglyn/aglyn/app-utils/utm-touch'\nimport { mdiCalendarClock } from '@aglyn/shared-data-mdi'\nimport Alert from '@mui/material/Alert'\nimport Box from '@mui/material/Box'\nimport Button from '@mui/material/Button'\nimport Checkbox from '@mui/material/Checkbox'\nimport Chip from '@mui/material/Chip'\nimport CircularProgress from '@mui/material/CircularProgress'\nimport FormControlLabel from '@mui/material/FormControlLabel'\nimport MenuItem from '@mui/material/MenuItem'\nimport Stack from '@mui/material/Stack'\nimport TextField from '@mui/material/TextField'\nimport Typography from '@mui/material/Typography'\nimport { forwardRef, useCallback, useEffect, useMemo, useState } from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n BOOKING_RECORD_PARAM,\n BOOKING_SERVICE_PARAM,\n formatBookingRecordRef,\n parseBookingRecordRef,\n} from '../model/booking-record'\nimport { generatePresetId } from '../utils/generate-preset-id'\nimport { useBookingPurchaseEvent } from '../utils/use-booking-purchase-event'\n\n// Component ids are persisted in screen documents; never rename.\nexport const ID: Aglyn.ComponentId = 'booking'\n\nexport interface BookingProps {\n /** Heading above the widget; empty hides it. */\n heading?: string\n successMessage?: string\n}\n\ninterface ServiceOption {\n $id: string\n name: string\n durationMinutes: number\n priceUsd: number\n description?: string\n}\n\n/**\n * One query value off the page the widget is on, or `null` — on the server\n * and in the besigner there is no page to read.\n *\n * A booking link built from a CRM record carries two (AGL-2660): the\n * service it is about, and the record it was dropped from. Read at the\n * moment each is needed rather than held in state, so a widget mounted\n * before the URL settles still sees the values.\n */\nfunction readLinkParam(key: string): string | null {\n if (typeof window === 'undefined') return null\n return new URLSearchParams(window.location.search).get(key)\n}\n\ninterface SlotOption {\n startsAtMs: number\n endsAtMs: number\n}\n\n/**\n * Booking widget (AGL-160): service picker → open-slot picker → contact\n * form, driven by the tenant /api/bookings endpoints (server-validated,\n * double-booking safe). Inert placeholder in the besigner/preview (no\n * SiteContext host id) so editing never creates bookings.\n */\nconst Booking = forwardRef<HTMLDivElement, BookingProps>((props, ref) => {\n const { heading, successMessage, ...rest } = props\n // Node styles ride the renderer-merged sx; recompose (stack.ts pattern).\n const nodeSx = Array.isArray(props['sx']) ? props['sx'] : [props['sx']]\n const { hostId } = Aglyn.useSite()\n const siteFetch = Aglyn.useSiteFetch()\n\n // The merchant's own `purchase` for a paid booking (AGL-2481). This widget\n // is where Stripe returns the guest, because a tenant site has no booking\n // confirmation route. No-ops on every ordinary render: the hook returns\n // immediately unless the URL carries `?booking=paid&session_id=…`.\n useBookingPurchaseEvent(hostId, siteFetch)\n\n const [services, setServices] = useState<ServiceOption[] | null>(null)\n const [serviceId, setServiceId] = useState('')\n const [slots, setSlots] = useState<SlotOption[] | null>(null)\n const [slotMs, setSlotMs] = useState<number | null>(null)\n const [name, setName] = useState('')\n const [email, setEmail] = useState('')\n const [marketingConsent, setMarketingConsent] = useState(false)\n const [status, setStatus] = useState<\n 'idle' | 'loading' | 'booking' | 'booked' | 'error'\n >('idle')\n const [errorMessage, setErrorMessage] = useState<string | null>(null)\n const [alerts, setAlerts] = useState<\n Array<{ message: string; severity?: string }>\n >([])\n\n useEffect(() => {\n if (!hostId) return\n let active = true\n void fetch(`/api/bookings/slots?hostId=${encodeURIComponent(hostId)}`)\n .then((response) => response.json())\n .then((payload) => {\n if (!active) return\n const loaded: ServiceOption[] = payload?.services ?? []\n setServices(loaded)\n // A booking link opens the widget on its service (AGL-2660). Only\n // a service the site still offers: a link to one since deleted\n // leaves the picker for the visitor rather than failing on it.\n const wanted = readLinkParam(BOOKING_SERVICE_PARAM)\n if (wanted && loaded.some((one) => one.$id === wanted)) {\n setServiceId(wanted)\n }\n })\n .catch(() => {\n if (active) setServices([])\n })\n return () => {\n active = false\n }\n }, [hostId])\n\n useEffect(() => {\n if (!hostId || !serviceId) return setSlots(null)\n let active = true\n setSlots(null)\n setSlotMs(null)\n void fetch(\n `/api/bookings/slots?hostId=${encodeURIComponent(hostId)}` +\n `&serviceId=${encodeURIComponent(serviceId)}`,\n )\n .then((response) => response.json())\n .then((payload) => {\n if (active) setSlots(payload?.slots ?? [])\n })\n .catch(() => {\n if (active) setSlots([])\n })\n return () => {\n active = false\n }\n }, [hostId, serviceId])\n\n // Slots grouped by local day for a compact two-step pick.\n const slotsByDay = useMemo(() => {\n const groups: Record<string, SlotOption[]> = {}\n for (const slot of slots ?? []) {\n const day = new Date(slot.startsAtMs).toLocaleDateString()\n ;(groups[day] ??= []).push(slot)\n }\n return groups\n }, [slots])\n const [day, setDay] = useState('')\n useEffect(() => {\n setDay('')\n }, [serviceId])\n\n const handleBook = useCallback(async () => {\n if (!hostId || !serviceId || !slotMs || status === 'booking') return\n setStatus('booking')\n setErrorMessage(null)\n // The record a booking link was dropped from (AGL-2660), carried onto\n // the request so the booking lands on that record even when the visitor\n // books with a different address. Parsed here so a value that is not a\n // reference is never sent.\n const recordRef = parseBookingRecordRef(readLinkParam(BOOKING_RECORD_PARAM))\n try {\n const response = await siteFetch('/api/bookings/book', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n hostId,\n serviceId,\n startsAtMs: slotMs,\n name,\n email,\n ...(recordRef ? { crmRef: formatBookingRecordRef(recordRef) } : {}),\n ...(marketingConsent ? { marketingConsent: true } : {}),\n // The campaign this visitor came from, when they came from one.\n // A booking is an identify moment like a form submission: the\n // visitor was anonymous until this request named them.\n ...utmTouchField(),\n }),\n })\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n setErrorMessage(payload?.error ?? 'Booking failed — try again')\n setStatus('error')\n return\n }\n if (payload?.checkoutUrl) {\n // Paid service (AGL-170): finish payment on Stripe; the webhook\n // confirms the held slot.\n //\n // The `begin_checkout` that `purchase` has never had a counterpart to\n // (AGL-2481 wired the completion and left the intent). Without it a\n // merchant selling appointments sees a purchase with no checkout step\n // in front of it, so GA4's shopping funnel reports every paid booking\n // as an abandoned one — the same shape the bookings plugin was\n // reporting before it sent anything at all.\n //\n // After the server accepted the hold, so a double-booked slot or a\n // closed calendar — both of which answer !ok above — never counts.\n //\n // `item_id` is the SERVICE id, matching the id\n // `buildBookingPurchaseParams` puts on the completed hit, so the two\n // steps join into one per-service funnel instead of naming the same\n // service twice.\n const service = (services ?? []).find((one) => one.$id === serviceId)\n await trackEventBeforeNavigation(\n 'begin_checkout',\n buildBeginCheckoutParams({\n items: [\n {\n item_id: serviceId,\n item_name: service?.name ?? 'Service',\n price: service?.priceUsd,\n quantity: 1,\n },\n ],\n }),\n )\n window.location.assign(payload.checkoutUrl)\n return\n }\n if (Array.isArray(payload?.alerts)) setAlerts(payload.alerts)\n // A FREE booking is confirmed right here — there is no Stripe leg, so\n // nothing downstream will ever report it, and a merchant whose services\n // are free saw an appointment book with no event of any kind.\n //\n // `generate_lead` rather than a zero-value `purchase`: GA4 has no\n // recommended booking event, `purchase` with `value: 0` would put a\n // free appointment into the merchant's revenue reports as a sale worth\n // nothing, and a confirmed request for someone's time IS the lead this\n // event names. The form's own generic submissions report the same event\n // from `form.tsx`, so the merchant reads one conversion count.\n //\n // `form_name` is the block, not the service and never the guest: the\n // name and email typed above are the reason this is the only param\n // shape that may leave here.\n trackEvent('generate_lead', {\n form_name: 'Booking',\n form_location: window.location.pathname,\n })\n setStatus('booked')\n } catch {\n setErrorMessage('Booking failed — try again')\n setStatus('error')\n }\n }, [\n hostId,\n serviceId,\n slotMs,\n name,\n email,\n marketingConsent,\n status,\n siteFetch,\n // Read for the checkout event's item name and price. Without it the\n // handler closes over the services list as it was when the handler was\n // created, which on the first render is `null` — and every paid booking\n // would report an unnamed, unpriced service.\n services,\n ])\n\n if (!hostId) {\n return (\n <Box\n ref={ref}\n {...rest}\n sx={[\n {\n p: 3,\n border: '1px dashed',\n borderColor: 'divider',\n borderRadius: 1,\n color: 'text.secondary',\n fontSize: 13,\n fontFamily: 'system-ui, sans-serif',\n },\n ...nodeSx,\n ]}\n >\n {'Booking widget — visitors pick a service and time here'}\n </Box>\n )\n }\n\n if (status === 'booked') {\n return (\n <Stack ref={ref} spacing={1.5} {...rest}>\n <Alert severity=\"success\">\n {successMessage ||\n 'Booking confirmed — check your email for the details.'}\n </Alert>\n {alerts.map((alert, index) => (\n <Alert key={index} severity={(alert.severity as any) || 'info'}>\n {alert.message}\n </Alert>\n ))}\n </Stack>\n )\n }\n\n const selected = (services ?? []).find(\n (service) => service.$id === serviceId,\n )\n\n return (\n <Stack ref={ref} spacing={2} {...rest}>\n {heading ? <Typography variant=\"h5\">{heading}</Typography> : null}\n {services === null ? (\n <CircularProgress size={24} />\n ) : services.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No bookable services yet.'}\n </Typography>\n ) : (\n <TextField\n select\n label=\"Service\"\n value={serviceId}\n onChange={(event) => setServiceId(event.target.value)}\n size=\"small\"\n fullWidth\n >\n {services.map((service) => (\n <MenuItem key={service.$id} value={service.$id}>\n {`${service.name} · ${service.durationMinutes} min` +\n (service.priceUsd > 0 ? ` · $${service.priceUsd}` : '')}\n </MenuItem>\n ))}\n </TextField>\n )}\n {selected?.description ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {selected.description}\n </Typography>\n ) : null}\n {serviceId ? (\n slots === null ? (\n <CircularProgress size={24} />\n ) : slots.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No open times in the next 60 days.'}\n </Typography>\n ) : (\n <>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ flexWrap: 'wrap', rowGap: 1 }}\n >\n {Object.keys(slotsByDay)\n .slice(0, 14)\n .map((value) => (\n <Chip\n key={value}\n label={value}\n color={day === value ? 'primary' : 'default'}\n variant={day === value ? 'filled' : 'outlined'}\n onClick={() => {\n setDay(value)\n setSlotMs(null)\n }}\n />\n ))}\n </Stack>\n {day ? (\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ flexWrap: 'wrap', rowGap: 1 }}\n >\n {(slotsByDay[day] ?? []).slice(0, 24).map((slot) => (\n <Chip\n key={slot.startsAtMs}\n label={new Date(slot.startsAtMs).toLocaleTimeString([], {\n hour: 'numeric',\n minute: '2-digit',\n })}\n color={slotMs === slot.startsAtMs ? 'primary' : 'default'}\n variant={\n slotMs === slot.startsAtMs ? 'filled' : 'outlined'\n }\n onClick={() => setSlotMs(slot.startsAtMs)}\n />\n ))}\n </Stack>\n ) : null}\n </>\n )\n ) : null}\n {slotMs ? (\n <Stack spacing={1.5}>\n <TextField\n label=\"Your name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n size=\"small\"\n fullWidth\n />\n <TextField\n label=\"Email\"\n type=\"email\"\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n size=\"small\"\n fullWidth\n />\n <FormControlLabel\n control={\n <Checkbox\n size=\"small\"\n checked={marketingConsent}\n onChange={(event) =>\n setMarketingConsent(event.target.checked)\n }\n />\n }\n label=\"Email me about offers and updates\"\n />\n {errorMessage ? <Alert severity=\"error\">{errorMessage}</Alert> : null}\n <Button\n variant=\"contained\"\n disabled={\n !name.trim() || !email.trim() || status === 'booking'\n }\n onClick={handleBook}\n sx={{ alignSelf: 'flex-start' }}\n >\n {status === 'booking' ? 'Booking…' : 'Confirm booking'}\n </Button>\n </Stack>\n ) : null}\n </Stack>\n )\n})\nBooking.displayName = 'Booking'\n\nexport const schema: Aglyn.ComponentSchema<BookingProps> = {\n $id: ID,\n pluginId: BUNDLE_ID,\n displayName: 'Booking',\n description:\n 'Service and time picker that books appointments. It is double-booking safe.',\n category: Aglyn.ComponentCategory.INPUT,\n icon: {\n path: mdiCalendarClock.path,\n sx: { color: '#0288d1' },\n },\n flags: {\n selfClosing: Aglyn.FEATURE_FLAG.ENABLED,\n },\n attributes: [\n {\n name: 'heading',\n description: 'Heading above the widget; empty hides it.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Heading',\n },\n {\n name: 'successMessage',\n description: 'Shown after a confirmed booking.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Success message',\n },\n ],\n}\n\nexport const presets: Aglyn.PresetSchema[] = [\n {\n $id: generatePresetId(ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Booking',\n pluginId: BUNDLE_ID,\n description: 'Service + time picker that books appointments',\n category: Aglyn.ComponentCategory.INPUT,\n icon: {\n path: mdiCalendarClock.path,\n sx: { color: '#0288d1' },\n },\n data: {\n $id: null,\n componentId: ID,\n pluginId: BUNDLE_ID,\n props: {},\n },\n },\n]\n\nexport default Booking\n"],"names":["Aglyn","buildBeginCheckoutParams","trackEvent","trackEventBeforeNavigation","utmTouchField","mdiCalendarClock","Alert","Box","Button","Checkbox","Chip","CircularProgress","FormControlLabel","MenuItem","Stack","TextField","Typography","forwardRef","useCallback","useEffect","useMemo","useState","BUNDLE_ID","BOOKING_RECORD_PARAM","BOOKING_SERVICE_PARAM","formatBookingRecordRef","parseBookingRecordRef","generatePresetId","useBookingPurchaseEvent","ID","readLinkParam","key","window","URLSearchParams","location","search","get","Booking","props","ref","slotsByDay","heading","successMessage","rest","nodeSx","Array","isArray","hostId","useSite","siteFetch","useSiteFetch","services","setServices","serviceId","setServiceId","slots","setSlots","slotMs","setSlotMs","name","setName","email","setEmail","marketingConsent","setMarketingConsent","status","setStatus","errorMessage","setErrorMessage","alerts","setAlerts","active","fetch","encodeURIComponent","then","response","json","payload","loaded","wanted","some","one","$id","catch","groups","slot","day","Date","startsAtMs","toLocaleDateString","push","setDay","handleBook","recordRef","method","headers","body","JSON","stringify","crmRef","ok","error","checkoutUrl","service","find","items","item_id","item_name","price","priceUsd","quantity","assign","form_name","form_location","pathname","sx","p","border","borderColor","borderRadius","color","fontSize","fontFamily","spacing","severity","map","alert","index","message","selected","variant","size","length","select","label","value","onChange","event","target","fullWidth","durationMinutes","description","direction","flexWrap","rowGap","Object","keys","slice","onClick","toLocaleTimeString","hour","minute","type","control","checked","disabled","trim","alignSelf","displayName","schema","pluginId","category","ComponentCategory","INPUT","icon","path","flags","selfClosing","FEATURE_FLAG","ENABLED","attributes","component","FieldComponentType","TEXT_FIELD","presets","NodeType","PRESET","data","componentId"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/components/booking.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport {\n buildBeginCheckoutParams,\n trackEvent,\n trackEventBeforeNavigation,\n} from '@aglyn/aglyn/app-utils/analytics-events'\nimport { utmTouchField } from '@aglyn/aglyn/app-utils/utm-touch'\nimport { mdiCalendarClock } from '@aglyn/shared-data-mdi'\nimport Alert from '@mui/material/Alert'\nimport Box from '@mui/material/Box'\nimport Button from '@mui/material/Button'\nimport Checkbox from '@mui/material/Checkbox'\nimport Chip from '@mui/material/Chip'\nimport CircularProgress from '@mui/material/CircularProgress'\nimport FormControlLabel from '@mui/material/FormControlLabel'\nimport MenuItem from '@mui/material/MenuItem'\nimport Stack from '@mui/material/Stack'\nimport TextField from '@mui/material/TextField'\nimport Typography from '@mui/material/Typography'\nimport {\n forwardRef,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n BOOKING_RECORD_PARAM,\n BOOKING_SERVICE_PARAM,\n formatBookingRecordRef,\n parseBookingRecordRef,\n} from '../model/booking-record'\nimport {\n bookingContactAsks,\n readBookingContactFields,\n readBookingPhone,\n} from '../model/booking-contact-fields'\nimport { bookingPriceDisplay, bookingPriceText } from '../model/booking-price'\nimport { BOOKING_MAX_DAYS_AHEAD, BOOKING_SLOT_PAGE_DAYS } from '../model/bookings'\nimport { generatePresetId } from '../utils/generate-preset-id'\nimport { useBookingPurchaseEvent } from '../utils/use-booking-purchase-event'\n\n// Component ids are persisted in screen documents; never rename.\nexport const ID: Aglyn.ComponentId = 'booking'\n\nexport interface BookingProps {\n /** Heading above the widget; empty hides it. */\n heading?: string\n successMessage?: string\n}\n\ninterface ServiceOption {\n $id: string\n name: string\n durationMinutes: number\n priceUsd: number\n /** How the service states its price (AGL-3475); a label books with no charge. */\n priceDisplay?: string\n description?: string\n /** What the service asks for beyond a name and an email (AGL-3493). */\n askPhone?: string\n askAddress?: string\n}\n\n/**\n * One query value off the page the widget is on, or `null` — on the server\n * and in the besigner there is no page to read.\n *\n * A booking link built from a CRM record carries two (AGL-2660): the\n * service it is about, and the record it was dropped from. Read at the\n * moment each is needed rather than held in state, so a widget mounted\n * before the URL settles still sees the values.\n */\nfunction readLinkParam(key: string): string | null {\n if (typeof window === 'undefined') return null\n return new URLSearchParams(window.location.search).get(key)\n}\n\ninterface SlotOption {\n startsAtMs: number\n endsAtMs: number\n}\n\n/** One day of the strip: the visitor's local calendar day and its times. */\nexport interface BookingSlotDay {\n /** Local midnight that starts the day, in epoch ms — key and sort order. */\n startMs: number\n slots: SlotOption[]\n}\n\n/**\n * Slots grouped by the VISITOR's local day, in order (AGL-3492).\n *\n * A page of the listing is whole days in the SERVICE's zone, so for a\n * visitor in another zone the last day of a page can be one the page ended\n * part way through. `nextFromMs` is where the page stopped — every open time\n * before it is loaded — so a day that runs past it is held back until the\n * next page completes it, rather than shown with its later times missing.\n */\nexport function groupSlotsByLocalDay(\n slots: SlotOption[],\n nextFromMs: number | null,\n): BookingSlotDay[] {\n const byDay = new Map<number, SlotOption[]>()\n for (const slot of slots) {\n const at = new Date(slot.startsAtMs)\n const startMs = new Date(\n at.getFullYear(),\n at.getMonth(),\n at.getDate(),\n ).getTime()\n const day = byDay.get(startMs)\n if (day) day.push(slot)\n else byDay.set(startMs, [slot])\n }\n return [...byDay.entries()]\n .map(([startMs, daySlots]) => ({\n startMs,\n slots: daySlots.sort((a, b) => a.startsAtMs - b.startsAtMs),\n }))\n .filter((day) => {\n if (nextFromMs === null) return true\n const start = new Date(day.startMs)\n const nextDayMs = new Date(\n start.getFullYear(),\n start.getMonth(),\n start.getDate() + 1,\n ).getTime()\n return nextDayMs <= nextFromMs\n })\n .sort((a, b) => a.startMs - b.startMs)\n}\n\n/** A day chip's label: `Mon, Oct 5` in the visitor's locale. */\nexport function bookingDayLabel(startMs: number): string {\n return new Date(startMs).toLocaleDateString(undefined, {\n weekday: 'short',\n month: 'short',\n day: 'numeric',\n })\n}\n\n/** A time chip's label: `2:30 PM` in the visitor's locale. */\nexport function bookingTimeLabel(startsAtMs: number): string {\n return new Date(startsAtMs).toLocaleTimeString([], {\n hour: 'numeric',\n minute: '2-digit',\n })\n}\n\n/**\n * A day's times in the parts of the day a visitor scans for (AGL-3492): a\n * day open 8 to 5 at 15-minute steps holds 33 starts, and one unbroken run\n * of chips that long reads as noise. Empty parts are left out.\n */\nexport function partsOfDay(\n slots: SlotOption[],\n): Array<{ label: string; slots: SlotOption[] }> {\n const parts = [\n { label: 'Morning', slots: [] as SlotOption[] },\n { label: 'Afternoon', slots: [] as SlotOption[] },\n { label: 'Evening', slots: [] as SlotOption[] },\n ]\n for (const slot of slots) {\n const hour = new Date(slot.startsAtMs).getHours()\n parts[hour < 12 ? 0 : hour < 17 ? 1 : 2].slots.push(slot)\n }\n return parts.filter((part) => part.slots.length > 0)\n}\n\n/**\n * Booking widget (AGL-160): service picker → open-slot picker → contact\n * form, driven by the tenant /api/bookings endpoints (server-validated,\n * double-booking safe). Inert placeholder in the besigner/preview (no\n * SiteContext host id) so editing never creates bookings.\n */\nconst Booking = forwardRef<HTMLDivElement, BookingProps>((props, ref) => {\n const { heading, successMessage, ...rest } = props\n // Node styles ride the renderer-merged sx; recompose (stack.ts pattern).\n const nodeSx = Array.isArray(props['sx']) ? props['sx'] : [props['sx']]\n const { hostId } = Aglyn.useSite()\n const siteFetch = Aglyn.useSiteFetch()\n\n // The merchant's own `purchase` for a paid booking (AGL-2481). This widget\n // is where Stripe returns the guest, because a tenant site has no booking\n // confirmation route. No-ops on every ordinary render: the hook returns\n // immediately unless the URL carries `?booking=paid&session_id=…`.\n useBookingPurchaseEvent(hostId, siteFetch)\n\n const [services, setServices] = useState<ServiceOption[] | null>(null)\n const [serviceId, setServiceId] = useState('')\n // Every page of the listing loaded so far, in order (AGL-3492).\n const [slots, setSlots] = useState<SlotOption[] | null>(null)\n // Where the next page starts; `null` once the horizon is loaded.\n const [nextFromMs, setNextFromMs] = useState<number | null>(null)\n const [horizonDays, setHorizonDays] = useState(BOOKING_MAX_DAYS_AHEAD)\n const [loadingMore, setLoadingMore] = useState(false)\n // The first day of the strip on screen, as an index into the loaded days.\n const [stripStart, setStripStart] = useState(0)\n // The service the loaded pages belong to, so a page that lands after the\n // visitor switched service is dropped rather than mixed into the new one.\n const slotsServiceRef = useRef('')\n const [slotMs, setSlotMs] = useState<number | null>(null)\n const [name, setName] = useState('')\n const [email, setEmail] = useState('')\n const [phone, setPhone] = useState('')\n const [address, setAddress] = useState('')\n const [marketingConsent, setMarketingConsent] = useState(false)\n const [status, setStatus] = useState<\n 'idle' | 'loading' | 'booking' | 'booked' | 'error'\n >('idle')\n const [errorMessage, setErrorMessage] = useState<string | null>(null)\n const [alerts, setAlerts] = useState<\n Array<{ message: string; severity?: string }>\n >([])\n\n useEffect(() => {\n if (!hostId) return\n let active = true\n void fetch(`/api/bookings/slots?hostId=${encodeURIComponent(hostId)}`)\n .then((response) => response.json())\n .then((payload) => {\n if (!active) return\n const loaded: ServiceOption[] = payload?.services ?? []\n setServices(loaded)\n // A booking link opens the widget on its service (AGL-2660). Only\n // a service the site still offers: a link to one since deleted\n // leaves the picker for the visitor rather than failing on it.\n const wanted = readLinkParam(BOOKING_SERVICE_PARAM)\n if (wanted && loaded.some((one) => one.$id === wanted)) {\n setServiceId(wanted)\n }\n })\n .catch(() => {\n if (active) setServices([])\n })\n return () => {\n active = false\n }\n }, [hostId])\n\n /**\n * One page of the listing (AGL-3492): the next strip's worth of days from\n * `fromMs`, or from now. Appended, never replacing, so a day split across\n * two pages for a visitor in another zone is whole once both are in.\n */\n const fetchSlotPage = useCallback(\n async (forService: string, fromMs: number | null) => {\n const response = await fetch(\n `/api/bookings/slots?hostId=${encodeURIComponent(hostId ?? '')}` +\n `&serviceId=${encodeURIComponent(forService)}` +\n (fromMs !== null ? `&from=${encodeURIComponent(String(fromMs))}` : ''),\n )\n const payload = await response.json()\n if (slotsServiceRef.current !== forService) return\n const page: SlotOption[] = Array.isArray(payload?.slots)\n ? payload.slots\n : []\n setSlots((loaded) => {\n const seen = new Set((loaded ?? []).map((slot) => slot.startsAtMs))\n return [\n ...(loaded ?? []),\n ...page.filter((slot) => !seen.has(slot.startsAtMs)),\n ]\n })\n setNextFromMs(\n typeof payload?.nextFromMs === 'number' ? payload.nextFromMs : null,\n )\n if (Number(payload?.horizonDays) > 0) {\n setHorizonDays(Number(payload.horizonDays))\n }\n },\n [hostId],\n )\n\n useEffect(() => {\n slotsServiceRef.current = serviceId\n setSlots(null)\n setNextFromMs(null)\n setStripStart(0)\n setSlotMs(null)\n if (!hostId || !serviceId) return\n fetchSlotPage(serviceId, null).catch(() => {\n if (slotsServiceRef.current === serviceId) setSlots([])\n })\n }, [hostId, serviceId, fetchSlotPage])\n\n const days = useMemo(\n () => groupSlotsByLocalDay(slots ?? [], nextFromMs),\n [slots, nextFromMs],\n )\n const visibleDays = days.slice(stripStart, stripStart + BOOKING_SLOT_PAGE_DAYS)\n\n // The strip asks for the next page itself when the days it is showing run\n // out before the horizon does — after \"Later dates\", or when a page held\n // no open day this visitor can see whole.\n useEffect(() => {\n if (slots === null || nextFromMs === null || loadingMore) return\n if (days.length >= stripStart + BOOKING_SLOT_PAGE_DAYS) return\n if (stripStart === 0 && days.length > 0) return\n const forService = serviceId\n setLoadingMore(true)\n fetchSlotPage(forService, nextFromMs)\n .catch(() => {\n // Stop asking: a page that failed would otherwise be asked for\n // again on every render.\n if (slotsServiceRef.current === forService) setNextFromMs(null)\n })\n .finally(() => setLoadingMore(false))\n }, [\n slots,\n nextFromMs,\n loadingMore,\n days.length,\n stripStart,\n serviceId,\n fetchSlotPage,\n ])\n\n const [day, setDay] = useState<number | null>(null)\n useEffect(() => {\n setDay(null)\n }, [serviceId])\n const dayTimes = useMemo(\n () => partsOfDay(days.find((one) => one.startMs === day)?.slots ?? []),\n [days, day],\n )\n const hasEarlierDates = stripStart > 0\n const hasLaterDates =\n days.length > stripStart + BOOKING_SLOT_PAGE_DAYS || nextFromMs !== null\n const moveStrip = (toStart: number) => {\n setStripStart(Math.max(0, toStart))\n setDay(null)\n setSlotMs(null)\n }\n\n // What the picked service asks for beyond a name and an email, and\n // whether what was typed answers it (AGL-3493) — the route's own reading,\n // so the button and the server cannot disagree about a required field.\n const asks = bookingContactAsks(\n (services ?? []).find((one) => one.$id === serviceId),\n )\n const contactCheck = readBookingContactFields(asks, { phone, address })\n const phoneUnreadable =\n asks.phone !== 'off' && Boolean(phone.trim()) && !readBookingPhone(phone)\n\n const handleBook = useCallback(async () => {\n if (!hostId || !serviceId || !slotMs || status === 'booking') return\n setStatus('booking')\n setErrorMessage(null)\n // The record a booking link was dropped from (AGL-2660), carried onto\n // the request so the booking lands on that record even when the visitor\n // books with a different address. Parsed here so a value that is not a\n // reference is never sent.\n const recordRef = parseBookingRecordRef(readLinkParam(BOOKING_RECORD_PARAM))\n try {\n const response = await siteFetch('/api/bookings/book', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n hostId,\n serviceId,\n startsAtMs: slotMs,\n name,\n email,\n // Only what the service asks for: the route drops anything else.\n ...(contactCheck.ok ? contactCheck.fields : {}),\n ...(recordRef ? { crmRef: formatBookingRecordRef(recordRef) } : {}),\n ...(marketingConsent ? { marketingConsent: true } : {}),\n // The campaign this visitor came from, when they came from one.\n // A booking is an identify moment like a form submission: the\n // visitor was anonymous until this request named them.\n ...utmTouchField(),\n }),\n })\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n setErrorMessage(payload?.error ?? 'Booking failed — try again')\n setStatus('error')\n return\n }\n if (payload?.checkoutUrl) {\n // Paid service (AGL-170): finish payment on Stripe; the webhook\n // confirms the held slot.\n //\n // The `begin_checkout` that `purchase` has never had a counterpart to\n // (AGL-2481 wired the completion and left the intent). Without it a\n // merchant selling appointments sees a purchase with no checkout step\n // in front of it, so GA4's shopping funnel reports every paid booking\n // as an abandoned one — the same shape the bookings plugin was\n // reporting before it sent anything at all.\n //\n // After the server accepted the hold, so a double-booked slot or a\n // closed calendar — both of which answer !ok above — never counts.\n //\n // `item_id` is the SERVICE id, matching the id\n // `buildBookingPurchaseParams` puts on the completed hit, so the two\n // steps join into one per-service funnel instead of naming the same\n // service twice.\n const service = (services ?? []).find((one) => one.$id === serviceId)\n await trackEventBeforeNavigation(\n 'begin_checkout',\n buildBeginCheckoutParams({\n items: [\n {\n item_id: serviceId,\n item_name: service?.name ?? 'Service',\n price: service?.priceUsd,\n quantity: 1,\n },\n ],\n }),\n )\n window.location.assign(payload.checkoutUrl)\n return\n }\n if (Array.isArray(payload?.alerts)) setAlerts(payload.alerts)\n // A FREE booking is confirmed right here — there is no Stripe leg, so\n // nothing downstream will ever report it, and a merchant whose services\n // are free saw an appointment book with no event of any kind.\n //\n // `generate_lead` rather than a zero-value `purchase`: GA4 has no\n // recommended booking event, `purchase` with `value: 0` would put a\n // free appointment into the merchant's revenue reports as a sale worth\n // nothing, and a confirmed request for someone's time IS the lead this\n // event names. The form's own generic submissions report the same event\n // from `form.tsx`, so the merchant reads one conversion count.\n //\n // `form_name` is the block, not the service and never the guest: the\n // name and email typed above are the reason this is the only param\n // shape that may leave here.\n trackEvent('generate_lead', {\n form_name: 'Booking',\n form_location: window.location.pathname,\n })\n setStatus('booked')\n } catch {\n setErrorMessage('Booking failed — try again')\n setStatus('error')\n }\n }, [\n hostId,\n serviceId,\n slotMs,\n name,\n email,\n contactCheck,\n marketingConsent,\n status,\n siteFetch,\n // Read for the checkout event's item name and price. Without it the\n // handler closes over the services list as it was when the handler was\n // created, which on the first render is `null` — and every paid booking\n // would report an unnamed, unpriced service.\n services,\n ])\n\n if (!hostId) {\n return (\n <Box\n ref={ref}\n {...rest}\n sx={[\n {\n p: 3,\n border: '1px dashed',\n borderColor: 'divider',\n borderRadius: 1,\n color: 'text.secondary',\n fontSize: 13,\n fontFamily: 'system-ui, sans-serif',\n },\n ...nodeSx,\n ]}\n >\n {'Booking widget — visitors pick a service and time here'}\n </Box>\n )\n }\n\n if (status === 'booked') {\n return (\n <Stack ref={ref} spacing={1.5} {...rest}>\n <Alert severity=\"success\">\n {successMessage ||\n 'Booking confirmed — check your email for the details.'}\n </Alert>\n {alerts.map((alert, index) => (\n <Alert key={index} severity={(alert.severity as any) || 'info'}>\n {alert.message}\n </Alert>\n ))}\n </Stack>\n )\n }\n\n const selected = (services ?? []).find(\n (service) => service.$id === serviceId,\n )\n\n return (\n <Stack ref={ref} spacing={2} {...rest}>\n {heading ? <Typography variant=\"h5\">{heading}</Typography> : null}\n {services === null ? (\n <CircularProgress size={24} />\n ) : services.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No bookable services yet.'}\n </Typography>\n ) : (\n <TextField\n select\n label=\"Service\"\n value={serviceId}\n onChange={(event) => setServiceId(event.target.value)}\n size=\"small\"\n fullWidth\n >\n {services.map((service) => (\n <MenuItem key={service.$id} value={service.$id}>\n {`${service.name} · ${service.durationMinutes} min` +\n (bookingPriceDisplay(service.priceDisplay) !== 'fixed' || service.priceUsd > 0\n ? ` · ${bookingPriceText(service)}`\n : '')}\n </MenuItem>\n ))}\n </TextField>\n )}\n {selected?.description ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {selected.description}\n </Typography>\n ) : null}\n {serviceId ? (\n slots === null || (days.length === 0 && nextFromMs !== null) ? (\n <CircularProgress size={24} />\n ) : days.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`No open times in the next ${horizonDays} days.`}\n </Typography>\n ) : (\n <>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ flexWrap: 'wrap', rowGap: 1 }}\n >\n {visibleDays.map((one) => (\n <Chip\n key={one.startMs}\n label={bookingDayLabel(one.startMs)}\n color={day === one.startMs ? 'primary' : 'default'}\n variant={day === one.startMs ? 'filled' : 'outlined'}\n onClick={() => {\n setDay(one.startMs)\n setSlotMs(null)\n }}\n />\n ))}\n {loadingMore && visibleDays.length < BOOKING_SLOT_PAGE_DAYS ? (\n <CircularProgress size={20} sx={{ alignSelf: 'center' }} />\n ) : null}\n </Stack>\n {hasEarlierDates || hasLaterDates ? (\n <Stack direction=\"row\" spacing={1}>\n {hasEarlierDates ? (\n <Button\n size=\"small\"\n onClick={() =>\n moveStrip(stripStart - BOOKING_SLOT_PAGE_DAYS)\n }\n >\n {'Earlier dates'}\n </Button>\n ) : null}\n {hasLaterDates ? (\n <Button\n size=\"small\"\n disabled={\n loadingMore && visibleDays.length < BOOKING_SLOT_PAGE_DAYS\n }\n // Never past the last day loaded: a strip that was short\n // moves on from where it ended, so no day is stepped over\n // while the next page loads.\n onClick={() =>\n moveStrip(\n Math.min(\n stripStart + BOOKING_SLOT_PAGE_DAYS,\n days.length,\n ),\n )\n }\n >\n {'Later dates'}\n </Button>\n ) : null}\n </Stack>\n ) : null}\n {day !== null ? (\n <Stack spacing={1.5}>\n {dayTimes.map((part) => (\n <Stack key={part.label} spacing={0.75}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {part.label}\n </Typography>\n <Box\n role=\"group\"\n aria-label={`${part.label} times`}\n sx={{\n display: 'grid',\n gridTemplateColumns:\n 'repeat(auto-fill, minmax(5.5rem, 1fr))',\n gap: 1,\n }}\n >\n {part.slots.map((slot) => (\n <Chip\n key={slot.startsAtMs}\n label={bookingTimeLabel(slot.startsAtMs)}\n color={\n slotMs === slot.startsAtMs ? 'primary' : 'default'\n }\n variant={\n slotMs === slot.startsAtMs ? 'filled' : 'outlined'\n }\n onClick={() => setSlotMs(slot.startsAtMs)}\n />\n ))}\n </Box>\n </Stack>\n ))}\n </Stack>\n ) : null}\n </>\n )\n ) : null}\n {slotMs ? (\n <Stack spacing={1.5}>\n <TextField\n label=\"Your name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n size=\"small\"\n fullWidth\n />\n <TextField\n label=\"Email\"\n type=\"email\"\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n size=\"small\"\n fullWidth\n />\n {asks.phone !== 'off' ? (\n <TextField\n label=\"Phone\"\n type=\"tel\"\n autoComplete=\"tel\"\n required={asks.phone === 'required'}\n value={phone}\n onChange={(event) => setPhone(event.target.value)}\n error={phoneUnreadable}\n helperText={phoneUnreadable ? 'Enter a valid phone number' : undefined}\n size=\"small\"\n fullWidth\n />\n ) : null}\n {asks.address !== 'off' ? (\n <TextField\n label=\"Address\"\n autoComplete=\"street-address\"\n required={asks.address === 'required'}\n value={address}\n onChange={(event) => setAddress(event.target.value)}\n size=\"small\"\n fullWidth\n multiline\n minRows={2}\n />\n ) : null}\n <FormControlLabel\n control={\n <Checkbox\n size=\"small\"\n checked={marketingConsent}\n onChange={(event) =>\n setMarketingConsent(event.target.checked)\n }\n />\n }\n label=\"Email me about offers and updates\"\n />\n {errorMessage ? <Alert severity=\"error\">{errorMessage}</Alert> : null}\n <Button\n variant=\"contained\"\n disabled={\n !name.trim() ||\n !email.trim() ||\n !contactCheck.ok ||\n status === 'booking'\n }\n onClick={handleBook}\n sx={{ alignSelf: 'flex-start' }}\n >\n {status === 'booking' ? 'Booking…' : 'Confirm booking'}\n </Button>\n </Stack>\n ) : null}\n </Stack>\n )\n})\nBooking.displayName = 'Booking'\n\nexport const schema: Aglyn.ComponentSchema<BookingProps> = {\n $id: ID,\n pluginId: BUNDLE_ID,\n displayName: 'Booking',\n description:\n 'Service and time picker that books appointments. It is double-booking safe.',\n category: Aglyn.ComponentCategory.INPUT,\n icon: {\n path: mdiCalendarClock.path,\n sx: { color: '#0288d1' },\n },\n flags: {\n selfClosing: Aglyn.FEATURE_FLAG.ENABLED,\n },\n attributes: [\n {\n name: 'heading',\n description: 'Heading above the widget; empty hides it.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Heading',\n },\n {\n name: 'successMessage',\n description: 'Shown after a confirmed booking.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Success message',\n },\n ],\n}\n\nexport const presets: Aglyn.PresetSchema[] = [\n {\n $id: generatePresetId(ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Booking',\n pluginId: BUNDLE_ID,\n description: 'Service + time picker that books appointments',\n category: Aglyn.ComponentCategory.INPUT,\n icon: {\n path: mdiCalendarClock.path,\n sx: { color: '#0288d1' },\n },\n data: {\n $id: null,\n componentId: ID,\n pluginId: BUNDLE_ID,\n props: {},\n },\n },\n]\n\nexport default Booking\n"],"names":["Aglyn","buildBeginCheckoutParams","trackEvent","trackEventBeforeNavigation","utmTouchField","mdiCalendarClock","Alert","Box","Button","Checkbox","Chip","CircularProgress","FormControlLabel","MenuItem","Stack","TextField","Typography","forwardRef","useCallback","useEffect","useMemo","useRef","useState","BUNDLE_ID","BOOKING_RECORD_PARAM","BOOKING_SERVICE_PARAM","formatBookingRecordRef","parseBookingRecordRef","bookingContactAsks","readBookingContactFields","readBookingPhone","bookingPriceDisplay","bookingPriceText","BOOKING_MAX_DAYS_AHEAD","BOOKING_SLOT_PAGE_DAYS","generatePresetId","useBookingPurchaseEvent","ID","readLinkParam","key","window","URLSearchParams","location","search","get","groupSlotsByLocalDay","slots","nextFromMs","byDay","Map","slot","at","Date","startsAtMs","startMs","getFullYear","getMonth","getDate","getTime","day","push","set","entries","map","daySlots","sort","a","b","filter","start","nextDayMs","bookingDayLabel","toLocaleDateString","undefined","weekday","month","bookingTimeLabel","toLocaleTimeString","hour","minute","partsOfDay","parts","label","getHours","part","length","Booking","props","ref","heading","successMessage","rest","nodeSx","Array","isArray","hostId","useSite","siteFetch","useSiteFetch","services","setServices","serviceId","setServiceId","setSlots","setNextFromMs","horizonDays","setHorizonDays","loadingMore","setLoadingMore","stripStart","setStripStart","slotsServiceRef","slotMs","setSlotMs","name","setName","email","setEmail","phone","setPhone","address","setAddress","marketingConsent","setMarketingConsent","status","setStatus","errorMessage","setErrorMessage","alerts","setAlerts","active","fetch","encodeURIComponent","then","response","json","payload","loaded","wanted","some","one","$id","catch","fetchSlotPage","forService","fromMs","String","current","page","seen","Set","has","Number","days","visibleDays","slice","finally","setDay","dayTimes","find","hasEarlierDates","hasLaterDates","moveStrip","toStart","Math","max","asks","contactCheck","phoneUnreadable","Boolean","trim","handleBook","recordRef","method","headers","body","JSON","stringify","ok","fields","crmRef","error","checkoutUrl","service","items","item_id","item_name","price","priceUsd","quantity","assign","form_name","form_location","pathname","sx","p","border","borderColor","borderRadius","color","fontSize","fontFamily","spacing","severity","alert","index","message","selected","variant","size","select","value","onChange","event","target","fullWidth","durationMinutes","priceDisplay","description","direction","flexWrap","rowGap","onClick","alignSelf","disabled","min","role","aria-label","display","gridTemplateColumns","gap","type","autoComplete","required","helperText","multiline","minRows","control","checked","displayName","schema","pluginId","category","ComponentCategory","INPUT","icon","path","flags","selfClosing","FEATURE_FLAG","ENABLED","attributes","component","FieldComponentType","TEXT_FIELD","presets","NodeType","PRESET","data","componentId"],"mappings":"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"}
|