@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.
Files changed (63) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +7 -0
  3. package/package.json +53 -0
  4. package/src/index.d.ts +19 -0
  5. package/src/index.js +20 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/book-meeting-action.d.ts +91 -0
  8. package/src/lib/components/book-meeting-action.js +296 -0
  9. package/src/lib/components/book-meeting-action.js.map +1 -0
  10. package/src/lib/components/booking.d.ts +33 -0
  11. package/src/lib/components/booking.js +463 -0
  12. package/src/lib/components/booking.js.map +1 -0
  13. package/src/lib/components/bookings-console-page.d.ts +12 -0
  14. package/src/lib/components/bookings-console-page.js +743 -0
  15. package/src/lib/components/bookings-console-page.js.map +1 -0
  16. package/src/lib/constants/bundle-common.d.ts +26 -0
  17. package/src/lib/constants/bundle-common.js +26 -0
  18. package/src/lib/constants/bundle-common.js.map +1 -0
  19. package/src/lib/model/booking-purchase-analytics.d.ts +129 -0
  20. package/src/lib/model/booking-purchase-analytics.js +197 -0
  21. package/src/lib/model/booking-purchase-analytics.js.map +1 -0
  22. package/src/lib/model/bookings.d.ts +141 -0
  23. package/src/lib/model/bookings.js +124 -0
  24. package/src/lib/model/bookings.js.map +1 -0
  25. package/src/lib/model/index.d.ts +22 -0
  26. package/src/lib/model/index.js +22 -0
  27. package/src/lib/model/index.js.map +1 -0
  28. package/src/lib/plugin-config.d.ts +24 -0
  29. package/src/lib/plugin-config.js +51 -0
  30. package/src/lib/plugin-config.js.map +1 -0
  31. package/src/lib/plugin.d.ts +22 -0
  32. package/src/lib/plugin.js +70 -0
  33. package/src/lib/plugin.js.map +1 -0
  34. package/src/lib/server/billing-webhook.d.ts +23 -0
  35. package/src/lib/server/billing-webhook.js +345 -0
  36. package/src/lib/server/billing-webhook.js.map +1 -0
  37. package/src/lib/server/booking-analytics.d.ts +68 -0
  38. package/src/lib/server/booking-analytics.js +115 -0
  39. package/src/lib/server/booking-analytics.js.map +1 -0
  40. package/src/lib/server/booking-crm.d.ts +39 -0
  41. package/src/lib/server/booking-crm.js +67 -0
  42. package/src/lib/server/booking-crm.js.map +1 -0
  43. package/src/lib/server/booking-figures.d.ts +32 -0
  44. package/src/lib/server/booking-figures.js +155 -0
  45. package/src/lib/server/booking-figures.js.map +1 -0
  46. package/src/lib/server/refund.d.ts +65 -0
  47. package/src/lib/server/refund.js +292 -0
  48. package/src/lib/server/refund.js.map +1 -0
  49. package/src/lib/server.d.ts +63 -0
  50. package/src/lib/server.js +968 -0
  51. package/src/lib/server.js.map +1 -0
  52. package/src/lib/site.d.ts +27 -0
  53. package/src/lib/site.js +50 -0
  54. package/src/lib/site.js.map +1 -0
  55. package/src/lib/testing/stand-in-tax-profile.d.ts +29 -0
  56. package/src/lib/testing/stand-in-tax-profile.js +64 -0
  57. package/src/lib/testing/stand-in-tax-profile.js.map +1 -0
  58. package/src/lib/utils/generate-preset-id.d.ts +19 -0
  59. package/src/lib/utils/generate-preset-id.js +25 -0
  60. package/src/lib/utils/generate-preset-id.js.map +1 -0
  61. package/src/lib/utils/use-booking-purchase-event.d.ts +65 -0
  62. package/src/lib/utils/use-booking-purchase-event.js +147 -0
  63. 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":"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"}
@@ -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;