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