payload-reserve 2.1.1 → 2.2.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 +58 -1
- package/dist/components/CalendarView/CalendarView.module.css +11 -0
- package/dist/components/CalendarView/LaneTimelineView.js +3 -1
- package/dist/components/CalendarView/LaneTimelineView.js.map +1 -1
- package/dist/components/CalendarView/index.js +15 -0
- package/dist/components/CalendarView/index.js.map +1 -1
- package/dist/defaults.js +1 -0
- package/dist/defaults.js.map +1 -1
- package/dist/endpoints/checkAvailability.js +1 -0
- package/dist/endpoints/checkAvailability.js.map +1 -1
- package/dist/endpoints/getSlots.js +1 -0
- package/dist/endpoints/getSlots.js.map +1 -1
- package/dist/endpoints/resourceAvailability.d.ts +6 -2
- package/dist/endpoints/resourceAvailability.js +19 -1
- package/dist/endpoints/resourceAvailability.js.map +1 -1
- package/dist/hooks/reservations/validateConflicts.js +1 -0
- package/dist/hooks/reservations/validateConflicts.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/services/AvailabilityService.d.ts +9 -2
- package/dist/services/AvailabilityService.js +26 -3
- package/dist/services/AvailabilityService.js.map +1 -1
- package/dist/translations/ar.json +1 -0
- package/dist/translations/de.json +1 -0
- package/dist/translations/en.json +1 -0
- package/dist/translations/es.json +1 -0
- package/dist/translations/fa.json +1 -0
- package/dist/translations/fr.json +1 -0
- package/dist/translations/hi.json +1 -0
- package/dist/translations/id.json +1 -0
- package/dist/translations/pl.json +1 -0
- package/dist/translations/ru.json +1 -0
- package/dist/translations/tr.json +1 -0
- package/dist/translations/zh.json +1 -0
- package/dist/types.d.ts +27 -0
- package/dist/types.js.map +1 -1
- package/dist/utilities/computeSlotStates.d.ts +2 -1
- package/dist/utilities/computeSlotStates.js +5 -1
- package/dist/utilities/computeSlotStates.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,6 +37,7 @@ Designed for salons, clinics, hotels, restaurants, event venues, and any busines
|
|
|
37
37
|
- **Recurring and Manual Schedules** — Weekly patterns with exception dates, or specific one-off dates
|
|
38
38
|
- **12 Bundled Languages** — Every admin string is translatable; ships with English, French, German, Spanish, Russian, Polish, Turkish, Arabic, Simplified Chinese, Indonesian, Persian/Farsi, and Hindi. Override any string or add your own language
|
|
39
39
|
- **Localization Support** — Collection field *content* can be localized when Payload localization is enabled (separate from the admin-UI language above)
|
|
40
|
+
- **External Busy** — Optional `getExternalBusy` resolver folds busy time from calendar sync, legacy booking systems, or ops tooling into availability, with distinct calendar display and fail-open error handling
|
|
40
41
|
- **Type-Safe** — Full TypeScript support with exported types
|
|
41
42
|
|
|
42
43
|
---
|
|
@@ -311,6 +312,62 @@ The `services` relationship on Resources is now optional. This lets a freshly pr
|
|
|
311
312
|
|
|
312
313
|
---
|
|
313
314
|
|
|
315
|
+
## External Busy (Calendar Sync & Other Sources)
|
|
316
|
+
|
|
317
|
+
`getExternalBusy` lets your app fold busy time that payload-reserve doesn't manage — an external calendar, a legacy booking system, ops tooling — into a resource's availability. The plugin never talks to any calendar API itself; it calls a resolver you provide once per candidate window and treats whatever it returns as busy time for that resource.
|
|
318
|
+
|
|
319
|
+
Use it to:
|
|
320
|
+
|
|
321
|
+
- Import busy time from two-way calendar sync (Google Calendar, Outlook, iCal feeds)
|
|
322
|
+
- Respect bookings made in another/legacy booking system during a migration
|
|
323
|
+
- Block maintenance/cleaning windows tracked in an ops system
|
|
324
|
+
- Reflect staff leave recorded in an HR system that isn't modeled as plugin time-off
|
|
325
|
+
|
|
326
|
+
A realistic setup keeps a local collection in sync (via webhooks or a cron job) and has the resolver run a single indexed query against it — the resolver itself never calls a remote API:
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
import type { ExternalBusyInterval, GetExternalBusy } from 'payload-reserve'
|
|
330
|
+
|
|
331
|
+
// A sync job (webhook handler or scheduled cron task) keeps this collection's
|
|
332
|
+
// rows current from whatever external source you're integrating — a
|
|
333
|
+
// Google/Outlook calendar sync, a legacy system export, an ops tool, etc.
|
|
334
|
+
// The resolver below never calls out itself; it only reads what the sync
|
|
335
|
+
// job already wrote, so it stays a single cheap, indexed query.
|
|
336
|
+
const getExternalBusy: GetExternalBusy = async ({ end, req, resourceId, start }) => {
|
|
337
|
+
const result = await req.payload.find({
|
|
338
|
+
collection: 'external-busy', // your locally-synced collection
|
|
339
|
+
where: {
|
|
340
|
+
and: [
|
|
341
|
+
{ resource: { equals: resourceId } },
|
|
342
|
+
{ start: { less_than: end.toISOString() } },
|
|
343
|
+
{ end: { greater_than: start.toISOString() } },
|
|
344
|
+
],
|
|
345
|
+
},
|
|
346
|
+
depth: 0,
|
|
347
|
+
limit: 0,
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
return result.docs.map(
|
|
351
|
+
(doc): ExternalBusyInterval => ({
|
|
352
|
+
start: doc.start,
|
|
353
|
+
end: doc.end,
|
|
354
|
+
label: doc.label,
|
|
355
|
+
}),
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
payloadReserve({
|
|
360
|
+
getExternalBusy,
|
|
361
|
+
})
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
- **Enforcement:** any booking (hooks, endpoints, slot listings) overlapping an interval is unavailable. An interval blocks the WHOLE resource (all `quantity` units) — external calendars aren't unit-aware.
|
|
365
|
+
- **Display:** the `resource-availability` endpoint returns the intervals as a separate `external[]` array (not mixed into `busy`), and the calendar renders them as a distinct, non-clickable hatched "External event" slot.
|
|
366
|
+
- **Fail-open:** if the resolver throws, the plugin treats it as no external busy for that call — a sync failure never blocks a real booking or breaks the grid.
|
|
367
|
+
- **Performance:** the resolver is called once per candidate window during slot computation, so keep it cheap — a local table lookup or a per-request cache, never a remote API call per invocation.
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
314
371
|
## Internationalization
|
|
315
372
|
|
|
316
373
|
Every admin string the plugin renders — field labels, descriptions, select options, calendar/dashboard components, and validation errors — is translatable. The plugin ships **12 languages**: English, French (`fr`), German (`de`), Spanish (`es`), Russian (`ru`), Polish (`pl`), Turkish (`tr`), Arabic (`ar`), Simplified Chinese (`zh`), Indonesian (`id`), Persian/Farsi (`fa`), and Hindi (`hi`). All but Hindi ship in Payload core and appear in the admin language switcher automatically.
|
|
@@ -340,7 +397,7 @@ This is separate from Payload **field localization** (localizing the *content* o
|
|
|
340
397
|
| Topic | Contents |
|
|
341
398
|
|-------|----------|
|
|
342
399
|
| [Getting Started](https://github.com/elghaied/payload-reserve/blob/main/docs/getting-started.md) | Installation, quick start, what gets created |
|
|
343
|
-
| [Configuration](https://github.com/elghaied/payload-reserve/blob/main/docs/configuration.md) | All plugin options with types and defaults, including `resourceOwnerMode` |
|
|
400
|
+
| [Configuration](https://github.com/elghaied/payload-reserve/blob/main/docs/configuration.md) | All plugin options with types and defaults, including `resourceOwnerMode` and `getExternalBusy` |
|
|
344
401
|
| [Collections](https://github.com/elghaied/payload-reserve/blob/main/docs/collections.md) | Services, Resources, Schedules, Customers, Reservations schemas |
|
|
345
402
|
| [Status Machine](https://github.com/elghaied/payload-reserve/blob/main/docs/status-machine.md) | Default flow, custom machines, business logic hooks, escape hatch |
|
|
346
403
|
| [Booking Features](https://github.com/elghaied/payload-reserve/blob/main/docs/booking-features.md) | Duration types, multi-resource bookings, capacity modes |
|
|
@@ -520,6 +520,17 @@
|
|
|
520
520
|
cursor: not-allowed;
|
|
521
521
|
}
|
|
522
522
|
|
|
523
|
+
.slotExternal {
|
|
524
|
+
background: repeating-linear-gradient(
|
|
525
|
+
45deg,
|
|
526
|
+
var(--theme-elevation-100),
|
|
527
|
+
var(--theme-elevation-100) 6px,
|
|
528
|
+
var(--theme-error-50) 6px,
|
|
529
|
+
var(--theme-error-50) 12px
|
|
530
|
+
);
|
|
531
|
+
cursor: not-allowed;
|
|
532
|
+
}
|
|
533
|
+
|
|
523
534
|
.slotFree {
|
|
524
535
|
cursor: pointer;
|
|
525
536
|
}
|
|
@@ -7,6 +7,7 @@ import { getDayKeyInTimezone } from '../../utilities/timezoneUtils.js';
|
|
|
7
7
|
import styles from './CalendarView.module.css';
|
|
8
8
|
import { useResourceAvailability } from './useResourceAvailability.js';
|
|
9
9
|
const SLOT_STATE_KEYS = {
|
|
10
|
+
external: 'reservation:slotExternal',
|
|
10
11
|
free: 'reservation:slotFree',
|
|
11
12
|
full: 'reservation:slotFull',
|
|
12
13
|
'off-shift': 'reservation:slotOffShift',
|
|
@@ -27,6 +28,7 @@ function Lane({ apiBase, day, endHour, onBook, resource, startHour, timeZone })
|
|
|
27
28
|
capacityMode: data.capacityMode,
|
|
28
29
|
dayEnd,
|
|
29
30
|
dayStart,
|
|
31
|
+
external: data.external,
|
|
30
32
|
quantity: data.quantity,
|
|
31
33
|
requiredPools: data.requiredPools,
|
|
32
34
|
shiftWindows: dayAvail.shiftWindows,
|
|
@@ -43,7 +45,7 @@ function Lane({ apiBase, day, endHour, onBook, resource, startHour, timeZone })
|
|
|
43
45
|
/*#__PURE__*/ _jsx("div", {
|
|
44
46
|
className: styles.laneTrack,
|
|
45
47
|
children: slots.map((s)=>{
|
|
46
|
-
const cls = s.state === 'off-shift' ? styles.slotOffShift : s.state === 'time-off' ? styles.slotTimeOff : s.state === 'full' ? styles.slotFull : styles.slotFree;
|
|
48
|
+
const cls = s.state === 'off-shift' ? styles.slotOffShift : s.state === 'time-off' ? styles.slotTimeOff : s.state === 'external' ? styles.slotExternal : s.state === 'full' ? styles.slotFull : styles.slotFree;
|
|
47
49
|
const isFree = s.state === 'free';
|
|
48
50
|
const slotLabel = `${s.start.toLocaleTimeString([], {
|
|
49
51
|
hour: '2-digit',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/CalendarView/LaneTimelineView.tsx"],"sourcesContent":["'use client'\nimport { useTranslation } from '@payloadcms/ui'\nimport React from 'react'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { SlotState } from '../../utilities/computeSlotStates.js'\n\nimport { computeSlotStates } from '../../utilities/computeSlotStates.js'\nimport { getDayKeyInTimezone } from '../../utilities/timezoneUtils.js'\nimport styles from './CalendarView.module.css'\nimport { useResourceAvailability } from './useResourceAvailability.js'\n\ntype LaneResource = { id: string; name: string }\n\nconst SLOT_STATE_KEYS: Record<SlotState, string> = {\n free: 'reservation:slotFree',\n full: 'reservation:slotFull',\n 'off-shift': 'reservation:slotOffShift',\n 'time-off': 'reservation:slotTimeOff',\n}\n\nfunction Lane({\n apiBase,\n day,\n endHour,\n onBook,\n resource,\n startHour,\n timeZone,\n}: {\n apiBase: string\n day: Date\n endHour: number\n onBook: (resourceId: string, startIso: string) => void\n resource: LaneResource\n startHour: number\n timeZone: string\n}) {\n const { t: _t } = useTranslation()\n const t = _t as PluginT\n\n const dayStart = new Date(day)\n dayStart.setHours(startHour, 0, 0, 0)\n const dayEnd = new Date(day)\n dayEnd.setHours(endHour, 0, 0, 0)\n\n const { data } = useResourceAvailability(apiBase, resource.id, dayStart, dayEnd)\n const isoDay = getDayKeyInTimezone(day, timeZone)\n const dayAvail = data?.days.find((d) => d.date === isoDay)\n\n const slots = dayAvail\n ? computeSlotStates({\n busy: data!.busy,\n capacityMode: data!.capacityMode,\n dayEnd,\n dayStart,\n quantity: data!.quantity,\n requiredPools: data!.requiredPools,\n shiftWindows: dayAvail.shiftWindows,\n step: 60,\n timeOff: dayAvail.timeOff,\n })\n : []\n\n return (\n <div className={styles.lane}>\n <div className={styles.laneLabel}>{resource.name}</div>\n <div className={styles.laneTrack}>\n {slots.map((s) => {\n const cls =\n s.state === 'off-shift'\n ? styles.slotOffShift\n : s.state === 'time-off'\n ? styles.slotTimeOff\n : s.state === '
|
|
1
|
+
{"version":3,"sources":["../../../src/components/CalendarView/LaneTimelineView.tsx"],"sourcesContent":["'use client'\nimport { useTranslation } from '@payloadcms/ui'\nimport React from 'react'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { SlotState } from '../../utilities/computeSlotStates.js'\n\nimport { computeSlotStates } from '../../utilities/computeSlotStates.js'\nimport { getDayKeyInTimezone } from '../../utilities/timezoneUtils.js'\nimport styles from './CalendarView.module.css'\nimport { useResourceAvailability } from './useResourceAvailability.js'\n\ntype LaneResource = { id: string; name: string }\n\nconst SLOT_STATE_KEYS: Record<SlotState, string> = {\n external: 'reservation:slotExternal',\n free: 'reservation:slotFree',\n full: 'reservation:slotFull',\n 'off-shift': 'reservation:slotOffShift',\n 'time-off': 'reservation:slotTimeOff',\n}\n\nfunction Lane({\n apiBase,\n day,\n endHour,\n onBook,\n resource,\n startHour,\n timeZone,\n}: {\n apiBase: string\n day: Date\n endHour: number\n onBook: (resourceId: string, startIso: string) => void\n resource: LaneResource\n startHour: number\n timeZone: string\n}) {\n const { t: _t } = useTranslation()\n const t = _t as PluginT\n\n const dayStart = new Date(day)\n dayStart.setHours(startHour, 0, 0, 0)\n const dayEnd = new Date(day)\n dayEnd.setHours(endHour, 0, 0, 0)\n\n const { data } = useResourceAvailability(apiBase, resource.id, dayStart, dayEnd)\n const isoDay = getDayKeyInTimezone(day, timeZone)\n const dayAvail = data?.days.find((d) => d.date === isoDay)\n\n const slots = dayAvail\n ? computeSlotStates({\n busy: data!.busy,\n capacityMode: data!.capacityMode,\n dayEnd,\n dayStart,\n external: data!.external,\n quantity: data!.quantity,\n requiredPools: data!.requiredPools,\n shiftWindows: dayAvail.shiftWindows,\n step: 60,\n timeOff: dayAvail.timeOff,\n })\n : []\n\n return (\n <div className={styles.lane}>\n <div className={styles.laneLabel}>{resource.name}</div>\n <div className={styles.laneTrack}>\n {slots.map((s) => {\n const cls =\n s.state === 'off-shift'\n ? styles.slotOffShift\n : s.state === 'time-off'\n ? styles.slotTimeOff\n : s.state === 'external'\n ? styles.slotExternal\n : s.state === 'full'\n ? styles.slotFull\n : styles.slotFree\n const isFree = s.state === 'free'\n const slotLabel = `${s.start.toLocaleTimeString([], {\n hour: '2-digit',\n minute: '2-digit',\n timeZone,\n })} — ${t(SLOT_STATE_KEYS[s.state])}`\n return isFree ? (\n <div\n aria-label={slotLabel}\n className={`${styles.laneCell} ${cls}`}\n key={s.start.toISOString()}\n onClick={() => onBook(resource.id, s.start.toISOString())}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n onBook(resource.id, s.start.toISOString())\n }\n }}\n role=\"button\"\n tabIndex={0}\n title={slotLabel}\n />\n ) : (\n <div\n className={`${styles.laneCell} ${cls}`}\n key={s.start.toISOString()}\n title={slotLabel}\n />\n )\n })}\n </div>\n </div>\n )\n}\n\nexport function LaneTimelineView({\n apiBase,\n day,\n endHour,\n onBook,\n resources,\n startHour,\n timeZone,\n}: {\n apiBase: string\n day: Date\n endHour: number\n onBook: (resourceId: string, startIso: string) => void\n resources: LaneResource[]\n startHour: number\n timeZone: string\n}) {\n const { t: _t } = useTranslation()\n const t = _t as PluginT\n if (resources.length === 0) {\n return <p className={styles.hint}>{t('reservation:laneNoResources')}</p>\n }\n const hours = Array.from({ length: endHour - startHour }, (_, i) => startHour + i)\n return (\n <div className={styles.lanes}>\n <div className={styles.laneHeader}>\n <div className={styles.laneLabel} />\n <div className={styles.laneTrack}>\n {hours.map((h) => (\n <div className={styles.laneTime} key={h}>\n {String(h).padStart(2, '0')}:00\n </div>\n ))}\n </div>\n </div>\n {resources.map((r) => (\n <Lane\n apiBase={apiBase}\n day={day}\n endHour={endHour}\n key={r.id}\n onBook={onBook}\n resource={r}\n startHour={startHour}\n timeZone={timeZone}\n />\n ))}\n </div>\n )\n}\n"],"names":["useTranslation","React","computeSlotStates","getDayKeyInTimezone","styles","useResourceAvailability","SLOT_STATE_KEYS","external","free","full","Lane","apiBase","day","endHour","onBook","resource","startHour","timeZone","t","_t","dayStart","Date","setHours","dayEnd","data","id","isoDay","dayAvail","days","find","d","date","slots","busy","capacityMode","quantity","requiredPools","shiftWindows","step","timeOff","div","className","lane","laneLabel","name","laneTrack","map","s","cls","state","slotOffShift","slotTimeOff","slotExternal","slotFull","slotFree","isFree","slotLabel","start","toLocaleTimeString","hour","minute","aria-label","laneCell","onClick","toISOString","onKeyDown","e","key","preventDefault","role","tabIndex","title","LaneTimelineView","resources","length","p","hint","hours","Array","from","_","i","lanes","laneHeader","h","laneTime","String","padStart","r"],"mappings":"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"}
|
|
@@ -219,6 +219,13 @@ export const CalendarView = ()=>{
|
|
|
219
219
|
id: d.id,
|
|
220
220
|
name: d.name ?? ''
|
|
221
221
|
})));
|
|
222
|
+
// Single-resource installs have no resource filter (it renders only for 2+),
|
|
223
|
+
// so nothing could ever select the resource and the week/day availability
|
|
224
|
+
// shading (off-shift / full / time-off / external) never loaded. Auto-select
|
|
225
|
+
// the sole resource; with 2+ the user chooses via the filter as before.
|
|
226
|
+
if (docs.length === 1) {
|
|
227
|
+
setSelectedResourceId((prev)=>prev || String(docs[0].id));
|
|
228
|
+
}
|
|
222
229
|
} catch {
|
|
223
230
|
setResources([]);
|
|
224
231
|
}
|
|
@@ -846,6 +853,7 @@ export const CalendarView = ()=>{
|
|
|
846
853
|
capacityMode: availability.capacityMode,
|
|
847
854
|
dayEnd,
|
|
848
855
|
dayStart,
|
|
856
|
+
external: availability.external,
|
|
849
857
|
quantity: availability.quantity,
|
|
850
858
|
requiredPools: availability.requiredPools,
|
|
851
859
|
shiftWindows: dayAvail.shiftWindows,
|
|
@@ -907,6 +915,9 @@ export const CalendarView = ()=>{
|
|
|
907
915
|
} else if (slotInfo.state === 'time-off') {
|
|
908
916
|
slotClass = styles.slotTimeOff;
|
|
909
917
|
isNonInteractive = true;
|
|
918
|
+
} else if (slotInfo.state === 'external') {
|
|
919
|
+
slotClass = styles.slotExternal;
|
|
920
|
+
isNonInteractive = true;
|
|
910
921
|
} else if (slotInfo.state === 'full') {
|
|
911
922
|
slotClass = styles.slotFull;
|
|
912
923
|
isNonInteractive = true;
|
|
@@ -981,6 +992,7 @@ export const CalendarView = ()=>{
|
|
|
981
992
|
capacityMode: availability.capacityMode,
|
|
982
993
|
dayEnd,
|
|
983
994
|
dayStart,
|
|
995
|
+
external: availability.external,
|
|
984
996
|
quantity: availability.quantity,
|
|
985
997
|
requiredPools: availability.requiredPools,
|
|
986
998
|
shiftWindows: dayAvail.shiftWindows,
|
|
@@ -1013,6 +1025,9 @@ export const CalendarView = ()=>{
|
|
|
1013
1025
|
} else if (slotInfo.state === 'time-off') {
|
|
1014
1026
|
slotClass = styles.slotTimeOff;
|
|
1015
1027
|
isNonInteractive = true;
|
|
1028
|
+
} else if (slotInfo.state === 'external') {
|
|
1029
|
+
slotClass = styles.slotExternal;
|
|
1030
|
+
isNonInteractive = true;
|
|
1016
1031
|
} else if (slotInfo.state === 'full') {
|
|
1017
1032
|
slotClass = styles.slotFull;
|
|
1018
1033
|
isNonInteractive = true;
|