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.
Files changed (104) hide show
  1. package/README.md +185 -4
  2. package/dist/collections/Reservations.js +47 -2
  3. package/dist/collections/Reservations.js.map +1 -1
  4. package/dist/collections/Resources.d.ts +16 -0
  5. package/dist/collections/Resources.js +35 -10
  6. package/dist/collections/Resources.js.map +1 -1
  7. package/dist/collections/Schedules.js +34 -0
  8. package/dist/collections/Schedules.js.map +1 -1
  9. package/dist/collections/Services.js +34 -1
  10. package/dist/collections/Services.js.map +1 -1
  11. package/dist/components/AvailabilityTimeField/AvailabilityTimeField.module.css +7 -0
  12. package/dist/components/AvailabilityTimeField/index.d.ts +2 -0
  13. package/dist/components/AvailabilityTimeField/index.js +109 -0
  14. package/dist/components/AvailabilityTimeField/index.js.map +1 -0
  15. package/dist/components/CalendarView/CalendarView.module.css +114 -0
  16. package/dist/components/CalendarView/LaneTimelineView.d.ts +12 -0
  17. package/dist/components/CalendarView/LaneTimelineView.js +116 -0
  18. package/dist/components/CalendarView/LaneTimelineView.js.map +1 -0
  19. package/dist/components/CalendarView/index.js +224 -22
  20. package/dist/components/CalendarView/index.js.map +1 -1
  21. package/dist/components/CalendarView/useResourceAvailability.d.ts +9 -0
  22. package/dist/components/CalendarView/useResourceAvailability.js +40 -0
  23. package/dist/components/CalendarView/useResourceAvailability.js.map +1 -0
  24. package/dist/defaults.d.ts +3 -0
  25. package/dist/defaults.js +53 -0
  26. package/dist/defaults.js.map +1 -1
  27. package/dist/endpoints/cancelBooking.js +34 -21
  28. package/dist/endpoints/cancelBooking.js.map +1 -1
  29. package/dist/endpoints/checkAvailability.js +16 -1
  30. package/dist/endpoints/checkAvailability.js.map +1 -1
  31. package/dist/endpoints/createBooking.js +4 -1
  32. package/dist/endpoints/createBooking.js.map +1 -1
  33. package/dist/endpoints/customerSearch.js +24 -5
  34. package/dist/endpoints/customerSearch.js.map +1 -1
  35. package/dist/endpoints/getSlots.js +16 -1
  36. package/dist/endpoints/getSlots.js.map +1 -1
  37. package/dist/endpoints/resourceAvailability.d.ts +43 -0
  38. package/dist/endpoints/resourceAvailability.js +214 -0
  39. package/dist/endpoints/resourceAvailability.js.map +1 -0
  40. package/dist/exports/client.d.ts +1 -0
  41. package/dist/exports/client.js +1 -0
  42. package/dist/exports/client.js.map +1 -1
  43. package/dist/hooks/reservations/calculateEndTime.js +21 -1
  44. package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
  45. package/dist/hooks/reservations/expandRequiredResources.d.ts +9 -0
  46. package/dist/hooks/reservations/expandRequiredResources.js +81 -0
  47. package/dist/hooks/reservations/expandRequiredResources.js.map +1 -0
  48. package/dist/hooks/reservations/validateGuestBooking.d.ts +3 -0
  49. package/dist/hooks/reservations/validateGuestBooking.js +93 -0
  50. package/dist/hooks/reservations/validateGuestBooking.js.map +1 -0
  51. package/dist/hooks/reservations/validateStatusTransition.js +4 -2
  52. package/dist/hooks/reservations/validateStatusTransition.js.map +1 -1
  53. package/dist/hooks/users/provisionStaffResource.d.ts +15 -0
  54. package/dist/hooks/users/provisionStaffResource.js +88 -0
  55. package/dist/hooks/users/provisionStaffResource.js.map +1 -0
  56. package/dist/index.d.ts +5 -1
  57. package/dist/index.js +4 -0
  58. package/dist/index.js.map +1 -1
  59. package/dist/plugin.js +28 -3
  60. package/dist/plugin.js.map +1 -1
  61. package/dist/services/AvailabilityService.d.ts +7 -6
  62. package/dist/services/AvailabilityService.js +86 -60
  63. package/dist/services/AvailabilityService.js.map +1 -1
  64. package/dist/translations/ar.json +156 -0
  65. package/dist/translations/de.json +156 -0
  66. package/dist/translations/en.json +32 -1
  67. package/dist/translations/es.json +156 -0
  68. package/dist/translations/fa.json +156 -0
  69. package/dist/translations/fr.json +156 -0
  70. package/dist/translations/hi.json +156 -0
  71. package/dist/translations/id.json +156 -0
  72. package/dist/translations/index.js +44 -0
  73. package/dist/translations/index.js.map +1 -1
  74. package/dist/translations/pl.json +156 -0
  75. package/dist/translations/ru.json +156 -0
  76. package/dist/translations/tr.json +156 -0
  77. package/dist/translations/zh.json +156 -0
  78. package/dist/types.d.ts +46 -0
  79. package/dist/types.js.map +1 -1
  80. package/dist/utilities/computeSlotStates.d.ts +39 -0
  81. package/dist/utilities/computeSlotStates.js +49 -0
  82. package/dist/utilities/computeSlotStates.js.map +1 -0
  83. package/dist/utilities/guestBooking.d.ts +10 -0
  84. package/dist/utilities/guestBooking.js +16 -0
  85. package/dist/utilities/guestBooking.js.map +1 -0
  86. package/dist/utilities/resolveRequiredResources.d.ts +8 -0
  87. package/dist/utilities/resolveRequiredResources.js +27 -0
  88. package/dist/utilities/resolveRequiredResources.js.map +1 -0
  89. package/dist/utilities/resolveReservationItems.d.ts +3 -2
  90. package/dist/utilities/resolveReservationItems.js +19 -6
  91. package/dist/utilities/resolveReservationItems.js.map +1 -1
  92. package/dist/utilities/scheduleUtils.d.ts +3 -0
  93. package/dist/utilities/scheduleUtils.js +5 -3
  94. package/dist/utilities/scheduleUtils.js.map +1 -1
  95. package/dist/utilities/selectOptions.d.ts +8 -0
  96. package/dist/utilities/selectOptions.js +11 -0
  97. package/dist/utilities/selectOptions.js.map +1 -0
  98. package/dist/utilities/slotUtils.d.ts +19 -0
  99. package/dist/utilities/slotUtils.js +28 -0
  100. package/dist/utilities/slotUtils.js.map +1 -1
  101. package/dist/utilities/userRoles.d.ts +20 -0
  102. package/dist/utilities/userRoles.js +32 -0
  103. package/dist/utilities/userRoles.js.map +1 -0
  104. 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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"}
@@ -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';
@@ -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,2CAA2C;YAC3C,KAAK,MAAMC,QAAQzB,KAAKK,KAAK,CAAoC;gBAC/D,IAAI,CAACoB,KAAKtB,SAAS,EAAE;oBAAC;gBAAQ;gBAE9B,MAAMuB,gBAAgB,OAAOD,KAAKrB,OAAO,KAAK,WAC1C,AAACqB,KAAKrB,OAAO,CAAoBI,EAAE,GACnC,AAACiB,KAAKrB,OAAO,IAAgB,CAAA,OAAOJ,KAAKI,OAAO,KAAK,WAAWJ,KAAKI,OAAO,CAACI,EAAE,GAAGR,KAAKI,OAAO,AAAD;gBAEjG,IAAI,CAACsB,eAAe;oBAAC;gBAAQ;gBAE7B,8DAA8D;gBAC9D,MAAMtB,UAAU,MAAM,AAACH,IAAIQ,OAAO,CAACC,QAAQ,CAAS;oBAClDF,IAAIkB;oBACJf,YAAYb,OAAOc,KAAK,CAACC,QAAQ;oBACjCZ;gBACF;gBAEA,IAAI,CAACG,SAASU,YAAYV,SAASW,iBAAiB,YAAY;oBAAC;gBAAQ;gBAEzE,MAAMA,eAAgB,AAACX,QAAQW,YAAY,IAAe;gBAE1D,IAAIA,iBAAiB,cAAc,CAACU,KAAKP,OAAO,EAAE;oBAAC;gBAAQ;gBAE3D,IAAIH,iBAAiB,YAAY;oBAC/B,MAAMQ,SAAS5B,eAAe;wBAC5BoB;wBACAO,iBAAiB,AAAClB,QAAQU,QAAQ,IAAe;wBACjDX,WAAW,IAAIc,KAAKQ,KAAKtB,SAAS;oBACpC;oBACAsB,KAAKP,OAAO,GAAGK,OAAOL,OAAO,CAACM,WAAW;gBAC3C;YACF;QACF;QAEA,OAAOxB;IACT,EAAC"}
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,3 @@
1
+ import type { CollectionBeforeChangeHook } from 'payload';
2
+ import type { ResolvedReservationPluginConfig } from '../../types.js';
3
+ export declare const validateGuestBooking: (config: ResolvedReservationPluginConfig) => CollectionBeforeChangeHook;
@@ -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
- // Admin = user from a non-customer collection (e.g., 'users' admin collection)
14
- const isAdmin = req.user != null && req.user.collection !== config.slugs.customers;
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 // Admin = user from a non-customer collection (e.g., 'users' admin collection)\n const isAdmin = req.user != null && req.user.collection !== config.slugs.customers\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","validateStatusTransition","config","context","data","operation","originalDoc","req","skipReservationHooks","newStatus","status","statusMachine","hasContextBypass","Boolean","allowConfirmedOnCreate","isAdmin","user","collection","slugs","customers","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;AAE1E,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,+EAA+E;YAC/E,MAAMC,UAAUR,IAAIS,IAAI,IAAI,QAAQT,IAAIS,IAAI,CAACC,UAAU,KAAKf,OAAOgB,KAAK,CAACC,SAAS;YAClF,MAAMC,gBAAgBT,cAAcS,aAAa;YACjD,MAAMC,qBAAqBV,cAAcW,WAAW,CAACF,cAAc,IAAI,EAAE;YACzE,MAAMG,kBAA4B,AAACR,WAAWH,mBAC1C;gBAACQ;mBAAkBC;aAAmB,GACtC;gBAACD;aAAc;YAEnB,IAAIX,aAAa,CAACc,gBAAgBC,QAAQ,CAACf,YAAY;gBACrD,MAAMgB,UAAUF,gBAAgBG,GAAG,CAAC,CAACC,IAAM,CAAC,CAAC,EAAEA,EAAE,CAAC,CAAC,EAAEC,IAAI,CAAC;gBAC1D,MAAM,IAAI7B,gBAAgB;oBACxB8B,QAAQ;wBACN;4BACEC,SAAS,AAACvB,IAAIwB,CAAC,CAAa,wCAAwC;gCAAEN;4BAAQ;4BAC9EO,MAAM;wBACR;qBACD;gBACH;YACF;YAEA,OAAO5B;QACT;QAEA,YAAY;QACZ,IAAIC,cAAc,YAAYI,WAAW;YACvC,MAAMwB,iBAAiB3B,aAAaI;YAEpC,IAAIuB,kBAAkBA,mBAAmBxB,WAAW;gBAClD,MAAMyB,SAASlC,mBAAmBiC,gBAAgBxB,WAAWE;gBAE7D,IAAI,CAACuB,OAAOC,KAAK,EAAE;oBACjB,MAAM,IAAIpC,gBAAgB;wBACxB8B,QAAQ;4BACN;gCACEC,SAAS,AAACvB,IAAIwB,CAAC,CAAa,sCAAsC;oCAChEK,MAAMH;oCACNI,IAAI5B;gCACN;gCACAuB,MAAM;4BACR;yBACD;oBACH;gBACF;gBAEA,yCAAyC;gBACzC,IAAIvB,cAAc,eAAeP,OAAOoC,KAAK,EAAEC,sBAAsB;oBACnE,KAAK,MAAMC,QAAQtC,OAAOoC,KAAK,CAACC,oBAAoB,CAAE;wBACpD,MAAMC,KAAK;4BACTC,KAAK;gCAAE,GAAInC,WAAW;gCAA8B,GAAIF,IAAI;4BAA6B;4BACzFK;4BACAF;wBACF;oBACF;gBACF;gBAEA,wCAAwC;gBACxC,IAAIE,cAAc,eAAeP,OAAOoC,KAAK,EAAEI,qBAAqB;oBAClE,KAAK,MAAMF,QAAQtC,OAAOoC,KAAK,CAACI,mBAAmB,CAAE;wBACnD,MAAMF,KAAK;4BACTC,KAAK;gCAAE,GAAInC,WAAW;gCAA8B,GAAIF,IAAI;4BAA6B;4BACzFuC,QAAQvC,MAAMwC;4BACdrC;wBACF;oBACF;gBACF;YACF;QACF;QAEA,OAAOH;IACT,EAAC"}
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;AAS5B,SAASC,sBAAsB,EAAEC,wBAAwB,QAAQ,aAAY;AAE7E,SAASC,uBAAuB,QAAQ,yCAAwC"}
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: 'Today\'s Reservations',
123
+ label: ({ t })=>t('reservation:dashboardTitle'),
99
124
  maxWidth: 'large',
100
125
  minWidth: 'medium'
101
126
  });