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
@@ -1,5 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { collectionHasTenantField, readCookie, tenantWhereClause } from '../../utilities/tenantFilter.js';
3
+ import { getEffectiveTenantTimezone } from '../../utilities/tenantTimezone.js';
4
+ import { addDaysToDayKey, combineDayKeyAndTime, getDayKeyInTimezone } from '../../utilities/timezoneUtils.js';
3
5
  import styles from './DashboardWidget.module.css';
4
6
  export const DashboardWidgetServer = async (props)=>{
5
7
  const { req } = props;
@@ -12,11 +14,13 @@ export const DashboardWidgetServer = async (props)=>{
12
14
  const tenantConfig = payload.config.admin?.custom?.reservationTenant ?? {};
13
15
  const cookieName = tenantConfig.cookieName ?? 'payload-tenant';
14
16
  const tenantField = tenantConfig.tenantField ?? 'tenant';
17
+ const timezoneField = tenantConfig.timezoneField ?? 'timezone';
15
18
  const reservationsCollection = payload.config.collections?.find((c)=>c.slug === slugs.reservations);
19
+ const tenantId = readCookie(req.headers.get('cookie'), cookieName);
16
20
  const tenantWhere = tenantWhereClause({
17
21
  hasField: collectionHasTenantField(reservationsCollection, tenantField),
18
22
  tenantField,
19
- tenantId: readCookie(req.headers.get('cookie'), cookieName)
23
+ tenantId
20
24
  });
21
25
  // Read status machine from config — never hardcode status values
22
26
  const statusMachine = payload.config.admin?.custom?.reservationStatusMachine;
@@ -24,9 +28,20 @@ export const DashboardWidgetServer = async (props)=>{
24
28
  const terminalStatuses = statusMachine?.terminalStatuses ?? [];
25
29
  const blockingSet = new Set(blockingStatuses);
26
30
  const terminalSet = new Set(terminalStatuses);
31
+ // "Today" is the business timezone's calendar day, not the server's — and in
32
+ // multiTenant mode that's the SELECTED tenant's zone (tenant → global → UTC).
33
+ const reservationTimezone = await getEffectiveTenantTimezone({
34
+ globalTimezone: payload.config.admin?.custom?.reservationTimezone ?? 'UTC',
35
+ payload,
36
+ scopedCollection: reservationsCollection,
37
+ tenantField,
38
+ tenantId,
39
+ timezoneField
40
+ });
27
41
  const now = new Date();
28
- const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate());
29
- const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
42
+ const todayKey = getDayKeyInTimezone(now, reservationTimezone);
43
+ const startOfDay = combineDayKeyAndTime(todayKey, '00:00', reservationTimezone);
44
+ const endOfDay = combineDayKeyAndTime(addDaysToDayKey(todayKey, 1), '00:00', reservationTimezone);
30
45
  const where = {
31
46
  startTime: {
32
47
  greater_than_equal: startOfDay.toISOString(),
@@ -36,21 +51,78 @@ export const DashboardWidgetServer = async (props)=>{
36
51
  if (tenantWhere) {
37
52
  Object.assign(where, tenantWhere);
38
53
  }
39
- const { docs: todayReservations } = await payload.find({
40
- collection: slugs.reservations,
41
- limit: 100,
42
- sort: 'startTime',
43
- where
44
- });
45
- const total = todayReservations.length;
46
- // Active = reservations in blockingStatuses (they hold a slot, past or future)
47
- const active = todayReservations.filter((r)=>blockingSet.has(r.status)).length;
48
- // Upcoming = active (blocking) reservations that haven't started yet
49
- const upcoming = todayReservations.filter((r)=>blockingSet.has(r.status) && new Date(r.startTime) > now).length;
50
- // Terminal = reservations in terminalStatuses (completed, cancelled, no-show, etc.)
51
- const terminal = todayReservations.filter((r)=>terminalSet.has(r.status)).length;
54
+ // Stats are computed with count queries rather than a capped fetch+filter,
55
+ // so they stay accurate past 100 reservations/day (review D7).
56
+ const blocking = Array.from(blockingSet);
57
+ const terminalArr = Array.from(terminalSet);
58
+ const countWhere = (extra)=>extra ? {
59
+ and: [
60
+ where,
61
+ extra
62
+ ]
63
+ } : where;
64
+ const [total, active, terminal, upcoming, nextResult] = await Promise.all([
65
+ payload.count({
66
+ collection: slugs.reservations,
67
+ where
68
+ }).then((r)=>r.totalDocs),
69
+ blocking.length ? payload.count({
70
+ collection: slugs.reservations,
71
+ where: countWhere({
72
+ status: {
73
+ in: blocking
74
+ }
75
+ })
76
+ }).then((r)=>r.totalDocs) : Promise.resolve(0),
77
+ terminalArr.length ? payload.count({
78
+ collection: slugs.reservations,
79
+ where: countWhere({
80
+ status: {
81
+ in: terminalArr
82
+ }
83
+ })
84
+ }).then((r)=>r.totalDocs) : Promise.resolve(0),
85
+ blocking.length ? payload.count({
86
+ collection: slugs.reservations,
87
+ where: countWhere({
88
+ and: [
89
+ {
90
+ status: {
91
+ in: blocking
92
+ }
93
+ },
94
+ {
95
+ startTime: {
96
+ greater_than: now.toISOString()
97
+ }
98
+ }
99
+ ]
100
+ })
101
+ }).then((r)=>r.totalDocs) : Promise.resolve(0),
102
+ blocking.length ? payload.find({
103
+ collection: slugs.reservations,
104
+ limit: 1,
105
+ sort: 'startTime',
106
+ where: countWhere({
107
+ and: [
108
+ {
109
+ status: {
110
+ in: blocking
111
+ }
112
+ },
113
+ {
114
+ startTime: {
115
+ greater_than: now.toISOString()
116
+ }
117
+ }
118
+ ]
119
+ })
120
+ }) : Promise.resolve({
121
+ docs: []
122
+ })
123
+ ]);
52
124
  // Next appointment = the earliest upcoming blocking reservation
53
- const nextAppointment = todayReservations.find((r)=>blockingSet.has(r.status) && new Date(r.startTime) > now);
125
+ const nextAppointment = nextResult.docs[0];
54
126
  return /*#__PURE__*/ _jsxs("div", {
55
127
  className: styles.wrapper,
56
128
  children: [
@@ -127,7 +199,8 @@ export const DashboardWidgetServer = async (props)=>{
127
199
  ' ',
128
200
  new Date(nextAppointment.startTime).toLocaleTimeString([], {
129
201
  hour: '2-digit',
130
- minute: '2-digit'
202
+ minute: '2-digit',
203
+ timeZone: reservationTimezone
131
204
  })
132
205
  ]
133
206
  }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/DashboardWidget/DashboardWidgetServer.tsx"],"sourcesContent":["import type { WidgetServerProps } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { StatusMachineConfig } from '../../types.js'\n\nimport { collectionHasTenantField, readCookie, tenantWhereClause } from '../../utilities/tenantFilter.js'\nimport styles from './DashboardWidget.module.css'\n\nexport const DashboardWidgetServer = async (props: WidgetServerProps) => {\n const { req } = props\n const { i18n, payload } = req\n const t = i18n.t as PluginT\n\n const slugs = payload.config.admin?.custom?.reservationSlugs\n if (!slugs) {\n return null\n }\n\n const tenantConfig =\n (payload.config.admin?.custom?.reservationTenant as\n | { cookieName?: string; tenantField?: string }\n | undefined) ?? {}\n const cookieName = tenantConfig.cookieName ?? 'payload-tenant'\n const tenantField = tenantConfig.tenantField ?? 'tenant'\n const reservationsCollection = payload.config.collections?.find((c) => c.slug === slugs.reservations)\n const tenantWhere = tenantWhereClause({\n hasField: collectionHasTenantField(reservationsCollection as { fields?: unknown[] } | undefined, tenantField),\n tenantField,\n tenantId: readCookie(req.headers.get('cookie'), cookieName),\n })\n\n // Read status machine from config — never hardcode status values\n const statusMachine: StatusMachineConfig | undefined =\n payload.config.admin?.custom?.reservationStatusMachine\n const blockingStatuses: string[] = statusMachine?.blockingStatuses ?? []\n const terminalStatuses: string[] = statusMachine?.terminalStatuses ?? []\n const blockingSet = new Set(blockingStatuses)\n const terminalSet = new Set(terminalStatuses)\n\n const now = new Date()\n const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate())\n const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1)\n\n const where: Parameters<typeof payload.find>[0]['where'] = {\n startTime: {\n greater_than_equal: startOfDay.toISOString(),\n less_than: endOfDay.toISOString(),\n },\n }\n if (tenantWhere) {\n Object.assign(where, tenantWhere)\n }\n\n const { docs: todayReservations } = await payload.find({\n collection: slugs.reservations,\n limit: 100,\n sort: 'startTime',\n where,\n })\n\n const total = todayReservations.length\n\n // Active = reservations in blockingStatuses (they hold a slot, past or future)\n const active = todayReservations.filter((r: Record<string, unknown>) =>\n blockingSet.has(r.status as string),\n ).length\n\n // Upcoming = active (blocking) reservations that haven't started yet\n const upcoming = todayReservations.filter(\n (r: Record<string, unknown>) =>\n blockingSet.has(r.status as string) && new Date(r.startTime as string) > now,\n ).length\n\n // Terminal = reservations in terminalStatuses (completed, cancelled, no-show, etc.)\n const terminal = todayReservations.filter((r: Record<string, unknown>) =>\n terminalSet.has(r.status as string),\n ).length\n\n // Next appointment = the earliest upcoming blocking reservation\n const nextAppointment = todayReservations.find(\n (r: Record<string, unknown>) =>\n blockingSet.has(r.status as string) && new Date(r.startTime as string) > now,\n )\n\n return (\n <div className={styles.wrapper}>\n <h3 className={styles.title}>{t('reservation:dashboardTitle')}</h3>\n <div className={styles.statsGrid}>\n <div className={styles.statCard}>\n <span className={styles.statValue}>{total}</span>\n <span className={styles.statLabel}>{t('reservation:dashboardTotal')}</span>\n </div>\n <div className={styles.statCard}>\n <span className={styles.statValue}>{active}</span>\n <span className={styles.statLabel}>{t('reservation:dashboardActive')}</span>\n </div>\n <div className={styles.statCard}>\n <span className={styles.statValue}>{upcoming}</span>\n <span className={styles.statLabel}>{t('reservation:dashboardUpcoming')}</span>\n </div>\n <div className={styles.statCard}>\n <span className={styles.statValue}>{terminal}</span>\n <span className={styles.statLabel}>{t('reservation:dashboardTerminal')}</span>\n </div>\n </div>\n {nextAppointment ? (\n <div className={styles.nextAppointment}>\n <strong>{t('reservation:dashboardNextAppointment')}</strong>\n <p>\n {t('reservation:dashboardTime')}{' '}\n {new Date(nextAppointment.startTime as string).toLocaleTimeString([], {\n hour: '2-digit',\n minute: '2-digit',\n })}\n </p>\n <p>\n {t('reservation:dashboardStatus')} {nextAppointment.status as string}\n </p>\n </div>\n ) : (\n <p className={styles.noData}>{t('reservation:dashboardNoUpcoming')}</p>\n )}\n </div>\n )\n}\n"],"names":["collectionHasTenantField","readCookie","tenantWhereClause","styles","DashboardWidgetServer","props","req","i18n","payload","t","slugs","config","admin","custom","reservationSlugs","tenantConfig","reservationTenant","cookieName","tenantField","reservationsCollection","collections","find","c","slug","reservations","tenantWhere","hasField","tenantId","headers","get","statusMachine","reservationStatusMachine","blockingStatuses","terminalStatuses","blockingSet","Set","terminalSet","now","Date","startOfDay","getFullYear","getMonth","getDate","endOfDay","where","startTime","greater_than_equal","toISOString","less_than","Object","assign","docs","todayReservations","collection","limit","sort","total","length","active","filter","r","has","status","upcoming","terminal","nextAppointment","div","className","wrapper","h3","title","statsGrid","statCard","span","statValue","statLabel","strong","p","toLocaleTimeString","hour","minute","noData"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/components/DashboardWidget/DashboardWidgetServer.tsx"],"sourcesContent":["import type { Where, WidgetServerProps } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { StatusMachineConfig } from '../../types.js'\n\nimport { collectionHasTenantField, readCookie, tenantWhereClause } from '../../utilities/tenantFilter.js'\nimport { getEffectiveTenantTimezone } from '../../utilities/tenantTimezone.js'\nimport {\n addDaysToDayKey,\n combineDayKeyAndTime,\n getDayKeyInTimezone,\n} from '../../utilities/timezoneUtils.js'\nimport styles from './DashboardWidget.module.css'\n\nexport const DashboardWidgetServer = async (props: WidgetServerProps) => {\n const { req } = props\n const { i18n, payload } = req\n const t = i18n.t as PluginT\n\n const slugs = payload.config.admin?.custom?.reservationSlugs\n if (!slugs) {\n return null\n }\n\n const tenantConfig =\n (payload.config.admin?.custom?.reservationTenant as\n | { cookieName?: string; tenantField?: string; timezoneField?: string }\n | undefined) ?? {}\n const cookieName = tenantConfig.cookieName ?? 'payload-tenant'\n const tenantField = tenantConfig.tenantField ?? 'tenant'\n const timezoneField = tenantConfig.timezoneField ?? 'timezone'\n const reservationsCollection = payload.config.collections?.find((c) => c.slug === slugs.reservations)\n const tenantId = readCookie(req.headers.get('cookie'), cookieName)\n const tenantWhere = tenantWhereClause({\n hasField: collectionHasTenantField(reservationsCollection as { fields?: unknown[] } | undefined, tenantField),\n tenantField,\n tenantId,\n })\n\n // Read status machine from config — never hardcode status values\n const statusMachine: StatusMachineConfig | undefined =\n payload.config.admin?.custom?.reservationStatusMachine\n const blockingStatuses: string[] = statusMachine?.blockingStatuses ?? []\n const terminalStatuses: string[] = statusMachine?.terminalStatuses ?? []\n const blockingSet = new Set(blockingStatuses)\n const terminalSet = new Set(terminalStatuses)\n\n // \"Today\" is the business timezone's calendar day, not the server's — and in\n // multiTenant mode that's the SELECTED tenant's zone (tenant → global → UTC).\n const reservationTimezone: string = await getEffectiveTenantTimezone({\n globalTimezone: payload.config.admin?.custom?.reservationTimezone ?? 'UTC',\n payload,\n scopedCollection: reservationsCollection as { fields?: unknown[] } | undefined,\n tenantField,\n tenantId,\n timezoneField,\n })\n const now = new Date()\n const todayKey = getDayKeyInTimezone(now, reservationTimezone)\n const startOfDay = combineDayKeyAndTime(todayKey, '00:00', reservationTimezone)\n const endOfDay = combineDayKeyAndTime(addDaysToDayKey(todayKey, 1), '00:00', reservationTimezone)\n\n const where: Where = {\n startTime: {\n greater_than_equal: startOfDay.toISOString(),\n less_than: endOfDay.toISOString(),\n },\n }\n if (tenantWhere) {\n Object.assign(where, tenantWhere)\n }\n\n // Stats are computed with count queries rather than a capped fetch+filter,\n // so they stay accurate past 100 reservations/day (review D7).\n const blocking = Array.from(blockingSet)\n const terminalArr = Array.from(terminalSet)\n const countWhere = (extra?: Where): Where => (extra ? { and: [where, extra] } : where)\n\n const [total, active, terminal, upcoming, nextResult] = await Promise.all([\n payload.count({ collection: slugs.reservations, where }).then((r) => r.totalDocs),\n blocking.length\n ? payload\n .count({ collection: slugs.reservations, where: countWhere({ status: { in: blocking } }) })\n .then((r) => r.totalDocs)\n : Promise.resolve(0),\n terminalArr.length\n ? payload\n .count({\n collection: slugs.reservations,\n where: countWhere({ status: { in: terminalArr } }),\n })\n .then((r) => r.totalDocs)\n : Promise.resolve(0),\n blocking.length\n ? payload\n .count({\n collection: slugs.reservations,\n where: countWhere({\n and: [{ status: { in: blocking } }, { startTime: { greater_than: now.toISOString() } }],\n }),\n })\n .then((r) => r.totalDocs)\n : Promise.resolve(0),\n blocking.length\n ? payload.find({\n collection: slugs.reservations,\n limit: 1,\n sort: 'startTime',\n where: countWhere({\n and: [{ status: { in: blocking } }, { startTime: { greater_than: now.toISOString() } }],\n }),\n })\n : Promise.resolve({ docs: [] as Record<string, unknown>[] }),\n ])\n\n // Next appointment = the earliest upcoming blocking reservation\n const nextAppointment = nextResult.docs[0] as Record<string, unknown> | undefined\n\n return (\n <div className={styles.wrapper}>\n <h3 className={styles.title}>{t('reservation:dashboardTitle')}</h3>\n <div className={styles.statsGrid}>\n <div className={styles.statCard}>\n <span className={styles.statValue}>{total}</span>\n <span className={styles.statLabel}>{t('reservation:dashboardTotal')}</span>\n </div>\n <div className={styles.statCard}>\n <span className={styles.statValue}>{active}</span>\n <span className={styles.statLabel}>{t('reservation:dashboardActive')}</span>\n </div>\n <div className={styles.statCard}>\n <span className={styles.statValue}>{upcoming}</span>\n <span className={styles.statLabel}>{t('reservation:dashboardUpcoming')}</span>\n </div>\n <div className={styles.statCard}>\n <span className={styles.statValue}>{terminal}</span>\n <span className={styles.statLabel}>{t('reservation:dashboardTerminal')}</span>\n </div>\n </div>\n {nextAppointment ? (\n <div className={styles.nextAppointment}>\n <strong>{t('reservation:dashboardNextAppointment')}</strong>\n <p>\n {t('reservation:dashboardTime')}{' '}\n {new Date(nextAppointment.startTime as string).toLocaleTimeString([], {\n hour: '2-digit',\n minute: '2-digit',\n timeZone: reservationTimezone,\n })}\n </p>\n <p>\n {t('reservation:dashboardStatus')} {nextAppointment.status as string}\n </p>\n </div>\n ) : (\n <p className={styles.noData}>{t('reservation:dashboardNoUpcoming')}</p>\n )}\n </div>\n )\n}\n"],"names":["collectionHasTenantField","readCookie","tenantWhereClause","getEffectiveTenantTimezone","addDaysToDayKey","combineDayKeyAndTime","getDayKeyInTimezone","styles","DashboardWidgetServer","props","req","i18n","payload","t","slugs","config","admin","custom","reservationSlugs","tenantConfig","reservationTenant","cookieName","tenantField","timezoneField","reservationsCollection","collections","find","c","slug","reservations","tenantId","headers","get","tenantWhere","hasField","statusMachine","reservationStatusMachine","blockingStatuses","terminalStatuses","blockingSet","Set","terminalSet","reservationTimezone","globalTimezone","scopedCollection","now","Date","todayKey","startOfDay","endOfDay","where","startTime","greater_than_equal","toISOString","less_than","Object","assign","blocking","Array","from","terminalArr","countWhere","extra","and","total","active","terminal","upcoming","nextResult","Promise","all","count","collection","then","r","totalDocs","length","status","in","resolve","greater_than","limit","sort","docs","nextAppointment","div","className","wrapper","h3","title","statsGrid","statCard","span","statValue","statLabel","strong","p","toLocaleTimeString","hour","minute","timeZone","noData"],"mappings":"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"}
package/dist/defaults.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { DEFAULT_STATUS_MACHINE } from './types.js';
2
+ import { validateTimezone } from './utilities/timezoneUtils.js';
2
3
  function validateStatusMachine(sm) {
3
4
  if (!sm.statuses.includes(sm.defaultStatus)) {
4
5
  throw new Error(`statusMachine.defaultStatus "${sm.defaultStatus}" is not in statuses array`);
@@ -23,6 +24,23 @@ function validateStatusMachine(sm) {
23
24
  }
24
25
  }
25
26
  }
27
+ // A terminal status must have no outgoing transitions — otherwise the config
28
+ // contradicts itself (the status is declared terminal but can be left).
29
+ for (const s of sm.terminalStatuses){
30
+ if ((sm.transitions[s]?.length ?? 0) > 0) {
31
+ throw new Error(`statusMachine.terminalStatuses contains "${s}" but transitions["${s}"] is non-empty — a terminal status cannot have outgoing transitions`);
32
+ }
33
+ }
34
+ // A reservation is born in defaultStatus, so it must not be terminal.
35
+ if (sm.terminalStatuses.includes(sm.defaultStatus)) {
36
+ throw new Error(`statusMachine.defaultStatus "${sm.defaultStatus}" cannot be a terminal status`);
37
+ }
38
+ if (!sm.statuses.includes(sm.confirmStatus)) {
39
+ throw new Error(`statusMachine.confirmStatus "${sm.confirmStatus}" is not in statuses array`);
40
+ }
41
+ if (!sm.statuses.includes(sm.cancelStatus)) {
42
+ throw new Error(`statusMachine.cancelStatus "${sm.cancelStatus}" is not in statuses array`);
43
+ }
26
44
  }
27
45
  export const DEFAULT_RESOURCE_TYPES = [
28
46
  'staff',
@@ -77,11 +95,17 @@ export const DEFAULT_ALLOW_GUEST_BOOKING = false;
77
95
  export const DEFAULT_BUFFER_TIME = 0;
78
96
  export const DEFAULT_CANCELLATION_NOTICE_PERIOD = 24;
79
97
  export function resolveConfig(pluginOptions) {
80
- if (pluginOptions.resourceTypes !== undefined && pluginOptions.resourceTypes.length === 0) {
81
- throw new Error('resourceTypes must be a non-empty array');
82
- }
83
- if (pluginOptions.leaveTypes !== undefined && pluginOptions.leaveTypes.length === 0) {
84
- throw new Error('leaveTypes must be a non-empty array');
98
+ // A disabled plugin still resolves (so collections register with the right
99
+ // slugs for schema stability) but skips all config validation — temporarily
100
+ // disabling a misconfigured plugin should not throw at boot (review C3).
101
+ const disabled = pluginOptions.disabled ?? false;
102
+ if (!disabled) {
103
+ if (pluginOptions.resourceTypes !== undefined && pluginOptions.resourceTypes.length === 0) {
104
+ throw new Error('resourceTypes must be a non-empty array');
105
+ }
106
+ if (pluginOptions.leaveTypes !== undefined && pluginOptions.leaveTypes.length === 0) {
107
+ throw new Error('leaveTypes must be a non-empty array');
108
+ }
85
109
  }
86
110
  const resourceTypes = pluginOptions.resourceTypes ?? DEFAULT_RESOURCE_TYPES;
87
111
  const userStatusMachine = pluginOptions.statusMachine;
@@ -91,21 +115,26 @@ export function resolveConfig(pluginOptions) {
91
115
  adminGroup: pluginOptions.adminGroup ?? DEFAULT_ADMIN_GROUP,
92
116
  allowGuestBooking: pluginOptions.allowGuestBooking ?? DEFAULT_ALLOW_GUEST_BOOKING,
93
117
  cancellationNoticePeriod: pluginOptions.cancellationNoticePeriod ?? DEFAULT_CANCELLATION_NOTICE_PERIOD,
118
+ collectionOverrides: pluginOptions.collectionOverrides ?? {},
94
119
  defaultBufferTime: pluginOptions.defaultBufferTime ?? DEFAULT_BUFFER_TIME,
95
120
  disabled: pluginOptions.disabled ?? false,
96
121
  extraReservationFields: pluginOptions.extraReservationFields ?? [],
122
+ // Real value is set by the plugin once config.collections is known (C8)
123
+ hasMediaCollection: false,
97
124
  hooks: pluginOptions.hooks ?? {},
98
125
  leaveTypes: pluginOptions.leaveTypes ?? DEFAULT_LEAVE_TYPES,
99
126
  localized: false,
100
127
  multiTenant: {
101
128
  cookieName: pluginOptions.multiTenant?.cookieName ?? 'payload-tenant',
102
- tenantField: pluginOptions.multiTenant?.tenantField ?? 'tenant'
129
+ tenantField: pluginOptions.multiTenant?.tenantField ?? 'tenant',
130
+ timezoneField: pluginOptions.multiTenant?.timezoneField ?? 'timezone'
103
131
  },
104
132
  resourceOwnerMode: rom ? {
105
133
  adminRoles: rom.adminRoles ?? [],
106
134
  ownedServices: rom.ownedServices ?? false,
107
135
  ownerCollection: rom.ownerCollection,
108
- ownerField: rom.ownerField ?? 'owner'
136
+ ownerField: rom.ownerField ?? 'owner',
137
+ roleField: rom.roleField ?? pluginOptions.staffProvisioning?.roleField ?? 'role'
109
138
  } : undefined,
110
139
  resourceTypes,
111
140
  slugs: {
@@ -116,9 +145,11 @@ export function resolveConfig(pluginOptions) {
116
145
  schedules: pluginOptions.slugs?.schedules ?? DEFAULT_SLUGS.schedules,
117
146
  services: pluginOptions.slugs?.services ?? DEFAULT_SLUGS.services
118
147
  },
119
- staffProvisioning: resolveStaffProvisioning(pluginOptions, resourceTypes),
148
+ staffProvisioning: disabled ? undefined : resolveStaffProvisioning(pluginOptions, resourceTypes),
120
149
  statusMachine: userStatusMachine ? {
121
150
  blockingStatuses: userStatusMachine.blockingStatuses ?? DEFAULT_STATUS_MACHINE.blockingStatuses,
151
+ cancelStatus: userStatusMachine.cancelStatus ?? DEFAULT_STATUS_MACHINE.cancelStatus,
152
+ confirmStatus: userStatusMachine.confirmStatus ?? DEFAULT_STATUS_MACHINE.confirmStatus,
122
153
  defaultStatus: userStatusMachine.defaultStatus ?? DEFAULT_STATUS_MACHINE.defaultStatus,
123
154
  statuses: userStatusMachine.statuses ?? DEFAULT_STATUS_MACHINE.statuses,
124
155
  terminalStatuses: userStatusMachine.terminalStatuses ?? DEFAULT_STATUS_MACHINE.terminalStatuses,
@@ -126,9 +157,13 @@ export function resolveConfig(pluginOptions) {
126
157
  } : {
127
158
  ...DEFAULT_STATUS_MACHINE
128
159
  },
160
+ timezone: pluginOptions.timezone ?? 'UTC',
129
161
  userCollection: pluginOptions.userCollection ?? undefined
130
162
  };
131
- validateStatusMachine(resolved.statusMachine);
163
+ if (!disabled) {
164
+ validateStatusMachine(resolved.statusMachine);
165
+ validateTimezone(resolved.timezone);
166
+ }
132
167
  return resolved;
133
168
  }
134
169
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/defaults.ts"],"sourcesContent":["import type {\n ReservationPluginConfig,\n ResolvedReservationPluginConfig,\n ResolvedStaffProvisioningConfig,\n StatusMachineConfig,\n} from './types.js'\n\nimport { DEFAULT_STATUS_MACHINE } from './types.js'\n\nfunction validateStatusMachine(sm: StatusMachineConfig): void {\n if (!sm.statuses.includes(sm.defaultStatus)) {\n throw new Error(`statusMachine.defaultStatus \"${sm.defaultStatus}\" is not in statuses array`)\n }\n for (const s of sm.blockingStatuses) {\n if (!sm.statuses.includes(s)) {\n throw new Error(`statusMachine.blockingStatuses contains \"${s}\" which is not in statuses array`)\n }\n }\n for (const s of sm.terminalStatuses) {\n if (!sm.statuses.includes(s)) {\n throw new Error(`statusMachine.terminalStatuses contains \"${s}\" which is not in statuses array`)\n }\n }\n for (const [from, targets] of Object.entries(sm.transitions)) {\n if (!sm.statuses.includes(from)) {\n throw new Error(`statusMachine.transitions has key \"${from}\" which is not in statuses array`)\n }\n for (const to of targets) {\n if (!sm.statuses.includes(to)) {\n throw new Error(`statusMachine.transitions[\"${from}\"] targets \"${to}\" which is not in statuses array`)\n }\n }\n }\n}\n\nexport const DEFAULT_RESOURCE_TYPES = ['staff', 'equipment', 'room']\nexport const DEFAULT_LEAVE_TYPES = ['vacation', 'sick', 'personal', 'closure', 'other']\n\nfunction resolveStaffProvisioning(\n pluginOptions: ReservationPluginConfig,\n resourceTypes: string[],\n): ResolvedStaffProvisioningConfig | undefined {\n const sp = pluginOptions.staffProvisioning\n if (!sp) {\n return undefined\n }\n\n if (!pluginOptions.resourceOwnerMode) {\n throw new Error('staffProvisioning requires resourceOwnerMode to be enabled')\n }\n if (sp.staffRoles.length === 0) {\n throw new Error('staffProvisioning.staffRoles must be a non-empty array')\n }\n const resourceType = sp.resourceType ?? 'staff'\n if (!resourceTypes.includes(resourceType)) {\n throw new Error(\n `staffProvisioning.resourceType \"${resourceType}\" is not in resourceTypes [${resourceTypes.join(', ')}]`,\n )\n }\n const userCollection = sp.userCollection ?? pluginOptions.userCollection\n if (!userCollection) {\n throw new Error(\n 'staffProvisioning.userCollection is required when top-level userCollection is unset',\n )\n }\n\n return {\n beforeCreate: sp.beforeCreate,\n nameFrom: sp.nameFrom ?? 'name',\n resourceType,\n roleField: sp.roleField ?? 'role',\n staffRoles: sp.staffRoles,\n userCollection,\n }\n}\n\nexport const DEFAULT_SLUGS = {\n customers: 'customers',\n media: 'media',\n reservations: 'reservations',\n resources: 'resources',\n schedules: 'schedules',\n services: 'services',\n} as const\n\nexport const DEFAULT_ADMIN_GROUP = 'Reservations'\nexport const DEFAULT_ALLOW_GUEST_BOOKING = false\nexport const DEFAULT_BUFFER_TIME = 0\nexport const DEFAULT_CANCELLATION_NOTICE_PERIOD = 24\n\nexport function resolveConfig(\n pluginOptions: ReservationPluginConfig,\n): ResolvedReservationPluginConfig {\n if (pluginOptions.resourceTypes !== undefined && pluginOptions.resourceTypes.length === 0) {\n throw new Error('resourceTypes must be a non-empty array')\n }\n if (pluginOptions.leaveTypes !== undefined && pluginOptions.leaveTypes.length === 0) {\n throw new Error('leaveTypes must be a non-empty array')\n }\n\n const resourceTypes = pluginOptions.resourceTypes ?? DEFAULT_RESOURCE_TYPES\n const userStatusMachine = pluginOptions.statusMachine\n const rom = pluginOptions.resourceOwnerMode\n const resolved: ResolvedReservationPluginConfig = {\n access: pluginOptions.access ?? {},\n adminGroup: pluginOptions.adminGroup ?? DEFAULT_ADMIN_GROUP,\n allowGuestBooking: pluginOptions.allowGuestBooking ?? DEFAULT_ALLOW_GUEST_BOOKING,\n cancellationNoticePeriod:\n pluginOptions.cancellationNoticePeriod ?? DEFAULT_CANCELLATION_NOTICE_PERIOD,\n defaultBufferTime: pluginOptions.defaultBufferTime ?? DEFAULT_BUFFER_TIME,\n disabled: pluginOptions.disabled ?? false,\n extraReservationFields: pluginOptions.extraReservationFields ?? [],\n hooks: pluginOptions.hooks ?? {},\n leaveTypes: pluginOptions.leaveTypes ?? DEFAULT_LEAVE_TYPES,\n localized: false,\n multiTenant: {\n cookieName: pluginOptions.multiTenant?.cookieName ?? 'payload-tenant',\n tenantField: pluginOptions.multiTenant?.tenantField ?? 'tenant',\n },\n resourceOwnerMode: rom\n ? {\n adminRoles: rom.adminRoles ?? [],\n ownedServices: rom.ownedServices ?? false,\n ownerCollection: rom.ownerCollection,\n ownerField: rom.ownerField ?? 'owner',\n }\n : undefined,\n resourceTypes,\n slugs: {\n customers: pluginOptions.slugs?.customers ?? DEFAULT_SLUGS.customers,\n media: pluginOptions.slugs?.media ?? DEFAULT_SLUGS.media,\n reservations: pluginOptions.slugs?.reservations ?? DEFAULT_SLUGS.reservations,\n resources: pluginOptions.slugs?.resources ?? DEFAULT_SLUGS.resources,\n schedules: pluginOptions.slugs?.schedules ?? DEFAULT_SLUGS.schedules,\n services: pluginOptions.slugs?.services ?? DEFAULT_SLUGS.services,\n },\n staffProvisioning: resolveStaffProvisioning(pluginOptions, resourceTypes),\n statusMachine: userStatusMachine\n ? {\n blockingStatuses:\n userStatusMachine.blockingStatuses ?? DEFAULT_STATUS_MACHINE.blockingStatuses,\n defaultStatus: userStatusMachine.defaultStatus ?? DEFAULT_STATUS_MACHINE.defaultStatus,\n statuses: userStatusMachine.statuses ?? DEFAULT_STATUS_MACHINE.statuses,\n terminalStatuses:\n userStatusMachine.terminalStatuses ?? DEFAULT_STATUS_MACHINE.terminalStatuses,\n transitions: userStatusMachine.transitions ?? DEFAULT_STATUS_MACHINE.transitions,\n }\n : { ...DEFAULT_STATUS_MACHINE },\n userCollection: pluginOptions.userCollection ?? undefined,\n }\n\n validateStatusMachine(resolved.statusMachine)\n\n return resolved\n}\n"],"names":["DEFAULT_STATUS_MACHINE","validateStatusMachine","sm","statuses","includes","defaultStatus","Error","s","blockingStatuses","terminalStatuses","from","targets","Object","entries","transitions","to","DEFAULT_RESOURCE_TYPES","DEFAULT_LEAVE_TYPES","resolveStaffProvisioning","pluginOptions","resourceTypes","sp","staffProvisioning","undefined","resourceOwnerMode","staffRoles","length","resourceType","join","userCollection","beforeCreate","nameFrom","roleField","DEFAULT_SLUGS","customers","media","reservations","resources","schedules","services","DEFAULT_ADMIN_GROUP","DEFAULT_ALLOW_GUEST_BOOKING","DEFAULT_BUFFER_TIME","DEFAULT_CANCELLATION_NOTICE_PERIOD","resolveConfig","leaveTypes","userStatusMachine","statusMachine","rom","resolved","access","adminGroup","allowGuestBooking","cancellationNoticePeriod","defaultBufferTime","disabled","extraReservationFields","hooks","localized","multiTenant","cookieName","tenantField","adminRoles","ownedServices","ownerCollection","ownerField","slugs"],"mappings":"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"}
1
+ {"version":3,"sources":["../src/defaults.ts"],"sourcesContent":["import type {\n ReservationPluginConfig,\n ResolvedReservationPluginConfig,\n ResolvedStaffProvisioningConfig,\n StatusMachineConfig,\n} from './types.js'\n\nimport { DEFAULT_STATUS_MACHINE } from './types.js'\nimport { validateTimezone } from './utilities/timezoneUtils.js'\n\nfunction validateStatusMachine(sm: StatusMachineConfig): void {\n if (!sm.statuses.includes(sm.defaultStatus)) {\n throw new Error(`statusMachine.defaultStatus \"${sm.defaultStatus}\" is not in statuses array`)\n }\n for (const s of sm.blockingStatuses) {\n if (!sm.statuses.includes(s)) {\n throw new Error(`statusMachine.blockingStatuses contains \"${s}\" which is not in statuses array`)\n }\n }\n for (const s of sm.terminalStatuses) {\n if (!sm.statuses.includes(s)) {\n throw new Error(`statusMachine.terminalStatuses contains \"${s}\" which is not in statuses array`)\n }\n }\n for (const [from, targets] of Object.entries(sm.transitions)) {\n if (!sm.statuses.includes(from)) {\n throw new Error(`statusMachine.transitions has key \"${from}\" which is not in statuses array`)\n }\n for (const to of targets) {\n if (!sm.statuses.includes(to)) {\n throw new Error(`statusMachine.transitions[\"${from}\"] targets \"${to}\" which is not in statuses array`)\n }\n }\n }\n // A terminal status must have no outgoing transitions — otherwise the config\n // contradicts itself (the status is declared terminal but can be left).\n for (const s of sm.terminalStatuses) {\n if ((sm.transitions[s]?.length ?? 0) > 0) {\n throw new Error(\n `statusMachine.terminalStatuses contains \"${s}\" but transitions[\"${s}\"] is non-empty — a terminal status cannot have outgoing transitions`,\n )\n }\n }\n // A reservation is born in defaultStatus, so it must not be terminal.\n if (sm.terminalStatuses.includes(sm.defaultStatus)) {\n throw new Error(`statusMachine.defaultStatus \"${sm.defaultStatus}\" cannot be a terminal status`)\n }\n if (!sm.statuses.includes(sm.confirmStatus)) {\n throw new Error(`statusMachine.confirmStatus \"${sm.confirmStatus}\" is not in statuses array`)\n }\n if (!sm.statuses.includes(sm.cancelStatus)) {\n throw new Error(`statusMachine.cancelStatus \"${sm.cancelStatus}\" is not in statuses array`)\n }\n}\n\nexport const DEFAULT_RESOURCE_TYPES = ['staff', 'equipment', 'room']\nexport const DEFAULT_LEAVE_TYPES = ['vacation', 'sick', 'personal', 'closure', 'other']\n\nfunction resolveStaffProvisioning(\n pluginOptions: ReservationPluginConfig,\n resourceTypes: string[],\n): ResolvedStaffProvisioningConfig | undefined {\n const sp = pluginOptions.staffProvisioning\n if (!sp) {\n return undefined\n }\n\n if (!pluginOptions.resourceOwnerMode) {\n throw new Error('staffProvisioning requires resourceOwnerMode to be enabled')\n }\n if (sp.staffRoles.length === 0) {\n throw new Error('staffProvisioning.staffRoles must be a non-empty array')\n }\n const resourceType = sp.resourceType ?? 'staff'\n if (!resourceTypes.includes(resourceType)) {\n throw new Error(\n `staffProvisioning.resourceType \"${resourceType}\" is not in resourceTypes [${resourceTypes.join(', ')}]`,\n )\n }\n const userCollection = sp.userCollection ?? pluginOptions.userCollection\n if (!userCollection) {\n throw new Error(\n 'staffProvisioning.userCollection is required when top-level userCollection is unset',\n )\n }\n\n return {\n beforeCreate: sp.beforeCreate,\n nameFrom: sp.nameFrom ?? 'name',\n resourceType,\n roleField: sp.roleField ?? 'role',\n staffRoles: sp.staffRoles,\n userCollection,\n }\n}\n\nexport const DEFAULT_SLUGS = {\n customers: 'customers',\n media: 'media',\n reservations: 'reservations',\n resources: 'resources',\n schedules: 'schedules',\n services: 'services',\n} as const\n\nexport const DEFAULT_ADMIN_GROUP = 'Reservations'\nexport const DEFAULT_ALLOW_GUEST_BOOKING = false\nexport const DEFAULT_BUFFER_TIME = 0\nexport const DEFAULT_CANCELLATION_NOTICE_PERIOD = 24\n\nexport function resolveConfig(\n pluginOptions: ReservationPluginConfig,\n): ResolvedReservationPluginConfig {\n // A disabled plugin still resolves (so collections register with the right\n // slugs for schema stability) but skips all config validation — temporarily\n // disabling a misconfigured plugin should not throw at boot (review C3).\n const disabled = pluginOptions.disabled ?? false\n\n if (!disabled) {\n if (pluginOptions.resourceTypes !== undefined && pluginOptions.resourceTypes.length === 0) {\n throw new Error('resourceTypes must be a non-empty array')\n }\n if (pluginOptions.leaveTypes !== undefined && pluginOptions.leaveTypes.length === 0) {\n throw new Error('leaveTypes must be a non-empty array')\n }\n }\n\n const resourceTypes = pluginOptions.resourceTypes ?? DEFAULT_RESOURCE_TYPES\n const userStatusMachine = pluginOptions.statusMachine\n const rom = pluginOptions.resourceOwnerMode\n const resolved: ResolvedReservationPluginConfig = {\n access: pluginOptions.access ?? {},\n adminGroup: pluginOptions.adminGroup ?? DEFAULT_ADMIN_GROUP,\n allowGuestBooking: pluginOptions.allowGuestBooking ?? DEFAULT_ALLOW_GUEST_BOOKING,\n cancellationNoticePeriod:\n pluginOptions.cancellationNoticePeriod ?? DEFAULT_CANCELLATION_NOTICE_PERIOD,\n collectionOverrides: pluginOptions.collectionOverrides ?? {},\n defaultBufferTime: pluginOptions.defaultBufferTime ?? DEFAULT_BUFFER_TIME,\n disabled: pluginOptions.disabled ?? false,\n extraReservationFields: pluginOptions.extraReservationFields ?? [],\n // Real value is set by the plugin once config.collections is known (C8)\n hasMediaCollection: false,\n hooks: pluginOptions.hooks ?? {},\n leaveTypes: pluginOptions.leaveTypes ?? DEFAULT_LEAVE_TYPES,\n localized: false,\n multiTenant: {\n cookieName: pluginOptions.multiTenant?.cookieName ?? 'payload-tenant',\n tenantField: pluginOptions.multiTenant?.tenantField ?? 'tenant',\n timezoneField: pluginOptions.multiTenant?.timezoneField ?? 'timezone',\n },\n resourceOwnerMode: rom\n ? {\n adminRoles: rom.adminRoles ?? [],\n ownedServices: rom.ownedServices ?? false,\n ownerCollection: rom.ownerCollection,\n ownerField: rom.ownerField ?? 'owner',\n roleField: rom.roleField ?? pluginOptions.staffProvisioning?.roleField ?? 'role',\n }\n : undefined,\n resourceTypes,\n slugs: {\n customers: pluginOptions.slugs?.customers ?? DEFAULT_SLUGS.customers,\n media: pluginOptions.slugs?.media ?? DEFAULT_SLUGS.media,\n reservations: pluginOptions.slugs?.reservations ?? DEFAULT_SLUGS.reservations,\n resources: pluginOptions.slugs?.resources ?? DEFAULT_SLUGS.resources,\n schedules: pluginOptions.slugs?.schedules ?? DEFAULT_SLUGS.schedules,\n services: pluginOptions.slugs?.services ?? DEFAULT_SLUGS.services,\n },\n staffProvisioning: disabled\n ? undefined\n : resolveStaffProvisioning(pluginOptions, resourceTypes),\n statusMachine: userStatusMachine\n ? {\n blockingStatuses:\n userStatusMachine.blockingStatuses ?? DEFAULT_STATUS_MACHINE.blockingStatuses,\n cancelStatus: userStatusMachine.cancelStatus ?? DEFAULT_STATUS_MACHINE.cancelStatus,\n confirmStatus: userStatusMachine.confirmStatus ?? DEFAULT_STATUS_MACHINE.confirmStatus,\n defaultStatus: userStatusMachine.defaultStatus ?? DEFAULT_STATUS_MACHINE.defaultStatus,\n statuses: userStatusMachine.statuses ?? DEFAULT_STATUS_MACHINE.statuses,\n terminalStatuses:\n userStatusMachine.terminalStatuses ?? DEFAULT_STATUS_MACHINE.terminalStatuses,\n transitions: userStatusMachine.transitions ?? DEFAULT_STATUS_MACHINE.transitions,\n }\n : { ...DEFAULT_STATUS_MACHINE },\n timezone: pluginOptions.timezone ?? 'UTC',\n userCollection: pluginOptions.userCollection ?? undefined,\n }\n\n if (!disabled) {\n validateStatusMachine(resolved.statusMachine)\n validateTimezone(resolved.timezone)\n }\n\n return resolved\n}\n"],"names":["DEFAULT_STATUS_MACHINE","validateTimezone","validateStatusMachine","sm","statuses","includes","defaultStatus","Error","s","blockingStatuses","terminalStatuses","from","targets","Object","entries","transitions","to","length","confirmStatus","cancelStatus","DEFAULT_RESOURCE_TYPES","DEFAULT_LEAVE_TYPES","resolveStaffProvisioning","pluginOptions","resourceTypes","sp","staffProvisioning","undefined","resourceOwnerMode","staffRoles","resourceType","join","userCollection","beforeCreate","nameFrom","roleField","DEFAULT_SLUGS","customers","media","reservations","resources","schedules","services","DEFAULT_ADMIN_GROUP","DEFAULT_ALLOW_GUEST_BOOKING","DEFAULT_BUFFER_TIME","DEFAULT_CANCELLATION_NOTICE_PERIOD","resolveConfig","disabled","leaveTypes","userStatusMachine","statusMachine","rom","resolved","access","adminGroup","allowGuestBooking","cancellationNoticePeriod","collectionOverrides","defaultBufferTime","extraReservationFields","hasMediaCollection","hooks","localized","multiTenant","cookieName","tenantField","timezoneField","adminRoles","ownedServices","ownerCollection","ownerField","slugs","timezone"],"mappings":"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"}
@@ -49,7 +49,7 @@ export function createCancelBookingEndpoint(config) {
49
49
  collection: config.slugs.reservations,
50
50
  data: {
51
51
  cancellationReason: reason,
52
- status: 'cancelled'
52
+ status: config.statusMachine.cancelStatus
53
53
  },
54
54
  // Authorization (owner / admin / matching token) is already enforced above.
55
55
  // overrideAccess lets the write proceed for anonymous guest cancellations,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/endpoints/cancelBooking.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { isPrivilegedUser } from '../utilities/userRoles.js'\n\nexport function createCancelBookingEndpoint(config: ResolvedReservationPluginConfig): Endpoint {\n return {\n handler: async (req) => {\n const body = await req.json?.()\n const { reason, reservationId, token } = (body ?? {}) as {\n reason?: string\n reservationId?: string\n token?: string\n }\n\n if (!reservationId) {\n return Response.json({ message: 'reservationId is required' }, { status: 400 })\n }\n\n // Fetch the reservation to check ownership / token.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const existing = await (req.payload.findByID as any)({\n id: reservationId,\n collection: config.slugs.reservations,\n depth: 0,\n overrideAccess: true,\n req,\n })\n\n if (req.user) {\n // Authenticated path: owner (customer === req.user) or admin/staff.\n const customerId =\n typeof existing.customer === 'object' ? existing.customer?.id : existing.customer\n const isOwner = customerId === req.user.id\n // Staff/admin detection (role-aware for single-collection deployments)\n const isAdmin = isPrivilegedUser(req.user, config)\n if (!isOwner && !isAdmin) {\n return Response.json({ message: 'Forbidden' }, { status: 403 })\n }\n } else {\n // Guest path: match the cancellation token.\n if (!token || !existing.cancellationToken || token !== existing.cancellationToken) {\n return Response.json({ message: 'Forbidden' }, { status: 403 })\n }\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const reservation = await (req.payload.update as any)({\n id: reservationId,\n collection: config.slugs.reservations,\n data: {\n cancellationReason: reason,\n status: 'cancelled',\n },\n // Authorization (owner / admin / matching token) is already enforced above.\n // overrideAccess lets the write proceed for anonymous guest cancellations,\n // and avoids a spurious 500 when resourceOwnerMode restricts update access.\n overrideAccess: true,\n req,\n })\n\n // Strip the cancellation token from the response, consistent with the book\n // endpoint — it must never be echoed back over HTTP.\n const { cancellationToken: _cancellationToken, ...safeReservation } =\n reservation as Record<string, unknown>\n\n return Response.json(safeReservation)\n },\n method: 'post',\n path: '/reserve/cancel',\n }\n}\n"],"names":["isPrivilegedUser","createCancelBookingEndpoint","config","handler","req","body","json","reason","reservationId","token","Response","message","status","existing","payload","findByID","id","collection","slugs","reservations","depth","overrideAccess","user","customerId","customer","isOwner","isAdmin","cancellationToken","reservation","update","data","cancellationReason","_cancellationToken","safeReservation","method","path"],"mappings":"AAIA,SAASA,gBAAgB,QAAQ,4BAA2B;AAE5D,OAAO,SAASC,4BAA4BC,MAAuC;IACjF,OAAO;QACLC,SAAS,OAAOC;YACd,MAAMC,OAAO,MAAMD,IAAIE,IAAI;YAC3B,MAAM,EAAEC,MAAM,EAAEC,aAAa,EAAEC,KAAK,EAAE,GAAIJ,QAAQ,CAAC;YAMnD,IAAI,CAACG,eAAe;gBAClB,OAAOE,SAASJ,IAAI,CAAC;oBAAEK,SAAS;gBAA4B,GAAG;oBAAEC,QAAQ;gBAAI;YAC/E;YAEA,oDAAoD;YACpD,8DAA8D;YAC9D,MAAMC,WAAW,MAAM,AAACT,IAAIU,OAAO,CAACC,QAAQ,CAAS;gBACnDC,IAAIR;gBACJS,YAAYf,OAAOgB,KAAK,CAACC,YAAY;gBACrCC,OAAO;gBACPC,gBAAgB;gBAChBjB;YACF;YAEA,IAAIA,IAAIkB,IAAI,EAAE;gBACZ,oEAAoE;gBACpE,MAAMC,aACJ,OAAOV,SAASW,QAAQ,KAAK,WAAWX,SAASW,QAAQ,EAAER,KAAKH,SAASW,QAAQ;gBACnF,MAAMC,UAAUF,eAAenB,IAAIkB,IAAI,CAACN,EAAE;gBAC1C,uEAAuE;gBACvE,MAAMU,UAAU1B,iBAAiBI,IAAIkB,IAAI,EAAEpB;gBAC3C,IAAI,CAACuB,WAAW,CAACC,SAAS;oBACxB,OAAOhB,SAASJ,IAAI,CAAC;wBAAEK,SAAS;oBAAY,GAAG;wBAAEC,QAAQ;oBAAI;gBAC/D;YACF,OAAO;gBACL,4CAA4C;gBAC5C,IAAI,CAACH,SAAS,CAACI,SAASc,iBAAiB,IAAIlB,UAAUI,SAASc,iBAAiB,EAAE;oBACjF,OAAOjB,SAASJ,IAAI,CAAC;wBAAEK,SAAS;oBAAY,GAAG;wBAAEC,QAAQ;oBAAI;gBAC/D;YACF;YAEA,8DAA8D;YAC9D,MAAMgB,cAAc,MAAM,AAACxB,IAAIU,OAAO,CAACe,MAAM,CAAS;gBACpDb,IAAIR;gBACJS,YAAYf,OAAOgB,KAAK,CAACC,YAAY;gBACrCW,MAAM;oBACJC,oBAAoBxB;oBACpBK,QAAQ;gBACV;gBACA,4EAA4E;gBAC5E,2EAA2E;gBAC3E,4EAA4E;gBAC5ES,gBAAgB;gBAChBjB;YACF;YAEA,2EAA2E;YAC3E,qDAAqD;YACrD,MAAM,EAAEuB,mBAAmBK,kBAAkB,EAAE,GAAGC,iBAAiB,GACjEL;YAEF,OAAOlB,SAASJ,IAAI,CAAC2B;QACvB;QACAC,QAAQ;QACRC,MAAM;IACR;AACF"}
1
+ {"version":3,"sources":["../../src/endpoints/cancelBooking.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { isPrivilegedUser } from '../utilities/userRoles.js'\n\nexport function createCancelBookingEndpoint(config: ResolvedReservationPluginConfig): Endpoint {\n return {\n handler: async (req) => {\n const body = await req.json?.()\n const { reason, reservationId, token } = (body ?? {}) as {\n reason?: string\n reservationId?: string\n token?: string\n }\n\n if (!reservationId) {\n return Response.json({ message: 'reservationId is required' }, { status: 400 })\n }\n\n // Fetch the reservation to check ownership / token.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const existing = await (req.payload.findByID as any)({\n id: reservationId,\n collection: config.slugs.reservations,\n depth: 0,\n overrideAccess: true,\n req,\n })\n\n if (req.user) {\n // Authenticated path: owner (customer === req.user) or admin/staff.\n const customerId =\n typeof existing.customer === 'object' ? existing.customer?.id : existing.customer\n const isOwner = customerId === req.user.id\n // Staff/admin detection (role-aware for single-collection deployments)\n const isAdmin = isPrivilegedUser(req.user, config)\n if (!isOwner && !isAdmin) {\n return Response.json({ message: 'Forbidden' }, { status: 403 })\n }\n } else {\n // Guest path: match the cancellation token.\n if (!token || !existing.cancellationToken || token !== existing.cancellationToken) {\n return Response.json({ message: 'Forbidden' }, { status: 403 })\n }\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const reservation = await (req.payload.update as any)({\n id: reservationId,\n collection: config.slugs.reservations,\n data: {\n cancellationReason: reason,\n status: config.statusMachine.cancelStatus,\n },\n // Authorization (owner / admin / matching token) is already enforced above.\n // overrideAccess lets the write proceed for anonymous guest cancellations,\n // and avoids a spurious 500 when resourceOwnerMode restricts update access.\n overrideAccess: true,\n req,\n })\n\n // Strip the cancellation token from the response, consistent with the book\n // endpoint — it must never be echoed back over HTTP.\n const { cancellationToken: _cancellationToken, ...safeReservation } =\n reservation as Record<string, unknown>\n\n return Response.json(safeReservation)\n },\n method: 'post',\n path: '/reserve/cancel',\n }\n}\n"],"names":["isPrivilegedUser","createCancelBookingEndpoint","config","handler","req","body","json","reason","reservationId","token","Response","message","status","existing","payload","findByID","id","collection","slugs","reservations","depth","overrideAccess","user","customerId","customer","isOwner","isAdmin","cancellationToken","reservation","update","data","cancellationReason","statusMachine","cancelStatus","_cancellationToken","safeReservation","method","path"],"mappings":"AAIA,SAASA,gBAAgB,QAAQ,4BAA2B;AAE5D,OAAO,SAASC,4BAA4BC,MAAuC;IACjF,OAAO;QACLC,SAAS,OAAOC;YACd,MAAMC,OAAO,MAAMD,IAAIE,IAAI;YAC3B,MAAM,EAAEC,MAAM,EAAEC,aAAa,EAAEC,KAAK,EAAE,GAAIJ,QAAQ,CAAC;YAMnD,IAAI,CAACG,eAAe;gBAClB,OAAOE,SAASJ,IAAI,CAAC;oBAAEK,SAAS;gBAA4B,GAAG;oBAAEC,QAAQ;gBAAI;YAC/E;YAEA,oDAAoD;YACpD,8DAA8D;YAC9D,MAAMC,WAAW,MAAM,AAACT,IAAIU,OAAO,CAACC,QAAQ,CAAS;gBACnDC,IAAIR;gBACJS,YAAYf,OAAOgB,KAAK,CAACC,YAAY;gBACrCC,OAAO;gBACPC,gBAAgB;gBAChBjB;YACF;YAEA,IAAIA,IAAIkB,IAAI,EAAE;gBACZ,oEAAoE;gBACpE,MAAMC,aACJ,OAAOV,SAASW,QAAQ,KAAK,WAAWX,SAASW,QAAQ,EAAER,KAAKH,SAASW,QAAQ;gBACnF,MAAMC,UAAUF,eAAenB,IAAIkB,IAAI,CAACN,EAAE;gBAC1C,uEAAuE;gBACvE,MAAMU,UAAU1B,iBAAiBI,IAAIkB,IAAI,EAAEpB;gBAC3C,IAAI,CAACuB,WAAW,CAACC,SAAS;oBACxB,OAAOhB,SAASJ,IAAI,CAAC;wBAAEK,SAAS;oBAAY,GAAG;wBAAEC,QAAQ;oBAAI;gBAC/D;YACF,OAAO;gBACL,4CAA4C;gBAC5C,IAAI,CAACH,SAAS,CAACI,SAASc,iBAAiB,IAAIlB,UAAUI,SAASc,iBAAiB,EAAE;oBACjF,OAAOjB,SAASJ,IAAI,CAAC;wBAAEK,SAAS;oBAAY,GAAG;wBAAEC,QAAQ;oBAAI;gBAC/D;YACF;YAEA,8DAA8D;YAC9D,MAAMgB,cAAc,MAAM,AAACxB,IAAIU,OAAO,CAACe,MAAM,CAAS;gBACpDb,IAAIR;gBACJS,YAAYf,OAAOgB,KAAK,CAACC,YAAY;gBACrCW,MAAM;oBACJC,oBAAoBxB;oBACpBK,QAAQV,OAAO8B,aAAa,CAACC,YAAY;gBAC3C;gBACA,4EAA4E;gBAC5E,2EAA2E;gBAC3E,4EAA4E;gBAC5EZ,gBAAgB;gBAChBjB;YACF;YAEA,2EAA2E;YAC3E,qDAAqD;YACrD,MAAM,EAAEuB,mBAAmBO,kBAAkB,EAAE,GAAGC,iBAAiB,GACjEP;YAEF,OAAOlB,SAASJ,IAAI,CAAC6B;QACvB;QACAC,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 createCheckAvailabilityEndpoint(config) {
4
5
  return {
5
6
  handler: async (req)=>{
@@ -14,15 +15,39 @@ export function createCheckAvailabilityEndpoint(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 createCheckAvailabilityEndpoint(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 createCheckAvailabilityEndpoint(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
  slots
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/endpoints/checkAvailability.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 createCheckAvailabilityEndpoint(\n config: ResolvedReservationPluginConfig,\n): 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 { message: '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({ slots })\n },\n method: 'get',\n path: '/reserve/availability',\n }\n}\n"],"names":["getAvailableSlots","extractId","mergeResourceIds","createCheckAvailabilityEndpoint","config","handler","req","url","URL","date","searchParams","get","resource","service","Response","json","message","status","parsedDate","Date","isNaN","getTime","error","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","method","path"],"mappings":"AAIA,SAASA,iBAAiB,QAAQ,qCAAoC;AACtE,SAASC,SAAS,EAAEC,gBAAgB,QAAQ,2CAA0C;AAEtF,OAAO,SAASC,gCACdC,MAAuC;IAEvC,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,SAAS;gBAAyD,GACpE;oBAAEC,QAAQ;gBAAI;YAElB;YAEA,MAAMC,aAAa,IAAIC,KAAKV;YAC5B,IAAIW,MAAMF,WAAWG,OAAO,KAAK;gBAC/B,OAAOP,SAASC,IAAI,CAClB;oBAAEO,OAAO;gBAA2C,GACpD;oBAAEL,QAAQ;gBAAI;YAElB;YAEA,MAAMM,aAAaC,KAAKC,GAAG,CAACC,OAAOnB,IAAIG,YAAY,CAACC,GAAG,CAAC,iBAAiB,MAAM;YAE/E,gFAAgF;YAChF,MAAMgB,WAAWpB,IAAIG,YAAY,CAACC,GAAG,CAAC;YACtC,MAAMiB,YAAYD,WAAWA,SAASE,KAAK,CAAC,KAAKC,GAAG,CAAC,CAACC,IAAMA,EAAEC,IAAI,IAAIC,MAAM,CAACC,WAAW;gBAACtB;aAAS;YAElG,8DAA8D;YAC9D,MAAMuB,SAAS,MAAM,AAAC7B,IAAI8B,OAAO,CAACC,QAAQ,CAAS;gBACjDC,IAAIzB;gBACJ0B,YAAYnC,OAAOoC,KAAK,CAACC,QAAQ;gBACjCC,OAAO;gBACPpC;YACF;YACA,MAAMqC,cAAc,AAAC,CAAA,AAACR,QAAQS,qBAAmC,EAAE,AAAD,EAC/Dd,GAAG,CAAC,CAACe,IAAM5C,UAAU4C,IACrBZ,MAAM,CAAC,CAACY,IAA4BA,MAAMC;YAC7C,MAAMC,cAAc7C,iBAAiB0B,WAAWe;YAEhD,MAAMK,QAAQ,MAAMhD,kBAAkB;gBACpCiD,kBAAkB7C,OAAO8C,aAAa,CAACD,gBAAgB;gBACvDxC,MAAMS;gBACNK;gBACAa,SAAS9B,IAAI8B,OAAO;gBACpB9B;gBACA6C,iBAAiB/C,OAAOoC,KAAK,CAACY,YAAY;gBAC1CL;gBACAM,cAAcjD,OAAOoC,KAAK,CAACc,SAAS;gBACpCC,cAAcnD,OAAOoC,KAAK,CAACgB,SAAS;gBACpCC,WAAW5C;gBACX6C,aAAatD,OAAOoC,KAAK,CAACC,QAAQ;YACpC;YAEA,OAAO3B,SAASC,IAAI,CAAC;gBAAEiC;YAAM;QAC/B;QACAW,QAAQ;QACRC,MAAM;IACR;AACF"}
1
+ {"version":3,"sources":["../../src/endpoints/checkAvailability.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 createCheckAvailabilityEndpoint(\n config: ResolvedReservationPluginConfig,\n): 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 { message: '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({ slots })\n },\n method: 'get',\n path: '/reserve/availability',\n }\n}\n"],"names":["getAvailableSlots","extractId","mergeResourceIds","getDayKeyInTimezone","isValidDayKey","createCheckAvailabilityEndpoint","config","handler","req","url","URL","date","searchParams","get","resource","service","Response","json","message","status","dayKey","test","error","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","method","path"],"mappings":"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"}
@@ -1,23 +1,32 @@
1
+ import { isPrivilegedUser } from '../utilities/userRoles.js';
1
2
  export function createBookingEndpoint(config) {
2
3
  return {
3
4
  handler: async (req)=>{
4
5
  const data = await req.json?.();
5
- // Call beforeBookingCreate plugin hooks before creating the reservation
6
- let bookingData = data;
7
- if (config.hooks?.beforeBookingCreate) {
8
- for (const hook of config.hooks.beforeBookingCreate){
9
- bookingData = await hook({
10
- data: bookingData,
11
- req
12
- }) ?? bookingData;
6
+ // Cancellation tokens are server-generated secrets never accept one
7
+ // from the request body.
8
+ delete data.cancellationToken;
9
+ // Who may book for whom: staff/admin for anyone (walk-ins); an
10
+ // authenticated customer only for themselves; anonymous callers never
11
+ // for an existing customer record (the guest flow covers them).
12
+ if (!isPrivilegedUser(req.user, config)) {
13
+ if (req.user) {
14
+ data.customer = req.user.id;
15
+ } else if (data.customer) {
16
+ return Response.json({
17
+ error: 'Anonymous bookings cannot set a customer'
18
+ }, {
19
+ status: 403
20
+ });
13
21
  }
14
22
  }
15
23
  // Create via Payload Local API — collection hooks handle conflict detection,
16
- // endTime calculation, and status transitions
24
+ // endTime calculation, status transitions, AND the beforeBookingCreate
25
+ // plugin hooks (running them here too made them fire twice per booking).
17
26
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
27
  const reservation = await req.payload.create({
19
28
  collection: config.slugs.reservations,
20
- data: bookingData,
29
+ data,
21
30
  req
22
31
  });
23
32
  // Never expose the cancellation token in the HTTP response — it is delivered
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/endpoints/createBooking.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nexport function createBookingEndpoint(config: ResolvedReservationPluginConfig): Endpoint {\n return {\n handler: async (req) => {\n const data = (await req.json?.()) as Record<string, unknown>\n\n // Call beforeBookingCreate plugin hooks before creating the reservation\n let bookingData = data\n if (config.hooks?.beforeBookingCreate) {\n for (const hook of config.hooks.beforeBookingCreate) {\n bookingData = (await hook({ data: bookingData, req })) ?? bookingData\n }\n }\n\n // Create via Payload Local API — collection hooks handle conflict detection,\n // endTime calculation, and status transitions\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const reservation = await (req.payload.create as any)({\n collection: config.slugs.reservations,\n data: bookingData,\n req,\n })\n\n // Never expose the cancellation token in the HTTP response — it is delivered\n // to the guest by the host project via the afterBookingCreate hook.\n const { cancellationToken: _cancellationToken, ...safeReservation } =\n reservation as Record<string, unknown>\n\n return Response.json(safeReservation, { status: 201 })\n },\n method: 'post',\n path: '/reserve/book',\n }\n}\n"],"names":["createBookingEndpoint","config","handler","req","data","json","bookingData","hooks","beforeBookingCreate","hook","reservation","payload","create","collection","slugs","reservations","cancellationToken","_cancellationToken","safeReservation","Response","status","method","path"],"mappings":"AAIA,OAAO,SAASA,sBAAsBC,MAAuC;IAC3E,OAAO;QACLC,SAAS,OAAOC;YACd,MAAMC,OAAQ,MAAMD,IAAIE,IAAI;YAE5B,wEAAwE;YACxE,IAAIC,cAAcF;YAClB,IAAIH,OAAOM,KAAK,EAAEC,qBAAqB;gBACrC,KAAK,MAAMC,QAAQR,OAAOM,KAAK,CAACC,mBAAmB,CAAE;oBACnDF,cAAc,AAAC,MAAMG,KAAK;wBAAEL,MAAME;wBAAaH;oBAAI,MAAOG;gBAC5D;YACF;YAEA,6EAA6E;YAC7E,8CAA8C;YAC9C,8DAA8D;YAC9D,MAAMI,cAAc,MAAM,AAACP,IAAIQ,OAAO,CAACC,MAAM,CAAS;gBACpDC,YAAYZ,OAAOa,KAAK,CAACC,YAAY;gBACrCX,MAAME;gBACNH;YACF;YAEA,6EAA6E;YAC7E,oEAAoE;YACpE,MAAM,EAAEa,mBAAmBC,kBAAkB,EAAE,GAAGC,iBAAiB,GACjER;YAEF,OAAOS,SAASd,IAAI,CAACa,iBAAiB;gBAAEE,QAAQ;YAAI;QACtD;QACAC,QAAQ;QACRC,MAAM;IACR;AACF"}
1
+ {"version":3,"sources":["../../src/endpoints/createBooking.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { isPrivilegedUser } from '../utilities/userRoles.js'\n\nexport function createBookingEndpoint(config: ResolvedReservationPluginConfig): Endpoint {\n return {\n handler: async (req) => {\n const data = (await req.json?.()) as Record<string, unknown>\n\n // Cancellation tokens are server-generated secrets never accept one\n // from the request body.\n delete data.cancellationToken\n\n // Who may book for whom: staff/admin for anyone (walk-ins); an\n // authenticated customer only for themselves; anonymous callers never\n // for an existing customer record (the guest flow covers them).\n if (!isPrivilegedUser(req.user, config)) {\n if (req.user) {\n data.customer = req.user.id\n } else if (data.customer) {\n return Response.json(\n { error: 'Anonymous bookings cannot set a customer' },\n { status: 403 },\n )\n }\n }\n\n // Create via Payload Local API — collection hooks handle conflict detection,\n // endTime calculation, status transitions, AND the beforeBookingCreate\n // plugin hooks (running them here too made them fire twice per booking).\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const reservation = await (req.payload.create as any)({\n collection: config.slugs.reservations,\n data,\n req,\n })\n\n // Never expose the cancellation token in the HTTP response — it is delivered\n // to the guest by the host project via the afterBookingCreate hook.\n const { cancellationToken: _cancellationToken, ...safeReservation } =\n reservation as Record<string, unknown>\n\n return Response.json(safeReservation, { status: 201 })\n },\n method: 'post',\n path: '/reserve/book',\n }\n}\n"],"names":["isPrivilegedUser","createBookingEndpoint","config","handler","req","data","json","cancellationToken","user","customer","id","Response","error","status","reservation","payload","create","collection","slugs","reservations","_cancellationToken","safeReservation","method","path"],"mappings":"AAIA,SAASA,gBAAgB,QAAQ,4BAA2B;AAE5D,OAAO,SAASC,sBAAsBC,MAAuC;IAC3E,OAAO;QACLC,SAAS,OAAOC;YACd,MAAMC,OAAQ,MAAMD,IAAIE,IAAI;YAE5B,sEAAsE;YACtE,yBAAyB;YACzB,OAAOD,KAAKE,iBAAiB;YAE7B,+DAA+D;YAC/D,sEAAsE;YACtE,gEAAgE;YAChE,IAAI,CAACP,iBAAiBI,IAAII,IAAI,EAAEN,SAAS;gBACvC,IAAIE,IAAII,IAAI,EAAE;oBACZH,KAAKI,QAAQ,GAAGL,IAAII,IAAI,CAACE,EAAE;gBAC7B,OAAO,IAAIL,KAAKI,QAAQ,EAAE;oBACxB,OAAOE,SAASL,IAAI,CAClB;wBAAEM,OAAO;oBAA2C,GACpD;wBAAEC,QAAQ;oBAAI;gBAElB;YACF;YAEA,6EAA6E;YAC7E,uEAAuE;YACvE,yEAAyE;YACzE,8DAA8D;YAC9D,MAAMC,cAAc,MAAM,AAACV,IAAIW,OAAO,CAACC,MAAM,CAAS;gBACpDC,YAAYf,OAAOgB,KAAK,CAACC,YAAY;gBACrCd;gBACAD;YACF;YAEA,6EAA6E;YAC7E,oEAAoE;YACpE,MAAM,EAAEG,mBAAmBa,kBAAkB,EAAE,GAAGC,iBAAiB,GACjEP;YAEF,OAAOH,SAASL,IAAI,CAACe,iBAAiB;gBAAER,QAAQ;YAAI;QACtD;QACAS,QAAQ;QACRC,MAAM;IACR;AACF"}