payload-reserve 1.0.2 → 1.1.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 +61 -1141
- package/dist/collections/Customers.d.ts +3 -0
- package/dist/collections/Customers.js +58 -0
- package/dist/collections/Customers.js.map +1 -0
- package/dist/collections/Reservations.js +129 -35
- package/dist/collections/Reservations.js.map +1 -1
- package/dist/collections/Resources.js +70 -2
- package/dist/collections/Resources.js.map +1 -1
- package/dist/collections/Schedules.js +4 -1
- package/dist/collections/Schedules.js.map +1 -1
- package/dist/collections/Services.js +59 -2
- package/dist/collections/Services.js.map +1 -1
- package/dist/components/AvailabilityOverview/AvailabilityOverview.module.css +31 -0
- package/dist/components/AvailabilityOverview/index.js +59 -8
- package/dist/components/AvailabilityOverview/index.js.map +1 -1
- package/dist/components/CalendarView/CalendarView.module.css +171 -0
- package/dist/components/CalendarView/index.js +547 -38
- package/dist/components/CalendarView/index.js.map +1 -1
- package/dist/components/CustomerField/index.js +24 -10
- package/dist/components/CustomerField/index.js.map +1 -1
- package/dist/components/DashboardWidget/DashboardWidgetServer.js +21 -11
- package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
- package/dist/defaults.d.ts +1 -2
- package/dist/defaults.js +22 -4
- package/dist/defaults.js.map +1 -1
- package/dist/endpoints/cancelBooking.d.ts +3 -0
- package/dist/endpoints/cancelBooking.js +37 -0
- package/dist/endpoints/cancelBooking.js.map +1 -0
- package/dist/endpoints/checkAvailability.d.ts +3 -0
- package/dist/endpoints/checkAvailability.js +37 -0
- package/dist/endpoints/checkAvailability.js.map +1 -0
- package/dist/endpoints/createBooking.d.ts +3 -0
- package/dist/endpoints/createBooking.js +32 -0
- package/dist/endpoints/createBooking.js.map +1 -0
- package/dist/endpoints/customerSearch.js +76 -58
- package/dist/endpoints/customerSearch.js.map +1 -1
- package/dist/endpoints/getSlots.d.ts +3 -0
- package/dist/endpoints/getSlots.js +51 -0
- package/dist/endpoints/getSlots.js.map +1 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/reservations/calculateEndTime.js +75 -9
- package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
- package/dist/hooks/reservations/checkIdempotency.d.ts +3 -0
- package/dist/hooks/reservations/checkIdempotency.js +32 -0
- package/dist/hooks/reservations/checkIdempotency.js.map +1 -0
- package/dist/hooks/reservations/onStatusChange.d.ts +3 -0
- package/dist/hooks/reservations/onStatusChange.js +41 -0
- package/dist/hooks/reservations/onStatusChange.js.map +1 -0
- package/dist/hooks/reservations/validateCancellation.js +1 -1
- package/dist/hooks/reservations/validateCancellation.js.map +1 -1
- package/dist/hooks/reservations/validateConflicts.js +34 -56
- package/dist/hooks/reservations/validateConflicts.js.map +1 -1
- package/dist/hooks/reservations/validateStatusTransition.d.ts +2 -1
- package/dist/hooks/reservations/validateStatusTransition.js +29 -6
- package/dist/hooks/reservations/validateStatusTransition.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin.js +49 -54
- package/dist/plugin.js.map +1 -1
- package/dist/services/AvailabilityService.d.ts +57 -0
- package/dist/services/AvailabilityService.js +232 -0
- package/dist/services/AvailabilityService.js.map +1 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +3 -0
- package/dist/services/index.js.map +1 -0
- package/dist/translations/en.json +33 -1
- package/dist/translations/index.d.ts +5 -0
- package/dist/translations/index.js.map +1 -1
- package/dist/types.d.ts +84 -6
- package/dist/types.js +29 -9
- package/dist/types.js.map +1 -1
- package/dist/utilities/ownerAccess.d.ts +24 -0
- package/dist/utilities/ownerAccess.js +128 -0
- package/dist/utilities/ownerAccess.js.map +1 -0
- package/dist/utilities/resolveReservationItems.d.ts +18 -0
- package/dist/utilities/resolveReservationItems.js +45 -0
- package/dist/utilities/resolveReservationItems.js.map +1 -0
- package/package.json +12 -9
package/README.md
CHANGED
|
@@ -1,1187 +1,107 @@
|
|
|
1
|
-
# payload-reserve
|
|
1
|
+
# payload-reserve
|
|
2
2
|
|
|
3
|
-
A full-featured
|
|
3
|
+
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
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## Table of Contents
|
|
8
|
-
|
|
9
|
-
- [Features](#features)
|
|
10
|
-
- [Installation](#installation)
|
|
11
|
-
- [Quick Start](#quick-start)
|
|
12
|
-
- [Configuration](#configuration)
|
|
13
|
-
- [Collections](#collections)
|
|
14
|
-
- [Services](#services)
|
|
15
|
-
- [Resources](#resources)
|
|
16
|
-
- [Schedules](#schedules)
|
|
17
|
-
- [User Collection Extension (Customers)](#user-collection-extension-customers)
|
|
18
|
-
- [Reservations](#reservations)
|
|
19
|
-
- [Business Logic Hooks](#business-logic-hooks)
|
|
20
|
-
- [Auto End Time Calculation](#auto-end-time-calculation)
|
|
21
|
-
- [Conflict Detection](#conflict-detection)
|
|
22
|
-
- [Status Transition Enforcement](#status-transition-enforcement)
|
|
23
|
-
- [Common Workflows](#common-workflows)
|
|
24
|
-
- [Cancellation Policy](#cancellation-policy)
|
|
25
|
-
- [Escape Hatch](#escape-hatch)
|
|
26
|
-
- [Integration Patterns](#integration-patterns)
|
|
27
|
-
- [Admin UI Components](#admin-ui-components)
|
|
28
|
-
- [Dashboard Widget](#dashboard-widget)
|
|
29
|
-
- [Calendar View](#calendar-view)
|
|
30
|
-
- [Customer Picker](#customer-picker)
|
|
31
|
-
- [Availability Overview](#availability-overview)
|
|
32
|
-
- [Utilities](#utilities)
|
|
33
|
-
- [Development](#development)
|
|
34
|
-
- [Project Structure](#project-structure)
|
|
35
|
-
- [API Reference](#api-reference)
|
|
5
|
+
Designed for salons, clinics, hotels, restaurants, event venues, and any business that needs appointment scheduling managed through Payload's admin panel.
|
|
36
6
|
|
|
37
7
|
---
|
|
38
8
|
|
|
39
9
|
## Features
|
|
40
10
|
|
|
41
|
-
- **
|
|
42
|
-
- **
|
|
43
|
-
- **
|
|
44
|
-
- **
|
|
45
|
-
- **
|
|
46
|
-
- **
|
|
47
|
-
- **
|
|
48
|
-
- **
|
|
49
|
-
- **
|
|
50
|
-
- **
|
|
51
|
-
- **
|
|
52
|
-
- **
|
|
53
|
-
- **
|
|
54
|
-
- **Availability
|
|
55
|
-
- **
|
|
56
|
-
- **
|
|
57
|
-
- **
|
|
11
|
+
- **5 Domain Collections** — Services, Resources, Schedules, Reservations, and Customers (standalone or user-collection extension)
|
|
12
|
+
- **User Collection Extension** — Optionally extend your existing auth collection with booking fields; set `userCollection: undefined` (default) to use a standalone Customers collection
|
|
13
|
+
- **Resource Owner Multi-Tenancy** — Opt-in `resourceOwnerMode` wires ownership access control so each resource owner (host) sees only their own listings and reservations
|
|
14
|
+
- **Configurable Status Machine** — Define your own statuses, transitions, blocking states, and terminal states
|
|
15
|
+
- **Double-Booking Prevention** — Server-side conflict detection with configurable buffer times; respects capacity modes
|
|
16
|
+
- **Auto End Time** — Calculates `endTime` from `startTime + service.duration` automatically
|
|
17
|
+
- **Three Duration Types** — `fixed` (service duration), `flexible` (customer-specified end), and `full-day` bookings
|
|
18
|
+
- **Multi-Resource Bookings** — Single reservation that spans multiple resources simultaneously via the `items` array
|
|
19
|
+
- **Capacity and Inventory** — `quantity > 1` allows multiple concurrent bookings per resource; `capacityMode` (`per-reservation` | `per-guest`) controls how capacity is counted
|
|
20
|
+
- **Idempotency** — Optional `idempotencyKey` prevents duplicate submissions
|
|
21
|
+
- **Extra Reservation Fields** — Inject custom fields into the Reservations collection via `extraReservationFields` without forking the plugin
|
|
22
|
+
- **Cancellation Policy** — Configurable minimum notice period enforcement
|
|
23
|
+
- **Plugin Hooks API** — Seven lifecycle hooks (`beforeBookingCreate`, `afterBookingCreate`, `beforeBookingConfirm`, `afterBookingConfirm`, `beforeBookingCancel`, `afterBookingCancel`, `afterStatusChange`) for integrating email, Stripe, and external systems
|
|
24
|
+
- **Availability Service** — Pure functions and DB helpers for slot generation and conflict checking
|
|
25
|
+
- **Public REST API** — Five pre-built endpoints for availability, slot listing, booking, cancellation, and customer search
|
|
26
|
+
- **Calendar View** — Month/week/day calendar replacing the default reservations list view
|
|
27
|
+
- **Dashboard Widget** — Server component showing today's booking stats
|
|
28
|
+
- **Availability Overview** — Weekly grid of resource availability vs. booked slots
|
|
29
|
+
- **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
|
|
31
|
+
- **Type-Safe** — Full TypeScript support with exported types
|
|
58
32
|
|
|
59
33
|
---
|
|
60
34
|
|
|
61
|
-
##
|
|
35
|
+
## Install
|
|
62
36
|
|
|
63
37
|
```bash
|
|
64
|
-
# Install the plugin as a dependency
|
|
65
38
|
pnpm add payload-reserve
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
pnpm link ./plugins/payload-reserve
|
|
39
|
+
# or
|
|
40
|
+
npm install payload-reserve
|
|
69
41
|
```
|
|
70
42
|
|
|
71
|
-
**Peer
|
|
43
|
+
**Peer dependencies:** `payload ^3.77.0`, `@payloadcms/ui ^3.77.0`, `@payloadcms/translations ^3.77.0`
|
|
72
44
|
|
|
73
45
|
---
|
|
74
46
|
|
|
75
47
|
## Quick Start
|
|
76
48
|
|
|
77
|
-
Add the plugin to your `payload.config.ts`:
|
|
78
|
-
|
|
79
49
|
```typescript
|
|
80
50
|
import { buildConfig } from 'payload'
|
|
81
51
|
import { payloadReserve } from 'payload-reserve'
|
|
82
52
|
|
|
83
53
|
export default buildConfig({
|
|
84
|
-
|
|
54
|
+
collections: [/* your collections */],
|
|
85
55
|
plugins: [
|
|
86
56
|
payloadReserve(),
|
|
87
57
|
],
|
|
88
58
|
})
|
|
89
59
|
```
|
|
90
60
|
|
|
91
|
-
That's it. The plugin registers 4 collections, extends your existing Users collection with customer fields (`name`, `phone`, `notes`, `bookings`), and adds a dashboard widget, a calendar list view, and an availability admin view automatically. All plugin collections appear under the **"Reservations"** group in the admin panel.
|
|
92
|
-
|
|
93
|
-
> **Important:** Your Payload config must explicitly define a `users` collection (or whichever collection you specify via `userCollection`). The plugin finds it in `config.collections` and appends customer fields to it.
|
|
94
|
-
|
|
95
|
-
---
|
|
96
|
-
|
|
97
|
-
## Configuration
|
|
98
|
-
|
|
99
|
-
All options are optional. The plugin works out of the box with sensible defaults.
|
|
100
|
-
|
|
101
|
-
```typescript
|
|
102
|
-
import { payloadReserve } from 'payload-reserve'
|
|
103
|
-
import type { ReservationPluginConfig } from 'payload-reserve'
|
|
104
|
-
|
|
105
|
-
const config: ReservationPluginConfig = {
|
|
106
|
-
// Disable the plugin entirely (collections are still registered for schema consistency)
|
|
107
|
-
disabled: false,
|
|
108
|
-
|
|
109
|
-
// Override collection slugs
|
|
110
|
-
slugs: {
|
|
111
|
-
services: 'services', // default
|
|
112
|
-
resources: 'resources', // default
|
|
113
|
-
schedules: 'schedules', // default
|
|
114
|
-
reservations: 'reservations', // default
|
|
115
|
-
media: 'media', // default (used by Resources image field)
|
|
116
|
-
},
|
|
117
|
-
|
|
118
|
-
// Slug of the existing auth collection to extend with customer fields
|
|
119
|
-
userCollection: 'users', // default
|
|
120
|
-
|
|
121
|
-
// Admin panel group name
|
|
122
|
-
adminGroup: 'Reservations', // default
|
|
123
|
-
|
|
124
|
-
// Default buffer time (minutes) between reservations
|
|
125
|
-
// Applied when a service doesn't define its own buffer times
|
|
126
|
-
defaultBufferTime: 0, // default
|
|
127
|
-
|
|
128
|
-
// Minimum hours of notice required before cancellation
|
|
129
|
-
cancellationNoticePeriod: 24, // default (hours)
|
|
130
|
-
|
|
131
|
-
// Filter customers by role in the reservation form
|
|
132
|
-
// Set to a role string to filter, or false to show all users
|
|
133
|
-
customerRole: false, // default (no filtering)
|
|
134
|
-
|
|
135
|
-
// Override access control per collection
|
|
136
|
-
access: {
|
|
137
|
-
services: {
|
|
138
|
-
read: () => true,
|
|
139
|
-
create: ({ req }) => !!req.user,
|
|
140
|
-
update: ({ req }) => !!req.user,
|
|
141
|
-
delete: ({ req }) => !!req.user,
|
|
142
|
-
},
|
|
143
|
-
resources: { /* ... */ },
|
|
144
|
-
schedules: { /* ... */ },
|
|
145
|
-
reservations: { /* ... */ },
|
|
146
|
-
},
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
payloadReserve(config)
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
### Configuration Defaults
|
|
153
|
-
|
|
154
|
-
| Option | Default | Description |
|
|
155
|
-
|--------|---------|-------------|
|
|
156
|
-
| `disabled` | `false` | Disable plugin functionality |
|
|
157
|
-
| `slugs.services` | `'services'` | Services collection slug |
|
|
158
|
-
| `slugs.resources` | `'resources'` | Resources collection slug |
|
|
159
|
-
| `slugs.schedules` | `'schedules'` | Schedules collection slug |
|
|
160
|
-
| `slugs.reservations` | `'reservations'` | Reservations collection slug |
|
|
161
|
-
| `slugs.media` | `'media'` | Media collection slug (used by Resources image field) |
|
|
162
|
-
| `userCollection` | `'users'` | Existing auth collection to extend with customer fields |
|
|
163
|
-
| `adminGroup` | `'Reservations'` | Admin panel group name |
|
|
164
|
-
| `defaultBufferTime` | `0` | Default buffer (minutes) between bookings |
|
|
165
|
-
| `cancellationNoticePeriod` | `24` | Minimum hours notice for cancellation |
|
|
166
|
-
| `customerRole` | `false` | Filter customers by role in reservation form (`string` or `false` to disable) |
|
|
167
|
-
|
|
168
|
-
---
|
|
169
|
-
|
|
170
|
-
## Collections
|
|
171
|
-
|
|
172
|
-
### Services
|
|
173
|
-
|
|
174
|
-
**Slug:** `services`
|
|
175
|
-
|
|
176
|
-
Defines what can be booked (e.g., "Haircut", "Consultation", "Massage").
|
|
177
|
-
|
|
178
|
-
| Field | Type | Required | Description |
|
|
179
|
-
|-------|------|----------|-------------|
|
|
180
|
-
| `name` | Text | Yes | Service name (max 200 chars, used as title) |
|
|
181
|
-
| `description` | Textarea | No | Service description |
|
|
182
|
-
| `duration` | Number | Yes | Duration in minutes (min: 1) |
|
|
183
|
-
| `price` | Number | No | Price (min: 0, step: 0.01) |
|
|
184
|
-
| `bufferTimeBefore` | Number | No | Buffer minutes before appointment (default: 0) |
|
|
185
|
-
| `bufferTimeAfter` | Number | No | Buffer minutes after appointment (default: 0) |
|
|
186
|
-
| `active` | Checkbox | No | Whether service is active (default: true, sidebar) |
|
|
187
|
-
|
|
188
|
-
**Example:**
|
|
189
|
-
```typescript
|
|
190
|
-
await payload.create({
|
|
191
|
-
collection: 'services',
|
|
192
|
-
data: {
|
|
193
|
-
name: 'Haircut',
|
|
194
|
-
description: 'Standard haircut service',
|
|
195
|
-
duration: 30,
|
|
196
|
-
price: 35.00,
|
|
197
|
-
bufferTimeBefore: 5,
|
|
198
|
-
bufferTimeAfter: 10,
|
|
199
|
-
active: true,
|
|
200
|
-
},
|
|
201
|
-
})
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
### Resources
|
|
205
|
-
|
|
206
|
-
**Slug:** `resources`
|
|
207
|
-
|
|
208
|
-
Who or what performs the service (e.g., a stylist, a room, a consultant).
|
|
209
|
-
|
|
210
|
-
| Field | Type | Required | Description |
|
|
211
|
-
|-------|------|----------|-------------|
|
|
212
|
-
| `name` | Text | Yes | Resource name (max 200 chars, used as title) |
|
|
213
|
-
| `image` | Upload | No | Resource image (references media collection) |
|
|
214
|
-
| `description` | Textarea | No | Resource description |
|
|
215
|
-
| `services` | Relationship | Yes | Services this resource can perform (hasMany) |
|
|
216
|
-
| `active` | Checkbox | No | Whether resource is active (default: true, sidebar) |
|
|
217
|
-
|
|
218
|
-
**Example:**
|
|
219
|
-
```typescript
|
|
220
|
-
await payload.create({
|
|
221
|
-
collection: 'resources',
|
|
222
|
-
data: {
|
|
223
|
-
name: 'Alice Johnson',
|
|
224
|
-
description: 'Senior Stylist',
|
|
225
|
-
services: [haircutId, coloringId],
|
|
226
|
-
active: true,
|
|
227
|
-
},
|
|
228
|
-
})
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
### Schedules
|
|
232
|
-
|
|
233
|
-
**Slug:** `schedules`
|
|
234
|
-
|
|
235
|
-
Defines when a resource is available. Supports **recurring** (weekly pattern) and **manual** (specific dates) modes, plus exception dates.
|
|
236
|
-
|
|
237
|
-
| Field | Type | Required | Description |
|
|
238
|
-
|-------|------|----------|-------------|
|
|
239
|
-
| `name` | Text | Yes | Schedule name (used as title) |
|
|
240
|
-
| `resource` | Relationship | Yes | Which resource this schedule belongs to |
|
|
241
|
-
| `scheduleType` | Select | No | `'recurring'` or `'manual'` (default: `'recurring'`) |
|
|
242
|
-
| `recurringSlots` | Array | No | Weekly slots (shown when type is recurring) |
|
|
243
|
-
| `recurringSlots.day` | Select | Yes | Day of week (mon-sun) |
|
|
244
|
-
| `recurringSlots.startTime` | Text | Yes | Start time (HH:mm format) |
|
|
245
|
-
| `recurringSlots.endTime` | Text | Yes | End time (HH:mm format) |
|
|
246
|
-
| `manualSlots` | Array | No | Specific date slots (shown when type is manual) |
|
|
247
|
-
| `manualSlots.date` | Date | Yes | Specific date (day only) |
|
|
248
|
-
| `manualSlots.startTime` | Text | Yes | Start time (HH:mm format) |
|
|
249
|
-
| `manualSlots.endTime` | Text | Yes | End time (HH:mm format) |
|
|
250
|
-
| `exceptions` | Array | No | Dates when the resource is unavailable |
|
|
251
|
-
| `exceptions.date` | Date | Yes | Exception date (day only) |
|
|
252
|
-
| `exceptions.reason` | Text | No | Reason for unavailability |
|
|
253
|
-
| `active` | Checkbox | No | Whether schedule is active (default: true, sidebar) |
|
|
254
|
-
|
|
255
|
-
**Example - Recurring Schedule:**
|
|
256
|
-
```typescript
|
|
257
|
-
await payload.create({
|
|
258
|
-
collection: 'schedules',
|
|
259
|
-
data: {
|
|
260
|
-
name: 'Alice - Weekdays',
|
|
261
|
-
resource: aliceId,
|
|
262
|
-
scheduleType: 'recurring',
|
|
263
|
-
recurringSlots: [
|
|
264
|
-
{ day: 'mon', startTime: '09:00', endTime: '17:00' },
|
|
265
|
-
{ day: 'tue', startTime: '09:00', endTime: '17:00' },
|
|
266
|
-
{ day: 'wed', startTime: '09:00', endTime: '17:00' },
|
|
267
|
-
{ day: 'thu', startTime: '09:00', endTime: '17:00' },
|
|
268
|
-
{ day: 'fri', startTime: '09:00', endTime: '15:00' },
|
|
269
|
-
],
|
|
270
|
-
exceptions: [
|
|
271
|
-
{ date: '2025-12-25', reason: 'Christmas' },
|
|
272
|
-
],
|
|
273
|
-
active: true,
|
|
274
|
-
},
|
|
275
|
-
})
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
### User Collection Extension (Customers)
|
|
279
|
-
|
|
280
|
-
Instead of a standalone Customers collection, the plugin extends your **existing auth-enabled Users collection** with customer fields. This means customers are real users who can log in to your site.
|
|
281
|
-
|
|
282
|
-
The plugin finds the collection specified by `userCollection` (default: `'users'`) and appends these fields if they don't already exist:
|
|
283
|
-
|
|
284
|
-
| Field | Type | Description |
|
|
285
|
-
|-------|------|-------------|
|
|
286
|
-
| `name` | Text | Customer name (max 200 chars) |
|
|
287
|
-
| `phone` | Text | Phone number (max 50 chars) |
|
|
288
|
-
| `notes` | Textarea | Internal notes |
|
|
289
|
-
| `bookings` | Join | Virtual field: all reservations for this customer |
|
|
290
|
-
|
|
291
|
-
The `bookings` field is a **join** field — it shows all reservations linked to this user without storing anything on the user document itself. Fields that already exist on the collection (e.g., if your Users collection already has a `name` field) are skipped to avoid duplicates.
|
|
292
|
-
|
|
293
|
-
**Example:**
|
|
294
|
-
```typescript
|
|
295
|
-
await payload.create({
|
|
296
|
-
collection: 'users',
|
|
297
|
-
data: {
|
|
298
|
-
name: 'Jane Doe',
|
|
299
|
-
email: 'jane@example.com',
|
|
300
|
-
password: 'securepassword',
|
|
301
|
-
phone: '555-0101',
|
|
302
|
-
notes: 'Prefers morning appointments',
|
|
303
|
-
},
|
|
304
|
-
})
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
> **Note:** Since users is an auth collection, you must provide `email` and `password` when creating customers. The `email` field comes from Payload's built-in auth — the plugin does not add it.
|
|
308
|
-
|
|
309
|
-
### Reservations
|
|
310
|
-
|
|
311
|
-
**Slug:** `reservations`
|
|
312
|
-
|
|
313
|
-
The core booking records. Each reservation links a customer to a service performed by a resource at a specific time.
|
|
314
|
-
|
|
315
|
-
| Field | Type | Required | Description |
|
|
316
|
-
|-------|------|----------|-------------|
|
|
317
|
-
| `service` | Relationship | Yes | Which service is being booked |
|
|
318
|
-
| `resource` | Relationship | Yes | Which resource performs the service |
|
|
319
|
-
| `customer` | Relationship | Yes | Who is booking (references the user collection) |
|
|
320
|
-
| `startTime` | Date | Yes | Appointment start (date + time picker) |
|
|
321
|
-
| `endTime` | Date | No | Auto-calculated, read-only (date + time picker) |
|
|
322
|
-
| `status` | Select | No | Workflow status (default: `'pending'`) |
|
|
323
|
-
| `cancellationReason` | Textarea | No | Shown only when status is `'cancelled'` |
|
|
324
|
-
| `notes` | Textarea | No | Additional notes |
|
|
325
|
-
|
|
326
|
-
**Status Options:** `pending`, `confirmed`, `completed`, `cancelled`, `no-show`
|
|
327
|
-
|
|
328
|
-
#### Status Definitions
|
|
329
|
-
|
|
330
|
-
| Status | Meaning | Terminal? |
|
|
331
|
-
|--------|---------|-----------|
|
|
332
|
-
| `pending` | Reservation created but not yet confirmed. Awaiting admin review, payment, or other verification. This is the default status for all new public reservations. | No |
|
|
333
|
-
| `confirmed` | Reservation is locked in. Payment received, admin approved, or created as a walk-in by staff. The time slot is committed. | No |
|
|
334
|
-
| `completed` | Appointment took place successfully. Set by admin after the service is delivered. | Yes |
|
|
335
|
-
| `cancelled` | Reservation was cancelled before the appointment (by customer or admin). Subject to the cancellation notice period. | Yes |
|
|
336
|
-
| `no-show` | Customer did not show up for a confirmed appointment. Set by admin after the scheduled time passes. | Yes |
|
|
337
|
-
|
|
338
|
-
Terminal statuses cannot transition to any other status. Once a reservation is `completed`, `cancelled`, or `no-show`, it is permanently closed.
|
|
339
|
-
|
|
340
|
-
**Example:**
|
|
341
|
-
```typescript
|
|
342
|
-
// Create a reservation (endTime is auto-calculated)
|
|
343
|
-
const reservation = await payload.create({
|
|
344
|
-
collection: 'reservations',
|
|
345
|
-
data: {
|
|
346
|
-
service: haircutId,
|
|
347
|
-
resource: aliceId,
|
|
348
|
-
customer: janeUserId,
|
|
349
|
-
startTime: '2025-06-15T10:00:00.000Z',
|
|
350
|
-
status: 'pending',
|
|
351
|
-
},
|
|
352
|
-
})
|
|
353
|
-
|
|
354
|
-
// endTime is automatically set to 10:30 (30 min haircut)
|
|
355
|
-
console.log(reservation.endTime) // '2025-06-15T10:30:00.000Z'
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
---
|
|
359
|
-
|
|
360
|
-
## Business Logic Hooks
|
|
361
|
-
|
|
362
|
-
Four `beforeChange` hooks are applied to the Reservations collection, executing in order:
|
|
363
|
-
|
|
364
|
-
### Auto End Time Calculation
|
|
365
|
-
|
|
366
|
-
**Hook:** `calculateEndTime`
|
|
367
|
-
|
|
368
|
-
Automatically computes `endTime` from `startTime + service.duration` on every create and update. The end time field is read-only in the admin UI.
|
|
369
|
-
|
|
370
|
-
```
|
|
371
|
-
endTime = startTime + service.duration (minutes)
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
**Why this matters:** Prevents manual calculation errors and ensures the calendar view and conflict detection always have accurate time ranges. Without auto-calculation, admins could accidentally enter wrong end times, causing invisible scheduling gaps or double-bookings that conflict detection wouldn't catch.
|
|
375
|
-
|
|
376
|
-
### Conflict Detection
|
|
377
|
-
|
|
378
|
-
**Hook:** `validateConflicts`
|
|
379
|
-
|
|
380
|
-
Prevents double-booking on the same resource. Checks for overlapping time ranges considering buffer times.
|
|
381
|
-
|
|
382
|
-
**How it works:**
|
|
383
|
-
1. Loads the service's `bufferTimeBefore` and `bufferTimeAfter` (falls back to `defaultBufferTime` from plugin config)
|
|
384
|
-
2. Computes the **blocked window**: `[startTime - bufferBefore, endTime + bufferAfter]`
|
|
385
|
-
3. Queries existing reservations for the same resource where status is not `cancelled` or `no-show`
|
|
386
|
-
4. On updates, excludes the current reservation from the conflict check
|
|
387
|
-
5. Throws a `ValidationError` if any overlap is found
|
|
388
|
-
|
|
389
|
-
**Example scenario:**
|
|
390
|
-
```
|
|
391
|
-
Service: Haircut (30 min, 5 min buffer before, 10 min buffer after)
|
|
392
|
-
Reservation: 10:00 - 10:30
|
|
393
|
-
Blocked window: 09:55 - 10:40
|
|
394
|
-
|
|
395
|
-
Another booking at 10:20 on the same resource -> CONFLICT ERROR
|
|
396
|
-
Another booking at 10:45 on the same resource -> OK
|
|
397
|
-
Another booking at 10:20 on a DIFFERENT resource -> OK
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
**Why this matters:** Protects against double-booking even when multiple users book simultaneously or when frontend data is stale. The server-side check is the single source of truth. Buffer times account for real-world setup and cleanup between appointments — a stylist needs time to clean their station, a room needs to be prepared, etc.
|
|
401
|
-
|
|
402
|
-
### Status Transition Enforcement
|
|
403
|
-
|
|
404
|
-
**Hook:** `validateStatusTransition`
|
|
405
|
-
|
|
406
|
-
Enforces a strict status state machine:
|
|
407
|
-
|
|
408
|
-
```
|
|
409
|
-
+-> confirmed --+-> completed
|
|
410
|
-
| |
|
|
411
|
-
pending ------+ +-> cancelled
|
|
412
|
-
| |
|
|
413
|
-
+-> cancelled +-> no-show
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
**Rules:**
|
|
417
|
-
- **On create (public/unauthenticated):** Status must be `pending` (or not set, defaults to `pending`)
|
|
418
|
-
- **On create (authenticated admin):** Status can be `pending` or `confirmed` — this allows staff to create walk-in reservations that are already confirmed without a second step
|
|
419
|
-
- **On update:** Only valid transitions are allowed:
|
|
420
|
-
- `pending` -> `confirmed`, `cancelled`
|
|
421
|
-
- `confirmed` -> `completed`, `cancelled`, `no-show`
|
|
422
|
-
- `completed`, `cancelled`, `no-show` -> *(terminal, no transitions allowed)*
|
|
423
|
-
|
|
424
|
-
Invalid transitions throw a `ValidationError`.
|
|
425
|
-
|
|
426
|
-
**Why this matters:** The state machine ensures data integrity by preventing nonsensical transitions (e.g., marking a cancelled reservation as completed). Terminal states (`completed`, `cancelled`, `no-show`) prevent accidental reopening of closed reservations. The admin quick-confirm feature supports walk-in workflows without bypassing the state machine — staff can create a reservation as `confirmed` in one step, but they still cannot skip directly to `completed`.
|
|
427
|
-
|
|
428
|
-
### Common Workflows
|
|
429
|
-
|
|
430
|
-
These workflows show how the status lifecycle and hooks work together in real-world scenarios.
|
|
431
|
-
|
|
432
|
-
**1. Online Booking (standard)**
|
|
433
|
-
```
|
|
434
|
-
Customer visits booking page → selects service, resource, time slot
|
|
435
|
-
→ payload.create({ status: 'pending' }) [hooks: endTime calculated, conflicts checked]
|
|
436
|
-
→ Admin reviews in admin panel
|
|
437
|
-
→ payload.update({ status: 'confirmed' }) [hooks: transition validated]
|
|
438
|
-
→ Appointment takes place
|
|
439
|
-
→ payload.update({ status: 'completed' }) [hooks: transition validated]
|
|
440
|
-
```
|
|
441
|
-
|
|
442
|
-
**2. Walk-In Booking**
|
|
443
|
-
```
|
|
444
|
-
Customer walks in, staff creates booking in admin panel
|
|
445
|
-
→ payload.create({ status: 'confirmed' }) [admin user, hooks: endTime + conflicts]
|
|
446
|
-
→ Appointment takes place
|
|
447
|
-
→ payload.update({ status: 'completed' })
|
|
448
|
-
```
|
|
449
|
-
Skips the `pending` step entirely — authenticated admin users can create directly as `confirmed`.
|
|
450
|
-
|
|
451
|
-
**3. Payment-Gated Booking**
|
|
452
|
-
```
|
|
453
|
-
Customer selects time slot on your frontend
|
|
454
|
-
→ payload.create({ status: 'pending' }) [slot is now held]
|
|
455
|
-
→ Your app creates a Stripe Checkout Session
|
|
456
|
-
→ Customer completes payment
|
|
457
|
-
→ Stripe webhook fires → payload.update({ status: 'confirmed' })
|
|
458
|
-
→ Appointment takes place → 'completed'
|
|
459
|
-
```
|
|
460
|
-
The `pending → confirmed` transition fits payment flows naturally. The slot is held from the moment of creation (conflict detection ran on create), so no one else can book the same time while payment processes. See [Integration Patterns](#integration-patterns) for a full code example.
|
|
461
|
-
|
|
462
|
-
**4. Customer Cancellation**
|
|
463
|
-
```
|
|
464
|
-
Customer requests cancellation (from 'pending' or 'confirmed')
|
|
465
|
-
→ payload.update({ status: 'cancelled', cancellationReason: '...' })
|
|
466
|
-
→ Hook checks: hours_until_appointment >= cancellationNoticePeriod
|
|
467
|
-
→ If enough notice: cancellation succeeds
|
|
468
|
-
→ If too late: ValidationError thrown, reservation unchanged
|
|
469
|
-
```
|
|
470
|
-
|
|
471
|
-
**5. No-Show Handling**
|
|
472
|
-
```
|
|
473
|
-
Appointment time passes, customer does not arrive
|
|
474
|
-
→ Admin marks: payload.update({ status: 'no-show' }) [only from 'confirmed']
|
|
475
|
-
→ Reservation is terminal — cannot be reopened
|
|
476
|
-
```
|
|
477
|
-
No-shows can only be marked on `confirmed` reservations. A `pending` reservation that nobody showed up for should be `cancelled` instead, since it was never confirmed.
|
|
478
|
-
|
|
479
|
-
### Cancellation Policy
|
|
480
|
-
|
|
481
|
-
**Hook:** `validateCancellation`
|
|
482
|
-
|
|
483
|
-
Enforces a minimum notice period for cancellations. When transitioning to `cancelled`, the hook checks that:
|
|
484
|
-
|
|
485
|
-
```
|
|
486
|
-
hours_until_appointment >= cancellationNoticePeriod
|
|
487
|
-
```
|
|
488
|
-
|
|
489
|
-
With the default `cancellationNoticePeriod: 24`, you cannot cancel a reservation that starts within the next 24 hours. The hook throws a `ValidationError` with details about how many hours remain.
|
|
490
|
-
|
|
491
|
-
**Why this matters:** Protects the business from last-minute cancellations that leave empty time slots that can't be filled by other customers. The configurable notice period lets each business set their own policy — a busy salon might require 48 hours, while a consultant might only need 2. Automated cleanup tasks can use the escape hatch to bypass this check when cancelling stale pending reservations.
|
|
492
|
-
|
|
493
|
-
### Escape Hatch
|
|
494
|
-
|
|
495
|
-
All four hooks check for `context.skipReservationHooks` and skip validation if it's truthy. This lets you bypass hooks for administrative operations, data migrations, or seeding:
|
|
496
|
-
|
|
497
|
-
```typescript
|
|
498
|
-
await payload.create({
|
|
499
|
-
collection: 'reservations',
|
|
500
|
-
data: {
|
|
501
|
-
service: serviceId,
|
|
502
|
-
resource: resourceId,
|
|
503
|
-
customer: customerId,
|
|
504
|
-
startTime: '2025-06-15T10:00:00.000Z',
|
|
505
|
-
status: 'completed', // Normally would fail (only pending/confirmed allowed on create)
|
|
506
|
-
},
|
|
507
|
-
context: {
|
|
508
|
-
skipReservationHooks: true,
|
|
509
|
-
},
|
|
510
|
-
})
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
> **Note:** Authenticated admin users can create reservations with `'confirmed'` status without the escape hatch. The escape hatch is only needed for statuses that are never allowed on create (e.g., `'completed'`, `'cancelled'`, `'no-show'`) or to bypass conflict detection and cancellation policy checks.
|
|
514
|
-
|
|
515
|
-
### Integration Patterns
|
|
516
|
-
|
|
517
|
-
#### Payment Integration (Stripe)
|
|
518
|
-
|
|
519
|
-
The `pending → confirmed` transition is a natural fit for payment-gated bookings. The reservation holds the time slot while payment processes, and the conflict detection hook has already validated availability on create.
|
|
520
|
-
|
|
521
|
-
**Flow:**
|
|
522
|
-
1. Customer creates a reservation → status is `pending`, slot is held
|
|
523
|
-
2. Your app creates a Stripe Checkout Session with the reservation ID in metadata
|
|
524
|
-
3. Customer completes payment on Stripe's hosted page
|
|
525
|
-
4. Stripe sends a `checkout.session.completed` webhook to your app
|
|
526
|
-
5. Your webhook handler updates the reservation to `confirmed`
|
|
527
|
-
6. If payment fails or expires, the reservation stays `pending` for cleanup
|
|
528
|
-
|
|
529
|
-
**Webhook handler example:**
|
|
530
|
-
|
|
531
|
-
```ts
|
|
532
|
-
// app/api/stripe-webhook/route.ts
|
|
533
|
-
import { getPayload } from 'payload'
|
|
534
|
-
import config from '@payload-config'
|
|
535
|
-
import Stripe from 'stripe'
|
|
536
|
-
|
|
537
|
-
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
|
|
538
|
-
|
|
539
|
-
export async function POST(req: Request) {
|
|
540
|
-
const body = await req.text()
|
|
541
|
-
const sig = req.headers.get('stripe-signature')!
|
|
542
|
-
|
|
543
|
-
const event = stripe.webhooks.constructEvent(
|
|
544
|
-
body,
|
|
545
|
-
sig,
|
|
546
|
-
process.env.STRIPE_WEBHOOK_SECRET!,
|
|
547
|
-
)
|
|
548
|
-
|
|
549
|
-
if (event.type === 'checkout.session.completed') {
|
|
550
|
-
const session = event.data.object as Stripe.Checkout.Session
|
|
551
|
-
const reservationId = session.metadata?.reservationId
|
|
552
|
-
|
|
553
|
-
if (reservationId) {
|
|
554
|
-
const payload = await getPayload({ config })
|
|
555
|
-
await payload.update({
|
|
556
|
-
collection: 'reservations',
|
|
557
|
-
id: reservationId,
|
|
558
|
-
data: { status: 'confirmed' },
|
|
559
|
-
})
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
return new Response('OK', { status: 200 })
|
|
564
|
-
}
|
|
565
|
-
```
|
|
566
|
-
|
|
567
|
-
> **Slot protection:** The `validateConflicts` hook runs on create, so the time slot is already reserved while the customer pays. No other booking can claim the same slot, even if the payment takes several minutes.
|
|
568
|
-
|
|
569
|
-
#### Notification Integration
|
|
570
|
-
|
|
571
|
-
Use Payload's `afterChange` hook on the Reservations collection (in your app config, outside the plugin) to trigger notifications when status changes. The plugin does not send notifications itself, giving you full control over messaging.
|
|
572
|
-
|
|
573
|
-
**Example scenarios:**
|
|
574
|
-
- **Confirmed** — send a confirmation email with appointment details
|
|
575
|
-
- **Upcoming reminder** — trigger a reminder 24 hours before the appointment (via a scheduled task)
|
|
576
|
-
- **Cancelled** — notify the customer and optionally alert staff about the freed slot
|
|
577
|
-
- **No-show** — notify staff for internal tracking
|
|
578
|
-
|
|
579
|
-
```ts
|
|
580
|
-
// In your payload.config.ts, add an afterChange hook to the reservations collection
|
|
581
|
-
// using Payload's collections override or a separate plugin
|
|
582
|
-
|
|
583
|
-
const notifyOnStatusChange: CollectionAfterChangeHook = async ({
|
|
584
|
-
doc,
|
|
585
|
-
previousDoc,
|
|
586
|
-
operation,
|
|
587
|
-
}) => {
|
|
588
|
-
if (operation === 'update' && doc.status !== previousDoc.status) {
|
|
589
|
-
switch (doc.status) {
|
|
590
|
-
case 'confirmed':
|
|
591
|
-
await sendConfirmationEmail(doc)
|
|
592
|
-
break
|
|
593
|
-
case 'cancelled':
|
|
594
|
-
await sendCancellationEmail(doc)
|
|
595
|
-
break
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
```
|
|
600
|
-
|
|
601
|
-
#### Scheduled Cleanup
|
|
602
|
-
|
|
603
|
-
Reservations that stay `pending` indefinitely (e.g., abandoned payment flows) hold time slots that could be used by other customers. Set up a scheduled task to cancel stale pending reservations:
|
|
604
|
-
|
|
605
|
-
- Query for `pending` reservations older than your threshold (e.g., 30 minutes)
|
|
606
|
-
- Update them to `cancelled` using the escape hatch to bypass the cancellation notice period
|
|
607
|
-
- Optionally notify the customer that their hold expired
|
|
608
|
-
|
|
609
|
-
```ts
|
|
610
|
-
// Example: cron job or scheduled task
|
|
611
|
-
const payload = await getPayload({ config })
|
|
612
|
-
|
|
613
|
-
const thirtyMinutesAgo = new Date(Date.now() - 30 * 60 * 1000)
|
|
614
|
-
|
|
615
|
-
const { docs: staleReservations } = await payload.find({
|
|
616
|
-
collection: 'reservations',
|
|
617
|
-
where: {
|
|
618
|
-
status: { equals: 'pending' },
|
|
619
|
-
createdAt: { less_than: thirtyMinutesAgo.toISOString() },
|
|
620
|
-
},
|
|
621
|
-
})
|
|
622
|
-
|
|
623
|
-
for (const reservation of staleReservations) {
|
|
624
|
-
await payload.update({
|
|
625
|
-
collection: 'reservations',
|
|
626
|
-
id: reservation.id,
|
|
627
|
-
data: {
|
|
628
|
-
status: 'cancelled',
|
|
629
|
-
cancellationReason: 'Automatically cancelled — payment not completed',
|
|
630
|
-
},
|
|
631
|
-
context: { skipReservationHooks: true }, // bypass cancellation notice period
|
|
632
|
-
})
|
|
633
|
-
}
|
|
634
|
-
```
|
|
635
|
-
|
|
636
61
|
---
|
|
637
62
|
|
|
638
|
-
##
|
|
639
|
-
|
|
640
|
-
### Dashboard Widget
|
|
641
|
-
|
|
642
|
-
**Type:** React Server Component (RSC)
|
|
643
|
-
**Location:** Modular Dashboard Widget (`admin.dashboard.widgets`)
|
|
644
|
-
**Widget slug:** `reservation-todays-reservations`
|
|
645
|
-
**Default size:** medium–large
|
|
646
|
-
|
|
647
|
-
Uses Payload's modular dashboard widget system (v3.69.0+), which supports configurable sizing, drag-and-drop layout, and add/remove functionality. Displays a summary of today's reservations:
|
|
648
|
-
|
|
649
|
-
- **Total** reservations today
|
|
650
|
-
- **Upcoming** reservations (not yet completed/cancelled, start time in future)
|
|
651
|
-
- **Completed** reservations
|
|
652
|
-
- **Cancelled** reservations
|
|
653
|
-
- **Next Appointment** details (time and status)
|
|
654
|
-
|
|
655
|
-
The widget queries the database directly via the Payload Local API (server-side, no HTTP requests).
|
|
656
|
-
|
|
657
|
-
### Calendar View
|
|
63
|
+
## Resource Owner Multi-Tenancy
|
|
658
64
|
|
|
659
|
-
|
|
660
|
-
**Location:** Replaces the Reservations list view
|
|
661
|
-
|
|
662
|
-
A CSS Grid-based calendar (no external dependencies) with three view modes:
|
|
663
|
-
|
|
664
|
-
- **Month View** - 6-week grid showing all days with reservation chips
|
|
665
|
-
- **Week View** - 7-day grid with hourly rows (7am-6pm)
|
|
666
|
-
- **Day View** - Single day with hourly rows (7am-8pm)
|
|
667
|
-
|
|
668
|
-
**Features:**
|
|
669
|
-
- Navigation (previous/next, "Today" button)
|
|
670
|
-
- Color-coded by status:
|
|
671
|
-
- Pending: Yellow
|
|
672
|
-
- Confirmed: Blue
|
|
673
|
-
- Completed: Green
|
|
674
|
-
- Cancelled: Gray
|
|
675
|
-
- No-show: Red
|
|
676
|
-
- **Status legend** displayed below the header explaining what each color means
|
|
677
|
-
- **Click-to-create:** Click any calendar cell to open a new reservation drawer with the start time pre-filled
|
|
678
|
-
- Month view: clicking a day cell pre-fills start time at 9:00 AM on that date
|
|
679
|
-
- Week/day views: clicking a time cell pre-fills the exact hour for that slot
|
|
680
|
-
- Click any existing reservation to open a Payload document drawer for editing
|
|
681
|
-
- **Enhanced event display:**
|
|
682
|
-
- Month view (compact): shows time + service name
|
|
683
|
-
- Week/day views (full): shows time + service name + customer name
|
|
684
|
-
- **Tooltips:** Hover any reservation event to see full details (service, time range, customer, resource, status) via native browser tooltip
|
|
685
|
-
- **Current time indicator:** A red horizontal line in week/day views showing the current time position within the matching hour cell
|
|
686
|
-
- Fetches data via REST API for the visible date range
|
|
687
|
-
|
|
688
|
-
### Customer Picker
|
|
689
|
-
|
|
690
|
-
**Type:** Client Component
|
|
691
|
-
**Location:** Replaces the default Reservations `customer` relationship field
|
|
692
|
-
|
|
693
|
-
A custom field component that replaces the standard relationship dropdown with a rich customer search experience:
|
|
694
|
-
|
|
695
|
-
- **Multi-field search** — searches across customer name, phone, and email simultaneously using debounced `contains` queries (300ms debounce)
|
|
696
|
-
- **Rich dropdown** — each result shows the customer's name (bold), phone number, and email address
|
|
697
|
-
- **Selected display** — selected customer shows full details (name, phone, email) instead of just a name
|
|
698
|
-
- **Inline create** — "Create new customer" button opens a Payload document drawer to create a customer without leaving the reservation form
|
|
699
|
-
- **Role filtering** — when `customerRole` is set (e.g., `'customer'`), only users with that role appear in search results. Set to `false` (default) to show all users
|
|
700
|
-
- **Custom search endpoint** — uses `/api/reservation-customer-search` for efficient multi-field search with pagination
|
|
701
|
-
|
|
702
|
-
**Configuration:**
|
|
65
|
+
Enable `resourceOwnerMode` to support Airbnb-style platforms where each user manages their own listings (Resources) and sees only the reservations made against them. This is opt-in — single-tenant installs are unaffected.
|
|
703
66
|
|
|
704
67
|
```typescript
|
|
705
|
-
// Show all users (default)
|
|
706
|
-
payloadReserve()
|
|
707
|
-
|
|
708
|
-
// Only show users with role 'customer'
|
|
709
|
-
payloadReserve({ customerRole: 'customer' })
|
|
710
|
-
```
|
|
711
|
-
|
|
712
|
-
> **Note:** When `customerRole` is set, your user collection must have a `role` field. The plugin does not add this field — define it in your Users collection config.
|
|
713
|
-
|
|
714
|
-
### Availability Overview
|
|
715
|
-
|
|
716
|
-
**Type:** Client Component
|
|
717
|
-
**Location:** Custom admin view at `/admin/reservation-availability`
|
|
718
|
-
|
|
719
|
-
A weekly grid showing resource availability:
|
|
720
|
-
|
|
721
|
-
- **Rows** = Active resources
|
|
722
|
-
- **Columns** = Days of the week
|
|
723
|
-
- **Green slots** = Available time ranges (from schedules)
|
|
724
|
-
- **Blue slots** = Booked times (from reservations)
|
|
725
|
-
- **Gray slots** = Exception dates (unavailable)
|
|
726
|
-
|
|
727
|
-
Navigate between weeks with previous/next buttons or jump to "This Week".
|
|
728
|
-
|
|
729
|
-
---
|
|
730
|
-
|
|
731
|
-
## Utilities
|
|
732
|
-
|
|
733
|
-
### slotUtils.ts
|
|
734
|
-
|
|
735
|
-
Time math helpers used by hooks:
|
|
736
|
-
|
|
737
|
-
| Function | Description |
|
|
738
|
-
|----------|-------------|
|
|
739
|
-
| `addMinutes(date, minutes)` | Add minutes to a date, returns new Date |
|
|
740
|
-
| `doRangesOverlap(startA, endA, startB, endB)` | Check if two time ranges overlap (half-open intervals) |
|
|
741
|
-
| `computeBlockedWindow(start, end, bufferBefore, bufferAfter)` | Compute effective blocked window with buffers |
|
|
742
|
-
| `hoursUntil(futureDate, now?)` | Calculate hours between now and a future date |
|
|
743
|
-
|
|
744
|
-
### scheduleUtils.ts
|
|
745
|
-
|
|
746
|
-
Schedule resolution helpers used by admin components:
|
|
747
|
-
|
|
748
|
-
| Function | Description |
|
|
749
|
-
|----------|-------------|
|
|
750
|
-
| `getDayOfWeek(date)` | Get the DayOfWeek value for a Date |
|
|
751
|
-
| `dateMatchesDay(date, day)` | Check if a date matches a DayOfWeek |
|
|
752
|
-
| `parseTime(time)` | Parse "HH:mm" string to hours/minutes |
|
|
753
|
-
| `combineDateAndTime(date, time)` | Merge a date with a "HH:mm" time string |
|
|
754
|
-
| `isExceptionDate(date, exceptions)` | Check if a date is an exception |
|
|
755
|
-
| `resolveScheduleForDate(schedule, date)` | Resolve concrete time ranges for a date |
|
|
756
|
-
|
|
757
|
-
---
|
|
758
|
-
|
|
759
|
-
## Frontend Reservation Guide
|
|
760
|
-
|
|
761
|
-
The plugin is backend-only — it adds collections, hooks, and admin UI to Payload but does not include any customer-facing pages. However, you can build a full booking flow using Payload's built-in Local API (from Server Components / Server Actions) or REST API. No custom endpoints are needed.
|
|
762
|
-
|
|
763
|
-
### Step 1: Configure Access Control
|
|
764
|
-
|
|
765
|
-
By default all collections use Payload's default access control (authenticated users only). To allow public booking, pass `access` overrides in your plugin config:
|
|
766
|
-
|
|
767
|
-
```ts
|
|
768
|
-
import { payloadReserve } from 'payload-reserve'
|
|
769
|
-
|
|
770
68
|
payloadReserve({
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
read: () => true, // anyone can browse resources
|
|
777
|
-
},
|
|
778
|
-
schedules: {
|
|
779
|
-
read: () => true, // anyone can check availability
|
|
780
|
-
},
|
|
781
|
-
reservations: {
|
|
782
|
-
create: () => true, // guests can book
|
|
783
|
-
read: ({ req }) => {
|
|
784
|
-
// customers can only read their own reservations
|
|
785
|
-
if (req.user) return true
|
|
786
|
-
return false
|
|
787
|
-
},
|
|
788
|
-
},
|
|
789
|
-
},
|
|
790
|
-
})
|
|
791
|
-
```
|
|
792
|
-
|
|
793
|
-
> **Note:** Access control for the Users collection itself is defined on your Users collection config, not via the plugin's `access` option. The plugin only extends the Users collection with fields — it doesn't control its access.
|
|
794
|
-
|
|
795
|
-
> **Important:** Always use `overrideAccess: false` in your frontend queries so these rules are enforced. Without it, Payload bypasses access control entirely.
|
|
796
|
-
|
|
797
|
-
### Step 2: Fetch Available Services
|
|
798
|
-
|
|
799
|
-
Use a React Server Component to list active services:
|
|
800
|
-
|
|
801
|
-
```tsx
|
|
802
|
-
// app/book/page.tsx
|
|
803
|
-
import { getPayload } from 'payload'
|
|
804
|
-
import config from '@payload-config'
|
|
805
|
-
|
|
806
|
-
export default async function BookingPage() {
|
|
807
|
-
const payload = await getPayload({ config })
|
|
808
|
-
|
|
809
|
-
const { docs: services } = await payload.find({
|
|
810
|
-
collection: 'services',
|
|
811
|
-
overrideAccess: false,
|
|
812
|
-
where: { active: { equals: true } },
|
|
813
|
-
})
|
|
814
|
-
|
|
815
|
-
return (
|
|
816
|
-
<ul>
|
|
817
|
-
{services.map((service) => (
|
|
818
|
-
<li key={service.id}>
|
|
819
|
-
{service.name} — {service.duration} min — ${service.price}
|
|
820
|
-
</li>
|
|
821
|
-
))}
|
|
822
|
-
</ul>
|
|
823
|
-
)
|
|
824
|
-
}
|
|
825
|
-
```
|
|
826
|
-
|
|
827
|
-
### Step 3: Fetch Resources for a Service
|
|
828
|
-
|
|
829
|
-
Once a customer picks a service, load the resources that offer it:
|
|
830
|
-
|
|
831
|
-
```ts
|
|
832
|
-
const { docs: resources } = await payload.find({
|
|
833
|
-
collection: 'resources',
|
|
834
|
-
overrideAccess: false,
|
|
835
|
-
where: {
|
|
836
|
-
services: { contains: selectedServiceId },
|
|
837
|
-
active: { equals: true },
|
|
69
|
+
userCollection: 'users', // required: which auth collection holds owners
|
|
70
|
+
resourceOwnerMode: {
|
|
71
|
+
adminRoles: ['admin'], // roles that bypass all filters (see all records)
|
|
72
|
+
ownerField: 'owner', // field name added to Resources (default: 'owner')
|
|
73
|
+
ownedServices: false, // set true if Services should also be owner-scoped
|
|
838
74
|
},
|
|
839
75
|
})
|
|
840
76
|
```
|
|
841
77
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
Fetch the resource's schedule and existing reservations for the target date, then compute open slots:
|
|
78
|
+
**What this does automatically:**
|
|
845
79
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
} from 'payload-reserve'
|
|
853
|
-
|
|
854
|
-
// 1. Get the resource's active schedule
|
|
855
|
-
const { docs: schedules } = await payload.find({
|
|
856
|
-
collection: 'schedules',
|
|
857
|
-
overrideAccess: false,
|
|
858
|
-
where: {
|
|
859
|
-
resource: { equals: resourceId },
|
|
860
|
-
active: { equals: true },
|
|
861
|
-
},
|
|
862
|
-
})
|
|
863
|
-
|
|
864
|
-
// 2. Resolve available time ranges for the target date
|
|
865
|
-
const targetDate = new Date('2025-03-15')
|
|
866
|
-
const availableRanges = schedules.flatMap((schedule) =>
|
|
867
|
-
resolveScheduleForDate(schedule, targetDate),
|
|
868
|
-
)
|
|
869
|
-
|
|
870
|
-
// 3. Fetch existing reservations for that resource on that date
|
|
871
|
-
const dayStart = new Date(targetDate)
|
|
872
|
-
dayStart.setHours(0, 0, 0, 0)
|
|
873
|
-
const dayEnd = new Date(targetDate)
|
|
874
|
-
dayEnd.setHours(23, 59, 59, 999)
|
|
875
|
-
|
|
876
|
-
const { docs: existingReservations } = await payload.find({
|
|
877
|
-
collection: 'reservations',
|
|
878
|
-
overrideAccess: false,
|
|
879
|
-
where: {
|
|
880
|
-
resource: { equals: resourceId },
|
|
881
|
-
startTime: { greater_than_equal: dayStart.toISOString() },
|
|
882
|
-
startTime: { less_than: dayEnd.toISOString() },
|
|
883
|
-
status: { not_equals: 'cancelled' },
|
|
884
|
-
},
|
|
885
|
-
})
|
|
886
|
-
|
|
887
|
-
// 4. Generate slots by stepping through available ranges
|
|
888
|
-
// and filtering out conflicts with existing reservations
|
|
889
|
-
const slots = []
|
|
890
|
-
for (const range of availableRanges) {
|
|
891
|
-
let cursor = range.start
|
|
892
|
-
while (addMinutes(cursor, serviceDuration) <= range.end) {
|
|
893
|
-
const slotEnd = addMinutes(cursor, serviceDuration)
|
|
894
|
-
const blocked = existingReservations.some((res) => {
|
|
895
|
-
const window = computeBlockedWindow(
|
|
896
|
-
new Date(res.startTime),
|
|
897
|
-
new Date(res.endTime),
|
|
898
|
-
service.bufferTimeBefore ?? 0,
|
|
899
|
-
service.bufferTimeAfter ?? 0,
|
|
900
|
-
)
|
|
901
|
-
return doRangesOverlap(cursor, slotEnd, window.start, window.end)
|
|
902
|
-
})
|
|
903
|
-
if (!blocked) slots.push(cursor)
|
|
904
|
-
cursor = addMinutes(cursor, 15) // 15-minute step
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
```
|
|
80
|
+
| Collection | Behaviour |
|
|
81
|
+
|------------|-----------|
|
|
82
|
+
| Resources | Adds an `owner` relationship field (auto-populated on create); owners read/update/delete only their own records |
|
|
83
|
+
| Schedules | Owners read/update/delete only schedules whose resource they own (join through `resource.owner`) |
|
|
84
|
+
| Reservations | Owners can read reservations for their resources; mutations are admin-only |
|
|
85
|
+
| Services | Unchanged by default; set `ownedServices: true` to apply the same owner pattern |
|
|
908
86
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
Use a Server Action to create the reservation. The plugin's `beforeChange` hooks automatically calculate the end time, validate conflicts, and enforce status transitions:
|
|
912
|
-
|
|
913
|
-
```ts
|
|
914
|
-
'use server'
|
|
915
|
-
|
|
916
|
-
import { getPayload } from 'payload'
|
|
917
|
-
import config from '@payload-config'
|
|
918
|
-
|
|
919
|
-
export async function createReservation(data: {
|
|
920
|
-
service: string
|
|
921
|
-
resource: string
|
|
922
|
-
customerName: string
|
|
923
|
-
customerEmail: string
|
|
924
|
-
startTime: string
|
|
925
|
-
notes?: string
|
|
926
|
-
}) {
|
|
927
|
-
const payload = await getPayload({ config })
|
|
928
|
-
|
|
929
|
-
// Find or create the customer (customers are users)
|
|
930
|
-
const existing = await payload.find({
|
|
931
|
-
collection: 'users',
|
|
932
|
-
overrideAccess: false,
|
|
933
|
-
where: { email: { equals: data.customerEmail } },
|
|
934
|
-
})
|
|
935
|
-
|
|
936
|
-
let customerId: string
|
|
937
|
-
if (existing.docs.length > 0) {
|
|
938
|
-
customerId = String(existing.docs[0].id)
|
|
939
|
-
} else {
|
|
940
|
-
const customer = await payload.create({
|
|
941
|
-
collection: 'users',
|
|
942
|
-
overrideAccess: false,
|
|
943
|
-
data: {
|
|
944
|
-
name: data.customerName,
|
|
945
|
-
email: data.customerEmail,
|
|
946
|
-
password: generateSecurePassword(), // auth collection requires a password
|
|
947
|
-
},
|
|
948
|
-
})
|
|
949
|
-
customerId = String(customer.id)
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
// Create the reservation — hooks handle endTime calculation and conflict checks
|
|
953
|
-
const reservation = await payload.create({
|
|
954
|
-
collection: 'reservations',
|
|
955
|
-
overrideAccess: false,
|
|
956
|
-
data: {
|
|
957
|
-
service: data.service,
|
|
958
|
-
resource: data.resource,
|
|
959
|
-
customer: customerId,
|
|
960
|
-
startTime: data.startTime,
|
|
961
|
-
notes: data.notes,
|
|
962
|
-
// status defaults to 'pending'
|
|
963
|
-
},
|
|
964
|
-
})
|
|
965
|
-
|
|
966
|
-
return reservation
|
|
967
|
-
}
|
|
968
|
-
```
|
|
969
|
-
|
|
970
|
-
If the time slot is already booked, the `validateConflicts` hook will throw a `ValidationError` — catch it in your UI and prompt the user to pick a different slot.
|
|
971
|
-
|
|
972
|
-
### Security Notes
|
|
973
|
-
|
|
974
|
-
- **Always pass `overrideAccess: false`** — without it, Payload skips access control and any user can read/write anything.
|
|
975
|
-
- Validate and sanitize all user input before passing it to `payload.create`.
|
|
976
|
-
- Consider adding rate limiting to your Server Actions or API routes to prevent abuse.
|
|
977
|
-
- The plugin's hooks enforce conflict detection server-side, so double-bookings are prevented even if the frontend has stale data.
|
|
978
|
-
|
|
979
|
-
### Alternative: REST API
|
|
980
|
-
|
|
981
|
-
If you prefer a client-side-only approach (e.g., a separate SPA), use Payload's auto-generated REST API:
|
|
982
|
-
|
|
983
|
-
```ts
|
|
984
|
-
// Fetch services
|
|
985
|
-
const res = await fetch('https://your-site.com/api/services?where[active][equals]=true')
|
|
986
|
-
const { docs: services } = await res.json()
|
|
987
|
-
|
|
988
|
-
// Create a reservation (customer is a user ID)
|
|
989
|
-
const res = await fetch('https://your-site.com/api/reservations', {
|
|
990
|
-
method: 'POST',
|
|
991
|
-
headers: { 'Content-Type': 'application/json' },
|
|
992
|
-
body: JSON.stringify({
|
|
993
|
-
service: serviceId,
|
|
994
|
-
resource: resourceId,
|
|
995
|
-
customer: userId,
|
|
996
|
-
startTime: '2025-03-15T10:00:00.000Z',
|
|
997
|
-
}),
|
|
998
|
-
})
|
|
999
|
-
```
|
|
1000
|
-
|
|
1001
|
-
The same hooks and access control rules apply to REST API requests.
|
|
1002
|
-
|
|
1003
|
-
---
|
|
1004
|
-
|
|
1005
|
-
## Development
|
|
1006
|
-
|
|
1007
|
-
### Prerequisites
|
|
1008
|
-
|
|
1009
|
-
- Node.js ^18.20.2 or >=20.9.0
|
|
1010
|
-
- pnpm ^9 or ^10
|
|
1011
|
-
- MongoDB (or the in-memory server used for testing)
|
|
1012
|
-
|
|
1013
|
-
### Commands
|
|
1014
|
-
|
|
1015
|
-
```bash
|
|
1016
|
-
# Start dev server (Next.js + Payload admin panel)
|
|
1017
|
-
pnpm dev
|
|
1018
|
-
|
|
1019
|
-
# Generate Payload types after schema changes
|
|
1020
|
-
pnpm dev:generate-types
|
|
1021
|
-
|
|
1022
|
-
# Generate import map after adding/removing components
|
|
1023
|
-
pnpm dev:generate-importmap
|
|
1024
|
-
|
|
1025
|
-
# Run integration tests (Vitest)
|
|
1026
|
-
pnpm test:int
|
|
1027
|
-
|
|
1028
|
-
# Run end-to-end tests (Playwright)
|
|
1029
|
-
pnpm test:e2e
|
|
1030
|
-
|
|
1031
|
-
# Run all tests
|
|
1032
|
-
pnpm test
|
|
1033
|
-
|
|
1034
|
-
# Lint source code
|
|
1035
|
-
pnpm lint
|
|
1036
|
-
|
|
1037
|
-
# Auto-fix lint issues
|
|
1038
|
-
pnpm lint:fix
|
|
1039
|
-
|
|
1040
|
-
# Build for production
|
|
1041
|
-
pnpm build
|
|
1042
|
-
|
|
1043
|
-
# Clean build artifacts
|
|
1044
|
-
pnpm clean
|
|
1045
|
-
```
|
|
1046
|
-
|
|
1047
|
-
### Dev Environment
|
|
1048
|
-
|
|
1049
|
-
The `dev/` directory contains a complete Payload CMS app for testing:
|
|
1050
|
-
|
|
1051
|
-
- **`dev/payload.config.ts`** - Payload config with the plugin installed
|
|
1052
|
-
- **`dev/seed.ts`** - Seeds sample data: 3 services, 2 resources, 2 schedules, 2 customer users, 3 reservations
|
|
1053
|
-
- **`dev/int.spec.ts`** - Integration tests covering all hooks and CRUD operations
|
|
1054
|
-
|
|
1055
|
-
The dev environment uses `mongodb-memory-server` for testing, so no external MongoDB instance is required.
|
|
1056
|
-
|
|
1057
|
-
### Seed Data
|
|
1058
|
-
|
|
1059
|
-
When you run `pnpm dev`, the seed script creates:
|
|
1060
|
-
|
|
1061
|
-
**Services:**
|
|
1062
|
-
- Haircut (30 min, $35, 5 min buffer before, 10 min after)
|
|
1063
|
-
- Hair Coloring (90 min, $120, 10 min buffer before, 15 min after)
|
|
1064
|
-
- Consultation (15 min, free, 0 min buffer before, 5 min after)
|
|
1065
|
-
|
|
1066
|
-
**Resources:**
|
|
1067
|
-
- Alice Johnson (Senior Stylist) - performs all 3 services
|
|
1068
|
-
- Bob Smith (Junior Stylist) - performs Haircut and Consultation
|
|
1069
|
-
|
|
1070
|
-
**Schedules:**
|
|
1071
|
-
- Alice: Mon-Thu 9am-5pm, Fri 9am-3pm
|
|
1072
|
-
- Bob: Mon/Wed/Fri 10am-6pm, Sat 9am-2pm
|
|
1073
|
-
|
|
1074
|
-
**Customers (created as users):**
|
|
1075
|
-
- Jane Doe (jane@example.com)
|
|
1076
|
-
- John Public (john@example.com)
|
|
1077
|
-
|
|
1078
|
-
**Reservations (today):**
|
|
1079
|
-
- 9:00 AM - Haircut with Alice (confirmed)
|
|
1080
|
-
- 10:00 AM - Consultation with Bob (pending)
|
|
1081
|
-
- 2:00 PM - Hair Coloring with Alice (pending)
|
|
1082
|
-
|
|
1083
|
-
---
|
|
1084
|
-
|
|
1085
|
-
## Project Structure
|
|
1086
|
-
|
|
1087
|
-
```
|
|
1088
|
-
src/
|
|
1089
|
-
index.ts # Public API: re-exports plugin + types
|
|
1090
|
-
plugin.ts # Main plugin factory function
|
|
1091
|
-
types.ts # All TypeScript types + status transitions
|
|
1092
|
-
defaults.ts # Default config values + resolver
|
|
1093
|
-
|
|
1094
|
-
collections/
|
|
1095
|
-
Services.ts # Service definitions
|
|
1096
|
-
Resources.ts # Providers/resources
|
|
1097
|
-
Schedules.ts # Availability schedules
|
|
1098
|
-
Reservations.ts # Bookings with hooks
|
|
1099
|
-
|
|
1100
|
-
hooks/
|
|
1101
|
-
reservations/
|
|
1102
|
-
calculateEndTime.ts # Auto-compute endTime
|
|
1103
|
-
validateConflicts.ts # Double-booking prevention
|
|
1104
|
-
validateStatusTransition.ts # Status state machine
|
|
1105
|
-
validateCancellation.ts # Cancellation notice period
|
|
1106
|
-
index.ts # Barrel export
|
|
1107
|
-
|
|
1108
|
-
utilities/
|
|
1109
|
-
slotUtils.ts # Time math helpers
|
|
1110
|
-
scheduleUtils.ts # Schedule resolution helpers
|
|
1111
|
-
|
|
1112
|
-
endpoints/
|
|
1113
|
-
customerSearch.ts # Custom search endpoint for customer picker
|
|
1114
|
-
|
|
1115
|
-
components/
|
|
1116
|
-
CalendarView/
|
|
1117
|
-
index.tsx # Client: calendar for reservations
|
|
1118
|
-
CalendarView.module.css
|
|
1119
|
-
CustomerField/
|
|
1120
|
-
index.tsx # Client: rich customer picker field
|
|
1121
|
-
CustomerField.module.css
|
|
1122
|
-
DashboardWidget/
|
|
1123
|
-
DashboardWidgetServer.tsx # RSC: today's booking stats
|
|
1124
|
-
DashboardWidget.module.css
|
|
1125
|
-
AvailabilityOverview/
|
|
1126
|
-
index.tsx # Client: weekly availability grid
|
|
1127
|
-
AvailabilityOverview.module.css
|
|
1128
|
-
|
|
1129
|
-
exports/
|
|
1130
|
-
client.ts # CalendarView, AvailabilityOverview, CustomerField
|
|
1131
|
-
rsc.ts # DashboardWidgetServer
|
|
1132
|
-
```
|
|
87
|
+
The `access` override in plugin config always takes precedence over the auto-wired functions, so you can fine-tune any collection without losing the rest.
|
|
1133
88
|
|
|
1134
89
|
---
|
|
1135
90
|
|
|
1136
|
-
##
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
```typescript
|
|
1154
|
-
import { DashboardWidgetServer } from 'payload-reserve/rsc'
|
|
1155
|
-
```
|
|
1156
|
-
|
|
1157
|
-
### Type Exports
|
|
1158
|
-
|
|
1159
|
-
```typescript
|
|
1160
|
-
import type {
|
|
1161
|
-
ReservationPluginConfig,
|
|
1162
|
-
ResolvedReservationPluginConfig,
|
|
1163
|
-
} from 'payload-reserve'
|
|
1164
|
-
```
|
|
1165
|
-
|
|
1166
|
-
### Integration Test Coverage
|
|
1167
|
-
|
|
1168
|
-
The plugin ships with 16 integration tests covering:
|
|
1169
|
-
|
|
1170
|
-
| Test | What It Verifies |
|
|
1171
|
-
|------|-----------------|
|
|
1172
|
-
| Collections registered | All 4 plugin collections exist after plugin init |
|
|
1173
|
-
| User collection extended | Users collection has phone, notes, bookings fields |
|
|
1174
|
-
| Create service | Service CRUD with all fields |
|
|
1175
|
-
| Create resource | Resource with service relationship |
|
|
1176
|
-
| Create schedule | Recurring schedule with slots |
|
|
1177
|
-
| Create user with customer fields | User with name, phone, email |
|
|
1178
|
-
| Auto endTime | endTime = startTime + duration |
|
|
1179
|
-
| Conflict: same resource | Double-booking is rejected |
|
|
1180
|
-
| Conflict: different resource | Same time on different resource is allowed |
|
|
1181
|
-
| Status: must start pending | Creating with non-pending status fails (public context) |
|
|
1182
|
-
| Status: valid transition | pending -> confirmed succeeds |
|
|
1183
|
-
| Status: admin create confirmed | Admin user can create reservation as confirmed |
|
|
1184
|
-
| Status: admin create completed | Admin user cannot create reservation as completed |
|
|
1185
|
-
| Status: invalid transition | completed -> pending fails |
|
|
1186
|
-
| Cancel: too late | Cancellation within notice period fails |
|
|
1187
|
-
| Cancel: sufficient notice | Cancellation with enough notice succeeds |
|
|
91
|
+
## Documentation
|
|
92
|
+
|
|
93
|
+
> 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.
|
|
94
|
+
|
|
95
|
+
| Topic | Contents |
|
|
96
|
+
|-------|----------|
|
|
97
|
+
| [Getting Started](https://github.com/elghaied/payload-reserve/blob/main/docs/getting-started.md) | Installation, quick start, what gets created |
|
|
98
|
+
| [Configuration](https://github.com/elghaied/payload-reserve/blob/main/docs/configuration.md) | All plugin options with types and defaults, including `resourceOwnerMode` |
|
|
99
|
+
| [Collections](https://github.com/elghaied/payload-reserve/blob/main/docs/collections.md) | Services, Resources, Schedules, Customers, Reservations schemas |
|
|
100
|
+
| [Status Machine](https://github.com/elghaied/payload-reserve/blob/main/docs/status-machine.md) | Default flow, custom machines, business logic hooks, escape hatch |
|
|
101
|
+
| [Booking Features](https://github.com/elghaied/payload-reserve/blob/main/docs/booking-features.md) | Duration types, multi-resource bookings, capacity modes |
|
|
102
|
+
| [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 |
|
|
104
|
+
| [Admin UI](https://github.com/elghaied/payload-reserve/blob/main/docs/admin-ui.md) | Calendar view, dashboard widget, availability overview |
|
|
105
|
+
| [Examples](https://github.com/elghaied/payload-reserve/blob/main/docs/examples.md) | Salon, hotel, restaurant, event venue, Stripe, email, multi-tenant (resource owner mode) |
|
|
106
|
+
| [Advanced](https://github.com/elghaied/payload-reserve/blob/main/docs/advanced.md) | DB indexes, reconciliation job for race condition detection |
|
|
107
|
+
| [Development](https://github.com/elghaied/payload-reserve/blob/main/docs/development.md) | Prerequisites, commands, project file tree |
|