payload-reserve 2.1.1 → 2.2.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.
Files changed (40) hide show
  1. package/README.md +58 -1
  2. package/dist/components/CalendarView/CalendarView.module.css +11 -0
  3. package/dist/components/CalendarView/LaneTimelineView.js +3 -1
  4. package/dist/components/CalendarView/LaneTimelineView.js.map +1 -1
  5. package/dist/components/CalendarView/index.js +8 -0
  6. package/dist/components/CalendarView/index.js.map +1 -1
  7. package/dist/defaults.js +1 -0
  8. package/dist/defaults.js.map +1 -1
  9. package/dist/endpoints/checkAvailability.js +1 -0
  10. package/dist/endpoints/checkAvailability.js.map +1 -1
  11. package/dist/endpoints/getSlots.js +1 -0
  12. package/dist/endpoints/getSlots.js.map +1 -1
  13. package/dist/endpoints/resourceAvailability.d.ts +6 -2
  14. package/dist/endpoints/resourceAvailability.js +19 -1
  15. package/dist/endpoints/resourceAvailability.js.map +1 -1
  16. package/dist/hooks/reservations/validateConflicts.js +1 -0
  17. package/dist/hooks/reservations/validateConflicts.js.map +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/services/AvailabilityService.d.ts +9 -2
  21. package/dist/services/AvailabilityService.js +26 -3
  22. package/dist/services/AvailabilityService.js.map +1 -1
  23. package/dist/translations/ar.json +1 -0
  24. package/dist/translations/de.json +1 -0
  25. package/dist/translations/en.json +1 -0
  26. package/dist/translations/es.json +1 -0
  27. package/dist/translations/fa.json +1 -0
  28. package/dist/translations/fr.json +1 -0
  29. package/dist/translations/hi.json +1 -0
  30. package/dist/translations/id.json +1 -0
  31. package/dist/translations/pl.json +1 -0
  32. package/dist/translations/ru.json +1 -0
  33. package/dist/translations/tr.json +1 -0
  34. package/dist/translations/zh.json +1 -0
  35. package/dist/types.d.ts +27 -0
  36. package/dist/types.js.map +1 -1
  37. package/dist/utilities/computeSlotStates.d.ts +2 -1
  38. package/dist/utilities/computeSlotStates.js +5 -1
  39. package/dist/utilities/computeSlotStates.js.map +1 -1
  40. 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 === '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","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","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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"}
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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"}
@@ -846,6 +846,7 @@ export const CalendarView = ()=>{
846
846
  capacityMode: availability.capacityMode,
847
847
  dayEnd,
848
848
  dayStart,
849
+ external: availability.external,
849
850
  quantity: availability.quantity,
850
851
  requiredPools: availability.requiredPools,
851
852
  shiftWindows: dayAvail.shiftWindows,
@@ -907,6 +908,9 @@ export const CalendarView = ()=>{
907
908
  } else if (slotInfo.state === 'time-off') {
908
909
  slotClass = styles.slotTimeOff;
909
910
  isNonInteractive = true;
911
+ } else if (slotInfo.state === 'external') {
912
+ slotClass = styles.slotExternal;
913
+ isNonInteractive = true;
910
914
  } else if (slotInfo.state === 'full') {
911
915
  slotClass = styles.slotFull;
912
916
  isNonInteractive = true;
@@ -981,6 +985,7 @@ export const CalendarView = ()=>{
981
985
  capacityMode: availability.capacityMode,
982
986
  dayEnd,
983
987
  dayStart,
988
+ external: availability.external,
984
989
  quantity: availability.quantity,
985
990
  requiredPools: availability.requiredPools,
986
991
  shiftWindows: dayAvail.shiftWindows,
@@ -1013,6 +1018,9 @@ export const CalendarView = ()=>{
1013
1018
  } else if (slotInfo.state === 'time-off') {
1014
1019
  slotClass = styles.slotTimeOff;
1015
1020
  isNonInteractive = true;
1021
+ } else if (slotInfo.state === 'external') {
1022
+ slotClass = styles.slotExternal;
1023
+ isNonInteractive = true;
1016
1024
  } else if (slotInfo.state === 'full') {
1017
1025
  slotClass = styles.slotFull;
1018
1026
  isNonInteractive = true;