payload-reserve 1.1.0 → 1.2.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 +29 -3
- package/dist/collections/Schedules.js +49 -4
- package/dist/collections/Schedules.js.map +1 -1
- package/dist/defaults.js +28 -1
- package/dist/defaults.js.map +1 -1
- package/dist/endpoints/cancelBooking.js +20 -0
- package/dist/endpoints/cancelBooking.js.map +1 -1
- package/dist/endpoints/checkAvailability.js +11 -1
- package/dist/endpoints/checkAvailability.js.map +1 -1
- package/dist/endpoints/customerSearch.js +8 -0
- package/dist/endpoints/customerSearch.js.map +1 -1
- package/dist/endpoints/getSlots.js +1 -0
- package/dist/endpoints/getSlots.js.map +1 -1
- package/dist/hooks/reservations/onStatusChange.js +35 -16
- package/dist/hooks/reservations/onStatusChange.js.map +1 -1
- package/dist/hooks/reservations/validateConflicts.js +23 -22
- package/dist/hooks/reservations/validateConflicts.js.map +1 -1
- package/dist/hooks/reservations/validateStatusTransition.js +16 -6
- package/dist/hooks/reservations/validateStatusTransition.js.map +1 -1
- package/dist/services/AvailabilityService.d.ts +1 -0
- package/dist/services/AvailabilityService.js +30 -4
- package/dist/services/AvailabilityService.js.map +1 -1
- package/dist/utilities/resolveReservationItems.d.ts +2 -1
- package/dist/utilities/resolveReservationItems.js +47 -5
- package/dist/utilities/resolveReservationItems.js.map +1 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Designed for salons, clinics, hotels, restaurants, event venues, and any busines
|
|
|
12
12
|
- **User Collection Extension** — Optionally extend your existing auth collection with booking fields; set `userCollection: undefined` (default) to use a standalone Customers collection
|
|
13
13
|
- **Resource Owner Multi-Tenancy** — Opt-in `resourceOwnerMode` wires ownership access control so each resource owner (host) sees only their own listings and reservations
|
|
14
14
|
- **Configurable Status Machine** — Define your own statuses, transitions, blocking states, and terminal states
|
|
15
|
-
- **Double-Booking Prevention** — Server-side conflict detection with
|
|
15
|
+
- **Double-Booking Prevention** — Server-side conflict detection with per-item buffer times; respects capacity modes
|
|
16
16
|
- **Auto End Time** — Calculates `endTime` from `startTime + service.duration` automatically
|
|
17
17
|
- **Three Duration Types** — `fixed` (service duration), `flexible` (customer-specified end), and `full-day` bookings
|
|
18
18
|
- **Multi-Resource Bookings** — Single reservation that spans multiple resources simultaneously via the `items` array
|
|
@@ -21,8 +21,8 @@ Designed for salons, clinics, hotels, restaurants, event venues, and any busines
|
|
|
21
21
|
- **Extra Reservation Fields** — Inject custom fields into the Reservations collection via `extraReservationFields` without forking the plugin
|
|
22
22
|
- **Cancellation Policy** — Configurable minimum notice period enforcement
|
|
23
23
|
- **Plugin Hooks API** — Seven lifecycle hooks (`beforeBookingCreate`, `afterBookingCreate`, `beforeBookingConfirm`, `afterBookingConfirm`, `beforeBookingCancel`, `afterBookingCancel`, `afterStatusChange`) for integrating email, Stripe, and external systems
|
|
24
|
-
- **Availability Service** — Pure functions and DB helpers for slot generation and conflict checking
|
|
25
|
-
- **Public REST API** — Five pre-built endpoints for availability, slot listing, booking, cancellation, and customer search
|
|
24
|
+
- **Availability Service** — Pure functions and DB helpers for slot generation (15-min step) and conflict checking with guest-count-aware filtering
|
|
25
|
+
- **Public REST API** — Five pre-built endpoints for availability, slot listing, booking, cancellation, and customer search — with ownership enforcement and input validation
|
|
26
26
|
- **Calendar View** — Month/week/day calendar replacing the default reservations list view
|
|
27
27
|
- **Dashboard Widget** — Server component showing today's booking stats
|
|
28
28
|
- **Availability Overview** — Weekly grid of resource availability vs. booked slots
|
|
@@ -105,3 +105,29 @@ The `access` override in plugin config always takes precedence over the auto-wir
|
|
|
105
105
|
| [Examples](https://github.com/elghaied/payload-reserve/blob/main/docs/examples.md) | Salon, hotel, restaurant, event venue, Stripe, email, multi-tenant (resource owner mode) |
|
|
106
106
|
| [Advanced](https://github.com/elghaied/payload-reserve/blob/main/docs/advanced.md) | DB indexes, reconciliation job for race condition detection |
|
|
107
107
|
| [Development](https://github.com/elghaied/payload-reserve/blob/main/docs/development.md) | Prerequisites, commands, project file tree |
|
|
108
|
+
| [v1.2.0 Breaking Changes](https://github.com/elghaied/payload-reserve/blob/main/docs/BREAKING-CHANGES-v1.2.md) | Migration guide for upgrading to v1.2.0 |
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Contributing
|
|
113
|
+
|
|
114
|
+
This project uses [Changesets](https://github.com/changesets/changesets) for versioning and changelogs.
|
|
115
|
+
|
|
116
|
+
When making a change that should appear in the release notes, run:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
pnpm changeset
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
This prompts for the semver bump type (patch/minor/major) and a summary. Commit the generated changeset file with your PR.
|
|
123
|
+
|
|
124
|
+
**Releasing:**
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pnpm changeset:version # consume changesets, bump version, update CHANGELOG.md
|
|
128
|
+
git add -A && git commit -m "release v<version>"
|
|
129
|
+
git tag v<version>
|
|
130
|
+
git push && git push --tags
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The GitHub Action will create a release with the changelog content and publish to npm.
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
+
import { ValidationError } from 'payload';
|
|
1
2
|
import { makeScheduleOwnerAccess } from '../utilities/ownerAccess.js';
|
|
3
|
+
const TIME_REGEX = /^(?:[01]\d|2[0-3]):[0-5]\d$/;
|
|
4
|
+
const validateTime = (value)=>{
|
|
5
|
+
if (!value) {
|
|
6
|
+
return true;
|
|
7
|
+
} // required handles emptiness
|
|
8
|
+
return TIME_REGEX.test(value) || 'Invalid time format. Use HH:mm (e.g., 09:00, 17:30)';
|
|
9
|
+
};
|
|
2
10
|
export function createSchedulesCollection(config) {
|
|
3
11
|
const rom = config.resourceOwnerMode;
|
|
4
12
|
const access = config.access.schedules ?? (rom ? makeScheduleOwnerAccess(rom) : {});
|
|
@@ -89,7 +97,8 @@ export function createSchedulesCollection(config) {
|
|
|
89
97
|
placeholder: '09:00'
|
|
90
98
|
},
|
|
91
99
|
label: ({ t })=>t('reservation:fieldStartTimeHHmm'),
|
|
92
|
-
required: true
|
|
100
|
+
required: true,
|
|
101
|
+
validate: validateTime
|
|
93
102
|
},
|
|
94
103
|
{
|
|
95
104
|
name: 'endTime',
|
|
@@ -98,7 +107,8 @@ export function createSchedulesCollection(config) {
|
|
|
98
107
|
placeholder: '17:00'
|
|
99
108
|
},
|
|
100
109
|
label: ({ t })=>t('reservation:fieldEndTimeHHmm'),
|
|
101
|
-
required: true
|
|
110
|
+
required: true,
|
|
111
|
+
validate: validateTime
|
|
102
112
|
}
|
|
103
113
|
],
|
|
104
114
|
label: ({ t })=>t('reservation:fieldRecurringSlots')
|
|
@@ -128,7 +138,8 @@ export function createSchedulesCollection(config) {
|
|
|
128
138
|
placeholder: '09:00'
|
|
129
139
|
},
|
|
130
140
|
label: ({ t })=>t('reservation:fieldStartTimeHHmm'),
|
|
131
|
-
required: true
|
|
141
|
+
required: true,
|
|
142
|
+
validate: validateTime
|
|
132
143
|
},
|
|
133
144
|
{
|
|
134
145
|
name: 'endTime',
|
|
@@ -137,7 +148,8 @@ export function createSchedulesCollection(config) {
|
|
|
137
148
|
placeholder: '17:00'
|
|
138
149
|
},
|
|
139
150
|
label: ({ t })=>t('reservation:fieldEndTimeHHmm'),
|
|
140
|
-
required: true
|
|
151
|
+
required: true,
|
|
152
|
+
validate: validateTime
|
|
141
153
|
}
|
|
142
154
|
],
|
|
143
155
|
label: ({ t })=>t('reservation:fieldManualSlots')
|
|
@@ -175,6 +187,39 @@ export function createSchedulesCollection(config) {
|
|
|
175
187
|
label: ({ t })=>t('reservation:fieldActive')
|
|
176
188
|
}
|
|
177
189
|
],
|
|
190
|
+
hooks: {
|
|
191
|
+
beforeValidate: [
|
|
192
|
+
({ data })=>{
|
|
193
|
+
const slots = data?.recurringSlots ?? [];
|
|
194
|
+
for (const slot of slots){
|
|
195
|
+
if (slot.startTime && slot.endTime && slot.startTime >= slot.endTime) {
|
|
196
|
+
throw new ValidationError({
|
|
197
|
+
errors: [
|
|
198
|
+
{
|
|
199
|
+
message: 'endTime must be after startTime',
|
|
200
|
+
path: 'recurringSlots'
|
|
201
|
+
}
|
|
202
|
+
]
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const manual = data?.manualSlots ?? [];
|
|
207
|
+
for (const slot of manual){
|
|
208
|
+
if (slot.startTime && slot.endTime && slot.startTime >= slot.endTime) {
|
|
209
|
+
throw new ValidationError({
|
|
210
|
+
errors: [
|
|
211
|
+
{
|
|
212
|
+
message: 'endTime must be after startTime',
|
|
213
|
+
path: 'manualSlots'
|
|
214
|
+
}
|
|
215
|
+
]
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return data;
|
|
220
|
+
}
|
|
221
|
+
]
|
|
222
|
+
},
|
|
178
223
|
labels: {
|
|
179
224
|
plural: ({ t })=>t('reservation:collectionSchedules'),
|
|
180
225
|
singular: ({ t })=>t('reservation:collectionSchedules')
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/collections/Schedules.ts"],"sourcesContent":["import type { CollectionConfig, CollectionSlug } from 'payload'\n\nimport type { PluginT } from '../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { makeScheduleOwnerAccess } from '../utilities/ownerAccess.js'\n\nexport function createSchedulesCollection(\n config: ResolvedReservationPluginConfig,\n): CollectionConfig {\n const rom = config.resourceOwnerMode\n const access =\n config.access.schedules ?? (rom ? makeScheduleOwnerAccess(rom) : {})\n\n return {\n slug: config.slugs.schedules,\n access,\n admin: {\n group: config.adminGroup,\n useAsTitle: 'name',\n },\n fields: [\n {\n name: 'name',\n type: 'text',\n label: ({ t }) => (t as PluginT)('reservation:fieldName'),\n required: true,\n },\n {\n name: 'resource',\n type: 'relationship',\n label: ({ t }) => (t as PluginT)('reservation:fieldResource'),\n relationTo: config.slugs.resources as unknown as CollectionSlug,\n required: true,\n },\n {\n name: 'scheduleType',\n type: 'select',\n defaultValue: 'recurring',\n label: ({ t }) => (t as PluginT)('reservation:fieldScheduleType'),\n options: [\n {\n label: ({ t }) => (t as PluginT)('reservation:scheduleTypeRecurring'),\n value: 'recurring',\n },\n {\n label: ({ t }) => (t as PluginT)('reservation:scheduleTypeManual'),\n value: 'manual',\n },\n ],\n },\n {\n name: 'recurringSlots',\n type: 'array',\n admin: {\n condition: (_, siblingData) => siblingData?.scheduleType === 'recurring',\n },\n fields: [\n {\n name: 'day',\n type: 'select',\n label: ({ t }) => (t as PluginT)('reservation:fieldDay'),\n options: [\n { label: ({ t }) => (t as PluginT)('reservation:dayMonday'), value: 'mon' },\n { label: ({ t }) => (t as PluginT)('reservation:dayTuesday'), value: 'tue' },\n { label: ({ t }) => (t as PluginT)('reservation:dayWednesday'), value: 'wed' },\n { label: ({ t }) => (t as PluginT)('reservation:dayThursday'), value: 'thu' },\n { label: ({ t }) => (t as PluginT)('reservation:dayFriday'), value: 'fri' },\n { label: ({ t }) => (t as PluginT)('reservation:daySaturday'), value: 'sat' },\n { label: ({ t }) => (t as PluginT)('reservation:daySunday'), value: 'sun' },\n ],\n required: true,\n },\n {\n name: 'startTime',\n type: 'text',\n admin: {\n placeholder: '09:00',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldStartTimeHHmm'),\n required: true,\n },\n {\n name: 'endTime',\n type: 'text',\n admin: {\n placeholder: '17:00',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldEndTimeHHmm'),\n required: true,\n },\n ],\n label: ({ t }) => (t as PluginT)('reservation:fieldRecurringSlots'),\n },\n {\n name: 'manualSlots',\n type: 'array',\n admin: {\n condition: (_, siblingData) => siblingData?.scheduleType === 'manual',\n },\n fields: [\n {\n name: 'date',\n type: 'date',\n admin: {\n date: {\n pickerAppearance: 'dayOnly',\n },\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldDate'),\n required: true,\n },\n {\n name: 'startTime',\n type: 'text',\n admin: {\n placeholder: '09:00',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldStartTimeHHmm'),\n required: true,\n },\n {\n name: 'endTime',\n type: 'text',\n admin: {\n placeholder: '17:00',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldEndTimeHHmm'),\n required: true,\n },\n ],\n label: ({ t }) => (t as PluginT)('reservation:fieldManualSlots'),\n },\n {\n name: 'exceptions',\n type: 'array',\n fields: [\n {\n name: 'date',\n type: 'date',\n admin: {\n date: {\n pickerAppearance: 'dayOnly',\n },\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldDate'),\n required: true,\n },\n {\n name: 'reason',\n type: 'text',\n label: ({ t }) => (t as PluginT)('reservation:fieldReason'),\n },\n ],\n label: ({ t }) => (t as PluginT)('reservation:fieldExceptions'),\n },\n {\n name: 'active',\n type: 'checkbox',\n admin: {\n position: 'sidebar',\n },\n defaultValue: true,\n label: ({ t }) => (t as PluginT)('reservation:fieldActive'),\n },\n ],\n labels: {\n plural: ({ t }) => (t as PluginT)('reservation:collectionSchedules'),\n singular: ({ t }) => (t as PluginT)('reservation:collectionSchedules'),\n },\n }\n}\n"],"names":["makeScheduleOwnerAccess","createSchedulesCollection","config","rom","resourceOwnerMode","access","schedules","slug","slugs","admin","group","adminGroup","useAsTitle","fields","name","type","label","t","required","relationTo","resources","defaultValue","options","value","condition","_","siblingData","scheduleType","placeholder","date","pickerAppearance","position","labels","plural","singular"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/collections/Schedules.ts"],"sourcesContent":["import type { CollectionConfig, CollectionSlug } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { makeScheduleOwnerAccess } from '../utilities/ownerAccess.js'\n\nconst TIME_REGEX = /^(?:[01]\\d|2[0-3]):[0-5]\\d$/\n\nconst validateTime = (value: null | string | undefined): string | true => {\n if (!value) { return true } // required handles emptiness\n return TIME_REGEX.test(value) || 'Invalid time format. Use HH:mm (e.g., 09:00, 17:30)'\n}\n\nexport function createSchedulesCollection(\n config: ResolvedReservationPluginConfig,\n): CollectionConfig {\n const rom = config.resourceOwnerMode\n const access =\n config.access.schedules ?? (rom ? makeScheduleOwnerAccess(rom) : {})\n\n return {\n slug: config.slugs.schedules,\n access,\n admin: {\n group: config.adminGroup,\n useAsTitle: 'name',\n },\n fields: [\n {\n name: 'name',\n type: 'text',\n label: ({ t }) => (t as PluginT)('reservation:fieldName'),\n required: true,\n },\n {\n name: 'resource',\n type: 'relationship',\n label: ({ t }) => (t as PluginT)('reservation:fieldResource'),\n relationTo: config.slugs.resources as unknown as CollectionSlug,\n required: true,\n },\n {\n name: 'scheduleType',\n type: 'select',\n defaultValue: 'recurring',\n label: ({ t }) => (t as PluginT)('reservation:fieldScheduleType'),\n options: [\n {\n label: ({ t }) => (t as PluginT)('reservation:scheduleTypeRecurring'),\n value: 'recurring',\n },\n {\n label: ({ t }) => (t as PluginT)('reservation:scheduleTypeManual'),\n value: 'manual',\n },\n ],\n },\n {\n name: 'recurringSlots',\n type: 'array',\n admin: {\n condition: (_, siblingData) => siblingData?.scheduleType === 'recurring',\n },\n fields: [\n {\n name: 'day',\n type: 'select',\n label: ({ t }) => (t as PluginT)('reservation:fieldDay'),\n options: [\n { label: ({ t }) => (t as PluginT)('reservation:dayMonday'), value: 'mon' },\n { label: ({ t }) => (t as PluginT)('reservation:dayTuesday'), value: 'tue' },\n { label: ({ t }) => (t as PluginT)('reservation:dayWednesday'), value: 'wed' },\n { label: ({ t }) => (t as PluginT)('reservation:dayThursday'), value: 'thu' },\n { label: ({ t }) => (t as PluginT)('reservation:dayFriday'), value: 'fri' },\n { label: ({ t }) => (t as PluginT)('reservation:daySaturday'), value: 'sat' },\n { label: ({ t }) => (t as PluginT)('reservation:daySunday'), value: 'sun' },\n ],\n required: true,\n },\n {\n name: 'startTime',\n type: 'text',\n admin: {\n placeholder: '09:00',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldStartTimeHHmm'),\n required: true,\n validate: validateTime,\n },\n {\n name: 'endTime',\n type: 'text',\n admin: {\n placeholder: '17:00',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldEndTimeHHmm'),\n required: true,\n validate: validateTime,\n },\n ],\n label: ({ t }) => (t as PluginT)('reservation:fieldRecurringSlots'),\n },\n {\n name: 'manualSlots',\n type: 'array',\n admin: {\n condition: (_, siblingData) => siblingData?.scheduleType === 'manual',\n },\n fields: [\n {\n name: 'date',\n type: 'date',\n admin: {\n date: {\n pickerAppearance: 'dayOnly',\n },\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldDate'),\n required: true,\n },\n {\n name: 'startTime',\n type: 'text',\n admin: {\n placeholder: '09:00',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldStartTimeHHmm'),\n required: true,\n validate: validateTime,\n },\n {\n name: 'endTime',\n type: 'text',\n admin: {\n placeholder: '17:00',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldEndTimeHHmm'),\n required: true,\n validate: validateTime,\n },\n ],\n label: ({ t }) => (t as PluginT)('reservation:fieldManualSlots'),\n },\n {\n name: 'exceptions',\n type: 'array',\n fields: [\n {\n name: 'date',\n type: 'date',\n admin: {\n date: {\n pickerAppearance: 'dayOnly',\n },\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldDate'),\n required: true,\n },\n {\n name: 'reason',\n type: 'text',\n label: ({ t }) => (t as PluginT)('reservation:fieldReason'),\n },\n ],\n label: ({ t }) => (t as PluginT)('reservation:fieldExceptions'),\n },\n {\n name: 'active',\n type: 'checkbox',\n admin: {\n position: 'sidebar',\n },\n defaultValue: true,\n label: ({ t }) => (t as PluginT)('reservation:fieldActive'),\n },\n ],\n hooks: {\n beforeValidate: [\n ({ data }) => {\n const slots = (data?.recurringSlots as Array<{ endTime?: string; startTime?: string }>) ?? []\n for (const slot of slots) {\n if (slot.startTime && slot.endTime && slot.startTime >= slot.endTime) {\n throw new ValidationError({\n errors: [{ message: 'endTime must be after startTime', path: 'recurringSlots' }],\n })\n }\n }\n const manual = (data?.manualSlots as Array<{ endTime?: string; startTime?: string }>) ?? []\n for (const slot of manual) {\n if (slot.startTime && slot.endTime && slot.startTime >= slot.endTime) {\n throw new ValidationError({\n errors: [{ message: 'endTime must be after startTime', path: 'manualSlots' }],\n })\n }\n }\n return data\n },\n ],\n },\n labels: {\n plural: ({ t }) => (t as PluginT)('reservation:collectionSchedules'),\n singular: ({ t }) => (t as PluginT)('reservation:collectionSchedules'),\n },\n }\n}\n"],"names":["ValidationError","makeScheduleOwnerAccess","TIME_REGEX","validateTime","value","test","createSchedulesCollection","config","rom","resourceOwnerMode","access","schedules","slug","slugs","admin","group","adminGroup","useAsTitle","fields","name","type","label","t","required","relationTo","resources","defaultValue","options","condition","_","siblingData","scheduleType","placeholder","validate","date","pickerAppearance","position","hooks","beforeValidate","data","slots","recurringSlots","slot","startTime","endTime","errors","message","path","manual","manualSlots","labels","plural","singular"],"mappings":"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"}
|
package/dist/defaults.js
CHANGED
|
@@ -1,4 +1,29 @@
|
|
|
1
1
|
import { DEFAULT_STATUS_MACHINE } from './types.js';
|
|
2
|
+
function validateStatusMachine(sm) {
|
|
3
|
+
if (!sm.statuses.includes(sm.defaultStatus)) {
|
|
4
|
+
throw new Error(`statusMachine.defaultStatus "${sm.defaultStatus}" is not in statuses array`);
|
|
5
|
+
}
|
|
6
|
+
for (const s of sm.blockingStatuses){
|
|
7
|
+
if (!sm.statuses.includes(s)) {
|
|
8
|
+
throw new Error(`statusMachine.blockingStatuses contains "${s}" which is not in statuses array`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
for (const s of sm.terminalStatuses){
|
|
12
|
+
if (!sm.statuses.includes(s)) {
|
|
13
|
+
throw new Error(`statusMachine.terminalStatuses contains "${s}" which is not in statuses array`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
for (const [from, targets] of Object.entries(sm.transitions)){
|
|
17
|
+
if (!sm.statuses.includes(from)) {
|
|
18
|
+
throw new Error(`statusMachine.transitions has key "${from}" which is not in statuses array`);
|
|
19
|
+
}
|
|
20
|
+
for (const to of targets){
|
|
21
|
+
if (!sm.statuses.includes(to)) {
|
|
22
|
+
throw new Error(`statusMachine.transitions["${from}"] targets "${to}" which is not in statuses array`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
2
27
|
export const DEFAULT_SLUGS = {
|
|
3
28
|
customers: 'customers',
|
|
4
29
|
media: 'media',
|
|
@@ -13,7 +38,7 @@ export const DEFAULT_CANCELLATION_NOTICE_PERIOD = 24;
|
|
|
13
38
|
export function resolveConfig(pluginOptions) {
|
|
14
39
|
const userStatusMachine = pluginOptions.statusMachine;
|
|
15
40
|
const rom = pluginOptions.resourceOwnerMode;
|
|
16
|
-
|
|
41
|
+
const resolved = {
|
|
17
42
|
access: pluginOptions.access ?? {},
|
|
18
43
|
adminGroup: pluginOptions.adminGroup ?? DEFAULT_ADMIN_GROUP,
|
|
19
44
|
cancellationNoticePeriod: pluginOptions.cancellationNoticePeriod ?? DEFAULT_CANCELLATION_NOTICE_PERIOD,
|
|
@@ -46,6 +71,8 @@ export function resolveConfig(pluginOptions) {
|
|
|
46
71
|
},
|
|
47
72
|
userCollection: pluginOptions.userCollection ?? undefined
|
|
48
73
|
};
|
|
74
|
+
validateStatusMachine(resolved.statusMachine);
|
|
75
|
+
return resolved;
|
|
49
76
|
}
|
|
50
77
|
|
|
51
78
|
//# sourceMappingURL=defaults.js.map
|
package/dist/defaults.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/defaults.ts"],"sourcesContent":["import type { ReservationPluginConfig, ResolvedReservationPluginConfig } from './types.js'\n\nimport { DEFAULT_STATUS_MACHINE } from './types.js'\n\nexport const DEFAULT_SLUGS = {\n customers: 'customers',\n media: 'media',\n reservations: 'reservations',\n resources: 'resources',\n schedules: 'schedules',\n services: 'services',\n} as const\n\nexport const DEFAULT_ADMIN_GROUP = 'Reservations'\nexport const DEFAULT_BUFFER_TIME = 0\nexport const DEFAULT_CANCELLATION_NOTICE_PERIOD = 24\n\nexport function resolveConfig(\n pluginOptions: ReservationPluginConfig,\n): ResolvedReservationPluginConfig {\n const userStatusMachine = pluginOptions.statusMachine\n const rom = pluginOptions.resourceOwnerMode\n
|
|
1
|
+
{"version":3,"sources":["../src/defaults.ts"],"sourcesContent":["import type { ReservationPluginConfig, ResolvedReservationPluginConfig, StatusMachineConfig } from './types.js'\n\nimport { DEFAULT_STATUS_MACHINE } from './types.js'\n\nfunction validateStatusMachine(sm: StatusMachineConfig): void {\n if (!sm.statuses.includes(sm.defaultStatus)) {\n throw new Error(`statusMachine.defaultStatus \"${sm.defaultStatus}\" is not in statuses array`)\n }\n for (const s of sm.blockingStatuses) {\n if (!sm.statuses.includes(s)) {\n throw new Error(`statusMachine.blockingStatuses contains \"${s}\" which is not in statuses array`)\n }\n }\n for (const s of sm.terminalStatuses) {\n if (!sm.statuses.includes(s)) {\n throw new Error(`statusMachine.terminalStatuses contains \"${s}\" which is not in statuses array`)\n }\n }\n for (const [from, targets] of Object.entries(sm.transitions)) {\n if (!sm.statuses.includes(from)) {\n throw new Error(`statusMachine.transitions has key \"${from}\" which is not in statuses array`)\n }\n for (const to of targets) {\n if (!sm.statuses.includes(to)) {\n throw new Error(`statusMachine.transitions[\"${from}\"] targets \"${to}\" which is not in statuses array`)\n }\n }\n }\n}\n\nexport const DEFAULT_SLUGS = {\n customers: 'customers',\n media: 'media',\n reservations: 'reservations',\n resources: 'resources',\n schedules: 'schedules',\n services: 'services',\n} as const\n\nexport const DEFAULT_ADMIN_GROUP = 'Reservations'\nexport const DEFAULT_BUFFER_TIME = 0\nexport const DEFAULT_CANCELLATION_NOTICE_PERIOD = 24\n\nexport function resolveConfig(\n pluginOptions: ReservationPluginConfig,\n): ResolvedReservationPluginConfig {\n const userStatusMachine = pluginOptions.statusMachine\n const rom = pluginOptions.resourceOwnerMode\n const resolved: ResolvedReservationPluginConfig = {\n access: pluginOptions.access ?? {},\n adminGroup: pluginOptions.adminGroup ?? DEFAULT_ADMIN_GROUP,\n cancellationNoticePeriod:\n pluginOptions.cancellationNoticePeriod ?? DEFAULT_CANCELLATION_NOTICE_PERIOD,\n defaultBufferTime: pluginOptions.defaultBufferTime ?? DEFAULT_BUFFER_TIME,\n disabled: pluginOptions.disabled ?? false,\n extraReservationFields: pluginOptions.extraReservationFields ?? [],\n hooks: pluginOptions.hooks ?? {},\n localized: false,\n resourceOwnerMode: rom\n ? {\n adminRoles: rom.adminRoles ?? [],\n ownedServices: rom.ownedServices ?? false,\n ownerField: rom.ownerField ?? 'owner',\n }\n : undefined,\n slugs: {\n customers: pluginOptions.slugs?.customers ?? DEFAULT_SLUGS.customers,\n media: pluginOptions.slugs?.media ?? DEFAULT_SLUGS.media,\n reservations: pluginOptions.slugs?.reservations ?? DEFAULT_SLUGS.reservations,\n resources: pluginOptions.slugs?.resources ?? DEFAULT_SLUGS.resources,\n schedules: pluginOptions.slugs?.schedules ?? DEFAULT_SLUGS.schedules,\n services: pluginOptions.slugs?.services ?? DEFAULT_SLUGS.services,\n },\n statusMachine: userStatusMachine\n ? {\n blockingStatuses:\n userStatusMachine.blockingStatuses ?? DEFAULT_STATUS_MACHINE.blockingStatuses,\n defaultStatus: userStatusMachine.defaultStatus ?? DEFAULT_STATUS_MACHINE.defaultStatus,\n statuses: userStatusMachine.statuses ?? DEFAULT_STATUS_MACHINE.statuses,\n terminalStatuses:\n userStatusMachine.terminalStatuses ?? DEFAULT_STATUS_MACHINE.terminalStatuses,\n transitions: userStatusMachine.transitions ?? DEFAULT_STATUS_MACHINE.transitions,\n }\n : { ...DEFAULT_STATUS_MACHINE },\n userCollection: pluginOptions.userCollection ?? undefined,\n }\n\n validateStatusMachine(resolved.statusMachine)\n\n return resolved\n}\n"],"names":["DEFAULT_STATUS_MACHINE","validateStatusMachine","sm","statuses","includes","defaultStatus","Error","s","blockingStatuses","terminalStatuses","from","targets","Object","entries","transitions","to","DEFAULT_SLUGS","customers","media","reservations","resources","schedules","services","DEFAULT_ADMIN_GROUP","DEFAULT_BUFFER_TIME","DEFAULT_CANCELLATION_NOTICE_PERIOD","resolveConfig","pluginOptions","userStatusMachine","statusMachine","rom","resourceOwnerMode","resolved","access","adminGroup","cancellationNoticePeriod","defaultBufferTime","disabled","extraReservationFields","hooks","localized","adminRoles","ownedServices","ownerField","undefined","slugs","userCollection"],"mappings":"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"}
|
|
@@ -17,6 +17,26 @@ export function createCancelBookingEndpoint(config) {
|
|
|
17
17
|
status: 400
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
|
+
// Fetch the reservation to check ownership
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
+
const existing = await req.payload.findByID({
|
|
23
|
+
id: reservationId,
|
|
24
|
+
collection: config.slugs.reservations,
|
|
25
|
+
depth: 0,
|
|
26
|
+
req
|
|
27
|
+
});
|
|
28
|
+
// Check ownership: customer must match req.user
|
|
29
|
+
const customerId = typeof existing.customer === 'object' ? existing.customer?.id : existing.customer;
|
|
30
|
+
const isOwner = customerId === req.user.id;
|
|
31
|
+
// Admin = user from non-customer collection
|
|
32
|
+
const isAdmin = req.user.collection !== config.slugs.customers;
|
|
33
|
+
if (!isOwner && !isAdmin) {
|
|
34
|
+
return Response.json({
|
|
35
|
+
message: 'Forbidden'
|
|
36
|
+
}, {
|
|
37
|
+
status: 403
|
|
38
|
+
});
|
|
39
|
+
}
|
|
20
40
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
41
|
const reservation = await req.payload.update({
|
|
22
42
|
id: reservationId,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/endpoints/cancelBooking.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nexport function createCancelBookingEndpoint(config: ResolvedReservationPluginConfig): Endpoint {\n return {\n handler: async (req) => {\n if (!req.user) {\n return Response.json({ message: 'Unauthorized' }, { status: 401 })\n }\n\n const body = await req.json?.()\n const { reason, reservationId } = (body ?? {}) as {\n reason?: string\n reservationId?: string\n }\n\n if (!reservationId) {\n return Response.json({ message: 'reservationId is required' }, { status: 400 })\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const reservation = await (req.payload.update as any)({\n id: reservationId,\n collection: config.slugs.reservations,\n data: {\n cancellationReason: reason,\n status: 'cancelled',\n },\n req,\n })\n\n return Response.json(reservation)\n },\n method: 'post',\n path: '/reserve/cancel',\n }\n}\n"],"names":["createCancelBookingEndpoint","config","handler","req","user","Response","json","message","status","body","reason","reservationId","
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/cancelBooking.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nexport function createCancelBookingEndpoint(config: ResolvedReservationPluginConfig): Endpoint {\n return {\n handler: async (req) => {\n if (!req.user) {\n return Response.json({ message: 'Unauthorized' }, { status: 401 })\n }\n\n const body = await req.json?.()\n const { reason, reservationId } = (body ?? {}) as {\n reason?: string\n reservationId?: string\n }\n\n if (!reservationId) {\n return Response.json({ message: 'reservationId is required' }, { status: 400 })\n }\n\n // Fetch the reservation to check ownership\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const existing = await (req.payload.findByID as any)({\n id: reservationId,\n collection: config.slugs.reservations,\n depth: 0,\n req,\n })\n\n // Check ownership: customer must match req.user\n const customerId =\n typeof existing.customer === 'object' ? existing.customer?.id : existing.customer\n const isOwner = customerId === req.user.id\n // Admin = user from non-customer collection\n const isAdmin = req.user.collection !== config.slugs.customers\n\n if (!isOwner && !isAdmin) {\n return Response.json({ message: 'Forbidden' }, { status: 403 })\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const reservation = await (req.payload.update as any)({\n id: reservationId,\n collection: config.slugs.reservations,\n data: {\n cancellationReason: reason,\n status: 'cancelled',\n },\n req,\n })\n\n return Response.json(reservation)\n },\n method: 'post',\n path: '/reserve/cancel',\n }\n}\n"],"names":["createCancelBookingEndpoint","config","handler","req","user","Response","json","message","status","body","reason","reservationId","existing","payload","findByID","id","collection","slugs","reservations","depth","customerId","customer","isOwner","isAdmin","customers","reservation","update","data","cancellationReason","method","path"],"mappings":"AAIA,OAAO,SAASA,4BAA4BC,MAAuC;IACjF,OAAO;QACLC,SAAS,OAAOC;YACd,IAAI,CAACA,IAAIC,IAAI,EAAE;gBACb,OAAOC,SAASC,IAAI,CAAC;oBAAEC,SAAS;gBAAe,GAAG;oBAAEC,QAAQ;gBAAI;YAClE;YAEA,MAAMC,OAAO,MAAMN,IAAIG,IAAI;YAC3B,MAAM,EAAEI,MAAM,EAAEC,aAAa,EAAE,GAAIF,QAAQ,CAAC;YAK5C,IAAI,CAACE,eAAe;gBAClB,OAAON,SAASC,IAAI,CAAC;oBAAEC,SAAS;gBAA4B,GAAG;oBAAEC,QAAQ;gBAAI;YAC/E;YAEA,2CAA2C;YAC3C,8DAA8D;YAC9D,MAAMI,WAAW,MAAM,AAACT,IAAIU,OAAO,CAACC,QAAQ,CAAS;gBACnDC,IAAIJ;gBACJK,YAAYf,OAAOgB,KAAK,CAACC,YAAY;gBACrCC,OAAO;gBACPhB;YACF;YAEA,gDAAgD;YAChD,MAAMiB,aACJ,OAAOR,SAASS,QAAQ,KAAK,WAAWT,SAASS,QAAQ,EAAEN,KAAKH,SAASS,QAAQ;YACnF,MAAMC,UAAUF,eAAejB,IAAIC,IAAI,CAACW,EAAE;YAC1C,4CAA4C;YAC5C,MAAMQ,UAAUpB,IAAIC,IAAI,CAACY,UAAU,KAAKf,OAAOgB,KAAK,CAACO,SAAS;YAE9D,IAAI,CAACF,WAAW,CAACC,SAAS;gBACxB,OAAOlB,SAASC,IAAI,CAAC;oBAAEC,SAAS;gBAAY,GAAG;oBAAEC,QAAQ;gBAAI;YAC/D;YAEA,8DAA8D;YAC9D,MAAMiB,cAAc,MAAM,AAACtB,IAAIU,OAAO,CAACa,MAAM,CAAS;gBACpDX,IAAIJ;gBACJK,YAAYf,OAAOgB,KAAK,CAACC,YAAY;gBACrCS,MAAM;oBACJC,oBAAoBlB;oBACpBF,QAAQ;gBACV;gBACAL;YACF;YAEA,OAAOE,SAASC,IAAI,CAACmB;QACvB;QACAI,QAAQ;QACRC,MAAM;IACR;AACF"}
|
|
@@ -13,9 +13,19 @@ export function createCheckAvailabilityEndpoint(config) {
|
|
|
13
13
|
status: 400
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
|
+
const parsedDate = new Date(date);
|
|
17
|
+
if (isNaN(parsedDate.getTime())) {
|
|
18
|
+
return Response.json({
|
|
19
|
+
error: 'Invalid date format. Expected YYYY-MM-DD'
|
|
20
|
+
}, {
|
|
21
|
+
status: 400
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
const guestCount = Math.max(Number(url.searchParams.get('guestCount') ?? '1'), 1);
|
|
16
25
|
const slots = await getAvailableSlots({
|
|
17
26
|
blockingStatuses: config.statusMachine.blockingStatuses,
|
|
18
|
-
date:
|
|
27
|
+
date: parsedDate,
|
|
28
|
+
guestCount,
|
|
19
29
|
payload: req.payload,
|
|
20
30
|
req,
|
|
21
31
|
reservationSlug: config.slugs.reservations,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/endpoints/checkAvailability.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { getAvailableSlots } from '../services/AvailabilityService.js'\n\nexport function createCheckAvailabilityEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n const url = new URL(req.url!)\n const date = url.searchParams.get('date')\n const resource = url.searchParams.get('resource')\n const service = url.searchParams.get('service')\n\n if (!date || !resource || !service) {\n return Response.json(\n { message: 'Missing required query params: resource, date, service' },\n { status: 400 },\n )\n }\n\n const slots = await getAvailableSlots({\n blockingStatuses: config.statusMachine.blockingStatuses,\n date:
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/checkAvailability.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { getAvailableSlots } from '../services/AvailabilityService.js'\n\nexport function createCheckAvailabilityEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n const url = new URL(req.url!)\n const date = url.searchParams.get('date')\n const resource = url.searchParams.get('resource')\n const service = url.searchParams.get('service')\n\n if (!date || !resource || !service) {\n return Response.json(\n { message: 'Missing required query params: resource, date, service' },\n { status: 400 },\n )\n }\n\n const parsedDate = new Date(date)\n if (isNaN(parsedDate.getTime())) {\n return Response.json(\n { error: 'Invalid date format. Expected YYYY-MM-DD' },\n { status: 400 },\n )\n }\n\n const guestCount = Math.max(Number(url.searchParams.get('guestCount') ?? '1'), 1)\n\n const slots = await getAvailableSlots({\n blockingStatuses: config.statusMachine.blockingStatuses,\n date: parsedDate,\n guestCount,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceId: resource,\n resourceSlug: config.slugs.resources,\n scheduleSlug: config.slugs.schedules,\n serviceId: service,\n serviceSlug: config.slugs.services,\n })\n\n return Response.json({ slots })\n },\n method: 'get',\n path: '/reserve/availability',\n }\n}\n"],"names":["getAvailableSlots","createCheckAvailabilityEndpoint","config","handler","req","url","URL","date","searchParams","get","resource","service","Response","json","message","status","parsedDate","Date","isNaN","getTime","error","guestCount","Math","max","Number","slots","blockingStatuses","statusMachine","payload","reservationSlug","slugs","reservations","resourceId","resourceSlug","resources","scheduleSlug","schedules","serviceId","serviceSlug","services","method","path"],"mappings":"AAIA,SAASA,iBAAiB,QAAQ,qCAAoC;AAEtE,OAAO,SAASC,gCACdC,MAAuC;IAEvC,OAAO;QACLC,SAAS,OAAOC;YACd,MAAMC,MAAM,IAAIC,IAAIF,IAAIC,GAAG;YAC3B,MAAME,OAAOF,IAAIG,YAAY,CAACC,GAAG,CAAC;YAClC,MAAMC,WAAWL,IAAIG,YAAY,CAACC,GAAG,CAAC;YACtC,MAAME,UAAUN,IAAIG,YAAY,CAACC,GAAG,CAAC;YAErC,IAAI,CAACF,QAAQ,CAACG,YAAY,CAACC,SAAS;gBAClC,OAAOC,SAASC,IAAI,CAClB;oBAAEC,SAAS;gBAAyD,GACpE;oBAAEC,QAAQ;gBAAI;YAElB;YAEA,MAAMC,aAAa,IAAIC,KAAKV;YAC5B,IAAIW,MAAMF,WAAWG,OAAO,KAAK;gBAC/B,OAAOP,SAASC,IAAI,CAClB;oBAAEO,OAAO;gBAA2C,GACpD;oBAAEL,QAAQ;gBAAI;YAElB;YAEA,MAAMM,aAAaC,KAAKC,GAAG,CAACC,OAAOnB,IAAIG,YAAY,CAACC,GAAG,CAAC,iBAAiB,MAAM;YAE/E,MAAMgB,QAAQ,MAAMzB,kBAAkB;gBACpC0B,kBAAkBxB,OAAOyB,aAAa,CAACD,gBAAgB;gBACvDnB,MAAMS;gBACNK;gBACAO,SAASxB,IAAIwB,OAAO;gBACpBxB;gBACAyB,iBAAiB3B,OAAO4B,KAAK,CAACC,YAAY;gBAC1CC,YAAYtB;gBACZuB,cAAc/B,OAAO4B,KAAK,CAACI,SAAS;gBACpCC,cAAcjC,OAAO4B,KAAK,CAACM,SAAS;gBACpCC,WAAW1B;gBACX2B,aAAapC,OAAO4B,KAAK,CAACS,QAAQ;YACpC;YAEA,OAAO3B,SAASC,IAAI,CAAC;gBAAEY;YAAM;QAC/B;QACAe,QAAQ;QACRC,MAAM;IACR;AACF"}
|
|
@@ -21,6 +21,14 @@ export function createCustomerSearchEndpoint(config) {
|
|
|
21
21
|
status: 401
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
|
+
// Only allow staff/admin users (non-customer collection) to search customers
|
|
25
|
+
if (req.user.collection === config.slugs.customers) {
|
|
26
|
+
return Response.json({
|
|
27
|
+
message: 'Forbidden'
|
|
28
|
+
}, {
|
|
29
|
+
status: 403
|
|
30
|
+
});
|
|
31
|
+
}
|
|
24
32
|
const url = new URL(req.url);
|
|
25
33
|
const search = url.searchParams.get('search') ?? '';
|
|
26
34
|
const limit = Math.min(Number(url.searchParams.get('limit') ?? '10'), 50);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/endpoints/customerSearch.ts"],"sourcesContent":["import type { CollectionSlug, Endpoint, Field, Where } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\n/**\n * Inspect a collection's field list and return the set of top-level named\n * fields as a plain Set<string>. Unnamed fields (rows, groups without a name,\n * etc.) are skipped.\n */\nfunction getNamedFields(fields: Field[]): Set<string> {\n const names = new Set<string>()\n for (const field of fields) {\n if ('name' in field) {\n names.add(field.name)\n }\n }\n return names\n}\n\nexport function createCustomerSearchEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n if (!req.user) {\n return Response.json({ message: 'Unauthorized' }, { status: 401 })\n }\n\n const url = new URL(req.url!)\n const search = url.searchParams.get('search') ?? ''\n const limit = Math.min(Number(url.searchParams.get('limit') ?? '10'), 50)\n const page = Math.max(Number(url.searchParams.get('page') ?? '1'), 1)\n\n // Detect which fields exist on the target collection at runtime\n const collectionConfig = req.payload.collections[config.slugs.customers as unknown as CollectionSlug]?.config\n const availableFields: Set<string> = collectionConfig\n ? getNamedFields(collectionConfig.fields)\n : new Set()\n\n const hasName = availableFields.has('name')\n const hasFirstName = availableFields.has('firstName')\n const hasLastName = availableFields.has('lastName')\n const hasPhone = availableFields.has('phone')\n\n let where: Where = {}\n\n if (search) {\n const orClauses: Where[] = []\n\n if (hasName) {\n orClauses.push({ name: { contains: search } })\n }\n if (hasFirstName) {\n orClauses.push({ firstName: { contains: search } })\n }\n if (hasLastName) {\n orClauses.push({ lastName: { contains: search } })\n }\n // email is always present on auth collections\n orClauses.push({ email: { contains: search } })\n if (hasPhone) {\n orClauses.push({ phone: { contains: search } })\n }\n\n where = { or: orClauses }\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const result = await (req.payload.find as any)({\n collection: config.slugs.customers,\n limit,\n page,\n where,\n })\n\n return Response.json({\n docs: (result.docs as Record<string, unknown>[]).map((doc) => {\n const entry: Record<string, unknown> = {\n id: doc['id'],\n email: doc['email'] ?? '',\n }\n\n if (hasName) {\n entry['name'] = doc['name'] ?? ''\n }\n if (hasFirstName) {\n entry['firstName'] = doc['firstName'] ?? ''\n }\n if (hasLastName) {\n entry['lastName'] = doc['lastName'] ?? ''\n }\n if (hasPhone) {\n entry['phone'] = doc['phone'] ?? ''\n }\n\n return entry\n }),\n hasNextPage: result.hasNextPage,\n totalDocs: result.totalDocs,\n })\n },\n method: 'get',\n path: '/reservation-customer-search',\n }\n}\n"],"names":["getNamedFields","fields","names","Set","field","add","name","createCustomerSearchEndpoint","config","handler","req","user","Response","json","message","status","url","URL","search","searchParams","get","limit","Math","min","Number","page","max","collectionConfig","payload","collections","
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/customerSearch.ts"],"sourcesContent":["import type { CollectionSlug, Endpoint, Field, Where } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\n/**\n * Inspect a collection's field list and return the set of top-level named\n * fields as a plain Set<string>. Unnamed fields (rows, groups without a name,\n * etc.) are skipped.\n */\nfunction getNamedFields(fields: Field[]): Set<string> {\n const names = new Set<string>()\n for (const field of fields) {\n if ('name' in field) {\n names.add(field.name)\n }\n }\n return names\n}\n\nexport function createCustomerSearchEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n if (!req.user) {\n return Response.json({ message: 'Unauthorized' }, { status: 401 })\n }\n\n // Only allow staff/admin users (non-customer collection) to search customers\n if (req.user.collection === config.slugs.customers) {\n return Response.json({ message: 'Forbidden' }, { status: 403 })\n }\n\n const url = new URL(req.url!)\n const search = url.searchParams.get('search') ?? ''\n const limit = Math.min(Number(url.searchParams.get('limit') ?? '10'), 50)\n const page = Math.max(Number(url.searchParams.get('page') ?? '1'), 1)\n\n // Detect which fields exist on the target collection at runtime\n const collectionConfig = req.payload.collections[config.slugs.customers as unknown as CollectionSlug]?.config\n const availableFields: Set<string> = collectionConfig\n ? getNamedFields(collectionConfig.fields)\n : new Set()\n\n const hasName = availableFields.has('name')\n const hasFirstName = availableFields.has('firstName')\n const hasLastName = availableFields.has('lastName')\n const hasPhone = availableFields.has('phone')\n\n let where: Where = {}\n\n if (search) {\n const orClauses: Where[] = []\n\n if (hasName) {\n orClauses.push({ name: { contains: search } })\n }\n if (hasFirstName) {\n orClauses.push({ firstName: { contains: search } })\n }\n if (hasLastName) {\n orClauses.push({ lastName: { contains: search } })\n }\n // email is always present on auth collections\n orClauses.push({ email: { contains: search } })\n if (hasPhone) {\n orClauses.push({ phone: { contains: search } })\n }\n\n where = { or: orClauses }\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const result = await (req.payload.find as any)({\n collection: config.slugs.customers,\n limit,\n page,\n where,\n })\n\n return Response.json({\n docs: (result.docs as Record<string, unknown>[]).map((doc) => {\n const entry: Record<string, unknown> = {\n id: doc['id'],\n email: doc['email'] ?? '',\n }\n\n if (hasName) {\n entry['name'] = doc['name'] ?? ''\n }\n if (hasFirstName) {\n entry['firstName'] = doc['firstName'] ?? ''\n }\n if (hasLastName) {\n entry['lastName'] = doc['lastName'] ?? ''\n }\n if (hasPhone) {\n entry['phone'] = doc['phone'] ?? ''\n }\n\n return entry\n }),\n hasNextPage: result.hasNextPage,\n totalDocs: result.totalDocs,\n })\n },\n method: 'get',\n path: '/reservation-customer-search',\n }\n}\n"],"names":["getNamedFields","fields","names","Set","field","add","name","createCustomerSearchEndpoint","config","handler","req","user","Response","json","message","status","collection","slugs","customers","url","URL","search","searchParams","get","limit","Math","min","Number","page","max","collectionConfig","payload","collections","availableFields","hasName","has","hasFirstName","hasLastName","hasPhone","where","orClauses","push","contains","firstName","lastName","email","phone","or","result","find","docs","map","doc","entry","id","hasNextPage","totalDocs","method","path"],"mappings":"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"}
|
|
@@ -25,6 +25,7 @@ export function createGetSlotsEndpoint(config) {
|
|
|
25
25
|
const slots = await getAvailableSlots({
|
|
26
26
|
blockingStatuses: config.statusMachine.blockingStatuses,
|
|
27
27
|
date: parsedDate,
|
|
28
|
+
guestCount,
|
|
28
29
|
payload: req.payload,
|
|
29
30
|
req,
|
|
30
31
|
reservationSlug: config.slugs.reservations,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/endpoints/getSlots.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { getAvailableSlots } from '../services/AvailabilityService.js'\n\nexport function createGetSlotsEndpoint(config: ResolvedReservationPluginConfig): Endpoint {\n return {\n handler: async (req) => {\n const url = new URL(req.url!)\n const date = url.searchParams.get('date')\n const resource = url.searchParams.get('resource')\n const service = url.searchParams.get('service')\n\n if (!date || !resource || !service) {\n return Response.json(\n { error: 'Missing required query params: resource, date, service' },\n { status: 400 },\n )\n }\n\n const parsedDate = new Date(date)\n if (isNaN(parsedDate.getTime())) {\n return Response.json(\n { error: 'Invalid date format. Expected YYYY-MM-DD' },\n { status: 400 },\n )\n }\n\n const guestCount = Math.max(Number(url.searchParams.get('guestCount') ?? '1'), 1)\n\n const slots = await getAvailableSlots({\n blockingStatuses: config.statusMachine.blockingStatuses,\n date: parsedDate,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceId: resource,\n resourceSlug: config.slugs.resources,\n scheduleSlug: config.slugs.schedules,\n serviceId: service,\n serviceSlug: config.slugs.services,\n })\n\n return Response.json({\n date,\n guestCount,\n slots: slots.map((s) => ({ end: s.end.toISOString(), start: s.start.toISOString() })),\n })\n },\n method: 'get',\n path: '/reserve/slots',\n }\n}\n"],"names":["getAvailableSlots","createGetSlotsEndpoint","config","handler","req","url","URL","date","searchParams","get","resource","service","Response","json","error","status","parsedDate","Date","isNaN","getTime","guestCount","Math","max","Number","slots","blockingStatuses","statusMachine","payload","reservationSlug","slugs","reservations","resourceId","resourceSlug","resources","scheduleSlug","schedules","serviceId","serviceSlug","services","map","s","end","toISOString","start","method","path"],"mappings":"AAIA,SAASA,iBAAiB,QAAQ,qCAAoC;AAEtE,OAAO,SAASC,uBAAuBC,MAAuC;IAC5E,OAAO;QACLC,SAAS,OAAOC;YACd,MAAMC,MAAM,IAAIC,IAAIF,IAAIC,GAAG;YAC3B,MAAME,OAAOF,IAAIG,YAAY,CAACC,GAAG,CAAC;YAClC,MAAMC,WAAWL,IAAIG,YAAY,CAACC,GAAG,CAAC;YACtC,MAAME,UAAUN,IAAIG,YAAY,CAACC,GAAG,CAAC;YAErC,IAAI,CAACF,QAAQ,CAACG,YAAY,CAACC,SAAS;gBAClC,OAAOC,SAASC,IAAI,CAClB;oBAAEC,OAAO;gBAAyD,GAClE;oBAAEC,QAAQ;gBAAI;YAElB;YAEA,MAAMC,aAAa,IAAIC,KAAKV;YAC5B,IAAIW,MAAMF,WAAWG,OAAO,KAAK;gBAC/B,OAAOP,SAASC,IAAI,CAClB;oBAAEC,OAAO;gBAA2C,GACpD;oBAAEC,QAAQ;gBAAI;YAElB;YAEA,MAAMK,aAAaC,KAAKC,GAAG,CAACC,OAAOlB,IAAIG,YAAY,CAACC,GAAG,CAAC,iBAAiB,MAAM;YAE/E,MAAMe,QAAQ,MAAMxB,kBAAkB;gBACpCyB,kBAAkBvB,OAAOwB,aAAa,CAACD,gBAAgB;gBACvDlB,MAAMS;
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/getSlots.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { getAvailableSlots } from '../services/AvailabilityService.js'\n\nexport function createGetSlotsEndpoint(config: ResolvedReservationPluginConfig): Endpoint {\n return {\n handler: async (req) => {\n const url = new URL(req.url!)\n const date = url.searchParams.get('date')\n const resource = url.searchParams.get('resource')\n const service = url.searchParams.get('service')\n\n if (!date || !resource || !service) {\n return Response.json(\n { error: 'Missing required query params: resource, date, service' },\n { status: 400 },\n )\n }\n\n const parsedDate = new Date(date)\n if (isNaN(parsedDate.getTime())) {\n return Response.json(\n { error: 'Invalid date format. Expected YYYY-MM-DD' },\n { status: 400 },\n )\n }\n\n const guestCount = Math.max(Number(url.searchParams.get('guestCount') ?? '1'), 1)\n\n const slots = await getAvailableSlots({\n blockingStatuses: config.statusMachine.blockingStatuses,\n date: parsedDate,\n guestCount,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceId: resource,\n resourceSlug: config.slugs.resources,\n scheduleSlug: config.slugs.schedules,\n serviceId: service,\n serviceSlug: config.slugs.services,\n })\n\n return Response.json({\n date,\n guestCount,\n slots: slots.map((s) => ({ end: s.end.toISOString(), start: s.start.toISOString() })),\n })\n },\n method: 'get',\n path: '/reserve/slots',\n }\n}\n"],"names":["getAvailableSlots","createGetSlotsEndpoint","config","handler","req","url","URL","date","searchParams","get","resource","service","Response","json","error","status","parsedDate","Date","isNaN","getTime","guestCount","Math","max","Number","slots","blockingStatuses","statusMachine","payload","reservationSlug","slugs","reservations","resourceId","resourceSlug","resources","scheduleSlug","schedules","serviceId","serviceSlug","services","map","s","end","toISOString","start","method","path"],"mappings":"AAIA,SAASA,iBAAiB,QAAQ,qCAAoC;AAEtE,OAAO,SAASC,uBAAuBC,MAAuC;IAC5E,OAAO;QACLC,SAAS,OAAOC;YACd,MAAMC,MAAM,IAAIC,IAAIF,IAAIC,GAAG;YAC3B,MAAME,OAAOF,IAAIG,YAAY,CAACC,GAAG,CAAC;YAClC,MAAMC,WAAWL,IAAIG,YAAY,CAACC,GAAG,CAAC;YACtC,MAAME,UAAUN,IAAIG,YAAY,CAACC,GAAG,CAAC;YAErC,IAAI,CAACF,QAAQ,CAACG,YAAY,CAACC,SAAS;gBAClC,OAAOC,SAASC,IAAI,CAClB;oBAAEC,OAAO;gBAAyD,GAClE;oBAAEC,QAAQ;gBAAI;YAElB;YAEA,MAAMC,aAAa,IAAIC,KAAKV;YAC5B,IAAIW,MAAMF,WAAWG,OAAO,KAAK;gBAC/B,OAAOP,SAASC,IAAI,CAClB;oBAAEC,OAAO;gBAA2C,GACpD;oBAAEC,QAAQ;gBAAI;YAElB;YAEA,MAAMK,aAAaC,KAAKC,GAAG,CAACC,OAAOlB,IAAIG,YAAY,CAACC,GAAG,CAAC,iBAAiB,MAAM;YAE/E,MAAMe,QAAQ,MAAMxB,kBAAkB;gBACpCyB,kBAAkBvB,OAAOwB,aAAa,CAACD,gBAAgB;gBACvDlB,MAAMS;gBACNI;gBACAO,SAASvB,IAAIuB,OAAO;gBACpBvB;gBACAwB,iBAAiB1B,OAAO2B,KAAK,CAACC,YAAY;gBAC1CC,YAAYrB;gBACZsB,cAAc9B,OAAO2B,KAAK,CAACI,SAAS;gBACpCC,cAAchC,OAAO2B,KAAK,CAACM,SAAS;gBACpCC,WAAWzB;gBACX0B,aAAanC,OAAO2B,KAAK,CAACS,QAAQ;YACpC;YAEA,OAAO1B,SAASC,IAAI,CAAC;gBACnBN;gBACAa;gBACAI,OAAOA,MAAMe,GAAG,CAAC,CAACC,IAAO,CAAA;wBAAEC,KAAKD,EAAEC,GAAG,CAACC,WAAW;wBAAIC,OAAOH,EAAEG,KAAK,CAACD,WAAW;oBAAG,CAAA;YACpF;QACF;QACAE,QAAQ;QACRC,MAAM;IACR;AACF"}
|
|
@@ -7,32 +7,51 @@ export const onStatusChange = (config)=>async ({ context, doc, previousDoc, req
|
|
|
7
7
|
}
|
|
8
8
|
const prev = previousDoc.status;
|
|
9
9
|
const next = doc.status;
|
|
10
|
-
// Call generic afterStatusChange plugin hooks
|
|
11
10
|
if (config.hooks?.afterStatusChange) {
|
|
12
11
|
for (const hook of config.hooks.afterStatusChange){
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
try {
|
|
13
|
+
await hook({
|
|
14
|
+
doc: doc,
|
|
15
|
+
newStatus: next,
|
|
16
|
+
previousStatus: prev,
|
|
17
|
+
req
|
|
18
|
+
});
|
|
19
|
+
} catch (err) {
|
|
20
|
+
req.payload.logger.error({
|
|
21
|
+
err,
|
|
22
|
+
msg: `afterStatusChange hook failed for reservation ${doc.id}`
|
|
23
|
+
});
|
|
24
|
+
}
|
|
19
25
|
}
|
|
20
26
|
}
|
|
21
|
-
// Call specific hooks based on transition
|
|
22
27
|
if (next === 'confirmed' && config.hooks?.afterBookingConfirm) {
|
|
23
28
|
for (const hook of config.hooks.afterBookingConfirm){
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
try {
|
|
30
|
+
await hook({
|
|
31
|
+
doc: doc,
|
|
32
|
+
req
|
|
33
|
+
});
|
|
34
|
+
} catch (err) {
|
|
35
|
+
req.payload.logger.error({
|
|
36
|
+
err,
|
|
37
|
+
msg: `afterBookingConfirm hook failed for reservation ${doc.id}`
|
|
38
|
+
});
|
|
39
|
+
}
|
|
28
40
|
}
|
|
29
41
|
}
|
|
30
42
|
if (next === 'cancelled' && config.hooks?.afterBookingCancel) {
|
|
31
43
|
for (const hook of config.hooks.afterBookingCancel){
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
try {
|
|
45
|
+
await hook({
|
|
46
|
+
doc: doc,
|
|
47
|
+
req
|
|
48
|
+
});
|
|
49
|
+
} catch (err) {
|
|
50
|
+
req.payload.logger.error({
|
|
51
|
+
err,
|
|
52
|
+
msg: `afterBookingCancel hook failed for reservation ${doc.id}`
|
|
53
|
+
});
|
|
54
|
+
}
|
|
36
55
|
}
|
|
37
56
|
}
|
|
38
57
|
return doc;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/hooks/reservations/onStatusChange.ts"],"sourcesContent":["import type { CollectionAfterChangeHook } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nexport const onStatusChange =\n (config: ResolvedReservationPluginConfig): CollectionAfterChangeHook =>\n async ({ context, doc, previousDoc, req }) => {\n if (context?.skipReservationHooks) {return doc}\n if (!previousDoc || previousDoc.status === doc.status) {return doc}\n\n const prev = previousDoc.status as string\n const next = doc.status as string\n\n
|
|
1
|
+
{"version":3,"sources":["../../../src/hooks/reservations/onStatusChange.ts"],"sourcesContent":["import type { CollectionAfterChangeHook } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nexport const onStatusChange =\n (config: ResolvedReservationPluginConfig): CollectionAfterChangeHook =>\n async ({ context, doc, previousDoc, req }) => {\n if (context?.skipReservationHooks) {return doc}\n if (!previousDoc || previousDoc.status === doc.status) {return doc}\n\n const prev = previousDoc.status as string\n const next = doc.status as string\n\n if (config.hooks?.afterStatusChange) {\n for (const hook of config.hooks.afterStatusChange) {\n try {\n await hook({ doc: doc as Record<string, unknown>, newStatus: next, previousStatus: prev, req })\n } catch (err) {\n req.payload.logger.error({ err, msg: `afterStatusChange hook failed for reservation ${doc.id}` })\n }\n }\n }\n\n if (next === 'confirmed' && config.hooks?.afterBookingConfirm) {\n for (const hook of config.hooks.afterBookingConfirm) {\n try {\n await hook({ doc: doc as Record<string, unknown>, req })\n } catch (err) {\n req.payload.logger.error({ err, msg: `afterBookingConfirm hook failed for reservation ${doc.id}` })\n }\n }\n }\n if (next === 'cancelled' && config.hooks?.afterBookingCancel) {\n for (const hook of config.hooks.afterBookingCancel) {\n try {\n await hook({ doc: doc as Record<string, unknown>, req })\n } catch (err) {\n req.payload.logger.error({ err, msg: `afterBookingCancel hook failed for reservation ${doc.id}` })\n }\n }\n }\n\n return doc\n }\n"],"names":["onStatusChange","config","context","doc","previousDoc","req","skipReservationHooks","status","prev","next","hooks","afterStatusChange","hook","newStatus","previousStatus","err","payload","logger","error","msg","id","afterBookingConfirm","afterBookingCancel"],"mappings":"AAIA,OAAO,MAAMA,iBACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,GAAG,EAAEC,WAAW,EAAEC,GAAG,EAAE;QACvC,IAAIH,SAASI,sBAAsB;YAAC,OAAOH;QAAG;QAC9C,IAAI,CAACC,eAAeA,YAAYG,MAAM,KAAKJ,IAAII,MAAM,EAAE;YAAC,OAAOJ;QAAG;QAElE,MAAMK,OAAOJ,YAAYG,MAAM;QAC/B,MAAME,OAAON,IAAII,MAAM;QAEvB,IAAIN,OAAOS,KAAK,EAAEC,mBAAmB;YACnC,KAAK,MAAMC,QAAQX,OAAOS,KAAK,CAACC,iBAAiB,CAAE;gBACjD,IAAI;oBACF,MAAMC,KAAK;wBAAET,KAAKA;wBAAgCU,WAAWJ;wBAAMK,gBAAgBN;wBAAMH;oBAAI;gBAC/F,EAAE,OAAOU,KAAK;oBACZV,IAAIW,OAAO,CAACC,MAAM,CAACC,KAAK,CAAC;wBAAEH;wBAAKI,KAAK,CAAC,8CAA8C,EAAEhB,IAAIiB,EAAE,EAAE;oBAAC;gBACjG;YACF;QACF;QAEA,IAAIX,SAAS,eAAeR,OAAOS,KAAK,EAAEW,qBAAqB;YAC7D,KAAK,MAAMT,QAAQX,OAAOS,KAAK,CAACW,mBAAmB,CAAE;gBACnD,IAAI;oBACF,MAAMT,KAAK;wBAAET,KAAKA;wBAAgCE;oBAAI;gBACxD,EAAE,OAAOU,KAAK;oBACZV,IAAIW,OAAO,CAACC,MAAM,CAACC,KAAK,CAAC;wBAAEH;wBAAKI,KAAK,CAAC,gDAAgD,EAAEhB,IAAIiB,EAAE,EAAE;oBAAC;gBACnG;YACF;QACF;QACA,IAAIX,SAAS,eAAeR,OAAOS,KAAK,EAAEY,oBAAoB;YAC5D,KAAK,MAAMV,QAAQX,OAAOS,KAAK,CAACY,kBAAkB,CAAE;gBAClD,IAAI;oBACF,MAAMV,KAAK;wBAAET,KAAKA;wBAAgCE;oBAAI;gBACxD,EAAE,OAAOU,KAAK;oBACZV,IAAIW,OAAO,CAACC,MAAM,CAACC,KAAK,CAAC;wBAAEH;wBAAKI,KAAK,CAAC,+CAA+C,EAAEhB,IAAIiB,EAAE,EAAE;oBAAC;gBAClG;YACF;QACF;QAEA,OAAOjB;IACT,EAAC"}
|
|
@@ -9,30 +9,31 @@ export const validateConflicts = (config)=>async ({ context, data, operation, or
|
|
|
9
9
|
if (items.length === 0) {
|
|
10
10
|
return data;
|
|
11
11
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
let bufferBefore = config.defaultBufferTime;
|
|
15
|
-
let bufferAfter = config.defaultBufferTime;
|
|
16
|
-
if (serviceId) {
|
|
17
|
-
try {
|
|
18
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
|
-
const service = await req.payload.findByID({
|
|
20
|
-
id: serviceId,
|
|
21
|
-
collection: config.slugs.services,
|
|
22
|
-
req
|
|
23
|
-
});
|
|
24
|
-
if (service) {
|
|
25
|
-
bufferBefore = service.bufferTimeBefore ?? config.defaultBufferTime;
|
|
26
|
-
bufferAfter = service.bufferTimeAfter ?? config.defaultBufferTime;
|
|
27
|
-
}
|
|
28
|
-
} catch {
|
|
29
|
-
// Use defaults if service lookup fails
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
for (const item of items){
|
|
12
|
+
for(let i = 0; i < items.length; i++){
|
|
13
|
+
const item = items[i];
|
|
33
14
|
if (!item.endTime) {
|
|
34
15
|
continue;
|
|
35
16
|
}
|
|
17
|
+
// Fetch buffer times from the item's own service (not just the primary)
|
|
18
|
+
const itemServiceId = item.service ?? (typeof data?.service === 'object' ? data.service.id : data?.service);
|
|
19
|
+
let bufferBefore = config.defaultBufferTime;
|
|
20
|
+
let bufferAfter = config.defaultBufferTime;
|
|
21
|
+
if (itemServiceId) {
|
|
22
|
+
try {
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
+
const service = await req.payload.findByID({
|
|
25
|
+
id: itemServiceId,
|
|
26
|
+
collection: config.slugs.services,
|
|
27
|
+
req
|
|
28
|
+
});
|
|
29
|
+
if (service) {
|
|
30
|
+
bufferBefore = service.bufferTimeBefore ?? config.defaultBufferTime;
|
|
31
|
+
bufferAfter = service.bufferTimeAfter ?? config.defaultBufferTime;
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// Use defaults if service lookup fails
|
|
35
|
+
}
|
|
36
|
+
}
|
|
36
37
|
const result = await checkAvailability({
|
|
37
38
|
blockingStatuses: config.statusMachine.blockingStatuses,
|
|
38
39
|
bufferAfter,
|
|
@@ -52,7 +53,7 @@ export const validateConflicts = (config)=>async ({ context, data, operation, or
|
|
|
52
53
|
errors: [
|
|
53
54
|
{
|
|
54
55
|
message: result.reason ?? req.t('reservation:errorConflict'),
|
|
55
|
-
path: 'startTime'
|
|
56
|
+
path: items.length > 1 ? `items.${i}.startTime` : 'startTime'
|
|
56
57
|
}
|
|
57
58
|
]
|
|
58
59
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/hooks/reservations/validateConflicts.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { checkAvailability } from '../../services/AvailabilityService.js'\nimport { resolveReservationItems } from '../../utilities/resolveReservationItems.js'\n\nexport const validateConflicts =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, originalDoc, req }) => {\n if (context?.skipReservationHooks) {return data}\n\n const items = resolveReservationItems(data as Record<string, unknown>)\n\n if (items.length === 0) {return data}\n\n // Fetch buffer times from the
|
|
1
|
+
{"version":3,"sources":["../../../src/hooks/reservations/validateConflicts.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { checkAvailability } from '../../services/AvailabilityService.js'\nimport { resolveReservationItems } from '../../utilities/resolveReservationItems.js'\n\nexport const validateConflicts =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, originalDoc, req }) => {\n if (context?.skipReservationHooks) {return data}\n\n const items = resolveReservationItems(data as Record<string, unknown>)\n\n if (items.length === 0) {return data}\n\n for (let i = 0; i < items.length; i++) {\n const item = items[i]\n if (!item.endTime) {continue}\n\n // Fetch buffer times from the item's own service (not just the primary)\n const itemServiceId = item.service\n ?? (typeof data?.service === 'object' ? data.service.id : data?.service)\n let bufferBefore = config.defaultBufferTime\n let bufferAfter = config.defaultBufferTime\n\n if (itemServiceId) {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (req.payload.findByID as any)({\n id: itemServiceId,\n collection: config.slugs.services,\n req,\n })\n if (service) {\n bufferBefore = (service.bufferTimeBefore as number) ?? config.defaultBufferTime\n bufferAfter = (service.bufferTimeAfter as number) ?? config.defaultBufferTime\n }\n } catch {\n // Use defaults if service lookup fails\n }\n }\n\n const result = await checkAvailability({\n blockingStatuses: config.statusMachine.blockingStatuses,\n bufferAfter,\n bufferBefore,\n endTime: new Date(item.endTime),\n excludeReservationId: operation === 'update' ? originalDoc?.id : undefined,\n guestCount: item.guestCount,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceId: item.resource,\n resourceSlug: config.slugs.resources,\n startTime: new Date(item.startTime),\n })\n\n if (!result.available) {\n throw new ValidationError({\n errors: [\n {\n message: result.reason ?? (req.t as PluginT)('reservation:errorConflict'),\n path: items.length > 1 ? `items.${i}.startTime` : 'startTime',\n },\n ],\n })\n }\n }\n\n return data\n }\n"],"names":["ValidationError","checkAvailability","resolveReservationItems","validateConflicts","config","context","data","operation","originalDoc","req","skipReservationHooks","items","length","i","item","endTime","itemServiceId","service","id","bufferBefore","defaultBufferTime","bufferAfter","payload","findByID","collection","slugs","services","bufferTimeBefore","bufferTimeAfter","result","blockingStatuses","statusMachine","Date","excludeReservationId","undefined","guestCount","reservationSlug","reservations","resourceId","resource","resourceSlug","resources","startTime","available","errors","message","reason","t","path"],"mappings":"AAEA,SAASA,eAAe,QAAQ,UAAS;AAKzC,SAASC,iBAAiB,QAAQ,wCAAuC;AACzE,SAASC,uBAAuB,QAAQ,6CAA4C;AAEpF,OAAO,MAAMC,oBACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,IAAI,EAAEC,SAAS,EAAEC,WAAW,EAAEC,GAAG,EAAE;QACnD,IAAIJ,SAASK,sBAAsB;YAAC,OAAOJ;QAAI;QAE/C,MAAMK,QAAQT,wBAAwBI;QAEtC,IAAIK,MAAMC,MAAM,KAAK,GAAG;YAAC,OAAON;QAAI;QAEpC,IAAK,IAAIO,IAAI,GAAGA,IAAIF,MAAMC,MAAM,EAAEC,IAAK;YACrC,MAAMC,OAAOH,KAAK,CAACE,EAAE;YACrB,IAAI,CAACC,KAAKC,OAAO,EAAE;gBAAC;YAAQ;YAE5B,wEAAwE;YACxE,MAAMC,gBAAgBF,KAAKG,OAAO,IAC5B,CAAA,OAAOX,MAAMW,YAAY,WAAWX,KAAKW,OAAO,CAACC,EAAE,GAAGZ,MAAMW,OAAM;YACxE,IAAIE,eAAef,OAAOgB,iBAAiB;YAC3C,IAAIC,cAAcjB,OAAOgB,iBAAiB;YAE1C,IAAIJ,eAAe;gBACjB,IAAI;oBACF,8DAA8D;oBAC9D,MAAMC,UAAU,MAAM,AAACR,IAAIa,OAAO,CAACC,QAAQ,CAAS;wBAClDL,IAAIF;wBACJQ,YAAYpB,OAAOqB,KAAK,CAACC,QAAQ;wBACjCjB;oBACF;oBACA,IAAIQ,SAAS;wBACXE,eAAe,AAACF,QAAQU,gBAAgB,IAAevB,OAAOgB,iBAAiB;wBAC/EC,cAAc,AAACJ,QAAQW,eAAe,IAAexB,OAAOgB,iBAAiB;oBAC/E;gBACF,EAAE,OAAM;gBACN,uCAAuC;gBACzC;YACF;YAEA,MAAMS,SAAS,MAAM5B,kBAAkB;gBACrC6B,kBAAkB1B,OAAO2B,aAAa,CAACD,gBAAgB;gBACvDT;gBACAF;gBACAJ,SAAS,IAAIiB,KAAKlB,KAAKC,OAAO;gBAC9BkB,sBAAsB1B,cAAc,WAAWC,aAAaU,KAAKgB;gBACjEC,YAAYrB,KAAKqB,UAAU;gBAC3Bb,SAASb,IAAIa,OAAO;gBACpBb;gBACA2B,iBAAiBhC,OAAOqB,KAAK,CAACY,YAAY;gBAC1CC,YAAYxB,KAAKyB,QAAQ;gBACzBC,cAAcpC,OAAOqB,KAAK,CAACgB,SAAS;gBACpCC,WAAW,IAAIV,KAAKlB,KAAK4B,SAAS;YACpC;YAEA,IAAI,CAACb,OAAOc,SAAS,EAAE;gBACrB,MAAM,IAAI3C,gBAAgB;oBACxB4C,QAAQ;wBACN;4BACEC,SAAShB,OAAOiB,MAAM,IAAI,AAACrC,IAAIsC,CAAC,CAAa;4BAC7CC,MAAMrC,MAAMC,MAAM,GAAG,IAAI,CAAC,MAAM,EAAEC,EAAE,UAAU,CAAC,GAAG;wBACpD;qBACD;gBACH;YACF;QACF;QAEA,OAAOP;IACT,EAAC"}
|
|
@@ -7,11 +7,16 @@ export const validateStatusTransition = (config)=>async ({ context, data, operat
|
|
|
7
7
|
const newStatus = data?.status;
|
|
8
8
|
const { statusMachine } = config;
|
|
9
9
|
if (operation === 'create') {
|
|
10
|
-
|
|
10
|
+
// context.allowConfirmedOnCreate is the escape hatch for payment hooks
|
|
11
|
+
// that need to create confirmed reservations programmatically
|
|
12
|
+
const hasContextBypass = Boolean(context?.allowConfirmedOnCreate);
|
|
13
|
+
// Admin = user from a non-customer collection (e.g., 'users' admin collection)
|
|
14
|
+
const isAdmin = req.user != null && req.user.collection !== config.slugs.customers;
|
|
11
15
|
const defaultStatus = statusMachine.defaultStatus;
|
|
12
|
-
const
|
|
16
|
+
const nonDefaultStatuses = statusMachine.transitions[defaultStatus] ?? [];
|
|
17
|
+
const allowedOnCreate = isAdmin || hasContextBypass ? [
|
|
13
18
|
defaultStatus,
|
|
14
|
-
|
|
19
|
+
...nonDefaultStatuses
|
|
15
20
|
] : [
|
|
16
21
|
defaultStatus
|
|
17
22
|
];
|
|
@@ -28,7 +33,6 @@ export const validateStatusTransition = (config)=>async ({ context, data, operat
|
|
|
28
33
|
]
|
|
29
34
|
});
|
|
30
35
|
}
|
|
31
|
-
// Call beforeBookingCreate hooks (handled by plugin hooks wrapper)
|
|
32
36
|
return data;
|
|
33
37
|
}
|
|
34
38
|
// On update
|
|
@@ -53,7 +57,10 @@ export const validateStatusTransition = (config)=>async ({ context, data, operat
|
|
|
53
57
|
if (newStatus === 'confirmed' && config.hooks?.beforeBookingConfirm) {
|
|
54
58
|
for (const hook of config.hooks.beforeBookingConfirm){
|
|
55
59
|
await hook({
|
|
56
|
-
doc:
|
|
60
|
+
doc: {
|
|
61
|
+
...originalDoc,
|
|
62
|
+
...data
|
|
63
|
+
},
|
|
57
64
|
newStatus,
|
|
58
65
|
req
|
|
59
66
|
});
|
|
@@ -63,7 +70,10 @@ export const validateStatusTransition = (config)=>async ({ context, data, operat
|
|
|
63
70
|
if (newStatus === 'cancelled' && config.hooks?.beforeBookingCancel) {
|
|
64
71
|
for (const hook of config.hooks.beforeBookingCancel){
|
|
65
72
|
await hook({
|
|
66
|
-
doc:
|
|
73
|
+
doc: {
|
|
74
|
+
...originalDoc,
|
|
75
|
+
...data
|
|
76
|
+
},
|
|
67
77
|
reason: data?.cancellationReason,
|
|
68
78
|
req
|
|
69
79
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/hooks/reservations/validateStatusTransition.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { validateTransition } from '../../services/AvailabilityService.js'\n\nexport const validateStatusTransition =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, originalDoc, req }) => {\n if (context?.skipReservationHooks) {return data}\n\n const newStatus = data?.status as string | undefined\n const { statusMachine } = config\n\n if (operation === 'create') {\n const
|
|
1
|
+
{"version":3,"sources":["../../../src/hooks/reservations/validateStatusTransition.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { validateTransition } from '../../services/AvailabilityService.js'\n\nexport const validateStatusTransition =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, originalDoc, req }) => {\n if (context?.skipReservationHooks) {return data}\n\n const newStatus = data?.status as string | undefined\n const { statusMachine } = config\n\n if (operation === 'create') {\n // context.allowConfirmedOnCreate is the escape hatch for payment hooks\n // that need to create confirmed reservations programmatically\n const hasContextBypass = Boolean(context?.allowConfirmedOnCreate)\n // Admin = user from a non-customer collection (e.g., 'users' admin collection)\n const isAdmin = req.user != null && req.user.collection !== config.slugs.customers\n const defaultStatus = statusMachine.defaultStatus\n const nonDefaultStatuses = statusMachine.transitions[defaultStatus] ?? []\n const allowedOnCreate: string[] = (isAdmin || hasContextBypass)\n ? [defaultStatus, ...nonDefaultStatuses]\n : [defaultStatus]\n\n if (newStatus && !allowedOnCreate.includes(newStatus)) {\n const allowed = allowedOnCreate.map((s) => `\"${s}\"`).join(' or ')\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorInvalidCreateStatus', { allowed }),\n path: 'status',\n },\n ],\n })\n }\n\n return data\n }\n\n // On update\n if (operation === 'update' && newStatus) {\n const previousStatus = originalDoc?.status as string | undefined\n\n if (previousStatus && previousStatus !== newStatus) {\n const result = validateTransition(previousStatus, newStatus, statusMachine)\n\n if (!result.valid) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorInvalidTransition', {\n from: previousStatus,\n to: newStatus,\n }),\n path: 'status',\n },\n ],\n })\n }\n\n // Call beforeBookingConfirm plugin hooks\n if (newStatus === 'confirmed' && config.hooks?.beforeBookingConfirm) {\n for (const hook of config.hooks.beforeBookingConfirm) {\n await hook({\n doc: { ...(originalDoc as Record<string, unknown>), ...(data as Record<string, unknown>) },\n newStatus,\n req,\n })\n }\n }\n\n // Call beforeBookingCancel plugin hooks\n if (newStatus === 'cancelled' && config.hooks?.beforeBookingCancel) {\n for (const hook of config.hooks.beforeBookingCancel) {\n await hook({\n doc: { ...(originalDoc as Record<string, unknown>), ...(data as Record<string, unknown>) },\n reason: data?.cancellationReason as string | undefined,\n req,\n })\n }\n }\n }\n }\n\n return data\n }\n"],"names":["ValidationError","validateTransition","validateStatusTransition","config","context","data","operation","originalDoc","req","skipReservationHooks","newStatus","status","statusMachine","hasContextBypass","Boolean","allowConfirmedOnCreate","isAdmin","user","collection","slugs","customers","defaultStatus","nonDefaultStatuses","transitions","allowedOnCreate","includes","allowed","map","s","join","errors","message","t","path","previousStatus","result","valid","from","to","hooks","beforeBookingConfirm","hook","doc","beforeBookingCancel","reason","cancellationReason"],"mappings":"AAEA,SAASA,eAAe,QAAQ,UAAS;AAKzC,SAASC,kBAAkB,QAAQ,wCAAuC;AAE1E,OAAO,MAAMC,2BACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,IAAI,EAAEC,SAAS,EAAEC,WAAW,EAAEC,GAAG,EAAE;QACnD,IAAIJ,SAASK,sBAAsB;YAAC,OAAOJ;QAAI;QAE/C,MAAMK,YAAYL,MAAMM;QACxB,MAAM,EAAEC,aAAa,EAAE,GAAGT;QAE1B,IAAIG,cAAc,UAAU;YAC1B,uEAAuE;YACvE,8DAA8D;YAC9D,MAAMO,mBAAmBC,QAAQV,SAASW;YAC1C,+EAA+E;YAC/E,MAAMC,UAAUR,IAAIS,IAAI,IAAI,QAAQT,IAAIS,IAAI,CAACC,UAAU,KAAKf,OAAOgB,KAAK,CAACC,SAAS;YAClF,MAAMC,gBAAgBT,cAAcS,aAAa;YACjD,MAAMC,qBAAqBV,cAAcW,WAAW,CAACF,cAAc,IAAI,EAAE;YACzE,MAAMG,kBAA4B,AAACR,WAAWH,mBAC1C;gBAACQ;mBAAkBC;aAAmB,GACtC;gBAACD;aAAc;YAEnB,IAAIX,aAAa,CAACc,gBAAgBC,QAAQ,CAACf,YAAY;gBACrD,MAAMgB,UAAUF,gBAAgBG,GAAG,CAAC,CAACC,IAAM,CAAC,CAAC,EAAEA,EAAE,CAAC,CAAC,EAAEC,IAAI,CAAC;gBAC1D,MAAM,IAAI7B,gBAAgB;oBACxB8B,QAAQ;wBACN;4BACEC,SAAS,AAACvB,IAAIwB,CAAC,CAAa,wCAAwC;gCAAEN;4BAAQ;4BAC9EO,MAAM;wBACR;qBACD;gBACH;YACF;YAEA,OAAO5B;QACT;QAEA,YAAY;QACZ,IAAIC,cAAc,YAAYI,WAAW;YACvC,MAAMwB,iBAAiB3B,aAAaI;YAEpC,IAAIuB,kBAAkBA,mBAAmBxB,WAAW;gBAClD,MAAMyB,SAASlC,mBAAmBiC,gBAAgBxB,WAAWE;gBAE7D,IAAI,CAACuB,OAAOC,KAAK,EAAE;oBACjB,MAAM,IAAIpC,gBAAgB;wBACxB8B,QAAQ;4BACN;gCACEC,SAAS,AAACvB,IAAIwB,CAAC,CAAa,sCAAsC;oCAChEK,MAAMH;oCACNI,IAAI5B;gCACN;gCACAuB,MAAM;4BACR;yBACD;oBACH;gBACF;gBAEA,yCAAyC;gBACzC,IAAIvB,cAAc,eAAeP,OAAOoC,KAAK,EAAEC,sBAAsB;oBACnE,KAAK,MAAMC,QAAQtC,OAAOoC,KAAK,CAACC,oBAAoB,CAAE;wBACpD,MAAMC,KAAK;4BACTC,KAAK;gCAAE,GAAInC,WAAW;gCAA8B,GAAIF,IAAI;4BAA6B;4BACzFK;4BACAF;wBACF;oBACF;gBACF;gBAEA,wCAAwC;gBACxC,IAAIE,cAAc,eAAeP,OAAOoC,KAAK,EAAEI,qBAAqB;oBAClE,KAAK,MAAMF,QAAQtC,OAAOoC,KAAK,CAACI,mBAAmB,CAAE;wBACnD,MAAMF,KAAK;4BACTC,KAAK;gCAAE,GAAInC,WAAW;gCAA8B,GAAIF,IAAI;4BAA6B;4BACzFuC,QAAQvC,MAAMwC;4BACdrC;wBACF;oBACF;gBACF;YACF;QACF;QAEA,OAAOH;IACT,EAAC"}
|
|
@@ -142,7 +142,7 @@ export async function checkAvailability(params) {
|
|
|
142
142
|
};
|
|
143
143
|
}
|
|
144
144
|
export async function getAvailableSlots(params) {
|
|
145
|
-
const { blockingStatuses, date, payload, req, reservationSlug, resourceId, resourceSlug, scheduleSlug, serviceId, serviceSlug } = params;
|
|
145
|
+
const { blockingStatuses, date, guestCount, payload, req, reservationSlug, resourceId, resourceSlug, scheduleSlug, serviceId, serviceSlug } = params;
|
|
146
146
|
// 1. Fetch service for duration + buffer times
|
|
147
147
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
148
148
|
const service = await payload.findByID({
|
|
@@ -195,6 +195,33 @@ export async function getAvailableSlots(params) {
|
|
|
195
195
|
const slotDuration = Math.round(slotEndOffset.getTime() / 60_000);
|
|
196
196
|
const effectiveDuration = durationType === 'fixed' ? duration : slotDuration;
|
|
197
197
|
const availableSlots = [];
|
|
198
|
+
// For full-day services, offer the entire range as a single slot per range
|
|
199
|
+
if (durationType === 'full-day') {
|
|
200
|
+
for (const range of timeRanges){
|
|
201
|
+
const result = await checkAvailability({
|
|
202
|
+
blockingStatuses,
|
|
203
|
+
bufferAfter: 0,
|
|
204
|
+
bufferBefore: 0,
|
|
205
|
+
endTime: range.end,
|
|
206
|
+
guestCount: guestCount ?? 1,
|
|
207
|
+
payload,
|
|
208
|
+
req,
|
|
209
|
+
reservationSlug,
|
|
210
|
+
resourceId,
|
|
211
|
+
resourceSlug,
|
|
212
|
+
startTime: range.start
|
|
213
|
+
});
|
|
214
|
+
if (result.available) {
|
|
215
|
+
availableSlots.push({
|
|
216
|
+
end: range.end,
|
|
217
|
+
start: range.start
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return availableSlots;
|
|
222
|
+
}
|
|
223
|
+
// Step by a smaller increment to catch slots between buffer gaps
|
|
224
|
+
const stepSize = Math.min(effectiveDuration, 15);
|
|
198
225
|
for (const range of timeRanges){
|
|
199
226
|
let candidateStart = new Date(range.start);
|
|
200
227
|
while(true){
|
|
@@ -208,7 +235,7 @@ export async function getAvailableSlots(params) {
|
|
|
208
235
|
bufferAfter,
|
|
209
236
|
bufferBefore,
|
|
210
237
|
endTime: candidateEnd,
|
|
211
|
-
guestCount: 1,
|
|
238
|
+
guestCount: guestCount ?? 1,
|
|
212
239
|
payload,
|
|
213
240
|
req,
|
|
214
241
|
reservationSlug,
|
|
@@ -222,8 +249,7 @@ export async function getAvailableSlots(params) {
|
|
|
222
249
|
start: new Date(candidateStart)
|
|
223
250
|
});
|
|
224
251
|
}
|
|
225
|
-
|
|
226
|
-
candidateStart = addMinutes(candidateStart, effectiveDuration);
|
|
252
|
+
candidateStart = addMinutes(candidateStart, stepSize);
|
|
227
253
|
}
|
|
228
254
|
}
|
|
229
255
|
return availableSlots;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/services/AvailabilityService.ts"],"sourcesContent":["import type { Payload, PayloadRequest, Where } from 'payload'\n\nimport type { CapacityMode, DurationType, StatusMachineConfig } from '../types.js'\n\nimport { resolveScheduleForDate } from '../utilities/scheduleUtils.js'\nimport { addMinutes, computeBlockedWindow } from '../utilities/slotUtils.js'\n\n// --- Pure functions (no DB) ---\n\nexport function computeEndTime(params: {\n durationType: DurationType\n endTime?: Date\n serviceDuration: number\n startTime: Date\n}): { durationMinutes: number; endTime: Date } {\n const { durationType, serviceDuration, startTime } = params\n\n if (durationType === 'full-day') {\n const end = new Date(startTime)\n end.setHours(23, 59, 59, 999)\n const durationMinutes = Math.round((end.getTime() - startTime.getTime()) / 60_000)\n return { durationMinutes, endTime: end }\n }\n\n if (durationType === 'flexible' && params.endTime) {\n const durationMinutes = Math.round(\n (params.endTime.getTime() - startTime.getTime()) / 60_000,\n )\n return { durationMinutes, endTime: params.endTime }\n }\n\n // fixed duration (default)\n const endTime = addMinutes(startTime, serviceDuration)\n return { durationMinutes: serviceDuration, endTime }\n}\n\nexport function buildOverlapQuery(params: {\n blockingStatuses: string[]\n effectiveEnd: Date\n effectiveStart: Date\n excludeReservationId?: string\n resourceId: string\n}): Where {\n const { blockingStatuses, effectiveEnd, effectiveStart, excludeReservationId, resourceId } =\n params\n\n const conditions: Where[] = [\n { resource: { equals: resourceId } },\n { status: { in: blockingStatuses } },\n { startTime: { less_than: effectiveEnd.toISOString() } },\n { endTime: { greater_than: effectiveStart.toISOString() } },\n ]\n\n if (excludeReservationId) {\n conditions.push({ id: { not_equals: excludeReservationId } })\n }\n\n return { and: conditions }\n}\n\nexport function isBlockingStatus(\n status: string,\n statusMachine: StatusMachineConfig,\n): boolean {\n return statusMachine.blockingStatuses.includes(status)\n}\n\nexport function validateTransition(\n fromStatus: string,\n toStatus: string,\n statusMachine: StatusMachineConfig,\n): { reason?: string; valid: boolean } {\n const allowed = statusMachine.transitions[fromStatus]\n if (!allowed) {\n return { reason: `Unknown status: ${fromStatus}`, valid: false }\n }\n if (!allowed.includes(toStatus)) {\n return {\n reason: `Cannot transition from \"${fromStatus}\" to \"${toStatus}\"`,\n valid: false,\n }\n }\n return { valid: true }\n}\n\n// --- DB functions (use Payload Local API only) ---\n\nexport async function checkAvailability(params: {\n blockingStatuses: string[]\n bufferAfter: number\n bufferBefore: number\n endTime: Date\n excludeReservationId?: string\n guestCount: number\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId: string\n resourceSlug: string\n startTime: Date\n}): Promise<{\n available: boolean\n currentCount: number\n reason?: string\n totalCapacity: number\n}> {\n const {\n blockingStatuses,\n bufferAfter,\n bufferBefore,\n endTime,\n excludeReservationId,\n guestCount,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n startTime,\n } = params\n\n // Fetch resource for quantity and capacity mode\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const resource = await (payload.findByID as any)({\n id: resourceId,\n collection: resourceSlug,\n depth: 0,\n req,\n })\n const quantity = (resource.quantity as number) ?? 1\n const capacityMode = ((resource.capacityMode as string) ?? 'per-reservation') as CapacityMode\n\n // Compute effective window with buffers\n const { effectiveEnd, effectiveStart } = computeBlockedWindow(\n startTime,\n endTime,\n bufferBefore,\n bufferAfter,\n )\n\n // Build overlap query\n const where = buildOverlapQuery({\n blockingStatuses,\n effectiveEnd,\n effectiveStart,\n excludeReservationId,\n resourceId,\n })\n\n if (capacityMode === 'per-guest') {\n // Must fetch docs to sum guestCount\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs } = await (payload.find as any)({\n collection: reservationSlug,\n depth: 0,\n limit: 0,\n req,\n select: { guestCount: true },\n where,\n })\n const currentGuests = docs.reduce(\n (sum: number, doc: Record<string, unknown>) => sum + ((doc.guestCount as number) ?? 1),\n 0,\n )\n return {\n available: currentGuests + guestCount <= quantity,\n currentCount: currentGuests,\n reason:\n currentGuests + guestCount > quantity ? 'Guest capacity exceeded' : undefined,\n totalCapacity: quantity,\n }\n }\n\n // per-reservation mode: count is sufficient\n // TODO: batch queries — linear per-item cost acceptable for 2-5 items\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { totalDocs } = await (payload.count as any)({\n collection: reservationSlug,\n req,\n where,\n })\n return {\n available: totalDocs + 1 <= quantity,\n currentCount: totalDocs,\n reason: totalDocs + 1 > quantity ? 'All units are booked for this time' : undefined,\n totalCapacity: quantity,\n }\n}\n\nexport async function getAvailableSlots(params: {\n blockingStatuses: string[]\n date: Date\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId: string\n resourceSlug: string\n scheduleSlug: string\n serviceId: string\n serviceSlug: string\n}): Promise<Array<{ end: Date; start: Date }>> {\n const {\n blockingStatuses,\n date,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n scheduleSlug,\n serviceId,\n serviceSlug,\n } = params\n\n // 1. Fetch service for duration + buffer times\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (payload.findByID as any)({\n id: serviceId,\n collection: serviceSlug,\n depth: 0,\n req,\n })\n const duration = (service.duration as number) ?? 60\n const bufferBefore = (service.bufferTimeBefore as number) ?? 0\n const bufferAfter = (service.bufferTimeAfter as number) ?? 0\n const durationType = ((service.durationType as string) ?? 'fixed') as DurationType\n\n // 2. Fetch resource's schedules for the date\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs: schedules } = await (payload.find as any)({\n collection: scheduleSlug,\n depth: 0,\n limit: 100,\n req,\n where: {\n and: [{ resource: { equals: resourceId } }, { active: { equals: true } }],\n },\n })\n\n // 3. Resolve schedules to time ranges for the date\n const timeRanges: Array<{ end: Date; start: Date }> = []\n for (const schedule of schedules) {\n const ranges = resolveScheduleForDate(\n schedule as unknown as Parameters<typeof resolveScheduleForDate>[0],\n date,\n )\n timeRanges.push(...ranges)\n }\n\n if (timeRanges.length === 0) {\n return []\n }\n\n // 4. Generate candidate slots from schedule ranges\n const { endTime: slotEndOffset } = computeEndTime({\n durationType,\n serviceDuration: duration,\n startTime: new Date(0),\n })\n const slotDuration = Math.round(slotEndOffset.getTime() / 60_000)\n const effectiveDuration = durationType === 'fixed' ? duration : slotDuration\n\n const availableSlots: Array<{ end: Date; start: Date }> = []\n\n for (const range of timeRanges) {\n let candidateStart = new Date(range.start)\n\n while (true) {\n const candidateEnd = addMinutes(candidateStart, effectiveDuration)\n if (candidateEnd > range.end) {break}\n\n // 5. Check availability for each candidate slot\n const result = await checkAvailability({\n blockingStatuses,\n bufferAfter,\n bufferBefore,\n endTime: candidateEnd,\n guestCount: 1,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n startTime: candidateStart,\n })\n\n if (result.available) {\n availableSlots.push({ end: candidateEnd, start: new Date(candidateStart) })\n }\n\n // Move to next slot (service duration as step)\n candidateStart = addMinutes(candidateStart, effectiveDuration)\n }\n }\n\n return availableSlots\n}\n"],"names":["resolveScheduleForDate","addMinutes","computeBlockedWindow","computeEndTime","params","durationType","serviceDuration","startTime","end","Date","setHours","durationMinutes","Math","round","getTime","endTime","buildOverlapQuery","blockingStatuses","effectiveEnd","effectiveStart","excludeReservationId","resourceId","conditions","resource","equals","status","in","less_than","toISOString","greater_than","push","id","not_equals","and","isBlockingStatus","statusMachine","includes","validateTransition","fromStatus","toStatus","allowed","transitions","reason","valid","checkAvailability","bufferAfter","bufferBefore","guestCount","payload","req","reservationSlug","resourceSlug","findByID","collection","depth","quantity","capacityMode","where","docs","find","limit","select","currentGuests","reduce","sum","doc","available","currentCount","undefined","totalCapacity","totalDocs","count","getAvailableSlots","date","scheduleSlug","serviceId","serviceSlug","service","duration","bufferTimeBefore","bufferTimeAfter","schedules","active","timeRanges","schedule","ranges","length","slotEndOffset","slotDuration","effectiveDuration","availableSlots","range","candidateStart","start","candidateEnd","result"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/services/AvailabilityService.ts"],"sourcesContent":["import type { Payload, PayloadRequest, Where } from 'payload'\n\nimport type { CapacityMode, DurationType, StatusMachineConfig } from '../types.js'\n\nimport { resolveScheduleForDate } from '../utilities/scheduleUtils.js'\nimport { addMinutes, computeBlockedWindow } from '../utilities/slotUtils.js'\n\n// --- Pure functions (no DB) ---\n\nexport function computeEndTime(params: {\n durationType: DurationType\n endTime?: Date\n serviceDuration: number\n startTime: Date\n}): { durationMinutes: number; endTime: Date } {\n const { durationType, serviceDuration, startTime } = params\n\n if (durationType === 'full-day') {\n const end = new Date(startTime)\n end.setHours(23, 59, 59, 999)\n const durationMinutes = Math.round((end.getTime() - startTime.getTime()) / 60_000)\n return { durationMinutes, endTime: end }\n }\n\n if (durationType === 'flexible' && params.endTime) {\n const durationMinutes = Math.round(\n (params.endTime.getTime() - startTime.getTime()) / 60_000,\n )\n return { durationMinutes, endTime: params.endTime }\n }\n\n // fixed duration (default)\n const endTime = addMinutes(startTime, serviceDuration)\n return { durationMinutes: serviceDuration, endTime }\n}\n\nexport function buildOverlapQuery(params: {\n blockingStatuses: string[]\n effectiveEnd: Date\n effectiveStart: Date\n excludeReservationId?: string\n resourceId: string\n}): Where {\n const { blockingStatuses, effectiveEnd, effectiveStart, excludeReservationId, resourceId } =\n params\n\n const conditions: Where[] = [\n { resource: { equals: resourceId } },\n { status: { in: blockingStatuses } },\n { startTime: { less_than: effectiveEnd.toISOString() } },\n { endTime: { greater_than: effectiveStart.toISOString() } },\n ]\n\n if (excludeReservationId) {\n conditions.push({ id: { not_equals: excludeReservationId } })\n }\n\n return { and: conditions }\n}\n\nexport function isBlockingStatus(\n status: string,\n statusMachine: StatusMachineConfig,\n): boolean {\n return statusMachine.blockingStatuses.includes(status)\n}\n\nexport function validateTransition(\n fromStatus: string,\n toStatus: string,\n statusMachine: StatusMachineConfig,\n): { reason?: string; valid: boolean } {\n const allowed = statusMachine.transitions[fromStatus]\n if (!allowed) {\n return { reason: `Unknown status: ${fromStatus}`, valid: false }\n }\n if (!allowed.includes(toStatus)) {\n return {\n reason: `Cannot transition from \"${fromStatus}\" to \"${toStatus}\"`,\n valid: false,\n }\n }\n return { valid: true }\n}\n\n// --- DB functions (use Payload Local API only) ---\n\nexport async function checkAvailability(params: {\n blockingStatuses: string[]\n bufferAfter: number\n bufferBefore: number\n endTime: Date\n excludeReservationId?: string\n guestCount: number\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId: string\n resourceSlug: string\n startTime: Date\n}): Promise<{\n available: boolean\n currentCount: number\n reason?: string\n totalCapacity: number\n}> {\n const {\n blockingStatuses,\n bufferAfter,\n bufferBefore,\n endTime,\n excludeReservationId,\n guestCount,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n startTime,\n } = params\n\n // Fetch resource for quantity and capacity mode\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const resource = await (payload.findByID as any)({\n id: resourceId,\n collection: resourceSlug,\n depth: 0,\n req,\n })\n const quantity = (resource.quantity as number) ?? 1\n const capacityMode = ((resource.capacityMode as string) ?? 'per-reservation') as CapacityMode\n\n // Compute effective window with buffers\n const { effectiveEnd, effectiveStart } = computeBlockedWindow(\n startTime,\n endTime,\n bufferBefore,\n bufferAfter,\n )\n\n // Build overlap query\n const where = buildOverlapQuery({\n blockingStatuses,\n effectiveEnd,\n effectiveStart,\n excludeReservationId,\n resourceId,\n })\n\n if (capacityMode === 'per-guest') {\n // Must fetch docs to sum guestCount\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs } = await (payload.find as any)({\n collection: reservationSlug,\n depth: 0,\n limit: 0,\n req,\n select: { guestCount: true },\n where,\n })\n const currentGuests = docs.reduce(\n (sum: number, doc: Record<string, unknown>) => sum + ((doc.guestCount as number) ?? 1),\n 0,\n )\n return {\n available: currentGuests + guestCount <= quantity,\n currentCount: currentGuests,\n reason:\n currentGuests + guestCount > quantity ? 'Guest capacity exceeded' : undefined,\n totalCapacity: quantity,\n }\n }\n\n // per-reservation mode: count is sufficient\n // TODO: batch queries — linear per-item cost acceptable for 2-5 items\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { totalDocs } = await (payload.count as any)({\n collection: reservationSlug,\n req,\n where,\n })\n return {\n available: totalDocs + 1 <= quantity,\n currentCount: totalDocs,\n reason: totalDocs + 1 > quantity ? 'All units are booked for this time' : undefined,\n totalCapacity: quantity,\n }\n}\n\nexport async function getAvailableSlots(params: {\n blockingStatuses: string[]\n date: Date\n guestCount?: number\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId: string\n resourceSlug: string\n scheduleSlug: string\n serviceId: string\n serviceSlug: string\n}): Promise<Array<{ end: Date; start: Date }>> {\n const {\n blockingStatuses,\n date,\n guestCount,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n scheduleSlug,\n serviceId,\n serviceSlug,\n } = params\n\n // 1. Fetch service for duration + buffer times\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (payload.findByID as any)({\n id: serviceId,\n collection: serviceSlug,\n depth: 0,\n req,\n })\n const duration = (service.duration as number) ?? 60\n const bufferBefore = (service.bufferTimeBefore as number) ?? 0\n const bufferAfter = (service.bufferTimeAfter as number) ?? 0\n const durationType = ((service.durationType as string) ?? 'fixed') as DurationType\n\n // 2. Fetch resource's schedules for the date\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs: schedules } = await (payload.find as any)({\n collection: scheduleSlug,\n depth: 0,\n limit: 100,\n req,\n where: {\n and: [{ resource: { equals: resourceId } }, { active: { equals: true } }],\n },\n })\n\n // 3. Resolve schedules to time ranges for the date\n const timeRanges: Array<{ end: Date; start: Date }> = []\n for (const schedule of schedules) {\n const ranges = resolveScheduleForDate(\n schedule as unknown as Parameters<typeof resolveScheduleForDate>[0],\n date,\n )\n timeRanges.push(...ranges)\n }\n\n if (timeRanges.length === 0) {\n return []\n }\n\n // 4. Generate candidate slots from schedule ranges\n const { endTime: slotEndOffset } = computeEndTime({\n durationType,\n serviceDuration: duration,\n startTime: new Date(0),\n })\n const slotDuration = Math.round(slotEndOffset.getTime() / 60_000)\n const effectiveDuration = durationType === 'fixed' ? duration : slotDuration\n\n const availableSlots: Array<{ end: Date; start: Date }> = []\n\n // For full-day services, offer the entire range as a single slot per range\n if (durationType === 'full-day') {\n for (const range of timeRanges) {\n const result = await checkAvailability({\n blockingStatuses,\n bufferAfter: 0,\n bufferBefore: 0,\n endTime: range.end,\n guestCount: guestCount ?? 1,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n startTime: range.start,\n })\n if (result.available) {\n availableSlots.push({ end: range.end, start: range.start })\n }\n }\n return availableSlots\n }\n\n // Step by a smaller increment to catch slots between buffer gaps\n const stepSize = Math.min(effectiveDuration, 15)\n\n for (const range of timeRanges) {\n let candidateStart = new Date(range.start)\n\n while (true) {\n const candidateEnd = addMinutes(candidateStart, effectiveDuration)\n if (candidateEnd > range.end) {break}\n\n // 5. Check availability for each candidate slot\n const result = await checkAvailability({\n blockingStatuses,\n bufferAfter,\n bufferBefore,\n endTime: candidateEnd,\n guestCount: guestCount ?? 1,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n startTime: candidateStart,\n })\n\n if (result.available) {\n availableSlots.push({ end: candidateEnd, start: new Date(candidateStart) })\n }\n\n candidateStart = addMinutes(candidateStart, stepSize)\n }\n }\n\n return availableSlots\n}\n"],"names":["resolveScheduleForDate","addMinutes","computeBlockedWindow","computeEndTime","params","durationType","serviceDuration","startTime","end","Date","setHours","durationMinutes","Math","round","getTime","endTime","buildOverlapQuery","blockingStatuses","effectiveEnd","effectiveStart","excludeReservationId","resourceId","conditions","resource","equals","status","in","less_than","toISOString","greater_than","push","id","not_equals","and","isBlockingStatus","statusMachine","includes","validateTransition","fromStatus","toStatus","allowed","transitions","reason","valid","checkAvailability","bufferAfter","bufferBefore","guestCount","payload","req","reservationSlug","resourceSlug","findByID","collection","depth","quantity","capacityMode","where","docs","find","limit","select","currentGuests","reduce","sum","doc","available","currentCount","undefined","totalCapacity","totalDocs","count","getAvailableSlots","date","scheduleSlug","serviceId","serviceSlug","service","duration","bufferTimeBefore","bufferTimeAfter","schedules","active","timeRanges","schedule","ranges","length","slotEndOffset","slotDuration","effectiveDuration","availableSlots","range","result","start","stepSize","min","candidateStart","candidateEnd"],"mappings":"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"}
|
|
@@ -9,7 +9,8 @@ export type ResolvedItem = {
|
|
|
9
9
|
* Normalize reservation data into a list of resource-level items.
|
|
10
10
|
*
|
|
11
11
|
* - If items[] is populated -> return items (filling defaults from parent).
|
|
12
|
-
* Items missing startTime or resource
|
|
12
|
+
* Items missing startTime or resource throw a ValidationError.
|
|
13
|
+
* Duplicate (resource, startTime) pairs throw a ValidationError.
|
|
13
14
|
* - If items[] is empty/absent -> return single item from top-level fields
|
|
14
15
|
*
|
|
15
16
|
* Every downstream function (conflict check, endTime calc, availability)
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { ValidationError } from 'payload';
|
|
1
2
|
/**
|
|
2
3
|
* Normalize reservation data into a list of resource-level items.
|
|
3
4
|
*
|
|
4
5
|
* - If items[] is populated -> return items (filling defaults from parent).
|
|
5
|
-
* Items missing startTime or resource
|
|
6
|
+
* Items missing startTime or resource throw a ValidationError.
|
|
7
|
+
* Duplicate (resource, startTime) pairs throw a ValidationError.
|
|
6
8
|
* - If items[] is empty/absent -> return single item from top-level fields
|
|
7
9
|
*
|
|
8
10
|
* Every downstream function (conflict check, endTime calc, availability)
|
|
@@ -10,13 +12,53 @@
|
|
|
10
12
|
*/ export function resolveReservationItems(data) {
|
|
11
13
|
const items = data.items;
|
|
12
14
|
if (items && items.length > 0) {
|
|
13
|
-
|
|
15
|
+
const resolved = [];
|
|
16
|
+
const seen = new Set();
|
|
17
|
+
for(let i = 0; i < items.length; i++){
|
|
18
|
+
const item = items[i];
|
|
19
|
+
const resource = extractId(item.resource) || extractId(data.resource) || '';
|
|
20
|
+
const startTime = item.startTime ?? data.startTime;
|
|
21
|
+
if (!resource) {
|
|
22
|
+
throw new ValidationError({
|
|
23
|
+
errors: [
|
|
24
|
+
{
|
|
25
|
+
message: `Item ${i} is missing a resource`,
|
|
26
|
+
path: `items.${i}.resource`
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (!startTime) {
|
|
32
|
+
throw new ValidationError({
|
|
33
|
+
errors: [
|
|
34
|
+
{
|
|
35
|
+
message: `Item ${i} is missing a startTime`,
|
|
36
|
+
path: `items.${i}.startTime`
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const key = `${resource}::${startTime}`;
|
|
42
|
+
if (seen.has(key)) {
|
|
43
|
+
throw new ValidationError({
|
|
44
|
+
errors: [
|
|
45
|
+
{
|
|
46
|
+
message: `Duplicate booking: item ${i} has the same resource and startTime as a previous item`,
|
|
47
|
+
path: `items.${i}.startTime`
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
seen.add(key);
|
|
53
|
+
resolved.push({
|
|
14
54
|
endTime: item.endTime ?? data.endTime,
|
|
15
55
|
guestCount: item.guestCount ?? data.guestCount ?? 1,
|
|
16
|
-
resource
|
|
56
|
+
resource,
|
|
17
57
|
service: extractId(item.service) || extractId(data.service) || undefined,
|
|
18
|
-
startTime
|
|
19
|
-
})
|
|
58
|
+
startTime
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return resolved;
|
|
20
62
|
}
|
|
21
63
|
// Single-resource fallback (current behavior)
|
|
22
64
|
if (!data.resource || !data.startTime) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utilities/resolveReservationItems.ts"],"sourcesContent":["
|
|
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) || extractId(data.resource) || ''\n const startTime = (item.startTime as string) ?? (data.startTime as string)\n\n if (!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) || undefined,\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 return [\n {\n endTime: data.endTime as string,\n guestCount: (data.guestCount as number) ?? 1,\n resource: extractId(data.resource) || '',\n service: extractId(data.service) || undefined,\n startTime: data.startTime as string,\n },\n ]\n}\n\nfunction extractId(value: unknown): string | undefined {\n if (typeof value === 'string' && value) {\n return value\n }\n if (value && typeof value === 'object' && 'id' in value) {\n return (value as { id: string }).id\n }\n return undefined\n}\n"],"names":["ValidationError","resolveReservationItems","data","items","length","resolved","seen","Set","i","item","resource","extractId","startTime","errors","message","path","key","has","add","push","endTime","guestCount","service","undefined","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,KAAK;YACzE,MAAME,YAAY,AAACH,KAAKG,SAAS,IAAgBV,KAAKU,SAAS;YAE/D,IAAI,CAACF,UAAU;gBACb,MAAM,IAAIV,gBAAgB;oBACxBa,QAAQ;wBACN;4BACEC,SAAS,CAAC,KAAK,EAAEN,EAAE,sBAAsB,CAAC;4BAC1CO,MAAM,CAAC,MAAM,EAAEP,EAAE,SAAS,CAAC;wBAC7B;qBACD;gBACH;YACF;YAEA,IAAI,CAACI,WAAW;gBACd,MAAM,IAAIZ,gBAAgB;oBACxBa,QAAQ;wBACN;4BACEC,SAAS,CAAC,KAAK,EAAEN,EAAE,uBAAuB,CAAC;4BAC3CO,MAAM,CAAC,MAAM,EAAEP,EAAE,UAAU,CAAC;wBAC9B;qBACD;gBACH;YACF;YAEA,MAAMQ,MAAM,GAAGN,SAAS,EAAE,EAAEE,WAAW;YACvC,IAAIN,KAAKW,GAAG,CAACD,MAAM;gBACjB,MAAM,IAAIhB,gBAAgB;oBACxBa,QAAQ;wBACN;4BACEC,SAAS,CAAC,wBAAwB,EAAEN,EAAE,uDAAuD,CAAC;4BAC9FO,MAAM,CAAC,MAAM,EAAEP,EAAE,UAAU,CAAC;wBAC9B;qBACD;gBACH;YACF;YACAF,KAAKY,GAAG,CAACF;YAETX,SAASc,IAAI,CAAC;gBACZC,SAAS,AAACX,KAAKW,OAAO,IAAgBlB,KAAKkB,OAAO;gBAClDC,YAAY,AAACZ,KAAKY,UAAU,IAAgBnB,KAAKmB,UAAU,IAAe;gBAC1EX;gBACAY,SAASX,UAAUF,KAAKa,OAAO,KAAKX,UAAUT,KAAKoB,OAAO,KAAKC;gBAC/DX;YACF;QACF;QAEA,OAAOP;IACT;IAEA,8CAA8C;IAC9C,IAAI,CAACH,KAAKQ,QAAQ,IAAI,CAACR,KAAKU,SAAS,EAAE;QACrC,OAAO,EAAE;IACX;IAEA,OAAO;QACL;YACEQ,SAASlB,KAAKkB,OAAO;YACrBC,YAAY,AAACnB,KAAKmB,UAAU,IAAe;YAC3CX,UAAUC,UAAUT,KAAKQ,QAAQ,KAAK;YACtCY,SAASX,UAAUT,KAAKoB,OAAO,KAAKC;YACpCX,WAAWV,KAAKU,SAAS;QAC3B;KACD;AACH;AAEA,SAASD,UAAUa,KAAc;IAC/B,IAAI,OAAOA,UAAU,YAAYA,OAAO;QACtC,OAAOA;IACT;IACA,IAAIA,SAAS,OAAOA,UAAU,YAAY,QAAQA,OAAO;QACvD,OAAO,AAACA,MAAyBC,EAAE;IACrC;IACA,OAAOF;AACT"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payload-reserve",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "A Payload CMS 3.x plugin for reservation and booking management with conflict detection, status workflows, and calendar UI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"payload",
|
|
@@ -46,14 +46,15 @@
|
|
|
46
46
|
"dist"
|
|
47
47
|
],
|
|
48
48
|
"devDependencies": {
|
|
49
|
+
"@changesets/cli": "^2.30.0",
|
|
49
50
|
"@eslint/eslintrc": "^3.2.0",
|
|
50
51
|
"@payloadcms/db-mongodb": "3.77.0",
|
|
51
52
|
"@payloadcms/db-postgres": "3.77.0",
|
|
52
53
|
"@payloadcms/db-sqlite": "3.77.0",
|
|
53
54
|
"@payloadcms/eslint-config": "3.9.0",
|
|
54
|
-
"@payloadcms/translations": "3.77.0",
|
|
55
55
|
"@payloadcms/next": "3.77.0",
|
|
56
56
|
"@payloadcms/richtext-lexical": "3.77.0",
|
|
57
|
+
"@payloadcms/translations": "3.77.0",
|
|
57
58
|
"@payloadcms/ui": "3.77.0",
|
|
58
59
|
"@playwright/test": "1.56.1",
|
|
59
60
|
"@swc-node/register": "1.10.9",
|
|
@@ -104,6 +105,8 @@
|
|
|
104
105
|
"generate:types": "pnpm dev:generate-types",
|
|
105
106
|
"lint": "eslint",
|
|
106
107
|
"lint:fix": "eslint ./src --fix",
|
|
108
|
+
"changeset": "changeset",
|
|
109
|
+
"changeset:version": "changeset version",
|
|
107
110
|
"test": "pnpm test:int && pnpm test:e2e",
|
|
108
111
|
"test:e2e": "playwright test",
|
|
109
112
|
"test:int": "vitest"
|