@aglyn/plugins-bookings 1.0.0-beta.143
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +7 -0
- package/package.json +53 -0
- package/src/index.d.ts +19 -0
- package/src/index.js +20 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/book-meeting-action.d.ts +91 -0
- package/src/lib/components/book-meeting-action.js +296 -0
- package/src/lib/components/book-meeting-action.js.map +1 -0
- package/src/lib/components/booking.d.ts +33 -0
- package/src/lib/components/booking.js +463 -0
- package/src/lib/components/booking.js.map +1 -0
- package/src/lib/components/bookings-console-page.d.ts +12 -0
- package/src/lib/components/bookings-console-page.js +743 -0
- package/src/lib/components/bookings-console-page.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +26 -0
- package/src/lib/constants/bundle-common.js +26 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/model/booking-purchase-analytics.d.ts +129 -0
- package/src/lib/model/booking-purchase-analytics.js +197 -0
- package/src/lib/model/booking-purchase-analytics.js.map +1 -0
- package/src/lib/model/bookings.d.ts +141 -0
- package/src/lib/model/bookings.js +124 -0
- package/src/lib/model/bookings.js.map +1 -0
- package/src/lib/model/index.d.ts +22 -0
- package/src/lib/model/index.js +22 -0
- package/src/lib/model/index.js.map +1 -0
- package/src/lib/plugin-config.d.ts +24 -0
- package/src/lib/plugin-config.js +51 -0
- package/src/lib/plugin-config.js.map +1 -0
- package/src/lib/plugin.d.ts +22 -0
- package/src/lib/plugin.js +70 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/server/billing-webhook.d.ts +23 -0
- package/src/lib/server/billing-webhook.js +345 -0
- package/src/lib/server/billing-webhook.js.map +1 -0
- package/src/lib/server/booking-analytics.d.ts +68 -0
- package/src/lib/server/booking-analytics.js +115 -0
- package/src/lib/server/booking-analytics.js.map +1 -0
- package/src/lib/server/booking-crm.d.ts +39 -0
- package/src/lib/server/booking-crm.js +67 -0
- package/src/lib/server/booking-crm.js.map +1 -0
- package/src/lib/server/booking-figures.d.ts +32 -0
- package/src/lib/server/booking-figures.js +155 -0
- package/src/lib/server/booking-figures.js.map +1 -0
- package/src/lib/server/refund.d.ts +65 -0
- package/src/lib/server/refund.js +292 -0
- package/src/lib/server/refund.js.map +1 -0
- package/src/lib/server.d.ts +63 -0
- package/src/lib/server.js +968 -0
- package/src/lib/server.js.map +1 -0
- package/src/lib/site.d.ts +27 -0
- package/src/lib/site.js +50 -0
- package/src/lib/site.js.map +1 -0
- package/src/lib/testing/stand-in-tax-profile.d.ts +29 -0
- package/src/lib/testing/stand-in-tax-profile.js +64 -0
- package/src/lib/testing/stand-in-tax-profile.js.map +1 -0
- package/src/lib/utils/generate-preset-id.d.ts +19 -0
- package/src/lib/utils/generate-preset-id.js +25 -0
- package/src/lib/utils/generate-preset-id.js.map +1 -0
- package/src/lib/utils/use-booking-purchase-event.d.ts +65 -0
- package/src/lib/utils/use-booking-purchase-event.js +147 -0
- package/src/lib/utils/use-booking-purchase-event.js.map +1 -0
|
@@ -0,0 +1,141 @@
|
|
|
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 CrmBookingRef } from '@aglyn/aglyn/app-utils/crm-booking';
|
|
18
|
+
/**
|
|
19
|
+
* Bookings v1 (AGL-159): services with weekly availability windows and
|
|
20
|
+
* pure, timezone-explicit slot computation. All times are minutes since
|
|
21
|
+
* midnight in the HOST's timezone; instants are epoch milliseconds. No
|
|
22
|
+
* I/O here — collision checks against stored bookings happen in the
|
|
23
|
+
* booking API over the values these helpers produce.
|
|
24
|
+
*/
|
|
25
|
+
/** `hosts/{hostId}/services/{id}` doc. */
|
|
26
|
+
export interface HostBookingService {
|
|
27
|
+
name: string;
|
|
28
|
+
durationMinutes: number;
|
|
29
|
+
description?: string;
|
|
30
|
+
/** Optional price; 0/absent means free. */
|
|
31
|
+
priceUsd?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Weekly availability: `windows[weekday]` (0 = Sunday … 6 = Saturday)
|
|
34
|
+
* lists open intervals in minutes since midnight, host-local.
|
|
35
|
+
*/
|
|
36
|
+
windows?: Partial<Record<number, Array<{
|
|
37
|
+
start: number;
|
|
38
|
+
end: number;
|
|
39
|
+
}>>>;
|
|
40
|
+
/** IANA timezone the windows are defined in, e.g. "America/Chicago". */
|
|
41
|
+
timezone?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Create a follow-up task on the CRM record when this service is booked
|
|
44
|
+
* (AGL-2660) — due one business day after the slot, on whoever holds the
|
|
45
|
+
* relationship. Off unless the service says so.
|
|
46
|
+
*/
|
|
47
|
+
crmFollowUpTask?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* File the booking as a `meeting` on the CRM record's timeline (AGL-2660).
|
|
50
|
+
* Absent means ON: a booking is a meeting the record has, and the service
|
|
51
|
+
* has to opt out of saying so.
|
|
52
|
+
*/
|
|
53
|
+
crmMeetingActivity?: boolean;
|
|
54
|
+
}
|
|
55
|
+
/** Booked interval as epoch-ms instants. */
|
|
56
|
+
export interface BookedInterval {
|
|
57
|
+
startsAtMs: number;
|
|
58
|
+
endsAtMs: number;
|
|
59
|
+
}
|
|
60
|
+
export declare const BOOKING_MIN_DURATION_MINUTES = 5;
|
|
61
|
+
export declare const BOOKING_MAX_DURATION_MINUTES: number;
|
|
62
|
+
/** Slot enumeration horizon; the console/API never look further out. */
|
|
63
|
+
export declare const BOOKING_MAX_DAYS_AHEAD = 60;
|
|
64
|
+
/**
|
|
65
|
+
* The band a booking has to fall in to earn its 24-hour reminder, in hours
|
|
66
|
+
* from now (AGL-160, scheduled in AGL-2431).
|
|
67
|
+
*
|
|
68
|
+
* IN THE MODEL, not beside the scan that uses them, so the console card can
|
|
69
|
+
* apply the identical window without importing server code into a client
|
|
70
|
+
* bundle. That sharing is the point rather than a convenience: a card that
|
|
71
|
+
* drew its own boundary would report a queue depth the job does not act on,
|
|
72
|
+
* and a merchant checking whether reminders are working would be reading a
|
|
73
|
+
* number produced by different arithmetic than the sender's.
|
|
74
|
+
*/
|
|
75
|
+
export declare const REMINDER_WINDOW_START_HOURS = 23;
|
|
76
|
+
export declare const REMINDER_WINDOW_END_HOURS = 25;
|
|
77
|
+
/**
|
|
78
|
+
* Whether `booking` is one this pass would mail — the three tests the scan
|
|
79
|
+
* applies, in one place both halves call.
|
|
80
|
+
*
|
|
81
|
+
* `nowMs` is a parameter and not `Date.now()` so the card and the job can be
|
|
82
|
+
* asked the same question about the same instant, and so a test can pin a
|
|
83
|
+
* booking exactly on either edge of the band.
|
|
84
|
+
*/
|
|
85
|
+
export declare function isBookingReminderDue(booking: {
|
|
86
|
+
status?: string;
|
|
87
|
+
email?: string;
|
|
88
|
+
reminderSentAt?: unknown;
|
|
89
|
+
startsAtMs?: number;
|
|
90
|
+
}, nowMs: number): boolean;
|
|
91
|
+
export interface BookingSlot {
|
|
92
|
+
startsAtMs: number;
|
|
93
|
+
endsAtMs: number;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Enumerates open slots for a service between two instants: windows are
|
|
97
|
+
* walked in the service timezone, quantized to the service duration, and
|
|
98
|
+
* intervals overlapping `booked` (or starting before `fromMs`) drop out.
|
|
99
|
+
* Bounded: at most `limit` slots, never beyond BOOKING_MAX_DAYS_AHEAD.
|
|
100
|
+
*/
|
|
101
|
+
export declare function computeOpenSlots(service: HostBookingService, fromMs: number, toMs: number, booked?: BookedInterval[], limit?: number): BookingSlot[];
|
|
102
|
+
/** True when the exact slot is open for the service (booking API check). */
|
|
103
|
+
export declare function isSlotOpen(service: HostBookingService, startsAtMs: number, booked?: BookedInterval[]): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* The page on the site that holds the Booking block, as the plugin's
|
|
106
|
+
* `bookingPath` setting stores it (AGL-2660). The site root by default: a
|
|
107
|
+
* site that put the block on its home page needs no setting, and a site
|
|
108
|
+
* that put it elsewhere says where once, per site.
|
|
109
|
+
*/
|
|
110
|
+
export declare const BOOKING_PATH_DEFAULT = "/";
|
|
111
|
+
/** `/book`, `book/`, ` /book?x ` → `/book`; anything empty → the root. */
|
|
112
|
+
export declare function normalizeBookingPath(raw: unknown): string;
|
|
113
|
+
export interface BookingLinkInput {
|
|
114
|
+
/** The host's public naming — its custom domain or its subdomain. */
|
|
115
|
+
site: {
|
|
116
|
+
cname?: string | null;
|
|
117
|
+
subdomain?: string | null;
|
|
118
|
+
} | null | undefined;
|
|
119
|
+
/** The service the link opens the widget on. */
|
|
120
|
+
service: {
|
|
121
|
+
id: string;
|
|
122
|
+
};
|
|
123
|
+
/** The site's `bookingPath` setting; the root when unset. */
|
|
124
|
+
path?: string | null;
|
|
125
|
+
/** The CRM record the link is dropped from, when there is one. */
|
|
126
|
+
crmRef?: CrmBookingRef | null;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The public URL a visitor books a service at (AGL-2660), or `null` for a
|
|
130
|
+
* site with no public origin yet.
|
|
131
|
+
*
|
|
132
|
+
* PER SERVICE, not per member: the model carries no staff or per-member
|
|
133
|
+
* notion — a service has one calendar — so there is one link per service
|
|
134
|
+
* and nothing narrower. The Bookings plugin renders no route of its own
|
|
135
|
+
* either; the widget is a block the site owner placed on a page, so the
|
|
136
|
+
* link is that page (the `bookingPath` setting) with the service
|
|
137
|
+
* preselected and, when dropped from a CRM record, the record carried
|
|
138
|
+
* along so the booking can be attributed even when the booker uses a
|
|
139
|
+
* different address.
|
|
140
|
+
*/
|
|
141
|
+
export declare function bookingLinkFor(input: BookingLinkInput): string | null;
|
|
@@ -0,0 +1,124 @@
|
|
|
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
|
+
*/ // Leaf paths, not the barrel: this model is read by the client bundle and
|
|
17
|
+
// the `/server` entry alike, and either barrel would drag the other's
|
|
18
|
+
// surface into a bundle that has no use for it.
|
|
19
|
+
import { BOOKING_SERVICE_PARAM, CRM_BOOKING_REF_PARAM, formatCrmBookingRef } from "@aglyn/aglyn/app-utils/crm-booking";
|
|
20
|
+
import { hostPublicOrigin } from "@aglyn/aglyn/app-utils/host-naming";
|
|
21
|
+
// The leaf entry: the package root extends the Firestore SDK's `Timestamp`,
|
|
22
|
+
// and this model is read on published pages.
|
|
23
|
+
import { zonedDateTime } from "@aglyn/shared-util-timestamp/zoned-time";
|
|
24
|
+
export const BOOKING_MIN_DURATION_MINUTES = 5;
|
|
25
|
+
export const BOOKING_MAX_DURATION_MINUTES = 8 * 60;
|
|
26
|
+
/** Slot enumeration horizon; the console/API never look further out. */ export const BOOKING_MAX_DAYS_AHEAD = 60;
|
|
27
|
+
/**
|
|
28
|
+
* The band a booking has to fall in to earn its 24-hour reminder, in hours
|
|
29
|
+
* from now (AGL-160, scheduled in AGL-2431).
|
|
30
|
+
*
|
|
31
|
+
* IN THE MODEL, not beside the scan that uses them, so the console card can
|
|
32
|
+
* apply the identical window without importing server code into a client
|
|
33
|
+
* bundle. That sharing is the point rather than a convenience: a card that
|
|
34
|
+
* drew its own boundary would report a queue depth the job does not act on,
|
|
35
|
+
* and a merchant checking whether reminders are working would be reading a
|
|
36
|
+
* number produced by different arithmetic than the sender's.
|
|
37
|
+
*/ export const REMINDER_WINDOW_START_HOURS = 23;
|
|
38
|
+
export const REMINDER_WINDOW_END_HOURS = 25;
|
|
39
|
+
/**
|
|
40
|
+
* Whether `booking` is one this pass would mail — the three tests the scan
|
|
41
|
+
* applies, in one place both halves call.
|
|
42
|
+
*
|
|
43
|
+
* `nowMs` is a parameter and not `Date.now()` so the card and the job can be
|
|
44
|
+
* asked the same question about the same instant, and so a test can pin a
|
|
45
|
+
* booking exactly on either edge of the band.
|
|
46
|
+
*/ export function isBookingReminderDue(booking, nowMs) {
|
|
47
|
+
var _booking_startsAtMs;
|
|
48
|
+
const startsAtMs = Number((_booking_startsAtMs = booking.startsAtMs) != null ? _booking_startsAtMs : 0);
|
|
49
|
+
return booking.status !== 'canceled' && !booking.reminderSentAt && Boolean(booking.email) && startsAtMs >= nowMs + REMINDER_WINDOW_START_HOURS * 60 * 60 * 1000 && startsAtMs <= nowMs + REMINDER_WINDOW_END_HOURS * 60 * 60 * 1000;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Enumerates open slots for a service between two instants: windows are
|
|
53
|
+
* walked in the service timezone, quantized to the service duration, and
|
|
54
|
+
* intervals overlapping `booked` (or starting before `fromMs`) drop out.
|
|
55
|
+
* Bounded: at most `limit` slots, never beyond BOOKING_MAX_DAYS_AHEAD.
|
|
56
|
+
*/ export function computeOpenSlots(service, fromMs, toMs, booked = [], limit = 200) {
|
|
57
|
+
const timezone = service.timezone || 'UTC';
|
|
58
|
+
const duration = Math.min(Math.max(Math.round(service.durationMinutes || 0), BOOKING_MIN_DURATION_MINUTES), BOOKING_MAX_DURATION_MINUTES);
|
|
59
|
+
const durationMs = duration * 60000;
|
|
60
|
+
const horizonMs = Math.min(toMs, fromMs + BOOKING_MAX_DAYS_AHEAD * 24 * 60 * 60000);
|
|
61
|
+
const slots = [];
|
|
62
|
+
// Walk in 15-minute steps and keep instants that start a window-aligned
|
|
63
|
+
// slot — O(minutes/15) and immune to DST arithmetic because weekday and
|
|
64
|
+
// minutes come from Intl per instant.
|
|
65
|
+
const stepMs = 15 * 60000;
|
|
66
|
+
const alignedFrom = Math.ceil(fromMs / stepMs) * stepMs;
|
|
67
|
+
for(let atMs = alignedFrom; atMs + durationMs <= horizonMs && slots.length < limit; atMs += stepMs){
|
|
68
|
+
var _ref;
|
|
69
|
+
var _service_windows;
|
|
70
|
+
const { weekday, hour, minute } = zonedDateTime(atMs, timezone);
|
|
71
|
+
const minutes = hour * 60 + minute;
|
|
72
|
+
const windows = (_ref = (_service_windows = service.windows) == null ? void 0 : _service_windows[weekday]) != null ? _ref : [];
|
|
73
|
+
const fitsWindow = windows.some((window)=>minutes >= window.start && minutes + duration <= window.end);
|
|
74
|
+
if (!fitsWindow) continue;
|
|
75
|
+
const endMs = atMs + durationMs;
|
|
76
|
+
const collides = booked.some((interval)=>atMs < interval.endsAtMs && endMs > interval.startsAtMs);
|
|
77
|
+
if (collides) continue;
|
|
78
|
+
slots.push({
|
|
79
|
+
startsAtMs: atMs,
|
|
80
|
+
endsAtMs: endMs
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return slots;
|
|
84
|
+
}
|
|
85
|
+
/** True when the exact slot is open for the service (booking API check). */ export function isSlotOpen(service, startsAtMs, booked = []) {
|
|
86
|
+
const slots = computeOpenSlots(service, startsAtMs, startsAtMs + BOOKING_MAX_DURATION_MINUTES * 60000, booked, 1);
|
|
87
|
+
return slots.length > 0 && slots[0].startsAtMs === startsAtMs;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The page on the site that holds the Booking block, as the plugin's
|
|
91
|
+
* `bookingPath` setting stores it (AGL-2660). The site root by default: a
|
|
92
|
+
* site that put the block on its home page needs no setting, and a site
|
|
93
|
+
* that put it elsewhere says where once, per site.
|
|
94
|
+
*/ export const BOOKING_PATH_DEFAULT = '/';
|
|
95
|
+
/** `/book`, `book/`, ` /book?x ` → `/book`; anything empty → the root. */ export function normalizeBookingPath(raw) {
|
|
96
|
+
const text = String(raw != null ? raw : '').trim().split(/[?#]/)[0];
|
|
97
|
+
if (!text) return BOOKING_PATH_DEFAULT;
|
|
98
|
+
const leading = text.startsWith('/') ? text : `/${text}`;
|
|
99
|
+
const trimmed = leading.replace(/\/+$/, '');
|
|
100
|
+
return trimmed || BOOKING_PATH_DEFAULT;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The public URL a visitor books a service at (AGL-2660), or `null` for a
|
|
104
|
+
* site with no public origin yet.
|
|
105
|
+
*
|
|
106
|
+
* PER SERVICE, not per member: the model carries no staff or per-member
|
|
107
|
+
* notion — a service has one calendar — so there is one link per service
|
|
108
|
+
* and nothing narrower. The Bookings plugin renders no route of its own
|
|
109
|
+
* either; the widget is a block the site owner placed on a page, so the
|
|
110
|
+
* link is that page (the `bookingPath` setting) with the service
|
|
111
|
+
* preselected and, when dropped from a CRM record, the record carried
|
|
112
|
+
* along so the booking can be attributed even when the booker uses a
|
|
113
|
+
* different address.
|
|
114
|
+
*/ export function bookingLinkFor(input) {
|
|
115
|
+
const origin = hostPublicOrigin(input.site);
|
|
116
|
+
if (!origin) return null;
|
|
117
|
+
const query = new URLSearchParams({
|
|
118
|
+
[BOOKING_SERVICE_PARAM]: input.service.id
|
|
119
|
+
});
|
|
120
|
+
if (input.crmRef) query.set(CRM_BOOKING_REF_PARAM, formatCrmBookingRef(input.crmRef));
|
|
121
|
+
return `${origin}${normalizeBookingPath(input.path)}?${query.toString()}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
//# sourceMappingURL=bookings.js.map
|
|
@@ -0,0 +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 {\n BOOKING_SERVICE_PARAM,\n CRM_BOOKING_REF_PARAM,\n type CrmBookingRef,\n formatCrmBookingRef,\n} from '@aglyn/aglyn/app-utils/crm-booking'\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'\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 * 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\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/**\n * Enumerates open slots for a service between two instants: windows are\n * walked in the service timezone, quantized to the service duration, and\n * intervals overlapping `booked` (or starting before `fromMs`) drop out.\n * Bounded: at 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 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 slots: BookingSlot[] = []\n // Walk in 15-minute steps and keep instants that start a window-aligned\n // slot — O(minutes/15) and immune to DST arithmetic because weekday and\n // minutes come from Intl per instant.\n const stepMs = 15 * 60_000\n const alignedFrom = Math.ceil(fromMs / stepMs) * stepMs\n for (\n let atMs = alignedFrom;\n atMs + durationMs <= horizonMs && slots.length < limit;\n atMs += stepMs\n ) {\n const { weekday, hour, minute } = zonedDateTime(atMs, timezone)\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 slots.push({ startsAtMs: atMs, endsAtMs: endMs })\n }\n return slots\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 CRM record the link is dropped from, when there is one. */\n crmRef?: CrmBookingRef | 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 CRM 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.crmRef) query.set(CRM_BOOKING_REF_PARAM, formatCrmBookingRef(input.crmRef))\n return `${origin}${normalizeBookingPath(input.path)}?${query.toString()}`\n}\n"],"names":["BOOKING_SERVICE_PARAM","CRM_BOOKING_REF_PARAM","formatCrmBookingRef","hostPublicOrigin","zonedDateTime","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","computeOpenSlots","service","fromMs","toMs","booked","limit","timezone","duration","Math","min","max","round","durationMinutes","durationMs","horizonMs","slots","stepMs","alignedFrom","ceil","atMs","length","weekday","hour","minute","minutes","windows","fitsWindow","some","window","start","end","endMs","collides","interval","endsAtMs","push","isSlotOpen","BOOKING_PATH_DEFAULT","normalizeBookingPath","raw","text","String","trim","split","leading","startsWith","trimmed","replace","bookingLinkFor","input","origin","site","query","URLSearchParams","id","crmRef","set","path","toString"],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* Bookings domain model (AGL-412): slot computation relocated from core app-utils.
|
|
19
|
+
* Context-free — importable by client components, /server handlers, and
|
|
20
|
+
* other plugins/apps via `@aglyn/plugins-bookings/model`.
|
|
21
|
+
*/
|
|
22
|
+
export * from './bookings';
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
* Bookings domain model (AGL-412): slot computation relocated from core app-utils.
|
|
18
|
+
* Context-free — importable by client components, /server handlers, and
|
|
19
|
+
* other plugins/apps via `@aglyn/plugins-bookings/model`.
|
|
20
|
+
*/ export * from "./bookings.js";
|
|
21
|
+
|
|
22
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/model/index.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/**\n * Bookings domain model (AGL-412): slot computation relocated from core app-utils.\n * Context-free — importable by client components, /server handlers, and\n * other plugins/apps via `@aglyn/plugins-bookings/model`.\n */\nexport * from './bookings'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;CAIC,GACD,cAAc,gBAAY"}
|
|
@@ -0,0 +1,24 @@
|
|
|
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 { PluginConfigSchema } from '@aglyn/aglyn';
|
|
18
|
+
/**
|
|
19
|
+
* Bookings plugin config schema (AGL-428) — the first adopter of the
|
|
20
|
+
* per-plugin settings framework. PURE DATA (type-only aglyn import): the
|
|
21
|
+
* client barrel registers it via '@aglyn/aglyn' and the /server entry via
|
|
22
|
+
* '@aglyn/aglyn/server', so neither bundle drags in the other's barrel.
|
|
23
|
+
*/
|
|
24
|
+
export declare const BOOKINGS_CONFIG_SCHEMA: PluginConfigSchema;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { BOOKING_MAX_DAYS_AHEAD, BOOKING_PATH_DEFAULT } from "./model/index.js";
|
|
17
|
+
/**
|
|
18
|
+
* Bookings plugin config schema (AGL-428) — the first adopter of the
|
|
19
|
+
* per-plugin settings framework. PURE DATA (type-only aglyn import): the
|
|
20
|
+
* client barrel registers it via '@aglyn/aglyn' and the /server entry via
|
|
21
|
+
* '@aglyn/aglyn/server', so neither bundle drags in the other's barrel.
|
|
22
|
+
*/ export const BOOKINGS_CONFIG_SCHEMA = {
|
|
23
|
+
pluginId: 'bookings',
|
|
24
|
+
fields: [
|
|
25
|
+
{
|
|
26
|
+
key: 'maxDaysAhead',
|
|
27
|
+
label: 'Booking horizon (days)',
|
|
28
|
+
type: 'number',
|
|
29
|
+
min: 1,
|
|
30
|
+
max: 365,
|
|
31
|
+
description: 'How far ahead visitors can book. Slots beyond this many days ' + 'from now are not offered.'
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
// Where a booking link sends people (AGL-2660). The Booking block is
|
|
35
|
+
// placed on whichever page the site owner chose, and nothing records
|
|
36
|
+
// which; the CRM's "Book a meeting" links and the "Insert booking
|
|
37
|
+
// link" chip build on this path. Per site, through the ordinary
|
|
38
|
+
// site-level override.
|
|
39
|
+
key: 'bookingPath',
|
|
40
|
+
label: 'Booking page path',
|
|
41
|
+
type: 'string',
|
|
42
|
+
description: 'The page on your site that holds the Booking block, e.g. /book. ' + 'Booking links from the CRM open this page with the service ' + 'preselected.'
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
defaults: {
|
|
46
|
+
maxDaysAhead: BOOKING_MAX_DAYS_AHEAD,
|
|
47
|
+
bookingPath: BOOKING_PATH_DEFAULT
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=plugin-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/bookings/src/lib/plugin-config.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginConfigSchema } from '@aglyn/aglyn'\nimport { BOOKING_MAX_DAYS_AHEAD, BOOKING_PATH_DEFAULT } from './model'\n\n/**\n * Bookings plugin config schema (AGL-428) — the first adopter of the\n * per-plugin settings framework. PURE DATA (type-only aglyn import): the\n * client barrel registers it via '@aglyn/aglyn' and the /server entry via\n * '@aglyn/aglyn/server', so neither bundle drags in the other's barrel.\n */\nexport const BOOKINGS_CONFIG_SCHEMA: PluginConfigSchema = {\n pluginId: 'bookings',\n fields: [\n {\n key: 'maxDaysAhead',\n label: 'Booking horizon (days)',\n type: 'number',\n min: 1,\n max: 365,\n description:\n 'How far ahead visitors can book. Slots beyond this many days ' +\n 'from now are not offered.',\n },\n {\n // Where a booking link sends people (AGL-2660). The Booking block is\n // placed on whichever page the site owner chose, and nothing records\n // which; the CRM's \"Book a meeting\" links and the \"Insert booking\n // link\" chip build on this path. Per site, through the ordinary\n // site-level override.\n key: 'bookingPath',\n label: 'Booking page path',\n type: 'string',\n description:\n 'The page on your site that holds the Booking block, e.g. /book. ' +\n 'Booking links from the CRM open this page with the service ' +\n 'preselected.',\n },\n ],\n defaults: { maxDaysAhead: BOOKING_MAX_DAYS_AHEAD, bookingPath: BOOKING_PATH_DEFAULT },\n}\n"],"names":["BOOKING_MAX_DAYS_AHEAD","BOOKING_PATH_DEFAULT","BOOKINGS_CONFIG_SCHEMA","pluginId","fields","key","label","type","min","max","description","defaults","maxDaysAhead","bookingPath"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,sBAAsB,EAAEC,oBAAoB,QAAQ,mBAAS;AAEtE;;;;;CAKC,GACD,OAAO,MAAMC,yBAA6C;IACxDC,UAAU;IACVC,QAAQ;QACN;YACEC,KAAK;YACLC,OAAO;YACPC,MAAM;YACNC,KAAK;YACLC,KAAK;YACLC,aACE,kEACA;QACJ;QACA;YACE,qEAAqE;YACrE,qEAAqE;YACrE,kEAAkE;YAClE,gEAAgE;YAChE,uBAAuB;YACvBL,KAAK;YACLC,OAAO;YACPC,MAAM;YACNG,aACE,qEACA,gEACA;QACJ;KACD;IACDC,UAAU;QAAEC,cAAcZ;QAAwBa,aAAaZ;IAAqB;AACtF,EAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* Console half only: registers the Bookings nav item + page. Safe to call
|
|
19
|
+
* at console app load — the page is lazy (no besigner/canvas code).
|
|
20
|
+
*/
|
|
21
|
+
export declare function registerBookingsConsole(): void;
|
|
22
|
+
export * from './site';
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import * as Aglyn from "@aglyn/aglyn";
|
|
17
|
+
import { mdiCalendarClock } from "@aglyn/shared-data-mdi";
|
|
18
|
+
import { lazy } from "react";
|
|
19
|
+
import { BUNDLE_ID } from "./constants/bundle-common.js";
|
|
20
|
+
import { BOOKINGS_CONFIG_SCHEMA } from "./plugin-config.js";
|
|
21
|
+
/** Code-split: the Bookings console page only loads when opened. */ const BookingsConsolePage = lazy(()=>import("./components/bookings-console-page.js"));
|
|
22
|
+
/** "Book a meeting", loaded where a record page draws the zone for it. */ const BookMeetingButton = lazy(()=>import("./components/book-meeting-action.js"));
|
|
23
|
+
/**
|
|
24
|
+
* Console half only: registers the Bookings nav item + page. Safe to call
|
|
25
|
+
* at console app load — the page is lazy (no besigner/canvas code).
|
|
26
|
+
*/ export function registerBookingsConsole() {
|
|
27
|
+
// Per-plugin settings (AGL-428): the schema powers the generic form on
|
|
28
|
+
// the Plugins & add-ons hub and defaults-merged reads everywhere.
|
|
29
|
+
Aglyn.registerPluginConfigSchema(BOOKINGS_CONFIG_SCHEMA);
|
|
30
|
+
Aglyn.registerConsoleExtension({
|
|
31
|
+
pluginId: BUNDLE_ID,
|
|
32
|
+
displayName: 'Bookings',
|
|
33
|
+
featureFlag: 'bookings',
|
|
34
|
+
// The control a CRM record carries for booking a meeting with the person.
|
|
35
|
+
// The CRM hosts the zone and hands it the record; the services, the page
|
|
36
|
+
// they are booked on and the link are this plugin's, so the control is
|
|
37
|
+
// too. It draws nothing where this site takes no bookings.
|
|
38
|
+
widgets: [
|
|
39
|
+
{
|
|
40
|
+
slot: 'crmRecordBooking',
|
|
41
|
+
widgetId: 'bookings-book-meeting',
|
|
42
|
+
title: 'Book a meeting',
|
|
43
|
+
Component: BookMeetingButton
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
navItems: [
|
|
47
|
+
{
|
|
48
|
+
label: 'Bookings',
|
|
49
|
+
href: '/bookings',
|
|
50
|
+
// Reuse the existing release-flag nav-tab so staff-preview gating is
|
|
51
|
+
// unchanged now that the tab comes from the plugin (AGL-395).
|
|
52
|
+
navTabId: 'nav-tab-bookings',
|
|
53
|
+
icon: {
|
|
54
|
+
path: mdiCalendarClock.path
|
|
55
|
+
},
|
|
56
|
+
header: {
|
|
57
|
+
title: 'Bookings',
|
|
58
|
+
icon: {
|
|
59
|
+
path: mdiCalendarClock.path
|
|
60
|
+
},
|
|
61
|
+
docsTopic: 'bookings'
|
|
62
|
+
},
|
|
63
|
+
Component: BookingsConsolePage
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
export * from "./site.js";
|
|
69
|
+
|
|
70
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/bookings/src/lib/plugin.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { mdiCalendarClock } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { BOOKINGS_CONFIG_SCHEMA } from './plugin-config'\n\n/** Code-split: the Bookings console page only loads when opened. */\nconst BookingsConsolePage = lazy(\n () => import('./components/bookings-console-page'),\n)\n\n/** \"Book a meeting\", loaded where a record page draws the zone for it. */\nconst BookMeetingButton = lazy(() => import('./components/book-meeting-action'))\n\n/**\n * Console half only: registers the Bookings nav item + page. Safe to call\n * at console app load — the page is lazy (no besigner/canvas code).\n */\nexport function registerBookingsConsole(): void {\n // Per-plugin settings (AGL-428): the schema powers the generic form on\n // the Plugins & add-ons hub and defaults-merged reads everywhere.\n Aglyn.registerPluginConfigSchema(BOOKINGS_CONFIG_SCHEMA)\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'Bookings',\n featureFlag: 'bookings',\n // The control a CRM record carries for booking a meeting with the person.\n // The CRM hosts the zone and hands it the record; the services, the page\n // they are booked on and the link are this plugin's, so the control is\n // too. It draws nothing where this site takes no bookings.\n widgets: [\n {\n slot: 'crmRecordBooking',\n widgetId: 'bookings-book-meeting',\n title: 'Book a meeting',\n Component: BookMeetingButton,\n },\n ],\n navItems: [\n {\n label: 'Bookings',\n href: '/bookings',\n // Reuse the existing release-flag nav-tab so staff-preview gating is\n // unchanged now that the tab comes from the plugin (AGL-395).\n navTabId: 'nav-tab-bookings',\n icon: { path: mdiCalendarClock.path },\n header: {\n title: 'Bookings',\n icon: { path: mdiCalendarClock.path },\n docsTopic: 'bookings',\n },\n Component: BookingsConsolePage,\n },\n ],\n })\n}\n\nexport * from './site'\n"],"names":["Aglyn","mdiCalendarClock","lazy","BUNDLE_ID","BOOKINGS_CONFIG_SCHEMA","BookingsConsolePage","BookMeetingButton","registerBookingsConsole","registerPluginConfigSchema","registerConsoleExtension","pluginId","displayName","featureFlag","widgets","slot","widgetId","title","Component","navItems","label","href","navTabId","icon","path","header","docsTopic"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,eAAc;AACrC,SAASC,gBAAgB,QAAQ,yBAAwB;AACzD,SAASC,IAAI,QAAQ,QAAO;AAC5B,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SAASC,sBAAsB,QAAQ,qBAAiB;AAExD,kEAAkE,GAClE,MAAMC,sBAAsBH,KAC1B,IAAM,MAAM,CAAC;AAGf,wEAAwE,GACxE,MAAMI,oBAAoBJ,KAAK,IAAM,MAAM,CAAC;AAE5C;;;CAGC,GACD,OAAO,SAASK;IACd,uEAAuE;IACvE,kEAAkE;IAClEP,MAAMQ,0BAA0B,CAACJ;IACjCJ,MAAMS,wBAAwB,CAAC;QAC7BC,UAAUP;QACVQ,aAAa;QACbC,aAAa;QACb,0EAA0E;QAC1E,yEAAyE;QACzE,uEAAuE;QACvE,2DAA2D;QAC3DC,SAAS;YACP;gBACEC,MAAM;gBACNC,UAAU;gBACVC,OAAO;gBACPC,WAAWX;YACb;SACD;QACDY,UAAU;YACR;gBACEC,OAAO;gBACPC,MAAM;gBACN,qEAAqE;gBACrE,8DAA8D;gBAC9DC,UAAU;gBACVC,MAAM;oBAAEC,MAAMtB,iBAAiBsB,IAAI;gBAAC;gBACpCC,QAAQ;oBACNR,OAAO;oBACPM,MAAM;wBAAEC,MAAMtB,iBAAiBsB,IAAI;oBAAC;oBACpCE,WAAW;gBACb;gBACAR,WAAWZ;YACb;SACD;IACH;AACF;AAEA,cAAc,YAAQ"}
|
|
@@ -0,0 +1,23 @@
|
|
|
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 { BillingWebhookHandler } from '@aglyn/aglyn/server';
|
|
18
|
+
/**
|
|
19
|
+
* Paid-booking section of the platform Stripe webhook (AGL-170/418):
|
|
20
|
+
* payment confirms the pendingPayment hold — relocated verbatim from the
|
|
21
|
+
* console route; registered via registerBookingsConsoleApi.
|
|
22
|
+
*/
|
|
23
|
+
export declare const bookingsBillingWebhookHandler: BillingWebhookHandler;
|