payload-reserve 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +185 -4
- package/dist/collections/Reservations.js +47 -2
- package/dist/collections/Reservations.js.map +1 -1
- package/dist/collections/Resources.d.ts +16 -0
- package/dist/collections/Resources.js +35 -10
- package/dist/collections/Resources.js.map +1 -1
- package/dist/collections/Schedules.js +34 -0
- package/dist/collections/Schedules.js.map +1 -1
- package/dist/collections/Services.js +34 -1
- package/dist/collections/Services.js.map +1 -1
- package/dist/components/AvailabilityTimeField/AvailabilityTimeField.module.css +7 -0
- package/dist/components/AvailabilityTimeField/index.d.ts +2 -0
- package/dist/components/AvailabilityTimeField/index.js +109 -0
- package/dist/components/AvailabilityTimeField/index.js.map +1 -0
- package/dist/components/CalendarView/CalendarView.module.css +114 -0
- package/dist/components/CalendarView/LaneTimelineView.d.ts +12 -0
- package/dist/components/CalendarView/LaneTimelineView.js +116 -0
- package/dist/components/CalendarView/LaneTimelineView.js.map +1 -0
- package/dist/components/CalendarView/index.js +224 -22
- package/dist/components/CalendarView/index.js.map +1 -1
- package/dist/components/CalendarView/useResourceAvailability.d.ts +9 -0
- package/dist/components/CalendarView/useResourceAvailability.js +40 -0
- package/dist/components/CalendarView/useResourceAvailability.js.map +1 -0
- package/dist/defaults.d.ts +3 -0
- package/dist/defaults.js +53 -0
- package/dist/defaults.js.map +1 -1
- package/dist/endpoints/cancelBooking.js +34 -21
- package/dist/endpoints/cancelBooking.js.map +1 -1
- package/dist/endpoints/checkAvailability.js +16 -1
- package/dist/endpoints/checkAvailability.js.map +1 -1
- package/dist/endpoints/createBooking.js +4 -1
- package/dist/endpoints/createBooking.js.map +1 -1
- package/dist/endpoints/customerSearch.js +24 -5
- package/dist/endpoints/customerSearch.js.map +1 -1
- package/dist/endpoints/getSlots.js +16 -1
- package/dist/endpoints/getSlots.js.map +1 -1
- package/dist/endpoints/resourceAvailability.d.ts +43 -0
- package/dist/endpoints/resourceAvailability.js +214 -0
- package/dist/endpoints/resourceAvailability.js.map +1 -0
- package/dist/exports/client.d.ts +1 -0
- package/dist/exports/client.js +1 -0
- package/dist/exports/client.js.map +1 -1
- package/dist/hooks/reservations/calculateEndTime.js +21 -1
- package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
- package/dist/hooks/reservations/expandRequiredResources.d.ts +9 -0
- package/dist/hooks/reservations/expandRequiredResources.js +81 -0
- package/dist/hooks/reservations/expandRequiredResources.js.map +1 -0
- package/dist/hooks/reservations/validateGuestBooking.d.ts +3 -0
- package/dist/hooks/reservations/validateGuestBooking.js +93 -0
- package/dist/hooks/reservations/validateGuestBooking.js.map +1 -0
- package/dist/hooks/reservations/validateStatusTransition.js +4 -2
- package/dist/hooks/reservations/validateStatusTransition.js.map +1 -1
- package/dist/hooks/users/provisionStaffResource.d.ts +15 -0
- package/dist/hooks/users/provisionStaffResource.js +88 -0
- package/dist/hooks/users/provisionStaffResource.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin.js +19 -2
- package/dist/plugin.js.map +1 -1
- package/dist/services/AvailabilityService.d.ts +2 -1
- package/dist/services/AvailabilityService.js +86 -60
- package/dist/services/AvailabilityService.js.map +1 -1
- package/dist/translations/ar.json +156 -0
- package/dist/translations/de.json +156 -0
- package/dist/translations/en.json +32 -1
- package/dist/translations/es.json +156 -0
- package/dist/translations/fa.json +156 -0
- package/dist/translations/fr.json +156 -0
- package/dist/translations/hi.json +156 -0
- package/dist/translations/id.json +156 -0
- package/dist/translations/index.js +44 -0
- package/dist/translations/index.js.map +1 -1
- package/dist/translations/pl.json +156 -0
- package/dist/translations/ru.json +156 -0
- package/dist/translations/tr.json +156 -0
- package/dist/translations/zh.json +156 -0
- package/dist/types.d.ts +46 -0
- package/dist/types.js.map +1 -1
- package/dist/utilities/computeSlotStates.d.ts +39 -0
- package/dist/utilities/computeSlotStates.js +49 -0
- package/dist/utilities/computeSlotStates.js.map +1 -0
- package/dist/utilities/guestBooking.d.ts +10 -0
- package/dist/utilities/guestBooking.js +16 -0
- package/dist/utilities/guestBooking.js.map +1 -0
- package/dist/utilities/resolveRequiredResources.d.ts +8 -0
- package/dist/utilities/resolveRequiredResources.js +27 -0
- package/dist/utilities/resolveRequiredResources.js.map +1 -0
- package/dist/utilities/scheduleUtils.d.ts +3 -0
- package/dist/utilities/scheduleUtils.js +5 -3
- package/dist/utilities/scheduleUtils.js.map +1 -1
- package/dist/utilities/selectOptions.d.ts +8 -0
- package/dist/utilities/selectOptions.js +11 -0
- package/dist/utilities/selectOptions.js.map +1 -0
- package/dist/utilities/slotUtils.d.ts +19 -0
- package/dist/utilities/slotUtils.js +28 -0
- package/dist/utilities/slotUtils.js.map +1 -1
- package/dist/utilities/userRoles.d.ts +20 -0
- package/dist/utilities/userRoles.js +32 -0
- package/dist/utilities/userRoles.js.map +1 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
# payload-reserve
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/payload-reserve)
|
|
4
|
+
[](https://www.npmjs.com/package/payload-reserve)
|
|
5
|
+
[](./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** —
|
|
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
|
-
- **
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
31
|
+
({ operation, req, value })=>resolveOwnerValue({
|
|
32
|
+
operation,
|
|
33
|
+
req,
|
|
34
|
+
value
|
|
35
|
+
})
|
|
20
36
|
]
|
|
21
37
|
},
|
|
22
|
-
label: '
|
|
23
|
-
relationTo:
|
|
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 }) => {
|
|
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":"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"}
|
|
@@ -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
|
]
|