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 +70 -28
- package/dist/collections/Reservations.js +15 -1
- package/dist/collections/Reservations.js.map +1 -1
- package/dist/collections/Resources.js +6 -0
- package/dist/collections/Resources.js.map +1 -1
- package/dist/components/CustomerField/CustomerField.module.css +147 -0
- package/dist/components/CustomerField/index.d.ts +2 -0
- package/dist/components/CustomerField/index.js +238 -0
- package/dist/components/CustomerField/index.js.map +1 -0
- package/dist/defaults.d.ts +5 -3
- package/dist/defaults.js +7 -3
- package/dist/defaults.js.map +1 -1
- package/dist/endpoints/customerSearch.d.ts +3 -0
- package/dist/endpoints/customerSearch.js +94 -0
- package/dist/endpoints/customerSearch.js.map +1 -0
- package/dist/exports/client.d.ts +1 -0
- package/dist/exports/client.js +1 -0
- package/dist/exports/client.js.map +1 -1
- package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
- package/dist/hooks/reservations/validateConflicts.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/plugin.d.ts +1 -1
- package/dist/plugin.js +19 -1
- package/dist/plugin.js.map +1 -1
- package/dist/translations/en.json +6 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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 {
|
|
81
|
+
import { payloadReserve } from 'payload-reserve'
|
|
80
82
|
|
|
81
83
|
export default buildConfig({
|
|
82
84
|
// ... your existing config
|
|
83
85
|
plugins: [
|
|
84
|
-
|
|
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 {
|
|
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: '
|
|
110
|
-
resources: '
|
|
111
|
-
schedules: '
|
|
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
|
-
|
|
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` | `'
|
|
151
|
-
| `slugs.resources` | `'
|
|
152
|
-
| `slugs.schedules` | `'
|
|
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:** `
|
|
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: '
|
|
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:** `
|
|
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: '
|
|
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:** `
|
|
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: '
|
|
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 {
|
|
768
|
+
import { payloadReserve } from 'payload-reserve'
|
|
733
769
|
|
|
734
|
-
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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/
|
|
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 {
|
|
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 '
|
|
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 '
|
|
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 '
|
|
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":"AAKA,SAASA,gBAAgB,QAAQ,4CAA2C;AAC5E,SAASC,oBAAoB,QAAQ,gDAA+C;AACpF,SAASC,iBAAiB,QAAQ,6CAA4C;AAC9E,SAASC,wBAAwB,QAAQ,oDAAmD;AAE5F,OAAO,SAASC,6BACdC,MAAuC;IAEvC,OAAO;QACLC,MAAMD,OAAOE,KAAK,CAACC,YAAY;QAC/BC,QAAQJ,OAAOI,MAAM,CAACD,YAAY,IAAI,CAAC;QACvCE,OAAO;YACLC,YAAY;gBACVC,OAAO;oBACLC,MAAM;wBACJC,WAAW;oBACb;gBACF;YACF;YACAC,OAAOV,OAAOW,UAAU;YACxBC,sBAAsB;gBAAC;aAAS;YAChCC,YAAY;QACd;QACAC,QAAQ;YACN;gBACEC,MAAM;gBACNC,MAAM;gBACNC,OAAO,CAAC,EAAEC,CAAC,EAAE,GAAK,AAACA,EAAc;gBACjCC,YAAYnB,OAAOE,KAAK,CAACkB,QAAQ;gBACjCC,UAAU;YACZ;YACA;gBACEN,MAAM;gBACNC,MAAM;gBACNC,OAAO,CAAC,EAAEC,CAAC,EAAE,GAAK,AAACA,EAAc;gBACjCC,YAAYnB,OAAOE,KAAK,CAACoB,SAAS;gBAClCD,UAAU;YACZ;YACA;gBACEN,MAAM;gBACNC,MAAM;gBACNC,OAAO,CAAC,EAAEC,CAAC,EAAE,GAAK,AAACA,EAAc;gBACjCC,YAAYnB,OAAOuB,cAAc;gBACjCF,UAAU;
|
|
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","
|
|
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,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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"}
|
package/dist/defaults.d.ts
CHANGED
|
@@ -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: "
|
|
5
|
-
readonly schedules: "
|
|
6
|
-
readonly 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: '
|
|
4
|
-
schedules: '
|
|
5
|
-
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,
|
package/dist/defaults.js.map
CHANGED
|
@@ -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: '
|
|
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,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"}
|
package/dist/exports/client.d.ts
CHANGED
package/dist/exports/client.js
CHANGED
|
@@ -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 '
|
|
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 '
|
|
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 {
|
|
1
|
+
export { payloadReserve } from './plugin.js';
|
|
2
2
|
export type { ReservationPluginConfig, ResolvedReservationPluginConfig } from './types.js';
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export {
|
|
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
|
|
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
|
|
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 = {
|
package/dist/plugin.js.map
CHANGED
|
@@ -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
|
|
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":"
|
|
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