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
|
@@ -0,0 +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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"}
|
package/dist/exports/client.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { AvailabilityOverview } from '../components/AvailabilityOverview/index.js';
|
|
2
|
+
export { AvailabilityTimeField } from '../components/AvailabilityTimeField/index.js';
|
|
2
3
|
export { CalendarView } from '../components/CalendarView/index.js';
|
|
3
4
|
export { CustomerField } from '../components/CustomerField/index.js';
|
package/dist/exports/client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { AvailabilityOverview } from '../components/AvailabilityOverview/index.js';
|
|
2
|
+
export { AvailabilityTimeField } from '../components/AvailabilityTimeField/index.js';
|
|
2
3
|
export { CalendarView } from '../components/CalendarView/index.js';
|
|
3
4
|
export { CustomerField } from '../components/CustomerField/index.js';
|
|
4
5
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["export { AvailabilityOverview } from '../components/AvailabilityOverview/index.js'\nexport { CalendarView } from '../components/CalendarView/index.js'\nexport { CustomerField } from '../components/CustomerField/index.js'\n"],"names":["AvailabilityOverview","CalendarView","CustomerField"],"mappings":"AAAA,SAASA,oBAAoB,QAAQ,8CAA6C;AAClF,SAASC,YAAY,QAAQ,sCAAqC;AAClE,SAASC,aAAa,QAAQ,uCAAsC"}
|
|
1
|
+
{"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["export { AvailabilityOverview } from '../components/AvailabilityOverview/index.js'\nexport { AvailabilityTimeField } from '../components/AvailabilityTimeField/index.js'\nexport { CalendarView } from '../components/CalendarView/index.js'\nexport { CustomerField } from '../components/CustomerField/index.js'\n"],"names":["AvailabilityOverview","AvailabilityTimeField","CalendarView","CustomerField"],"mappings":"AAAA,SAASA,oBAAoB,QAAQ,8CAA6C;AAClF,SAASC,qBAAqB,QAAQ,+CAA8C;AACpF,SAASC,YAAY,QAAQ,sCAAqC;AAClE,SAASC,aAAa,QAAQ,uCAAsC"}
|
|
@@ -50,7 +50,11 @@ export const calculateEndTime = (config)=>async ({ context, data, req })=>{
|
|
|
50
50
|
data.endTime = result.endTime.toISOString();
|
|
51
51
|
}
|
|
52
52
|
} else {
|
|
53
|
-
// Multi-resource: compute endTime per item
|
|
53
|
+
// Multi-resource: compute endTime per item, then set a top-level endTime
|
|
54
|
+
// that spans all items so conflict detection (which queries top-level
|
|
55
|
+
// startTime/endTime) can see this reservation.
|
|
56
|
+
let earliestStart;
|
|
57
|
+
let latestEnd;
|
|
54
58
|
for (const item of data.items){
|
|
55
59
|
if (!item.startTime) {
|
|
56
60
|
continue;
|
|
@@ -80,6 +84,22 @@ export const calculateEndTime = (config)=>async ({ context, data, req })=>{
|
|
|
80
84
|
});
|
|
81
85
|
item.endTime = result.endTime.toISOString();
|
|
82
86
|
}
|
|
87
|
+
const start = new Date(item.startTime);
|
|
88
|
+
if (!earliestStart || start < earliestStart) {
|
|
89
|
+
earliestStart = start;
|
|
90
|
+
}
|
|
91
|
+
if (item.endTime) {
|
|
92
|
+
const end = new Date(item.endTime);
|
|
93
|
+
if (!latestEnd || end > latestEnd) {
|
|
94
|
+
latestEnd = end;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (earliestStart) {
|
|
99
|
+
data.startTime = earliestStart.toISOString();
|
|
100
|
+
}
|
|
101
|
+
if (latestEnd) {
|
|
102
|
+
data.endTime = latestEnd.toISOString();
|
|
83
103
|
}
|
|
84
104
|
}
|
|
85
105
|
return data;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/hooks/reservations/calculateEndTime.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { DurationType, ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { computeEndTime } from '../../services/AvailabilityService.js'\nimport { resolveReservationItems } from '../../utilities/resolveReservationItems.js'\n\nexport const calculateEndTime =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, req }) => {\n if (context?.skipReservationHooks) {return data}\n\n if (!data?.startTime || !data?.service) {return data}\n\n const items = resolveReservationItems(data)\n\n if (items.length <= 1) {\n // Single-resource: compute top-level endTime\n const serviceId = typeof data.service === 'object' ? data.service.id : data.service\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (req.payload.findByID as any)({\n id: serviceId,\n collection: config.slugs.services,\n req,\n })\n\n if (!service?.duration && service?.durationType !== 'full-day') {return data}\n\n const durationType = ((service.durationType as string) ?? 'fixed') as DurationType\n const startDate = new Date(data.startTime)\n\n if (durationType === 'flexible') {\n if (!data.endTime) {\n throw new ValidationError({\n errors: [{ message: 'endTime is required for flexible duration services', path: 'endTime' }],\n })\n }\n // Validate customer-provided endTime (computeEndTime returns it back)\n computeEndTime({\n durationType: 'flexible',\n endTime: new Date(data.endTime),\n serviceDuration: service.duration as number,\n startTime: startDate,\n })\n } else {\n const result = computeEndTime({\n durationType,\n serviceDuration: (service.duration as number) ?? 0,\n startTime: startDate,\n })\n data.endTime = result.endTime.toISOString()\n }\n } else {\n // Multi-resource: compute endTime per item\n for (const item of data.items as Array<Record<string, unknown>>) {\n if (!item.startTime) {continue}\n\n const itemServiceId = typeof item.service === 'object'\n ? (item.service as { id: string }).id\n : (item.service as string) ?? (typeof data.service === 'object' ? data.service.id : data.service)\n\n if (!itemServiceId) {continue}\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (req.payload.findByID as any)({\n id: itemServiceId,\n collection: config.slugs.services,\n req,\n })\n\n if (!service?.duration && service?.durationType !== 'full-day') {continue}\n\n const durationType = ((service.durationType as string) ?? 'fixed') as DurationType\n\n if (durationType === 'flexible' && !item.endTime) {continue}\n\n if (durationType !== 'flexible') {\n const result = computeEndTime({\n durationType,\n serviceDuration: (service.duration as number) ?? 0,\n startTime: new Date(item.startTime as string),\n })\n item.endTime = result.endTime.toISOString()\n }\n }\n }\n\n return data\n }\n"],"names":["ValidationError","computeEndTime","resolveReservationItems","calculateEndTime","config","context","data","req","skipReservationHooks","startTime","service","items","length","serviceId","id","payload","findByID","collection","slugs","services","duration","durationType","startDate","Date","endTime","errors","message","path","serviceDuration","result","toISOString","item","itemServiceId"],"mappings":"AAEA,SAASA,eAAe,QAAQ,UAAS;AAIzC,SAASC,cAAc,QAAQ,wCAAuC;AACtE,SAASC,uBAAuB,QAAQ,6CAA4C;AAEpF,OAAO,MAAMC,mBACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,IAAI,EAAEC,GAAG,EAAE;QAC3B,IAAIF,SAASG,sBAAsB;YAAC,OAAOF;QAAI;QAE/C,IAAI,CAACA,MAAMG,aAAa,CAACH,MAAMI,SAAS;YAAC,OAAOJ;QAAI;QAEpD,MAAMK,QAAQT,wBAAwBI;QAEtC,IAAIK,MAAMC,MAAM,IAAI,GAAG;YACrB,6CAA6C;YAC7C,MAAMC,YAAY,OAAOP,KAAKI,OAAO,KAAK,WAAWJ,KAAKI,OAAO,CAACI,EAAE,GAAGR,KAAKI,OAAO;YAEnF,8DAA8D;YAC9D,MAAMA,UAAU,MAAM,AAACH,IAAIQ,OAAO,CAACC,QAAQ,CAAS;gBAClDF,IAAID;gBACJI,YAAYb,OAAOc,KAAK,CAACC,QAAQ;gBACjCZ;YACF;YAEA,IAAI,CAACG,SAASU,YAAYV,SAASW,iBAAiB,YAAY;gBAAC,OAAOf;YAAI;YAE5E,MAAMe,eAAgB,AAACX,QAAQW,YAAY,IAAe;YAC1D,MAAMC,YAAY,IAAIC,KAAKjB,KAAKG,SAAS;YAEzC,IAAIY,iBAAiB,YAAY;gBAC/B,IAAI,CAACf,KAAKkB,OAAO,EAAE;oBACjB,MAAM,IAAIxB,gBAAgB;wBACxByB,QAAQ;4BAAC;gCAAEC,SAAS;gCAAsDC,MAAM;4BAAU;yBAAE;oBAC9F;gBACF;gBACA,sEAAsE;gBACtE1B,eAAe;oBACboB,cAAc;oBACdG,SAAS,IAAID,KAAKjB,KAAKkB,OAAO;oBAC9BI,iBAAiBlB,QAAQU,QAAQ;oBACjCX,WAAWa;gBACb;YACF,OAAO;gBACL,MAAMO,SAAS5B,eAAe;oBAC5BoB;oBACAO,iBAAiB,AAAClB,QAAQU,QAAQ,IAAe;oBACjDX,WAAWa;gBACb;gBACAhB,KAAKkB,OAAO,GAAGK,OAAOL,OAAO,CAACM,WAAW;YAC3C;QACF,OAAO;YACL,
|
|
1
|
+
{"version":3,"sources":["../../../src/hooks/reservations/calculateEndTime.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { DurationType, ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { computeEndTime } from '../../services/AvailabilityService.js'\nimport { resolveReservationItems } from '../../utilities/resolveReservationItems.js'\n\nexport const calculateEndTime =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, req }) => {\n if (context?.skipReservationHooks) {return data}\n\n if (!data?.startTime || !data?.service) {return data}\n\n const items = resolveReservationItems(data)\n\n if (items.length <= 1) {\n // Single-resource: compute top-level endTime\n const serviceId = typeof data.service === 'object' ? data.service.id : data.service\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (req.payload.findByID as any)({\n id: serviceId,\n collection: config.slugs.services,\n req,\n })\n\n if (!service?.duration && service?.durationType !== 'full-day') {return data}\n\n const durationType = ((service.durationType as string) ?? 'fixed') as DurationType\n const startDate = new Date(data.startTime)\n\n if (durationType === 'flexible') {\n if (!data.endTime) {\n throw new ValidationError({\n errors: [{ message: 'endTime is required for flexible duration services', path: 'endTime' }],\n })\n }\n // Validate customer-provided endTime (computeEndTime returns it back)\n computeEndTime({\n durationType: 'flexible',\n endTime: new Date(data.endTime),\n serviceDuration: service.duration as number,\n startTime: startDate,\n })\n } else {\n const result = computeEndTime({\n durationType,\n serviceDuration: (service.duration as number) ?? 0,\n startTime: startDate,\n })\n data.endTime = result.endTime.toISOString()\n }\n } else {\n // Multi-resource: compute endTime per item, then set a top-level endTime\n // that spans all items so conflict detection (which queries top-level\n // startTime/endTime) can see this reservation.\n let earliestStart: Date | undefined\n let latestEnd: Date | undefined\n for (const item of data.items as Array<Record<string, unknown>>) {\n if (!item.startTime) {continue}\n\n const itemServiceId = typeof item.service === 'object'\n ? (item.service as { id: string }).id\n : (item.service as string) ?? (typeof data.service === 'object' ? data.service.id : data.service)\n\n if (!itemServiceId) {continue}\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (req.payload.findByID as any)({\n id: itemServiceId,\n collection: config.slugs.services,\n req,\n })\n\n if (!service?.duration && service?.durationType !== 'full-day') {continue}\n\n const durationType = ((service.durationType as string) ?? 'fixed') as DurationType\n\n if (durationType === 'flexible' && !item.endTime) {continue}\n\n if (durationType !== 'flexible') {\n const result = computeEndTime({\n durationType,\n serviceDuration: (service.duration as number) ?? 0,\n startTime: new Date(item.startTime as string),\n })\n item.endTime = result.endTime.toISOString()\n }\n\n const start = new Date(item.startTime as string)\n if (!earliestStart || start < earliestStart) {earliestStart = start}\n\n if (item.endTime) {\n const end = new Date(item.endTime as string)\n if (!latestEnd || end > latestEnd) {latestEnd = end}\n }\n }\n\n if (earliestStart) {\n data.startTime = earliestStart.toISOString()\n }\n\n if (latestEnd) {\n data.endTime = latestEnd.toISOString()\n }\n }\n\n return data\n }\n"],"names":["ValidationError","computeEndTime","resolveReservationItems","calculateEndTime","config","context","data","req","skipReservationHooks","startTime","service","items","length","serviceId","id","payload","findByID","collection","slugs","services","duration","durationType","startDate","Date","endTime","errors","message","path","serviceDuration","result","toISOString","earliestStart","latestEnd","item","itemServiceId","start","end"],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CollectionBeforeChangeHook } from 'payload';
|
|
2
|
+
import type { ResolvedReservationPluginConfig } from '../../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Expand a service's `requiredResources` into the reservation's `items[]` so the
|
|
5
|
+
* booking actually occupies every required resource pool. Runs before
|
|
6
|
+
* calculateEndTime and validateConflicts so the appended items get endTimes and
|
|
7
|
+
* are conflict-checked.
|
|
8
|
+
*/
|
|
9
|
+
export declare const expandRequiredResources: (config: ResolvedReservationPluginConfig) => CollectionBeforeChangeHook;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { extractId } from '../../utilities/resolveRequiredResources.js';
|
|
2
|
+
/**
|
|
3
|
+
* Expand a service's `requiredResources` into the reservation's `items[]` so the
|
|
4
|
+
* booking actually occupies every required resource pool. Runs before
|
|
5
|
+
* calculateEndTime and validateConflicts so the appended items get endTimes and
|
|
6
|
+
* are conflict-checked.
|
|
7
|
+
*/ export const expandRequiredResources = (config)=>async ({ context, data, operation, req })=>{
|
|
8
|
+
if (context?.skipReservationHooks) {
|
|
9
|
+
return data;
|
|
10
|
+
}
|
|
11
|
+
if (operation !== 'create') {
|
|
12
|
+
return data;
|
|
13
|
+
}
|
|
14
|
+
const serviceId = extractId(data?.service);
|
|
15
|
+
if (!serviceId || !data?.startTime) {
|
|
16
|
+
return data;
|
|
17
|
+
}
|
|
18
|
+
let required = [];
|
|
19
|
+
try {
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
|
+
const service = await req.payload.findByID({
|
|
22
|
+
id: serviceId,
|
|
23
|
+
collection: config.slugs.services,
|
|
24
|
+
depth: 0,
|
|
25
|
+
req
|
|
26
|
+
});
|
|
27
|
+
required = (service?.requiredResources ?? []).map((r)=>extractId(r)).filter((r)=>r !== undefined);
|
|
28
|
+
} catch {
|
|
29
|
+
return data;
|
|
30
|
+
}
|
|
31
|
+
if (required.length === 0) {
|
|
32
|
+
return data;
|
|
33
|
+
}
|
|
34
|
+
const existingItems = Array.isArray(data.items) ? [
|
|
35
|
+
...data.items
|
|
36
|
+
] : [];
|
|
37
|
+
const present = new Set();
|
|
38
|
+
if (existingItems.length > 0) {
|
|
39
|
+
for (const it of existingItems){
|
|
40
|
+
const r = extractId(it.resource);
|
|
41
|
+
if (r !== undefined) {
|
|
42
|
+
present.add(String(r));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
const primary = extractId(data.resource);
|
|
47
|
+
if (primary !== undefined) {
|
|
48
|
+
present.add(String(primary));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const additions = required.filter((r)=>!present.has(String(r)));
|
|
52
|
+
if (additions.length === 0) {
|
|
53
|
+
return data;
|
|
54
|
+
}
|
|
55
|
+
const items = [
|
|
56
|
+
...existingItems
|
|
57
|
+
];
|
|
58
|
+
if (existingItems.length === 0) {
|
|
59
|
+
const primary = extractId(data.resource);
|
|
60
|
+
if (primary !== undefined) {
|
|
61
|
+
items.push({
|
|
62
|
+
endTime: data.endTime,
|
|
63
|
+
resource: primary,
|
|
64
|
+
service: serviceId,
|
|
65
|
+
startTime: data.startTime
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (const r of additions){
|
|
70
|
+
items.push({
|
|
71
|
+
endTime: data.endTime,
|
|
72
|
+
resource: r,
|
|
73
|
+
service: serviceId,
|
|
74
|
+
startTime: data.startTime
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
data.items = items;
|
|
78
|
+
return data;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
//# sourceMappingURL=expandRequiredResources.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/hooks/reservations/expandRequiredResources.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { extractId } from '../../utilities/resolveRequiredResources.js'\n\n/**\n * Expand a service's `requiredResources` into the reservation's `items[]` so the\n * booking actually occupies every required resource pool. Runs before\n * calculateEndTime and validateConflicts so the appended items get endTimes and\n * are conflict-checked.\n */\nexport const expandRequiredResources =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, req }) => {\n if (context?.skipReservationHooks) {return data}\n if (operation !== 'create') {return data}\n\n const serviceId = extractId(data?.service)\n if (!serviceId || !data?.startTime) {return data}\n\n let required: Array<number | string> = []\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (req.payload.findByID as any)({\n id: serviceId,\n collection: config.slugs.services,\n depth: 0,\n req,\n })\n required = ((service?.requiredResources as unknown[]) ?? [])\n .map((r) => extractId(r))\n .filter((r): r is number | string => r !== undefined)\n } catch {\n return data\n }\n\n if (required.length === 0) {return data}\n\n const existingItems = Array.isArray(data.items)\n ? [...(data.items as Array<Record<string, unknown>>)]\n : []\n const present = new Set<string>()\n if (existingItems.length > 0) {\n for (const it of existingItems) {\n const r = extractId(it.resource)\n if (r !== undefined) {present.add(String(r))}\n }\n } else {\n const primary = extractId(data.resource)\n if (primary !== undefined) {present.add(String(primary))}\n }\n\n const additions = required.filter((r) => !present.has(String(r)))\n if (additions.length === 0) {return data}\n\n const items: Array<Record<string, unknown>> = [...existingItems]\n if (existingItems.length === 0) {\n const primary = extractId(data.resource)\n if (primary !== undefined) {\n items.push({\n endTime: data.endTime,\n resource: primary,\n service: serviceId,\n startTime: data.startTime,\n })\n }\n }\n\n for (const r of additions) {\n items.push({\n endTime: data.endTime,\n resource: r,\n service: serviceId,\n startTime: data.startTime,\n })\n }\n\n data.items = items\n return data\n }\n"],"names":["extractId","expandRequiredResources","config","context","data","operation","req","skipReservationHooks","serviceId","service","startTime","required","payload","findByID","id","collection","slugs","services","depth","requiredResources","map","r","filter","undefined","length","existingItems","Array","isArray","items","present","Set","it","resource","add","String","primary","additions","has","push","endTime"],"mappings":"AAIA,SAASA,SAAS,QAAQ,8CAA6C;AAEvE;;;;;CAKC,GACD,OAAO,MAAMC,0BACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,IAAI,EAAEC,SAAS,EAAEC,GAAG,EAAE;QACtC,IAAIH,SAASI,sBAAsB;YAAC,OAAOH;QAAI;QAC/C,IAAIC,cAAc,UAAU;YAAC,OAAOD;QAAI;QAExC,MAAMI,YAAYR,UAAUI,MAAMK;QAClC,IAAI,CAACD,aAAa,CAACJ,MAAMM,WAAW;YAAC,OAAON;QAAI;QAEhD,IAAIO,WAAmC,EAAE;QACzC,IAAI;YACF,8DAA8D;YAC9D,MAAMF,UAAU,MAAM,AAACH,IAAIM,OAAO,CAACC,QAAQ,CAAS;gBAClDC,IAAIN;gBACJO,YAAYb,OAAOc,KAAK,CAACC,QAAQ;gBACjCC,OAAO;gBACPZ;YACF;YACAK,WAAW,AAAC,CAAA,AAACF,SAASU,qBAAmC,EAAE,AAAD,EACvDC,GAAG,CAAC,CAACC,IAAMrB,UAAUqB,IACrBC,MAAM,CAAC,CAACD,IAA4BA,MAAME;QAC/C,EAAE,OAAM;YACN,OAAOnB;QACT;QAEA,IAAIO,SAASa,MAAM,KAAK,GAAG;YAAC,OAAOpB;QAAI;QAEvC,MAAMqB,gBAAgBC,MAAMC,OAAO,CAACvB,KAAKwB,KAAK,IAC1C;eAAKxB,KAAKwB,KAAK;SAAoC,GACnD,EAAE;QACN,MAAMC,UAAU,IAAIC;QACpB,IAAIL,cAAcD,MAAM,GAAG,GAAG;YAC5B,KAAK,MAAMO,MAAMN,cAAe;gBAC9B,MAAMJ,IAAIrB,UAAU+B,GAAGC,QAAQ;gBAC/B,IAAIX,MAAME,WAAW;oBAACM,QAAQI,GAAG,CAACC,OAAOb;gBAAG;YAC9C;QACF,OAAO;YACL,MAAMc,UAAUnC,UAAUI,KAAK4B,QAAQ;YACvC,IAAIG,YAAYZ,WAAW;gBAACM,QAAQI,GAAG,CAACC,OAAOC;YAAS;QAC1D;QAEA,MAAMC,YAAYzB,SAASW,MAAM,CAAC,CAACD,IAAM,CAACQ,QAAQQ,GAAG,CAACH,OAAOb;QAC7D,IAAIe,UAAUZ,MAAM,KAAK,GAAG;YAAC,OAAOpB;QAAI;QAExC,MAAMwB,QAAwC;eAAIH;SAAc;QAChE,IAAIA,cAAcD,MAAM,KAAK,GAAG;YAC9B,MAAMW,UAAUnC,UAAUI,KAAK4B,QAAQ;YACvC,IAAIG,YAAYZ,WAAW;gBACzBK,MAAMU,IAAI,CAAC;oBACTC,SAASnC,KAAKmC,OAAO;oBACrBP,UAAUG;oBACV1B,SAASD;oBACTE,WAAWN,KAAKM,SAAS;gBAC3B;YACF;QACF;QAEA,KAAK,MAAMW,KAAKe,UAAW;YACzBR,MAAMU,IAAI,CAAC;gBACTC,SAASnC,KAAKmC,OAAO;gBACrBP,UAAUX;gBACVZ,SAASD;gBACTE,WAAWN,KAAKM,SAAS;YAC3B;QACF;QAEAN,KAAKwB,KAAK,GAAGA;QACb,OAAOxB;IACT,EAAC"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { ValidationError } from 'payload';
|
|
3
|
+
import { resolveGuestBookingAllowed } from '../../utilities/guestBooking.js';
|
|
4
|
+
export const validateGuestBooking = (config)=>async ({ context, data, operation, req })=>{
|
|
5
|
+
if (context?.skipReservationHooks) {
|
|
6
|
+
return data;
|
|
7
|
+
}
|
|
8
|
+
if (operation !== 'create') {
|
|
9
|
+
return data;
|
|
10
|
+
}
|
|
11
|
+
const customer = data?.customer;
|
|
12
|
+
const guest = data?.guest;
|
|
13
|
+
const hasCustomer = customer != null && customer !== '';
|
|
14
|
+
const hasGuest = guest != null && (Boolean(guest.name) || Boolean(guest.email) || Boolean(guest.phone));
|
|
15
|
+
if (!hasCustomer && !hasGuest) {
|
|
16
|
+
throw new ValidationError({
|
|
17
|
+
errors: [
|
|
18
|
+
{
|
|
19
|
+
message: req.t('reservation:errorGuestOrCustomerRequired'),
|
|
20
|
+
path: 'customer'
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
if (hasCustomer && hasGuest) {
|
|
26
|
+
throw new ValidationError({
|
|
27
|
+
errors: [
|
|
28
|
+
{
|
|
29
|
+
message: req.t('reservation:errorGuestAndCustomer'),
|
|
30
|
+
path: 'guest'
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (hasCustomer) {
|
|
36
|
+
return data;
|
|
37
|
+
}
|
|
38
|
+
// Guest path
|
|
39
|
+
if (!guest?.name) {
|
|
40
|
+
throw new ValidationError({
|
|
41
|
+
errors: [
|
|
42
|
+
{
|
|
43
|
+
message: req.t('reservation:errorGuestNameRequired'),
|
|
44
|
+
path: 'guest.name'
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (!guest.email && !guest.phone) {
|
|
50
|
+
throw new ValidationError({
|
|
51
|
+
errors: [
|
|
52
|
+
{
|
|
53
|
+
message: req.t('reservation:errorGuestContactRequired'),
|
|
54
|
+
path: 'guest.email'
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
// Gate by service — admins (non-customer collection users) bypass.
|
|
60
|
+
const isAdmin = req.user != null && req.user.collection !== config.slugs.customers;
|
|
61
|
+
if (!isAdmin) {
|
|
62
|
+
const serviceId = typeof data.service === 'object' ? data.service?.id : data.service;
|
|
63
|
+
// `service` is a required field on the collection, so Payload's field
|
|
64
|
+
// validation (which runs before this beforeChange hook) guarantees it is
|
|
65
|
+
// present for any booking that reaches here. The guard is purely defensive.
|
|
66
|
+
if (serviceId) {
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
68
|
+
const service = await req.payload.findByID({
|
|
69
|
+
id: serviceId,
|
|
70
|
+
collection: config.slugs.services,
|
|
71
|
+
depth: 0,
|
|
72
|
+
req
|
|
73
|
+
});
|
|
74
|
+
if (!resolveGuestBookingAllowed(service, config.allowGuestBooking)) {
|
|
75
|
+
throw new ValidationError({
|
|
76
|
+
errors: [
|
|
77
|
+
{
|
|
78
|
+
message: req.t('reservation:errorGuestNotAllowed'),
|
|
79
|
+
path: 'guest'
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Generate a cancellation token the host project can deliver to the guest.
|
|
87
|
+
if (!data.cancellationToken) {
|
|
88
|
+
data.cancellationToken = randomUUID();
|
|
89
|
+
}
|
|
90
|
+
return data;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
//# sourceMappingURL=validateGuestBooking.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/hooks/reservations/validateGuestBooking.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { randomUUID } from 'node:crypto'\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { resolveGuestBookingAllowed } from '../../utilities/guestBooking.js'\n\ntype GuestData = { email?: string; name?: string; phone?: string }\n\nexport const validateGuestBooking =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, req }) => {\n if (context?.skipReservationHooks) {\n return data\n }\n if (operation !== 'create') {\n return data\n }\n\n const customer = data?.customer\n const guest = data?.guest as GuestData | undefined\n const hasCustomer = customer != null && customer !== ''\n const hasGuest =\n guest != null && (Boolean(guest.name) || Boolean(guest.email) || Boolean(guest.phone))\n\n if (!hasCustomer && !hasGuest) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorGuestOrCustomerRequired'),\n path: 'customer',\n },\n ],\n })\n }\n\n if (hasCustomer && hasGuest) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorGuestAndCustomer'),\n path: 'guest',\n },\n ],\n })\n }\n\n if (hasCustomer) {\n return data\n }\n\n // Guest path\n if (!guest?.name) {\n throw new ValidationError({\n errors: [\n { message: (req.t as PluginT)('reservation:errorGuestNameRequired'), path: 'guest.name' },\n ],\n })\n }\n if (!guest.email && !guest.phone) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorGuestContactRequired'),\n path: 'guest.email',\n },\n ],\n })\n }\n\n // Gate by service — admins (non-customer collection users) bypass.\n const isAdmin = req.user != null && req.user.collection !== config.slugs.customers\n if (!isAdmin) {\n const serviceId =\n typeof data.service === 'object'\n ? (data.service as { id?: string } | null)?.id\n : data.service\n // `service` is a required field on the collection, so Payload's field\n // validation (which runs before this beforeChange hook) guarantees it is\n // present for any booking that reaches here. The guard is purely defensive.\n if (serviceId) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (req.payload.findByID as any)({\n id: serviceId,\n collection: config.slugs.services,\n depth: 0,\n req,\n })\n if (!resolveGuestBookingAllowed(service, config.allowGuestBooking)) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorGuestNotAllowed'),\n path: 'guest',\n },\n ],\n })\n }\n }\n }\n\n // Generate a cancellation token the host project can deliver to the guest.\n if (!data.cancellationToken) {\n data.cancellationToken = randomUUID()\n }\n\n return data\n }\n"],"names":["randomUUID","ValidationError","resolveGuestBookingAllowed","validateGuestBooking","config","context","data","operation","req","skipReservationHooks","customer","guest","hasCustomer","hasGuest","Boolean","name","email","phone","errors","message","t","path","isAdmin","user","collection","slugs","customers","serviceId","service","id","payload","findByID","services","depth","allowGuestBooking","cancellationToken"],"mappings":"AAEA,SAASA,UAAU,QAAQ,cAAa;AACxC,SAASC,eAAe,QAAQ,UAAS;AAKzC,SAASC,0BAA0B,QAAQ,kCAAiC;AAI5E,OAAO,MAAMC,uBACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,IAAI,EAAEC,SAAS,EAAEC,GAAG,EAAE;QACtC,IAAIH,SAASI,sBAAsB;YACjC,OAAOH;QACT;QACA,IAAIC,cAAc,UAAU;YAC1B,OAAOD;QACT;QAEA,MAAMI,WAAWJ,MAAMI;QACvB,MAAMC,QAAQL,MAAMK;QACpB,MAAMC,cAAcF,YAAY,QAAQA,aAAa;QACrD,MAAMG,WACJF,SAAS,QAASG,CAAAA,QAAQH,MAAMI,IAAI,KAAKD,QAAQH,MAAMK,KAAK,KAAKF,QAAQH,MAAMM,KAAK,CAAA;QAEtF,IAAI,CAACL,eAAe,CAACC,UAAU;YAC7B,MAAM,IAAIZ,gBAAgB;gBACxBiB,QAAQ;oBACN;wBACEC,SAAS,AAACX,IAAIY,CAAC,CAAa;wBAC5BC,MAAM;oBACR;iBACD;YACH;QACF;QAEA,IAAIT,eAAeC,UAAU;YAC3B,MAAM,IAAIZ,gBAAgB;gBACxBiB,QAAQ;oBACN;wBACEC,SAAS,AAACX,IAAIY,CAAC,CAAa;wBAC5BC,MAAM;oBACR;iBACD;YACH;QACF;QAEA,IAAIT,aAAa;YACf,OAAON;QACT;QAEA,aAAa;QACb,IAAI,CAACK,OAAOI,MAAM;YAChB,MAAM,IAAId,gBAAgB;gBACxBiB,QAAQ;oBACN;wBAAEC,SAAS,AAACX,IAAIY,CAAC,CAAa;wBAAuCC,MAAM;oBAAa;iBACzF;YACH;QACF;QACA,IAAI,CAACV,MAAMK,KAAK,IAAI,CAACL,MAAMM,KAAK,EAAE;YAChC,MAAM,IAAIhB,gBAAgB;gBACxBiB,QAAQ;oBACN;wBACEC,SAAS,AAACX,IAAIY,CAAC,CAAa;wBAC5BC,MAAM;oBACR;iBACD;YACH;QACF;QAEA,mEAAmE;QACnE,MAAMC,UAAUd,IAAIe,IAAI,IAAI,QAAQf,IAAIe,IAAI,CAACC,UAAU,KAAKpB,OAAOqB,KAAK,CAACC,SAAS;QAClF,IAAI,CAACJ,SAAS;YACZ,MAAMK,YACJ,OAAOrB,KAAKsB,OAAO,KAAK,WACnBtB,KAAKsB,OAAO,EAA6BC,KAC1CvB,KAAKsB,OAAO;YAClB,sEAAsE;YACtE,yEAAyE;YACzE,4EAA4E;YAC5E,IAAID,WAAW;gBACb,8DAA8D;gBAC9D,MAAMC,UAAU,MAAM,AAACpB,IAAIsB,OAAO,CAACC,QAAQ,CAAS;oBAClDF,IAAIF;oBACJH,YAAYpB,OAAOqB,KAAK,CAACO,QAAQ;oBACjCC,OAAO;oBACPzB;gBACF;gBACA,IAAI,CAACN,2BAA2B0B,SAASxB,OAAO8B,iBAAiB,GAAG;oBAClE,MAAM,IAAIjC,gBAAgB;wBACxBiB,QAAQ;4BACN;gCACEC,SAAS,AAACX,IAAIY,CAAC,CAAa;gCAC5BC,MAAM;4BACR;yBACD;oBACH;gBACF;YACF;QACF;QAEA,2EAA2E;QAC3E,IAAI,CAACf,KAAK6B,iBAAiB,EAAE;YAC3B7B,KAAK6B,iBAAiB,GAAGnC;QAC3B;QAEA,OAAOM;IACT,EAAC"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ValidationError } from 'payload';
|
|
2
2
|
import { validateTransition } from '../../services/AvailabilityService.js';
|
|
3
|
+
import { isPrivilegedUser } from '../../utilities/userRoles.js';
|
|
3
4
|
export const validateStatusTransition = (config)=>async ({ context, data, operation, originalDoc, req })=>{
|
|
4
5
|
if (context?.skipReservationHooks) {
|
|
5
6
|
return data;
|
|
@@ -10,8 +11,9 @@ export const validateStatusTransition = (config)=>async ({ context, data, operat
|
|
|
10
11
|
// context.allowConfirmedOnCreate is the escape hatch for payment hooks
|
|
11
12
|
// that need to create confirmed reservations programmatically
|
|
12
13
|
const hasContextBypass = Boolean(context?.allowConfirmedOnCreate);
|
|
13
|
-
//
|
|
14
|
-
|
|
14
|
+
// Staff/admin detection: collection-based, with a role-based fallback for
|
|
15
|
+
// single-collection deployments (userCollection set). See isPrivilegedUser.
|
|
16
|
+
const isAdmin = isPrivilegedUser(req.user, config);
|
|
15
17
|
const defaultStatus = statusMachine.defaultStatus;
|
|
16
18
|
const nonDefaultStatuses = statusMachine.transitions[defaultStatus] ?? [];
|
|
17
19
|
const allowedOnCreate = isAdmin || hasContextBypass ? [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/hooks/reservations/validateStatusTransition.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { validateTransition } from '../../services/AvailabilityService.js'\n\nexport const validateStatusTransition =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, originalDoc, req }) => {\n if (context?.skipReservationHooks) {return data}\n\n const newStatus = data?.status as string | undefined\n const { statusMachine } = config\n\n if (operation === 'create') {\n // context.allowConfirmedOnCreate is the escape hatch for payment hooks\n // that need to create confirmed reservations programmatically\n const hasContextBypass = Boolean(context?.allowConfirmedOnCreate)\n //
|
|
1
|
+
{"version":3,"sources":["../../../src/hooks/reservations/validateStatusTransition.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { validateTransition } from '../../services/AvailabilityService.js'\nimport { isPrivilegedUser } from '../../utilities/userRoles.js'\n\nexport const validateStatusTransition =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, originalDoc, req }) => {\n if (context?.skipReservationHooks) {return data}\n\n const newStatus = data?.status as string | undefined\n const { statusMachine } = config\n\n if (operation === 'create') {\n // context.allowConfirmedOnCreate is the escape hatch for payment hooks\n // that need to create confirmed reservations programmatically\n const hasContextBypass = Boolean(context?.allowConfirmedOnCreate)\n // Staff/admin detection: collection-based, with a role-based fallback for\n // single-collection deployments (userCollection set). See isPrivilegedUser.\n const isAdmin = isPrivilegedUser(req.user, config)\n const defaultStatus = statusMachine.defaultStatus\n const nonDefaultStatuses = statusMachine.transitions[defaultStatus] ?? []\n const allowedOnCreate: string[] = (isAdmin || hasContextBypass)\n ? [defaultStatus, ...nonDefaultStatuses]\n : [defaultStatus]\n\n if (newStatus && !allowedOnCreate.includes(newStatus)) {\n const allowed = allowedOnCreate.map((s) => `\"${s}\"`).join(' or ')\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorInvalidCreateStatus', { allowed }),\n path: 'status',\n },\n ],\n })\n }\n\n return data\n }\n\n // On update\n if (operation === 'update' && newStatus) {\n const previousStatus = originalDoc?.status as string | undefined\n\n if (previousStatus && previousStatus !== newStatus) {\n const result = validateTransition(previousStatus, newStatus, statusMachine)\n\n if (!result.valid) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorInvalidTransition', {\n from: previousStatus,\n to: newStatus,\n }),\n path: 'status',\n },\n ],\n })\n }\n\n // Call beforeBookingConfirm plugin hooks\n if (newStatus === 'confirmed' && config.hooks?.beforeBookingConfirm) {\n for (const hook of config.hooks.beforeBookingConfirm) {\n await hook({\n doc: { ...(originalDoc as Record<string, unknown>), ...(data as Record<string, unknown>) },\n newStatus,\n req,\n })\n }\n }\n\n // Call beforeBookingCancel plugin hooks\n if (newStatus === 'cancelled' && config.hooks?.beforeBookingCancel) {\n for (const hook of config.hooks.beforeBookingCancel) {\n await hook({\n doc: { ...(originalDoc as Record<string, unknown>), ...(data as Record<string, unknown>) },\n reason: data?.cancellationReason as string | undefined,\n req,\n })\n }\n }\n }\n }\n\n return data\n }\n"],"names":["ValidationError","validateTransition","isPrivilegedUser","validateStatusTransition","config","context","data","operation","originalDoc","req","skipReservationHooks","newStatus","status","statusMachine","hasContextBypass","Boolean","allowConfirmedOnCreate","isAdmin","user","defaultStatus","nonDefaultStatuses","transitions","allowedOnCreate","includes","allowed","map","s","join","errors","message","t","path","previousStatus","result","valid","from","to","hooks","beforeBookingConfirm","hook","doc","beforeBookingCancel","reason","cancellationReason"],"mappings":"AAEA,SAASA,eAAe,QAAQ,UAAS;AAKzC,SAASC,kBAAkB,QAAQ,wCAAuC;AAC1E,SAASC,gBAAgB,QAAQ,+BAA8B;AAE/D,OAAO,MAAMC,2BACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,IAAI,EAAEC,SAAS,EAAEC,WAAW,EAAEC,GAAG,EAAE;QACnD,IAAIJ,SAASK,sBAAsB;YAAC,OAAOJ;QAAI;QAE/C,MAAMK,YAAYL,MAAMM;QACxB,MAAM,EAAEC,aAAa,EAAE,GAAGT;QAE1B,IAAIG,cAAc,UAAU;YAC1B,uEAAuE;YACvE,8DAA8D;YAC9D,MAAMO,mBAAmBC,QAAQV,SAASW;YAC1C,0EAA0E;YAC1E,4EAA4E;YAC5E,MAAMC,UAAUf,iBAAiBO,IAAIS,IAAI,EAAEd;YAC3C,MAAMe,gBAAgBN,cAAcM,aAAa;YACjD,MAAMC,qBAAqBP,cAAcQ,WAAW,CAACF,cAAc,IAAI,EAAE;YACzE,MAAMG,kBAA4B,AAACL,WAAWH,mBAC1C;gBAACK;mBAAkBC;aAAmB,GACtC;gBAACD;aAAc;YAEnB,IAAIR,aAAa,CAACW,gBAAgBC,QAAQ,CAACZ,YAAY;gBACrD,MAAMa,UAAUF,gBAAgBG,GAAG,CAAC,CAACC,IAAM,CAAC,CAAC,EAAEA,EAAE,CAAC,CAAC,EAAEC,IAAI,CAAC;gBAC1D,MAAM,IAAI3B,gBAAgB;oBACxB4B,QAAQ;wBACN;4BACEC,SAAS,AAACpB,IAAIqB,CAAC,CAAa,wCAAwC;gCAAEN;4BAAQ;4BAC9EO,MAAM;wBACR;qBACD;gBACH;YACF;YAEA,OAAOzB;QACT;QAEA,YAAY;QACZ,IAAIC,cAAc,YAAYI,WAAW;YACvC,MAAMqB,iBAAiBxB,aAAaI;YAEpC,IAAIoB,kBAAkBA,mBAAmBrB,WAAW;gBAClD,MAAMsB,SAAShC,mBAAmB+B,gBAAgBrB,WAAWE;gBAE7D,IAAI,CAACoB,OAAOC,KAAK,EAAE;oBACjB,MAAM,IAAIlC,gBAAgB;wBACxB4B,QAAQ;4BACN;gCACEC,SAAS,AAACpB,IAAIqB,CAAC,CAAa,sCAAsC;oCAChEK,MAAMH;oCACNI,IAAIzB;gCACN;gCACAoB,MAAM;4BACR;yBACD;oBACH;gBACF;gBAEA,yCAAyC;gBACzC,IAAIpB,cAAc,eAAeP,OAAOiC,KAAK,EAAEC,sBAAsB;oBACnE,KAAK,MAAMC,QAAQnC,OAAOiC,KAAK,CAACC,oBAAoB,CAAE;wBACpD,MAAMC,KAAK;4BACTC,KAAK;gCAAE,GAAIhC,WAAW;gCAA8B,GAAIF,IAAI;4BAA6B;4BACzFK;4BACAF;wBACF;oBACF;gBACF;gBAEA,wCAAwC;gBACxC,IAAIE,cAAc,eAAeP,OAAOiC,KAAK,EAAEI,qBAAqB;oBAClE,KAAK,MAAMF,QAAQnC,OAAOiC,KAAK,CAACI,mBAAmB,CAAE;wBACnD,MAAMF,KAAK;4BACTC,KAAK;gCAAE,GAAIhC,WAAW;gCAA8B,GAAIF,IAAI;4BAA6B;4BACzFoC,QAAQpC,MAAMqC;4BACdlC;wBACF;oBACF;gBACF;YACF;QACF;QAEA,OAAOH;IACT,EAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CollectionAfterChangeHook } from 'payload';
|
|
2
|
+
import type { ResolvedReservationPluginConfig } from '../../types.js';
|
|
3
|
+
/** True if the user's role value (string or string[]) intersects staffRoles. */
|
|
4
|
+
export declare function roleMatches(role: unknown, staffRoles: string[]): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* afterChange hook for the staff user collection. On create — or on an update
|
|
7
|
+
* that promotes a user into a staff role — provisions a paired Resource owned
|
|
8
|
+
* by that user, unless one already exists.
|
|
9
|
+
*
|
|
10
|
+
* Ownership is assigned via IMPERSONATION: the Resource is created with a `req`
|
|
11
|
+
* whose `user` is the new staff user (spreading the original req preserves
|
|
12
|
+
* `transactionID` for atomicity). The owner field's force-owner-to-req.user
|
|
13
|
+
* rule then assigns the correct owner with no bypass flag — nothing to exploit.
|
|
14
|
+
*/
|
|
15
|
+
export declare const provisionStaffResource: (config: ResolvedReservationPluginConfig) => CollectionAfterChangeHook;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** True if the user's role value (string or string[]) intersects staffRoles. */ export function roleMatches(role, staffRoles) {
|
|
2
|
+
if (Array.isArray(role)) {
|
|
3
|
+
return role.some((r)=>staffRoles.includes(r));
|
|
4
|
+
}
|
|
5
|
+
return typeof role === 'string' && staffRoles.includes(role);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* afterChange hook for the staff user collection. On create — or on an update
|
|
9
|
+
* that promotes a user into a staff role — provisions a paired Resource owned
|
|
10
|
+
* by that user, unless one already exists.
|
|
11
|
+
*
|
|
12
|
+
* Ownership is assigned via IMPERSONATION: the Resource is created with a `req`
|
|
13
|
+
* whose `user` is the new staff user (spreading the original req preserves
|
|
14
|
+
* `transactionID` for atomicity). The owner field's force-owner-to-req.user
|
|
15
|
+
* rule then assigns the correct owner with no bypass flag — nothing to exploit.
|
|
16
|
+
*/ export const provisionStaffResource = (config)=>async ({ context, doc, operation, previousDoc, req })=>{
|
|
17
|
+
if (context?.skipReservationHooks) {
|
|
18
|
+
return doc;
|
|
19
|
+
}
|
|
20
|
+
const sp = config.staffProvisioning;
|
|
21
|
+
if (!sp) {
|
|
22
|
+
return doc;
|
|
23
|
+
}
|
|
24
|
+
const d = doc;
|
|
25
|
+
const isStaffNow = roleMatches(d[sp.roleField], sp.staffRoles);
|
|
26
|
+
if (!isStaffNow) {
|
|
27
|
+
return doc;
|
|
28
|
+
}
|
|
29
|
+
if (operation === 'update') {
|
|
30
|
+
// Skip if the user was already staff — provisioned on a prior save.
|
|
31
|
+
const wasStaff = roleMatches(previousDoc?.[sp.roleField], sp.staffRoles);
|
|
32
|
+
if (wasStaff) {
|
|
33
|
+
return doc;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const ownerField = config.resourceOwnerMode?.ownerField ?? 'owner';
|
|
37
|
+
try {
|
|
38
|
+
// Idempotency: skip if a resource already owns this user.
|
|
39
|
+
const existing = await req.payload.find({
|
|
40
|
+
collection: config.slugs.resources,
|
|
41
|
+
depth: 0,
|
|
42
|
+
limit: 1,
|
|
43
|
+
req,
|
|
44
|
+
where: {
|
|
45
|
+
[ownerField]: {
|
|
46
|
+
equals: d.id
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
if (existing.docs.length > 0) {
|
|
51
|
+
return doc;
|
|
52
|
+
}
|
|
53
|
+
let data = {
|
|
54
|
+
name: d[sp.nameFrom] ?? d.email,
|
|
55
|
+
[ownerField]: d.id,
|
|
56
|
+
quantity: 1,
|
|
57
|
+
resourceType: sp.resourceType
|
|
58
|
+
};
|
|
59
|
+
if (sp.beforeCreate) {
|
|
60
|
+
data = await sp.beforeCreate({
|
|
61
|
+
data,
|
|
62
|
+
req,
|
|
63
|
+
user: d
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
// Impersonate the new staff user so the owner field resolves to them, not
|
|
67
|
+
// the admin who triggered the create. Spread preserves the transaction.
|
|
68
|
+
await req.payload.create({
|
|
69
|
+
collection: config.slugs.resources,
|
|
70
|
+
data,
|
|
71
|
+
req: {
|
|
72
|
+
...req,
|
|
73
|
+
user: {
|
|
74
|
+
...d,
|
|
75
|
+
collection: sp.userCollection
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
} catch (err) {
|
|
80
|
+
req.payload.logger.error({
|
|
81
|
+
err,
|
|
82
|
+
msg: `provisionStaffResource: failed to provision a resource for user ${String(d.id)}`
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return doc;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
//# sourceMappingURL=provisionStaffResource.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/hooks/users/provisionStaffResource.ts"],"sourcesContent":["import type { CollectionAfterChangeHook, CollectionSlug } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\n/** True if the user's role value (string or string[]) intersects staffRoles. */\nexport function roleMatches(role: unknown, staffRoles: string[]): boolean {\n if (Array.isArray(role)) {\n return role.some((r) => staffRoles.includes(r as string))\n }\n return typeof role === 'string' && staffRoles.includes(role)\n}\n\n/**\n * afterChange hook for the staff user collection. On create — or on an update\n * that promotes a user into a staff role — provisions a paired Resource owned\n * by that user, unless one already exists.\n *\n * Ownership is assigned via IMPERSONATION: the Resource is created with a `req`\n * whose `user` is the new staff user (spreading the original req preserves\n * `transactionID` for atomicity). The owner field's force-owner-to-req.user\n * rule then assigns the correct owner with no bypass flag — nothing to exploit.\n */\nexport const provisionStaffResource =\n (config: ResolvedReservationPluginConfig): CollectionAfterChangeHook =>\n async ({ context, doc, operation, previousDoc, req }) => {\n if (context?.skipReservationHooks) {\n return doc\n }\n\n const sp = config.staffProvisioning\n if (!sp) {\n return doc\n }\n\n const d = doc as Record<string, unknown>\n const isStaffNow = roleMatches(d[sp.roleField], sp.staffRoles)\n if (!isStaffNow) {\n return doc\n }\n\n if (operation === 'update') {\n // Skip if the user was already staff — provisioned on a prior save.\n const wasStaff = roleMatches(\n (previousDoc as Record<string, unknown> | undefined)?.[sp.roleField],\n sp.staffRoles,\n )\n if (wasStaff) {\n return doc\n }\n }\n\n const ownerField = config.resourceOwnerMode?.ownerField ?? 'owner'\n\n try {\n // Idempotency: skip if a resource already owns this user.\n const existing = await req.payload.find({\n collection: config.slugs.resources as unknown as CollectionSlug,\n depth: 0,\n limit: 1,\n req,\n where: { [ownerField]: { equals: d.id } },\n })\n if (existing.docs.length > 0) {\n return doc\n }\n\n let data: Record<string, unknown> = {\n name: (d[sp.nameFrom] as string) ?? (d.email as string),\n [ownerField]: d.id,\n quantity: 1,\n resourceType: sp.resourceType,\n }\n\n if (sp.beforeCreate) {\n data = await sp.beforeCreate({ data, req, user: d })\n }\n\n // Impersonate the new staff user so the owner field resolves to them, not\n // the admin who triggered the create. Spread preserves the transaction.\n await req.payload.create({\n collection: config.slugs.resources as unknown as CollectionSlug,\n data,\n req: { ...req, user: { ...d, collection: sp.userCollection } } as typeof req,\n })\n } catch (err) {\n req.payload.logger.error({\n err,\n msg: `provisionStaffResource: failed to provision a resource for user ${String(d.id)}`,\n })\n }\n\n return doc\n }\n"],"names":["roleMatches","role","staffRoles","Array","isArray","some","r","includes","provisionStaffResource","config","context","doc","operation","previousDoc","req","skipReservationHooks","sp","staffProvisioning","d","isStaffNow","roleField","wasStaff","ownerField","resourceOwnerMode","existing","payload","find","collection","slugs","resources","depth","limit","where","equals","id","docs","length","data","name","nameFrom","email","quantity","resourceType","beforeCreate","user","create","userCollection","err","logger","error","msg","String"],"mappings":"AAIA,8EAA8E,GAC9E,OAAO,SAASA,YAAYC,IAAa,EAAEC,UAAoB;IAC7D,IAAIC,MAAMC,OAAO,CAACH,OAAO;QACvB,OAAOA,KAAKI,IAAI,CAAC,CAACC,IAAMJ,WAAWK,QAAQ,CAACD;IAC9C;IACA,OAAO,OAAOL,SAAS,YAAYC,WAAWK,QAAQ,CAACN;AACzD;AAEA;;;;;;;;;CASC,GACD,OAAO,MAAMO,yBACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,GAAG,EAAEC,SAAS,EAAEC,WAAW,EAAEC,GAAG,EAAE;QAClD,IAAIJ,SAASK,sBAAsB;YACjC,OAAOJ;QACT;QAEA,MAAMK,KAAKP,OAAOQ,iBAAiB;QACnC,IAAI,CAACD,IAAI;YACP,OAAOL;QACT;QAEA,MAAMO,IAAIP;QACV,MAAMQ,aAAanB,YAAYkB,CAAC,CAACF,GAAGI,SAAS,CAAC,EAAEJ,GAAGd,UAAU;QAC7D,IAAI,CAACiB,YAAY;YACf,OAAOR;QACT;QAEA,IAAIC,cAAc,UAAU;YAC1B,oEAAoE;YACpE,MAAMS,WAAWrB,YACda,aAAqD,CAACG,GAAGI,SAAS,CAAC,EACpEJ,GAAGd,UAAU;YAEf,IAAImB,UAAU;gBACZ,OAAOV;YACT;QACF;QAEA,MAAMW,aAAab,OAAOc,iBAAiB,EAAED,cAAc;QAE3D,IAAI;YACF,0DAA0D;YAC1D,MAAME,WAAW,MAAMV,IAAIW,OAAO,CAACC,IAAI,CAAC;gBACtCC,YAAYlB,OAAOmB,KAAK,CAACC,SAAS;gBAClCC,OAAO;gBACPC,OAAO;gBACPjB;gBACAkB,OAAO;oBAAE,CAACV,WAAW,EAAE;wBAAEW,QAAQf,EAAEgB,EAAE;oBAAC;gBAAE;YAC1C;YACA,IAAIV,SAASW,IAAI,CAACC,MAAM,GAAG,GAAG;gBAC5B,OAAOzB;YACT;YAEA,IAAI0B,OAAgC;gBAClCC,MAAM,AAACpB,CAAC,CAACF,GAAGuB,QAAQ,CAAC,IAAgBrB,EAAEsB,KAAK;gBAC5C,CAAClB,WAAW,EAAEJ,EAAEgB,EAAE;gBAClBO,UAAU;gBACVC,cAAc1B,GAAG0B,YAAY;YAC/B;YAEA,IAAI1B,GAAG2B,YAAY,EAAE;gBACnBN,OAAO,MAAMrB,GAAG2B,YAAY,CAAC;oBAAEN;oBAAMvB;oBAAK8B,MAAM1B;gBAAE;YACpD;YAEA,0EAA0E;YAC1E,wEAAwE;YACxE,MAAMJ,IAAIW,OAAO,CAACoB,MAAM,CAAC;gBACvBlB,YAAYlB,OAAOmB,KAAK,CAACC,SAAS;gBAClCQ;gBACAvB,KAAK;oBAAE,GAAGA,GAAG;oBAAE8B,MAAM;wBAAE,GAAG1B,CAAC;wBAAES,YAAYX,GAAG8B,cAAc;oBAAC;gBAAE;YAC/D;QACF,EAAE,OAAOC,KAAK;YACZjC,IAAIW,OAAO,CAACuB,MAAM,CAACC,KAAK,CAAC;gBACvBF;gBACAG,KAAK,CAAC,gEAAgE,EAAEC,OAAOjC,EAAEgB,EAAE,GAAG;YACxF;QACF;QAEA,OAAOvB;IACT,EAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
export { provisionStaffResource, roleMatches } from './hooks/users/provisionStaffResource.js';
|
|
1
2
|
export { payloadReserve } from './plugin.js';
|
|
2
3
|
export { buildOverlapQuery, checkAvailability, computeEndTime, getAvailableSlots, isBlockingStatus, validateTransition, } from './services/index.js';
|
|
3
|
-
export type { CapacityMode, DurationType, ReservationPluginConfig, ReservationPluginHooks, ResolvedReservationPluginConfig, StatusMachineConfig, } from './types.js';
|
|
4
|
+
export type { CapacityMode, DurationType, ReservationPluginConfig, ReservationPluginHooks, ResolvedReservationPluginConfig, StaffProvisioningConfig, StatusMachineConfig, } from './types.js';
|
|
4
5
|
export { DEFAULT_STATUS_MACHINE, VALID_STATUS_TRANSITIONS } from './types.js';
|
|
6
|
+
export { mergeResourceIds } from './utilities/resolveRequiredResources.js';
|
|
5
7
|
export type { ResolvedItem } from './utilities/resolveReservationItems.js';
|
|
6
8
|
export { resolveReservationItems } from './utilities/resolveReservationItems.js';
|
|
9
|
+
export { buildSelectOptions } from './utilities/selectOptions.js';
|
|
10
|
+
export { intersectIntervals } from './utilities/slotUtils.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
export { provisionStaffResource, roleMatches } from './hooks/users/provisionStaffResource.js';
|
|
1
2
|
export { payloadReserve } from './plugin.js';
|
|
2
3
|
export { buildOverlapQuery, checkAvailability, computeEndTime, getAvailableSlots, isBlockingStatus, validateTransition } from './services/index.js';
|
|
3
4
|
export { DEFAULT_STATUS_MACHINE, VALID_STATUS_TRANSITIONS } from './types.js';
|
|
5
|
+
export { mergeResourceIds } from './utilities/resolveRequiredResources.js';
|
|
4
6
|
export { resolveReservationItems } from './utilities/resolveReservationItems.js';
|
|
7
|
+
export { buildSelectOptions } from './utilities/selectOptions.js';
|
|
8
|
+
export { intersectIntervals } from './utilities/slotUtils.js';
|
|
5
9
|
|
|
6
10
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export { payloadReserve } from './plugin.js'\nexport {\n buildOverlapQuery,\n checkAvailability,\n computeEndTime,\n getAvailableSlots,\n isBlockingStatus,\n validateTransition,\n} from './services/index.js'\nexport type {\n CapacityMode,\n DurationType,\n ReservationPluginConfig,\n ReservationPluginHooks,\n ResolvedReservationPluginConfig,\n StatusMachineConfig,\n} from './types.js'\nexport { DEFAULT_STATUS_MACHINE, VALID_STATUS_TRANSITIONS } from './types.js'\nexport type { ResolvedItem } from './utilities/resolveReservationItems.js'\nexport { resolveReservationItems } from './utilities/resolveReservationItems.js'\n"],"names":["payloadReserve","buildOverlapQuery","checkAvailability","computeEndTime","getAvailableSlots","isBlockingStatus","validateTransition","DEFAULT_STATUS_MACHINE","VALID_STATUS_TRANSITIONS","resolveReservationItems"],"mappings":"AAAA,SAASA,cAAc,QAAQ,cAAa;AAC5C,SACEC,iBAAiB,EACjBC,iBAAiB,EACjBC,cAAc,EACdC,iBAAiB,EACjBC,gBAAgB,EAChBC,kBAAkB,QACb,sBAAqB;
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export { provisionStaffResource, roleMatches } from './hooks/users/provisionStaffResource.js'\nexport { payloadReserve } from './plugin.js'\nexport {\n buildOverlapQuery,\n checkAvailability,\n computeEndTime,\n getAvailableSlots,\n isBlockingStatus,\n validateTransition,\n} from './services/index.js'\nexport type {\n CapacityMode,\n DurationType,\n ReservationPluginConfig,\n ReservationPluginHooks,\n ResolvedReservationPluginConfig,\n StaffProvisioningConfig,\n StatusMachineConfig,\n} from './types.js'\nexport { DEFAULT_STATUS_MACHINE, VALID_STATUS_TRANSITIONS } from './types.js'\nexport { mergeResourceIds } from './utilities/resolveRequiredResources.js'\nexport type { ResolvedItem } from './utilities/resolveReservationItems.js'\nexport { resolveReservationItems } from './utilities/resolveReservationItems.js'\nexport { buildSelectOptions } from './utilities/selectOptions.js'\nexport { intersectIntervals } from './utilities/slotUtils.js'\n"],"names":["provisionStaffResource","roleMatches","payloadReserve","buildOverlapQuery","checkAvailability","computeEndTime","getAvailableSlots","isBlockingStatus","validateTransition","DEFAULT_STATUS_MACHINE","VALID_STATUS_TRANSITIONS","mergeResourceIds","resolveReservationItems","buildSelectOptions","intersectIntervals"],"mappings":"AAAA,SAASA,sBAAsB,EAAEC,WAAW,QAAQ,0CAAyC;AAC7F,SAASC,cAAc,QAAQ,cAAa;AAC5C,SACEC,iBAAiB,EACjBC,iBAAiB,EACjBC,cAAc,EACdC,iBAAiB,EACjBC,gBAAgB,EAChBC,kBAAkB,QACb,sBAAqB;AAU5B,SAASC,sBAAsB,EAAEC,wBAAwB,QAAQ,aAAY;AAC7E,SAASC,gBAAgB,QAAQ,0CAAyC;AAE1E,SAASC,uBAAuB,QAAQ,yCAAwC;AAChF,SAASC,kBAAkB,QAAQ,+BAA8B;AACjE,SAASC,kBAAkB,QAAQ,2BAA0B"}
|
package/dist/plugin.js
CHANGED
|
@@ -10,6 +10,8 @@ import { createCheckAvailabilityEndpoint } from './endpoints/checkAvailability.j
|
|
|
10
10
|
import { createBookingEndpoint } from './endpoints/createBooking.js';
|
|
11
11
|
import { createCustomerSearchEndpoint } from './endpoints/customerSearch.js';
|
|
12
12
|
import { createGetSlotsEndpoint } from './endpoints/getSlots.js';
|
|
13
|
+
import { createResourceAvailabilityEndpoint } from './endpoints/resourceAvailability.js';
|
|
14
|
+
import { provisionStaffResource } from './hooks/users/provisionStaffResource.js';
|
|
13
15
|
import { translations } from './translations/index.js';
|
|
14
16
|
export const payloadReserve = (pluginOptions = {})=>(config)=>{
|
|
15
17
|
const resolved = resolveConfig(pluginOptions);
|
|
@@ -29,8 +31,16 @@ export const payloadReserve = (pluginOptions = {})=>(config)=>{
|
|
|
29
31
|
if (targetCollection) {
|
|
30
32
|
// Collect existing field names for deduplication check
|
|
31
33
|
const existingFieldNames = new Set(targetCollection.fields.map((field)=>'name' in field ? field.name : undefined).filter(Boolean));
|
|
32
|
-
// Fields to inject if not already present
|
|
34
|
+
// Fields to inject if not already present. `name` is added so that
|
|
35
|
+
// admin.useAsTitle: 'name' works out of the box on the extended user
|
|
36
|
+
// collection (matches the v1.0.0 behaviour documented in README/SKILL).
|
|
33
37
|
const fieldsToAdd = [
|
|
38
|
+
{
|
|
39
|
+
name: 'name',
|
|
40
|
+
type: 'text',
|
|
41
|
+
maxLength: 200,
|
|
42
|
+
required: true
|
|
43
|
+
},
|
|
34
44
|
{
|
|
35
45
|
name: 'phone',
|
|
36
46
|
type: 'text',
|
|
@@ -67,7 +77,22 @@ export const payloadReserve = (pluginOptions = {})=>(config)=>{
|
|
|
67
77
|
if (!config.endpoints) {
|
|
68
78
|
config.endpoints = [];
|
|
69
79
|
}
|
|
70
|
-
config.endpoints.push(createCancelBookingEndpoint(resolved), createCheckAvailabilityEndpoint(resolved), createBookingEndpoint(resolved), createCustomerSearchEndpoint(resolved), createGetSlotsEndpoint(resolved));
|
|
80
|
+
config.endpoints.push(createCancelBookingEndpoint(resolved), createCheckAvailabilityEndpoint(resolved), createBookingEndpoint(resolved), createCustomerSearchEndpoint(resolved), createGetSlotsEndpoint(resolved), createResourceAvailabilityEndpoint(resolved));
|
|
81
|
+
// Wire staff auto-provisioning onto the staff user collection
|
|
82
|
+
if (resolved.staffProvisioning) {
|
|
83
|
+
const staffUserSlug = resolved.staffProvisioning.userCollection;
|
|
84
|
+
const staffCollection = config.collections.find((col)=>col.slug === staffUserSlug);
|
|
85
|
+
if (!staffCollection) {
|
|
86
|
+
throw new Error(`staffProvisioning.userCollection "${staffUserSlug}" was not found in config.collections`);
|
|
87
|
+
}
|
|
88
|
+
staffCollection.hooks = {
|
|
89
|
+
...staffCollection.hooks,
|
|
90
|
+
afterChange: [
|
|
91
|
+
...staffCollection.hooks?.afterChange ?? [],
|
|
92
|
+
provisionStaffResource(resolved)
|
|
93
|
+
]
|
|
94
|
+
};
|
|
95
|
+
}
|
|
71
96
|
// Set up admin configuration
|
|
72
97
|
if (!config.admin) {
|
|
73
98
|
config.admin = {};
|
|
@@ -95,7 +120,7 @@ export const payloadReserve = (pluginOptions = {})=>(config)=>{
|
|
|
95
120
|
config.admin.dashboard.widgets.push({
|
|
96
121
|
slug: 'reservation-todays-reservations',
|
|
97
122
|
Component: 'payload-reserve/rsc#DashboardWidgetServer',
|
|
98
|
-
label: '
|
|
123
|
+
label: ({ t })=>t('reservation:dashboardTitle'),
|
|
99
124
|
maxWidth: 'large',
|
|
100
125
|
minWidth: 'medium'
|
|
101
126
|
});
|