payload-reserve 1.3.2 → 1.5.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 (104) hide show
  1. package/README.md +185 -4
  2. package/dist/collections/Reservations.js +47 -2
  3. package/dist/collections/Reservations.js.map +1 -1
  4. package/dist/collections/Resources.d.ts +16 -0
  5. package/dist/collections/Resources.js +35 -10
  6. package/dist/collections/Resources.js.map +1 -1
  7. package/dist/collections/Schedules.js +34 -0
  8. package/dist/collections/Schedules.js.map +1 -1
  9. package/dist/collections/Services.js +34 -1
  10. package/dist/collections/Services.js.map +1 -1
  11. package/dist/components/AvailabilityTimeField/AvailabilityTimeField.module.css +7 -0
  12. package/dist/components/AvailabilityTimeField/index.d.ts +2 -0
  13. package/dist/components/AvailabilityTimeField/index.js +109 -0
  14. package/dist/components/AvailabilityTimeField/index.js.map +1 -0
  15. package/dist/components/CalendarView/CalendarView.module.css +114 -0
  16. package/dist/components/CalendarView/LaneTimelineView.d.ts +12 -0
  17. package/dist/components/CalendarView/LaneTimelineView.js +116 -0
  18. package/dist/components/CalendarView/LaneTimelineView.js.map +1 -0
  19. package/dist/components/CalendarView/index.js +224 -22
  20. package/dist/components/CalendarView/index.js.map +1 -1
  21. package/dist/components/CalendarView/useResourceAvailability.d.ts +9 -0
  22. package/dist/components/CalendarView/useResourceAvailability.js +40 -0
  23. package/dist/components/CalendarView/useResourceAvailability.js.map +1 -0
  24. package/dist/defaults.d.ts +3 -0
  25. package/dist/defaults.js +53 -0
  26. package/dist/defaults.js.map +1 -1
  27. package/dist/endpoints/cancelBooking.js +34 -21
  28. package/dist/endpoints/cancelBooking.js.map +1 -1
  29. package/dist/endpoints/checkAvailability.js +16 -1
  30. package/dist/endpoints/checkAvailability.js.map +1 -1
  31. package/dist/endpoints/createBooking.js +4 -1
  32. package/dist/endpoints/createBooking.js.map +1 -1
  33. package/dist/endpoints/customerSearch.js +24 -5
  34. package/dist/endpoints/customerSearch.js.map +1 -1
  35. package/dist/endpoints/getSlots.js +16 -1
  36. package/dist/endpoints/getSlots.js.map +1 -1
  37. package/dist/endpoints/resourceAvailability.d.ts +43 -0
  38. package/dist/endpoints/resourceAvailability.js +214 -0
  39. package/dist/endpoints/resourceAvailability.js.map +1 -0
  40. package/dist/exports/client.d.ts +1 -0
  41. package/dist/exports/client.js +1 -0
  42. package/dist/exports/client.js.map +1 -1
  43. package/dist/hooks/reservations/calculateEndTime.js +21 -1
  44. package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
  45. package/dist/hooks/reservations/expandRequiredResources.d.ts +9 -0
  46. package/dist/hooks/reservations/expandRequiredResources.js +81 -0
  47. package/dist/hooks/reservations/expandRequiredResources.js.map +1 -0
  48. package/dist/hooks/reservations/validateGuestBooking.d.ts +3 -0
  49. package/dist/hooks/reservations/validateGuestBooking.js +93 -0
  50. package/dist/hooks/reservations/validateGuestBooking.js.map +1 -0
  51. package/dist/hooks/reservations/validateStatusTransition.js +4 -2
  52. package/dist/hooks/reservations/validateStatusTransition.js.map +1 -1
  53. package/dist/hooks/users/provisionStaffResource.d.ts +15 -0
  54. package/dist/hooks/users/provisionStaffResource.js +88 -0
  55. package/dist/hooks/users/provisionStaffResource.js.map +1 -0
  56. package/dist/index.d.ts +5 -1
  57. package/dist/index.js +4 -0
  58. package/dist/index.js.map +1 -1
  59. package/dist/plugin.js +28 -3
  60. package/dist/plugin.js.map +1 -1
  61. package/dist/services/AvailabilityService.d.ts +7 -6
  62. package/dist/services/AvailabilityService.js +86 -60
  63. package/dist/services/AvailabilityService.js.map +1 -1
  64. package/dist/translations/ar.json +156 -0
  65. package/dist/translations/de.json +156 -0
  66. package/dist/translations/en.json +32 -1
  67. package/dist/translations/es.json +156 -0
  68. package/dist/translations/fa.json +156 -0
  69. package/dist/translations/fr.json +156 -0
  70. package/dist/translations/hi.json +156 -0
  71. package/dist/translations/id.json +156 -0
  72. package/dist/translations/index.js +44 -0
  73. package/dist/translations/index.js.map +1 -1
  74. package/dist/translations/pl.json +156 -0
  75. package/dist/translations/ru.json +156 -0
  76. package/dist/translations/tr.json +156 -0
  77. package/dist/translations/zh.json +156 -0
  78. package/dist/types.d.ts +46 -0
  79. package/dist/types.js.map +1 -1
  80. package/dist/utilities/computeSlotStates.d.ts +39 -0
  81. package/dist/utilities/computeSlotStates.js +49 -0
  82. package/dist/utilities/computeSlotStates.js.map +1 -0
  83. package/dist/utilities/guestBooking.d.ts +10 -0
  84. package/dist/utilities/guestBooking.js +16 -0
  85. package/dist/utilities/guestBooking.js.map +1 -0
  86. package/dist/utilities/resolveRequiredResources.d.ts +8 -0
  87. package/dist/utilities/resolveRequiredResources.js +27 -0
  88. package/dist/utilities/resolveRequiredResources.js.map +1 -0
  89. package/dist/utilities/resolveReservationItems.d.ts +3 -2
  90. package/dist/utilities/resolveReservationItems.js +19 -6
  91. package/dist/utilities/resolveReservationItems.js.map +1 -1
  92. package/dist/utilities/scheduleUtils.d.ts +3 -0
  93. package/dist/utilities/scheduleUtils.js +5 -3
  94. package/dist/utilities/scheduleUtils.js.map +1 -1
  95. package/dist/utilities/selectOptions.d.ts +8 -0
  96. package/dist/utilities/selectOptions.js +11 -0
  97. package/dist/utilities/selectOptions.js.map +1 -0
  98. package/dist/utilities/slotUtils.d.ts +19 -0
  99. package/dist/utilities/slotUtils.js +28 -0
  100. package/dist/utilities/slotUtils.js.map +1 -1
  101. package/dist/utilities/userRoles.d.ts +20 -0
  102. package/dist/utilities/userRoles.js +32 -0
  103. package/dist/utilities/userRoles.js.map +1 -0
  104. package/package.json +2 -1
@@ -2,8 +2,12 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useConfig, useDocumentDrawer, useTranslation } from '@payloadcms/ui';
4
4
  import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
5
+ import { computeSlotStates } from '../../utilities/computeSlotStates.js';
5
6
  import { statusToI18nKey } from '../../utilities/i18nUtils.js';
7
+ import { localDayKey } from '../../utilities/slotUtils.js';
6
8
  import styles from './CalendarView.module.css';
9
+ import { LaneTimelineView } from './LaneTimelineView.js';
10
+ import { useResourceAvailability } from './useResourceAvailability.js';
7
11
  // Built-in status → CSS class map (for known statuses; custom statuses use inline style)
8
12
  const STATUS_CLASS_MAP = {
9
13
  cancelled: styles.statusCancelled,
@@ -35,6 +39,7 @@ export const CalendarView = ()=>{
35
39
  const slugs = config.admin?.custom?.reservationSlugs;
36
40
  const reservationSlug = slugs?.reservations ?? 'reservations';
37
41
  const apiUrl = `${config.serverURL ?? ''}${config.routes.api}/${reservationSlug}`;
42
+ const apiBase = `${config.serverURL ?? ''}${config.routes.api}`;
38
43
  const statusMachine = config.admin?.custom?.reservationStatusMachine;
39
44
  // The initial/pending status (what "pending" view shows)
40
45
  const defaultStatus = statusMachine?.defaultStatus ?? 'pending';
@@ -173,6 +178,8 @@ export const CalendarView = ()=>{
173
178
  currentDate,
174
179
  viewMode
175
180
  ]);
181
+ // Availability data for the selected resource (null when no resource selected — grid unshaded)
182
+ const { data: availability } = useResourceAvailability(apiBase, selectedResourceId || undefined, rangeStart, rangeEnd);
176
183
  const fetchReservations = useCallback(async ()=>{
177
184
  setLoading(true);
178
185
  try {
@@ -453,6 +460,28 @@ export const CalendarView = ()=>{
453
460
  });
454
461
  pendingDrawerOpen.current = true;
455
462
  }, []);
463
+ // Click-to-book: open new-reservation drawer pre-filled with startTime + optional resource
464
+ const handleSlotClick = useCallback((startIso)=>{
465
+ setDrawerDocId(null);
466
+ setInitialData({
467
+ ...selectedResourceId ? {
468
+ resource: selectedResourceId
469
+ } : {},
470
+ startTime: startIso
471
+ });
472
+ pendingDrawerOpen.current = true;
473
+ }, [
474
+ selectedResourceId
475
+ ]);
476
+ // Lane-specific book: pre-fills both the specific resource and startTime
477
+ const handleLaneBook = useCallback((resourceId, startIso)=>{
478
+ setDrawerDocId(null);
479
+ setInitialData({
480
+ resource: resourceId,
481
+ startTime: startIso
482
+ });
483
+ pendingDrawerOpen.current = true;
484
+ }, []);
456
485
  const openDocDrawer = useCallback((id)=>{
457
486
  setDrawerDocId(id);
458
487
  setInitialData(undefined);
@@ -466,6 +495,7 @@ export const CalendarView = ()=>{
466
495
  } else if (viewMode === 'week') {
467
496
  next.setDate(next.getDate() + 7 * direction);
468
497
  } else {
498
+ // day, lanes: step one day at a time
469
499
  next.setDate(next.getDate() + direction);
470
500
  }
471
501
  return next;
@@ -556,15 +586,28 @@ export const CalendarView = ()=>{
556
586
  const inlineStyle = cssClass ? undefined : {
557
587
  background: color
558
588
  };
559
- return /*#__PURE__*/ _jsx("div", {
560
- className: `${styles.eventItem} ${cssClass}`,
589
+ const hasItems = Array.isArray(r.items) && r.items.length > 0;
590
+ return /*#__PURE__*/ _jsxs("div", {
591
+ className: `${styles.eventItem} ${cssClass} ${hasItems && !compact ? styles.eventItemExpanded : ''}`,
561
592
  onClick: (e)=>handleEventClick(e, r.id),
562
593
  onKeyDown: (e)=>handleEventKeyDown(e, r.id),
563
594
  role: "button",
564
595
  style: inlineStyle,
565
596
  tabIndex: 0,
566
597
  title: getEventTooltip(r),
567
- children: getEventLabel(r, compact)
598
+ children: [
599
+ getEventLabel(r, compact),
600
+ hasItems && /*#__PURE__*/ _jsx("div", {
601
+ className: styles.itemBadges,
602
+ children: r.items.map((it, i)=>{
603
+ const name = typeof it.resource === 'object' ? it.resource?.name : it.resource;
604
+ return /*#__PURE__*/ _jsx("span", {
605
+ className: styles.itemBadge,
606
+ children: String(name ?? '')
607
+ }, i);
608
+ })
609
+ })
610
+ ]
568
611
  }, r.id);
569
612
  };
570
613
  // Dynamic legend: iterates all statuses from the status machine config
@@ -672,6 +715,40 @@ export const CalendarView = ()=>{
672
715
  const hours = Array.from({
673
716
  length: 12
674
717
  }, (_, i)=>i + 7);
718
+ // Grid bounds: hour 7 to hour 19 (end of last slot), step 60 min
719
+ const gridStartHour = 7;
720
+ const gridEndHour = gridStartHour + hours.length // 19
721
+ ;
722
+ const gridStep = 60;
723
+ // Build per-day slot-state maps when a resource is selected
724
+ const daySlotMaps = availability ? new Map(weekDays.map((day)=>{
725
+ const isoDay = localDayKey(day);
726
+ const dayAvail = availability.days.find((d)=>d.date === isoDay);
727
+ const dayStart = new Date(day);
728
+ dayStart.setHours(gridStartHour, 0, 0, 0);
729
+ const dayEnd = new Date(day);
730
+ dayEnd.setHours(gridEndHour, 0, 0, 0);
731
+ const slots = dayAvail ? computeSlotStates({
732
+ busy: availability.busy,
733
+ capacityMode: availability.capacityMode,
734
+ dayEnd,
735
+ dayStart,
736
+ quantity: availability.quantity,
737
+ requiredPools: availability.requiredPools,
738
+ shiftWindows: dayAvail.shiftWindows,
739
+ step: gridStep,
740
+ timeOff: dayAvail.timeOff
741
+ }) : [];
742
+ // Index by slot start ISO for fast lookup
743
+ const slotByStart = new Map(slots.map((s)=>[
744
+ s.start.toISOString(),
745
+ s
746
+ ]));
747
+ return [
748
+ isoDay,
749
+ slotByStart
750
+ ];
751
+ })) : null;
675
752
  return /*#__PURE__*/ _jsxs("div", {
676
753
  className: styles.weekView,
677
754
  children: [
@@ -702,19 +779,62 @@ export const CalendarView = ()=>{
702
779
  });
703
780
  const clickDate = new Date(day);
704
781
  clickDate.setHours(hour, 0, 0, 0);
705
- return /*#__PURE__*/ _jsxs("div", {
706
- className: styles.weekCell,
707
- onClick: ()=>handleDateClick(clickDate),
708
- onKeyDown: (e)=>{
709
- if (e.key === 'Enter' || e.key === ' ') {
710
- e.preventDefault();
782
+ // Slot state (only when a resource is selected)
783
+ const isoDay = localDayKey(day);
784
+ const slotMap = daySlotMaps?.get(isoDay);
785
+ const slotInfo = slotMap?.get(clickDate.toISOString()) ?? null;
786
+ // Derive cell CSS class and interactivity based on slot state
787
+ let slotClass = '';
788
+ let isNonInteractive = false;
789
+ if (slotInfo) {
790
+ if (slotInfo.state === 'off-shift') {
791
+ slotClass = styles.slotOffShift;
792
+ isNonInteractive = true;
793
+ } else if (slotInfo.state === 'time-off') {
794
+ slotClass = styles.slotTimeOff;
795
+ isNonInteractive = true;
796
+ } else if (slotInfo.state === 'full') {
797
+ slotClass = styles.slotFull;
798
+ isNonInteractive = true;
799
+ } else {
800
+ slotClass = styles.slotFree;
801
+ }
802
+ }
803
+ // Time-off label: show type/reason from dayAvail when in time-off state
804
+ const dayAvail = availability?.days.find((d)=>d.date === isoDay);
805
+ const timeOffEntry = slotInfo?.state === 'time-off' ? dayAvail?.timeOff.find((to)=>new Date(to.start) <= clickDate && clickDate < new Date(to.end)) : undefined;
806
+ const timeOffLabel = timeOffEntry?.type ?? timeOffEntry?.reason ?? null;
807
+ const handleClick = isNonInteractive ? undefined : availability ? ()=>handleSlotClick(clickDate.toISOString()) : ()=>handleDateClick(clickDate);
808
+ const handleKeyDown = isNonInteractive ? undefined : (e)=>{
809
+ if (e.key === 'Enter' || e.key === ' ') {
810
+ e.preventDefault();
811
+ if (availability) {
812
+ handleSlotClick(clickDate.toISOString());
813
+ } else {
711
814
  handleDateClick(clickDate);
712
815
  }
713
- },
816
+ }
817
+ };
818
+ return /*#__PURE__*/ _jsxs("div", {
819
+ className: `${styles.weekCell} ${slotClass}`,
820
+ onClick: handleClick,
821
+ onKeyDown: handleKeyDown,
714
822
  role: "button",
715
- tabIndex: 0,
823
+ tabIndex: isNonInteractive ? -1 : 0,
716
824
  children: [
717
825
  renderCurrentTimeLine(day, hour),
826
+ timeOffLabel && /*#__PURE__*/ _jsx("span", {
827
+ className: styles.timeOffLabel,
828
+ children: timeOffLabel
829
+ }),
830
+ slotInfo && availability && availability.quantity > 1 && /*#__PURE__*/ _jsxs("span", {
831
+ className: styles.capacityBadge,
832
+ children: [
833
+ slotInfo.occupancy,
834
+ "/",
835
+ availability.quantity
836
+ ]
837
+ }),
718
838
  cellReservations.map((r)=>renderEventItem(r, false))
719
839
  ]
720
840
  }, `cell-${hour}-${di}`);
@@ -728,6 +848,36 @@ export const CalendarView = ()=>{
728
848
  const hours = Array.from({
729
849
  length: 14
730
850
  }, (_, i)=>i + 7);
851
+ // Grid bounds: hour 7 to hour 21 (end of last slot), step 60 min
852
+ const gridStartHour = 7;
853
+ const gridEndHour = gridStartHour + hours.length // 21
854
+ ;
855
+ const gridStep = 60;
856
+ // Build slot-state map for the current day when a resource is selected
857
+ let daySlotMap = null;
858
+ if (availability) {
859
+ const isoDay = localDayKey(currentDate);
860
+ const dayAvail = availability.days.find((d)=>d.date === isoDay);
861
+ const dayStart = new Date(currentDate);
862
+ dayStart.setHours(gridStartHour, 0, 0, 0);
863
+ const dayEnd = new Date(currentDate);
864
+ dayEnd.setHours(gridEndHour, 0, 0, 0);
865
+ const slots = dayAvail ? computeSlotStates({
866
+ busy: availability.busy,
867
+ capacityMode: availability.capacityMode,
868
+ dayEnd,
869
+ dayStart,
870
+ quantity: availability.quantity,
871
+ requiredPools: availability.requiredPools,
872
+ shiftWindows: dayAvail.shiftWindows,
873
+ step: gridStep,
874
+ timeOff: dayAvail.timeOff
875
+ }) : [];
876
+ daySlotMap = new Map(slots.map((s)=>[
877
+ s.start.toISOString(),
878
+ s
879
+ ]));
880
+ }
731
881
  return /*#__PURE__*/ _jsx("div", {
732
882
  className: styles.dayView,
733
883
  children: hours.map((hour)=>{
@@ -737,6 +887,41 @@ export const CalendarView = ()=>{
737
887
  });
738
888
  const clickDate = new Date(currentDate);
739
889
  clickDate.setHours(hour, 0, 0, 0);
890
+ // Slot state (only when a resource is selected)
891
+ const slotInfo = daySlotMap?.get(clickDate.toISOString()) ?? null;
892
+ // Derive cell CSS class and interactivity based on slot state
893
+ let slotClass = '';
894
+ let isNonInteractive = false;
895
+ if (slotInfo) {
896
+ if (slotInfo.state === 'off-shift') {
897
+ slotClass = styles.slotOffShift;
898
+ isNonInteractive = true;
899
+ } else if (slotInfo.state === 'time-off') {
900
+ slotClass = styles.slotTimeOff;
901
+ isNonInteractive = true;
902
+ } else if (slotInfo.state === 'full') {
903
+ slotClass = styles.slotFull;
904
+ isNonInteractive = true;
905
+ } else {
906
+ slotClass = styles.slotFree;
907
+ }
908
+ }
909
+ // Time-off label
910
+ const isoDay = localDayKey(currentDate);
911
+ const dayAvail = availability?.days.find((d)=>d.date === isoDay);
912
+ const timeOffEntry = slotInfo?.state === 'time-off' ? dayAvail?.timeOff.find((to)=>new Date(to.start) <= clickDate && clickDate < new Date(to.end)) : undefined;
913
+ const timeOffLabel = timeOffEntry?.type ?? timeOffEntry?.reason ?? null;
914
+ const handleClick = isNonInteractive ? undefined : availability ? ()=>handleSlotClick(clickDate.toISOString()) : ()=>handleDateClick(clickDate);
915
+ const handleKeyDown = isNonInteractive ? undefined : (e)=>{
916
+ if (e.key === 'Enter' || e.key === ' ') {
917
+ e.preventDefault();
918
+ if (availability) {
919
+ handleSlotClick(clickDate.toISOString());
920
+ } else {
921
+ handleDateClick(clickDate);
922
+ }
923
+ }
924
+ };
740
925
  return /*#__PURE__*/ _jsxs(Fragment, {
741
926
  children: [
742
927
  /*#__PURE__*/ _jsxs("div", {
@@ -747,18 +932,25 @@ export const CalendarView = ()=>{
747
932
  ]
748
933
  }),
749
934
  /*#__PURE__*/ _jsxs("div", {
750
- className: styles.dayViewCell,
751
- onClick: ()=>handleDateClick(clickDate),
752
- onKeyDown: (e)=>{
753
- if (e.key === 'Enter' || e.key === ' ') {
754
- e.preventDefault();
755
- handleDateClick(clickDate);
756
- }
757
- },
935
+ className: `${styles.dayViewCell} ${slotClass}`,
936
+ onClick: handleClick,
937
+ onKeyDown: handleKeyDown,
758
938
  role: "button",
759
- tabIndex: 0,
939
+ tabIndex: isNonInteractive ? -1 : 0,
760
940
  children: [
761
941
  renderCurrentTimeLine(currentDate, hour),
942
+ timeOffLabel && /*#__PURE__*/ _jsx("span", {
943
+ className: styles.timeOffLabel,
944
+ children: timeOffLabel
945
+ }),
946
+ slotInfo && availability && availability.quantity > 1 && /*#__PURE__*/ _jsxs("span", {
947
+ className: styles.capacityBadge,
948
+ children: [
949
+ slotInfo.occupancy,
950
+ "/",
951
+ availability.quantity
952
+ ]
953
+ }),
762
954
  hourReservations.map((r)=>renderEventItem(r, false))
763
955
  ]
764
956
  })
@@ -1041,6 +1233,10 @@ export const CalendarView = ()=>{
1041
1233
  key: 'day',
1042
1234
  label: t('reservation:calendarDay')
1043
1235
  },
1236
+ {
1237
+ key: 'lanes',
1238
+ label: t('reservation:calendarLanes')
1239
+ },
1044
1240
  {
1045
1241
  key: 'pending',
1046
1242
  label: t('reservation:calendarPending')
@@ -1081,14 +1277,20 @@ export const CalendarView = ()=>{
1081
1277
  ]
1082
1278
  })
1083
1279
  }),
1084
- loading && viewMode !== 'pending' ? /*#__PURE__*/ _jsx("div", {
1280
+ loading && viewMode !== 'pending' && viewMode !== 'lanes' ? /*#__PURE__*/ _jsx("div", {
1085
1281
  className: styles.loading,
1086
1282
  children: t('reservation:calendarLoading')
1087
1283
  }) : /*#__PURE__*/ _jsxs(_Fragment, {
1088
1284
  children: [
1089
1285
  viewMode === 'month' && renderMonthView(),
1090
1286
  viewMode === 'week' && renderWeekView(),
1091
- viewMode === 'day' && renderDayView()
1287
+ viewMode === 'day' && renderDayView(),
1288
+ viewMode === 'lanes' && /*#__PURE__*/ _jsx(LaneTimelineView, {
1289
+ apiBase: apiBase,
1290
+ day: currentDate,
1291
+ onBook: handleLaneBook,
1292
+ resources: selectedResourceId ? resources.filter((r)=>r.id === selectedResourceId) : resources
1293
+ })
1092
1294
  ]
1093
1295
  }),
1094
1296
  viewMode === 'pending' && renderPendingView(),