payload-reserve 1.0.0 → 1.0.1

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 CHANGED
@@ -27,6 +27,7 @@ A full-featured, reusable reservation/booking plugin for Payload CMS 3.x. Design
27
27
  - [Admin UI Components](#admin-ui-components)
28
28
  - [Dashboard Widget](#dashboard-widget)
29
29
  - [Calendar View](#calendar-view)
30
+ - [Customer Picker](#customer-picker)
30
31
  - [Availability Overview](#availability-overview)
31
32
  - [Utilities](#utilities)
32
33
  - [Development](#development)
@@ -49,6 +50,7 @@ A full-featured, reusable reservation/booking plugin for Payload CMS 3.x. Design
49
50
  - **Status Legend** - Color key displayed in the calendar explaining each status color
50
51
  - **Current Time Indicator** - Red line in week/day views marking the current time
51
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
52
54
  - **Availability Grid** - Weekly overview of resource availability vs. booked slots
53
55
  - **Recurring & Manual Schedules** - Flexible schedule types with exception dates
54
56
  - **Fully Configurable** - Override slugs, access control, buffer times, and admin grouping
@@ -66,7 +68,7 @@ pnpm add payload-reserve
66
68
  pnpm link ./plugins/payload-reserve
67
69
  ```
68
70
 
69
- **Peer Dependency:** Requires `payload ^3.69.0` (modular dashboard widget API).
71
+ **Peer Dependency:** Requires `payload ^3.76.1`.
70
72
 
71
73
  ---
72
74
 
@@ -76,12 +78,12 @@ Add the plugin to your `payload.config.ts`:
76
78
 
77
79
  ```typescript
78
80
  import { buildConfig } from 'payload'
79
- import { reservationPlugin } from 'payload-reserve'
81
+ import { payloadReserve } from 'payload-reserve'
80
82
 
81
83
  export default buildConfig({
82
84
  // ... your existing config
83
85
  plugins: [
84
- reservationPlugin(),
86
+ payloadReserve(),
85
87
  ],
86
88
  })
87
89
  ```
@@ -97,7 +99,7 @@ That's it. The plugin registers 4 collections, extends your existing Users colle
97
99
  All options are optional. The plugin works out of the box with sensible defaults.
98
100
 
99
101
  ```typescript
100
- import { reservationPlugin } from 'payload-reserve'
102
+ import { payloadReserve } from 'payload-reserve'
101
103
  import type { ReservationPluginConfig } from 'payload-reserve'
102
104
 
103
105
  const config: ReservationPluginConfig = {
@@ -106,10 +108,11 @@ const config: ReservationPluginConfig = {
106
108
 
107
109
  // Override collection slugs
108
110
  slugs: {
109
- services: 'reservation-services', // default
110
- resources: 'reservation-resources', // default
111
- schedules: 'reservation-schedules', // default
111
+ services: 'services', // default
112
+ resources: 'resources', // default
113
+ schedules: 'schedules', // default
112
114
  reservations: 'reservations', // default
115
+ media: 'media', // default (used by Resources image field)
113
116
  },
114
117
 
115
118
  // Slug of the existing auth collection to extend with customer fields
@@ -125,6 +128,10 @@ const config: ReservationPluginConfig = {
125
128
  // Minimum hours of notice required before cancellation
126
129
  cancellationNoticePeriod: 24, // default (hours)
127
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
+
128
135
  // Override access control per collection
129
136
  access: {
130
137
  services: {
@@ -139,7 +146,7 @@ const config: ReservationPluginConfig = {
139
146
  },
140
147
  }
141
148
 
142
- reservationPlugin(config)
149
+ payloadReserve(config)
143
150
  ```
144
151
 
145
152
  ### Configuration Defaults
@@ -147,14 +154,16 @@ reservationPlugin(config)
147
154
  | Option | Default | Description |
148
155
  |--------|---------|-------------|
149
156
  | `disabled` | `false` | Disable plugin functionality |
150
- | `slugs.services` | `'reservation-services'` | Services collection slug |
151
- | `slugs.resources` | `'reservation-resources'` | Resources collection slug |
152
- | `slugs.schedules` | `'reservation-schedules'` | Schedules collection slug |
157
+ | `slugs.services` | `'services'` | Services collection slug |
158
+ | `slugs.resources` | `'resources'` | Resources collection slug |
159
+ | `slugs.schedules` | `'schedules'` | Schedules collection slug |
153
160
  | `slugs.reservations` | `'reservations'` | Reservations collection slug |
161
+ | `slugs.media` | `'media'` | Media collection slug (used by Resources image field) |
154
162
  | `userCollection` | `'users'` | Existing auth collection to extend with customer fields |
155
163
  | `adminGroup` | `'Reservations'` | Admin panel group name |
156
164
  | `defaultBufferTime` | `0` | Default buffer (minutes) between bookings |
157
165
  | `cancellationNoticePeriod` | `24` | Minimum hours notice for cancellation |
166
+ | `customerRole` | `false` | Filter customers by role in reservation form (`string` or `false` to disable) |
158
167
 
159
168
  ---
160
169
 
@@ -162,7 +171,7 @@ reservationPlugin(config)
162
171
 
163
172
  ### Services
164
173
 
165
- **Slug:** `reservation-services`
174
+ **Slug:** `services`
166
175
 
167
176
  Defines what can be booked (e.g., "Haircut", "Consultation", "Massage").
168
177
 
@@ -179,7 +188,7 @@ Defines what can be booked (e.g., "Haircut", "Consultation", "Massage").
179
188
  **Example:**
180
189
  ```typescript
181
190
  await payload.create({
182
- collection: 'reservation-services',
191
+ collection: 'services',
183
192
  data: {
184
193
  name: 'Haircut',
185
194
  description: 'Standard haircut service',
@@ -194,13 +203,14 @@ await payload.create({
194
203
 
195
204
  ### Resources
196
205
 
197
- **Slug:** `reservation-resources`
206
+ **Slug:** `resources`
198
207
 
199
208
  Who or what performs the service (e.g., a stylist, a room, a consultant).
200
209
 
201
210
  | Field | Type | Required | Description |
202
211
  |-------|------|----------|-------------|
203
212
  | `name` | Text | Yes | Resource name (max 200 chars, used as title) |
213
+ | `image` | Upload | No | Resource image (references media collection) |
204
214
  | `description` | Textarea | No | Resource description |
205
215
  | `services` | Relationship | Yes | Services this resource can perform (hasMany) |
206
216
  | `active` | Checkbox | No | Whether resource is active (default: true, sidebar) |
@@ -208,7 +218,7 @@ Who or what performs the service (e.g., a stylist, a room, a consultant).
208
218
  **Example:**
209
219
  ```typescript
210
220
  await payload.create({
211
- collection: 'reservation-resources',
221
+ collection: 'resources',
212
222
  data: {
213
223
  name: 'Alice Johnson',
214
224
  description: 'Senior Stylist',
@@ -220,7 +230,7 @@ await payload.create({
220
230
 
221
231
  ### Schedules
222
232
 
223
- **Slug:** `reservation-schedules`
233
+ **Slug:** `schedules`
224
234
 
225
235
  Defines when a resource is available. Supports **recurring** (weekly pattern) and **manual** (specific dates) modes, plus exception dates.
226
236
 
@@ -245,7 +255,7 @@ Defines when a resource is available. Supports **recurring** (weekly pattern) an
245
255
  **Example - Recurring Schedule:**
246
256
  ```typescript
247
257
  await payload.create({
248
- collection: 'reservation-schedules',
258
+ collection: 'schedules',
249
259
  data: {
250
260
  name: 'Alice - Weekdays',
251
261
  resource: aliceId,
@@ -675,6 +685,32 @@ A CSS Grid-based calendar (no external dependencies) with three view modes:
675
685
  - **Current time indicator:** A red horizontal line in week/day views showing the current time position within the matching hour cell
676
686
  - Fetches data via REST API for the visible date range
677
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
+
678
714
  ### Availability Overview
679
715
 
680
716
  **Type:** Client Component
@@ -729,9 +765,9 @@ The plugin is backend-only — it adds collections, hooks, and admin UI to Paylo
729
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:
730
766
 
731
767
  ```ts
732
- import { reservationPlugin } from 'payload-reserve'
768
+ import { payloadReserve } from 'payload-reserve'
733
769
 
734
- reservationPlugin({
770
+ payloadReserve({
735
771
  access: {
736
772
  services: {
737
773
  read: () => true, // anyone can browse services
@@ -771,7 +807,7 @@ export default async function BookingPage() {
771
807
  const payload = await getPayload({ config })
772
808
 
773
809
  const { docs: services } = await payload.find({
774
- collection: 'reservation-services',
810
+ collection: 'services',
775
811
  overrideAccess: false,
776
812
  where: { active: { equals: true } },
777
813
  })
@@ -794,7 +830,7 @@ Once a customer picks a service, load the resources that offer it:
794
830
 
795
831
  ```ts
796
832
  const { docs: resources } = await payload.find({
797
- collection: 'reservation-resources',
833
+ collection: 'resources',
798
834
  overrideAccess: false,
799
835
  where: {
800
836
  services: { contains: selectedServiceId },
@@ -817,7 +853,7 @@ import {
817
853
 
818
854
  // 1. Get the resource's active schedule
819
855
  const { docs: schedules } = await payload.find({
820
- collection: 'reservation-schedules',
856
+ collection: 'schedules',
821
857
  overrideAccess: false,
822
858
  where: {
823
859
  resource: { equals: resourceId },
@@ -946,7 +982,7 @@ If you prefer a client-side-only approach (e.g., a separate SPA), use Payload's
946
982
 
947
983
  ```ts
948
984
  // Fetch services
949
- const res = await fetch('https://your-site.com/api/reservation-services?where[active][equals]=true')
985
+ const res = await fetch('https://your-site.com/api/services?where[active][equals]=true')
950
986
  const { docs: services } = await res.json()
951
987
 
952
988
  // Create a reservation (customer is a user ID)
@@ -1073,10 +1109,16 @@ src/
1073
1109
  slotUtils.ts # Time math helpers
1074
1110
  scheduleUtils.ts # Schedule resolution helpers
1075
1111
 
1112
+ endpoints/
1113
+ customerSearch.ts # Custom search endpoint for customer picker
1114
+
1076
1115
  components/
1077
1116
  CalendarView/
1078
1117
  index.tsx # Client: calendar for reservations
1079
1118
  CalendarView.module.css
1119
+ CustomerField/
1120
+ index.tsx # Client: rich customer picker field
1121
+ CustomerField.module.css
1080
1122
  DashboardWidget/
1081
1123
  DashboardWidgetServer.tsx # RSC: today's booking stats
1082
1124
  DashboardWidget.module.css
@@ -1085,7 +1127,7 @@ src/
1085
1127
  AvailabilityOverview.module.css
1086
1128
 
1087
1129
  exports/
1088
- client.ts # CalendarView, AvailabilityOverview
1130
+ client.ts # CalendarView, AvailabilityOverview, CustomerField
1089
1131
  rsc.ts # DashboardWidgetServer
1090
1132
  ```
1091
1133
 
@@ -1096,20 +1138,20 @@ src/
1096
1138
  ### Plugin Export
1097
1139
 
1098
1140
  ```typescript
1099
- import { reservationPlugin } from 'payload-reserve'
1141
+ import { payloadReserve } from 'payload-reserve'
1100
1142
  import type { ReservationPluginConfig } from 'payload-reserve'
1101
1143
  ```
1102
1144
 
1103
1145
  ### Client Exports
1104
1146
 
1105
1147
  ```typescript
1106
- import { CalendarView, AvailabilityOverview } from 'reservation-plugin/client'
1148
+ import { CalendarView, AvailabilityOverview, CustomerField } from 'payload-reserve/client'
1107
1149
  ```
1108
1150
 
1109
1151
  ### RSC Exports
1110
1152
 
1111
1153
  ```typescript
1112
- import { DashboardWidgetServer } from 'reservation-plugin/rsc'
1154
+ import { DashboardWidgetServer } from 'payload-reserve/rsc'
1113
1155
  ```
1114
1156
 
1115
1157
  ### Type Exports
@@ -1118,7 +1160,7 @@ import { DashboardWidgetServer } from 'reservation-plugin/rsc'
1118
1160
  import type {
1119
1161
  ReservationPluginConfig,
1120
1162
  ResolvedReservationPluginConfig,
1121
- } from 'reservation-plugin'
1163
+ } from 'payload-reserve'
1122
1164
  ```
1123
1165
 
1124
1166
  ### Integration Test Coverage
@@ -40,7 +40,21 @@ export function createReservationsCollection(config) {
40
40
  type: 'relationship',
41
41
  label: ({ t })=>t('reservation:fieldCustomer'),
42
42
  relationTo: config.userCollection,
43
- required: true
43
+ required: true,
44
+ ...config.customerRole ? {
45
+ filterOptions: ()=>({
46
+ role: {
47
+ equals: config.customerRole
48
+ }
49
+ })
50
+ } : {},
51
+ admin: {
52
+ allowCreate: true,
53
+ allowEdit: true,
54
+ components: {
55
+ Field: 'payload-reserve/client#CustomerField'
56
+ }
57
+ }
44
58
  },
45
59
  {
46
60
  name: 'startTime',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/collections/Reservations.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload'\n\nimport type { PluginT } from '../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { calculateEndTime } from '../hooks/reservations/calculateEndTime.js'\nimport { validateCancellation } from '../hooks/reservations/validateCancellation.js'\nimport { validateConflicts } from '../hooks/reservations/validateConflicts.js'\nimport { validateStatusTransition } from '../hooks/reservations/validateStatusTransition.js'\n\nexport function createReservationsCollection(\n config: ResolvedReservationPluginConfig,\n): CollectionConfig {\n return {\n slug: config.slugs.reservations,\n access: config.access.reservations ?? {},\n admin: {\n components: {\n views: {\n list: {\n Component: 'payload-reserve/client#CalendarView',\n },\n },\n },\n group: config.adminGroup,\n listSearchableFields: ['status'],\n useAsTitle: 'startTime',\n },\n fields: [\n {\n name: 'service',\n type: 'relationship',\n label: ({ t }) => (t as PluginT)('reservation:fieldService'),\n relationTo: config.slugs.services,\n required: true,\n },\n {\n name: 'resource',\n type: 'relationship',\n label: ({ t }) => (t as PluginT)('reservation:fieldResource'),\n relationTo: config.slugs.resources,\n required: true,\n },\n {\n name: 'customer',\n type: 'relationship',\n label: ({ t }) => (t as PluginT)('reservation:fieldCustomer'),\n relationTo: config.userCollection,\n required: true,\n },\n {\n name: 'startTime',\n type: 'date',\n admin: {\n date: {\n pickerAppearance: 'dayAndTime',\n },\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldStartTime'),\n required: true,\n },\n {\n name: 'endTime',\n type: 'date',\n admin: {\n date: {\n pickerAppearance: 'dayAndTime',\n },\n readOnly: true,\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldEndTime'),\n },\n {\n name: 'status',\n type: 'select',\n defaultValue: 'pending',\n label: ({ t }) => (t as PluginT)('reservation:fieldStatus'),\n options: [\n { label: ({ t }) => (t as PluginT)('reservation:statusPending'), value: 'pending' },\n { label: ({ t }) => (t as PluginT)('reservation:statusConfirmed'), value: 'confirmed' },\n { label: ({ t }) => (t as PluginT)('reservation:statusCompleted'), value: 'completed' },\n { label: ({ t }) => (t as PluginT)('reservation:statusCancelled'), value: 'cancelled' },\n { label: ({ t }) => (t as PluginT)('reservation:statusNoShow'), value: 'no-show' },\n ],\n },\n {\n name: 'cancellationReason',\n type: 'textarea',\n admin: {\n condition: (_, siblingData) => siblingData?.status === 'cancelled',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldCancellationReason'),\n },\n {\n name: 'notes',\n type: 'textarea',\n label: ({ t }) => (t as PluginT)('reservation:fieldNotes'),\n },\n ],\n hooks: {\n beforeChange: [\n calculateEndTime(config),\n validateConflicts(config),\n validateStatusTransition(),\n validateCancellation(config),\n ],\n },\n labels: {\n plural: ({ t }) => (t as PluginT)('reservation:collectionReservations'),\n singular: ({ t }) => (t as PluginT)('reservation:collectionReservations'),\n },\n }\n}\n"],"names":["calculateEndTime","validateCancellation","validateConflicts","validateStatusTransition","createReservationsCollection","config","slug","slugs","reservations","access","admin","components","views","list","Component","group","adminGroup","listSearchableFields","useAsTitle","fields","name","type","label","t","relationTo","services","required","resources","userCollection","date","pickerAppearance","readOnly","defaultValue","options","value","condition","_","siblingData","status","hooks","beforeChange","labels","plural","singular"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/collections/Reservations.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload'\n\nimport type { PluginT } from '../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { calculateEndTime } from '../hooks/reservations/calculateEndTime.js'\nimport { validateCancellation } from '../hooks/reservations/validateCancellation.js'\nimport { validateConflicts } from '../hooks/reservations/validateConflicts.js'\nimport { validateStatusTransition } from '../hooks/reservations/validateStatusTransition.js'\n\nexport function createReservationsCollection(\n config: ResolvedReservationPluginConfig,\n): CollectionConfig {\n return {\n slug: config.slugs.reservations,\n access: config.access.reservations ?? {},\n admin: {\n components: {\n views: {\n list: {\n Component: 'payload-reserve/client#CalendarView',\n },\n },\n },\n group: config.adminGroup,\n listSearchableFields: ['status'],\n useAsTitle: 'startTime',\n },\n fields: [\n {\n name: 'service',\n type: 'relationship',\n label: ({ t }) => (t as PluginT)('reservation:fieldService'),\n relationTo: config.slugs.services,\n required: true,\n },\n {\n name: 'resource',\n type: 'relationship',\n label: ({ t }) => (t as PluginT)('reservation:fieldResource'),\n relationTo: config.slugs.resources,\n required: true,\n },\n {\n name: 'customer',\n type: 'relationship',\n label: ({ t }) => (t as PluginT)('reservation:fieldCustomer'),\n relationTo: config.userCollection,\n required: true,\n ...(config.customerRole\n ? { filterOptions: () => ({ role: { equals: config.customerRole } }) }\n : {}),\n admin: {\n allowCreate: true,\n allowEdit: true,\n components: {\n Field: 'payload-reserve/client#CustomerField',\n },\n },\n },\n {\n name: 'startTime',\n type: 'date',\n admin: {\n date: {\n pickerAppearance: 'dayAndTime',\n },\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldStartTime'),\n required: true,\n },\n {\n name: 'endTime',\n type: 'date',\n admin: {\n date: {\n pickerAppearance: 'dayAndTime',\n },\n readOnly: true,\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldEndTime'),\n },\n {\n name: 'status',\n type: 'select',\n defaultValue: 'pending',\n label: ({ t }) => (t as PluginT)('reservation:fieldStatus'),\n options: [\n { label: ({ t }) => (t as PluginT)('reservation:statusPending'), value: 'pending' },\n { label: ({ t }) => (t as PluginT)('reservation:statusConfirmed'), value: 'confirmed' },\n { label: ({ t }) => (t as PluginT)('reservation:statusCompleted'), value: 'completed' },\n { label: ({ t }) => (t as PluginT)('reservation:statusCancelled'), value: 'cancelled' },\n { label: ({ t }) => (t as PluginT)('reservation:statusNoShow'), value: 'no-show' },\n ],\n },\n {\n name: 'cancellationReason',\n type: 'textarea',\n admin: {\n condition: (_, siblingData) => siblingData?.status === 'cancelled',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldCancellationReason'),\n },\n {\n name: 'notes',\n type: 'textarea',\n label: ({ t }) => (t as PluginT)('reservation:fieldNotes'),\n },\n ],\n hooks: {\n beforeChange: [\n calculateEndTime(config),\n validateConflicts(config),\n validateStatusTransition(),\n validateCancellation(config),\n ],\n },\n labels: {\n plural: ({ t }) => (t as PluginT)('reservation:collectionReservations'),\n singular: ({ t }) => (t as PluginT)('reservation:collectionReservations'),\n },\n }\n}\n"],"names":["calculateEndTime","validateCancellation","validateConflicts","validateStatusTransition","createReservationsCollection","config","slug","slugs","reservations","access","admin","components","views","list","Component","group","adminGroup","listSearchableFields","useAsTitle","fields","name","type","label","t","relationTo","services","required","resources","userCollection","customerRole","filterOptions","role","equals","allowCreate","allowEdit","Field","date","pickerAppearance","readOnly","defaultValue","options","value","condition","_","siblingData","status","hooks","beforeChange","labels","plural","singular"],"mappings":"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"}
@@ -17,6 +17,12 @@ export function createResourcesCollection(config) {
17
17
  maxLength: 200,
18
18
  required: true
19
19
  },
20
+ {
21
+ name: 'image',
22
+ type: 'upload',
23
+ label: ({ t })=>t('reservation:fieldImage'),
24
+ relationTo: config.slugs.media
25
+ },
20
26
  {
21
27
  name: 'description',
22
28
  type: 'textarea',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/collections/Resources.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload'\n\nimport type { PluginT } from '../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nexport function createResourcesCollection(\n config: ResolvedReservationPluginConfig,\n): CollectionConfig {\n return {\n slug: config.slugs.resources,\n access: config.access.resources ?? {},\n admin: {\n group: config.adminGroup,\n useAsTitle: 'name',\n },\n fields: [\n {\n name: 'name',\n type: 'text',\n label: ({ t }) => (t as PluginT)('reservation:fieldName'),\n ...(config.localized ? { localized: true } : {}),\n maxLength: 200,\n required: true,\n },\n {\n name: 'description',\n type: 'textarea',\n label: ({ t }) => (t as PluginT)('reservation:fieldDescription'),\n ...(config.localized ? { localized: true } : {}),\n },\n {\n name: 'services',\n type: 'relationship',\n hasMany: true,\n label: ({ t }) => (t as PluginT)('reservation:fieldServices'),\n relationTo: config.slugs.services,\n required: true,\n },\n {\n name: 'active',\n type: 'checkbox',\n admin: {\n position: 'sidebar',\n },\n defaultValue: true,\n label: ({ t }) => (t as PluginT)('reservation:fieldActive'),\n },\n ],\n labels: {\n plural: ({ t }) => (t as PluginT)('reservation:collectionResources'),\n singular: ({ t }) => (t as PluginT)('reservation:collectionResources'),\n },\n }\n}\n"],"names":["createResourcesCollection","config","slug","slugs","resources","access","admin","group","adminGroup","useAsTitle","fields","name","type","label","t","localized","maxLength","required","hasMany","relationTo","services","position","defaultValue","labels","plural","singular"],"mappings":"AAKA,OAAO,SAASA,0BACdC,MAAuC;IAEvC,OAAO;QACLC,MAAMD,OAAOE,KAAK,CAACC,SAAS;QAC5BC,QAAQJ,OAAOI,MAAM,CAACD,SAAS,IAAI,CAAC;QACpCE,OAAO;YACLC,OAAON,OAAOO,UAAU;YACxBC,YAAY;QACd;QACAC,QAAQ;YACN;gBACEC,MAAM;gBACNC,MAAM;gBACNC,OAAO,CAAC,EAAEC,CAAC,EAAE,GAAK,AAACA,EAAc;gBACjC,GAAIb,OAAOc,SAAS,GAAG;oBAAEA,WAAW;gBAAK,IAAI,CAAC,CAAC;gBAC/CC,WAAW;gBACXC,UAAU;YACZ;YACA;gBACEN,MAAM;gBACNC,MAAM;gBACNC,OAAO,CAAC,EAAEC,CAAC,EAAE,GAAK,AAACA,EAAc;gBACjC,GAAIb,OAAOc,SAAS,GAAG;oBAAEA,WAAW;gBAAK,IAAI,CAAC,CAAC;YACjD;YACA;gBACEJ,MAAM;gBACNC,MAAM;gBACNM,SAAS;gBACTL,OAAO,CAAC,EAAEC,CAAC,EAAE,GAAK,AAACA,EAAc;gBACjCK,YAAYlB,OAAOE,KAAK,CAACiB,QAAQ;gBACjCH,UAAU;YACZ;YACA;gBACEN,MAAM;gBACNC,MAAM;gBACNN,OAAO;oBACLe,UAAU;gBACZ;gBACAC,cAAc;gBACdT,OAAO,CAAC,EAAEC,CAAC,EAAE,GAAK,AAACA,EAAc;YACnC;SACD;QACDS,QAAQ;YACNC,QAAQ,CAAC,EAAEV,CAAC,EAAE,GAAK,AAACA,EAAc;YAClCW,UAAU,CAAC,EAAEX,CAAC,EAAE,GAAK,AAACA,EAAc;QACtC;IACF;AACF"}
1
+ {"version":3,"sources":["../../src/collections/Resources.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload'\n\nimport type { PluginT } from '../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nexport function createResourcesCollection(\n config: ResolvedReservationPluginConfig,\n): CollectionConfig {\n return {\n slug: config.slugs.resources,\n access: config.access.resources ?? {},\n admin: {\n group: config.adminGroup,\n useAsTitle: 'name',\n },\n fields: [\n {\n name: 'name',\n type: 'text',\n label: ({ t }) => (t as PluginT)('reservation:fieldName'),\n ...(config.localized ? { localized: true } : {}),\n maxLength: 200,\n required: true,\n },\n {\n name: 'image',\n type: 'upload',\n label: ({ t }) => (t as PluginT)('reservation:fieldImage'),\n relationTo: config.slugs.media,\n },\n {\n name: 'description',\n type: 'textarea',\n label: ({ t }) => (t as PluginT)('reservation:fieldDescription'),\n ...(config.localized ? { localized: true } : {}),\n },\n {\n name: 'services',\n type: 'relationship',\n hasMany: true,\n label: ({ t }) => (t as PluginT)('reservation:fieldServices'),\n relationTo: config.slugs.services,\n required: true,\n },\n {\n name: 'active',\n type: 'checkbox',\n admin: {\n position: 'sidebar',\n },\n defaultValue: true,\n label: ({ t }) => (t as PluginT)('reservation:fieldActive'),\n },\n ],\n labels: {\n plural: ({ t }) => (t as PluginT)('reservation:collectionResources'),\n singular: ({ t }) => (t as PluginT)('reservation:collectionResources'),\n },\n }\n}\n"],"names":["createResourcesCollection","config","slug","slugs","resources","access","admin","group","adminGroup","useAsTitle","fields","name","type","label","t","localized","maxLength","required","relationTo","media","hasMany","services","position","defaultValue","labels","plural","singular"],"mappings":"AAKA,OAAO,SAASA,0BACdC,MAAuC;IAEvC,OAAO;QACLC,MAAMD,OAAOE,KAAK,CAACC,SAAS;QAC5BC,QAAQJ,OAAOI,MAAM,CAACD,SAAS,IAAI,CAAC;QACpCE,OAAO;YACLC,OAAON,OAAOO,UAAU;YACxBC,YAAY;QACd;QACAC,QAAQ;YACN;gBACEC,MAAM;gBACNC,MAAM;gBACNC,OAAO,CAAC,EAAEC,CAAC,EAAE,GAAK,AAACA,EAAc;gBACjC,GAAIb,OAAOc,SAAS,GAAG;oBAAEA,WAAW;gBAAK,IAAI,CAAC,CAAC;gBAC/CC,WAAW;gBACXC,UAAU;YACZ;YACA;gBACEN,MAAM;gBACNC,MAAM;gBACNC,OAAO,CAAC,EAAEC,CAAC,EAAE,GAAK,AAACA,EAAc;gBACjCI,YAAYjB,OAAOE,KAAK,CAACgB,KAAK;YAChC;YACA;gBACER,MAAM;gBACNC,MAAM;gBACNC,OAAO,CAAC,EAAEC,CAAC,EAAE,GAAK,AAACA,EAAc;gBACjC,GAAIb,OAAOc,SAAS,GAAG;oBAAEA,WAAW;gBAAK,IAAI,CAAC,CAAC;YACjD;YACA;gBACEJ,MAAM;gBACNC,MAAM;gBACNQ,SAAS;gBACTP,OAAO,CAAC,EAAEC,CAAC,EAAE,GAAK,AAACA,EAAc;gBACjCI,YAAYjB,OAAOE,KAAK,CAACkB,QAAQ;gBACjCJ,UAAU;YACZ;YACA;gBACEN,MAAM;gBACNC,MAAM;gBACNN,OAAO;oBACLgB,UAAU;gBACZ;gBACAC,cAAc;gBACdV,OAAO,CAAC,EAAEC,CAAC,EAAE,GAAK,AAACA,EAAc;YACnC;SACD;QACDU,QAAQ;YACNC,QAAQ,CAAC,EAAEX,CAAC,EAAE,GAAK,AAACA,EAAc;YAClCY,UAAU,CAAC,EAAEZ,CAAC,EAAE,GAAK,AAACA,EAAc;QACtC;IACF;AACF"}
@@ -0,0 +1,147 @@
1
+ .wrapper {
2
+ position: relative;
3
+ width: 100%;
4
+ }
5
+
6
+ .selected {
7
+ display: flex;
8
+ align-items: center;
9
+ justify-content: space-between;
10
+ padding: 10px 12px;
11
+ border: 1px solid var(--theme-elevation-150);
12
+ border-radius: 4px;
13
+ background: var(--theme-input-bg);
14
+ cursor: pointer;
15
+ transition: border-color 0.15s;
16
+ }
17
+
18
+ .selected:hover {
19
+ border-color: var(--theme-elevation-300);
20
+ }
21
+
22
+ .selectedInfo {
23
+ display: flex;
24
+ flex-direction: column;
25
+ gap: 2px;
26
+ min-width: 0;
27
+ }
28
+
29
+ .selectedName {
30
+ font-weight: 600;
31
+ color: var(--theme-text);
32
+ white-space: nowrap;
33
+ overflow: hidden;
34
+ text-overflow: ellipsis;
35
+ }
36
+
37
+ .selectedMeta {
38
+ font-size: 12px;
39
+ color: var(--theme-elevation-500);
40
+ white-space: nowrap;
41
+ overflow: hidden;
42
+ text-overflow: ellipsis;
43
+ }
44
+
45
+ .clearButton {
46
+ flex-shrink: 0;
47
+ margin-left: 8px;
48
+ padding: 4px 8px;
49
+ border: none;
50
+ border-radius: 3px;
51
+ background: var(--theme-elevation-100);
52
+ color: var(--theme-elevation-500);
53
+ cursor: pointer;
54
+ font-size: 12px;
55
+ transition: background 0.15s, color 0.15s;
56
+ }
57
+
58
+ .clearButton:hover {
59
+ background: var(--theme-elevation-200);
60
+ color: var(--theme-text);
61
+ }
62
+
63
+ .searchInput {
64
+ width: 100%;
65
+ padding: 10px 12px;
66
+ border: 1px solid var(--theme-elevation-150);
67
+ border-radius: 4px;
68
+ background: var(--theme-input-bg);
69
+ color: var(--theme-text);
70
+ font-size: 14px;
71
+ outline: none;
72
+ transition: border-color 0.15s;
73
+ }
74
+
75
+ .searchInput:focus {
76
+ border-color: var(--theme-elevation-400);
77
+ }
78
+
79
+ .dropdown {
80
+ position: absolute;
81
+ z-index: 100;
82
+ top: 100%;
83
+ left: 0;
84
+ right: 0;
85
+ max-height: 240px;
86
+ margin-top: 2px;
87
+ border: 1px solid var(--theme-elevation-150);
88
+ border-radius: 4px;
89
+ background: var(--theme-bg);
90
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
91
+ overflow-y: auto;
92
+ }
93
+
94
+ .option {
95
+ display: flex;
96
+ flex-direction: column;
97
+ gap: 2px;
98
+ padding: 8px 12px;
99
+ cursor: pointer;
100
+ transition: background 0.1s;
101
+ }
102
+
103
+ .option:hover {
104
+ background: var(--theme-elevation-50);
105
+ }
106
+
107
+ .optionName {
108
+ font-weight: 600;
109
+ color: var(--theme-text);
110
+ }
111
+
112
+ .optionMeta {
113
+ font-size: 12px;
114
+ color: var(--theme-elevation-500);
115
+ }
116
+
117
+ .noResults {
118
+ padding: 12px;
119
+ text-align: center;
120
+ color: var(--theme-elevation-400);
121
+ font-size: 13px;
122
+ }
123
+
124
+ .createButton {
125
+ display: flex;
126
+ align-items: center;
127
+ gap: 6px;
128
+ width: 100%;
129
+ padding: 8px 12px;
130
+ border: none;
131
+ border-top: 1px solid var(--theme-elevation-100);
132
+ background: transparent;
133
+ color: var(--theme-text);
134
+ cursor: pointer;
135
+ font-size: 13px;
136
+ transition: background 0.1s;
137
+ }
138
+
139
+ .createButton:hover {
140
+ background: var(--theme-elevation-50);
141
+ }
142
+
143
+ .error {
144
+ margin-top: 4px;
145
+ color: var(--theme-error-500);
146
+ font-size: 12px;
147
+ }
@@ -0,0 +1,2 @@
1
+ import type { RelationshipFieldClientComponent } from 'payload';
2
+ export declare const CustomerField: RelationshipFieldClientComponent;
@@ -0,0 +1,238 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { FieldLabel, useConfig, useDocumentDrawer, useField, useTranslation } from '@payloadcms/ui';
4
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
5
+ import styles from './CustomerField.module.css';
6
+ export const CustomerField = ({ field, path: pathProp })=>{
7
+ const fieldPath = pathProp ?? field?.name ?? 'customer';
8
+ const { config } = useConfig();
9
+ const { t: _t } = useTranslation();
10
+ const t = _t;
11
+ const { setValue, value } = useField({
12
+ path: fieldPath
13
+ });
14
+ const slugs = config.admin?.custom?.reservationSlugs;
15
+ const userCollection = slugs?.userCollection ?? 'users';
16
+ const [search, setSearch] = useState('');
17
+ const [results, setResults] = useState([]);
18
+ const [selectedCustomer, setSelectedCustomer] = useState(null);
19
+ const [isOpen, setIsOpen] = useState(false);
20
+ const [loading, setLoading] = useState(false);
21
+ const wrapperRef = useRef(null);
22
+ const debounceRef = useRef(null);
23
+ const [DocumentDrawer, , { openDrawer }] = useDocumentDrawer({
24
+ collectionSlug: userCollection
25
+ });
26
+ // Fetch selected customer details when value changes
27
+ useEffect(()=>{
28
+ if (!value) {
29
+ setSelectedCustomer(null);
30
+ return;
31
+ }
32
+ // If we already have the selected customer data, skip fetch
33
+ if (selectedCustomer?.id === value) {
34
+ return;
35
+ }
36
+ const fetchCustomer = async ()=>{
37
+ try {
38
+ const res = await fetch(`/api/${userCollection}/${value}`);
39
+ if (res.ok) {
40
+ const doc = await res.json();
41
+ setSelectedCustomer({
42
+ id: doc.id,
43
+ name: doc.name ?? '',
44
+ email: doc.email ?? '',
45
+ phone: doc.phone ?? ''
46
+ });
47
+ }
48
+ } catch {
49
+ // Silently fail — the field will still show the ID
50
+ }
51
+ };
52
+ void fetchCustomer();
53
+ }, [
54
+ value,
55
+ userCollection,
56
+ selectedCustomer?.id
57
+ ]);
58
+ // Debounced search
59
+ const doSearch = useCallback(async (query)=>{
60
+ setLoading(true);
61
+ try {
62
+ const params = new URLSearchParams({
63
+ limit: '10',
64
+ search: query
65
+ });
66
+ const res = await fetch(`/api/reservation-customer-search?${params.toString()}`);
67
+ if (res.ok) {
68
+ const data = await res.json();
69
+ setResults(data.docs);
70
+ }
71
+ } catch {
72
+ setResults([]);
73
+ } finally{
74
+ setLoading(false);
75
+ }
76
+ }, []);
77
+ const handleSearchChange = useCallback((e)=>{
78
+ const val = e.target.value;
79
+ setSearch(val);
80
+ if (debounceRef.current) {
81
+ clearTimeout(debounceRef.current);
82
+ }
83
+ debounceRef.current = setTimeout(()=>{
84
+ void doSearch(val);
85
+ }, 300);
86
+ }, [
87
+ doSearch
88
+ ]);
89
+ // Click outside to close
90
+ useEffect(()=>{
91
+ const handleClickOutside = (e)=>{
92
+ if (wrapperRef.current && !wrapperRef.current.contains(e.target)) {
93
+ setIsOpen(false);
94
+ }
95
+ };
96
+ document.addEventListener('mousedown', handleClickOutside);
97
+ return ()=>document.removeEventListener('mousedown', handleClickOutside);
98
+ }, []);
99
+ const handleSelect = useCallback((customer)=>{
100
+ setValue(customer.id);
101
+ setSelectedCustomer(customer);
102
+ setIsOpen(false);
103
+ setSearch('');
104
+ }, [
105
+ setValue
106
+ ]);
107
+ const handleClear = useCallback(()=>{
108
+ setValue(null);
109
+ setSelectedCustomer(null);
110
+ setSearch('');
111
+ }, [
112
+ setValue
113
+ ]);
114
+ const handleFocus = useCallback(()=>{
115
+ setIsOpen(true);
116
+ if (results.length === 0) {
117
+ void doSearch('');
118
+ }
119
+ }, [
120
+ doSearch,
121
+ results.length
122
+ ]);
123
+ const handleCreate = useCallback(()=>{
124
+ setIsOpen(false);
125
+ openDrawer();
126
+ }, [
127
+ openDrawer
128
+ ]);
129
+ const handleDrawerSave = useCallback(({ doc })=>{
130
+ const customer = {
131
+ id: doc.id,
132
+ name: doc.name ?? '',
133
+ email: doc.email ?? '',
134
+ phone: doc.phone ?? ''
135
+ };
136
+ setValue(customer.id);
137
+ setSelectedCustomer(customer);
138
+ }, [
139
+ setValue
140
+ ]);
141
+ return /*#__PURE__*/ _jsxs("div", {
142
+ className: styles.wrapper,
143
+ ref: wrapperRef,
144
+ children: [
145
+ /*#__PURE__*/ _jsx(FieldLabel, {
146
+ label: field?.label ?? t('reservation:fieldCustomer'),
147
+ path: fieldPath,
148
+ required: field?.required
149
+ }),
150
+ selectedCustomer ? /*#__PURE__*/ _jsxs("div", {
151
+ className: styles.selected,
152
+ children: [
153
+ /*#__PURE__*/ _jsxs("div", {
154
+ className: styles.selectedInfo,
155
+ children: [
156
+ /*#__PURE__*/ _jsx("span", {
157
+ className: styles.selectedName,
158
+ children: selectedCustomer.name || selectedCustomer.email
159
+ }),
160
+ /*#__PURE__*/ _jsx("span", {
161
+ className: styles.selectedMeta,
162
+ children: [
163
+ selectedCustomer.phone,
164
+ selectedCustomer.email
165
+ ].filter(Boolean).join(' · ')
166
+ })
167
+ ]
168
+ }),
169
+ /*#__PURE__*/ _jsx("button", {
170
+ className: styles.clearButton,
171
+ onClick: handleClear,
172
+ type: "button",
173
+ children: t('reservation:fieldCustomerClear')
174
+ })
175
+ ]
176
+ }) : /*#__PURE__*/ _jsx("input", {
177
+ "aria-label": t('reservation:fieldCustomerSearch'),
178
+ className: styles.searchInput,
179
+ onChange: handleSearchChange,
180
+ onFocus: handleFocus,
181
+ placeholder: t('reservation:fieldCustomerSearch'),
182
+ type: "text",
183
+ value: search
184
+ }),
185
+ isOpen && !selectedCustomer && /*#__PURE__*/ _jsxs("div", {
186
+ className: styles.dropdown,
187
+ children: [
188
+ loading && results.length === 0 ? /*#__PURE__*/ _jsx("div", {
189
+ className: styles.noResults,
190
+ children: "..."
191
+ }) : results.length === 0 && search ? /*#__PURE__*/ _jsx("div", {
192
+ className: styles.noResults,
193
+ children: t('reservation:fieldCustomerNoResults')
194
+ }) : results.map((customer)=>/*#__PURE__*/ _jsxs("div", {
195
+ "aria-selected": false,
196
+ className: styles.option,
197
+ onClick: ()=>handleSelect(customer),
198
+ onKeyDown: (e)=>{
199
+ if (e.key === 'Enter' || e.key === ' ') {
200
+ e.preventDefault();
201
+ handleSelect(customer);
202
+ }
203
+ },
204
+ role: "option",
205
+ tabIndex: 0,
206
+ children: [
207
+ /*#__PURE__*/ _jsx("span", {
208
+ className: styles.optionName,
209
+ children: customer.name || customer.email
210
+ }),
211
+ /*#__PURE__*/ _jsx("span", {
212
+ className: styles.optionMeta,
213
+ children: [
214
+ customer.phone,
215
+ customer.email
216
+ ].filter(Boolean).join(' · ')
217
+ })
218
+ ]
219
+ }, customer.id)),
220
+ /*#__PURE__*/ _jsxs("button", {
221
+ className: styles.createButton,
222
+ onClick: handleCreate,
223
+ type: "button",
224
+ children: [
225
+ "+ ",
226
+ t('reservation:fieldCustomerCreateNew')
227
+ ]
228
+ })
229
+ ]
230
+ }),
231
+ /*#__PURE__*/ _jsx(DocumentDrawer, {
232
+ onSave: handleDrawerSave
233
+ })
234
+ ]
235
+ });
236
+ };
237
+
238
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/CustomerField/index.tsx"],"sourcesContent":["'use client'\nimport type { RelationshipFieldClientComponent } from 'payload'\n\nimport { FieldLabel, useConfig, useDocumentDrawer, useField, useTranslation } from '@payloadcms/ui'\nimport React, { useCallback, useEffect, useRef, useState } from 'react'\n\nimport type { PluginT } from '../../translations/index.js'\n\nimport styles from './CustomerField.module.css'\n\ntype CustomerDoc = {\n email: string\n id: string\n name: string\n phone: string\n}\n\nexport const CustomerField: RelationshipFieldClientComponent = ({ field, path: pathProp }) => {\n const fieldPath = pathProp ?? field?.name ?? 'customer'\n const { config } = useConfig()\n const { t: _t } = useTranslation()\n const t = _t as PluginT\n\n const { setValue, value } = useField<string>({ path: fieldPath })\n\n const slugs = config.admin?.custom?.reservationSlugs\n const userCollection: string = slugs?.userCollection ?? 'users'\n\n const [search, setSearch] = useState('')\n const [results, setResults] = useState<CustomerDoc[]>([])\n const [selectedCustomer, setSelectedCustomer] = useState<CustomerDoc | null>(null)\n const [isOpen, setIsOpen] = useState(false)\n const [loading, setLoading] = useState(false)\n\n const wrapperRef = useRef<HTMLDivElement>(null)\n const debounceRef = useRef<null | ReturnType<typeof setTimeout>>(null)\n\n const [DocumentDrawer, , { openDrawer }] = useDocumentDrawer({\n collectionSlug: userCollection,\n })\n\n // Fetch selected customer details when value changes\n useEffect(() => {\n if (!value) {\n setSelectedCustomer(null)\n return\n }\n\n // If we already have the selected customer data, skip fetch\n if (selectedCustomer?.id === value) {return}\n\n const fetchCustomer = async () => {\n try {\n const res = await fetch(`/api/${userCollection}/${value}`)\n if (res.ok) {\n const doc = await res.json()\n setSelectedCustomer({\n id: doc.id,\n name: doc.name ?? '',\n email: doc.email ?? '',\n phone: doc.phone ?? '',\n })\n }\n } catch {\n // Silently fail — the field will still show the ID\n }\n }\n void fetchCustomer()\n }, [value, userCollection, selectedCustomer?.id])\n\n // Debounced search\n const doSearch = useCallback(\n async (query: string) => {\n setLoading(true)\n try {\n const params = new URLSearchParams({ limit: '10', search: query })\n const res = await fetch(`/api/reservation-customer-search?${params.toString()}`)\n if (res.ok) {\n const data = await res.json()\n setResults(data.docs)\n }\n } catch {\n setResults([])\n } finally {\n setLoading(false)\n }\n },\n [],\n )\n\n const handleSearchChange = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n const val = e.target.value\n setSearch(val)\n\n if (debounceRef.current) {\n clearTimeout(debounceRef.current)\n }\n\n debounceRef.current = setTimeout(() => {\n void doSearch(val)\n }, 300)\n },\n [doSearch],\n )\n\n // Click outside to close\n useEffect(() => {\n const handleClickOutside = (e: MouseEvent) => {\n if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) {\n setIsOpen(false)\n }\n }\n document.addEventListener('mousedown', handleClickOutside)\n return () => document.removeEventListener('mousedown', handleClickOutside)\n }, [])\n\n const handleSelect = useCallback(\n (customer: CustomerDoc) => {\n setValue(customer.id)\n setSelectedCustomer(customer)\n setIsOpen(false)\n setSearch('')\n },\n [setValue],\n )\n\n const handleClear = useCallback(() => {\n setValue(null as unknown as string)\n setSelectedCustomer(null)\n setSearch('')\n }, [setValue])\n\n const handleFocus = useCallback(() => {\n setIsOpen(true)\n if (results.length === 0) {\n void doSearch('')\n }\n }, [doSearch, results.length])\n\n const handleCreate = useCallback(() => {\n setIsOpen(false)\n openDrawer()\n }, [openDrawer])\n\n const handleDrawerSave = useCallback(\n ({ doc }: { doc: Record<string, unknown> }) => {\n const customer: CustomerDoc = {\n id: doc.id as string,\n name: (doc.name as string) ?? '',\n email: (doc.email as string) ?? '',\n phone: (doc.phone as string) ?? '',\n }\n setValue(customer.id)\n setSelectedCustomer(customer)\n },\n [setValue],\n )\n\n return (\n <div className={styles.wrapper} ref={wrapperRef}>\n <FieldLabel label={field?.label ?? t('reservation:fieldCustomer')} path={fieldPath} required={field?.required} />\n\n {selectedCustomer ? (\n <div className={styles.selected}>\n <div className={styles.selectedInfo}>\n <span className={styles.selectedName}>{selectedCustomer.name || selectedCustomer.email}</span>\n <span className={styles.selectedMeta}>\n {[selectedCustomer.phone, selectedCustomer.email].filter(Boolean).join(' · ')}\n </span>\n </div>\n <button\n className={styles.clearButton}\n onClick={handleClear}\n type=\"button\"\n >\n {t('reservation:fieldCustomerClear')}\n </button>\n </div>\n ) : (\n <input\n aria-label={t('reservation:fieldCustomerSearch')}\n className={styles.searchInput}\n onChange={handleSearchChange}\n onFocus={handleFocus}\n placeholder={t('reservation:fieldCustomerSearch')}\n type=\"text\"\n value={search}\n />\n )}\n\n {isOpen && !selectedCustomer && (\n <div className={styles.dropdown}>\n {loading && results.length === 0 ? (\n <div className={styles.noResults}>...</div>\n ) : results.length === 0 && search ? (\n <div className={styles.noResults}>{t('reservation:fieldCustomerNoResults')}</div>\n ) : (\n results.map((customer) => (\n <div\n aria-selected={false}\n className={styles.option}\n key={customer.id}\n onClick={() => handleSelect(customer)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n handleSelect(customer)\n }\n }}\n role=\"option\"\n tabIndex={0}\n >\n <span className={styles.optionName}>{customer.name || customer.email}</span>\n <span className={styles.optionMeta}>\n {[customer.phone, customer.email].filter(Boolean).join(' · ')}\n </span>\n </div>\n ))\n )}\n <button\n className={styles.createButton}\n onClick={handleCreate}\n type=\"button\"\n >\n + {t('reservation:fieldCustomerCreateNew')}\n </button>\n </div>\n )}\n\n <DocumentDrawer onSave={handleDrawerSave} />\n </div>\n )\n}\n"],"names":["FieldLabel","useConfig","useDocumentDrawer","useField","useTranslation","React","useCallback","useEffect","useRef","useState","styles","CustomerField","field","path","pathProp","fieldPath","name","config","t","_t","setValue","value","slugs","admin","custom","reservationSlugs","userCollection","search","setSearch","results","setResults","selectedCustomer","setSelectedCustomer","isOpen","setIsOpen","loading","setLoading","wrapperRef","debounceRef","DocumentDrawer","openDrawer","collectionSlug","id","fetchCustomer","res","fetch","ok","doc","json","email","phone","doSearch","query","params","URLSearchParams","limit","toString","data","docs","handleSearchChange","e","val","target","current","clearTimeout","setTimeout","handleClickOutside","contains","document","addEventListener","removeEventListener","handleSelect","customer","handleClear","handleFocus","length","handleCreate","handleDrawerSave","div","className","wrapper","ref","label","required","selected","selectedInfo","span","selectedName","selectedMeta","filter","Boolean","join","button","clearButton","onClick","type","input","aria-label","searchInput","onChange","onFocus","placeholder","dropdown","noResults","map","aria-selected","option","onKeyDown","key","preventDefault","role","tabIndex","optionName","optionMeta","createButton","onSave"],"mappings":"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"}
@@ -1,12 +1,14 @@
1
1
  import type { ReservationPluginConfig, ResolvedReservationPluginConfig } from './types.js';
2
2
  export declare const DEFAULT_SLUGS: {
3
+ readonly media: "media";
3
4
  readonly reservations: "reservations";
4
- readonly resources: "reservation-resources";
5
- readonly schedules: "reservation-schedules";
6
- readonly services: "reservation-services";
5
+ readonly resources: "resources";
6
+ readonly schedules: "schedules";
7
+ readonly services: "services";
7
8
  };
8
9
  export declare const DEFAULT_ADMIN_GROUP = "Reservations";
9
10
  export declare const DEFAULT_BUFFER_TIME = 0;
10
11
  export declare const DEFAULT_CANCELLATION_NOTICE_PERIOD = 24;
11
12
  export declare const DEFAULT_USER_COLLECTION = "users";
13
+ export declare const DEFAULT_CUSTOMER_ROLE: false | string;
12
14
  export declare function resolveConfig(pluginOptions: ReservationPluginConfig): ResolvedReservationPluginConfig;
package/dist/defaults.js CHANGED
@@ -1,22 +1,26 @@
1
1
  export const DEFAULT_SLUGS = {
2
+ media: 'media',
2
3
  reservations: 'reservations',
3
- resources: 'reservation-resources',
4
- schedules: 'reservation-schedules',
5
- services: 'reservation-services'
4
+ resources: 'resources',
5
+ schedules: 'schedules',
6
+ services: 'services'
6
7
  };
7
8
  export const DEFAULT_ADMIN_GROUP = 'Reservations';
8
9
  export const DEFAULT_BUFFER_TIME = 0;
9
10
  export const DEFAULT_CANCELLATION_NOTICE_PERIOD = 24;
10
11
  export const DEFAULT_USER_COLLECTION = 'users';
12
+ export const DEFAULT_CUSTOMER_ROLE = false;
11
13
  export function resolveConfig(pluginOptions) {
12
14
  return {
13
15
  access: pluginOptions.access ?? {},
14
16
  adminGroup: pluginOptions.adminGroup ?? DEFAULT_ADMIN_GROUP,
15
17
  cancellationNoticePeriod: pluginOptions.cancellationNoticePeriod ?? DEFAULT_CANCELLATION_NOTICE_PERIOD,
18
+ customerRole: pluginOptions.customerRole ?? DEFAULT_CUSTOMER_ROLE,
16
19
  defaultBufferTime: pluginOptions.defaultBufferTime ?? DEFAULT_BUFFER_TIME,
17
20
  disabled: pluginOptions.disabled ?? false,
18
21
  localized: false,
19
22
  slugs: {
23
+ media: pluginOptions.slugs?.media ?? DEFAULT_SLUGS.media,
20
24
  reservations: pluginOptions.slugs?.reservations ?? DEFAULT_SLUGS.reservations,
21
25
  resources: pluginOptions.slugs?.resources ?? DEFAULT_SLUGS.resources,
22
26
  schedules: pluginOptions.slugs?.schedules ?? DEFAULT_SLUGS.schedules,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/defaults.ts"],"sourcesContent":["import type { ReservationPluginConfig, ResolvedReservationPluginConfig } from './types.js'\n\nexport const DEFAULT_SLUGS = {\n reservations: 'reservations',\n resources: 'reservation-resources',\n schedules: 'reservation-schedules',\n services: 'reservation-services',\n} as const\n\nexport const DEFAULT_ADMIN_GROUP = 'Reservations'\nexport const DEFAULT_BUFFER_TIME = 0\nexport const DEFAULT_CANCELLATION_NOTICE_PERIOD = 24\nexport const DEFAULT_USER_COLLECTION = 'users'\n\nexport function resolveConfig(\n pluginOptions: ReservationPluginConfig,\n): ResolvedReservationPluginConfig {\n return {\n access: pluginOptions.access ?? {},\n adminGroup: pluginOptions.adminGroup ?? DEFAULT_ADMIN_GROUP,\n cancellationNoticePeriod:\n pluginOptions.cancellationNoticePeriod ?? DEFAULT_CANCELLATION_NOTICE_PERIOD,\n defaultBufferTime: pluginOptions.defaultBufferTime ?? DEFAULT_BUFFER_TIME,\n disabled: pluginOptions.disabled ?? false,\n localized: false,\n slugs: {\n reservations: pluginOptions.slugs?.reservations ?? DEFAULT_SLUGS.reservations,\n resources: pluginOptions.slugs?.resources ?? DEFAULT_SLUGS.resources,\n schedules: pluginOptions.slugs?.schedules ?? DEFAULT_SLUGS.schedules,\n services: pluginOptions.slugs?.services ?? DEFAULT_SLUGS.services,\n },\n userCollection: pluginOptions.userCollection ?? DEFAULT_USER_COLLECTION,\n }\n}\n"],"names":["DEFAULT_SLUGS","reservations","resources","schedules","services","DEFAULT_ADMIN_GROUP","DEFAULT_BUFFER_TIME","DEFAULT_CANCELLATION_NOTICE_PERIOD","DEFAULT_USER_COLLECTION","resolveConfig","pluginOptions","access","adminGroup","cancellationNoticePeriod","defaultBufferTime","disabled","localized","slugs","userCollection"],"mappings":"AAEA,OAAO,MAAMA,gBAAgB;IAC3BC,cAAc;IACdC,WAAW;IACXC,WAAW;IACXC,UAAU;AACZ,EAAU;AAEV,OAAO,MAAMC,sBAAsB,eAAc;AACjD,OAAO,MAAMC,sBAAsB,EAAC;AACpC,OAAO,MAAMC,qCAAqC,GAAE;AACpD,OAAO,MAAMC,0BAA0B,QAAO;AAE9C,OAAO,SAASC,cACdC,aAAsC;IAEtC,OAAO;QACLC,QAAQD,cAAcC,MAAM,IAAI,CAAC;QACjCC,YAAYF,cAAcE,UAAU,IAAIP;QACxCQ,0BACEH,cAAcG,wBAAwB,IAAIN;QAC5CO,mBAAmBJ,cAAcI,iBAAiB,IAAIR;QACtDS,UAAUL,cAAcK,QAAQ,IAAI;QACpCC,WAAW;QACXC,OAAO;YACLhB,cAAcS,cAAcO,KAAK,EAAEhB,gBAAgBD,cAAcC,YAAY;YAC7EC,WAAWQ,cAAcO,KAAK,EAAEf,aAAaF,cAAcE,SAAS;YACpEC,WAAWO,cAAcO,KAAK,EAAEd,aAAaH,cAAcG,SAAS;YACpEC,UAAUM,cAAcO,KAAK,EAAEb,YAAYJ,cAAcI,QAAQ;QACnE;QACAc,gBAAgBR,cAAcQ,cAAc,IAAIV;IAClD;AACF"}
1
+ {"version":3,"sources":["../src/defaults.ts"],"sourcesContent":["import type { ReservationPluginConfig, ResolvedReservationPluginConfig } from './types.js'\n\nexport const DEFAULT_SLUGS = {\n media: 'media',\n reservations: 'reservations',\n resources: 'resources',\n schedules: 'schedules',\n services: 'services',\n} as const\n\nexport const DEFAULT_ADMIN_GROUP = 'Reservations'\nexport const DEFAULT_BUFFER_TIME = 0\nexport const DEFAULT_CANCELLATION_NOTICE_PERIOD = 24\nexport const DEFAULT_USER_COLLECTION = 'users'\nexport const DEFAULT_CUSTOMER_ROLE: false | string = false\n\nexport function resolveConfig(\n pluginOptions: ReservationPluginConfig,\n): ResolvedReservationPluginConfig {\n return {\n access: pluginOptions.access ?? {},\n adminGroup: pluginOptions.adminGroup ?? DEFAULT_ADMIN_GROUP,\n cancellationNoticePeriod:\n pluginOptions.cancellationNoticePeriod ?? DEFAULT_CANCELLATION_NOTICE_PERIOD,\n customerRole: pluginOptions.customerRole ?? DEFAULT_CUSTOMER_ROLE,\n defaultBufferTime: pluginOptions.defaultBufferTime ?? DEFAULT_BUFFER_TIME,\n disabled: pluginOptions.disabled ?? false,\n localized: false,\n slugs: {\n media: pluginOptions.slugs?.media ?? DEFAULT_SLUGS.media,\n reservations: pluginOptions.slugs?.reservations ?? DEFAULT_SLUGS.reservations,\n resources: pluginOptions.slugs?.resources ?? DEFAULT_SLUGS.resources,\n schedules: pluginOptions.slugs?.schedules ?? DEFAULT_SLUGS.schedules,\n services: pluginOptions.slugs?.services ?? DEFAULT_SLUGS.services,\n },\n userCollection: pluginOptions.userCollection ?? DEFAULT_USER_COLLECTION,\n }\n}\n"],"names":["DEFAULT_SLUGS","media","reservations","resources","schedules","services","DEFAULT_ADMIN_GROUP","DEFAULT_BUFFER_TIME","DEFAULT_CANCELLATION_NOTICE_PERIOD","DEFAULT_USER_COLLECTION","DEFAULT_CUSTOMER_ROLE","resolveConfig","pluginOptions","access","adminGroup","cancellationNoticePeriod","customerRole","defaultBufferTime","disabled","localized","slugs","userCollection"],"mappings":"AAEA,OAAO,MAAMA,gBAAgB;IAC3BC,OAAO;IACPC,cAAc;IACdC,WAAW;IACXC,WAAW;IACXC,UAAU;AACZ,EAAU;AAEV,OAAO,MAAMC,sBAAsB,eAAc;AACjD,OAAO,MAAMC,sBAAsB,EAAC;AACpC,OAAO,MAAMC,qCAAqC,GAAE;AACpD,OAAO,MAAMC,0BAA0B,QAAO;AAC9C,OAAO,MAAMC,wBAAwC,MAAK;AAE1D,OAAO,SAASC,cACdC,aAAsC;IAEtC,OAAO;QACLC,QAAQD,cAAcC,MAAM,IAAI,CAAC;QACjCC,YAAYF,cAAcE,UAAU,IAAIR;QACxCS,0BACEH,cAAcG,wBAAwB,IAAIP;QAC5CQ,cAAcJ,cAAcI,YAAY,IAAIN;QAC5CO,mBAAmBL,cAAcK,iBAAiB,IAAIV;QACtDW,UAAUN,cAAcM,QAAQ,IAAI;QACpCC,WAAW;QACXC,OAAO;YACLnB,OAAOW,cAAcQ,KAAK,EAAEnB,SAASD,cAAcC,KAAK;YACxDC,cAAcU,cAAcQ,KAAK,EAAElB,gBAAgBF,cAAcE,YAAY;YAC7EC,WAAWS,cAAcQ,KAAK,EAAEjB,aAAaH,cAAcG,SAAS;YACpEC,WAAWQ,cAAcQ,KAAK,EAAEhB,aAAaJ,cAAcI,SAAS;YACpEC,UAAUO,cAAcQ,KAAK,EAAEf,YAAYL,cAAcK,QAAQ;QACnE;QACAgB,gBAAgBT,cAAcS,cAAc,IAAIZ;IAClD;AACF"}
@@ -0,0 +1,3 @@
1
+ import type { Endpoint } from 'payload';
2
+ import type { ResolvedReservationPluginConfig } from '../types.js';
3
+ export declare function createCustomerSearchEndpoint(config: ResolvedReservationPluginConfig): Endpoint;
@@ -0,0 +1,94 @@
1
+ export function createCustomerSearchEndpoint(config) {
2
+ return {
3
+ handler: async (req)=>{
4
+ if (!req.user) {
5
+ return Response.json({
6
+ message: 'Unauthorized'
7
+ }, {
8
+ status: 401
9
+ });
10
+ }
11
+ const url = new URL(req.url);
12
+ const search = url.searchParams.get('search') ?? '';
13
+ const limit = Math.min(Number(url.searchParams.get('limit') ?? '10'), 50);
14
+ const page = Math.max(Number(url.searchParams.get('page') ?? '1'), 1);
15
+ let where = {};
16
+ if (search && config.customerRole) {
17
+ where = {
18
+ and: [
19
+ {
20
+ or: [
21
+ {
22
+ name: {
23
+ contains: search
24
+ }
25
+ },
26
+ {
27
+ phone: {
28
+ contains: search
29
+ }
30
+ },
31
+ {
32
+ email: {
33
+ contains: search
34
+ }
35
+ }
36
+ ]
37
+ },
38
+ {
39
+ role: {
40
+ equals: config.customerRole
41
+ }
42
+ }
43
+ ]
44
+ };
45
+ } else if (search) {
46
+ where = {
47
+ or: [
48
+ {
49
+ name: {
50
+ contains: search
51
+ }
52
+ },
53
+ {
54
+ phone: {
55
+ contains: search
56
+ }
57
+ },
58
+ {
59
+ email: {
60
+ contains: search
61
+ }
62
+ }
63
+ ]
64
+ };
65
+ } else if (config.customerRole) {
66
+ where = {
67
+ role: {
68
+ equals: config.customerRole
69
+ }
70
+ };
71
+ }
72
+ const result = await req.payload.find({
73
+ collection: config.userCollection,
74
+ limit,
75
+ page,
76
+ where
77
+ });
78
+ return Response.json({
79
+ docs: result.docs.map((doc)=>({
80
+ id: doc.id,
81
+ name: doc.name ?? '',
82
+ email: doc.email ?? '',
83
+ phone: doc.phone ?? ''
84
+ })),
85
+ hasNextPage: result.hasNextPage,
86
+ totalDocs: result.totalDocs
87
+ });
88
+ },
89
+ method: 'get',
90
+ path: '/reservation-customer-search'
91
+ };
92
+ }
93
+
94
+ //# sourceMappingURL=customerSearch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/endpoints/customerSearch.ts"],"sourcesContent":["import type { Endpoint, Where } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nexport function createCustomerSearchEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n if (!req.user) {\n return Response.json({ message: 'Unauthorized' }, { status: 401 })\n }\n\n const url = new URL(req.url!)\n const search = url.searchParams.get('search') ?? ''\n const limit = Math.min(Number(url.searchParams.get('limit') ?? '10'), 50)\n const page = Math.max(Number(url.searchParams.get('page') ?? '1'), 1)\n\n let where: Where = {}\n\n if (search && config.customerRole) {\n where = {\n and: [\n {\n or: [\n { name: { contains: search } },\n { phone: { contains: search } },\n { email: { contains: search } },\n ],\n },\n { role: { equals: config.customerRole } },\n ],\n }\n } else if (search) {\n where = {\n or: [\n { name: { contains: search } },\n { phone: { contains: search } },\n { email: { contains: search } },\n ],\n }\n } else if (config.customerRole) {\n where = { role: { equals: config.customerRole } }\n }\n\n const result = await req.payload.find({\n collection: config.userCollection,\n limit,\n page,\n where,\n })\n\n return Response.json({\n docs: result.docs.map((doc: Record<string, unknown>) => ({\n id: doc.id,\n name: doc.name ?? '',\n email: doc.email ?? '',\n phone: doc.phone ?? '',\n })),\n hasNextPage: result.hasNextPage,\n totalDocs: result.totalDocs,\n })\n },\n method: 'get',\n path: '/reservation-customer-search',\n }\n}\n"],"names":["createCustomerSearchEndpoint","config","handler","req","user","Response","json","message","status","url","URL","search","searchParams","get","limit","Math","min","Number","page","max","where","customerRole","and","or","name","contains","phone","email","role","equals","result","payload","find","collection","userCollection","docs","map","doc","id","hasNextPage","totalDocs","method","path"],"mappings":"AAIA,OAAO,SAASA,6BACdC,MAAuC;IAEvC,OAAO;QACLC,SAAS,OAAOC;YACd,IAAI,CAACA,IAAIC,IAAI,EAAE;gBACb,OAAOC,SAASC,IAAI,CAAC;oBAAEC,SAAS;gBAAe,GAAG;oBAAEC,QAAQ;gBAAI;YAClE;YAEA,MAAMC,MAAM,IAAIC,IAAIP,IAAIM,GAAG;YAC3B,MAAME,SAASF,IAAIG,YAAY,CAACC,GAAG,CAAC,aAAa;YACjD,MAAMC,QAAQC,KAAKC,GAAG,CAACC,OAAOR,IAAIG,YAAY,CAACC,GAAG,CAAC,YAAY,OAAO;YACtE,MAAMK,OAAOH,KAAKI,GAAG,CAACF,OAAOR,IAAIG,YAAY,CAACC,GAAG,CAAC,WAAW,MAAM;YAEnE,IAAIO,QAAe,CAAC;YAEpB,IAAIT,UAAUV,OAAOoB,YAAY,EAAE;gBACjCD,QAAQ;oBACNE,KAAK;wBACH;4BACEC,IAAI;gCACF;oCAAEC,MAAM;wCAAEC,UAAUd;oCAAO;gCAAE;gCAC7B;oCAAEe,OAAO;wCAAED,UAAUd;oCAAO;gCAAE;gCAC9B;oCAAEgB,OAAO;wCAAEF,UAAUd;oCAAO;gCAAE;6BAC/B;wBACH;wBACA;4BAAEiB,MAAM;gCAAEC,QAAQ5B,OAAOoB,YAAY;4BAAC;wBAAE;qBACzC;gBACH;YACF,OAAO,IAAIV,QAAQ;gBACjBS,QAAQ;oBACNG,IAAI;wBACF;4BAAEC,MAAM;gCAAEC,UAAUd;4BAAO;wBAAE;wBAC7B;4BAAEe,OAAO;gCAAED,UAAUd;4BAAO;wBAAE;wBAC9B;4BAAEgB,OAAO;gCAAEF,UAAUd;4BAAO;wBAAE;qBAC/B;gBACH;YACF,OAAO,IAAIV,OAAOoB,YAAY,EAAE;gBAC9BD,QAAQ;oBAAEQ,MAAM;wBAAEC,QAAQ5B,OAAOoB,YAAY;oBAAC;gBAAE;YAClD;YAEA,MAAMS,SAAS,MAAM3B,IAAI4B,OAAO,CAACC,IAAI,CAAC;gBACpCC,YAAYhC,OAAOiC,cAAc;gBACjCpB;gBACAI;gBACAE;YACF;YAEA,OAAOf,SAASC,IAAI,CAAC;gBACnB6B,MAAML,OAAOK,IAAI,CAACC,GAAG,CAAC,CAACC,MAAkC,CAAA;wBACvDC,IAAID,IAAIC,EAAE;wBACVd,MAAMa,IAAIb,IAAI,IAAI;wBAClBG,OAAOU,IAAIV,KAAK,IAAI;wBACpBD,OAAOW,IAAIX,KAAK,IAAI;oBACtB,CAAA;gBACAa,aAAaT,OAAOS,WAAW;gBAC/BC,WAAWV,OAAOU,SAAS;YAC7B;QACF;QACAC,QAAQ;QACRC,MAAM;IACR;AACF"}
@@ -1,2 +1,3 @@
1
1
  export { AvailabilityOverview } from '../components/AvailabilityOverview/index.js';
2
2
  export { CalendarView } from '../components/CalendarView/index.js';
3
+ export { CustomerField } from '../components/CustomerField/index.js';
@@ -1,4 +1,5 @@
1
1
  export { AvailabilityOverview } from '../components/AvailabilityOverview/index.js';
2
2
  export { CalendarView } from '../components/CalendarView/index.js';
3
+ export { CustomerField } from '../components/CustomerField/index.js';
3
4
 
4
5
  //# sourceMappingURL=client.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["export { AvailabilityOverview } from '../components/AvailabilityOverview/index.js'\nexport { CalendarView } from '../components/CalendarView/index.js'\n"],"names":["AvailabilityOverview","CalendarView"],"mappings":"AAAA,SAASA,oBAAoB,QAAQ,8CAA6C;AAClF,SAASC,YAAY,QAAQ,sCAAqC"}
1
+ {"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["export { AvailabilityOverview } from '../components/AvailabilityOverview/index.js'\nexport { CalendarView } from '../components/CalendarView/index.js'\nexport { CustomerField } from '../components/CustomerField/index.js'\n"],"names":["AvailabilityOverview","CalendarView","CustomerField"],"mappings":"AAAA,SAASA,oBAAoB,QAAQ,8CAA6C;AAClF,SAASC,YAAY,QAAQ,sCAAqC;AAClE,SAASC,aAAa,QAAQ,uCAAsC"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/reservations/calculateEndTime.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { addMinutes } from '../../utilities/slotUtils.js'\n\nexport const calculateEndTime =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, req }) => {\n if (context?.skipReservationHooks) {return data}\n\n if (!data?.startTime || !data?.service) {return data}\n\n const serviceId = typeof data.service === 'object' ? data.service.id : data.service\n\n const service = await req.payload.findByID({\n id: serviceId,\n collection: config.slugs.services as 'reservation-services',\n req,\n })\n\n if (service?.duration) {\n const startDate = new Date(data.startTime)\n data.endTime = addMinutes(startDate, service.duration).toISOString()\n }\n\n return data\n }\n"],"names":["addMinutes","calculateEndTime","config","context","data","req","skipReservationHooks","startTime","service","serviceId","id","payload","findByID","collection","slugs","services","duration","startDate","Date","endTime","toISOString"],"mappings":"AAIA,SAASA,UAAU,QAAQ,+BAA8B;AAEzD,OAAO,MAAMC,mBACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,IAAI,EAAEC,GAAG,EAAE;QAC3B,IAAIF,SAASG,sBAAsB;YAAC,OAAOF;QAAI;QAE/C,IAAI,CAACA,MAAMG,aAAa,CAACH,MAAMI,SAAS;YAAC,OAAOJ;QAAI;QAEpD,MAAMK,YAAY,OAAOL,KAAKI,OAAO,KAAK,WAAWJ,KAAKI,OAAO,CAACE,EAAE,GAAGN,KAAKI,OAAO;QAEnF,MAAMA,UAAU,MAAMH,IAAIM,OAAO,CAACC,QAAQ,CAAC;YACzCF,IAAID;YACJI,YAAYX,OAAOY,KAAK,CAACC,QAAQ;YACjCV;QACF;QAEA,IAAIG,SAASQ,UAAU;YACrB,MAAMC,YAAY,IAAIC,KAAKd,KAAKG,SAAS;YACzCH,KAAKe,OAAO,GAAGnB,WAAWiB,WAAWT,QAAQQ,QAAQ,EAAEI,WAAW;QACpE;QAEA,OAAOhB;IACT,EAAC"}
1
+ {"version":3,"sources":["../../../src/hooks/reservations/calculateEndTime.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { addMinutes } from '../../utilities/slotUtils.js'\n\nexport const calculateEndTime =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, req }) => {\n if (context?.skipReservationHooks) {return data}\n\n if (!data?.startTime || !data?.service) {return data}\n\n const serviceId = typeof data.service === 'object' ? data.service.id : data.service\n\n const service = await req.payload.findByID({\n id: serviceId,\n collection: config.slugs.services as 'services',\n req,\n })\n\n if (service?.duration) {\n const startDate = new Date(data.startTime)\n data.endTime = addMinutes(startDate, service.duration).toISOString()\n }\n\n return data\n }\n"],"names":["addMinutes","calculateEndTime","config","context","data","req","skipReservationHooks","startTime","service","serviceId","id","payload","findByID","collection","slugs","services","duration","startDate","Date","endTime","toISOString"],"mappings":"AAIA,SAASA,UAAU,QAAQ,+BAA8B;AAEzD,OAAO,MAAMC,mBACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,IAAI,EAAEC,GAAG,EAAE;QAC3B,IAAIF,SAASG,sBAAsB;YAAC,OAAOF;QAAI;QAE/C,IAAI,CAACA,MAAMG,aAAa,CAACH,MAAMI,SAAS;YAAC,OAAOJ;QAAI;QAEpD,MAAMK,YAAY,OAAOL,KAAKI,OAAO,KAAK,WAAWJ,KAAKI,OAAO,CAACE,EAAE,GAAGN,KAAKI,OAAO;QAEnF,MAAMA,UAAU,MAAMH,IAAIM,OAAO,CAACC,QAAQ,CAAC;YACzCF,IAAID;YACJI,YAAYX,OAAOY,KAAK,CAACC,QAAQ;YACjCV;QACF;QAEA,IAAIG,SAASQ,UAAU;YACrB,MAAMC,YAAY,IAAIC,KAAKd,KAAKG,SAAS;YACzCH,KAAKe,OAAO,GAAGnB,WAAWiB,WAAWT,QAAQQ,QAAQ,EAAEI,WAAW;QACpE;QAEA,OAAOhB;IACT,EAAC"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/reservations/validateConflicts.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook, Where } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { computeBlockedWindow } from '../../utilities/slotUtils.js'\n\nexport const validateConflicts =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, originalDoc, req }) => {\n if (context?.skipReservationHooks) {return data}\n\n if (!data?.startTime || !data?.endTime || !data?.resource) {return data}\n\n const serviceId = typeof data.service === 'object' ? data.service.id : data.service\n\n let bufferBefore = config.defaultBufferTime\n let bufferAfter = config.defaultBufferTime\n\n if (serviceId) {\n try {\n const service = await req.payload.findByID({\n id: serviceId,\n collection: config.slugs.services as 'reservation-services',\n req,\n })\n if (service) {\n bufferBefore = (service.bufferTimeBefore as number) ?? config.defaultBufferTime\n bufferAfter = (service.bufferTimeAfter as number) ?? config.defaultBufferTime\n }\n } catch {\n // Use defaults if service lookup fails\n }\n }\n\n const startTime = new Date(data.startTime)\n const endTime = new Date(data.endTime)\n const { effectiveEnd, effectiveStart } = computeBlockedWindow(\n startTime,\n endTime,\n bufferBefore,\n bufferAfter,\n )\n\n const resourceId = typeof data.resource === 'object' ? data.resource.id : data.resource\n\n const where: Where = {\n and: [\n { resource: { equals: resourceId } },\n {\n status: {\n not_in: ['cancelled', 'no-show'],\n },\n },\n { startTime: { less_than: effectiveEnd.toISOString() } },\n { endTime: { greater_than: effectiveStart.toISOString() } },\n ],\n }\n\n // Exclude self on update\n if (operation === 'update' && originalDoc?.id) {\n ;(where.and as Where[]).push({ id: { not_equals: originalDoc.id } })\n }\n\n const { totalDocs } = await req.payload.count({\n collection: config.slugs.reservations as 'reservations',\n req,\n where,\n })\n\n if (totalDocs > 0) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorConflict'),\n path: 'startTime',\n },\n ],\n })\n }\n\n return data\n }\n"],"names":["ValidationError","computeBlockedWindow","validateConflicts","config","context","data","operation","originalDoc","req","skipReservationHooks","startTime","endTime","resource","serviceId","service","id","bufferBefore","defaultBufferTime","bufferAfter","payload","findByID","collection","slugs","services","bufferTimeBefore","bufferTimeAfter","Date","effectiveEnd","effectiveStart","resourceId","where","and","equals","status","not_in","less_than","toISOString","greater_than","push","not_equals","totalDocs","count","reservations","errors","message","t","path"],"mappings":"AAEA,SAASA,eAAe,QAAQ,UAAS;AAKzC,SAASC,oBAAoB,QAAQ,+BAA8B;AAEnE,OAAO,MAAMC,oBACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,IAAI,EAAEC,SAAS,EAAEC,WAAW,EAAEC,GAAG,EAAE;QACnD,IAAIJ,SAASK,sBAAsB;YAAC,OAAOJ;QAAI;QAE/C,IAAI,CAACA,MAAMK,aAAa,CAACL,MAAMM,WAAW,CAACN,MAAMO,UAAU;YAAC,OAAOP;QAAI;QAEvE,MAAMQ,YAAY,OAAOR,KAAKS,OAAO,KAAK,WAAWT,KAAKS,OAAO,CAACC,EAAE,GAAGV,KAAKS,OAAO;QAEnF,IAAIE,eAAeb,OAAOc,iBAAiB;QAC3C,IAAIC,cAAcf,OAAOc,iBAAiB;QAE1C,IAAIJ,WAAW;YACb,IAAI;gBACF,MAAMC,UAAU,MAAMN,IAAIW,OAAO,CAACC,QAAQ,CAAC;oBACzCL,IAAIF;oBACJQ,YAAYlB,OAAOmB,KAAK,CAACC,QAAQ;oBACjCf;gBACF;gBACA,IAAIM,SAAS;oBACXE,eAAe,AAACF,QAAQU,gBAAgB,IAAerB,OAAOc,iBAAiB;oBAC/EC,cAAc,AAACJ,QAAQW,eAAe,IAAetB,OAAOc,iBAAiB;gBAC/E;YACF,EAAE,OAAM;YACN,uCAAuC;YACzC;QACF;QAEA,MAAMP,YAAY,IAAIgB,KAAKrB,KAAKK,SAAS;QACzC,MAAMC,UAAU,IAAIe,KAAKrB,KAAKM,OAAO;QACrC,MAAM,EAAEgB,YAAY,EAAEC,cAAc,EAAE,GAAG3B,qBACvCS,WACAC,SACAK,cACAE;QAGF,MAAMW,aAAa,OAAOxB,KAAKO,QAAQ,KAAK,WAAWP,KAAKO,QAAQ,CAACG,EAAE,GAAGV,KAAKO,QAAQ;QAEvF,MAAMkB,QAAe;YACnBC,KAAK;gBACH;oBAAEnB,UAAU;wBAAEoB,QAAQH;oBAAW;gBAAE;gBACnC;oBACEI,QAAQ;wBACNC,QAAQ;4BAAC;4BAAa;yBAAU;oBAClC;gBACF;gBACA;oBAAExB,WAAW;wBAAEyB,WAAWR,aAAaS,WAAW;oBAAG;gBAAE;gBACvD;oBAAEzB,SAAS;wBAAE0B,cAAcT,eAAeQ,WAAW;oBAAG;gBAAE;aAC3D;QACH;QAEA,yBAAyB;QACzB,IAAI9B,cAAc,YAAYC,aAAaQ,IAAI;;YAC3Ce,MAAMC,GAAG,CAAaO,IAAI,CAAC;gBAAEvB,IAAI;oBAAEwB,YAAYhC,YAAYQ,EAAE;gBAAC;YAAE;QACpE;QAEA,MAAM,EAAEyB,SAAS,EAAE,GAAG,MAAMhC,IAAIW,OAAO,CAACsB,KAAK,CAAC;YAC5CpB,YAAYlB,OAAOmB,KAAK,CAACoB,YAAY;YACrClC;YACAsB;QACF;QAEA,IAAIU,YAAY,GAAG;YACjB,MAAM,IAAIxC,gBAAgB;gBACxB2C,QAAQ;oBACN;wBACEC,SAAS,AAACpC,IAAIqC,CAAC,CAAa;wBAC5BC,MAAM;oBACR;iBACD;YACH;QACF;QAEA,OAAOzC;IACT,EAAC"}
1
+ {"version":3,"sources":["../../../src/hooks/reservations/validateConflicts.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook, Where } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { computeBlockedWindow } from '../../utilities/slotUtils.js'\n\nexport const validateConflicts =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, originalDoc, req }) => {\n if (context?.skipReservationHooks) {return data}\n\n if (!data?.startTime || !data?.endTime || !data?.resource) {return data}\n\n const serviceId = typeof data.service === 'object' ? data.service.id : data.service\n\n let bufferBefore = config.defaultBufferTime\n let bufferAfter = config.defaultBufferTime\n\n if (serviceId) {\n try {\n const service = await req.payload.findByID({\n id: serviceId,\n collection: config.slugs.services as 'services',\n req,\n })\n if (service) {\n bufferBefore = (service.bufferTimeBefore as number) ?? config.defaultBufferTime\n bufferAfter = (service.bufferTimeAfter as number) ?? config.defaultBufferTime\n }\n } catch {\n // Use defaults if service lookup fails\n }\n }\n\n const startTime = new Date(data.startTime)\n const endTime = new Date(data.endTime)\n const { effectiveEnd, effectiveStart } = computeBlockedWindow(\n startTime,\n endTime,\n bufferBefore,\n bufferAfter,\n )\n\n const resourceId = typeof data.resource === 'object' ? data.resource.id : data.resource\n\n const where: Where = {\n and: [\n { resource: { equals: resourceId } },\n {\n status: {\n not_in: ['cancelled', 'no-show'],\n },\n },\n { startTime: { less_than: effectiveEnd.toISOString() } },\n { endTime: { greater_than: effectiveStart.toISOString() } },\n ],\n }\n\n // Exclude self on update\n if (operation === 'update' && originalDoc?.id) {\n ;(where.and as Where[]).push({ id: { not_equals: originalDoc.id } })\n }\n\n const { totalDocs } = await req.payload.count({\n collection: config.slugs.reservations as 'reservations',\n req,\n where,\n })\n\n if (totalDocs > 0) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorConflict'),\n path: 'startTime',\n },\n ],\n })\n }\n\n return data\n }\n"],"names":["ValidationError","computeBlockedWindow","validateConflicts","config","context","data","operation","originalDoc","req","skipReservationHooks","startTime","endTime","resource","serviceId","service","id","bufferBefore","defaultBufferTime","bufferAfter","payload","findByID","collection","slugs","services","bufferTimeBefore","bufferTimeAfter","Date","effectiveEnd","effectiveStart","resourceId","where","and","equals","status","not_in","less_than","toISOString","greater_than","push","not_equals","totalDocs","count","reservations","errors","message","t","path"],"mappings":"AAEA,SAASA,eAAe,QAAQ,UAAS;AAKzC,SAASC,oBAAoB,QAAQ,+BAA8B;AAEnE,OAAO,MAAMC,oBACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,IAAI,EAAEC,SAAS,EAAEC,WAAW,EAAEC,GAAG,EAAE;QACnD,IAAIJ,SAASK,sBAAsB;YAAC,OAAOJ;QAAI;QAE/C,IAAI,CAACA,MAAMK,aAAa,CAACL,MAAMM,WAAW,CAACN,MAAMO,UAAU;YAAC,OAAOP;QAAI;QAEvE,MAAMQ,YAAY,OAAOR,KAAKS,OAAO,KAAK,WAAWT,KAAKS,OAAO,CAACC,EAAE,GAAGV,KAAKS,OAAO;QAEnF,IAAIE,eAAeb,OAAOc,iBAAiB;QAC3C,IAAIC,cAAcf,OAAOc,iBAAiB;QAE1C,IAAIJ,WAAW;YACb,IAAI;gBACF,MAAMC,UAAU,MAAMN,IAAIW,OAAO,CAACC,QAAQ,CAAC;oBACzCL,IAAIF;oBACJQ,YAAYlB,OAAOmB,KAAK,CAACC,QAAQ;oBACjCf;gBACF;gBACA,IAAIM,SAAS;oBACXE,eAAe,AAACF,QAAQU,gBAAgB,IAAerB,OAAOc,iBAAiB;oBAC/EC,cAAc,AAACJ,QAAQW,eAAe,IAAetB,OAAOc,iBAAiB;gBAC/E;YACF,EAAE,OAAM;YACN,uCAAuC;YACzC;QACF;QAEA,MAAMP,YAAY,IAAIgB,KAAKrB,KAAKK,SAAS;QACzC,MAAMC,UAAU,IAAIe,KAAKrB,KAAKM,OAAO;QACrC,MAAM,EAAEgB,YAAY,EAAEC,cAAc,EAAE,GAAG3B,qBACvCS,WACAC,SACAK,cACAE;QAGF,MAAMW,aAAa,OAAOxB,KAAKO,QAAQ,KAAK,WAAWP,KAAKO,QAAQ,CAACG,EAAE,GAAGV,KAAKO,QAAQ;QAEvF,MAAMkB,QAAe;YACnBC,KAAK;gBACH;oBAAEnB,UAAU;wBAAEoB,QAAQH;oBAAW;gBAAE;gBACnC;oBACEI,QAAQ;wBACNC,QAAQ;4BAAC;4BAAa;yBAAU;oBAClC;gBACF;gBACA;oBAAExB,WAAW;wBAAEyB,WAAWR,aAAaS,WAAW;oBAAG;gBAAE;gBACvD;oBAAEzB,SAAS;wBAAE0B,cAAcT,eAAeQ,WAAW;oBAAG;gBAAE;aAC3D;QACH;QAEA,yBAAyB;QACzB,IAAI9B,cAAc,YAAYC,aAAaQ,IAAI;;YAC3Ce,MAAMC,GAAG,CAAaO,IAAI,CAAC;gBAAEvB,IAAI;oBAAEwB,YAAYhC,YAAYQ,EAAE;gBAAC;YAAE;QACpE;QAEA,MAAM,EAAEyB,SAAS,EAAE,GAAG,MAAMhC,IAAIW,OAAO,CAACsB,KAAK,CAAC;YAC5CpB,YAAYlB,OAAOmB,KAAK,CAACoB,YAAY;YACrClC;YACAsB;QACF;QAEA,IAAIU,YAAY,GAAG;YACjB,MAAM,IAAIxC,gBAAgB;gBACxB2C,QAAQ;oBACN;wBACEC,SAAS,AAACpC,IAAIqC,CAAC,CAAa;wBAC5BC,MAAM;oBACR;iBACD;YACH;QACF;QAEA,OAAOzC;IACT,EAAC"}
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { reservationPlugin } from './plugin.js';
1
+ export { payloadReserve } from './plugin.js';
2
2
  export type { ReservationPluginConfig, ResolvedReservationPluginConfig } from './types.js';
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export { reservationPlugin } from './plugin.js';
1
+ export { payloadReserve } from './plugin.js';
2
2
 
3
3
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export { reservationPlugin } from './plugin.js'\nexport type { ReservationPluginConfig, ResolvedReservationPluginConfig } from './types.js'\n"],"names":["reservationPlugin"],"mappings":"AAAA,SAASA,iBAAiB,QAAQ,cAAa"}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export { payloadReserve } from './plugin.js'\nexport type { ReservationPluginConfig, ResolvedReservationPluginConfig } from './types.js'\n"],"names":["payloadReserve"],"mappings":"AAAA,SAASA,cAAc,QAAQ,cAAa"}
package/dist/plugin.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import type { Config } from 'payload';
2
2
  import type { ReservationPluginConfig } from './types.js';
3
- export declare const reservationPlugin: (pluginOptions?: ReservationPluginConfig) => (config: Config) => Config;
3
+ export declare const payloadReserve: (pluginOptions?: ReservationPluginConfig) => (config: Config) => Config;
package/dist/plugin.js CHANGED
@@ -4,9 +4,10 @@ import { createResourcesCollection } from './collections/Resources.js';
4
4
  import { createSchedulesCollection } from './collections/Schedules.js';
5
5
  import { createServicesCollection } from './collections/Services.js';
6
6
  import { resolveConfig } from './defaults.js';
7
+ import { createCustomerSearchEndpoint } from './endpoints/customerSearch.js';
7
8
  import { translations } from './translations/index.js';
8
9
  /** Check whether a top-level field with the given name already exists */ const hasField = (fields, name)=>fields.some((f)=>'name' in f && f.name === name);
9
- export const reservationPlugin = (pluginOptions = {})=>(config)=>{
10
+ export const payloadReserve = (pluginOptions = {})=>(config)=>{
10
11
  const resolved = resolveConfig(pluginOptions);
11
12
  // Detect localization from the Payload config
12
13
  if (config.localization) {
@@ -50,10 +51,26 @@ export const reservationPlugin = (pluginOptions = {})=>(config)=>{
50
51
  userCol.fields.push(field);
51
52
  }
52
53
  }
54
+ // Enable multi-field search on the user collection
55
+ if (!userCol.admin) {
56
+ userCol.admin = {};
57
+ }
58
+ if (!userCol.admin.listSearchableFields) {
59
+ userCol.admin.listSearchableFields = [
60
+ 'name',
61
+ 'phone',
62
+ 'email'
63
+ ];
64
+ }
53
65
  } else {
54
66
  // eslint-disable-next-line no-console
55
67
  console.warn(`[payload-reserve] Could not find collection "${resolved.userCollection}" to extend with customer fields. ` + 'Make sure your Payload config defines this collection before the reservation plugin runs.');
56
68
  }
69
+ // Register custom endpoints
70
+ if (!config.endpoints) {
71
+ config.endpoints = [];
72
+ }
73
+ config.endpoints.push(createCustomerSearchEndpoint(resolved));
57
74
  // Set up admin configuration
58
75
  if (!config.admin) {
59
76
  config.admin = {};
@@ -69,6 +86,7 @@ export const reservationPlugin = (pluginOptions = {})=>(config)=>{
69
86
  ...resolved.slugs,
70
87
  userCollection: resolved.userCollection
71
88
  };
89
+ config.admin.custom.reservationCustomerRole = resolved.customerRole;
72
90
  // Add dashboard widget
73
91
  if (!config.admin.dashboard) {
74
92
  config.admin.dashboard = {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import type { Config, Field } from 'payload'\n\nimport { deepMergeSimple } from 'payload/shared'\n\nimport type { ReservationPluginConfig } from './types.js'\n\nimport { createReservationsCollection } from './collections/Reservations.js'\nimport { createResourcesCollection } from './collections/Resources.js'\nimport { createSchedulesCollection } from './collections/Schedules.js'\nimport { createServicesCollection } from './collections/Services.js'\nimport { resolveConfig } from './defaults.js'\nimport { translations } from './translations/index.js'\n\n/** Check whether a top-level field with the given name already exists */\nconst hasField = (fields: Field[], name: string): boolean =>\n fields.some((f) => 'name' in f && f.name === name)\n\nexport const reservationPlugin =\n (pluginOptions: ReservationPluginConfig = {}) =>\n (config: Config): Config => {\n const resolved = resolveConfig(pluginOptions)\n\n // Detect localization from the Payload config\n if (config.localization) {\n resolved.localized = true\n }\n\n if (!config.collections) {\n config.collections = []\n }\n\n if (resolved.disabled) {\n return config\n }\n\n // Add the 4 plugin collections\n config.collections.push(\n createServicesCollection(resolved),\n createResourcesCollection(resolved),\n createSchedulesCollection(resolved),\n createReservationsCollection(resolved),\n )\n\n // Extend the existing user collection with customer fields\n const userCol = config.collections.find((c) => c.slug === resolved.userCollection)\n if (userCol) {\n const fieldsToAdd: Field[] = [\n { name: 'name', type: 'text', maxLength: 200 },\n { name: 'phone', type: 'text', maxLength: 50 },\n { name: 'notes', type: 'textarea' },\n {\n name: 'bookings',\n type: 'join',\n collection: resolved.slugs.reservations,\n on: 'customer',\n },\n ]\n\n for (const field of fieldsToAdd) {\n if (!hasField(userCol.fields, (field as { name: string }).name)) {\n userCol.fields.push(field)\n }\n }\n } else {\n // eslint-disable-next-line no-console\n console.warn(\n `[payload-reserve] Could not find collection \"${resolved.userCollection}\" to extend with customer fields. ` +\n 'Make sure your Payload config defines this collection before the reservation plugin runs.',\n )\n }\n\n // Set up admin configuration\n if (!config.admin) {config.admin = {}}\n if (!config.admin.components) {config.admin.components = {}}\n\n // Store slugs in admin custom for component access\n if (!config.admin.custom) {config.admin.custom = {}}\n config.admin.custom.reservationSlugs = {\n ...resolved.slugs,\n userCollection: resolved.userCollection,\n }\n\n // Add dashboard widget\n if (!config.admin.dashboard) {\n config.admin.dashboard = { widgets: [] }\n }\n if (!config.admin.dashboard.widgets) {\n config.admin.dashboard.widgets = []\n }\n config.admin.dashboard.widgets.push({\n slug: 'reservation-todays-reservations',\n ComponentPath: 'payload-reserve/rsc#DashboardWidgetServer',\n label: 'Today\\'s Reservations',\n maxWidth: 'large',\n minWidth: 'medium',\n })\n\n // Add availability overview as custom admin view\n if (!config.admin.components.views) {\n config.admin.components.views = {}\n }\n ;(config.admin.components.views as Record<string, unknown>)['reservation-availability'] = {\n Component: 'payload-reserve/client#AvailabilityOverview',\n path: '/reservation-availability',\n }\n\n // Merge plugin translations (user translations take precedence)\n if (!config.i18n) {config.i18n = {}}\n ;(config.i18n as Record<string, unknown>).translations = deepMergeSimple(\n translations,\n (config.i18n as Record<string, unknown>).translations ?? {},\n )\n\n return config\n }\n"],"names":["deepMergeSimple","createReservationsCollection","createResourcesCollection","createSchedulesCollection","createServicesCollection","resolveConfig","translations","hasField","fields","name","some","f","reservationPlugin","pluginOptions","config","resolved","localization","localized","collections","disabled","push","userCol","find","c","slug","userCollection","fieldsToAdd","type","maxLength","collection","slugs","reservations","on","field","console","warn","admin","components","custom","reservationSlugs","dashboard","widgets","ComponentPath","label","maxWidth","minWidth","views","Component","path","i18n"],"mappings":"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"}
1
+ {"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import type { Config, Field } from 'payload'\n\nimport { deepMergeSimple } from 'payload/shared'\n\nimport type { ReservationPluginConfig } from './types.js'\n\nimport { createReservationsCollection } from './collections/Reservations.js'\nimport { createResourcesCollection } from './collections/Resources.js'\nimport { createSchedulesCollection } from './collections/Schedules.js'\nimport { createServicesCollection } from './collections/Services.js'\nimport { resolveConfig } from './defaults.js'\nimport { createCustomerSearchEndpoint } from './endpoints/customerSearch.js'\nimport { translations } from './translations/index.js'\n\n/** Check whether a top-level field with the given name already exists */\nconst hasField = (fields: Field[], name: string): boolean =>\n fields.some((f) => 'name' in f && f.name === name)\n\nexport const payloadReserve =\n (pluginOptions: ReservationPluginConfig = {}) =>\n (config: Config): Config => {\n const resolved = resolveConfig(pluginOptions)\n\n // Detect localization from the Payload config\n if (config.localization) {\n resolved.localized = true\n }\n\n if (!config.collections) {\n config.collections = []\n }\n\n if (resolved.disabled) {\n return config\n }\n\n // Add the 4 plugin collections\n config.collections.push(\n createServicesCollection(resolved),\n createResourcesCollection(resolved),\n createSchedulesCollection(resolved),\n createReservationsCollection(resolved),\n )\n\n // Extend the existing user collection with customer fields\n const userCol = config.collections.find((c) => c.slug === resolved.userCollection)\n if (userCol) {\n const fieldsToAdd: Field[] = [\n { name: 'name', type: 'text', maxLength: 200 },\n { name: 'phone', type: 'text', maxLength: 50 },\n { name: 'notes', type: 'textarea' },\n {\n name: 'bookings',\n type: 'join',\n collection: resolved.slugs.reservations,\n on: 'customer',\n },\n ]\n\n for (const field of fieldsToAdd) {\n if (!hasField(userCol.fields, (field as { name: string }).name)) {\n userCol.fields.push(field)\n }\n }\n\n // Enable multi-field search on the user collection\n if (!userCol.admin) {userCol.admin = {}}\n if (!userCol.admin.listSearchableFields) {\n userCol.admin.listSearchableFields = ['name', 'phone', 'email']\n }\n } else {\n // eslint-disable-next-line no-console\n console.warn(\n `[payload-reserve] Could not find collection \"${resolved.userCollection}\" to extend with customer fields. ` +\n 'Make sure your Payload config defines this collection before the reservation plugin runs.',\n )\n }\n\n // Register custom endpoints\n if (!config.endpoints) {config.endpoints = []}\n config.endpoints.push(createCustomerSearchEndpoint(resolved))\n\n // Set up admin configuration\n if (!config.admin) {config.admin = {}}\n if (!config.admin.components) {config.admin.components = {}}\n\n // Store slugs in admin custom for component access\n if (!config.admin.custom) {config.admin.custom = {}}\n config.admin.custom.reservationSlugs = {\n ...resolved.slugs,\n userCollection: resolved.userCollection,\n }\n config.admin.custom.reservationCustomerRole = resolved.customerRole\n\n // Add dashboard widget\n if (!config.admin.dashboard) {\n config.admin.dashboard = { widgets: [] }\n }\n if (!config.admin.dashboard.widgets) {\n config.admin.dashboard.widgets = []\n }\n config.admin.dashboard.widgets.push({\n slug: 'reservation-todays-reservations',\n ComponentPath: 'payload-reserve/rsc#DashboardWidgetServer',\n label: 'Today\\'s Reservations',\n maxWidth: 'large',\n minWidth: 'medium',\n })\n\n // Add availability overview as custom admin view\n if (!config.admin.components.views) {\n config.admin.components.views = {}\n }\n ;(config.admin.components.views as Record<string, unknown>)['reservation-availability'] = {\n Component: 'payload-reserve/client#AvailabilityOverview',\n path: '/reservation-availability',\n }\n\n // Merge plugin translations (user translations take precedence)\n if (!config.i18n) {config.i18n = {}}\n ;(config.i18n as Record<string, unknown>).translations = deepMergeSimple(\n translations,\n (config.i18n as Record<string, unknown>).translations ?? {},\n )\n\n return config\n }\n"],"names":["deepMergeSimple","createReservationsCollection","createResourcesCollection","createSchedulesCollection","createServicesCollection","resolveConfig","createCustomerSearchEndpoint","translations","hasField","fields","name","some","f","payloadReserve","pluginOptions","config","resolved","localization","localized","collections","disabled","push","userCol","find","c","slug","userCollection","fieldsToAdd","type","maxLength","collection","slugs","reservations","on","field","admin","listSearchableFields","console","warn","endpoints","components","custom","reservationSlugs","reservationCustomerRole","customerRole","dashboard","widgets","ComponentPath","label","maxWidth","minWidth","views","Component","path","i18n"],"mappings":"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"}
@@ -5,6 +5,7 @@
5
5
  "collectionReservations": "Reservations",
6
6
  "fieldName": "Name",
7
7
  "fieldDescription": "Description",
8
+ "fieldImage": "Image",
8
9
  "fieldPrice": "Price",
9
10
  "fieldActive": "Active",
10
11
  "fieldServices": "Services",
@@ -82,5 +83,9 @@
82
83
  "dashboardNextAppointment": "Next Appointment",
83
84
  "dashboardTime": "Time:",
84
85
  "dashboardStatus": "Status:",
85
- "dashboardNoUpcoming": "No upcoming appointments today."
86
+ "dashboardNoUpcoming": "No upcoming appointments today.",
87
+ "fieldCustomerSearch": "Search customers...",
88
+ "fieldCustomerNoResults": "No customers found",
89
+ "fieldCustomerCreateNew": "Create new customer",
90
+ "fieldCustomerClear": "Clear selection"
86
91
  }
package/dist/types.d.ts CHANGED
@@ -11,12 +11,15 @@ export type ReservationPluginConfig = {
11
11
  adminGroup?: string;
12
12
  /** Hours of notice required before cancellation */
13
13
  cancellationNoticePeriod?: number;
14
+ /** Role to filter customers by in the reservation form. Set false to disable filtering. (default: 'customer') */
15
+ customerRole?: false | string;
14
16
  /** Default buffer time in minutes between reservations */
15
17
  defaultBufferTime?: number;
16
18
  /** Disable the plugin entirely */
17
19
  disabled?: boolean;
18
20
  /** Override collection slugs */
19
21
  slugs?: {
22
+ media?: string;
20
23
  reservations?: string;
21
24
  resources?: string;
22
25
  schedules?: string;
@@ -34,10 +37,12 @@ export type ResolvedReservationPluginConfig = {
34
37
  };
35
38
  adminGroup: string;
36
39
  cancellationNoticePeriod: number;
40
+ customerRole: false | string;
37
41
  defaultBufferTime: number;
38
42
  disabled: boolean;
39
43
  localized: boolean;
40
44
  slugs: {
45
+ media: string;
41
46
  reservations: string;
42
47
  resources: string;
43
48
  schedules: string;
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload'\n\nexport type ReservationPluginConfig = {\n /** Override access control per collection */\n access?: {\n reservations?: CollectionConfig['access']\n resources?: CollectionConfig['access']\n schedules?: CollectionConfig['access']\n services?: CollectionConfig['access']\n }\n /** Admin group name for all reservation collections */\n adminGroup?: string\n /** Hours of notice required before cancellation */\n cancellationNoticePeriod?: number\n /** Default buffer time in minutes between reservations */\n defaultBufferTime?: number\n /** Disable the plugin entirely */\n disabled?: boolean\n /** Override collection slugs */\n slugs?: {\n reservations?: string\n resources?: string\n schedules?: string\n services?: string\n }\n /** Slug of the existing auth collection to extend with customer fields (default: 'users') */\n userCollection?: string\n}\n\nexport type ResolvedReservationPluginConfig = {\n access: {\n reservations?: CollectionConfig['access']\n resources?: CollectionConfig['access']\n schedules?: CollectionConfig['access']\n services?: CollectionConfig['access']\n }\n adminGroup: string\n cancellationNoticePeriod: number\n defaultBufferTime: number\n disabled: boolean\n localized: boolean\n slugs: {\n reservations: string\n resources: string\n schedules: string\n services: string\n }\n userCollection: string\n}\n\nexport type ReservationStatus = 'cancelled' | 'completed' | 'confirmed' | 'no-show' | 'pending'\n\nexport type DayOfWeek = 'fri' | 'mon' | 'sat' | 'sun' | 'thu' | 'tue' | 'wed'\n\nexport type ScheduleType = 'manual' | 'recurring'\n\nexport const VALID_STATUS_TRANSITIONS: Record<ReservationStatus, ReservationStatus[]> = {\n cancelled: [],\n completed: [],\n confirmed: ['completed', 'cancelled', 'no-show'],\n 'no-show': [],\n pending: ['confirmed', 'cancelled'],\n}\n"],"names":["VALID_STATUS_TRANSITIONS","cancelled","completed","confirmed","pending"],"mappings":"AAwDA,OAAO,MAAMA,2BAA2E;IACtFC,WAAW,EAAE;IACbC,WAAW,EAAE;IACbC,WAAW;QAAC;QAAa;QAAa;KAAU;IAChD,WAAW,EAAE;IACbC,SAAS;QAAC;QAAa;KAAY;AACrC,EAAC"}
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload'\n\nexport type ReservationPluginConfig = {\n /** Override access control per collection */\n access?: {\n reservations?: CollectionConfig['access']\n resources?: CollectionConfig['access']\n schedules?: CollectionConfig['access']\n services?: CollectionConfig['access']\n }\n /** Admin group name for all reservation collections */\n adminGroup?: string\n /** Hours of notice required before cancellation */\n cancellationNoticePeriod?: number\n /** Role to filter customers by in the reservation form. Set false to disable filtering. (default: 'customer') */\n customerRole?: false | string\n /** Default buffer time in minutes between reservations */\n defaultBufferTime?: number\n /** Disable the plugin entirely */\n disabled?: boolean\n /** Override collection slugs */\n slugs?: {\n media?: string\n reservations?: string\n resources?: string\n schedules?: string\n services?: string\n }\n /** Slug of the existing auth collection to extend with customer fields (default: 'users') */\n userCollection?: string\n}\n\nexport type ResolvedReservationPluginConfig = {\n access: {\n reservations?: CollectionConfig['access']\n resources?: CollectionConfig['access']\n schedules?: CollectionConfig['access']\n services?: CollectionConfig['access']\n }\n adminGroup: string\n cancellationNoticePeriod: number\n customerRole: false | string\n defaultBufferTime: number\n disabled: boolean\n localized: boolean\n slugs: {\n media: string\n reservations: string\n resources: string\n schedules: string\n services: string\n }\n userCollection: string\n}\n\nexport type ReservationStatus = 'cancelled' | 'completed' | 'confirmed' | 'no-show' | 'pending'\n\nexport type DayOfWeek = 'fri' | 'mon' | 'sat' | 'sun' | 'thu' | 'tue' | 'wed'\n\nexport type ScheduleType = 'manual' | 'recurring'\n\nexport const VALID_STATUS_TRANSITIONS: Record<ReservationStatus, ReservationStatus[]> = {\n cancelled: [],\n completed: [],\n confirmed: ['completed', 'cancelled', 'no-show'],\n 'no-show': [],\n pending: ['confirmed', 'cancelled'],\n}\n"],"names":["VALID_STATUS_TRANSITIONS","cancelled","completed","confirmed","pending"],"mappings":"AA6DA,OAAO,MAAMA,2BAA2E;IACtFC,WAAW,EAAE;IACbC,WAAW,EAAE;IACbC,WAAW;QAAC;QAAa;QAAa;KAAU;IAChD,WAAW,EAAE;IACbC,SAAS;QAAC;QAAa;KAAY;AACrC,EAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payload-reserve",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "A Payload CMS 3.x plugin for reservation and booking management with conflict detection, status workflows, and calendar UI",
5
5
  "keywords": [
6
6
  "payload",