@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,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
18
|
+
import * as Aglyn from "@aglyn/aglyn";
|
|
19
|
+
import { checkEntitlement, isHostPluginEnabled } from "@aglyn/aglyn";
|
|
20
|
+
import { BOOKING_PATH_DEFAULT, bookingLinkFor } from "../model/bookings.js";
|
|
21
|
+
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
22
|
+
import { mdiCalendarClock, mdiContentCopy } from "@aglyn/shared-data-mdi";
|
|
23
|
+
import { AppLink, MdiIcon } from "@aglyn/shared-ui-jsx";
|
|
24
|
+
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
25
|
+
import { useFirestore, useFirestoreCollection, useFirestoreDoc, useOrgDataScope, useSitePluginConfig } from "@aglyn/tenant-feature-instance";
|
|
26
|
+
import { Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Stack, Tooltip, Typography } from "@mui/material";
|
|
27
|
+
import { collection, doc, limit, query } from "firebase/firestore";
|
|
28
|
+
import { useParams } from "next/navigation";
|
|
29
|
+
import { useCallback, useState } from "react";
|
|
30
|
+
/**
|
|
31
|
+
* WHETHER THIS SITE HAS A BOOKING DOOR (AGL-2660).
|
|
32
|
+
*
|
|
33
|
+
* The action is offered only where a booking can actually be taken: the
|
|
34
|
+
* Bookings plugin runs on the site — the org's set less the site's own
|
|
35
|
+
* deny-list, which is the one resolver every other per-site gate reads —
|
|
36
|
+
* and the org holds the `bookings` entitlement, without which the booking
|
|
37
|
+
* API refuses every request. One listen on the site document answers the
|
|
38
|
+
* deny-list and, for the link, the site's public naming.
|
|
39
|
+
*
|
|
40
|
+
* At the organization level the record's own capturing site is the site
|
|
41
|
+
* asked about, which is how a contact captured by one brand of an agency
|
|
42
|
+
* offers that brand's services and not another's. A record no site has
|
|
43
|
+
* captured has no door.
|
|
44
|
+
*/ export function useBookingDoor(hostId, org) {
|
|
45
|
+
const firestore = useFirestore();
|
|
46
|
+
const { data: host, status } = useFirestoreDoc(()=>hostId ? doc(firestore, 'hosts', hostId) : null, [
|
|
47
|
+
firestore,
|
|
48
|
+
hostId
|
|
49
|
+
]);
|
|
50
|
+
const open = Boolean(hostId) && status === 'success' && Boolean(host) && isHostPluginEnabled(org != null ? org : null, host, BUNDLE_ID) && checkEntitlement(org != null ? org : null, 'bookings');
|
|
51
|
+
return {
|
|
52
|
+
open,
|
|
53
|
+
host: host != null ? host : null
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* THE "BOOK A MEETING" ACTION A RECORD CARRIES (AGL-2660): one control, and
|
|
58
|
+
* the dialog it opens, mounted only while open. Renders NOTHING where the
|
|
59
|
+
* site has no booking door, rather than a disabled control with a reason —
|
|
60
|
+
* a site that has never enabled Bookings should not read every record page
|
|
61
|
+
* as missing something.
|
|
62
|
+
*/ export function BookMeetingButton(props) {
|
|
63
|
+
const { hostId, org, kind, recordId, onInsert, variant = 'button' } = props;
|
|
64
|
+
const door = useBookingDoor(hostId, org);
|
|
65
|
+
const [open, setOpen] = useState(false);
|
|
66
|
+
if (!door.open || !hostId || !recordId) return null;
|
|
67
|
+
const icon = /*#__PURE__*/ _jsx(MdiIcon, {
|
|
68
|
+
path: mdiCalendarClock.path,
|
|
69
|
+
size: 0.8
|
|
70
|
+
});
|
|
71
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
72
|
+
children: [
|
|
73
|
+
variant === 'chip' ? /*#__PURE__*/ _jsx(Chip, {
|
|
74
|
+
size: "small",
|
|
75
|
+
variant: "outlined",
|
|
76
|
+
icon: icon,
|
|
77
|
+
label: "Insert booking link",
|
|
78
|
+
onClick: ()=>setOpen(true),
|
|
79
|
+
// A flex item in the dialog's column, kept to its own width.
|
|
80
|
+
sx: {
|
|
81
|
+
alignSelf: 'flex-start'
|
|
82
|
+
}
|
|
83
|
+
}) : /*#__PURE__*/ _jsx(Button, {
|
|
84
|
+
size: "small",
|
|
85
|
+
variant: "outlined",
|
|
86
|
+
startIcon: icon,
|
|
87
|
+
onClick: ()=>setOpen(true),
|
|
88
|
+
children: 'Book a meeting'
|
|
89
|
+
}),
|
|
90
|
+
open ? /*#__PURE__*/ _jsx(BookMeetingDialog, {
|
|
91
|
+
open: true,
|
|
92
|
+
onClose: ()=>setOpen(false),
|
|
93
|
+
hostId: hostId,
|
|
94
|
+
host: door.host,
|
|
95
|
+
org: org,
|
|
96
|
+
kind: kind,
|
|
97
|
+
recordId: recordId,
|
|
98
|
+
onInsert: onInsert
|
|
99
|
+
}) : null
|
|
100
|
+
]
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
BookMeetingButton.displayName = 'BookMeetingButton';
|
|
104
|
+
/**
|
|
105
|
+
* The site's bookable services, each with the link a visitor books it at.
|
|
106
|
+
*
|
|
107
|
+
* The link is the Bookings plugin's own builder over three facts read here:
|
|
108
|
+
* the site's public naming (off the site document), the page the Booking
|
|
109
|
+
* block lives on (the plugin's per-site `bookingPath` setting), and the
|
|
110
|
+
* record this dialog was opened from. A site with no public address yet
|
|
111
|
+
* lists its services and says why there is no link.
|
|
112
|
+
*/ export function BookMeetingDialog(props) {
|
|
113
|
+
var _config_bookingPath;
|
|
114
|
+
const { open, onClose, hostId, host, org, kind, recordId, onInsert } = props;
|
|
115
|
+
const firestore = useFirestore();
|
|
116
|
+
const { enqueueSnackbar } = useSnackbar();
|
|
117
|
+
const { orgId } = useOrgDataScope({
|
|
118
|
+
hostId,
|
|
119
|
+
orgId: org == null ? void 0 : org.$id
|
|
120
|
+
});
|
|
121
|
+
const { config, ready: configReady } = useSitePluginConfig(orgId, hostId, BUNDLE_ID);
|
|
122
|
+
const { data: serviceDocs, status } = useFirestoreCollection(()=>query(collection(firestore, 'hosts', hostId, 'services'), limit(100)), [
|
|
123
|
+
firestore,
|
|
124
|
+
hostId
|
|
125
|
+
], {
|
|
126
|
+
idField: '$id'
|
|
127
|
+
});
|
|
128
|
+
const services = (serviceDocs != null ? serviceDocs : []).filter((service)=>!service['deletedAt']).sort((a, b)=>{
|
|
129
|
+
var _a_name, _b_name;
|
|
130
|
+
return String((_a_name = a['name']) != null ? _a_name : '').localeCompare(String((_b_name = b['name']) != null ? _b_name : ''));
|
|
131
|
+
});
|
|
132
|
+
// Where services are set up, for the empty state — addressable only
|
|
133
|
+
// under a site route, where the URL names the org and the site.
|
|
134
|
+
const params = useParams();
|
|
135
|
+
const bookingsHref = (params == null ? void 0 : params.orgSlug) && (params == null ? void 0 : params.host) ? Aglyn.siteRecordLinks({
|
|
136
|
+
orgSlug: String(params.orgSlug),
|
|
137
|
+
host: String(params.host)
|
|
138
|
+
}).bookings() : null;
|
|
139
|
+
const site = {
|
|
140
|
+
cname: host == null ? void 0 : host['cname'],
|
|
141
|
+
subdomain: host == null ? void 0 : host['subdomain']
|
|
142
|
+
};
|
|
143
|
+
const path = configReady ? String((_config_bookingPath = config['bookingPath']) != null ? _config_bookingPath : BOOKING_PATH_DEFAULT) : null;
|
|
144
|
+
const crmRef = {
|
|
145
|
+
kind,
|
|
146
|
+
id: recordId
|
|
147
|
+
};
|
|
148
|
+
const copy = useCallback(async (link)=>{
|
|
149
|
+
try {
|
|
150
|
+
await navigator.clipboard.writeText(link);
|
|
151
|
+
enqueueSnackbar('Booking link copied', {
|
|
152
|
+
variant: 'success',
|
|
153
|
+
persist: false
|
|
154
|
+
});
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error(error);
|
|
157
|
+
enqueueSnackbar('The link could not be copied', {
|
|
158
|
+
variant: 'error',
|
|
159
|
+
allowDuplicate: true
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}, [
|
|
163
|
+
enqueueSnackbar
|
|
164
|
+
]);
|
|
165
|
+
return /*#__PURE__*/ _jsxs(Dialog, {
|
|
166
|
+
open: open,
|
|
167
|
+
onClose: onClose,
|
|
168
|
+
maxWidth: "sm",
|
|
169
|
+
fullWidth: true,
|
|
170
|
+
children: [
|
|
171
|
+
/*#__PURE__*/ _jsx(DialogTitle, {
|
|
172
|
+
children: 'Book a meeting'
|
|
173
|
+
}),
|
|
174
|
+
/*#__PURE__*/ _jsxs(DialogContent, {
|
|
175
|
+
sx: {
|
|
176
|
+
display: 'flex',
|
|
177
|
+
flexDirection: 'column',
|
|
178
|
+
gap: 2
|
|
179
|
+
},
|
|
180
|
+
children: [
|
|
181
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
182
|
+
variant: "body2",
|
|
183
|
+
color: "text.secondary",
|
|
184
|
+
children: 'Each link opens your site’s booking page on one service, with this ' + 'record attached so the booking lands on its timeline — whatever ' + 'address the person books with.'
|
|
185
|
+
}),
|
|
186
|
+
status === 'loading' || path === null ? /*#__PURE__*/ _jsx(Typography, {
|
|
187
|
+
variant: "body2",
|
|
188
|
+
color: "text.secondary",
|
|
189
|
+
children: 'Loading…'
|
|
190
|
+
}) : services.length === 0 ? /*#__PURE__*/ _jsxs(Typography, {
|
|
191
|
+
variant: "body2",
|
|
192
|
+
color: "text.secondary",
|
|
193
|
+
children: [
|
|
194
|
+
'No bookable services yet. ',
|
|
195
|
+
bookingsHref ? /*#__PURE__*/ _jsx(AppLink, {
|
|
196
|
+
href: bookingsHref,
|
|
197
|
+
children: 'Set up services'
|
|
198
|
+
}) : null
|
|
199
|
+
]
|
|
200
|
+
}) : /*#__PURE__*/ _jsx(Stack, {
|
|
201
|
+
spacing: 1.5,
|
|
202
|
+
children: services.map((service)=>{
|
|
203
|
+
var _service_name, _service_priceUsd, _service_durationMinutes;
|
|
204
|
+
const id = String(service['$id']);
|
|
205
|
+
const name = String((_service_name = service['name']) != null ? _service_name : '');
|
|
206
|
+
const link = bookingLinkFor({
|
|
207
|
+
site,
|
|
208
|
+
service: {
|
|
209
|
+
id
|
|
210
|
+
},
|
|
211
|
+
path,
|
|
212
|
+
crmRef
|
|
213
|
+
});
|
|
214
|
+
const price = Number((_service_priceUsd = service['priceUsd']) != null ? _service_priceUsd : 0);
|
|
215
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
216
|
+
direction: "row",
|
|
217
|
+
spacing: 1,
|
|
218
|
+
sx: {
|
|
219
|
+
alignItems: 'center'
|
|
220
|
+
},
|
|
221
|
+
children: [
|
|
222
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
223
|
+
sx: {
|
|
224
|
+
flex: 1,
|
|
225
|
+
minWidth: 0
|
|
226
|
+
},
|
|
227
|
+
children: [
|
|
228
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
229
|
+
variant: "body2",
|
|
230
|
+
noWrap: true,
|
|
231
|
+
children: name
|
|
232
|
+
}),
|
|
233
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
234
|
+
variant: "caption",
|
|
235
|
+
color: "text.secondary",
|
|
236
|
+
noWrap: true,
|
|
237
|
+
children: `${Number((_service_durationMinutes = service['durationMinutes']) != null ? _service_durationMinutes : 0)} min` + (price > 0 ? ` · $${price}` : ' · free')
|
|
238
|
+
}),
|
|
239
|
+
link ? /*#__PURE__*/ _jsx(Typography, {
|
|
240
|
+
variant: "caption",
|
|
241
|
+
sx: {
|
|
242
|
+
wordBreak: 'break-all'
|
|
243
|
+
},
|
|
244
|
+
children: link
|
|
245
|
+
}) : /*#__PURE__*/ _jsx(Typography, {
|
|
246
|
+
variant: "caption",
|
|
247
|
+
color: "text.secondary",
|
|
248
|
+
children: 'This site has no public address yet, so there is no link.'
|
|
249
|
+
})
|
|
250
|
+
]
|
|
251
|
+
}),
|
|
252
|
+
/*#__PURE__*/ _jsx(Tooltip, {
|
|
253
|
+
title: "Copy link",
|
|
254
|
+
children: /*#__PURE__*/ _jsx("span", {
|
|
255
|
+
children: /*#__PURE__*/ _jsx(IconButton, {
|
|
256
|
+
size: "small",
|
|
257
|
+
"aria-label": `Copy link for ${name}`,
|
|
258
|
+
disabled: !link,
|
|
259
|
+
onClick: ()=>link && void copy(link),
|
|
260
|
+
children: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
261
|
+
path: mdiContentCopy.path,
|
|
262
|
+
size: 0.8
|
|
263
|
+
})
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
}),
|
|
267
|
+
onInsert ? /*#__PURE__*/ _jsx(Button, {
|
|
268
|
+
size: "small",
|
|
269
|
+
variant: "outlined",
|
|
270
|
+
disabled: !link,
|
|
271
|
+
onClick: ()=>{
|
|
272
|
+
if (!link) return;
|
|
273
|
+
onInsert(link);
|
|
274
|
+
onClose();
|
|
275
|
+
},
|
|
276
|
+
children: 'Insert into email'
|
|
277
|
+
}) : null
|
|
278
|
+
]
|
|
279
|
+
}, id);
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
]
|
|
283
|
+
}),
|
|
284
|
+
/*#__PURE__*/ _jsx(DialogActions, {
|
|
285
|
+
children: /*#__PURE__*/ _jsx(Button, {
|
|
286
|
+
onClick: onClose,
|
|
287
|
+
children: 'Close'
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
]
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
BookMeetingDialog.displayName = 'BookMeetingDialog';
|
|
294
|
+
export default BookMeetingButton;
|
|
295
|
+
|
|
296
|
+
//# sourceMappingURL=book-meeting-action.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/components/book-meeting-action.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\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport {\n type AglynOrgBilling,\n checkEntitlement,\n type CrmBookingRefKind,\n isHostPluginEnabled,\n} from '@aglyn/aglyn'\nimport { BOOKING_PATH_DEFAULT, bookingLinkFor } from '../model/bookings'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { mdiCalendarClock, mdiContentCopy } from '@aglyn/shared-data-mdi'\nimport { AppLink, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n useFirestoreDoc,\n useOrgDataScope,\n useSitePluginConfig,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n IconButton,\n Stack,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport { collection, doc, limit, query } from 'firebase/firestore'\nimport { useParams } from 'next/navigation'\nimport { useCallback, useState } from 'react'\n\n\n/**\n * WHETHER THIS SITE HAS A BOOKING DOOR (AGL-2660).\n *\n * The action is offered only where a booking can actually be taken: the\n * Bookings plugin runs on the site — the org's set less the site's own\n * deny-list, which is the one resolver every other per-site gate reads —\n * and the org holds the `bookings` entitlement, without which the booking\n * API refuses every request. One listen on the site document answers the\n * deny-list and, for the link, the site's public naming.\n *\n * At the organization level the record's own capturing site is the site\n * asked about, which is how a contact captured by one brand of an agency\n * offers that brand's services and not another's. A record no site has\n * captured has no door.\n */\nexport function useBookingDoor(\n hostId: string | null | undefined,\n org?: Partial<AglynOrgBilling> | null,\n): { open: boolean; host: Record<string, unknown> | null } {\n const firestore = useFirestore()\n const { data: host, status } = useFirestoreDoc<Record<string, unknown>>(\n () => (hostId ? doc(firestore, 'hosts', hostId) : null),\n [firestore, hostId],\n )\n const open =\n Boolean(hostId) &&\n status === 'success' &&\n Boolean(host) &&\n isHostPluginEnabled(org ?? null, host as never, BUNDLE_ID) &&\n checkEntitlement(org ?? null, 'bookings')\n return { open, host: host ?? null }\n}\n\nexport interface BookMeetingButtonProps {\n /**\n * The site whose services are offered — the mounted site, or at the\n * organization level the record's own capturing site; `null` for a\n * record no site has captured, which offers nothing.\n */\n hostId: string | null\n org?: Partial<AglynOrgBilling> | null\n /** The record the link is dropped from, carried as `?crm=kind:id`. */\n kind: CrmBookingRefKind\n recordId: string\n /**\n * When given, every service also offers **Insert into email** and the\n * chosen link is handed here — the email dialog's use.\n */\n onInsert?: (link: string) => void\n /** `chip` is the compact form the email dialog carries beside its fields. */\n variant?: 'button' | 'chip'\n}\n\n/**\n * THE \"BOOK A MEETING\" ACTION A RECORD CARRIES (AGL-2660): one control, and\n * the dialog it opens, mounted only while open. Renders NOTHING where the\n * site has no booking door, rather than a disabled control with a reason —\n * a site that has never enabled Bookings should not read every record page\n * as missing something.\n */\nexport function BookMeetingButton(props: BookMeetingButtonProps) {\n const { hostId, org, kind, recordId, onInsert, variant = 'button' } = props\n const door = useBookingDoor(hostId, org)\n const [open, setOpen] = useState(false)\n if (!door.open || !hostId || !recordId) return null\n const icon = <MdiIcon path={mdiCalendarClock.path} size={0.8} />\n return (\n <>\n {variant === 'chip' ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n icon={icon}\n label=\"Insert booking link\"\n onClick={() => setOpen(true)}\n // A flex item in the dialog's column, kept to its own width.\n sx={{ alignSelf: 'flex-start' }}\n />\n ) : (\n <Button size=\"small\" variant=\"outlined\" startIcon={icon} onClick={() => setOpen(true)}>\n {'Book a meeting'}\n </Button>\n )}\n {open ? (\n <BookMeetingDialog\n open\n onClose={() => setOpen(false)}\n hostId={hostId}\n host={door.host}\n org={org}\n kind={kind}\n recordId={recordId}\n onInsert={onInsert}\n />\n ) : null}\n </>\n )\n}\nBookMeetingButton.displayName = 'BookMeetingButton'\n\ninterface BookMeetingDialogProps {\n open: boolean\n onClose: () => void\n hostId: string\n /** The site document the door already read — its public naming. */\n host: Record<string, unknown> | null\n org?: Partial<AglynOrgBilling> | null\n kind: CrmBookingRefKind\n recordId: string\n onInsert?: (link: string) => void\n}\n\n/**\n * The site's bookable services, each with the link a visitor books it at.\n *\n * The link is the Bookings plugin's own builder over three facts read here:\n * the site's public naming (off the site document), the page the Booking\n * block lives on (the plugin's per-site `bookingPath` setting), and the\n * record this dialog was opened from. A site with no public address yet\n * lists its services and says why there is no link.\n */\nexport function BookMeetingDialog(props: BookMeetingDialogProps) {\n const { open, onClose, hostId, host, org, kind, recordId, onInsert } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { orgId } = useOrgDataScope({ hostId, orgId: org?.$id })\n const { config, ready: configReady } = useSitePluginConfig(orgId, hostId, BUNDLE_ID)\n const { data: serviceDocs, status } = useFirestoreCollection<Record<string, unknown>>(\n () => query(collection(firestore, 'hosts', hostId, 'services'), limit(100)),\n [firestore, hostId],\n { idField: '$id' },\n )\n const services = (serviceDocs ?? [])\n .filter((service) => !service['deletedAt'])\n .sort((a, b) => String(a['name'] ?? '').localeCompare(String(b['name'] ?? '')))\n\n // Where services are set up, for the empty state — addressable only\n // under a site route, where the URL names the org and the site.\n const params = useParams<{ orgSlug?: string; host?: string }>()\n const bookingsHref =\n params?.orgSlug && params?.host\n ? Aglyn.siteRecordLinks({\n orgSlug: String(params.orgSlug),\n host: String(params.host),\n }).bookings()\n : null\n\n const site = {\n cname: host?.['cname'] as string | null | undefined,\n subdomain: host?.['subdomain'] as string | null | undefined,\n }\n const path = configReady ? String(config['bookingPath'] ?? BOOKING_PATH_DEFAULT) : null\n const crmRef = { kind, id: recordId }\n\n const copy = useCallback(\n async (link: string) => {\n try {\n await navigator.clipboard.writeText(link)\n enqueueSnackbar('Booking link copied', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('The link could not be copied', { variant: 'error', allowDuplicate: true })\n }\n },\n [enqueueSnackbar],\n )\n\n return (\n <Dialog open={open} onClose={onClose} maxWidth=\"sm\" fullWidth>\n <DialogTitle>{'Book a meeting'}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Each link opens your site’s booking page on one service, with this ' +\n 'record attached so the booking lands on its timeline — whatever ' +\n 'address the person books with.'}\n </Typography>\n {status === 'loading' || path === null ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Loading…'}\n </Typography>\n ) : services.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No bookable services yet. '}\n {bookingsHref ? <AppLink href={bookingsHref}>{'Set up services'}</AppLink> : null}\n </Typography>\n ) : (\n <Stack spacing={1.5}>\n {services.map((service) => {\n const id = String(service['$id'])\n const name = String(service['name'] ?? '')\n const link = bookingLinkFor({ site, service: { id }, path, crmRef })\n const price = Number(service['priceUsd'] ?? 0)\n return (\n <Stack key={id} direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Stack sx={{ flex: 1, minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {name}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {`${Number(service['durationMinutes'] ?? 0)} min` +\n (price > 0 ? ` · $${price}` : ' · free')}\n </Typography>\n {link ? (\n <Typography variant=\"caption\" sx={{ wordBreak: 'break-all' }}>\n {link}\n </Typography>\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'This site has no public address yet, so there is no link.'}\n </Typography>\n )}\n </Stack>\n <Tooltip title=\"Copy link\">\n <span>\n <IconButton\n size=\"small\"\n aria-label={`Copy link for ${name}`}\n disabled={!link}\n onClick={() => link && void copy(link)}\n >\n <MdiIcon path={mdiContentCopy.path} size={0.8} />\n </IconButton>\n </span>\n </Tooltip>\n {onInsert ? (\n <Button\n size=\"small\"\n variant=\"outlined\"\n disabled={!link}\n onClick={() => {\n if (!link) return\n onInsert(link)\n onClose()\n }}\n >\n {'Insert into email'}\n </Button>\n ) : null}\n </Stack>\n )\n })}\n </Stack>\n )}\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose}>{'Close'}</Button>\n </DialogActions>\n </Dialog>\n )\n}\nBookMeetingDialog.displayName = 'BookMeetingDialog'\n\nexport default BookMeetingButton\n"],"names":["Aglyn","checkEntitlement","isHostPluginEnabled","BOOKING_PATH_DEFAULT","bookingLinkFor","BUNDLE_ID","mdiCalendarClock","mdiContentCopy","AppLink","MdiIcon","useSnackbar","useFirestore","useFirestoreCollection","useFirestoreDoc","useOrgDataScope","useSitePluginConfig","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","IconButton","Stack","Tooltip","Typography","collection","doc","limit","query","useParams","useCallback","useState","useBookingDoor","hostId","org","firestore","data","host","status","open","Boolean","BookMeetingButton","props","kind","recordId","onInsert","variant","door","setOpen","icon","path","size","label","onClick","sx","alignSelf","startIcon","BookMeetingDialog","onClose","displayName","config","enqueueSnackbar","orgId","$id","ready","configReady","serviceDocs","idField","services","filter","service","sort","a","b","String","localeCompare","params","bookingsHref","orgSlug","siteRecordLinks","bookings","site","cname","subdomain","crmRef","id","copy","link","navigator","clipboard","writeText","persist","error","console","allowDuplicate","maxWidth","fullWidth","display","flexDirection","gap","color","length","href","spacing","map","name","price","Number","direction","alignItems","flex","minWidth","noWrap","wordBreak","title","span","aria-label","disabled"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;AAEA,YAAYA,WAAW,eAAc;AACrC,SAEEC,gBAAgB,EAEhBC,mBAAmB,QACd,eAAc;AACrB,SAASC,oBAAoB,EAAEC,cAAc,QAAQ,uBAAmB;AACxE,SAASC,SAAS,QAAQ,gCAA4B;AACtD,SAASC,gBAAgB,EAAEC,cAAc,QAAQ,yBAAwB;AACzE,SAASC,OAAO,EAAEC,OAAO,QAAQ,uBAAsB;AACvD,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SACEC,YAAY,EACZC,sBAAsB,EACtBC,eAAe,EACfC,eAAe,EACfC,mBAAmB,QACd,iCAAgC;AACvC,SACEC,MAAM,EACNC,IAAI,EACJC,MAAM,EACNC,aAAa,EACbC,aAAa,EACbC,WAAW,EACXC,UAAU,EACVC,KAAK,EACLC,OAAO,EACPC,UAAU,QACL,gBAAe;AACtB,SAASC,UAAU,EAAEC,GAAG,EAAEC,KAAK,EAAEC,KAAK,QAAQ,qBAAoB;AAClE,SAASC,SAAS,QAAQ,kBAAiB;AAC3C,SAASC,WAAW,EAAEC,QAAQ,QAAQ,QAAO;AAG7C;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASC,eACdC,MAAiC,EACjCC,GAAqC;IAErC,MAAMC,YAAYzB;IAClB,MAAM,EAAE0B,MAAMC,IAAI,EAAEC,MAAM,EAAE,GAAG1B,gBAC7B,IAAOqB,SAASP,IAAIS,WAAW,SAASF,UAAU,MAClD;QAACE;QAAWF;KAAO;IAErB,MAAMM,OACJC,QAAQP,WACRK,WAAW,aACXE,QAAQH,SACRpC,oBAAoBiC,cAAAA,MAAO,MAAMG,MAAejC,cAChDJ,iBAAiBkC,cAAAA,MAAO,MAAM;IAChC,OAAO;QAAEK;QAAMF,IAAI,EAAEA,eAAAA,OAAQ;IAAK;AACpC;AAsBA;;;;;;CAMC,GACD,OAAO,SAASI,kBAAkBC,KAA6B;IAC7D,MAAM,EAAET,MAAM,EAAEC,GAAG,EAAES,IAAI,EAAEC,QAAQ,EAAEC,QAAQ,EAAEC,UAAU,QAAQ,EAAE,GAAGJ;IACtE,MAAMK,OAAOf,eAAeC,QAAQC;IACpC,MAAM,CAACK,MAAMS,QAAQ,GAAGjB,SAAS;IACjC,IAAI,CAACgB,KAAKR,IAAI,IAAI,CAACN,UAAU,CAACW,UAAU,OAAO;IAC/C,MAAMK,qBAAO,KAACzC;QAAQ0C,MAAM7C,iBAAiB6C,IAAI;QAAEC,MAAM;;IACzD,qBACE;;YACGL,YAAY,uBACX,KAAC9B;gBACCmC,MAAK;gBACLL,SAAQ;gBACRG,MAAMA;gBACNG,OAAM;gBACNC,SAAS,IAAML,QAAQ;gBACvB,6DAA6D;gBAC7DM,IAAI;oBAAEC,WAAW;gBAAa;+BAGhC,KAACxC;gBAAOoC,MAAK;gBAAQL,SAAQ;gBAAWU,WAAWP;gBAAMI,SAAS,IAAML,QAAQ;0BAC7E;;YAGJT,qBACC,KAACkB;gBACClB,IAAI;gBACJmB,SAAS,IAAMV,QAAQ;gBACvBf,QAAQA;gBACRI,MAAMU,KAAKV,IAAI;gBACfH,KAAKA;gBACLS,MAAMA;gBACNC,UAAUA;gBACVC,UAAUA;iBAEV;;;AAGV;AACAJ,kBAAkBkB,WAAW,GAAG;AAchC;;;;;;;;CAQC,GACD,OAAO,SAASF,kBAAkBf,KAA6B;QA8B3BkB;IA7BlC,MAAM,EAAErB,IAAI,EAAEmB,OAAO,EAAEzB,MAAM,EAAEI,IAAI,EAAEH,GAAG,EAAES,IAAI,EAAEC,QAAQ,EAAEC,QAAQ,EAAE,GAAGH;IACvE,MAAMP,YAAYzB;IAClB,MAAM,EAAEmD,eAAe,EAAE,GAAGpD;IAC5B,MAAM,EAAEqD,KAAK,EAAE,GAAGjD,gBAAgB;QAAEoB;QAAQ6B,KAAK,EAAE5B,uBAAAA,IAAK6B,GAAG;IAAC;IAC5D,MAAM,EAAEH,MAAM,EAAEI,OAAOC,WAAW,EAAE,GAAGnD,oBAAoBgD,OAAO7B,QAAQ7B;IAC1E,MAAM,EAAEgC,MAAM8B,WAAW,EAAE5B,MAAM,EAAE,GAAG3B,uBACpC,IAAMiB,MAAMH,WAAWU,WAAW,SAASF,QAAQ,aAAaN,MAAM,OACtE;QAACQ;QAAWF;KAAO,EACnB;QAAEkC,SAAS;IAAM;IAEnB,MAAMC,WAAW,CAACF,sBAAAA,cAAe,EAAE,EAChCG,MAAM,CAAC,CAACC,UAAY,CAACA,OAAO,CAAC,YAAY,EACzCC,IAAI,CAAC,CAACC,GAAGC;YAAaD,SAAsCC;eAA7CC,QAAOF,UAAAA,CAAC,CAAC,OAAO,YAATA,UAAa,IAAIG,aAAa,CAACD,QAAOD,UAAAA,CAAC,CAAC,OAAO,YAATA,UAAa;;IAE5E,oEAAoE;IACpE,gEAAgE;IAChE,MAAMG,SAAS/C;IACf,MAAMgD,eACJD,CAAAA,0BAAAA,OAAQE,OAAO,MAAIF,0BAAAA,OAAQvC,IAAI,IAC3BtC,MAAMgF,eAAe,CAAC;QACpBD,SAASJ,OAAOE,OAAOE,OAAO;QAC9BzC,MAAMqC,OAAOE,OAAOvC,IAAI;IAC1B,GAAG2C,QAAQ,KACX;IAEN,MAAMC,OAAO;QACXC,KAAK,EAAE7C,wBAAAA,IAAM,CAAC,QAAQ;QACtB8C,SAAS,EAAE9C,wBAAAA,IAAM,CAAC,YAAY;IAChC;IACA,MAAMa,OAAOe,cAAcS,QAAOd,sBAAAA,MAAM,CAAC,cAAc,YAArBA,sBAAyB1D,wBAAwB;IACnF,MAAMkF,SAAS;QAAEzC;QAAM0C,IAAIzC;IAAS;IAEpC,MAAM0C,OAAOxD,YACX,OAAOyD;QACL,IAAI;YACF,MAAMC,UAAUC,SAAS,CAACC,SAAS,CAACH;YACpC1B,gBAAgB,uBAAuB;gBAAEf,SAAS;gBAAW6C,SAAS;YAAM;QAC9E,EAAE,OAAOC,OAAO;YACdC,QAAQD,KAAK,CAACA;YACd/B,gBAAgB,gCAAgC;gBAAEf,SAAS;gBAASgD,gBAAgB;YAAK;QAC3F;IACF,GACA;QAACjC;KAAgB;IAGnB,qBACE,MAAC5C;QAAOsB,MAAMA;QAAMmB,SAASA;QAASqC,UAAS;QAAKC,SAAS;;0BAC3D,KAAC5E;0BAAa;;0BACd,MAACD;gBAAcmC,IAAI;oBAAE2C,SAAS;oBAAQC,eAAe;oBAAUC,KAAK;gBAAE;;kCACpE,KAAC3E;wBAAWsB,SAAQ;wBAAQsD,OAAM;kCAC/B,wEACC,qEACA;;oBAEH9D,WAAW,aAAaY,SAAS,qBAChC,KAAC1B;wBAAWsB,SAAQ;wBAAQsD,OAAM;kCAC/B;yBAEDhC,SAASiC,MAAM,KAAK,kBACtB,MAAC7E;wBAAWsB,SAAQ;wBAAQsD,OAAM;;4BAC/B;4BACAvB,6BAAe,KAACtE;gCAAQ+F,MAAMzB;0CAAe;iCAA+B;;uCAG/E,KAACvD;wBAAMiF,SAAS;kCACbnC,SAASoC,GAAG,CAAC,CAAClC;gCAEOA,eAECA,mBAQFA;4BAXnB,MAAMe,KAAKX,OAAOJ,OAAO,CAAC,MAAM;4BAChC,MAAMmC,OAAO/B,QAAOJ,gBAAAA,OAAO,CAAC,OAAO,YAAfA,gBAAmB;4BACvC,MAAMiB,OAAOpF,eAAe;gCAAE8E;gCAAMX,SAAS;oCAAEe;gCAAG;gCAAGnC;gCAAMkC;4BAAO;4BAClE,MAAMsB,QAAQC,QAAOrC,oBAAAA,OAAO,CAAC,WAAW,YAAnBA,oBAAuB;4BAC5C,qBACE,MAAChD;gCAAesF,WAAU;gCAAML,SAAS;gCAAGjD,IAAI;oCAAEuD,YAAY;gCAAS;;kDACrE,MAACvF;wCAAMgC,IAAI;4CAAEwD,MAAM;4CAAGC,UAAU;wCAAE;;0DAChC,KAACvF;gDAAWsB,SAAQ;gDAAQkE,MAAM;0DAC/BP;;0DAEH,KAACjF;gDAAWsB,SAAQ;gDAAUsD,OAAM;gDAAiBY,MAAM;0DACxD,GAAGL,QAAOrC,2BAAAA,OAAO,CAAC,kBAAkB,YAA1BA,2BAA8B,GAAG,IAAI,CAAC,GAC9CoC,CAAAA,QAAQ,IAAI,CAAC,IAAI,EAAEA,OAAO,GAAG,SAAQ;;4CAEzCnB,qBACC,KAAC/D;gDAAWsB,SAAQ;gDAAUQ,IAAI;oDAAE2D,WAAW;gDAAY;0DACxD1B;+DAGH,KAAC/D;gDAAWsB,SAAQ;gDAAUsD,OAAM;0DACjC;;;;kDAIP,KAAC7E;wCAAQ2F,OAAM;kDACb,cAAA,KAACC;sDACC,cAAA,KAAC9F;gDACC8B,MAAK;gDACLiE,cAAY,CAAC,cAAc,EAAEX,MAAM;gDACnCY,UAAU,CAAC9B;gDACXlC,SAAS,IAAMkC,QAAQ,KAAKD,KAAKC;0DAEjC,cAAA,KAAC/E;oDAAQ0C,MAAM5C,eAAe4C,IAAI;oDAAEC,MAAM;;;;;oCAI/CN,yBACC,KAAC9B;wCACCoC,MAAK;wCACLL,SAAQ;wCACRuE,UAAU,CAAC9B;wCACXlC,SAAS;4CACP,IAAI,CAACkC,MAAM;4CACX1C,SAAS0C;4CACT7B;wCACF;kDAEC;yCAED;;+BA5CM2B;wBA+ChB;;;;0BAIN,KAACnE;0BACC,cAAA,KAACH;oBAAOsC,SAASK;8BAAU;;;;;AAInC;AACAD,kBAAkBE,WAAW,GAAG;AAEhC,eAAelB,kBAAiB"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import * as Aglyn from '@aglyn/aglyn';
|
|
18
|
+
export declare const ID: Aglyn.ComponentId;
|
|
19
|
+
export interface BookingProps {
|
|
20
|
+
/** Heading above the widget; empty hides it. */
|
|
21
|
+
heading?: string;
|
|
22
|
+
successMessage?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Booking widget (AGL-160): service picker → open-slot picker → contact
|
|
26
|
+
* form, driven by the tenant /api/bookings endpoints (server-validated,
|
|
27
|
+
* double-booking safe). Inert placeholder in the besigner/preview (no
|
|
28
|
+
* SiteContext host id) so editing never creates bookings.
|
|
29
|
+
*/
|
|
30
|
+
declare const Booking: import("react").ForwardRefExoticComponent<BookingProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
31
|
+
export declare const schema: Aglyn.ComponentSchema<BookingProps>;
|
|
32
|
+
export declare const presets: Aglyn.PresetSchema[];
|
|
33
|
+
export default Booking;
|