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
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import type { CollectionSlug, Config, Field } from 'payload'\n\nimport { deepMergeSimple } from 'payload/shared'\n\nimport type { ReservationPluginConfig } from './types.js'\n\nimport { createCustomersCollection } from './collections/Customers.js'\nimport { createReservationsCollection } from './collections/Reservations.js'\nimport { createResourcesCollection } from './collections/Resources.js'\nimport { createSchedulesCollection } from './collections/Schedules.js'\nimport { createServicesCollection } from './collections/Services.js'\nimport { resolveConfig } from './defaults.js'\nimport { createCancelBookingEndpoint } from './endpoints/cancelBooking.js'\nimport { createCheckAvailabilityEndpoint } from './endpoints/checkAvailability.js'\nimport { createBookingEndpoint } from './endpoints/createBooking.js'\nimport { createCustomerSearchEndpoint } from './endpoints/customerSearch.js'\nimport { createGetSlotsEndpoint } from './endpoints/getSlots.js'\nimport { translations } from './translations/index.js'\n\nexport const payloadReserve =\n (pluginOptions: ReservationPluginConfig = {}) =>\n (config: Config): Config => {\n const resolved = resolveConfig(pluginOptions)\n\n // Detect localization from the Payload config\n if (config.localization) {\n resolved.localized = true\n }\n\n if (!config.collections) {\n config.collections = []\n }\n\n if (resolved.disabled) {\n return config\n }\n\n if (resolved.userCollection) {\n // Extend the existing auth collection with customer fields\n const targetCollection = config.collections.find(\n (col) => col.slug === resolved.userCollection,\n )\n\n if (targetCollection) {\n // Collect existing field names for deduplication check\n const existingFieldNames = new Set(\n targetCollection.fields\n .map((field) => ('name' in field ? field.name : undefined))\n .filter(Boolean),\n )\n\n // Fields to inject if not already present\n const fieldsToAdd: Field[] = [\n {\n name: 'phone',\n type: 'text',\n maxLength: 50,\n },\n {\n name: 'notes',\n type: 'textarea',\n },\n {\n name: 'bookings',\n type: 'join',\n collection: resolved.slugs.reservations as unknown as CollectionSlug,\n on: 'customer',\n },\n ]\n\n for (const field of fieldsToAdd) {\n const fieldName = 'name' in field ? field.name : undefined\n if (fieldName && !existingFieldNames.has(fieldName)) {\n targetCollection.fields.push(field)\n }\n }\n }\n\n // Point the customers slug at the user collection so other parts of the\n // plugin (endpoints, hooks) reference the correct collection\n resolved.slugs.customers = resolved.userCollection\n\n // Push only the 4 domain collections (no standalone Customers)\n config.collections.push(\n createServicesCollection(resolved),\n createResourcesCollection(resolved),\n createSchedulesCollection(resolved),\n createReservationsCollection(resolved),\n )\n } else {\n // Default behaviour: push all 5 collections including standalone Customers\n config.collections.push(\n createServicesCollection(resolved),\n createResourcesCollection(resolved),\n createSchedulesCollection(resolved),\n createReservationsCollection(resolved),\n createCustomersCollection(resolved),\n )\n }\n\n // Register custom endpoints\n if (!config.endpoints) {config.endpoints = []}\n config.endpoints.push(\n createCancelBookingEndpoint(resolved),\n createCheckAvailabilityEndpoint(resolved),\n createBookingEndpoint(resolved),\n createCustomerSearchEndpoint(resolved),\n createGetSlotsEndpoint(resolved),\n )\n\n // Set up admin configuration\n if (!config.admin) {config.admin = {}}\n if (!config.admin.components) {config.admin.components = {}}\n\n // Store slugs and status machine in admin custom for component access\n if (!config.admin.custom) {config.admin.custom = {}}\n config.admin.custom.reservationSlugs = {\n ...resolved.slugs,\n }\n config.admin.custom.reservationStatusMachine = resolved.statusMachine\n\n // Add dashboard widget\n if (!config.admin.dashboard) {\n config.admin.dashboard = { widgets: [] }\n }\n if (!config.admin.dashboard.widgets) {\n config.admin.dashboard.widgets = []\n }\n config.admin.dashboard.widgets.push({\n slug: 'reservation-todays-reservations',\n Component: 'payload-reserve/rsc#DashboardWidgetServer',\n label: 'Today\\'s Reservations',\n maxWidth: 'large',\n minWidth: 'medium',\n })\n\n // Add availability overview as custom admin view\n if (!config.admin.components.views) {\n config.admin.components.views = {}\n }\n ;(config.admin.components.views as Record<string, unknown>)['reservation-availability'] = {\n Component: 'payload-reserve/client#AvailabilityOverview',\n path: '/reservation-availability',\n }\n\n // Merge plugin translations (user translations take precedence)\n config.i18n = {\n ...(config.i18n ?? {}),\n translations: deepMergeSimple(\n translations,\n (config.i18n?.translations as Record<string, Record<string, unknown>>) ?? {},\n ),\n }\n\n return config\n }\n"],"names":["deepMergeSimple","createCustomersCollection","createReservationsCollection","createResourcesCollection","createSchedulesCollection","createServicesCollection","resolveConfig","createCancelBookingEndpoint","createCheckAvailabilityEndpoint","createBookingEndpoint","createCustomerSearchEndpoint","createGetSlotsEndpoint","translations","payloadReserve","pluginOptions","config","resolved","localization","localized","collections","disabled","userCollection","targetCollection","find","col","slug","existingFieldNames","Set","fields","map","field","name","undefined","filter","Boolean","fieldsToAdd","type","maxLength","collection","slugs","reservations","on","fieldName","has","push","customers","endpoints","admin","components","custom","reservationSlugs","reservationStatusMachine","statusMachine","dashboard","widgets","Component","label","maxWidth","minWidth","views","path","i18n"],"mappings":"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"}
1
+ {"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import type { CollectionSlug, Config, Field } from 'payload'\n\nimport { deepMergeSimple } from 'payload/shared'\n\nimport type { ReservationPluginConfig } from './types.js'\n\nimport { createCustomersCollection } from './collections/Customers.js'\nimport { createReservationsCollection } from './collections/Reservations.js'\nimport { createResourcesCollection } from './collections/Resources.js'\nimport { createSchedulesCollection } from './collections/Schedules.js'\nimport { createServicesCollection } from './collections/Services.js'\nimport { resolveConfig } from './defaults.js'\nimport { createCancelBookingEndpoint } from './endpoints/cancelBooking.js'\nimport { createCheckAvailabilityEndpoint } from './endpoints/checkAvailability.js'\nimport { createBookingEndpoint } from './endpoints/createBooking.js'\nimport { createCustomerSearchEndpoint } from './endpoints/customerSearch.js'\nimport { createGetSlotsEndpoint } from './endpoints/getSlots.js'\nimport { createResourceAvailabilityEndpoint } from './endpoints/resourceAvailability.js'\nimport { provisionStaffResource } from './hooks/users/provisionStaffResource.js'\nimport { type PluginT, translations } from './translations/index.js'\n\nexport const payloadReserve =\n (pluginOptions: ReservationPluginConfig = {}) =>\n (config: Config): Config => {\n const resolved = resolveConfig(pluginOptions)\n\n // Detect localization from the Payload config\n if (config.localization) {\n resolved.localized = true\n }\n\n if (!config.collections) {\n config.collections = []\n }\n\n if (resolved.disabled) {\n return config\n }\n\n if (resolved.userCollection) {\n // Extend the existing auth collection with customer fields\n const targetCollection = config.collections.find(\n (col) => col.slug === resolved.userCollection,\n )\n\n if (targetCollection) {\n // Collect existing field names for deduplication check\n const existingFieldNames = new Set(\n targetCollection.fields\n .map((field) => ('name' in field ? field.name : undefined))\n .filter(Boolean),\n )\n\n // Fields to inject if not already present. `name` is added so that\n // admin.useAsTitle: 'name' works out of the box on the extended user\n // collection (matches the v1.0.0 behaviour documented in README/SKILL).\n const fieldsToAdd: Field[] = [\n {\n name: 'name',\n type: 'text',\n maxLength: 200,\n required: true,\n },\n {\n name: 'phone',\n type: 'text',\n maxLength: 50,\n },\n {\n name: 'notes',\n type: 'textarea',\n },\n {\n name: 'bookings',\n type: 'join',\n collection: resolved.slugs.reservations as unknown as CollectionSlug,\n on: 'customer',\n },\n ]\n\n for (const field of fieldsToAdd) {\n const fieldName = 'name' in field ? field.name : undefined\n if (fieldName && !existingFieldNames.has(fieldName)) {\n targetCollection.fields.push(field)\n }\n }\n }\n\n // Point the customers slug at the user collection so other parts of the\n // plugin (endpoints, hooks) reference the correct collection\n resolved.slugs.customers = resolved.userCollection\n\n // Push only the 4 domain collections (no standalone Customers)\n config.collections.push(\n createServicesCollection(resolved),\n createResourcesCollection(resolved),\n createSchedulesCollection(resolved),\n createReservationsCollection(resolved),\n )\n } else {\n // Default behaviour: push all 5 collections including standalone Customers\n config.collections.push(\n createServicesCollection(resolved),\n createResourcesCollection(resolved),\n createSchedulesCollection(resolved),\n createReservationsCollection(resolved),\n createCustomersCollection(resolved),\n )\n }\n\n // Register custom endpoints\n if (!config.endpoints) {config.endpoints = []}\n config.endpoints.push(\n createCancelBookingEndpoint(resolved),\n createCheckAvailabilityEndpoint(resolved),\n createBookingEndpoint(resolved),\n createCustomerSearchEndpoint(resolved),\n createGetSlotsEndpoint(resolved),\n createResourceAvailabilityEndpoint(resolved),\n )\n\n // Wire staff auto-provisioning onto the staff user collection\n if (resolved.staffProvisioning) {\n const staffUserSlug = resolved.staffProvisioning.userCollection\n const staffCollection = config.collections.find((col) => col.slug === staffUserSlug)\n if (!staffCollection) {\n throw new Error(\n `staffProvisioning.userCollection \"${staffUserSlug}\" was not found in config.collections`,\n )\n }\n staffCollection.hooks = {\n ...staffCollection.hooks,\n afterChange: [\n ...(staffCollection.hooks?.afterChange ?? []),\n provisionStaffResource(resolved),\n ],\n }\n }\n\n // Set up admin configuration\n if (!config.admin) {config.admin = {}}\n if (!config.admin.components) {config.admin.components = {}}\n\n // Store slugs and status machine in admin custom for component access\n if (!config.admin.custom) {config.admin.custom = {}}\n config.admin.custom.reservationSlugs = {\n ...resolved.slugs,\n }\n config.admin.custom.reservationStatusMachine = resolved.statusMachine\n\n // Add dashboard widget\n if (!config.admin.dashboard) {\n config.admin.dashboard = { widgets: [] }\n }\n if (!config.admin.dashboard.widgets) {\n config.admin.dashboard.widgets = []\n }\n config.admin.dashboard.widgets.push({\n slug: 'reservation-todays-reservations',\n Component: 'payload-reserve/rsc#DashboardWidgetServer',\n label: ({ t }) => (t as PluginT)('reservation:dashboardTitle'),\n maxWidth: 'large',\n minWidth: 'medium',\n })\n\n // Add availability overview as custom admin view\n if (!config.admin.components.views) {\n config.admin.components.views = {}\n }\n ;(config.admin.components.views as Record<string, unknown>)['reservation-availability'] = {\n Component: 'payload-reserve/client#AvailabilityOverview',\n path: '/reservation-availability',\n }\n\n // Merge plugin translations (user translations take precedence)\n config.i18n = {\n ...(config.i18n ?? {}),\n translations: deepMergeSimple(\n translations,\n (config.i18n?.translations as Record<string, Record<string, unknown>>) ?? {},\n ),\n }\n\n return config\n }\n"],"names":["deepMergeSimple","createCustomersCollection","createReservationsCollection","createResourcesCollection","createSchedulesCollection","createServicesCollection","resolveConfig","createCancelBookingEndpoint","createCheckAvailabilityEndpoint","createBookingEndpoint","createCustomerSearchEndpoint","createGetSlotsEndpoint","createResourceAvailabilityEndpoint","provisionStaffResource","translations","payloadReserve","pluginOptions","config","resolved","localization","localized","collections","disabled","userCollection","targetCollection","find","col","slug","existingFieldNames","Set","fields","map","field","name","undefined","filter","Boolean","fieldsToAdd","type","maxLength","required","collection","slugs","reservations","on","fieldName","has","push","customers","endpoints","staffProvisioning","staffUserSlug","staffCollection","Error","hooks","afterChange","admin","components","custom","reservationSlugs","reservationStatusMachine","statusMachine","dashboard","widgets","Component","label","t","maxWidth","minWidth","views","path","i18n"],"mappings":"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"}
@@ -13,8 +13,8 @@ export declare function buildOverlapQuery(params: {
13
13
  blockingStatuses: string[];
14
14
  effectiveEnd: Date;
15
15
  effectiveStart: Date;
16
- excludeReservationId?: string;
17
- resourceId: string;
16
+ excludeReservationId?: number | string;
17
+ resourceId: number | string;
18
18
  }): Where;
19
19
  export declare function isBlockingStatus(status: string, statusMachine: StatusMachineConfig): boolean;
20
20
  export declare function validateTransition(fromStatus: string, toStatus: string, statusMachine: StatusMachineConfig): {
@@ -26,12 +26,12 @@ export declare function checkAvailability(params: {
26
26
  bufferAfter: number;
27
27
  bufferBefore: number;
28
28
  endTime: Date;
29
- excludeReservationId?: string;
29
+ excludeReservationId?: number | string;
30
30
  guestCount: number;
31
31
  payload: Payload;
32
32
  req: PayloadRequest;
33
33
  reservationSlug: string;
34
- resourceId: string;
34
+ resourceId: number | string;
35
35
  resourceSlug: string;
36
36
  startTime: Date;
37
37
  }): Promise<{
@@ -47,10 +47,11 @@ export declare function getAvailableSlots(params: {
47
47
  payload: Payload;
48
48
  req: PayloadRequest;
49
49
  reservationSlug: string;
50
- resourceId: string;
50
+ resourceId?: number | string;
51
+ resourceIds?: Array<number | string>;
51
52
  resourceSlug: string;
52
53
  scheduleSlug: string;
53
- serviceId: string;
54
+ serviceId: number | string;
54
55
  serviceSlug: string;
55
56
  }): Promise<Array<{
56
57
  end: Date;
@@ -1,5 +1,5 @@
1
1
  import { resolveScheduleForDate } from '../utilities/scheduleUtils.js';
2
- import { addMinutes, computeBlockedWindow } from '../utilities/slotUtils.js';
2
+ import { addMinutes, computeBlockedWindow, intersectIntervals } from '../utilities/slotUtils.js';
3
3
  // --- Pure functions (no DB) ---
4
4
  export function computeEndTime(params) {
5
5
  const { durationType, serviceDuration, startTime } = params;
@@ -29,11 +29,6 @@ export function computeEndTime(params) {
29
29
  export function buildOverlapQuery(params) {
30
30
  const { blockingStatuses, effectiveEnd, effectiveStart, excludeReservationId, resourceId } = params;
31
31
  const conditions = [
32
- {
33
- resource: {
34
- equals: resourceId
35
- }
36
- },
37
32
  {
38
33
  status: {
39
34
  in: blockingStatuses
@@ -48,6 +43,20 @@ export function buildOverlapQuery(params) {
48
43
  endTime: {
49
44
  greater_than: effectiveStart.toISOString()
50
45
  }
46
+ },
47
+ {
48
+ or: [
49
+ {
50
+ resource: {
51
+ equals: resourceId
52
+ }
53
+ },
54
+ {
55
+ 'items.resource': {
56
+ equals: resourceId
57
+ }
58
+ }
59
+ ]
51
60
  }
52
61
  ];
53
62
  if (excludeReservationId) {
@@ -142,8 +151,15 @@ export async function checkAvailability(params) {
142
151
  };
143
152
  }
144
153
  export async function getAvailableSlots(params) {
145
- const { blockingStatuses, date, guestCount, payload, req, reservationSlug, resourceId, resourceSlug, scheduleSlug, serviceId, serviceSlug } = params;
146
- // 1. Fetch service for duration + buffer times
154
+ const { blockingStatuses, date, guestCount, payload, req, reservationSlug, resourceId, resourceIds, resourceSlug, scheduleSlug, serviceId, serviceSlug } = params;
155
+ // Resolve the set of resources to intersect (single-resource callers still work)
156
+ const ids = resourceIds && resourceIds.length > 0 ? resourceIds : resourceId !== undefined ? [
157
+ resourceId
158
+ ] : [];
159
+ if (ids.length === 0) {
160
+ return [];
161
+ }
162
+ // 1. Service for duration + buffer times (from the primary service)
147
163
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
148
164
  const service = await payload.findByID({
149
165
  id: serviceId,
@@ -155,38 +171,54 @@ export async function getAvailableSlots(params) {
155
171
  const bufferBefore = service.bufferTimeBefore ?? 0;
156
172
  const bufferAfter = service.bufferTimeAfter ?? 0;
157
173
  const durationType = service.durationType ?? 'fixed';
158
- // 2. Fetch resource's schedules for the date
159
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
160
- const { docs: schedules } = await payload.find({
161
- collection: scheduleSlug,
162
- depth: 0,
163
- limit: 100,
164
- req,
165
- where: {
166
- and: [
167
- {
168
- resource: {
169
- equals: resourceId
170
- }
171
- },
172
- {
173
- active: {
174
- equals: true
174
+ // 2. Per resource: fetch schedules and resolve to windows. A resource with >=1
175
+ // schedule is "schedule-bearing" and constrains time; a resource with zero
176
+ // schedules is capacity-only and contributes no time windows.
177
+ const scheduleBearingWindowLists = [];
178
+ for (const rid of ids){
179
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
180
+ const { docs: schedules } = await payload.find({
181
+ collection: scheduleSlug,
182
+ depth: 0,
183
+ limit: 100,
184
+ req,
185
+ where: {
186
+ and: [
187
+ {
188
+ resource: {
189
+ equals: rid
190
+ }
191
+ },
192
+ {
193
+ active: {
194
+ equals: true
195
+ }
175
196
  }
176
- }
177
- ]
197
+ ]
198
+ }
199
+ });
200
+ if (!schedules || schedules.length === 0) {
201
+ continue;
178
202
  }
179
- });
180
- // 3. Resolve schedules to time ranges for the date
181
- const timeRanges = [];
182
- for (const schedule of schedules){
183
- const ranges = resolveScheduleForDate(schedule, date);
184
- timeRanges.push(...ranges);
203
+ const windows = [];
204
+ for (const schedule of schedules){
205
+ windows.push(...resolveScheduleForDate(schedule, date));
206
+ }
207
+ scheduleBearingWindowLists.push(windows);
208
+ }
209
+ // No resource constrains time → no basis for generating slots
210
+ if (scheduleBearingWindowLists.length === 0) {
211
+ return [];
212
+ }
213
+ // 3. Intersect all schedule-bearing window lists
214
+ let timeRanges = scheduleBearingWindowLists[0];
215
+ for(let i = 1; i < scheduleBearingWindowLists.length; i++){
216
+ timeRanges = intersectIntervals(timeRanges, scheduleBearingWindowLists[i]);
185
217
  }
186
218
  if (timeRanges.length === 0) {
187
219
  return [];
188
220
  }
189
- // 4. Generate candidate slots from schedule ranges
221
+ // 4. Candidate slot sizing
190
222
  const { endTime: slotEndOffset } = computeEndTime({
191
223
  durationType,
192
224
  serviceDuration: duration,
@@ -194,24 +226,33 @@ export async function getAvailableSlots(params) {
194
226
  });
195
227
  const slotDuration = Math.round(slotEndOffset.getTime() / 60_000);
196
228
  const effectiveDuration = durationType === 'fixed' ? duration : slotDuration;
197
- const availableSlots = [];
198
- // For full-day services, offer the entire range as a single slot per range
199
- if (durationType === 'full-day') {
200
- for (const range of timeRanges){
229
+ // Helper: a window is available only if EVERY required resource is free
230
+ const allAvailable = async (start, end, bBefore, bAfter)=>{
231
+ for (const rid of ids){
201
232
  const result = await checkAvailability({
202
233
  blockingStatuses,
203
- bufferAfter: 0,
204
- bufferBefore: 0,
205
- endTime: range.end,
234
+ bufferAfter: bAfter,
235
+ bufferBefore: bBefore,
236
+ endTime: end,
206
237
  guestCount: guestCount ?? 1,
207
238
  payload,
208
239
  req,
209
240
  reservationSlug,
210
- resourceId,
241
+ resourceId: rid,
211
242
  resourceSlug,
212
- startTime: range.start
243
+ startTime: start
213
244
  });
214
- if (result.available) {
245
+ if (!result.available) {
246
+ return false;
247
+ }
248
+ }
249
+ return true;
250
+ };
251
+ const availableSlots = [];
252
+ // Full-day: offer each range as a single slot if all resources are free
253
+ if (durationType === 'full-day') {
254
+ for (const range of timeRanges){
255
+ if (await allAvailable(range.start, range.end, 0, 0)) {
215
256
  availableSlots.push({
216
257
  end: range.end,
217
258
  start: range.start
@@ -220,7 +261,6 @@ export async function getAvailableSlots(params) {
220
261
  }
221
262
  return availableSlots;
222
263
  }
223
- // Step by a smaller increment to catch slots between buffer gaps
224
264
  const stepSize = Math.min(effectiveDuration, 15);
225
265
  for (const range of timeRanges){
226
266
  let candidateStart = new Date(range.start);
@@ -229,21 +269,7 @@ export async function getAvailableSlots(params) {
229
269
  if (candidateEnd > range.end) {
230
270
  break;
231
271
  }
232
- // 5. Check availability for each candidate slot
233
- const result = await checkAvailability({
234
- blockingStatuses,
235
- bufferAfter,
236
- bufferBefore,
237
- endTime: candidateEnd,
238
- guestCount: guestCount ?? 1,
239
- payload,
240
- req,
241
- reservationSlug,
242
- resourceId,
243
- resourceSlug,
244
- startTime: candidateStart
245
- });
246
- if (result.available) {
272
+ if (await allAvailable(candidateStart, candidateEnd, bufferBefore, bufferAfter)) {
247
273
  availableSlots.push({
248
274
  end: candidateEnd,
249
275
  start: new Date(candidateStart)
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/services/AvailabilityService.ts"],"sourcesContent":["import type { Payload, PayloadRequest, Where } from 'payload'\n\nimport type { CapacityMode, DurationType, StatusMachineConfig } from '../types.js'\n\nimport { resolveScheduleForDate } from '../utilities/scheduleUtils.js'\nimport { addMinutes, computeBlockedWindow } from '../utilities/slotUtils.js'\n\n// --- Pure functions (no DB) ---\n\nexport function computeEndTime(params: {\n durationType: DurationType\n endTime?: Date\n serviceDuration: number\n startTime: Date\n}): { durationMinutes: number; endTime: Date } {\n const { durationType, serviceDuration, startTime } = params\n\n if (durationType === 'full-day') {\n const end = new Date(startTime)\n end.setHours(23, 59, 59, 999)\n const durationMinutes = Math.round((end.getTime() - startTime.getTime()) / 60_000)\n return { durationMinutes, endTime: end }\n }\n\n if (durationType === 'flexible' && params.endTime) {\n const durationMinutes = Math.round(\n (params.endTime.getTime() - startTime.getTime()) / 60_000,\n )\n return { durationMinutes, endTime: params.endTime }\n }\n\n // fixed duration (default)\n const endTime = addMinutes(startTime, serviceDuration)\n return { durationMinutes: serviceDuration, endTime }\n}\n\nexport function buildOverlapQuery(params: {\n blockingStatuses: string[]\n effectiveEnd: Date\n effectiveStart: Date\n excludeReservationId?: string\n resourceId: string\n}): Where {\n const { blockingStatuses, effectiveEnd, effectiveStart, excludeReservationId, resourceId } =\n params\n\n const conditions: Where[] = [\n { resource: { equals: resourceId } },\n { status: { in: blockingStatuses } },\n { startTime: { less_than: effectiveEnd.toISOString() } },\n { endTime: { greater_than: effectiveStart.toISOString() } },\n ]\n\n if (excludeReservationId) {\n conditions.push({ id: { not_equals: excludeReservationId } })\n }\n\n return { and: conditions }\n}\n\nexport function isBlockingStatus(\n status: string,\n statusMachine: StatusMachineConfig,\n): boolean {\n return statusMachine.blockingStatuses.includes(status)\n}\n\nexport function validateTransition(\n fromStatus: string,\n toStatus: string,\n statusMachine: StatusMachineConfig,\n): { reason?: string; valid: boolean } {\n const allowed = statusMachine.transitions[fromStatus]\n if (!allowed) {\n return { reason: `Unknown status: ${fromStatus}`, valid: false }\n }\n if (!allowed.includes(toStatus)) {\n return {\n reason: `Cannot transition from \"${fromStatus}\" to \"${toStatus}\"`,\n valid: false,\n }\n }\n return { valid: true }\n}\n\n// --- DB functions (use Payload Local API only) ---\n\nexport async function checkAvailability(params: {\n blockingStatuses: string[]\n bufferAfter: number\n bufferBefore: number\n endTime: Date\n excludeReservationId?: string\n guestCount: number\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId: string\n resourceSlug: string\n startTime: Date\n}): Promise<{\n available: boolean\n currentCount: number\n reason?: string\n totalCapacity: number\n}> {\n const {\n blockingStatuses,\n bufferAfter,\n bufferBefore,\n endTime,\n excludeReservationId,\n guestCount,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n startTime,\n } = params\n\n // Fetch resource for quantity and capacity mode\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const resource = await (payload.findByID as any)({\n id: resourceId,\n collection: resourceSlug,\n depth: 0,\n req,\n })\n const quantity = (resource.quantity as number) ?? 1\n const capacityMode = ((resource.capacityMode as string) ?? 'per-reservation') as CapacityMode\n\n // Compute effective window with buffers\n const { effectiveEnd, effectiveStart } = computeBlockedWindow(\n startTime,\n endTime,\n bufferBefore,\n bufferAfter,\n )\n\n // Build overlap query\n const where = buildOverlapQuery({\n blockingStatuses,\n effectiveEnd,\n effectiveStart,\n excludeReservationId,\n resourceId,\n })\n\n if (capacityMode === 'per-guest') {\n // Must fetch docs to sum guestCount\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs } = await (payload.find as any)({\n collection: reservationSlug,\n depth: 0,\n limit: 0,\n req,\n select: { guestCount: true },\n where,\n })\n const currentGuests = docs.reduce(\n (sum: number, doc: Record<string, unknown>) => sum + ((doc.guestCount as number) ?? 1),\n 0,\n )\n return {\n available: currentGuests + guestCount <= quantity,\n currentCount: currentGuests,\n reason:\n currentGuests + guestCount > quantity ? 'Guest capacity exceeded' : undefined,\n totalCapacity: quantity,\n }\n }\n\n // per-reservation mode: count is sufficient\n // TODO: batch queries — linear per-item cost acceptable for 2-5 items\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { totalDocs } = await (payload.count as any)({\n collection: reservationSlug,\n req,\n where,\n })\n return {\n available: totalDocs + 1 <= quantity,\n currentCount: totalDocs,\n reason: totalDocs + 1 > quantity ? 'All units are booked for this time' : undefined,\n totalCapacity: quantity,\n }\n}\n\nexport async function getAvailableSlots(params: {\n blockingStatuses: string[]\n date: Date\n guestCount?: number\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId: string\n resourceSlug: string\n scheduleSlug: string\n serviceId: string\n serviceSlug: string\n}): Promise<Array<{ end: Date; start: Date }>> {\n const {\n blockingStatuses,\n date,\n guestCount,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n scheduleSlug,\n serviceId,\n serviceSlug,\n } = params\n\n // 1. Fetch service for duration + buffer times\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (payload.findByID as any)({\n id: serviceId,\n collection: serviceSlug,\n depth: 0,\n req,\n })\n const duration = (service.duration as number) ?? 60\n const bufferBefore = (service.bufferTimeBefore as number) ?? 0\n const bufferAfter = (service.bufferTimeAfter as number) ?? 0\n const durationType = ((service.durationType as string) ?? 'fixed') as DurationType\n\n // 2. Fetch resource's schedules for the date\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs: schedules } = await (payload.find as any)({\n collection: scheduleSlug,\n depth: 0,\n limit: 100,\n req,\n where: {\n and: [{ resource: { equals: resourceId } }, { active: { equals: true } }],\n },\n })\n\n // 3. Resolve schedules to time ranges for the date\n const timeRanges: Array<{ end: Date; start: Date }> = []\n for (const schedule of schedules) {\n const ranges = resolveScheduleForDate(\n schedule as unknown as Parameters<typeof resolveScheduleForDate>[0],\n date,\n )\n timeRanges.push(...ranges)\n }\n\n if (timeRanges.length === 0) {\n return []\n }\n\n // 4. Generate candidate slots from schedule ranges\n const { endTime: slotEndOffset } = computeEndTime({\n durationType,\n serviceDuration: duration,\n startTime: new Date(0),\n })\n const slotDuration = Math.round(slotEndOffset.getTime() / 60_000)\n const effectiveDuration = durationType === 'fixed' ? duration : slotDuration\n\n const availableSlots: Array<{ end: Date; start: Date }> = []\n\n // For full-day services, offer the entire range as a single slot per range\n if (durationType === 'full-day') {\n for (const range of timeRanges) {\n const result = await checkAvailability({\n blockingStatuses,\n bufferAfter: 0,\n bufferBefore: 0,\n endTime: range.end,\n guestCount: guestCount ?? 1,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n startTime: range.start,\n })\n if (result.available) {\n availableSlots.push({ end: range.end, start: range.start })\n }\n }\n return availableSlots\n }\n\n // Step by a smaller increment to catch slots between buffer gaps\n const stepSize = Math.min(effectiveDuration, 15)\n\n for (const range of timeRanges) {\n let candidateStart = new Date(range.start)\n\n while (true) {\n const candidateEnd = addMinutes(candidateStart, effectiveDuration)\n if (candidateEnd > range.end) {break}\n\n // 5. Check availability for each candidate slot\n const result = await checkAvailability({\n blockingStatuses,\n bufferAfter,\n bufferBefore,\n endTime: candidateEnd,\n guestCount: guestCount ?? 1,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n startTime: candidateStart,\n })\n\n if (result.available) {\n availableSlots.push({ end: candidateEnd, start: new Date(candidateStart) })\n }\n\n candidateStart = addMinutes(candidateStart, stepSize)\n }\n }\n\n return availableSlots\n}\n"],"names":["resolveScheduleForDate","addMinutes","computeBlockedWindow","computeEndTime","params","durationType","serviceDuration","startTime","end","Date","setHours","durationMinutes","Math","round","getTime","endTime","buildOverlapQuery","blockingStatuses","effectiveEnd","effectiveStart","excludeReservationId","resourceId","conditions","resource","equals","status","in","less_than","toISOString","greater_than","push","id","not_equals","and","isBlockingStatus","statusMachine","includes","validateTransition","fromStatus","toStatus","allowed","transitions","reason","valid","checkAvailability","bufferAfter","bufferBefore","guestCount","payload","req","reservationSlug","resourceSlug","findByID","collection","depth","quantity","capacityMode","where","docs","find","limit","select","currentGuests","reduce","sum","doc","available","currentCount","undefined","totalCapacity","totalDocs","count","getAvailableSlots","date","scheduleSlug","serviceId","serviceSlug","service","duration","bufferTimeBefore","bufferTimeAfter","schedules","active","timeRanges","schedule","ranges","length","slotEndOffset","slotDuration","effectiveDuration","availableSlots","range","result","start","stepSize","min","candidateStart","candidateEnd"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/services/AvailabilityService.ts"],"sourcesContent":["import type { Payload, PayloadRequest, Where } from 'payload'\n\nimport type { CapacityMode, DurationType, StatusMachineConfig } from '../types.js'\n\nimport { resolveScheduleForDate } from '../utilities/scheduleUtils.js'\nimport { addMinutes, computeBlockedWindow, intersectIntervals } from '../utilities/slotUtils.js'\n\n// --- Pure functions (no DB) ---\n\nexport function computeEndTime(params: {\n durationType: DurationType\n endTime?: Date\n serviceDuration: number\n startTime: Date\n}): { durationMinutes: number; endTime: Date } {\n const { durationType, serviceDuration, startTime } = params\n\n if (durationType === 'full-day') {\n const end = new Date(startTime)\n end.setHours(23, 59, 59, 999)\n const durationMinutes = Math.round((end.getTime() - startTime.getTime()) / 60_000)\n return { durationMinutes, endTime: end }\n }\n\n if (durationType === 'flexible' && params.endTime) {\n const durationMinutes = Math.round(\n (params.endTime.getTime() - startTime.getTime()) / 60_000,\n )\n return { durationMinutes, endTime: params.endTime }\n }\n\n // fixed duration (default)\n const endTime = addMinutes(startTime, serviceDuration)\n return { durationMinutes: serviceDuration, endTime }\n}\n\nexport function buildOverlapQuery(params: {\n blockingStatuses: string[]\n effectiveEnd: Date\n effectiveStart: Date\n excludeReservationId?: number | string\n resourceId: number | string\n}): Where {\n const { blockingStatuses, effectiveEnd, effectiveStart, excludeReservationId, resourceId } =\n params\n\n const conditions: Where[] = [\n { status: { in: blockingStatuses } },\n { startTime: { less_than: effectiveEnd.toISOString() } },\n { endTime: { greater_than: effectiveStart.toISOString() } },\n {\n or: [\n { resource: { equals: resourceId } },\n { 'items.resource': { equals: resourceId } },\n ],\n },\n ]\n\n if (excludeReservationId) {\n conditions.push({ id: { not_equals: excludeReservationId } })\n }\n\n return { and: conditions }\n}\n\nexport function isBlockingStatus(\n status: string,\n statusMachine: StatusMachineConfig,\n): boolean {\n return statusMachine.blockingStatuses.includes(status)\n}\n\nexport function validateTransition(\n fromStatus: string,\n toStatus: string,\n statusMachine: StatusMachineConfig,\n): { reason?: string; valid: boolean } {\n const allowed = statusMachine.transitions[fromStatus]\n if (!allowed) {\n return { reason: `Unknown status: ${fromStatus}`, valid: false }\n }\n if (!allowed.includes(toStatus)) {\n return {\n reason: `Cannot transition from \"${fromStatus}\" to \"${toStatus}\"`,\n valid: false,\n }\n }\n return { valid: true }\n}\n\n// --- DB functions (use Payload Local API only) ---\n\nexport async function checkAvailability(params: {\n blockingStatuses: string[]\n bufferAfter: number\n bufferBefore: number\n endTime: Date\n excludeReservationId?: number | string\n guestCount: number\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId: number | string\n resourceSlug: string\n startTime: Date\n}): Promise<{\n available: boolean\n currentCount: number\n reason?: string\n totalCapacity: number\n}> {\n const {\n blockingStatuses,\n bufferAfter,\n bufferBefore,\n endTime,\n excludeReservationId,\n guestCount,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n startTime,\n } = params\n\n // Fetch resource for quantity and capacity mode\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const resource = await (payload.findByID as any)({\n id: resourceId,\n collection: resourceSlug,\n depth: 0,\n req,\n })\n const quantity = (resource.quantity as number) ?? 1\n const capacityMode = ((resource.capacityMode as string) ?? 'per-reservation') as CapacityMode\n\n // Compute effective window with buffers\n const { effectiveEnd, effectiveStart } = computeBlockedWindow(\n startTime,\n endTime,\n bufferBefore,\n bufferAfter,\n )\n\n // Build overlap query\n const where = buildOverlapQuery({\n blockingStatuses,\n effectiveEnd,\n effectiveStart,\n excludeReservationId,\n resourceId,\n })\n\n if (capacityMode === 'per-guest') {\n // Must fetch docs to sum guestCount\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs } = await (payload.find as any)({\n collection: reservationSlug,\n depth: 0,\n limit: 0,\n req,\n select: { guestCount: true },\n where,\n })\n const currentGuests = docs.reduce(\n (sum: number, doc: Record<string, unknown>) => sum + ((doc.guestCount as number) ?? 1),\n 0,\n )\n return {\n available: currentGuests + guestCount <= quantity,\n currentCount: currentGuests,\n reason:\n currentGuests + guestCount > quantity ? 'Guest capacity exceeded' : undefined,\n totalCapacity: quantity,\n }\n }\n\n // per-reservation mode: count is sufficient\n // TODO: batch queries — linear per-item cost acceptable for 2-5 items\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { totalDocs } = await (payload.count as any)({\n collection: reservationSlug,\n req,\n where,\n })\n return {\n available: totalDocs + 1 <= quantity,\n currentCount: totalDocs,\n reason: totalDocs + 1 > quantity ? 'All units are booked for this time' : undefined,\n totalCapacity: quantity,\n }\n}\n\nexport async function getAvailableSlots(params: {\n blockingStatuses: string[]\n date: Date\n guestCount?: number\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId?: number | string\n resourceIds?: Array<number | string>\n resourceSlug: string\n scheduleSlug: string\n serviceId: number | string\n serviceSlug: string\n}): Promise<Array<{ end: Date; start: Date }>> {\n const {\n blockingStatuses,\n date,\n guestCount,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceIds,\n resourceSlug,\n scheduleSlug,\n serviceId,\n serviceSlug,\n } = params\n\n // Resolve the set of resources to intersect (single-resource callers still work)\n const ids =\n resourceIds && resourceIds.length > 0\n ? resourceIds\n : resourceId !== undefined\n ? [resourceId]\n : []\n if (ids.length === 0) {\n return []\n }\n\n // 1. Service for duration + buffer times (from the primary service)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (payload.findByID as any)({\n id: serviceId,\n collection: serviceSlug,\n depth: 0,\n req,\n })\n const duration = (service.duration as number) ?? 60\n const bufferBefore = (service.bufferTimeBefore as number) ?? 0\n const bufferAfter = (service.bufferTimeAfter as number) ?? 0\n const durationType = ((service.durationType as string) ?? 'fixed') as DurationType\n\n // 2. Per resource: fetch schedules and resolve to windows. A resource with >=1\n // schedule is \"schedule-bearing\" and constrains time; a resource with zero\n // schedules is capacity-only and contributes no time windows.\n const scheduleBearingWindowLists: Array<Array<{ end: Date; start: Date }>> = []\n for (const rid of ids) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs: schedules } = await (payload.find as any)({\n collection: scheduleSlug,\n depth: 0,\n limit: 100,\n req,\n where: {\n and: [{ resource: { equals: rid } }, { active: { equals: true } }],\n },\n })\n if (!schedules || schedules.length === 0) {\n continue\n }\n const windows: Array<{ end: Date; start: Date }> = []\n for (const schedule of schedules) {\n windows.push(\n ...resolveScheduleForDate(\n schedule as unknown as Parameters<typeof resolveScheduleForDate>[0],\n date,\n ),\n )\n }\n scheduleBearingWindowLists.push(windows)\n }\n\n // No resource constrains time → no basis for generating slots\n if (scheduleBearingWindowLists.length === 0) {\n return []\n }\n\n // 3. Intersect all schedule-bearing window lists\n let timeRanges = scheduleBearingWindowLists[0]\n for (let i = 1; i < scheduleBearingWindowLists.length; i++) {\n timeRanges = intersectIntervals(timeRanges, scheduleBearingWindowLists[i])\n }\n if (timeRanges.length === 0) {\n return []\n }\n\n // 4. Candidate slot sizing\n const { endTime: slotEndOffset } = computeEndTime({\n durationType,\n serviceDuration: duration,\n startTime: new Date(0),\n })\n const slotDuration = Math.round(slotEndOffset.getTime() / 60_000)\n const effectiveDuration = durationType === 'fixed' ? duration : slotDuration\n\n // Helper: a window is available only if EVERY required resource is free\n const allAvailable = async (\n start: Date,\n end: Date,\n bBefore: number,\n bAfter: number,\n ): Promise<boolean> => {\n for (const rid of ids) {\n const result = await checkAvailability({\n blockingStatuses,\n bufferAfter: bAfter,\n bufferBefore: bBefore,\n endTime: end,\n guestCount: guestCount ?? 1,\n payload,\n req,\n reservationSlug,\n resourceId: rid,\n resourceSlug,\n startTime: start,\n })\n if (!result.available) {\n return false\n }\n }\n return true\n }\n\n const availableSlots: Array<{ end: Date; start: Date }> = []\n\n // Full-day: offer each range as a single slot if all resources are free\n if (durationType === 'full-day') {\n for (const range of timeRanges) {\n if (await allAvailable(range.start, range.end, 0, 0)) {\n availableSlots.push({ end: range.end, start: range.start })\n }\n }\n return availableSlots\n }\n\n const stepSize = Math.min(effectiveDuration, 15)\n\n for (const range of timeRanges) {\n let candidateStart = new Date(range.start)\n\n while (true) {\n const candidateEnd = addMinutes(candidateStart, effectiveDuration)\n if (candidateEnd > range.end) {\n break\n }\n\n if (await allAvailable(candidateStart, candidateEnd, bufferBefore, bufferAfter)) {\n availableSlots.push({ end: candidateEnd, start: new Date(candidateStart) })\n }\n\n candidateStart = addMinutes(candidateStart, stepSize)\n }\n }\n\n return availableSlots\n}\n"],"names":["resolveScheduleForDate","addMinutes","computeBlockedWindow","intersectIntervals","computeEndTime","params","durationType","serviceDuration","startTime","end","Date","setHours","durationMinutes","Math","round","getTime","endTime","buildOverlapQuery","blockingStatuses","effectiveEnd","effectiveStart","excludeReservationId","resourceId","conditions","status","in","less_than","toISOString","greater_than","or","resource","equals","push","id","not_equals","and","isBlockingStatus","statusMachine","includes","validateTransition","fromStatus","toStatus","allowed","transitions","reason","valid","checkAvailability","bufferAfter","bufferBefore","guestCount","payload","req","reservationSlug","resourceSlug","findByID","collection","depth","quantity","capacityMode","where","docs","find","limit","select","currentGuests","reduce","sum","doc","available","currentCount","undefined","totalCapacity","totalDocs","count","getAvailableSlots","date","resourceIds","scheduleSlug","serviceId","serviceSlug","ids","length","service","duration","bufferTimeBefore","bufferTimeAfter","scheduleBearingWindowLists","rid","schedules","active","windows","schedule","timeRanges","i","slotEndOffset","slotDuration","effectiveDuration","allAvailable","start","bBefore","bAfter","result","availableSlots","range","stepSize","min","candidateStart","candidateEnd"],"mappings":"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"}
@@ -0,0 +1,156 @@
1
+ {
2
+ "collectionServices": "الخدمات",
3
+ "collectionResources": "الموارد",
4
+ "collectionSchedules": "الجداول الزمنية",
5
+ "collectionReservations": "الحجوزات",
6
+ "collectionCustomers": "العملاء",
7
+ "collectionCustomer": "العميل",
8
+ "fieldFirstName": "الاسم الأول",
9
+ "fieldLastName": "اسم العائلة",
10
+ "fieldName": "الاسم",
11
+ "fieldDescription": "الوصف",
12
+ "fieldImage": "الصورة",
13
+ "fieldPrice": "السعر",
14
+ "fieldActive": "نشط",
15
+ "fieldServices": "الخدمات",
16
+ "fieldResource": "المورد",
17
+ "fieldService": "الخدمة",
18
+ "fieldCustomer": "العميل",
19
+ "fieldStartTime": "وقت البدء",
20
+ "fieldEndTime": "وقت الانتهاء",
21
+ "fieldCancellationReason": "سبب الإلغاء",
22
+ "fieldNotes": "الملاحظات",
23
+ "fieldStatus": "الحالة",
24
+ "fieldScheduleType": "نوع الجدول الزمني",
25
+ "fieldRecurringSlots": "الفترات المتكررة",
26
+ "fieldDay": "اليوم",
27
+ "fieldManualSlots": "الفترات اليدوية",
28
+ "fieldDate": "التاريخ",
29
+ "fieldEndDate": "تاريخ الانتهاء",
30
+ "fieldExceptions": "الاستثناءات",
31
+ "fieldLeaveType": "النوع",
32
+ "fieldReason": "السبب",
33
+ "fieldDurationMinutes": "المدة (بالدقائق)",
34
+ "fieldBufferTimeBefore": "الوقت الفاصل قبل (بالدقائق)",
35
+ "fieldBufferTimeAfter": "الوقت الفاصل بعد (بالدقائق)",
36
+ "fieldStartTimeHHmm": "وقت البدء (HH:mm)",
37
+ "fieldEndTimeHHmm": "وقت الانتهاء (HH:mm)",
38
+ "scheduleTypeRecurring": "متكرر",
39
+ "scheduleTypeManual": "يدوي",
40
+ "dayMonday": "الإثنين",
41
+ "dayTuesday": "الثلاثاء",
42
+ "dayWednesday": "الأربعاء",
43
+ "dayThursday": "الخميس",
44
+ "dayFriday": "الجمعة",
45
+ "daySaturday": "السبت",
46
+ "daySunday": "الأحد",
47
+ "dayShortSun": "أحد",
48
+ "dayShortMon": "إثن",
49
+ "dayShortTue": "ثلا",
50
+ "dayShortWed": "أرب",
51
+ "dayShortThu": "خمي",
52
+ "dayShortFri": "جمع",
53
+ "dayShortSat": "سبت",
54
+ "statusPending": "قيد الانتظار",
55
+ "statusConfirmed": "مؤكد",
56
+ "statusCompleted": "مكتمل",
57
+ "statusCancelled": "ملغى",
58
+ "statusNoShow": "لم يحضر",
59
+ "statusNoShowLabel": "عدم الحضور",
60
+ "errorInvalidCreateStatus": "يجب أن تبدأ الحجوزات الجديدة بحالة {{allowed}}.",
61
+ "errorInvalidTransition": "لا يمكن الانتقال من \"{{from}}\" إلى \"{{to}}\".",
62
+ "errorCancellationNotice": "يتطلب الإلغاء إشعاراً مسبقاً لا يقل عن {{period}} ساعة. لم يتبقَّ سوى {{hours}} ساعة على الموعد.",
63
+ "errorConflict": "يتعارض هذا الموعد الزمني مع حجز قائم لهذا المورد.",
64
+ "calendarLoading": "جارٍ تحميل الحجوزات...",
65
+ "calendarToday": "اليوم",
66
+ "calendarCreateNew": "إنشاء جديد",
67
+ "calendarMonth": "الشهر",
68
+ "calendarWeek": "الأسبوع",
69
+ "calendarDay": "اليوم",
70
+ "calendarLanes": "المسارات",
71
+ "calendarUnknownService": "خدمة غير معروفة",
72
+ "calendarUnknownCustomer": "عميل غير معروف",
73
+ "calendarUnknownResource": "مورد غير معروف",
74
+ "tooltipCustomer": "العميل:",
75
+ "tooltipResource": "المورد:",
76
+ "tooltipStatus": "الحالة:",
77
+ "availabilityLoading": "جارٍ تحميل التوفر...",
78
+ "availabilityTitle": "نظرة عامة على التوفر",
79
+ "availabilityThisWeek": "هذا الأسبوع",
80
+ "availabilityNoResources": "لم يتم العثور على موارد نشطة.",
81
+ "availabilityNotConfigured": "لم يتم تكوين إضافة الحجوزات.",
82
+ "availabilityBooked": "محجوز",
83
+ "availabilityXofYBooked": "{{booked}}/{{total}} محجوز",
84
+ "availabilityUnavailable": "غير متوفر",
85
+ "availabilityResource": "المورد",
86
+ "dashboardTitle": "حجوزات اليوم",
87
+ "dashboardTotal": "الإجمالي",
88
+ "dashboardActive": "نشط",
89
+ "dashboardUpcoming": "القادمة",
90
+ "dashboardTerminal": "مغلق",
91
+ "dashboardCompleted": "مكتمل",
92
+ "dashboardCancelled": "ملغى",
93
+ "dashboardNextAppointment": "الموعد التالي",
94
+ "dashboardTime": "الوقت:",
95
+ "dashboardStatus": "الحالة:",
96
+ "dashboardNoUpcoming": "لا توجد مواعيد قادمة اليوم.",
97
+ "fieldCustomerSearch": "البحث عن العملاء...",
98
+ "fieldCustomerNoResults": "لم يتم العثور على عملاء",
99
+ "fieldCustomerCreateNew": "إنشاء عميل جديد",
100
+ "fieldCustomerClear": "مسح التحديد",
101
+ "calendarPending": "قيد الانتظار",
102
+ "pendingDateTime": "التاريخ / الوقت",
103
+ "pendingActions": "الإجراءات",
104
+ "pendingSelectAll": "تحديد الكل",
105
+ "pendingConfirmSelected": "تأكيد المحدد ({{count}})",
106
+ "pendingConfirming": "جارٍ التأكيد...",
107
+ "pendingConfirm": "تأكيد",
108
+ "pendingCancel": "إلغاء",
109
+ "pendingConfirmSuccess": "تم تأكيد الحجز",
110
+ "pendingConfirmError": "فشل تأكيد الحجز",
111
+ "pendingCancelSuccess": "تم إلغاء الحجز",
112
+ "pendingCancelError": "فشل إلغاء الحجز",
113
+ "pendingBulkConfirmSuccess": "تم تأكيد {{succeeded}}، وفشل {{failed}}",
114
+ "pendingEmpty": "لا توجد حجوزات قيد الانتظار",
115
+ "fieldQuantity": "الكمية",
116
+ "fieldCapacityMode": "وضع السعة",
117
+ "fieldTimezone": "المنطقة الزمنية",
118
+ "fieldDurationType": "نوع المدة",
119
+ "capacityPerReservation": "لكل حجز",
120
+ "capacityPerGuest": "لكل ضيف",
121
+ "durationFixed": "ثابتة",
122
+ "durationFlexible": "مرنة",
123
+ "durationFullDay": "يوم كامل",
124
+ "fieldItems": "العناصر",
125
+ "fieldGuestCount": "عدد الضيوف",
126
+ "filterAllResources": "جميع الموارد",
127
+ "filterByResource": "التصفية حسب المورد",
128
+ "fieldOwner": "المالك",
129
+ "fieldResourceType": "نوع المورد",
130
+ "fieldRequiredResources": "الموارد المطلوبة",
131
+ "fieldRequiredResourcesDesc": "مجموعات الموارد الإضافية التي يشغلها كل حجز لهذه الخدمة (مثل الكرسي). يتم توسيع الحجوزات تلقائياً لتشمل هذه الموارد، وتُحظر إذا كانت أي مجموعة ممتلئة.",
132
+ "fieldAllowGuestBooking": "حجز الضيف",
133
+ "fieldAllowGuestBookingDesc": "السماح بالحجوزات دون حساب عميل. \"وراثة\" تستخدم الإعداد الافتراضي على مستوى الإضافة.",
134
+ "guestBookingInherit": "وراثة الإعداد الافتراضي للإضافة",
135
+ "guestBookingEnabled": "مُفعّل",
136
+ "guestBookingDisabled": "مُعطّل",
137
+ "fieldGuest": "الضيف",
138
+ "fieldGuestDesc": "تفاصيل الاتصال لحجز تم دون حساب عميل. اتركها فارغة عند تعيين عميل.",
139
+ "fieldGuestName": "اسم الضيف",
140
+ "fieldGuestEmail": "البريد الإلكتروني للضيف",
141
+ "fieldGuestPhone": "هاتف الضيف",
142
+ "fieldItemsDesc": "الموارد المضمّنة في هذا الحجز. اتركها فارغة للحجوزات ذات المورد الواحد.",
143
+ "errorGuestOrCustomerRequired": "يتطلب الحجز إما عميلاً أو معلومات اتصال ضيف.",
144
+ "errorGuestAndCustomer": "لا يمكن أن يحتوي الحجز على عميل ومعلومات اتصال ضيف في آنٍ واحد.",
145
+ "errorGuestNameRequired": "تتطلب حجوزات الضيوف اسماً.",
146
+ "errorGuestContactRequired": "تتطلب حجوزات الضيوف بريداً إلكترونياً أو رقم هاتف.",
147
+ "errorGuestNotAllowed": "حجوزات الضيوف غير مسموح بها لهذه الخدمة.",
148
+ "pickPrompt": "اختر خدمة وموظفاً لعرض الأوقات المتاحة.",
149
+ "pickLoading": "جارٍ تحميل الأوقات المتاحة…",
150
+ "pickNone": "لا توجد أوقات متاحة لهذا اليوم.",
151
+ "laneNoResources": "لا توجد موارد نشطة لعرضها.",
152
+ "slotFree": "متاح",
153
+ "slotFull": "ممتلئ",
154
+ "slotOffShift": "خارج الدوام",
155
+ "slotTimeOff": "إجازة"
156
+ }