payload-reserve 2.3.0 → 3.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 (111) hide show
  1. package/README.md +218 -4
  2. package/dist/collections/Holds.d.ts +18 -0
  3. package/dist/collections/Holds.js +113 -0
  4. package/dist/collections/Holds.js.map +1 -0
  5. package/dist/collections/Reservations.js +8 -0
  6. package/dist/collections/Reservations.js.map +1 -1
  7. package/dist/collections/Resources.js +35 -1
  8. package/dist/collections/Resources.js.map +1 -1
  9. package/dist/collections/Services.js +41 -0
  10. package/dist/collections/Services.js.map +1 -1
  11. package/dist/components/CalendarView/LaneTimelineView.js +4 -5
  12. package/dist/components/CalendarView/LaneTimelineView.js.map +1 -1
  13. package/dist/components/CalendarView/index.js +55 -71
  14. package/dist/components/CalendarView/index.js.map +1 -1
  15. package/dist/components/DashboardWidget/DashboardWidgetServer.js +11 -74
  16. package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
  17. package/dist/components/DashboardWidget/fetchDashboardStats.d.ts +42 -0
  18. package/dist/components/DashboardWidget/fetchDashboardStats.js +113 -0
  19. package/dist/components/DashboardWidget/fetchDashboardStats.js.map +1 -0
  20. package/dist/defaults.d.ts +2 -0
  21. package/dist/defaults.js +12 -0
  22. package/dist/defaults.js.map +1 -1
  23. package/dist/endpoints/cancelBooking.js +43 -14
  24. package/dist/endpoints/cancelBooking.js.map +1 -1
  25. package/dist/endpoints/checkAvailability.js +27 -1
  26. package/dist/endpoints/checkAvailability.js.map +1 -1
  27. package/dist/endpoints/createBooking.js +108 -7
  28. package/dist/endpoints/createBooking.js.map +1 -1
  29. package/dist/endpoints/customerSearch.js +6 -0
  30. package/dist/endpoints/customerSearch.js.map +1 -1
  31. package/dist/endpoints/effectiveTimezone.js +1 -0
  32. package/dist/endpoints/effectiveTimezone.js.map +1 -1
  33. package/dist/endpoints/getSlots.js +27 -1
  34. package/dist/endpoints/getSlots.js.map +1 -1
  35. package/dist/endpoints/holdSlot.d.ts +12 -0
  36. package/dist/endpoints/holdSlot.js +126 -0
  37. package/dist/endpoints/holdSlot.js.map +1 -0
  38. package/dist/endpoints/releaseSlot.d.ts +8 -0
  39. package/dist/endpoints/releaseSlot.js +34 -0
  40. package/dist/endpoints/releaseSlot.js.map +1 -0
  41. package/dist/endpoints/resourceAvailability.d.ts +4 -0
  42. package/dist/endpoints/resourceAvailability.js +105 -4
  43. package/dist/endpoints/resourceAvailability.js.map +1 -1
  44. package/dist/hooks/holds/validateHoldSlot.d.ts +14 -0
  45. package/dist/hooks/holds/validateHoldSlot.js +90 -0
  46. package/dist/hooks/holds/validateHoldSlot.js.map +1 -0
  47. package/dist/hooks/reservations/acquireBookingLock.d.ts +30 -0
  48. package/dist/hooks/reservations/acquireBookingLock.js +71 -0
  49. package/dist/hooks/reservations/acquireBookingLock.js.map +1 -0
  50. package/dist/hooks/reservations/calculateEndTime.js +13 -1
  51. package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
  52. package/dist/hooks/reservations/expandRequiredResources.js +3 -0
  53. package/dist/hooks/reservations/expandRequiredResources.js.map +1 -1
  54. package/dist/hooks/reservations/validateActive.d.ts +11 -0
  55. package/dist/hooks/reservations/validateActive.js +103 -0
  56. package/dist/hooks/reservations/validateActive.js.map +1 -0
  57. package/dist/hooks/reservations/validateConflicts.js +22 -1
  58. package/dist/hooks/reservations/validateConflicts.js.map +1 -1
  59. package/dist/hooks/reservations/validateGuestBooking.js +3 -0
  60. package/dist/hooks/reservations/validateGuestBooking.js.map +1 -1
  61. package/dist/hooks/shared/preventDeleteWhenReferenced.d.ts +42 -0
  62. package/dist/hooks/shared/preventDeleteWhenReferenced.js +87 -0
  63. package/dist/hooks/shared/preventDeleteWhenReferenced.js.map +1 -0
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.js.map +1 -1
  66. package/dist/plugin.js +179 -3
  67. package/dist/plugin.js.map +1 -1
  68. package/dist/services/AvailabilityService.d.ts +51 -5
  69. package/dist/services/AvailabilityService.js +315 -38
  70. package/dist/services/AvailabilityService.js.map +1 -1
  71. package/dist/services/HoldService.d.ts +53 -0
  72. package/dist/services/HoldService.js +169 -0
  73. package/dist/services/HoldService.js.map +1 -0
  74. package/dist/services/index.d.ts +1 -0
  75. package/dist/services/index.js.map +1 -1
  76. package/dist/translations/ar.json +5 -1
  77. package/dist/translations/de.json +5 -1
  78. package/dist/translations/en.json +5 -1
  79. package/dist/translations/es.json +5 -1
  80. package/dist/translations/fa.json +5 -1
  81. package/dist/translations/fr.json +5 -1
  82. package/dist/translations/hi.json +5 -1
  83. package/dist/translations/id.json +5 -1
  84. package/dist/translations/pl.json +5 -1
  85. package/dist/translations/ru.json +5 -1
  86. package/dist/translations/tr.json +5 -1
  87. package/dist/translations/zh.json +5 -1
  88. package/dist/types.d.ts +32 -0
  89. package/dist/types.js.map +1 -1
  90. package/dist/utilities/calendarGrid.d.ts +44 -0
  91. package/dist/utilities/calendarGrid.js +67 -0
  92. package/dist/utilities/calendarGrid.js.map +1 -0
  93. package/dist/utilities/reservationChanges.d.ts +6 -1
  94. package/dist/utilities/reservationChanges.js +1 -1
  95. package/dist/utilities/reservationChanges.js.map +1 -1
  96. package/dist/utilities/reserveDebug.d.ts +26 -0
  97. package/dist/utilities/reserveDebug.js +35 -0
  98. package/dist/utilities/reserveDebug.js.map +1 -0
  99. package/dist/utilities/resolveReservationItems.d.ts +13 -2
  100. package/dist/utilities/resolveReservationItems.js +87 -2
  101. package/dist/utilities/resolveReservationItems.js.map +1 -1
  102. package/dist/utilities/retryOnWriteConflict.d.ts +66 -0
  103. package/dist/utilities/retryOnWriteConflict.js +135 -0
  104. package/dist/utilities/retryOnWriteConflict.js.map +1 -0
  105. package/dist/utilities/tenantTimezone.d.ts +77 -1
  106. package/dist/utilities/tenantTimezone.js +163 -2
  107. package/dist/utilities/tenantTimezone.js.map +1 -1
  108. package/dist/utilities/transactionSupport.d.ts +13 -0
  109. package/dist/utilities/transactionSupport.js +30 -0
  110. package/dist/utilities/transactionSupport.js.map +1 -0
  111. package/package.json +13 -13
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/collections/Resources.ts"],"sourcesContent":["import type { CollectionConfig, CollectionSlug, Field } from 'payload'\n\nimport type { PluginT } from '../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { composeAccess, makeResourceOwnerAccess } from '../utilities/ownerAccess.js'\nimport { buildSelectOptions } from '../utilities/selectOptions.js'\n\n/**\n * Owner-field beforeChange logic, extracted for testability. On create the\n * owner defaults to the requesting user; on other operations the existing\n * value is preserved. Staff provisioning assigns the correct owner by creating\n * the Resource AS the new staff user (impersonation in the provisioning hook),\n * so this function needs no special-case branch.\n */\nexport function resolveOwnerValue({\n operation,\n req,\n value,\n}: {\n operation: string | undefined\n req: { user?: { id: unknown } | null }\n value: unknown\n}): unknown {\n if (operation === 'create' && req.user) {\n return req.user.id\n }\n return value\n}\n\nexport function createResourcesCollection(\n config: ResolvedReservationPluginConfig,\n): CollectionConfig {\n const rom = config.resourceOwnerMode\n const ownerField = rom?.ownerField ?? 'owner'\n // The owner relationship points to where owners/staff live: an explicit\n // ownerCollection, else the staff-provisioning user collection, else customers.\n // (Previously hardcoded to customers, which broke separate users/customers setups.)\n const ownerCollection =\n rom?.ownerCollection ?? config.staffProvisioning?.userCollection ?? config.slugs.customers\n\n // Build the owner field when resourceOwnerMode is enabled\n const ownerFieldDef: Field | null = rom\n ? {\n name: ownerField,\n type: 'relationship',\n admin: {\n position: 'sidebar',\n },\n hooks: {\n beforeChange: [\n ({ operation, req, value }) => resolveOwnerValue({ operation, req, value }),\n ],\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldOwner'),\n relationTo: ownerCollection as unknown as CollectionSlug,\n required: true,\n }\n : null\n\n // Determine access: app override → owner-mode auto-wired → unrestricted\n const access =\n composeAccess(rom ? makeResourceOwnerAccess(rom) : {}, config.access.resources)\n\n return {\n slug: config.slugs.resources,\n access,\n admin: {\n group: config.adminGroup,\n useAsTitle: 'name',\n },\n fields: [\n {\n name: 'name',\n type: 'text',\n label: ({ t }) => (t as PluginT)('reservation:fieldName'),\n ...(config.localized ? { localized: true } : {}),\n maxLength: 200,\n required: true,\n },\n ...(config.hasMediaCollection\n ? [\n {\n name: 'image',\n type: 'upload' as const,\n label: ({ t }: { t: unknown }) => (t as PluginT)('reservation:fieldImage'),\n relationTo: config.slugs.media as unknown as CollectionSlug,\n },\n ]\n : []),\n {\n name: 'description',\n type: 'textarea',\n label: ({ t }) => (t as PluginT)('reservation:fieldDescription'),\n ...(config.localized ? { localized: true } : {}),\n },\n {\n name: 'services',\n type: 'relationship',\n hasMany: true,\n label: ({ t }) => (t as PluginT)('reservation:fieldServices'),\n relationTo: config.slugs.services as unknown as CollectionSlug,\n },\n {\n name: 'active',\n type: 'checkbox',\n admin: {\n position: 'sidebar',\n },\n defaultValue: true,\n label: ({ t }) => (t as PluginT)('reservation:fieldActive'),\n },\n {\n name: 'quantity',\n type: 'number',\n admin: {\n position: 'sidebar',\n },\n defaultValue: 1,\n label: ({ t }) => (t as PluginT)('reservation:fieldQuantity'),\n min: 1,\n required: true,\n },\n {\n name: 'capacityMode',\n type: 'select',\n admin: {\n condition: (data) => (data?.quantity ?? 1) > 1,\n position: 'sidebar',\n },\n defaultValue: 'per-reservation',\n label: ({ t }) => (t as PluginT)('reservation:fieldCapacityMode'),\n options: [\n {\n label: ({ t }) => (t as PluginT)('reservation:capacityPerReservation'),\n value: 'per-reservation',\n },\n {\n label: ({ t }) => (t as PluginT)('reservation:capacityPerGuest'),\n value: 'per-guest',\n },\n ],\n },\n {\n name: 'timezone',\n type: 'text',\n admin: {\n description:\n 'Deprecated — not used by the plugin. Schedule resolution uses the plugin-level `timezone` option.',\n position: 'sidebar',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldTimezone'),\n },\n {\n name: 'resourceType',\n type: 'select',\n admin: {\n position: 'sidebar',\n },\n defaultValue: config.resourceTypes[0],\n label: ({ t }) => (t as PluginT)('reservation:fieldResourceType'),\n options: buildSelectOptions(config.resourceTypes),\n },\n ...(ownerFieldDef ? [ownerFieldDef] : []),\n ],\n labels: {\n plural: ({ t }) => (t as PluginT)('reservation:collectionResources'),\n singular: ({ t }) => (t as PluginT)('reservation:collectionResources'),\n },\n }\n}\n"],"names":["composeAccess","makeResourceOwnerAccess","buildSelectOptions","resolveOwnerValue","operation","req","value","user","id","createResourcesCollection","config","rom","resourceOwnerMode","ownerField","ownerCollection","staffProvisioning","userCollection","slugs","customers","ownerFieldDef","name","type","admin","position","hooks","beforeChange","label","t","relationTo","required","access","resources","slug","group","adminGroup","useAsTitle","fields","localized","maxLength","hasMediaCollection","media","hasMany","services","defaultValue","min","condition","data","quantity","options","description","resourceTypes","labels","plural","singular"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/collections/Resources.ts"],"sourcesContent":["import type { CollectionConfig, CollectionSlug, Field } from 'payload'\n\nimport type { PluginT } from '../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { preventDeleteWhenReferenced } from '../hooks/shared/preventDeleteWhenReferenced.js'\nimport { composeAccess, makeResourceOwnerAccess } from '../utilities/ownerAccess.js'\nimport { buildSelectOptions } from '../utilities/selectOptions.js'\n\n/**\n * Owner-field beforeChange logic, extracted for testability. On create the\n * owner defaults to the requesting user; on other operations the existing\n * value is preserved. Staff provisioning assigns the correct owner by creating\n * the Resource AS the new staff user (impersonation in the provisioning hook),\n * so this function needs no special-case branch.\n */\nexport function resolveOwnerValue({\n operation,\n req,\n value,\n}: {\n operation: string | undefined\n req: { user?: { id: unknown } | null }\n value: unknown\n}): unknown {\n if (operation === 'create' && req.user) {\n return req.user.id\n }\n return value\n}\n\nexport function createResourcesCollection(\n config: ResolvedReservationPluginConfig,\n): CollectionConfig {\n const rom = config.resourceOwnerMode\n const ownerField = rom?.ownerField ?? 'owner'\n // The owner relationship points to where owners/staff live: an explicit\n // ownerCollection, else the staff-provisioning user collection, else customers.\n // (Previously hardcoded to customers, which broke separate users/customers setups.)\n const ownerCollection =\n rom?.ownerCollection ?? config.staffProvisioning?.userCollection ?? config.slugs.customers\n\n // Build the owner field when resourceOwnerMode is enabled\n const ownerFieldDef: Field | null = rom\n ? {\n name: ownerField,\n type: 'relationship',\n admin: {\n position: 'sidebar',\n },\n hooks: {\n beforeChange: [\n ({ operation, req, value }) => resolveOwnerValue({ operation, req, value }),\n ],\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldOwner'),\n relationTo: ownerCollection as unknown as CollectionSlug,\n required: true,\n }\n : null\n\n // Determine access: app override → owner-mode auto-wired → unrestricted\n const access =\n composeAccess(rom ? makeResourceOwnerAccess(rom) : {}, config.access.resources)\n\n return {\n slug: config.slugs.resources,\n access,\n admin: {\n group: config.adminGroup,\n useAsTitle: 'name',\n },\n fields: [\n {\n name: 'name',\n type: 'text',\n label: ({ t }) => (t as PluginT)('reservation:fieldName'),\n ...(config.localized ? { localized: true } : {}),\n maxLength: 200,\n required: true,\n },\n ...(config.hasMediaCollection\n ? [\n {\n name: 'image',\n type: 'upload' as const,\n label: ({ t }: { t: unknown }) => (t as PluginT)('reservation:fieldImage'),\n relationTo: config.slugs.media as unknown as CollectionSlug,\n },\n ]\n : []),\n {\n name: 'description',\n type: 'textarea',\n label: ({ t }) => (t as PluginT)('reservation:fieldDescription'),\n ...(config.localized ? { localized: true } : {}),\n },\n {\n name: 'services',\n type: 'relationship',\n hasMany: true,\n label: ({ t }) => (t as PluginT)('reservation:fieldServices'),\n relationTo: config.slugs.services as unknown as CollectionSlug,\n },\n {\n name: 'active',\n type: 'checkbox',\n admin: {\n position: 'sidebar',\n },\n defaultValue: true,\n label: ({ t }) => (t as PluginT)('reservation:fieldActive'),\n },\n {\n name: 'quantity',\n type: 'number',\n admin: {\n position: 'sidebar',\n },\n defaultValue: 1,\n label: ({ t }) => (t as PluginT)('reservation:fieldQuantity'),\n min: 1,\n required: true,\n },\n {\n name: 'capacityMode',\n type: 'select',\n admin: {\n condition: (data) => (data?.quantity ?? 1) > 1,\n position: 'sidebar',\n },\n defaultValue: 'per-reservation',\n label: ({ t }) => (t as PluginT)('reservation:fieldCapacityMode'),\n options: [\n {\n label: ({ t }) => (t as PluginT)('reservation:capacityPerReservation'),\n value: 'per-reservation',\n },\n {\n label: ({ t }) => (t as PluginT)('reservation:capacityPerGuest'),\n value: 'per-guest',\n },\n ],\n },\n {\n name: 'timezone',\n type: 'text',\n admin: {\n description:\n 'Deprecated — not used by the plugin. Schedule resolution uses the plugin-level `timezone` option.',\n position: 'sidebar',\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldTimezone'),\n },\n {\n name: 'resourceType',\n type: 'select',\n admin: {\n position: 'sidebar',\n },\n defaultValue: config.resourceTypes[0],\n label: ({ t }) => (t as PluginT)('reservation:fieldResourceType'),\n options: buildSelectOptions(config.resourceTypes),\n },\n ...(ownerFieldDef ? [ownerFieldDef] : []),\n {\n // Serialization point for concurrent bookings. Every booking write\n // touches this field on each resource it claims, inside the booking's\n // transaction — which turns \"two transactions insert different\n // reservation documents\" (which no database serializes) into \"two\n // transactions write the SAME document\" (which every database does).\n // The value is never read; only the write matters.\n name: 'bookingLock',\n type: 'text',\n admin: { hidden: true },\n },\n ],\n hooks: {\n beforeDelete: [\n preventDeleteWhenReferenced({\n config,\n // Schedules.resource is also required: true — the exact same\n // NOT NULL / ON DELETE SET NULL contradiction, one relationship\n // over. A resource with schedules but zero reservations must be\n // blocked too.\n extraChecks: [\n { collection: config.slugs.schedules, field: 'resource', label: 'schedule' },\n ],\n field: 'resource',\n label: 'resource',\n }),\n ],\n },\n labels: {\n plural: ({ t }) => (t as PluginT)('reservation:collectionResources'),\n singular: ({ t }) => (t as PluginT)('reservation:collectionResources'),\n },\n }\n}\n"],"names":["preventDeleteWhenReferenced","composeAccess","makeResourceOwnerAccess","buildSelectOptions","resolveOwnerValue","operation","req","value","user","id","createResourcesCollection","config","rom","resourceOwnerMode","ownerField","ownerCollection","staffProvisioning","userCollection","slugs","customers","ownerFieldDef","name","type","admin","position","hooks","beforeChange","label","t","relationTo","required","access","resources","slug","group","adminGroup","useAsTitle","fields","localized","maxLength","hasMediaCollection","media","hasMany","services","defaultValue","min","condition","data","quantity","options","description","resourceTypes","hidden","beforeDelete","extraChecks","collection","schedules","field","labels","plural","singular"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { preventDeleteWhenReferenced } from '../hooks/shared/preventDeleteWhenReferenced.js';
1
2
  import { composeAccess, makeServiceOwnerAccess } from '../utilities/ownerAccess.js';
2
3
  export function createServicesCollection(config) {
3
4
  const rom = config.resourceOwnerMode;
@@ -118,6 +119,37 @@ export function createServicesCollection(config) {
118
119
  max: 1439,
119
120
  min: 0
120
121
  },
122
+ ...config.hasResourceServicesField ? [
123
+ {
124
+ name: 'resources',
125
+ type: 'join',
126
+ admin: {
127
+ // A join's only write affordance is creating a BRAND-NEW resource
128
+ // from here; it can never link an existing one (no allowAddExisting
129
+ // in 3.86). Left off deliberately — `getInitialDrawerData` pre-fills
130
+ // the target relationship as a SCALAR (`{ services: docID }`), while
131
+ // `Resources.services` is hasMany and expects an array, so the
132
+ // pre-fill is not reliably correct for this shape. Assignment belongs
133
+ // on the Resource, which the description says.
134
+ allowCreate: false,
135
+ // Shows `active` in the table, so an admin can see at a glance that a
136
+ // linked resource is disabled — the exact diagnosis this field exists
137
+ // to make possible.
138
+ defaultColumns: [
139
+ 'name',
140
+ 'resourceType',
141
+ 'active'
142
+ ],
143
+ description: ({ t })=>t('reservation:fieldResourcesDesc')
144
+ },
145
+ collection: config.slugs.resources,
146
+ // Joins default to 10 (buildJoinAggregation). A service performed by
147
+ // more than 10 resources would otherwise silently show 10 of them.
148
+ defaultLimit: 100,
149
+ label: ({ t })=>t('reservation:fieldResources'),
150
+ on: 'services'
151
+ }
152
+ ] : [],
121
153
  {
122
154
  name: 'requiredResources',
123
155
  type: 'relationship',
@@ -164,6 +196,15 @@ export function createServicesCollection(config) {
164
196
  ownerFieldDef
165
197
  ] : []
166
198
  ],
199
+ hooks: {
200
+ beforeDelete: [
201
+ preventDeleteWhenReferenced({
202
+ config,
203
+ field: 'service',
204
+ label: 'service'
205
+ })
206
+ ]
207
+ },
167
208
  labels: {
168
209
  plural: ({ t })=>t('reservation:collectionServices'),
169
210
  singular: ({ t })=>t('reservation:collectionServices')
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/collections/Services.ts"],"sourcesContent":["import type { CollectionConfig, CollectionSlug, Field } from 'payload'\n\nimport type { PluginT } from '../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { composeAccess, makeServiceOwnerAccess } from '../utilities/ownerAccess.js'\n\nexport function createServicesCollection(config: ResolvedReservationPluginConfig): CollectionConfig {\n const rom = config.resourceOwnerMode\n const ownedServices = rom?.ownedServices ?? false\n const ownerField = rom?.ownerField ?? 'owner'\n // Owner relationship points where owners/staff live — same resolution as\n // Resources; previously hardcoded to customers, breaking separate user setups.\n const ownerCollection =\n rom?.ownerCollection ?? config.staffProvisioning?.userCollection ?? config.slugs.customers\n\n // Owner field on Services (only when ownedServices: true)\n const ownerFieldDef: Field | null =\n rom && ownedServices\n ? {\n name: ownerField,\n type: 'relationship',\n admin: {\n position: 'sidebar',\n },\n hooks: {\n beforeChange: [\n ({ operation, req, value }) => {\n if (operation === 'create' && req.user) {return req.user.id}\n return value\n },\n ],\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldOwner'),\n relationTo: ownerCollection as unknown as CollectionSlug,\n required: true,\n }\n : null\n\n // Owner-mode base rules with any app overrides composed on top (per operation)\n const access = composeAccess(\n rom && ownedServices ? makeServiceOwnerAccess(rom, ownerField) : {},\n config.access.services,\n )\n\n return {\n slug: config.slugs.services,\n access,\n admin: {\n group: config.adminGroup,\n useAsTitle: 'name',\n },\n fields: [\n {\n name: 'name',\n type: 'text',\n label: ({ t }) => (t as PluginT)('reservation:fieldName'),\n ...(config.localized ? { localized: true } : {}),\n maxLength: 200,\n required: true,\n },\n ...(config.hasMediaCollection\n ? [\n {\n name: 'image',\n type: 'upload' as const,\n label: ({ t }: { t: unknown }) => (t as PluginT)('reservation:fieldImage'),\n relationTo: config.slugs.media as unknown as CollectionSlug,\n },\n ]\n : []),\n {\n name: 'description',\n type: 'textarea',\n label: ({ t }) => (t as PluginT)('reservation:fieldDescription'),\n ...(config.localized ? { localized: true } : {}),\n },\n {\n name: 'duration',\n type: 'number',\n label: ({ t }) => (t as PluginT)('reservation:fieldDurationMinutes'),\n min: 1,\n required: true,\n },\n {\n name: 'durationType',\n type: 'select',\n defaultValue: 'fixed',\n label: ({ t }) => (t as PluginT)('reservation:fieldDurationType'),\n options: [\n {\n label: ({ t }) => (t as PluginT)('reservation:durationFixed'),\n value: 'fixed',\n },\n {\n label: ({ t }) => (t as PluginT)('reservation:durationFlexible'),\n value: 'flexible',\n },\n {\n label: ({ t }) => (t as PluginT)('reservation:durationFullDay'),\n value: 'full-day',\n },\n ],\n required: true,\n },\n {\n name: 'price',\n type: 'number',\n admin: {\n step: 0.01,\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldPrice'),\n min: 0,\n },\n {\n // max < 24h keeps the conflict-detection coarse pre-filter\n // (COARSE_MARGIN_MS = 24h) provably sufficient.\n name: 'bufferTimeBefore',\n type: 'number',\n defaultValue: 0,\n label: ({ t }) => (t as PluginT)('reservation:fieldBufferTimeBefore'),\n max: 1439,\n min: 0,\n },\n {\n name: 'bufferTimeAfter',\n type: 'number',\n defaultValue: 0,\n label: ({ t }) => (t as PluginT)('reservation:fieldBufferTimeAfter'),\n max: 1439,\n min: 0,\n },\n {\n name: 'requiredResources',\n type: 'relationship',\n admin: {\n description: ({ t }) => (t as PluginT)('reservation:fieldRequiredResourcesDesc'),\n },\n hasMany: true,\n label: ({ t }) => (t as PluginT)('reservation:fieldRequiredResources'),\n relationTo: config.slugs.resources as unknown as CollectionSlug,\n },\n {\n name: 'allowGuestBooking',\n type: 'select',\n admin: {\n description: ({ t }) => (t as PluginT)('reservation:fieldAllowGuestBookingDesc'),\n },\n defaultValue: 'inherit',\n label: ({ t }) => (t as PluginT)('reservation:fieldAllowGuestBooking'),\n options: [\n {\n label: ({ t }) => (t as PluginT)('reservation:guestBookingInherit'),\n value: 'inherit',\n },\n {\n label: ({ t }) => (t as PluginT)('reservation:guestBookingEnabled'),\n value: 'enabled',\n },\n {\n label: ({ t }) => (t as PluginT)('reservation:guestBookingDisabled'),\n value: 'disabled',\n },\n ],\n },\n {\n name: 'active',\n type: 'checkbox',\n admin: {\n position: 'sidebar',\n },\n defaultValue: true,\n label: ({ t }) => (t as PluginT)('reservation:fieldActive'),\n },\n ...(ownerFieldDef ? [ownerFieldDef] : []),\n ],\n labels: {\n plural: ({ t }) => (t as PluginT)('reservation:collectionServices'),\n singular: ({ t }) => (t as PluginT)('reservation:collectionServices'),\n },\n }\n}\n"],"names":["composeAccess","makeServiceOwnerAccess","createServicesCollection","config","rom","resourceOwnerMode","ownedServices","ownerField","ownerCollection","staffProvisioning","userCollection","slugs","customers","ownerFieldDef","name","type","admin","position","hooks","beforeChange","operation","req","value","user","id","label","t","relationTo","required","access","services","slug","group","adminGroup","useAsTitle","fields","localized","maxLength","hasMediaCollection","media","min","defaultValue","options","step","max","description","hasMany","resources","labels","plural","singular"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/collections/Services.ts"],"sourcesContent":["import type { CollectionConfig, CollectionSlug, Field } from 'payload'\n\nimport type { PluginT } from '../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { preventDeleteWhenReferenced } from '../hooks/shared/preventDeleteWhenReferenced.js'\nimport { composeAccess, makeServiceOwnerAccess } from '../utilities/ownerAccess.js'\n\nexport function createServicesCollection(config: ResolvedReservationPluginConfig): CollectionConfig {\n const rom = config.resourceOwnerMode\n const ownedServices = rom?.ownedServices ?? false\n const ownerField = rom?.ownerField ?? 'owner'\n // Owner relationship points where owners/staff live — same resolution as\n // Resources; previously hardcoded to customers, breaking separate user setups.\n const ownerCollection =\n rom?.ownerCollection ?? config.staffProvisioning?.userCollection ?? config.slugs.customers\n\n // Owner field on Services (only when ownedServices: true)\n const ownerFieldDef: Field | null =\n rom && ownedServices\n ? {\n name: ownerField,\n type: 'relationship',\n admin: {\n position: 'sidebar',\n },\n hooks: {\n beforeChange: [\n ({ operation, req, value }) => {\n if (operation === 'create' && req.user) {return req.user.id}\n return value\n },\n ],\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldOwner'),\n relationTo: ownerCollection as unknown as CollectionSlug,\n required: true,\n }\n : null\n\n // Owner-mode base rules with any app overrides composed on top (per operation)\n const access = composeAccess(\n rom && ownedServices ? makeServiceOwnerAccess(rom, ownerField) : {},\n config.access.services,\n )\n\n return {\n slug: config.slugs.services,\n access,\n admin: {\n group: config.adminGroup,\n useAsTitle: 'name',\n },\n fields: [\n {\n name: 'name',\n type: 'text',\n label: ({ t }) => (t as PluginT)('reservation:fieldName'),\n ...(config.localized ? { localized: true } : {}),\n maxLength: 200,\n required: true,\n },\n ...(config.hasMediaCollection\n ? [\n {\n name: 'image',\n type: 'upload' as const,\n label: ({ t }: { t: unknown }) => (t as PluginT)('reservation:fieldImage'),\n relationTo: config.slugs.media as unknown as CollectionSlug,\n },\n ]\n : []),\n {\n name: 'description',\n type: 'textarea',\n label: ({ t }) => (t as PluginT)('reservation:fieldDescription'),\n ...(config.localized ? { localized: true } : {}),\n },\n {\n name: 'duration',\n type: 'number',\n label: ({ t }) => (t as PluginT)('reservation:fieldDurationMinutes'),\n min: 1,\n required: true,\n },\n {\n name: 'durationType',\n type: 'select',\n defaultValue: 'fixed',\n label: ({ t }) => (t as PluginT)('reservation:fieldDurationType'),\n options: [\n {\n label: ({ t }) => (t as PluginT)('reservation:durationFixed'),\n value: 'fixed',\n },\n {\n label: ({ t }) => (t as PluginT)('reservation:durationFlexible'),\n value: 'flexible',\n },\n {\n label: ({ t }) => (t as PluginT)('reservation:durationFullDay'),\n value: 'full-day',\n },\n ],\n required: true,\n },\n {\n name: 'price',\n type: 'number',\n admin: {\n step: 0.01,\n },\n label: ({ t }) => (t as PluginT)('reservation:fieldPrice'),\n min: 0,\n },\n {\n // max < 24h keeps the conflict-detection coarse pre-filter\n // (COARSE_MARGIN_MS = 24h) provably sufficient.\n name: 'bufferTimeBefore',\n type: 'number',\n defaultValue: 0,\n label: ({ t }) => (t as PluginT)('reservation:fieldBufferTimeBefore'),\n max: 1439,\n min: 0,\n },\n {\n name: 'bufferTimeAfter',\n type: 'number',\n defaultValue: 0,\n label: ({ t }) => (t as PluginT)('reservation:fieldBufferTimeAfter'),\n max: 1439,\n min: 0,\n },\n ...(config.hasResourceServicesField\n ? [\n {\n name: 'resources',\n type: 'join' as const,\n admin: {\n // A join's only write affordance is creating a BRAND-NEW resource\n // from here; it can never link an existing one (no allowAddExisting\n // in 3.86). Left off deliberately — `getInitialDrawerData` pre-fills\n // the target relationship as a SCALAR (`{ services: docID }`), while\n // `Resources.services` is hasMany and expects an array, so the\n // pre-fill is not reliably correct for this shape. Assignment belongs\n // on the Resource, which the description says.\n allowCreate: false,\n // Shows `active` in the table, so an admin can see at a glance that a\n // linked resource is disabled — the exact diagnosis this field exists\n // to make possible.\n defaultColumns: ['name', 'resourceType', 'active'],\n description: ({ t }: { t: unknown }) =>\n (t as PluginT)('reservation:fieldResourcesDesc'),\n },\n collection: config.slugs.resources as unknown as CollectionSlug,\n // Joins default to 10 (buildJoinAggregation). A service performed by\n // more than 10 resources would otherwise silently show 10 of them.\n defaultLimit: 100,\n label: ({ t }: { t: unknown }) => (t as PluginT)('reservation:fieldResources'),\n on: 'services',\n },\n ]\n : []),\n {\n name: 'requiredResources',\n type: 'relationship',\n admin: {\n description: ({ t }) => (t as PluginT)('reservation:fieldRequiredResourcesDesc'),\n },\n hasMany: true,\n label: ({ t }) => (t as PluginT)('reservation:fieldRequiredResources'),\n relationTo: config.slugs.resources as unknown as CollectionSlug,\n },\n {\n name: 'allowGuestBooking',\n type: 'select',\n admin: {\n description: ({ t }) => (t as PluginT)('reservation:fieldAllowGuestBookingDesc'),\n },\n defaultValue: 'inherit',\n label: ({ t }) => (t as PluginT)('reservation:fieldAllowGuestBooking'),\n options: [\n {\n label: ({ t }) => (t as PluginT)('reservation:guestBookingInherit'),\n value: 'inherit',\n },\n {\n label: ({ t }) => (t as PluginT)('reservation:guestBookingEnabled'),\n value: 'enabled',\n },\n {\n label: ({ t }) => (t as PluginT)('reservation:guestBookingDisabled'),\n value: 'disabled',\n },\n ],\n },\n {\n name: 'active',\n type: 'checkbox',\n admin: {\n position: 'sidebar',\n },\n defaultValue: true,\n label: ({ t }) => (t as PluginT)('reservation:fieldActive'),\n },\n ...(ownerFieldDef ? [ownerFieldDef] : []),\n ],\n hooks: {\n beforeDelete: [preventDeleteWhenReferenced({ config, field: 'service', label: 'service' })],\n },\n labels: {\n plural: ({ t }) => (t as PluginT)('reservation:collectionServices'),\n singular: ({ t }) => (t as PluginT)('reservation:collectionServices'),\n },\n }\n}\n"],"names":["preventDeleteWhenReferenced","composeAccess","makeServiceOwnerAccess","createServicesCollection","config","rom","resourceOwnerMode","ownedServices","ownerField","ownerCollection","staffProvisioning","userCollection","slugs","customers","ownerFieldDef","name","type","admin","position","hooks","beforeChange","operation","req","value","user","id","label","t","relationTo","required","access","services","slug","group","adminGroup","useAsTitle","fields","localized","maxLength","hasMediaCollection","media","min","defaultValue","options","step","max","hasResourceServicesField","allowCreate","defaultColumns","description","collection","resources","defaultLimit","on","hasMany","beforeDelete","field","labels","plural","singular"],"mappings":"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"}
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useTranslation } from '@payloadcms/ui';
4
4
  import React from 'react';
5
+ import { gridInstant } from '../../utilities/calendarGrid.js';
5
6
  import { computeSlotStates } from '../../utilities/computeSlotStates.js';
6
7
  import { getDayKeyInTimezone } from '../../utilities/timezoneUtils.js';
7
8
  import styles from './CalendarView.module.css';
@@ -16,12 +17,10 @@ const SLOT_STATE_KEYS = {
16
17
  function Lane({ apiBase, day, endHour, onBook, resource, startHour, timeZone }) {
17
18
  const { t: _t } = useTranslation();
18
19
  const t = _t;
19
- const dayStart = new Date(day);
20
- dayStart.setHours(startHour, 0, 0, 0);
21
- const dayEnd = new Date(day);
22
- dayEnd.setHours(endHour, 0, 0, 0);
23
- const { data } = useResourceAvailability(apiBase, resource.id, dayStart, dayEnd);
24
20
  const isoDay = getDayKeyInTimezone(day, timeZone);
21
+ const dayStart = gridInstant(isoDay, startHour, timeZone);
22
+ const dayEnd = gridInstant(isoDay, endHour, timeZone);
23
+ const { data } = useResourceAvailability(apiBase, resource.id, dayStart, dayEnd);
25
24
  const dayAvail = data?.days.find((d)=>d.date === isoDay);
26
25
  const slots = dayAvail ? computeSlotStates({
27
26
  busy: data.busy,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/CalendarView/LaneTimelineView.tsx"],"sourcesContent":["'use client'\nimport { useTranslation } from '@payloadcms/ui'\nimport React from 'react'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { SlotState } from '../../utilities/computeSlotStates.js'\n\nimport { computeSlotStates } from '../../utilities/computeSlotStates.js'\nimport { getDayKeyInTimezone } from '../../utilities/timezoneUtils.js'\nimport styles from './CalendarView.module.css'\nimport { useResourceAvailability } from './useResourceAvailability.js'\n\ntype LaneResource = { id: string; name: string }\n\nconst SLOT_STATE_KEYS: Record<SlotState, string> = {\n external: 'reservation:slotExternal',\n free: 'reservation:slotFree',\n full: 'reservation:slotFull',\n 'off-shift': 'reservation:slotOffShift',\n 'time-off': 'reservation:slotTimeOff',\n}\n\nfunction Lane({\n apiBase,\n day,\n endHour,\n onBook,\n resource,\n startHour,\n timeZone,\n}: {\n apiBase: string\n day: Date\n endHour: number\n onBook: (resourceId: string, startIso: string) => void\n resource: LaneResource\n startHour: number\n timeZone: string\n}) {\n const { t: _t } = useTranslation()\n const t = _t as PluginT\n\n const dayStart = new Date(day)\n dayStart.setHours(startHour, 0, 0, 0)\n const dayEnd = new Date(day)\n dayEnd.setHours(endHour, 0, 0, 0)\n\n const { data } = useResourceAvailability(apiBase, resource.id, dayStart, dayEnd)\n const isoDay = getDayKeyInTimezone(day, timeZone)\n const dayAvail = data?.days.find((d) => d.date === isoDay)\n\n const slots = dayAvail\n ? computeSlotStates({\n busy: data!.busy,\n capacityMode: data!.capacityMode,\n dayEnd,\n dayStart,\n external: data!.external,\n quantity: data!.quantity,\n requiredPools: data!.requiredPools,\n shiftWindows: dayAvail.shiftWindows,\n step: 60,\n timeOff: dayAvail.timeOff,\n })\n : []\n\n return (\n <div className={styles.lane}>\n <div className={styles.laneLabel}>{resource.name}</div>\n <div className={styles.laneTrack}>\n {slots.map((s) => {\n const cls =\n s.state === 'off-shift'\n ? styles.slotOffShift\n : s.state === 'time-off'\n ? styles.slotTimeOff\n : s.state === 'external'\n ? styles.slotExternal\n : s.state === 'full'\n ? styles.slotFull\n : styles.slotFree\n const isFree = s.state === 'free'\n const slotLabel = `${s.start.toLocaleTimeString([], {\n hour: '2-digit',\n minute: '2-digit',\n timeZone,\n })} — ${t(SLOT_STATE_KEYS[s.state])}`\n return isFree ? (\n <div\n aria-label={slotLabel}\n className={`${styles.laneCell} ${cls}`}\n key={s.start.toISOString()}\n onClick={() => onBook(resource.id, s.start.toISOString())}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n onBook(resource.id, s.start.toISOString())\n }\n }}\n role=\"button\"\n tabIndex={0}\n title={slotLabel}\n />\n ) : (\n <div\n className={`${styles.laneCell} ${cls}`}\n key={s.start.toISOString()}\n title={slotLabel}\n />\n )\n })}\n </div>\n </div>\n )\n}\n\nexport function LaneTimelineView({\n apiBase,\n day,\n endHour,\n onBook,\n resources,\n startHour,\n timeZone,\n}: {\n apiBase: string\n day: Date\n endHour: number\n onBook: (resourceId: string, startIso: string) => void\n resources: LaneResource[]\n startHour: number\n timeZone: string\n}) {\n const { t: _t } = useTranslation()\n const t = _t as PluginT\n if (resources.length === 0) {\n return <p className={styles.hint}>{t('reservation:laneNoResources')}</p>\n }\n const hours = Array.from({ length: endHour - startHour }, (_, i) => startHour + i)\n return (\n <div className={styles.lanes}>\n <div className={styles.laneHeader}>\n <div className={styles.laneLabel} />\n <div className={styles.laneTrack}>\n {hours.map((h) => (\n <div className={styles.laneTime} key={h}>\n {String(h).padStart(2, '0')}:00\n </div>\n ))}\n </div>\n </div>\n {resources.map((r) => (\n <Lane\n apiBase={apiBase}\n day={day}\n endHour={endHour}\n key={r.id}\n onBook={onBook}\n resource={r}\n startHour={startHour}\n timeZone={timeZone}\n />\n ))}\n </div>\n )\n}\n"],"names":["useTranslation","React","computeSlotStates","getDayKeyInTimezone","styles","useResourceAvailability","SLOT_STATE_KEYS","external","free","full","Lane","apiBase","day","endHour","onBook","resource","startHour","timeZone","t","_t","dayStart","Date","setHours","dayEnd","data","id","isoDay","dayAvail","days","find","d","date","slots","busy","capacityMode","quantity","requiredPools","shiftWindows","step","timeOff","div","className","lane","laneLabel","name","laneTrack","map","s","cls","state","slotOffShift","slotTimeOff","slotExternal","slotFull","slotFree","isFree","slotLabel","start","toLocaleTimeString","hour","minute","aria-label","laneCell","onClick","toISOString","onKeyDown","e","key","preventDefault","role","tabIndex","title","LaneTimelineView","resources","length","p","hint","hours","Array","from","_","i","lanes","laneHeader","h","laneTime","String","padStart","r"],"mappings":"AAAA;;AACA,SAASA,cAAc,QAAQ,iBAAgB;AAC/C,OAAOC,WAAW,QAAO;AAKzB,SAASC,iBAAiB,QAAQ,uCAAsC;AACxE,SAASC,mBAAmB,QAAQ,mCAAkC;AACtE,OAAOC,YAAY,4BAA2B;AAC9C,SAASC,uBAAuB,QAAQ,+BAA8B;AAItE,MAAMC,kBAA6C;IACjDC,UAAU;IACVC,MAAM;IACNC,MAAM;IACN,aAAa;IACb,YAAY;AACd;AAEA,SAASC,KAAK,EACZC,OAAO,EACPC,GAAG,EACHC,OAAO,EACPC,MAAM,EACNC,QAAQ,EACRC,SAAS,EACTC,QAAQ,EAST;IACC,MAAM,EAAEC,GAAGC,EAAE,EAAE,GAAGnB;IAClB,MAAMkB,IAAIC;IAEV,MAAMC,WAAW,IAAIC,KAAKT;IAC1BQ,SAASE,QAAQ,CAACN,WAAW,GAAG,GAAG;IACnC,MAAMO,SAAS,IAAIF,KAAKT;IACxBW,OAAOD,QAAQ,CAACT,SAAS,GAAG,GAAG;IAE/B,MAAM,EAAEW,IAAI,EAAE,GAAGnB,wBAAwBM,SAASI,SAASU,EAAE,EAAEL,UAAUG;IACzE,MAAMG,SAASvB,oBAAoBS,KAAKK;IACxC,MAAMU,WAAWH,MAAMI,KAAKC,KAAK,CAACC,IAAMA,EAAEC,IAAI,KAAKL;IAEnD,MAAMM,QAAQL,WACVzB,kBAAkB;QAChB+B,MAAMT,KAAMS,IAAI;QAChBC,cAAcV,KAAMU,YAAY;QAChCX;QACAH;QACAb,UAAUiB,KAAMjB,QAAQ;QACxB4B,UAAUX,KAAMW,QAAQ;QACxBC,eAAeZ,KAAMY,aAAa;QAClCC,cAAcV,SAASU,YAAY;QACnCC,MAAM;QACNC,SAASZ,SAASY,OAAO;IAC3B,KACA,EAAE;IAEN,qBACE,MAACC;QAAIC,WAAWrC,OAAOsC,IAAI;;0BACzB,KAACF;gBAAIC,WAAWrC,OAAOuC,SAAS;0BAAG5B,SAAS6B,IAAI;;0BAChD,KAACJ;gBAAIC,WAAWrC,OAAOyC,SAAS;0BAC7Bb,MAAMc,GAAG,CAAC,CAACC;oBACV,MAAMC,MACJD,EAAEE,KAAK,KAAK,cACR7C,OAAO8C,YAAY,GACnBH,EAAEE,KAAK,KAAK,aACV7C,OAAO+C,WAAW,GAClBJ,EAAEE,KAAK,KAAK,aACV7C,OAAOgD,YAAY,GACnBL,EAAEE,KAAK,KAAK,SACV7C,OAAOiD,QAAQ,GACfjD,OAAOkD,QAAQ;oBAC3B,MAAMC,SAASR,EAAEE,KAAK,KAAK;oBAC3B,MAAMO,YAAY,GAAGT,EAAEU,KAAK,CAACC,kBAAkB,CAAC,EAAE,EAAE;wBAClDC,MAAM;wBACNC,QAAQ;wBACR3C;oBACF,GAAG,GAAG,EAAEC,EAAEZ,eAAe,CAACyC,EAAEE,KAAK,CAAC,GAAG;oBACrC,OAAOM,uBACL,KAACf;wBACCqB,cAAYL;wBACZf,WAAW,GAAGrC,OAAO0D,QAAQ,CAAC,CAAC,EAAEd,KAAK;wBAEtCe,SAAS,IAAMjD,OAAOC,SAASU,EAAE,EAAEsB,EAAEU,KAAK,CAACO,WAAW;wBACtDC,WAAW,CAACC;4BACV,IAAIA,EAAEC,GAAG,KAAK,WAAWD,EAAEC,GAAG,KAAK,KAAK;gCACtCD,EAAEE,cAAc;gCAChBtD,OAAOC,SAASU,EAAE,EAAEsB,EAAEU,KAAK,CAACO,WAAW;4BACzC;wBACF;wBACAK,MAAK;wBACLC,UAAU;wBACVC,OAAOf;uBAVFT,EAAEU,KAAK,CAACO,WAAW,oBAa1B,KAACxB;wBACCC,WAAW,GAAGrC,OAAO0D,QAAQ,CAAC,CAAC,EAAEd,KAAK;wBAEtCuB,OAAOf;uBADFT,EAAEU,KAAK,CAACO,WAAW;gBAI9B;;;;AAIR;AAEA,OAAO,SAASQ,iBAAiB,EAC/B7D,OAAO,EACPC,GAAG,EACHC,OAAO,EACPC,MAAM,EACN2D,SAAS,EACTzD,SAAS,EACTC,QAAQ,EAST;IACC,MAAM,EAAEC,GAAGC,EAAE,EAAE,GAAGnB;IAClB,MAAMkB,IAAIC;IACV,IAAIsD,UAAUC,MAAM,KAAK,GAAG;QAC1B,qBAAO,KAACC;YAAElC,WAAWrC,OAAOwE,IAAI;sBAAG1D,EAAE;;IACvC;IACA,MAAM2D,QAAQC,MAAMC,IAAI,CAAC;QAAEL,QAAQ7D,UAAUG;IAAU,GAAG,CAACgE,GAAGC,IAAMjE,YAAYiE;IAChF,qBACE,MAACzC;QAAIC,WAAWrC,OAAO8E,KAAK;;0BAC1B,MAAC1C;gBAAIC,WAAWrC,OAAO+E,UAAU;;kCAC/B,KAAC3C;wBAAIC,WAAWrC,OAAOuC,SAAS;;kCAChC,KAACH;wBAAIC,WAAWrC,OAAOyC,SAAS;kCAC7BgC,MAAM/B,GAAG,CAAC,CAACsC,kBACV,MAAC5C;gCAAIC,WAAWrC,OAAOiF,QAAQ;;oCAC5BC,OAAOF,GAAGG,QAAQ,CAAC,GAAG;oCAAK;;+BADQH;;;;YAM3CX,UAAU3B,GAAG,CAAC,CAAC0C,kBACd,KAAC9E;oBACCC,SAASA;oBACTC,KAAKA;oBACLC,SAASA;oBAETC,QAAQA;oBACRC,UAAUyE;oBACVxE,WAAWA;oBACXC,UAAUA;mBAJLuE,EAAE/D,EAAE;;;AASnB"}
1
+ {"version":3,"sources":["../../../src/components/CalendarView/LaneTimelineView.tsx"],"sourcesContent":["'use client'\nimport { useTranslation } from '@payloadcms/ui'\nimport React from 'react'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { SlotState } from '../../utilities/computeSlotStates.js'\n\nimport { gridInstant } from '../../utilities/calendarGrid.js'\nimport { computeSlotStates } from '../../utilities/computeSlotStates.js'\nimport { getDayKeyInTimezone } from '../../utilities/timezoneUtils.js'\nimport styles from './CalendarView.module.css'\nimport { useResourceAvailability } from './useResourceAvailability.js'\n\ntype LaneResource = { id: string; name: string }\n\nconst SLOT_STATE_KEYS: Record<SlotState, string> = {\n external: 'reservation:slotExternal',\n free: 'reservation:slotFree',\n full: 'reservation:slotFull',\n 'off-shift': 'reservation:slotOffShift',\n 'time-off': 'reservation:slotTimeOff',\n}\n\nfunction Lane({\n apiBase,\n day,\n endHour,\n onBook,\n resource,\n startHour,\n timeZone,\n}: {\n apiBase: string\n day: Date\n endHour: number\n onBook: (resourceId: string, startIso: string) => void\n resource: LaneResource\n startHour: number\n timeZone: string\n}) {\n const { t: _t } = useTranslation()\n const t = _t as PluginT\n\n const isoDay = getDayKeyInTimezone(day, timeZone)\n const dayStart = gridInstant(isoDay, startHour, timeZone)\n const dayEnd = gridInstant(isoDay, endHour, timeZone)\n\n const { data } = useResourceAvailability(apiBase, resource.id, dayStart, dayEnd)\n const dayAvail = data?.days.find((d) => d.date === isoDay)\n\n const slots = dayAvail\n ? computeSlotStates({\n busy: data!.busy,\n capacityMode: data!.capacityMode,\n dayEnd,\n dayStart,\n external: data!.external,\n quantity: data!.quantity,\n requiredPools: data!.requiredPools,\n shiftWindows: dayAvail.shiftWindows,\n step: 60,\n timeOff: dayAvail.timeOff,\n })\n : []\n\n return (\n <div className={styles.lane}>\n <div className={styles.laneLabel}>{resource.name}</div>\n <div className={styles.laneTrack}>\n {slots.map((s) => {\n const cls =\n s.state === 'off-shift'\n ? styles.slotOffShift\n : s.state === 'time-off'\n ? styles.slotTimeOff\n : s.state === 'external'\n ? styles.slotExternal\n : s.state === 'full'\n ? styles.slotFull\n : styles.slotFree\n const isFree = s.state === 'free'\n const slotLabel = `${s.start.toLocaleTimeString([], {\n hour: '2-digit',\n minute: '2-digit',\n timeZone,\n })} — ${t(SLOT_STATE_KEYS[s.state])}`\n return isFree ? (\n <div\n aria-label={slotLabel}\n className={`${styles.laneCell} ${cls}`}\n key={s.start.toISOString()}\n onClick={() => onBook(resource.id, s.start.toISOString())}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n onBook(resource.id, s.start.toISOString())\n }\n }}\n role=\"button\"\n tabIndex={0}\n title={slotLabel}\n />\n ) : (\n <div\n className={`${styles.laneCell} ${cls}`}\n key={s.start.toISOString()}\n title={slotLabel}\n />\n )\n })}\n </div>\n </div>\n )\n}\n\nexport function LaneTimelineView({\n apiBase,\n day,\n endHour,\n onBook,\n resources,\n startHour,\n timeZone,\n}: {\n apiBase: string\n day: Date\n endHour: number\n onBook: (resourceId: string, startIso: string) => void\n resources: LaneResource[]\n startHour: number\n timeZone: string\n}) {\n const { t: _t } = useTranslation()\n const t = _t as PluginT\n if (resources.length === 0) {\n return <p className={styles.hint}>{t('reservation:laneNoResources')}</p>\n }\n const hours = Array.from({ length: endHour - startHour }, (_, i) => startHour + i)\n return (\n <div className={styles.lanes}>\n <div className={styles.laneHeader}>\n <div className={styles.laneLabel} />\n <div className={styles.laneTrack}>\n {hours.map((h) => (\n <div className={styles.laneTime} key={h}>\n {String(h).padStart(2, '0')}:00\n </div>\n ))}\n </div>\n </div>\n {resources.map((r) => (\n <Lane\n apiBase={apiBase}\n day={day}\n endHour={endHour}\n key={r.id}\n onBook={onBook}\n resource={r}\n startHour={startHour}\n timeZone={timeZone}\n />\n ))}\n </div>\n )\n}\n"],"names":["useTranslation","React","gridInstant","computeSlotStates","getDayKeyInTimezone","styles","useResourceAvailability","SLOT_STATE_KEYS","external","free","full","Lane","apiBase","day","endHour","onBook","resource","startHour","timeZone","t","_t","isoDay","dayStart","dayEnd","data","id","dayAvail","days","find","d","date","slots","busy","capacityMode","quantity","requiredPools","shiftWindows","step","timeOff","div","className","lane","laneLabel","name","laneTrack","map","s","cls","state","slotOffShift","slotTimeOff","slotExternal","slotFull","slotFree","isFree","slotLabel","start","toLocaleTimeString","hour","minute","aria-label","laneCell","onClick","toISOString","onKeyDown","e","key","preventDefault","role","tabIndex","title","LaneTimelineView","resources","length","p","hint","hours","Array","from","_","i","lanes","laneHeader","h","laneTime","String","padStart","r"],"mappings":"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"}
@@ -2,11 +2,12 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useConfig, useDocumentDrawer, useTranslation } from '@payloadcms/ui';
4
4
  import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
5
+ import { dayKeySequence, displayDateForDayKey, gridInstant, instantAtHour, monthGridStartDayKey, startOfWeekDayKey } from '../../utilities/calendarGrid.js';
5
6
  import { computeSlotStates } from '../../utilities/computeSlotStates.js';
6
7
  import { externalPillLabel } from '../../utilities/externalPillLabel.js';
7
8
  import { statusToI18nKey } from '../../utilities/i18nUtils.js';
8
9
  import { reservationMatchesResource, sameId } from '../../utilities/reservationResourceFilter.js';
9
- import { getDayKeyInTimezone, getHourInTimezone } from '../../utilities/timezoneUtils.js';
10
+ import { addDaysToDayKey, getDayKeyInTimezone, getHourInTimezone } from '../../utilities/timezoneUtils.js';
10
11
  import { useTenantFilter } from '../../utilities/useTenantFilter.js';
11
12
  import styles from './CalendarView.module.css';
12
13
  import { LaneTimelineView } from './LaneTimelineView.js';
@@ -240,28 +241,28 @@ export const CalendarView = ()=>{
240
241
  resourceTenantParams
241
242
  ]);
242
243
  const { rangeEnd, rangeStart } = useMemo(()=>{
243
- const start = new Date(currentDate);
244
- const end = new Date(currentDate);
244
+ const currentKey = getDayKeyInTimezone(currentDate, reservationTimezone);
245
+ let startKey;
246
+ let dayCount;
245
247
  if (viewMode === 'month') {
246
- start.setDate(1);
247
- start.setDate(start.getDate() - start.getDay());
248
- // The grid always renders 42 cells (6 weeks) from `start`; fetch the same
249
- // span so trailing weeks aren't silently empty (review D1).
250
- end.setTime(start.getTime());
251
- end.setDate(start.getDate() + 41);
248
+ // The grid always renders 42 cells (6 weeks); fetch the same span so
249
+ // trailing weeks aren't silently empty (review D1).
250
+ startKey = monthGridStartDayKey(currentKey);
251
+ dayCount = 42;
252
252
  } else if (viewMode === 'week') {
253
- const dayOfWeek = start.getDay();
254
- start.setDate(start.getDate() - dayOfWeek);
255
- end.setDate(start.getDate() + 6);
253
+ startKey = startOfWeekDayKey(currentKey);
254
+ dayCount = 7;
255
+ } else {
256
+ startKey = currentKey;
257
+ dayCount = 1;
256
258
  }
257
- start.setHours(0, 0, 0, 0);
258
- end.setHours(23, 59, 59, 999);
259
259
  return {
260
- rangeEnd: end,
261
- rangeStart: start
260
+ rangeEnd: instantAtHour(addDaysToDayKey(startKey, dayCount), 0, reservationTimezone),
261
+ rangeStart: instantAtHour(startKey, 0, reservationTimezone)
262
262
  };
263
263
  }, [
264
264
  currentDate,
265
+ reservationTimezone,
265
266
  viewMode
266
267
  ]);
267
268
  // Availability data for the selected resource (null when no resource selected — grid unshaded)
@@ -275,7 +276,10 @@ export const CalendarView = ()=>{
275
276
  limit: String(MAX_LIST_LIMIT),
276
277
  sort: 'startTime',
277
278
  'where[startTime][greater_than_equal]': rangeStart.toISOString(),
278
- 'where[startTime][less_than_equal]': rangeEnd.toISOString(),
279
+ // rangeEnd is exclusive (midnight starting the day after the last
280
+ // rendered day), so the bound must be too — a reservation starting
281
+ // exactly at that midnight belongs to the next span, not this one.
282
+ 'where[startTime][less_than]': rangeEnd.toISOString(),
279
283
  ...reservationTenantParams
280
284
  });
281
285
  const response = await fetch(`${apiUrl}?${params}`);
@@ -735,9 +739,9 @@ export const CalendarView = ()=>{
735
739
  }, key))
736
740
  });
737
741
  };
738
- const renderCurrentTimeLine = (cellDate, cellHour)=>{
742
+ const renderCurrentTimeLine = (cellDayKey, cellHour)=>{
739
743
  const now = new Date();
740
- if (now.getFullYear() !== cellDate.getFullYear() || now.getMonth() !== cellDate.getMonth() || now.getDate() !== cellDate.getDate() || now.getHours() !== cellHour) {
744
+ if (getDayKeyInTimezone(now, reservationTimezone) !== cellDayKey || getHourInTimezone(now, reservationTimezone) !== cellHour) {
741
745
  return null;
742
746
  }
743
747
  const topPercent = now.getMinutes() / 60 * 100;
@@ -749,15 +753,8 @@ export const CalendarView = ()=>{
749
753
  });
750
754
  };
751
755
  const renderMonthView = ()=>{
752
- const firstDay = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1);
753
- const startDay = new Date(firstDay);
754
- startDay.setDate(startDay.getDate() - startDay.getDay());
755
- const days = [];
756
- const d = new Date(startDay);
757
- for(let i = 0; i < 42; i++){
758
- days.push(new Date(d));
759
- d.setDate(d.getDate() + 1);
760
- }
756
+ const currentKey = getDayKeyInTimezone(currentDate, reservationTimezone);
757
+ const dayKeys = dayKeySequence(monthGridStartDayKey(currentKey), 42);
761
758
  const today = new Date();
762
759
  const todayStr = getDayKeyInTimezone(today, reservationTimezone);
763
760
  return /*#__PURE__*/ _jsxs("div", {
@@ -775,10 +772,9 @@ export const CalendarView = ()=>{
775
772
  className: styles.dayHeader,
776
773
  children: d
777
774
  }, d)),
778
- days.map((day, i)=>{
779
- const dayKey = getDayKeyInTimezone(day, reservationTimezone);
775
+ dayKeys.map((dayKey, i)=>{
780
776
  const isToday = dayKey === todayStr;
781
- const isOtherMonth = dayKey.slice(0, 7) !== getDayKeyInTimezone(currentDate, reservationTimezone).slice(0, 7);
777
+ const isOtherMonth = dayKey.slice(0, 7) !== currentKey.slice(0, 7);
782
778
  const dayReservations = filteredReservations.filter((r)=>{
783
779
  const rKey = getDayKeyInTimezone(new Date(r.startTime), reservationTimezone);
784
780
  return rKey === dayKey;
@@ -792,8 +788,7 @@ export const CalendarView = ()=>{
792
788
  const endKey = getDayKeyInTimezone(new Date(new Date(ev.end).getTime() - 1), reservationTimezone);
793
789
  return startKey <= dayKey && dayKey <= endKey;
794
790
  });
795
- const clickDate = new Date(day);
796
- clickDate.setHours(9, 0, 0, 0);
791
+ const clickDate = instantAtHour(dayKey, 9, reservationTimezone);
797
792
  return /*#__PURE__*/ _jsxs("div", {
798
793
  className: `${styles.dayCell} ${isOtherMonth ? styles.dayCellOtherMonth : ''} ${isToday ? styles.dayCellToday : ''}`,
799
794
  onClick: ()=>handleDateClick(clickDate),
@@ -808,7 +803,7 @@ export const CalendarView = ()=>{
808
803
  children: [
809
804
  /*#__PURE__*/ _jsx("div", {
810
805
  className: styles.dayNumber,
811
- children: day.getDate()
806
+ children: Number(dayKey.slice(8, 10))
812
807
  }),
813
808
  dayReservations.map((r)=>renderEventItem(r, true)),
814
809
  dayExternal.map((ev, j)=>/*#__PURE__*/ _jsx("div", {
@@ -825,19 +820,12 @@ export const CalendarView = ()=>{
825
820
  });
826
821
  };
827
822
  const renderWeekView = ()=>{
828
- const startOfWeek = new Date(currentDate);
829
- startOfWeek.setDate(startOfWeek.getDate() - startOfWeek.getDay());
830
- startOfWeek.setHours(0, 0, 0, 0);
831
- const weekDays = [];
832
- for(let i = 0; i < 7; i++){
833
- const d = new Date(startOfWeek);
834
- d.setDate(d.getDate() + i);
835
- weekDays.push(d);
836
- }
823
+ const currentKey = getDayKeyInTimezone(currentDate, reservationTimezone);
824
+ const weekDayKeys = dayKeySequence(startOfWeekDayKey(currentKey), 7);
837
825
  // Visible-hour window derived from the week's bookings (review D8)
838
826
  const weekReservations = filteredReservations.filter((r)=>{
839
827
  const k = getDayKeyInTimezone(new Date(r.startTime), reservationTimezone);
840
- return weekDays.some((d)=>getDayKeyInTimezone(d, reservationTimezone) === k);
828
+ return weekDayKeys.includes(k);
841
829
  });
842
830
  const { endHour: gridEndHour, startHour: gridStartHour } = computeHourWindow(weekReservations, reservationTimezone);
843
831
  const hours = Array.from({
@@ -845,13 +833,10 @@ export const CalendarView = ()=>{
845
833
  }, (_, i)=>i + gridStartHour);
846
834
  const gridStep = 60;
847
835
  // Build per-day slot-state maps when a resource is selected
848
- const daySlotMaps = availability ? new Map(weekDays.map((day)=>{
849
- const isoDay = getDayKeyInTimezone(day, reservationTimezone);
836
+ const daySlotMaps = availability ? new Map(weekDayKeys.map((isoDay)=>{
850
837
  const dayAvail = availability.days.find((d)=>d.date === isoDay);
851
- const dayStart = new Date(day);
852
- dayStart.setHours(gridStartHour, 0, 0, 0);
853
- const dayEnd = new Date(day);
854
- dayEnd.setHours(gridEndHour, 0, 0, 0);
838
+ const dayStart = gridInstant(isoDay, gridStartHour, reservationTimezone);
839
+ const dayEnd = gridInstant(isoDay, gridEndHour, reservationTimezone);
855
840
  const slots = dayAvail ? computeSlotStates({
856
841
  busy: availability.busy,
857
842
  capacityMode: availability.capacityMode,
@@ -880,9 +865,9 @@ export const CalendarView = ()=>{
880
865
  /*#__PURE__*/ _jsx("div", {
881
866
  className: styles.dayHeader
882
867
  }),
883
- weekDays.map((d, i)=>/*#__PURE__*/ _jsx("div", {
868
+ weekDayKeys.map((dayKey, i)=>/*#__PURE__*/ _jsx("div", {
884
869
  className: styles.dayHeader,
885
- children: d.toLocaleDateString([], {
870
+ children: displayDateForDayKey(dayKey, reservationTimezone).toLocaleDateString([], {
886
871
  day: 'numeric',
887
872
  month: 'numeric',
888
873
  timeZone: reservationTimezone,
@@ -898,14 +883,12 @@ export const CalendarView = ()=>{
898
883
  ":00"
899
884
  ]
900
885
  }),
901
- weekDays.map((day, di)=>{
902
- const isoDay = getDayKeyInTimezone(day, reservationTimezone);
886
+ weekDayKeys.map((isoDay, di)=>{
903
887
  const cellReservations = filteredReservations.filter((r)=>{
904
888
  const rDate = new Date(r.startTime);
905
889
  return getDayKeyInTimezone(rDate, reservationTimezone) === isoDay && getHourInTimezone(rDate, reservationTimezone) === hour;
906
890
  });
907
- const clickDate = new Date(day);
908
- clickDate.setHours(hour, 0, 0, 0);
891
+ const clickDate = instantAtHour(isoDay, hour, reservationTimezone);
909
892
  // Slot state (only when a resource is selected)
910
893
  const slotMap = daySlotMaps?.get(isoDay);
911
894
  const slotInfo = slotMap?.get(clickDate.toISOString()) ?? null;
@@ -951,7 +934,7 @@ export const CalendarView = ()=>{
951
934
  role: "button",
952
935
  tabIndex: isNonInteractive ? -1 : 0,
953
936
  children: [
954
- renderCurrentTimeLine(day, hour),
937
+ renderCurrentTimeLine(isoDay, hour),
955
938
  timeOffLabel && /*#__PURE__*/ _jsx("span", {
956
939
  className: styles.timeOffLabel,
957
940
  children: timeOffLabel
@@ -987,10 +970,8 @@ export const CalendarView = ()=>{
987
970
  if (availability) {
988
971
  const isoDay = currentDayKey;
989
972
  const dayAvail = availability.days.find((d)=>d.date === isoDay);
990
- const dayStart = new Date(currentDate);
991
- dayStart.setHours(gridStartHour, 0, 0, 0);
992
- const dayEnd = new Date(currentDate);
993
- dayEnd.setHours(gridEndHour, 0, 0, 0);
973
+ const dayStart = gridInstant(isoDay, gridStartHour, reservationTimezone);
974
+ const dayEnd = gridInstant(isoDay, gridEndHour, reservationTimezone);
994
975
  const slots = dayAvail ? computeSlotStates({
995
976
  busy: availability.busy,
996
977
  capacityMode: availability.capacityMode,
@@ -1015,8 +996,7 @@ export const CalendarView = ()=>{
1015
996
  const rDate = new Date(r.startTime);
1016
997
  return getDayKeyInTimezone(rDate, reservationTimezone) === currentDayKey && getHourInTimezone(rDate, reservationTimezone) === hour;
1017
998
  });
1018
- const clickDate = new Date(currentDate);
1019
- clickDate.setHours(hour, 0, 0, 0);
999
+ const clickDate = instantAtHour(currentDayKey, hour, reservationTimezone);
1020
1000
  // Slot state (only when a resource is selected)
1021
1001
  const slotInfo = daySlotMap?.get(clickDate.toISOString()) ?? null;
1022
1002
  // Derive cell CSS class and interactivity based on slot state
@@ -1070,7 +1050,7 @@ export const CalendarView = ()=>{
1070
1050
  role: "button",
1071
1051
  tabIndex: isNonInteractive ? -1 : 0,
1072
1052
  children: [
1073
- renderCurrentTimeLine(currentDate, hour),
1053
+ renderCurrentTimeLine(currentDayKey, hour),
1074
1054
  timeOffLabel && /*#__PURE__*/ _jsx("span", {
1075
1055
  className: styles.timeOffLabel,
1076
1056
  children: timeOffLabel
@@ -1267,28 +1247,32 @@ export const CalendarView = ()=>{
1267
1247
  ]
1268
1248
  });
1269
1249
  };
1250
+ // The header must name the same span the grid renders, so it resolves the
1251
+ // month/week in the BUSINESS zone exactly as renderMonthView/renderWeekView do.
1252
+ // Deriving it from viewer-zone Date math instead lets the label drift a day —
1253
+ // and, at a week boundary, a whole week — away from the cells below it.
1270
1254
  const dateLabel = useMemo(()=>{
1271
1255
  if (viewMode === 'month') {
1272
1256
  return currentDate.toLocaleDateString([], {
1273
1257
  month: 'long',
1258
+ timeZone: reservationTimezone,
1274
1259
  year: 'numeric'
1275
1260
  });
1276
1261
  }
1277
1262
  if (viewMode === 'week') {
1278
- const startOfWeek = new Date(currentDate);
1279
- startOfWeek.setDate(startOfWeek.getDate() - startOfWeek.getDay());
1280
- const endOfWeek = new Date(startOfWeek);
1281
- endOfWeek.setDate(endOfWeek.getDate() + 6);
1282
- return `${startOfWeek.toLocaleDateString([], {
1263
+ const startKey = startOfWeekDayKey(getDayKeyInTimezone(currentDate, reservationTimezone));
1264
+ const start = displayDateForDayKey(startKey, reservationTimezone).toLocaleDateString([], {
1283
1265
  day: 'numeric',
1284
1266
  month: 'short',
1285
1267
  timeZone: reservationTimezone
1286
- })} - ${endOfWeek.toLocaleDateString([], {
1268
+ });
1269
+ const end = displayDateForDayKey(addDaysToDayKey(startKey, 6), reservationTimezone).toLocaleDateString([], {
1287
1270
  day: 'numeric',
1288
1271
  month: 'short',
1289
1272
  timeZone: reservationTimezone,
1290
1273
  year: 'numeric'
1291
- })}`;
1274
+ });
1275
+ return `${start} - ${end}`;
1292
1276
  }
1293
1277
  return currentDate.toLocaleDateString([], {
1294
1278
  day: 'numeric',