payload-reserve 1.5.0 → 2.0.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 (95) hide show
  1. package/README.md +40 -3
  2. package/dist/collections/Reservations.js +19 -7
  3. package/dist/collections/Reservations.js.map +1 -1
  4. package/dist/collections/Resources.js +11 -8
  5. package/dist/collections/Resources.js.map +1 -1
  6. package/dist/collections/Schedules.js +12 -6
  7. package/dist/collections/Schedules.js.map +1 -1
  8. package/dist/collections/Services.js +19 -10
  9. package/dist/collections/Services.js.map +1 -1
  10. package/dist/components/AvailabilityOverview/index.js +70 -26
  11. package/dist/components/AvailabilityOverview/index.js.map +1 -1
  12. package/dist/components/CalendarView/CalendarView.module.css +9 -0
  13. package/dist/components/CalendarView/LaneTimelineView.d.ts +4 -1
  14. package/dist/components/CalendarView/LaneTimelineView.js +17 -12
  15. package/dist/components/CalendarView/LaneTimelineView.js.map +1 -1
  16. package/dist/components/CalendarView/index.js +154 -53
  17. package/dist/components/CalendarView/index.js.map +1 -1
  18. package/dist/components/CustomerField/index.js +8 -3
  19. package/dist/components/CustomerField/index.js.map +1 -1
  20. package/dist/components/DashboardWidget/DashboardWidgetServer.js +97 -21
  21. package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
  22. package/dist/defaults.js +46 -8
  23. package/dist/defaults.js.map +1 -1
  24. package/dist/endpoints/cancelBooking.js +1 -1
  25. package/dist/endpoints/cancelBooking.js.map +1 -1
  26. package/dist/endpoints/checkAvailability.js +56 -7
  27. package/dist/endpoints/checkAvailability.js.map +1 -1
  28. package/dist/endpoints/createBooking.js +19 -10
  29. package/dist/endpoints/createBooking.js.map +1 -1
  30. package/dist/endpoints/customerSearch.js +5 -2
  31. package/dist/endpoints/customerSearch.js.map +1 -1
  32. package/dist/endpoints/getSlots.js +56 -7
  33. package/dist/endpoints/getSlots.js.map +1 -1
  34. package/dist/endpoints/resourceAvailability.d.ts +2 -1
  35. package/dist/endpoints/resourceAvailability.js +85 -25
  36. package/dist/endpoints/resourceAvailability.js.map +1 -1
  37. package/dist/hooks/reservations/calculateEndTime.js +48 -20
  38. package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
  39. package/dist/hooks/reservations/enforceCustomerOwnership.d.ts +11 -0
  40. package/dist/hooks/reservations/enforceCustomerOwnership.js +30 -0
  41. package/dist/hooks/reservations/enforceCustomerOwnership.js.map +1 -0
  42. package/dist/hooks/reservations/onStatusChange.js +10 -4
  43. package/dist/hooks/reservations/onStatusChange.js.map +1 -1
  44. package/dist/hooks/reservations/validateCancellation.js +3 -2
  45. package/dist/hooks/reservations/validateCancellation.js.map +1 -1
  46. package/dist/hooks/reservations/validateConflicts.js +23 -4
  47. package/dist/hooks/reservations/validateConflicts.js.map +1 -1
  48. package/dist/hooks/reservations/validateGuestBooking.js +3 -4
  49. package/dist/hooks/reservations/validateGuestBooking.js.map +1 -1
  50. package/dist/hooks/reservations/validateStatusTransition.js +2 -2
  51. package/dist/hooks/reservations/validateStatusTransition.js.map +1 -1
  52. package/dist/hooks/users/provisionStaffResource.js +5 -8
  53. package/dist/hooks/users/provisionStaffResource.js.map +1 -1
  54. package/dist/plugin.js +82 -13
  55. package/dist/plugin.js.map +1 -1
  56. package/dist/services/AvailabilityService.d.ts +54 -2
  57. package/dist/services/AvailabilityService.js +180 -46
  58. package/dist/services/AvailabilityService.js.map +1 -1
  59. package/dist/translations/ar.json +1 -0
  60. package/dist/translations/de.json +1 -0
  61. package/dist/translations/en.json +1 -0
  62. package/dist/translations/es.json +1 -0
  63. package/dist/translations/fa.json +1 -0
  64. package/dist/translations/fr.json +1 -0
  65. package/dist/translations/hi.json +1 -0
  66. package/dist/translations/id.json +1 -0
  67. package/dist/translations/pl.json +1 -0
  68. package/dist/translations/ru.json +1 -0
  69. package/dist/translations/tr.json +1 -0
  70. package/dist/translations/zh.json +1 -0
  71. package/dist/types.d.ts +50 -1
  72. package/dist/types.js +2 -0
  73. package/dist/types.js.map +1 -1
  74. package/dist/utilities/collectionOverrides.d.ts +14 -0
  75. package/dist/utilities/collectionOverrides.js +47 -0
  76. package/dist/utilities/collectionOverrides.js.map +1 -0
  77. package/dist/utilities/ownerAccess.d.ts +6 -0
  78. package/dist/utilities/ownerAccess.js +25 -12
  79. package/dist/utilities/ownerAccess.js.map +1 -1
  80. package/dist/utilities/reservationChanges.d.ts +17 -0
  81. package/dist/utilities/reservationChanges.js +88 -0
  82. package/dist/utilities/reservationChanges.js.map +1 -0
  83. package/dist/utilities/scheduleUtils.d.ts +14 -8
  84. package/dist/utilities/scheduleUtils.js +26 -19
  85. package/dist/utilities/scheduleUtils.js.map +1 -1
  86. package/dist/utilities/tenantFilter.d.ts +25 -0
  87. package/dist/utilities/tenantFilter.js +56 -0
  88. package/dist/utilities/tenantFilter.js.map +1 -0
  89. package/dist/utilities/timezoneUtils.d.ts +39 -0
  90. package/dist/utilities/timezoneUtils.js +134 -0
  91. package/dist/utilities/timezoneUtils.js.map +1 -0
  92. package/dist/utilities/useTenantFilter.d.ts +6 -0
  93. package/dist/utilities/useTenantFilter.js +28 -0
  94. package/dist/utilities/useTenantFilter.js.map +1 -0
  95. package/package.json +2 -1
@@ -99,6 +99,7 @@
99
99
  "fieldCustomerCreateNew": "Utwórz nowego klienta",
100
100
  "fieldCustomerClear": "Wyczyść wybór",
101
101
  "calendarPending": "Oczekujące",
102
+ "calendarShowingNofM": "Wyświetlono {{shown}} z {{total}} rezerwacji — zawęź zakres lub filtruj, aby zobaczyć resztę.",
102
103
  "pendingDateTime": "Data / Czas",
103
104
  "pendingActions": "Akcje",
104
105
  "pendingSelectAll": "Zaznacz wszystkie",
@@ -99,6 +99,7 @@
99
99
  "fieldCustomerCreateNew": "Создать нового клиента",
100
100
  "fieldCustomerClear": "Очистить выбор",
101
101
  "calendarPending": "Ожидает",
102
+ "calendarShowingNofM": "Показано {{shown}} из {{total}} броней — сузьте диапазон или примените фильтр, чтобы увидеть остальные.",
102
103
  "pendingDateTime": "Дата / Время",
103
104
  "pendingActions": "Действия",
104
105
  "pendingSelectAll": "Выбрать все",
@@ -99,6 +99,7 @@
99
99
  "fieldCustomerCreateNew": "Yeni müşteri oluştur",
100
100
  "fieldCustomerClear": "Seçimi temizle",
101
101
  "calendarPending": "Beklemede",
102
+ "calendarShowingNofM": "{{total}} rezervasyondan {{shown}} tanesi gösteriliyor — geri kalanını görmek için aralığı daraltın veya filtreleyin.",
102
103
  "pendingDateTime": "Tarih / Saat",
103
104
  "pendingActions": "İşlemler",
104
105
  "pendingSelectAll": "Tümünü seç",
@@ -99,6 +99,7 @@
99
99
  "fieldCustomerCreateNew": "新建客户",
100
100
  "fieldCustomerClear": "清除选择",
101
101
  "calendarPending": "待处理",
102
+ "calendarShowingNofM": "显示 {{total}} 个预约中的 {{shown}} 个 — 缩小范围或筛选以查看其余部分。",
102
103
  "pendingDateTime": "日期 / 时间",
103
104
  "pendingActions": "操作",
104
105
  "pendingSelectAll": "全选",
package/dist/types.d.ts CHANGED
@@ -3,6 +3,10 @@ export type DurationType = 'fixed' | 'flexible' | 'full-day';
3
3
  export type CapacityMode = 'per-guest' | 'per-reservation';
4
4
  export type StatusMachineConfig = {
5
5
  blockingStatuses: string[];
6
+ /** Status treated as "cancelled" — fires beforeBookingCancel/afterBookingCancel, the cancellation notice period, and the cancellationReason field. */
7
+ cancelStatus: string;
8
+ /** Status treated as "confirmed" — fires beforeBookingConfirm/afterBookingConfirm. */
9
+ confirmStatus: string;
6
10
  defaultStatus: string;
7
11
  statuses: string[];
8
12
  terminalStatuses: string[];
@@ -64,12 +68,15 @@ export type ResourceOwnerModeConfig = {
64
68
  ownerCollection?: string;
65
69
  /** Field name for the owner relationship on Resources (default: 'owner') */
66
70
  ownerField?: string;
71
+ /** User field holding the role for admin detection (default: staffProvisioning.roleField or 'role') */
72
+ roleField?: string;
67
73
  };
68
74
  export type ResolvedResourceOwnerModeConfig = {
69
75
  adminRoles: string[];
70
76
  ownedServices: boolean;
71
77
  ownerCollection?: string;
72
78
  ownerField: string;
79
+ roleField: string;
73
80
  };
74
81
  export type StaffProvisioningConfig = {
75
82
  /** Stamp tenant / custom fields onto the provisioned Resource before create. */
@@ -97,6 +104,17 @@ export type ResolvedStaffProvisioningConfig = {
97
104
  staffRoles: string[];
98
105
  userCollection: string;
99
106
  };
107
+ /**
108
+ * Per-collection override applied to a generated collection. `fields` is a
109
+ * function receiving the plugin's default fields so you can append/reorder/
110
+ * replace them; supplied `hooks` are merged with (not replacing) the plugin's;
111
+ * `access` composes per operation; `slug` is ignored (use the `slugs` option).
112
+ */
113
+ export type CollectionOverride = {
114
+ fields?: (args: {
115
+ defaultFields: Field[];
116
+ }) => Field[];
117
+ } & Omit<Partial<CollectionConfig>, 'fields' | 'slug'>;
100
118
  export type ReservationPluginConfig = {
101
119
  /** Override access control per collection */
102
120
  access?: {
@@ -112,16 +130,31 @@ export type ReservationPluginConfig = {
112
130
  allowGuestBooking?: boolean;
113
131
  /** Hours of notice required before cancellation */
114
132
  cancellationNoticePeriod?: number;
133
+ /** Per-collection overrides applied to the generated collections (issue #4) */
134
+ collectionOverrides?: {
135
+ customers?: CollectionOverride;
136
+ reservations?: CollectionOverride;
137
+ resources?: CollectionOverride;
138
+ schedules?: CollectionOverride;
139
+ services?: CollectionOverride;
140
+ };
115
141
  /** Default buffer time in minutes between reservations */
116
142
  defaultBufferTime?: number;
117
143
  /** Disable the plugin entirely */
118
144
  disabled?: boolean;
119
- /** Extra fields to append to the Reservations collection */
145
+ /** @deprecated Use `collectionOverrides.reservations.fields` instead. Extra fields appended to Reservations. */
120
146
  extraReservationFields?: Field[];
121
147
  /** Plugin hooks for external integrations */
122
148
  hooks?: ReservationPluginHooks;
123
149
  /** Configurable leave/exception type vocabulary (default: vacation/sick/personal/closure/other) */
124
150
  leaveTypes?: string[];
151
+ /** Tenant scoping for the custom admin views (calendar, availability, dashboard). Applied only when the scoped collection has the tenant field AND the tenant cookie is set. */
152
+ multiTenant?: {
153
+ /** Cookie written by the tenant-selector (default 'payload-tenant'). */
154
+ cookieName?: string;
155
+ /** Tenant field name on scoped collections (default 'tenant'). */
156
+ tenantField?: string;
157
+ };
125
158
  /** Enable resource-owner multi-tenancy (opt-in) */
126
159
  resourceOwnerMode?: ResourceOwnerModeConfig;
127
160
  /** Configurable resourceType vocabulary (default: staff/equipment/room) */
@@ -139,6 +172,8 @@ export type ReservationPluginConfig = {
139
172
  staffProvisioning?: StaffProvisioningConfig;
140
173
  /** Configurable status machine (defaults to current behavior) */
141
174
  statusMachine?: Partial<StatusMachineConfig>;
175
+ /** IANA business timezone governing schedules and day boundaries (default 'UTC') */
176
+ timezone?: string;
142
177
  /** Which existing auth collection to extend with customer fields */
143
178
  userCollection?: string;
144
179
  };
@@ -153,12 +188,25 @@ export type ResolvedReservationPluginConfig = {
153
188
  adminGroup: string;
154
189
  allowGuestBooking: boolean;
155
190
  cancellationNoticePeriod: number;
191
+ collectionOverrides: {
192
+ customers?: CollectionOverride;
193
+ reservations?: CollectionOverride;
194
+ resources?: CollectionOverride;
195
+ schedules?: CollectionOverride;
196
+ services?: CollectionOverride;
197
+ };
156
198
  defaultBufferTime: number;
157
199
  disabled: boolean;
158
200
  extraReservationFields: Field[];
201
+ /** Whether the media collection (`slugs.media`) exists — set by the plugin; gates the image upload fields. */
202
+ hasMediaCollection: boolean;
159
203
  hooks: ReservationPluginHooks;
160
204
  leaveTypes: string[];
161
205
  localized: boolean;
206
+ multiTenant: {
207
+ cookieName: string;
208
+ tenantField: string;
209
+ };
162
210
  resourceOwnerMode: ResolvedResourceOwnerModeConfig | undefined;
163
211
  resourceTypes: string[];
164
212
  slugs: {
@@ -171,6 +219,7 @@ export type ResolvedReservationPluginConfig = {
171
219
  };
172
220
  staffProvisioning: ResolvedStaffProvisioningConfig | undefined;
173
221
  statusMachine: StatusMachineConfig;
222
+ timezone: string;
174
223
  userCollection: string | undefined;
175
224
  };
176
225
  export type ReservationStatus = 'cancelled' | 'completed' | 'confirmed' | 'no-show' | 'pending';
package/dist/types.js CHANGED
@@ -3,6 +3,8 @@ export const DEFAULT_STATUS_MACHINE = {
3
3
  'pending',
4
4
  'confirmed'
5
5
  ],
6
+ cancelStatus: 'cancelled',
7
+ confirmStatus: 'confirmed',
6
8
  defaultStatus: 'pending',
7
9
  statuses: [
8
10
  'pending',
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { CollectionConfig, Field, PayloadRequest } from 'payload'\n\n// --- Duration & Capacity models ---\n\nexport type DurationType = 'fixed' | 'flexible' | 'full-day'\n\nexport type CapacityMode = 'per-guest' | 'per-reservation'\n\n// --- Configurable status machine ---\n\nexport type StatusMachineConfig = {\n blockingStatuses: string[]\n defaultStatus: string\n statuses: string[]\n terminalStatuses: string[]\n transitions: Record<string, string[]>\n}\n\nexport const DEFAULT_STATUS_MACHINE: StatusMachineConfig = {\n blockingStatuses: ['pending', 'confirmed'],\n defaultStatus: 'pending',\n statuses: ['pending', 'confirmed', 'completed', 'cancelled', 'no-show'],\n terminalStatuses: ['completed', 'cancelled', 'no-show'],\n transitions: {\n cancelled: [],\n completed: [],\n confirmed: ['completed', 'cancelled', 'no-show'],\n 'no-show': [],\n pending: ['confirmed', 'cancelled'],\n },\n}\n\n// --- Reservation item (for multi-resource bookings, Phase 3) ---\n\nexport type ReservationItemConfig = {\n endTime?: string\n guestCount?: number\n resource: string\n service?: string\n startTime?: string\n}\n\n// --- Plugin hooks for external integrations ---\n\nexport type ReservationPluginHooks = {\n afterBookingCancel?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterBookingConfirm?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterBookingCreate?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterStatusChange?: Array<\n (args: {\n doc: Record<string, unknown>\n newStatus: string\n previousStatus: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingCancel?: Array<\n (args: {\n doc: Record<string, unknown>\n reason?: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingConfirm?: Array<\n (args: {\n doc: Record<string, unknown>\n newStatus: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingCreate?: Array<\n (args: {\n data: Record<string, unknown>\n req: PayloadRequest\n }) => Promise<Record<string, unknown>> | Record<string, unknown>\n >\n}\n\n// --- Resource owner mode ---\n\nexport type ResourceOwnerModeConfig = {\n /** Roles that can see all records (default: check req.user.collection === adminCollection) */\n adminRoles?: string[]\n /** Whether Services also get an owner field (default: false — Services are platform-managed) */\n ownedServices?: boolean\n /**\n * Collection the owner field relates to (where owners/staff live). Defaults to\n * `staffProvisioning.userCollection` when set, otherwise `slugs.customers`. Set\n * this when owners live in a different collection than your customers (e.g.\n * separate `users` and `customers` collections).\n */\n ownerCollection?: string\n /** Field name for the owner relationship on Resources (default: 'owner') */\n ownerField?: string\n}\n\nexport type ResolvedResourceOwnerModeConfig = {\n adminRoles: string[]\n ownedServices: boolean\n ownerCollection?: string\n ownerField: string\n}\n\n// --- Staff provisioning ---\n\nexport type StaffProvisioningConfig = {\n /** Stamp tenant / custom fields onto the provisioned Resource before create. */\n beforeCreate?: (args: {\n data: Record<string, unknown>\n req: PayloadRequest\n user: Record<string, unknown>\n }) => Promise<Record<string, unknown>> | Record<string, unknown>\n /** User field copied into Resource `name` (default 'name', falls back to email). */\n nameFrom?: string\n /** resourceType to stamp (default 'staff'). Must be a valid resourceType. */\n resourceType?: string\n /** Field on the user holding the role (default 'role'). */\n roleField?: string\n /** Role value(s) marking a user as staff. Required, non-empty. */\n staffRoles: string[]\n /** Auth collection holding staff users. Defaults to top-level `userCollection`. */\n userCollection?: string\n}\n\nexport type ResolvedStaffProvisioningConfig = {\n beforeCreate?: StaffProvisioningConfig['beforeCreate']\n nameFrom: string\n resourceType: string\n roleField: string\n staffRoles: string[]\n userCollection: string\n}\n\n// --- Plugin configuration ---\n\nexport type ReservationPluginConfig = {\n /** Override access control per collection */\n access?: {\n customers?: CollectionConfig['access']\n reservations?: CollectionConfig['access']\n resources?: CollectionConfig['access']\n schedules?: CollectionConfig['access']\n services?: CollectionConfig['access']\n }\n /** Admin group name for all reservation collections */\n adminGroup?: string\n /** Allow bookings without a customer account by default (per-service override available) */\n allowGuestBooking?: boolean\n /** Hours of notice required before cancellation */\n cancellationNoticePeriod?: number\n /** Default buffer time in minutes between reservations */\n defaultBufferTime?: number\n /** Disable the plugin entirely */\n disabled?: boolean\n /** Extra fields to append to the Reservations collection */\n extraReservationFields?: Field[]\n /** Plugin hooks for external integrations */\n hooks?: ReservationPluginHooks\n /** Configurable leave/exception type vocabulary (default: vacation/sick/personal/closure/other) */\n leaveTypes?: string[]\n /** Enable resource-owner multi-tenancy (opt-in) */\n resourceOwnerMode?: ResourceOwnerModeConfig\n /** Configurable resourceType vocabulary (default: staff/equipment/room) */\n resourceTypes?: string[]\n /** Override collection slugs */\n slugs?: {\n customers?: string\n media?: string\n reservations?: string\n resources?: string\n schedules?: string\n services?: string\n }\n /** Auto-provision a Resource from staff-role users (opt-in; requires resourceOwnerMode) */\n staffProvisioning?: StaffProvisioningConfig\n /** Configurable status machine (defaults to current behavior) */\n statusMachine?: Partial<StatusMachineConfig>\n /** Which existing auth collection to extend with customer fields */\n userCollection?: string\n}\n\nexport type ResolvedReservationPluginConfig = {\n access: {\n customers?: CollectionConfig['access']\n reservations?: CollectionConfig['access']\n resources?: CollectionConfig['access']\n schedules?: CollectionConfig['access']\n services?: CollectionConfig['access']\n }\n adminGroup: string\n allowGuestBooking: boolean\n cancellationNoticePeriod: number\n defaultBufferTime: number\n disabled: boolean\n extraReservationFields: Field[]\n hooks: ReservationPluginHooks\n leaveTypes: string[]\n localized: boolean\n resourceOwnerMode: ResolvedResourceOwnerModeConfig | undefined\n resourceTypes: string[]\n slugs: {\n customers: string\n media: string\n reservations: string\n resources: string\n schedules: string\n services: string\n }\n staffProvisioning: ResolvedStaffProvisioningConfig | undefined\n statusMachine: StatusMachineConfig\n userCollection: string | undefined\n}\n\nexport type ReservationStatus = 'cancelled' | 'completed' | 'confirmed' | 'no-show' | 'pending'\n\nexport type DayOfWeek = 'fri' | 'mon' | 'sat' | 'sun' | 'thu' | 'tue' | 'wed'\n\nexport type ScheduleType = 'manual' | 'recurring'\n\n/** @deprecated Use DEFAULT_STATUS_MACHINE.transitions instead */\nexport const VALID_STATUS_TRANSITIONS: Record<ReservationStatus, ReservationStatus[]> =\n DEFAULT_STATUS_MACHINE.transitions as Record<ReservationStatus, ReservationStatus[]>\n"],"names":["DEFAULT_STATUS_MACHINE","blockingStatuses","defaultStatus","statuses","terminalStatuses","transitions","cancelled","completed","confirmed","pending","VALID_STATUS_TRANSITIONS"],"mappings":"AAkBA,OAAO,MAAMA,yBAA8C;IACzDC,kBAAkB;QAAC;QAAW;KAAY;IAC1CC,eAAe;IACfC,UAAU;QAAC;QAAW;QAAa;QAAa;QAAa;KAAU;IACvEC,kBAAkB;QAAC;QAAa;QAAa;KAAU;IACvDC,aAAa;QACXC,WAAW,EAAE;QACbC,WAAW,EAAE;QACbC,WAAW;YAAC;YAAa;YAAa;SAAU;QAChD,WAAW,EAAE;QACbC,SAAS;YAAC;YAAa;SAAY;IACrC;AACF,EAAC;AAmMD,+DAA+D,GAC/D,OAAO,MAAMC,2BACXV,uBAAuBK,WAAW,CAAkD"}
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { CollectionConfig, Field, PayloadRequest } from 'payload'\n\n// --- Duration & Capacity models ---\n\nexport type DurationType = 'fixed' | 'flexible' | 'full-day'\n\nexport type CapacityMode = 'per-guest' | 'per-reservation'\n\n// --- Configurable status machine ---\n\nexport type StatusMachineConfig = {\n blockingStatuses: string[]\n /** Status treated as \"cancelled\" — fires beforeBookingCancel/afterBookingCancel, the cancellation notice period, and the cancellationReason field. */\n cancelStatus: string\n /** Status treated as \"confirmed\" — fires beforeBookingConfirm/afterBookingConfirm. */\n confirmStatus: string\n defaultStatus: string\n statuses: string[]\n terminalStatuses: string[]\n transitions: Record<string, string[]>\n}\n\nexport const DEFAULT_STATUS_MACHINE: StatusMachineConfig = {\n blockingStatuses: ['pending', 'confirmed'],\n cancelStatus: 'cancelled',\n confirmStatus: 'confirmed',\n defaultStatus: 'pending',\n statuses: ['pending', 'confirmed', 'completed', 'cancelled', 'no-show'],\n terminalStatuses: ['completed', 'cancelled', 'no-show'],\n transitions: {\n cancelled: [],\n completed: [],\n confirmed: ['completed', 'cancelled', 'no-show'],\n 'no-show': [],\n pending: ['confirmed', 'cancelled'],\n },\n}\n\n// --- Reservation item (for multi-resource bookings, Phase 3) ---\n\nexport type ReservationItemConfig = {\n endTime?: string\n guestCount?: number\n resource: string\n service?: string\n startTime?: string\n}\n\n// --- Plugin hooks for external integrations ---\n\nexport type ReservationPluginHooks = {\n afterBookingCancel?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterBookingConfirm?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterBookingCreate?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterStatusChange?: Array<\n (args: {\n doc: Record<string, unknown>\n newStatus: string\n previousStatus: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingCancel?: Array<\n (args: {\n doc: Record<string, unknown>\n reason?: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingConfirm?: Array<\n (args: {\n doc: Record<string, unknown>\n newStatus: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingCreate?: Array<\n (args: {\n data: Record<string, unknown>\n req: PayloadRequest\n }) => Promise<Record<string, unknown>> | Record<string, unknown>\n >\n}\n\n// --- Resource owner mode ---\n\nexport type ResourceOwnerModeConfig = {\n /** Roles that can see all records (default: check req.user.collection === adminCollection) */\n adminRoles?: string[]\n /** Whether Services also get an owner field (default: false — Services are platform-managed) */\n ownedServices?: boolean\n /**\n * Collection the owner field relates to (where owners/staff live). Defaults to\n * `staffProvisioning.userCollection` when set, otherwise `slugs.customers`. Set\n * this when owners live in a different collection than your customers (e.g.\n * separate `users` and `customers` collections).\n */\n ownerCollection?: string\n /** Field name for the owner relationship on Resources (default: 'owner') */\n ownerField?: string\n /** User field holding the role for admin detection (default: staffProvisioning.roleField or 'role') */\n roleField?: string\n}\n\nexport type ResolvedResourceOwnerModeConfig = {\n adminRoles: string[]\n ownedServices: boolean\n ownerCollection?: string\n ownerField: string\n roleField: string\n}\n\n// --- Staff provisioning ---\n\nexport type StaffProvisioningConfig = {\n /** Stamp tenant / custom fields onto the provisioned Resource before create. */\n beforeCreate?: (args: {\n data: Record<string, unknown>\n req: PayloadRequest\n user: Record<string, unknown>\n }) => Promise<Record<string, unknown>> | Record<string, unknown>\n /** User field copied into Resource `name` (default 'name', falls back to email). */\n nameFrom?: string\n /** resourceType to stamp (default 'staff'). Must be a valid resourceType. */\n resourceType?: string\n /** Field on the user holding the role (default 'role'). */\n roleField?: string\n /** Role value(s) marking a user as staff. Required, non-empty. */\n staffRoles: string[]\n /** Auth collection holding staff users. Defaults to top-level `userCollection`. */\n userCollection?: string\n}\n\nexport type ResolvedStaffProvisioningConfig = {\n beforeCreate?: StaffProvisioningConfig['beforeCreate']\n nameFrom: string\n resourceType: string\n roleField: string\n staffRoles: string[]\n userCollection: string\n}\n\n// --- Plugin configuration ---\n\n/**\n * Per-collection override applied to a generated collection. `fields` is a\n * function receiving the plugin's default fields so you can append/reorder/\n * replace them; supplied `hooks` are merged with (not replacing) the plugin's;\n * `access` composes per operation; `slug` is ignored (use the `slugs` option).\n */\nexport type CollectionOverride = {\n fields?: (args: { defaultFields: Field[] }) => Field[]\n} & Omit<Partial<CollectionConfig>, 'fields' | 'slug'>\n\nexport type ReservationPluginConfig = {\n /** Override access control per collection */\n access?: {\n customers?: CollectionConfig['access']\n reservations?: CollectionConfig['access']\n resources?: CollectionConfig['access']\n schedules?: CollectionConfig['access']\n services?: CollectionConfig['access']\n }\n /** Admin group name for all reservation collections */\n adminGroup?: string\n /** Allow bookings without a customer account by default (per-service override available) */\n allowGuestBooking?: boolean\n /** Hours of notice required before cancellation */\n cancellationNoticePeriod?: number\n /** Per-collection overrides applied to the generated collections (issue #4) */\n collectionOverrides?: {\n customers?: CollectionOverride\n reservations?: CollectionOverride\n resources?: CollectionOverride\n schedules?: CollectionOverride\n services?: CollectionOverride\n }\n /** Default buffer time in minutes between reservations */\n defaultBufferTime?: number\n /** Disable the plugin entirely */\n disabled?: boolean\n /** @deprecated Use `collectionOverrides.reservations.fields` instead. Extra fields appended to Reservations. */\n extraReservationFields?: Field[]\n /** Plugin hooks for external integrations */\n hooks?: ReservationPluginHooks\n /** Configurable leave/exception type vocabulary (default: vacation/sick/personal/closure/other) */\n leaveTypes?: string[]\n /** Tenant scoping for the custom admin views (calendar, availability, dashboard). Applied only when the scoped collection has the tenant field AND the tenant cookie is set. */\n multiTenant?: {\n /** Cookie written by the tenant-selector (default 'payload-tenant'). */\n cookieName?: string\n /** Tenant field name on scoped collections (default 'tenant'). */\n tenantField?: string\n }\n /** Enable resource-owner multi-tenancy (opt-in) */\n resourceOwnerMode?: ResourceOwnerModeConfig\n /** Configurable resourceType vocabulary (default: staff/equipment/room) */\n resourceTypes?: string[]\n /** Override collection slugs */\n slugs?: {\n customers?: string\n media?: string\n reservations?: string\n resources?: string\n schedules?: string\n services?: string\n }\n /** Auto-provision a Resource from staff-role users (opt-in; requires resourceOwnerMode) */\n staffProvisioning?: StaffProvisioningConfig\n /** Configurable status machine (defaults to current behavior) */\n statusMachine?: Partial<StatusMachineConfig>\n /** IANA business timezone governing schedules and day boundaries (default 'UTC') */\n timezone?: string\n /** Which existing auth collection to extend with customer fields */\n userCollection?: string\n}\n\nexport type ResolvedReservationPluginConfig = {\n access: {\n customers?: CollectionConfig['access']\n reservations?: CollectionConfig['access']\n resources?: CollectionConfig['access']\n schedules?: CollectionConfig['access']\n services?: CollectionConfig['access']\n }\n adminGroup: string\n allowGuestBooking: boolean\n cancellationNoticePeriod: number\n collectionOverrides: {\n customers?: CollectionOverride\n reservations?: CollectionOverride\n resources?: CollectionOverride\n schedules?: CollectionOverride\n services?: CollectionOverride\n }\n defaultBufferTime: number\n disabled: boolean\n extraReservationFields: Field[]\n /** Whether the media collection (`slugs.media`) exists — set by the plugin; gates the image upload fields. */\n hasMediaCollection: boolean\n hooks: ReservationPluginHooks\n leaveTypes: string[]\n localized: boolean\n multiTenant: {\n cookieName: string\n tenantField: string\n }\n resourceOwnerMode: ResolvedResourceOwnerModeConfig | undefined\n resourceTypes: string[]\n slugs: {\n customers: string\n media: string\n reservations: string\n resources: string\n schedules: string\n services: string\n }\n staffProvisioning: ResolvedStaffProvisioningConfig | undefined\n statusMachine: StatusMachineConfig\n timezone: string\n userCollection: string | undefined\n}\n\nexport type ReservationStatus = 'cancelled' | 'completed' | 'confirmed' | 'no-show' | 'pending'\n\nexport type DayOfWeek = 'fri' | 'mon' | 'sat' | 'sun' | 'thu' | 'tue' | 'wed'\n\nexport type ScheduleType = 'manual' | 'recurring'\n\n/** @deprecated Use DEFAULT_STATUS_MACHINE.transitions instead */\nexport const VALID_STATUS_TRANSITIONS: Record<ReservationStatus, ReservationStatus[]> =\n DEFAULT_STATUS_MACHINE.transitions as Record<ReservationStatus, ReservationStatus[]>\n"],"names":["DEFAULT_STATUS_MACHINE","blockingStatuses","cancelStatus","confirmStatus","defaultStatus","statuses","terminalStatuses","transitions","cancelled","completed","confirmed","pending","VALID_STATUS_TRANSITIONS"],"mappings":"AAsBA,OAAO,MAAMA,yBAA8C;IACzDC,kBAAkB;QAAC;QAAW;KAAY;IAC1CC,cAAc;IACdC,eAAe;IACfC,eAAe;IACfC,UAAU;QAAC;QAAW;QAAa;QAAa;QAAa;KAAU;IACvEC,kBAAkB;QAAC;QAAa;QAAa;KAAU;IACvDC,aAAa;QACXC,WAAW,EAAE;QACbC,WAAW,EAAE;QACbC,WAAW;YAAC;YAAa;YAAa;SAAU;QAChD,WAAW,EAAE;QACbC,SAAS;YAAC;YAAa;SAAY;IACrC;AACF,EAAC;AA+OD,+DAA+D,GAC/D,OAAO,MAAMC,2BACXZ,uBAAuBO,WAAW,CAAkD"}
@@ -0,0 +1,14 @@
1
+ import type { CollectionConfig } from 'payload';
2
+ import type { CollectionOverride } from '../types.js';
3
+ /**
4
+ * Apply a per-collection override to a generated collection, protecting the
5
+ * plugin's load-bearing behavior:
6
+ * - `fields`: a function receiving the plugin's default fields, returning the
7
+ * final list (append/reorder/replace — covers the issue #4 join-field case).
8
+ * - `hooks`: MERGED per event array (plugin hooks run first, then the user's) —
9
+ * an override can add hooks but never clobber conflict detection / status hooks.
10
+ * - `access`: composed per operation (rules the override omits survive).
11
+ * - `slug`: ignored (the `slugs` option owns slugs).
12
+ * - everything else (admin, labels, custom, …): shallow-merged.
13
+ */
14
+ export declare function applyCollectionOverride(collection: CollectionConfig, override?: CollectionOverride): CollectionConfig;
@@ -0,0 +1,47 @@
1
+ import { composeAccess } from './ownerAccess.js';
2
+ /**
3
+ * Apply a per-collection override to a generated collection, protecting the
4
+ * plugin's load-bearing behavior:
5
+ * - `fields`: a function receiving the plugin's default fields, returning the
6
+ * final list (append/reorder/replace — covers the issue #4 join-field case).
7
+ * - `hooks`: MERGED per event array (plugin hooks run first, then the user's) —
8
+ * an override can add hooks but never clobber conflict detection / status hooks.
9
+ * - `access`: composed per operation (rules the override omits survive).
10
+ * - `slug`: ignored (the `slugs` option owns slugs).
11
+ * - everything else (admin, labels, custom, …): shallow-merged.
12
+ */ export function applyCollectionOverride(collection, override) {
13
+ if (!override) {
14
+ return collection;
15
+ }
16
+ // Pull out the specially-handled keys; the rest shallow-merges. `slug` is
17
+ // omitted from CollectionOverride's type, but strip it defensively in case a
18
+ // caller cast around the type — the `slugs` option owns slugs.
19
+ const { access, fields, hooks, ...rest } = override;
20
+ delete rest.slug;
21
+ const mergedFields = fields ? fields({
22
+ defaultFields: collection.fields
23
+ }) : collection.fields;
24
+ // Merge hooks per event array: plugin's first, then the override's.
25
+ const mergedHooks = {
26
+ ...collection.hooks
27
+ };
28
+ if (hooks) {
29
+ for (const key of Object.keys(hooks)){
30
+ const pluginHooks = collection.hooks?.[key] ?? [];
31
+ const overrideHooks = hooks[key] ?? [];
32
+ mergedHooks[key] = [
33
+ ...pluginHooks,
34
+ ...overrideHooks
35
+ ];
36
+ }
37
+ }
38
+ return {
39
+ ...collection,
40
+ ...rest,
41
+ access: composeAccess(collection.access ?? {}, access),
42
+ fields: mergedFields,
43
+ hooks: mergedHooks
44
+ };
45
+ }
46
+
47
+ //# sourceMappingURL=collectionOverrides.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utilities/collectionOverrides.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload'\n\nimport type { CollectionOverride } from '../types.js'\n\nimport { composeAccess } from './ownerAccess.js'\n\n/**\n * Apply a per-collection override to a generated collection, protecting the\n * plugin's load-bearing behavior:\n * - `fields`: a function receiving the plugin's default fields, returning the\n * final list (append/reorder/replace — covers the issue #4 join-field case).\n * - `hooks`: MERGED per event array (plugin hooks run first, then the user's) —\n * an override can add hooks but never clobber conflict detection / status hooks.\n * - `access`: composed per operation (rules the override omits survive).\n * - `slug`: ignored (the `slugs` option owns slugs).\n * - everything else (admin, labels, custom, …): shallow-merged.\n */\nexport function applyCollectionOverride(\n collection: CollectionConfig,\n override?: CollectionOverride,\n): CollectionConfig {\n if (!override) {\n return collection\n }\n\n // Pull out the specially-handled keys; the rest shallow-merges. `slug` is\n // omitted from CollectionOverride's type, but strip it defensively in case a\n // caller cast around the type — the `slugs` option owns slugs.\n const { access, fields, hooks, ...rest } = override\n delete (rest as { slug?: unknown }).slug\n\n const mergedFields = fields ? fields({ defaultFields: collection.fields }) : collection.fields\n\n // Merge hooks per event array: plugin's first, then the override's.\n const mergedHooks: CollectionConfig['hooks'] = { ...collection.hooks }\n if (hooks) {\n for (const key of Object.keys(hooks) as Array<keyof NonNullable<CollectionConfig['hooks']>>) {\n const pluginHooks = (collection.hooks?.[key] ?? []) as unknown[]\n const overrideHooks = (hooks[key] ?? []) as unknown[]\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ;(mergedHooks as any)[key] = [...pluginHooks, ...overrideHooks]\n }\n }\n\n return {\n ...collection,\n ...rest,\n access: composeAccess(collection.access ?? {}, access),\n fields: mergedFields,\n hooks: mergedHooks,\n }\n}\n"],"names":["composeAccess","applyCollectionOverride","collection","override","access","fields","hooks","rest","slug","mergedFields","defaultFields","mergedHooks","key","Object","keys","pluginHooks","overrideHooks"],"mappings":"AAIA,SAASA,aAAa,QAAQ,mBAAkB;AAEhD;;;;;;;;;;CAUC,GACD,OAAO,SAASC,wBACdC,UAA4B,EAC5BC,QAA6B;IAE7B,IAAI,CAACA,UAAU;QACb,OAAOD;IACT;IAEA,0EAA0E;IAC1E,6EAA6E;IAC7E,+DAA+D;IAC/D,MAAM,EAAEE,MAAM,EAAEC,MAAM,EAAEC,KAAK,EAAE,GAAGC,MAAM,GAAGJ;IAC3C,OAAO,AAACI,KAA4BC,IAAI;IAExC,MAAMC,eAAeJ,SAASA,OAAO;QAAEK,eAAeR,WAAWG,MAAM;IAAC,KAAKH,WAAWG,MAAM;IAE9F,oEAAoE;IACpE,MAAMM,cAAyC;QAAE,GAAGT,WAAWI,KAAK;IAAC;IACrE,IAAIA,OAAO;QACT,KAAK,MAAMM,OAAOC,OAAOC,IAAI,CAACR,OAA+D;YAC3F,MAAMS,cAAeb,WAAWI,KAAK,EAAE,CAACM,IAAI,IAAI,EAAE;YAClD,MAAMI,gBAAiBV,KAAK,CAACM,IAAI,IAAI,EAAE;YAErCD,WAAmB,CAACC,IAAI,GAAG;mBAAIG;mBAAgBC;aAAc;QACjE;IACF;IAEA,OAAO;QACL,GAAGd,UAAU;QACb,GAAGK,IAAI;QACPH,QAAQJ,cAAcE,WAAWE,MAAM,IAAI,CAAC,GAAGA;QAC/CC,QAAQI;QACRH,OAAOK;IACT;AACF"}
@@ -1,6 +1,12 @@
1
1
  import type { CollectionConfig } from 'payload';
2
2
  import type { ResolvedResourceOwnerModeConfig } from '../types.js';
3
3
  type CollectionAccess = NonNullable<CollectionConfig['access']>;
4
+ /**
5
+ * Overlay app-provided access overrides onto a base (owner-mode) access object,
6
+ * per operation. Specifying only `read` keeps the base's create/update/delete
7
+ * rules intact, instead of replacing them wholesale (review C9).
8
+ */
9
+ export declare function composeAccess(base: CollectionAccess, override: CollectionConfig['access']): CollectionAccess;
4
10
  /**
5
11
  * Access factories for Resources collection.
6
12
  * Owners may read/update/delete their own resources; anyone authenticated may create.
@@ -1,13 +1,26 @@
1
+ /**
2
+ * Overlay app-provided access overrides onto a base (owner-mode) access object,
3
+ * per operation. Specifying only `read` keeps the base's create/update/delete
4
+ * rules intact, instead of replacing them wholesale (review C9).
5
+ */ export function composeAccess(base, override) {
6
+ return {
7
+ ...base,
8
+ ...override ?? {}
9
+ };
10
+ }
1
11
  /**
2
12
  * Returns true if the requesting user is considered an "admin" for resource-owner mode:
3
13
  * - No user → deny
4
- * - adminRoles provided → user.role must be in that list
14
+ * - adminRoles provided → the user's `roleField` value must be in that list
5
15
  * - adminRoles empty → no bypass role; all authenticated users are treated as owners
6
- */ function isAdmin(user, adminRoles) {
16
+ *
17
+ * Reads the configured `roleField` (not a hardcoded `user.role`) so apps using a
18
+ * `roles: string[]` field — or any custom role field — aren't silently demoted.
19
+ */ function isAdmin(user, adminRoles, roleField) {
7
20
  if (!adminRoles.length) {
8
21
  return false;
9
22
  }
10
- const role = user.role;
23
+ const role = user[roleField];
11
24
  if (!role) {
12
25
  return false;
13
26
  }
@@ -17,13 +30,13 @@
17
30
  * Access factories for Resources collection.
18
31
  * Owners may read/update/delete their own resources; anyone authenticated may create.
19
32
  */ export function makeResourceOwnerAccess(rom) {
20
- const { adminRoles, ownerField } = rom;
33
+ const { adminRoles, ownerField, roleField } = rom;
21
34
  const ownerOrAdmin = ({ req })=>{
22
35
  if (!req.user) {
23
36
  return false;
24
37
  }
25
38
  const user = req.user;
26
- if (isAdmin(user, adminRoles)) {
39
+ if (isAdmin(user, adminRoles, roleField)) {
27
40
  return true;
28
41
  }
29
42
  return {
@@ -43,13 +56,13 @@
43
56
  * Access factories for Schedules collection.
44
57
  * A schedule's ownership is determined through its `resource.owner` relationship.
45
58
  */ export function makeScheduleOwnerAccess(rom) {
46
- const { adminRoles, ownerField } = rom;
59
+ const { adminRoles, ownerField, roleField } = rom;
47
60
  const ownerOrAdmin = ({ req })=>{
48
61
  if (!req.user) {
49
62
  return false;
50
63
  }
51
64
  const user = req.user;
52
- if (isAdmin(user, adminRoles)) {
65
+ if (isAdmin(user, adminRoles, roleField)) {
53
66
  return true;
54
67
  }
55
68
  return {
@@ -70,13 +83,13 @@
70
83
  * Resource owners can see reservations for their resources (read-only);
71
84
  * mutations are admin-only to prevent owners from unilaterally cancelling guest bookings.
72
85
  */ export function makeReservationOwnerAccess(rom) {
73
- const { adminRoles, ownerField } = rom;
86
+ const { adminRoles, ownerField, roleField } = rom;
74
87
  const readAccess = ({ req })=>{
75
88
  if (!req.user) {
76
89
  return false;
77
90
  }
78
91
  const user = req.user;
79
- if (isAdmin(user, adminRoles)) {
92
+ if (isAdmin(user, adminRoles, roleField)) {
80
93
  return true;
81
94
  }
82
95
  return {
@@ -90,7 +103,7 @@
90
103
  return false;
91
104
  }
92
105
  const user = req.user;
93
- return isAdmin(user, adminRoles);
106
+ return isAdmin(user, adminRoles, roleField);
94
107
  };
95
108
  return {
96
109
  create: adminOnly,
@@ -102,13 +115,13 @@
102
115
  /**
103
116
  * Access factories for Services collection when `ownedServices: true`.
104
117
  */ export function makeServiceOwnerAccess(rom, ownerField) {
105
- const { adminRoles } = rom;
118
+ const { adminRoles, roleField } = rom;
106
119
  const ownerOrAdmin = ({ req })=>{
107
120
  if (!req.user) {
108
121
  return false;
109
122
  }
110
123
  const user = req.user;
111
- if (isAdmin(user, adminRoles)) {
124
+ if (isAdmin(user, adminRoles, roleField)) {
112
125
  return true;
113
126
  }
114
127
  return {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utilities/ownerAccess.ts"],"sourcesContent":["import type { Access, CollectionConfig, PayloadRequest } from 'payload'\n\nimport type { ResolvedResourceOwnerModeConfig } from '../types.js'\n\ntype CollectionAccess = NonNullable<CollectionConfig['access']>\n\n/**\n * Returns true if the requesting user is considered an \"admin\" for resource-owner mode:\n * - No user → deny\n * - adminRoles provided → user.role must be in that list\n * - adminRoles empty → no bypass role; all authenticated users are treated as owners\n */\nfunction isAdmin(user: Record<string, unknown>, adminRoles: string[]): boolean {\n if (!adminRoles.length) {return false}\n const role = user.role as string | string[] | undefined\n if (!role) {return false}\n return Array.isArray(role) ? role.some((r) => adminRoles.includes(r)) : adminRoles.includes(role)\n}\n\n/**\n * Access factories for Resources collection.\n * Owners may read/update/delete their own resources; anyone authenticated may create.\n */\nexport function makeResourceOwnerAccess(rom: ResolvedResourceOwnerModeConfig): CollectionAccess {\n const { adminRoles, ownerField } = rom\n\n const ownerOrAdmin: Access = ({ req }: { req: PayloadRequest }) => {\n if (!req.user) {return false}\n const user = req.user as Record<string, unknown>\n if (isAdmin(user, adminRoles)) {return true}\n return { [ownerField]: { equals: user.id } }\n }\n\n return {\n create: ({ req }: { req: PayloadRequest }) => Boolean(req.user),\n delete: ownerOrAdmin,\n read: ownerOrAdmin,\n update: ownerOrAdmin,\n }\n}\n\n/**\n * Access factories for Schedules collection.\n * A schedule's ownership is determined through its `resource.owner` relationship.\n */\nexport function makeScheduleOwnerAccess(rom: ResolvedResourceOwnerModeConfig): CollectionAccess {\n const { adminRoles, ownerField } = rom\n\n const ownerOrAdmin: Access = ({ req }: { req: PayloadRequest }) => {\n if (!req.user) {return false}\n const user = req.user as Record<string, unknown>\n if (isAdmin(user, adminRoles)) {return true}\n return { [`resource.${ownerField}`]: { equals: user.id } }\n }\n\n return {\n create: ({ req }: { req: PayloadRequest }) => Boolean(req.user),\n delete: ownerOrAdmin,\n read: ownerOrAdmin,\n update: ownerOrAdmin,\n }\n}\n\n/**\n * Access factories for Reservations collection.\n * Resource owners can see reservations for their resources (read-only);\n * mutations are admin-only to prevent owners from unilaterally cancelling guest bookings.\n */\nexport function makeReservationOwnerAccess(\n rom: ResolvedResourceOwnerModeConfig,\n): CollectionAccess {\n const { adminRoles, ownerField } = rom\n\n const readAccess: Access = ({ req }: { req: PayloadRequest }) => {\n if (!req.user) {return false}\n const user = req.user as Record<string, unknown>\n if (isAdmin(user, adminRoles)) {return true}\n return { [`resource.${ownerField}`]: { equals: user.id } }\n }\n\n const adminOnly: Access = ({ req }: { req: PayloadRequest }) => {\n if (!req.user) {return false}\n const user = req.user as Record<string, unknown>\n return isAdmin(user, adminRoles)\n }\n\n return {\n create: adminOnly,\n delete: adminOnly,\n read: readAccess,\n update: adminOnly,\n }\n}\n\n/**\n * Access factories for Services collection when `ownedServices: true`.\n */\nexport function makeServiceOwnerAccess(\n rom: ResolvedResourceOwnerModeConfig,\n ownerField: string,\n): CollectionAccess {\n const { adminRoles } = rom\n\n const ownerOrAdmin: Access = ({ req }: { req: PayloadRequest }) => {\n if (!req.user) {return false}\n const user = req.user as Record<string, unknown>\n if (isAdmin(user, adminRoles)) {return true}\n return { [ownerField]: { equals: user.id } }\n }\n\n return {\n create: ({ req }: { req: PayloadRequest }) => Boolean(req.user),\n delete: ownerOrAdmin,\n read: ownerOrAdmin,\n update: ownerOrAdmin,\n }\n}\n"],"names":["isAdmin","user","adminRoles","length","role","Array","isArray","some","r","includes","makeResourceOwnerAccess","rom","ownerField","ownerOrAdmin","req","equals","id","create","Boolean","delete","read","update","makeScheduleOwnerAccess","makeReservationOwnerAccess","readAccess","adminOnly","makeServiceOwnerAccess"],"mappings":"AAMA;;;;;CAKC,GACD,SAASA,QAAQC,IAA6B,EAAEC,UAAoB;IAClE,IAAI,CAACA,WAAWC,MAAM,EAAE;QAAC,OAAO;IAAK;IACrC,MAAMC,OAAOH,KAAKG,IAAI;IACtB,IAAI,CAACA,MAAM;QAAC,OAAO;IAAK;IACxB,OAAOC,MAAMC,OAAO,CAACF,QAAQA,KAAKG,IAAI,CAAC,CAACC,IAAMN,WAAWO,QAAQ,CAACD,MAAMN,WAAWO,QAAQ,CAACL;AAC9F;AAEA;;;CAGC,GACD,OAAO,SAASM,wBAAwBC,GAAoC;IAC1E,MAAM,EAAET,UAAU,EAAEU,UAAU,EAAE,GAAGD;IAEnC,MAAME,eAAuB,CAAC,EAAEC,GAAG,EAA2B;QAC5D,IAAI,CAACA,IAAIb,IAAI,EAAE;YAAC,OAAO;QAAK;QAC5B,MAAMA,OAAOa,IAAIb,IAAI;QACrB,IAAID,QAAQC,MAAMC,aAAa;YAAC,OAAO;QAAI;QAC3C,OAAO;YAAE,CAACU,WAAW,EAAE;gBAAEG,QAAQd,KAAKe,EAAE;YAAC;QAAE;IAC7C;IAEA,OAAO;QACLC,QAAQ,CAAC,EAAEH,GAAG,EAA2B,GAAKI,QAAQJ,IAAIb,IAAI;QAC9DkB,QAAQN;QACRO,MAAMP;QACNQ,QAAQR;IACV;AACF;AAEA;;;CAGC,GACD,OAAO,SAASS,wBAAwBX,GAAoC;IAC1E,MAAM,EAAET,UAAU,EAAEU,UAAU,EAAE,GAAGD;IAEnC,MAAME,eAAuB,CAAC,EAAEC,GAAG,EAA2B;QAC5D,IAAI,CAACA,IAAIb,IAAI,EAAE;YAAC,OAAO;QAAK;QAC5B,MAAMA,OAAOa,IAAIb,IAAI;QACrB,IAAID,QAAQC,MAAMC,aAAa;YAAC,OAAO;QAAI;QAC3C,OAAO;YAAE,CAAC,CAAC,SAAS,EAAEU,YAAY,CAAC,EAAE;gBAAEG,QAAQd,KAAKe,EAAE;YAAC;QAAE;IAC3D;IAEA,OAAO;QACLC,QAAQ,CAAC,EAAEH,GAAG,EAA2B,GAAKI,QAAQJ,IAAIb,IAAI;QAC9DkB,QAAQN;QACRO,MAAMP;QACNQ,QAAQR;IACV;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASU,2BACdZ,GAAoC;IAEpC,MAAM,EAAET,UAAU,EAAEU,UAAU,EAAE,GAAGD;IAEnC,MAAMa,aAAqB,CAAC,EAAEV,GAAG,EAA2B;QAC1D,IAAI,CAACA,IAAIb,IAAI,EAAE;YAAC,OAAO;QAAK;QAC5B,MAAMA,OAAOa,IAAIb,IAAI;QACrB,IAAID,QAAQC,MAAMC,aAAa;YAAC,OAAO;QAAI;QAC3C,OAAO;YAAE,CAAC,CAAC,SAAS,EAAEU,YAAY,CAAC,EAAE;gBAAEG,QAAQd,KAAKe,EAAE;YAAC;QAAE;IAC3D;IAEA,MAAMS,YAAoB,CAAC,EAAEX,GAAG,EAA2B;QACzD,IAAI,CAACA,IAAIb,IAAI,EAAE;YAAC,OAAO;QAAK;QAC5B,MAAMA,OAAOa,IAAIb,IAAI;QACrB,OAAOD,QAAQC,MAAMC;IACvB;IAEA,OAAO;QACLe,QAAQQ;QACRN,QAAQM;QACRL,MAAMI;QACNH,QAAQI;IACV;AACF;AAEA;;CAEC,GACD,OAAO,SAASC,uBACdf,GAAoC,EACpCC,UAAkB;IAElB,MAAM,EAAEV,UAAU,EAAE,GAAGS;IAEvB,MAAME,eAAuB,CAAC,EAAEC,GAAG,EAA2B;QAC5D,IAAI,CAACA,IAAIb,IAAI,EAAE;YAAC,OAAO;QAAK;QAC5B,MAAMA,OAAOa,IAAIb,IAAI;QACrB,IAAID,QAAQC,MAAMC,aAAa;YAAC,OAAO;QAAI;QAC3C,OAAO;YAAE,CAACU,WAAW,EAAE;gBAAEG,QAAQd,KAAKe,EAAE;YAAC;QAAE;IAC7C;IAEA,OAAO;QACLC,QAAQ,CAAC,EAAEH,GAAG,EAA2B,GAAKI,QAAQJ,IAAIb,IAAI;QAC9DkB,QAAQN;QACRO,MAAMP;QACNQ,QAAQR;IACV;AACF"}
1
+ {"version":3,"sources":["../../src/utilities/ownerAccess.ts"],"sourcesContent":["import type { Access, CollectionConfig, PayloadRequest } from 'payload'\n\nimport type { ResolvedResourceOwnerModeConfig } from '../types.js'\n\ntype CollectionAccess = NonNullable<CollectionConfig['access']>\n\n/**\n * Overlay app-provided access overrides onto a base (owner-mode) access object,\n * per operation. Specifying only `read` keeps the base's create/update/delete\n * rules intact, instead of replacing them wholesale (review C9).\n */\nexport function composeAccess(\n base: CollectionAccess,\n override: CollectionConfig['access'],\n): CollectionAccess {\n return { ...base, ...(override ?? {}) }\n}\n\n/**\n * Returns true if the requesting user is considered an \"admin\" for resource-owner mode:\n * - No user → deny\n * - adminRoles provided → the user's `roleField` value must be in that list\n * - adminRoles empty → no bypass role; all authenticated users are treated as owners\n *\n * Reads the configured `roleField` (not a hardcoded `user.role`) so apps using a\n * `roles: string[]` field — or any custom role field — aren't silently demoted.\n */\nfunction isAdmin(user: Record<string, unknown>, adminRoles: string[], roleField: string): boolean {\n if (!adminRoles.length) {return false}\n const role = user[roleField] as string | string[] | undefined\n if (!role) {return false}\n return Array.isArray(role) ? role.some((r) => adminRoles.includes(r)) : adminRoles.includes(role)\n}\n\n/**\n * Access factories for Resources collection.\n * Owners may read/update/delete their own resources; anyone authenticated may create.\n */\nexport function makeResourceOwnerAccess(rom: ResolvedResourceOwnerModeConfig): CollectionAccess {\n const { adminRoles, ownerField, roleField } = rom\n\n const ownerOrAdmin: Access = ({ req }: { req: PayloadRequest }) => {\n if (!req.user) {return false}\n const user = req.user as Record<string, unknown>\n if (isAdmin(user, adminRoles, roleField)) {return true}\n return { [ownerField]: { equals: user.id } }\n }\n\n return {\n create: ({ req }: { req: PayloadRequest }) => Boolean(req.user),\n delete: ownerOrAdmin,\n read: ownerOrAdmin,\n update: ownerOrAdmin,\n }\n}\n\n/**\n * Access factories for Schedules collection.\n * A schedule's ownership is determined through its `resource.owner` relationship.\n */\nexport function makeScheduleOwnerAccess(rom: ResolvedResourceOwnerModeConfig): CollectionAccess {\n const { adminRoles, ownerField, roleField } = rom\n\n const ownerOrAdmin: Access = ({ req }: { req: PayloadRequest }) => {\n if (!req.user) {return false}\n const user = req.user as Record<string, unknown>\n if (isAdmin(user, adminRoles, roleField)) {return true}\n return { [`resource.${ownerField}`]: { equals: user.id } }\n }\n\n return {\n create: ({ req }: { req: PayloadRequest }) => Boolean(req.user),\n delete: ownerOrAdmin,\n read: ownerOrAdmin,\n update: ownerOrAdmin,\n }\n}\n\n/**\n * Access factories for Reservations collection.\n * Resource owners can see reservations for their resources (read-only);\n * mutations are admin-only to prevent owners from unilaterally cancelling guest bookings.\n */\nexport function makeReservationOwnerAccess(\n rom: ResolvedResourceOwnerModeConfig,\n): CollectionAccess {\n const { adminRoles, ownerField, roleField } = rom\n\n const readAccess: Access = ({ req }: { req: PayloadRequest }) => {\n if (!req.user) {return false}\n const user = req.user as Record<string, unknown>\n if (isAdmin(user, adminRoles, roleField)) {return true}\n return { [`resource.${ownerField}`]: { equals: user.id } }\n }\n\n const adminOnly: Access = ({ req }: { req: PayloadRequest }) => {\n if (!req.user) {return false}\n const user = req.user as Record<string, unknown>\n return isAdmin(user, adminRoles, roleField)\n }\n\n return {\n create: adminOnly,\n delete: adminOnly,\n read: readAccess,\n update: adminOnly,\n }\n}\n\n/**\n * Access factories for Services collection when `ownedServices: true`.\n */\nexport function makeServiceOwnerAccess(\n rom: ResolvedResourceOwnerModeConfig,\n ownerField: string,\n): CollectionAccess {\n const { adminRoles, roleField } = rom\n\n const ownerOrAdmin: Access = ({ req }: { req: PayloadRequest }) => {\n if (!req.user) {return false}\n const user = req.user as Record<string, unknown>\n if (isAdmin(user, adminRoles, roleField)) {return true}\n return { [ownerField]: { equals: user.id } }\n }\n\n return {\n create: ({ req }: { req: PayloadRequest }) => Boolean(req.user),\n delete: ownerOrAdmin,\n read: ownerOrAdmin,\n update: ownerOrAdmin,\n }\n}\n"],"names":["composeAccess","base","override","isAdmin","user","adminRoles","roleField","length","role","Array","isArray","some","r","includes","makeResourceOwnerAccess","rom","ownerField","ownerOrAdmin","req","equals","id","create","Boolean","delete","read","update","makeScheduleOwnerAccess","makeReservationOwnerAccess","readAccess","adminOnly","makeServiceOwnerAccess"],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Read-only merged view of an update: the original document overlaid with the
3
+ * incoming partial patch. Used for validation only — never assign the result
4
+ * back into `data`, or unchanged fields get written back to the database.
5
+ */
6
+ export declare function mergeReservationData(data: Record<string, unknown>, originalDoc: Record<string, unknown> | undefined): Record<string, unknown>;
7
+ /**
8
+ * True when the update patch changes any scheduling-relevant field, or moves
9
+ * status from a non-blocking value into a blocking one (re-occupying a slot).
10
+ * Key presence alone is not a change — full-document admin saves include every
11
+ * field, and a notes-only edit must not trigger re-validation.
12
+ */
13
+ export declare function schedulingFieldsChanged({ blockingStatuses, data, originalDoc, }: {
14
+ blockingStatuses: string[];
15
+ data: Record<string, unknown>;
16
+ originalDoc: Record<string, unknown> | undefined;
17
+ }): boolean;
@@ -0,0 +1,88 @@
1
+ import { extractId } from './resolveReservationItems.js';
2
+ /**
3
+ * Fields whose change means a reservation's slot occupancy may differ,
4
+ * requiring conflict re-validation and endTime recomputation on update.
5
+ */ const SCHEDULING_FIELDS = [
6
+ 'endTime',
7
+ 'guestCount',
8
+ 'items',
9
+ 'resource',
10
+ 'service',
11
+ 'startTime'
12
+ ];
13
+ /**
14
+ * Read-only merged view of an update: the original document overlaid with the
15
+ * incoming partial patch. Used for validation only — never assign the result
16
+ * back into `data`, or unchanged fields get written back to the database.
17
+ */ export function mergeReservationData(data, originalDoc) {
18
+ return {
19
+ ...originalDoc ?? {},
20
+ ...data ?? {}
21
+ };
22
+ }
23
+ function normalizeDate(value) {
24
+ if (value === null || value === undefined || value === '') {
25
+ return null;
26
+ }
27
+ const time = new Date(value).getTime();
28
+ return Number.isNaN(time) ? null : time;
29
+ }
30
+ function normalizeRelationship(value) {
31
+ const id = extractId(value);
32
+ return id === undefined ? null : String(id);
33
+ }
34
+ function itemsEqual(a, b) {
35
+ const listA = Array.isArray(a) ? a : [];
36
+ const listB = Array.isArray(b) ? b : [];
37
+ if (listA.length !== listB.length) {
38
+ return false;
39
+ }
40
+ return listA.every((itemA, i)=>{
41
+ const itemB = listB[i];
42
+ return normalizeRelationship(itemA.resource) === normalizeRelationship(itemB.resource) && normalizeRelationship(itemA.service) === normalizeRelationship(itemB.service) && normalizeDate(itemA.startTime) === normalizeDate(itemB.startTime) && normalizeDate(itemA.endTime) === normalizeDate(itemB.endTime) && (itemA.guestCount ?? null) === (itemB.guestCount ?? null);
43
+ });
44
+ }
45
+ /**
46
+ * True when the update patch changes any scheduling-relevant field, or moves
47
+ * status from a non-blocking value into a blocking one (re-occupying a slot).
48
+ * Key presence alone is not a change — full-document admin saves include every
49
+ * field, and a notes-only edit must not trigger re-validation.
50
+ */ export function schedulingFieldsChanged({ blockingStatuses, data, originalDoc }) {
51
+ if (!originalDoc) {
52
+ return true;
53
+ }
54
+ for (const field of SCHEDULING_FIELDS){
55
+ if (!(field in data)) {
56
+ continue;
57
+ }
58
+ const next = data[field];
59
+ const prev = originalDoc[field];
60
+ let changed;
61
+ switch(field){
62
+ case 'endTime':
63
+ case 'startTime':
64
+ changed = normalizeDate(next) !== normalizeDate(prev);
65
+ break;
66
+ case 'guestCount':
67
+ changed = (next ?? null) !== (prev ?? null);
68
+ break;
69
+ case 'items':
70
+ changed = !itemsEqual(next, prev);
71
+ break;
72
+ default:
73
+ changed = normalizeRelationship(next) !== normalizeRelationship(prev);
74
+ }
75
+ if (changed) {
76
+ return true;
77
+ }
78
+ }
79
+ if ('status' in data && typeof data.status === 'string') {
80
+ const prevStatus = originalDoc.status;
81
+ if (data.status !== prevStatus && blockingStatuses.includes(data.status) && (prevStatus === undefined || !blockingStatuses.includes(prevStatus))) {
82
+ return true;
83
+ }
84
+ }
85
+ return false;
86
+ }
87
+
88
+ //# sourceMappingURL=reservationChanges.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utilities/reservationChanges.ts"],"sourcesContent":["import { extractId } from './resolveReservationItems.js'\n\n/**\n * Fields whose change means a reservation's slot occupancy may differ,\n * requiring conflict re-validation and endTime recomputation on update.\n */\nconst SCHEDULING_FIELDS = [\n 'endTime',\n 'guestCount',\n 'items',\n 'resource',\n 'service',\n 'startTime',\n] as const\n\n/**\n * Read-only merged view of an update: the original document overlaid with the\n * incoming partial patch. Used for validation only — never assign the result\n * back into `data`, or unchanged fields get written back to the database.\n */\nexport function mergeReservationData(\n data: Record<string, unknown>,\n originalDoc: Record<string, unknown> | undefined,\n): Record<string, unknown> {\n return { ...(originalDoc ?? {}), ...(data ?? {}) }\n}\n\nfunction normalizeDate(value: unknown): null | number {\n if (value === null || value === undefined || value === '') {\n return null\n }\n const time = new Date(value as string).getTime()\n return Number.isNaN(time) ? null : time\n}\n\nfunction normalizeRelationship(value: unknown): null | string {\n const id = extractId(value)\n return id === undefined ? null : String(id)\n}\n\nfunction itemsEqual(a: unknown, b: unknown): boolean {\n const listA = Array.isArray(a) ? (a as Array<Record<string, unknown>>) : []\n const listB = Array.isArray(b) ? (b as Array<Record<string, unknown>>) : []\n if (listA.length !== listB.length) {\n return false\n }\n return listA.every((itemA, i) => {\n const itemB = listB[i]\n return (\n normalizeRelationship(itemA.resource) === normalizeRelationship(itemB.resource) &&\n normalizeRelationship(itemA.service) === normalizeRelationship(itemB.service) &&\n normalizeDate(itemA.startTime) === normalizeDate(itemB.startTime) &&\n normalizeDate(itemA.endTime) === normalizeDate(itemB.endTime) &&\n ((itemA.guestCount ?? null) as null | number) ===\n ((itemB.guestCount ?? null) as null | number)\n )\n })\n}\n\n/**\n * True when the update patch changes any scheduling-relevant field, or moves\n * status from a non-blocking value into a blocking one (re-occupying a slot).\n * Key presence alone is not a change — full-document admin saves include every\n * field, and a notes-only edit must not trigger re-validation.\n */\nexport function schedulingFieldsChanged({\n blockingStatuses,\n data,\n originalDoc,\n}: {\n blockingStatuses: string[]\n data: Record<string, unknown>\n originalDoc: Record<string, unknown> | undefined\n}): boolean {\n if (!originalDoc) {\n return true\n }\n\n for (const field of SCHEDULING_FIELDS) {\n if (!(field in data)) {\n continue\n }\n const next = data[field]\n const prev = originalDoc[field]\n let changed: boolean\n switch (field) {\n case 'endTime':\n case 'startTime':\n changed = normalizeDate(next) !== normalizeDate(prev)\n break\n case 'guestCount':\n changed = ((next ?? null) as null | number) !== ((prev ?? null) as null | number)\n break\n case 'items':\n changed = !itemsEqual(next, prev)\n break\n default:\n changed = normalizeRelationship(next) !== normalizeRelationship(prev)\n }\n if (changed) {\n return true\n }\n }\n\n if ('status' in data && typeof data.status === 'string') {\n const prevStatus = originalDoc.status as string | undefined\n if (\n data.status !== prevStatus &&\n blockingStatuses.includes(data.status) &&\n (prevStatus === undefined || !blockingStatuses.includes(prevStatus))\n ) {\n return true\n }\n }\n\n return false\n}\n"],"names":["extractId","SCHEDULING_FIELDS","mergeReservationData","data","originalDoc","normalizeDate","value","undefined","time","Date","getTime","Number","isNaN","normalizeRelationship","id","String","itemsEqual","a","b","listA","Array","isArray","listB","length","every","itemA","i","itemB","resource","service","startTime","endTime","guestCount","schedulingFieldsChanged","blockingStatuses","field","next","prev","changed","status","prevStatus","includes"],"mappings":"AAAA,SAASA,SAAS,QAAQ,+BAA8B;AAExD;;;CAGC,GACD,MAAMC,oBAAoB;IACxB;IACA;IACA;IACA;IACA;IACA;CACD;AAED;;;;CAIC,GACD,OAAO,SAASC,qBACdC,IAA6B,EAC7BC,WAAgD;IAEhD,OAAO;QAAE,GAAIA,eAAe,CAAC,CAAC;QAAG,GAAID,QAAQ,CAAC,CAAC;IAAE;AACnD;AAEA,SAASE,cAAcC,KAAc;IACnC,IAAIA,UAAU,QAAQA,UAAUC,aAAaD,UAAU,IAAI;QACzD,OAAO;IACT;IACA,MAAME,OAAO,IAAIC,KAAKH,OAAiBI,OAAO;IAC9C,OAAOC,OAAOC,KAAK,CAACJ,QAAQ,OAAOA;AACrC;AAEA,SAASK,sBAAsBP,KAAc;IAC3C,MAAMQ,KAAKd,UAAUM;IACrB,OAAOQ,OAAOP,YAAY,OAAOQ,OAAOD;AAC1C;AAEA,SAASE,WAAWC,CAAU,EAAEC,CAAU;IACxC,MAAMC,QAAQC,MAAMC,OAAO,CAACJ,KAAMA,IAAuC,EAAE;IAC3E,MAAMK,QAAQF,MAAMC,OAAO,CAACH,KAAMA,IAAuC,EAAE;IAC3E,IAAIC,MAAMI,MAAM,KAAKD,MAAMC,MAAM,EAAE;QACjC,OAAO;IACT;IACA,OAAOJ,MAAMK,KAAK,CAAC,CAACC,OAAOC;QACzB,MAAMC,QAAQL,KAAK,CAACI,EAAE;QACtB,OACEb,sBAAsBY,MAAMG,QAAQ,MAAMf,sBAAsBc,MAAMC,QAAQ,KAC9Ef,sBAAsBY,MAAMI,OAAO,MAAMhB,sBAAsBc,MAAME,OAAO,KAC5ExB,cAAcoB,MAAMK,SAAS,MAAMzB,cAAcsB,MAAMG,SAAS,KAChEzB,cAAcoB,MAAMM,OAAO,MAAM1B,cAAcsB,MAAMI,OAAO,KAC5D,AAAEN,CAAAA,MAAMO,UAAU,IAAI,IAAG,MACrBL,CAAAA,MAAMK,UAAU,IAAI,IAAG;IAE/B;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAASC,wBAAwB,EACtCC,gBAAgB,EAChB/B,IAAI,EACJC,WAAW,EAKZ;IACC,IAAI,CAACA,aAAa;QAChB,OAAO;IACT;IAEA,KAAK,MAAM+B,SAASlC,kBAAmB;QACrC,IAAI,CAAEkC,CAAAA,SAAShC,IAAG,GAAI;YACpB;QACF;QACA,MAAMiC,OAAOjC,IAAI,CAACgC,MAAM;QACxB,MAAME,OAAOjC,WAAW,CAAC+B,MAAM;QAC/B,IAAIG;QACJ,OAAQH;YACN,KAAK;YACL,KAAK;gBACHG,UAAUjC,cAAc+B,UAAU/B,cAAcgC;gBAChD;YACF,KAAK;gBACHC,UAAU,AAAEF,CAAAA,QAAQ,IAAG,MAA2BC,CAAAA,QAAQ,IAAG;gBAC7D;YACF,KAAK;gBACHC,UAAU,CAACtB,WAAWoB,MAAMC;gBAC5B;YACF;gBACEC,UAAUzB,sBAAsBuB,UAAUvB,sBAAsBwB;QACpE;QACA,IAAIC,SAAS;YACX,OAAO;QACT;IACF;IAEA,IAAI,YAAYnC,QAAQ,OAAOA,KAAKoC,MAAM,KAAK,UAAU;QACvD,MAAMC,aAAapC,YAAYmC,MAAM;QACrC,IACEpC,KAAKoC,MAAM,KAAKC,cAChBN,iBAAiBO,QAAQ,CAACtC,KAAKoC,MAAM,KACpCC,CAAAA,eAAejC,aAAa,CAAC2B,iBAAiBO,QAAQ,CAACD,WAAU,GAClE;YACA,OAAO;QACT;IACF;IAEA,OAAO;AACT"}
@@ -1,10 +1,12 @@
1
1
  import type { DayOfWeek } from '../types.js';
2
2
  /**
3
- * Get the DayOfWeek value for a given Date.
3
+ * @deprecated Server-TZ-dependent legacy helper no longer used by the plugin's
4
+ * own resolution path. Use timezoneUtils (getDayOfWeekFromDayKey / combineDayKeyAndTime).
4
5
  */
5
6
  export declare function getDayOfWeek(date: Date): DayOfWeek;
6
7
  /**
7
- * Check if a date string (ISO) matches a DayOfWeek.
8
+ * @deprecated Server-TZ-dependent legacy helper no longer used by the plugin's
9
+ * own resolution path. Use timezoneUtils (getDayOfWeekFromDayKey / combineDayKeyAndTime).
8
10
  */
9
11
  export declare function dateMatchesDay(date: Date, day: DayOfWeek): boolean;
10
12
  /**
@@ -15,17 +17,19 @@ export declare function parseTime(time: string): {
15
17
  minutes: number;
16
18
  };
17
19
  /**
18
- * Combine a date (day) with a HH:mm time string into a full Date.
20
+ * @deprecated Server-TZ-dependent legacy helper no longer used by the plugin's
21
+ * own resolution path. Use timezoneUtils (getDayOfWeekFromDayKey / combineDayKeyAndTime).
19
22
  */
20
23
  export declare function combineDateAndTime(date: Date, time: string): Date;
21
24
  /**
22
- * Check if a given date is an exception date in the schedule.
25
+ * Check if a calendar day is an exception day in the schedule.
23
26
  * Supports range exceptions via optional endDate (inclusive on both ends).
27
+ * `date` may be a Date instant (keyed in `timeZone`) or a YYYY-MM-DD day key.
24
28
  */
25
- export declare function isExceptionDate(date: Date, exceptions: Array<{
29
+ export declare function isExceptionDate(date: Date | string, exceptions: Array<{
26
30
  date: string;
27
31
  endDate?: string;
28
- }>): boolean;
32
+ }>, timeZone?: string): boolean;
29
33
  type RecurringSlot = {
30
34
  day: DayOfWeek;
31
35
  endTime: string;
@@ -51,7 +55,9 @@ type TimeRange = {
51
55
  start: Date;
52
56
  };
53
57
  /**
54
- * Resolve a schedule to concrete available time ranges for a given date.
58
+ * Resolve a schedule to concrete available time ranges for a calendar day.
59
+ * `date` may be a Date instant (keyed in `timeZone`) or a YYYY-MM-DD day key.
60
+ * All wall-clock times (HH:mm) are interpreted in `timeZone`.
55
61
  */
56
- export declare function resolveScheduleForDate(schedule: Schedule, date: Date): TimeRange[];
62
+ export declare function resolveScheduleForDate(schedule: Schedule, date: Date | string, timeZone?: string): TimeRange[];
57
63
  export {};