payload-reserve 1.6.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +55 -3
  2. package/dist/collections/Reservations.js +19 -7
  3. package/dist/collections/Reservations.js.map +1 -1
  4. package/dist/collections/Resources.js +11 -8
  5. package/dist/collections/Resources.js.map +1 -1
  6. package/dist/collections/Schedules.js +12 -6
  7. package/dist/collections/Schedules.js.map +1 -1
  8. package/dist/collections/Services.js +19 -10
  9. package/dist/collections/Services.js.map +1 -1
  10. package/dist/components/AvailabilityOverview/index.js +76 -18
  11. package/dist/components/AvailabilityOverview/index.js.map +1 -1
  12. package/dist/components/CalendarView/CalendarView.module.css +9 -0
  13. package/dist/components/CalendarView/LaneTimelineView.d.ts +4 -1
  14. package/dist/components/CalendarView/LaneTimelineView.js +17 -12
  15. package/dist/components/CalendarView/LaneTimelineView.js.map +1 -1
  16. package/dist/components/CalendarView/index.js +166 -44
  17. package/dist/components/CalendarView/index.js.map +1 -1
  18. package/dist/components/CustomerField/index.js +8 -3
  19. package/dist/components/CustomerField/index.js.map +1 -1
  20. package/dist/components/DashboardWidget/DashboardWidgetServer.js +91 -18
  21. package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
  22. package/dist/defaults.js +44 -9
  23. package/dist/defaults.js.map +1 -1
  24. package/dist/endpoints/cancelBooking.js +1 -1
  25. package/dist/endpoints/cancelBooking.js.map +1 -1
  26. package/dist/endpoints/checkAvailability.js +56 -7
  27. package/dist/endpoints/checkAvailability.js.map +1 -1
  28. package/dist/endpoints/createBooking.js +19 -10
  29. package/dist/endpoints/createBooking.js.map +1 -1
  30. package/dist/endpoints/customerSearch.js +5 -2
  31. package/dist/endpoints/customerSearch.js.map +1 -1
  32. package/dist/endpoints/effectiveTimezone.d.ts +13 -0
  33. package/dist/endpoints/effectiveTimezone.js +41 -0
  34. package/dist/endpoints/effectiveTimezone.js.map +1 -0
  35. package/dist/endpoints/getSlots.js +56 -7
  36. package/dist/endpoints/getSlots.js.map +1 -1
  37. package/dist/endpoints/resourceAvailability.d.ts +4 -1
  38. package/dist/endpoints/resourceAvailability.js +102 -26
  39. package/dist/endpoints/resourceAvailability.js.map +1 -1
  40. package/dist/hooks/reservations/calculateEndTime.js +48 -20
  41. package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
  42. package/dist/hooks/reservations/enforceCustomerOwnership.d.ts +11 -0
  43. package/dist/hooks/reservations/enforceCustomerOwnership.js +30 -0
  44. package/dist/hooks/reservations/enforceCustomerOwnership.js.map +1 -0
  45. package/dist/hooks/reservations/onStatusChange.js +10 -4
  46. package/dist/hooks/reservations/onStatusChange.js.map +1 -1
  47. package/dist/hooks/reservations/validateCancellation.js +3 -2
  48. package/dist/hooks/reservations/validateCancellation.js.map +1 -1
  49. package/dist/hooks/reservations/validateConflicts.js +23 -4
  50. package/dist/hooks/reservations/validateConflicts.js.map +1 -1
  51. package/dist/hooks/reservations/validateGuestBooking.js +3 -4
  52. package/dist/hooks/reservations/validateGuestBooking.js.map +1 -1
  53. package/dist/hooks/reservations/validateStatusTransition.js +2 -2
  54. package/dist/hooks/reservations/validateStatusTransition.js.map +1 -1
  55. package/dist/hooks/users/provisionStaffResource.js +5 -8
  56. package/dist/hooks/users/provisionStaffResource.js.map +1 -1
  57. package/dist/plugin.js +83 -14
  58. package/dist/plugin.js.map +1 -1
  59. package/dist/services/AvailabilityService.d.ts +54 -2
  60. package/dist/services/AvailabilityService.js +180 -46
  61. package/dist/services/AvailabilityService.js.map +1 -1
  62. package/dist/translations/ar.json +1 -0
  63. package/dist/translations/de.json +1 -0
  64. package/dist/translations/en.json +1 -0
  65. package/dist/translations/es.json +1 -0
  66. package/dist/translations/fa.json +1 -0
  67. package/dist/translations/fr.json +1 -0
  68. package/dist/translations/hi.json +1 -0
  69. package/dist/translations/id.json +1 -0
  70. package/dist/translations/pl.json +1 -0
  71. package/dist/translations/ru.json +1 -0
  72. package/dist/translations/tr.json +1 -0
  73. package/dist/translations/zh.json +1 -0
  74. package/dist/types.d.ts +46 -1
  75. package/dist/types.js +2 -0
  76. package/dist/types.js.map +1 -1
  77. package/dist/utilities/collectionOverrides.d.ts +14 -0
  78. package/dist/utilities/collectionOverrides.js +47 -0
  79. package/dist/utilities/collectionOverrides.js.map +1 -0
  80. package/dist/utilities/ownerAccess.d.ts +6 -0
  81. package/dist/utilities/ownerAccess.js +25 -12
  82. package/dist/utilities/ownerAccess.js.map +1 -1
  83. package/dist/utilities/reservationChanges.d.ts +17 -0
  84. package/dist/utilities/reservationChanges.js +88 -0
  85. package/dist/utilities/reservationChanges.js.map +1 -0
  86. package/dist/utilities/scheduleUtils.d.ts +14 -8
  87. package/dist/utilities/scheduleUtils.js +26 -19
  88. package/dist/utilities/scheduleUtils.js.map +1 -1
  89. package/dist/utilities/tenantTimezone.d.ts +41 -0
  90. package/dist/utilities/tenantTimezone.js +77 -0
  91. package/dist/utilities/tenantTimezone.js.map +1 -0
  92. package/dist/utilities/timezoneUtils.d.ts +44 -0
  93. package/dist/utilities/timezoneUtils.js +146 -0
  94. package/dist/utilities/timezoneUtils.js.map +1 -0
  95. package/package.json +1 -1
@@ -33,8 +33,11 @@ export function createCustomerSearchEndpoint(config) {
33
33
  }
34
34
  const url = new URL(req.url);
35
35
  const search = url.searchParams.get('search') ?? '';
36
- const limit = Math.min(Number(url.searchParams.get('limit') ?? '10'), 50);
37
- const page = Math.max(Number(url.searchParams.get('page') ?? '1'), 1);
36
+ const limitRaw = Number(url.searchParams.get('limit') ?? '10');
37
+ const pageRaw = Number(url.searchParams.get('page') ?? '1');
38
+ // Non-numeric input falls back to defaults instead of passing NaN to the DB
39
+ const limit = Number.isFinite(limitRaw) ? Math.min(Math.max(Math.floor(limitRaw), 1), 50) : 10;
40
+ const page = Number.isFinite(pageRaw) ? Math.max(Math.floor(pageRaw), 1) : 1;
38
41
  // Detect which fields exist on the target collection at runtime
39
42
  const collectionConfig = req.payload.collections[config.slugs.customers]?.config;
40
43
  const availableFields = collectionConfig ? getNamedFields(collectionConfig.fields) : new Set();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/endpoints/customerSearch.ts"],"sourcesContent":["import type { CollectionSlug, Endpoint, Field, Where } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { isPrivilegedUser, privilegedRoles } from '../utilities/userRoles.js'\n\n/**\n * Inspect a collection's field list and return the set of top-level named\n * fields as a plain Set<string>. Unnamed fields (rows, groups without a name,\n * etc.) are skipped.\n */\nfunction getNamedFields(fields: Field[]): Set<string> {\n const names = new Set<string>()\n for (const field of fields) {\n if ('name' in field) {\n names.add(field.name)\n }\n }\n return names\n}\n\nexport function createCustomerSearchEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n if (!req.user) {\n return Response.json({ message: 'Unauthorized' }, { status: 401 })\n }\n\n // Only staff/admin may search customers. Role-aware so it works when staff\n // and customers share one auth collection (userCollection set).\n if (!isPrivilegedUser(req.user, config)) {\n return Response.json({ message: 'Forbidden' }, { status: 403 })\n }\n\n const url = new URL(req.url!)\n const search = url.searchParams.get('search') ?? ''\n const limit = Math.min(Number(url.searchParams.get('limit') ?? '10'), 50)\n const page = Math.max(Number(url.searchParams.get('page') ?? '1'), 1)\n\n // Detect which fields exist on the target collection at runtime\n const collectionConfig = req.payload.collections[config.slugs.customers as unknown as CollectionSlug]?.config\n const availableFields: Set<string> = collectionConfig\n ? getNamedFields(collectionConfig.fields)\n : new Set()\n\n const hasName = availableFields.has('name')\n const hasFirstName = availableFields.has('firstName')\n const hasLastName = availableFields.has('lastName')\n const hasPhone = availableFields.has('phone')\n\n const andClauses: Where[] = []\n\n if (search) {\n const orClauses: Where[] = []\n\n if (hasName) {\n orClauses.push({ name: { contains: search } })\n }\n if (hasFirstName) {\n orClauses.push({ firstName: { contains: search } })\n }\n if (hasLastName) {\n orClauses.push({ lastName: { contains: search } })\n }\n // email is always present on auth collections\n orClauses.push({ email: { contains: search } })\n if (hasPhone) {\n orClauses.push({ phone: { contains: search } })\n }\n\n andClauses.push({ or: orClauses })\n }\n\n // Single-collection mode: staff/admin live in the same collection as\n // customers, so exclude privileged roles — the dropdown should list only\n // actual customers, not bookable-looking staff.\n if (config.userCollection) {\n const roleField = config.staffProvisioning?.roleField ?? 'role'\n const priv = privilegedRoles(config)\n if (priv.length > 0) {\n andClauses.push({ [roleField]: { not_in: priv } })\n }\n }\n\n const where: Where =\n andClauses.length === 0\n ? {}\n : andClauses.length === 1\n ? andClauses[0]\n : { and: andClauses }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const result = await (req.payload.find as any)({\n collection: config.slugs.customers,\n limit,\n page,\n where,\n })\n\n return Response.json({\n docs: (result.docs as Record<string, unknown>[]).map((doc) => {\n const entry: Record<string, unknown> = {\n id: doc['id'],\n email: doc['email'] ?? '',\n }\n\n if (hasName) {\n entry['name'] = doc['name'] ?? ''\n }\n if (hasFirstName) {\n entry['firstName'] = doc['firstName'] ?? ''\n }\n if (hasLastName) {\n entry['lastName'] = doc['lastName'] ?? ''\n }\n if (hasPhone) {\n entry['phone'] = doc['phone'] ?? ''\n }\n\n return entry\n }),\n hasNextPage: result.hasNextPage,\n totalDocs: result.totalDocs,\n })\n },\n method: 'get',\n path: '/reservation-customer-search',\n }\n}\n"],"names":["isPrivilegedUser","privilegedRoles","getNamedFields","fields","names","Set","field","add","name","createCustomerSearchEndpoint","config","handler","req","user","Response","json","message","status","url","URL","search","searchParams","get","limit","Math","min","Number","page","max","collectionConfig","payload","collections","slugs","customers","availableFields","hasName","has","hasFirstName","hasLastName","hasPhone","andClauses","orClauses","push","contains","firstName","lastName","email","phone","or","userCollection","roleField","staffProvisioning","priv","length","not_in","where","and","result","find","collection","docs","map","doc","entry","id","hasNextPage","totalDocs","method","path"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/endpoints/customerSearch.ts"],"sourcesContent":["import type { CollectionSlug, Endpoint, Field, Where } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { isPrivilegedUser, privilegedRoles } from '../utilities/userRoles.js'\n\n/**\n * Inspect a collection's field list and return the set of top-level named\n * fields as a plain Set<string>. Unnamed fields (rows, groups without a name,\n * etc.) are skipped.\n */\nfunction getNamedFields(fields: Field[]): Set<string> {\n const names = new Set<string>()\n for (const field of fields) {\n if ('name' in field) {\n names.add(field.name)\n }\n }\n return names\n}\n\nexport function createCustomerSearchEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n if (!req.user) {\n return Response.json({ message: 'Unauthorized' }, { status: 401 })\n }\n\n // Only staff/admin may search customers. Role-aware so it works when staff\n // and customers share one auth collection (userCollection set).\n if (!isPrivilegedUser(req.user, config)) {\n return Response.json({ message: 'Forbidden' }, { status: 403 })\n }\n\n const url = new URL(req.url!)\n const search = url.searchParams.get('search') ?? ''\n const limitRaw = Number(url.searchParams.get('limit') ?? '10')\n const pageRaw = Number(url.searchParams.get('page') ?? '1')\n // Non-numeric input falls back to defaults instead of passing NaN to the DB\n const limit = Number.isFinite(limitRaw) ? Math.min(Math.max(Math.floor(limitRaw), 1), 50) : 10\n const page = Number.isFinite(pageRaw) ? Math.max(Math.floor(pageRaw), 1) : 1\n\n // Detect which fields exist on the target collection at runtime\n const collectionConfig = req.payload.collections[config.slugs.customers as unknown as CollectionSlug]?.config\n const availableFields: Set<string> = collectionConfig\n ? getNamedFields(collectionConfig.fields)\n : new Set()\n\n const hasName = availableFields.has('name')\n const hasFirstName = availableFields.has('firstName')\n const hasLastName = availableFields.has('lastName')\n const hasPhone = availableFields.has('phone')\n\n const andClauses: Where[] = []\n\n if (search) {\n const orClauses: Where[] = []\n\n if (hasName) {\n orClauses.push({ name: { contains: search } })\n }\n if (hasFirstName) {\n orClauses.push({ firstName: { contains: search } })\n }\n if (hasLastName) {\n orClauses.push({ lastName: { contains: search } })\n }\n // email is always present on auth collections\n orClauses.push({ email: { contains: search } })\n if (hasPhone) {\n orClauses.push({ phone: { contains: search } })\n }\n\n andClauses.push({ or: orClauses })\n }\n\n // Single-collection mode: staff/admin live in the same collection as\n // customers, so exclude privileged roles — the dropdown should list only\n // actual customers, not bookable-looking staff.\n if (config.userCollection) {\n const roleField = config.staffProvisioning?.roleField ?? 'role'\n const priv = privilegedRoles(config)\n if (priv.length > 0) {\n andClauses.push({ [roleField]: { not_in: priv } })\n }\n }\n\n const where: Where =\n andClauses.length === 0\n ? {}\n : andClauses.length === 1\n ? andClauses[0]\n : { and: andClauses }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const result = await (req.payload.find as any)({\n collection: config.slugs.customers,\n limit,\n page,\n where,\n })\n\n return Response.json({\n docs: (result.docs as Record<string, unknown>[]).map((doc) => {\n const entry: Record<string, unknown> = {\n id: doc['id'],\n email: doc['email'] ?? '',\n }\n\n if (hasName) {\n entry['name'] = doc['name'] ?? ''\n }\n if (hasFirstName) {\n entry['firstName'] = doc['firstName'] ?? ''\n }\n if (hasLastName) {\n entry['lastName'] = doc['lastName'] ?? ''\n }\n if (hasPhone) {\n entry['phone'] = doc['phone'] ?? ''\n }\n\n return entry\n }),\n hasNextPage: result.hasNextPage,\n totalDocs: result.totalDocs,\n })\n },\n method: 'get',\n path: '/reservation-customer-search',\n }\n}\n"],"names":["isPrivilegedUser","privilegedRoles","getNamedFields","fields","names","Set","field","add","name","createCustomerSearchEndpoint","config","handler","req","user","Response","json","message","status","url","URL","search","searchParams","get","limitRaw","Number","pageRaw","limit","isFinite","Math","min","max","floor","page","collectionConfig","payload","collections","slugs","customers","availableFields","hasName","has","hasFirstName","hasLastName","hasPhone","andClauses","orClauses","push","contains","firstName","lastName","email","phone","or","userCollection","roleField","staffProvisioning","priv","length","not_in","where","and","result","find","collection","docs","map","doc","entry","id","hasNextPage","totalDocs","method","path"],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { Endpoint } from 'payload';
2
+ import type { ResolvedReservationPluginConfig } from '../types.js';
3
+ /**
4
+ * Resolve the effective business timezone for the current request's selected
5
+ * tenant (tenant zone → global → UTC), read from the tenant cookie. The custom
6
+ * admin calendar calls this so its day-boundary rendering matches the selected
7
+ * tenant — the client can't resolve it itself because the tenant→timezone map
8
+ * lives server-side and the tenant collection slug is only known at request time.
9
+ *
10
+ * Plain (non-multiTenant) installs always get `config.timezone` here, with no
11
+ * tenant DB read.
12
+ */
13
+ export declare function createEffectiveTimezoneEndpoint(config: ResolvedReservationPluginConfig): Endpoint;
@@ -0,0 +1,41 @@
1
+ import { readCookie } from '../utilities/tenantFilter.js';
2
+ import { getEffectiveTenantTimezone } from '../utilities/tenantTimezone.js';
3
+ /**
4
+ * Resolve the effective business timezone for the current request's selected
5
+ * tenant (tenant zone → global → UTC), read from the tenant cookie. The custom
6
+ * admin calendar calls this so its day-boundary rendering matches the selected
7
+ * tenant — the client can't resolve it itself because the tenant→timezone map
8
+ * lives server-side and the tenant collection slug is only known at request time.
9
+ *
10
+ * Plain (non-multiTenant) installs always get `config.timezone` here, with no
11
+ * tenant DB read.
12
+ */ export function createEffectiveTimezoneEndpoint(config) {
13
+ return {
14
+ handler: async (req)=>{
15
+ // Staff/admin-only view data — require an authenticated user.
16
+ if (!req.user) {
17
+ return Response.json({
18
+ error: 'Unauthorized'
19
+ }, {
20
+ status: 401
21
+ });
22
+ }
23
+ const reservationsCollection = req.payload.config.collections?.find((c)=>c.slug === config.slugs.reservations);
24
+ const timeZone = await getEffectiveTenantTimezone({
25
+ globalTimezone: config.timezone,
26
+ payload: req.payload,
27
+ scopedCollection: reservationsCollection,
28
+ tenantField: config.multiTenant.tenantField,
29
+ tenantId: readCookie(req.headers?.get('cookie'), config.multiTenant.cookieName),
30
+ timezoneField: config.multiTenant.timezoneField
31
+ });
32
+ return Response.json({
33
+ timeZone
34
+ });
35
+ },
36
+ method: 'get',
37
+ path: '/reserve/effective-timezone'
38
+ };
39
+ }
40
+
41
+ //# sourceMappingURL=effectiveTimezone.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/endpoints/effectiveTimezone.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { readCookie } from '../utilities/tenantFilter.js'\nimport { getEffectiveTenantTimezone } from '../utilities/tenantTimezone.js'\n\n/**\n * Resolve the effective business timezone for the current request's selected\n * tenant (tenant zone → global → UTC), read from the tenant cookie. The custom\n * admin calendar calls this so its day-boundary rendering matches the selected\n * tenant — the client can't resolve it itself because the tenant→timezone map\n * lives server-side and the tenant collection slug is only known at request time.\n *\n * Plain (non-multiTenant) installs always get `config.timezone` here, with no\n * tenant DB read.\n */\nexport function createEffectiveTimezoneEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n // Staff/admin-only view data — require an authenticated user.\n if (!req.user) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const reservationsCollection = req.payload.config.collections?.find(\n (c) => c.slug === config.slugs.reservations,\n )\n const timeZone = await getEffectiveTenantTimezone({\n globalTimezone: config.timezone,\n payload: req.payload,\n scopedCollection: reservationsCollection as { fields?: unknown[] } | undefined,\n tenantField: config.multiTenant.tenantField,\n tenantId: readCookie(req.headers?.get('cookie'), config.multiTenant.cookieName),\n timezoneField: config.multiTenant.timezoneField,\n })\n\n return Response.json({ timeZone })\n },\n method: 'get',\n path: '/reserve/effective-timezone',\n }\n}\n"],"names":["readCookie","getEffectiveTenantTimezone","createEffectiveTimezoneEndpoint","config","handler","req","user","Response","json","error","status","reservationsCollection","payload","collections","find","c","slug","slugs","reservations","timeZone","globalTimezone","timezone","scopedCollection","tenantField","multiTenant","tenantId","headers","get","cookieName","timezoneField","method","path"],"mappings":"AAIA,SAASA,UAAU,QAAQ,+BAA8B;AACzD,SAASC,0BAA0B,QAAQ,iCAAgC;AAE3E;;;;;;;;;CASC,GACD,OAAO,SAASC,gCACdC,MAAuC;IAEvC,OAAO;QACLC,SAAS,OAAOC;YACd,8DAA8D;YAC9D,IAAI,CAACA,IAAIC,IAAI,EAAE;gBACb,OAAOC,SAASC,IAAI,CAAC;oBAAEC,OAAO;gBAAe,GAAG;oBAAEC,QAAQ;gBAAI;YAChE;YAEA,MAAMC,yBAAyBN,IAAIO,OAAO,CAACT,MAAM,CAACU,WAAW,EAAEC,KAC7D,CAACC,IAAMA,EAAEC,IAAI,KAAKb,OAAOc,KAAK,CAACC,YAAY;YAE7C,MAAMC,WAAW,MAAMlB,2BAA2B;gBAChDmB,gBAAgBjB,OAAOkB,QAAQ;gBAC/BT,SAASP,IAAIO,OAAO;gBACpBU,kBAAkBX;gBAClBY,aAAapB,OAAOqB,WAAW,CAACD,WAAW;gBAC3CE,UAAUzB,WAAWK,IAAIqB,OAAO,EAAEC,IAAI,WAAWxB,OAAOqB,WAAW,CAACI,UAAU;gBAC9EC,eAAe1B,OAAOqB,WAAW,CAACK,aAAa;YACjD;YAEA,OAAOtB,SAASC,IAAI,CAAC;gBAAEW;YAAS;QAClC;QACAW,QAAQ;QACRC,MAAM;IACR;AACF"}
@@ -1,5 +1,6 @@
1
1
  import { getAvailableSlots } from '../services/AvailabilityService.js';
2
2
  import { extractId, mergeResourceIds } from '../utilities/resolveRequiredResources.js';
3
+ import { getDayKeyInTimezone, isValidDayKey } from '../utilities/timezoneUtils.js';
3
4
  export function createGetSlotsEndpoint(config) {
4
5
  return {
5
6
  handler: async (req)=>{
@@ -14,15 +15,39 @@ export function createGetSlotsEndpoint(config) {
14
15
  status: 400
15
16
  });
16
17
  }
17
- const parsedDate = new Date(date);
18
- if (isNaN(parsedDate.getTime())) {
18
+ // YYYY-MM-DD is taken as a business-TZ calendar day verbatim; any other
19
+ // parseable date is re-keyed into the business timezone. Never
20
+ // `new Date('YYYY-MM-DD')` — that pins the day to UTC midnight.
21
+ let dayKey;
22
+ if (/^\d{4}-\d{2}-\d{2}$/.test(date)) {
23
+ if (!isValidDayKey(date)) {
24
+ return Response.json({
25
+ error: 'Invalid date format. Expected YYYY-MM-DD'
26
+ }, {
27
+ status: 400
28
+ });
29
+ }
30
+ dayKey = date;
31
+ } else {
32
+ const parsed = new Date(date);
33
+ if (isNaN(parsed.getTime())) {
34
+ return Response.json({
35
+ error: 'Invalid date format. Expected YYYY-MM-DD'
36
+ }, {
37
+ status: 400
38
+ });
39
+ }
40
+ dayKey = getDayKeyInTimezone(parsed, config.timezone);
41
+ }
42
+ const guestCountRaw = Number(url.searchParams.get('guestCount') ?? '1');
43
+ if (!Number.isFinite(guestCountRaw)) {
19
44
  return Response.json({
20
- error: 'Invalid date format. Expected YYYY-MM-DD'
45
+ error: 'Invalid guestCount'
21
46
  }, {
22
47
  status: 400
23
48
  });
24
49
  }
25
- const guestCount = Math.max(Number(url.searchParams.get('guestCount') ?? '1'), 1);
50
+ const guestCount = Math.max(Math.floor(guestCountRaw), 1);
26
51
  // Resolve required resource set: caller resource(s) ∪ service.requiredResources
27
52
  const explicit = url.searchParams.get('resources');
28
53
  const callerIds = explicit ? explicit.split(',').map((s)=>s.trim()).filter(Boolean) : [
@@ -34,12 +59,35 @@ export function createGetSlotsEndpoint(config) {
34
59
  collection: config.slugs.services,
35
60
  depth: 0,
36
61
  req
37
- });
62
+ }).catch(()=>null);
63
+ if (!svcDoc) {
64
+ return Response.json({
65
+ error: 'Service not found'
66
+ }, {
67
+ status: 404
68
+ });
69
+ }
70
+ // Validate the primary resource id up front — a malformed id in a query
71
+ // surfaces as an adapter cast error (500) instead of a clean 404.
72
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
73
+ const resourceDoc = await req.payload.findByID({
74
+ id: resource,
75
+ collection: config.slugs.resources,
76
+ depth: 0,
77
+ req
78
+ }).catch(()=>null);
79
+ if (!resourceDoc) {
80
+ return Response.json({
81
+ error: 'Resource not found'
82
+ }, {
83
+ status: 404
84
+ });
85
+ }
38
86
  const requiredIds = (svcDoc?.requiredResources ?? []).map((r)=>extractId(r)).filter((r)=>r !== undefined);
39
87
  const resourceIds = mergeResourceIds(callerIds, requiredIds);
40
88
  const slots = await getAvailableSlots({
41
89
  blockingStatuses: config.statusMachine.blockingStatuses,
42
- date: parsedDate,
90
+ date: dayKey,
43
91
  guestCount,
44
92
  payload: req.payload,
45
93
  req,
@@ -48,7 +96,8 @@ export function createGetSlotsEndpoint(config) {
48
96
  resourceSlug: config.slugs.resources,
49
97
  scheduleSlug: config.slugs.schedules,
50
98
  serviceId: service,
51
- serviceSlug: config.slugs.services
99
+ serviceSlug: config.slugs.services,
100
+ timeZone: config.timezone
52
101
  });
53
102
  return Response.json({
54
103
  date,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/endpoints/getSlots.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { getAvailableSlots } from '../services/AvailabilityService.js'\nimport { extractId, mergeResourceIds } from '../utilities/resolveRequiredResources.js'\n\nexport function createGetSlotsEndpoint(config: ResolvedReservationPluginConfig): Endpoint {\n return {\n handler: async (req) => {\n const url = new URL(req.url!)\n const date = url.searchParams.get('date')\n const resource = url.searchParams.get('resource')\n const service = url.searchParams.get('service')\n\n if (!date || !resource || !service) {\n return Response.json(\n { error: 'Missing required query params: resource, date, service' },\n { status: 400 },\n )\n }\n\n const parsedDate = new Date(date)\n if (isNaN(parsedDate.getTime())) {\n return Response.json(\n { error: 'Invalid date format. Expected YYYY-MM-DD' },\n { status: 400 },\n )\n }\n\n const guestCount = Math.max(Number(url.searchParams.get('guestCount') ?? '1'), 1)\n\n // Resolve required resource set: caller resource(s) ∪ service.requiredResources\n const explicit = url.searchParams.get('resources')\n const callerIds = explicit ? explicit.split(',').map((s) => s.trim()).filter(Boolean) : [resource]\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const svcDoc = await (req.payload.findByID as any)({\n id: service,\n collection: config.slugs.services,\n depth: 0,\n req,\n })\n const requiredIds = ((svcDoc?.requiredResources as unknown[]) ?? [])\n .map((r) => extractId(r))\n .filter((r): r is number | string => r !== undefined)\n const resourceIds = mergeResourceIds(callerIds, requiredIds)\n\n const slots = await getAvailableSlots({\n blockingStatuses: config.statusMachine.blockingStatuses,\n date: parsedDate,\n guestCount,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceIds,\n resourceSlug: config.slugs.resources,\n scheduleSlug: config.slugs.schedules,\n serviceId: service,\n serviceSlug: config.slugs.services,\n })\n\n return Response.json({\n date,\n guestCount,\n slots: slots.map((s) => ({ end: s.end.toISOString(), start: s.start.toISOString() })),\n })\n },\n method: 'get',\n path: '/reserve/slots',\n }\n}\n"],"names":["getAvailableSlots","extractId","mergeResourceIds","createGetSlotsEndpoint","config","handler","req","url","URL","date","searchParams","get","resource","service","Response","json","error","status","parsedDate","Date","isNaN","getTime","guestCount","Math","max","Number","explicit","callerIds","split","map","s","trim","filter","Boolean","svcDoc","payload","findByID","id","collection","slugs","services","depth","requiredIds","requiredResources","r","undefined","resourceIds","slots","blockingStatuses","statusMachine","reservationSlug","reservations","resourceSlug","resources","scheduleSlug","schedules","serviceId","serviceSlug","end","toISOString","start","method","path"],"mappings":"AAIA,SAASA,iBAAiB,QAAQ,qCAAoC;AACtE,SAASC,SAAS,EAAEC,gBAAgB,QAAQ,2CAA0C;AAEtF,OAAO,SAASC,uBAAuBC,MAAuC;IAC5E,OAAO;QACLC,SAAS,OAAOC;YACd,MAAMC,MAAM,IAAIC,IAAIF,IAAIC,GAAG;YAC3B,MAAME,OAAOF,IAAIG,YAAY,CAACC,GAAG,CAAC;YAClC,MAAMC,WAAWL,IAAIG,YAAY,CAACC,GAAG,CAAC;YACtC,MAAME,UAAUN,IAAIG,YAAY,CAACC,GAAG,CAAC;YAErC,IAAI,CAACF,QAAQ,CAACG,YAAY,CAACC,SAAS;gBAClC,OAAOC,SAASC,IAAI,CAClB;oBAAEC,OAAO;gBAAyD,GAClE;oBAAEC,QAAQ;gBAAI;YAElB;YAEA,MAAMC,aAAa,IAAIC,KAAKV;YAC5B,IAAIW,MAAMF,WAAWG,OAAO,KAAK;gBAC/B,OAAOP,SAASC,IAAI,CAClB;oBAAEC,OAAO;gBAA2C,GACpD;oBAAEC,QAAQ;gBAAI;YAElB;YAEA,MAAMK,aAAaC,KAAKC,GAAG,CAACC,OAAOlB,IAAIG,YAAY,CAACC,GAAG,CAAC,iBAAiB,MAAM;YAE/E,gFAAgF;YAChF,MAAMe,WAAWnB,IAAIG,YAAY,CAACC,GAAG,CAAC;YACtC,MAAMgB,YAAYD,WAAWA,SAASE,KAAK,CAAC,KAAKC,GAAG,CAAC,CAACC,IAAMA,EAAEC,IAAI,IAAIC,MAAM,CAACC,WAAW;gBAACrB;aAAS;YAElG,8DAA8D;YAC9D,MAAMsB,SAAS,MAAM,AAAC5B,IAAI6B,OAAO,CAACC,QAAQ,CAAS;gBACjDC,IAAIxB;gBACJyB,YAAYlC,OAAOmC,KAAK,CAACC,QAAQ;gBACjCC,OAAO;gBACPnC;YACF;YACA,MAAMoC,cAAc,AAAC,CAAA,AAACR,QAAQS,qBAAmC,EAAE,AAAD,EAC/Dd,GAAG,CAAC,CAACe,IAAM3C,UAAU2C,IACrBZ,MAAM,CAAC,CAACY,IAA4BA,MAAMC;YAC7C,MAAMC,cAAc5C,iBAAiByB,WAAWe;YAEhD,MAAMK,QAAQ,MAAM/C,kBAAkB;gBACpCgD,kBAAkB5C,OAAO6C,aAAa,CAACD,gBAAgB;gBACvDvC,MAAMS;gBACNI;gBACAa,SAAS7B,IAAI6B,OAAO;gBACpB7B;gBACA4C,iBAAiB9C,OAAOmC,KAAK,CAACY,YAAY;gBAC1CL;gBACAM,cAAchD,OAAOmC,KAAK,CAACc,SAAS;gBACpCC,cAAclD,OAAOmC,KAAK,CAACgB,SAAS;gBACpCC,WAAW3C;gBACX4C,aAAarD,OAAOmC,KAAK,CAACC,QAAQ;YACpC;YAEA,OAAO1B,SAASC,IAAI,CAAC;gBACnBN;gBACAa;gBACAyB,OAAOA,MAAMlB,GAAG,CAAC,CAACC,IAAO,CAAA;wBAAE4B,KAAK5B,EAAE4B,GAAG,CAACC,WAAW;wBAAIC,OAAO9B,EAAE8B,KAAK,CAACD,WAAW;oBAAG,CAAA;YACpF;QACF;QACAE,QAAQ;QACRC,MAAM;IACR;AACF"}
1
+ {"version":3,"sources":["../../src/endpoints/getSlots.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { getAvailableSlots } from '../services/AvailabilityService.js'\nimport { extractId, mergeResourceIds } from '../utilities/resolveRequiredResources.js'\nimport { getDayKeyInTimezone, isValidDayKey } from '../utilities/timezoneUtils.js'\n\nexport function createGetSlotsEndpoint(config: ResolvedReservationPluginConfig): Endpoint {\n return {\n handler: async (req) => {\n const url = new URL(req.url!)\n const date = url.searchParams.get('date')\n const resource = url.searchParams.get('resource')\n const service = url.searchParams.get('service')\n\n if (!date || !resource || !service) {\n return Response.json(\n { error: 'Missing required query params: resource, date, service' },\n { status: 400 },\n )\n }\n\n // YYYY-MM-DD is taken as a business-TZ calendar day verbatim; any other\n // parseable date is re-keyed into the business timezone. Never\n // `new Date('YYYY-MM-DD')` — that pins the day to UTC midnight.\n let dayKey: string\n if (/^\\d{4}-\\d{2}-\\d{2}$/.test(date)) {\n if (!isValidDayKey(date)) {\n return Response.json(\n { error: 'Invalid date format. Expected YYYY-MM-DD' },\n { status: 400 },\n )\n }\n dayKey = date\n } else {\n const parsed = new Date(date)\n if (isNaN(parsed.getTime())) {\n return Response.json(\n { error: 'Invalid date format. Expected YYYY-MM-DD' },\n { status: 400 },\n )\n }\n dayKey = getDayKeyInTimezone(parsed, config.timezone)\n }\n\n const guestCountRaw = Number(url.searchParams.get('guestCount') ?? '1')\n if (!Number.isFinite(guestCountRaw)) {\n return Response.json({ error: 'Invalid guestCount' }, { status: 400 })\n }\n const guestCount = Math.max(Math.floor(guestCountRaw), 1)\n\n // Resolve required resource set: caller resource(s) ∪ service.requiredResources\n const explicit = url.searchParams.get('resources')\n const callerIds = explicit ? explicit.split(',').map((s) => s.trim()).filter(Boolean) : [resource]\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const svcDoc = await (req.payload.findByID as any)({\n id: service,\n collection: config.slugs.services,\n depth: 0,\n req,\n }).catch(() => null)\n if (!svcDoc) {\n return Response.json({ error: 'Service not found' }, { status: 404 })\n }\n\n // Validate the primary resource id up front — a malformed id in a query\n // surfaces as an adapter cast error (500) instead of a clean 404.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const resourceDoc = await (req.payload.findByID as any)({\n id: resource,\n collection: config.slugs.resources,\n depth: 0,\n req,\n }).catch(() => null)\n if (!resourceDoc) {\n return Response.json({ error: 'Resource not found' }, { status: 404 })\n }\n const requiredIds = ((svcDoc?.requiredResources as unknown[]) ?? [])\n .map((r) => extractId(r))\n .filter((r): r is number | string => r !== undefined)\n const resourceIds = mergeResourceIds(callerIds, requiredIds)\n\n const slots = await getAvailableSlots({\n blockingStatuses: config.statusMachine.blockingStatuses,\n date: dayKey,\n guestCount,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceIds,\n resourceSlug: config.slugs.resources,\n scheduleSlug: config.slugs.schedules,\n serviceId: service,\n serviceSlug: config.slugs.services,\n timeZone: config.timezone,\n })\n\n return Response.json({\n date,\n guestCount,\n slots: slots.map((s) => ({ end: s.end.toISOString(), start: s.start.toISOString() })),\n })\n },\n method: 'get',\n path: '/reserve/slots',\n }\n}\n"],"names":["getAvailableSlots","extractId","mergeResourceIds","getDayKeyInTimezone","isValidDayKey","createGetSlotsEndpoint","config","handler","req","url","URL","date","searchParams","get","resource","service","Response","json","error","status","dayKey","test","parsed","Date","isNaN","getTime","timezone","guestCountRaw","Number","isFinite","guestCount","Math","max","floor","explicit","callerIds","split","map","s","trim","filter","Boolean","svcDoc","payload","findByID","id","collection","slugs","services","depth","catch","resourceDoc","resources","requiredIds","requiredResources","r","undefined","resourceIds","slots","blockingStatuses","statusMachine","reservationSlug","reservations","resourceSlug","scheduleSlug","schedules","serviceId","serviceSlug","timeZone","end","toISOString","start","method","path"],"mappings":"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"}
@@ -28,6 +28,8 @@ export type ResourceAvailability = {
28
28
  busy: Busy;
29
29
  quantity: number;
30
30
  }>;
31
+ /** IANA zone the day windows were resolved in (selected tenant's zone, else global). */
32
+ timeZone: string;
31
33
  };
32
34
  export declare function buildResourceAvailability(params: {
33
35
  blockingStatuses: string[];
@@ -38,6 +40,7 @@ export declare function buildResourceAvailability(params: {
38
40
  resourceSlug: string;
39
41
  scheduleSlug: string;
40
42
  start: Date;
41
- }): Promise<ResourceAvailability>;
43
+ timeZone: string;
44
+ }): Promise<null | ResourceAvailability>;
42
45
  export declare function createResourceAvailabilityEndpoint(config: ResolvedReservationPluginConfig): Endpoint;
43
46
  export {};
@@ -1,5 +1,9 @@
1
1
  import { resolveScheduleForDate } from '../utilities/scheduleUtils.js';
2
- import { localDayKey } from '../utilities/slotUtils.js';
2
+ import { readCookie } from '../utilities/tenantFilter.js';
3
+ import { getEffectiveTenantTimezone } from '../utilities/tenantTimezone.js';
4
+ import { addDaysToDayKey, combineDayKeyAndTime, getDayKeyInTimezone } from '../utilities/timezoneUtils.js';
5
+ import { isPrivilegedUser } from '../utilities/userRoles.js';
6
+ const MAX_RANGE_MS = 90 * 86_400_000;
3
7
  /** Busy intervals (with capacity units) for one resource over [start, end). */ async function busyFor(args) {
4
8
  const { blockingStatuses, capacityMode, end, payload, reservationSlug, resourceId, start } = args;
5
9
  const where = {
@@ -35,11 +39,13 @@ import { localDayKey } from '../utilities/slotUtils.js';
35
39
  }
36
40
  ]
37
41
  };
42
+ // limit:0 = all matching — bounded by the endpoint's 90-day range cap, so this
43
+ // can't run away, and the grid no longer silently drops busy intervals (D9).
38
44
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
45
  const { docs } = await payload.find({
40
46
  collection: reservationSlug,
41
47
  depth: 0,
42
- limit: 500,
48
+ limit: 0,
43
49
  where
44
50
  });
45
51
  return docs.filter((r)=>r.startTime && r.endTime).map((r)=>({
@@ -49,14 +55,17 @@ import { localDayKey } from '../utilities/slotUtils.js';
49
55
  }));
50
56
  }
51
57
  export async function buildResourceAvailability(params) {
52
- const { blockingStatuses, end, payload, reservationSlug, resourceId, resourceSlug, scheduleSlug, start } = params;
58
+ const { blockingStatuses, end, payload, reservationSlug, resourceId, resourceSlug, scheduleSlug, start, timeZone } = params;
53
59
  // depth 1 so `services` are populated (their `requiredResources` come back as ids)
54
60
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
55
61
  const resource = await payload.findByID({
56
62
  id: resourceId,
57
63
  collection: resourceSlug,
58
64
  depth: 1
59
- });
65
+ }).catch(()=>null);
66
+ if (!resource) {
67
+ return null;
68
+ }
60
69
  const quantity = resource?.quantity ?? 1;
61
70
  const capacityMode = resource?.capacityMode ?? 'per-reservation';
62
71
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -80,32 +89,46 @@ export async function buildResourceAvailability(params) {
80
89
  }
81
90
  });
82
91
  const days = [];
83
- for(let d = new Date(start); d < end; d = new Date(d.getTime() + 86_400_000)){
84
- const date = localDayKey(d);
92
+ const startKey = getDayKeyInTimezone(start, timeZone);
93
+ const lastKey = getDayKeyInTimezone(new Date(end.getTime() - 1), timeZone);
94
+ for(let date = startKey; date <= lastKey; date = addDaysToDayKey(date, 1)){
85
95
  const shiftWindows = [];
86
96
  const timeOff = [];
87
- const localMidnight = new Date(d.getFullYear(), d.getMonth(), d.getDate());
97
+ // A11: an exception on ANY of the resource's schedules makes the whole
98
+ // resource unavailable that day. Find the first matching exception (if any)
99
+ // — it suppresses all shift windows and marks the full day as time-off.
100
+ let dayException;
88
101
  for (const sched of schedules){
89
- // resolveScheduleForDate accepts a Schedule-shaped object; cast through unknown
90
- const ranges = resolveScheduleForDate(sched, localMidnight);
91
- for (const r of ranges){
92
- shiftWindows.push({
93
- end: r.end.toISOString(),
94
- start: r.start.toISOString()
95
- });
96
- }
97
102
  const exceptions = sched.exceptions ?? [];
98
103
  for (const exc of exceptions){
99
- const excStart = localDayKey(new Date(exc.date));
100
- const excEnd = exc.endDate ? localDayKey(new Date(exc.endDate)) : excStart;
104
+ const excStart = getDayKeyInTimezone(new Date(exc.date), timeZone);
105
+ const excEnd = exc.endDate ? getDayKeyInTimezone(new Date(exc.endDate), timeZone) : excStart;
101
106
  if (date >= excStart && date <= excEnd) {
102
- const localStart = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0);
103
- const localEnd = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59, 999);
104
- timeOff.push({
105
- type: exc.type,
106
- end: localEnd.toISOString(),
107
- reason: exc.reason,
108
- start: localStart.toISOString()
107
+ dayException = exc;
108
+ break;
109
+ }
110
+ }
111
+ if (dayException) {
112
+ break;
113
+ }
114
+ }
115
+ if (dayException) {
116
+ const dayStart = combineDayKeyAndTime(date, '00:00', timeZone);
117
+ const dayEnd = new Date(combineDayKeyAndTime(date, '23:59', timeZone).getTime() + 59_999);
118
+ timeOff.push({
119
+ type: dayException.type,
120
+ end: dayEnd.toISOString(),
121
+ reason: dayException.reason,
122
+ start: dayStart.toISOString()
123
+ });
124
+ } else {
125
+ for (const sched of schedules){
126
+ // resolveScheduleForDate accepts a Schedule-shaped object; cast through unknown
127
+ const ranges = resolveScheduleForDate(sched, date, timeZone);
128
+ for (const r of ranges){
129
+ shiftWindows.push({
130
+ end: r.end.toISOString(),
131
+ start: r.start.toISOString()
109
132
  });
110
133
  }
111
134
  }
@@ -168,12 +191,29 @@ export async function buildResourceAvailability(params) {
168
191
  capacityMode,
169
192
  days,
170
193
  quantity,
171
- requiredPools
194
+ requiredPools,
195
+ timeZone
172
196
  };
173
197
  }
174
198
  export function createResourceAvailabilityEndpoint(config) {
175
199
  return {
176
200
  handler: async (req)=>{
201
+ // The grid data (every reservation's busy window) is staff/admin-only —
202
+ // same gate as customer search.
203
+ if (!req.user) {
204
+ return Response.json({
205
+ error: 'Unauthorized'
206
+ }, {
207
+ status: 401
208
+ });
209
+ }
210
+ if (!isPrivilegedUser(req.user, config)) {
211
+ return Response.json({
212
+ error: 'Forbidden'
213
+ }, {
214
+ status: 403
215
+ });
216
+ }
177
217
  const url = new URL(req.url);
178
218
  const resource = url.searchParams.get('resource');
179
219
  const start = url.searchParams.get('start');
@@ -194,6 +234,34 @@ export function createResourceAvailabilityEndpoint(config) {
194
234
  status: 400
195
235
  });
196
236
  }
237
+ if (endDate <= startDate) {
238
+ return Response.json({
239
+ error: 'end must be after start'
240
+ }, {
241
+ status: 400
242
+ });
243
+ }
244
+ // Unbounded ranges turn the per-day resolution loop into a CPU sink
245
+ if (endDate.getTime() - startDate.getTime() > MAX_RANGE_MS) {
246
+ return Response.json({
247
+ error: 'Date range too large (max 90 days)'
248
+ }, {
249
+ status: 400
250
+ });
251
+ }
252
+ // In multiTenant mode, resolve day-boundaries in the SELECTED tenant's zone
253
+ // (tenant timezone → global → UTC). Degrades to the global zone for plain
254
+ // installs: no tenant relationship on reservations / no tenant cookie ⇒ no
255
+ // DB read, same output as before.
256
+ const reservationsCollection = req.payload.config.collections?.find((c)=>c.slug === config.slugs.reservations);
257
+ const timeZone = await getEffectiveTenantTimezone({
258
+ globalTimezone: config.timezone,
259
+ payload: req.payload,
260
+ scopedCollection: reservationsCollection,
261
+ tenantField: config.multiTenant.tenantField,
262
+ tenantId: readCookie(req.headers?.get('cookie'), config.multiTenant.cookieName),
263
+ timezoneField: config.multiTenant.timezoneField
264
+ });
197
265
  const result = await buildResourceAvailability({
198
266
  blockingStatuses: config.statusMachine.blockingStatuses,
199
267
  end: endDate,
@@ -202,8 +270,16 @@ export function createResourceAvailabilityEndpoint(config) {
202
270
  resourceId: resource,
203
271
  resourceSlug: config.slugs.resources,
204
272
  scheduleSlug: config.slugs.schedules,
205
- start: startDate
273
+ start: startDate,
274
+ timeZone
206
275
  });
276
+ if (!result) {
277
+ return Response.json({
278
+ error: 'Resource not found'
279
+ }, {
280
+ status: 404
281
+ });
282
+ }
207
283
  return Response.json(result);
208
284
  },
209
285
  method: 'get',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/endpoints/resourceAvailability.ts"],"sourcesContent":["import type { Endpoint, Payload, Where } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { resolveScheduleForDate } from '../utilities/scheduleUtils.js'\nimport { localDayKey } from '../utilities/slotUtils.js'\n\ntype DayAvailability = {\n date: string\n shiftWindows: Array<{ end: string; start: string }>\n timeOff: Array<{ end: string; reason?: string; start: string; type?: string }>\n}\n\ntype Busy = Array<{ end: string; start: string; units: number }>\n\nexport type ResourceAvailability = {\n busy: Busy\n capacityMode: 'per-guest' | 'per-reservation'\n days: DayAvailability[]\n quantity: number\n /** Capacity of resources this resource's services also require (e.g. a chair pool). */\n requiredPools: Array<{ busy: Busy; quantity: number }>\n}\n\n/** Busy intervals (with capacity units) for one resource over [start, end). */\nasync function busyFor(args: {\n blockingStatuses: string[]\n capacityMode: 'per-guest' | 'per-reservation'\n end: Date\n payload: Payload\n reservationSlug: string\n resourceId: number | string\n start: Date\n}): Promise<Busy> {\n const { blockingStatuses, capacityMode, end, payload, reservationSlug, resourceId, start } = args\n const where: Where = {\n and: [\n { status: { in: blockingStatuses } },\n { startTime: { less_than: end.toISOString() } },\n { endTime: { greater_than: start.toISOString() } },\n { or: [{ resource: { equals: resourceId } }, { 'items.resource': { equals: resourceId } }] },\n ],\n }\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs } = await (payload.find as any)({ collection: reservationSlug, depth: 0, limit: 500, where })\n return (docs as Array<Record<string, unknown>>)\n .filter((r) => r.startTime && r.endTime)\n .map((r) => ({\n end: new Date(r.endTime as string).toISOString(),\n start: new Date(r.startTime as string).toISOString(),\n units: capacityMode === 'per-guest' ? ((r.guestCount as number) ?? 1) : 1,\n }))\n}\n\nexport async function buildResourceAvailability(params: {\n blockingStatuses: string[]\n end: Date\n payload: Payload\n reservationSlug: string\n resourceId: number | string\n resourceSlug: string\n scheduleSlug: string\n start: Date\n}): Promise<ResourceAvailability> {\n const {\n blockingStatuses,\n end,\n payload,\n reservationSlug,\n resourceId,\n resourceSlug,\n scheduleSlug,\n start,\n } = params\n\n // depth 1 so `services` are populated (their `requiredResources` come back as ids)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const resource = await (payload.findByID as any)({\n id: resourceId,\n collection: resourceSlug,\n depth: 1,\n })\n const quantity = (resource?.quantity as number) ?? 1\n const capacityMode = (resource?.capacityMode as 'per-guest' | 'per-reservation') ?? 'per-reservation'\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs: schedules } = await (payload.find as any)({\n collection: scheduleSlug,\n depth: 0,\n limit: 100,\n where: { and: [{ active: { equals: true } }, { resource: { equals: resourceId } }] },\n })\n\n type RawException = {\n date: string\n endDate?: string\n reason?: string\n type?: string\n }\n\n const days: DayAvailability[] = []\n for (let d = new Date(start); d < end; d = new Date(d.getTime() + 86_400_000)) {\n const date = localDayKey(d)\n const shiftWindows: DayAvailability['shiftWindows'] = []\n const timeOff: DayAvailability['timeOff'] = []\n const localMidnight = new Date(d.getFullYear(), d.getMonth(), d.getDate())\n\n for (const sched of schedules as Array<Record<string, unknown>>) {\n // resolveScheduleForDate accepts a Schedule-shaped object; cast through unknown\n const ranges = resolveScheduleForDate(\n sched as unknown as Parameters<typeof resolveScheduleForDate>[0],\n localMidnight,\n )\n for (const r of ranges) {\n shiftWindows.push({ end: r.end.toISOString(), start: r.start.toISOString() })\n }\n\n const exceptions = (sched.exceptions as RawException[] | undefined) ?? []\n for (const exc of exceptions) {\n const excStart = localDayKey(new Date(exc.date))\n const excEnd = exc.endDate ? localDayKey(new Date(exc.endDate)) : excStart\n if (date >= excStart && date <= excEnd) {\n const localStart = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0)\n const localEnd = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59, 999)\n timeOff.push({\n type: exc.type,\n end: localEnd.toISOString(),\n reason: exc.reason,\n start: localStart.toISOString(),\n })\n }\n }\n }\n\n days.push({ date, shiftWindows, timeOff })\n }\n\n const busy = await busyFor({\n blockingStatuses,\n capacityMode,\n end,\n payload,\n reservationSlug,\n resourceId,\n start,\n })\n\n // Resources this resource's services ALSO require (e.g. a shared chair pool).\n // A slot isn't truly bookable if any of these is at capacity, even when the\n // resource itself is free — so the calendar reflects real availability.\n const poolIds = new Set<string>()\n for (const svc of (resource?.services as Array<Record<string, unknown>>) ?? []) {\n const reqs = (typeof svc === 'object' ? (svc.requiredResources as unknown[]) : []) ?? []\n for (const rr of reqs) {\n const id: number | string | undefined =\n typeof rr === 'object' && rr !== null\n ? (rr as { id?: number | string }).id\n : (rr as number | string)\n if (id != null && String(id) !== String(resourceId)) {\n poolIds.add(String(id))\n }\n }\n }\n\n const requiredPools: ResourceAvailability['requiredPools'] = []\n for (const poolId of poolIds) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const pool = await (payload.findByID as any)({ id: poolId, collection: resourceSlug, depth: 0 }).catch(\n () => null,\n )\n if (!pool) {\n continue\n }\n const poolCapacityMode =\n (pool.capacityMode as 'per-guest' | 'per-reservation') ?? 'per-reservation'\n requiredPools.push({\n busy: await busyFor({\n blockingStatuses,\n capacityMode: poolCapacityMode,\n end,\n payload,\n reservationSlug,\n resourceId: poolId,\n start,\n }),\n quantity: (pool.quantity as number) ?? 1,\n })\n }\n\n return { busy, capacityMode, days, quantity, requiredPools }\n}\n\nexport function createResourceAvailabilityEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n const url = new URL(req.url!)\n const resource = url.searchParams.get('resource')\n const start = url.searchParams.get('start')\n const end = url.searchParams.get('end')\n\n if (!resource || !start || !end) {\n return Response.json(\n { error: 'Missing required query params: resource, start, end' },\n { status: 400 },\n )\n }\n\n const startDate = new Date(start)\n const endDate = new Date(end)\n if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {\n return Response.json({ error: 'Invalid start/end date' }, { status: 400 })\n }\n\n const result = await buildResourceAvailability({\n blockingStatuses: config.statusMachine.blockingStatuses,\n end: endDate,\n payload: req.payload,\n reservationSlug: config.slugs.reservations,\n resourceId: resource,\n resourceSlug: config.slugs.resources,\n scheduleSlug: config.slugs.schedules,\n start: startDate,\n })\n\n return Response.json(result)\n },\n method: 'get',\n path: '/reserve/resource-availability',\n }\n}\n"],"names":["resolveScheduleForDate","localDayKey","busyFor","args","blockingStatuses","capacityMode","end","payload","reservationSlug","resourceId","start","where","and","status","in","startTime","less_than","toISOString","endTime","greater_than","or","resource","equals","docs","find","collection","depth","limit","filter","r","map","Date","units","guestCount","buildResourceAvailability","params","resourceSlug","scheduleSlug","findByID","id","quantity","schedules","active","days","d","getTime","date","shiftWindows","timeOff","localMidnight","getFullYear","getMonth","getDate","sched","ranges","push","exceptions","exc","excStart","excEnd","endDate","localStart","localEnd","type","reason","busy","poolIds","Set","svc","services","reqs","requiredResources","rr","String","add","requiredPools","poolId","pool","catch","poolCapacityMode","createResourceAvailabilityEndpoint","config","handler","req","url","URL","searchParams","get","Response","json","error","startDate","isNaN","result","statusMachine","slugs","reservations","resources","method","path"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/endpoints/resourceAvailability.ts"],"sourcesContent":["import type { Endpoint, Payload, Where } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { resolveScheduleForDate } from '../utilities/scheduleUtils.js'\nimport { readCookie } from '../utilities/tenantFilter.js'\nimport { getEffectiveTenantTimezone } from '../utilities/tenantTimezone.js'\nimport {\n addDaysToDayKey,\n combineDayKeyAndTime,\n getDayKeyInTimezone,\n} from '../utilities/timezoneUtils.js'\nimport { isPrivilegedUser } from '../utilities/userRoles.js'\n\nconst MAX_RANGE_MS = 90 * 86_400_000\n\ntype DayAvailability = {\n date: string\n shiftWindows: Array<{ end: string; start: string }>\n timeOff: Array<{ end: string; reason?: string; start: string; type?: string }>\n}\n\ntype Busy = Array<{ end: string; start: string; units: number }>\n\nexport type ResourceAvailability = {\n busy: Busy\n capacityMode: 'per-guest' | 'per-reservation'\n days: DayAvailability[]\n quantity: number\n /** Capacity of resources this resource's services also require (e.g. a chair pool). */\n requiredPools: Array<{ busy: Busy; quantity: number }>\n /** IANA zone the day windows were resolved in (selected tenant's zone, else global). */\n timeZone: string\n}\n\n/** Busy intervals (with capacity units) for one resource over [start, end). */\nasync function busyFor(args: {\n blockingStatuses: string[]\n capacityMode: 'per-guest' | 'per-reservation'\n end: Date\n payload: Payload\n reservationSlug: string\n resourceId: number | string\n start: Date\n}): Promise<Busy> {\n const { blockingStatuses, capacityMode, end, payload, reservationSlug, resourceId, start } = args\n const where: Where = {\n and: [\n { status: { in: blockingStatuses } },\n { startTime: { less_than: end.toISOString() } },\n { endTime: { greater_than: start.toISOString() } },\n { or: [{ resource: { equals: resourceId } }, { 'items.resource': { equals: resourceId } }] },\n ],\n }\n // limit:0 = all matching — bounded by the endpoint's 90-day range cap, so this\n // can't run away, and the grid no longer silently drops busy intervals (D9).\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs } = await (payload.find as any)({ collection: reservationSlug, depth: 0, limit: 0, where })\n return (docs as Array<Record<string, unknown>>)\n .filter((r) => r.startTime && r.endTime)\n .map((r) => ({\n end: new Date(r.endTime as string).toISOString(),\n start: new Date(r.startTime as string).toISOString(),\n units: capacityMode === 'per-guest' ? ((r.guestCount as number) ?? 1) : 1,\n }))\n}\n\nexport async function buildResourceAvailability(params: {\n blockingStatuses: string[]\n end: Date\n payload: Payload\n reservationSlug: string\n resourceId: number | string\n resourceSlug: string\n scheduleSlug: string\n start: Date\n timeZone: string\n}): Promise<null | ResourceAvailability> {\n const {\n blockingStatuses,\n end,\n payload,\n reservationSlug,\n resourceId,\n resourceSlug,\n scheduleSlug,\n start,\n timeZone,\n } = params\n\n // depth 1 so `services` are populated (their `requiredResources` come back as ids)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const resource = await (payload.findByID as any)({\n id: resourceId,\n collection: resourceSlug,\n depth: 1,\n }).catch(() => null)\n if (!resource) {\n return null\n }\n const quantity = (resource?.quantity as number) ?? 1\n const capacityMode = (resource?.capacityMode as 'per-guest' | 'per-reservation') ?? 'per-reservation'\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs: schedules } = await (payload.find as any)({\n collection: scheduleSlug,\n depth: 0,\n limit: 100,\n where: { and: [{ active: { equals: true } }, { resource: { equals: resourceId } }] },\n })\n\n type RawException = {\n date: string\n endDate?: string\n reason?: string\n type?: string\n }\n\n const days: DayAvailability[] = []\n const startKey = getDayKeyInTimezone(start, timeZone)\n const lastKey = getDayKeyInTimezone(new Date(end.getTime() - 1), timeZone)\n for (let date = startKey; date <= lastKey; date = addDaysToDayKey(date, 1)) {\n const shiftWindows: DayAvailability['shiftWindows'] = []\n const timeOff: DayAvailability['timeOff'] = []\n\n // A11: an exception on ANY of the resource's schedules makes the whole\n // resource unavailable that day. Find the first matching exception (if any)\n // — it suppresses all shift windows and marks the full day as time-off.\n let dayException: RawException | undefined\n for (const sched of schedules as Array<Record<string, unknown>>) {\n const exceptions = (sched.exceptions as RawException[] | undefined) ?? []\n for (const exc of exceptions) {\n const excStart = getDayKeyInTimezone(new Date(exc.date), timeZone)\n const excEnd = exc.endDate ? getDayKeyInTimezone(new Date(exc.endDate), timeZone) : excStart\n if (date >= excStart && date <= excEnd) {\n dayException = exc\n break\n }\n }\n if (dayException) {\n break\n }\n }\n\n if (dayException) {\n const dayStart = combineDayKeyAndTime(date, '00:00', timeZone)\n const dayEnd = new Date(combineDayKeyAndTime(date, '23:59', timeZone).getTime() + 59_999)\n timeOff.push({\n type: dayException.type,\n end: dayEnd.toISOString(),\n reason: dayException.reason,\n start: dayStart.toISOString(),\n })\n } else {\n for (const sched of schedules as Array<Record<string, unknown>>) {\n // resolveScheduleForDate accepts a Schedule-shaped object; cast through unknown\n const ranges = resolveScheduleForDate(\n sched as unknown as Parameters<typeof resolveScheduleForDate>[0],\n date,\n timeZone,\n )\n for (const r of ranges) {\n shiftWindows.push({ end: r.end.toISOString(), start: r.start.toISOString() })\n }\n }\n }\n\n days.push({ date, shiftWindows, timeOff })\n }\n\n const busy = await busyFor({\n blockingStatuses,\n capacityMode,\n end,\n payload,\n reservationSlug,\n resourceId,\n start,\n })\n\n // Resources this resource's services ALSO require (e.g. a shared chair pool).\n // A slot isn't truly bookable if any of these is at capacity, even when the\n // resource itself is free — so the calendar reflects real availability.\n const poolIds = new Set<string>()\n for (const svc of (resource?.services as Array<Record<string, unknown>>) ?? []) {\n const reqs = (typeof svc === 'object' ? (svc.requiredResources as unknown[]) : []) ?? []\n for (const rr of reqs) {\n const id: number | string | undefined =\n typeof rr === 'object' && rr !== null\n ? (rr as { id?: number | string }).id\n : (rr as number | string)\n if (id != null && String(id) !== String(resourceId)) {\n poolIds.add(String(id))\n }\n }\n }\n\n const requiredPools: ResourceAvailability['requiredPools'] = []\n for (const poolId of poolIds) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const pool = await (payload.findByID as any)({ id: poolId, collection: resourceSlug, depth: 0 }).catch(\n () => null,\n )\n if (!pool) {\n continue\n }\n const poolCapacityMode =\n (pool.capacityMode as 'per-guest' | 'per-reservation') ?? 'per-reservation'\n requiredPools.push({\n busy: await busyFor({\n blockingStatuses,\n capacityMode: poolCapacityMode,\n end,\n payload,\n reservationSlug,\n resourceId: poolId,\n start,\n }),\n quantity: (pool.quantity as number) ?? 1,\n })\n }\n\n return { busy, capacityMode, days, quantity, requiredPools, timeZone }\n}\n\nexport function createResourceAvailabilityEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n // The grid data (every reservation's busy window) is staff/admin-only —\n // same gate as customer search.\n if (!req.user) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n if (!isPrivilegedUser(req.user, config)) {\n return Response.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const url = new URL(req.url!)\n const resource = url.searchParams.get('resource')\n const start = url.searchParams.get('start')\n const end = url.searchParams.get('end')\n\n if (!resource || !start || !end) {\n return Response.json(\n { error: 'Missing required query params: resource, start, end' },\n { status: 400 },\n )\n }\n\n const startDate = new Date(start)\n const endDate = new Date(end)\n if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {\n return Response.json({ error: 'Invalid start/end date' }, { status: 400 })\n }\n if (endDate <= startDate) {\n return Response.json({ error: 'end must be after start' }, { status: 400 })\n }\n // Unbounded ranges turn the per-day resolution loop into a CPU sink\n if (endDate.getTime() - startDate.getTime() > MAX_RANGE_MS) {\n return Response.json({ error: 'Date range too large (max 90 days)' }, { status: 400 })\n }\n\n // In multiTenant mode, resolve day-boundaries in the SELECTED tenant's zone\n // (tenant timezone → global → UTC). Degrades to the global zone for plain\n // installs: no tenant relationship on reservations / no tenant cookie ⇒ no\n // DB read, same output as before.\n const reservationsCollection = req.payload.config.collections?.find(\n (c) => c.slug === config.slugs.reservations,\n )\n const timeZone = await getEffectiveTenantTimezone({\n globalTimezone: config.timezone,\n payload: req.payload,\n scopedCollection: reservationsCollection as { fields?: unknown[] } | undefined,\n tenantField: config.multiTenant.tenantField,\n tenantId: readCookie(req.headers?.get('cookie'), config.multiTenant.cookieName),\n timezoneField: config.multiTenant.timezoneField,\n })\n\n const result = await buildResourceAvailability({\n blockingStatuses: config.statusMachine.blockingStatuses,\n end: endDate,\n payload: req.payload,\n reservationSlug: config.slugs.reservations,\n resourceId: resource,\n resourceSlug: config.slugs.resources,\n scheduleSlug: config.slugs.schedules,\n start: startDate,\n timeZone,\n })\n\n if (!result) {\n return Response.json({ error: 'Resource not found' }, { status: 404 })\n }\n\n return Response.json(result)\n },\n method: 'get',\n path: '/reserve/resource-availability',\n }\n}\n"],"names":["resolveScheduleForDate","readCookie","getEffectiveTenantTimezone","addDaysToDayKey","combineDayKeyAndTime","getDayKeyInTimezone","isPrivilegedUser","MAX_RANGE_MS","busyFor","args","blockingStatuses","capacityMode","end","payload","reservationSlug","resourceId","start","where","and","status","in","startTime","less_than","toISOString","endTime","greater_than","or","resource","equals","docs","find","collection","depth","limit","filter","r","map","Date","units","guestCount","buildResourceAvailability","params","resourceSlug","scheduleSlug","timeZone","findByID","id","catch","quantity","schedules","active","days","startKey","lastKey","getTime","date","shiftWindows","timeOff","dayException","sched","exceptions","exc","excStart","excEnd","endDate","dayStart","dayEnd","push","type","reason","ranges","busy","poolIds","Set","svc","services","reqs","requiredResources","rr","String","add","requiredPools","poolId","pool","poolCapacityMode","createResourceAvailabilityEndpoint","config","handler","req","user","Response","json","error","url","URL","searchParams","get","startDate","isNaN","reservationsCollection","collections","c","slug","slugs","reservations","globalTimezone","timezone","scopedCollection","tenantField","multiTenant","tenantId","headers","cookieName","timezoneField","result","statusMachine","resources","method","path"],"mappings":"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"}