@aglyn/plugins-bookings 1.0.0-beta.143
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/LICENSE +201 -0
- package/README.md +7 -0
- package/package.json +53 -0
- package/src/index.d.ts +19 -0
- package/src/index.js +20 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/book-meeting-action.d.ts +91 -0
- package/src/lib/components/book-meeting-action.js +296 -0
- package/src/lib/components/book-meeting-action.js.map +1 -0
- package/src/lib/components/booking.d.ts +33 -0
- package/src/lib/components/booking.js +463 -0
- package/src/lib/components/booking.js.map +1 -0
- package/src/lib/components/bookings-console-page.d.ts +12 -0
- package/src/lib/components/bookings-console-page.js +743 -0
- package/src/lib/components/bookings-console-page.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +26 -0
- package/src/lib/constants/bundle-common.js +26 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/model/booking-purchase-analytics.d.ts +129 -0
- package/src/lib/model/booking-purchase-analytics.js +197 -0
- package/src/lib/model/booking-purchase-analytics.js.map +1 -0
- package/src/lib/model/bookings.d.ts +141 -0
- package/src/lib/model/bookings.js +124 -0
- package/src/lib/model/bookings.js.map +1 -0
- package/src/lib/model/index.d.ts +22 -0
- package/src/lib/model/index.js +22 -0
- package/src/lib/model/index.js.map +1 -0
- package/src/lib/plugin-config.d.ts +24 -0
- package/src/lib/plugin-config.js +51 -0
- package/src/lib/plugin-config.js.map +1 -0
- package/src/lib/plugin.d.ts +22 -0
- package/src/lib/plugin.js +70 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/server/billing-webhook.d.ts +23 -0
- package/src/lib/server/billing-webhook.js +345 -0
- package/src/lib/server/billing-webhook.js.map +1 -0
- package/src/lib/server/booking-analytics.d.ts +68 -0
- package/src/lib/server/booking-analytics.js +115 -0
- package/src/lib/server/booking-analytics.js.map +1 -0
- package/src/lib/server/booking-crm.d.ts +39 -0
- package/src/lib/server/booking-crm.js +67 -0
- package/src/lib/server/booking-crm.js.map +1 -0
- package/src/lib/server/booking-figures.d.ts +32 -0
- package/src/lib/server/booking-figures.js +155 -0
- package/src/lib/server/booking-figures.js.map +1 -0
- package/src/lib/server/refund.d.ts +65 -0
- package/src/lib/server/refund.js +292 -0
- package/src/lib/server/refund.js.map +1 -0
- package/src/lib/server.d.ts +63 -0
- package/src/lib/server.js +968 -0
- package/src/lib/server.js.map +1 -0
- package/src/lib/site.d.ts +27 -0
- package/src/lib/site.js +50 -0
- package/src/lib/site.js.map +1 -0
- package/src/lib/testing/stand-in-tax-profile.d.ts +29 -0
- package/src/lib/testing/stand-in-tax-profile.js +64 -0
- package/src/lib/testing/stand-in-tax-profile.js.map +1 -0
- package/src/lib/utils/generate-preset-id.d.ts +19 -0
- package/src/lib/utils/generate-preset-id.js +25 -0
- package/src/lib/utils/generate-preset-id.js.map +1 -0
- package/src/lib/utils/use-booking-purchase-event.d.ts +65 -0
- package/src/lib/utils/use-booking-purchase-event.js +147 -0
- package/src/lib/utils/use-booking-purchase-event.js.map +1 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
|
|
3
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
4
|
+
/**
|
|
5
|
+
* @license
|
|
6
|
+
* Copyright 2026 Aglyn LLC
|
|
7
|
+
*
|
|
8
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
9
|
+
* you may not use this file except in compliance with the License.
|
|
10
|
+
* You may obtain a copy of the License at
|
|
11
|
+
*
|
|
12
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
13
|
+
*
|
|
14
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
15
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
16
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
17
|
+
* See the License for the specific language governing permissions and
|
|
18
|
+
* limitations under the License.
|
|
19
|
+
*/ import * as Aglyn from "@aglyn/aglyn";
|
|
20
|
+
import { buildBeginCheckoutParams, trackEvent, trackEventBeforeNavigation } from "@aglyn/aglyn/app-utils/analytics-events";
|
|
21
|
+
import { campaignTouchField } from "@aglyn/aglyn/app-utils/campaign-touch";
|
|
22
|
+
import { BOOKING_SERVICE_PARAM, CRM_BOOKING_REF_PARAM, formatCrmBookingRef, parseCrmBookingRef } from "@aglyn/aglyn/app-utils/crm-booking";
|
|
23
|
+
import { mdiCalendarClock } from "@aglyn/shared-data-mdi";
|
|
24
|
+
import Alert from "@mui/material/Alert";
|
|
25
|
+
import Box from "@mui/material/Box";
|
|
26
|
+
import Button from "@mui/material/Button";
|
|
27
|
+
import Checkbox from "@mui/material/Checkbox";
|
|
28
|
+
import Chip from "@mui/material/Chip";
|
|
29
|
+
import CircularProgress from "@mui/material/CircularProgress";
|
|
30
|
+
import FormControlLabel from "@mui/material/FormControlLabel";
|
|
31
|
+
import MenuItem from "@mui/material/MenuItem";
|
|
32
|
+
import Stack from "@mui/material/Stack";
|
|
33
|
+
import TextField from "@mui/material/TextField";
|
|
34
|
+
import Typography from "@mui/material/Typography";
|
|
35
|
+
import { forwardRef, useCallback, useEffect, useMemo, useState } from "react";
|
|
36
|
+
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
37
|
+
import { generatePresetId } from "../utils/generate-preset-id.js";
|
|
38
|
+
import { useBookingPurchaseEvent } from "../utils/use-booking-purchase-event.js";
|
|
39
|
+
// Component ids are persisted in screen documents; never rename.
|
|
40
|
+
export const ID = 'booking';
|
|
41
|
+
/**
|
|
42
|
+
* One query value off the page the widget is on, or `null` — on the server
|
|
43
|
+
* and in the besigner there is no page to read.
|
|
44
|
+
*
|
|
45
|
+
* A booking link built from a CRM record carries two (AGL-2660): the
|
|
46
|
+
* service it is about, and the record it was dropped from. Read at the
|
|
47
|
+
* moment each is needed rather than held in state, so a widget mounted
|
|
48
|
+
* before the URL settles still sees the values.
|
|
49
|
+
*/ function readLinkParam(key) {
|
|
50
|
+
if (typeof window === 'undefined') return null;
|
|
51
|
+
return new URLSearchParams(window.location.search).get(key);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Booking widget (AGL-160): service picker → open-slot picker → contact
|
|
55
|
+
* form, driven by the tenant /api/bookings endpoints (server-validated,
|
|
56
|
+
* double-booking safe). Inert placeholder in the besigner/preview (no
|
|
57
|
+
* SiteContext host id) so editing never creates bookings.
|
|
58
|
+
*/ const Booking = /*#__PURE__*/ forwardRef((props, ref)=>{
|
|
59
|
+
var _slotsByDay_day;
|
|
60
|
+
const { heading, successMessage } = props, rest = _object_without_properties_loose(props, [
|
|
61
|
+
"heading",
|
|
62
|
+
"successMessage"
|
|
63
|
+
]);
|
|
64
|
+
// Node styles ride the renderer-merged sx; recompose (stack.ts pattern).
|
|
65
|
+
const nodeSx = Array.isArray(props['sx']) ? props['sx'] : [
|
|
66
|
+
props['sx']
|
|
67
|
+
];
|
|
68
|
+
const { hostId } = Aglyn.useSite();
|
|
69
|
+
const siteFetch = Aglyn.useSiteFetch();
|
|
70
|
+
// The merchant's own `purchase` for a paid booking (AGL-2481). This widget
|
|
71
|
+
// is where Stripe returns the guest, because a tenant site has no booking
|
|
72
|
+
// confirmation route. No-ops on every ordinary render: the hook returns
|
|
73
|
+
// immediately unless the URL carries `?booking=paid&session_id=…`.
|
|
74
|
+
useBookingPurchaseEvent(hostId, siteFetch);
|
|
75
|
+
const [services, setServices] = useState(null);
|
|
76
|
+
const [serviceId, setServiceId] = useState('');
|
|
77
|
+
const [slots, setSlots] = useState(null);
|
|
78
|
+
const [slotMs, setSlotMs] = useState(null);
|
|
79
|
+
const [name, setName] = useState('');
|
|
80
|
+
const [email, setEmail] = useState('');
|
|
81
|
+
const [marketingConsent, setMarketingConsent] = useState(false);
|
|
82
|
+
const [status, setStatus] = useState('idle');
|
|
83
|
+
const [errorMessage, setErrorMessage] = useState(null);
|
|
84
|
+
const [alerts, setAlerts] = useState([]);
|
|
85
|
+
useEffect(()=>{
|
|
86
|
+
if (!hostId) return;
|
|
87
|
+
let active = true;
|
|
88
|
+
void fetch(`/api/bookings/slots?hostId=${encodeURIComponent(hostId)}`).then((response)=>response.json()).then((payload)=>{
|
|
89
|
+
var _ref;
|
|
90
|
+
if (!active) return;
|
|
91
|
+
const loaded = (_ref = payload == null ? void 0 : payload.services) != null ? _ref : [];
|
|
92
|
+
setServices(loaded);
|
|
93
|
+
// A booking link opens the widget on its service (AGL-2660). Only
|
|
94
|
+
// a service the site still offers: a link to one since deleted
|
|
95
|
+
// leaves the picker for the visitor rather than failing on it.
|
|
96
|
+
const wanted = readLinkParam(BOOKING_SERVICE_PARAM);
|
|
97
|
+
if (wanted && loaded.some((one)=>one.$id === wanted)) {
|
|
98
|
+
setServiceId(wanted);
|
|
99
|
+
}
|
|
100
|
+
}).catch(()=>{
|
|
101
|
+
if (active) setServices([]);
|
|
102
|
+
});
|
|
103
|
+
return ()=>{
|
|
104
|
+
active = false;
|
|
105
|
+
};
|
|
106
|
+
}, [
|
|
107
|
+
hostId
|
|
108
|
+
]);
|
|
109
|
+
useEffect(()=>{
|
|
110
|
+
if (!hostId || !serviceId) return setSlots(null);
|
|
111
|
+
let active = true;
|
|
112
|
+
setSlots(null);
|
|
113
|
+
setSlotMs(null);
|
|
114
|
+
void fetch(`/api/bookings/slots?hostId=${encodeURIComponent(hostId)}` + `&serviceId=${encodeURIComponent(serviceId)}`).then((response)=>response.json()).then((payload)=>{
|
|
115
|
+
var _ref;
|
|
116
|
+
if (active) setSlots((_ref = payload == null ? void 0 : payload.slots) != null ? _ref : []);
|
|
117
|
+
}).catch(()=>{
|
|
118
|
+
if (active) setSlots([]);
|
|
119
|
+
});
|
|
120
|
+
return ()=>{
|
|
121
|
+
active = false;
|
|
122
|
+
};
|
|
123
|
+
}, [
|
|
124
|
+
hostId,
|
|
125
|
+
serviceId
|
|
126
|
+
]);
|
|
127
|
+
// Slots grouped by local day for a compact two-step pick.
|
|
128
|
+
const slotsByDay = useMemo(()=>{
|
|
129
|
+
const groups = {};
|
|
130
|
+
for (const slot of slots != null ? slots : []){
|
|
131
|
+
var _groups, _day, _;
|
|
132
|
+
const day = new Date(slot.startsAtMs).toLocaleDateString();
|
|
133
|
+
((_ = (_groups = groups)[_day = day]) != null ? _ : _groups[_day] = []).push(slot);
|
|
134
|
+
}
|
|
135
|
+
return groups;
|
|
136
|
+
}, [
|
|
137
|
+
slots
|
|
138
|
+
]);
|
|
139
|
+
const [day, setDay] = useState('');
|
|
140
|
+
useEffect(()=>{
|
|
141
|
+
setDay('');
|
|
142
|
+
}, [
|
|
143
|
+
serviceId
|
|
144
|
+
]);
|
|
145
|
+
const handleBook = useCallback(async ()=>{
|
|
146
|
+
if (!hostId || !serviceId || !slotMs || status === 'booking') return;
|
|
147
|
+
setStatus('booking');
|
|
148
|
+
setErrorMessage(null);
|
|
149
|
+
// The CRM record a booking link was dropped from (AGL-2660), carried
|
|
150
|
+
// onto the request so the booking lands on that record even when the
|
|
151
|
+
// visitor books with a different address. Parsed here so a value that
|
|
152
|
+
// is not a reference is never sent.
|
|
153
|
+
const crmRef = parseCrmBookingRef(readLinkParam(CRM_BOOKING_REF_PARAM));
|
|
154
|
+
try {
|
|
155
|
+
const response = await siteFetch('/api/bookings/book', {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
headers: {
|
|
158
|
+
'Content-Type': 'application/json'
|
|
159
|
+
},
|
|
160
|
+
body: JSON.stringify(_extends({
|
|
161
|
+
hostId,
|
|
162
|
+
serviceId,
|
|
163
|
+
startsAtMs: slotMs,
|
|
164
|
+
name,
|
|
165
|
+
email
|
|
166
|
+
}, crmRef ? {
|
|
167
|
+
crmRef: formatCrmBookingRef(crmRef)
|
|
168
|
+
} : {}, marketingConsent ? {
|
|
169
|
+
marketingConsent: true
|
|
170
|
+
} : {}, campaignTouchField()))
|
|
171
|
+
});
|
|
172
|
+
const payload = await response.json().catch(()=>({}));
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
var _ref;
|
|
175
|
+
setErrorMessage((_ref = payload == null ? void 0 : payload.error) != null ? _ref : 'Booking failed — try again');
|
|
176
|
+
setStatus('error');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (payload == null ? void 0 : payload.checkoutUrl) {
|
|
180
|
+
var _ref1;
|
|
181
|
+
// Paid service (AGL-170): finish payment on Stripe; the webhook
|
|
182
|
+
// confirms the held slot.
|
|
183
|
+
//
|
|
184
|
+
// The `begin_checkout` that `purchase` has never had a counterpart to
|
|
185
|
+
// (AGL-2481 wired the completion and left the intent). Without it a
|
|
186
|
+
// merchant selling appointments sees a purchase with no checkout step
|
|
187
|
+
// in front of it, so GA4's shopping funnel reports every paid booking
|
|
188
|
+
// as an abandoned one — the same shape the bookings plugin was
|
|
189
|
+
// reporting before it sent anything at all.
|
|
190
|
+
//
|
|
191
|
+
// After the server accepted the hold, so a double-booked slot or a
|
|
192
|
+
// closed calendar — both of which answer !ok above — never counts.
|
|
193
|
+
//
|
|
194
|
+
// `item_id` is the SERVICE id, matching the id
|
|
195
|
+
// `buildBookingPurchaseParams` puts on the completed hit, so the two
|
|
196
|
+
// steps join into one per-service funnel instead of naming the same
|
|
197
|
+
// service twice.
|
|
198
|
+
const service = (services != null ? services : []).find((one)=>one.$id === serviceId);
|
|
199
|
+
await trackEventBeforeNavigation('begin_checkout', buildBeginCheckoutParams({
|
|
200
|
+
items: [
|
|
201
|
+
{
|
|
202
|
+
item_id: serviceId,
|
|
203
|
+
item_name: (_ref1 = service == null ? void 0 : service.name) != null ? _ref1 : 'Service',
|
|
204
|
+
price: service == null ? void 0 : service.priceUsd,
|
|
205
|
+
quantity: 1
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
}));
|
|
209
|
+
window.location.assign(payload.checkoutUrl);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (Array.isArray(payload == null ? void 0 : payload.alerts)) setAlerts(payload.alerts);
|
|
213
|
+
// A FREE booking is confirmed right here — there is no Stripe leg, so
|
|
214
|
+
// nothing downstream will ever report it, and a merchant whose services
|
|
215
|
+
// are free saw an appointment book with no event of any kind.
|
|
216
|
+
//
|
|
217
|
+
// `generate_lead` rather than a zero-value `purchase`: GA4 has no
|
|
218
|
+
// recommended booking event, `purchase` with `value: 0` would put a
|
|
219
|
+
// free appointment into the merchant's revenue reports as a sale worth
|
|
220
|
+
// nothing, and a confirmed request for someone's time IS the lead this
|
|
221
|
+
// event names. The form's own generic submissions report the same event
|
|
222
|
+
// from `form.tsx`, so the merchant reads one conversion count.
|
|
223
|
+
//
|
|
224
|
+
// `form_name` is the block, not the service and never the guest: the
|
|
225
|
+
// name and email typed above are the reason this is the only param
|
|
226
|
+
// shape that may leave here.
|
|
227
|
+
trackEvent('generate_lead', {
|
|
228
|
+
form_name: 'Booking',
|
|
229
|
+
form_location: window.location.pathname
|
|
230
|
+
});
|
|
231
|
+
setStatus('booked');
|
|
232
|
+
} catch (unused) {
|
|
233
|
+
setErrorMessage('Booking failed — try again');
|
|
234
|
+
setStatus('error');
|
|
235
|
+
}
|
|
236
|
+
}, [
|
|
237
|
+
hostId,
|
|
238
|
+
serviceId,
|
|
239
|
+
slotMs,
|
|
240
|
+
name,
|
|
241
|
+
email,
|
|
242
|
+
marketingConsent,
|
|
243
|
+
status,
|
|
244
|
+
siteFetch,
|
|
245
|
+
// Read for the checkout event's item name and price. Without it the
|
|
246
|
+
// handler closes over the services list as it was when the handler was
|
|
247
|
+
// created, which on the first render is `null` — and every paid booking
|
|
248
|
+
// would report an unnamed, unpriced service.
|
|
249
|
+
services
|
|
250
|
+
]);
|
|
251
|
+
if (!hostId) {
|
|
252
|
+
return /*#__PURE__*/ _jsx(Box, _extends({
|
|
253
|
+
ref: ref
|
|
254
|
+
}, rest, {
|
|
255
|
+
sx: [
|
|
256
|
+
{
|
|
257
|
+
p: 3,
|
|
258
|
+
border: '1px dashed',
|
|
259
|
+
borderColor: 'divider',
|
|
260
|
+
borderRadius: 1,
|
|
261
|
+
color: 'text.secondary',
|
|
262
|
+
fontSize: 13,
|
|
263
|
+
fontFamily: 'system-ui, sans-serif'
|
|
264
|
+
},
|
|
265
|
+
...nodeSx
|
|
266
|
+
],
|
|
267
|
+
children: 'Booking widget — visitors pick a service and time here'
|
|
268
|
+
}));
|
|
269
|
+
}
|
|
270
|
+
if (status === 'booked') {
|
|
271
|
+
return /*#__PURE__*/ _jsxs(Stack, _extends({
|
|
272
|
+
ref: ref,
|
|
273
|
+
spacing: 1.5
|
|
274
|
+
}, rest, {
|
|
275
|
+
children: [
|
|
276
|
+
/*#__PURE__*/ _jsx(Alert, {
|
|
277
|
+
severity: "success",
|
|
278
|
+
children: successMessage || 'Booking confirmed — check your email for the details.'
|
|
279
|
+
}),
|
|
280
|
+
alerts.map((alert, index)=>/*#__PURE__*/ _jsx(Alert, {
|
|
281
|
+
severity: alert.severity || 'info',
|
|
282
|
+
children: alert.message
|
|
283
|
+
}, index))
|
|
284
|
+
]
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
const selected = (services != null ? services : []).find((service)=>service.$id === serviceId);
|
|
288
|
+
return /*#__PURE__*/ _jsxs(Stack, _extends({
|
|
289
|
+
ref: ref,
|
|
290
|
+
spacing: 2
|
|
291
|
+
}, rest, {
|
|
292
|
+
children: [
|
|
293
|
+
heading ? /*#__PURE__*/ _jsx(Typography, {
|
|
294
|
+
variant: "h5",
|
|
295
|
+
children: heading
|
|
296
|
+
}) : null,
|
|
297
|
+
services === null ? /*#__PURE__*/ _jsx(CircularProgress, {
|
|
298
|
+
size: 24
|
|
299
|
+
}) : services.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
|
|
300
|
+
variant: "body2",
|
|
301
|
+
color: "text.secondary",
|
|
302
|
+
children: 'No bookable services yet.'
|
|
303
|
+
}) : /*#__PURE__*/ _jsx(TextField, {
|
|
304
|
+
select: true,
|
|
305
|
+
label: "Service",
|
|
306
|
+
value: serviceId,
|
|
307
|
+
onChange: (event)=>setServiceId(event.target.value),
|
|
308
|
+
size: "small",
|
|
309
|
+
fullWidth: true,
|
|
310
|
+
children: services.map((service)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
311
|
+
value: service.$id,
|
|
312
|
+
children: `${service.name} · ${service.durationMinutes} min` + (service.priceUsd > 0 ? ` · $${service.priceUsd}` : '')
|
|
313
|
+
}, service.$id))
|
|
314
|
+
}),
|
|
315
|
+
(selected == null ? void 0 : selected.description) ? /*#__PURE__*/ _jsx(Typography, {
|
|
316
|
+
variant: "body2",
|
|
317
|
+
color: "text.secondary",
|
|
318
|
+
children: selected.description
|
|
319
|
+
}) : null,
|
|
320
|
+
serviceId ? slots === null ? /*#__PURE__*/ _jsx(CircularProgress, {
|
|
321
|
+
size: 24
|
|
322
|
+
}) : slots.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
|
|
323
|
+
variant: "body2",
|
|
324
|
+
color: "text.secondary",
|
|
325
|
+
children: 'No open times in the next 60 days.'
|
|
326
|
+
}) : /*#__PURE__*/ _jsxs(_Fragment, {
|
|
327
|
+
children: [
|
|
328
|
+
/*#__PURE__*/ _jsx(Stack, {
|
|
329
|
+
direction: "row",
|
|
330
|
+
spacing: 1,
|
|
331
|
+
sx: {
|
|
332
|
+
flexWrap: 'wrap',
|
|
333
|
+
rowGap: 1
|
|
334
|
+
},
|
|
335
|
+
children: Object.keys(slotsByDay).slice(0, 14).map((value)=>/*#__PURE__*/ _jsx(Chip, {
|
|
336
|
+
label: value,
|
|
337
|
+
color: day === value ? 'primary' : 'default',
|
|
338
|
+
variant: day === value ? 'filled' : 'outlined',
|
|
339
|
+
onClick: ()=>{
|
|
340
|
+
setDay(value);
|
|
341
|
+
setSlotMs(null);
|
|
342
|
+
}
|
|
343
|
+
}, value))
|
|
344
|
+
}),
|
|
345
|
+
day ? /*#__PURE__*/ _jsx(Stack, {
|
|
346
|
+
direction: "row",
|
|
347
|
+
spacing: 1,
|
|
348
|
+
sx: {
|
|
349
|
+
flexWrap: 'wrap',
|
|
350
|
+
rowGap: 1
|
|
351
|
+
},
|
|
352
|
+
children: ((_slotsByDay_day = slotsByDay[day]) != null ? _slotsByDay_day : []).slice(0, 24).map((slot)=>/*#__PURE__*/ _jsx(Chip, {
|
|
353
|
+
label: new Date(slot.startsAtMs).toLocaleTimeString([], {
|
|
354
|
+
hour: 'numeric',
|
|
355
|
+
minute: '2-digit'
|
|
356
|
+
}),
|
|
357
|
+
color: slotMs === slot.startsAtMs ? 'primary' : 'default',
|
|
358
|
+
variant: slotMs === slot.startsAtMs ? 'filled' : 'outlined',
|
|
359
|
+
onClick: ()=>setSlotMs(slot.startsAtMs)
|
|
360
|
+
}, slot.startsAtMs))
|
|
361
|
+
}) : null
|
|
362
|
+
]
|
|
363
|
+
}) : null,
|
|
364
|
+
slotMs ? /*#__PURE__*/ _jsxs(Stack, {
|
|
365
|
+
spacing: 1.5,
|
|
366
|
+
children: [
|
|
367
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
368
|
+
label: "Your name",
|
|
369
|
+
value: name,
|
|
370
|
+
onChange: (event)=>setName(event.target.value),
|
|
371
|
+
size: "small",
|
|
372
|
+
fullWidth: true
|
|
373
|
+
}),
|
|
374
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
375
|
+
label: "Email",
|
|
376
|
+
type: "email",
|
|
377
|
+
value: email,
|
|
378
|
+
onChange: (event)=>setEmail(event.target.value),
|
|
379
|
+
size: "small",
|
|
380
|
+
fullWidth: true
|
|
381
|
+
}),
|
|
382
|
+
/*#__PURE__*/ _jsx(FormControlLabel, {
|
|
383
|
+
control: /*#__PURE__*/ _jsx(Checkbox, {
|
|
384
|
+
size: "small",
|
|
385
|
+
checked: marketingConsent,
|
|
386
|
+
onChange: (event)=>setMarketingConsent(event.target.checked)
|
|
387
|
+
}),
|
|
388
|
+
label: "Email me about offers and updates"
|
|
389
|
+
}),
|
|
390
|
+
errorMessage ? /*#__PURE__*/ _jsx(Alert, {
|
|
391
|
+
severity: "error",
|
|
392
|
+
children: errorMessage
|
|
393
|
+
}) : null,
|
|
394
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
395
|
+
variant: "contained",
|
|
396
|
+
disabled: !name.trim() || !email.trim() || status === 'booking',
|
|
397
|
+
onClick: handleBook,
|
|
398
|
+
sx: {
|
|
399
|
+
alignSelf: 'flex-start'
|
|
400
|
+
},
|
|
401
|
+
children: status === 'booking' ? 'Booking…' : 'Confirm booking'
|
|
402
|
+
})
|
|
403
|
+
]
|
|
404
|
+
}) : null
|
|
405
|
+
]
|
|
406
|
+
}));
|
|
407
|
+
});
|
|
408
|
+
Booking.displayName = 'Booking';
|
|
409
|
+
export const schema = {
|
|
410
|
+
$id: ID,
|
|
411
|
+
pluginId: BUNDLE_ID,
|
|
412
|
+
displayName: 'Booking',
|
|
413
|
+
description: 'Service and time picker that books appointments. It is double-booking safe.',
|
|
414
|
+
category: Aglyn.ComponentCategory.INPUT,
|
|
415
|
+
icon: {
|
|
416
|
+
path: mdiCalendarClock.path,
|
|
417
|
+
sx: {
|
|
418
|
+
color: '#0288d1'
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
flags: {
|
|
422
|
+
selfClosing: Aglyn.FEATURE_FLAG.ENABLED
|
|
423
|
+
},
|
|
424
|
+
attributes: [
|
|
425
|
+
{
|
|
426
|
+
name: 'heading',
|
|
427
|
+
description: 'Heading above the widget; empty hides it.',
|
|
428
|
+
component: Aglyn.FieldComponentType.TEXT_FIELD,
|
|
429
|
+
label: 'Heading'
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
name: 'successMessage',
|
|
433
|
+
description: 'Shown after a confirmed booking.',
|
|
434
|
+
component: Aglyn.FieldComponentType.TEXT_FIELD,
|
|
435
|
+
label: 'Success message'
|
|
436
|
+
}
|
|
437
|
+
]
|
|
438
|
+
};
|
|
439
|
+
export const presets = [
|
|
440
|
+
{
|
|
441
|
+
$id: generatePresetId(ID),
|
|
442
|
+
type: Aglyn.NodeType.PRESET,
|
|
443
|
+
displayName: 'Booking',
|
|
444
|
+
pluginId: BUNDLE_ID,
|
|
445
|
+
description: 'Service + time picker that books appointments',
|
|
446
|
+
category: Aglyn.ComponentCategory.INPUT,
|
|
447
|
+
icon: {
|
|
448
|
+
path: mdiCalendarClock.path,
|
|
449
|
+
sx: {
|
|
450
|
+
color: '#0288d1'
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
data: {
|
|
454
|
+
$id: null,
|
|
455
|
+
componentId: ID,
|
|
456
|
+
pluginId: BUNDLE_ID,
|
|
457
|
+
props: {}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
];
|
|
461
|
+
export default Booking;
|
|
462
|
+
|
|
463
|
+
//# sourceMappingURL=booking.js.map
|
|
@@ -0,0 +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 { campaignTouchField } from '@aglyn/aglyn/app-utils/campaign-touch'\nimport {\n BOOKING_SERVICE_PARAM,\n CRM_BOOKING_REF_PARAM,\n formatCrmBookingRef,\n parseCrmBookingRef,\n} from '@aglyn/aglyn/app-utils/crm-booking'\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 { 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 CRM record a booking link was dropped from (AGL-2660), carried\n // onto the request so the booking lands on that record even when the\n // visitor books with a different address. Parsed here so a value that\n // is not a reference is never sent.\n const crmRef = parseCrmBookingRef(readLinkParam(CRM_BOOKING_REF_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 ...(crmRef ? { crmRef: formatCrmBookingRef(crmRef) } : {}),\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 ...campaignTouchField(),\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","campaignTouchField","BOOKING_SERVICE_PARAM","CRM_BOOKING_REF_PARAM","formatCrmBookingRef","parseCrmBookingRef","mdiCalendarClock","Alert","Box","Button","Checkbox","Chip","CircularProgress","FormControlLabel","MenuItem","Stack","TextField","Typography","forwardRef","useCallback","useEffect","useMemo","useState","BUNDLE_ID","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","crmRef","method","headers","body","JSON","stringify","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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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ConsolePluginPageProps } from '@aglyn/aglyn';
|
|
2
|
+
/**
|
|
3
|
+
* Bookings manager (AGL-159): bookable services with weekly availability
|
|
4
|
+
* windows, and the upcoming-bookings list with cancel. Visitors book via
|
|
5
|
+
* the org /api/bookings endpoints; confirmations email through the
|
|
6
|
+
* env-gated Resend path. Plan-gated (`bookings` flag + `servicesPerHost`).
|
|
7
|
+
*/
|
|
8
|
+
export declare function BookingsConsolePage(props: ConsolePluginPageProps): import("react").JSX.Element;
|
|
9
|
+
export declare namespace BookingsConsolePage {
|
|
10
|
+
var displayName: string;
|
|
11
|
+
}
|
|
12
|
+
export default BookingsConsolePage;
|