payload-reserve 1.3.2 → 1.5.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 +185 -4
- package/dist/collections/Reservations.js +47 -2
- package/dist/collections/Reservations.js.map +1 -1
- package/dist/collections/Resources.d.ts +16 -0
- package/dist/collections/Resources.js +35 -10
- package/dist/collections/Resources.js.map +1 -1
- package/dist/collections/Schedules.js +34 -0
- package/dist/collections/Schedules.js.map +1 -1
- package/dist/collections/Services.js +34 -1
- package/dist/collections/Services.js.map +1 -1
- package/dist/components/AvailabilityTimeField/AvailabilityTimeField.module.css +7 -0
- package/dist/components/AvailabilityTimeField/index.d.ts +2 -0
- package/dist/components/AvailabilityTimeField/index.js +109 -0
- package/dist/components/AvailabilityTimeField/index.js.map +1 -0
- package/dist/components/CalendarView/CalendarView.module.css +114 -0
- package/dist/components/CalendarView/LaneTimelineView.d.ts +12 -0
- package/dist/components/CalendarView/LaneTimelineView.js +116 -0
- package/dist/components/CalendarView/LaneTimelineView.js.map +1 -0
- package/dist/components/CalendarView/index.js +224 -22
- package/dist/components/CalendarView/index.js.map +1 -1
- package/dist/components/CalendarView/useResourceAvailability.d.ts +9 -0
- package/dist/components/CalendarView/useResourceAvailability.js +40 -0
- package/dist/components/CalendarView/useResourceAvailability.js.map +1 -0
- package/dist/defaults.d.ts +3 -0
- package/dist/defaults.js +53 -0
- package/dist/defaults.js.map +1 -1
- package/dist/endpoints/cancelBooking.js +34 -21
- package/dist/endpoints/cancelBooking.js.map +1 -1
- package/dist/endpoints/checkAvailability.js +16 -1
- package/dist/endpoints/checkAvailability.js.map +1 -1
- package/dist/endpoints/createBooking.js +4 -1
- package/dist/endpoints/createBooking.js.map +1 -1
- package/dist/endpoints/customerSearch.js +24 -5
- package/dist/endpoints/customerSearch.js.map +1 -1
- package/dist/endpoints/getSlots.js +16 -1
- package/dist/endpoints/getSlots.js.map +1 -1
- package/dist/endpoints/resourceAvailability.d.ts +43 -0
- package/dist/endpoints/resourceAvailability.js +214 -0
- package/dist/endpoints/resourceAvailability.js.map +1 -0
- package/dist/exports/client.d.ts +1 -0
- package/dist/exports/client.js +1 -0
- package/dist/exports/client.js.map +1 -1
- package/dist/hooks/reservations/calculateEndTime.js +21 -1
- package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
- package/dist/hooks/reservations/expandRequiredResources.d.ts +9 -0
- package/dist/hooks/reservations/expandRequiredResources.js +81 -0
- package/dist/hooks/reservations/expandRequiredResources.js.map +1 -0
- package/dist/hooks/reservations/validateGuestBooking.d.ts +3 -0
- package/dist/hooks/reservations/validateGuestBooking.js +93 -0
- package/dist/hooks/reservations/validateGuestBooking.js.map +1 -0
- package/dist/hooks/reservations/validateStatusTransition.js +4 -2
- package/dist/hooks/reservations/validateStatusTransition.js.map +1 -1
- package/dist/hooks/users/provisionStaffResource.d.ts +15 -0
- package/dist/hooks/users/provisionStaffResource.js +88 -0
- package/dist/hooks/users/provisionStaffResource.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin.js +28 -3
- package/dist/plugin.js.map +1 -1
- package/dist/services/AvailabilityService.d.ts +7 -6
- package/dist/services/AvailabilityService.js +86 -60
- package/dist/services/AvailabilityService.js.map +1 -1
- package/dist/translations/ar.json +156 -0
- package/dist/translations/de.json +156 -0
- package/dist/translations/en.json +32 -1
- package/dist/translations/es.json +156 -0
- package/dist/translations/fa.json +156 -0
- package/dist/translations/fr.json +156 -0
- package/dist/translations/hi.json +156 -0
- package/dist/translations/id.json +156 -0
- package/dist/translations/index.js +44 -0
- package/dist/translations/index.js.map +1 -1
- package/dist/translations/pl.json +156 -0
- package/dist/translations/ru.json +156 -0
- package/dist/translations/tr.json +156 -0
- package/dist/translations/zh.json +156 -0
- package/dist/types.d.ts +46 -0
- package/dist/types.js.map +1 -1
- package/dist/utilities/computeSlotStates.d.ts +39 -0
- package/dist/utilities/computeSlotStates.js +49 -0
- package/dist/utilities/computeSlotStates.js.map +1 -0
- package/dist/utilities/guestBooking.d.ts +10 -0
- package/dist/utilities/guestBooking.js +16 -0
- package/dist/utilities/guestBooking.js.map +1 -0
- package/dist/utilities/resolveRequiredResources.d.ts +8 -0
- package/dist/utilities/resolveRequiredResources.js +27 -0
- package/dist/utilities/resolveRequiredResources.js.map +1 -0
- package/dist/utilities/resolveReservationItems.d.ts +3 -2
- package/dist/utilities/resolveReservationItems.js +19 -6
- package/dist/utilities/resolveReservationItems.js.map +1 -1
- package/dist/utilities/scheduleUtils.d.ts +3 -0
- package/dist/utilities/scheduleUtils.js +5 -3
- package/dist/utilities/scheduleUtils.js.map +1 -1
- package/dist/utilities/selectOptions.d.ts +8 -0
- package/dist/utilities/selectOptions.js +11 -0
- package/dist/utilities/selectOptions.js.map +1 -0
- package/dist/utilities/slotUtils.d.ts +19 -0
- package/dist/utilities/slotUtils.js +28 -0
- package/dist/utilities/slotUtils.js.map +1 -1
- package/dist/utilities/userRoles.d.ts +20 -0
- package/dist/utilities/userRoles.js +32 -0
- package/dist/utilities/userRoles.js.map +1 -0
- package/package.json +2 -1
|
@@ -1,15 +1,9 @@
|
|
|
1
|
+
import { isPrivilegedUser } from '../utilities/userRoles.js';
|
|
1
2
|
export function createCancelBookingEndpoint(config) {
|
|
2
3
|
return {
|
|
3
4
|
handler: async (req)=>{
|
|
4
|
-
if (!req.user) {
|
|
5
|
-
return Response.json({
|
|
6
|
-
message: 'Unauthorized'
|
|
7
|
-
}, {
|
|
8
|
-
status: 401
|
|
9
|
-
});
|
|
10
|
-
}
|
|
11
5
|
const body = await req.json?.();
|
|
12
|
-
const { reason, reservationId } = body ?? {};
|
|
6
|
+
const { reason, reservationId, token } = body ?? {};
|
|
13
7
|
if (!reservationId) {
|
|
14
8
|
return Response.json({
|
|
15
9
|
message: 'reservationId is required'
|
|
@@ -17,25 +11,37 @@ export function createCancelBookingEndpoint(config) {
|
|
|
17
11
|
status: 400
|
|
18
12
|
});
|
|
19
13
|
}
|
|
20
|
-
// Fetch the reservation to check ownership
|
|
14
|
+
// Fetch the reservation to check ownership / token.
|
|
21
15
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
16
|
const existing = await req.payload.findByID({
|
|
23
17
|
id: reservationId,
|
|
24
18
|
collection: config.slugs.reservations,
|
|
25
19
|
depth: 0,
|
|
20
|
+
overrideAccess: true,
|
|
26
21
|
req
|
|
27
22
|
});
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
23
|
+
if (req.user) {
|
|
24
|
+
// Authenticated path: owner (customer === req.user) or admin/staff.
|
|
25
|
+
const customerId = typeof existing.customer === 'object' ? existing.customer?.id : existing.customer;
|
|
26
|
+
const isOwner = customerId === req.user.id;
|
|
27
|
+
// Staff/admin detection (role-aware for single-collection deployments)
|
|
28
|
+
const isAdmin = isPrivilegedUser(req.user, config);
|
|
29
|
+
if (!isOwner && !isAdmin) {
|
|
30
|
+
return Response.json({
|
|
31
|
+
message: 'Forbidden'
|
|
32
|
+
}, {
|
|
33
|
+
status: 403
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
// Guest path: match the cancellation token.
|
|
38
|
+
if (!token || !existing.cancellationToken || token !== existing.cancellationToken) {
|
|
39
|
+
return Response.json({
|
|
40
|
+
message: 'Forbidden'
|
|
41
|
+
}, {
|
|
42
|
+
status: 403
|
|
43
|
+
});
|
|
44
|
+
}
|
|
39
45
|
}
|
|
40
46
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
41
47
|
const reservation = await req.payload.update({
|
|
@@ -45,9 +51,16 @@ export function createCancelBookingEndpoint(config) {
|
|
|
45
51
|
cancellationReason: reason,
|
|
46
52
|
status: 'cancelled'
|
|
47
53
|
},
|
|
54
|
+
// Authorization (owner / admin / matching token) is already enforced above.
|
|
55
|
+
// overrideAccess lets the write proceed for anonymous guest cancellations,
|
|
56
|
+
// and avoids a spurious 500 when resourceOwnerMode restricts update access.
|
|
57
|
+
overrideAccess: true,
|
|
48
58
|
req
|
|
49
59
|
});
|
|
50
|
-
|
|
60
|
+
// Strip the cancellation token from the response, consistent with the book
|
|
61
|
+
// endpoint — it must never be echoed back over HTTP.
|
|
62
|
+
const { cancellationToken: _cancellationToken, ...safeReservation } = reservation;
|
|
63
|
+
return Response.json(safeReservation);
|
|
51
64
|
},
|
|
52
65
|
method: 'post',
|
|
53
66
|
path: '/reserve/cancel'
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/endpoints/cancelBooking.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nexport function createCancelBookingEndpoint(config: ResolvedReservationPluginConfig): Endpoint {\n return {\n handler: async (req) => {\n
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/cancelBooking.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { isPrivilegedUser } from '../utilities/userRoles.js'\n\nexport function createCancelBookingEndpoint(config: ResolvedReservationPluginConfig): Endpoint {\n return {\n handler: async (req) => {\n const body = await req.json?.()\n const { reason, reservationId, token } = (body ?? {}) as {\n reason?: string\n reservationId?: string\n token?: string\n }\n\n if (!reservationId) {\n return Response.json({ message: 'reservationId is required' }, { status: 400 })\n }\n\n // Fetch the reservation to check ownership / token.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const existing = await (req.payload.findByID as any)({\n id: reservationId,\n collection: config.slugs.reservations,\n depth: 0,\n overrideAccess: true,\n req,\n })\n\n if (req.user) {\n // Authenticated path: owner (customer === req.user) or admin/staff.\n const customerId =\n typeof existing.customer === 'object' ? existing.customer?.id : existing.customer\n const isOwner = customerId === req.user.id\n // Staff/admin detection (role-aware for single-collection deployments)\n const isAdmin = isPrivilegedUser(req.user, config)\n if (!isOwner && !isAdmin) {\n return Response.json({ message: 'Forbidden' }, { status: 403 })\n }\n } else {\n // Guest path: match the cancellation token.\n if (!token || !existing.cancellationToken || token !== existing.cancellationToken) {\n return Response.json({ message: 'Forbidden' }, { status: 403 })\n }\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const reservation = await (req.payload.update as any)({\n id: reservationId,\n collection: config.slugs.reservations,\n data: {\n cancellationReason: reason,\n status: 'cancelled',\n },\n // Authorization (owner / admin / matching token) is already enforced above.\n // overrideAccess lets the write proceed for anonymous guest cancellations,\n // and avoids a spurious 500 when resourceOwnerMode restricts update access.\n overrideAccess: true,\n req,\n })\n\n // Strip the cancellation token from the response, consistent with the book\n // endpoint — it must never be echoed back over HTTP.\n const { cancellationToken: _cancellationToken, ...safeReservation } =\n reservation as Record<string, unknown>\n\n return Response.json(safeReservation)\n },\n method: 'post',\n path: '/reserve/cancel',\n }\n}\n"],"names":["isPrivilegedUser","createCancelBookingEndpoint","config","handler","req","body","json","reason","reservationId","token","Response","message","status","existing","payload","findByID","id","collection","slugs","reservations","depth","overrideAccess","user","customerId","customer","isOwner","isAdmin","cancellationToken","reservation","update","data","cancellationReason","_cancellationToken","safeReservation","method","path"],"mappings":"AAIA,SAASA,gBAAgB,QAAQ,4BAA2B;AAE5D,OAAO,SAASC,4BAA4BC,MAAuC;IACjF,OAAO;QACLC,SAAS,OAAOC;YACd,MAAMC,OAAO,MAAMD,IAAIE,IAAI;YAC3B,MAAM,EAAEC,MAAM,EAAEC,aAAa,EAAEC,KAAK,EAAE,GAAIJ,QAAQ,CAAC;YAMnD,IAAI,CAACG,eAAe;gBAClB,OAAOE,SAASJ,IAAI,CAAC;oBAAEK,SAAS;gBAA4B,GAAG;oBAAEC,QAAQ;gBAAI;YAC/E;YAEA,oDAAoD;YACpD,8DAA8D;YAC9D,MAAMC,WAAW,MAAM,AAACT,IAAIU,OAAO,CAACC,QAAQ,CAAS;gBACnDC,IAAIR;gBACJS,YAAYf,OAAOgB,KAAK,CAACC,YAAY;gBACrCC,OAAO;gBACPC,gBAAgB;gBAChBjB;YACF;YAEA,IAAIA,IAAIkB,IAAI,EAAE;gBACZ,oEAAoE;gBACpE,MAAMC,aACJ,OAAOV,SAASW,QAAQ,KAAK,WAAWX,SAASW,QAAQ,EAAER,KAAKH,SAASW,QAAQ;gBACnF,MAAMC,UAAUF,eAAenB,IAAIkB,IAAI,CAACN,EAAE;gBAC1C,uEAAuE;gBACvE,MAAMU,UAAU1B,iBAAiBI,IAAIkB,IAAI,EAAEpB;gBAC3C,IAAI,CAACuB,WAAW,CAACC,SAAS;oBACxB,OAAOhB,SAASJ,IAAI,CAAC;wBAAEK,SAAS;oBAAY,GAAG;wBAAEC,QAAQ;oBAAI;gBAC/D;YACF,OAAO;gBACL,4CAA4C;gBAC5C,IAAI,CAACH,SAAS,CAACI,SAASc,iBAAiB,IAAIlB,UAAUI,SAASc,iBAAiB,EAAE;oBACjF,OAAOjB,SAASJ,IAAI,CAAC;wBAAEK,SAAS;oBAAY,GAAG;wBAAEC,QAAQ;oBAAI;gBAC/D;YACF;YAEA,8DAA8D;YAC9D,MAAMgB,cAAc,MAAM,AAACxB,IAAIU,OAAO,CAACe,MAAM,CAAS;gBACpDb,IAAIR;gBACJS,YAAYf,OAAOgB,KAAK,CAACC,YAAY;gBACrCW,MAAM;oBACJC,oBAAoBxB;oBACpBK,QAAQ;gBACV;gBACA,4EAA4E;gBAC5E,2EAA2E;gBAC3E,4EAA4E;gBAC5ES,gBAAgB;gBAChBjB;YACF;YAEA,2EAA2E;YAC3E,qDAAqD;YACrD,MAAM,EAAEuB,mBAAmBK,kBAAkB,EAAE,GAAGC,iBAAiB,GACjEL;YAEF,OAAOlB,SAASJ,IAAI,CAAC2B;QACvB;QACAC,QAAQ;QACRC,MAAM;IACR;AACF"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getAvailableSlots } from '../services/AvailabilityService.js';
|
|
2
|
+
import { extractId, mergeResourceIds } from '../utilities/resolveRequiredResources.js';
|
|
2
3
|
export function createCheckAvailabilityEndpoint(config) {
|
|
3
4
|
return {
|
|
4
5
|
handler: async (req)=>{
|
|
@@ -22,6 +23,20 @@ export function createCheckAvailabilityEndpoint(config) {
|
|
|
22
23
|
});
|
|
23
24
|
}
|
|
24
25
|
const guestCount = Math.max(Number(url.searchParams.get('guestCount') ?? '1'), 1);
|
|
26
|
+
// Resolve required resource set: caller resource(s) ∪ service.requiredResources
|
|
27
|
+
const explicit = url.searchParams.get('resources');
|
|
28
|
+
const callerIds = explicit ? explicit.split(',').map((s)=>s.trim()).filter(Boolean) : [
|
|
29
|
+
resource
|
|
30
|
+
];
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
+
const svcDoc = await req.payload.findByID({
|
|
33
|
+
id: service,
|
|
34
|
+
collection: config.slugs.services,
|
|
35
|
+
depth: 0,
|
|
36
|
+
req
|
|
37
|
+
});
|
|
38
|
+
const requiredIds = (svcDoc?.requiredResources ?? []).map((r)=>extractId(r)).filter((r)=>r !== undefined);
|
|
39
|
+
const resourceIds = mergeResourceIds(callerIds, requiredIds);
|
|
25
40
|
const slots = await getAvailableSlots({
|
|
26
41
|
blockingStatuses: config.statusMachine.blockingStatuses,
|
|
27
42
|
date: parsedDate,
|
|
@@ -29,7 +44,7 @@ export function createCheckAvailabilityEndpoint(config) {
|
|
|
29
44
|
payload: req.payload,
|
|
30
45
|
req,
|
|
31
46
|
reservationSlug: config.slugs.reservations,
|
|
32
|
-
|
|
47
|
+
resourceIds,
|
|
33
48
|
resourceSlug: config.slugs.resources,
|
|
34
49
|
scheduleSlug: config.slugs.schedules,
|
|
35
50
|
serviceId: service,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/endpoints/checkAvailability.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { getAvailableSlots } from '../services/AvailabilityService.js'\n\nexport function createCheckAvailabilityEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n const url = new URL(req.url!)\n const date = url.searchParams.get('date')\n const resource = url.searchParams.get('resource')\n const service = url.searchParams.get('service')\n\n if (!date || !resource || !service) {\n return Response.json(\n { message: 'Missing required query params: resource, date, service' },\n { status: 400 },\n )\n }\n\n const parsedDate = new Date(date)\n if (isNaN(parsedDate.getTime())) {\n return Response.json(\n { error: 'Invalid date format. Expected YYYY-MM-DD' },\n { status: 400 },\n )\n }\n\n const guestCount = Math.max(Number(url.searchParams.get('guestCount') ?? '1'), 1)\n\n const slots = await getAvailableSlots({\n blockingStatuses: config.statusMachine.blockingStatuses,\n date: parsedDate,\n guestCount,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/checkAvailability.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { getAvailableSlots } from '../services/AvailabilityService.js'\nimport { extractId, mergeResourceIds } from '../utilities/resolveRequiredResources.js'\n\nexport function createCheckAvailabilityEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n const url = new URL(req.url!)\n const date = url.searchParams.get('date')\n const resource = url.searchParams.get('resource')\n const service = url.searchParams.get('service')\n\n if (!date || !resource || !service) {\n return Response.json(\n { message: 'Missing required query params: resource, date, service' },\n { status: 400 },\n )\n }\n\n const parsedDate = new Date(date)\n if (isNaN(parsedDate.getTime())) {\n return Response.json(\n { error: 'Invalid date format. Expected YYYY-MM-DD' },\n { status: 400 },\n )\n }\n\n const guestCount = Math.max(Number(url.searchParams.get('guestCount') ?? '1'), 1)\n\n // Resolve required resource set: caller resource(s) ∪ service.requiredResources\n const explicit = url.searchParams.get('resources')\n const callerIds = explicit ? explicit.split(',').map((s) => s.trim()).filter(Boolean) : [resource]\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const svcDoc = await (req.payload.findByID as any)({\n id: service,\n collection: config.slugs.services,\n depth: 0,\n req,\n })\n const requiredIds = ((svcDoc?.requiredResources as unknown[]) ?? [])\n .map((r) => extractId(r))\n .filter((r): r is number | string => r !== undefined)\n const resourceIds = mergeResourceIds(callerIds, requiredIds)\n\n const slots = await getAvailableSlots({\n blockingStatuses: config.statusMachine.blockingStatuses,\n date: parsedDate,\n guestCount,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceIds,\n resourceSlug: config.slugs.resources,\n scheduleSlug: config.slugs.schedules,\n serviceId: service,\n serviceSlug: config.slugs.services,\n })\n\n return Response.json({ slots })\n },\n method: 'get',\n path: '/reserve/availability',\n }\n}\n"],"names":["getAvailableSlots","extractId","mergeResourceIds","createCheckAvailabilityEndpoint","config","handler","req","url","URL","date","searchParams","get","resource","service","Response","json","message","status","parsedDate","Date","isNaN","getTime","error","guestCount","Math","max","Number","explicit","callerIds","split","map","s","trim","filter","Boolean","svcDoc","payload","findByID","id","collection","slugs","services","depth","requiredIds","requiredResources","r","undefined","resourceIds","slots","blockingStatuses","statusMachine","reservationSlug","reservations","resourceSlug","resources","scheduleSlug","schedules","serviceId","serviceSlug","method","path"],"mappings":"AAIA,SAASA,iBAAiB,QAAQ,qCAAoC;AACtE,SAASC,SAAS,EAAEC,gBAAgB,QAAQ,2CAA0C;AAEtF,OAAO,SAASC,gCACdC,MAAuC;IAEvC,OAAO;QACLC,SAAS,OAAOC;YACd,MAAMC,MAAM,IAAIC,IAAIF,IAAIC,GAAG;YAC3B,MAAME,OAAOF,IAAIG,YAAY,CAACC,GAAG,CAAC;YAClC,MAAMC,WAAWL,IAAIG,YAAY,CAACC,GAAG,CAAC;YACtC,MAAME,UAAUN,IAAIG,YAAY,CAACC,GAAG,CAAC;YAErC,IAAI,CAACF,QAAQ,CAACG,YAAY,CAACC,SAAS;gBAClC,OAAOC,SAASC,IAAI,CAClB;oBAAEC,SAAS;gBAAyD,GACpE;oBAAEC,QAAQ;gBAAI;YAElB;YAEA,MAAMC,aAAa,IAAIC,KAAKV;YAC5B,IAAIW,MAAMF,WAAWG,OAAO,KAAK;gBAC/B,OAAOP,SAASC,IAAI,CAClB;oBAAEO,OAAO;gBAA2C,GACpD;oBAAEL,QAAQ;gBAAI;YAElB;YAEA,MAAMM,aAAaC,KAAKC,GAAG,CAACC,OAAOnB,IAAIG,YAAY,CAACC,GAAG,CAAC,iBAAiB,MAAM;YAE/E,gFAAgF;YAChF,MAAMgB,WAAWpB,IAAIG,YAAY,CAACC,GAAG,CAAC;YACtC,MAAMiB,YAAYD,WAAWA,SAASE,KAAK,CAAC,KAAKC,GAAG,CAAC,CAACC,IAAMA,EAAEC,IAAI,IAAIC,MAAM,CAACC,WAAW;gBAACtB;aAAS;YAElG,8DAA8D;YAC9D,MAAMuB,SAAS,MAAM,AAAC7B,IAAI8B,OAAO,CAACC,QAAQ,CAAS;gBACjDC,IAAIzB;gBACJ0B,YAAYnC,OAAOoC,KAAK,CAACC,QAAQ;gBACjCC,OAAO;gBACPpC;YACF;YACA,MAAMqC,cAAc,AAAC,CAAA,AAACR,QAAQS,qBAAmC,EAAE,AAAD,EAC/Dd,GAAG,CAAC,CAACe,IAAM5C,UAAU4C,IACrBZ,MAAM,CAAC,CAACY,IAA4BA,MAAMC;YAC7C,MAAMC,cAAc7C,iBAAiB0B,WAAWe;YAEhD,MAAMK,QAAQ,MAAMhD,kBAAkB;gBACpCiD,kBAAkB7C,OAAO8C,aAAa,CAACD,gBAAgB;gBACvDxC,MAAMS;gBACNK;gBACAa,SAAS9B,IAAI8B,OAAO;gBACpB9B;gBACA6C,iBAAiB/C,OAAOoC,KAAK,CAACY,YAAY;gBAC1CL;gBACAM,cAAcjD,OAAOoC,KAAK,CAACc,SAAS;gBACpCC,cAAcnD,OAAOoC,KAAK,CAACgB,SAAS;gBACpCC,WAAW5C;gBACX6C,aAAatD,OAAOoC,KAAK,CAACC,QAAQ;YACpC;YAEA,OAAO3B,SAASC,IAAI,CAAC;gBAAEiC;YAAM;QAC/B;QACAW,QAAQ;QACRC,MAAM;IACR;AACF"}
|
|
@@ -20,7 +20,10 @@ export function createBookingEndpoint(config) {
|
|
|
20
20
|
data: bookingData,
|
|
21
21
|
req
|
|
22
22
|
});
|
|
23
|
-
|
|
23
|
+
// Never expose the cancellation token in the HTTP response — it is delivered
|
|
24
|
+
// to the guest by the host project via the afterBookingCreate hook.
|
|
25
|
+
const { cancellationToken: _cancellationToken, ...safeReservation } = reservation;
|
|
26
|
+
return Response.json(safeReservation, {
|
|
24
27
|
status: 201
|
|
25
28
|
});
|
|
26
29
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/endpoints/createBooking.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nexport function createBookingEndpoint(config: ResolvedReservationPluginConfig): Endpoint {\n return {\n handler: async (req) => {\n const data = (await req.json?.()) as Record<string, unknown>\n\n // Call beforeBookingCreate plugin hooks before creating the reservation\n let bookingData = data\n if (config.hooks?.beforeBookingCreate) {\n for (const hook of config.hooks.beforeBookingCreate) {\n bookingData = (await hook({ data: bookingData, req })) ?? bookingData\n }\n }\n\n // Create via Payload Local API — collection hooks handle conflict detection,\n // endTime calculation, and status transitions\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const reservation = await (req.payload.create as any)({\n collection: config.slugs.reservations,\n data: bookingData,\n req,\n })\n\n return Response.json(
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/createBooking.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nexport function createBookingEndpoint(config: ResolvedReservationPluginConfig): Endpoint {\n return {\n handler: async (req) => {\n const data = (await req.json?.()) as Record<string, unknown>\n\n // Call beforeBookingCreate plugin hooks before creating the reservation\n let bookingData = data\n if (config.hooks?.beforeBookingCreate) {\n for (const hook of config.hooks.beforeBookingCreate) {\n bookingData = (await hook({ data: bookingData, req })) ?? bookingData\n }\n }\n\n // Create via Payload Local API — collection hooks handle conflict detection,\n // endTime calculation, and status transitions\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const reservation = await (req.payload.create as any)({\n collection: config.slugs.reservations,\n data: bookingData,\n req,\n })\n\n // Never expose the cancellation token in the HTTP response — it is delivered\n // to the guest by the host project via the afterBookingCreate hook.\n const { cancellationToken: _cancellationToken, ...safeReservation } =\n reservation as Record<string, unknown>\n\n return Response.json(safeReservation, { status: 201 })\n },\n method: 'post',\n path: '/reserve/book',\n }\n}\n"],"names":["createBookingEndpoint","config","handler","req","data","json","bookingData","hooks","beforeBookingCreate","hook","reservation","payload","create","collection","slugs","reservations","cancellationToken","_cancellationToken","safeReservation","Response","status","method","path"],"mappings":"AAIA,OAAO,SAASA,sBAAsBC,MAAuC;IAC3E,OAAO;QACLC,SAAS,OAAOC;YACd,MAAMC,OAAQ,MAAMD,IAAIE,IAAI;YAE5B,wEAAwE;YACxE,IAAIC,cAAcF;YAClB,IAAIH,OAAOM,KAAK,EAAEC,qBAAqB;gBACrC,KAAK,MAAMC,QAAQR,OAAOM,KAAK,CAACC,mBAAmB,CAAE;oBACnDF,cAAc,AAAC,MAAMG,KAAK;wBAAEL,MAAME;wBAAaH;oBAAI,MAAOG;gBAC5D;YACF;YAEA,6EAA6E;YAC7E,8CAA8C;YAC9C,8DAA8D;YAC9D,MAAMI,cAAc,MAAM,AAACP,IAAIQ,OAAO,CAACC,MAAM,CAAS;gBACpDC,YAAYZ,OAAOa,KAAK,CAACC,YAAY;gBACrCX,MAAME;gBACNH;YACF;YAEA,6EAA6E;YAC7E,oEAAoE;YACpE,MAAM,EAAEa,mBAAmBC,kBAAkB,EAAE,GAAGC,iBAAiB,GACjER;YAEF,OAAOS,SAASd,IAAI,CAACa,iBAAiB;gBAAEE,QAAQ;YAAI;QACtD;QACAC,QAAQ;QACRC,MAAM;IACR;AACF"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPrivilegedUser, privilegedRoles } from '../utilities/userRoles.js';
|
|
1
2
|
/**
|
|
2
3
|
* Inspect a collection's field list and return the set of top-level named
|
|
3
4
|
* fields as a plain Set<string>. Unnamed fields (rows, groups without a name,
|
|
@@ -21,8 +22,9 @@ export function createCustomerSearchEndpoint(config) {
|
|
|
21
22
|
status: 401
|
|
22
23
|
});
|
|
23
24
|
}
|
|
24
|
-
// Only
|
|
25
|
-
|
|
25
|
+
// Only staff/admin may search customers. Role-aware so it works when staff
|
|
26
|
+
// and customers share one auth collection (userCollection set).
|
|
27
|
+
if (!isPrivilegedUser(req.user, config)) {
|
|
26
28
|
return Response.json({
|
|
27
29
|
message: 'Forbidden'
|
|
28
30
|
}, {
|
|
@@ -40,7 +42,7 @@ export function createCustomerSearchEndpoint(config) {
|
|
|
40
42
|
const hasFirstName = availableFields.has('firstName');
|
|
41
43
|
const hasLastName = availableFields.has('lastName');
|
|
42
44
|
const hasPhone = availableFields.has('phone');
|
|
43
|
-
|
|
45
|
+
const andClauses = [];
|
|
44
46
|
if (search) {
|
|
45
47
|
const orClauses = [];
|
|
46
48
|
if (hasName) {
|
|
@@ -77,10 +79,27 @@ export function createCustomerSearchEndpoint(config) {
|
|
|
77
79
|
}
|
|
78
80
|
});
|
|
79
81
|
}
|
|
80
|
-
|
|
82
|
+
andClauses.push({
|
|
81
83
|
or: orClauses
|
|
82
|
-
};
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
// Single-collection mode: staff/admin live in the same collection as
|
|
87
|
+
// customers, so exclude privileged roles — the dropdown should list only
|
|
88
|
+
// actual customers, not bookable-looking staff.
|
|
89
|
+
if (config.userCollection) {
|
|
90
|
+
const roleField = config.staffProvisioning?.roleField ?? 'role';
|
|
91
|
+
const priv = privilegedRoles(config);
|
|
92
|
+
if (priv.length > 0) {
|
|
93
|
+
andClauses.push({
|
|
94
|
+
[roleField]: {
|
|
95
|
+
not_in: priv
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
83
99
|
}
|
|
100
|
+
const where = andClauses.length === 0 ? {} : andClauses.length === 1 ? andClauses[0] : {
|
|
101
|
+
and: andClauses
|
|
102
|
+
};
|
|
84
103
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
85
104
|
const result = await req.payload.find({
|
|
86
105
|
collection: config.slugs.customers,
|
|
@@ -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\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
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/customerSearch.ts"],"sourcesContent":["import type { CollectionSlug, Endpoint, Field, Where } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { isPrivilegedUser, privilegedRoles } from '../utilities/userRoles.js'\n\n/**\n * Inspect a collection's field list and return the set of top-level named\n * fields as a plain Set<string>. Unnamed fields (rows, groups without a name,\n * etc.) are skipped.\n */\nfunction getNamedFields(fields: Field[]): Set<string> {\n const names = new Set<string>()\n for (const field of fields) {\n if ('name' in field) {\n names.add(field.name)\n }\n }\n return names\n}\n\nexport function createCustomerSearchEndpoint(\n config: ResolvedReservationPluginConfig,\n): Endpoint {\n return {\n handler: async (req) => {\n if (!req.user) {\n return Response.json({ message: 'Unauthorized' }, { status: 401 })\n }\n\n // Only staff/admin may search customers. Role-aware so it works when staff\n // and customers share one auth collection (userCollection set).\n if (!isPrivilegedUser(req.user, config)) {\n return Response.json({ message: 'Forbidden' }, { status: 403 })\n }\n\n const url = new URL(req.url!)\n const search = url.searchParams.get('search') ?? ''\n const limit = Math.min(Number(url.searchParams.get('limit') ?? '10'), 50)\n const page = Math.max(Number(url.searchParams.get('page') ?? '1'), 1)\n\n // Detect which fields exist on the target collection at runtime\n const collectionConfig = req.payload.collections[config.slugs.customers as unknown as CollectionSlug]?.config\n const availableFields: Set<string> = collectionConfig\n ? getNamedFields(collectionConfig.fields)\n : new Set()\n\n const hasName = availableFields.has('name')\n const hasFirstName = availableFields.has('firstName')\n const hasLastName = availableFields.has('lastName')\n const hasPhone = availableFields.has('phone')\n\n const andClauses: Where[] = []\n\n if (search) {\n const orClauses: Where[] = []\n\n if (hasName) {\n orClauses.push({ name: { contains: search } })\n }\n if (hasFirstName) {\n orClauses.push({ firstName: { contains: search } })\n }\n if (hasLastName) {\n orClauses.push({ lastName: { contains: search } })\n }\n // email is always present on auth collections\n orClauses.push({ email: { contains: search } })\n if (hasPhone) {\n orClauses.push({ phone: { contains: search } })\n }\n\n andClauses.push({ or: orClauses })\n }\n\n // Single-collection mode: staff/admin live in the same collection as\n // customers, so exclude privileged roles — the dropdown should list only\n // actual customers, not bookable-looking staff.\n if (config.userCollection) {\n const roleField = config.staffProvisioning?.roleField ?? 'role'\n const priv = privilegedRoles(config)\n if (priv.length > 0) {\n andClauses.push({ [roleField]: { not_in: priv } })\n }\n }\n\n const where: Where =\n andClauses.length === 0\n ? {}\n : andClauses.length === 1\n ? andClauses[0]\n : { and: andClauses }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const result = await (req.payload.find as any)({\n collection: config.slugs.customers,\n limit,\n page,\n where,\n })\n\n return Response.json({\n docs: (result.docs as Record<string, unknown>[]).map((doc) => {\n const entry: Record<string, unknown> = {\n id: doc['id'],\n email: doc['email'] ?? '',\n }\n\n if (hasName) {\n entry['name'] = doc['name'] ?? ''\n }\n if (hasFirstName) {\n entry['firstName'] = doc['firstName'] ?? ''\n }\n if (hasLastName) {\n entry['lastName'] = doc['lastName'] ?? ''\n }\n if (hasPhone) {\n entry['phone'] = doc['phone'] ?? ''\n }\n\n return entry\n }),\n hasNextPage: result.hasNextPage,\n totalDocs: result.totalDocs,\n })\n },\n method: 'get',\n path: '/reservation-customer-search',\n }\n}\n"],"names":["isPrivilegedUser","privilegedRoles","getNamedFields","fields","names","Set","field","add","name","createCustomerSearchEndpoint","config","handler","req","user","Response","json","message","status","url","URL","search","searchParams","get","limit","Math","min","Number","page","max","collectionConfig","payload","collections","slugs","customers","availableFields","hasName","has","hasFirstName","hasLastName","hasPhone","andClauses","orClauses","push","contains","firstName","lastName","email","phone","or","userCollection","roleField","staffProvisioning","priv","length","not_in","where","and","result","find","collection","docs","map","doc","entry","id","hasNextPage","totalDocs","method","path"],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getAvailableSlots } from '../services/AvailabilityService.js';
|
|
2
|
+
import { extractId, mergeResourceIds } from '../utilities/resolveRequiredResources.js';
|
|
2
3
|
export function createGetSlotsEndpoint(config) {
|
|
3
4
|
return {
|
|
4
5
|
handler: async (req)=>{
|
|
@@ -22,6 +23,20 @@ export function createGetSlotsEndpoint(config) {
|
|
|
22
23
|
});
|
|
23
24
|
}
|
|
24
25
|
const guestCount = Math.max(Number(url.searchParams.get('guestCount') ?? '1'), 1);
|
|
26
|
+
// Resolve required resource set: caller resource(s) ∪ service.requiredResources
|
|
27
|
+
const explicit = url.searchParams.get('resources');
|
|
28
|
+
const callerIds = explicit ? explicit.split(',').map((s)=>s.trim()).filter(Boolean) : [
|
|
29
|
+
resource
|
|
30
|
+
];
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
+
const svcDoc = await req.payload.findByID({
|
|
33
|
+
id: service,
|
|
34
|
+
collection: config.slugs.services,
|
|
35
|
+
depth: 0,
|
|
36
|
+
req
|
|
37
|
+
});
|
|
38
|
+
const requiredIds = (svcDoc?.requiredResources ?? []).map((r)=>extractId(r)).filter((r)=>r !== undefined);
|
|
39
|
+
const resourceIds = mergeResourceIds(callerIds, requiredIds);
|
|
25
40
|
const slots = await getAvailableSlots({
|
|
26
41
|
blockingStatuses: config.statusMachine.blockingStatuses,
|
|
27
42
|
date: parsedDate,
|
|
@@ -29,7 +44,7 @@ export function createGetSlotsEndpoint(config) {
|
|
|
29
44
|
payload: req.payload,
|
|
30
45
|
req,
|
|
31
46
|
reservationSlug: config.slugs.reservations,
|
|
32
|
-
|
|
47
|
+
resourceIds,
|
|
33
48
|
resourceSlug: config.slugs.resources,
|
|
34
49
|
scheduleSlug: config.slugs.schedules,
|
|
35
50
|
serviceId: service,
|
|
@@ -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'\n\nexport function createGetSlotsEndpoint(config: ResolvedReservationPluginConfig): Endpoint {\n return {\n handler: async (req) => {\n const url = new URL(req.url!)\n const date = url.searchParams.get('date')\n const resource = url.searchParams.get('resource')\n const service = url.searchParams.get('service')\n\n if (!date || !resource || !service) {\n return Response.json(\n { error: 'Missing required query params: resource, date, service' },\n { status: 400 },\n )\n }\n\n const parsedDate = new Date(date)\n if (isNaN(parsedDate.getTime())) {\n return Response.json(\n { error: 'Invalid date format. Expected YYYY-MM-DD' },\n { status: 400 },\n )\n }\n\n const guestCount = Math.max(Number(url.searchParams.get('guestCount') ?? '1'), 1)\n\n const slots = await getAvailableSlots({\n blockingStatuses: config.statusMachine.blockingStatuses,\n date: parsedDate,\n guestCount,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/getSlots.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { getAvailableSlots } from '../services/AvailabilityService.js'\nimport { extractId, mergeResourceIds } from '../utilities/resolveRequiredResources.js'\n\nexport function createGetSlotsEndpoint(config: ResolvedReservationPluginConfig): Endpoint {\n return {\n handler: async (req) => {\n const url = new URL(req.url!)\n const date = url.searchParams.get('date')\n const resource = url.searchParams.get('resource')\n const service = url.searchParams.get('service')\n\n if (!date || !resource || !service) {\n return Response.json(\n { error: 'Missing required query params: resource, date, service' },\n { status: 400 },\n )\n }\n\n const parsedDate = new Date(date)\n if (isNaN(parsedDate.getTime())) {\n return Response.json(\n { error: 'Invalid date format. Expected YYYY-MM-DD' },\n { status: 400 },\n )\n }\n\n const guestCount = Math.max(Number(url.searchParams.get('guestCount') ?? '1'), 1)\n\n // Resolve required resource set: caller resource(s) ∪ service.requiredResources\n const explicit = url.searchParams.get('resources')\n const callerIds = explicit ? explicit.split(',').map((s) => s.trim()).filter(Boolean) : [resource]\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const svcDoc = await (req.payload.findByID as any)({\n id: service,\n collection: config.slugs.services,\n depth: 0,\n req,\n })\n const requiredIds = ((svcDoc?.requiredResources as unknown[]) ?? [])\n .map((r) => extractId(r))\n .filter((r): r is number | string => r !== undefined)\n const resourceIds = mergeResourceIds(callerIds, requiredIds)\n\n const slots = await getAvailableSlots({\n blockingStatuses: config.statusMachine.blockingStatuses,\n date: parsedDate,\n guestCount,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceIds,\n resourceSlug: config.slugs.resources,\n scheduleSlug: config.slugs.schedules,\n serviceId: service,\n serviceSlug: config.slugs.services,\n })\n\n return Response.json({\n date,\n guestCount,\n slots: slots.map((s) => ({ end: s.end.toISOString(), start: s.start.toISOString() })),\n })\n },\n method: 'get',\n path: '/reserve/slots',\n }\n}\n"],"names":["getAvailableSlots","extractId","mergeResourceIds","createGetSlotsEndpoint","config","handler","req","url","URL","date","searchParams","get","resource","service","Response","json","error","status","parsedDate","Date","isNaN","getTime","guestCount","Math","max","Number","explicit","callerIds","split","map","s","trim","filter","Boolean","svcDoc","payload","findByID","id","collection","slugs","services","depth","requiredIds","requiredResources","r","undefined","resourceIds","slots","blockingStatuses","statusMachine","reservationSlug","reservations","resourceSlug","resources","scheduleSlug","schedules","serviceId","serviceSlug","end","toISOString","start","method","path"],"mappings":"AAIA,SAASA,iBAAiB,QAAQ,qCAAoC;AACtE,SAASC,SAAS,EAAEC,gBAAgB,QAAQ,2CAA0C;AAEtF,OAAO,SAASC,uBAAuBC,MAAuC;IAC5E,OAAO;QACLC,SAAS,OAAOC;YACd,MAAMC,MAAM,IAAIC,IAAIF,IAAIC,GAAG;YAC3B,MAAME,OAAOF,IAAIG,YAAY,CAACC,GAAG,CAAC;YAClC,MAAMC,WAAWL,IAAIG,YAAY,CAACC,GAAG,CAAC;YACtC,MAAME,UAAUN,IAAIG,YAAY,CAACC,GAAG,CAAC;YAErC,IAAI,CAACF,QAAQ,CAACG,YAAY,CAACC,SAAS;gBAClC,OAAOC,SAASC,IAAI,CAClB;oBAAEC,OAAO;gBAAyD,GAClE;oBAAEC,QAAQ;gBAAI;YAElB;YAEA,MAAMC,aAAa,IAAIC,KAAKV;YAC5B,IAAIW,MAAMF,WAAWG,OAAO,KAAK;gBAC/B,OAAOP,SAASC,IAAI,CAClB;oBAAEC,OAAO;gBAA2C,GACpD;oBAAEC,QAAQ;gBAAI;YAElB;YAEA,MAAMK,aAAaC,KAAKC,GAAG,CAACC,OAAOlB,IAAIG,YAAY,CAACC,GAAG,CAAC,iBAAiB,MAAM;YAE/E,gFAAgF;YAChF,MAAMe,WAAWnB,IAAIG,YAAY,CAACC,GAAG,CAAC;YACtC,MAAMgB,YAAYD,WAAWA,SAASE,KAAK,CAAC,KAAKC,GAAG,CAAC,CAACC,IAAMA,EAAEC,IAAI,IAAIC,MAAM,CAACC,WAAW;gBAACrB;aAAS;YAElG,8DAA8D;YAC9D,MAAMsB,SAAS,MAAM,AAAC5B,IAAI6B,OAAO,CAACC,QAAQ,CAAS;gBACjDC,IAAIxB;gBACJyB,YAAYlC,OAAOmC,KAAK,CAACC,QAAQ;gBACjCC,OAAO;gBACPnC;YACF;YACA,MAAMoC,cAAc,AAAC,CAAA,AAACR,QAAQS,qBAAmC,EAAE,AAAD,EAC/Dd,GAAG,CAAC,CAACe,IAAM3C,UAAU2C,IACrBZ,MAAM,CAAC,CAACY,IAA4BA,MAAMC;YAC7C,MAAMC,cAAc5C,iBAAiByB,WAAWe;YAEhD,MAAMK,QAAQ,MAAM/C,kBAAkB;gBACpCgD,kBAAkB5C,OAAO6C,aAAa,CAACD,gBAAgB;gBACvDvC,MAAMS;gBACNI;gBACAa,SAAS7B,IAAI6B,OAAO;gBACpB7B;gBACA4C,iBAAiB9C,OAAOmC,KAAK,CAACY,YAAY;gBAC1CL;gBACAM,cAAchD,OAAOmC,KAAK,CAACc,SAAS;gBACpCC,cAAclD,OAAOmC,KAAK,CAACgB,SAAS;gBACpCC,WAAW3C;gBACX4C,aAAarD,OAAOmC,KAAK,CAACC,QAAQ;YACpC;YAEA,OAAO1B,SAASC,IAAI,CAAC;gBACnBN;gBACAa;gBACAyB,OAAOA,MAAMlB,GAAG,CAAC,CAACC,IAAO,CAAA;wBAAE4B,KAAK5B,EAAE4B,GAAG,CAACC,WAAW;wBAAIC,OAAO9B,EAAE8B,KAAK,CAACD,WAAW;oBAAG,CAAA;YACpF;QACF;QACAE,QAAQ;QACRC,MAAM;IACR;AACF"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Endpoint, Payload } from 'payload';
|
|
2
|
+
import type { ResolvedReservationPluginConfig } from '../types.js';
|
|
3
|
+
type DayAvailability = {
|
|
4
|
+
date: string;
|
|
5
|
+
shiftWindows: Array<{
|
|
6
|
+
end: string;
|
|
7
|
+
start: string;
|
|
8
|
+
}>;
|
|
9
|
+
timeOff: Array<{
|
|
10
|
+
end: string;
|
|
11
|
+
reason?: string;
|
|
12
|
+
start: string;
|
|
13
|
+
type?: string;
|
|
14
|
+
}>;
|
|
15
|
+
};
|
|
16
|
+
type Busy = Array<{
|
|
17
|
+
end: string;
|
|
18
|
+
start: string;
|
|
19
|
+
units: number;
|
|
20
|
+
}>;
|
|
21
|
+
export type ResourceAvailability = {
|
|
22
|
+
busy: Busy;
|
|
23
|
+
capacityMode: 'per-guest' | 'per-reservation';
|
|
24
|
+
days: DayAvailability[];
|
|
25
|
+
quantity: number;
|
|
26
|
+
/** Capacity of resources this resource's services also require (e.g. a chair pool). */
|
|
27
|
+
requiredPools: Array<{
|
|
28
|
+
busy: Busy;
|
|
29
|
+
quantity: number;
|
|
30
|
+
}>;
|
|
31
|
+
};
|
|
32
|
+
export declare function buildResourceAvailability(params: {
|
|
33
|
+
blockingStatuses: string[];
|
|
34
|
+
end: Date;
|
|
35
|
+
payload: Payload;
|
|
36
|
+
reservationSlug: string;
|
|
37
|
+
resourceId: number | string;
|
|
38
|
+
resourceSlug: string;
|
|
39
|
+
scheduleSlug: string;
|
|
40
|
+
start: Date;
|
|
41
|
+
}): Promise<ResourceAvailability>;
|
|
42
|
+
export declare function createResourceAvailabilityEndpoint(config: ResolvedReservationPluginConfig): Endpoint;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { resolveScheduleForDate } from '../utilities/scheduleUtils.js';
|
|
2
|
+
import { localDayKey } from '../utilities/slotUtils.js';
|
|
3
|
+
/** Busy intervals (with capacity units) for one resource over [start, end). */ async function busyFor(args) {
|
|
4
|
+
const { blockingStatuses, capacityMode, end, payload, reservationSlug, resourceId, start } = args;
|
|
5
|
+
const where = {
|
|
6
|
+
and: [
|
|
7
|
+
{
|
|
8
|
+
status: {
|
|
9
|
+
in: blockingStatuses
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
startTime: {
|
|
14
|
+
less_than: end.toISOString()
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
endTime: {
|
|
19
|
+
greater_than: start.toISOString()
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
or: [
|
|
24
|
+
{
|
|
25
|
+
resource: {
|
|
26
|
+
equals: resourceId
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
'items.resource': {
|
|
31
|
+
equals: resourceId
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
};
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
+
const { docs } = await payload.find({
|
|
40
|
+
collection: reservationSlug,
|
|
41
|
+
depth: 0,
|
|
42
|
+
limit: 500,
|
|
43
|
+
where
|
|
44
|
+
});
|
|
45
|
+
return docs.filter((r)=>r.startTime && r.endTime).map((r)=>({
|
|
46
|
+
end: new Date(r.endTime).toISOString(),
|
|
47
|
+
start: new Date(r.startTime).toISOString(),
|
|
48
|
+
units: capacityMode === 'per-guest' ? r.guestCount ?? 1 : 1
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
export async function buildResourceAvailability(params) {
|
|
52
|
+
const { blockingStatuses, end, payload, reservationSlug, resourceId, resourceSlug, scheduleSlug, start } = params;
|
|
53
|
+
// depth 1 so `services` are populated (their `requiredResources` come back as ids)
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
55
|
+
const resource = await payload.findByID({
|
|
56
|
+
id: resourceId,
|
|
57
|
+
collection: resourceSlug,
|
|
58
|
+
depth: 1
|
|
59
|
+
});
|
|
60
|
+
const quantity = resource?.quantity ?? 1;
|
|
61
|
+
const capacityMode = resource?.capacityMode ?? 'per-reservation';
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
63
|
+
const { docs: schedules } = await payload.find({
|
|
64
|
+
collection: scheduleSlug,
|
|
65
|
+
depth: 0,
|
|
66
|
+
limit: 100,
|
|
67
|
+
where: {
|
|
68
|
+
and: [
|
|
69
|
+
{
|
|
70
|
+
active: {
|
|
71
|
+
equals: true
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
resource: {
|
|
76
|
+
equals: resourceId
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
const days = [];
|
|
83
|
+
for(let d = new Date(start); d < end; d = new Date(d.getTime() + 86_400_000)){
|
|
84
|
+
const date = localDayKey(d);
|
|
85
|
+
const shiftWindows = [];
|
|
86
|
+
const timeOff = [];
|
|
87
|
+
const localMidnight = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
88
|
+
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
|
+
const exceptions = sched.exceptions ?? [];
|
|
98
|
+
for (const exc of exceptions){
|
|
99
|
+
const excStart = localDayKey(new Date(exc.date));
|
|
100
|
+
const excEnd = exc.endDate ? localDayKey(new Date(exc.endDate)) : excStart;
|
|
101
|
+
if (date >= excStart && date <= excEnd) {
|
|
102
|
+
const localStart = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0);
|
|
103
|
+
const localEnd = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59, 999);
|
|
104
|
+
timeOff.push({
|
|
105
|
+
type: exc.type,
|
|
106
|
+
end: localEnd.toISOString(),
|
|
107
|
+
reason: exc.reason,
|
|
108
|
+
start: localStart.toISOString()
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
days.push({
|
|
114
|
+
date,
|
|
115
|
+
shiftWindows,
|
|
116
|
+
timeOff
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
const busy = await busyFor({
|
|
120
|
+
blockingStatuses,
|
|
121
|
+
capacityMode,
|
|
122
|
+
end,
|
|
123
|
+
payload,
|
|
124
|
+
reservationSlug,
|
|
125
|
+
resourceId,
|
|
126
|
+
start
|
|
127
|
+
});
|
|
128
|
+
// Resources this resource's services ALSO require (e.g. a shared chair pool).
|
|
129
|
+
// A slot isn't truly bookable if any of these is at capacity, even when the
|
|
130
|
+
// resource itself is free — so the calendar reflects real availability.
|
|
131
|
+
const poolIds = new Set();
|
|
132
|
+
for (const svc of resource?.services ?? []){
|
|
133
|
+
const reqs = (typeof svc === 'object' ? svc.requiredResources : []) ?? [];
|
|
134
|
+
for (const rr of reqs){
|
|
135
|
+
const id = typeof rr === 'object' && rr !== null ? rr.id : rr;
|
|
136
|
+
if (id != null && String(id) !== String(resourceId)) {
|
|
137
|
+
poolIds.add(String(id));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const requiredPools = [];
|
|
142
|
+
for (const poolId of poolIds){
|
|
143
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
144
|
+
const pool = await payload.findByID({
|
|
145
|
+
id: poolId,
|
|
146
|
+
collection: resourceSlug,
|
|
147
|
+
depth: 0
|
|
148
|
+
}).catch(()=>null);
|
|
149
|
+
if (!pool) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const poolCapacityMode = pool.capacityMode ?? 'per-reservation';
|
|
153
|
+
requiredPools.push({
|
|
154
|
+
busy: await busyFor({
|
|
155
|
+
blockingStatuses,
|
|
156
|
+
capacityMode: poolCapacityMode,
|
|
157
|
+
end,
|
|
158
|
+
payload,
|
|
159
|
+
reservationSlug,
|
|
160
|
+
resourceId: poolId,
|
|
161
|
+
start
|
|
162
|
+
}),
|
|
163
|
+
quantity: pool.quantity ?? 1
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
busy,
|
|
168
|
+
capacityMode,
|
|
169
|
+
days,
|
|
170
|
+
quantity,
|
|
171
|
+
requiredPools
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
export function createResourceAvailabilityEndpoint(config) {
|
|
175
|
+
return {
|
|
176
|
+
handler: async (req)=>{
|
|
177
|
+
const url = new URL(req.url);
|
|
178
|
+
const resource = url.searchParams.get('resource');
|
|
179
|
+
const start = url.searchParams.get('start');
|
|
180
|
+
const end = url.searchParams.get('end');
|
|
181
|
+
if (!resource || !start || !end) {
|
|
182
|
+
return Response.json({
|
|
183
|
+
error: 'Missing required query params: resource, start, end'
|
|
184
|
+
}, {
|
|
185
|
+
status: 400
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
const startDate = new Date(start);
|
|
189
|
+
const endDate = new Date(end);
|
|
190
|
+
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
|
|
191
|
+
return Response.json({
|
|
192
|
+
error: 'Invalid start/end date'
|
|
193
|
+
}, {
|
|
194
|
+
status: 400
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
const result = await buildResourceAvailability({
|
|
198
|
+
blockingStatuses: config.statusMachine.blockingStatuses,
|
|
199
|
+
end: endDate,
|
|
200
|
+
payload: req.payload,
|
|
201
|
+
reservationSlug: config.slugs.reservations,
|
|
202
|
+
resourceId: resource,
|
|
203
|
+
resourceSlug: config.slugs.resources,
|
|
204
|
+
scheduleSlug: config.slugs.schedules,
|
|
205
|
+
start: startDate
|
|
206
|
+
});
|
|
207
|
+
return Response.json(result);
|
|
208
|
+
},
|
|
209
|
+
method: 'get',
|
|
210
|
+
path: '/reserve/resource-availability'
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
//# sourceMappingURL=resourceAvailability.js.map
|