payload-reserve 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +44 -1154
  2. package/dist/collections/Customers.d.ts +3 -0
  3. package/dist/collections/Customers.js +58 -0
  4. package/dist/collections/Customers.js.map +1 -0
  5. package/dist/collections/Reservations.js +124 -34
  6. package/dist/collections/Reservations.js.map +1 -1
  7. package/dist/collections/Resources.js +39 -0
  8. package/dist/collections/Resources.js.map +1 -1
  9. package/dist/collections/Schedules.js.map +1 -1
  10. package/dist/collections/Services.js +27 -0
  11. package/dist/collections/Services.js.map +1 -1
  12. package/dist/components/AvailabilityOverview/AvailabilityOverview.module.css +31 -0
  13. package/dist/components/AvailabilityOverview/index.js +59 -8
  14. package/dist/components/AvailabilityOverview/index.js.map +1 -1
  15. package/dist/components/CalendarView/CalendarView.module.css +171 -0
  16. package/dist/components/CalendarView/index.js +547 -38
  17. package/dist/components/CalendarView/index.js.map +1 -1
  18. package/dist/components/CustomerField/index.js +24 -10
  19. package/dist/components/CustomerField/index.js.map +1 -1
  20. package/dist/components/DashboardWidget/DashboardWidgetServer.js +21 -11
  21. package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
  22. package/dist/defaults.d.ts +1 -2
  23. package/dist/defaults.js +15 -4
  24. package/dist/defaults.js.map +1 -1
  25. package/dist/endpoints/cancelBooking.d.ts +3 -0
  26. package/dist/endpoints/cancelBooking.js +37 -0
  27. package/dist/endpoints/cancelBooking.js.map +1 -0
  28. package/dist/endpoints/checkAvailability.d.ts +3 -0
  29. package/dist/endpoints/checkAvailability.js +37 -0
  30. package/dist/endpoints/checkAvailability.js.map +1 -0
  31. package/dist/endpoints/createBooking.d.ts +3 -0
  32. package/dist/endpoints/createBooking.js +32 -0
  33. package/dist/endpoints/createBooking.js.map +1 -0
  34. package/dist/endpoints/customerSearch.js +76 -58
  35. package/dist/endpoints/customerSearch.js.map +1 -1
  36. package/dist/endpoints/getSlots.d.ts +3 -0
  37. package/dist/endpoints/getSlots.js +51 -0
  38. package/dist/endpoints/getSlots.js.map +1 -0
  39. package/dist/hooks/index.d.ts +2 -0
  40. package/dist/hooks/index.js +2 -0
  41. package/dist/hooks/index.js.map +1 -1
  42. package/dist/hooks/reservations/calculateEndTime.js +75 -9
  43. package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
  44. package/dist/hooks/reservations/checkIdempotency.d.ts +3 -0
  45. package/dist/hooks/reservations/checkIdempotency.js +32 -0
  46. package/dist/hooks/reservations/checkIdempotency.js.map +1 -0
  47. package/dist/hooks/reservations/onStatusChange.d.ts +3 -0
  48. package/dist/hooks/reservations/onStatusChange.js +38 -0
  49. package/dist/hooks/reservations/onStatusChange.js.map +1 -0
  50. package/dist/hooks/reservations/validateCancellation.js +1 -1
  51. package/dist/hooks/reservations/validateCancellation.js.map +1 -1
  52. package/dist/hooks/reservations/validateConflicts.js +34 -56
  53. package/dist/hooks/reservations/validateConflicts.js.map +1 -1
  54. package/dist/hooks/reservations/validateStatusTransition.d.ts +2 -1
  55. package/dist/hooks/reservations/validateStatusTransition.js +29 -6
  56. package/dist/hooks/reservations/validateStatusTransition.js.map +1 -1
  57. package/dist/index.d.ts +5 -1
  58. package/dist/index.js +3 -0
  59. package/dist/index.js.map +1 -1
  60. package/dist/plugin.js +49 -54
  61. package/dist/plugin.js.map +1 -1
  62. package/dist/services/AvailabilityService.d.ts +57 -0
  63. package/dist/services/AvailabilityService.js +232 -0
  64. package/dist/services/AvailabilityService.js.map +1 -0
  65. package/dist/services/index.d.ts +1 -0
  66. package/dist/services/index.js +3 -0
  67. package/dist/services/index.js.map +1 -0
  68. package/dist/translations/en.json +33 -1
  69. package/dist/translations/index.d.ts +5 -0
  70. package/dist/translations/index.js.map +1 -1
  71. package/dist/types.d.ts +65 -6
  72. package/dist/types.js +29 -9
  73. package/dist/types.js.map +1 -1
  74. package/dist/utilities/resolveReservationItems.d.ts +18 -0
  75. package/dist/utilities/resolveReservationItems.js +45 -0
  76. package/dist/utilities/resolveReservationItems.js.map +1 -0
  77. package/package.json +12 -9
package/README.md CHANGED
@@ -1,1187 +1,77 @@
1
- # payload-reserve - Reservation Plugin for Payload CMS 3.x
1
+ # payload-reserve
2
2
 
3
- A full-featured, reusable reservation/booking plugin for Payload CMS 3.x. Designed for salons, clinics, consultants, and any business that needs appointment scheduling with conflict prevention, status workflows, and admin UI components.
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
- - **4 Collections + User Extension** - Services, Resources, Schedules, and Reservations, plus customer fields added to your existing Users collection
42
- - **Double-Booking Prevention** - Automatic conflict detection with configurable buffer times
43
- - **Status State Machine** - Enforced workflow: pending -> confirmed -> completed/cancelled/no-show
44
- - **Auto End Time** - Automatically calculates reservation end time from service duration
45
- - **Cancellation Policy** - Configurable notice period enforcement
46
- - **Admin Quick-Confirm** - Authenticated admin users can create reservations directly as "confirmed" for walk-ins
47
- - **Calendar View** - Month/week/day calendar replacing the default reservations list view
48
- - **Click-to-Create** - Click any calendar cell to create a reservation with the start time pre-filled
49
- - **Event Tooltips** - Hover events to see full details (service, time range, customer, resource, status)
50
- - **Status Legend** - Color key displayed in the calendar explaining each status color
51
- - **Current Time Indicator** - Red line in week/day views marking the current time
52
- - **Dashboard Widget** - Server component showing today's booking stats at a glance
53
- - **Customer Picker** - Rich customer search field with multi-field search (name, phone, email), inline create/edit via document drawer, and optional role filtering
54
- - **Availability Grid** - Weekly overview of resource availability vs. booked slots
55
- - **Recurring & Manual Schedules** - Flexible schedule types with exception dates
56
- - **Fully Configurable** - Override slugs, access control, buffer times, and admin grouping
57
- - **Type-Safe** - Full TypeScript support with exported types
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
+ - **Configurable Status Machine** Define your own statuses, transitions, blocking states, and terminal states
14
+ - **Double-Booking Prevention** — Server-side conflict detection with configurable buffer times; respects capacity modes
15
+ - **Auto End Time** Calculates `endTime` from `startTime + service.duration` automatically
16
+ - **Three Duration Types** `fixed` (service duration), `flexible` (customer-specified end), and `full-day` bookings
17
+ - **Multi-Resource Bookings** Single reservation that spans multiple resources simultaneously via the `items` array
18
+ - **Capacity and Inventory** `quantity > 1` allows multiple concurrent bookings per resource; `capacityMode` (`per-reservation` | `per-guest`) controls how capacity is counted
19
+ - **Idempotency** Optional `idempotencyKey` prevents duplicate submissions
20
+ - **Cancellation Policy** Configurable minimum notice period enforcement
21
+ - **Plugin Hooks API** Seven lifecycle hooks (`beforeBookingCreate`, `afterBookingCreate`, `beforeBookingConfirm`, `afterBookingConfirm`, `beforeBookingCancel`, `afterBookingCancel`, `afterStatusChange`) for integrating email, Stripe, and external systems
22
+ - **Availability Service** Pure functions and DB helpers for slot generation and conflict checking
23
+ - **Public REST API** Five pre-built endpoints for availability, slot listing, booking, cancellation, and customer search
24
+ - **Calendar View** Month/week/day calendar replacing the default reservations list view
25
+ - **Dashboard Widget** Server component showing today's booking stats
26
+ - **Availability Overview** Weekly grid of resource availability vs. booked slots
27
+ - **Recurring and Manual Schedules** Weekly patterns with exception dates, or specific one-off dates
28
+ - **Localization Support** — Collection fields can be localized when Payload localization is enabled
29
+ - **Type-Safe** — Full TypeScript support with exported types
58
30
 
59
31
  ---
60
32
 
61
- ## Installation
33
+ ## Install
62
34
 
63
35
  ```bash
64
- # Install the plugin as a dependency
65
36
  pnpm add payload-reserve
66
-
67
- # Or if developing locally, link it
68
- pnpm link ./plugins/payload-reserve
37
+ # or
38
+ npm install payload-reserve
69
39
  ```
70
40
 
71
- **Peer Dependency:** Requires `payload ^3.76.1`.
41
+ **Peer dependencies:** `payload ^3.77.0`, `@payloadcms/ui ^3.77.0`, `@payloadcms/translations ^3.77.0`
72
42
 
73
43
  ---
74
44
 
75
45
  ## Quick Start
76
46
 
77
- Add the plugin to your `payload.config.ts`:
78
-
79
47
  ```typescript
80
48
  import { buildConfig } from 'payload'
81
49
  import { payloadReserve } from 'payload-reserve'
82
50
 
83
51
  export default buildConfig({
84
- // ... your existing config
52
+ collections: [/* your collections */],
85
53
  plugins: [
86
54
  payloadReserve(),
87
55
  ],
88
56
  })
89
57
  ```
90
58
 
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
- ---
637
-
638
- ## Admin UI Components
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
658
-
659
- **Type:** Client Component
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:**
703
-
704
- ```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
- payloadReserve({
771
- access: {
772
- services: {
773
- read: () => true, // anyone can browse services
774
- },
775
- resources: {
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 },
838
- },
839
- })
840
- ```
841
-
842
- ### Step 4: Check Availability
843
-
844
- Fetch the resource's schedule and existing reservations for the target date, then compute open slots:
845
-
846
- ```ts
847
- import {
848
- resolveScheduleForDate,
849
- addMinutes,
850
- doRangesOverlap,
851
- computeBlockedWindow,
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
- ```
908
-
909
- ### Step 5: Create the Reservation
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
- ```
1133
-
1134
59
  ---
1135
60
 
1136
- ## API Reference
1137
-
1138
- ### Plugin Export
1139
-
1140
- ```typescript
1141
- import { payloadReserve } from 'payload-reserve'
1142
- import type { ReservationPluginConfig } from 'payload-reserve'
1143
- ```
1144
-
1145
- ### Client Exports
1146
-
1147
- ```typescript
1148
- import { CalendarView, AvailabilityOverview, CustomerField } from 'payload-reserve/client'
1149
- ```
1150
-
1151
- ### RSC Exports
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 |
61
+ ## Documentation
62
+
63
+ > 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.
64
+
65
+ | Topic | Contents |
66
+ |-------|----------|
67
+ | [Getting Started](https://github.com/elghaied/payload-reserve/blob/main/docs/getting-started.md) | Installation, quick start, what gets created |
68
+ | [Configuration](https://github.com/elghaied/payload-reserve/blob/main/docs/configuration.md) | All plugin options with types and defaults |
69
+ | [Collections](https://github.com/elghaied/payload-reserve/blob/main/docs/collections.md) | Services, Resources, Schedules, Customers, Reservations schemas |
70
+ | [Status Machine](https://github.com/elghaied/payload-reserve/blob/main/docs/status-machine.md) | Default flow, custom machines, business logic hooks, escape hatch |
71
+ | [Booking Features](https://github.com/elghaied/payload-reserve/blob/main/docs/booking-features.md) | Duration types, multi-resource bookings, capacity modes |
72
+ | [Hooks API](https://github.com/elghaied/payload-reserve/blob/main/docs/hooks-api.md) | All 7 plugin hook types with signatures and examples |
73
+ | [REST API](https://github.com/elghaied/payload-reserve/blob/main/docs/rest-api.md) | All 5 public endpoints with params, responses, and fetch examples |
74
+ | [Admin UI](https://github.com/elghaied/payload-reserve/blob/main/docs/admin-ui.md) | Calendar view, dashboard widget, availability overview |
75
+ | [Examples](https://github.com/elghaied/payload-reserve/blob/main/docs/examples.md) | Salon, hotel, restaurant, event venue, Stripe, email, multi-tenant |
76
+ | [Advanced](https://github.com/elghaied/payload-reserve/blob/main/docs/advanced.md) | DB indexes, reconciliation job for race condition detection |
77
+ | [Development](https://github.com/elghaied/payload-reserve/blob/main/docs/development.md) | Prerequisites, commands, project file tree |