payload-reserve 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -3
- package/dist/collections/Schedules.js +49 -4
- package/dist/collections/Schedules.js.map +1 -1
- package/dist/components/CalendarView/CalendarView.module.css +35 -0
- package/dist/components/CalendarView/index.js +94 -9
- package/dist/components/CalendarView/index.js.map +1 -1
- package/dist/defaults.js +28 -1
- package/dist/defaults.js.map +1 -1
- package/dist/endpoints/cancelBooking.js +20 -0
- package/dist/endpoints/cancelBooking.js.map +1 -1
- package/dist/endpoints/checkAvailability.js +11 -1
- package/dist/endpoints/checkAvailability.js.map +1 -1
- package/dist/endpoints/customerSearch.js +8 -0
- package/dist/endpoints/customerSearch.js.map +1 -1
- package/dist/endpoints/getSlots.js +1 -0
- package/dist/endpoints/getSlots.js.map +1 -1
- package/dist/hooks/reservations/onStatusChange.js +35 -16
- package/dist/hooks/reservations/onStatusChange.js.map +1 -1
- package/dist/hooks/reservations/validateConflicts.js +23 -22
- package/dist/hooks/reservations/validateConflicts.js.map +1 -1
- package/dist/hooks/reservations/validateStatusTransition.js +16 -6
- package/dist/hooks/reservations/validateStatusTransition.js.map +1 -1
- package/dist/services/AvailabilityService.d.ts +1 -0
- package/dist/services/AvailabilityService.js +30 -4
- package/dist/services/AvailabilityService.js.map +1 -1
- package/dist/translations/en.json +3 -1
- package/dist/utilities/resolveReservationItems.d.ts +2 -1
- package/dist/utilities/resolveReservationItems.js +47 -5
- package/dist/utilities/resolveReservationItems.js.map +1 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Designed for salons, clinics, hotels, restaurants, event venues, and any busines
|
|
|
12
12
|
- **User Collection Extension** — Optionally extend your existing auth collection with booking fields; set `userCollection: undefined` (default) to use a standalone Customers collection
|
|
13
13
|
- **Resource Owner Multi-Tenancy** — Opt-in `resourceOwnerMode` wires ownership access control so each resource owner (host) sees only their own listings and reservations
|
|
14
14
|
- **Configurable Status Machine** — Define your own statuses, transitions, blocking states, and terminal states
|
|
15
|
-
- **Double-Booking Prevention** — Server-side conflict detection with
|
|
15
|
+
- **Double-Booking Prevention** — Server-side conflict detection with per-item buffer times; respects capacity modes
|
|
16
16
|
- **Auto End Time** — Calculates `endTime` from `startTime + service.duration` automatically
|
|
17
17
|
- **Three Duration Types** — `fixed` (service duration), `flexible` (customer-specified end), and `full-day` bookings
|
|
18
18
|
- **Multi-Resource Bookings** — Single reservation that spans multiple resources simultaneously via the `items` array
|
|
@@ -21,8 +21,8 @@ Designed for salons, clinics, hotels, restaurants, event venues, and any busines
|
|
|
21
21
|
- **Extra Reservation Fields** — Inject custom fields into the Reservations collection via `extraReservationFields` without forking the plugin
|
|
22
22
|
- **Cancellation Policy** — Configurable minimum notice period enforcement
|
|
23
23
|
- **Plugin Hooks API** — Seven lifecycle hooks (`beforeBookingCreate`, `afterBookingCreate`, `beforeBookingConfirm`, `afterBookingConfirm`, `beforeBookingCancel`, `afterBookingCancel`, `afterStatusChange`) for integrating email, Stripe, and external systems
|
|
24
|
-
- **Availability Service** — Pure functions and DB helpers for slot generation and conflict checking
|
|
25
|
-
- **Public REST API** — Five pre-built endpoints for availability, slot listing, booking, cancellation, and customer search
|
|
24
|
+
- **Availability Service** — Pure functions and DB helpers for slot generation (15-min step) and conflict checking with guest-count-aware filtering
|
|
25
|
+
- **Public REST API** — Five pre-built endpoints for availability, slot listing, booking, cancellation, and customer search — with ownership enforcement and input validation
|
|
26
26
|
- **Calendar View** — Month/week/day calendar replacing the default reservations list view
|
|
27
27
|
- **Dashboard Widget** — Server component showing today's booking stats
|
|
28
28
|
- **Availability Overview** — Weekly grid of resource availability vs. booked slots
|
|
@@ -105,3 +105,29 @@ The `access` override in plugin config always takes precedence over the auto-wir
|
|
|
105
105
|
| [Examples](https://github.com/elghaied/payload-reserve/blob/main/docs/examples.md) | Salon, hotel, restaurant, event venue, Stripe, email, multi-tenant (resource owner mode) |
|
|
106
106
|
| [Advanced](https://github.com/elghaied/payload-reserve/blob/main/docs/advanced.md) | DB indexes, reconciliation job for race condition detection |
|
|
107
107
|
| [Development](https://github.com/elghaied/payload-reserve/blob/main/docs/development.md) | Prerequisites, commands, project file tree |
|
|
108
|
+
| [v1.2.0 Breaking Changes](https://github.com/elghaied/payload-reserve/blob/main/docs/BREAKING-CHANGES-v1.2.md) | Migration guide for upgrading to v1.2.0 |
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Contributing
|
|
113
|
+
|
|
114
|
+
This project uses [Changesets](https://github.com/changesets/changesets) for versioning and changelogs.
|
|
115
|
+
|
|
116
|
+
When making a change that should appear in the release notes, run:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
pnpm changeset
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
This prompts for the semver bump type (patch/minor/major) and a summary. Commit the generated changeset file with your PR.
|
|
123
|
+
|
|
124
|
+
**Releasing:**
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pnpm changeset:version # consume changesets, bump version, update CHANGELOG.md
|
|
128
|
+
git add -A && git commit -m "release v<version>"
|
|
129
|
+
git tag v<version>
|
|
130
|
+
git push && git push --tags
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The GitHub Action will create a release with the changelog content and publish to npm.
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
+
import { ValidationError } from 'payload';
|
|
1
2
|
import { makeScheduleOwnerAccess } from '../utilities/ownerAccess.js';
|
|
3
|
+
const TIME_REGEX = /^(?:[01]\d|2[0-3]):[0-5]\d$/;
|
|
4
|
+
const validateTime = (value)=>{
|
|
5
|
+
if (!value) {
|
|
6
|
+
return true;
|
|
7
|
+
} // required handles emptiness
|
|
8
|
+
return TIME_REGEX.test(value) || 'Invalid time format. Use HH:mm (e.g., 09:00, 17:30)';
|
|
9
|
+
};
|
|
2
10
|
export function createSchedulesCollection(config) {
|
|
3
11
|
const rom = config.resourceOwnerMode;
|
|
4
12
|
const access = config.access.schedules ?? (rom ? makeScheduleOwnerAccess(rom) : {});
|
|
@@ -89,7 +97,8 @@ export function createSchedulesCollection(config) {
|
|
|
89
97
|
placeholder: '09:00'
|
|
90
98
|
},
|
|
91
99
|
label: ({ t })=>t('reservation:fieldStartTimeHHmm'),
|
|
92
|
-
required: true
|
|
100
|
+
required: true,
|
|
101
|
+
validate: validateTime
|
|
93
102
|
},
|
|
94
103
|
{
|
|
95
104
|
name: 'endTime',
|
|
@@ -98,7 +107,8 @@ export function createSchedulesCollection(config) {
|
|
|
98
107
|
placeholder: '17:00'
|
|
99
108
|
},
|
|
100
109
|
label: ({ t })=>t('reservation:fieldEndTimeHHmm'),
|
|
101
|
-
required: true
|
|
110
|
+
required: true,
|
|
111
|
+
validate: validateTime
|
|
102
112
|
}
|
|
103
113
|
],
|
|
104
114
|
label: ({ t })=>t('reservation:fieldRecurringSlots')
|
|
@@ -128,7 +138,8 @@ export function createSchedulesCollection(config) {
|
|
|
128
138
|
placeholder: '09:00'
|
|
129
139
|
},
|
|
130
140
|
label: ({ t })=>t('reservation:fieldStartTimeHHmm'),
|
|
131
|
-
required: true
|
|
141
|
+
required: true,
|
|
142
|
+
validate: validateTime
|
|
132
143
|
},
|
|
133
144
|
{
|
|
134
145
|
name: 'endTime',
|
|
@@ -137,7 +148,8 @@ export function createSchedulesCollection(config) {
|
|
|
137
148
|
placeholder: '17:00'
|
|
138
149
|
},
|
|
139
150
|
label: ({ t })=>t('reservation:fieldEndTimeHHmm'),
|
|
140
|
-
required: true
|
|
151
|
+
required: true,
|
|
152
|
+
validate: validateTime
|
|
141
153
|
}
|
|
142
154
|
],
|
|
143
155
|
label: ({ t })=>t('reservation:fieldManualSlots')
|
|
@@ -175,6 +187,39 @@ export function createSchedulesCollection(config) {
|
|
|
175
187
|
label: ({ t })=>t('reservation:fieldActive')
|
|
176
188
|
}
|
|
177
189
|
],
|
|
190
|
+
hooks: {
|
|
191
|
+
beforeValidate: [
|
|
192
|
+
({ data })=>{
|
|
193
|
+
const slots = data?.recurringSlots ?? [];
|
|
194
|
+
for (const slot of slots){
|
|
195
|
+
if (slot.startTime && slot.endTime && slot.startTime >= slot.endTime) {
|
|
196
|
+
throw new ValidationError({
|
|
197
|
+
errors: [
|
|
198
|
+
{
|
|
199
|
+
message: 'endTime must be after startTime',
|
|
200
|
+
path: 'recurringSlots'
|
|
201
|
+
}
|
|
202
|
+
]
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const manual = data?.manualSlots ?? [];
|
|
207
|
+
for (const slot of manual){
|
|
208
|
+
if (slot.startTime && slot.endTime && slot.startTime >= slot.endTime) {
|
|
209
|
+
throw new ValidationError({
|
|
210
|
+
errors: [
|
|
211
|
+
{
|
|
212
|
+
message: 'endTime must be after startTime',
|
|
213
|
+
path: 'manualSlots'
|
|
214
|
+
}
|
|
215
|
+
]
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return data;
|
|
220
|
+
}
|
|
221
|
+
]
|
|
222
|
+
},
|
|
178
223
|
labels: {
|
|
179
224
|
plural: ({ t })=>t('reservation:collectionSchedules'),
|
|
180
225
|
singular: ({ t })=>t('reservation:collectionSchedules')
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/collections/Schedules.ts"],"sourcesContent":["import type { CollectionConfig, CollectionSlug } from 'payload'\n\nimport type { PluginT } from '../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { makeScheduleOwnerAccess } from '../utilities/ownerAccess.js'\n\nexport function createSchedulesCollection(\n config: ResolvedReservationPluginConfig,\n): CollectionConfig {\n const rom = config.resourceOwnerMode\n const access =\n config.access.schedules ?? (rom ? makeScheduleOwnerAccess(rom) : {})\n\n return {\n slug: config.slugs.schedules,\n access,\n admin: {\n group: config.adminGroup,\n useAsTitle: 'name',\n },\n fields: [\n {\n name: 'name',\n type: 'text',\n label: ({ t }) => (t as PluginT)('reservation:fieldName'),\n required: true,\n },\n {\n name: 'resource',\n type: 'relationship',\n label: ({ t }) => (t as PluginT)('reservation:fieldResource'),\n relationTo: config.slugs.resources as unknown as CollectionSlug,\n required: true,\n },\n {\n name: 'scheduleType',\n type: 'select',\n defaultValue: 'recurring',\n label: ({ t }) => (t as PluginT)('reservation:fieldScheduleType'),\n options: [\n {\n label: ({ t }) => (t as PluginT)('reservation:scheduleTypeRecurring'),\n value: 'recurring',\n },\n {\n label: ({ t }) => (t as PluginT)('reservation:scheduleTypeManual'),\n value: 'manual',\n },\n ],\n },\n {\n name: 'recurringSlots',\n type: 'array',\n admin: {\n condition: (_, siblingData) => siblingData?.scheduleType === 'recurring',\n },\n fields: [\n {\n name: 'day',\n type: 'select',\n label: ({ t }) => (t as PluginT)('reservation:fieldDay'),\n options: [\n { label: ({ t }) => (t as PluginT)('reservation:dayMonday'), value: 'mon' },\n { label: ({ t }) => (t as PluginT)('reservation:dayTuesday'), value: 'tue' },\n { label: ({ t }) => (t as PluginT)('reservation:dayWednesday'), value: 'wed' },\n { label: ({ t }) => (t as PluginT)('reservation:dayThursday'), value: 'thu' },\n { label: ({ t }) => (t as PluginT)('reservation:dayFriday'), value: 'fri' },\n { label: ({ t }) => (t as PluginT)('reservation:daySaturday'), value: 'sat' },\n { label: ({ t }) => (t as PluginT)('reservation:daySunday'), value: 'sun' },\n ],\n required: true,\n },\n {\n name: 'startTime',\n type: 'text',\n admin: {\n placeholder: '09:00',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldStartTimeHHmm'),\n required: true,\n },\n {\n name: 'endTime',\n type: 'text',\n admin: {\n placeholder: '17:00',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldEndTimeHHmm'),\n required: true,\n },\n ],\n label: ({ t }) => (t as PluginT)('reservation:fieldRecurringSlots'),\n },\n {\n name: 'manualSlots',\n type: 'array',\n admin: {\n condition: (_, siblingData) => siblingData?.scheduleType === 'manual',\n },\n fields: [\n {\n name: 'date',\n type: 'date',\n admin: {\n date: {\n pickerAppearance: 'dayOnly',\n },\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldDate'),\n required: true,\n },\n {\n name: 'startTime',\n type: 'text',\n admin: {\n placeholder: '09:00',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldStartTimeHHmm'),\n required: true,\n },\n {\n name: 'endTime',\n type: 'text',\n admin: {\n placeholder: '17:00',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldEndTimeHHmm'),\n required: true,\n },\n ],\n label: ({ t }) => (t as PluginT)('reservation:fieldManualSlots'),\n },\n {\n name: 'exceptions',\n type: 'array',\n fields: [\n {\n name: 'date',\n type: 'date',\n admin: {\n date: {\n pickerAppearance: 'dayOnly',\n },\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldDate'),\n required: true,\n },\n {\n name: 'reason',\n type: 'text',\n label: ({ t }) => (t as PluginT)('reservation:fieldReason'),\n },\n ],\n label: ({ t }) => (t as PluginT)('reservation:fieldExceptions'),\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:collectionSchedules'),\n singular: ({ t }) => (t as PluginT)('reservation:collectionSchedules'),\n },\n }\n}\n"],"names":["makeScheduleOwnerAccess","createSchedulesCollection","config","rom","resourceOwnerMode","access","schedules","slug","slugs","admin","group","adminGroup","useAsTitle","fields","name","type","label","t","required","relationTo","resources","defaultValue","options","value","condition","_","siblingData","scheduleType","placeholder","date","pickerAppearance","position","labels","plural","singular"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/collections/Schedules.ts"],"sourcesContent":["import type { CollectionConfig, CollectionSlug } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { makeScheduleOwnerAccess } from '../utilities/ownerAccess.js'\n\nconst TIME_REGEX = /^(?:[01]\\d|2[0-3]):[0-5]\\d$/\n\nconst validateTime = (value: null | string | undefined): string | true => {\n if (!value) { return true } // required handles emptiness\n return TIME_REGEX.test(value) || 'Invalid time format. Use HH:mm (e.g., 09:00, 17:30)'\n}\n\nexport function createSchedulesCollection(\n config: ResolvedReservationPluginConfig,\n): CollectionConfig {\n const rom = config.resourceOwnerMode\n const access =\n config.access.schedules ?? (rom ? makeScheduleOwnerAccess(rom) : {})\n\n return {\n slug: config.slugs.schedules,\n access,\n admin: {\n group: config.adminGroup,\n useAsTitle: 'name',\n },\n fields: [\n {\n name: 'name',\n type: 'text',\n label: ({ t }) => (t as PluginT)('reservation:fieldName'),\n required: true,\n },\n {\n name: 'resource',\n type: 'relationship',\n label: ({ t }) => (t as PluginT)('reservation:fieldResource'),\n relationTo: config.slugs.resources as unknown as CollectionSlug,\n required: true,\n },\n {\n name: 'scheduleType',\n type: 'select',\n defaultValue: 'recurring',\n label: ({ t }) => (t as PluginT)('reservation:fieldScheduleType'),\n options: [\n {\n label: ({ t }) => (t as PluginT)('reservation:scheduleTypeRecurring'),\n value: 'recurring',\n },\n {\n label: ({ t }) => (t as PluginT)('reservation:scheduleTypeManual'),\n value: 'manual',\n },\n ],\n },\n {\n name: 'recurringSlots',\n type: 'array',\n admin: {\n condition: (_, siblingData) => siblingData?.scheduleType === 'recurring',\n },\n fields: [\n {\n name: 'day',\n type: 'select',\n label: ({ t }) => (t as PluginT)('reservation:fieldDay'),\n options: [\n { label: ({ t }) => (t as PluginT)('reservation:dayMonday'), value: 'mon' },\n { label: ({ t }) => (t as PluginT)('reservation:dayTuesday'), value: 'tue' },\n { label: ({ t }) => (t as PluginT)('reservation:dayWednesday'), value: 'wed' },\n { label: ({ t }) => (t as PluginT)('reservation:dayThursday'), value: 'thu' },\n { label: ({ t }) => (t as PluginT)('reservation:dayFriday'), value: 'fri' },\n { label: ({ t }) => (t as PluginT)('reservation:daySaturday'), value: 'sat' },\n { label: ({ t }) => (t as PluginT)('reservation:daySunday'), value: 'sun' },\n ],\n required: true,\n },\n {\n name: 'startTime',\n type: 'text',\n admin: {\n placeholder: '09:00',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldStartTimeHHmm'),\n required: true,\n validate: validateTime,\n },\n {\n name: 'endTime',\n type: 'text',\n admin: {\n placeholder: '17:00',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldEndTimeHHmm'),\n required: true,\n validate: validateTime,\n },\n ],\n label: ({ t }) => (t as PluginT)('reservation:fieldRecurringSlots'),\n },\n {\n name: 'manualSlots',\n type: 'array',\n admin: {\n condition: (_, siblingData) => siblingData?.scheduleType === 'manual',\n },\n fields: [\n {\n name: 'date',\n type: 'date',\n admin: {\n date: {\n pickerAppearance: 'dayOnly',\n },\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldDate'),\n required: true,\n },\n {\n name: 'startTime',\n type: 'text',\n admin: {\n placeholder: '09:00',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldStartTimeHHmm'),\n required: true,\n validate: validateTime,\n },\n {\n name: 'endTime',\n type: 'text',\n admin: {\n placeholder: '17:00',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldEndTimeHHmm'),\n required: true,\n validate: validateTime,\n },\n ],\n label: ({ t }) => (t as PluginT)('reservation:fieldManualSlots'),\n },\n {\n name: 'exceptions',\n type: 'array',\n fields: [\n {\n name: 'date',\n type: 'date',\n admin: {\n date: {\n pickerAppearance: 'dayOnly',\n },\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldDate'),\n required: true,\n },\n {\n name: 'reason',\n type: 'text',\n label: ({ t }) => (t as PluginT)('reservation:fieldReason'),\n },\n ],\n label: ({ t }) => (t as PluginT)('reservation:fieldExceptions'),\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 hooks: {\n beforeValidate: [\n ({ data }) => {\n const slots = (data?.recurringSlots as Array<{ endTime?: string; startTime?: string }>) ?? []\n for (const slot of slots) {\n if (slot.startTime && slot.endTime && slot.startTime >= slot.endTime) {\n throw new ValidationError({\n errors: [{ message: 'endTime must be after startTime', path: 'recurringSlots' }],\n })\n }\n }\n const manual = (data?.manualSlots as Array<{ endTime?: string; startTime?: string }>) ?? []\n for (const slot of manual) {\n if (slot.startTime && slot.endTime && slot.startTime >= slot.endTime) {\n throw new ValidationError({\n errors: [{ message: 'endTime must be after startTime', path: 'manualSlots' }],\n })\n }\n }\n return data\n },\n ],\n },\n labels: {\n plural: ({ t }) => (t as PluginT)('reservation:collectionSchedules'),\n singular: ({ t }) => (t as PluginT)('reservation:collectionSchedules'),\n },\n }\n}\n"],"names":["ValidationError","makeScheduleOwnerAccess","TIME_REGEX","validateTime","value","test","createSchedulesCollection","config","rom","resourceOwnerMode","access","schedules","slug","slugs","admin","group","adminGroup","useAsTitle","fields","name","type","label","t","required","relationTo","resources","defaultValue","options","condition","_","siblingData","scheduleType","placeholder","validate","date","pickerAppearance","position","hooks","beforeValidate","data","slots","recurringSlots","slot","startTime","endTime","errors","message","path","manual","manualSlots","labels","plural","singular"],"mappings":"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"}
|
|
@@ -452,3 +452,38 @@
|
|
|
452
452
|
color: var(--theme-elevation-400);
|
|
453
453
|
font-size: 0.9375rem;
|
|
454
454
|
}
|
|
455
|
+
|
|
456
|
+
/* Resource filter */
|
|
457
|
+
.filterBar {
|
|
458
|
+
display: flex;
|
|
459
|
+
align-items: center;
|
|
460
|
+
gap: 8px;
|
|
461
|
+
margin-bottom: 12px;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.resourceFilter {
|
|
465
|
+
appearance: none;
|
|
466
|
+
background: var(--theme-bg);
|
|
467
|
+
border: none;
|
|
468
|
+
border-radius: 3px;
|
|
469
|
+
box-shadow: inset 0 0 0 1px var(--theme-elevation-250);
|
|
470
|
+
padding: 6px 28px 6px 10px;
|
|
471
|
+
cursor: pointer;
|
|
472
|
+
font-family: inherit;
|
|
473
|
+
font-size: 0.8125rem;
|
|
474
|
+
color: var(--theme-text);
|
|
475
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23666'/%3E%3C/svg%3E");
|
|
476
|
+
background-repeat: no-repeat;
|
|
477
|
+
background-position: right 10px center;
|
|
478
|
+
min-width: 160px;
|
|
479
|
+
transition: box-shadow 100ms cubic-bezier(0, 0.2, 0.2, 1);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.resourceFilter:hover {
|
|
483
|
+
box-shadow: inset 0 0 0 1px var(--theme-elevation-400);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.resourceFilter:focus-visible {
|
|
487
|
+
outline: 2px solid var(--theme-text);
|
|
488
|
+
outline-offset: 2px;
|
|
489
|
+
}
|
|
@@ -100,6 +100,9 @@ export const CalendarView = ()=>{
|
|
|
100
100
|
const [loading, setLoading] = useState(true);
|
|
101
101
|
const [drawerDocId, setDrawerDocId] = useState(null);
|
|
102
102
|
const [initialData, setInitialData] = useState(undefined);
|
|
103
|
+
// Resource filter state
|
|
104
|
+
const [resources, setResources] = useState([]);
|
|
105
|
+
const [selectedResourceId, setSelectedResourceId] = useState('');
|
|
103
106
|
// Pending tab state
|
|
104
107
|
const [pendingReservations, setPendingReservations] = useState([]);
|
|
105
108
|
const [pendingCount, setPendingCount] = useState(0);
|
|
@@ -117,6 +120,35 @@ export const CalendarView = ()=>{
|
|
|
117
120
|
openDrawer();
|
|
118
121
|
}
|
|
119
122
|
});
|
|
123
|
+
// Fetch active resources for filter dropdown
|
|
124
|
+
useEffect(()=>{
|
|
125
|
+
const fetchResources = async ()=>{
|
|
126
|
+
try {
|
|
127
|
+
const resourceSlug = slugs?.resources ?? 'resources';
|
|
128
|
+
const params = new URLSearchParams({
|
|
129
|
+
depth: '0',
|
|
130
|
+
limit: '100',
|
|
131
|
+
sort: 'name',
|
|
132
|
+
'where[active][equals]': 'true'
|
|
133
|
+
});
|
|
134
|
+
const url = `${config.serverURL ?? ''}${config.routes.api}/${resourceSlug}?${params}`;
|
|
135
|
+
const response = await fetch(url);
|
|
136
|
+
const result = await response.json();
|
|
137
|
+
const docs = result.docs ?? [];
|
|
138
|
+
setResources(docs.map((d)=>({
|
|
139
|
+
id: d.id,
|
|
140
|
+
name: d.name ?? ''
|
|
141
|
+
})));
|
|
142
|
+
} catch {
|
|
143
|
+
setResources([]);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
void fetchResources();
|
|
147
|
+
}, [
|
|
148
|
+
config.routes.api,
|
|
149
|
+
config.serverURL,
|
|
150
|
+
slugs?.resources
|
|
151
|
+
]);
|
|
120
152
|
const { rangeEnd, rangeStart } = useMemo(()=>{
|
|
121
153
|
const start = new Date(currentDate);
|
|
122
154
|
const end = new Date(currentDate);
|
|
@@ -216,7 +248,36 @@ export const CalendarView = ()=>{
|
|
|
216
248
|
viewMode,
|
|
217
249
|
fetchPendingReservations
|
|
218
250
|
]);
|
|
219
|
-
//
|
|
251
|
+
// Client-side resource filtering
|
|
252
|
+
const matchesResourceFilter = useCallback((r)=>{
|
|
253
|
+
if (!selectedResourceId) {
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
// Check top-level resource
|
|
257
|
+
const topId = typeof r.resource === 'string' ? r.resource : r.resource?.id;
|
|
258
|
+
if (topId === selectedResourceId) {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
// Check items array for multi-resource bookings
|
|
262
|
+
if (r.items && r.items.length > 0) {
|
|
263
|
+
return r.items.some((item)=>{
|
|
264
|
+
const itemId = typeof item.resource === 'string' ? item.resource : item.resource?.id;
|
|
265
|
+
return itemId === selectedResourceId;
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return false;
|
|
269
|
+
}, [
|
|
270
|
+
selectedResourceId
|
|
271
|
+
]);
|
|
272
|
+
const filteredReservations = useMemo(()=>reservations.filter(matchesResourceFilter), [
|
|
273
|
+
reservations,
|
|
274
|
+
matchesResourceFilter
|
|
275
|
+
]);
|
|
276
|
+
const filteredPendingReservations = useMemo(()=>pendingReservations.filter(matchesResourceFilter), [
|
|
277
|
+
pendingReservations,
|
|
278
|
+
matchesResourceFilter
|
|
279
|
+
]);
|
|
280
|
+
// Clear selection when leaving pending view or changing resource filter
|
|
220
281
|
useEffect(()=>{
|
|
221
282
|
if (viewMode !== 'pending') {
|
|
222
283
|
setSelectedIds(new Set());
|
|
@@ -225,6 +286,11 @@ export const CalendarView = ()=>{
|
|
|
225
286
|
}, [
|
|
226
287
|
viewMode
|
|
227
288
|
]);
|
|
289
|
+
useEffect(()=>{
|
|
290
|
+
setSelectedIds(new Set());
|
|
291
|
+
}, [
|
|
292
|
+
selectedResourceId
|
|
293
|
+
]);
|
|
228
294
|
// Auto-clear feedback toast
|
|
229
295
|
useEffect(()=>{
|
|
230
296
|
if (!actionFeedback) {
|
|
@@ -563,7 +629,7 @@ export const CalendarView = ()=>{
|
|
|
563
629
|
const dayStr = `${day.getFullYear()}-${day.getMonth()}-${day.getDate()}`;
|
|
564
630
|
const isToday = dayStr === todayStr;
|
|
565
631
|
const isOtherMonth = day.getMonth() !== currentDate.getMonth();
|
|
566
|
-
const dayReservations =
|
|
632
|
+
const dayReservations = filteredReservations.filter((r)=>{
|
|
567
633
|
const rDate = new Date(r.startTime);
|
|
568
634
|
return rDate.getFullYear() === day.getFullYear() && rDate.getMonth() === day.getMonth() && rDate.getDate() === day.getDate();
|
|
569
635
|
});
|
|
@@ -629,7 +695,7 @@ export const CalendarView = ()=>{
|
|
|
629
695
|
]
|
|
630
696
|
}),
|
|
631
697
|
weekDays.map((day, di)=>{
|
|
632
|
-
const cellReservations =
|
|
698
|
+
const cellReservations = filteredReservations.filter((r)=>{
|
|
633
699
|
const rDate = new Date(r.startTime);
|
|
634
700
|
return rDate.getFullYear() === day.getFullYear() && rDate.getMonth() === day.getMonth() && rDate.getDate() === day.getDate() && rDate.getHours() === hour;
|
|
635
701
|
});
|
|
@@ -664,7 +730,7 @@ export const CalendarView = ()=>{
|
|
|
664
730
|
return /*#__PURE__*/ _jsx("div", {
|
|
665
731
|
className: styles.dayView,
|
|
666
732
|
children: hours.map((hour)=>{
|
|
667
|
-
const hourReservations =
|
|
733
|
+
const hourReservations = filteredReservations.filter((r)=>{
|
|
668
734
|
const rDate = new Date(r.startTime);
|
|
669
735
|
return rDate.getFullYear() === currentDate.getFullYear() && rDate.getMonth() === currentDate.getMonth() && rDate.getDate() === currentDate.getDate() && rDate.getHours() === hour;
|
|
670
736
|
});
|
|
@@ -701,18 +767,18 @@ export const CalendarView = ()=>{
|
|
|
701
767
|
});
|
|
702
768
|
};
|
|
703
769
|
const renderPendingView = ()=>{
|
|
704
|
-
if (
|
|
770
|
+
if (filteredPendingReservations.length === 0) {
|
|
705
771
|
return /*#__PURE__*/ _jsx("div", {
|
|
706
772
|
className: styles.pendingEmpty,
|
|
707
773
|
children: t('reservation:pendingEmpty')
|
|
708
774
|
});
|
|
709
775
|
}
|
|
710
|
-
const allSelected =
|
|
776
|
+
const allSelected = filteredPendingReservations.length > 0 && filteredPendingReservations.every((r)=>selectedIds.has(r.id));
|
|
711
777
|
const toggleSelectAll = ()=>{
|
|
712
778
|
if (allSelected) {
|
|
713
779
|
setSelectedIds(new Set());
|
|
714
780
|
} else {
|
|
715
|
-
setSelectedIds(new Set(
|
|
781
|
+
setSelectedIds(new Set(filteredPendingReservations.map((r)=>r.id)));
|
|
716
782
|
}
|
|
717
783
|
};
|
|
718
784
|
const toggleSelect = (id)=>{
|
|
@@ -801,7 +867,7 @@ export const CalendarView = ()=>{
|
|
|
801
867
|
})
|
|
802
868
|
}),
|
|
803
869
|
/*#__PURE__*/ _jsx("tbody", {
|
|
804
|
-
children:
|
|
870
|
+
children: filteredPendingReservations.map((r)=>{
|
|
805
871
|
const isConfirming = confirmingIds.has(r.id);
|
|
806
872
|
// Show all resources from items array if present, else top-level resource
|
|
807
873
|
const resourceDisplay = getResourceNames(r).join(', ') || t('reservation:calendarUnknownResource');
|
|
@@ -986,7 +1052,7 @@ export const CalendarView = ()=>{
|
|
|
986
1052
|
label,
|
|
987
1053
|
key === 'pending' && pendingCount > 0 && /*#__PURE__*/ _jsx("span", {
|
|
988
1054
|
className: styles.pendingBadge,
|
|
989
|
-
children: pendingCount
|
|
1055
|
+
children: selectedResourceId ? filteredPendingReservations.length : pendingCount
|
|
990
1056
|
})
|
|
991
1057
|
]
|
|
992
1058
|
}, key))
|
|
@@ -995,6 +1061,25 @@ export const CalendarView = ()=>{
|
|
|
995
1061
|
]
|
|
996
1062
|
}),
|
|
997
1063
|
viewMode !== 'pending' && renderStatusLegend(),
|
|
1064
|
+
resources.length > 1 && /*#__PURE__*/ _jsx("div", {
|
|
1065
|
+
className: styles.filterBar,
|
|
1066
|
+
children: /*#__PURE__*/ _jsxs("select", {
|
|
1067
|
+
"aria-label": t('reservation:filterByResource'),
|
|
1068
|
+
className: styles.resourceFilter,
|
|
1069
|
+
onChange: (e)=>setSelectedResourceId(e.target.value),
|
|
1070
|
+
value: selectedResourceId,
|
|
1071
|
+
children: [
|
|
1072
|
+
/*#__PURE__*/ _jsx("option", {
|
|
1073
|
+
value: "",
|
|
1074
|
+
children: t('reservation:filterAllResources')
|
|
1075
|
+
}),
|
|
1076
|
+
resources.map((r)=>/*#__PURE__*/ _jsx("option", {
|
|
1077
|
+
value: r.id,
|
|
1078
|
+
children: r.name
|
|
1079
|
+
}, r.id))
|
|
1080
|
+
]
|
|
1081
|
+
})
|
|
1082
|
+
}),
|
|
998
1083
|
loading && viewMode !== 'pending' ? /*#__PURE__*/ _jsx("div", {
|
|
999
1084
|
className: styles.loading,
|
|
1000
1085
|
children: t('reservation:calendarLoading')
|