payload-reserve 2.0.0 → 2.1.1
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 +30 -2
- package/dist/collections/Reservations.js +3 -2
- package/dist/collections/Reservations.js.map +1 -1
- package/dist/components/AvailabilityOverview/index.js +34 -1
- package/dist/components/AvailabilityOverview/index.js.map +1 -1
- package/dist/components/CalendarView/index.js +33 -1
- package/dist/components/CalendarView/index.js.map +1 -1
- package/dist/components/DashboardWidget/DashboardWidgetServer.js +14 -3
- package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
- package/dist/defaults.js +2 -1
- package/dist/defaults.js.map +1 -1
- package/dist/endpoints/customerSearch.js +12 -0
- package/dist/endpoints/customerSearch.js.map +1 -1
- package/dist/endpoints/effectiveTimezone.d.ts +13 -0
- package/dist/endpoints/effectiveTimezone.js +41 -0
- package/dist/endpoints/effectiveTimezone.js.map +1 -0
- package/dist/endpoints/resourceAvailability.d.ts +2 -0
- package/dist/endpoints/resourceAvailability.js +18 -2
- package/dist/endpoints/resourceAvailability.js.map +1 -1
- package/dist/plugin.js +2 -1
- package/dist/plugin.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.js.map +1 -1
- package/dist/utilities/tenantTimezone.d.ts +41 -0
- package/dist/utilities/tenantTimezone.js +77 -0
- package/dist/utilities/tenantTimezone.js.map +1 -0
- package/dist/utilities/timezoneUtils.d.ts +5 -0
- package/dist/utilities/timezoneUtils.js +14 -2
- package/dist/utilities/timezoneUtils.js.map +1 -1
- package/package.json +1 -1
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'\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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"}
|
|
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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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectionHasTenantField, readCookie, tenantWhereClause } from '../utilities/tenantFilter.js';
|
|
1
2
|
import { isPrivilegedUser, privilegedRoles } from '../utilities/userRoles.js';
|
|
2
3
|
/**
|
|
3
4
|
* Inspect a collection's field list and return the set of top-level named
|
|
@@ -100,6 +101,17 @@ export function createCustomerSearchEndpoint(config) {
|
|
|
100
101
|
});
|
|
101
102
|
}
|
|
102
103
|
}
|
|
104
|
+
// Tenant scoping: when the customers collection carries the multi-tenant
|
|
105
|
+
// tenant field and a tenant is selected (cookie), restrict the search to
|
|
106
|
+
// that tenant. Plain installs (no tenant field / no cookie) add nothing.
|
|
107
|
+
const tenantClause = tenantWhereClause({
|
|
108
|
+
hasField: collectionHasTenantField(collectionConfig, config.multiTenant.tenantField),
|
|
109
|
+
tenantField: config.multiTenant.tenantField,
|
|
110
|
+
tenantId: readCookie(req.headers?.get('cookie'), config.multiTenant.cookieName)
|
|
111
|
+
});
|
|
112
|
+
if (tenantClause) {
|
|
113
|
+
andClauses.push(tenantClause);
|
|
114
|
+
}
|
|
103
115
|
const where = andClauses.length === 0 ? {} : andClauses.length === 1 ? andClauses[0] : {
|
|
104
116
|
and: andClauses
|
|
105
117
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/endpoints/customerSearch.ts"],"sourcesContent":["import type { CollectionSlug, Endpoint, Field, Where } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { isPrivilegedUser, privilegedRoles } from '../utilities/userRoles.js'\n\n/**\n * Inspect a collection's field list and return the set of top-level named\n * fields as a plain Set<string>. Unnamed fields (rows, groups without a name,\n * etc.) are skipped.\n */\nfunction getNamedFields(fields: Field[]): Set<string> {\n const names = new Set<string>()\n for (const field of fields) {\n if ('name' in field) {\n names.add(field.name)\n }\n }\n return names\n}\n\nexport function createCustomerSearchEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n if (!req.user) {\n return Response.json({ message: 'Unauthorized' }, { status: 401 })\n }\n\n // Only staff/admin may search customers. Role-aware so it works when staff\n // and customers share one auth collection (userCollection set).\n if (!isPrivilegedUser(req.user, config)) {\n return Response.json({ message: 'Forbidden' }, { status: 403 })\n }\n\n const url = new URL(req.url!)\n const search = url.searchParams.get('search') ?? ''\n const limitRaw = Number(url.searchParams.get('limit') ?? '10')\n const pageRaw = Number(url.searchParams.get('page') ?? '1')\n // Non-numeric input falls back to defaults instead of passing NaN to the DB\n const limit = Number.isFinite(limitRaw) ? Math.min(Math.max(Math.floor(limitRaw), 1), 50) : 10\n const page = Number.isFinite(pageRaw) ? Math.max(Math.floor(pageRaw), 1) : 1\n\n // Detect which fields exist on the target collection at runtime\n const collectionConfig = req.payload.collections[config.slugs.customers as unknown as CollectionSlug]?.config\n const availableFields: Set<string> = collectionConfig\n ? getNamedFields(collectionConfig.fields)\n : new Set()\n\n const hasName = availableFields.has('name')\n const hasFirstName = availableFields.has('firstName')\n const hasLastName = availableFields.has('lastName')\n const hasPhone = availableFields.has('phone')\n\n const andClauses: Where[] = []\n\n if (search) {\n const orClauses: Where[] = []\n\n if (hasName) {\n orClauses.push({ name: { contains: search } })\n }\n if (hasFirstName) {\n orClauses.push({ firstName: { contains: search } })\n }\n if (hasLastName) {\n orClauses.push({ lastName: { contains: search } })\n }\n // email is always present on auth collections\n orClauses.push({ email: { contains: search } })\n if (hasPhone) {\n orClauses.push({ phone: { contains: search } })\n }\n\n andClauses.push({ or: orClauses })\n }\n\n // Single-collection mode: staff/admin live in the same collection as\n // customers, so exclude privileged roles — the dropdown should list only\n // actual customers, not bookable-looking staff.\n if (config.userCollection) {\n const roleField = config.staffProvisioning?.roleField ?? 'role'\n const priv = privilegedRoles(config)\n if (priv.length > 0) {\n andClauses.push({ [roleField]: { not_in: priv } })\n }\n }\n\n const where: Where =\n andClauses.length === 0\n ? {}\n : andClauses.length === 1\n ? andClauses[0]\n : { and: andClauses }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const result = await (req.payload.find as any)({\n collection: config.slugs.customers,\n limit,\n page,\n where,\n })\n\n return Response.json({\n docs: (result.docs as Record<string, unknown>[]).map((doc) => {\n const entry: Record<string, unknown> = {\n id: doc['id'],\n email: doc['email'] ?? '',\n }\n\n if (hasName) {\n entry['name'] = doc['name'] ?? ''\n }\n if (hasFirstName) {\n entry['firstName'] = doc['firstName'] ?? ''\n }\n if (hasLastName) {\n entry['lastName'] = doc['lastName'] ?? ''\n }\n if (hasPhone) {\n entry['phone'] = doc['phone'] ?? ''\n }\n\n return entry\n }),\n hasNextPage: result.hasNextPage,\n totalDocs: result.totalDocs,\n })\n },\n method: 'get',\n path: '/reservation-customer-search',\n }\n}\n"],"names":["isPrivilegedUser","privilegedRoles","getNamedFields","fields","names","Set","field","add","name","createCustomerSearchEndpoint","config","handler","req","user","Response","json","message","status","url","URL","search","searchParams","get","limitRaw","Number","pageRaw","limit","isFinite","Math","min","max","floor","page","collectionConfig","payload","collections","slugs","customers","availableFields","hasName","has","hasFirstName","hasLastName","hasPhone","andClauses","orClauses","push","contains","firstName","lastName","email","phone","or","userCollection","roleField","staffProvisioning","priv","length","not_in","where","and","result","find","collection","docs","map","doc","entry","id","hasNextPage","totalDocs","method","path"],"mappings":"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
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/customerSearch.ts"],"sourcesContent":["import type { CollectionSlug, Endpoint, Field, Where } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { collectionHasTenantField, readCookie, tenantWhereClause } from '../utilities/tenantFilter.js'\nimport { isPrivilegedUser, privilegedRoles } from '../utilities/userRoles.js'\n\n/**\n * Inspect a collection's field list and return the set of top-level named\n * fields as a plain Set<string>. Unnamed fields (rows, groups without a name,\n * etc.) are skipped.\n */\nfunction getNamedFields(fields: Field[]): Set<string> {\n const names = new Set<string>()\n for (const field of fields) {\n if ('name' in field) {\n names.add(field.name)\n }\n }\n return names\n}\n\nexport function createCustomerSearchEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n if (!req.user) {\n return Response.json({ message: 'Unauthorized' }, { status: 401 })\n }\n\n // Only staff/admin may search customers. Role-aware so it works when staff\n // and customers share one auth collection (userCollection set).\n if (!isPrivilegedUser(req.user, config)) {\n return Response.json({ message: 'Forbidden' }, { status: 403 })\n }\n\n const url = new URL(req.url!)\n const search = url.searchParams.get('search') ?? ''\n const limitRaw = Number(url.searchParams.get('limit') ?? '10')\n const pageRaw = Number(url.searchParams.get('page') ?? '1')\n // Non-numeric input falls back to defaults instead of passing NaN to the DB\n const limit = Number.isFinite(limitRaw) ? Math.min(Math.max(Math.floor(limitRaw), 1), 50) : 10\n const page = Number.isFinite(pageRaw) ? Math.max(Math.floor(pageRaw), 1) : 1\n\n // Detect which fields exist on the target collection at runtime\n const collectionConfig = req.payload.collections[config.slugs.customers as unknown as CollectionSlug]?.config\n const availableFields: Set<string> = collectionConfig\n ? getNamedFields(collectionConfig.fields)\n : new Set()\n\n const hasName = availableFields.has('name')\n const hasFirstName = availableFields.has('firstName')\n const hasLastName = availableFields.has('lastName')\n const hasPhone = availableFields.has('phone')\n\n const andClauses: Where[] = []\n\n if (search) {\n const orClauses: Where[] = []\n\n if (hasName) {\n orClauses.push({ name: { contains: search } })\n }\n if (hasFirstName) {\n orClauses.push({ firstName: { contains: search } })\n }\n if (hasLastName) {\n orClauses.push({ lastName: { contains: search } })\n }\n // email is always present on auth collections\n orClauses.push({ email: { contains: search } })\n if (hasPhone) {\n orClauses.push({ phone: { contains: search } })\n }\n\n andClauses.push({ or: orClauses })\n }\n\n // Single-collection mode: staff/admin live in the same collection as\n // customers, so exclude privileged roles — the dropdown should list only\n // actual customers, not bookable-looking staff.\n if (config.userCollection) {\n const roleField = config.staffProvisioning?.roleField ?? 'role'\n const priv = privilegedRoles(config)\n if (priv.length > 0) {\n andClauses.push({ [roleField]: { not_in: priv } })\n }\n }\n\n // Tenant scoping: when the customers collection carries the multi-tenant\n // tenant field and a tenant is selected (cookie), restrict the search to\n // that tenant. Plain installs (no tenant field / no cookie) add nothing.\n const tenantClause = tenantWhereClause({\n hasField: collectionHasTenantField(collectionConfig, config.multiTenant.tenantField),\n tenantField: config.multiTenant.tenantField,\n tenantId: readCookie(req.headers?.get('cookie'), config.multiTenant.cookieName),\n })\n if (tenantClause) {\n andClauses.push(tenantClause)\n }\n\n const where: Where =\n andClauses.length === 0\n ? {}\n : andClauses.length === 1\n ? andClauses[0]\n : { and: andClauses }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const result = await (req.payload.find as any)({\n collection: config.slugs.customers,\n limit,\n page,\n where,\n })\n\n return Response.json({\n docs: (result.docs as Record<string, unknown>[]).map((doc) => {\n const entry: Record<string, unknown> = {\n id: doc['id'],\n email: doc['email'] ?? '',\n }\n\n if (hasName) {\n entry['name'] = doc['name'] ?? ''\n }\n if (hasFirstName) {\n entry['firstName'] = doc['firstName'] ?? ''\n }\n if (hasLastName) {\n entry['lastName'] = doc['lastName'] ?? ''\n }\n if (hasPhone) {\n entry['phone'] = doc['phone'] ?? ''\n }\n\n return entry\n }),\n hasNextPage: result.hasNextPage,\n totalDocs: result.totalDocs,\n })\n },\n method: 'get',\n path: '/reservation-customer-search',\n }\n}\n"],"names":["collectionHasTenantField","readCookie","tenantWhereClause","isPrivilegedUser","privilegedRoles","getNamedFields","fields","names","Set","field","add","name","createCustomerSearchEndpoint","config","handler","req","user","Response","json","message","status","url","URL","search","searchParams","get","limitRaw","Number","pageRaw","limit","isFinite","Math","min","max","floor","page","collectionConfig","payload","collections","slugs","customers","availableFields","hasName","has","hasFirstName","hasLastName","hasPhone","andClauses","orClauses","push","contains","firstName","lastName","email","phone","or","userCollection","roleField","staffProvisioning","priv","length","not_in","tenantClause","hasField","multiTenant","tenantField","tenantId","headers","cookieName","where","and","result","find","collection","docs","map","doc","entry","id","hasNextPage","totalDocs","method","path"],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Endpoint } from 'payload';
|
|
2
|
+
import type { ResolvedReservationPluginConfig } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the effective business timezone for the current request's selected
|
|
5
|
+
* tenant (tenant zone → global → UTC), read from the tenant cookie. The custom
|
|
6
|
+
* admin calendar calls this so its day-boundary rendering matches the selected
|
|
7
|
+
* tenant — the client can't resolve it itself because the tenant→timezone map
|
|
8
|
+
* lives server-side and the tenant collection slug is only known at request time.
|
|
9
|
+
*
|
|
10
|
+
* Plain (non-multiTenant) installs always get `config.timezone` here, with no
|
|
11
|
+
* tenant DB read.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createEffectiveTimezoneEndpoint(config: ResolvedReservationPluginConfig): Endpoint;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readCookie } from '../utilities/tenantFilter.js';
|
|
2
|
+
import { getEffectiveTenantTimezone } from '../utilities/tenantTimezone.js';
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the effective business timezone for the current request's selected
|
|
5
|
+
* tenant (tenant zone → global → UTC), read from the tenant cookie. The custom
|
|
6
|
+
* admin calendar calls this so its day-boundary rendering matches the selected
|
|
7
|
+
* tenant — the client can't resolve it itself because the tenant→timezone map
|
|
8
|
+
* lives server-side and the tenant collection slug is only known at request time.
|
|
9
|
+
*
|
|
10
|
+
* Plain (non-multiTenant) installs always get `config.timezone` here, with no
|
|
11
|
+
* tenant DB read.
|
|
12
|
+
*/ export function createEffectiveTimezoneEndpoint(config) {
|
|
13
|
+
return {
|
|
14
|
+
handler: async (req)=>{
|
|
15
|
+
// Staff/admin-only view data — require an authenticated user.
|
|
16
|
+
if (!req.user) {
|
|
17
|
+
return Response.json({
|
|
18
|
+
error: 'Unauthorized'
|
|
19
|
+
}, {
|
|
20
|
+
status: 401
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const reservationsCollection = req.payload.config.collections?.find((c)=>c.slug === config.slugs.reservations);
|
|
24
|
+
const timeZone = await getEffectiveTenantTimezone({
|
|
25
|
+
globalTimezone: config.timezone,
|
|
26
|
+
payload: req.payload,
|
|
27
|
+
scopedCollection: reservationsCollection,
|
|
28
|
+
tenantField: config.multiTenant.tenantField,
|
|
29
|
+
tenantId: readCookie(req.headers?.get('cookie'), config.multiTenant.cookieName),
|
|
30
|
+
timezoneField: config.multiTenant.timezoneField
|
|
31
|
+
});
|
|
32
|
+
return Response.json({
|
|
33
|
+
timeZone
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
method: 'get',
|
|
37
|
+
path: '/reserve/effective-timezone'
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
//# sourceMappingURL=effectiveTimezone.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/effectiveTimezone.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { readCookie } from '../utilities/tenantFilter.js'\nimport { getEffectiveTenantTimezone } from '../utilities/tenantTimezone.js'\n\n/**\n * Resolve the effective business timezone for the current request's selected\n * tenant (tenant zone → global → UTC), read from the tenant cookie. The custom\n * admin calendar calls this so its day-boundary rendering matches the selected\n * tenant — the client can't resolve it itself because the tenant→timezone map\n * lives server-side and the tenant collection slug is only known at request time.\n *\n * Plain (non-multiTenant) installs always get `config.timezone` here, with no\n * tenant DB read.\n */\nexport function createEffectiveTimezoneEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n // Staff/admin-only view data — require an authenticated user.\n if (!req.user) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const reservationsCollection = req.payload.config.collections?.find(\n (c) => c.slug === config.slugs.reservations,\n )\n const timeZone = await getEffectiveTenantTimezone({\n globalTimezone: config.timezone,\n payload: req.payload,\n scopedCollection: reservationsCollection as { fields?: unknown[] } | undefined,\n tenantField: config.multiTenant.tenantField,\n tenantId: readCookie(req.headers?.get('cookie'), config.multiTenant.cookieName),\n timezoneField: config.multiTenant.timezoneField,\n })\n\n return Response.json({ timeZone })\n },\n method: 'get',\n path: '/reserve/effective-timezone',\n }\n}\n"],"names":["readCookie","getEffectiveTenantTimezone","createEffectiveTimezoneEndpoint","config","handler","req","user","Response","json","error","status","reservationsCollection","payload","collections","find","c","slug","slugs","reservations","timeZone","globalTimezone","timezone","scopedCollection","tenantField","multiTenant","tenantId","headers","get","cookieName","timezoneField","method","path"],"mappings":"AAIA,SAASA,UAAU,QAAQ,+BAA8B;AACzD,SAASC,0BAA0B,QAAQ,iCAAgC;AAE3E;;;;;;;;;CASC,GACD,OAAO,SAASC,gCACdC,MAAuC;IAEvC,OAAO;QACLC,SAAS,OAAOC;YACd,8DAA8D;YAC9D,IAAI,CAACA,IAAIC,IAAI,EAAE;gBACb,OAAOC,SAASC,IAAI,CAAC;oBAAEC,OAAO;gBAAe,GAAG;oBAAEC,QAAQ;gBAAI;YAChE;YAEA,MAAMC,yBAAyBN,IAAIO,OAAO,CAACT,MAAM,CAACU,WAAW,EAAEC,KAC7D,CAACC,IAAMA,EAAEC,IAAI,KAAKb,OAAOc,KAAK,CAACC,YAAY;YAE7C,MAAMC,WAAW,MAAMlB,2BAA2B;gBAChDmB,gBAAgBjB,OAAOkB,QAAQ;gBAC/BT,SAASP,IAAIO,OAAO;gBACpBU,kBAAkBX;gBAClBY,aAAapB,OAAOqB,WAAW,CAACD,WAAW;gBAC3CE,UAAUzB,WAAWK,IAAIqB,OAAO,EAAEC,IAAI,WAAWxB,OAAOqB,WAAW,CAACI,UAAU;gBAC9EC,eAAe1B,OAAOqB,WAAW,CAACK,aAAa;YACjD;YAEA,OAAOtB,SAASC,IAAI,CAAC;gBAAEW;YAAS;QAClC;QACAW,QAAQ;QACRC,MAAM;IACR;AACF"}
|
|
@@ -28,6 +28,8 @@ export type ResourceAvailability = {
|
|
|
28
28
|
busy: Busy;
|
|
29
29
|
quantity: number;
|
|
30
30
|
}>;
|
|
31
|
+
/** IANA zone the day windows were resolved in (selected tenant's zone, else global). */
|
|
32
|
+
timeZone: string;
|
|
31
33
|
};
|
|
32
34
|
export declare function buildResourceAvailability(params: {
|
|
33
35
|
blockingStatuses: string[];
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { resolveScheduleForDate } from '../utilities/scheduleUtils.js';
|
|
2
|
+
import { readCookie } from '../utilities/tenantFilter.js';
|
|
3
|
+
import { getEffectiveTenantTimezone } from '../utilities/tenantTimezone.js';
|
|
2
4
|
import { addDaysToDayKey, combineDayKeyAndTime, getDayKeyInTimezone } from '../utilities/timezoneUtils.js';
|
|
3
5
|
import { isPrivilegedUser } from '../utilities/userRoles.js';
|
|
4
6
|
const MAX_RANGE_MS = 90 * 86_400_000;
|
|
@@ -189,7 +191,8 @@ export async function buildResourceAvailability(params) {
|
|
|
189
191
|
capacityMode,
|
|
190
192
|
days,
|
|
191
193
|
quantity,
|
|
192
|
-
requiredPools
|
|
194
|
+
requiredPools,
|
|
195
|
+
timeZone
|
|
193
196
|
};
|
|
194
197
|
}
|
|
195
198
|
export function createResourceAvailabilityEndpoint(config) {
|
|
@@ -246,6 +249,19 @@ export function createResourceAvailabilityEndpoint(config) {
|
|
|
246
249
|
status: 400
|
|
247
250
|
});
|
|
248
251
|
}
|
|
252
|
+
// In multiTenant mode, resolve day-boundaries in the SELECTED tenant's zone
|
|
253
|
+
// (tenant timezone → global → UTC). Degrades to the global zone for plain
|
|
254
|
+
// installs: no tenant relationship on reservations / no tenant cookie ⇒ no
|
|
255
|
+
// DB read, same output as before.
|
|
256
|
+
const reservationsCollection = req.payload.config.collections?.find((c)=>c.slug === config.slugs.reservations);
|
|
257
|
+
const timeZone = await getEffectiveTenantTimezone({
|
|
258
|
+
globalTimezone: config.timezone,
|
|
259
|
+
payload: req.payload,
|
|
260
|
+
scopedCollection: reservationsCollection,
|
|
261
|
+
tenantField: config.multiTenant.tenantField,
|
|
262
|
+
tenantId: readCookie(req.headers?.get('cookie'), config.multiTenant.cookieName),
|
|
263
|
+
timezoneField: config.multiTenant.timezoneField
|
|
264
|
+
});
|
|
249
265
|
const result = await buildResourceAvailability({
|
|
250
266
|
blockingStatuses: config.statusMachine.blockingStatuses,
|
|
251
267
|
end: endDate,
|
|
@@ -255,7 +271,7 @@ export function createResourceAvailabilityEndpoint(config) {
|
|
|
255
271
|
resourceSlug: config.slugs.resources,
|
|
256
272
|
scheduleSlug: config.slugs.schedules,
|
|
257
273
|
start: startDate,
|
|
258
|
-
timeZone
|
|
274
|
+
timeZone
|
|
259
275
|
});
|
|
260
276
|
if (!result) {
|
|
261
277
|
return Response.json({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/endpoints/resourceAvailability.ts"],"sourcesContent":["import type { Endpoint, Payload, Where } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { resolveScheduleForDate } from '../utilities/scheduleUtils.js'\nimport {\n addDaysToDayKey,\n combineDayKeyAndTime,\n getDayKeyInTimezone,\n} from '../utilities/timezoneUtils.js'\nimport { isPrivilegedUser } from '../utilities/userRoles.js'\n\nconst MAX_RANGE_MS = 90 * 86_400_000\n\ntype DayAvailability = {\n date: string\n shiftWindows: Array<{ end: string; start: string }>\n timeOff: Array<{ end: string; reason?: string; start: string; type?: string }>\n}\n\ntype Busy = Array<{ end: string; start: string; units: number }>\n\nexport type ResourceAvailability = {\n busy: Busy\n capacityMode: 'per-guest' | 'per-reservation'\n days: DayAvailability[]\n quantity: number\n /** Capacity of resources this resource's services also require (e.g. a chair pool). */\n requiredPools: Array<{ busy: Busy; quantity: number }>\n}\n\n/** Busy intervals (with capacity units) for one resource over [start, end). */\nasync function busyFor(args: {\n blockingStatuses: string[]\n capacityMode: 'per-guest' | 'per-reservation'\n end: Date\n payload: Payload\n reservationSlug: string\n resourceId: number | string\n start: Date\n}): Promise<Busy> {\n const { blockingStatuses, capacityMode, end, payload, reservationSlug, resourceId, start } = args\n const where: Where = {\n and: [\n { status: { in: blockingStatuses } },\n { startTime: { less_than: end.toISOString() } },\n { endTime: { greater_than: start.toISOString() } },\n { or: [{ resource: { equals: resourceId } }, { 'items.resource': { equals: resourceId } }] },\n ],\n }\n // limit:0 = all matching — bounded by the endpoint's 90-day range cap, so this\n // can't run away, and the grid no longer silently drops busy intervals (D9).\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs } = await (payload.find as any)({ collection: reservationSlug, depth: 0, limit: 0, where })\n return (docs as Array<Record<string, unknown>>)\n .filter((r) => r.startTime && r.endTime)\n .map((r) => ({\n end: new Date(r.endTime as string).toISOString(),\n start: new Date(r.startTime as string).toISOString(),\n units: capacityMode === 'per-guest' ? ((r.guestCount as number) ?? 1) : 1,\n }))\n}\n\nexport async function buildResourceAvailability(params: {\n blockingStatuses: string[]\n end: Date\n payload: Payload\n reservationSlug: string\n resourceId: number | string\n resourceSlug: string\n scheduleSlug: string\n start: Date\n timeZone: string\n}): Promise<null | ResourceAvailability> {\n const {\n blockingStatuses,\n end,\n payload,\n reservationSlug,\n resourceId,\n resourceSlug,\n scheduleSlug,\n start,\n timeZone,\n } = params\n\n // depth 1 so `services` are populated (their `requiredResources` come back as ids)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const resource = await (payload.findByID as any)({\n id: resourceId,\n collection: resourceSlug,\n depth: 1,\n }).catch(() => null)\n if (!resource) {\n return null\n }\n const quantity = (resource?.quantity as number) ?? 1\n const capacityMode = (resource?.capacityMode as 'per-guest' | 'per-reservation') ?? 'per-reservation'\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs: schedules } = await (payload.find as any)({\n collection: scheduleSlug,\n depth: 0,\n limit: 100,\n where: { and: [{ active: { equals: true } }, { resource: { equals: resourceId } }] },\n })\n\n type RawException = {\n date: string\n endDate?: string\n reason?: string\n type?: string\n }\n\n const days: DayAvailability[] = []\n const startKey = getDayKeyInTimezone(start, timeZone)\n const lastKey = getDayKeyInTimezone(new Date(end.getTime() - 1), timeZone)\n for (let date = startKey; date <= lastKey; date = addDaysToDayKey(date, 1)) {\n const shiftWindows: DayAvailability['shiftWindows'] = []\n const timeOff: DayAvailability['timeOff'] = []\n\n // A11: an exception on ANY of the resource's schedules makes the whole\n // resource unavailable that day. Find the first matching exception (if any)\n // — it suppresses all shift windows and marks the full day as time-off.\n let dayException: RawException | undefined\n for (const sched of schedules as Array<Record<string, unknown>>) {\n const exceptions = (sched.exceptions as RawException[] | undefined) ?? []\n for (const exc of exceptions) {\n const excStart = getDayKeyInTimezone(new Date(exc.date), timeZone)\n const excEnd = exc.endDate ? getDayKeyInTimezone(new Date(exc.endDate), timeZone) : excStart\n if (date >= excStart && date <= excEnd) {\n dayException = exc\n break\n }\n }\n if (dayException) {\n break\n }\n }\n\n if (dayException) {\n const dayStart = combineDayKeyAndTime(date, '00:00', timeZone)\n const dayEnd = new Date(combineDayKeyAndTime(date, '23:59', timeZone).getTime() + 59_999)\n timeOff.push({\n type: dayException.type,\n end: dayEnd.toISOString(),\n reason: dayException.reason,\n start: dayStart.toISOString(),\n })\n } else {\n for (const sched of schedules as Array<Record<string, unknown>>) {\n // resolveScheduleForDate accepts a Schedule-shaped object; cast through unknown\n const ranges = resolveScheduleForDate(\n sched as unknown as Parameters<typeof resolveScheduleForDate>[0],\n date,\n timeZone,\n )\n for (const r of ranges) {\n shiftWindows.push({ end: r.end.toISOString(), start: r.start.toISOString() })\n }\n }\n }\n\n days.push({ date, shiftWindows, timeOff })\n }\n\n const busy = await busyFor({\n blockingStatuses,\n capacityMode,\n end,\n payload,\n reservationSlug,\n resourceId,\n start,\n })\n\n // Resources this resource's services ALSO require (e.g. a shared chair pool).\n // A slot isn't truly bookable if any of these is at capacity, even when the\n // resource itself is free — so the calendar reflects real availability.\n const poolIds = new Set<string>()\n for (const svc of (resource?.services as Array<Record<string, unknown>>) ?? []) {\n const reqs = (typeof svc === 'object' ? (svc.requiredResources as unknown[]) : []) ?? []\n for (const rr of reqs) {\n const id: number | string | undefined =\n typeof rr === 'object' && rr !== null\n ? (rr as { id?: number | string }).id\n : (rr as number | string)\n if (id != null && String(id) !== String(resourceId)) {\n poolIds.add(String(id))\n }\n }\n }\n\n const requiredPools: ResourceAvailability['requiredPools'] = []\n for (const poolId of poolIds) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const pool = await (payload.findByID as any)({ id: poolId, collection: resourceSlug, depth: 0 }).catch(\n () => null,\n )\n if (!pool) {\n continue\n }\n const poolCapacityMode =\n (pool.capacityMode as 'per-guest' | 'per-reservation') ?? 'per-reservation'\n requiredPools.push({\n busy: await busyFor({\n blockingStatuses,\n capacityMode: poolCapacityMode,\n end,\n payload,\n reservationSlug,\n resourceId: poolId,\n start,\n }),\n quantity: (pool.quantity as number) ?? 1,\n })\n }\n\n return { busy, capacityMode, days, quantity, requiredPools }\n}\n\nexport function createResourceAvailabilityEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n // The grid data (every reservation's busy window) is staff/admin-only —\n // same gate as customer search.\n if (!req.user) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n if (!isPrivilegedUser(req.user, config)) {\n return Response.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const url = new URL(req.url!)\n const resource = url.searchParams.get('resource')\n const start = url.searchParams.get('start')\n const end = url.searchParams.get('end')\n\n if (!resource || !start || !end) {\n return Response.json(\n { error: 'Missing required query params: resource, start, end' },\n { status: 400 },\n )\n }\n\n const startDate = new Date(start)\n const endDate = new Date(end)\n if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {\n return Response.json({ error: 'Invalid start/end date' }, { status: 400 })\n }\n if (endDate <= startDate) {\n return Response.json({ error: 'end must be after start' }, { status: 400 })\n }\n // Unbounded ranges turn the per-day resolution loop into a CPU sink\n if (endDate.getTime() - startDate.getTime() > MAX_RANGE_MS) {\n return Response.json({ error: 'Date range too large (max 90 days)' }, { status: 400 })\n }\n\n const result = await buildResourceAvailability({\n blockingStatuses: config.statusMachine.blockingStatuses,\n end: endDate,\n payload: req.payload,\n reservationSlug: config.slugs.reservations,\n resourceId: resource,\n resourceSlug: config.slugs.resources,\n scheduleSlug: config.slugs.schedules,\n start: startDate,\n timeZone: config.timezone,\n })\n\n if (!result) {\n return Response.json({ error: 'Resource not found' }, { status: 404 })\n }\n\n return Response.json(result)\n },\n method: 'get',\n path: '/reserve/resource-availability',\n }\n}\n"],"names":["resolveScheduleForDate","addDaysToDayKey","combineDayKeyAndTime","getDayKeyInTimezone","isPrivilegedUser","MAX_RANGE_MS","busyFor","args","blockingStatuses","capacityMode","end","payload","reservationSlug","resourceId","start","where","and","status","in","startTime","less_than","toISOString","endTime","greater_than","or","resource","equals","docs","find","collection","depth","limit","filter","r","map","Date","units","guestCount","buildResourceAvailability","params","resourceSlug","scheduleSlug","timeZone","findByID","id","catch","quantity","schedules","active","days","startKey","lastKey","getTime","date","shiftWindows","timeOff","dayException","sched","exceptions","exc","excStart","excEnd","endDate","dayStart","dayEnd","push","type","reason","ranges","busy","poolIds","Set","svc","services","reqs","requiredResources","rr","String","add","requiredPools","poolId","pool","poolCapacityMode","createResourceAvailabilityEndpoint","config","handler","req","user","Response","json","error","url","URL","searchParams","get","startDate","isNaN","result","statusMachine","slugs","reservations","resources","timezone","method","path"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/resourceAvailability.ts"],"sourcesContent":["import type { Endpoint, Payload, Where } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { resolveScheduleForDate } from '../utilities/scheduleUtils.js'\nimport { readCookie } from '../utilities/tenantFilter.js'\nimport { getEffectiveTenantTimezone } from '../utilities/tenantTimezone.js'\nimport {\n addDaysToDayKey,\n combineDayKeyAndTime,\n getDayKeyInTimezone,\n} from '../utilities/timezoneUtils.js'\nimport { isPrivilegedUser } from '../utilities/userRoles.js'\n\nconst MAX_RANGE_MS = 90 * 86_400_000\n\ntype DayAvailability = {\n date: string\n shiftWindows: Array<{ end: string; start: string }>\n timeOff: Array<{ end: string; reason?: string; start: string; type?: string }>\n}\n\ntype Busy = Array<{ end: string; start: string; units: number }>\n\nexport type ResourceAvailability = {\n busy: Busy\n capacityMode: 'per-guest' | 'per-reservation'\n days: DayAvailability[]\n quantity: number\n /** Capacity of resources this resource's services also require (e.g. a chair pool). */\n requiredPools: Array<{ busy: Busy; quantity: number }>\n /** IANA zone the day windows were resolved in (selected tenant's zone, else global). */\n timeZone: string\n}\n\n/** Busy intervals (with capacity units) for one resource over [start, end). */\nasync function busyFor(args: {\n blockingStatuses: string[]\n capacityMode: 'per-guest' | 'per-reservation'\n end: Date\n payload: Payload\n reservationSlug: string\n resourceId: number | string\n start: Date\n}): Promise<Busy> {\n const { blockingStatuses, capacityMode, end, payload, reservationSlug, resourceId, start } = args\n const where: Where = {\n and: [\n { status: { in: blockingStatuses } },\n { startTime: { less_than: end.toISOString() } },\n { endTime: { greater_than: start.toISOString() } },\n { or: [{ resource: { equals: resourceId } }, { 'items.resource': { equals: resourceId } }] },\n ],\n }\n // limit:0 = all matching — bounded by the endpoint's 90-day range cap, so this\n // can't run away, and the grid no longer silently drops busy intervals (D9).\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs } = await (payload.find as any)({ collection: reservationSlug, depth: 0, limit: 0, where })\n return (docs as Array<Record<string, unknown>>)\n .filter((r) => r.startTime && r.endTime)\n .map((r) => ({\n end: new Date(r.endTime as string).toISOString(),\n start: new Date(r.startTime as string).toISOString(),\n units: capacityMode === 'per-guest' ? ((r.guestCount as number) ?? 1) : 1,\n }))\n}\n\nexport async function buildResourceAvailability(params: {\n blockingStatuses: string[]\n end: Date\n payload: Payload\n reservationSlug: string\n resourceId: number | string\n resourceSlug: string\n scheduleSlug: string\n start: Date\n timeZone: string\n}): Promise<null | ResourceAvailability> {\n const {\n blockingStatuses,\n end,\n payload,\n reservationSlug,\n resourceId,\n resourceSlug,\n scheduleSlug,\n start,\n timeZone,\n } = params\n\n // depth 1 so `services` are populated (their `requiredResources` come back as ids)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const resource = await (payload.findByID as any)({\n id: resourceId,\n collection: resourceSlug,\n depth: 1,\n }).catch(() => null)\n if (!resource) {\n return null\n }\n const quantity = (resource?.quantity as number) ?? 1\n const capacityMode = (resource?.capacityMode as 'per-guest' | 'per-reservation') ?? 'per-reservation'\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs: schedules } = await (payload.find as any)({\n collection: scheduleSlug,\n depth: 0,\n limit: 100,\n where: { and: [{ active: { equals: true } }, { resource: { equals: resourceId } }] },\n })\n\n type RawException = {\n date: string\n endDate?: string\n reason?: string\n type?: string\n }\n\n const days: DayAvailability[] = []\n const startKey = getDayKeyInTimezone(start, timeZone)\n const lastKey = getDayKeyInTimezone(new Date(end.getTime() - 1), timeZone)\n for (let date = startKey; date <= lastKey; date = addDaysToDayKey(date, 1)) {\n const shiftWindows: DayAvailability['shiftWindows'] = []\n const timeOff: DayAvailability['timeOff'] = []\n\n // A11: an exception on ANY of the resource's schedules makes the whole\n // resource unavailable that day. Find the first matching exception (if any)\n // — it suppresses all shift windows and marks the full day as time-off.\n let dayException: RawException | undefined\n for (const sched of schedules as Array<Record<string, unknown>>) {\n const exceptions = (sched.exceptions as RawException[] | undefined) ?? []\n for (const exc of exceptions) {\n const excStart = getDayKeyInTimezone(new Date(exc.date), timeZone)\n const excEnd = exc.endDate ? getDayKeyInTimezone(new Date(exc.endDate), timeZone) : excStart\n if (date >= excStart && date <= excEnd) {\n dayException = exc\n break\n }\n }\n if (dayException) {\n break\n }\n }\n\n if (dayException) {\n const dayStart = combineDayKeyAndTime(date, '00:00', timeZone)\n const dayEnd = new Date(combineDayKeyAndTime(date, '23:59', timeZone).getTime() + 59_999)\n timeOff.push({\n type: dayException.type,\n end: dayEnd.toISOString(),\n reason: dayException.reason,\n start: dayStart.toISOString(),\n })\n } else {\n for (const sched of schedules as Array<Record<string, unknown>>) {\n // resolveScheduleForDate accepts a Schedule-shaped object; cast through unknown\n const ranges = resolveScheduleForDate(\n sched as unknown as Parameters<typeof resolveScheduleForDate>[0],\n date,\n timeZone,\n )\n for (const r of ranges) {\n shiftWindows.push({ end: r.end.toISOString(), start: r.start.toISOString() })\n }\n }\n }\n\n days.push({ date, shiftWindows, timeOff })\n }\n\n const busy = await busyFor({\n blockingStatuses,\n capacityMode,\n end,\n payload,\n reservationSlug,\n resourceId,\n start,\n })\n\n // Resources this resource's services ALSO require (e.g. a shared chair pool).\n // A slot isn't truly bookable if any of these is at capacity, even when the\n // resource itself is free — so the calendar reflects real availability.\n const poolIds = new Set<string>()\n for (const svc of (resource?.services as Array<Record<string, unknown>>) ?? []) {\n const reqs = (typeof svc === 'object' ? (svc.requiredResources as unknown[]) : []) ?? []\n for (const rr of reqs) {\n const id: number | string | undefined =\n typeof rr === 'object' && rr !== null\n ? (rr as { id?: number | string }).id\n : (rr as number | string)\n if (id != null && String(id) !== String(resourceId)) {\n poolIds.add(String(id))\n }\n }\n }\n\n const requiredPools: ResourceAvailability['requiredPools'] = []\n for (const poolId of poolIds) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const pool = await (payload.findByID as any)({ id: poolId, collection: resourceSlug, depth: 0 }).catch(\n () => null,\n )\n if (!pool) {\n continue\n }\n const poolCapacityMode =\n (pool.capacityMode as 'per-guest' | 'per-reservation') ?? 'per-reservation'\n requiredPools.push({\n busy: await busyFor({\n blockingStatuses,\n capacityMode: poolCapacityMode,\n end,\n payload,\n reservationSlug,\n resourceId: poolId,\n start,\n }),\n quantity: (pool.quantity as number) ?? 1,\n })\n }\n\n return { busy, capacityMode, days, quantity, requiredPools, timeZone }\n}\n\nexport function createResourceAvailabilityEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n // The grid data (every reservation's busy window) is staff/admin-only —\n // same gate as customer search.\n if (!req.user) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n if (!isPrivilegedUser(req.user, config)) {\n return Response.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n const url = new URL(req.url!)\n const resource = url.searchParams.get('resource')\n const start = url.searchParams.get('start')\n const end = url.searchParams.get('end')\n\n if (!resource || !start || !end) {\n return Response.json(\n { error: 'Missing required query params: resource, start, end' },\n { status: 400 },\n )\n }\n\n const startDate = new Date(start)\n const endDate = new Date(end)\n if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {\n return Response.json({ error: 'Invalid start/end date' }, { status: 400 })\n }\n if (endDate <= startDate) {\n return Response.json({ error: 'end must be after start' }, { status: 400 })\n }\n // Unbounded ranges turn the per-day resolution loop into a CPU sink\n if (endDate.getTime() - startDate.getTime() > MAX_RANGE_MS) {\n return Response.json({ error: 'Date range too large (max 90 days)' }, { status: 400 })\n }\n\n // In multiTenant mode, resolve day-boundaries in the SELECTED tenant's zone\n // (tenant timezone → global → UTC). Degrades to the global zone for plain\n // installs: no tenant relationship on reservations / no tenant cookie ⇒ no\n // DB read, same output as before.\n const reservationsCollection = req.payload.config.collections?.find(\n (c) => c.slug === config.slugs.reservations,\n )\n const timeZone = await getEffectiveTenantTimezone({\n globalTimezone: config.timezone,\n payload: req.payload,\n scopedCollection: reservationsCollection as { fields?: unknown[] } | undefined,\n tenantField: config.multiTenant.tenantField,\n tenantId: readCookie(req.headers?.get('cookie'), config.multiTenant.cookieName),\n timezoneField: config.multiTenant.timezoneField,\n })\n\n const result = await buildResourceAvailability({\n blockingStatuses: config.statusMachine.blockingStatuses,\n end: endDate,\n payload: req.payload,\n reservationSlug: config.slugs.reservations,\n resourceId: resource,\n resourceSlug: config.slugs.resources,\n scheduleSlug: config.slugs.schedules,\n start: startDate,\n timeZone,\n })\n\n if (!result) {\n return Response.json({ error: 'Resource not found' }, { status: 404 })\n }\n\n return Response.json(result)\n },\n method: 'get',\n path: '/reserve/resource-availability',\n }\n}\n"],"names":["resolveScheduleForDate","readCookie","getEffectiveTenantTimezone","addDaysToDayKey","combineDayKeyAndTime","getDayKeyInTimezone","isPrivilegedUser","MAX_RANGE_MS","busyFor","args","blockingStatuses","capacityMode","end","payload","reservationSlug","resourceId","start","where","and","status","in","startTime","less_than","toISOString","endTime","greater_than","or","resource","equals","docs","find","collection","depth","limit","filter","r","map","Date","units","guestCount","buildResourceAvailability","params","resourceSlug","scheduleSlug","timeZone","findByID","id","catch","quantity","schedules","active","days","startKey","lastKey","getTime","date","shiftWindows","timeOff","dayException","sched","exceptions","exc","excStart","excEnd","endDate","dayStart","dayEnd","push","type","reason","ranges","busy","poolIds","Set","svc","services","reqs","requiredResources","rr","String","add","requiredPools","poolId","pool","poolCapacityMode","createResourceAvailabilityEndpoint","config","handler","req","user","Response","json","error","url","URL","searchParams","get","startDate","isNaN","reservationsCollection","collections","c","slug","slugs","reservations","globalTimezone","timezone","scopedCollection","tenantField","multiTenant","tenantId","headers","cookieName","timezoneField","result","statusMachine","resources","method","path"],"mappings":"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"}
|
package/dist/plugin.js
CHANGED
|
@@ -9,6 +9,7 @@ import { createCancelBookingEndpoint } from './endpoints/cancelBooking.js';
|
|
|
9
9
|
import { createCheckAvailabilityEndpoint } from './endpoints/checkAvailability.js';
|
|
10
10
|
import { createBookingEndpoint } from './endpoints/createBooking.js';
|
|
11
11
|
import { createCustomerSearchEndpoint } from './endpoints/customerSearch.js';
|
|
12
|
+
import { createEffectiveTimezoneEndpoint } from './endpoints/effectiveTimezone.js';
|
|
12
13
|
import { createGetSlotsEndpoint } from './endpoints/getSlots.js';
|
|
13
14
|
import { createResourceAvailabilityEndpoint } from './endpoints/resourceAvailability.js';
|
|
14
15
|
import { provisionStaffResource } from './hooks/users/provisionStaffResource.js';
|
|
@@ -144,7 +145,7 @@ export const payloadReserve = (pluginOptions = {})=>(config)=>{
|
|
|
144
145
|
if (!config.endpoints) {
|
|
145
146
|
config.endpoints = [];
|
|
146
147
|
}
|
|
147
|
-
config.endpoints.push(createCancelBookingEndpoint(resolved), createCheckAvailabilityEndpoint(resolved), createBookingEndpoint(resolved), createCustomerSearchEndpoint(resolved), createGetSlotsEndpoint(resolved), createResourceAvailabilityEndpoint(resolved));
|
|
148
|
+
config.endpoints.push(createCancelBookingEndpoint(resolved), createCheckAvailabilityEndpoint(resolved), createBookingEndpoint(resolved), createCustomerSearchEndpoint(resolved), createEffectiveTimezoneEndpoint(resolved), createGetSlotsEndpoint(resolved), createResourceAvailabilityEndpoint(resolved));
|
|
148
149
|
// Wire staff auto-provisioning onto the staff user collection
|
|
149
150
|
if (resolved.staffProvisioning) {
|
|
150
151
|
const staffUserSlug = resolved.staffProvisioning.userCollection;
|
package/dist/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import type { CollectionSlug, Config, Field } from 'payload'\n\nimport { deepMergeSimple } from 'payload/shared'\n\nimport type { ReservationPluginConfig } from './types.js'\n\nimport { createCustomersCollection } from './collections/Customers.js'\nimport { createReservationsCollection } from './collections/Reservations.js'\nimport { createResourcesCollection } from './collections/Resources.js'\nimport { createSchedulesCollection } from './collections/Schedules.js'\nimport { createServicesCollection } from './collections/Services.js'\nimport { resolveConfig } from './defaults.js'\nimport { createCancelBookingEndpoint } from './endpoints/cancelBooking.js'\nimport { createCheckAvailabilityEndpoint } from './endpoints/checkAvailability.js'\nimport { createBookingEndpoint } from './endpoints/createBooking.js'\nimport { createCustomerSearchEndpoint } from './endpoints/customerSearch.js'\nimport { createGetSlotsEndpoint } from './endpoints/getSlots.js'\nimport { createResourceAvailabilityEndpoint } from './endpoints/resourceAvailability.js'\nimport { provisionStaffResource } from './hooks/users/provisionStaffResource.js'\nimport { type PluginT, translations } from './translations/index.js'\nimport { applyCollectionOverride } from './utilities/collectionOverrides.js'\n\n/**\n * All named field paths reachable from a field list, descending through\n * presentational containers (tabs, rows, collapsibles, unnamed groups) that\n * don't create their own data nesting — so dedup catches a field declared\n * inside one of them. Named groups/arrays DO nest data, so we don't recurse\n * into them (a `name` inside a named group is a different path).\n */\nfunction collectFieldNames(fields: Field[]): Set<string> {\n const names = new Set<string>()\n const walk = (list: Field[]): void => {\n for (const field of list) {\n if ('name' in field && field.name) {\n names.add(field.name)\n } else if ('tabs' in field && Array.isArray(field.tabs)) {\n for (const tab of field.tabs) {\n if ('name' in tab && tab.name) {\n names.add(tab.name)\n } else if (Array.isArray(tab.fields)) {\n walk(tab.fields)\n }\n }\n } else if ('fields' in field && Array.isArray(field.fields)) {\n // row / collapsible / unnamed group\n walk(field.fields)\n }\n }\n }\n walk(fields)\n return names\n}\n\nexport const payloadReserve =\n (pluginOptions: ReservationPluginConfig = {}) =>\n (config: Config): Config => {\n const resolved = resolveConfig(pluginOptions)\n\n // Detect localization from the Payload config\n if (config.localization) {\n resolved.localized = true\n }\n\n if (!config.collections) {\n config.collections = []\n }\n\n if (resolved.userCollection) {\n // Extend the existing auth collection with customer fields\n const targetCollection = config.collections.find(\n (col) => col.slug === resolved.userCollection,\n )\n\n if (!targetCollection) {\n // Fail loudly rather than silently skipping field injection and pointing\n // the customers slug at a collection that doesn't exist (review C2).\n throw new Error(\n `payload-reserve: userCollection \"${resolved.userCollection}\" was not found in config.collections. ` +\n `Define it before payloadReserve() runs, or correct the slug.`,\n )\n }\n\n {\n // Collect existing field names — descend into presentational containers\n // (tabs/rows/collapsibles/groups) so a field nested there isn't\n // re-injected at the top level (review C4).\n const existingFieldNames = collectFieldNames(targetCollection.fields)\n\n // Fields to inject if not already present. `name` is added so that\n // admin.useAsTitle: 'name' works out of the box on the extended user\n // collection (matches the v1.0.0 behaviour documented in README/SKILL).\n // It is NOT required — an existing users collection may have rows\n // without a name, and forcing required would fail their next update (C4).\n const fieldsToAdd: Field[] = [\n {\n name: 'name',\n type: 'text',\n maxLength: 200,\n },\n {\n name: 'phone',\n type: 'text',\n maxLength: 50,\n },\n {\n name: 'notes',\n type: 'textarea',\n },\n {\n name: 'bookings',\n type: 'join',\n collection: resolved.slugs.reservations as unknown as CollectionSlug,\n on: 'customer',\n },\n ]\n\n for (const field of fieldsToAdd) {\n const fieldName = 'name' in field ? field.name : undefined\n if (fieldName && !existingFieldNames.has(fieldName)) {\n targetCollection.fields.push(field)\n }\n }\n }\n\n // Point the customers slug at the user collection so other parts of the\n // plugin (endpoints, hooks) reference the correct collection\n resolved.slugs.customers = resolved.userCollection\n }\n\n // The slugs this plugin is about to register (Customers only in standalone mode)\n const slugsToRegister = [\n resolved.slugs.services,\n resolved.slugs.resources,\n resolved.slugs.schedules,\n resolved.slugs.reservations,\n ...(resolved.userCollection ? [] : [resolved.slugs.customers]),\n ]\n\n // C11: fail with a clear, actionable error on slug collision instead of\n // Payload's generic DuplicateCollection throw.\n for (const slug of slugsToRegister) {\n if (config.collections.some((col) => col.slug === slug)) {\n throw new Error(\n `payload-reserve: a collection with slug \"${slug}\" already exists. ` +\n `Override the plugin's slug via the \\`slugs\\` option.`,\n )\n }\n }\n\n // Image upload fields are added only when the media collection actually\n // exists, so installs without one don't hit an opaque init error (C8).\n resolved.hasMediaCollection = config.collections.some(\n (col) => col.slug === resolved.slugs.media,\n )\n\n const ov = resolved.collectionOverrides\n config.collections.push(\n applyCollectionOverride(createServicesCollection(resolved), ov.services),\n applyCollectionOverride(createResourcesCollection(resolved), ov.resources),\n applyCollectionOverride(createSchedulesCollection(resolved), ov.schedules),\n applyCollectionOverride(createReservationsCollection(resolved), ov.reservations),\n // The customers override applies only in standalone mode; in userCollection\n // mode the host owns that collection and can edit it directly.\n ...(resolved.userCollection\n ? []\n : [applyCollectionOverride(createCustomersCollection(resolved), ov.customers)]),\n )\n\n // C3: collections are registered (above) even when disabled so the DB schema\n // stays stable; behavior (hooks, endpoints, admin, provisioning) is inert.\n if (resolved.disabled) {\n for (const slug of slugsToRegister) {\n const col = config.collections.find((c) => c.slug === slug)\n if (col) {\n delete col.hooks\n }\n }\n return config\n }\n\n // Register custom endpoints\n if (!config.endpoints) {config.endpoints = []}\n config.endpoints.push(\n createCancelBookingEndpoint(resolved),\n createCheckAvailabilityEndpoint(resolved),\n createBookingEndpoint(resolved),\n createCustomerSearchEndpoint(resolved),\n createGetSlotsEndpoint(resolved),\n createResourceAvailabilityEndpoint(resolved),\n )\n\n // Wire staff auto-provisioning onto the staff user collection\n if (resolved.staffProvisioning) {\n const staffUserSlug = resolved.staffProvisioning.userCollection\n const staffCollection = config.collections.find((col) => col.slug === staffUserSlug)\n if (!staffCollection) {\n throw new Error(\n `staffProvisioning.userCollection \"${staffUserSlug}\" was not found in config.collections`,\n )\n }\n staffCollection.hooks = {\n ...staffCollection.hooks,\n afterChange: [\n ...(staffCollection.hooks?.afterChange ?? []),\n provisionStaffResource(resolved),\n ],\n }\n }\n\n // Set up admin configuration\n if (!config.admin) {config.admin = {}}\n if (!config.admin.components) {config.admin.components = {}}\n\n // Store slugs and status machine in admin custom for component access\n if (!config.admin.custom) {config.admin.custom = {}}\n config.admin.custom.reservationSlugs = {\n ...resolved.slugs,\n }\n config.admin.custom.reservationStatusMachine = resolved.statusMachine\n config.admin.custom.reservationTenant = resolved.multiTenant\n config.admin.custom.reservationTimezone = resolved.timezone\n\n // Add dashboard widget\n if (!config.admin.dashboard) {\n config.admin.dashboard = { widgets: [] }\n }\n if (!config.admin.dashboard.widgets) {\n config.admin.dashboard.widgets = []\n }\n config.admin.dashboard.widgets.push({\n slug: 'reservation-todays-reservations',\n Component: 'payload-reserve/rsc#DashboardWidgetServer',\n label: ({ t }) => (t as PluginT)('reservation:dashboardTitle'),\n maxWidth: 'large',\n minWidth: 'medium',\n })\n\n // Add availability overview as custom admin view\n if (!config.admin.components.views) {\n config.admin.components.views = {}\n }\n ;(config.admin.components.views as Record<string, unknown>)['reservation-availability'] = {\n Component: 'payload-reserve/client#AvailabilityOverview',\n path: '/reservation-availability',\n }\n\n // Merge plugin translations (user translations take precedence)\n config.i18n = {\n ...(config.i18n ?? {}),\n translations: deepMergeSimple(\n translations,\n (config.i18n?.translations as Record<string, Record<string, unknown>>) ?? {},\n ),\n }\n\n return config\n }\n"],"names":["deepMergeSimple","createCustomersCollection","createReservationsCollection","createResourcesCollection","createSchedulesCollection","createServicesCollection","resolveConfig","createCancelBookingEndpoint","createCheckAvailabilityEndpoint","createBookingEndpoint","createCustomerSearchEndpoint","createGetSlotsEndpoint","createResourceAvailabilityEndpoint","provisionStaffResource","translations","applyCollectionOverride","collectFieldNames","fields","names","Set","walk","list","field","name","add","Array","isArray","tabs","tab","payloadReserve","pluginOptions","config","resolved","localization","localized","collections","userCollection","targetCollection","find","col","slug","Error","existingFieldNames","fieldsToAdd","type","maxLength","collection","slugs","reservations","on","fieldName","undefined","has","push","customers","slugsToRegister","services","resources","schedules","some","hasMediaCollection","media","ov","collectionOverrides","disabled","c","hooks","endpoints","staffProvisioning","staffUserSlug","staffCollection","afterChange","admin","components","custom","reservationSlugs","reservationStatusMachine","statusMachine","reservationTenant","multiTenant","reservationTimezone","timezone","dashboard","widgets","Component","label","t","maxWidth","minWidth","views","path","i18n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import type { CollectionSlug, Config, Field } from 'payload'\n\nimport { deepMergeSimple } from 'payload/shared'\n\nimport type { ReservationPluginConfig } from './types.js'\n\nimport { createCustomersCollection } from './collections/Customers.js'\nimport { createReservationsCollection } from './collections/Reservations.js'\nimport { createResourcesCollection } from './collections/Resources.js'\nimport { createSchedulesCollection } from './collections/Schedules.js'\nimport { createServicesCollection } from './collections/Services.js'\nimport { resolveConfig } from './defaults.js'\nimport { createCancelBookingEndpoint } from './endpoints/cancelBooking.js'\nimport { createCheckAvailabilityEndpoint } from './endpoints/checkAvailability.js'\nimport { createBookingEndpoint } from './endpoints/createBooking.js'\nimport { createCustomerSearchEndpoint } from './endpoints/customerSearch.js'\nimport { createEffectiveTimezoneEndpoint } from './endpoints/effectiveTimezone.js'\nimport { createGetSlotsEndpoint } from './endpoints/getSlots.js'\nimport { createResourceAvailabilityEndpoint } from './endpoints/resourceAvailability.js'\nimport { provisionStaffResource } from './hooks/users/provisionStaffResource.js'\nimport { type PluginT, translations } from './translations/index.js'\nimport { applyCollectionOverride } from './utilities/collectionOverrides.js'\n\n/**\n * All named field paths reachable from a field list, descending through\n * presentational containers (tabs, rows, collapsibles, unnamed groups) that\n * don't create their own data nesting — so dedup catches a field declared\n * inside one of them. Named groups/arrays DO nest data, so we don't recurse\n * into them (a `name` inside a named group is a different path).\n */\nfunction collectFieldNames(fields: Field[]): Set<string> {\n const names = new Set<string>()\n const walk = (list: Field[]): void => {\n for (const field of list) {\n if ('name' in field && field.name) {\n names.add(field.name)\n } else if ('tabs' in field && Array.isArray(field.tabs)) {\n for (const tab of field.tabs) {\n if ('name' in tab && tab.name) {\n names.add(tab.name)\n } else if (Array.isArray(tab.fields)) {\n walk(tab.fields)\n }\n }\n } else if ('fields' in field && Array.isArray(field.fields)) {\n // row / collapsible / unnamed group\n walk(field.fields)\n }\n }\n }\n walk(fields)\n return names\n}\n\nexport const payloadReserve =\n (pluginOptions: ReservationPluginConfig = {}) =>\n (config: Config): Config => {\n const resolved = resolveConfig(pluginOptions)\n\n // Detect localization from the Payload config\n if (config.localization) {\n resolved.localized = true\n }\n\n if (!config.collections) {\n config.collections = []\n }\n\n if (resolved.userCollection) {\n // Extend the existing auth collection with customer fields\n const targetCollection = config.collections.find(\n (col) => col.slug === resolved.userCollection,\n )\n\n if (!targetCollection) {\n // Fail loudly rather than silently skipping field injection and pointing\n // the customers slug at a collection that doesn't exist (review C2).\n throw new Error(\n `payload-reserve: userCollection \"${resolved.userCollection}\" was not found in config.collections. ` +\n `Define it before payloadReserve() runs, or correct the slug.`,\n )\n }\n\n {\n // Collect existing field names — descend into presentational containers\n // (tabs/rows/collapsibles/groups) so a field nested there isn't\n // re-injected at the top level (review C4).\n const existingFieldNames = collectFieldNames(targetCollection.fields)\n\n // Fields to inject if not already present. `name` is added so that\n // admin.useAsTitle: 'name' works out of the box on the extended user\n // collection (matches the v1.0.0 behaviour documented in README/SKILL).\n // It is NOT required — an existing users collection may have rows\n // without a name, and forcing required would fail their next update (C4).\n const fieldsToAdd: Field[] = [\n {\n name: 'name',\n type: 'text',\n maxLength: 200,\n },\n {\n name: 'phone',\n type: 'text',\n maxLength: 50,\n },\n {\n name: 'notes',\n type: 'textarea',\n },\n {\n name: 'bookings',\n type: 'join',\n collection: resolved.slugs.reservations as unknown as CollectionSlug,\n on: 'customer',\n },\n ]\n\n for (const field of fieldsToAdd) {\n const fieldName = 'name' in field ? field.name : undefined\n if (fieldName && !existingFieldNames.has(fieldName)) {\n targetCollection.fields.push(field)\n }\n }\n }\n\n // Point the customers slug at the user collection so other parts of the\n // plugin (endpoints, hooks) reference the correct collection\n resolved.slugs.customers = resolved.userCollection\n }\n\n // The slugs this plugin is about to register (Customers only in standalone mode)\n const slugsToRegister = [\n resolved.slugs.services,\n resolved.slugs.resources,\n resolved.slugs.schedules,\n resolved.slugs.reservations,\n ...(resolved.userCollection ? [] : [resolved.slugs.customers]),\n ]\n\n // C11: fail with a clear, actionable error on slug collision instead of\n // Payload's generic DuplicateCollection throw.\n for (const slug of slugsToRegister) {\n if (config.collections.some((col) => col.slug === slug)) {\n throw new Error(\n `payload-reserve: a collection with slug \"${slug}\" already exists. ` +\n `Override the plugin's slug via the \\`slugs\\` option.`,\n )\n }\n }\n\n // Image upload fields are added only when the media collection actually\n // exists, so installs without one don't hit an opaque init error (C8).\n resolved.hasMediaCollection = config.collections.some(\n (col) => col.slug === resolved.slugs.media,\n )\n\n const ov = resolved.collectionOverrides\n config.collections.push(\n applyCollectionOverride(createServicesCollection(resolved), ov.services),\n applyCollectionOverride(createResourcesCollection(resolved), ov.resources),\n applyCollectionOverride(createSchedulesCollection(resolved), ov.schedules),\n applyCollectionOverride(createReservationsCollection(resolved), ov.reservations),\n // The customers override applies only in standalone mode; in userCollection\n // mode the host owns that collection and can edit it directly.\n ...(resolved.userCollection\n ? []\n : [applyCollectionOverride(createCustomersCollection(resolved), ov.customers)]),\n )\n\n // C3: collections are registered (above) even when disabled so the DB schema\n // stays stable; behavior (hooks, endpoints, admin, provisioning) is inert.\n if (resolved.disabled) {\n for (const slug of slugsToRegister) {\n const col = config.collections.find((c) => c.slug === slug)\n if (col) {\n delete col.hooks\n }\n }\n return config\n }\n\n // Register custom endpoints\n if (!config.endpoints) {config.endpoints = []}\n config.endpoints.push(\n createCancelBookingEndpoint(resolved),\n createCheckAvailabilityEndpoint(resolved),\n createBookingEndpoint(resolved),\n createCustomerSearchEndpoint(resolved),\n createEffectiveTimezoneEndpoint(resolved),\n createGetSlotsEndpoint(resolved),\n createResourceAvailabilityEndpoint(resolved),\n )\n\n // Wire staff auto-provisioning onto the staff user collection\n if (resolved.staffProvisioning) {\n const staffUserSlug = resolved.staffProvisioning.userCollection\n const staffCollection = config.collections.find((col) => col.slug === staffUserSlug)\n if (!staffCollection) {\n throw new Error(\n `staffProvisioning.userCollection \"${staffUserSlug}\" was not found in config.collections`,\n )\n }\n staffCollection.hooks = {\n ...staffCollection.hooks,\n afterChange: [\n ...(staffCollection.hooks?.afterChange ?? []),\n provisionStaffResource(resolved),\n ],\n }\n }\n\n // Set up admin configuration\n if (!config.admin) {config.admin = {}}\n if (!config.admin.components) {config.admin.components = {}}\n\n // Store slugs and status machine in admin custom for component access\n if (!config.admin.custom) {config.admin.custom = {}}\n config.admin.custom.reservationSlugs = {\n ...resolved.slugs,\n }\n config.admin.custom.reservationStatusMachine = resolved.statusMachine\n config.admin.custom.reservationTenant = resolved.multiTenant\n config.admin.custom.reservationTimezone = resolved.timezone\n\n // Add dashboard widget\n if (!config.admin.dashboard) {\n config.admin.dashboard = { widgets: [] }\n }\n if (!config.admin.dashboard.widgets) {\n config.admin.dashboard.widgets = []\n }\n config.admin.dashboard.widgets.push({\n slug: 'reservation-todays-reservations',\n Component: 'payload-reserve/rsc#DashboardWidgetServer',\n label: ({ t }) => (t as PluginT)('reservation:dashboardTitle'),\n maxWidth: 'large',\n minWidth: 'medium',\n })\n\n // Add availability overview as custom admin view\n if (!config.admin.components.views) {\n config.admin.components.views = {}\n }\n ;(config.admin.components.views as Record<string, unknown>)['reservation-availability'] = {\n Component: 'payload-reserve/client#AvailabilityOverview',\n path: '/reservation-availability',\n }\n\n // Merge plugin translations (user translations take precedence)\n config.i18n = {\n ...(config.i18n ?? {}),\n translations: deepMergeSimple(\n translations,\n (config.i18n?.translations as Record<string, Record<string, unknown>>) ?? {},\n ),\n }\n\n return config\n }\n"],"names":["deepMergeSimple","createCustomersCollection","createReservationsCollection","createResourcesCollection","createSchedulesCollection","createServicesCollection","resolveConfig","createCancelBookingEndpoint","createCheckAvailabilityEndpoint","createBookingEndpoint","createCustomerSearchEndpoint","createEffectiveTimezoneEndpoint","createGetSlotsEndpoint","createResourceAvailabilityEndpoint","provisionStaffResource","translations","applyCollectionOverride","collectFieldNames","fields","names","Set","walk","list","field","name","add","Array","isArray","tabs","tab","payloadReserve","pluginOptions","config","resolved","localization","localized","collections","userCollection","targetCollection","find","col","slug","Error","existingFieldNames","fieldsToAdd","type","maxLength","collection","slugs","reservations","on","fieldName","undefined","has","push","customers","slugsToRegister","services","resources","schedules","some","hasMediaCollection","media","ov","collectionOverrides","disabled","c","hooks","endpoints","staffProvisioning","staffUserSlug","staffCollection","afterChange","admin","components","custom","reservationSlugs","reservationStatusMachine","statusMachine","reservationTenant","multiTenant","reservationTimezone","timezone","dashboard","widgets","Component","label","t","maxWidth","minWidth","views","path","i18n"],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -154,6 +154,12 @@ export type ReservationPluginConfig = {
|
|
|
154
154
|
cookieName?: string;
|
|
155
155
|
/** Tenant field name on scoped collections (default 'tenant'). */
|
|
156
156
|
tenantField?: string;
|
|
157
|
+
/**
|
|
158
|
+
* Field on the tenant document holding its IANA timezone (default 'timezone').
|
|
159
|
+
* When set and the selected tenant has a valid value, the admin views resolve
|
|
160
|
+
* day-boundaries in that tenant's zone instead of the global `timezone`.
|
|
161
|
+
*/
|
|
162
|
+
timezoneField?: string;
|
|
157
163
|
};
|
|
158
164
|
/** Enable resource-owner multi-tenancy (opt-in) */
|
|
159
165
|
resourceOwnerMode?: ResourceOwnerModeConfig;
|
|
@@ -206,6 +212,7 @@ export type ResolvedReservationPluginConfig = {
|
|
|
206
212
|
multiTenant: {
|
|
207
213
|
cookieName: string;
|
|
208
214
|
tenantField: string;
|
|
215
|
+
timezoneField: string;
|
|
209
216
|
};
|
|
210
217
|
resourceOwnerMode: ResolvedResourceOwnerModeConfig | undefined;
|
|
211
218
|
resourceTypes: string[];
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { CollectionConfig, Field, PayloadRequest } from 'payload'\n\n// --- Duration & Capacity models ---\n\nexport type DurationType = 'fixed' | 'flexible' | 'full-day'\n\nexport type CapacityMode = 'per-guest' | 'per-reservation'\n\n// --- Configurable status machine ---\n\nexport type StatusMachineConfig = {\n blockingStatuses: string[]\n /** Status treated as \"cancelled\" — fires beforeBookingCancel/afterBookingCancel, the cancellation notice period, and the cancellationReason field. */\n cancelStatus: string\n /** Status treated as \"confirmed\" — fires beforeBookingConfirm/afterBookingConfirm. */\n confirmStatus: string\n defaultStatus: string\n statuses: string[]\n terminalStatuses: string[]\n transitions: Record<string, string[]>\n}\n\nexport const DEFAULT_STATUS_MACHINE: StatusMachineConfig = {\n blockingStatuses: ['pending', 'confirmed'],\n cancelStatus: 'cancelled',\n confirmStatus: 'confirmed',\n defaultStatus: 'pending',\n statuses: ['pending', 'confirmed', 'completed', 'cancelled', 'no-show'],\n terminalStatuses: ['completed', 'cancelled', 'no-show'],\n transitions: {\n cancelled: [],\n completed: [],\n confirmed: ['completed', 'cancelled', 'no-show'],\n 'no-show': [],\n pending: ['confirmed', 'cancelled'],\n },\n}\n\n// --- Reservation item (for multi-resource bookings, Phase 3) ---\n\nexport type ReservationItemConfig = {\n endTime?: string\n guestCount?: number\n resource: string\n service?: string\n startTime?: string\n}\n\n// --- Plugin hooks for external integrations ---\n\nexport type ReservationPluginHooks = {\n afterBookingCancel?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterBookingConfirm?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterBookingCreate?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterStatusChange?: Array<\n (args: {\n doc: Record<string, unknown>\n newStatus: string\n previousStatus: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingCancel?: Array<\n (args: {\n doc: Record<string, unknown>\n reason?: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingConfirm?: Array<\n (args: {\n doc: Record<string, unknown>\n newStatus: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingCreate?: Array<\n (args: {\n data: Record<string, unknown>\n req: PayloadRequest\n }) => Promise<Record<string, unknown>> | Record<string, unknown>\n >\n}\n\n// --- Resource owner mode ---\n\nexport type ResourceOwnerModeConfig = {\n /** Roles that can see all records (default: check req.user.collection === adminCollection) */\n adminRoles?: string[]\n /** Whether Services also get an owner field (default: false — Services are platform-managed) */\n ownedServices?: boolean\n /**\n * Collection the owner field relates to (where owners/staff live). Defaults to\n * `staffProvisioning.userCollection` when set, otherwise `slugs.customers`. Set\n * this when owners live in a different collection than your customers (e.g.\n * separate `users` and `customers` collections).\n */\n ownerCollection?: string\n /** Field name for the owner relationship on Resources (default: 'owner') */\n ownerField?: string\n /** User field holding the role for admin detection (default: staffProvisioning.roleField or 'role') */\n roleField?: string\n}\n\nexport type ResolvedResourceOwnerModeConfig = {\n adminRoles: string[]\n ownedServices: boolean\n ownerCollection?: string\n ownerField: string\n roleField: string\n}\n\n// --- Staff provisioning ---\n\nexport type StaffProvisioningConfig = {\n /** Stamp tenant / custom fields onto the provisioned Resource before create. */\n beforeCreate?: (args: {\n data: Record<string, unknown>\n req: PayloadRequest\n user: Record<string, unknown>\n }) => Promise<Record<string, unknown>> | Record<string, unknown>\n /** User field copied into Resource `name` (default 'name', falls back to email). */\n nameFrom?: string\n /** resourceType to stamp (default 'staff'). Must be a valid resourceType. */\n resourceType?: string\n /** Field on the user holding the role (default 'role'). */\n roleField?: string\n /** Role value(s) marking a user as staff. Required, non-empty. */\n staffRoles: string[]\n /** Auth collection holding staff users. Defaults to top-level `userCollection`. */\n userCollection?: string\n}\n\nexport type ResolvedStaffProvisioningConfig = {\n beforeCreate?: StaffProvisioningConfig['beforeCreate']\n nameFrom: string\n resourceType: string\n roleField: string\n staffRoles: string[]\n userCollection: string\n}\n\n// --- Plugin configuration ---\n\n/**\n * Per-collection override applied to a generated collection. `fields` is a\n * function receiving the plugin's default fields so you can append/reorder/\n * replace them; supplied `hooks` are merged with (not replacing) the plugin's;\n * `access` composes per operation; `slug` is ignored (use the `slugs` option).\n */\nexport type CollectionOverride = {\n fields?: (args: { defaultFields: Field[] }) => Field[]\n} & Omit<Partial<CollectionConfig>, 'fields' | 'slug'>\n\nexport type ReservationPluginConfig = {\n /** Override access control per collection */\n access?: {\n customers?: CollectionConfig['access']\n reservations?: CollectionConfig['access']\n resources?: CollectionConfig['access']\n schedules?: CollectionConfig['access']\n services?: CollectionConfig['access']\n }\n /** Admin group name for all reservation collections */\n adminGroup?: string\n /** Allow bookings without a customer account by default (per-service override available) */\n allowGuestBooking?: boolean\n /** Hours of notice required before cancellation */\n cancellationNoticePeriod?: number\n /** Per-collection overrides applied to the generated collections (issue #4) */\n collectionOverrides?: {\n customers?: CollectionOverride\n reservations?: CollectionOverride\n resources?: CollectionOverride\n schedules?: CollectionOverride\n services?: CollectionOverride\n }\n /** Default buffer time in minutes between reservations */\n defaultBufferTime?: number\n /** Disable the plugin entirely */\n disabled?: boolean\n /** @deprecated Use `collectionOverrides.reservations.fields` instead. Extra fields appended to Reservations. */\n extraReservationFields?: Field[]\n /** Plugin hooks for external integrations */\n hooks?: ReservationPluginHooks\n /** Configurable leave/exception type vocabulary (default: vacation/sick/personal/closure/other) */\n leaveTypes?: string[]\n /** Tenant scoping for the custom admin views (calendar, availability, dashboard). Applied only when the scoped collection has the tenant field AND the tenant cookie is set. */\n multiTenant?: {\n /** Cookie written by the tenant-selector (default 'payload-tenant'). */\n cookieName?: string\n /** Tenant field name on scoped collections (default 'tenant'). */\n tenantField?: string\n }\n /** Enable resource-owner multi-tenancy (opt-in) */\n resourceOwnerMode?: ResourceOwnerModeConfig\n /** Configurable resourceType vocabulary (default: staff/equipment/room) */\n resourceTypes?: string[]\n /** Override collection slugs */\n slugs?: {\n customers?: string\n media?: string\n reservations?: string\n resources?: string\n schedules?: string\n services?: string\n }\n /** Auto-provision a Resource from staff-role users (opt-in; requires resourceOwnerMode) */\n staffProvisioning?: StaffProvisioningConfig\n /** Configurable status machine (defaults to current behavior) */\n statusMachine?: Partial<StatusMachineConfig>\n /** IANA business timezone governing schedules and day boundaries (default 'UTC') */\n timezone?: string\n /** Which existing auth collection to extend with customer fields */\n userCollection?: string\n}\n\nexport type ResolvedReservationPluginConfig = {\n access: {\n customers?: CollectionConfig['access']\n reservations?: CollectionConfig['access']\n resources?: CollectionConfig['access']\n schedules?: CollectionConfig['access']\n services?: CollectionConfig['access']\n }\n adminGroup: string\n allowGuestBooking: boolean\n cancellationNoticePeriod: number\n collectionOverrides: {\n customers?: CollectionOverride\n reservations?: CollectionOverride\n resources?: CollectionOverride\n schedules?: CollectionOverride\n services?: CollectionOverride\n }\n defaultBufferTime: number\n disabled: boolean\n extraReservationFields: Field[]\n /** Whether the media collection (`slugs.media`) exists — set by the plugin; gates the image upload fields. */\n hasMediaCollection: boolean\n hooks: ReservationPluginHooks\n leaveTypes: string[]\n localized: boolean\n multiTenant: {\n cookieName: string\n tenantField: string\n }\n resourceOwnerMode: ResolvedResourceOwnerModeConfig | undefined\n resourceTypes: string[]\n slugs: {\n customers: string\n media: string\n reservations: string\n resources: string\n schedules: string\n services: string\n }\n staffProvisioning: ResolvedStaffProvisioningConfig | undefined\n statusMachine: StatusMachineConfig\n timezone: string\n userCollection: string | undefined\n}\n\nexport type ReservationStatus = 'cancelled' | 'completed' | 'confirmed' | 'no-show' | 'pending'\n\nexport type DayOfWeek = 'fri' | 'mon' | 'sat' | 'sun' | 'thu' | 'tue' | 'wed'\n\nexport type ScheduleType = 'manual' | 'recurring'\n\n/** @deprecated Use DEFAULT_STATUS_MACHINE.transitions instead */\nexport const VALID_STATUS_TRANSITIONS: Record<ReservationStatus, ReservationStatus[]> =\n DEFAULT_STATUS_MACHINE.transitions as Record<ReservationStatus, ReservationStatus[]>\n"],"names":["DEFAULT_STATUS_MACHINE","blockingStatuses","cancelStatus","confirmStatus","defaultStatus","statuses","terminalStatuses","transitions","cancelled","completed","confirmed","pending","VALID_STATUS_TRANSITIONS"],"mappings":"AAsBA,OAAO,MAAMA,yBAA8C;IACzDC,kBAAkB;QAAC;QAAW;KAAY;IAC1CC,cAAc;IACdC,eAAe;IACfC,eAAe;IACfC,UAAU;QAAC;QAAW;QAAa;QAAa;QAAa;KAAU;IACvEC,kBAAkB;QAAC;QAAa;QAAa;KAAU;IACvDC,aAAa;QACXC,WAAW,EAAE;QACbC,WAAW,EAAE;QACbC,WAAW;YAAC;YAAa;YAAa;SAAU;QAChD,WAAW,EAAE;QACbC,SAAS;YAAC;YAAa;SAAY;IACrC;AACF,EAAC;AA+OD,+DAA+D,GAC/D,OAAO,MAAMC,2BACXZ,uBAAuBO,WAAW,CAAkD"}
|
|
1
|
+
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { CollectionConfig, Field, PayloadRequest } from 'payload'\n\n// --- Duration & Capacity models ---\n\nexport type DurationType = 'fixed' | 'flexible' | 'full-day'\n\nexport type CapacityMode = 'per-guest' | 'per-reservation'\n\n// --- Configurable status machine ---\n\nexport type StatusMachineConfig = {\n blockingStatuses: string[]\n /** Status treated as \"cancelled\" — fires beforeBookingCancel/afterBookingCancel, the cancellation notice period, and the cancellationReason field. */\n cancelStatus: string\n /** Status treated as \"confirmed\" — fires beforeBookingConfirm/afterBookingConfirm. */\n confirmStatus: string\n defaultStatus: string\n statuses: string[]\n terminalStatuses: string[]\n transitions: Record<string, string[]>\n}\n\nexport const DEFAULT_STATUS_MACHINE: StatusMachineConfig = {\n blockingStatuses: ['pending', 'confirmed'],\n cancelStatus: 'cancelled',\n confirmStatus: 'confirmed',\n defaultStatus: 'pending',\n statuses: ['pending', 'confirmed', 'completed', 'cancelled', 'no-show'],\n terminalStatuses: ['completed', 'cancelled', 'no-show'],\n transitions: {\n cancelled: [],\n completed: [],\n confirmed: ['completed', 'cancelled', 'no-show'],\n 'no-show': [],\n pending: ['confirmed', 'cancelled'],\n },\n}\n\n// --- Reservation item (for multi-resource bookings, Phase 3) ---\n\nexport type ReservationItemConfig = {\n endTime?: string\n guestCount?: number\n resource: string\n service?: string\n startTime?: string\n}\n\n// --- Plugin hooks for external integrations ---\n\nexport type ReservationPluginHooks = {\n afterBookingCancel?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterBookingConfirm?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterBookingCreate?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterStatusChange?: Array<\n (args: {\n doc: Record<string, unknown>\n newStatus: string\n previousStatus: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingCancel?: Array<\n (args: {\n doc: Record<string, unknown>\n reason?: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingConfirm?: Array<\n (args: {\n doc: Record<string, unknown>\n newStatus: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingCreate?: Array<\n (args: {\n data: Record<string, unknown>\n req: PayloadRequest\n }) => Promise<Record<string, unknown>> | Record<string, unknown>\n >\n}\n\n// --- Resource owner mode ---\n\nexport type ResourceOwnerModeConfig = {\n /** Roles that can see all records (default: check req.user.collection === adminCollection) */\n adminRoles?: string[]\n /** Whether Services also get an owner field (default: false — Services are platform-managed) */\n ownedServices?: boolean\n /**\n * Collection the owner field relates to (where owners/staff live). Defaults to\n * `staffProvisioning.userCollection` when set, otherwise `slugs.customers`. Set\n * this when owners live in a different collection than your customers (e.g.\n * separate `users` and `customers` collections).\n */\n ownerCollection?: string\n /** Field name for the owner relationship on Resources (default: 'owner') */\n ownerField?: string\n /** User field holding the role for admin detection (default: staffProvisioning.roleField or 'role') */\n roleField?: string\n}\n\nexport type ResolvedResourceOwnerModeConfig = {\n adminRoles: string[]\n ownedServices: boolean\n ownerCollection?: string\n ownerField: string\n roleField: string\n}\n\n// --- Staff provisioning ---\n\nexport type StaffProvisioningConfig = {\n /** Stamp tenant / custom fields onto the provisioned Resource before create. */\n beforeCreate?: (args: {\n data: Record<string, unknown>\n req: PayloadRequest\n user: Record<string, unknown>\n }) => Promise<Record<string, unknown>> | Record<string, unknown>\n /** User field copied into Resource `name` (default 'name', falls back to email). */\n nameFrom?: string\n /** resourceType to stamp (default 'staff'). Must be a valid resourceType. */\n resourceType?: string\n /** Field on the user holding the role (default 'role'). */\n roleField?: string\n /** Role value(s) marking a user as staff. Required, non-empty. */\n staffRoles: string[]\n /** Auth collection holding staff users. Defaults to top-level `userCollection`. */\n userCollection?: string\n}\n\nexport type ResolvedStaffProvisioningConfig = {\n beforeCreate?: StaffProvisioningConfig['beforeCreate']\n nameFrom: string\n resourceType: string\n roleField: string\n staffRoles: string[]\n userCollection: string\n}\n\n// --- Plugin configuration ---\n\n/**\n * Per-collection override applied to a generated collection. `fields` is a\n * function receiving the plugin's default fields so you can append/reorder/\n * replace them; supplied `hooks` are merged with (not replacing) the plugin's;\n * `access` composes per operation; `slug` is ignored (use the `slugs` option).\n */\nexport type CollectionOverride = {\n fields?: (args: { defaultFields: Field[] }) => Field[]\n} & Omit<Partial<CollectionConfig>, 'fields' | 'slug'>\n\nexport type ReservationPluginConfig = {\n /** Override access control per collection */\n access?: {\n customers?: CollectionConfig['access']\n reservations?: CollectionConfig['access']\n resources?: CollectionConfig['access']\n schedules?: CollectionConfig['access']\n services?: CollectionConfig['access']\n }\n /** Admin group name for all reservation collections */\n adminGroup?: string\n /** Allow bookings without a customer account by default (per-service override available) */\n allowGuestBooking?: boolean\n /** Hours of notice required before cancellation */\n cancellationNoticePeriod?: number\n /** Per-collection overrides applied to the generated collections (issue #4) */\n collectionOverrides?: {\n customers?: CollectionOverride\n reservations?: CollectionOverride\n resources?: CollectionOverride\n schedules?: CollectionOverride\n services?: CollectionOverride\n }\n /** Default buffer time in minutes between reservations */\n defaultBufferTime?: number\n /** Disable the plugin entirely */\n disabled?: boolean\n /** @deprecated Use `collectionOverrides.reservations.fields` instead. Extra fields appended to Reservations. */\n extraReservationFields?: Field[]\n /** Plugin hooks for external integrations */\n hooks?: ReservationPluginHooks\n /** Configurable leave/exception type vocabulary (default: vacation/sick/personal/closure/other) */\n leaveTypes?: string[]\n /** Tenant scoping for the custom admin views (calendar, availability, dashboard). Applied only when the scoped collection has the tenant field AND the tenant cookie is set. */\n multiTenant?: {\n /** Cookie written by the tenant-selector (default 'payload-tenant'). */\n cookieName?: string\n /** Tenant field name on scoped collections (default 'tenant'). */\n tenantField?: string\n /**\n * Field on the tenant document holding its IANA timezone (default 'timezone').\n * When set and the selected tenant has a valid value, the admin views resolve\n * day-boundaries in that tenant's zone instead of the global `timezone`.\n */\n timezoneField?: string\n }\n /** Enable resource-owner multi-tenancy (opt-in) */\n resourceOwnerMode?: ResourceOwnerModeConfig\n /** Configurable resourceType vocabulary (default: staff/equipment/room) */\n resourceTypes?: string[]\n /** Override collection slugs */\n slugs?: {\n customers?: string\n media?: string\n reservations?: string\n resources?: string\n schedules?: string\n services?: string\n }\n /** Auto-provision a Resource from staff-role users (opt-in; requires resourceOwnerMode) */\n staffProvisioning?: StaffProvisioningConfig\n /** Configurable status machine (defaults to current behavior) */\n statusMachine?: Partial<StatusMachineConfig>\n /** IANA business timezone governing schedules and day boundaries (default 'UTC') */\n timezone?: string\n /** Which existing auth collection to extend with customer fields */\n userCollection?: string\n}\n\nexport type ResolvedReservationPluginConfig = {\n access: {\n customers?: CollectionConfig['access']\n reservations?: CollectionConfig['access']\n resources?: CollectionConfig['access']\n schedules?: CollectionConfig['access']\n services?: CollectionConfig['access']\n }\n adminGroup: string\n allowGuestBooking: boolean\n cancellationNoticePeriod: number\n collectionOverrides: {\n customers?: CollectionOverride\n reservations?: CollectionOverride\n resources?: CollectionOverride\n schedules?: CollectionOverride\n services?: CollectionOverride\n }\n defaultBufferTime: number\n disabled: boolean\n extraReservationFields: Field[]\n /** Whether the media collection (`slugs.media`) exists — set by the plugin; gates the image upload fields. */\n hasMediaCollection: boolean\n hooks: ReservationPluginHooks\n leaveTypes: string[]\n localized: boolean\n multiTenant: {\n cookieName: string\n tenantField: string\n timezoneField: string\n }\n resourceOwnerMode: ResolvedResourceOwnerModeConfig | undefined\n resourceTypes: string[]\n slugs: {\n customers: string\n media: string\n reservations: string\n resources: string\n schedules: string\n services: string\n }\n staffProvisioning: ResolvedStaffProvisioningConfig | undefined\n statusMachine: StatusMachineConfig\n timezone: string\n userCollection: string | undefined\n}\n\nexport type ReservationStatus = 'cancelled' | 'completed' | 'confirmed' | 'no-show' | 'pending'\n\nexport type DayOfWeek = 'fri' | 'mon' | 'sat' | 'sun' | 'thu' | 'tue' | 'wed'\n\nexport type ScheduleType = 'manual' | 'recurring'\n\n/** @deprecated Use DEFAULT_STATUS_MACHINE.transitions instead */\nexport const VALID_STATUS_TRANSITIONS: Record<ReservationStatus, ReservationStatus[]> =\n DEFAULT_STATUS_MACHINE.transitions as Record<ReservationStatus, ReservationStatus[]>\n"],"names":["DEFAULT_STATUS_MACHINE","blockingStatuses","cancelStatus","confirmStatus","defaultStatus","statuses","terminalStatuses","transitions","cancelled","completed","confirmed","pending","VALID_STATUS_TRANSITIONS"],"mappings":"AAsBA,OAAO,MAAMA,yBAA8C;IACzDC,kBAAkB;QAAC;QAAW;KAAY;IAC1CC,cAAc;IACdC,eAAe;IACfC,eAAe;IACfC,UAAU;QAAC;QAAW;QAAa;QAAa;QAAa;KAAU;IACvEC,kBAAkB;QAAC;QAAa;QAAa;KAAU;IACvDC,aAAa;QACXC,WAAW,EAAE;QACbC,WAAW,EAAE;QACbC,WAAW;YAAC;YAAa;YAAa;SAAU;QAChD,WAAW,EAAE;QACbC,SAAS;YAAC;YAAa;SAAY;IACrC;AACF,EAAC;AAsPD,+DAA+D,GAC/D,OAAO,MAAMC,2BACXZ,uBAAuBO,WAAW,CAAkD"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Payload } from 'payload';
|
|
2
|
+
type CollectionLike = {
|
|
3
|
+
fields?: unknown[];
|
|
4
|
+
} | null | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* Pure precedence resolver for a tenant's effective timezone:
|
|
7
|
+
*
|
|
8
|
+
* selected tenant's zone → global default → 'UTC'
|
|
9
|
+
*
|
|
10
|
+
* An absent or invalid zone at any step is skipped, never thrown — so a tenant
|
|
11
|
+
* with a bad/empty timezone value transparently falls back to the global default.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveTenantTimezone(args: {
|
|
14
|
+
globalTimezone: string;
|
|
15
|
+
tenantTimezone?: null | string;
|
|
16
|
+
}): string;
|
|
17
|
+
/**
|
|
18
|
+
* The tenant collection's slug, read off the scoped collection's tenant
|
|
19
|
+
* relationship field (`relationTo`). Keeps the plugin tenant-agnostic — it never
|
|
20
|
+
* hardcodes a tenants slug. Returns null for an absent, polymorphic, or
|
|
21
|
+
* non-relationship tenant field.
|
|
22
|
+
*/
|
|
23
|
+
export declare function tenantCollectionSlug(collection: CollectionLike, tenantField: string): null | string;
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the effective timezone for the selected tenant in multiTenant mode.
|
|
26
|
+
*
|
|
27
|
+
* Loads the tenant document (slug derived from the scoped collection's tenant
|
|
28
|
+
* relationship) and reads `timezoneField`, then applies {@link resolveTenantTimezone}
|
|
29
|
+
* precedence. Returns the global default — without a DB read — when no tenant is
|
|
30
|
+
* selected, when the scoped collection lacks a tenant relationship, or when the
|
|
31
|
+
* lookup fails. Never throws.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getEffectiveTenantTimezone(args: {
|
|
34
|
+
globalTimezone: string;
|
|
35
|
+
payload: Payload;
|
|
36
|
+
scopedCollection: CollectionLike;
|
|
37
|
+
tenantField: string;
|
|
38
|
+
tenantId: null | string;
|
|
39
|
+
timezoneField: string;
|
|
40
|
+
}): Promise<string>;
|
|
41
|
+
export {};
|