@aglyn/plugins-bookings 1.0.0-beta.229 → 1.0.0-beta.230
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 +11 -11
- package/src/lib/components/book-meeting-action.js +3 -2
- package/src/lib/components/book-meeting-action.js.map +1 -1
- package/src/lib/components/booking.js +4 -0
- package/src/lib/components/booking.js.map +1 -1
- package/src/lib/components/bookings-console-page.js +62 -4
- package/src/lib/components/bookings-console-page.js.map +1 -1
- package/src/lib/model/bookings.d.ts +22 -0
- package/src/lib/model/bookings.js +11 -0
- package/src/lib/model/bookings.js.map +1 -1
- package/src/lib/server/booking-service-ai-capability.d.ts +50 -0
- package/src/lib/server/booking-service-ai-capability.js +203 -0
- package/src/lib/server/booking-service-ai-capability.js.map +1 -0
- package/src/lib/server/booking-service-drafts.d.ts +107 -0
- package/src/lib/server/booking-service-drafts.js +320 -0
- package/src/lib/server/booking-service-drafts.js.map +1 -0
- package/src/lib/server.js +15 -4
- package/src/lib/server.js.map +1 -1
- package/src/lib/utils/use-booking-purchase-event.js +5 -0
- package/src/lib/utils/use-booking-purchase-event.js.map +1 -1
|
@@ -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, pluginDocsHelp, useTransferLauncher } 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 {\n BOOKINGS_TRANSFER_RESOURCE,\n bookerBookingsFilterValue,\n upcomingBookingsFilterValue,\n} from '../transfer/bookings-transfer-common'\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 {\n BOOKING_FIELD_ASKS,\n type BookingFieldAsk,\n bookingContactAsks,\n} from '../model/booking-contact-fields'\nimport {\n BOOKING_PRICE_DISPLAYS,\n BOOKING_PRICE_LABELS,\n type BookingPriceDisplay,\n bookingPriceDisplay,\n bookingPriceText,\n} from '../model/booking-price'\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 MenuItem,\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 /** What the widget asks the booker for (AGL-3493). */\n askPhone: BookingFieldAsk\n askAddress: BookingFieldAsk\n /** How the price is stated (AGL-3475); a label books with no charge. */\n priceDisplay: BookingPriceDisplay\n}\n\n/** How each way of stating the price reads in the service dialog's picker. */\nconst PRICE_DISPLAY_LABELS: Record<BookingPriceDisplay, string> = {\n fixed: 'The price',\n ...BOOKING_PRICE_LABELS,\n}\n\n/** How each answer reads in the service dialog's two pickers. */\nconst FIELD_ASK_LABELS: Record<BookingFieldAsk, string> = {\n off: \"Don't ask\",\n optional: 'Optional',\n required: 'Required',\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 * Export, opened on the list as it stands: one booker's bookings when the\n * page is narrowed to them, otherwise the upcoming ones from this moment.\n * `null` outside the console shell, where there is nothing to export into.\n * No Import: a booking is only ever made on the booking page.\n */\n const transferLauncher = useTransferLauncher()\n const handleExport = useCallback(() => {\n transferLauncher?.openExport({\n resource: BOOKINGS_TRANSFER_RESOURCE,\n scope: 'host',\n hostId,\n filter: bookerFilter\n ? { label: `Bookings for ${bookerFilter}`, value: bookerBookingsFilterValue(bookerFilter) }\n : { label: 'Upcoming bookings', value: upcomingBookingsFilterValue(Date.now()) },\n })\n }, [transferLauncher, hostId, bookerFilter])\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 // Name and email only, until the service says otherwise.\n askPhone: 'off',\n askAddress: 'off',\n priceDisplay: 'fixed',\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 // Written explicitly for the same reason: switching a field back off\n // has to land `'off'` over the stored answer.\n askPhone: draft.askPhone,\n askAddress: draft.askAddress,\n priceDisplay: draft.priceDisplay,\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 · ${bookingPriceText(service).replace(/^Free$/, '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 askPhone: bookingContactAsks(service).phone,\n askAddress: bookingContactAsks(service).address,\n priceDisplay: bookingPriceDisplay(service.priceDisplay),\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 HeaderProps={{\n action: transferLauncher?.can('export', { resource: BOOKINGS_TRANSFER_RESOURCE, scope: 'host', hostId }) ? (\n <Button size=\"small\" color=\"primary\" onClick={handleExport}>\n {'Export'}\n </Button>\n ) : undefined,\n }}\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 {typeof booking.phone === 'string' && booking.phone\n ? ` · ${booking.phone}`\n : ''}\n </Typography>\n {/* Where the job is, when the service asked (AGL-3493).\n Wrapped rather than cut: an address read to its end is\n the one a crew can drive to. */}\n {typeof booking.address === 'string' && booking.address ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ whiteSpace: 'pre-line' }}\n >\n {booking.address}\n </Typography>\n ) : null}\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={\n draft && draft.priceDisplay !== 'fixed'\n ? `Not charged: this service shows “${BOOKING_PRICE_LABELS[draft.priceDisplay]}” and books with no charge.`\n : `${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 }\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 {/*\n A contractor who quotes at the job states the price as a label\n instead (AGL-3475). Any label books with no charge, like an\n estimate appointment, and the stored price is neither shown nor\n charged.\n */}\n <TextField\n select\n label=\"Show the price as\"\n value={draft?.priceDisplay ?? 'fixed'}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? { ...prev, priceDisplay: bookingPriceDisplay(event.target.value) }\n : prev,\n )\n }\n size=\"small\"\n sx={{ minWidth: 180 }}\n >\n {BOOKING_PRICE_DISPLAYS.map((display) => (\n <MenuItem key={display} value={display}>\n {PRICE_DISPLAY_LABELS[display]}\n </MenuItem>\n ))}\n </TextField>\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 {'Booking form'}\n </Typography>\n {/*\n What the widget asks for beyond a name and an email (AGL-3493).\n Off by default; an on-site service asks for both, required.\n */}\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1}>\n {(\n [\n ['askPhone', 'Phone number'],\n ['askAddress', 'Address'],\n ] as const\n ).map(([key, label]) => (\n <TextField\n key={key}\n select\n label={label}\n value={draft?.[key] ?? 'off'}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? { ...prev, [key]: event.target.value as BookingFieldAsk }\n : prev,\n )\n }\n size=\"small\"\n fullWidth\n >\n {BOOKING_FIELD_ASKS.map((ask) => (\n <MenuItem key={ask} value={ask}>\n {FIELD_ASK_LABELS[ask]}\n </MenuItem>\n ))}\n </TextField>\n ))}\n </Stack>\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","useTransferLauncher","normalizeContactEmail","pluginRecordByEmailHref","BOOKINGS_BOOKER_PARAM","BOOKINGS_TRANSFER_RESOURCE","bookerBookingsFilterValue","upcomingBookingsFilterValue","PLATFORM_BRAND_NAME","describePaymentRisk","isBookingReminderDue","BOOKING_FIELD_ASKS","bookingContactAsks","BOOKING_PRICE_DISPLAYS","BOOKING_PRICE_LABELS","bookingPriceDisplay","bookingPriceText","AppLink","CardDisplay","HelpTip","useConfirmationContext","QuotaReadoutComponent","useSnackbar","Timestamp","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","FormControlLabel","MenuItem","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","PRICE_DISPLAY_LABELS","fixed","FIELD_ASK_LABELS","off","optional","required","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","transferLauncher","handleExport","openExport","resource","scope","filter","value","Date","now","serviceDocs","status","servicesStatus","fromCache","servicesFromCache","idField","bookingDocs","services","service","deletedAt","sort","a","b","name","localeCompare","upcoming","booking","endsAtMs","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","askPhone","askAddress","priceDisplay","handleSave","forEach","text","weekday","parsed","fields","slice","max","min","round","verdict","subject","unreadable","updatedAt","merge","ok","message","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","replace","onClick","phone","address","alignSelf","ready","used","noun","HeaderProps","action","can","undefined","component","mb","href","ml","fontSize","serviceName","toLocaleString","whiteSpace","paymentRisk","componentVariant","nativeButton","open","Boolean","onClose","maxWidth","fullWidth","display","flexDirection","gap","pt","onChange","event","prev","target","autoFocus","mt","helperText","select","multiline","minRows","day","placeholder","i","xs","sm","key","ask","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, useTransferLauncher } 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 {\n BOOKINGS_TRANSFER_RESOURCE,\n bookerBookingsFilterValue,\n upcomingBookingsFilterValue,\n} from '../transfer/bookings-transfer-common'\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 { type BookingServiceStatus, bookingServiceStatus } from '../model/bookings'\nimport {\n BOOKING_FIELD_ASKS,\n type BookingFieldAsk,\n bookingContactAsks,\n} from '../model/booking-contact-fields'\nimport {\n BOOKING_PRICE_DISPLAYS,\n BOOKING_PRICE_LABELS,\n type BookingPriceDisplay,\n bookingPriceDisplay,\n bookingPriceText,\n} from '../model/booking-price'\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 MenuItem,\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 /** What the widget asks the booker for (AGL-3493). */\n askPhone: BookingFieldAsk\n askAddress: BookingFieldAsk\n /** How the price is stated (AGL-3475); a label books with no charge. */\n priceDisplay: BookingPriceDisplay\n}\n\n/** How each way of stating the price reads in the service dialog's picker. */\nconst PRICE_DISPLAY_LABELS: Record<BookingPriceDisplay, string> = {\n fixed: 'The price',\n ...BOOKING_PRICE_LABELS,\n}\n\n/** How each answer reads in the service dialog's two pickers. */\nconst FIELD_ASK_LABELS: Record<BookingFieldAsk, string> = {\n off: \"Don't ask\",\n optional: 'Optional',\n required: 'Required',\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 * Export, opened on the list as it stands: one booker's bookings when the\n * page is narrowed to them, otherwise the upcoming ones from this moment.\n * `null` outside the console shell, where there is nothing to export into.\n * No Import: a booking is only ever made on the booking page.\n */\n const transferLauncher = useTransferLauncher()\n const handleExport = useCallback(() => {\n transferLauncher?.openExport({\n resource: BOOKINGS_TRANSFER_RESOURCE,\n scope: 'host',\n hostId,\n filter: bookerFilter\n ? { label: `Bookings for ${bookerFilter}`, value: bookerBookingsFilterValue(bookerFilter) }\n : { label: 'Upcoming bookings', value: upcomingBookingsFilterValue(Date.now()) },\n })\n }, [transferLauncher, hostId, bookerFilter])\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 // Name and email only, until the service says otherwise.\n askPhone: 'off',\n askAddress: 'off',\n priceDisplay: 'fixed',\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 // Written explicitly for the same reason: switching a field back off\n // has to land `'off'` over the stored answer.\n askPhone: draft.askPhone,\n askAddress: draft.askAddress,\n priceDisplay: draft.priceDisplay,\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 /**\n * A draft is activated here, and only here (AGL-3616): a service a draft\n * writer made is set up and offered nowhere until a person looks at it and\n * says it takes bookings. Deactivating sends a live one back to draft,\n * which takes it off the site without deleting it; bookings it already\n * holds stand. One field, so the seed guard the dialog's whole-document\n * save needs has nothing to protect here.\n */\n const handleServiceStatus = useCallback(\n (service: any, status: BookingServiceStatus) => async () => {\n try {\n await updateDoc(doc(firestore, 'hosts', hostId, 'services', service.$id), {\n status,\n updatedAt: Timestamp.now(),\n })\n enqueueSnackbar(\n status === 'active'\n ? `${service.name} now takes bookings`\n : `${service.name} is a draft — it takes no bookings until activated`,\n { variant: 'success', persist: false },\n )\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 [firestore, hostId, enqueueSnackbar],\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 <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center', minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {service.name}\n </Typography>\n {bookingServiceStatus(service.status) === 'draft' ? (\n <Tooltip title=\"Set up, and offered nowhere until you activate it\">\n <Chip label=\"Draft\" size=\"small\" />\n </Tooltip>\n ) : null}\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {`${service.durationMinutes} min · ${bookingPriceText(service).replace(/^Free$/, '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 askPhone: bookingContactAsks(service).phone,\n askAddress: bookingContactAsks(service).address,\n priceDisplay: bookingPriceDisplay(service.priceDisplay),\n })\n }\n >\n {'Edit'}\n </Button>\n {bookingServiceStatus(service.status) === 'draft' ? (\n <Button\n size=\"small\"\n color=\"primary\"\n onClick={handleServiceStatus(service, 'active')}\n >\n {'Activate'}\n </Button>\n ) : (\n <Button size=\"small\" onClick={handleServiceStatus(service, 'draft')}>\n {'Deactivate'}\n </Button>\n )}\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 HeaderProps={{\n action: transferLauncher?.can('export', { resource: BOOKINGS_TRANSFER_RESOURCE, scope: 'host', hostId }) ? (\n <Button size=\"small\" color=\"primary\" onClick={handleExport}>\n {'Export'}\n </Button>\n ) : undefined,\n }}\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 {typeof booking.phone === 'string' && booking.phone\n ? ` · ${booking.phone}`\n : ''}\n </Typography>\n {/* Where the job is, when the service asked (AGL-3493).\n Wrapped rather than cut: an address read to its end is\n the one a crew can drive to. */}\n {typeof booking.address === 'string' && booking.address ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ whiteSpace: 'pre-line' }}\n >\n {booking.address}\n </Typography>\n ) : null}\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={\n draft && draft.priceDisplay !== 'fixed'\n ? `Not charged: this service shows “${BOOKING_PRICE_LABELS[draft.priceDisplay]}” and books with no charge.`\n : `${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 }\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 {/*\n A contractor who quotes at the job states the price as a label\n instead (AGL-3475). Any label books with no charge, like an\n estimate appointment, and the stored price is neither shown nor\n charged.\n */}\n <TextField\n select\n label=\"Show the price as\"\n value={draft?.priceDisplay ?? 'fixed'}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? { ...prev, priceDisplay: bookingPriceDisplay(event.target.value) }\n : prev,\n )\n }\n size=\"small\"\n sx={{ minWidth: 180 }}\n >\n {BOOKING_PRICE_DISPLAYS.map((display) => (\n <MenuItem key={display} value={display}>\n {PRICE_DISPLAY_LABELS[display]}\n </MenuItem>\n ))}\n </TextField>\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 {'Booking form'}\n </Typography>\n {/*\n What the widget asks for beyond a name and an email (AGL-3493).\n Off by default; an on-site service asks for both, required.\n */}\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1}>\n {(\n [\n ['askPhone', 'Phone number'],\n ['askAddress', 'Address'],\n ] as const\n ).map(([key, label]) => (\n <TextField\n key={key}\n select\n label={label}\n value={draft?.[key] ?? 'off'}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? { ...prev, [key]: event.target.value as BookingFieldAsk }\n : prev,\n )\n }\n size=\"small\"\n fullWidth\n >\n {BOOKING_FIELD_ASKS.map((ask) => (\n <MenuItem key={ask} value={ask}>\n {FIELD_ASK_LABELS[ask]}\n </MenuItem>\n ))}\n </TextField>\n ))}\n </Stack>\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","useTransferLauncher","normalizeContactEmail","pluginRecordByEmailHref","BOOKINGS_BOOKER_PARAM","BOOKINGS_TRANSFER_RESOURCE","bookerBookingsFilterValue","upcomingBookingsFilterValue","PLATFORM_BRAND_NAME","describePaymentRisk","isBookingReminderDue","bookingServiceStatus","BOOKING_FIELD_ASKS","bookingContactAsks","BOOKING_PRICE_DISPLAYS","BOOKING_PRICE_LABELS","bookingPriceDisplay","bookingPriceText","AppLink","CardDisplay","HelpTip","useConfirmationContext","QuotaReadoutComponent","useSnackbar","Timestamp","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","FormControlLabel","MenuItem","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","PRICE_DISPLAY_LABELS","fixed","FIELD_ASK_LABELS","off","optional","required","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","transferLauncher","handleExport","openExport","resource","scope","filter","value","Date","now","serviceDocs","status","servicesStatus","fromCache","servicesFromCache","idField","bookingDocs","services","service","deletedAt","sort","a","b","name","localeCompare","upcoming","booking","endsAtMs","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","askPhone","askAddress","priceDisplay","handleSave","forEach","text","weekday","parsed","fields","slice","max","min","round","verdict","subject","unreadable","updatedAt","merge","ok","message","error","console","allowDuplicate","handleServiceStatus","$id","handleDeleteService","confirmed","confirmationText","confirmationButtonProps","then","catch","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","replace","onClick","phone","address","alignSelf","ready","used","noun","HeaderProps","action","can","undefined","component","mb","href","ml","fontSize","serviceName","toLocaleString","whiteSpace","paymentRisk","componentVariant","nativeButton","open","Boolean","onClose","maxWidth","fullWidth","display","flexDirection","gap","pt","onChange","event","prev","target","autoFocus","mt","helperText","select","multiline","minRows","day","placeholder","i","xs","sm","key","ask","control","checked","disabled","displayName"],"mappings":"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"}
|
|
@@ -64,7 +64,29 @@ export interface HostBookingService {
|
|
|
64
64
|
askPhone?: 'off' | 'optional' | 'required';
|
|
65
65
|
/** Ask the booker for the address the service is at (AGL-3493); as above. */
|
|
66
66
|
askAddress?: 'off' | 'optional' | 'required';
|
|
67
|
+
/**
|
|
68
|
+
* Whether the service takes bookings (AGL-3616). A `draft` is set up and
|
|
69
|
+
* offered nowhere: the public listing leaves it out, its slots are not
|
|
70
|
+
* served and the booking route refuses it, until someone activates it in
|
|
71
|
+
* the console. Absent is `active`, so every service made before the field
|
|
72
|
+
* existed keeps taking bookings. Read through `bookingServiceStatus`.
|
|
73
|
+
*/
|
|
74
|
+
status?: BookingServiceStatus;
|
|
67
75
|
}
|
|
76
|
+
/** Whether a service takes bookings (AGL-3616). */
|
|
77
|
+
export type BookingServiceStatus = 'draft' | 'active';
|
|
78
|
+
/** The status as stored, read so only an explicit `draft` holds a service back. */
|
|
79
|
+
export declare function bookingServiceStatus(value: unknown): BookingServiceStatus;
|
|
80
|
+
/**
|
|
81
|
+
* Whether a stored service is offered to visitors (AGL-3616): not deleted,
|
|
82
|
+
* and not a draft. The one predicate every public read asks — the service
|
|
83
|
+
* directory, a service's slots, the booking route — and the console's
|
|
84
|
+
* booking-link picker, which hands visitors a link to the same door.
|
|
85
|
+
*/
|
|
86
|
+
export declare function bookingServiceIsOffered(service: {
|
|
87
|
+
deletedAt?: unknown;
|
|
88
|
+
status?: unknown;
|
|
89
|
+
} | null | undefined): boolean;
|
|
68
90
|
/** Booked interval as epoch-ms instants. */
|
|
69
91
|
export interface BookedInterval {
|
|
70
92
|
startsAtMs: number;
|
|
@@ -21,6 +21,17 @@ import { hostPublicOrigin } from "@aglyn/aglyn/app-utils/host-naming";
|
|
|
21
21
|
// and this model is read on published pages.
|
|
22
22
|
import { zonedDateTime } from "@aglyn/shared-util-timestamp/zoned-time";
|
|
23
23
|
import { BOOKING_RECORD_PARAM, BOOKING_SERVICE_PARAM, formatBookingRecordRef } from "./booking-record.js";
|
|
24
|
+
/** The status as stored, read so only an explicit `draft` holds a service back. */ export function bookingServiceStatus(value) {
|
|
25
|
+
return value === 'draft' ? 'draft' : 'active';
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Whether a stored service is offered to visitors (AGL-3616): not deleted,
|
|
29
|
+
* and not a draft. The one predicate every public read asks — the service
|
|
30
|
+
* directory, a service's slots, the booking route — and the console's
|
|
31
|
+
* booking-link picker, which hands visitors a link to the same door.
|
|
32
|
+
*/ export function bookingServiceIsOffered(service) {
|
|
33
|
+
return Boolean(service) && !(service == null ? void 0 : service.deletedAt) && bookingServiceStatus(service == null ? void 0 : service.status) === 'active';
|
|
34
|
+
}
|
|
24
35
|
export const BOOKING_MIN_DURATION_MINUTES = 5;
|
|
25
36
|
export const BOOKING_MAX_DURATION_MINUTES = 8 * 60;
|
|
26
37
|
/** Slot enumeration horizon; the console/API never look further out. */ export const BOOKING_MAX_DAYS_AHEAD = 60;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/model/bookings.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\n// Leaf paths, not the barrel: this model is read by the client bundle and\n// the `/server` entry alike, and either barrel would drag the other's\n// surface into a bundle that has no use for it.\nimport { hostPublicOrigin } from '@aglyn/aglyn/app-utils/host-naming'\n// The leaf entry: the package root extends the Firestore SDK's `Timestamp`,\n// and this model is read on published pages.\nimport { zonedDateTime } from '@aglyn/shared-util-timestamp/zoned-time'\nimport {\n BOOKING_RECORD_PARAM,\n BOOKING_SERVICE_PARAM,\n type BookingRecordRef,\n formatBookingRecordRef,\n} from './booking-record'\n\n/**\n * Bookings v1 (AGL-159): services with weekly availability windows and\n * pure, timezone-explicit slot computation. All times are minutes since\n * midnight in the HOST's timezone; instants are epoch milliseconds. No\n * I/O here — collision checks against stored bookings happen in the\n * booking API over the values these helpers produce.\n */\n\n/** `hosts/{hostId}/services/{id}` doc. */\nexport interface HostBookingService {\n name: string\n durationMinutes: number\n description?: string\n /** Optional price; 0/absent means free. */\n priceUsd?: number\n /**\n * How the price is stated (AGL-3475): `fixed` (absent) charges `priceUsd`;\n * `varies`, `estimate` and `contact` show their label and book with no\n * charge. Read through `bookingPriceDisplay` / `bookingChargeUsd`.\n */\n priceDisplay?: 'fixed' | 'varies' | 'estimate' | 'contact'\n /**\n * Weekly availability: `windows[weekday]` (0 = Sunday … 6 = Saturday)\n * lists open intervals in minutes since midnight, host-local.\n */\n windows?: Partial<Record<number, Array<{ start: number; end: number }>>>\n /** IANA timezone the windows are defined in, e.g. \"America/Chicago\". */\n timezone?: string\n /**\n * Create a follow-up task on the CRM record when this service is booked\n * (AGL-2660) — due one business day after the slot, on whoever holds the\n * relationship. Off unless the service says so.\n */\n crmFollowUpTask?: boolean\n /**\n * File the booking as a `meeting` on the CRM record's timeline (AGL-2660).\n * Absent means ON: a booking is a meeting the record has, and the service\n * has to opt out of saying so.\n */\n crmMeetingActivity?: boolean\n /**\n * Ask the booker for a phone number (AGL-3493): `optional` or `required`;\n * absent is off. Read through `bookingFieldAsk`.\n */\n askPhone?: 'off' | 'optional' | 'required'\n /** Ask the booker for the address the service is at (AGL-3493); as above. */\n askAddress?: 'off' | 'optional' | 'required'\n}\n\n/** Booked interval as epoch-ms instants. */\nexport interface BookedInterval {\n startsAtMs: number\n endsAtMs: number\n}\n\nexport const BOOKING_MIN_DURATION_MINUTES = 5\nexport const BOOKING_MAX_DURATION_MINUTES = 8 * 60\n/** Slot enumeration horizon; the console/API never look further out. */\nexport const BOOKING_MAX_DAYS_AHEAD = 60\n\n/**\n * The band a booking has to fall in to earn its 24-hour reminder, in hours\n * from now (AGL-160, scheduled in AGL-2431).\n *\n * IN THE MODEL, not beside the scan that uses them, so the console card can\n * apply the identical window without importing server code into a client\n * bundle. That sharing is the point rather than a convenience: a card that\n * drew its own boundary would report a queue depth the job does not act on,\n * and a merchant checking whether reminders are working would be reading a\n * number produced by different arithmetic than the sender's.\n */\nexport const REMINDER_WINDOW_START_HOURS = 23\nexport const REMINDER_WINDOW_END_HOURS = 25\n\n/**\n * Whether `booking` is one this pass would mail — the three tests the scan\n * applies, in one place both halves call.\n *\n * `nowMs` is a parameter and not `Date.now()` so the card and the job can be\n * asked the same question about the same instant, and so a test can pin a\n * booking exactly on either edge of the band.\n */\nexport function isBookingReminderDue(\n booking: {\n status?: string\n email?: string\n reminderSentAt?: unknown\n startsAtMs?: number\n },\n nowMs: number,\n): boolean {\n const startsAtMs = Number(booking.startsAtMs ?? 0)\n return (\n booking.status !== 'canceled' &&\n !booking.reminderSentAt &&\n Boolean(booking.email) &&\n startsAtMs >= nowMs + REMINDER_WINDOW_START_HOURS * 60 * 60 * 1000 &&\n startsAtMs <= nowMs + REMINDER_WINDOW_END_HOURS * 60 * 60 * 1000\n )\n}\n\nexport interface BookingSlot {\n startsAtMs: number\n endsAtMs: number\n}\n\n/** The walk's resolution: a slot may start on any quarter hour. */\nexport const BOOKING_SLOT_STEP_MINUTES = 15\n\n/**\n * The most starts one calendar day can hold at that resolution — 96. The\n * per-day unit every bound on a slot listing is counted in.\n */\nexport const BOOKING_MAX_SLOTS_PER_DAY =\n (24 * 60) / BOOKING_SLOT_STEP_MINUTES\n\n/**\n * Days of open times one slot listing answers with, and the length of the\n * Booking widget's day strip (AGL-3492). One constant, read by both halves:\n * the listing is bounded in DAYS so every day the strip shows carries every\n * open time it has. A flat slot count was the bound before, and at 15-minute\n * steps 120 of them ran out three and a half days in — the widget then drew\n * those days as the whole calendar and cut the fourth at lunch.\n */\nexport const BOOKING_SLOT_PAGE_DAYS = 14\n\n/**\n * The hard ceiling on one listing: a page of days, every one of them open\n * around the clock. Only a service that really is open that much reaches it,\n * and the page then ends early with its next page named, never truncated\n * silently.\n */\nexport const BOOKING_SLOT_PAGE_MAX_SLOTS =\n BOOKING_SLOT_PAGE_DAYS * BOOKING_MAX_SLOTS_PER_DAY\n\n/**\n * Walks the open slots of a service from `fromMs`, handing each to `take`\n * with the service-local day it falls on, until `take` answers `false` or\n * the walk reaches `toMs` (never more than BOOKING_MAX_DAYS_AHEAD past\n * `fromMs`). Answers where it stopped: the first start it did not hand over\n * — the slot `take` refused, or the first one past the walked range — and\n * whether that was `toMs` itself, so nothing is left to walk.\n *\n * Windows are read in the service timezone, 15 minutes at a time —\n * O(minutes/15) and immune to DST arithmetic because weekday and minutes\n * come from Intl per instant. Intervals overlapping `booked` drop out.\n */\nfunction walkOpenSlots(\n service: HostBookingService,\n fromMs: number,\n toMs: number,\n booked: BookedInterval[],\n take: (slot: BookingSlot, dayKey: string) => boolean,\n): { stoppedAtMs: number; reachedEnd: boolean } {\n const timezone = service.timezone || 'UTC'\n const duration = Math.min(\n Math.max(\n Math.round(service.durationMinutes || 0),\n BOOKING_MIN_DURATION_MINUTES,\n ),\n BOOKING_MAX_DURATION_MINUTES,\n )\n const durationMs = duration * 60_000\n const horizonMs = Math.min(\n toMs,\n fromMs + BOOKING_MAX_DAYS_AHEAD * 24 * 60 * 60_000,\n )\n const stepMs = BOOKING_SLOT_STEP_MINUTES * 60_000\n let atMs = Math.ceil(fromMs / stepMs) * stepMs\n for (; atMs + durationMs <= horizonMs; atMs += stepMs) {\n const { year, month, day, weekday, hour, minute } = zonedDateTime(\n atMs,\n timezone,\n )\n const minutes = hour * 60 + minute\n const windows = service.windows?.[weekday] ?? []\n const fitsWindow = windows.some(\n (window) =>\n minutes >= window.start && minutes + duration <= window.end,\n )\n if (!fitsWindow) continue\n const endMs = atMs + durationMs\n const collides = booked.some(\n (interval) =>\n atMs < interval.endsAtMs && endMs > interval.startsAtMs,\n )\n if (collides) continue\n if (!take({ startsAtMs: atMs, endsAtMs: endMs }, `${year}-${month}-${day}`)) {\n return { stoppedAtMs: atMs, reachedEnd: false }\n }\n }\n // The first start the walk did not try, not the horizon itself: a slot\n // that starts before the cap and ends after it belongs to the next walk.\n return { stoppedAtMs: atMs, reachedEnd: horizonMs >= toMs }\n}\n\n/**\n * Enumerates open slots for a service between two instants. Bounded: at\n * most `limit` slots, never beyond BOOKING_MAX_DAYS_AHEAD.\n */\nexport function computeOpenSlots(\n service: HostBookingService,\n fromMs: number,\n toMs: number,\n booked: BookedInterval[] = [],\n limit = 200,\n): BookingSlot[] {\n const slots: BookingSlot[] = []\n if (limit <= 0) return slots\n walkOpenSlots(service, fromMs, toMs, booked, (slot) => {\n slots.push(slot)\n return slots.length < limit\n })\n return slots\n}\n\n/** One page of a slot listing (AGL-3492). */\nexport interface BookingSlotPage {\n /** Every open slot of the page's days, in order. */\n slots: BookingSlot[]\n /**\n * Where the next page starts, or `null` once the page reached `toMs`.\n * Every open slot before it is in `slots` — the guarantee a reader needs\n * to tell a day it holds whole from one the page ended inside.\n */\n nextFromMs: number | null\n}\n\n/**\n * The open slots of the next `days` service-local days that have any, from\n * `fromMs` up to `toMs` — whole days, never a day cut part way through\n * (AGL-3492). `maxSlots` is the ceiling for a service open around the clock;\n * reaching it ends the page early with `nextFromMs` naming the first slot\n * left out. A walk is also never longer than BOOKING_MAX_DAYS_AHEAD, so a\n * horizon further out than that is answered a page at a time.\n */\nexport function computeOpenSlotPage(\n service: HostBookingService,\n fromMs: number,\n toMs: number,\n booked: BookedInterval[] = [],\n {\n days = BOOKING_SLOT_PAGE_DAYS,\n maxSlots = BOOKING_SLOT_PAGE_MAX_SLOTS,\n }: { days?: number; maxSlots?: number } = {},\n): BookingSlotPage {\n const slots: BookingSlot[] = []\n const dayKeys = new Set<string>()\n const { stoppedAtMs, reachedEnd } = walkOpenSlots(\n service,\n fromMs,\n toMs,\n booked,\n (slot, dayKey) => {\n if (\n slots.length >= maxSlots ||\n (!dayKeys.has(dayKey) && dayKeys.size >= days)\n ) {\n return false\n }\n dayKeys.add(dayKey)\n slots.push(slot)\n return true\n },\n )\n return {\n slots,\n nextFromMs: reachedEnd ? null : stoppedAtMs,\n }\n}\n\n/** True when the exact slot is open for the service (booking API check). */\nexport function isSlotOpen(\n service: HostBookingService,\n startsAtMs: number,\n booked: BookedInterval[] = [],\n): boolean {\n const slots = computeOpenSlots(\n service,\n startsAtMs,\n startsAtMs + BOOKING_MAX_DURATION_MINUTES * 60_000,\n booked,\n 1,\n )\n return slots.length > 0 && slots[0].startsAtMs === startsAtMs\n}\n\n/**\n * The page on the site that holds the Booking block, as the plugin's\n * `bookingPath` setting stores it (AGL-2660). The site root by default: a\n * site that put the block on its home page needs no setting, and a site\n * that put it elsewhere says where once, per site.\n */\nexport const BOOKING_PATH_DEFAULT = '/'\n\n/** `/book`, `book/`, ` /book?x ` → `/book`; anything empty → the root. */\nexport function normalizeBookingPath(raw: unknown): string {\n const text = String(raw ?? '')\n .trim()\n .split(/[?#]/)[0]\n if (!text) return BOOKING_PATH_DEFAULT\n const leading = text.startsWith('/') ? text : `/${text}`\n const trimmed = leading.replace(/\\/+$/, '')\n return trimmed || BOOKING_PATH_DEFAULT\n}\n\nexport interface BookingLinkInput {\n /** The host's public naming — its custom domain or its subdomain. */\n site: { cname?: string | null; subdomain?: string | null } | null | undefined\n /** The service the link opens the widget on. */\n service: { id: string }\n /** The site's `bookingPath` setting; the root when unset. */\n path?: string | null\n /** The record the link is dropped from, when there is one. */\n recordRef?: BookingRecordRef | null\n}\n\n/**\n * The public URL a visitor books a service at (AGL-2660), or `null` for a\n * site with no public origin yet.\n *\n * PER SERVICE, not per member: the model carries no staff or per-member\n * notion — a service has one calendar — so there is one link per service\n * and nothing narrower. The Bookings plugin renders no route of its own\n * either; the widget is a block the site owner placed on a page, so the\n * link is that page (the `bookingPath` setting) with the service\n * preselected and, when dropped from a record, the record carried\n * along so the booking can be attributed even when the booker uses a\n * different address.\n */\nexport function bookingLinkFor(input: BookingLinkInput): string | null {\n const origin = hostPublicOrigin(input.site)\n if (!origin) return null\n const query = new URLSearchParams({ [BOOKING_SERVICE_PARAM]: input.service.id })\n if (input.recordRef) query.set(BOOKING_RECORD_PARAM, formatBookingRecordRef(input.recordRef))\n return `${origin}${normalizeBookingPath(input.path)}?${query.toString()}`\n}\n"],"names":["hostPublicOrigin","zonedDateTime","BOOKING_RECORD_PARAM","BOOKING_SERVICE_PARAM","formatBookingRecordRef","BOOKING_MIN_DURATION_MINUTES","BOOKING_MAX_DURATION_MINUTES","BOOKING_MAX_DAYS_AHEAD","REMINDER_WINDOW_START_HOURS","REMINDER_WINDOW_END_HOURS","isBookingReminderDue","booking","nowMs","startsAtMs","Number","status","reminderSentAt","Boolean","email","BOOKING_SLOT_STEP_MINUTES","BOOKING_MAX_SLOTS_PER_DAY","BOOKING_SLOT_PAGE_DAYS","BOOKING_SLOT_PAGE_MAX_SLOTS","walkOpenSlots","service","fromMs","toMs","booked","take","timezone","duration","Math","min","max","round","durationMinutes","durationMs","horizonMs","stepMs","atMs","ceil","year","month","day","weekday","hour","minute","minutes","windows","fitsWindow","some","window","start","end","endMs","collides","interval","endsAtMs","stoppedAtMs","reachedEnd","computeOpenSlots","limit","slots","slot","push","length","computeOpenSlotPage","days","maxSlots","dayKeys","Set","dayKey","has","size","add","nextFromMs","isSlotOpen","BOOKING_PATH_DEFAULT","normalizeBookingPath","raw","text","String","trim","split","leading","startsWith","trimmed","replace","bookingLinkFor","input","origin","site","query","URLSearchParams","id","recordRef","set","path","toString"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/model/bookings.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\n// Leaf paths, not the barrel: this model is read by the client bundle and\n// the `/server` entry alike, and either barrel would drag the other's\n// surface into a bundle that has no use for it.\nimport { hostPublicOrigin } from '@aglyn/aglyn/app-utils/host-naming'\n// The leaf entry: the package root extends the Firestore SDK's `Timestamp`,\n// and this model is read on published pages.\nimport { zonedDateTime } from '@aglyn/shared-util-timestamp/zoned-time'\nimport {\n BOOKING_RECORD_PARAM,\n BOOKING_SERVICE_PARAM,\n type BookingRecordRef,\n formatBookingRecordRef,\n} from './booking-record'\n\n/**\n * Bookings v1 (AGL-159): services with weekly availability windows and\n * pure, timezone-explicit slot computation. All times are minutes since\n * midnight in the HOST's timezone; instants are epoch milliseconds. No\n * I/O here — collision checks against stored bookings happen in the\n * booking API over the values these helpers produce.\n */\n\n/** `hosts/{hostId}/services/{id}` doc. */\nexport interface HostBookingService {\n name: string\n durationMinutes: number\n description?: string\n /** Optional price; 0/absent means free. */\n priceUsd?: number\n /**\n * How the price is stated (AGL-3475): `fixed` (absent) charges `priceUsd`;\n * `varies`, `estimate` and `contact` show their label and book with no\n * charge. Read through `bookingPriceDisplay` / `bookingChargeUsd`.\n */\n priceDisplay?: 'fixed' | 'varies' | 'estimate' | 'contact'\n /**\n * Weekly availability: `windows[weekday]` (0 = Sunday … 6 = Saturday)\n * lists open intervals in minutes since midnight, host-local.\n */\n windows?: Partial<Record<number, Array<{ start: number; end: number }>>>\n /** IANA timezone the windows are defined in, e.g. \"America/Chicago\". */\n timezone?: string\n /**\n * Create a follow-up task on the CRM record when this service is booked\n * (AGL-2660) — due one business day after the slot, on whoever holds the\n * relationship. Off unless the service says so.\n */\n crmFollowUpTask?: boolean\n /**\n * File the booking as a `meeting` on the CRM record's timeline (AGL-2660).\n * Absent means ON: a booking is a meeting the record has, and the service\n * has to opt out of saying so.\n */\n crmMeetingActivity?: boolean\n /**\n * Ask the booker for a phone number (AGL-3493): `optional` or `required`;\n * absent is off. Read through `bookingFieldAsk`.\n */\n askPhone?: 'off' | 'optional' | 'required'\n /** Ask the booker for the address the service is at (AGL-3493); as above. */\n askAddress?: 'off' | 'optional' | 'required'\n /**\n * Whether the service takes bookings (AGL-3616). A `draft` is set up and\n * offered nowhere: the public listing leaves it out, its slots are not\n * served and the booking route refuses it, until someone activates it in\n * the console. Absent is `active`, so every service made before the field\n * existed keeps taking bookings. Read through `bookingServiceStatus`.\n */\n status?: BookingServiceStatus\n}\n\n/** Whether a service takes bookings (AGL-3616). */\nexport type BookingServiceStatus = 'draft' | 'active'\n\n/** The status as stored, read so only an explicit `draft` holds a service back. */\nexport function bookingServiceStatus(value: unknown): BookingServiceStatus {\n return value === 'draft' ? 'draft' : 'active'\n}\n\n/**\n * Whether a stored service is offered to visitors (AGL-3616): not deleted,\n * and not a draft. The one predicate every public read asks — the service\n * directory, a service's slots, the booking route — and the console's\n * booking-link picker, which hands visitors a link to the same door.\n */\nexport function bookingServiceIsOffered(\n service: { deletedAt?: unknown; status?: unknown } | null | undefined,\n): boolean {\n return Boolean(service) && !service?.deletedAt && bookingServiceStatus(service?.status) === 'active'\n}\n\n/** Booked interval as epoch-ms instants. */\nexport interface BookedInterval {\n startsAtMs: number\n endsAtMs: number\n}\n\nexport const BOOKING_MIN_DURATION_MINUTES = 5\nexport const BOOKING_MAX_DURATION_MINUTES = 8 * 60\n/** Slot enumeration horizon; the console/API never look further out. */\nexport const BOOKING_MAX_DAYS_AHEAD = 60\n\n/**\n * The band a booking has to fall in to earn its 24-hour reminder, in hours\n * from now (AGL-160, scheduled in AGL-2431).\n *\n * IN THE MODEL, not beside the scan that uses them, so the console card can\n * apply the identical window without importing server code into a client\n * bundle. That sharing is the point rather than a convenience: a card that\n * drew its own boundary would report a queue depth the job does not act on,\n * and a merchant checking whether reminders are working would be reading a\n * number produced by different arithmetic than the sender's.\n */\nexport const REMINDER_WINDOW_START_HOURS = 23\nexport const REMINDER_WINDOW_END_HOURS = 25\n\n/**\n * Whether `booking` is one this pass would mail — the three tests the scan\n * applies, in one place both halves call.\n *\n * `nowMs` is a parameter and not `Date.now()` so the card and the job can be\n * asked the same question about the same instant, and so a test can pin a\n * booking exactly on either edge of the band.\n */\nexport function isBookingReminderDue(\n booking: {\n status?: string\n email?: string\n reminderSentAt?: unknown\n startsAtMs?: number\n },\n nowMs: number,\n): boolean {\n const startsAtMs = Number(booking.startsAtMs ?? 0)\n return (\n booking.status !== 'canceled' &&\n !booking.reminderSentAt &&\n Boolean(booking.email) &&\n startsAtMs >= nowMs + REMINDER_WINDOW_START_HOURS * 60 * 60 * 1000 &&\n startsAtMs <= nowMs + REMINDER_WINDOW_END_HOURS * 60 * 60 * 1000\n )\n}\n\nexport interface BookingSlot {\n startsAtMs: number\n endsAtMs: number\n}\n\n/** The walk's resolution: a slot may start on any quarter hour. */\nexport const BOOKING_SLOT_STEP_MINUTES = 15\n\n/**\n * The most starts one calendar day can hold at that resolution — 96. The\n * per-day unit every bound on a slot listing is counted in.\n */\nexport const BOOKING_MAX_SLOTS_PER_DAY =\n (24 * 60) / BOOKING_SLOT_STEP_MINUTES\n\n/**\n * Days of open times one slot listing answers with, and the length of the\n * Booking widget's day strip (AGL-3492). One constant, read by both halves:\n * the listing is bounded in DAYS so every day the strip shows carries every\n * open time it has. A flat slot count was the bound before, and at 15-minute\n * steps 120 of them ran out three and a half days in — the widget then drew\n * those days as the whole calendar and cut the fourth at lunch.\n */\nexport const BOOKING_SLOT_PAGE_DAYS = 14\n\n/**\n * The hard ceiling on one listing: a page of days, every one of them open\n * around the clock. Only a service that really is open that much reaches it,\n * and the page then ends early with its next page named, never truncated\n * silently.\n */\nexport const BOOKING_SLOT_PAGE_MAX_SLOTS =\n BOOKING_SLOT_PAGE_DAYS * BOOKING_MAX_SLOTS_PER_DAY\n\n/**\n * Walks the open slots of a service from `fromMs`, handing each to `take`\n * with the service-local day it falls on, until `take` answers `false` or\n * the walk reaches `toMs` (never more than BOOKING_MAX_DAYS_AHEAD past\n * `fromMs`). Answers where it stopped: the first start it did not hand over\n * — the slot `take` refused, or the first one past the walked range — and\n * whether that was `toMs` itself, so nothing is left to walk.\n *\n * Windows are read in the service timezone, 15 minutes at a time —\n * O(minutes/15) and immune to DST arithmetic because weekday and minutes\n * come from Intl per instant. Intervals overlapping `booked` drop out.\n */\nfunction walkOpenSlots(\n service: HostBookingService,\n fromMs: number,\n toMs: number,\n booked: BookedInterval[],\n take: (slot: BookingSlot, dayKey: string) => boolean,\n): { stoppedAtMs: number; reachedEnd: boolean } {\n const timezone = service.timezone || 'UTC'\n const duration = Math.min(\n Math.max(\n Math.round(service.durationMinutes || 0),\n BOOKING_MIN_DURATION_MINUTES,\n ),\n BOOKING_MAX_DURATION_MINUTES,\n )\n const durationMs = duration * 60_000\n const horizonMs = Math.min(\n toMs,\n fromMs + BOOKING_MAX_DAYS_AHEAD * 24 * 60 * 60_000,\n )\n const stepMs = BOOKING_SLOT_STEP_MINUTES * 60_000\n let atMs = Math.ceil(fromMs / stepMs) * stepMs\n for (; atMs + durationMs <= horizonMs; atMs += stepMs) {\n const { year, month, day, weekday, hour, minute } = zonedDateTime(\n atMs,\n timezone,\n )\n const minutes = hour * 60 + minute\n const windows = service.windows?.[weekday] ?? []\n const fitsWindow = windows.some(\n (window) =>\n minutes >= window.start && minutes + duration <= window.end,\n )\n if (!fitsWindow) continue\n const endMs = atMs + durationMs\n const collides = booked.some(\n (interval) =>\n atMs < interval.endsAtMs && endMs > interval.startsAtMs,\n )\n if (collides) continue\n if (!take({ startsAtMs: atMs, endsAtMs: endMs }, `${year}-${month}-${day}`)) {\n return { stoppedAtMs: atMs, reachedEnd: false }\n }\n }\n // The first start the walk did not try, not the horizon itself: a slot\n // that starts before the cap and ends after it belongs to the next walk.\n return { stoppedAtMs: atMs, reachedEnd: horizonMs >= toMs }\n}\n\n/**\n * Enumerates open slots for a service between two instants. Bounded: at\n * most `limit` slots, never beyond BOOKING_MAX_DAYS_AHEAD.\n */\nexport function computeOpenSlots(\n service: HostBookingService,\n fromMs: number,\n toMs: number,\n booked: BookedInterval[] = [],\n limit = 200,\n): BookingSlot[] {\n const slots: BookingSlot[] = []\n if (limit <= 0) return slots\n walkOpenSlots(service, fromMs, toMs, booked, (slot) => {\n slots.push(slot)\n return slots.length < limit\n })\n return slots\n}\n\n/** One page of a slot listing (AGL-3492). */\nexport interface BookingSlotPage {\n /** Every open slot of the page's days, in order. */\n slots: BookingSlot[]\n /**\n * Where the next page starts, or `null` once the page reached `toMs`.\n * Every open slot before it is in `slots` — the guarantee a reader needs\n * to tell a day it holds whole from one the page ended inside.\n */\n nextFromMs: number | null\n}\n\n/**\n * The open slots of the next `days` service-local days that have any, from\n * `fromMs` up to `toMs` — whole days, never a day cut part way through\n * (AGL-3492). `maxSlots` is the ceiling for a service open around the clock;\n * reaching it ends the page early with `nextFromMs` naming the first slot\n * left out. A walk is also never longer than BOOKING_MAX_DAYS_AHEAD, so a\n * horizon further out than that is answered a page at a time.\n */\nexport function computeOpenSlotPage(\n service: HostBookingService,\n fromMs: number,\n toMs: number,\n booked: BookedInterval[] = [],\n {\n days = BOOKING_SLOT_PAGE_DAYS,\n maxSlots = BOOKING_SLOT_PAGE_MAX_SLOTS,\n }: { days?: number; maxSlots?: number } = {},\n): BookingSlotPage {\n const slots: BookingSlot[] = []\n const dayKeys = new Set<string>()\n const { stoppedAtMs, reachedEnd } = walkOpenSlots(\n service,\n fromMs,\n toMs,\n booked,\n (slot, dayKey) => {\n if (\n slots.length >= maxSlots ||\n (!dayKeys.has(dayKey) && dayKeys.size >= days)\n ) {\n return false\n }\n dayKeys.add(dayKey)\n slots.push(slot)\n return true\n },\n )\n return {\n slots,\n nextFromMs: reachedEnd ? null : stoppedAtMs,\n }\n}\n\n/** True when the exact slot is open for the service (booking API check). */\nexport function isSlotOpen(\n service: HostBookingService,\n startsAtMs: number,\n booked: BookedInterval[] = [],\n): boolean {\n const slots = computeOpenSlots(\n service,\n startsAtMs,\n startsAtMs + BOOKING_MAX_DURATION_MINUTES * 60_000,\n booked,\n 1,\n )\n return slots.length > 0 && slots[0].startsAtMs === startsAtMs\n}\n\n/**\n * The page on the site that holds the Booking block, as the plugin's\n * `bookingPath` setting stores it (AGL-2660). The site root by default: a\n * site that put the block on its home page needs no setting, and a site\n * that put it elsewhere says where once, per site.\n */\nexport const BOOKING_PATH_DEFAULT = '/'\n\n/** `/book`, `book/`, ` /book?x ` → `/book`; anything empty → the root. */\nexport function normalizeBookingPath(raw: unknown): string {\n const text = String(raw ?? '')\n .trim()\n .split(/[?#]/)[0]\n if (!text) return BOOKING_PATH_DEFAULT\n const leading = text.startsWith('/') ? text : `/${text}`\n const trimmed = leading.replace(/\\/+$/, '')\n return trimmed || BOOKING_PATH_DEFAULT\n}\n\nexport interface BookingLinkInput {\n /** The host's public naming — its custom domain or its subdomain. */\n site: { cname?: string | null; subdomain?: string | null } | null | undefined\n /** The service the link opens the widget on. */\n service: { id: string }\n /** The site's `bookingPath` setting; the root when unset. */\n path?: string | null\n /** The record the link is dropped from, when there is one. */\n recordRef?: BookingRecordRef | null\n}\n\n/**\n * The public URL a visitor books a service at (AGL-2660), or `null` for a\n * site with no public origin yet.\n *\n * PER SERVICE, not per member: the model carries no staff or per-member\n * notion — a service has one calendar — so there is one link per service\n * and nothing narrower. The Bookings plugin renders no route of its own\n * either; the widget is a block the site owner placed on a page, so the\n * link is that page (the `bookingPath` setting) with the service\n * preselected and, when dropped from a record, the record carried\n * along so the booking can be attributed even when the booker uses a\n * different address.\n */\nexport function bookingLinkFor(input: BookingLinkInput): string | null {\n const origin = hostPublicOrigin(input.site)\n if (!origin) return null\n const query = new URLSearchParams({ [BOOKING_SERVICE_PARAM]: input.service.id })\n if (input.recordRef) query.set(BOOKING_RECORD_PARAM, formatBookingRecordRef(input.recordRef))\n return `${origin}${normalizeBookingPath(input.path)}?${query.toString()}`\n}\n"],"names":["hostPublicOrigin","zonedDateTime","BOOKING_RECORD_PARAM","BOOKING_SERVICE_PARAM","formatBookingRecordRef","bookingServiceStatus","value","bookingServiceIsOffered","service","Boolean","deletedAt","status","BOOKING_MIN_DURATION_MINUTES","BOOKING_MAX_DURATION_MINUTES","BOOKING_MAX_DAYS_AHEAD","REMINDER_WINDOW_START_HOURS","REMINDER_WINDOW_END_HOURS","isBookingReminderDue","booking","nowMs","startsAtMs","Number","reminderSentAt","email","BOOKING_SLOT_STEP_MINUTES","BOOKING_MAX_SLOTS_PER_DAY","BOOKING_SLOT_PAGE_DAYS","BOOKING_SLOT_PAGE_MAX_SLOTS","walkOpenSlots","fromMs","toMs","booked","take","timezone","duration","Math","min","max","round","durationMinutes","durationMs","horizonMs","stepMs","atMs","ceil","year","month","day","weekday","hour","minute","minutes","windows","fitsWindow","some","window","start","end","endMs","collides","interval","endsAtMs","stoppedAtMs","reachedEnd","computeOpenSlots","limit","slots","slot","push","length","computeOpenSlotPage","days","maxSlots","dayKeys","Set","dayKey","has","size","add","nextFromMs","isSlotOpen","BOOKING_PATH_DEFAULT","normalizeBookingPath","raw","text","String","trim","split","leading","startsWith","trimmed","replace","bookingLinkFor","input","origin","site","query","URLSearchParams","id","recordRef","set","path","toString"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,0EAA0E;AAC1E,sEAAsE;AACtE,gDAAgD;AAChD,SAASA,gBAAgB,QAAQ,qCAAoC;AACrE,4EAA4E;AAC5E,6CAA6C;AAC7C,SAASC,aAAa,QAAQ,0CAAyC;AACvE,SACEC,oBAAoB,EACpBC,qBAAqB,EAErBC,sBAAsB,QACjB,sBAAkB;AA8DzB,iFAAiF,GACjF,OAAO,SAASC,qBAAqBC,KAAc;IACjD,OAAOA,UAAU,UAAU,UAAU;AACvC;AAEA;;;;;CAKC,GACD,OAAO,SAASC,wBACdC,OAAqE;IAErE,OAAOC,QAAQD,YAAY,EAACA,2BAAAA,QAASE,SAAS,KAAIL,qBAAqBG,2BAAAA,QAASG,MAAM,MAAM;AAC9F;AAQA,OAAO,MAAMC,+BAA+B,EAAC;AAC7C,OAAO,MAAMC,+BAA+B,IAAI,GAAE;AAClD,sEAAsE,GACtE,OAAO,MAAMC,yBAAyB,GAAE;AAExC;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,8BAA8B,GAAE;AAC7C,OAAO,MAAMC,4BAA4B,GAAE;AAE3C;;;;;;;CAOC,GACD,OAAO,SAASC,qBACdC,OAKC,EACDC,KAAa;QAEaD;IAA1B,MAAME,aAAaC,QAAOH,sBAAAA,QAAQE,UAAU,YAAlBF,sBAAsB;IAChD,OACEA,QAAQP,MAAM,KAAK,cACnB,CAACO,QAAQI,cAAc,IACvBb,QAAQS,QAAQK,KAAK,KACrBH,cAAcD,QAAQJ,8BAA8B,KAAK,KAAK,QAC9DK,cAAcD,QAAQH,4BAA4B,KAAK,KAAK;AAEhE;AAOA,iEAAiE,GACjE,OAAO,MAAMQ,4BAA4B,GAAE;AAE3C;;;CAGC,GACD,OAAO,MAAMC,4BACX,AAAC,KAAK,KAAMD,0BAAyB;AAEvC;;;;;;;CAOC,GACD,OAAO,MAAME,yBAAyB,GAAE;AAExC;;;;;CAKC,GACD,OAAO,MAAMC,8BACXD,yBAAyBD,0BAAyB;AAEpD;;;;;;;;;;;CAWC,GACD,SAASG,cACPpB,OAA2B,EAC3BqB,MAAc,EACdC,IAAY,EACZC,MAAwB,EACxBC,IAAoD;IAEpD,MAAMC,WAAWzB,QAAQyB,QAAQ,IAAI;IACrC,MAAMC,WAAWC,KAAKC,GAAG,CACvBD,KAAKE,GAAG,CACNF,KAAKG,KAAK,CAAC9B,QAAQ+B,eAAe,IAAI,IACtC3B,+BAEFC;IAEF,MAAM2B,aAAaN,WAAW;IAC9B,MAAMO,YAAYN,KAAKC,GAAG,CACxBN,MACAD,SAASf,yBAAyB,KAAK,KAAK;IAE9C,MAAM4B,SAASlB,4BAA4B;IAC3C,IAAImB,OAAOR,KAAKS,IAAI,CAACf,SAASa,UAAUA;IACxC,MAAOC,OAAOH,cAAcC,WAAWE,QAAQD,OAAQ;;YAMrClC;QALhB,MAAM,EAAEqC,IAAI,EAAEC,KAAK,EAAEC,GAAG,EAAEC,OAAO,EAAEC,IAAI,EAAEC,MAAM,EAAE,GAAGjD,cAClD0C,MACAV;QAEF,MAAMkB,UAAUF,OAAO,KAAKC;QAC5B,MAAME,mBAAU5C,mBAAAA,QAAQ4C,OAAO,qBAAf5C,gBAAiB,CAACwC,QAAQ,mBAAI,EAAE;QAChD,MAAMK,aAAaD,QAAQE,IAAI,CAC7B,CAACC,SACCJ,WAAWI,OAAOC,KAAK,IAAIL,UAAUjB,YAAYqB,OAAOE,GAAG;QAE/D,IAAI,CAACJ,YAAY;QACjB,MAAMK,QAAQf,OAAOH;QACrB,MAAMmB,WAAW5B,OAAOuB,IAAI,CAC1B,CAACM,WACCjB,OAAOiB,SAASC,QAAQ,IAAIH,QAAQE,SAASxC,UAAU;QAE3D,IAAIuC,UAAU;QACd,IAAI,CAAC3B,KAAK;YAAEZ,YAAYuB;YAAMkB,UAAUH;QAAM,GAAG,GAAGb,KAAK,CAAC,EAAEC,MAAM,CAAC,EAAEC,KAAK,GAAG;YAC3E,OAAO;gBAAEe,aAAanB;gBAAMoB,YAAY;YAAM;QAChD;IACF;IACA,uEAAuE;IACvE,yEAAyE;IACzE,OAAO;QAAED,aAAanB;QAAMoB,YAAYtB,aAAaX;IAAK;AAC5D;AAEA;;;CAGC,GACD,OAAO,SAASkC,iBACdxD,OAA2B,EAC3BqB,MAAc,EACdC,IAAY,EACZC,SAA2B,EAAE,EAC7BkC,QAAQ,GAAG;IAEX,MAAMC,QAAuB,EAAE;IAC/B,IAAID,SAAS,GAAG,OAAOC;IACvBtC,cAAcpB,SAASqB,QAAQC,MAAMC,QAAQ,CAACoC;QAC5CD,MAAME,IAAI,CAACD;QACX,OAAOD,MAAMG,MAAM,GAAGJ;IACxB;IACA,OAAOC;AACT;AAcA;;;;;;;CAOC,GACD,OAAO,SAASI,oBACd9D,OAA2B,EAC3BqB,MAAc,EACdC,IAAY,EACZC,SAA2B,EAAE,EAC7B,EACEwC,OAAO7C,sBAAsB,EAC7B8C,WAAW7C,2BAA2B,EACD,GAAG,CAAC,CAAC;IAE5C,MAAMuC,QAAuB,EAAE;IAC/B,MAAMO,UAAU,IAAIC;IACpB,MAAM,EAAEZ,WAAW,EAAEC,UAAU,EAAE,GAAGnC,cAClCpB,SACAqB,QACAC,MACAC,QACA,CAACoC,MAAMQ;QACL,IACET,MAAMG,MAAM,IAAIG,YACf,CAACC,QAAQG,GAAG,CAACD,WAAWF,QAAQI,IAAI,IAAIN,MACzC;YACA,OAAO;QACT;QACAE,QAAQK,GAAG,CAACH;QACZT,MAAME,IAAI,CAACD;QACX,OAAO;IACT;IAEF,OAAO;QACLD;QACAa,YAAYhB,aAAa,OAAOD;IAClC;AACF;AAEA,0EAA0E,GAC1E,OAAO,SAASkB,WACdxE,OAA2B,EAC3BY,UAAkB,EAClBW,SAA2B,EAAE;IAE7B,MAAMmC,QAAQF,iBACZxD,SACAY,YACAA,aAAaP,+BAA+B,OAC5CkB,QACA;IAEF,OAAOmC,MAAMG,MAAM,GAAG,KAAKH,KAAK,CAAC,EAAE,CAAC9C,UAAU,KAAKA;AACrD;AAEA;;;;;CAKC,GACD,OAAO,MAAM6D,uBAAuB,IAAG;AAEvC,wEAAwE,GACxE,OAAO,SAASC,qBAAqBC,GAAY;IAC/C,MAAMC,OAAOC,OAAOF,cAAAA,MAAO,IACxBG,IAAI,GACJC,KAAK,CAAC,OAAO,CAAC,EAAE;IACnB,IAAI,CAACH,MAAM,OAAOH;IAClB,MAAMO,UAAUJ,KAAKK,UAAU,CAAC,OAAOL,OAAO,CAAC,CAAC,EAAEA,MAAM;IACxD,MAAMM,UAAUF,QAAQG,OAAO,CAAC,QAAQ;IACxC,OAAOD,WAAWT;AACpB;AAaA;;;;;;;;;;;;CAYC,GACD,OAAO,SAASW,eAAeC,KAAuB;IACpD,MAAMC,SAAS9F,iBAAiB6F,MAAME,IAAI;IAC1C,IAAI,CAACD,QAAQ,OAAO;IACpB,MAAME,QAAQ,IAAIC,gBAAgB;QAAE,CAAC9F,sBAAsB,EAAE0F,MAAMrF,OAAO,CAAC0F,EAAE;IAAC;IAC9E,IAAIL,MAAMM,SAAS,EAAEH,MAAMI,GAAG,CAAClG,sBAAsBE,uBAAuByF,MAAMM,SAAS;IAC3F,OAAO,GAAGL,SAASZ,qBAAqBW,MAAMQ,IAAI,EAAE,CAAC,EAAEL,MAAMM,QAAQ,IAAI;AAC3E"}
|
|
@@ -0,0 +1,50 @@
|
|
|
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 PluginAiCapability, type PluginAiCapabilityArgs } from '@aglyn/aglyn/plugin-manager/plugin-ai-capabilities';
|
|
18
|
+
/**
|
|
19
|
+
* WHAT AN AI BUILD CAN MAKE IN BOOKINGS (AGL-3616): a bookable service, as a
|
|
20
|
+
* draft.
|
|
21
|
+
*
|
|
22
|
+
* The capability the AI plugin's build planner offers for "a way to book
|
|
23
|
+
* me". Its item is written by this plugin's `booking-service` draft writer,
|
|
24
|
+
* so every rule is the writer's: the plan's `bookings` feature and service
|
|
25
|
+
* allowance, the member's role, the shape a service takes. The service is
|
|
26
|
+
* born a draft and takes no bookings until a person activates it on the
|
|
27
|
+
* Bookings page.
|
|
28
|
+
*
|
|
29
|
+
* The arguments are flat, as the contract requires: one set of opening hours
|
|
30
|
+
* on the weekdays named, which `draftContent` turns into the weekly windows
|
|
31
|
+
* the service stores. A page that places the Booking block depends on the
|
|
32
|
+
* service; when the service cannot be made, the page is built without the
|
|
33
|
+
* block (`degrade: 'omit'`).
|
|
34
|
+
*/
|
|
35
|
+
/** The operation's name in a build plan. */
|
|
36
|
+
export declare const BOOKING_SERVICE_AI_OP = "booking-service";
|
|
37
|
+
/** Weekday names as the planner writes them, in the model's order (0 = Sunday). */
|
|
38
|
+
export declare const BOOKING_SERVICE_AI_DAYS: readonly ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
|
|
39
|
+
/**
|
|
40
|
+
* The writer's content for an item. Hours that cannot be read are handed on
|
|
41
|
+
* as a window the writer refuses, so the item fails with the writer's own
|
|
42
|
+
* sentence rather than being stored with hours nobody asked for.
|
|
43
|
+
*/
|
|
44
|
+
export declare function bookingServiceDraftContentFromArgs(args: PluginAiCapabilityArgs): Readonly<Record<string, unknown>>;
|
|
45
|
+
export declare const bookingServiceAiCapability: PluginAiCapability;
|
|
46
|
+
/**
|
|
47
|
+
* Registers the capability; the console surface calls it beside the writer,
|
|
48
|
+
* since only the console runs AI jobs (AGL-3026). Idempotent.
|
|
49
|
+
*/
|
|
50
|
+
export declare function registerBookingServiceAiCapability(): void;
|