payload-reserve 1.3.2 → 1.5.0
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/README.md +185 -4
- package/dist/collections/Reservations.js +47 -2
- package/dist/collections/Reservations.js.map +1 -1
- package/dist/collections/Resources.d.ts +16 -0
- package/dist/collections/Resources.js +35 -10
- package/dist/collections/Resources.js.map +1 -1
- package/dist/collections/Schedules.js +34 -0
- package/dist/collections/Schedules.js.map +1 -1
- package/dist/collections/Services.js +34 -1
- package/dist/collections/Services.js.map +1 -1
- package/dist/components/AvailabilityTimeField/AvailabilityTimeField.module.css +7 -0
- package/dist/components/AvailabilityTimeField/index.d.ts +2 -0
- package/dist/components/AvailabilityTimeField/index.js +109 -0
- package/dist/components/AvailabilityTimeField/index.js.map +1 -0
- package/dist/components/CalendarView/CalendarView.module.css +114 -0
- package/dist/components/CalendarView/LaneTimelineView.d.ts +12 -0
- package/dist/components/CalendarView/LaneTimelineView.js +116 -0
- package/dist/components/CalendarView/LaneTimelineView.js.map +1 -0
- package/dist/components/CalendarView/index.js +224 -22
- package/dist/components/CalendarView/index.js.map +1 -1
- package/dist/components/CalendarView/useResourceAvailability.d.ts +9 -0
- package/dist/components/CalendarView/useResourceAvailability.js +40 -0
- package/dist/components/CalendarView/useResourceAvailability.js.map +1 -0
- package/dist/defaults.d.ts +3 -0
- package/dist/defaults.js +53 -0
- package/dist/defaults.js.map +1 -1
- package/dist/endpoints/cancelBooking.js +34 -21
- package/dist/endpoints/cancelBooking.js.map +1 -1
- package/dist/endpoints/checkAvailability.js +16 -1
- package/dist/endpoints/checkAvailability.js.map +1 -1
- package/dist/endpoints/createBooking.js +4 -1
- package/dist/endpoints/createBooking.js.map +1 -1
- package/dist/endpoints/customerSearch.js +24 -5
- package/dist/endpoints/customerSearch.js.map +1 -1
- package/dist/endpoints/getSlots.js +16 -1
- package/dist/endpoints/getSlots.js.map +1 -1
- package/dist/endpoints/resourceAvailability.d.ts +43 -0
- package/dist/endpoints/resourceAvailability.js +214 -0
- package/dist/endpoints/resourceAvailability.js.map +1 -0
- package/dist/exports/client.d.ts +1 -0
- package/dist/exports/client.js +1 -0
- package/dist/exports/client.js.map +1 -1
- package/dist/hooks/reservations/calculateEndTime.js +21 -1
- package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
- package/dist/hooks/reservations/expandRequiredResources.d.ts +9 -0
- package/dist/hooks/reservations/expandRequiredResources.js +81 -0
- package/dist/hooks/reservations/expandRequiredResources.js.map +1 -0
- package/dist/hooks/reservations/validateGuestBooking.d.ts +3 -0
- package/dist/hooks/reservations/validateGuestBooking.js +93 -0
- package/dist/hooks/reservations/validateGuestBooking.js.map +1 -0
- package/dist/hooks/reservations/validateStatusTransition.js +4 -2
- package/dist/hooks/reservations/validateStatusTransition.js.map +1 -1
- package/dist/hooks/users/provisionStaffResource.d.ts +15 -0
- package/dist/hooks/users/provisionStaffResource.js +88 -0
- package/dist/hooks/users/provisionStaffResource.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin.js +28 -3
- package/dist/plugin.js.map +1 -1
- package/dist/services/AvailabilityService.d.ts +7 -6
- package/dist/services/AvailabilityService.js +86 -60
- package/dist/services/AvailabilityService.js.map +1 -1
- package/dist/translations/ar.json +156 -0
- package/dist/translations/de.json +156 -0
- package/dist/translations/en.json +32 -1
- package/dist/translations/es.json +156 -0
- package/dist/translations/fa.json +156 -0
- package/dist/translations/fr.json +156 -0
- package/dist/translations/hi.json +156 -0
- package/dist/translations/id.json +156 -0
- package/dist/translations/index.js +44 -0
- package/dist/translations/index.js.map +1 -1
- package/dist/translations/pl.json +156 -0
- package/dist/translations/ru.json +156 -0
- package/dist/translations/tr.json +156 -0
- package/dist/translations/zh.json +156 -0
- package/dist/types.d.ts +46 -0
- package/dist/types.js.map +1 -1
- package/dist/utilities/computeSlotStates.d.ts +39 -0
- package/dist/utilities/computeSlotStates.js +49 -0
- package/dist/utilities/computeSlotStates.js.map +1 -0
- package/dist/utilities/guestBooking.d.ts +10 -0
- package/dist/utilities/guestBooking.js +16 -0
- package/dist/utilities/guestBooking.js.map +1 -0
- package/dist/utilities/resolveRequiredResources.d.ts +8 -0
- package/dist/utilities/resolveRequiredResources.js +27 -0
- package/dist/utilities/resolveRequiredResources.js.map +1 -0
- package/dist/utilities/resolveReservationItems.d.ts +3 -2
- package/dist/utilities/resolveReservationItems.js +19 -6
- package/dist/utilities/resolveReservationItems.js.map +1 -1
- package/dist/utilities/scheduleUtils.d.ts +3 -0
- package/dist/utilities/scheduleUtils.js +5 -3
- package/dist/utilities/scheduleUtils.js.map +1 -1
- package/dist/utilities/selectOptions.d.ts +8 -0
- package/dist/utilities/selectOptions.js +11 -0
- package/dist/utilities/selectOptions.js.map +1 -0
- package/dist/utilities/slotUtils.d.ts +19 -0
- package/dist/utilities/slotUtils.js +28 -0
- package/dist/utilities/slotUtils.js.map +1 -1
- package/dist/utilities/userRoles.d.ts +20 -0
- package/dist/utilities/userRoles.js +32 -0
- package/dist/utilities/userRoles.js.map +1 -0
- package/package.json +2 -1
package/dist/types.d.ts
CHANGED
|
@@ -55,14 +55,48 @@ export type ResourceOwnerModeConfig = {
|
|
|
55
55
|
adminRoles?: string[];
|
|
56
56
|
/** Whether Services also get an owner field (default: false — Services are platform-managed) */
|
|
57
57
|
ownedServices?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Collection the owner field relates to (where owners/staff live). Defaults to
|
|
60
|
+
* `staffProvisioning.userCollection` when set, otherwise `slugs.customers`. Set
|
|
61
|
+
* this when owners live in a different collection than your customers (e.g.
|
|
62
|
+
* separate `users` and `customers` collections).
|
|
63
|
+
*/
|
|
64
|
+
ownerCollection?: string;
|
|
58
65
|
/** Field name for the owner relationship on Resources (default: 'owner') */
|
|
59
66
|
ownerField?: string;
|
|
60
67
|
};
|
|
61
68
|
export type ResolvedResourceOwnerModeConfig = {
|
|
62
69
|
adminRoles: string[];
|
|
63
70
|
ownedServices: boolean;
|
|
71
|
+
ownerCollection?: string;
|
|
64
72
|
ownerField: string;
|
|
65
73
|
};
|
|
74
|
+
export type StaffProvisioningConfig = {
|
|
75
|
+
/** Stamp tenant / custom fields onto the provisioned Resource before create. */
|
|
76
|
+
beforeCreate?: (args: {
|
|
77
|
+
data: Record<string, unknown>;
|
|
78
|
+
req: PayloadRequest;
|
|
79
|
+
user: Record<string, unknown>;
|
|
80
|
+
}) => Promise<Record<string, unknown>> | Record<string, unknown>;
|
|
81
|
+
/** User field copied into Resource `name` (default 'name', falls back to email). */
|
|
82
|
+
nameFrom?: string;
|
|
83
|
+
/** resourceType to stamp (default 'staff'). Must be a valid resourceType. */
|
|
84
|
+
resourceType?: string;
|
|
85
|
+
/** Field on the user holding the role (default 'role'). */
|
|
86
|
+
roleField?: string;
|
|
87
|
+
/** Role value(s) marking a user as staff. Required, non-empty. */
|
|
88
|
+
staffRoles: string[];
|
|
89
|
+
/** Auth collection holding staff users. Defaults to top-level `userCollection`. */
|
|
90
|
+
userCollection?: string;
|
|
91
|
+
};
|
|
92
|
+
export type ResolvedStaffProvisioningConfig = {
|
|
93
|
+
beforeCreate?: StaffProvisioningConfig['beforeCreate'];
|
|
94
|
+
nameFrom: string;
|
|
95
|
+
resourceType: string;
|
|
96
|
+
roleField: string;
|
|
97
|
+
staffRoles: string[];
|
|
98
|
+
userCollection: string;
|
|
99
|
+
};
|
|
66
100
|
export type ReservationPluginConfig = {
|
|
67
101
|
/** Override access control per collection */
|
|
68
102
|
access?: {
|
|
@@ -74,6 +108,8 @@ export type ReservationPluginConfig = {
|
|
|
74
108
|
};
|
|
75
109
|
/** Admin group name for all reservation collections */
|
|
76
110
|
adminGroup?: string;
|
|
111
|
+
/** Allow bookings without a customer account by default (per-service override available) */
|
|
112
|
+
allowGuestBooking?: boolean;
|
|
77
113
|
/** Hours of notice required before cancellation */
|
|
78
114
|
cancellationNoticePeriod?: number;
|
|
79
115
|
/** Default buffer time in minutes between reservations */
|
|
@@ -84,8 +120,12 @@ export type ReservationPluginConfig = {
|
|
|
84
120
|
extraReservationFields?: Field[];
|
|
85
121
|
/** Plugin hooks for external integrations */
|
|
86
122
|
hooks?: ReservationPluginHooks;
|
|
123
|
+
/** Configurable leave/exception type vocabulary (default: vacation/sick/personal/closure/other) */
|
|
124
|
+
leaveTypes?: string[];
|
|
87
125
|
/** Enable resource-owner multi-tenancy (opt-in) */
|
|
88
126
|
resourceOwnerMode?: ResourceOwnerModeConfig;
|
|
127
|
+
/** Configurable resourceType vocabulary (default: staff/equipment/room) */
|
|
128
|
+
resourceTypes?: string[];
|
|
89
129
|
/** Override collection slugs */
|
|
90
130
|
slugs?: {
|
|
91
131
|
customers?: string;
|
|
@@ -95,6 +135,8 @@ export type ReservationPluginConfig = {
|
|
|
95
135
|
schedules?: string;
|
|
96
136
|
services?: string;
|
|
97
137
|
};
|
|
138
|
+
/** Auto-provision a Resource from staff-role users (opt-in; requires resourceOwnerMode) */
|
|
139
|
+
staffProvisioning?: StaffProvisioningConfig;
|
|
98
140
|
/** Configurable status machine (defaults to current behavior) */
|
|
99
141
|
statusMachine?: Partial<StatusMachineConfig>;
|
|
100
142
|
/** Which existing auth collection to extend with customer fields */
|
|
@@ -109,13 +151,16 @@ export type ResolvedReservationPluginConfig = {
|
|
|
109
151
|
services?: CollectionConfig['access'];
|
|
110
152
|
};
|
|
111
153
|
adminGroup: string;
|
|
154
|
+
allowGuestBooking: boolean;
|
|
112
155
|
cancellationNoticePeriod: number;
|
|
113
156
|
defaultBufferTime: number;
|
|
114
157
|
disabled: boolean;
|
|
115
158
|
extraReservationFields: Field[];
|
|
116
159
|
hooks: ReservationPluginHooks;
|
|
160
|
+
leaveTypes: string[];
|
|
117
161
|
localized: boolean;
|
|
118
162
|
resourceOwnerMode: ResolvedResourceOwnerModeConfig | undefined;
|
|
163
|
+
resourceTypes: string[];
|
|
119
164
|
slugs: {
|
|
120
165
|
customers: string;
|
|
121
166
|
media: string;
|
|
@@ -124,6 +169,7 @@ export type ResolvedReservationPluginConfig = {
|
|
|
124
169
|
schedules: string;
|
|
125
170
|
services: string;
|
|
126
171
|
};
|
|
172
|
+
staffProvisioning: ResolvedStaffProvisioningConfig | undefined;
|
|
127
173
|
statusMachine: StatusMachineConfig;
|
|
128
174
|
userCollection: string | undefined;
|
|
129
175
|
};
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { CollectionConfig, Field, PayloadRequest } from 'payload'\n\n// --- Duration & Capacity models ---\n\nexport type DurationType = 'fixed' | 'flexible' | 'full-day'\n\nexport type CapacityMode = 'per-guest' | 'per-reservation'\n\n// --- Configurable status machine ---\n\nexport type StatusMachineConfig = {\n blockingStatuses: string[]\n defaultStatus: string\n statuses: string[]\n terminalStatuses: string[]\n transitions: Record<string, string[]>\n}\n\nexport const DEFAULT_STATUS_MACHINE: StatusMachineConfig = {\n blockingStatuses: ['pending', 'confirmed'],\n defaultStatus: 'pending',\n statuses: ['pending', 'confirmed', 'completed', 'cancelled', 'no-show'],\n terminalStatuses: ['completed', 'cancelled', 'no-show'],\n transitions: {\n cancelled: [],\n completed: [],\n confirmed: ['completed', 'cancelled', 'no-show'],\n 'no-show': [],\n pending: ['confirmed', 'cancelled'],\n },\n}\n\n// --- Reservation item (for multi-resource bookings, Phase 3) ---\n\nexport type ReservationItemConfig = {\n endTime?: string\n guestCount?: number\n resource: string\n service?: string\n startTime?: string\n}\n\n// --- Plugin hooks for external integrations ---\n\nexport type ReservationPluginHooks = {\n afterBookingCancel?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterBookingConfirm?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterBookingCreate?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterStatusChange?: Array<\n (args: {\n doc: Record<string, unknown>\n newStatus: string\n previousStatus: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingCancel?: Array<\n (args: {\n doc: Record<string, unknown>\n reason?: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingConfirm?: Array<\n (args: {\n doc: Record<string, unknown>\n newStatus: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingCreate?: Array<\n (args: {\n data: Record<string, unknown>\n req: PayloadRequest\n }) => Promise<Record<string, unknown>> | Record<string, unknown>\n >\n}\n\n// --- Resource owner mode ---\n\nexport type ResourceOwnerModeConfig = {\n /** Roles that can see all records (default: check req.user.collection === adminCollection) */\n adminRoles?: string[]\n /** Whether Services also get an owner field (default: false — Services are platform-managed) */\n ownedServices?: boolean\n /** Field name for the owner relationship on Resources (default: 'owner') */\n ownerField?: string\n}\n\nexport type ResolvedResourceOwnerModeConfig = {\n adminRoles: string[]\n ownedServices: boolean\n ownerField: string\n}\n\n// --- Plugin configuration ---\n\nexport type ReservationPluginConfig = {\n /** Override access control per collection */\n access?: {\n customers?: CollectionConfig['access']\n reservations?: CollectionConfig['access']\n resources?: CollectionConfig['access']\n schedules?: CollectionConfig['access']\n services?: CollectionConfig['access']\n }\n /** Admin group name for all reservation collections */\n adminGroup?: string\n /** Hours of notice required before cancellation */\n cancellationNoticePeriod?: number\n /** Default buffer time in minutes between reservations */\n defaultBufferTime?: number\n /** Disable the plugin entirely */\n disabled?: boolean\n /** Extra fields to append to the Reservations collection */\n extraReservationFields?: Field[]\n /** Plugin hooks for external integrations */\n hooks?: ReservationPluginHooks\n /** Enable resource-owner multi-tenancy (opt-in) */\n resourceOwnerMode?: ResourceOwnerModeConfig\n /** Override collection slugs */\n slugs?: {\n customers?: string\n media?: string\n reservations?: string\n resources?: string\n schedules?: string\n services?: string\n }\n /** Configurable status machine (defaults to current behavior) */\n statusMachine?: Partial<StatusMachineConfig>\n /** Which existing auth collection to extend with customer fields */\n userCollection?: string\n}\n\nexport type ResolvedReservationPluginConfig = {\n access: {\n customers?: CollectionConfig['access']\n reservations?: CollectionConfig['access']\n resources?: CollectionConfig['access']\n schedules?: CollectionConfig['access']\n services?: CollectionConfig['access']\n }\n adminGroup: string\n cancellationNoticePeriod: number\n defaultBufferTime: number\n disabled: boolean\n extraReservationFields: Field[]\n hooks: ReservationPluginHooks\n localized: boolean\n resourceOwnerMode: ResolvedResourceOwnerModeConfig | undefined\n slugs: {\n customers: string\n media: string\n reservations: string\n resources: string\n schedules: string\n services: string\n }\n statusMachine: StatusMachineConfig\n userCollection: string | undefined\n}\n\nexport type ReservationStatus = 'cancelled' | 'completed' | 'confirmed' | 'no-show' | 'pending'\n\nexport type DayOfWeek = 'fri' | 'mon' | 'sat' | 'sun' | 'thu' | 'tue' | 'wed'\n\nexport type ScheduleType = 'manual' | 'recurring'\n\n/** @deprecated Use DEFAULT_STATUS_MACHINE.transitions instead */\nexport const VALID_STATUS_TRANSITIONS: Record<ReservationStatus, ReservationStatus[]> =\n DEFAULT_STATUS_MACHINE.transitions as Record<ReservationStatus, ReservationStatus[]>\n"],"names":["DEFAULT_STATUS_MACHINE","blockingStatuses","defaultStatus","statuses","terminalStatuses","transitions","cancelled","completed","confirmed","pending","VALID_STATUS_TRANSITIONS"],"mappings":"AAkBA,OAAO,MAAMA,yBAA8C;IACzDC,kBAAkB;QAAC;QAAW;KAAY;IAC1CC,eAAe;IACfC,UAAU;QAAC;QAAW;QAAa;QAAa;QAAa;KAAU;IACvEC,kBAAkB;QAAC;QAAa;QAAa;KAAU;IACvDC,aAAa;QACXC,WAAW,EAAE;QACbC,WAAW,EAAE;QACbC,WAAW;YAAC;YAAa;YAAa;SAAU;QAChD,WAAW,EAAE;QACbC,SAAS;YAAC;YAAa;SAAY;IACrC;AACF,EAAC;
|
|
1
|
+
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { CollectionConfig, Field, PayloadRequest } from 'payload'\n\n// --- Duration & Capacity models ---\n\nexport type DurationType = 'fixed' | 'flexible' | 'full-day'\n\nexport type CapacityMode = 'per-guest' | 'per-reservation'\n\n// --- Configurable status machine ---\n\nexport type StatusMachineConfig = {\n blockingStatuses: string[]\n defaultStatus: string\n statuses: string[]\n terminalStatuses: string[]\n transitions: Record<string, string[]>\n}\n\nexport const DEFAULT_STATUS_MACHINE: StatusMachineConfig = {\n blockingStatuses: ['pending', 'confirmed'],\n defaultStatus: 'pending',\n statuses: ['pending', 'confirmed', 'completed', 'cancelled', 'no-show'],\n terminalStatuses: ['completed', 'cancelled', 'no-show'],\n transitions: {\n cancelled: [],\n completed: [],\n confirmed: ['completed', 'cancelled', 'no-show'],\n 'no-show': [],\n pending: ['confirmed', 'cancelled'],\n },\n}\n\n// --- Reservation item (for multi-resource bookings, Phase 3) ---\n\nexport type ReservationItemConfig = {\n endTime?: string\n guestCount?: number\n resource: string\n service?: string\n startTime?: string\n}\n\n// --- Plugin hooks for external integrations ---\n\nexport type ReservationPluginHooks = {\n afterBookingCancel?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterBookingConfirm?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterBookingCreate?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterStatusChange?: Array<\n (args: {\n doc: Record<string, unknown>\n newStatus: string\n previousStatus: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingCancel?: Array<\n (args: {\n doc: Record<string, unknown>\n reason?: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingConfirm?: Array<\n (args: {\n doc: Record<string, unknown>\n newStatus: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingCreate?: Array<\n (args: {\n data: Record<string, unknown>\n req: PayloadRequest\n }) => Promise<Record<string, unknown>> | Record<string, unknown>\n >\n}\n\n// --- Resource owner mode ---\n\nexport type ResourceOwnerModeConfig = {\n /** Roles that can see all records (default: check req.user.collection === adminCollection) */\n adminRoles?: string[]\n /** Whether Services also get an owner field (default: false — Services are platform-managed) */\n ownedServices?: boolean\n /**\n * Collection the owner field relates to (where owners/staff live). Defaults to\n * `staffProvisioning.userCollection` when set, otherwise `slugs.customers`. Set\n * this when owners live in a different collection than your customers (e.g.\n * separate `users` and `customers` collections).\n */\n ownerCollection?: string\n /** Field name for the owner relationship on Resources (default: 'owner') */\n ownerField?: string\n}\n\nexport type ResolvedResourceOwnerModeConfig = {\n adminRoles: string[]\n ownedServices: boolean\n ownerCollection?: string\n ownerField: string\n}\n\n// --- Staff provisioning ---\n\nexport type StaffProvisioningConfig = {\n /** Stamp tenant / custom fields onto the provisioned Resource before create. */\n beforeCreate?: (args: {\n data: Record<string, unknown>\n req: PayloadRequest\n user: Record<string, unknown>\n }) => Promise<Record<string, unknown>> | Record<string, unknown>\n /** User field copied into Resource `name` (default 'name', falls back to email). */\n nameFrom?: string\n /** resourceType to stamp (default 'staff'). Must be a valid resourceType. */\n resourceType?: string\n /** Field on the user holding the role (default 'role'). */\n roleField?: string\n /** Role value(s) marking a user as staff. Required, non-empty. */\n staffRoles: string[]\n /** Auth collection holding staff users. Defaults to top-level `userCollection`. */\n userCollection?: string\n}\n\nexport type ResolvedStaffProvisioningConfig = {\n beforeCreate?: StaffProvisioningConfig['beforeCreate']\n nameFrom: string\n resourceType: string\n roleField: string\n staffRoles: string[]\n userCollection: string\n}\n\n// --- Plugin configuration ---\n\nexport type ReservationPluginConfig = {\n /** Override access control per collection */\n access?: {\n customers?: CollectionConfig['access']\n reservations?: CollectionConfig['access']\n resources?: CollectionConfig['access']\n schedules?: CollectionConfig['access']\n services?: CollectionConfig['access']\n }\n /** Admin group name for all reservation collections */\n adminGroup?: string\n /** Allow bookings without a customer account by default (per-service override available) */\n allowGuestBooking?: boolean\n /** Hours of notice required before cancellation */\n cancellationNoticePeriod?: number\n /** Default buffer time in minutes between reservations */\n defaultBufferTime?: number\n /** Disable the plugin entirely */\n disabled?: boolean\n /** Extra fields to append to the Reservations collection */\n extraReservationFields?: Field[]\n /** Plugin hooks for external integrations */\n hooks?: ReservationPluginHooks\n /** Configurable leave/exception type vocabulary (default: vacation/sick/personal/closure/other) */\n leaveTypes?: string[]\n /** Enable resource-owner multi-tenancy (opt-in) */\n resourceOwnerMode?: ResourceOwnerModeConfig\n /** Configurable resourceType vocabulary (default: staff/equipment/room) */\n resourceTypes?: string[]\n /** Override collection slugs */\n slugs?: {\n customers?: string\n media?: string\n reservations?: string\n resources?: string\n schedules?: string\n services?: string\n }\n /** Auto-provision a Resource from staff-role users (opt-in; requires resourceOwnerMode) */\n staffProvisioning?: StaffProvisioningConfig\n /** Configurable status machine (defaults to current behavior) */\n statusMachine?: Partial<StatusMachineConfig>\n /** Which existing auth collection to extend with customer fields */\n userCollection?: string\n}\n\nexport type ResolvedReservationPluginConfig = {\n access: {\n customers?: CollectionConfig['access']\n reservations?: CollectionConfig['access']\n resources?: CollectionConfig['access']\n schedules?: CollectionConfig['access']\n services?: CollectionConfig['access']\n }\n adminGroup: string\n allowGuestBooking: boolean\n cancellationNoticePeriod: number\n defaultBufferTime: number\n disabled: boolean\n extraReservationFields: Field[]\n hooks: ReservationPluginHooks\n leaveTypes: string[]\n localized: boolean\n resourceOwnerMode: ResolvedResourceOwnerModeConfig | undefined\n resourceTypes: string[]\n slugs: {\n customers: string\n media: string\n reservations: string\n resources: string\n schedules: string\n services: string\n }\n staffProvisioning: ResolvedStaffProvisioningConfig | undefined\n statusMachine: StatusMachineConfig\n userCollection: string | undefined\n}\n\nexport type ReservationStatus = 'cancelled' | 'completed' | 'confirmed' | 'no-show' | 'pending'\n\nexport type DayOfWeek = 'fri' | 'mon' | 'sat' | 'sun' | 'thu' | 'tue' | 'wed'\n\nexport type ScheduleType = 'manual' | 'recurring'\n\n/** @deprecated Use DEFAULT_STATUS_MACHINE.transitions instead */\nexport const VALID_STATUS_TRANSITIONS: Record<ReservationStatus, ReservationStatus[]> =\n DEFAULT_STATUS_MACHINE.transitions as Record<ReservationStatus, ReservationStatus[]>\n"],"names":["DEFAULT_STATUS_MACHINE","blockingStatuses","defaultStatus","statuses","terminalStatuses","transitions","cancelled","completed","confirmed","pending","VALID_STATUS_TRANSITIONS"],"mappings":"AAkBA,OAAO,MAAMA,yBAA8C;IACzDC,kBAAkB;QAAC;QAAW;KAAY;IAC1CC,eAAe;IACfC,UAAU;QAAC;QAAW;QAAa;QAAa;QAAa;KAAU;IACvEC,kBAAkB;QAAC;QAAa;QAAa;KAAU;IACvDC,aAAa;QACXC,WAAW,EAAE;QACbC,WAAW,EAAE;QACbC,WAAW;YAAC;YAAa;YAAa;SAAU;QAChD,WAAW,EAAE;QACbC,SAAS;YAAC;YAAa;SAAY;IACrC;AACF,EAAC;AAmMD,+DAA+D,GAC/D,OAAO,MAAMC,2BACXV,uBAAuBK,WAAW,CAAkD"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type SlotState = 'free' | 'full' | 'off-shift' | 'time-off';
|
|
2
|
+
export type SlotInfo = {
|
|
3
|
+
end: Date;
|
|
4
|
+
occupancy: number;
|
|
5
|
+
start: Date;
|
|
6
|
+
state: SlotState;
|
|
7
|
+
};
|
|
8
|
+
type Busy = Array<{
|
|
9
|
+
end: string;
|
|
10
|
+
start: string;
|
|
11
|
+
units: number;
|
|
12
|
+
}>;
|
|
13
|
+
type Interval = {
|
|
14
|
+
end: string;
|
|
15
|
+
start: string;
|
|
16
|
+
};
|
|
17
|
+
type RequiredPool = {
|
|
18
|
+
busy: Busy;
|
|
19
|
+
quantity: number;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Classify each step-sized slot in [dayStart, dayEnd) for a single resource.
|
|
23
|
+
* A slot is `full` when the resource itself is at capacity OR any of its
|
|
24
|
+
* `requiredPools` (e.g. a shared chair pool a service also needs) is at
|
|
25
|
+
* capacity — so the grid reflects true bookability, not just the stylist.
|
|
26
|
+
* Pure — no DB, no clock.
|
|
27
|
+
*/
|
|
28
|
+
export declare function computeSlotStates(params: {
|
|
29
|
+
busy: Busy;
|
|
30
|
+
capacityMode: 'per-guest' | 'per-reservation';
|
|
31
|
+
dayEnd: Date;
|
|
32
|
+
dayStart: Date;
|
|
33
|
+
quantity: number;
|
|
34
|
+
requiredPools?: RequiredPool[];
|
|
35
|
+
shiftWindows: Interval[];
|
|
36
|
+
step: number;
|
|
37
|
+
timeOff: Interval[];
|
|
38
|
+
}): SlotInfo[];
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { addMinutes, doRangesOverlap } from './slotUtils.js';
|
|
2
|
+
const within = (slotStart, slotEnd, windows)=>windows.some((w)=>doRangesOverlap(slotStart, slotEnd, new Date(w.start), new Date(w.end)));
|
|
3
|
+
/** Sum the `units` of busy intervals overlapping [slotStart, slotEnd). */ const occupancyAt = (slotStart, slotEnd, busy)=>{
|
|
4
|
+
let occ = 0;
|
|
5
|
+
for (const b of busy){
|
|
6
|
+
if (doRangesOverlap(slotStart, slotEnd, new Date(b.start), new Date(b.end))) {
|
|
7
|
+
occ += b.units;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
return occ;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Classify each step-sized slot in [dayStart, dayEnd) for a single resource.
|
|
14
|
+
* A slot is `full` when the resource itself is at capacity OR any of its
|
|
15
|
+
* `requiredPools` (e.g. a shared chair pool a service also needs) is at
|
|
16
|
+
* capacity — so the grid reflects true bookability, not just the stylist.
|
|
17
|
+
* Pure — no DB, no clock.
|
|
18
|
+
*/ export function computeSlotStates(params) {
|
|
19
|
+
const { busy, dayEnd, dayStart, quantity, requiredPools, shiftWindows, step, timeOff } = params;
|
|
20
|
+
const pools = requiredPools ?? [];
|
|
21
|
+
const slots = [];
|
|
22
|
+
let cursor = new Date(dayStart);
|
|
23
|
+
while(cursor < dayEnd){
|
|
24
|
+
const slotStart = new Date(cursor);
|
|
25
|
+
const slotEnd = addMinutes(slotStart, step);
|
|
26
|
+
const occupancy = occupancyAt(slotStart, slotEnd, busy);
|
|
27
|
+
const poolFull = pools.some((p)=>occupancyAt(slotStart, slotEnd, p.busy) >= p.quantity);
|
|
28
|
+
let state;
|
|
29
|
+
if (!within(slotStart, slotEnd, shiftWindows)) {
|
|
30
|
+
state = 'off-shift';
|
|
31
|
+
} else if (within(slotStart, slotEnd, timeOff)) {
|
|
32
|
+
state = 'time-off';
|
|
33
|
+
} else if (occupancy >= quantity || poolFull) {
|
|
34
|
+
state = 'full';
|
|
35
|
+
} else {
|
|
36
|
+
state = 'free';
|
|
37
|
+
}
|
|
38
|
+
slots.push({
|
|
39
|
+
end: slotEnd,
|
|
40
|
+
occupancy,
|
|
41
|
+
start: slotStart,
|
|
42
|
+
state
|
|
43
|
+
});
|
|
44
|
+
cursor = slotEnd;
|
|
45
|
+
}
|
|
46
|
+
return slots;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
//# sourceMappingURL=computeSlotStates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/computeSlotStates.ts"],"sourcesContent":["import { addMinutes, doRangesOverlap } from './slotUtils.js'\n\nexport type SlotState = 'free' | 'full' | 'off-shift' | 'time-off'\n\nexport type SlotInfo = {\n end: Date\n occupancy: number\n start: Date\n state: SlotState\n}\n\ntype Busy = Array<{ end: string; start: string; units: number }>\ntype Interval = { end: string; start: string }\ntype RequiredPool = { busy: Busy; quantity: number }\n\nconst within = (slotStart: Date, slotEnd: Date, windows: Interval[]): boolean =>\n windows.some((w) => doRangesOverlap(slotStart, slotEnd, new Date(w.start), new Date(w.end)))\n\n/** Sum the `units` of busy intervals overlapping [slotStart, slotEnd). */\nconst occupancyAt = (slotStart: Date, slotEnd: Date, busy: Busy): number => {\n let occ = 0\n for (const b of busy) {\n if (doRangesOverlap(slotStart, slotEnd, new Date(b.start), new Date(b.end))) {\n occ += b.units\n }\n }\n return occ\n}\n\n/**\n * Classify each step-sized slot in [dayStart, dayEnd) for a single resource.\n * A slot is `full` when the resource itself is at capacity OR any of its\n * `requiredPools` (e.g. a shared chair pool a service also needs) is at\n * capacity — so the grid reflects true bookability, not just the stylist.\n * Pure — no DB, no clock.\n */\nexport function computeSlotStates(params: {\n busy: Busy\n capacityMode: 'per-guest' | 'per-reservation'\n dayEnd: Date\n dayStart: Date\n quantity: number\n requiredPools?: RequiredPool[]\n shiftWindows: Interval[]\n step: number\n timeOff: Interval[]\n}): SlotInfo[] {\n const { busy, dayEnd, dayStart, quantity, requiredPools, shiftWindows, step, timeOff } = params\n const pools = requiredPools ?? []\n const slots: SlotInfo[] = []\n\n let cursor = new Date(dayStart)\n while (cursor < dayEnd) {\n const slotStart = new Date(cursor)\n const slotEnd = addMinutes(slotStart, step)\n\n const occupancy = occupancyAt(slotStart, slotEnd, busy)\n const poolFull = pools.some((p) => occupancyAt(slotStart, slotEnd, p.busy) >= p.quantity)\n\n let state: SlotState\n if (!within(slotStart, slotEnd, shiftWindows)) {\n state = 'off-shift'\n } else if (within(slotStart, slotEnd, timeOff)) {\n state = 'time-off'\n } else if (occupancy >= quantity || poolFull) {\n state = 'full'\n } else {\n state = 'free'\n }\n\n slots.push({ end: slotEnd, occupancy, start: slotStart, state })\n cursor = slotEnd\n }\n\n return slots\n}\n"],"names":["addMinutes","doRangesOverlap","within","slotStart","slotEnd","windows","some","w","Date","start","end","occupancyAt","busy","occ","b","units","computeSlotStates","params","dayEnd","dayStart","quantity","requiredPools","shiftWindows","step","timeOff","pools","slots","cursor","occupancy","poolFull","p","state","push"],"mappings":"AAAA,SAASA,UAAU,EAAEC,eAAe,QAAQ,iBAAgB;AAe5D,MAAMC,SAAS,CAACC,WAAiBC,SAAeC,UAC9CA,QAAQC,IAAI,CAAC,CAACC,IAAMN,gBAAgBE,WAAWC,SAAS,IAAII,KAAKD,EAAEE,KAAK,GAAG,IAAID,KAAKD,EAAEG,GAAG;AAE3F,wEAAwE,GACxE,MAAMC,cAAc,CAACR,WAAiBC,SAAeQ;IACnD,IAAIC,MAAM;IACV,KAAK,MAAMC,KAAKF,KAAM;QACpB,IAAIX,gBAAgBE,WAAWC,SAAS,IAAII,KAAKM,EAAEL,KAAK,GAAG,IAAID,KAAKM,EAAEJ,GAAG,IAAI;YAC3EG,OAAOC,EAAEC,KAAK;QAChB;IACF;IACA,OAAOF;AACT;AAEA;;;;;;CAMC,GACD,OAAO,SAASG,kBAAkBC,MAUjC;IACC,MAAM,EAAEL,IAAI,EAAEM,MAAM,EAAEC,QAAQ,EAAEC,QAAQ,EAAEC,aAAa,EAAEC,YAAY,EAAEC,IAAI,EAAEC,OAAO,EAAE,GAAGP;IACzF,MAAMQ,QAAQJ,iBAAiB,EAAE;IACjC,MAAMK,QAAoB,EAAE;IAE5B,IAAIC,SAAS,IAAInB,KAAKW;IACtB,MAAOQ,SAAST,OAAQ;QACtB,MAAMf,YAAY,IAAIK,KAAKmB;QAC3B,MAAMvB,UAAUJ,WAAWG,WAAWoB;QAEtC,MAAMK,YAAYjB,YAAYR,WAAWC,SAASQ;QAClD,MAAMiB,WAAWJ,MAAMnB,IAAI,CAAC,CAACwB,IAAMnB,YAAYR,WAAWC,SAAS0B,EAAElB,IAAI,KAAKkB,EAAEV,QAAQ;QAExF,IAAIW;QACJ,IAAI,CAAC7B,OAAOC,WAAWC,SAASkB,eAAe;YAC7CS,QAAQ;QACV,OAAO,IAAI7B,OAAOC,WAAWC,SAASoB,UAAU;YAC9CO,QAAQ;QACV,OAAO,IAAIH,aAAaR,YAAYS,UAAU;YAC5CE,QAAQ;QACV,OAAO;YACLA,QAAQ;QACV;QAEAL,MAAMM,IAAI,CAAC;YAAEtB,KAAKN;YAASwB;YAAWnB,OAAON;YAAW4B;QAAM;QAC9DJ,SAASvB;IACX;IAEA,OAAOsB;AACT"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Tri-state per-service guest booking setting. */
|
|
2
|
+
export type GuestBookingSetting = 'disabled' | 'enabled' | 'inherit';
|
|
3
|
+
/**
|
|
4
|
+
* Resolve whether guest bookings are allowed for a service.
|
|
5
|
+
* 'enabled'/'disabled' on the service win; 'inherit' (or unset) falls back to
|
|
6
|
+
* the plugin-level default.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveGuestBookingAllowed(service: {
|
|
9
|
+
allowGuestBooking?: GuestBookingSetting | null;
|
|
10
|
+
} | null | undefined, pluginDefault: boolean): boolean;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Tri-state per-service guest booking setting. */ /**
|
|
2
|
+
* Resolve whether guest bookings are allowed for a service.
|
|
3
|
+
* 'enabled'/'disabled' on the service win; 'inherit' (or unset) falls back to
|
|
4
|
+
* the plugin-level default.
|
|
5
|
+
*/ export function resolveGuestBookingAllowed(service, pluginDefault) {
|
|
6
|
+
const value = service?.allowGuestBooking;
|
|
7
|
+
if (value === 'enabled') {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
if (value === 'disabled') {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
return pluginDefault;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
//# sourceMappingURL=guestBooking.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/guestBooking.ts"],"sourcesContent":["/** Tri-state per-service guest booking setting. */\nexport type GuestBookingSetting = 'disabled' | 'enabled' | 'inherit'\n\n/**\n * Resolve whether guest bookings are allowed for a service.\n * 'enabled'/'disabled' on the service win; 'inherit' (or unset) falls back to\n * the plugin-level default.\n */\nexport function resolveGuestBookingAllowed(\n service: { allowGuestBooking?: GuestBookingSetting | null } | null | undefined,\n pluginDefault: boolean,\n): boolean {\n const value = service?.allowGuestBooking\n if (value === 'enabled') {\n return true\n }\n if (value === 'disabled') {\n return false\n }\n return pluginDefault\n}\n"],"names":["resolveGuestBookingAllowed","service","pluginDefault","value","allowGuestBooking"],"mappings":"AAAA,iDAAiD,GAGjD;;;;CAIC,GACD,OAAO,SAASA,2BACdC,OAA8E,EAC9EC,aAAsB;IAEtB,MAAMC,QAAQF,SAASG;IACvB,IAAID,UAAU,WAAW;QACvB,OAAO;IACT;IACA,IAAIA,UAAU,YAAY;QACxB,OAAO;IACT;IACA,OAAOD;AACT"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { extractId } from './resolveReservationItems.js';
|
|
2
|
+
export { extractId };
|
|
3
|
+
/**
|
|
4
|
+
* Merge a primary set of resource ids with a service's required-resource ids,
|
|
5
|
+
* deduping by string value and preserving first-seen order. Empty/undefined
|
|
6
|
+
* values are dropped.
|
|
7
|
+
*/
|
|
8
|
+
export declare function mergeResourceIds(primary: Array<number | string | undefined>, required: Array<number | string | undefined>): Array<number | string>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { extractId } from './resolveReservationItems.js';
|
|
2
|
+
export { extractId };
|
|
3
|
+
/**
|
|
4
|
+
* Merge a primary set of resource ids with a service's required-resource ids,
|
|
5
|
+
* deduping by string value and preserving first-seen order. Empty/undefined
|
|
6
|
+
* values are dropped.
|
|
7
|
+
*/ export function mergeResourceIds(primary, required) {
|
|
8
|
+
const seen = new Set();
|
|
9
|
+
const out = [];
|
|
10
|
+
for (const id of [
|
|
11
|
+
...primary,
|
|
12
|
+
...required
|
|
13
|
+
]){
|
|
14
|
+
if (id === undefined || id === null || id === '') {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const key = String(id);
|
|
18
|
+
if (seen.has(key)) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
seen.add(key);
|
|
22
|
+
out.push(id);
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
//# sourceMappingURL=resolveRequiredResources.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/resolveRequiredResources.ts"],"sourcesContent":["import { extractId } from './resolveReservationItems.js'\n\nexport { extractId }\n\n/**\n * Merge a primary set of resource ids with a service's required-resource ids,\n * deduping by string value and preserving first-seen order. Empty/undefined\n * values are dropped.\n */\nexport function mergeResourceIds(\n primary: Array<number | string | undefined>,\n required: Array<number | string | undefined>,\n): Array<number | string> {\n const seen = new Set<string>()\n const out: Array<number | string> = []\n for (const id of [...primary, ...required]) {\n if (id === undefined || id === null || id === '') {\n continue\n }\n const key = String(id)\n if (seen.has(key)) {\n continue\n }\n seen.add(key)\n out.push(id)\n }\n return out\n}\n"],"names":["extractId","mergeResourceIds","primary","required","seen","Set","out","id","undefined","key","String","has","add","push"],"mappings":"AAAA,SAASA,SAAS,QAAQ,+BAA8B;AAExD,SAASA,SAAS,GAAE;AAEpB;;;;CAIC,GACD,OAAO,SAASC,iBACdC,OAA2C,EAC3CC,QAA4C;IAE5C,MAAMC,OAAO,IAAIC;IACjB,MAAMC,MAA8B,EAAE;IACtC,KAAK,MAAMC,MAAM;WAAIL;WAAYC;KAAS,CAAE;QAC1C,IAAII,OAAOC,aAAaD,OAAO,QAAQA,OAAO,IAAI;YAChD;QACF;QACA,MAAME,MAAMC,OAAOH;QACnB,IAAIH,KAAKO,GAAG,CAACF,MAAM;YACjB;QACF;QACAL,KAAKQ,GAAG,CAACH;QACTH,IAAIO,IAAI,CAACN;IACX;IACA,OAAOD;AACT"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export type ResolvedItem = {
|
|
2
2
|
endTime: string;
|
|
3
3
|
guestCount: number;
|
|
4
|
-
resource: string;
|
|
5
|
-
service?: string;
|
|
4
|
+
resource: number | string;
|
|
5
|
+
service?: number | string;
|
|
6
6
|
startTime: string;
|
|
7
7
|
};
|
|
8
8
|
/**
|
|
@@ -17,3 +17,4 @@ export type ResolvedItem = {
|
|
|
17
17
|
* works with ResolvedItem[], never with raw reservation data.
|
|
18
18
|
*/
|
|
19
19
|
export declare function resolveReservationItems(data: Record<string, unknown>): ResolvedItem[];
|
|
20
|
+
export declare function extractId(value: unknown): number | string | undefined;
|
|
@@ -16,9 +16,9 @@ import { ValidationError } from 'payload';
|
|
|
16
16
|
const seen = new Set();
|
|
17
17
|
for(let i = 0; i < items.length; i++){
|
|
18
18
|
const item = items[i];
|
|
19
|
-
const resource = extractId(item.resource)
|
|
19
|
+
const resource = extractId(item.resource) ?? extractId(data.resource);
|
|
20
20
|
const startTime = item.startTime ?? data.startTime;
|
|
21
|
-
if (
|
|
21
|
+
if (resource === undefined || resource === '') {
|
|
22
22
|
throw new ValidationError({
|
|
23
23
|
errors: [
|
|
24
24
|
{
|
|
@@ -54,7 +54,7 @@ import { ValidationError } from 'payload';
|
|
|
54
54
|
endTime: item.endTime ?? data.endTime,
|
|
55
55
|
guestCount: item.guestCount ?? data.guestCount ?? 1,
|
|
56
56
|
resource,
|
|
57
|
-
service: extractId(item.service)
|
|
57
|
+
service: extractId(item.service) ?? extractId(data.service),
|
|
58
58
|
startTime
|
|
59
59
|
});
|
|
60
60
|
}
|
|
@@ -64,20 +64,33 @@ import { ValidationError } from 'payload';
|
|
|
64
64
|
if (!data.resource || !data.startTime) {
|
|
65
65
|
return [];
|
|
66
66
|
}
|
|
67
|
+
const resource = extractId(data.resource);
|
|
68
|
+
if (resource === undefined || resource === '') {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
67
71
|
return [
|
|
68
72
|
{
|
|
69
73
|
endTime: data.endTime,
|
|
70
74
|
guestCount: data.guestCount ?? 1,
|
|
71
|
-
resource
|
|
72
|
-
service: extractId(data.service)
|
|
75
|
+
resource,
|
|
76
|
+
service: extractId(data.service),
|
|
73
77
|
startTime: data.startTime
|
|
74
78
|
}
|
|
75
79
|
];
|
|
76
80
|
}
|
|
77
|
-
|
|
81
|
+
// Exported for unit testing. Returns the underlying id value from a relationship
|
|
82
|
+
// field which Payload represents as either the raw id (string for Mongo, number
|
|
83
|
+
// for Postgres) or a populated document `{ id, ... }`.
|
|
84
|
+
//
|
|
85
|
+
// Note: `0` is a valid numeric Postgres id but rare; we still return it rather
|
|
86
|
+
// than treat it as missing.
|
|
87
|
+
export function extractId(value) {
|
|
78
88
|
if (typeof value === 'string' && value) {
|
|
79
89
|
return value;
|
|
80
90
|
}
|
|
91
|
+
if (typeof value === 'number') {
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
81
94
|
if (value && typeof value === 'object' && 'id' in value) {
|
|
82
95
|
return value.id;
|
|
83
96
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utilities/resolveReservationItems.ts"],"sourcesContent":["import { ValidationError } from 'payload'\n\nexport type ResolvedItem = {\n endTime: string\n guestCount: number\n resource: string\n service?: string\n startTime: string\n}\n\n/**\n * Normalize reservation data into a list of resource-level items.\n *\n * - If items[] is populated -> return items (filling defaults from parent).\n * Items missing startTime or resource throw a ValidationError.\n * Duplicate (resource, startTime) pairs throw a ValidationError.\n * - If items[] is empty/absent -> return single item from top-level fields\n *\n * Every downstream function (conflict check, endTime calc, availability)\n * works with ResolvedItem[], never with raw reservation data.\n */\nexport function resolveReservationItems(data: Record<string, unknown>): ResolvedItem[] {\n const items = data.items as Array<Record<string, unknown>> | undefined\n\n if (items && items.length > 0) {\n const resolved: ResolvedItem[] = []\n const seen = new Set<string>()\n\n for (let i = 0; i < items.length; i++) {\n const item = items[i]\n const resource = extractId(item.resource)
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/resolveReservationItems.ts"],"sourcesContent":["import { ValidationError } from 'payload'\n\nexport type ResolvedItem = {\n endTime: string\n guestCount: number\n resource: number | string\n service?: number | string\n startTime: string\n}\n\n/**\n * Normalize reservation data into a list of resource-level items.\n *\n * - If items[] is populated -> return items (filling defaults from parent).\n * Items missing startTime or resource throw a ValidationError.\n * Duplicate (resource, startTime) pairs throw a ValidationError.\n * - If items[] is empty/absent -> return single item from top-level fields\n *\n * Every downstream function (conflict check, endTime calc, availability)\n * works with ResolvedItem[], never with raw reservation data.\n */\nexport function resolveReservationItems(data: Record<string, unknown>): ResolvedItem[] {\n const items = data.items as Array<Record<string, unknown>> | undefined\n\n if (items && items.length > 0) {\n const resolved: ResolvedItem[] = []\n const seen = new Set<string>()\n\n for (let i = 0; i < items.length; i++) {\n const item = items[i]\n const resource = extractId(item.resource) ?? extractId(data.resource)\n const startTime = (item.startTime as string) ?? (data.startTime as string)\n\n if (resource === undefined || resource === '') {\n throw new ValidationError({\n errors: [\n {\n message: `Item ${i} is missing a resource`,\n path: `items.${i}.resource`,\n },\n ],\n })\n }\n\n if (!startTime) {\n throw new ValidationError({\n errors: [\n {\n message: `Item ${i} is missing a startTime`,\n path: `items.${i}.startTime`,\n },\n ],\n })\n }\n\n const key = `${resource}::${startTime}`\n if (seen.has(key)) {\n throw new ValidationError({\n errors: [\n {\n message: `Duplicate booking: item ${i} has the same resource and startTime as a previous item`,\n path: `items.${i}.startTime`,\n },\n ],\n })\n }\n seen.add(key)\n\n resolved.push({\n endTime: (item.endTime as string) ?? (data.endTime as string),\n guestCount: (item.guestCount as number) ?? (data.guestCount as number) ?? 1,\n resource,\n service: extractId(item.service) ?? extractId(data.service),\n startTime,\n })\n }\n\n return resolved\n }\n\n // Single-resource fallback (current behavior)\n if (!data.resource || !data.startTime) {\n return []\n }\n\n const resource = extractId(data.resource)\n if (resource === undefined || resource === '') {\n return []\n }\n\n return [\n {\n endTime: data.endTime as string,\n guestCount: (data.guestCount as number) ?? 1,\n resource,\n service: extractId(data.service),\n startTime: data.startTime as string,\n },\n ]\n}\n\n// Exported for unit testing. Returns the underlying id value from a relationship\n// field which Payload represents as either the raw id (string for Mongo, number\n// for Postgres) or a populated document `{ id, ... }`.\n//\n// Note: `0` is a valid numeric Postgres id but rare; we still return it rather\n// than treat it as missing.\nexport function extractId(value: unknown): number | string | undefined {\n if (typeof value === 'string' && value) {\n return value\n }\n if (typeof value === 'number') {\n return value\n }\n if (value && typeof value === 'object' && 'id' in value) {\n return (value as { id: number | string }).id\n }\n return undefined\n}\n"],"names":["ValidationError","resolveReservationItems","data","items","length","resolved","seen","Set","i","item","resource","extractId","startTime","undefined","errors","message","path","key","has","add","push","endTime","guestCount","service","value","id"],"mappings":"AAAA,SAASA,eAAe,QAAQ,UAAS;AAUzC;;;;;;;;;;CAUC,GACD,OAAO,SAASC,wBAAwBC,IAA6B;IACnE,MAAMC,QAAQD,KAAKC,KAAK;IAExB,IAAIA,SAASA,MAAMC,MAAM,GAAG,GAAG;QAC7B,MAAMC,WAA2B,EAAE;QACnC,MAAMC,OAAO,IAAIC;QAEjB,IAAK,IAAIC,IAAI,GAAGA,IAAIL,MAAMC,MAAM,EAAEI,IAAK;YACrC,MAAMC,OAAON,KAAK,CAACK,EAAE;YACrB,MAAME,WAAWC,UAAUF,KAAKC,QAAQ,KAAKC,UAAUT,KAAKQ,QAAQ;YACpE,MAAME,YAAY,AAACH,KAAKG,SAAS,IAAgBV,KAAKU,SAAS;YAE/D,IAAIF,aAAaG,aAAaH,aAAa,IAAI;gBAC7C,MAAM,IAAIV,gBAAgB;oBACxBc,QAAQ;wBACN;4BACEC,SAAS,CAAC,KAAK,EAAEP,EAAE,sBAAsB,CAAC;4BAC1CQ,MAAM,CAAC,MAAM,EAAER,EAAE,SAAS,CAAC;wBAC7B;qBACD;gBACH;YACF;YAEA,IAAI,CAACI,WAAW;gBACd,MAAM,IAAIZ,gBAAgB;oBACxBc,QAAQ;wBACN;4BACEC,SAAS,CAAC,KAAK,EAAEP,EAAE,uBAAuB,CAAC;4BAC3CQ,MAAM,CAAC,MAAM,EAAER,EAAE,UAAU,CAAC;wBAC9B;qBACD;gBACH;YACF;YAEA,MAAMS,MAAM,GAAGP,SAAS,EAAE,EAAEE,WAAW;YACvC,IAAIN,KAAKY,GAAG,CAACD,MAAM;gBACjB,MAAM,IAAIjB,gBAAgB;oBACxBc,QAAQ;wBACN;4BACEC,SAAS,CAAC,wBAAwB,EAAEP,EAAE,uDAAuD,CAAC;4BAC9FQ,MAAM,CAAC,MAAM,EAAER,EAAE,UAAU,CAAC;wBAC9B;qBACD;gBACH;YACF;YACAF,KAAKa,GAAG,CAACF;YAETZ,SAASe,IAAI,CAAC;gBACZC,SAAS,AAACZ,KAAKY,OAAO,IAAgBnB,KAAKmB,OAAO;gBAClDC,YAAY,AAACb,KAAKa,UAAU,IAAgBpB,KAAKoB,UAAU,IAAe;gBAC1EZ;gBACAa,SAASZ,UAAUF,KAAKc,OAAO,KAAKZ,UAAUT,KAAKqB,OAAO;gBAC1DX;YACF;QACF;QAEA,OAAOP;IACT;IAEA,8CAA8C;IAC9C,IAAI,CAACH,KAAKQ,QAAQ,IAAI,CAACR,KAAKU,SAAS,EAAE;QACrC,OAAO,EAAE;IACX;IAEA,MAAMF,WAAWC,UAAUT,KAAKQ,QAAQ;IACxC,IAAIA,aAAaG,aAAaH,aAAa,IAAI;QAC7C,OAAO,EAAE;IACX;IAEA,OAAO;QACL;YACEW,SAASnB,KAAKmB,OAAO;YACrBC,YAAY,AAACpB,KAAKoB,UAAU,IAAe;YAC3CZ;YACAa,SAASZ,UAAUT,KAAKqB,OAAO;YAC/BX,WAAWV,KAAKU,SAAS;QAC3B;KACD;AACH;AAEA,iFAAiF;AACjF,gFAAgF;AAChF,uDAAuD;AACvD,EAAE;AACF,+EAA+E;AAC/E,4BAA4B;AAC5B,OAAO,SAASD,UAAUa,KAAc;IACtC,IAAI,OAAOA,UAAU,YAAYA,OAAO;QACtC,OAAOA;IACT;IACA,IAAI,OAAOA,UAAU,UAAU;QAC7B,OAAOA;IACT;IACA,IAAIA,SAAS,OAAOA,UAAU,YAAY,QAAQA,OAAO;QACvD,OAAO,AAACA,MAAkCC,EAAE;IAC9C;IACA,OAAOZ;AACT"}
|
|
@@ -20,9 +20,11 @@ export declare function parseTime(time: string): {
|
|
|
20
20
|
export declare function combineDateAndTime(date: Date, time: string): Date;
|
|
21
21
|
/**
|
|
22
22
|
* Check if a given date is an exception date in the schedule.
|
|
23
|
+
* Supports range exceptions via optional endDate (inclusive on both ends).
|
|
23
24
|
*/
|
|
24
25
|
export declare function isExceptionDate(date: Date, exceptions: Array<{
|
|
25
26
|
date: string;
|
|
27
|
+
endDate?: string;
|
|
26
28
|
}>): boolean;
|
|
27
29
|
type RecurringSlot = {
|
|
28
30
|
day: DayOfWeek;
|
|
@@ -38,6 +40,7 @@ type Schedule = {
|
|
|
38
40
|
active?: boolean;
|
|
39
41
|
exceptions?: Array<{
|
|
40
42
|
date: string;
|
|
43
|
+
endDate?: string;
|
|
41
44
|
}>;
|
|
42
45
|
manualSlots?: ManualSlot[];
|
|
43
46
|
recurringSlots?: RecurringSlot[];
|
|
@@ -39,11 +39,13 @@ const DAY_MAP = {
|
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
41
|
* Check if a given date is an exception date in the schedule.
|
|
42
|
+
* Supports range exceptions via optional endDate (inclusive on both ends).
|
|
42
43
|
*/ export function isExceptionDate(date, exceptions) {
|
|
43
|
-
const
|
|
44
|
+
const target = date.toISOString().split('T')[0];
|
|
44
45
|
return exceptions.some((exc)=>{
|
|
45
|
-
const
|
|
46
|
-
|
|
46
|
+
const start = new Date(exc.date).toISOString().split('T')[0];
|
|
47
|
+
const end = exc.endDate ? new Date(exc.endDate).toISOString().split('T')[0] : start;
|
|
48
|
+
return target >= start && target <= end;
|
|
47
49
|
});
|
|
48
50
|
}
|
|
49
51
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utilities/scheduleUtils.ts"],"sourcesContent":["import type { DayOfWeek } from '../types.js'\n\nconst DAY_MAP: Record<DayOfWeek, number> = {\n fri: 5,\n mon: 1,\n sat: 6,\n sun: 0,\n thu: 4,\n tue: 2,\n wed: 3,\n}\n\n/**\n * Get the DayOfWeek value for a given Date.\n */\nexport function getDayOfWeek(date: Date): DayOfWeek {\n const jsDay = date.getDay()\n const entries = Object.entries(DAY_MAP) as [DayOfWeek, number][]\n const found = entries.find(([, num]) => num === jsDay)\n return found ? found[0] : 'mon'\n}\n\n/**\n * Check if a date string (ISO) matches a DayOfWeek.\n */\nexport function dateMatchesDay(date: Date, day: DayOfWeek): boolean {\n return date.getDay() === DAY_MAP[day]\n}\n\n/**\n * Parse a HH:mm time string to hours and minutes.\n */\nexport function parseTime(time: string): { hours: number; minutes: number } {\n const [h, m] = time.split(':').map(Number)\n return { hours: h, minutes: m }\n}\n\n/**\n * Combine a date (day) with a HH:mm time string into a full Date.\n */\nexport function combineDateAndTime(date: Date, time: string): Date {\n const { hours, minutes } = parseTime(time)\n const combined = new Date(date)\n combined.setHours(hours, minutes, 0, 0)\n return combined\n}\n\n/**\n * Check if a given date is an exception date in the schedule.\n */\nexport function isExceptionDate(\n date: Date,\n exceptions: Array<{ date: string }>,\n): boolean {\n const
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/scheduleUtils.ts"],"sourcesContent":["import type { DayOfWeek } from '../types.js'\n\nconst DAY_MAP: Record<DayOfWeek, number> = {\n fri: 5,\n mon: 1,\n sat: 6,\n sun: 0,\n thu: 4,\n tue: 2,\n wed: 3,\n}\n\n/**\n * Get the DayOfWeek value for a given Date.\n */\nexport function getDayOfWeek(date: Date): DayOfWeek {\n const jsDay = date.getDay()\n const entries = Object.entries(DAY_MAP) as [DayOfWeek, number][]\n const found = entries.find(([, num]) => num === jsDay)\n return found ? found[0] : 'mon'\n}\n\n/**\n * Check if a date string (ISO) matches a DayOfWeek.\n */\nexport function dateMatchesDay(date: Date, day: DayOfWeek): boolean {\n return date.getDay() === DAY_MAP[day]\n}\n\n/**\n * Parse a HH:mm time string to hours and minutes.\n */\nexport function parseTime(time: string): { hours: number; minutes: number } {\n const [h, m] = time.split(':').map(Number)\n return { hours: h, minutes: m }\n}\n\n/**\n * Combine a date (day) with a HH:mm time string into a full Date.\n */\nexport function combineDateAndTime(date: Date, time: string): Date {\n const { hours, minutes } = parseTime(time)\n const combined = new Date(date)\n combined.setHours(hours, minutes, 0, 0)\n return combined\n}\n\n/**\n * Check if a given date is an exception date in the schedule.\n * Supports range exceptions via optional endDate (inclusive on both ends).\n */\nexport function isExceptionDate(\n date: Date,\n exceptions: Array<{ date: string; endDate?: string }>,\n): boolean {\n const target = date.toISOString().split('T')[0]\n return exceptions.some((exc) => {\n const start = new Date(exc.date).toISOString().split('T')[0]\n const end = exc.endDate ? new Date(exc.endDate).toISOString().split('T')[0] : start\n return target >= start && target <= end\n })\n}\n\ntype RecurringSlot = {\n day: DayOfWeek\n endTime: string\n startTime: string\n}\n\ntype ManualSlot = {\n date: string\n endTime: string\n startTime: string\n}\n\ntype Schedule = {\n active?: boolean\n exceptions?: Array<{ date: string; endDate?: string }>\n manualSlots?: ManualSlot[]\n recurringSlots?: RecurringSlot[]\n scheduleType: 'manual' | 'recurring'\n}\n\ntype TimeRange = {\n end: Date\n start: Date\n}\n\n/**\n * Resolve a schedule to concrete available time ranges for a given date.\n */\nexport function resolveScheduleForDate(schedule: Schedule, date: Date): TimeRange[] {\n if (schedule.active === false) {return []}\n\n const exceptions = schedule.exceptions ?? []\n if (isExceptionDate(date, exceptions)) {return []}\n\n const ranges: TimeRange[] = []\n\n if (schedule.scheduleType === 'recurring') {\n const slots = schedule.recurringSlots ?? []\n const dayOfWeek = getDayOfWeek(date)\n for (const slot of slots) {\n if (slot.day === dayOfWeek) {\n ranges.push({\n end: combineDateAndTime(date, slot.endTime),\n start: combineDateAndTime(date, slot.startTime),\n })\n }\n }\n } else if (schedule.scheduleType === 'manual') {\n const slots = schedule.manualSlots ?? []\n const dateStr = date.toISOString().split('T')[0]\n for (const slot of slots) {\n const slotDateStr = new Date(slot.date).toISOString().split('T')[0]\n if (slotDateStr === dateStr) {\n ranges.push({\n end: combineDateAndTime(date, slot.endTime),\n start: combineDateAndTime(date, slot.startTime),\n })\n }\n }\n }\n\n return ranges\n}\n"],"names":["DAY_MAP","fri","mon","sat","sun","thu","tue","wed","getDayOfWeek","date","jsDay","getDay","entries","Object","found","find","num","dateMatchesDay","day","parseTime","time","h","m","split","map","Number","hours","minutes","combineDateAndTime","combined","Date","setHours","isExceptionDate","exceptions","target","toISOString","some","exc","start","end","endDate","resolveScheduleForDate","schedule","active","ranges","scheduleType","slots","recurringSlots","dayOfWeek","slot","push","endTime","startTime","manualSlots","dateStr","slotDateStr"],"mappings":"AAEA,MAAMA,UAAqC;IACzCC,KAAK;IACLC,KAAK;IACLC,KAAK;IACLC,KAAK;IACLC,KAAK;IACLC,KAAK;IACLC,KAAK;AACP;AAEA;;CAEC,GACD,OAAO,SAASC,aAAaC,IAAU;IACrC,MAAMC,QAAQD,KAAKE,MAAM;IACzB,MAAMC,UAAUC,OAAOD,OAAO,CAACZ;IAC/B,MAAMc,QAAQF,QAAQG,IAAI,CAAC,CAAC,GAAGC,IAAI,GAAKA,QAAQN;IAChD,OAAOI,QAAQA,KAAK,CAAC,EAAE,GAAG;AAC5B;AAEA;;CAEC,GACD,OAAO,SAASG,eAAeR,IAAU,EAAES,GAAc;IACvD,OAAOT,KAAKE,MAAM,OAAOX,OAAO,CAACkB,IAAI;AACvC;AAEA;;CAEC,GACD,OAAO,SAASC,UAAUC,IAAY;IACpC,MAAM,CAACC,GAAGC,EAAE,GAAGF,KAAKG,KAAK,CAAC,KAAKC,GAAG,CAACC;IACnC,OAAO;QAAEC,OAAOL;QAAGM,SAASL;IAAE;AAChC;AAEA;;CAEC,GACD,OAAO,SAASM,mBAAmBnB,IAAU,EAAEW,IAAY;IACzD,MAAM,EAAEM,KAAK,EAAEC,OAAO,EAAE,GAAGR,UAAUC;IACrC,MAAMS,WAAW,IAAIC,KAAKrB;IAC1BoB,SAASE,QAAQ,CAACL,OAAOC,SAAS,GAAG;IACrC,OAAOE;AACT;AAEA;;;CAGC,GACD,OAAO,SAASG,gBACdvB,IAAU,EACVwB,UAAqD;IAErD,MAAMC,SAASzB,KAAK0B,WAAW,GAAGZ,KAAK,CAAC,IAAI,CAAC,EAAE;IAC/C,OAAOU,WAAWG,IAAI,CAAC,CAACC;QACtB,MAAMC,QAAQ,IAAIR,KAAKO,IAAI5B,IAAI,EAAE0B,WAAW,GAAGZ,KAAK,CAAC,IAAI,CAAC,EAAE;QAC5D,MAAMgB,MAAMF,IAAIG,OAAO,GAAG,IAAIV,KAAKO,IAAIG,OAAO,EAAEL,WAAW,GAAGZ,KAAK,CAAC,IAAI,CAAC,EAAE,GAAGe;QAC9E,OAAOJ,UAAUI,SAASJ,UAAUK;IACtC;AACF;AA2BA;;CAEC,GACD,OAAO,SAASE,uBAAuBC,QAAkB,EAAEjC,IAAU;IACnE,IAAIiC,SAASC,MAAM,KAAK,OAAO;QAAC,OAAO,EAAE;IAAA;IAEzC,MAAMV,aAAaS,SAAST,UAAU,IAAI,EAAE;IAC5C,IAAID,gBAAgBvB,MAAMwB,aAAa;QAAC,OAAO,EAAE;IAAA;IAEjD,MAAMW,SAAsB,EAAE;IAE9B,IAAIF,SAASG,YAAY,KAAK,aAAa;QACzC,MAAMC,QAAQJ,SAASK,cAAc,IAAI,EAAE;QAC3C,MAAMC,YAAYxC,aAAaC;QAC/B,KAAK,MAAMwC,QAAQH,MAAO;YACxB,IAAIG,KAAK/B,GAAG,KAAK8B,WAAW;gBAC1BJ,OAAOM,IAAI,CAAC;oBACVX,KAAKX,mBAAmBnB,MAAMwC,KAAKE,OAAO;oBAC1Cb,OAAOV,mBAAmBnB,MAAMwC,KAAKG,SAAS;gBAChD;YACF;QACF;IACF,OAAO,IAAIV,SAASG,YAAY,KAAK,UAAU;QAC7C,MAAMC,QAAQJ,SAASW,WAAW,IAAI,EAAE;QACxC,MAAMC,UAAU7C,KAAK0B,WAAW,GAAGZ,KAAK,CAAC,IAAI,CAAC,EAAE;QAChD,KAAK,MAAM0B,QAAQH,MAAO;YACxB,MAAMS,cAAc,IAAIzB,KAAKmB,KAAKxC,IAAI,EAAE0B,WAAW,GAAGZ,KAAK,CAAC,IAAI,CAAC,EAAE;YACnE,IAAIgC,gBAAgBD,SAAS;gBAC3BV,OAAOM,IAAI,CAAC;oBACVX,KAAKX,mBAAmBnB,MAAMwC,KAAKE,OAAO;oBAC1Cb,OAAOV,mBAAmBnB,MAAMwC,KAAKG,SAAS;gBAChD;YACF;QACF;IACF;IAEA,OAAOR;AACT"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build Payload select `options` from a list of string values.
|
|
3
|
+
* Labels default to a capitalized form of the value.
|
|
4
|
+
*/ export function buildSelectOptions(values) {
|
|
5
|
+
return values.map((value)=>({
|
|
6
|
+
label: value.charAt(0).toUpperCase() + value.slice(1),
|
|
7
|
+
value
|
|
8
|
+
}));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
//# sourceMappingURL=selectOptions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/selectOptions.ts"],"sourcesContent":["/**\n * Build Payload select `options` from a list of string values.\n * Labels default to a capitalized form of the value.\n */\nexport function buildSelectOptions(\n values: string[],\n): Array<{ label: string; value: string }> {\n return values.map((value) => ({\n label: value.charAt(0).toUpperCase() + value.slice(1),\n value,\n }))\n}\n"],"names":["buildSelectOptions","values","map","value","label","charAt","toUpperCase","slice"],"mappings":"AAAA;;;CAGC,GACD,OAAO,SAASA,mBACdC,MAAgB;IAEhB,OAAOA,OAAOC,GAAG,CAAC,CAACC,QAAW,CAAA;YAC5BC,OAAOD,MAAME,MAAM,CAAC,GAAGC,WAAW,KAAKH,MAAMI,KAAK,CAAC;YACnDJ;QACF,CAAA;AACF"}
|
|
@@ -19,3 +19,22 @@ export declare function computeBlockedWindow(startTime: Date, endTime: Date, buf
|
|
|
19
19
|
* Calculate hours between now and a future date.
|
|
20
20
|
*/
|
|
21
21
|
export declare function hoursUntil(futureDate: Date, now?: Date): number;
|
|
22
|
+
/**
|
|
23
|
+
* Local-calendar day key (YYYY-MM-DD) for a Date, using local components (not UTC).
|
|
24
|
+
*/
|
|
25
|
+
export declare function localDayKey(date: Date): string;
|
|
26
|
+
/**
|
|
27
|
+
* Intersect two lists of half-open [start, end) intervals.
|
|
28
|
+
* Returns every non-empty overlap between an interval in `a` and one in `b`.
|
|
29
|
+
* Fold over N lists to intersect more than two.
|
|
30
|
+
*/
|
|
31
|
+
export declare function intersectIntervals(a: Array<{
|
|
32
|
+
end: Date;
|
|
33
|
+
start: Date;
|
|
34
|
+
}>, b: Array<{
|
|
35
|
+
end: Date;
|
|
36
|
+
start: Date;
|
|
37
|
+
}>): Array<{
|
|
38
|
+
end: Date;
|
|
39
|
+
start: Date;
|
|
40
|
+
}>;
|
|
@@ -24,5 +24,33 @@
|
|
|
24
24
|
const reference = now ?? new Date();
|
|
25
25
|
return (futureDate.getTime() - reference.getTime()) / (1000 * 60 * 60);
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Local-calendar day key (YYYY-MM-DD) for a Date, using local components (not UTC).
|
|
29
|
+
*/ export function localDayKey(date) {
|
|
30
|
+
const y = date.getFullYear();
|
|
31
|
+
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
32
|
+
const d = String(date.getDate()).padStart(2, '0');
|
|
33
|
+
return `${y}-${m}-${d}`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Intersect two lists of half-open [start, end) intervals.
|
|
37
|
+
* Returns every non-empty overlap between an interval in `a` and one in `b`.
|
|
38
|
+
* Fold over N lists to intersect more than two.
|
|
39
|
+
*/ export function intersectIntervals(a, b) {
|
|
40
|
+
const out = [];
|
|
41
|
+
for (const x of a){
|
|
42
|
+
for (const y of b){
|
|
43
|
+
const start = x.start > y.start ? x.start : y.start;
|
|
44
|
+
const end = x.end < y.end ? x.end : y.end;
|
|
45
|
+
if (start < end) {
|
|
46
|
+
out.push({
|
|
47
|
+
end,
|
|
48
|
+
start
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
27
55
|
|
|
28
56
|
//# sourceMappingURL=slotUtils.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utilities/slotUtils.ts"],"sourcesContent":["/**\n * Add minutes to a date and return a new Date.\n */\nexport function addMinutes(date: Date, minutes: number): Date {\n return new Date(date.getTime() + minutes * 60_000)\n}\n\n/**\n * Check if two time ranges overlap.\n * Ranges are [startA, endA) and [startB, endB) (half-open intervals).\n */\nexport function doRangesOverlap(\n startA: Date,\n endA: Date,\n startB: Date,\n endB: Date,\n): boolean {\n return startA < endB && startB < endA\n}\n\n/**\n * Compute the effective blocked window for a reservation,\n * applying buffer times before and after.\n */\nexport function computeBlockedWindow(\n startTime: Date,\n endTime: Date,\n bufferBefore: number,\n bufferAfter: number,\n): { effectiveEnd: Date; effectiveStart: Date } {\n return {\n effectiveEnd: addMinutes(endTime, bufferAfter),\n effectiveStart: addMinutes(startTime, -bufferBefore),\n }\n}\n\n/**\n * Calculate hours between now and a future date.\n */\nexport function hoursUntil(futureDate: Date, now?: Date): number {\n const reference = now ?? new Date()\n return (futureDate.getTime() - reference.getTime()) / (1000 * 60 * 60)\n}\n"],"names":["addMinutes","date","minutes","Date","getTime","doRangesOverlap","startA","endA","startB","endB","computeBlockedWindow","startTime","endTime","bufferBefore","bufferAfter","effectiveEnd","effectiveStart","hoursUntil","futureDate","now","reference"],"mappings":"AAAA;;CAEC,GACD,OAAO,SAASA,WAAWC,IAAU,EAAEC,OAAe;IACpD,OAAO,IAAIC,KAAKF,KAAKG,OAAO,KAAKF,UAAU;AAC7C;AAEA;;;CAGC,GACD,OAAO,SAASG,gBACdC,MAAY,EACZC,IAAU,EACVC,MAAY,EACZC,IAAU;IAEV,OAAOH,SAASG,QAAQD,SAASD;AACnC;AAEA;;;CAGC,GACD,OAAO,SAASG,qBACdC,SAAe,EACfC,OAAa,EACbC,YAAoB,EACpBC,WAAmB;IAEnB,OAAO;QACLC,cAAcf,WAAWY,SAASE;QAClCE,gBAAgBhB,WAAWW,WAAW,CAACE;IACzC;AACF;AAEA;;CAEC,GACD,OAAO,SAASI,WAAWC,UAAgB,EAAEC,GAAU;IACrD,MAAMC,YAAYD,OAAO,IAAIhB;IAC7B,OAAO,AAACe,CAAAA,WAAWd,OAAO,KAAKgB,UAAUhB,OAAO,EAAC,IAAM,CAAA,OAAO,KAAK,EAAC;AACtE"}
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/slotUtils.ts"],"sourcesContent":["/**\n * Add minutes to a date and return a new Date.\n */\nexport function addMinutes(date: Date, minutes: number): Date {\n return new Date(date.getTime() + minutes * 60_000)\n}\n\n/**\n * Check if two time ranges overlap.\n * Ranges are [startA, endA) and [startB, endB) (half-open intervals).\n */\nexport function doRangesOverlap(\n startA: Date,\n endA: Date,\n startB: Date,\n endB: Date,\n): boolean {\n return startA < endB && startB < endA\n}\n\n/**\n * Compute the effective blocked window for a reservation,\n * applying buffer times before and after.\n */\nexport function computeBlockedWindow(\n startTime: Date,\n endTime: Date,\n bufferBefore: number,\n bufferAfter: number,\n): { effectiveEnd: Date; effectiveStart: Date } {\n return {\n effectiveEnd: addMinutes(endTime, bufferAfter),\n effectiveStart: addMinutes(startTime, -bufferBefore),\n }\n}\n\n/**\n * Calculate hours between now and a future date.\n */\nexport function hoursUntil(futureDate: Date, now?: Date): number {\n const reference = now ?? new Date()\n return (futureDate.getTime() - reference.getTime()) / (1000 * 60 * 60)\n}\n\n/**\n * Local-calendar day key (YYYY-MM-DD) for a Date, using local components (not UTC).\n */\nexport function localDayKey(date: Date): string {\n const y = date.getFullYear()\n const m = String(date.getMonth() + 1).padStart(2, '0')\n const d = String(date.getDate()).padStart(2, '0')\n return `${y}-${m}-${d}`\n}\n\n/**\n * Intersect two lists of half-open [start, end) intervals.\n * Returns every non-empty overlap between an interval in `a` and one in `b`.\n * Fold over N lists to intersect more than two.\n */\nexport function intersectIntervals(\n a: Array<{ end: Date; start: Date }>,\n b: Array<{ end: Date; start: Date }>,\n): Array<{ end: Date; start: Date }> {\n const out: Array<{ end: Date; start: Date }> = []\n for (const x of a) {\n for (const y of b) {\n const start = x.start > y.start ? x.start : y.start\n const end = x.end < y.end ? x.end : y.end\n if (start < end) {\n out.push({ end, start })\n }\n }\n }\n return out\n}\n"],"names":["addMinutes","date","minutes","Date","getTime","doRangesOverlap","startA","endA","startB","endB","computeBlockedWindow","startTime","endTime","bufferBefore","bufferAfter","effectiveEnd","effectiveStart","hoursUntil","futureDate","now","reference","localDayKey","y","getFullYear","m","String","getMonth","padStart","d","getDate","intersectIntervals","a","b","out","x","start","end","push"],"mappings":"AAAA;;CAEC,GACD,OAAO,SAASA,WAAWC,IAAU,EAAEC,OAAe;IACpD,OAAO,IAAIC,KAAKF,KAAKG,OAAO,KAAKF,UAAU;AAC7C;AAEA;;;CAGC,GACD,OAAO,SAASG,gBACdC,MAAY,EACZC,IAAU,EACVC,MAAY,EACZC,IAAU;IAEV,OAAOH,SAASG,QAAQD,SAASD;AACnC;AAEA;;;CAGC,GACD,OAAO,SAASG,qBACdC,SAAe,EACfC,OAAa,EACbC,YAAoB,EACpBC,WAAmB;IAEnB,OAAO;QACLC,cAAcf,WAAWY,SAASE;QAClCE,gBAAgBhB,WAAWW,WAAW,CAACE;IACzC;AACF;AAEA;;CAEC,GACD,OAAO,SAASI,WAAWC,UAAgB,EAAEC,GAAU;IACrD,MAAMC,YAAYD,OAAO,IAAIhB;IAC7B,OAAO,AAACe,CAAAA,WAAWd,OAAO,KAAKgB,UAAUhB,OAAO,EAAC,IAAM,CAAA,OAAO,KAAK,EAAC;AACtE;AAEA;;CAEC,GACD,OAAO,SAASiB,YAAYpB,IAAU;IACpC,MAAMqB,IAAIrB,KAAKsB,WAAW;IAC1B,MAAMC,IAAIC,OAAOxB,KAAKyB,QAAQ,KAAK,GAAGC,QAAQ,CAAC,GAAG;IAClD,MAAMC,IAAIH,OAAOxB,KAAK4B,OAAO,IAAIF,QAAQ,CAAC,GAAG;IAC7C,OAAO,GAAGL,EAAE,CAAC,EAAEE,EAAE,CAAC,EAAEI,GAAG;AACzB;AAEA;;;;CAIC,GACD,OAAO,SAASE,mBACdC,CAAoC,EACpCC,CAAoC;IAEpC,MAAMC,MAAyC,EAAE;IACjD,KAAK,MAAMC,KAAKH,EAAG;QACjB,KAAK,MAAMT,KAAKU,EAAG;YACjB,MAAMG,QAAQD,EAAEC,KAAK,GAAGb,EAAEa,KAAK,GAAGD,EAAEC,KAAK,GAAGb,EAAEa,KAAK;YACnD,MAAMC,MAAMF,EAAEE,GAAG,GAAGd,EAAEc,GAAG,GAAGF,EAAEE,GAAG,GAAGd,EAAEc,GAAG;YACzC,IAAID,QAAQC,KAAK;gBACfH,IAAII,IAAI,CAAC;oBAAED;oBAAKD;gBAAM;YACxB;QACF;IACF;IACA,OAAOF;AACT"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ResolvedReservationPluginConfig } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Roles considered privileged (staff/admin) — the union of `resourceOwnerMode`
|
|
4
|
+
* admin roles and `staffProvisioning` staff roles. Empty when neither is set.
|
|
5
|
+
*/
|
|
6
|
+
export declare function privilegedRoles(config: ResolvedReservationPluginConfig): string[];
|
|
7
|
+
/**
|
|
8
|
+
* True if the request user is staff/admin (i.e. NOT a customer).
|
|
9
|
+
*
|
|
10
|
+
* Two-collection model (default): anyone whose collection differs from
|
|
11
|
+
* `slugs.customers` is staff/admin — the original behaviour, unchanged.
|
|
12
|
+
*
|
|
13
|
+
* Single-collection model (`userCollection` set, so `slugs.customers` IS the
|
|
14
|
+
* auth collection): collection can't distinguish staff from customers, so fall
|
|
15
|
+
* back to the user's role against `privilegedRoles`. With no privileged roles
|
|
16
|
+
* configured this returns false (safe — treats everyone as a customer).
|
|
17
|
+
*/
|
|
18
|
+
export declare function isPrivilegedUser(user: ({
|
|
19
|
+
collection?: string;
|
|
20
|
+
} & Record<string, unknown>) | null | undefined, config: ResolvedReservationPluginConfig): boolean;
|