payload-reserve 1.6.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -3
- package/dist/collections/Reservations.js +19 -7
- package/dist/collections/Reservations.js.map +1 -1
- package/dist/collections/Resources.js +11 -8
- package/dist/collections/Resources.js.map +1 -1
- package/dist/collections/Schedules.js +12 -6
- package/dist/collections/Schedules.js.map +1 -1
- package/dist/collections/Services.js +19 -10
- package/dist/collections/Services.js.map +1 -1
- package/dist/components/AvailabilityOverview/index.js +43 -18
- package/dist/components/AvailabilityOverview/index.js.map +1 -1
- package/dist/components/CalendarView/CalendarView.module.css +9 -0
- package/dist/components/CalendarView/LaneTimelineView.d.ts +4 -1
- package/dist/components/CalendarView/LaneTimelineView.js +17 -12
- package/dist/components/CalendarView/LaneTimelineView.js.map +1 -1
- package/dist/components/CalendarView/index.js +134 -44
- package/dist/components/CalendarView/index.js.map +1 -1
- package/dist/components/CustomerField/index.js +8 -3
- package/dist/components/CustomerField/index.js.map +1 -1
- package/dist/components/DashboardWidget/DashboardWidgetServer.js +79 -17
- package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
- package/dist/defaults.js +42 -8
- package/dist/defaults.js.map +1 -1
- package/dist/endpoints/cancelBooking.js +1 -1
- package/dist/endpoints/cancelBooking.js.map +1 -1
- package/dist/endpoints/checkAvailability.js +56 -7
- package/dist/endpoints/checkAvailability.js.map +1 -1
- package/dist/endpoints/createBooking.js +19 -10
- package/dist/endpoints/createBooking.js.map +1 -1
- package/dist/endpoints/customerSearch.js +5 -2
- package/dist/endpoints/customerSearch.js.map +1 -1
- package/dist/endpoints/getSlots.js +56 -7
- package/dist/endpoints/getSlots.js.map +1 -1
- package/dist/endpoints/resourceAvailability.d.ts +2 -1
- package/dist/endpoints/resourceAvailability.js +85 -25
- package/dist/endpoints/resourceAvailability.js.map +1 -1
- package/dist/hooks/reservations/calculateEndTime.js +48 -20
- package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
- package/dist/hooks/reservations/enforceCustomerOwnership.d.ts +11 -0
- package/dist/hooks/reservations/enforceCustomerOwnership.js +30 -0
- package/dist/hooks/reservations/enforceCustomerOwnership.js.map +1 -0
- package/dist/hooks/reservations/onStatusChange.js +10 -4
- package/dist/hooks/reservations/onStatusChange.js.map +1 -1
- package/dist/hooks/reservations/validateCancellation.js +3 -2
- package/dist/hooks/reservations/validateCancellation.js.map +1 -1
- package/dist/hooks/reservations/validateConflicts.js +23 -4
- package/dist/hooks/reservations/validateConflicts.js.map +1 -1
- package/dist/hooks/reservations/validateGuestBooking.js +3 -4
- package/dist/hooks/reservations/validateGuestBooking.js.map +1 -1
- package/dist/hooks/reservations/validateStatusTransition.js +2 -2
- package/dist/hooks/reservations/validateStatusTransition.js.map +1 -1
- package/dist/hooks/users/provisionStaffResource.js +5 -8
- package/dist/hooks/users/provisionStaffResource.js.map +1 -1
- package/dist/plugin.js +81 -13
- package/dist/plugin.js.map +1 -1
- package/dist/services/AvailabilityService.d.ts +54 -2
- package/dist/services/AvailabilityService.js +180 -46
- package/dist/services/AvailabilityService.js.map +1 -1
- package/dist/translations/ar.json +1 -0
- package/dist/translations/de.json +1 -0
- package/dist/translations/en.json +1 -0
- package/dist/translations/es.json +1 -0
- package/dist/translations/fa.json +1 -0
- package/dist/translations/fr.json +1 -0
- package/dist/translations/hi.json +1 -0
- package/dist/translations/id.json +1 -0
- package/dist/translations/pl.json +1 -0
- package/dist/translations/ru.json +1 -0
- package/dist/translations/tr.json +1 -0
- package/dist/translations/zh.json +1 -0
- package/dist/types.d.ts +39 -1
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/dist/utilities/collectionOverrides.d.ts +14 -0
- package/dist/utilities/collectionOverrides.js +47 -0
- package/dist/utilities/collectionOverrides.js.map +1 -0
- package/dist/utilities/ownerAccess.d.ts +6 -0
- package/dist/utilities/ownerAccess.js +25 -12
- package/dist/utilities/ownerAccess.js.map +1 -1
- package/dist/utilities/reservationChanges.d.ts +17 -0
- package/dist/utilities/reservationChanges.js +88 -0
- package/dist/utilities/reservationChanges.js.map +1 -0
- package/dist/utilities/scheduleUtils.d.ts +14 -8
- package/dist/utilities/scheduleUtils.js +26 -19
- package/dist/utilities/scheduleUtils.js.map +1 -1
- package/dist/utilities/timezoneUtils.d.ts +39 -0
- package/dist/utilities/timezoneUtils.js +134 -0
- package/dist/utilities/timezoneUtils.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
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 { addDaysToDayKey, combineDayKeyAndTime, getDayKeyInTimezone } from '../../utilities/timezoneUtils.js';
|
|
3
4
|
import styles from './DashboardWidget.module.css';
|
|
4
5
|
export const DashboardWidgetServer = async (props)=>{
|
|
5
6
|
const { req } = props;
|
|
@@ -24,9 +25,12 @@ export const DashboardWidgetServer = async (props)=>{
|
|
|
24
25
|
const terminalStatuses = statusMachine?.terminalStatuses ?? [];
|
|
25
26
|
const blockingSet = new Set(blockingStatuses);
|
|
26
27
|
const terminalSet = new Set(terminalStatuses);
|
|
28
|
+
// "Today" is the business timezone's calendar day, not the server's
|
|
29
|
+
const reservationTimezone = payload.config.admin?.custom?.reservationTimezone ?? 'UTC';
|
|
27
30
|
const now = new Date();
|
|
28
|
-
const
|
|
29
|
-
const
|
|
31
|
+
const todayKey = getDayKeyInTimezone(now, reservationTimezone);
|
|
32
|
+
const startOfDay = combineDayKeyAndTime(todayKey, '00:00', reservationTimezone);
|
|
33
|
+
const endOfDay = combineDayKeyAndTime(addDaysToDayKey(todayKey, 1), '00:00', reservationTimezone);
|
|
30
34
|
const where = {
|
|
31
35
|
startTime: {
|
|
32
36
|
greater_than_equal: startOfDay.toISOString(),
|
|
@@ -36,21 +40,78 @@ export const DashboardWidgetServer = async (props)=>{
|
|
|
36
40
|
if (tenantWhere) {
|
|
37
41
|
Object.assign(where, tenantWhere);
|
|
38
42
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
// Stats are computed with count queries rather than a capped fetch+filter,
|
|
44
|
+
// so they stay accurate past 100 reservations/day (review D7).
|
|
45
|
+
const blocking = Array.from(blockingSet);
|
|
46
|
+
const terminalArr = Array.from(terminalSet);
|
|
47
|
+
const countWhere = (extra)=>extra ? {
|
|
48
|
+
and: [
|
|
49
|
+
where,
|
|
50
|
+
extra
|
|
51
|
+
]
|
|
52
|
+
} : where;
|
|
53
|
+
const [total, active, terminal, upcoming, nextResult] = await Promise.all([
|
|
54
|
+
payload.count({
|
|
55
|
+
collection: slugs.reservations,
|
|
56
|
+
where
|
|
57
|
+
}).then((r)=>r.totalDocs),
|
|
58
|
+
blocking.length ? payload.count({
|
|
59
|
+
collection: slugs.reservations,
|
|
60
|
+
where: countWhere({
|
|
61
|
+
status: {
|
|
62
|
+
in: blocking
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
}).then((r)=>r.totalDocs) : Promise.resolve(0),
|
|
66
|
+
terminalArr.length ? payload.count({
|
|
67
|
+
collection: slugs.reservations,
|
|
68
|
+
where: countWhere({
|
|
69
|
+
status: {
|
|
70
|
+
in: terminalArr
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
}).then((r)=>r.totalDocs) : Promise.resolve(0),
|
|
74
|
+
blocking.length ? payload.count({
|
|
75
|
+
collection: slugs.reservations,
|
|
76
|
+
where: countWhere({
|
|
77
|
+
and: [
|
|
78
|
+
{
|
|
79
|
+
status: {
|
|
80
|
+
in: blocking
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
startTime: {
|
|
85
|
+
greater_than: now.toISOString()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
})
|
|
90
|
+
}).then((r)=>r.totalDocs) : Promise.resolve(0),
|
|
91
|
+
blocking.length ? payload.find({
|
|
92
|
+
collection: slugs.reservations,
|
|
93
|
+
limit: 1,
|
|
94
|
+
sort: 'startTime',
|
|
95
|
+
where: countWhere({
|
|
96
|
+
and: [
|
|
97
|
+
{
|
|
98
|
+
status: {
|
|
99
|
+
in: blocking
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
startTime: {
|
|
104
|
+
greater_than: now.toISOString()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
})
|
|
109
|
+
}) : Promise.resolve({
|
|
110
|
+
docs: []
|
|
111
|
+
})
|
|
112
|
+
]);
|
|
52
113
|
// Next appointment = the earliest upcoming blocking reservation
|
|
53
|
-
const nextAppointment =
|
|
114
|
+
const nextAppointment = nextResult.docs[0];
|
|
54
115
|
return /*#__PURE__*/ _jsxs("div", {
|
|
55
116
|
className: styles.wrapper,
|
|
56
117
|
children: [
|
|
@@ -127,7 +188,8 @@ export const DashboardWidgetServer = async (props)=>{
|
|
|
127
188
|
' ',
|
|
128
189
|
new Date(nextAppointment.startTime).toLocaleTimeString([], {
|
|
129
190
|
hour: '2-digit',
|
|
130
|
-
minute: '2-digit'
|
|
191
|
+
minute: '2-digit',
|
|
192
|
+
timeZone: reservationTimezone
|
|
131
193
|
})
|
|
132
194
|
]
|
|
133
195
|
}),
|
|
@@ -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 {\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 }\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 // \"Today\" is the business timezone's calendar day, not the server's\n const reservationTimezone: string =\n payload.config.admin?.custom?.reservationTimezone ?? 'UTC'\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","addDaysToDayKey","combineDayKeyAndTime","getDayKeyInTimezone","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","reservationTimezone","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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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,9 +115,12 @@ 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,
|
|
@@ -105,7 +132,8 @@ export function resolveConfig(pluginOptions) {
|
|
|
105
132
|
adminRoles: rom.adminRoles ?? [],
|
|
106
133
|
ownedServices: rom.ownedServices ?? false,
|
|
107
134
|
ownerCollection: rom.ownerCollection,
|
|
108
|
-
ownerField: rom.ownerField ?? 'owner'
|
|
135
|
+
ownerField: rom.ownerField ?? 'owner',
|
|
136
|
+
roleField: rom.roleField ?? pluginOptions.staffProvisioning?.roleField ?? 'role'
|
|
109
137
|
} : undefined,
|
|
110
138
|
resourceTypes,
|
|
111
139
|
slugs: {
|
|
@@ -116,9 +144,11 @@ export function resolveConfig(pluginOptions) {
|
|
|
116
144
|
schedules: pluginOptions.slugs?.schedules ?? DEFAULT_SLUGS.schedules,
|
|
117
145
|
services: pluginOptions.slugs?.services ?? DEFAULT_SLUGS.services
|
|
118
146
|
},
|
|
119
|
-
staffProvisioning: resolveStaffProvisioning(pluginOptions, resourceTypes),
|
|
147
|
+
staffProvisioning: disabled ? undefined : resolveStaffProvisioning(pluginOptions, resourceTypes),
|
|
120
148
|
statusMachine: userStatusMachine ? {
|
|
121
149
|
blockingStatuses: userStatusMachine.blockingStatuses ?? DEFAULT_STATUS_MACHINE.blockingStatuses,
|
|
150
|
+
cancelStatus: userStatusMachine.cancelStatus ?? DEFAULT_STATUS_MACHINE.cancelStatus,
|
|
151
|
+
confirmStatus: userStatusMachine.confirmStatus ?? DEFAULT_STATUS_MACHINE.confirmStatus,
|
|
122
152
|
defaultStatus: userStatusMachine.defaultStatus ?? DEFAULT_STATUS_MACHINE.defaultStatus,
|
|
123
153
|
statuses: userStatusMachine.statuses ?? DEFAULT_STATUS_MACHINE.statuses,
|
|
124
154
|
terminalStatuses: userStatusMachine.terminalStatuses ?? DEFAULT_STATUS_MACHINE.terminalStatuses,
|
|
@@ -126,9 +156,13 @@ export function resolveConfig(pluginOptions) {
|
|
|
126
156
|
} : {
|
|
127
157
|
...DEFAULT_STATUS_MACHINE
|
|
128
158
|
},
|
|
159
|
+
timezone: pluginOptions.timezone ?? 'UTC',
|
|
129
160
|
userCollection: pluginOptions.userCollection ?? undefined
|
|
130
161
|
};
|
|
131
|
-
|
|
162
|
+
if (!disabled) {
|
|
163
|
+
validateStatusMachine(resolved.statusMachine);
|
|
164
|
+
validateTimezone(resolved.timezone);
|
|
165
|
+
}
|
|
132
166
|
return resolved;
|
|
133
167
|
}
|
|
134
168
|
|
package/dist/defaults.js.map
CHANGED
|
@@ -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 },\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","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:
|
|
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:
|
|
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
|
-
|
|
18
|
-
|
|
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
|
|
45
|
+
error: 'Invalid guestCount'
|
|
21
46
|
}, {
|
|
22
47
|
status: 400
|
|
23
48
|
});
|
|
24
49
|
}
|
|
25
|
-
const guestCount = Math.max(
|
|
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:
|
|
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
|
|
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
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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,
|
|
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
|
|
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 //
|
|
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"}
|
|
@@ -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
|
|
37
|
-
const
|
|
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();
|