payload-reserve 2.4.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.
- package/README.md +217 -4
- package/dist/collections/Holds.d.ts +18 -0
- package/dist/collections/Holds.js +113 -0
- package/dist/collections/Holds.js.map +1 -0
- package/dist/collections/Reservations.js +8 -0
- package/dist/collections/Reservations.js.map +1 -1
- package/dist/collections/Resources.js +35 -1
- package/dist/collections/Resources.js.map +1 -1
- package/dist/collections/Services.js +41 -0
- package/dist/collections/Services.js.map +1 -1
- package/dist/components/CalendarView/LaneTimelineView.js +4 -5
- package/dist/components/CalendarView/LaneTimelineView.js.map +1 -1
- package/dist/components/CalendarView/index.js +55 -71
- package/dist/components/CalendarView/index.js.map +1 -1
- package/dist/components/DashboardWidget/DashboardWidgetServer.js +11 -74
- package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
- package/dist/components/DashboardWidget/fetchDashboardStats.d.ts +42 -0
- package/dist/components/DashboardWidget/fetchDashboardStats.js +113 -0
- package/dist/components/DashboardWidget/fetchDashboardStats.js.map +1 -0
- package/dist/defaults.d.ts +2 -0
- package/dist/defaults.js +11 -0
- package/dist/defaults.js.map +1 -1
- package/dist/endpoints/cancelBooking.js +43 -14
- package/dist/endpoints/cancelBooking.js.map +1 -1
- package/dist/endpoints/checkAvailability.js +12 -1
- package/dist/endpoints/checkAvailability.js.map +1 -1
- package/dist/endpoints/createBooking.js +108 -7
- package/dist/endpoints/createBooking.js.map +1 -1
- package/dist/endpoints/customerSearch.js +6 -0
- package/dist/endpoints/customerSearch.js.map +1 -1
- package/dist/endpoints/effectiveTimezone.js +1 -0
- package/dist/endpoints/effectiveTimezone.js.map +1 -1
- package/dist/endpoints/getSlots.js +12 -1
- package/dist/endpoints/getSlots.js.map +1 -1
- package/dist/endpoints/holdSlot.d.ts +12 -0
- package/dist/endpoints/holdSlot.js +126 -0
- package/dist/endpoints/holdSlot.js.map +1 -0
- package/dist/endpoints/releaseSlot.d.ts +8 -0
- package/dist/endpoints/releaseSlot.js +34 -0
- package/dist/endpoints/releaseSlot.js.map +1 -0
- package/dist/endpoints/resourceAvailability.d.ts +2 -0
- package/dist/endpoints/resourceAvailability.js +79 -3
- package/dist/endpoints/resourceAvailability.js.map +1 -1
- package/dist/hooks/holds/validateHoldSlot.d.ts +14 -0
- package/dist/hooks/holds/validateHoldSlot.js +90 -0
- package/dist/hooks/holds/validateHoldSlot.js.map +1 -0
- package/dist/hooks/reservations/acquireBookingLock.d.ts +30 -0
- package/dist/hooks/reservations/acquireBookingLock.js +71 -0
- package/dist/hooks/reservations/acquireBookingLock.js.map +1 -0
- package/dist/hooks/reservations/calculateEndTime.js +13 -1
- package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
- package/dist/hooks/reservations/expandRequiredResources.js +3 -0
- package/dist/hooks/reservations/expandRequiredResources.js.map +1 -1
- package/dist/hooks/reservations/validateActive.d.ts +11 -0
- package/dist/hooks/reservations/validateActive.js +103 -0
- package/dist/hooks/reservations/validateActive.js.map +1 -0
- package/dist/hooks/reservations/validateConflicts.js +7 -1
- package/dist/hooks/reservations/validateConflicts.js.map +1 -1
- package/dist/hooks/reservations/validateGuestBooking.js +3 -0
- package/dist/hooks/reservations/validateGuestBooking.js.map +1 -1
- package/dist/hooks/shared/preventDeleteWhenReferenced.d.ts +42 -0
- package/dist/hooks/shared/preventDeleteWhenReferenced.js +87 -0
- package/dist/hooks/shared/preventDeleteWhenReferenced.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin.js +179 -3
- package/dist/plugin.js.map +1 -1
- package/dist/services/AvailabilityService.d.ts +46 -5
- package/dist/services/AvailabilityService.js +168 -12
- package/dist/services/AvailabilityService.js.map +1 -1
- package/dist/services/HoldService.d.ts +53 -0
- package/dist/services/HoldService.js +169 -0
- package/dist/services/HoldService.js.map +1 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js.map +1 -1
- package/dist/translations/ar.json +5 -1
- package/dist/translations/de.json +5 -1
- package/dist/translations/en.json +5 -1
- package/dist/translations/es.json +5 -1
- package/dist/translations/fa.json +5 -1
- package/dist/translations/fr.json +5 -1
- package/dist/translations/hi.json +5 -1
- package/dist/translations/id.json +5 -1
- package/dist/translations/pl.json +5 -1
- package/dist/translations/ru.json +5 -1
- package/dist/translations/tr.json +5 -1
- package/dist/translations/zh.json +5 -1
- package/dist/types.d.ts +29 -0
- package/dist/types.js.map +1 -1
- package/dist/utilities/calendarGrid.d.ts +44 -0
- package/dist/utilities/calendarGrid.js +67 -0
- package/dist/utilities/calendarGrid.js.map +1 -0
- package/dist/utilities/reservationChanges.d.ts +6 -1
- package/dist/utilities/reservationChanges.js +1 -1
- package/dist/utilities/reservationChanges.js.map +1 -1
- package/dist/utilities/resolveReservationItems.d.ts +13 -2
- package/dist/utilities/resolveReservationItems.js +87 -2
- package/dist/utilities/resolveReservationItems.js.map +1 -1
- package/dist/utilities/retryOnWriteConflict.d.ts +66 -0
- package/dist/utilities/retryOnWriteConflict.js +135 -0
- package/dist/utilities/retryOnWriteConflict.js.map +1 -0
- package/dist/utilities/tenantTimezone.d.ts +77 -1
- package/dist/utilities/tenantTimezone.js +163 -2
- package/dist/utilities/tenantTimezone.js.map +1 -1
- package/dist/utilities/transactionSupport.d.ts +13 -0
- package/dist/utilities/transactionSupport.js +30 -0
- package/dist/utilities/transactionSupport.js.map +1 -0
- 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
|
|
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
|
|
244
|
-
|
|
244
|
+
const currentKey = getDayKeyInTimezone(currentDate, reservationTimezone);
|
|
245
|
+
let startKey;
|
|
246
|
+
let dayCount;
|
|
245
247
|
if (viewMode === 'month') {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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:
|
|
261
|
-
rangeStart:
|
|
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
|
-
|
|
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 = (
|
|
742
|
+
const renderCurrentTimeLine = (cellDayKey, cellHour)=>{
|
|
739
743
|
const now = new Date();
|
|
740
|
-
if (now
|
|
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
|
|
753
|
-
const
|
|
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
|
-
|
|
779
|
-
const dayKey = getDayKeyInTimezone(day, reservationTimezone);
|
|
775
|
+
dayKeys.map((dayKey, i)=>{
|
|
780
776
|
const isToday = dayKey === todayStr;
|
|
781
|
-
const isOtherMonth = dayKey.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 =
|
|
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:
|
|
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
|
|
829
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
852
|
-
|
|
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
|
-
|
|
868
|
+
weekDayKeys.map((dayKey, i)=>/*#__PURE__*/ _jsx("div", {
|
|
884
869
|
className: styles.dayHeader,
|
|
885
|
-
children:
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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 =
|
|
991
|
-
|
|
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 =
|
|
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(
|
|
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
|
|
1279
|
-
|
|
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
|
-
})
|
|
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',
|