@aglyn/plugins-bookings 1.0.0-beta.218 → 1.0.0-beta.219

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-bookings",
3
- "version": "1.0.0-beta.218",
3
+ "version": "1.0.0-beta.219",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,16 +25,16 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.218",
29
- "@aglyn/shared-data-mdi": "1.0.0-beta.218",
30
- "@aglyn/shared-ui-jsx": "1.0.0-beta.218",
31
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.218",
32
- "@aglyn/shared-util-email": "1.0.0-beta.218",
33
- "@aglyn/shared-util-http": "1.0.0-beta.218",
34
- "@aglyn/shared-util-timestamp": "1.0.0-beta.218",
35
- "@aglyn/tenant-data-admin": "1.0.0-beta.218",
36
- "@aglyn/tenant-feature-instance": "1.0.0-beta.218",
37
- "@aglyn/tenant-runtime": "1.0.0-beta.218",
28
+ "@aglyn/aglyn": "1.0.0-beta.219",
29
+ "@aglyn/shared-data-mdi": "1.0.0-beta.219",
30
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.219",
31
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.219",
32
+ "@aglyn/shared-util-email": "1.0.0-beta.219",
33
+ "@aglyn/shared-util-http": "1.0.0-beta.219",
34
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.219",
35
+ "@aglyn/tenant-data-admin": "1.0.0-beta.219",
36
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.219",
37
+ "@aglyn/tenant-runtime": "1.0.0-beta.219",
38
38
  "@swc/helpers": "0.5.23"
39
39
  },
40
40
  "peerDependencies": {
@@ -15,8 +15,8 @@
15
15
  * limitations under the License.
16
16
  */ 'use client';
17
17
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
18
- import * as Aglyn from "@aglyn/aglyn";
19
18
  import { checkEntitlement, isHostPluginEnabled } from "@aglyn/aglyn";
19
+ import { bookingsPageHref } from "../model/bookings-record-routes.js";
20
20
  import { BOOKING_PATH_DEFAULT, bookingLinkFor } from "../model/bookings.js";
21
21
  import { BUNDLE_ID } from "../constants/bundle-common.js";
22
22
  import { mdiCalendarClock, mdiContentCopy } from "@aglyn/shared-data-mdi";
@@ -132,10 +132,10 @@ BookMeetingButton.displayName = 'BookMeetingButton';
132
132
  // Where services are set up, for the empty state — addressable only
133
133
  // under a site route, where the URL names the org and the site.
134
134
  const params = useParams();
135
- const bookingsHref = (params == null ? void 0 : params.orgSlug) && (params == null ? void 0 : params.host) ? Aglyn.siteRecordLinks({
135
+ const bookingsHref = (params == null ? void 0 : params.orgSlug) && (params == null ? void 0 : params.host) ? bookingsPageHref({
136
136
  orgSlug: String(params.orgSlug),
137
137
  host: String(params.host)
138
- }).bookings() : null;
138
+ }) : null;
139
139
  const site = {
140
140
  cname: host == null ? void 0 : host['cname'],
141
141
  subdomain: host == null ? void 0 : host['subdomain']
@@ -1 +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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"}
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 {\n type AglynOrgBilling,\n checkEntitlement,\n type CrmBookingRefKind,\n isHostPluginEnabled,\n} from '@aglyn/aglyn'\nimport { bookingsPageHref } from '../model/bookings-record-routes'\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 ? bookingsPageHref({ orgSlug: String(params.orgSlug), host: String(params.host) })\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":["checkEntitlement","isHostPluginEnabled","bookingsPageHref","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","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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"}
@@ -16,8 +16,10 @@
16
16
  */ 'use client';
17
17
  import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
- import { checkQuota, crmContactByEmailHref, pluginDocsHelp } from "@aglyn/aglyn";
20
- import { BOOKINGS_BOOKER_PARAM, normalizeContactEmail } from "@aglyn/aglyn";
19
+ import { checkQuota, pluginDocsHelp } from "@aglyn/aglyn";
20
+ import { normalizeContactEmail } from "@aglyn/aglyn";
21
+ import { pluginRecordByEmailHref } from "@aglyn/aglyn/plugin-manager/plugin-record-routes";
22
+ import { BOOKINGS_BOOKER_PARAM } from "../model/bookings-record-routes.js";
21
23
  import { PLATFORM_BRAND_NAME } from "@aglyn/aglyn/app-utils/platform-brand";
22
24
  import { describePaymentRisk } from "@aglyn/aglyn/app-utils/payment-risk";
23
25
  import { isBookingReminderDue } from "../model/index.js";
@@ -88,17 +90,17 @@ function formatWindows(windows) {
88
90
  * Where a booker's CONTACT is (AGL-2622). A booking updated a person in
89
91
  * the CRM by the address it carried, and the row links there by that
90
92
  * address — the Contacts list holds the id nothing here does and opens
91
- * the record on one match. The address comes from the SHARED builder
92
- * rather than the CRM plugin, the way the commerce dialog's does: a
93
- * plugin importing a sibling is a cycle waiting for its third edge, and
94
- * the shared builder is pinned against the CRM's own routes by a spec.
95
- * `null` until the route params settle, so no row renders a link to
96
- * nowhere.
93
+ * the record on one match. The address is the one the plugin that keeps
94
+ * contacts publishes, asked of the record-route registry the way the
95
+ * commerce dialog asks: a plugin importing a sibling is a cycle waiting
96
+ * for its third edge. `null` until the route params settle, and where no
97
+ * plugin keeps contacts, so no row renders a link to nowhere.
97
98
  */ const params = useParams();
98
99
  const siteContext = (params == null ? void 0 : params.orgSlug) && (params == null ? void 0 : params.host) ? {
99
100
  orgSlug: String(params.orgSlug),
100
101
  host: String(params.host)
101
102
  } : null;
103
+ const contactHrefOf = (email)=>siteContext && email ? pluginRecordByEmailHref('contact', siteContext, String(email)) : null;
102
104
  /*
103
105
  * The list narrowed to one booker (AGL-2660): a contact's record links
104
106
  * here with `?email=`, and the page answers with every booking that
@@ -537,7 +539,9 @@ function formatWindows(windows) {
537
539
  children: bookerFilter ? 'No bookings for this address.' : 'No upcoming bookings.'
538
540
  }) : /*#__PURE__*/ _jsx(Stack, {
539
541
  spacing: 1,
540
- children: shown.map((booking)=>/*#__PURE__*/ _jsxs(Stack, {
542
+ children: shown.map((booking)=>{
543
+ var _contactHrefOf;
544
+ return /*#__PURE__*/ _jsxs(Stack, {
541
545
  direction: "row",
542
546
  spacing: 1,
543
547
  sx: {
@@ -569,13 +573,13 @@ function formatWindows(windows) {
569
573
  /*#__PURE__*/ _jsx(PaymentRiskChip, {
570
574
  risk: booking.paymentRisk
571
575
  }),
572
- siteContext && booking.email ? /*#__PURE__*/ _jsx(Button, _extends({
576
+ contactHrefOf(booking.email) ? /*#__PURE__*/ _jsx(Button, _extends({
573
577
  component: AppLink
574
578
  }, {
575
579
  componentVariant: 'naked',
576
580
  nativeButton: false
577
581
  }, {
578
- href: crmContactByEmailHref(siteContext, String(booking.email)),
582
+ href: (_contactHrefOf = contactHrefOf(booking.email)) != null ? _contactHrefOf : undefined,
579
583
  size: "small",
580
584
  children: 'View in CRM'
581
585
  })) : null,
@@ -586,7 +590,8 @@ function formatWindows(windows) {
586
590
  children: 'Cancel'
587
591
  }) : null
588
592
  ]
589
- }, booking.$id))
593
+ }, booking.$id);
594
+ })
590
595
  })
591
596
  ]
592
597
  }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/components/bookings-console-page.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'use client'\n\nimport { checkQuota, crmContactByEmailHref, pluginDocsHelp } from '@aglyn/aglyn'\nimport { BOOKINGS_BOOKER_PARAM, normalizeContactEmail } from '@aglyn/aglyn'\nimport { PLATFORM_BRAND_NAME } from '@aglyn/aglyn/app-utils/platform-brand'\nimport { describePaymentRisk, type PaymentRisk } from '@aglyn/aglyn/app-utils/payment-risk'\nimport { type ConsolePluginPageProps } from '@aglyn/aglyn'\nimport { type HostBookingService, isBookingReminderDue } from '../model'\nimport { AppLink, CardDisplay, HelpTip, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport QuotaReadoutComponent from '@aglyn/shared-ui-jsx/components/quota-readout.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport {\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n FormControlLabel,\n Stack,\n Switch,\n TextField,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n doc,\n limit,\n orderBy,\n query,\n setDoc,\n updateDoc,\n} from 'firebase/firestore'\nimport { useParams, useSearchParams } from 'next/navigation'\nimport { useCallback, useState } from 'react'\nimport {\n useFirestore,\n useFirestoreCollection,\n useHostResourceApi,\n useUser,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\n\nconst WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']\n\n/** \"09:00-12:00, 13:00-17:00\" → open intervals in minutes. */\nfunction parseWindows(input: string): Array<{ start: number; end: number }> {\n const windows: Array<{ start: number; end: number }> = []\n for (const chunk of input.split(',')) {\n const match = chunk.trim().match(/^(\\d{1,2}):(\\d{2})\\s*-\\s*(\\d{1,2}):(\\d{2})$/)\n if (!match) continue\n const start = Number(match[1]) * 60 + Number(match[2])\n const end = Number(match[3]) * 60 + Number(match[4])\n if (end > start && end <= 24 * 60) windows.push({ start, end })\n }\n return windows\n}\n\nfunction formatWindows(\n windows: Array<{ start: number; end: number }> | undefined,\n): string {\n const pad = (minutes: number) =>\n `${String(Math.floor(minutes / 60)).padStart(2, '0')}:${String(\n minutes % 60,\n ).padStart(2, '0')}`\n return (windows ?? [])\n .map((window) => `${pad(window.start)}-${pad(window.end)}`)\n .join(', ')\n}\n\ninterface ServiceDraft {\n id: string | null\n name: string\n durationMinutes: string\n priceUsd: string\n timezone: string\n description: string\n /** Per-weekday window text, e.g. \"09:00-17:00\". */\n windowText: string[]\n /** The service's two CRM switches (AGL-2660) — see `HostBookingService`. */\n crmMeetingActivity: boolean\n crmFollowUpTask: boolean\n}\n\n/** A fraud warning, review or chargeback on the booking's payment (AGL-3360). */\nfunction PaymentRiskChip({ risk }: { risk: PaymentRisk | undefined }) {\n const described = describePaymentRisk(risk)\n if (!described) return null\n return (\n <Tooltip title={described.detail}>\n <Chip label={described.label} size=\"small\" color=\"warning\" />\n </Tooltip>\n )\n}\n\n/**\n * Bookings manager (AGL-159): bookable services with weekly availability\n * windows, and the upcoming-bookings list with cancel. Visitors book via\n * the org /api/bookings endpoints; confirmations email through the\n * env-gated Resend path. Plan-gated (`bookings` flag + `servicesPerHost`).\n */\nexport function BookingsConsolePage(props: ConsolePluginPageProps) {\n const { hostId, entitled, org } = props\n const firestore = useFirestore()\n const createHostResource = useHostResourceApi()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n // The id token the refund route authenticates with (AGL-2315).\n const { data: user } = useUser()\n /*\n * Where a booker's CONTACT is (AGL-2622). A booking updated a person in\n * the CRM by the address it carried, and the row links there by that\n * address — the Contacts list holds the id nothing here does and opens\n * the record on one match. The address comes from the SHARED builder\n * rather than the CRM plugin, the way the commerce dialog's does: a\n * plugin importing a sibling is a cycle waiting for its third edge, and\n * the shared builder is pinned against the CRM's own routes by a spec.\n * `null` until the route params settle, so no row renders a link to\n * nowhere.\n */\n const params = useParams<{ orgSlug?: string; host?: string }>()\n const siteContext =\n params?.orgSlug && params?.host\n ? { orgSlug: String(params.orgSlug), host: String(params.host) }\n : null\n /*\n * The list narrowed to one booker (AGL-2660): a contact's record links\n * here with `?email=`, and the page answers with every booking that\n * address holds — past ones too, because that link asks about a\n * person's history rather than the week ahead. The same match rule as\n * the row's own \"View in CRM\": the address, normalized, equal. `null`\n * outside a router, where there is no query to read.\n */\n const searchParams = useSearchParams()\n const bookerFilter = normalizeContactEmail(searchParams?.get(BOOKINGS_BOOKER_PARAM))\n const { basePath } = props\n\n const {\n data: serviceDocs,\n status: servicesStatus,\n /**\n * The rows this editor is seeded from are unconfirmed by the server\n * (AGL-1358). Editing copies a whole stored service into `draft` and\n * writes all of it back, so `merge: true` protects nothing. `windows` is\n * the weekly availability map, rebuilt in full on every save: a cached\n * seed re-opens slots that were closed and closes ones customers can\n * already book, and `priceUsd` goes back to whatever that snapshot\n * charged.\n */\n fromCache: servicesFromCache,\n } = useFirestoreCollection<any>(\n () =>\n query(collection(firestore, 'hosts', hostId, 'services'), limit(100)),\n [firestore, hostId],\n { idField: '$id' },\n )\n const { data: bookingDocs } = useFirestoreCollection<any>(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'bookings'),\n orderBy('startsAtMs', 'desc'),\n limit(100),\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const services = [...(serviceDocs ?? [])]\n .filter((service: any) => !service.deletedAt)\n .sort((a: any, b: any) =>\n String(a.name ?? '').localeCompare(String(b.name ?? '')),\n )\n const upcoming = [...(bookingDocs ?? [])]\n .filter((booking: any) => booking.endsAtMs >= Date.now())\n .sort((a: any, b: any) => a.startsAtMs - b.startsAtMs)\n // Newest first when narrowed to a booker — the listener already orders\n // by start, descending — so the history reads down from the latest.\n const shown = bookerFilter\n ? (bookingDocs ?? []).filter(\n (booking: any) => normalizeContactEmail(booking.email) === bookerFilter,\n )\n : upcoming\n\n /**\n * The 24-hour reminder queue (AGL-2431).\n *\n * Reminders are sent by the `booking-reminders` beat, not by anything on\n * this page, and until that beat existed they were never sent at all — the\n * feature had a designed email template, white-label branding and an\n * `x-cron-secret` door, and no scheduler pointed at it. So the merchant had\n * no way to tell working from broken, and still would not if this card\n * merely asserted that reminders happen.\n *\n * READ-ONLY, and deliberately no \"send now\" button: the beat runs hourly\n * and stamps `reminderSentAt`, so a manual sender would race it for a\n * reminder nobody can un-send. Same call AGL-2227 made for the recovery\n * queue.\n *\n * `isBookingReminderDue` is the sender's own predicate, against one instant\n * shared by both counts, so this is the queue the next pass will drain\n * rather than an approximation of it.\n */\n const reminderNowMs = Date.now()\n const reminderQueue = (bookingDocs ?? []).filter((booking: any) =>\n isBookingReminderDue(booking, reminderNowMs),\n ).length\n const remindersSent = (bookingDocs ?? []).filter(\n (booking: any) => booking.reminderSentAt,\n ).length\n\n const [draft, setDraft] = useState<ServiceDraft | null>(null)\n\n const handleAdd = useCallback(() => {\n if (!entitled) {\n return void enqueueSnackbar(\n 'Bookings require a Starter plan — see Billing to upgrade',\n { variant: 'warning', persist: false },\n )\n }\n const quota = checkQuota(org, 'servicesPerHost', services.length)\n if (!quota.allowed) {\n return void enqueueSnackbar(\n `Service limit reached (${quota.limit}) — upgrade in Billing`,\n { variant: 'warning', persist: false },\n )\n }\n setDraft({\n id: null,\n name: '',\n durationMinutes: '30',\n priceUsd: '0',\n timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC',\n description: '',\n windowText: WEEKDAYS.map((_, index) =>\n index >= 1 && index <= 5 ? '09:00-17:00' : '',\n ),\n // A booking is a meeting the record has; the follow-up is opt-in.\n crmMeetingActivity: true,\n crmFollowUpTask: false,\n })\n }, [entitled, org, services.length, enqueueSnackbar])\n\n const handleSave = useCallback(async () => {\n if (!draft || !draft.name.trim()) return\n const windows: HostBookingService['windows'] = {}\n draft.windowText.forEach((text, weekday) => {\n const parsed = parseWindows(text)\n if (parsed.length) windows[weekday] = parsed\n })\n const fields = {\n name: draft.name.trim().slice(0, 80),\n durationMinutes: Math.max(\n 5,\n Math.min(480, Math.round(Number(draft.durationMinutes) || 30)),\n ),\n priceUsd: Math.max(0, Math.round(Number(draft.priceUsd) || 0)),\n timezone: draft.timezone.trim() || 'UTC',\n ...(draft.description.trim() && {\n description: draft.description.trim().slice(0, 500),\n }),\n windows,\n // Written explicitly rather than only when on: an edit that switches\n // the meeting off has to land a `false`, and `merge: true` would keep\n // an absent key exactly as it was.\n crmMeetingActivity: draft.crmMeetingActivity,\n crmFollowUpTask: draft.crmFollowUpTask,\n }\n try {\n if (draft.id) {\n /**\n * Edit stays client-direct (no quota consumed) — and is refused when\n * its seed was never confirmed by the server (AGL-1358). `fields` is\n * every editable key of the stored service, `windows` included, so\n * `merge: true` has nothing left to protect.\n *\n * The guard WRAPS the write. An early return is a shape you can keep\n * while losing the protection; here the write is only reachable\n * through the verdict.\n */\n const verdict = await writeGuardedBySeed(\n {\n subject: 'service',\n unreadable: servicesStatus === 'error',\n fromCache: servicesFromCache,\n },\n async () => {\n await setDoc(\n doc(firestore, 'hosts', hostId, 'services', draft.id),\n { ...fields, updatedAt: Timestamp.now() },\n { merge: true },\n )\n },\n )\n // Before `setDraft(null)`, so a refusal keeps the dialog open with\n // every window that was typed. A save that silently does nothing\n // sends the user back to retype a form that will be refused again\n // just as quietly.\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n } else {\n // New service rides the quota-enforcing resources API (AGL-473) —\n // it also re-checks the `bookings` entitlement server-side.\n await createHostResource({ hostId, resource: 'service', data: fields })\n }\n setDraft(null)\n enqueueSnackbar('Service saved', { variant: 'success', persist: false })\n } catch (error: any) {\n console.error(error)\n enqueueSnackbar(error?.message ?? 'An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n }, [\n draft,\n firestore,\n hostId,\n createHostResource,\n enqueueSnackbar,\n servicesStatus,\n servicesFromCache,\n ])\n\n const handleDeleteService = useCallback(\n (service: any) => async () => {\n const confirmed = await confirm({\n title: 'Delete this service?',\n description: `\"${service.name}\" stops accepting bookings.`,\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await updateDoc(doc(firestore, 'hosts', hostId, 'services', service.$id), {\n deletedAt: Timestamp.now(),\n })\n },\n [confirm, firestore, hostId],\n )\n\n /**\n * Cancelling a PAID booking refunds the guest (AGL-2315).\n *\n * This used to be one `updateDoc` writing `status: 'canceled'`, for a paid\n * booking exactly as for a free one. The slot reopened, the appointment was\n * gone, and the guest had paid for it — the money moved nowhere and nothing\n * in the console said so. That was survivable only while the charge settled\n * in Aglyn's own balance and could be given back by hand; a paid booking is\n * a destination charge now, so the funds are at the MERCHANT and there is no\n * later opportunity to notice.\n *\n * So a paid booking cancels through the refund route, which reverses the\n * merchant's share and Aglyn's fee, and which sets `canceled` itself once\n * the money is actually back. A FAILED refund deliberately leaves the\n * booking standing: a cancelled-and-unrefunded appointment is the worst of\n * the three outcomes, and an admin who sees the row still there knows the\n * cancel did not happen.\n */\n const handleCancelBooking = useCallback(\n (booking: any) => async () => {\n const paidCents = Math.max(0, Number(booking.paidAmountCents ?? 0))\n const refundedCents = Math.max(0, Number(booking.refundedCents ?? 0))\n const outstandingCents = Math.max(0, paidCents - refundedCents)\n const refundable = outstandingCents > 0\n const confirmed = await confirm({\n title: 'Cancel this booking?',\n description:\n `${booking.name} (${booking.email}) — the slot reopens.` +\n (refundable\n ? ` $${(outstandingCents / 100).toFixed(2)} is refunded to them` +\n ' through Stripe.'\n : ''),\n confirmationText: refundable ? 'Cancel and refund' : 'Cancel booking',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n\n if (refundable) {\n // `randomUUID` needs a secure context, which the console always is,\n // but fall back rather than throw on a refund.\n const attemptKey =\n globalThis.crypto?.randomUUID?.() ??\n `${Date.now()}-${Math.random().toString(36).slice(2)}`\n try {\n const response = await authorizedFetch(user, '/api/bookings/refund', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Idempotency-Key': attemptKey,\n },\n body: JSON.stringify({ hostId, bookingId: booking.$id }),\n })\n const payload = await response.json().catch(() => null)\n if (!response.ok) {\n // A 409 is a guard REFUSING with a body that says what to do\n // instead — a booking paid before the PaymentIntent was recorded\n // names the Stripe dashboard — not the refund machinery failing.\n // It surfaces verbatim, the AGL-1818 rule.\n enqueueSnackbar(payload?.error ?? 'Refund failed', {\n variant: response.status === 409 ? 'warning' : 'error',\n allowDuplicate: true,\n })\n // The booking stays. See the note above: cancelled-and-unrefunded\n // is worse than not cancelled.\n return\n }\n // The route wrote `canceled` itself once the money was back.\n enqueueSnackbar('Booking canceled and refunded', {\n variant: 'success',\n persist: false,\n })\n return\n } catch {\n enqueueSnackbar('Refund failed — the booking was not canceled', {\n variant: 'error',\n allowDuplicate: true,\n })\n return\n }\n }\n\n await updateDoc(\n doc(firestore, 'hosts', hostId, 'bookings', booking.$id),\n { status: 'canceled' },\n )\n enqueueSnackbar('Booking canceled', {\n variant: 'success',\n persist: false,\n })\n },\n [confirm, firestore, hostId, enqueueSnackbar, user],\n )\n\n return (\n <Stack spacing={3}>\n <CardDisplay\n header={'Services'}\n help={pluginDocsHelp('bookings', {\n anchor: '#set-up-bookings',\n excerpt:\n 'The bookable services this site offers — how long each takes, ' +\n 'what it costs, and the hours it can be booked in.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n {services.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Define a bookable service — duration, price, and weekly ' +\n 'availability — then visitors can book from your site.'}\n </Typography>\n ) : (\n services.map((service: any) => (\n <Stack\n key={service.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Stack sx={{ flex: 1, minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {service.name}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {`${service.durationMinutes} min` +\n (Number(service.priceUsd) > 0\n ? ` · $${service.priceUsd}`\n : ' · free') +\n ` · ${service.timezone ?? 'UTC'}`}\n </Typography>\n </Stack>\n <Button\n size=\"small\"\n onClick={() =>\n setDraft({\n id: service.$id,\n name: service.name ?? '',\n durationMinutes: String(service.durationMinutes ?? 30),\n priceUsd: String(service.priceUsd ?? 0),\n timezone: service.timezone ?? 'UTC',\n description: service.description ?? '',\n windowText: WEEKDAYS.map((_, index) =>\n formatWindows(service.windows?.[index]),\n ),\n // Absent reads as ON for the meeting and OFF for the\n // follow-up — the model's defaults, seeded so the\n // switches show what the service actually does.\n crmMeetingActivity: service.crmMeetingActivity !== false,\n crmFollowUpTask: service.crmFollowUpTask === true,\n })\n }\n >\n {'Edit'}\n </Button>\n <Button\n size=\"small\"\n color=\"error\"\n onClick={handleDeleteService(service)}\n >\n {'Delete'}\n </Button>\n </Stack>\n ))\n )}\n <Button\n size=\"small\"\n color=\"primary\"\n sx={{ alignSelf: 'flex-start' }}\n onClick={handleAdd}\n >\n {'Add service'}\n </Button>\n {/* The cap, standing rather than only on refusal (AGL-2113).\n `services.length` is the same count `handleAdd` hands to\n `checkQuota`, so the readout and the gate cannot disagree. */}\n <QuotaReadoutComponent\n ready={org != null}\n used={services.length}\n limit={checkQuota(org, 'servicesPerHost', services.length).limit}\n noun=\"service\"\n />\n </Stack>\n </CardDisplay>\n\n <CardDisplay\n header={bookerFilter ? `Bookings for ${bookerFilter}` : 'Upcoming bookings'}\n help={pluginDocsHelp('bookings', {\n anchor: '#manage',\n excerpt:\n 'Bookings taken on this site, with the customer and the slot each ' +\n 'one holds.',\n })}\n contentGutterX\n contentGutterY\n >\n {bookerFilter ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n component=\"div\"\n sx={{ mb: 1 }}\n >\n {'Every booking this address holds, newest first. '}\n {basePath ? <AppLink href={basePath}>{'Show upcoming bookings'}</AppLink> : null}\n </Typography>\n ) : null}\n {/* What the reminder beat will do next, and what it has done. Named\n \"24-hour reminders\" rather than \"emails\" because that is the only\n mail this queue governs — confirmations go out on booking. */}\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n component=\"div\"\n sx={{ mb: 1 }}\n >\n {`24-hour reminders · ${reminderQueue} due in the next pass · ` +\n `${remindersSent} already sent`}\n {/* Its OWN heading, not the card's `#manage`: a merchant reading\n this line wants the timing and the once-only rule, and landing\n them on \"manage your bookings\" is the presence-not-correctness\n failure a help-coverage check cannot see. */}\n <HelpTip\n {...pluginDocsHelp('bookings', { anchor: '#reminders' })}\n sx={{ ml: 0.5, fontSize: '0.9em' }}\n />\n </Typography>\n {shown.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {bookerFilter ? 'No bookings for this address.' : 'No upcoming bookings.'}\n </Typography>\n ) : (\n <Stack spacing={1}>\n {shown.map((booking: any) => (\n <Stack\n key={booking.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Stack sx={{ flex: 1, minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {`${booking.serviceName} — ${booking.name}`}\n {booking.status === 'canceled' ? ' (canceled)' : ''}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {`${new Date(booking.startsAtMs).toLocaleString()} · ${\n booking.email\n }`}\n </Typography>\n </Stack>\n <PaymentRiskChip risk={booking.paymentRisk} />\n {siteContext && booking.email ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={crmContactByEmailHref(siteContext, String(booking.email))}\n size=\"small\"\n >\n {'View in CRM'}\n </Button>\n ) : null}\n {booking.status !== 'canceled' ? (\n <Button\n size=\"small\"\n color=\"error\"\n onClick={handleCancelBooking(booking)}\n >\n {'Cancel'}\n </Button>\n ) : null}\n </Stack>\n ))}\n </Stack>\n )}\n </CardDisplay>\n\n <Dialog\n open={Boolean(draft)}\n onClose={() => setDraft(null)}\n maxWidth=\"sm\"\n fullWidth\n >\n <DialogTitle>{draft?.id ? 'Edit service' : 'Add service'}</DialogTitle>\n <DialogContent\n sx={{ display: 'flex', flexDirection: 'column', gap: 1.5, pt: 1 }}\n >\n <TextField\n label=\"Name\"\n value={draft?.name ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, name: event.target.value } : prev,\n )\n }\n size=\"small\"\n autoFocus\n sx={{ mt: 1 }}\n />\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Duration (minutes)\"\n value={draft?.durationMinutes ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n durationMinutes: event.target.value.replace(\n /[^0-9]/g,\n '',\n ),\n }\n : prev,\n )\n }\n size=\"small\"\n />\n <TextField\n label=\"Price (USD, 0 = free)\"\n // WHAT THIS PRICE DOES NOT INCLUDE (AGL-2028).\n //\n // A paid booking is charged exactly this figure: the handler\n // sends no tax parameter at all, by a stated decision (AGL-2000)\n // — whether a service is taxable is jurisdiction-specific and\n // frequently the opposite answer from goods, so applying the\n // commerce plugin's GOODS sales rate would be confidently wrong\n // rather than merely absent.\n //\n // That decision was recorded in the source and in the\n // `storefrontTaxCollected` ledger, and in neither place can the\n // merchant see it. A service business is one of the three named\n // ICPs, and one who configured a rate in Commerce → Settings →\n // Taxes has every reason to assume it covers the appointments\n // they sell on the same site. Saying so here is the half of\n // AGL-2028 that holds however the rate question is answered.\n //\n // States the MECHANISM and makes no remittance determination —\n // the same line `describeOrderTaxMode` holds. Who owes which\n // authority what attaches by operation of law and is counsel's\n // call (AGL-1904/AGL-1956).\n helperText={`${PLATFORM_BRAND_NAME} charges this price as typed — no tax is added. Your Commerce tax settings are a goods sales rate and do not apply to bookings.`}\n value={draft?.priceUsd ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n priceUsd: event.target.value.replace(/[^0-9]/g, ''),\n }\n : prev,\n )\n }\n size=\"small\"\n />\n <TextField\n label=\"Timezone\"\n value={draft?.timezone ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, timezone: event.target.value } : prev,\n )\n }\n size=\"small\"\n sx={{ minWidth: 180 }}\n />\n </Stack>\n <TextField\n label=\"Description (optional)\"\n value={draft?.description ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, description: event.target.value } : prev,\n )\n }\n size=\"small\"\n multiline\n minRows={2}\n />\n <Typography variant=\"overline\" color=\"text.secondary\">\n {'Weekly availability'}\n </Typography>\n {WEEKDAYS.map((day, index) => (\n <TextField\n key={day}\n label={day}\n placeholder=\"09:00-12:00, 13:00-17:00 (empty = closed)\"\n value={draft?.windowText[index] ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n windowText: prev.windowText.map((text, i) =>\n i === index ? event.target.value : text,\n ),\n }\n : prev,\n )\n }\n size=\"small\"\n />\n ))}\n <Typography variant=\"overline\" color=\"text.secondary\">\n {'CRM'}\n </Typography>\n {/*\n What a booking of this service does to the booker's CRM record\n (AGL-2660). The meeting is on by default — a booking IS a\n meeting the record has — and the follow-up is the service's\n choice, because not every service earns a call afterwards.\n */}\n <FormControlLabel\n control={\n <Switch\n checked={draft?.crmMeetingActivity ?? true}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? { ...prev, crmMeetingActivity: event.target.checked }\n : prev,\n )\n }\n size=\"small\"\n />\n }\n label=\"Log a meeting on the CRM record when this service is booked\"\n />\n <FormControlLabel\n control={\n <Switch\n checked={draft?.crmFollowUpTask ?? false}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? { ...prev, crmFollowUpTask: event.target.checked }\n : prev,\n )\n }\n size=\"small\"\n />\n }\n label=\"Create a follow-up task on the CRM record when this service is booked\"\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'The follow-up is due one business day after the slot, on whoever ' +\n 'owns the record. Nothing is filed on a site whose CRM is off.'}\n </Typography>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setDraft(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!draft?.name.trim()}\n onClick={handleSave}\n >\n {'Save service'}\n </Button>\n </DialogActions>\n </Dialog>\n </Stack>\n )\n}\nBookingsConsolePage.displayName = 'BookingsConsolePage'\n\nexport default BookingsConsolePage\n"],"names":["checkQuota","crmContactByEmailHref","pluginDocsHelp","BOOKINGS_BOOKER_PARAM","normalizeContactEmail","PLATFORM_BRAND_NAME","describePaymentRisk","isBookingReminderDue","AppLink","CardDisplay","HelpTip","useConfirmationContext","QuotaReadoutComponent","useSnackbar","Timestamp","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","FormControlLabel","Stack","Switch","TextField","Tooltip","Typography","collection","doc","limit","orderBy","query","setDoc","updateDoc","useParams","useSearchParams","useCallback","useState","useFirestore","useFirestoreCollection","useHostResourceApi","useUser","writeGuardedBySeed","authorizedFetch","WEEKDAYS","parseWindows","input","windows","chunk","split","match","trim","start","Number","end","push","formatWindows","pad","minutes","String","Math","floor","padStart","map","window","join","PaymentRiskChip","risk","described","title","detail","label","size","color","BookingsConsolePage","props","hostId","entitled","org","firestore","createHostResource","enqueueSnackbar","confirm","data","user","params","siteContext","orgSlug","host","searchParams","bookerFilter","get","basePath","serviceDocs","status","servicesStatus","fromCache","servicesFromCache","idField","bookingDocs","services","filter","service","deletedAt","sort","a","b","name","localeCompare","upcoming","booking","endsAtMs","Date","now","startsAtMs","shown","email","reminderNowMs","reminderQueue","length","remindersSent","reminderSentAt","draft","setDraft","handleAdd","variant","persist","quota","allowed","id","durationMinutes","priceUsd","timezone","Intl","DateTimeFormat","resolvedOptions","timeZone","description","windowText","_","index","crmMeetingActivity","crmFollowUpTask","handleSave","forEach","text","weekday","parsed","fields","slice","max","min","round","verdict","subject","unreadable","updatedAt","merge","ok","message","resource","error","console","allowDuplicate","handleDeleteService","confirmed","confirmationText","confirmationButtonProps","then","catch","$id","handleCancelBooking","paidCents","paidAmountCents","refundedCents","outstandingCents","refundable","toFixed","globalThis","attemptKey","crypto","randomUUID","random","toString","response","method","headers","body","JSON","stringify","bookingId","payload","json","spacing","header","help","anchor","excerpt","contentGutterX","contentGutterY","direction","sx","alignItems","flex","minWidth","noWrap","onClick","alignSelf","ready","used","noun","component","mb","href","ml","fontSize","serviceName","toLocaleString","paymentRisk","componentVariant","nativeButton","open","Boolean","onClose","maxWidth","fullWidth","display","flexDirection","gap","pt","value","onChange","event","prev","target","autoFocus","mt","replace","helperText","multiline","minRows","day","placeholder","i","control","checked","disabled","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/components/bookings-console-page.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'use client'\n\nimport { checkQuota, pluginDocsHelp } from '@aglyn/aglyn'\nimport { normalizeContactEmail } from '@aglyn/aglyn'\nimport { pluginRecordByEmailHref } from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { BOOKINGS_BOOKER_PARAM } from '../model/bookings-record-routes'\nimport { PLATFORM_BRAND_NAME } from '@aglyn/aglyn/app-utils/platform-brand'\nimport { describePaymentRisk, type PaymentRisk } from '@aglyn/aglyn/app-utils/payment-risk'\nimport { type ConsolePluginPageProps } from '@aglyn/aglyn'\nimport { type HostBookingService, isBookingReminderDue } from '../model'\nimport { AppLink, CardDisplay, HelpTip, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport QuotaReadoutComponent from '@aglyn/shared-ui-jsx/components/quota-readout.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport {\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n FormControlLabel,\n Stack,\n Switch,\n TextField,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n doc,\n limit,\n orderBy,\n query,\n setDoc,\n updateDoc,\n} from 'firebase/firestore'\nimport { useParams, useSearchParams } from 'next/navigation'\nimport { useCallback, useState } from 'react'\nimport {\n useFirestore,\n useFirestoreCollection,\n useHostResourceApi,\n useUser,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\n\nconst WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']\n\n/** \"09:00-12:00, 13:00-17:00\" → open intervals in minutes. */\nfunction parseWindows(input: string): Array<{ start: number; end: number }> {\n const windows: Array<{ start: number; end: number }> = []\n for (const chunk of input.split(',')) {\n const match = chunk.trim().match(/^(\\d{1,2}):(\\d{2})\\s*-\\s*(\\d{1,2}):(\\d{2})$/)\n if (!match) continue\n const start = Number(match[1]) * 60 + Number(match[2])\n const end = Number(match[3]) * 60 + Number(match[4])\n if (end > start && end <= 24 * 60) windows.push({ start, end })\n }\n return windows\n}\n\nfunction formatWindows(\n windows: Array<{ start: number; end: number }> | undefined,\n): string {\n const pad = (minutes: number) =>\n `${String(Math.floor(minutes / 60)).padStart(2, '0')}:${String(\n minutes % 60,\n ).padStart(2, '0')}`\n return (windows ?? [])\n .map((window) => `${pad(window.start)}-${pad(window.end)}`)\n .join(', ')\n}\n\ninterface ServiceDraft {\n id: string | null\n name: string\n durationMinutes: string\n priceUsd: string\n timezone: string\n description: string\n /** Per-weekday window text, e.g. \"09:00-17:00\". */\n windowText: string[]\n /** The service's two CRM switches (AGL-2660) — see `HostBookingService`. */\n crmMeetingActivity: boolean\n crmFollowUpTask: boolean\n}\n\n/** A fraud warning, review or chargeback on the booking's payment (AGL-3360). */\nfunction PaymentRiskChip({ risk }: { risk: PaymentRisk | undefined }) {\n const described = describePaymentRisk(risk)\n if (!described) return null\n return (\n <Tooltip title={described.detail}>\n <Chip label={described.label} size=\"small\" color=\"warning\" />\n </Tooltip>\n )\n}\n\n/**\n * Bookings manager (AGL-159): bookable services with weekly availability\n * windows, and the upcoming-bookings list with cancel. Visitors book via\n * the org /api/bookings endpoints; confirmations email through the\n * env-gated Resend path. Plan-gated (`bookings` flag + `servicesPerHost`).\n */\nexport function BookingsConsolePage(props: ConsolePluginPageProps) {\n const { hostId, entitled, org } = props\n const firestore = useFirestore()\n const createHostResource = useHostResourceApi()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n // The id token the refund route authenticates with (AGL-2315).\n const { data: user } = useUser()\n /*\n * Where a booker's CONTACT is (AGL-2622). A booking updated a person in\n * the CRM by the address it carried, and the row links there by that\n * address — the Contacts list holds the id nothing here does and opens\n * the record on one match. The address is the one the plugin that keeps\n * contacts publishes, asked of the record-route registry the way the\n * commerce dialog asks: a plugin importing a sibling is a cycle waiting\n * for its third edge. `null` until the route params settle, and where no\n * plugin keeps contacts, so no row renders a link to nowhere.\n */\n const params = useParams<{ orgSlug?: string; host?: string }>()\n const siteContext =\n params?.orgSlug && params?.host\n ? { orgSlug: String(params.orgSlug), host: String(params.host) }\n : null\n const contactHrefOf = (email: unknown): string | null =>\n siteContext && email ? pluginRecordByEmailHref('contact', siteContext, String(email)) : null\n /*\n * The list narrowed to one booker (AGL-2660): a contact's record links\n * here with `?email=`, and the page answers with every booking that\n * address holds — past ones too, because that link asks about a\n * person's history rather than the week ahead. The same match rule as\n * the row's own \"View in CRM\": the address, normalized, equal. `null`\n * outside a router, where there is no query to read.\n */\n const searchParams = useSearchParams()\n const bookerFilter = normalizeContactEmail(searchParams?.get(BOOKINGS_BOOKER_PARAM))\n const { basePath } = props\n\n const {\n data: serviceDocs,\n status: servicesStatus,\n /**\n * The rows this editor is seeded from are unconfirmed by the server\n * (AGL-1358). Editing copies a whole stored service into `draft` and\n * writes all of it back, so `merge: true` protects nothing. `windows` is\n * the weekly availability map, rebuilt in full on every save: a cached\n * seed re-opens slots that were closed and closes ones customers can\n * already book, and `priceUsd` goes back to whatever that snapshot\n * charged.\n */\n fromCache: servicesFromCache,\n } = useFirestoreCollection<any>(\n () =>\n query(collection(firestore, 'hosts', hostId, 'services'), limit(100)),\n [firestore, hostId],\n { idField: '$id' },\n )\n const { data: bookingDocs } = useFirestoreCollection<any>(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'bookings'),\n orderBy('startsAtMs', 'desc'),\n limit(100),\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const services = [...(serviceDocs ?? [])]\n .filter((service: any) => !service.deletedAt)\n .sort((a: any, b: any) =>\n String(a.name ?? '').localeCompare(String(b.name ?? '')),\n )\n const upcoming = [...(bookingDocs ?? [])]\n .filter((booking: any) => booking.endsAtMs >= Date.now())\n .sort((a: any, b: any) => a.startsAtMs - b.startsAtMs)\n // Newest first when narrowed to a booker — the listener already orders\n // by start, descending — so the history reads down from the latest.\n const shown = bookerFilter\n ? (bookingDocs ?? []).filter(\n (booking: any) => normalizeContactEmail(booking.email) === bookerFilter,\n )\n : upcoming\n\n /**\n * The 24-hour reminder queue (AGL-2431).\n *\n * Reminders are sent by the `booking-reminders` beat, not by anything on\n * this page, and until that beat existed they were never sent at all — the\n * feature had a designed email template, white-label branding and an\n * `x-cron-secret` door, and no scheduler pointed at it. So the merchant had\n * no way to tell working from broken, and still would not if this card\n * merely asserted that reminders happen.\n *\n * READ-ONLY, and deliberately no \"send now\" button: the beat runs hourly\n * and stamps `reminderSentAt`, so a manual sender would race it for a\n * reminder nobody can un-send. Same call AGL-2227 made for the recovery\n * queue.\n *\n * `isBookingReminderDue` is the sender's own predicate, against one instant\n * shared by both counts, so this is the queue the next pass will drain\n * rather than an approximation of it.\n */\n const reminderNowMs = Date.now()\n const reminderQueue = (bookingDocs ?? []).filter((booking: any) =>\n isBookingReminderDue(booking, reminderNowMs),\n ).length\n const remindersSent = (bookingDocs ?? []).filter(\n (booking: any) => booking.reminderSentAt,\n ).length\n\n const [draft, setDraft] = useState<ServiceDraft | null>(null)\n\n const handleAdd = useCallback(() => {\n if (!entitled) {\n return void enqueueSnackbar(\n 'Bookings require a Starter plan — see Billing to upgrade',\n { variant: 'warning', persist: false },\n )\n }\n const quota = checkQuota(org, 'servicesPerHost', services.length)\n if (!quota.allowed) {\n return void enqueueSnackbar(\n `Service limit reached (${quota.limit}) — upgrade in Billing`,\n { variant: 'warning', persist: false },\n )\n }\n setDraft({\n id: null,\n name: '',\n durationMinutes: '30',\n priceUsd: '0',\n timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC',\n description: '',\n windowText: WEEKDAYS.map((_, index) =>\n index >= 1 && index <= 5 ? '09:00-17:00' : '',\n ),\n // A booking is a meeting the record has; the follow-up is opt-in.\n crmMeetingActivity: true,\n crmFollowUpTask: false,\n })\n }, [entitled, org, services.length, enqueueSnackbar])\n\n const handleSave = useCallback(async () => {\n if (!draft || !draft.name.trim()) return\n const windows: HostBookingService['windows'] = {}\n draft.windowText.forEach((text, weekday) => {\n const parsed = parseWindows(text)\n if (parsed.length) windows[weekday] = parsed\n })\n const fields = {\n name: draft.name.trim().slice(0, 80),\n durationMinutes: Math.max(\n 5,\n Math.min(480, Math.round(Number(draft.durationMinutes) || 30)),\n ),\n priceUsd: Math.max(0, Math.round(Number(draft.priceUsd) || 0)),\n timezone: draft.timezone.trim() || 'UTC',\n ...(draft.description.trim() && {\n description: draft.description.trim().slice(0, 500),\n }),\n windows,\n // Written explicitly rather than only when on: an edit that switches\n // the meeting off has to land a `false`, and `merge: true` would keep\n // an absent key exactly as it was.\n crmMeetingActivity: draft.crmMeetingActivity,\n crmFollowUpTask: draft.crmFollowUpTask,\n }\n try {\n if (draft.id) {\n /**\n * Edit stays client-direct (no quota consumed) — and is refused when\n * its seed was never confirmed by the server (AGL-1358). `fields` is\n * every editable key of the stored service, `windows` included, so\n * `merge: true` has nothing left to protect.\n *\n * The guard WRAPS the write. An early return is a shape you can keep\n * while losing the protection; here the write is only reachable\n * through the verdict.\n */\n const verdict = await writeGuardedBySeed(\n {\n subject: 'service',\n unreadable: servicesStatus === 'error',\n fromCache: servicesFromCache,\n },\n async () => {\n await setDoc(\n doc(firestore, 'hosts', hostId, 'services', draft.id),\n { ...fields, updatedAt: Timestamp.now() },\n { merge: true },\n )\n },\n )\n // Before `setDraft(null)`, so a refusal keeps the dialog open with\n // every window that was typed. A save that silently does nothing\n // sends the user back to retype a form that will be refused again\n // just as quietly.\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n } else {\n // New service rides the quota-enforcing resources API (AGL-473) —\n // it also re-checks the `bookings` entitlement server-side.\n await createHostResource({ hostId, resource: 'service', data: fields })\n }\n setDraft(null)\n enqueueSnackbar('Service saved', { variant: 'success', persist: false })\n } catch (error: any) {\n console.error(error)\n enqueueSnackbar(error?.message ?? 'An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n }, [\n draft,\n firestore,\n hostId,\n createHostResource,\n enqueueSnackbar,\n servicesStatus,\n servicesFromCache,\n ])\n\n const handleDeleteService = useCallback(\n (service: any) => async () => {\n const confirmed = await confirm({\n title: 'Delete this service?',\n description: `\"${service.name}\" stops accepting bookings.`,\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await updateDoc(doc(firestore, 'hosts', hostId, 'services', service.$id), {\n deletedAt: Timestamp.now(),\n })\n },\n [confirm, firestore, hostId],\n )\n\n /**\n * Cancelling a PAID booking refunds the guest (AGL-2315).\n *\n * This used to be one `updateDoc` writing `status: 'canceled'`, for a paid\n * booking exactly as for a free one. The slot reopened, the appointment was\n * gone, and the guest had paid for it — the money moved nowhere and nothing\n * in the console said so. That was survivable only while the charge settled\n * in Aglyn's own balance and could be given back by hand; a paid booking is\n * a destination charge now, so the funds are at the MERCHANT and there is no\n * later opportunity to notice.\n *\n * So a paid booking cancels through the refund route, which reverses the\n * merchant's share and Aglyn's fee, and which sets `canceled` itself once\n * the money is actually back. A FAILED refund deliberately leaves the\n * booking standing: a cancelled-and-unrefunded appointment is the worst of\n * the three outcomes, and an admin who sees the row still there knows the\n * cancel did not happen.\n */\n const handleCancelBooking = useCallback(\n (booking: any) => async () => {\n const paidCents = Math.max(0, Number(booking.paidAmountCents ?? 0))\n const refundedCents = Math.max(0, Number(booking.refundedCents ?? 0))\n const outstandingCents = Math.max(0, paidCents - refundedCents)\n const refundable = outstandingCents > 0\n const confirmed = await confirm({\n title: 'Cancel this booking?',\n description:\n `${booking.name} (${booking.email}) — the slot reopens.` +\n (refundable\n ? ` $${(outstandingCents / 100).toFixed(2)} is refunded to them` +\n ' through Stripe.'\n : ''),\n confirmationText: refundable ? 'Cancel and refund' : 'Cancel booking',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n\n if (refundable) {\n // `randomUUID` needs a secure context, which the console always is,\n // but fall back rather than throw on a refund.\n const attemptKey =\n globalThis.crypto?.randomUUID?.() ??\n `${Date.now()}-${Math.random().toString(36).slice(2)}`\n try {\n const response = await authorizedFetch(user, '/api/bookings/refund', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Idempotency-Key': attemptKey,\n },\n body: JSON.stringify({ hostId, bookingId: booking.$id }),\n })\n const payload = await response.json().catch(() => null)\n if (!response.ok) {\n // A 409 is a guard REFUSING with a body that says what to do\n // instead — a booking paid before the PaymentIntent was recorded\n // names the Stripe dashboard — not the refund machinery failing.\n // It surfaces verbatim, the AGL-1818 rule.\n enqueueSnackbar(payload?.error ?? 'Refund failed', {\n variant: response.status === 409 ? 'warning' : 'error',\n allowDuplicate: true,\n })\n // The booking stays. See the note above: cancelled-and-unrefunded\n // is worse than not cancelled.\n return\n }\n // The route wrote `canceled` itself once the money was back.\n enqueueSnackbar('Booking canceled and refunded', {\n variant: 'success',\n persist: false,\n })\n return\n } catch {\n enqueueSnackbar('Refund failed — the booking was not canceled', {\n variant: 'error',\n allowDuplicate: true,\n })\n return\n }\n }\n\n await updateDoc(\n doc(firestore, 'hosts', hostId, 'bookings', booking.$id),\n { status: 'canceled' },\n )\n enqueueSnackbar('Booking canceled', {\n variant: 'success',\n persist: false,\n })\n },\n [confirm, firestore, hostId, enqueueSnackbar, user],\n )\n\n return (\n <Stack spacing={3}>\n <CardDisplay\n header={'Services'}\n help={pluginDocsHelp('bookings', {\n anchor: '#set-up-bookings',\n excerpt:\n 'The bookable services this site offers — how long each takes, ' +\n 'what it costs, and the hours it can be booked in.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n {services.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Define a bookable service — duration, price, and weekly ' +\n 'availability — then visitors can book from your site.'}\n </Typography>\n ) : (\n services.map((service: any) => (\n <Stack\n key={service.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Stack sx={{ flex: 1, minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {service.name}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {`${service.durationMinutes} min` +\n (Number(service.priceUsd) > 0\n ? ` · $${service.priceUsd}`\n : ' · free') +\n ` · ${service.timezone ?? 'UTC'}`}\n </Typography>\n </Stack>\n <Button\n size=\"small\"\n onClick={() =>\n setDraft({\n id: service.$id,\n name: service.name ?? '',\n durationMinutes: String(service.durationMinutes ?? 30),\n priceUsd: String(service.priceUsd ?? 0),\n timezone: service.timezone ?? 'UTC',\n description: service.description ?? '',\n windowText: WEEKDAYS.map((_, index) =>\n formatWindows(service.windows?.[index]),\n ),\n // Absent reads as ON for the meeting and OFF for the\n // follow-up — the model's defaults, seeded so the\n // switches show what the service actually does.\n crmMeetingActivity: service.crmMeetingActivity !== false,\n crmFollowUpTask: service.crmFollowUpTask === true,\n })\n }\n >\n {'Edit'}\n </Button>\n <Button\n size=\"small\"\n color=\"error\"\n onClick={handleDeleteService(service)}\n >\n {'Delete'}\n </Button>\n </Stack>\n ))\n )}\n <Button\n size=\"small\"\n color=\"primary\"\n sx={{ alignSelf: 'flex-start' }}\n onClick={handleAdd}\n >\n {'Add service'}\n </Button>\n {/* The cap, standing rather than only on refusal (AGL-2113).\n `services.length` is the same count `handleAdd` hands to\n `checkQuota`, so the readout and the gate cannot disagree. */}\n <QuotaReadoutComponent\n ready={org != null}\n used={services.length}\n limit={checkQuota(org, 'servicesPerHost', services.length).limit}\n noun=\"service\"\n />\n </Stack>\n </CardDisplay>\n\n <CardDisplay\n header={bookerFilter ? `Bookings for ${bookerFilter}` : 'Upcoming bookings'}\n help={pluginDocsHelp('bookings', {\n anchor: '#manage',\n excerpt:\n 'Bookings taken on this site, with the customer and the slot each ' +\n 'one holds.',\n })}\n contentGutterX\n contentGutterY\n >\n {bookerFilter ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n component=\"div\"\n sx={{ mb: 1 }}\n >\n {'Every booking this address holds, newest first. '}\n {basePath ? <AppLink href={basePath}>{'Show upcoming bookings'}</AppLink> : null}\n </Typography>\n ) : null}\n {/* What the reminder beat will do next, and what it has done. Named\n \"24-hour reminders\" rather than \"emails\" because that is the only\n mail this queue governs — confirmations go out on booking. */}\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n component=\"div\"\n sx={{ mb: 1 }}\n >\n {`24-hour reminders · ${reminderQueue} due in the next pass · ` +\n `${remindersSent} already sent`}\n {/* Its OWN heading, not the card's `#manage`: a merchant reading\n this line wants the timing and the once-only rule, and landing\n them on \"manage your bookings\" is the presence-not-correctness\n failure a help-coverage check cannot see. */}\n <HelpTip\n {...pluginDocsHelp('bookings', { anchor: '#reminders' })}\n sx={{ ml: 0.5, fontSize: '0.9em' }}\n />\n </Typography>\n {shown.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {bookerFilter ? 'No bookings for this address.' : 'No upcoming bookings.'}\n </Typography>\n ) : (\n <Stack spacing={1}>\n {shown.map((booking: any) => (\n <Stack\n key={booking.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Stack sx={{ flex: 1, minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {`${booking.serviceName} — ${booking.name}`}\n {booking.status === 'canceled' ? ' (canceled)' : ''}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {`${new Date(booking.startsAtMs).toLocaleString()} · ${\n booking.email\n }`}\n </Typography>\n </Stack>\n <PaymentRiskChip risk={booking.paymentRisk} />\n {contactHrefOf(booking.email) ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={contactHrefOf(booking.email) ?? undefined}\n size=\"small\"\n >\n {'View in CRM'}\n </Button>\n ) : null}\n {booking.status !== 'canceled' ? (\n <Button\n size=\"small\"\n color=\"error\"\n onClick={handleCancelBooking(booking)}\n >\n {'Cancel'}\n </Button>\n ) : null}\n </Stack>\n ))}\n </Stack>\n )}\n </CardDisplay>\n\n <Dialog\n open={Boolean(draft)}\n onClose={() => setDraft(null)}\n maxWidth=\"sm\"\n fullWidth\n >\n <DialogTitle>{draft?.id ? 'Edit service' : 'Add service'}</DialogTitle>\n <DialogContent\n sx={{ display: 'flex', flexDirection: 'column', gap: 1.5, pt: 1 }}\n >\n <TextField\n label=\"Name\"\n value={draft?.name ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, name: event.target.value } : prev,\n )\n }\n size=\"small\"\n autoFocus\n sx={{ mt: 1 }}\n />\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Duration (minutes)\"\n value={draft?.durationMinutes ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n durationMinutes: event.target.value.replace(\n /[^0-9]/g,\n '',\n ),\n }\n : prev,\n )\n }\n size=\"small\"\n />\n <TextField\n label=\"Price (USD, 0 = free)\"\n // WHAT THIS PRICE DOES NOT INCLUDE (AGL-2028).\n //\n // A paid booking is charged exactly this figure: the handler\n // sends no tax parameter at all, by a stated decision (AGL-2000)\n // — whether a service is taxable is jurisdiction-specific and\n // frequently the opposite answer from goods, so applying the\n // commerce plugin's GOODS sales rate would be confidently wrong\n // rather than merely absent.\n //\n // That decision was recorded in the source and in the\n // `storefrontTaxCollected` ledger, and in neither place can the\n // merchant see it. A service business is one of the three named\n // ICPs, and one who configured a rate in Commerce → Settings →\n // Taxes has every reason to assume it covers the appointments\n // they sell on the same site. Saying so here is the half of\n // AGL-2028 that holds however the rate question is answered.\n //\n // States the MECHANISM and makes no remittance determination —\n // the same line `describeOrderTaxMode` holds. Who owes which\n // authority what attaches by operation of law and is counsel's\n // call (AGL-1904/AGL-1956).\n helperText={`${PLATFORM_BRAND_NAME} charges this price as typed — no tax is added. Your Commerce tax settings are a goods sales rate and do not apply to bookings.`}\n value={draft?.priceUsd ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n priceUsd: event.target.value.replace(/[^0-9]/g, ''),\n }\n : prev,\n )\n }\n size=\"small\"\n />\n <TextField\n label=\"Timezone\"\n value={draft?.timezone ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, timezone: event.target.value } : prev,\n )\n }\n size=\"small\"\n sx={{ minWidth: 180 }}\n />\n </Stack>\n <TextField\n label=\"Description (optional)\"\n value={draft?.description ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, description: event.target.value } : prev,\n )\n }\n size=\"small\"\n multiline\n minRows={2}\n />\n <Typography variant=\"overline\" color=\"text.secondary\">\n {'Weekly availability'}\n </Typography>\n {WEEKDAYS.map((day, index) => (\n <TextField\n key={day}\n label={day}\n placeholder=\"09:00-12:00, 13:00-17:00 (empty = closed)\"\n value={draft?.windowText[index] ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n windowText: prev.windowText.map((text, i) =>\n i === index ? event.target.value : text,\n ),\n }\n : prev,\n )\n }\n size=\"small\"\n />\n ))}\n <Typography variant=\"overline\" color=\"text.secondary\">\n {'CRM'}\n </Typography>\n {/*\n What a booking of this service does to the booker's CRM record\n (AGL-2660). The meeting is on by default — a booking IS a\n meeting the record has — and the follow-up is the service's\n choice, because not every service earns a call afterwards.\n */}\n <FormControlLabel\n control={\n <Switch\n checked={draft?.crmMeetingActivity ?? true}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? { ...prev, crmMeetingActivity: event.target.checked }\n : prev,\n )\n }\n size=\"small\"\n />\n }\n label=\"Log a meeting on the CRM record when this service is booked\"\n />\n <FormControlLabel\n control={\n <Switch\n checked={draft?.crmFollowUpTask ?? false}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? { ...prev, crmFollowUpTask: event.target.checked }\n : prev,\n )\n }\n size=\"small\"\n />\n }\n label=\"Create a follow-up task on the CRM record when this service is booked\"\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'The follow-up is due one business day after the slot, on whoever ' +\n 'owns the record. Nothing is filed on a site whose CRM is off.'}\n </Typography>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setDraft(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!draft?.name.trim()}\n onClick={handleSave}\n >\n {'Save service'}\n </Button>\n </DialogActions>\n </Dialog>\n </Stack>\n )\n}\nBookingsConsolePage.displayName = 'BookingsConsolePage'\n\nexport default BookingsConsolePage\n"],"names":["checkQuota","pluginDocsHelp","normalizeContactEmail","pluginRecordByEmailHref","BOOKINGS_BOOKER_PARAM","PLATFORM_BRAND_NAME","describePaymentRisk","isBookingReminderDue","AppLink","CardDisplay","HelpTip","useConfirmationContext","QuotaReadoutComponent","useSnackbar","Timestamp","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","FormControlLabel","Stack","Switch","TextField","Tooltip","Typography","collection","doc","limit","orderBy","query","setDoc","updateDoc","useParams","useSearchParams","useCallback","useState","useFirestore","useFirestoreCollection","useHostResourceApi","useUser","writeGuardedBySeed","authorizedFetch","WEEKDAYS","parseWindows","input","windows","chunk","split","match","trim","start","Number","end","push","formatWindows","pad","minutes","String","Math","floor","padStart","map","window","join","PaymentRiskChip","risk","described","title","detail","label","size","color","BookingsConsolePage","props","hostId","entitled","org","firestore","createHostResource","enqueueSnackbar","confirm","data","user","params","siteContext","orgSlug","host","contactHrefOf","email","searchParams","bookerFilter","get","basePath","serviceDocs","status","servicesStatus","fromCache","servicesFromCache","idField","bookingDocs","services","filter","service","deletedAt","sort","a","b","name","localeCompare","upcoming","booking","endsAtMs","Date","now","startsAtMs","shown","reminderNowMs","reminderQueue","length","remindersSent","reminderSentAt","draft","setDraft","handleAdd","variant","persist","quota","allowed","id","durationMinutes","priceUsd","timezone","Intl","DateTimeFormat","resolvedOptions","timeZone","description","windowText","_","index","crmMeetingActivity","crmFollowUpTask","handleSave","forEach","text","weekday","parsed","fields","slice","max","min","round","verdict","subject","unreadable","updatedAt","merge","ok","message","resource","error","console","allowDuplicate","handleDeleteService","confirmed","confirmationText","confirmationButtonProps","then","catch","$id","handleCancelBooking","paidCents","paidAmountCents","refundedCents","outstandingCents","refundable","toFixed","globalThis","attemptKey","crypto","randomUUID","random","toString","response","method","headers","body","JSON","stringify","bookingId","payload","json","spacing","header","help","anchor","excerpt","contentGutterX","contentGutterY","direction","sx","alignItems","flex","minWidth","noWrap","onClick","alignSelf","ready","used","noun","component","mb","href","ml","fontSize","serviceName","toLocaleString","paymentRisk","componentVariant","nativeButton","undefined","open","Boolean","onClose","maxWidth","fullWidth","display","flexDirection","gap","pt","value","onChange","event","prev","target","autoFocus","mt","replace","helperText","multiline","minRows","day","placeholder","i","control","checked","disabled","displayName"],"mappings":"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"}
@@ -0,0 +1,31 @@
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 { type PluginRecordRouteContext } from '@aglyn/aglyn/plugin-manager/plugin-record-routes';
18
+ /**
19
+ * The query key the Bookings page narrows to one booker by (AGL-2660) — the
20
+ * same word the CRM's contacts list and the Orders list use: one address,
21
+ * one word.
22
+ */
23
+ export declare const BOOKINGS_BOOKER_PARAM = "email";
24
+ /** The site's Bookings page, or `null` at the organization, where there is none. */
25
+ export declare function bookingsPageHref(context: PluginRecordRouteContext): string | null;
26
+ /**
27
+ * Called from the console registrar. The owner is named rather than read off
28
+ * the loader's marker, so the route registers the same way under a spec that
29
+ * calls the registrar directly.
30
+ */
31
+ export declare function registerBookingsRecordRoutes(): void;
@@ -0,0 +1,63 @@
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
+ */ import { buildRoute, Route } from "@aglyn/aglyn/app-utils/console-routes";
17
+ import { registerPluginRecordRoute } from "@aglyn/aglyn/plugin-manager/plugin-record-routes";
18
+ import { BUNDLE_ID } from "../constants/bundle-common.js";
19
+ /**
20
+ * Where a booking is read, published for every other surface (AGL-3080).
21
+ *
22
+ * A contact's timeline opens the bookings it names and a person's page lists
23
+ * the bookings their address holds. Each asks the record-route registry for
24
+ * `booking` rather than spelling this plugin's console path, and gets `null`
25
+ * (text instead of a link) where this plugin is not loaded.
26
+ *
27
+ * Bookings are a site's, so only a site has an address. A booking has no page
28
+ * of its own — the Bookings page lists them — so a record answers the page,
29
+ * and a person's bookings are the page narrowed by
30
+ * {@link BOOKINGS_BOOKER_PARAM}.
31
+ */ /** The nav slug the shell resolves the Bookings page by. */ const BOOKINGS_SLUG = 'bookings';
32
+ /**
33
+ * The query key the Bookings page narrows to one booker by (AGL-2660) — the
34
+ * same word the CRM's contacts list and the Orders list use: one address,
35
+ * one word.
36
+ */ export const BOOKINGS_BOOKER_PARAM = 'email';
37
+ /** The site's Bookings page, or `null` at the organization, where there is none. */ export function bookingsPageHref(context) {
38
+ return context.host ? buildRoute(Route.HOST_PLUGIN, {
39
+ orgSlug: context.orgSlug,
40
+ host: context.host,
41
+ pluginSlug: BOOKINGS_SLUG
42
+ }) : null;
43
+ }
44
+ /**
45
+ * Called from the console registrar. The owner is named rather than read off
46
+ * the loader's marker, so the route registers the same way under a spec that
47
+ * calls the registrar directly.
48
+ */ export function registerBookingsRecordRoutes() {
49
+ registerPluginRecordRoute('booking', {
50
+ list: bookingsPageHref,
51
+ record: (context)=>bookingsPageHref(context),
52
+ byEmail: (context, email)=>{
53
+ const page = bookingsPageHref(context);
54
+ return page ? `${page}?${new URLSearchParams({
55
+ [BOOKINGS_BOOKER_PARAM]: email
56
+ }).toString()}` : null;
57
+ }
58
+ }, {
59
+ pluginId: BUNDLE_ID
60
+ });
61
+ }
62
+
63
+ //# sourceMappingURL=bookings-record-routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/model/bookings-record-routes.ts"],"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 { buildRoute, Route } from '@aglyn/aglyn/app-utils/console-routes'\nimport {\n registerPluginRecordRoute,\n type PluginRecordRouteContext,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { BUNDLE_ID } from '../constants/bundle-common'\n\n/**\n * Where a booking is read, published for every other surface (AGL-3080).\n *\n * A contact's timeline opens the bookings it names and a person's page lists\n * the bookings their address holds. Each asks the record-route registry for\n * `booking` rather than spelling this plugin's console path, and gets `null`\n * (text instead of a link) where this plugin is not loaded.\n *\n * Bookings are a site's, so only a site has an address. A booking has no page\n * of its own — the Bookings page lists them — so a record answers the page,\n * and a person's bookings are the page narrowed by\n * {@link BOOKINGS_BOOKER_PARAM}.\n */\n\n/** The nav slug the shell resolves the Bookings page by. */\nconst BOOKINGS_SLUG = 'bookings'\n\n/**\n * The query key the Bookings page narrows to one booker by (AGL-2660) — the\n * same word the CRM's contacts list and the Orders list use: one address,\n * one word.\n */\nexport const BOOKINGS_BOOKER_PARAM = 'email'\n\n/** The site's Bookings page, or `null` at the organization, where there is none. */\nexport function bookingsPageHref(context: PluginRecordRouteContext): string | null {\n return context.host\n ? buildRoute(Route.HOST_PLUGIN, {\n orgSlug: context.orgSlug,\n host: context.host,\n pluginSlug: BOOKINGS_SLUG,\n })\n : null\n}\n\n/**\n * Called from the console registrar. The owner is named rather than read off\n * the loader's marker, so the route registers the same way under a spec that\n * calls the registrar directly.\n */\nexport function registerBookingsRecordRoutes(): void {\n registerPluginRecordRoute(\n 'booking',\n {\n list: bookingsPageHref,\n record: (context) => bookingsPageHref(context),\n byEmail: (context, email) => {\n const page = bookingsPageHref(context)\n return page\n ? `${page}?${new URLSearchParams({ [BOOKINGS_BOOKER_PARAM]: email }).toString()}`\n : null\n },\n },\n { pluginId: BUNDLE_ID },\n )\n}\n"],"names":["buildRoute","Route","registerPluginRecordRoute","BUNDLE_ID","BOOKINGS_SLUG","BOOKINGS_BOOKER_PARAM","bookingsPageHref","context","host","HOST_PLUGIN","orgSlug","pluginSlug","registerBookingsRecordRoutes","list","record","byEmail","email","page","URLSearchParams","toString","pluginId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,UAAU,EAAEC,KAAK,QAAQ,wCAAuC;AACzE,SACEC,yBAAyB,QAEpB,mDAAkD;AACzD,SAASC,SAAS,QAAQ,gCAA4B;AAEtD;;;;;;;;;;;;CAYC,GAED,0DAA0D,GAC1D,MAAMC,gBAAgB;AAEtB;;;;CAIC,GACD,OAAO,MAAMC,wBAAwB,QAAO;AAE5C,kFAAkF,GAClF,OAAO,SAASC,iBAAiBC,OAAiC;IAChE,OAAOA,QAAQC,IAAI,GACfR,WAAWC,MAAMQ,WAAW,EAAE;QAC5BC,SAASH,QAAQG,OAAO;QACxBF,MAAMD,QAAQC,IAAI;QAClBG,YAAYP;IACd,KACA;AACN;AAEA;;;;CAIC,GACD,OAAO,SAASQ;IACdV,0BACE,WACA;QACEW,MAAMP;QACNQ,QAAQ,CAACP,UAAYD,iBAAiBC;QACtCQ,SAAS,CAACR,SAASS;YACjB,MAAMC,OAAOX,iBAAiBC;YAC9B,OAAOU,OACH,GAAGA,KAAK,CAAC,EAAE,IAAIC,gBAAgB;gBAAE,CAACb,sBAAsB,EAAEW;YAAM,GAAGG,QAAQ,IAAI,GAC/E;QACN;IACF,GACA;QAAEC,UAAUjB;IAAU;AAE1B"}