payload-reserve 1.4.0 → 1.6.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.
Files changed (111) hide show
  1. package/README.md +185 -4
  2. package/dist/collections/Reservations.js +47 -2
  3. package/dist/collections/Reservations.js.map +1 -1
  4. package/dist/collections/Resources.d.ts +16 -0
  5. package/dist/collections/Resources.js +35 -10
  6. package/dist/collections/Resources.js.map +1 -1
  7. package/dist/collections/Schedules.js +34 -0
  8. package/dist/collections/Schedules.js.map +1 -1
  9. package/dist/collections/Services.js +34 -1
  10. package/dist/collections/Services.js.map +1 -1
  11. package/dist/components/AvailabilityOverview/index.js +29 -10
  12. package/dist/components/AvailabilityOverview/index.js.map +1 -1
  13. package/dist/components/AvailabilityTimeField/AvailabilityTimeField.module.css +7 -0
  14. package/dist/components/AvailabilityTimeField/index.d.ts +2 -0
  15. package/dist/components/AvailabilityTimeField/index.js +109 -0
  16. package/dist/components/AvailabilityTimeField/index.js.map +1 -0
  17. package/dist/components/CalendarView/CalendarView.module.css +114 -0
  18. package/dist/components/CalendarView/LaneTimelineView.d.ts +12 -0
  19. package/dist/components/CalendarView/LaneTimelineView.js +116 -0
  20. package/dist/components/CalendarView/LaneTimelineView.js.map +1 -0
  21. package/dist/components/CalendarView/index.js +244 -31
  22. package/dist/components/CalendarView/index.js.map +1 -1
  23. package/dist/components/CalendarView/useResourceAvailability.d.ts +9 -0
  24. package/dist/components/CalendarView/useResourceAvailability.js +40 -0
  25. package/dist/components/CalendarView/useResourceAvailability.js.map +1 -0
  26. package/dist/components/DashboardWidget/DashboardWidgetServer.js +20 -6
  27. package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
  28. package/dist/defaults.d.ts +3 -0
  29. package/dist/defaults.js +57 -0
  30. package/dist/defaults.js.map +1 -1
  31. package/dist/endpoints/cancelBooking.js +34 -21
  32. package/dist/endpoints/cancelBooking.js.map +1 -1
  33. package/dist/endpoints/checkAvailability.js +16 -1
  34. package/dist/endpoints/checkAvailability.js.map +1 -1
  35. package/dist/endpoints/createBooking.js +4 -1
  36. package/dist/endpoints/createBooking.js.map +1 -1
  37. package/dist/endpoints/customerSearch.js +24 -5
  38. package/dist/endpoints/customerSearch.js.map +1 -1
  39. package/dist/endpoints/getSlots.js +16 -1
  40. package/dist/endpoints/getSlots.js.map +1 -1
  41. package/dist/endpoints/resourceAvailability.d.ts +43 -0
  42. package/dist/endpoints/resourceAvailability.js +214 -0
  43. package/dist/endpoints/resourceAvailability.js.map +1 -0
  44. package/dist/exports/client.d.ts +1 -0
  45. package/dist/exports/client.js +1 -0
  46. package/dist/exports/client.js.map +1 -1
  47. package/dist/hooks/reservations/calculateEndTime.js +21 -1
  48. package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
  49. package/dist/hooks/reservations/expandRequiredResources.d.ts +9 -0
  50. package/dist/hooks/reservations/expandRequiredResources.js +81 -0
  51. package/dist/hooks/reservations/expandRequiredResources.js.map +1 -0
  52. package/dist/hooks/reservations/validateGuestBooking.d.ts +3 -0
  53. package/dist/hooks/reservations/validateGuestBooking.js +93 -0
  54. package/dist/hooks/reservations/validateGuestBooking.js.map +1 -0
  55. package/dist/hooks/reservations/validateStatusTransition.js +4 -2
  56. package/dist/hooks/reservations/validateStatusTransition.js.map +1 -1
  57. package/dist/hooks/users/provisionStaffResource.d.ts +15 -0
  58. package/dist/hooks/users/provisionStaffResource.js +88 -0
  59. package/dist/hooks/users/provisionStaffResource.js.map +1 -0
  60. package/dist/index.d.ts +5 -1
  61. package/dist/index.js +4 -0
  62. package/dist/index.js.map +1 -1
  63. package/dist/plugin.js +20 -2
  64. package/dist/plugin.js.map +1 -1
  65. package/dist/services/AvailabilityService.d.ts +2 -1
  66. package/dist/services/AvailabilityService.js +86 -60
  67. package/dist/services/AvailabilityService.js.map +1 -1
  68. package/dist/translations/ar.json +156 -0
  69. package/dist/translations/de.json +156 -0
  70. package/dist/translations/en.json +32 -1
  71. package/dist/translations/es.json +156 -0
  72. package/dist/translations/fa.json +156 -0
  73. package/dist/translations/fr.json +156 -0
  74. package/dist/translations/hi.json +156 -0
  75. package/dist/translations/id.json +156 -0
  76. package/dist/translations/index.js +44 -0
  77. package/dist/translations/index.js.map +1 -1
  78. package/dist/translations/pl.json +156 -0
  79. package/dist/translations/ru.json +156 -0
  80. package/dist/translations/tr.json +156 -0
  81. package/dist/translations/zh.json +156 -0
  82. package/dist/types.d.ts +57 -0
  83. package/dist/types.js.map +1 -1
  84. package/dist/utilities/computeSlotStates.d.ts +39 -0
  85. package/dist/utilities/computeSlotStates.js +49 -0
  86. package/dist/utilities/computeSlotStates.js.map +1 -0
  87. package/dist/utilities/guestBooking.d.ts +10 -0
  88. package/dist/utilities/guestBooking.js +16 -0
  89. package/dist/utilities/guestBooking.js.map +1 -0
  90. package/dist/utilities/resolveRequiredResources.d.ts +8 -0
  91. package/dist/utilities/resolveRequiredResources.js +27 -0
  92. package/dist/utilities/resolveRequiredResources.js.map +1 -0
  93. package/dist/utilities/scheduleUtils.d.ts +3 -0
  94. package/dist/utilities/scheduleUtils.js +5 -3
  95. package/dist/utilities/scheduleUtils.js.map +1 -1
  96. package/dist/utilities/selectOptions.d.ts +8 -0
  97. package/dist/utilities/selectOptions.js +11 -0
  98. package/dist/utilities/selectOptions.js.map +1 -0
  99. package/dist/utilities/slotUtils.d.ts +19 -0
  100. package/dist/utilities/slotUtils.js +28 -0
  101. package/dist/utilities/slotUtils.js.map +1 -1
  102. package/dist/utilities/tenantFilter.d.ts +25 -0
  103. package/dist/utilities/tenantFilter.js +56 -0
  104. package/dist/utilities/tenantFilter.js.map +1 -0
  105. package/dist/utilities/useTenantFilter.d.ts +6 -0
  106. package/dist/utilities/useTenantFilter.js +28 -0
  107. package/dist/utilities/useTenantFilter.js.map +1 -0
  108. package/dist/utilities/userRoles.d.ts +20 -0
  109. package/dist/utilities/userRoles.js +32 -0
  110. package/dist/utilities/userRoles.js.map +1 -0
  111. package/package.json +3 -1
package/README.md CHANGED
@@ -1,9 +1,15 @@
1
1
  # payload-reserve
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/payload-reserve.svg)](https://www.npmjs.com/package/payload-reserve)
4
+ [![npm downloads](https://img.shields.io/npm/dm/payload-reserve.svg)](https://www.npmjs.com/package/payload-reserve)
5
+ [![license](https://img.shields.io/npm/l/payload-reserve.svg)](./LICENSE)
6
+
3
7
  A full-featured reservation and booking plugin for Payload CMS 3.x. Adds a scheduling system with conflict detection, a configurable status machine, multi-resource bookings, capacity and inventory tracking, a public REST API, and admin UI components.
4
8
 
5
9
  Designed for salons, clinics, hotels, restaurants, event venues, and any business that needs appointment scheduling managed through Payload's admin panel.
6
10
 
11
+ 📦 **npm:** https://www.npmjs.com/package/payload-reserve
12
+
7
13
  ---
8
14
 
9
15
  ## Features
@@ -17,17 +23,19 @@ Designed for salons, clinics, hotels, restaurants, event venues, and any busines
17
23
  - **Three Duration Types** — `fixed` (service duration), `flexible` (customer-specified end), and `full-day` bookings
18
24
  - **Multi-Resource Bookings** — Single reservation that spans multiple resources simultaneously via the `items` array
19
25
  - **Capacity and Inventory** — `quantity > 1` allows multiple concurrent bookings per resource; `capacityMode` (`per-reservation` | `per-guest`) controls how capacity is counted
26
+ - **Guest Bookings** — Account-less reservations with inline contact details (name + email/phone); `allowGuestBooking` plugin option and per-service `inherit`/`enabled`/`disabled` override; guests receive a `cancellationToken` via the `afterBookingCreate` hook for cancel-link delivery
20
27
  - **Idempotency** — Optional `idempotencyKey` prevents duplicate submissions
21
28
  - **Extra Reservation Fields** — Inject custom fields into the Reservations collection via `extraReservationFields` without forking the plugin
22
29
  - **Cancellation Policy** — Configurable minimum notice period enforcement
23
30
  - **Plugin Hooks API** — Seven lifecycle hooks (`beforeBookingCreate`, `afterBookingCreate`, `beforeBookingConfirm`, `afterBookingConfirm`, `beforeBookingCancel`, `afterBookingCancel`, `afterStatusChange`) for integrating email, Stripe, and external systems
24
31
  - **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
- - **Calendar View** — Month/week/day calendar replacing the default reservations list view
32
+ - **Public REST API** — Six pre-built endpoints for availability, slot listing, resource availability, booking (incl. guest bookings), cancellation, and customer search — with ownership enforcement and input validation
33
+ - **Calendar View** — Month/week/day/lanes/pending calendar replacing the default reservations list view, with per-resource availability shading and click-a-free-slot-to-book; plus an availability-aware slot picker on the reservation form
27
34
  - **Dashboard Widget** — Server component showing today's booking stats
28
35
  - **Availability Overview** — Weekly grid of resource availability vs. booked slots
29
36
  - **Recurring and Manual Schedules** — Weekly patterns with exception dates, or specific one-off dates
30
- - **Localization Support** — Collection fields can be localized when Payload localization is enabled
37
+ - **12 Bundled Languages** — Every admin string is translatable; ships with English, French, German, Spanish, Russian, Polish, Turkish, Arabic, Simplified Chinese, Indonesian, Persian/Farsi, and Hindi. Override any string or add your own language
38
+ - **Localization Support** — Collection field *content* can be localized when Payload localization is enabled (separate from the admin-UI language above)
31
39
  - **Type-Safe** — Full TypeScript support with exported types
32
40
 
33
41
  ---
@@ -88,6 +96,178 @@ The `access` override in plugin config always takes precedence over the auto-wir
88
96
 
89
97
  ---
90
98
 
99
+ ## Guest Bookings
100
+
101
+ Enable `allowGuestBooking` to accept reservations from users who don't have a customer account. Guests provide inline contact details (name + email or phone) instead of linking to a customer record.
102
+
103
+ ```typescript
104
+ payloadReserve({
105
+ allowGuestBooking: true, // enable guest bookings globally (default: false)
106
+ })
107
+ ```
108
+
109
+ ### Per-service override
110
+
111
+ Each Service has its own `allowGuestBooking` select field that overrides the plugin-level default:
112
+
113
+ | Value | Behaviour |
114
+ |-------|-----------|
115
+ | `inherit` | Use the plugin-level `allowGuestBooking` value *(default)* |
116
+ | `enabled` | Allow guest bookings for this service regardless of the global setting |
117
+ | `disabled` | Require a customer account for this service regardless of the global setting |
118
+
119
+ > **Note:** For multi-resource bookings (the `items` array), the guest-booking gate is evaluated against the reservation's top-level `service`. Per-item service overrides are not individually enforced.
120
+
121
+ ### Customer vs. guest
122
+
123
+ The `customer` relationship field on Reservations is now **optional**. A reservation must have **either** a `customer` **or** a `guest` block — not both, not neither.
124
+
125
+ The `guest` block requires `name` and at least one of `email` or `phone`:
126
+
127
+ ```typescript
128
+ // POST /api/reserve/book
129
+ {
130
+ "service": "...",
131
+ "resource": "...",
132
+ "startTime": "2026-06-01T10:00:00.000Z",
133
+ "guest": {
134
+ "name": "Jane Smith",
135
+ "email": "jane@example.com" // or "phone": "+1-555-0100"
136
+ }
137
+ }
138
+ ```
139
+
140
+ ### Cancellation token
141
+
142
+ When a guest booking is created the plugin generates a `cancellationToken` (random UUID). It is **not** returned in the `/api/reserve/book` HTTP response. It is exposed server-side via the `afterBookingCreate` plugin hook so the host project can deliver a cancel link by email or an SMS code:
143
+
144
+ ```typescript
145
+ payloadReserve({
146
+ allowGuestBooking: true,
147
+ hooks: {
148
+ afterBookingCreate: [
149
+ async ({ doc, req }) => {
150
+ if (doc.guest && doc.cancellationToken) {
151
+ // Send the token however you like — the plugin sends nothing itself
152
+ await sendEmail({
153
+ to: doc.guest.email,
154
+ cancelUrl: `https://example.com/cancel?reservationId=${doc.id}&token=${doc.cancellationToken}`,
155
+ })
156
+ }
157
+ },
158
+ ],
159
+ },
160
+ })
161
+ ```
162
+
163
+ To cancel with the token, POST to `/api/reserve/cancel` without authentication:
164
+
165
+ ```typescript
166
+ // POST /api/reserve/cancel
167
+ {
168
+ "reservationId": "...",
169
+ "token": "<cancellationToken>"
170
+ }
171
+ ```
172
+
173
+ Authenticated owner/admin cancellation (without a token) is unchanged.
174
+
175
+ ## Staff Scheduling
176
+
177
+ ### Staff Auto-Provisioning
178
+
179
+ Enable `staffProvisioning` to automatically create an owner-scoped Resource whenever a user gains a staff role. Requires `resourceOwnerMode`.
180
+
181
+ ```typescript
182
+ payloadReserve({
183
+ userCollection: 'users',
184
+ resourceOwnerMode: {
185
+ adminRoles: ['admin'],
186
+ },
187
+ staffProvisioning: {
188
+ staffRoles: ['staff', 'therapist'], // roles that trigger auto-provisioning
189
+ roleField: 'role', // field on the user doc (default: 'role')
190
+ resourceType: 'staff', // resourceType stamped on the new Resource (default: 'staff')
191
+ nameFrom: 'name', // user field to use as Resource name (default: 'name', falls back to email)
192
+ },
193
+ })
194
+ ```
195
+
196
+ **Use `beforeCreate` to stamp tenant IDs or custom fields** before the Resource is saved:
197
+
198
+ ```typescript
199
+ staffProvisioning: {
200
+ staffRoles: ['staff'],
201
+ beforeCreate: ({ data, user }) => ({
202
+ ...data,
203
+ tenant: user.tenant, // forward the user's tenant to the new Resource
204
+ }),
205
+ },
206
+ ```
207
+
208
+ **Key behaviours:**
209
+
210
+ - **Idempotent** — deduplicates by owner; creating or re-saving a staff user never creates a second Resource.
211
+ - **Non-blocking** — provisioning failures are logged and do not prevent user creation or update.
212
+ - **Impersonation-based ownership** — the Resource is created as the new staff user, so `resource.owner` is always the user themselves (no ownership-bypass flag).
213
+ - **No auto-delete on demotion** — removing a staff role from a user does not delete their Resource.
214
+
215
+ ### Full-Day-Range Time-Off
216
+
217
+ Schedule exceptions now support a date range and a leave type:
218
+
219
+ ```typescript
220
+ // Schedule.exceptions[] — each entry can have:
221
+ {
222
+ date: '2025-12-25', // start date (always required)
223
+ endDate: '2025-12-26', // optional range end, inclusive
224
+ type: 'vacation', // optional leave category
225
+ }
226
+ ```
227
+
228
+ Any date falling within the range (inclusive) makes the resource fully unavailable for that day. This powers multi-day leave entries for staff schedules.
229
+
230
+ ### Configurable Vocabularies
231
+
232
+ Customize the option lists for resource types and leave types:
233
+
234
+ ```typescript
235
+ payloadReserve({
236
+ resourceTypes: ['staff', 'room', 'equipment', 'vehicle'], // default: ['staff','equipment','room']
237
+ leaveTypes: ['vacation', 'sick', 'training', 'closure'], // default: ['vacation','sick','personal','closure','other']
238
+ })
239
+ ```
240
+
241
+ The first entry of `resourceTypes` becomes the default value for the `Resource.resourceType` field.
242
+
243
+ ### Optional `Resource.services`
244
+
245
+ The `services` relationship on Resources is now optional. This lets a freshly provisioned staff Resource exist before services are assigned, avoiding validation errors during auto-provisioning.
246
+
247
+ ---
248
+
249
+ ## Internationalization
250
+
251
+ Every admin string the plugin renders — field labels, descriptions, select options, calendar/dashboard components, and validation errors — is translatable. The plugin ships **12 languages**: English, French (`fr`), German (`de`), Spanish (`es`), Russian (`ru`), Polish (`pl`), Turkish (`tr`), Arabic (`ar`), Simplified Chinese (`zh`), Indonesian (`id`), Persian/Farsi (`fa`), and Hindi (`hi`). All but Hindi ship in Payload core and appear in the admin language switcher automatically.
252
+
253
+ Translations merge into your config and **your translations take precedence**, so you can override any string or add a language:
254
+
255
+ ```typescript
256
+ payloadReserve()
257
+ // and in buildConfig:
258
+ i18n: {
259
+ translations: {
260
+ en: { reservation: { calendarLanes: 'Timeline' } }, // override a plugin string
261
+ },
262
+ }
263
+ ```
264
+
265
+ > **Hindi** (`hi`) is not bundled by Payload core, so register it as a custom language in your Payload `i18n` config to make it selectable — the plugin's `hi` strings then appear automatically. See the [Internationalization docs](https://github.com/elghaied/payload-reserve/blob/main/docs/i18n.md).
266
+
267
+ This is separate from Payload **field localization** (localizing the *content* of fields), which the plugin's fields also support when Payload localization is enabled.
268
+
269
+ ---
270
+
91
271
  ## Documentation
92
272
 
93
273
  > The docs below live in the [GitHub repository](https://github.com/elghaied/payload-reserve/tree/main/docs) and are not included in the published npm package.
@@ -100,8 +280,9 @@ The `access` override in plugin config always takes precedence over the auto-wir
100
280
  | [Status Machine](https://github.com/elghaied/payload-reserve/blob/main/docs/status-machine.md) | Default flow, custom machines, business logic hooks, escape hatch |
101
281
  | [Booking Features](https://github.com/elghaied/payload-reserve/blob/main/docs/booking-features.md) | Duration types, multi-resource bookings, capacity modes |
102
282
  | [Hooks API](https://github.com/elghaied/payload-reserve/blob/main/docs/hooks-api.md) | All 7 plugin hook types with signatures and examples |
103
- | [REST API](https://github.com/elghaied/payload-reserve/blob/main/docs/rest-api.md) | All 5 public endpoints with params, responses, and fetch examples |
283
+ | [REST API](https://github.com/elghaied/payload-reserve/blob/main/docs/rest-api.md) | All 6 public endpoints with params, responses, and fetch examples |
104
284
  | [Admin UI](https://github.com/elghaied/payload-reserve/blob/main/docs/admin-ui.md) | Calendar view, dashboard widget, availability overview |
285
+ | [Internationalization](https://github.com/elghaied/payload-reserve/blob/main/docs/i18n.md) | 12 bundled languages, overriding strings, adding a language, Hindi setup |
105
286
  | [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
287
  | [Advanced](https://github.com/elghaied/payload-reserve/blob/main/docs/advanced.md) | DB indexes, reconciliation job for race condition detection |
107
288
  | [Development](https://github.com/elghaied/payload-reserve/blob/main/docs/development.md) | Prerequisites, commands, project file tree |
@@ -1,8 +1,10 @@
1
1
  import { calculateEndTime } from '../hooks/reservations/calculateEndTime.js';
2
2
  import { checkIdempotency } from '../hooks/reservations/checkIdempotency.js';
3
+ import { expandRequiredResources } from '../hooks/reservations/expandRequiredResources.js';
3
4
  import { onStatusChange } from '../hooks/reservations/onStatusChange.js';
4
5
  import { validateCancellation } from '../hooks/reservations/validateCancellation.js';
5
6
  import { validateConflicts } from '../hooks/reservations/validateConflicts.js';
7
+ import { validateGuestBooking } from '../hooks/reservations/validateGuestBooking.js';
6
8
  import { validateStatusTransition } from '../hooks/reservations/validateStatusTransition.js';
7
9
  import { statusToI18nKey } from '../utilities/i18nUtils.js';
8
10
  import { makeReservationOwnerAccess } from '../utilities/ownerAccess.js';
@@ -89,12 +91,53 @@ export function createReservationsCollection(config) {
89
91
  },
90
92
  label: ({ t })=>t('reservation:fieldCustomer'),
91
93
  relationTo: config.slugs.customers,
92
- required: true
94
+ required: false
95
+ },
96
+ {
97
+ name: 'guest',
98
+ type: 'group',
99
+ admin: {
100
+ description: ({ t })=>t('reservation:fieldGuestDesc')
101
+ },
102
+ fields: [
103
+ {
104
+ name: 'name',
105
+ type: 'text',
106
+ label: ({ t })=>t('reservation:fieldGuestName'),
107
+ maxLength: 200
108
+ },
109
+ {
110
+ name: 'email',
111
+ type: 'email',
112
+ label: ({ t })=>t('reservation:fieldGuestEmail')
113
+ },
114
+ {
115
+ name: 'phone',
116
+ type: 'text',
117
+ label: ({ t })=>t('reservation:fieldGuestPhone'),
118
+ maxLength: 50
119
+ }
120
+ ],
121
+ label: ({ t })=>t('reservation:fieldGuest')
122
+ },
123
+ {
124
+ name: 'cancellationToken',
125
+ type: 'text',
126
+ access: {
127
+ read: ({ req })=>Boolean(req.user) && req.user.collection !== config.slugs.customers
128
+ },
129
+ admin: {
130
+ hidden: true
131
+ },
132
+ index: true
93
133
  },
94
134
  {
95
135
  name: 'startTime',
96
136
  type: 'date',
97
137
  admin: {
138
+ components: {
139
+ Field: 'payload-reserve/client#AvailabilityTimeField'
140
+ },
98
141
  date: {
99
142
  pickerAppearance: 'dayAndTime'
100
143
  }
@@ -151,7 +194,7 @@ export function createReservationsCollection(config) {
151
194
  name: 'items',
152
195
  type: 'array',
153
196
  admin: {
154
- description: 'Resources included in this booking. Leave empty for single-resource bookings.'
197
+ description: ({ t })=>t('reservation:fieldItemsDesc')
155
198
  },
156
199
  fields: [
157
200
  {
@@ -217,6 +260,8 @@ export function createReservationsCollection(config) {
217
260
  beforeChange: [
218
261
  createPluginHooksBeforeCreate(config.hooks),
219
262
  checkIdempotency(config),
263
+ validateGuestBooking(config),
264
+ expandRequiredResources(config),
220
265
  calculateEndTime(config),
221
266
  validateConflicts(config),
222
267
  validateStatusTransition(config),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/collections/Reservations.ts"],"sourcesContent":["import type {\n CollectionAfterChangeHook,\n CollectionBeforeChangeHook,\n CollectionConfig,\n CollectionSlug,\n} from 'payload'\n\nimport type { PluginT } from '../translations/index.js'\nimport type { ReservationPluginHooks, ResolvedReservationPluginConfig } from '../types.js'\n\nimport { calculateEndTime } from '../hooks/reservations/calculateEndTime.js'\nimport { checkIdempotency } from '../hooks/reservations/checkIdempotency.js'\nimport { onStatusChange } from '../hooks/reservations/onStatusChange.js'\nimport { validateCancellation } from '../hooks/reservations/validateCancellation.js'\nimport { validateConflicts } from '../hooks/reservations/validateConflicts.js'\nimport { validateStatusTransition } from '../hooks/reservations/validateStatusTransition.js'\nimport { statusToI18nKey } from '../utilities/i18nUtils.js'\nimport { makeReservationOwnerAccess } from '../utilities/ownerAccess.js'\n\nfunction createPluginHooksBeforeCreate(\n hooks: ReservationPluginHooks,\n): CollectionBeforeChangeHook {\n return async ({ context, data, operation, req }) => {\n if (context?.skipReservationHooks) {return data}\n\n if (operation === 'create' && hooks.beforeBookingCreate) {\n let mutatedData = data\n for (const hook of hooks.beforeBookingCreate) {\n const result = await hook({ data: mutatedData, req })\n if (result) {mutatedData = result}\n }\n return mutatedData\n }\n\n return data\n }\n}\n\nfunction createPluginHooksAfterCreate(\n hooks: ReservationPluginHooks,\n): CollectionAfterChangeHook {\n return async ({ doc, operation, req }) => {\n if (operation === 'create' && hooks.afterBookingCreate) {\n const docRecord = doc as Record<string, unknown>\n for (const hook of hooks.afterBookingCreate) {\n await hook({ doc: docRecord, req })\n }\n }\n return doc\n }\n}\n\nexport function createReservationsCollection(\n config: ResolvedReservationPluginConfig,\n): CollectionConfig {\n const { statusMachine } = config\n const rom = config.resourceOwnerMode\n const access =\n config.access.reservations ?? (rom ? makeReservationOwnerAccess(rom) : {})\n\n return {\n slug: config.slugs.reservations,\n access,\n admin: {\n components: {\n views: {\n list: {\n Component: 'payload-reserve/client#CalendarView',\n },\n },\n },\n group: config.adminGroup,\n listSearchableFields: ['status'],\n useAsTitle: 'startTime',\n },\n fields: [\n {\n name: 'service',\n type: 'relationship',\n label: ({ t }) => (t as PluginT)('reservation:fieldService'),\n relationTo: config.slugs.services as unknown as CollectionSlug,\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: 'customer',\n type: 'relationship',\n admin: {\n allowCreate: true,\n allowEdit: true,\n components: {\n Field: 'payload-reserve/client#CustomerField',\n },\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldCustomer'),\n relationTo: config.slugs.customers as unknown as CollectionSlug,\n required: true,\n },\n {\n name: 'startTime',\n type: 'date',\n admin: {\n date: {\n pickerAppearance: 'dayAndTime',\n },\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldStartTime'),\n required: true,\n },\n {\n name: 'endTime',\n type: 'date',\n admin: {\n date: {\n pickerAppearance: 'dayAndTime',\n },\n readOnly: true,\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldEndTime'),\n },\n {\n name: 'status',\n type: 'select',\n defaultValue: statusMachine.defaultStatus,\n label: ({ t }) => (t as PluginT)('reservation:fieldStatus'),\n options: statusMachine.statuses.map((s) => ({\n label: ({ t }) => {\n const key = statusToI18nKey(s)\n const translated = (t as PluginT)(key)\n return translated !== key ? translated : s.charAt(0).toUpperCase() + s.slice(1)\n },\n value: s,\n })),\n },\n {\n name: 'cancellationReason',\n type: 'textarea',\n admin: {\n condition: (_, siblingData) => siblingData?.status === 'cancelled',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldCancellationReason'),\n },\n {\n name: 'guestCount',\n type: 'number',\n defaultValue: 1,\n label: ({ t }) => (t as PluginT)('reservation:fieldGuestCount'),\n min: 1,\n },\n {\n name: 'notes',\n type: 'textarea',\n label: ({ t }) => (t as PluginT)('reservation:fieldNotes'),\n },\n {\n name: 'items',\n type: 'array',\n admin: {\n description: 'Resources included in this booking. Leave empty for single-resource bookings.',\n },\n fields: [\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: 'service',\n type: 'relationship',\n label: ({ t }) => (t as PluginT)('reservation:fieldService'),\n relationTo: config.slugs.services as unknown as CollectionSlug,\n },\n {\n name: 'startTime',\n type: 'date',\n admin: { date: { pickerAppearance: 'dayAndTime' } },\n label: ({ t }) => (t as PluginT)('reservation:fieldStartTime'),\n },\n {\n name: 'endTime',\n type: 'date',\n admin: { date: { pickerAppearance: 'dayAndTime' }, readOnly: false },\n label: ({ t }) => (t as PluginT)('reservation:fieldEndTime'),\n },\n {\n name: 'guestCount',\n type: 'number',\n label: ({ t }) => (t as PluginT)('reservation:fieldGuestCount'),\n min: 1,\n },\n ],\n label: ({ t }) => (t as PluginT)('reservation:fieldItems'),\n },\n {\n name: 'idempotencyKey',\n type: 'text',\n admin: { position: 'sidebar', readOnly: true },\n index: true,\n unique: true,\n },\n ...config.extraReservationFields,\n ],\n hooks: {\n afterChange: [\n createPluginHooksAfterCreate(config.hooks),\n onStatusChange(config),\n ],\n beforeChange: [\n createPluginHooksBeforeCreate(config.hooks),\n checkIdempotency(config),\n calculateEndTime(config),\n validateConflicts(config),\n validateStatusTransition(config),\n validateCancellation(config),\n ],\n },\n labels: {\n plural: ({ t }) => (t as PluginT)('reservation:collectionReservations'),\n singular: ({ t }) => (t as PluginT)('reservation:collectionReservations'),\n },\n }\n}\n"],"names":["calculateEndTime","checkIdempotency","onStatusChange","validateCancellation","validateConflicts","validateStatusTransition","statusToI18nKey","makeReservationOwnerAccess","createPluginHooksBeforeCreate","hooks","context","data","operation","req","skipReservationHooks","beforeBookingCreate","mutatedData","hook","result","createPluginHooksAfterCreate","doc","afterBookingCreate","docRecord","createReservationsCollection","config","statusMachine","rom","resourceOwnerMode","access","reservations","slug","slugs","admin","components","views","list","Component","group","adminGroup","listSearchableFields","useAsTitle","fields","name","type","label","t","relationTo","services","required","resources","allowCreate","allowEdit","Field","customers","date","pickerAppearance","readOnly","defaultValue","defaultStatus","options","statuses","map","s","key","translated","charAt","toUpperCase","slice","value","condition","_","siblingData","status","min","description","position","index","unique","extraReservationFields","afterChange","beforeChange","labels","plural","singular"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/collections/Reservations.ts"],"sourcesContent":["import type {\n CollectionAfterChangeHook,\n CollectionBeforeChangeHook,\n CollectionConfig,\n CollectionSlug,\n} from 'payload'\n\nimport type { PluginT } from '../translations/index.js'\nimport type { ReservationPluginHooks, ResolvedReservationPluginConfig } from '../types.js'\n\nimport { calculateEndTime } from '../hooks/reservations/calculateEndTime.js'\nimport { checkIdempotency } from '../hooks/reservations/checkIdempotency.js'\nimport { expandRequiredResources } from '../hooks/reservations/expandRequiredResources.js'\nimport { onStatusChange } from '../hooks/reservations/onStatusChange.js'\nimport { validateCancellation } from '../hooks/reservations/validateCancellation.js'\nimport { validateConflicts } from '../hooks/reservations/validateConflicts.js'\nimport { validateGuestBooking } from '../hooks/reservations/validateGuestBooking.js'\nimport { validateStatusTransition } from '../hooks/reservations/validateStatusTransition.js'\nimport { statusToI18nKey } from '../utilities/i18nUtils.js'\nimport { makeReservationOwnerAccess } from '../utilities/ownerAccess.js'\n\nfunction createPluginHooksBeforeCreate(\n hooks: ReservationPluginHooks,\n): CollectionBeforeChangeHook {\n return async ({ context, data, operation, req }) => {\n if (context?.skipReservationHooks) {return data}\n\n if (operation === 'create' && hooks.beforeBookingCreate) {\n let mutatedData = data\n for (const hook of hooks.beforeBookingCreate) {\n const result = await hook({ data: mutatedData, req })\n if (result) {mutatedData = result}\n }\n return mutatedData\n }\n\n return data\n }\n}\n\nfunction createPluginHooksAfterCreate(\n hooks: ReservationPluginHooks,\n): CollectionAfterChangeHook {\n return async ({ doc, operation, req }) => {\n if (operation === 'create' && hooks.afterBookingCreate) {\n const docRecord = doc as Record<string, unknown>\n for (const hook of hooks.afterBookingCreate) {\n await hook({ doc: docRecord, req })\n }\n }\n return doc\n }\n}\n\nexport function createReservationsCollection(\n config: ResolvedReservationPluginConfig,\n): CollectionConfig {\n const { statusMachine } = config\n const rom = config.resourceOwnerMode\n const access =\n config.access.reservations ?? (rom ? makeReservationOwnerAccess(rom) : {})\n\n return {\n slug: config.slugs.reservations,\n access,\n admin: {\n components: {\n views: {\n list: {\n Component: 'payload-reserve/client#CalendarView',\n },\n },\n },\n group: config.adminGroup,\n listSearchableFields: ['status'],\n useAsTitle: 'startTime',\n },\n fields: [\n {\n name: 'service',\n type: 'relationship',\n label: ({ t }) => (t as PluginT)('reservation:fieldService'),\n relationTo: config.slugs.services as unknown as CollectionSlug,\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: 'customer',\n type: 'relationship',\n admin: {\n allowCreate: true,\n allowEdit: true,\n components: {\n Field: 'payload-reserve/client#CustomerField',\n },\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldCustomer'),\n relationTo: config.slugs.customers as unknown as CollectionSlug,\n required: false,\n },\n {\n name: 'guest',\n type: 'group',\n admin: {\n description: ({ t }) => (t as PluginT)('reservation:fieldGuestDesc'),\n },\n fields: [\n {\n name: 'name',\n type: 'text',\n label: ({ t }) => (t as PluginT)('reservation:fieldGuestName'),\n maxLength: 200,\n },\n {\n name: 'email',\n type: 'email',\n label: ({ t }) => (t as PluginT)('reservation:fieldGuestEmail'),\n },\n {\n name: 'phone',\n type: 'text',\n label: ({ t }) => (t as PluginT)('reservation:fieldGuestPhone'),\n maxLength: 50,\n },\n ],\n label: ({ t }) => (t as PluginT)('reservation:fieldGuest'),\n },\n {\n name: 'cancellationToken',\n type: 'text',\n access: {\n read: ({ req }) =>\n Boolean(req.user) && req.user!.collection !== config.slugs.customers,\n },\n admin: {\n hidden: true,\n },\n index: true,\n },\n {\n name: 'startTime',\n type: 'date',\n admin: {\n components: {\n Field: 'payload-reserve/client#AvailabilityTimeField',\n },\n date: {\n pickerAppearance: 'dayAndTime',\n },\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldStartTime'),\n required: true,\n },\n {\n name: 'endTime',\n type: 'date',\n admin: {\n date: {\n pickerAppearance: 'dayAndTime',\n },\n readOnly: true,\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldEndTime'),\n },\n {\n name: 'status',\n type: 'select',\n defaultValue: statusMachine.defaultStatus,\n label: ({ t }) => (t as PluginT)('reservation:fieldStatus'),\n options: statusMachine.statuses.map((s) => ({\n label: ({ t }) => {\n const key = statusToI18nKey(s)\n const translated = (t as PluginT)(key)\n return translated !== key ? translated : s.charAt(0).toUpperCase() + s.slice(1)\n },\n value: s,\n })),\n },\n {\n name: 'cancellationReason',\n type: 'textarea',\n admin: {\n condition: (_, siblingData) => siblingData?.status === 'cancelled',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldCancellationReason'),\n },\n {\n name: 'guestCount',\n type: 'number',\n defaultValue: 1,\n label: ({ t }) => (t as PluginT)('reservation:fieldGuestCount'),\n min: 1,\n },\n {\n name: 'notes',\n type: 'textarea',\n label: ({ t }) => (t as PluginT)('reservation:fieldNotes'),\n },\n {\n name: 'items',\n type: 'array',\n admin: {\n description: ({ t }) => (t as PluginT)('reservation:fieldItemsDesc'),\n },\n fields: [\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: 'service',\n type: 'relationship',\n label: ({ t }) => (t as PluginT)('reservation:fieldService'),\n relationTo: config.slugs.services as unknown as CollectionSlug,\n },\n {\n name: 'startTime',\n type: 'date',\n admin: { date: { pickerAppearance: 'dayAndTime' } },\n label: ({ t }) => (t as PluginT)('reservation:fieldStartTime'),\n },\n {\n name: 'endTime',\n type: 'date',\n admin: { date: { pickerAppearance: 'dayAndTime' }, readOnly: false },\n label: ({ t }) => (t as PluginT)('reservation:fieldEndTime'),\n },\n {\n name: 'guestCount',\n type: 'number',\n label: ({ t }) => (t as PluginT)('reservation:fieldGuestCount'),\n min: 1,\n },\n ],\n label: ({ t }) => (t as PluginT)('reservation:fieldItems'),\n },\n {\n name: 'idempotencyKey',\n type: 'text',\n admin: { position: 'sidebar', readOnly: true },\n index: true,\n unique: true,\n },\n ...config.extraReservationFields,\n ],\n hooks: {\n afterChange: [\n createPluginHooksAfterCreate(config.hooks),\n onStatusChange(config),\n ],\n beforeChange: [\n createPluginHooksBeforeCreate(config.hooks),\n checkIdempotency(config),\n validateGuestBooking(config),\n expandRequiredResources(config),\n calculateEndTime(config),\n validateConflicts(config),\n validateStatusTransition(config),\n validateCancellation(config),\n ],\n },\n labels: {\n plural: ({ t }) => (t as PluginT)('reservation:collectionReservations'),\n singular: ({ t }) => (t as PluginT)('reservation:collectionReservations'),\n },\n }\n}\n"],"names":["calculateEndTime","checkIdempotency","expandRequiredResources","onStatusChange","validateCancellation","validateConflicts","validateGuestBooking","validateStatusTransition","statusToI18nKey","makeReservationOwnerAccess","createPluginHooksBeforeCreate","hooks","context","data","operation","req","skipReservationHooks","beforeBookingCreate","mutatedData","hook","result","createPluginHooksAfterCreate","doc","afterBookingCreate","docRecord","createReservationsCollection","config","statusMachine","rom","resourceOwnerMode","access","reservations","slug","slugs","admin","components","views","list","Component","group","adminGroup","listSearchableFields","useAsTitle","fields","name","type","label","t","relationTo","services","required","resources","allowCreate","allowEdit","Field","customers","description","maxLength","read","Boolean","user","collection","hidden","index","date","pickerAppearance","readOnly","defaultValue","defaultStatus","options","statuses","map","s","key","translated","charAt","toUpperCase","slice","value","condition","_","siblingData","status","min","position","unique","extraReservationFields","afterChange","beforeChange","labels","plural","singular"],"mappings":"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"}
@@ -1,3 +1,19 @@
1
1
  import type { CollectionConfig } from 'payload';
2
2
  import type { ResolvedReservationPluginConfig } from '../types.js';
3
+ /**
4
+ * Owner-field beforeChange logic, extracted for testability. On create the
5
+ * owner defaults to the requesting user; on other operations the existing
6
+ * value is preserved. Staff provisioning assigns the correct owner by creating
7
+ * the Resource AS the new staff user (impersonation in the provisioning hook),
8
+ * so this function needs no special-case branch.
9
+ */
10
+ export declare function resolveOwnerValue({ operation, req, value, }: {
11
+ operation: string | undefined;
12
+ req: {
13
+ user?: {
14
+ id: unknown;
15
+ } | null;
16
+ };
17
+ value: unknown;
18
+ }): unknown;
3
19
  export declare function createResourcesCollection(config: ResolvedReservationPluginConfig): CollectionConfig;
@@ -1,7 +1,24 @@
1
1
  import { makeResourceOwnerAccess } from '../utilities/ownerAccess.js';
2
+ import { buildSelectOptions } from '../utilities/selectOptions.js';
3
+ /**
4
+ * Owner-field beforeChange logic, extracted for testability. On create the
5
+ * owner defaults to the requesting user; on other operations the existing
6
+ * value is preserved. Staff provisioning assigns the correct owner by creating
7
+ * the Resource AS the new staff user (impersonation in the provisioning hook),
8
+ * so this function needs no special-case branch.
9
+ */ export function resolveOwnerValue({ operation, req, value }) {
10
+ if (operation === 'create' && req.user) {
11
+ return req.user.id;
12
+ }
13
+ return value;
14
+ }
2
15
  export function createResourcesCollection(config) {
3
16
  const rom = config.resourceOwnerMode;
4
17
  const ownerField = rom?.ownerField ?? 'owner';
18
+ // The owner relationship points to where owners/staff live: an explicit
19
+ // ownerCollection, else the staff-provisioning user collection, else customers.
20
+ // (Previously hardcoded to customers, which broke separate users/customers setups.)
21
+ const ownerCollection = rom?.ownerCollection ?? config.staffProvisioning?.userCollection ?? config.slugs.customers;
5
22
  // Build the owner field when resourceOwnerMode is enabled
6
23
  const ownerFieldDef = rom ? {
7
24
  name: ownerField,
@@ -11,16 +28,15 @@ export function createResourcesCollection(config) {
11
28
  },
12
29
  hooks: {
13
30
  beforeChange: [
14
- ({ operation, req, value })=>{
15
- if (operation === 'create' && req.user) {
16
- return req.user.id;
17
- }
18
- return value;
19
- }
31
+ ({ operation, req, value })=>resolveOwnerValue({
32
+ operation,
33
+ req,
34
+ value
35
+ })
20
36
  ]
21
37
  },
22
- label: 'Owner',
23
- relationTo: config.slugs.customers,
38
+ label: ({ t })=>t('reservation:fieldOwner'),
39
+ relationTo: ownerCollection,
24
40
  required: true
25
41
  } : null;
26
42
  // Determine access: app override → owner-mode auto-wired → unrestricted
@@ -62,8 +78,7 @@ export function createResourcesCollection(config) {
62
78
  type: 'relationship',
63
79
  hasMany: true,
64
80
  label: ({ t })=>t('reservation:fieldServices'),
65
- relationTo: config.slugs.services,
66
- required: true
81
+ relationTo: config.slugs.services
67
82
  },
68
83
  {
69
84
  name: 'active',
@@ -113,6 +128,16 @@ export function createResourcesCollection(config) {
113
128
  },
114
129
  label: ({ t })=>t('reservation:fieldTimezone')
115
130
  },
131
+ {
132
+ name: 'resourceType',
133
+ type: 'select',
134
+ admin: {
135
+ position: 'sidebar'
136
+ },
137
+ defaultValue: config.resourceTypes[0],
138
+ label: ({ t })=>t('reservation:fieldResourceType'),
139
+ options: buildSelectOptions(config.resourceTypes)
140
+ },
116
141
  ...ownerFieldDef ? [
117
142
  ownerFieldDef
118
143
  ] : []
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/collections/Resources.ts"],"sourcesContent":["import type { CollectionConfig, CollectionSlug, Field } from 'payload'\n\nimport type { PluginT } from '../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { makeResourceOwnerAccess } from '../utilities/ownerAccess.js'\n\nexport function createResourcesCollection(\n config: ResolvedReservationPluginConfig,\n): CollectionConfig {\n const rom = config.resourceOwnerMode\n const ownerField = rom?.ownerField ?? 'owner'\n\n // Build the owner field when resourceOwnerMode is enabled\n const ownerFieldDef: Field | null = rom\n ? {\n name: ownerField,\n type: 'relationship',\n admin: {\n position: 'sidebar',\n },\n hooks: {\n beforeChange: [\n ({ operation, req, value }) => {\n if (operation === 'create' && req.user) {return req.user.id}\n return value\n },\n ],\n },\n label: 'Owner',\n relationTo: config.slugs.customers as unknown as CollectionSlug,\n required: true,\n }\n : null\n\n // Determine access: app override → owner-mode auto-wired → unrestricted\n const access =\n config.access.resources ?? (rom ? makeResourceOwnerAccess(rom) : {})\n\n return {\n slug: config.slugs.resources,\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 ...(config.localized ? { localized: true } : {}),\n maxLength: 200,\n required: true,\n },\n {\n name: 'image',\n type: 'upload',\n label: ({ t }) => (t as PluginT)('reservation:fieldImage'),\n relationTo: config.slugs.media as unknown as CollectionSlug,\n },\n {\n name: 'description',\n type: 'textarea',\n label: ({ t }) => (t as PluginT)('reservation:fieldDescription'),\n ...(config.localized ? { localized: true } : {}),\n },\n {\n name: 'services',\n type: 'relationship',\n hasMany: true,\n label: ({ t }) => (t as PluginT)('reservation:fieldServices'),\n relationTo: config.slugs.services as unknown as CollectionSlug,\n required: true,\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 name: 'quantity',\n type: 'number',\n admin: {\n position: 'sidebar',\n },\n defaultValue: 1,\n label: ({ t }) => (t as PluginT)('reservation:fieldQuantity'),\n min: 1,\n required: true,\n },\n {\n name: 'capacityMode',\n type: 'select',\n admin: {\n condition: (data) => (data?.quantity ?? 1) > 1,\n position: 'sidebar',\n },\n defaultValue: 'per-reservation',\n label: ({ t }) => (t as PluginT)('reservation:fieldCapacityMode'),\n options: [\n {\n label: ({ t }) => (t as PluginT)('reservation:capacityPerReservation'),\n value: 'per-reservation',\n },\n {\n label: ({ t }) => (t as PluginT)('reservation:capacityPerGuest'),\n value: 'per-guest',\n },\n ],\n },\n {\n name: 'timezone',\n type: 'text',\n admin: {\n position: 'sidebar',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldTimezone'),\n },\n ...(ownerFieldDef ? [ownerFieldDef] : []),\n ],\n labels: {\n plural: ({ t }) => (t as PluginT)('reservation:collectionResources'),\n singular: ({ t }) => (t as PluginT)('reservation:collectionResources'),\n },\n }\n}\n"],"names":["makeResourceOwnerAccess","createResourcesCollection","config","rom","resourceOwnerMode","ownerField","ownerFieldDef","name","type","admin","position","hooks","beforeChange","operation","req","value","user","id","label","relationTo","slugs","customers","required","access","resources","slug","group","adminGroup","useAsTitle","fields","t","localized","maxLength","media","hasMany","services","defaultValue","min","condition","data","quantity","options","labels","plural","singular"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/collections/Resources.ts"],"sourcesContent":["import type { CollectionConfig, CollectionSlug, Field } from 'payload'\n\nimport type { PluginT } from '../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { makeResourceOwnerAccess } from '../utilities/ownerAccess.js'\nimport { buildSelectOptions } from '../utilities/selectOptions.js'\n\n/**\n * Owner-field beforeChange logic, extracted for testability. On create the\n * owner defaults to the requesting user; on other operations the existing\n * value is preserved. Staff provisioning assigns the correct owner by creating\n * the Resource AS the new staff user (impersonation in the provisioning hook),\n * so this function needs no special-case branch.\n */\nexport function resolveOwnerValue({\n operation,\n req,\n value,\n}: {\n operation: string | undefined\n req: { user?: { id: unknown } | null }\n value: unknown\n}): unknown {\n if (operation === 'create' && req.user) {\n return req.user.id\n }\n return value\n}\n\nexport function createResourcesCollection(\n config: ResolvedReservationPluginConfig,\n): CollectionConfig {\n const rom = config.resourceOwnerMode\n const ownerField = rom?.ownerField ?? 'owner'\n // The owner relationship points to where owners/staff live: an explicit\n // ownerCollection, else the staff-provisioning user collection, else customers.\n // (Previously hardcoded to customers, which broke separate users/customers setups.)\n const ownerCollection =\n rom?.ownerCollection ?? config.staffProvisioning?.userCollection ?? config.slugs.customers\n\n // Build the owner field when resourceOwnerMode is enabled\n const ownerFieldDef: Field | null = rom\n ? {\n name: ownerField,\n type: 'relationship',\n admin: {\n position: 'sidebar',\n },\n hooks: {\n beforeChange: [\n ({ operation, req, value }) => resolveOwnerValue({ operation, req, value }),\n ],\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldOwner'),\n relationTo: ownerCollection as unknown as CollectionSlug,\n required: true,\n }\n : null\n\n // Determine access: app override → owner-mode auto-wired → unrestricted\n const access =\n config.access.resources ?? (rom ? makeResourceOwnerAccess(rom) : {})\n\n return {\n slug: config.slugs.resources,\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 ...(config.localized ? { localized: true } : {}),\n maxLength: 200,\n required: true,\n },\n {\n name: 'image',\n type: 'upload',\n label: ({ t }) => (t as PluginT)('reservation:fieldImage'),\n relationTo: config.slugs.media as unknown as CollectionSlug,\n },\n {\n name: 'description',\n type: 'textarea',\n label: ({ t }) => (t as PluginT)('reservation:fieldDescription'),\n ...(config.localized ? { localized: true } : {}),\n },\n {\n name: 'services',\n type: 'relationship',\n hasMany: true,\n label: ({ t }) => (t as PluginT)('reservation:fieldServices'),\n relationTo: config.slugs.services as unknown as CollectionSlug,\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 name: 'quantity',\n type: 'number',\n admin: {\n position: 'sidebar',\n },\n defaultValue: 1,\n label: ({ t }) => (t as PluginT)('reservation:fieldQuantity'),\n min: 1,\n required: true,\n },\n {\n name: 'capacityMode',\n type: 'select',\n admin: {\n condition: (data) => (data?.quantity ?? 1) > 1,\n position: 'sidebar',\n },\n defaultValue: 'per-reservation',\n label: ({ t }) => (t as PluginT)('reservation:fieldCapacityMode'),\n options: [\n {\n label: ({ t }) => (t as PluginT)('reservation:capacityPerReservation'),\n value: 'per-reservation',\n },\n {\n label: ({ t }) => (t as PluginT)('reservation:capacityPerGuest'),\n value: 'per-guest',\n },\n ],\n },\n {\n name: 'timezone',\n type: 'text',\n admin: {\n position: 'sidebar',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldTimezone'),\n },\n {\n name: 'resourceType',\n type: 'select',\n admin: {\n position: 'sidebar',\n },\n defaultValue: config.resourceTypes[0],\n label: ({ t }) => (t as PluginT)('reservation:fieldResourceType'),\n options: buildSelectOptions(config.resourceTypes),\n },\n ...(ownerFieldDef ? [ownerFieldDef] : []),\n ],\n labels: {\n plural: ({ t }) => (t as PluginT)('reservation:collectionResources'),\n singular: ({ t }) => (t as PluginT)('reservation:collectionResources'),\n },\n }\n}\n"],"names":["makeResourceOwnerAccess","buildSelectOptions","resolveOwnerValue","operation","req","value","user","id","createResourcesCollection","config","rom","resourceOwnerMode","ownerField","ownerCollection","staffProvisioning","userCollection","slugs","customers","ownerFieldDef","name","type","admin","position","hooks","beforeChange","label","t","relationTo","required","access","resources","slug","group","adminGroup","useAsTitle","fields","localized","maxLength","media","hasMany","services","defaultValue","min","condition","data","quantity","options","resourceTypes","labels","plural","singular"],"mappings":"AAKA,SAASA,uBAAuB,QAAQ,8BAA6B;AACrE,SAASC,kBAAkB,QAAQ,gCAA+B;AAElE;;;;;;CAMC,GACD,OAAO,SAASC,kBAAkB,EAChCC,SAAS,EACTC,GAAG,EACHC,KAAK,EAKN;IACC,IAAIF,cAAc,YAAYC,IAAIE,IAAI,EAAE;QACtC,OAAOF,IAAIE,IAAI,CAACC,EAAE;IACpB;IACA,OAAOF;AACT;AAEA,OAAO,SAASG,0BACdC,MAAuC;IAEvC,MAAMC,MAAMD,OAAOE,iBAAiB;IACpC,MAAMC,aAAaF,KAAKE,cAAc;IACtC,wEAAwE;IACxE,gFAAgF;IAChF,oFAAoF;IACpF,MAAMC,kBACJH,KAAKG,mBAAmBJ,OAAOK,iBAAiB,EAAEC,kBAAkBN,OAAOO,KAAK,CAACC,SAAS;IAE5F,0DAA0D;IAC1D,MAAMC,gBAA8BR,MAChC;QACES,MAAMP;QACNQ,MAAM;QACNC,OAAO;YACLC,UAAU;QACZ;QACAC,OAAO;YACLC,cAAc;gBACZ,CAAC,EAAErB,SAAS,EAAEC,GAAG,EAAEC,KAAK,EAAE,GAAKH,kBAAkB;wBAAEC;wBAAWC;wBAAKC;oBAAM;aAC1E;QACH;QACAoB,OAAO,CAAC,EAAEC,CAAC,EAAE,GAAK,AAACA,EAAc;QACjCC,YAAYd;QACZe,UAAU;IACZ,IACA;IAEJ,wEAAwE;IACxE,MAAMC,SACJpB,OAAOoB,MAAM,CAACC,SAAS,IAAKpB,CAAAA,MAAMV,wBAAwBU,OAAO,CAAC,CAAA;IAEpE,OAAO;QACLqB,MAAMtB,OAAOO,KAAK,CAACc,SAAS;QAC5BD;QACAR,OAAO;YACLW,OAAOvB,OAAOwB,UAAU;YACxBC,YAAY;QACd;QACAC,QAAQ;YACN;gBACEhB,MAAM;gBACNC,MAAM;gBACNK,OAAO,CAAC,EAAEC,CAAC,EAAE,GAAK,AAACA,EAAc;gBACjC,GAAIjB,OAAO2B,SAAS,GAAG;oBAAEA,WAAW;gBAAK,IAAI,CAAC,CAAC;gBAC/CC,WAAW;gBACXT,UAAU;YACZ;YACA;gBACET,MAAM;gBACNC,MAAM;gBACNK,OAAO,CAAC,EAAEC,CAAC,EAAE,GAAK,AAACA,EAAc;gBACjCC,YAAYlB,OAAOO,KAAK,CAACsB,KAAK;YAChC;YACA;gBACEnB,MAAM;gBACNC,MAAM;gBACNK,OAAO,CAAC,EAAEC,CAAC,EAAE,GAAK,AAACA,EAAc;gBACjC,GAAIjB,OAAO2B,SAAS,GAAG;oBAAEA,WAAW;gBAAK,IAAI,CAAC,CAAC;YACjD;YACA;gBACEjB,MAAM;gBACNC,MAAM;gBACNmB,SAAS;gBACTd,OAAO,CAAC,EAAEC,CAAC,EAAE,GAAK,AAACA,EAAc;gBACjCC,YAAYlB,OAAOO,KAAK,CAACwB,QAAQ;YACnC;YACA;gBACErB,MAAM;gBACNC,MAAM;gBACNC,OAAO;oBACLC,UAAU;gBACZ;gBACAmB,cAAc;gBACdhB,OAAO,CAAC,EAAEC,CAAC,EAAE,GAAK,AAACA,EAAc;YACnC;YACA;gBACEP,MAAM;gBACNC,MAAM;gBACNC,OAAO;oBACLC,UAAU;gBACZ;gBACAmB,cAAc;gBACdhB,OAAO,CAAC,EAAEC,CAAC,EAAE,GAAK,AAACA,EAAc;gBACjCgB,KAAK;gBACLd,UAAU;YACZ;YACA;gBACET,MAAM;gBACNC,MAAM;gBACNC,OAAO;oBACLsB,WAAW,CAACC,OAAS,AAACA,CAAAA,MAAMC,YAAY,CAAA,IAAK;oBAC7CvB,UAAU;gBACZ;gBACAmB,cAAc;gBACdhB,OAAO,CAAC,EAAEC,CAAC,EAAE,GAAK,AAACA,EAAc;gBACjCoB,SAAS;oBACP;wBACErB,OAAO,CAAC,EAAEC,CAAC,EAAE,GAAK,AAACA,EAAc;wBACjCrB,OAAO;oBACT;oBACA;wBACEoB,OAAO,CAAC,EAAEC,CAAC,EAAE,GAAK,AAACA,EAAc;wBACjCrB,OAAO;oBACT;iBACD;YACH;YACA;gBACEc,MAAM;gBACNC,MAAM;gBACNC,OAAO;oBACLC,UAAU;gBACZ;gBACAG,OAAO,CAAC,EAAEC,CAAC,EAAE,GAAK,AAACA,EAAc;YACnC;YACA;gBACEP,MAAM;gBACNC,MAAM;gBACNC,OAAO;oBACLC,UAAU;gBACZ;gBACAmB,cAAchC,OAAOsC,aAAa,CAAC,EAAE;gBACrCtB,OAAO,CAAC,EAAEC,CAAC,EAAE,GAAK,AAACA,EAAc;gBACjCoB,SAAS7C,mBAAmBQ,OAAOsC,aAAa;YAClD;eACI7B,gBAAgB;gBAACA;aAAc,GAAG,EAAE;SACzC;QACD8B,QAAQ;YACNC,QAAQ,CAAC,EAAEvB,CAAC,EAAE,GAAK,AAACA,EAAc;YAClCwB,UAAU,CAAC,EAAExB,CAAC,EAAE,GAAK,AAACA,EAAc;QACtC;IACF;AACF"}
@@ -1,5 +1,6 @@
1
1
  import { ValidationError } from 'payload';
2
2
  import { makeScheduleOwnerAccess } from '../utilities/ownerAccess.js';
3
+ import { buildSelectOptions } from '../utilities/selectOptions.js';
3
4
  const TIME_REGEX = /^(?:[01]\d|2[0-3]):[0-5]\d$/;
4
5
  const validateTime = (value)=>{
5
6
  if (!value) {
@@ -169,6 +170,22 @@ export function createSchedulesCollection(config) {
169
170
  label: ({ t })=>t('reservation:fieldDate'),
170
171
  required: true
171
172
  },
173
+ {
174
+ name: 'endDate',
175
+ type: 'date',
176
+ admin: {
177
+ date: {
178
+ pickerAppearance: 'dayOnly'
179
+ }
180
+ },
181
+ label: ({ t })=>t('reservation:fieldEndDate')
182
+ },
183
+ {
184
+ name: 'type',
185
+ type: 'select',
186
+ label: ({ t })=>t('reservation:fieldLeaveType'),
187
+ options: buildSelectOptions(config.leaveTypes)
188
+ },
172
189
  {
173
190
  name: 'reason',
174
191
  type: 'text',
@@ -216,6 +233,23 @@ export function createSchedulesCollection(config) {
216
233
  });
217
234
  }
218
235
  }
236
+ const exceptions = data?.exceptions ?? [];
237
+ for (const exc of exceptions){
238
+ if (exc.date && exc.endDate) {
239
+ const start = new Date(exc.date).toISOString().split('T')[0];
240
+ const end = new Date(exc.endDate).toISOString().split('T')[0];
241
+ if (end < start) {
242
+ throw new ValidationError({
243
+ errors: [
244
+ {
245
+ message: 'exception endDate must be on or after date',
246
+ path: 'exceptions'
247
+ }
248
+ ]
249
+ });
250
+ }
251
+ }
252
+ }
219
253
  return data;
220
254
  }
221
255
  ]