payload-reserve 1.6.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +55 -3
  2. package/dist/collections/Reservations.js +19 -7
  3. package/dist/collections/Reservations.js.map +1 -1
  4. package/dist/collections/Resources.js +11 -8
  5. package/dist/collections/Resources.js.map +1 -1
  6. package/dist/collections/Schedules.js +12 -6
  7. package/dist/collections/Schedules.js.map +1 -1
  8. package/dist/collections/Services.js +19 -10
  9. package/dist/collections/Services.js.map +1 -1
  10. package/dist/components/AvailabilityOverview/index.js +76 -18
  11. package/dist/components/AvailabilityOverview/index.js.map +1 -1
  12. package/dist/components/CalendarView/CalendarView.module.css +9 -0
  13. package/dist/components/CalendarView/LaneTimelineView.d.ts +4 -1
  14. package/dist/components/CalendarView/LaneTimelineView.js +17 -12
  15. package/dist/components/CalendarView/LaneTimelineView.js.map +1 -1
  16. package/dist/components/CalendarView/index.js +166 -44
  17. package/dist/components/CalendarView/index.js.map +1 -1
  18. package/dist/components/CustomerField/index.js +8 -3
  19. package/dist/components/CustomerField/index.js.map +1 -1
  20. package/dist/components/DashboardWidget/DashboardWidgetServer.js +91 -18
  21. package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
  22. package/dist/defaults.js +44 -9
  23. package/dist/defaults.js.map +1 -1
  24. package/dist/endpoints/cancelBooking.js +1 -1
  25. package/dist/endpoints/cancelBooking.js.map +1 -1
  26. package/dist/endpoints/checkAvailability.js +56 -7
  27. package/dist/endpoints/checkAvailability.js.map +1 -1
  28. package/dist/endpoints/createBooking.js +19 -10
  29. package/dist/endpoints/createBooking.js.map +1 -1
  30. package/dist/endpoints/customerSearch.js +5 -2
  31. package/dist/endpoints/customerSearch.js.map +1 -1
  32. package/dist/endpoints/effectiveTimezone.d.ts +13 -0
  33. package/dist/endpoints/effectiveTimezone.js +41 -0
  34. package/dist/endpoints/effectiveTimezone.js.map +1 -0
  35. package/dist/endpoints/getSlots.js +56 -7
  36. package/dist/endpoints/getSlots.js.map +1 -1
  37. package/dist/endpoints/resourceAvailability.d.ts +4 -1
  38. package/dist/endpoints/resourceAvailability.js +102 -26
  39. package/dist/endpoints/resourceAvailability.js.map +1 -1
  40. package/dist/hooks/reservations/calculateEndTime.js +48 -20
  41. package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
  42. package/dist/hooks/reservations/enforceCustomerOwnership.d.ts +11 -0
  43. package/dist/hooks/reservations/enforceCustomerOwnership.js +30 -0
  44. package/dist/hooks/reservations/enforceCustomerOwnership.js.map +1 -0
  45. package/dist/hooks/reservations/onStatusChange.js +10 -4
  46. package/dist/hooks/reservations/onStatusChange.js.map +1 -1
  47. package/dist/hooks/reservations/validateCancellation.js +3 -2
  48. package/dist/hooks/reservations/validateCancellation.js.map +1 -1
  49. package/dist/hooks/reservations/validateConflicts.js +23 -4
  50. package/dist/hooks/reservations/validateConflicts.js.map +1 -1
  51. package/dist/hooks/reservations/validateGuestBooking.js +3 -4
  52. package/dist/hooks/reservations/validateGuestBooking.js.map +1 -1
  53. package/dist/hooks/reservations/validateStatusTransition.js +2 -2
  54. package/dist/hooks/reservations/validateStatusTransition.js.map +1 -1
  55. package/dist/hooks/users/provisionStaffResource.js +5 -8
  56. package/dist/hooks/users/provisionStaffResource.js.map +1 -1
  57. package/dist/plugin.js +83 -14
  58. package/dist/plugin.js.map +1 -1
  59. package/dist/services/AvailabilityService.d.ts +54 -2
  60. package/dist/services/AvailabilityService.js +180 -46
  61. package/dist/services/AvailabilityService.js.map +1 -1
  62. package/dist/translations/ar.json +1 -0
  63. package/dist/translations/de.json +1 -0
  64. package/dist/translations/en.json +1 -0
  65. package/dist/translations/es.json +1 -0
  66. package/dist/translations/fa.json +1 -0
  67. package/dist/translations/fr.json +1 -0
  68. package/dist/translations/hi.json +1 -0
  69. package/dist/translations/id.json +1 -0
  70. package/dist/translations/pl.json +1 -0
  71. package/dist/translations/ru.json +1 -0
  72. package/dist/translations/tr.json +1 -0
  73. package/dist/translations/zh.json +1 -0
  74. package/dist/types.d.ts +46 -1
  75. package/dist/types.js +2 -0
  76. package/dist/types.js.map +1 -1
  77. package/dist/utilities/collectionOverrides.d.ts +14 -0
  78. package/dist/utilities/collectionOverrides.js +47 -0
  79. package/dist/utilities/collectionOverrides.js.map +1 -0
  80. package/dist/utilities/ownerAccess.d.ts +6 -0
  81. package/dist/utilities/ownerAccess.js +25 -12
  82. package/dist/utilities/ownerAccess.js.map +1 -1
  83. package/dist/utilities/reservationChanges.d.ts +17 -0
  84. package/dist/utilities/reservationChanges.js +88 -0
  85. package/dist/utilities/reservationChanges.js.map +1 -0
  86. package/dist/utilities/scheduleUtils.d.ts +14 -8
  87. package/dist/utilities/scheduleUtils.js +26 -19
  88. package/dist/utilities/scheduleUtils.js.map +1 -1
  89. package/dist/utilities/tenantTimezone.d.ts +41 -0
  90. package/dist/utilities/tenantTimezone.js +77 -0
  91. package/dist/utilities/tenantTimezone.js.map +1 -0
  92. package/dist/utilities/timezoneUtils.d.ts +44 -0
  93. package/dist/utilities/timezoneUtils.js +146 -0
  94. package/dist/utilities/timezoneUtils.js.map +1 -0
  95. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/services/AvailabilityService.ts"],"sourcesContent":["import type { Payload, PayloadRequest, Where } from 'payload'\n\nimport type { CapacityMode, DurationType, StatusMachineConfig } from '../types.js'\n\nimport { resolveScheduleForDate } from '../utilities/scheduleUtils.js'\nimport { addMinutes, computeBlockedWindow, intersectIntervals } from '../utilities/slotUtils.js'\n\n// --- Pure functions (no DB) ---\n\nexport function computeEndTime(params: {\n durationType: DurationType\n endTime?: Date\n serviceDuration: number\n startTime: Date\n}): { durationMinutes: number; endTime: Date } {\n const { durationType, serviceDuration, startTime } = params\n\n if (durationType === 'full-day') {\n const end = new Date(startTime)\n end.setHours(23, 59, 59, 999)\n const durationMinutes = Math.round((end.getTime() - startTime.getTime()) / 60_000)\n return { durationMinutes, endTime: end }\n }\n\n if (durationType === 'flexible' && params.endTime) {\n const durationMinutes = Math.round(\n (params.endTime.getTime() - startTime.getTime()) / 60_000,\n )\n return { durationMinutes, endTime: params.endTime }\n }\n\n // fixed duration (default)\n const endTime = addMinutes(startTime, serviceDuration)\n return { durationMinutes: serviceDuration, endTime }\n}\n\nexport function buildOverlapQuery(params: {\n blockingStatuses: string[]\n effectiveEnd: Date\n effectiveStart: Date\n excludeReservationId?: number | string\n resourceId: number | string\n}): Where {\n const { blockingStatuses, effectiveEnd, effectiveStart, excludeReservationId, resourceId } =\n params\n\n const conditions: Where[] = [\n { status: { in: blockingStatuses } },\n { startTime: { less_than: effectiveEnd.toISOString() } },\n { endTime: { greater_than: effectiveStart.toISOString() } },\n {\n or: [\n { resource: { equals: resourceId } },\n { 'items.resource': { equals: resourceId } },\n ],\n },\n ]\n\n if (excludeReservationId) {\n conditions.push({ id: { not_equals: excludeReservationId } })\n }\n\n return { and: conditions }\n}\n\nexport function isBlockingStatus(\n status: string,\n statusMachine: StatusMachineConfig,\n): boolean {\n return statusMachine.blockingStatuses.includes(status)\n}\n\nexport function validateTransition(\n fromStatus: string,\n toStatus: string,\n statusMachine: StatusMachineConfig,\n): { reason?: string; valid: boolean } {\n const allowed = statusMachine.transitions[fromStatus]\n if (!allowed) {\n return { reason: `Unknown status: ${fromStatus}`, valid: false }\n }\n if (!allowed.includes(toStatus)) {\n return {\n reason: `Cannot transition from \"${fromStatus}\" to \"${toStatus}\"`,\n valid: false,\n }\n }\n return { valid: true }\n}\n\n// --- DB functions (use Payload Local API only) ---\n\nexport async function checkAvailability(params: {\n blockingStatuses: string[]\n bufferAfter: number\n bufferBefore: number\n endTime: Date\n excludeReservationId?: number | string\n guestCount: number\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId: number | string\n resourceSlug: string\n startTime: Date\n}): Promise<{\n available: boolean\n currentCount: number\n reason?: string\n totalCapacity: number\n}> {\n const {\n blockingStatuses,\n bufferAfter,\n bufferBefore,\n endTime,\n excludeReservationId,\n guestCount,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n startTime,\n } = params\n\n // Fetch resource for quantity and capacity mode\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: 0,\n req,\n })\n const quantity = (resource.quantity as number) ?? 1\n const capacityMode = ((resource.capacityMode as string) ?? 'per-reservation') as CapacityMode\n\n // Compute effective window with buffers\n const { effectiveEnd, effectiveStart } = computeBlockedWindow(\n startTime,\n endTime,\n bufferBefore,\n bufferAfter,\n )\n\n // Build overlap query\n const where = buildOverlapQuery({\n blockingStatuses,\n effectiveEnd,\n effectiveStart,\n excludeReservationId,\n resourceId,\n })\n\n if (capacityMode === 'per-guest') {\n // Must fetch docs to sum guestCount\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs } = await (payload.find as any)({\n collection: reservationSlug,\n depth: 0,\n limit: 0,\n req,\n select: { guestCount: true },\n where,\n })\n const currentGuests = docs.reduce(\n (sum: number, doc: Record<string, unknown>) => sum + ((doc.guestCount as number) ?? 1),\n 0,\n )\n return {\n available: currentGuests + guestCount <= quantity,\n currentCount: currentGuests,\n reason:\n currentGuests + guestCount > quantity ? 'Guest capacity exceeded' : undefined,\n totalCapacity: quantity,\n }\n }\n\n // per-reservation mode: count is sufficient\n // TODO: batch queries — linear per-item cost acceptable for 2-5 items\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { totalDocs } = await (payload.count as any)({\n collection: reservationSlug,\n req,\n where,\n })\n return {\n available: totalDocs + 1 <= quantity,\n currentCount: totalDocs,\n reason: totalDocs + 1 > quantity ? 'All units are booked for this time' : undefined,\n totalCapacity: quantity,\n }\n}\n\nexport async function getAvailableSlots(params: {\n blockingStatuses: string[]\n date: Date\n guestCount?: number\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId?: number | string\n resourceIds?: Array<number | string>\n resourceSlug: string\n scheduleSlug: string\n serviceId: number | string\n serviceSlug: string\n}): Promise<Array<{ end: Date; start: Date }>> {\n const {\n blockingStatuses,\n date,\n guestCount,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceIds,\n resourceSlug,\n scheduleSlug,\n serviceId,\n serviceSlug,\n } = params\n\n // Resolve the set of resources to intersect (single-resource callers still work)\n const ids =\n resourceIds && resourceIds.length > 0\n ? resourceIds\n : resourceId !== undefined\n ? [resourceId]\n : []\n if (ids.length === 0) {\n return []\n }\n\n // 1. Service for duration + buffer times (from the primary service)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (payload.findByID as any)({\n id: serviceId,\n collection: serviceSlug,\n depth: 0,\n req,\n })\n const duration = (service.duration as number) ?? 60\n const bufferBefore = (service.bufferTimeBefore as number) ?? 0\n const bufferAfter = (service.bufferTimeAfter as number) ?? 0\n const durationType = ((service.durationType as string) ?? 'fixed') as DurationType\n\n // 2. Per resource: fetch schedules and resolve to windows. A resource with >=1\n // schedule is \"schedule-bearing\" and constrains time; a resource with zero\n // schedules is capacity-only and contributes no time windows.\n const scheduleBearingWindowLists: Array<Array<{ end: Date; start: Date }>> = []\n for (const rid of ids) {\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 req,\n where: {\n and: [{ resource: { equals: rid } }, { active: { equals: true } }],\n },\n })\n if (!schedules || schedules.length === 0) {\n continue\n }\n const windows: Array<{ end: Date; start: Date }> = []\n for (const schedule of schedules) {\n windows.push(\n ...resolveScheduleForDate(\n schedule as unknown as Parameters<typeof resolveScheduleForDate>[0],\n date,\n ),\n )\n }\n scheduleBearingWindowLists.push(windows)\n }\n\n // No resource constrains time → no basis for generating slots\n if (scheduleBearingWindowLists.length === 0) {\n return []\n }\n\n // 3. Intersect all schedule-bearing window lists\n let timeRanges = scheduleBearingWindowLists[0]\n for (let i = 1; i < scheduleBearingWindowLists.length; i++) {\n timeRanges = intersectIntervals(timeRanges, scheduleBearingWindowLists[i])\n }\n if (timeRanges.length === 0) {\n return []\n }\n\n // 4. Candidate slot sizing\n const { endTime: slotEndOffset } = computeEndTime({\n durationType,\n serviceDuration: duration,\n startTime: new Date(0),\n })\n const slotDuration = Math.round(slotEndOffset.getTime() / 60_000)\n const effectiveDuration = durationType === 'fixed' ? duration : slotDuration\n\n // Helper: a window is available only if EVERY required resource is free\n const allAvailable = async (\n start: Date,\n end: Date,\n bBefore: number,\n bAfter: number,\n ): Promise<boolean> => {\n for (const rid of ids) {\n const result = await checkAvailability({\n blockingStatuses,\n bufferAfter: bAfter,\n bufferBefore: bBefore,\n endTime: end,\n guestCount: guestCount ?? 1,\n payload,\n req,\n reservationSlug,\n resourceId: rid,\n resourceSlug,\n startTime: start,\n })\n if (!result.available) {\n return false\n }\n }\n return true\n }\n\n const availableSlots: Array<{ end: Date; start: Date }> = []\n\n // Full-day: offer each range as a single slot if all resources are free\n if (durationType === 'full-day') {\n for (const range of timeRanges) {\n if (await allAvailable(range.start, range.end, 0, 0)) {\n availableSlots.push({ end: range.end, start: range.start })\n }\n }\n return availableSlots\n }\n\n const stepSize = Math.min(effectiveDuration, 15)\n\n for (const range of timeRanges) {\n let candidateStart = new Date(range.start)\n\n while (true) {\n const candidateEnd = addMinutes(candidateStart, effectiveDuration)\n if (candidateEnd > range.end) {\n break\n }\n\n if (await allAvailable(candidateStart, candidateEnd, bufferBefore, bufferAfter)) {\n availableSlots.push({ end: candidateEnd, start: new Date(candidateStart) })\n }\n\n candidateStart = addMinutes(candidateStart, stepSize)\n }\n }\n\n return availableSlots\n}\n"],"names":["resolveScheduleForDate","addMinutes","computeBlockedWindow","intersectIntervals","computeEndTime","params","durationType","serviceDuration","startTime","end","Date","setHours","durationMinutes","Math","round","getTime","endTime","buildOverlapQuery","blockingStatuses","effectiveEnd","effectiveStart","excludeReservationId","resourceId","conditions","status","in","less_than","toISOString","greater_than","or","resource","equals","push","id","not_equals","and","isBlockingStatus","statusMachine","includes","validateTransition","fromStatus","toStatus","allowed","transitions","reason","valid","checkAvailability","bufferAfter","bufferBefore","guestCount","payload","req","reservationSlug","resourceSlug","findByID","collection","depth","quantity","capacityMode","where","docs","find","limit","select","currentGuests","reduce","sum","doc","available","currentCount","undefined","totalCapacity","totalDocs","count","getAvailableSlots","date","resourceIds","scheduleSlug","serviceId","serviceSlug","ids","length","service","duration","bufferTimeBefore","bufferTimeAfter","scheduleBearingWindowLists","rid","schedules","active","windows","schedule","timeRanges","i","slotEndOffset","slotDuration","effectiveDuration","allAvailable","start","bBefore","bAfter","result","availableSlots","range","stepSize","min","candidateStart","candidateEnd"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/services/AvailabilityService.ts"],"sourcesContent":["import type { Payload, PayloadRequest, Where } from 'payload'\n\nimport type { CapacityMode, DurationType, StatusMachineConfig } from '../types.js'\nimport type { ResolvedItem } from '../utilities/resolveReservationItems.js'\n\nimport { resolveReservationItems } from '../utilities/resolveReservationItems.js'\nimport { isExceptionDate, resolveScheduleForDate } from '../utilities/scheduleUtils.js'\nimport {\n addMinutes,\n computeBlockedWindow,\n doRangesOverlap,\n intersectIntervals,\n} from '../utilities/slotUtils.js'\nimport { endOfDayInTimezone } from '../utilities/timezoneUtils.js'\n\n/** A window during which a resource is occupied, expanded by buffer times. */\nexport type Occupancy = { blockedEnd: Date; blockedStart: Date; units: number }\n\n/** Coarse pre-filter widen: covers any realistic neighbor buffer (buffers are\n * minutes). The precise per-item overlap check runs in memory afterwards. */\nconst COARSE_MARGIN_MS = 24 * 60 * 60 * 1000\n\n// --- Pure functions (no DB) ---\n\nexport function computeEndTime(params: {\n durationType: DurationType\n endTime?: Date\n serviceDuration: number\n startTime: Date\n timeZone?: string\n}): { durationMinutes: number; endTime: Date } {\n const { durationType, serviceDuration, startTime } = params\n\n if (durationType === 'full-day') {\n const end = endOfDayInTimezone(startTime, params.timeZone ?? 'UTC')\n const durationMinutes = Math.round((end.getTime() - startTime.getTime()) / 60_000)\n return { durationMinutes, endTime: end }\n }\n\n if (durationType === 'flexible' && params.endTime) {\n const durationMinutes = Math.round(\n (params.endTime.getTime() - startTime.getTime()) / 60_000,\n )\n return { durationMinutes, endTime: params.endTime }\n }\n\n // fixed duration (default)\n const endTime = addMinutes(startTime, serviceDuration)\n return { durationMinutes: serviceDuration, endTime }\n}\n\nexport function buildOverlapQuery(params: {\n blockingStatuses: string[]\n effectiveEnd: Date\n effectiveStart: Date\n excludeReservationId?: number | string\n resourceId: number | string\n}): Where {\n const { blockingStatuses, effectiveEnd, effectiveStart, excludeReservationId, resourceId } =\n params\n\n const conditions: Where[] = [\n { status: { in: blockingStatuses } },\n { startTime: { less_than: effectiveEnd.toISOString() } },\n { endTime: { greater_than: effectiveStart.toISOString() } },\n {\n or: [\n { resource: { equals: resourceId } },\n { 'items.resource': { equals: resourceId } },\n ],\n },\n ]\n\n if (excludeReservationId) {\n conditions.push({ id: { not_equals: excludeReservationId } })\n }\n\n return { and: conditions }\n}\n\n/**\n * Coarse superset query: blocking reservations whose top-level (span) window\n * comes within COARSE_MARGIN_MS of the candidate window and reference the\n * resource at top level or in items[]. The precise per-item overlap is computed\n * in memory afterwards — the top-level span is a superset of every item's\n * window, so this never misses a real conflict (margin covers neighbor buffers).\n */\nexport function buildCoarseOverlapQuery(params: {\n blockingStatuses: string[]\n candidateEnd: Date\n candidateStart: Date\n excludeReservationId?: number | string\n resourceId: number | string\n}): Where {\n const { blockingStatuses, candidateEnd, candidateStart, excludeReservationId, resourceId } =\n params\n const windowStart = new Date(candidateStart.getTime() - COARSE_MARGIN_MS)\n const windowEnd = new Date(candidateEnd.getTime() + COARSE_MARGIN_MS)\n\n const conditions: Where[] = [\n { status: { in: blockingStatuses } },\n { startTime: { less_than: windowEnd.toISOString() } },\n { endTime: { greater_than: windowStart.toISOString() } },\n {\n or: [{ resource: { equals: resourceId } }, { 'items.resource': { equals: resourceId } }],\n },\n ]\n\n if (excludeReservationId !== undefined) {\n conditions.push({ id: { not_equals: excludeReservationId } })\n }\n\n return { and: conditions }\n}\n\n/**\n * The occupancy windows a set of resolved items imposes on `resourceId`. Each\n * matching item's [startTime, endTime) is expanded by that item's own service\n * buffers (so neighbor buffers are enforced — review A3), and only the items\n * that actually reference `resourceId` count (so a multi-resource booking blocks\n * each resource only for its own item's window — review A4).\n */\nexport async function itemsToOccupancies(params: {\n bufferFor: (serviceId: number | string | undefined) => Promise<{ after: number; before: number }>\n capacityMode: CapacityMode\n items: ResolvedItem[]\n resourceId: number | string\n}): Promise<Occupancy[]> {\n const { bufferFor, capacityMode, items, resourceId } = params\n const occupancies: Occupancy[] = []\n\n for (const item of items) {\n if (String(item.resource) !== String(resourceId) || !item.endTime) {\n continue\n }\n const { after, before } = await bufferFor(item.service)\n const { effectiveEnd, effectiveStart } = computeBlockedWindow(\n new Date(item.startTime),\n new Date(item.endTime),\n before,\n after,\n )\n occupancies.push({\n blockedEnd: effectiveEnd,\n blockedStart: effectiveStart,\n units: capacityMode === 'per-guest' ? item.guestCount : 1,\n })\n }\n\n return occupancies\n}\n\n/** Occupancy a single fetched reservation imposes on `resourceId`. */\nexport async function reservationOccupancies(params: {\n bufferFor: (serviceId: number | string | undefined) => Promise<{ after: number; before: number }>\n capacityMode: CapacityMode\n reservation: Record<string, unknown>\n resourceId: number | string\n}): Promise<Occupancy[]> {\n const { bufferFor, capacityMode, reservation, resourceId } = params\n return itemsToOccupancies({\n bufferFor,\n capacityMode,\n items: resolveReservationItems(reservation),\n resourceId,\n })\n}\n\nexport function isBlockingStatus(\n status: string,\n statusMachine: StatusMachineConfig,\n): boolean {\n return statusMachine.blockingStatuses.includes(status)\n}\n\nexport function validateTransition(\n fromStatus: string,\n toStatus: string,\n statusMachine: StatusMachineConfig,\n): { reason?: string; valid: boolean } {\n const allowed = statusMachine.transitions[fromStatus]\n if (!allowed) {\n return { reason: `Unknown status: ${fromStatus}`, valid: false }\n }\n if (!allowed.includes(toStatus)) {\n return {\n reason: `Cannot transition from \"${fromStatus}\" to \"${toStatus}\"`,\n valid: false,\n }\n }\n return { valid: true }\n}\n\n// --- DB functions (use Payload Local API only) ---\n\nexport async function checkAvailability(params: {\n blockingStatuses: string[]\n bufferAfter: number\n bufferBefore: number\n endTime: Date\n excludeReservationId?: number | string\n guestCount: number\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId: number | string\n resourceSlug: string\n servicesSlug: string\n /** Other items from the same booking — counted as occupancy (review A5). */\n siblingItems?: ResolvedItem[]\n startTime: Date\n}): Promise<{\n available: boolean\n currentCount: number\n reason?: string\n totalCapacity: number\n}> {\n const {\n blockingStatuses,\n bufferAfter,\n bufferBefore,\n endTime,\n excludeReservationId,\n guestCount,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n servicesSlug,\n siblingItems,\n startTime,\n } = params\n\n // Fetch resource for quantity and capacity mode\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: 0,\n req,\n })\n const quantity = (resource.quantity as number) ?? 1\n const capacityMode = ((resource.capacityMode as string) ?? 'per-reservation') as CapacityMode\n\n // Candidate window expanded by its own buffers\n const { effectiveEnd: candidateEnd, effectiveStart: candidateStart } = computeBlockedWindow(\n startTime,\n endTime,\n bufferBefore,\n bufferAfter,\n )\n\n // Coarse superset fetch — precise per-item overlap is computed in memory below\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs } = await (payload.find as any)({\n collection: reservationSlug,\n depth: 0,\n limit: 0,\n req,\n where: buildCoarseOverlapQuery({\n blockingStatuses,\n candidateEnd,\n candidateStart,\n excludeReservationId,\n resourceId,\n }),\n })\n\n // Per-call cache: fetch each distinct service's buffers at most once\n const bufferCache = new Map<string, { after: number; before: number }>()\n const bufferFor = async (\n serviceId: number | string | undefined,\n ): Promise<{ after: number; before: number }> => {\n const key = serviceId === undefined ? '' : String(serviceId)\n const cached = bufferCache.get(key)\n if (cached) {\n return cached\n }\n let result = { after: 0, before: 0 }\n if (serviceId !== undefined) {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (payload.findByID as any)({\n id: serviceId,\n collection: servicesSlug,\n depth: 0,\n req,\n })\n if (service) {\n result = {\n after: (service.bufferTimeAfter as number) ?? 0,\n before: (service.bufferTimeBefore as number) ?? 0,\n }\n }\n } catch {\n // service missing — no buffers\n }\n }\n bufferCache.set(key, result)\n return result\n }\n\n const fetchedOccupancies = (\n await Promise.all(\n (docs as Array<Record<string, unknown>>).map((doc) =>\n reservationOccupancies({ bufferFor, capacityMode, reservation: doc, resourceId }),\n ),\n )\n ).flat()\n\n // Sibling items from the same booking (review A5) — expanded with the same\n // per-service buffers and capacity mode.\n const siblingOccupancies = siblingItems\n ? await itemsToOccupancies({ bufferFor, capacityMode, items: siblingItems, resourceId })\n : []\n\n const occupancies = [...fetchedOccupancies, ...siblingOccupancies]\n\n // Sum the units of every occupancy whose buffered window overlaps the candidate\n const currentUnits = occupancies.reduce(\n (sum, occ) =>\n doRangesOverlap(candidateStart, candidateEnd, occ.blockedStart, occ.blockedEnd)\n ? sum + occ.units\n : sum,\n 0,\n )\n\n const candidateUnits = capacityMode === 'per-guest' ? guestCount : 1\n const available = currentUnits + candidateUnits <= quantity\n\n return {\n available,\n currentCount: currentUnits,\n reason: available\n ? undefined\n : capacityMode === 'per-guest'\n ? 'Guest capacity exceeded'\n : 'All units are booked for this time',\n totalCapacity: quantity,\n }\n}\n\nexport async function getAvailableSlots(params: {\n blockingStatuses: string[]\n date: Date | string\n guestCount?: number\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId?: number | string\n resourceIds?: Array<number | string>\n resourceSlug: string\n scheduleSlug: string\n serviceId: number | string\n serviceSlug: string\n timeZone?: string\n}): Promise<Array<{ end: Date; start: Date }>> {\n const {\n blockingStatuses,\n date,\n guestCount,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceIds,\n resourceSlug,\n scheduleSlug,\n serviceId,\n serviceSlug,\n timeZone,\n } = params\n\n const tz = timeZone ?? 'UTC'\n\n // Resolve the set of resources to intersect (single-resource callers still work)\n const ids =\n resourceIds && resourceIds.length > 0\n ? resourceIds\n : resourceId !== undefined\n ? [resourceId]\n : []\n if (ids.length === 0) {\n return []\n }\n\n // 1. Service for duration + buffer times (from the primary service)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (payload.findByID as any)({\n id: serviceId,\n collection: serviceSlug,\n depth: 0,\n req,\n })\n const duration = (service.duration as number) ?? 60\n const bufferBefore = (service.bufferTimeBefore as number) ?? 0\n const bufferAfter = (service.bufferTimeAfter as number) ?? 0\n const durationType = ((service.durationType as string) ?? 'fixed') as DurationType\n\n // 2. Per resource: fetch schedules and resolve to windows. A resource with >=1\n // schedule is \"schedule-bearing\" and constrains time; a resource with zero\n // schedules is capacity-only and contributes no time windows.\n const scheduleBearingWindowLists: Array<Array<{ end: Date; start: Date }>> = []\n for (const rid of ids) {\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 req,\n where: {\n and: [{ resource: { equals: rid } }, { active: { equals: true } }],\n },\n })\n if (!schedules || schedules.length === 0) {\n continue\n }\n // A11: an exception on ANY of the resource's schedules makes the whole\n // resource unavailable that day — not just the schedule it's recorded on.\n const exceptedToday = (schedules as Array<Record<string, unknown>>).some((s) =>\n isExceptionDate(\n date,\n (s.exceptions as Array<{ date: string; endDate?: string }> | undefined) ?? [],\n tz,\n ),\n )\n if (exceptedToday) {\n scheduleBearingWindowLists.push([])\n continue\n }\n const windows: Array<{ end: Date; start: Date }> = []\n for (const schedule of schedules) {\n windows.push(\n ...resolveScheduleForDate(\n schedule as unknown as Parameters<typeof resolveScheduleForDate>[0],\n date,\n tz,\n ),\n )\n }\n scheduleBearingWindowLists.push(windows)\n }\n\n // No resource constrains time → no basis for generating slots\n if (scheduleBearingWindowLists.length === 0) {\n return []\n }\n\n // 3. Intersect all schedule-bearing window lists\n let timeRanges = scheduleBearingWindowLists[0]\n for (let i = 1; i < scheduleBearingWindowLists.length; i++) {\n timeRanges = intersectIntervals(timeRanges, scheduleBearingWindowLists[i])\n }\n if (timeRanges.length === 0) {\n return []\n }\n\n // 4. Candidate slot sizing\n // NOTE: epoch-trick sizing is only meaningful for fixed/flexible durations.\n // full-day services return early via the range-as-slot branch below and never\n // consume slotDuration — keep it that way if reordering this function.\n const { endTime: slotEndOffset } = computeEndTime({\n durationType,\n serviceDuration: duration,\n startTime: new Date(0),\n timeZone: tz,\n })\n const slotDuration = Math.round(slotEndOffset.getTime() / 60_000)\n const effectiveDuration = durationType === 'fixed' ? duration : slotDuration\n\n // Helper: a window is available only if EVERY required resource is free\n const allAvailable = async (\n start: Date,\n end: Date,\n bBefore: number,\n bAfter: number,\n ): Promise<boolean> => {\n for (const rid of ids) {\n const result = await checkAvailability({\n blockingStatuses,\n bufferAfter: bAfter,\n bufferBefore: bBefore,\n endTime: end,\n guestCount: guestCount ?? 1,\n payload,\n req,\n reservationSlug,\n resourceId: rid,\n resourceSlug,\n servicesSlug: serviceSlug,\n startTime: start,\n })\n if (!result.available) {\n return false\n }\n }\n return true\n }\n\n const availableSlots: Array<{ end: Date; start: Date }> = []\n\n // Full-day: offer each range as a single slot if all resources are free\n if (durationType === 'full-day') {\n for (const range of timeRanges) {\n if (await allAvailable(range.start, range.end, 0, 0)) {\n availableSlots.push({ end: range.end, start: range.start })\n }\n }\n return availableSlots\n }\n\n const stepSize = Math.min(effectiveDuration, 15)\n\n for (const range of timeRanges) {\n let candidateStart = new Date(range.start)\n\n while (true) {\n const candidateEnd = addMinutes(candidateStart, effectiveDuration)\n if (candidateEnd > range.end) {\n break\n }\n\n if (await allAvailable(candidateStart, candidateEnd, bufferBefore, bufferAfter)) {\n availableSlots.push({ end: candidateEnd, start: new Date(candidateStart) })\n }\n\n candidateStart = addMinutes(candidateStart, stepSize)\n }\n }\n\n return availableSlots\n}\n"],"names":["resolveReservationItems","isExceptionDate","resolveScheduleForDate","addMinutes","computeBlockedWindow","doRangesOverlap","intersectIntervals","endOfDayInTimezone","COARSE_MARGIN_MS","computeEndTime","params","durationType","serviceDuration","startTime","end","timeZone","durationMinutes","Math","round","getTime","endTime","buildOverlapQuery","blockingStatuses","effectiveEnd","effectiveStart","excludeReservationId","resourceId","conditions","status","in","less_than","toISOString","greater_than","or","resource","equals","push","id","not_equals","and","buildCoarseOverlapQuery","candidateEnd","candidateStart","windowStart","Date","windowEnd","undefined","itemsToOccupancies","bufferFor","capacityMode","items","occupancies","item","String","after","before","service","blockedEnd","blockedStart","units","guestCount","reservationOccupancies","reservation","isBlockingStatus","statusMachine","includes","validateTransition","fromStatus","toStatus","allowed","transitions","reason","valid","checkAvailability","bufferAfter","bufferBefore","payload","req","reservationSlug","resourceSlug","servicesSlug","siblingItems","findByID","collection","depth","quantity","docs","find","limit","where","bufferCache","Map","serviceId","key","cached","get","result","bufferTimeAfter","bufferTimeBefore","set","fetchedOccupancies","Promise","all","map","doc","flat","siblingOccupancies","currentUnits","reduce","sum","occ","candidateUnits","available","currentCount","totalCapacity","getAvailableSlots","date","resourceIds","scheduleSlug","serviceSlug","tz","ids","length","duration","scheduleBearingWindowLists","rid","schedules","active","exceptedToday","some","s","exceptions","windows","schedule","timeRanges","i","slotEndOffset","slotDuration","effectiveDuration","allAvailable","start","bBefore","bAfter","availableSlots","range","stepSize","min"],"mappings":"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"}
@@ -99,6 +99,7 @@
99
99
  "fieldCustomerCreateNew": "إنشاء عميل جديد",
100
100
  "fieldCustomerClear": "مسح التحديد",
101
101
  "calendarPending": "قيد الانتظار",
102
+ "calendarShowingNofM": "عرض {{shown}} من {{total}} حجزًا — قم بتضييق النطاق أو التصفية لرؤية الباقي.",
102
103
  "pendingDateTime": "التاريخ / الوقت",
103
104
  "pendingActions": "الإجراءات",
104
105
  "pendingSelectAll": "تحديد الكل",
@@ -99,6 +99,7 @@
99
99
  "fieldCustomerCreateNew": "Neuen Kunden anlegen",
100
100
  "fieldCustomerClear": "Auswahl aufheben",
101
101
  "calendarPending": "Ausstehend",
102
+ "calendarShowingNofM": "{{shown}} von {{total}} Reservierungen werden angezeigt – Zeitraum eingrenzen oder filtern, um den Rest zu sehen.",
102
103
  "pendingDateTime": "Datum / Zeit",
103
104
  "pendingActions": "Aktionen",
104
105
  "pendingSelectAll": "Alle auswählen",
@@ -99,6 +99,7 @@
99
99
  "fieldCustomerCreateNew": "Create new customer",
100
100
  "fieldCustomerClear": "Clear selection",
101
101
  "calendarPending": "Pending",
102
+ "calendarShowingNofM": "Showing {{shown}} of {{total}} reservations — narrow the range or filter to see the rest.",
102
103
  "pendingDateTime": "Date / Time",
103
104
  "pendingActions": "Actions",
104
105
  "pendingSelectAll": "Select all",
@@ -99,6 +99,7 @@
99
99
  "fieldCustomerCreateNew": "Crear nuevo cliente",
100
100
  "fieldCustomerClear": "Borrar selección",
101
101
  "calendarPending": "Pendiente",
102
+ "calendarShowingNofM": "Mostrando {{shown}} de {{total}} reservas: reduce el rango o filtra para ver el resto.",
102
103
  "pendingDateTime": "Fecha / Hora",
103
104
  "pendingActions": "Acciones",
104
105
  "pendingSelectAll": "Seleccionar todo",
@@ -99,6 +99,7 @@
99
99
  "fieldCustomerCreateNew": "ایجاد مشتری جدید",
100
100
  "fieldCustomerClear": "پاک کردن انتخاب",
101
101
  "calendarPending": "در انتظار",
102
+ "calendarShowingNofM": "نمایش {{shown}} از {{total}} رزرو — برای دیدن بقیه بازه را محدود یا فیلتر کنید.",
102
103
  "pendingDateTime": "تاریخ / زمان",
103
104
  "pendingActions": "اقدامات",
104
105
  "pendingSelectAll": "انتخاب همه",
@@ -99,6 +99,7 @@
99
99
  "fieldCustomerCreateNew": "Créer un nouveau client",
100
100
  "fieldCustomerClear": "Effacer la sélection",
101
101
  "calendarPending": "En attente",
102
+ "calendarShowingNofM": "Affichage de {{shown}} sur {{total}} réservations — réduisez la plage ou filtrez pour voir le reste.",
102
103
  "pendingDateTime": "Date / Heure",
103
104
  "pendingActions": "Actions",
104
105
  "pendingSelectAll": "Tout sélectionner",
@@ -99,6 +99,7 @@
99
99
  "fieldCustomerCreateNew": "नया ग्राहक बनाएँ",
100
100
  "fieldCustomerClear": "चयन हटाएँ",
101
101
  "calendarPending": "लंबित",
102
+ "calendarShowingNofM": "{{total}} में से {{shown}} आरक्षण दिखाए जा रहे हैं — बाकी देखने के लिए सीमा कम करें या फ़िल्टर करें।",
102
103
  "pendingDateTime": "तारीख / समय",
103
104
  "pendingActions": "क्रियाएँ",
104
105
  "pendingSelectAll": "सभी चुनें",
@@ -99,6 +99,7 @@
99
99
  "fieldCustomerCreateNew": "Buat pelanggan baru",
100
100
  "fieldCustomerClear": "Hapus pilihan",
101
101
  "calendarPending": "Menunggu",
102
+ "calendarShowingNofM": "Menampilkan {{shown}} dari {{total}} reservasi — persempit rentang atau filter untuk melihat sisanya.",
102
103
  "pendingDateTime": "Tanggal / Waktu",
103
104
  "pendingActions": "Tindakan",
104
105
  "pendingSelectAll": "Pilih semua",
@@ -99,6 +99,7 @@
99
99
  "fieldCustomerCreateNew": "Utwórz nowego klienta",
100
100
  "fieldCustomerClear": "Wyczyść wybór",
101
101
  "calendarPending": "Oczekujące",
102
+ "calendarShowingNofM": "Wyświetlono {{shown}} z {{total}} rezerwacji — zawęź zakres lub filtruj, aby zobaczyć resztę.",
102
103
  "pendingDateTime": "Data / Czas",
103
104
  "pendingActions": "Akcje",
104
105
  "pendingSelectAll": "Zaznacz wszystkie",
@@ -99,6 +99,7 @@
99
99
  "fieldCustomerCreateNew": "Создать нового клиента",
100
100
  "fieldCustomerClear": "Очистить выбор",
101
101
  "calendarPending": "Ожидает",
102
+ "calendarShowingNofM": "Показано {{shown}} из {{total}} броней — сузьте диапазон или примените фильтр, чтобы увидеть остальные.",
102
103
  "pendingDateTime": "Дата / Время",
103
104
  "pendingActions": "Действия",
104
105
  "pendingSelectAll": "Выбрать все",
@@ -99,6 +99,7 @@
99
99
  "fieldCustomerCreateNew": "Yeni müşteri oluştur",
100
100
  "fieldCustomerClear": "Seçimi temizle",
101
101
  "calendarPending": "Beklemede",
102
+ "calendarShowingNofM": "{{total}} rezervasyondan {{shown}} tanesi gösteriliyor — geri kalanını görmek için aralığı daraltın veya filtreleyin.",
102
103
  "pendingDateTime": "Tarih / Saat",
103
104
  "pendingActions": "İşlemler",
104
105
  "pendingSelectAll": "Tümünü seç",
@@ -99,6 +99,7 @@
99
99
  "fieldCustomerCreateNew": "新建客户",
100
100
  "fieldCustomerClear": "清除选择",
101
101
  "calendarPending": "待处理",
102
+ "calendarShowingNofM": "显示 {{total}} 个预约中的 {{shown}} 个 — 缩小范围或筛选以查看其余部分。",
102
103
  "pendingDateTime": "日期 / 时间",
103
104
  "pendingActions": "操作",
104
105
  "pendingSelectAll": "全选",
package/dist/types.d.ts CHANGED
@@ -3,6 +3,10 @@ export type DurationType = 'fixed' | 'flexible' | 'full-day';
3
3
  export type CapacityMode = 'per-guest' | 'per-reservation';
4
4
  export type StatusMachineConfig = {
5
5
  blockingStatuses: string[];
6
+ /** Status treated as "cancelled" — fires beforeBookingCancel/afterBookingCancel, the cancellation notice period, and the cancellationReason field. */
7
+ cancelStatus: string;
8
+ /** Status treated as "confirmed" — fires beforeBookingConfirm/afterBookingConfirm. */
9
+ confirmStatus: string;
6
10
  defaultStatus: string;
7
11
  statuses: string[];
8
12
  terminalStatuses: string[];
@@ -64,12 +68,15 @@ export type ResourceOwnerModeConfig = {
64
68
  ownerCollection?: string;
65
69
  /** Field name for the owner relationship on Resources (default: 'owner') */
66
70
  ownerField?: string;
71
+ /** User field holding the role for admin detection (default: staffProvisioning.roleField or 'role') */
72
+ roleField?: string;
67
73
  };
68
74
  export type ResolvedResourceOwnerModeConfig = {
69
75
  adminRoles: string[];
70
76
  ownedServices: boolean;
71
77
  ownerCollection?: string;
72
78
  ownerField: string;
79
+ roleField: string;
73
80
  };
74
81
  export type StaffProvisioningConfig = {
75
82
  /** Stamp tenant / custom fields onto the provisioned Resource before create. */
@@ -97,6 +104,17 @@ export type ResolvedStaffProvisioningConfig = {
97
104
  staffRoles: string[];
98
105
  userCollection: string;
99
106
  };
107
+ /**
108
+ * Per-collection override applied to a generated collection. `fields` is a
109
+ * function receiving the plugin's default fields so you can append/reorder/
110
+ * replace them; supplied `hooks` are merged with (not replacing) the plugin's;
111
+ * `access` composes per operation; `slug` is ignored (use the `slugs` option).
112
+ */
113
+ export type CollectionOverride = {
114
+ fields?: (args: {
115
+ defaultFields: Field[];
116
+ }) => Field[];
117
+ } & Omit<Partial<CollectionConfig>, 'fields' | 'slug'>;
100
118
  export type ReservationPluginConfig = {
101
119
  /** Override access control per collection */
102
120
  access?: {
@@ -112,11 +130,19 @@ export type ReservationPluginConfig = {
112
130
  allowGuestBooking?: boolean;
113
131
  /** Hours of notice required before cancellation */
114
132
  cancellationNoticePeriod?: number;
133
+ /** Per-collection overrides applied to the generated collections (issue #4) */
134
+ collectionOverrides?: {
135
+ customers?: CollectionOverride;
136
+ reservations?: CollectionOverride;
137
+ resources?: CollectionOverride;
138
+ schedules?: CollectionOverride;
139
+ services?: CollectionOverride;
140
+ };
115
141
  /** Default buffer time in minutes between reservations */
116
142
  defaultBufferTime?: number;
117
143
  /** Disable the plugin entirely */
118
144
  disabled?: boolean;
119
- /** Extra fields to append to the Reservations collection */
145
+ /** @deprecated Use `collectionOverrides.reservations.fields` instead. Extra fields appended to Reservations. */
120
146
  extraReservationFields?: Field[];
121
147
  /** Plugin hooks for external integrations */
122
148
  hooks?: ReservationPluginHooks;
@@ -128,6 +154,12 @@ export type ReservationPluginConfig = {
128
154
  cookieName?: string;
129
155
  /** Tenant field name on scoped collections (default 'tenant'). */
130
156
  tenantField?: string;
157
+ /**
158
+ * Field on the tenant document holding its IANA timezone (default 'timezone').
159
+ * When set and the selected tenant has a valid value, the admin views resolve
160
+ * day-boundaries in that tenant's zone instead of the global `timezone`.
161
+ */
162
+ timezoneField?: string;
131
163
  };
132
164
  /** Enable resource-owner multi-tenancy (opt-in) */
133
165
  resourceOwnerMode?: ResourceOwnerModeConfig;
@@ -146,6 +178,8 @@ export type ReservationPluginConfig = {
146
178
  staffProvisioning?: StaffProvisioningConfig;
147
179
  /** Configurable status machine (defaults to current behavior) */
148
180
  statusMachine?: Partial<StatusMachineConfig>;
181
+ /** IANA business timezone governing schedules and day boundaries (default 'UTC') */
182
+ timezone?: string;
149
183
  /** Which existing auth collection to extend with customer fields */
150
184
  userCollection?: string;
151
185
  };
@@ -160,15 +194,25 @@ export type ResolvedReservationPluginConfig = {
160
194
  adminGroup: string;
161
195
  allowGuestBooking: boolean;
162
196
  cancellationNoticePeriod: number;
197
+ collectionOverrides: {
198
+ customers?: CollectionOverride;
199
+ reservations?: CollectionOverride;
200
+ resources?: CollectionOverride;
201
+ schedules?: CollectionOverride;
202
+ services?: CollectionOverride;
203
+ };
163
204
  defaultBufferTime: number;
164
205
  disabled: boolean;
165
206
  extraReservationFields: Field[];
207
+ /** Whether the media collection (`slugs.media`) exists — set by the plugin; gates the image upload fields. */
208
+ hasMediaCollection: boolean;
166
209
  hooks: ReservationPluginHooks;
167
210
  leaveTypes: string[];
168
211
  localized: boolean;
169
212
  multiTenant: {
170
213
  cookieName: string;
171
214
  tenantField: string;
215
+ timezoneField: string;
172
216
  };
173
217
  resourceOwnerMode: ResolvedResourceOwnerModeConfig | undefined;
174
218
  resourceTypes: string[];
@@ -182,6 +226,7 @@ export type ResolvedReservationPluginConfig = {
182
226
  };
183
227
  staffProvisioning: ResolvedStaffProvisioningConfig | undefined;
184
228
  statusMachine: StatusMachineConfig;
229
+ timezone: string;
185
230
  userCollection: string | undefined;
186
231
  };
187
232
  export type ReservationStatus = 'cancelled' | 'completed' | 'confirmed' | 'no-show' | 'pending';
package/dist/types.js CHANGED
@@ -3,6 +3,8 @@ export const DEFAULT_STATUS_MACHINE = {
3
3
  'pending',
4
4
  'confirmed'
5
5
  ],
6
+ cancelStatus: 'cancelled',
7
+ confirmStatus: 'confirmed',
6
8
  defaultStatus: 'pending',
7
9
  statuses: [
8
10
  'pending',
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { CollectionConfig, Field, PayloadRequest } from 'payload'\n\n// --- Duration & Capacity models ---\n\nexport type DurationType = 'fixed' | 'flexible' | 'full-day'\n\nexport type CapacityMode = 'per-guest' | 'per-reservation'\n\n// --- Configurable status machine ---\n\nexport type StatusMachineConfig = {\n blockingStatuses: string[]\n defaultStatus: string\n statuses: string[]\n terminalStatuses: string[]\n transitions: Record<string, string[]>\n}\n\nexport const DEFAULT_STATUS_MACHINE: StatusMachineConfig = {\n blockingStatuses: ['pending', 'confirmed'],\n defaultStatus: 'pending',\n statuses: ['pending', 'confirmed', 'completed', 'cancelled', 'no-show'],\n terminalStatuses: ['completed', 'cancelled', 'no-show'],\n transitions: {\n cancelled: [],\n completed: [],\n confirmed: ['completed', 'cancelled', 'no-show'],\n 'no-show': [],\n pending: ['confirmed', 'cancelled'],\n },\n}\n\n// --- Reservation item (for multi-resource bookings, Phase 3) ---\n\nexport type ReservationItemConfig = {\n endTime?: string\n guestCount?: number\n resource: string\n service?: string\n startTime?: string\n}\n\n// --- Plugin hooks for external integrations ---\n\nexport type ReservationPluginHooks = {\n afterBookingCancel?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterBookingConfirm?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterBookingCreate?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterStatusChange?: Array<\n (args: {\n doc: Record<string, unknown>\n newStatus: string\n previousStatus: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingCancel?: Array<\n (args: {\n doc: Record<string, unknown>\n reason?: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingConfirm?: Array<\n (args: {\n doc: Record<string, unknown>\n newStatus: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingCreate?: Array<\n (args: {\n data: Record<string, unknown>\n req: PayloadRequest\n }) => Promise<Record<string, unknown>> | Record<string, unknown>\n >\n}\n\n// --- Resource owner mode ---\n\nexport type ResourceOwnerModeConfig = {\n /** Roles that can see all records (default: check req.user.collection === adminCollection) */\n adminRoles?: string[]\n /** Whether Services also get an owner field (default: false — Services are platform-managed) */\n ownedServices?: boolean\n /**\n * Collection the owner field relates to (where owners/staff live). Defaults to\n * `staffProvisioning.userCollection` when set, otherwise `slugs.customers`. Set\n * this when owners live in a different collection than your customers (e.g.\n * separate `users` and `customers` collections).\n */\n ownerCollection?: string\n /** Field name for the owner relationship on Resources (default: 'owner') */\n ownerField?: string\n}\n\nexport type ResolvedResourceOwnerModeConfig = {\n adminRoles: string[]\n ownedServices: boolean\n ownerCollection?: string\n ownerField: string\n}\n\n// --- Staff provisioning ---\n\nexport type StaffProvisioningConfig = {\n /** Stamp tenant / custom fields onto the provisioned Resource before create. */\n beforeCreate?: (args: {\n data: Record<string, unknown>\n req: PayloadRequest\n user: Record<string, unknown>\n }) => Promise<Record<string, unknown>> | Record<string, unknown>\n /** User field copied into Resource `name` (default 'name', falls back to email). */\n nameFrom?: string\n /** resourceType to stamp (default 'staff'). Must be a valid resourceType. */\n resourceType?: string\n /** Field on the user holding the role (default 'role'). */\n roleField?: string\n /** Role value(s) marking a user as staff. Required, non-empty. */\n staffRoles: string[]\n /** Auth collection holding staff users. Defaults to top-level `userCollection`. */\n userCollection?: string\n}\n\nexport type ResolvedStaffProvisioningConfig = {\n beforeCreate?: StaffProvisioningConfig['beforeCreate']\n nameFrom: string\n resourceType: string\n roleField: string\n staffRoles: string[]\n userCollection: string\n}\n\n// --- Plugin configuration ---\n\nexport type ReservationPluginConfig = {\n /** Override access control per collection */\n access?: {\n customers?: CollectionConfig['access']\n reservations?: CollectionConfig['access']\n resources?: CollectionConfig['access']\n schedules?: CollectionConfig['access']\n services?: CollectionConfig['access']\n }\n /** Admin group name for all reservation collections */\n adminGroup?: string\n /** Allow bookings without a customer account by default (per-service override available) */\n allowGuestBooking?: boolean\n /** Hours of notice required before cancellation */\n cancellationNoticePeriod?: number\n /** Default buffer time in minutes between reservations */\n defaultBufferTime?: number\n /** Disable the plugin entirely */\n disabled?: boolean\n /** Extra fields to append to the Reservations collection */\n extraReservationFields?: Field[]\n /** Plugin hooks for external integrations */\n hooks?: ReservationPluginHooks\n /** Configurable leave/exception type vocabulary (default: vacation/sick/personal/closure/other) */\n leaveTypes?: string[]\n /** Tenant scoping for the custom admin views (calendar, availability, dashboard). Applied only when the scoped collection has the tenant field AND the tenant cookie is set. */\n multiTenant?: {\n /** Cookie written by the tenant-selector (default 'payload-tenant'). */\n cookieName?: string\n /** Tenant field name on scoped collections (default 'tenant'). */\n tenantField?: string\n }\n /** Enable resource-owner multi-tenancy (opt-in) */\n resourceOwnerMode?: ResourceOwnerModeConfig\n /** Configurable resourceType vocabulary (default: staff/equipment/room) */\n resourceTypes?: string[]\n /** Override collection slugs */\n slugs?: {\n customers?: string\n media?: string\n reservations?: string\n resources?: string\n schedules?: string\n services?: string\n }\n /** Auto-provision a Resource from staff-role users (opt-in; requires resourceOwnerMode) */\n staffProvisioning?: StaffProvisioningConfig\n /** Configurable status machine (defaults to current behavior) */\n statusMachine?: Partial<StatusMachineConfig>\n /** Which existing auth collection to extend with customer fields */\n userCollection?: string\n}\n\nexport type ResolvedReservationPluginConfig = {\n access: {\n customers?: CollectionConfig['access']\n reservations?: CollectionConfig['access']\n resources?: CollectionConfig['access']\n schedules?: CollectionConfig['access']\n services?: CollectionConfig['access']\n }\n adminGroup: string\n allowGuestBooking: boolean\n cancellationNoticePeriod: number\n defaultBufferTime: number\n disabled: boolean\n extraReservationFields: Field[]\n hooks: ReservationPluginHooks\n leaveTypes: string[]\n localized: boolean\n multiTenant: {\n cookieName: string\n tenantField: string\n }\n resourceOwnerMode: ResolvedResourceOwnerModeConfig | undefined\n resourceTypes: string[]\n slugs: {\n customers: string\n media: string\n reservations: string\n resources: string\n schedules: string\n services: string\n }\n staffProvisioning: ResolvedStaffProvisioningConfig | undefined\n statusMachine: StatusMachineConfig\n userCollection: string | undefined\n}\n\nexport type ReservationStatus = 'cancelled' | 'completed' | 'confirmed' | 'no-show' | 'pending'\n\nexport type DayOfWeek = 'fri' | 'mon' | 'sat' | 'sun' | 'thu' | 'tue' | 'wed'\n\nexport type ScheduleType = 'manual' | 'recurring'\n\n/** @deprecated Use DEFAULT_STATUS_MACHINE.transitions instead */\nexport const VALID_STATUS_TRANSITIONS: Record<ReservationStatus, ReservationStatus[]> =\n DEFAULT_STATUS_MACHINE.transitions as Record<ReservationStatus, ReservationStatus[]>\n"],"names":["DEFAULT_STATUS_MACHINE","blockingStatuses","defaultStatus","statuses","terminalStatuses","transitions","cancelled","completed","confirmed","pending","VALID_STATUS_TRANSITIONS"],"mappings":"AAkBA,OAAO,MAAMA,yBAA8C;IACzDC,kBAAkB;QAAC;QAAW;KAAY;IAC1CC,eAAe;IACfC,UAAU;QAAC;QAAW;QAAa;QAAa;QAAa;KAAU;IACvEC,kBAAkB;QAAC;QAAa;QAAa;KAAU;IACvDC,aAAa;QACXC,WAAW,EAAE;QACbC,WAAW,EAAE;QACbC,WAAW;YAAC;YAAa;YAAa;SAAU;QAChD,WAAW,EAAE;QACbC,SAAS;YAAC;YAAa;SAAY;IACrC;AACF,EAAC;AA8MD,+DAA+D,GAC/D,OAAO,MAAMC,2BACXV,uBAAuBK,WAAW,CAAkD"}
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { CollectionConfig, Field, PayloadRequest } from 'payload'\n\n// --- Duration & Capacity models ---\n\nexport type DurationType = 'fixed' | 'flexible' | 'full-day'\n\nexport type CapacityMode = 'per-guest' | 'per-reservation'\n\n// --- Configurable status machine ---\n\nexport type StatusMachineConfig = {\n blockingStatuses: string[]\n /** Status treated as \"cancelled\" — fires beforeBookingCancel/afterBookingCancel, the cancellation notice period, and the cancellationReason field. */\n cancelStatus: string\n /** Status treated as \"confirmed\" — fires beforeBookingConfirm/afterBookingConfirm. */\n confirmStatus: string\n defaultStatus: string\n statuses: string[]\n terminalStatuses: string[]\n transitions: Record<string, string[]>\n}\n\nexport const DEFAULT_STATUS_MACHINE: StatusMachineConfig = {\n blockingStatuses: ['pending', 'confirmed'],\n cancelStatus: 'cancelled',\n confirmStatus: 'confirmed',\n defaultStatus: 'pending',\n statuses: ['pending', 'confirmed', 'completed', 'cancelled', 'no-show'],\n terminalStatuses: ['completed', 'cancelled', 'no-show'],\n transitions: {\n cancelled: [],\n completed: [],\n confirmed: ['completed', 'cancelled', 'no-show'],\n 'no-show': [],\n pending: ['confirmed', 'cancelled'],\n },\n}\n\n// --- Reservation item (for multi-resource bookings, Phase 3) ---\n\nexport type ReservationItemConfig = {\n endTime?: string\n guestCount?: number\n resource: string\n service?: string\n startTime?: string\n}\n\n// --- Plugin hooks for external integrations ---\n\nexport type ReservationPluginHooks = {\n afterBookingCancel?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterBookingConfirm?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterBookingCreate?: Array<\n (args: { doc: Record<string, unknown>; req: PayloadRequest }) => Promise<void> | void\n >\n afterStatusChange?: Array<\n (args: {\n doc: Record<string, unknown>\n newStatus: string\n previousStatus: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingCancel?: Array<\n (args: {\n doc: Record<string, unknown>\n reason?: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingConfirm?: Array<\n (args: {\n doc: Record<string, unknown>\n newStatus: string\n req: PayloadRequest\n }) => Promise<void> | void\n >\n beforeBookingCreate?: Array<\n (args: {\n data: Record<string, unknown>\n req: PayloadRequest\n }) => Promise<Record<string, unknown>> | Record<string, unknown>\n >\n}\n\n// --- Resource owner mode ---\n\nexport type ResourceOwnerModeConfig = {\n /** Roles that can see all records (default: check req.user.collection === adminCollection) */\n adminRoles?: string[]\n /** Whether Services also get an owner field (default: false — Services are platform-managed) */\n ownedServices?: boolean\n /**\n * Collection the owner field relates to (where owners/staff live). Defaults to\n * `staffProvisioning.userCollection` when set, otherwise `slugs.customers`. Set\n * this when owners live in a different collection than your customers (e.g.\n * separate `users` and `customers` collections).\n */\n ownerCollection?: string\n /** Field name for the owner relationship on Resources (default: 'owner') */\n ownerField?: string\n /** User field holding the role for admin detection (default: staffProvisioning.roleField or 'role') */\n roleField?: string\n}\n\nexport type ResolvedResourceOwnerModeConfig = {\n adminRoles: string[]\n ownedServices: boolean\n ownerCollection?: string\n ownerField: string\n roleField: string\n}\n\n// --- Staff provisioning ---\n\nexport type StaffProvisioningConfig = {\n /** Stamp tenant / custom fields onto the provisioned Resource before create. */\n beforeCreate?: (args: {\n data: Record<string, unknown>\n req: PayloadRequest\n user: Record<string, unknown>\n }) => Promise<Record<string, unknown>> | Record<string, unknown>\n /** User field copied into Resource `name` (default 'name', falls back to email). */\n nameFrom?: string\n /** resourceType to stamp (default 'staff'). Must be a valid resourceType. */\n resourceType?: string\n /** Field on the user holding the role (default 'role'). */\n roleField?: string\n /** Role value(s) marking a user as staff. Required, non-empty. */\n staffRoles: string[]\n /** Auth collection holding staff users. Defaults to top-level `userCollection`. */\n userCollection?: string\n}\n\nexport type ResolvedStaffProvisioningConfig = {\n beforeCreate?: StaffProvisioningConfig['beforeCreate']\n nameFrom: string\n resourceType: string\n roleField: string\n staffRoles: string[]\n userCollection: string\n}\n\n// --- Plugin configuration ---\n\n/**\n * Per-collection override applied to a generated collection. `fields` is a\n * function receiving the plugin's default fields so you can append/reorder/\n * replace them; supplied `hooks` are merged with (not replacing) the plugin's;\n * `access` composes per operation; `slug` is ignored (use the `slugs` option).\n */\nexport type CollectionOverride = {\n fields?: (args: { defaultFields: Field[] }) => Field[]\n} & Omit<Partial<CollectionConfig>, 'fields' | 'slug'>\n\nexport type ReservationPluginConfig = {\n /** Override access control per collection */\n access?: {\n customers?: CollectionConfig['access']\n reservations?: CollectionConfig['access']\n resources?: CollectionConfig['access']\n schedules?: CollectionConfig['access']\n services?: CollectionConfig['access']\n }\n /** Admin group name for all reservation collections */\n adminGroup?: string\n /** Allow bookings without a customer account by default (per-service override available) */\n allowGuestBooking?: boolean\n /** Hours of notice required before cancellation */\n cancellationNoticePeriod?: number\n /** Per-collection overrides applied to the generated collections (issue #4) */\n collectionOverrides?: {\n customers?: CollectionOverride\n reservations?: CollectionOverride\n resources?: CollectionOverride\n schedules?: CollectionOverride\n services?: CollectionOverride\n }\n /** Default buffer time in minutes between reservations */\n defaultBufferTime?: number\n /** Disable the plugin entirely */\n disabled?: boolean\n /** @deprecated Use `collectionOverrides.reservations.fields` instead. Extra fields appended to Reservations. */\n extraReservationFields?: Field[]\n /** Plugin hooks for external integrations */\n hooks?: ReservationPluginHooks\n /** Configurable leave/exception type vocabulary (default: vacation/sick/personal/closure/other) */\n leaveTypes?: string[]\n /** Tenant scoping for the custom admin views (calendar, availability, dashboard). Applied only when the scoped collection has the tenant field AND the tenant cookie is set. */\n multiTenant?: {\n /** Cookie written by the tenant-selector (default 'payload-tenant'). */\n cookieName?: string\n /** Tenant field name on scoped collections (default 'tenant'). */\n tenantField?: string\n /**\n * Field on the tenant document holding its IANA timezone (default 'timezone').\n * When set and the selected tenant has a valid value, the admin views resolve\n * day-boundaries in that tenant's zone instead of the global `timezone`.\n */\n timezoneField?: string\n }\n /** Enable resource-owner multi-tenancy (opt-in) */\n resourceOwnerMode?: ResourceOwnerModeConfig\n /** Configurable resourceType vocabulary (default: staff/equipment/room) */\n resourceTypes?: string[]\n /** Override collection slugs */\n slugs?: {\n customers?: string\n media?: string\n reservations?: string\n resources?: string\n schedules?: string\n services?: string\n }\n /** Auto-provision a Resource from staff-role users (opt-in; requires resourceOwnerMode) */\n staffProvisioning?: StaffProvisioningConfig\n /** Configurable status machine (defaults to current behavior) */\n statusMachine?: Partial<StatusMachineConfig>\n /** IANA business timezone governing schedules and day boundaries (default 'UTC') */\n timezone?: string\n /** Which existing auth collection to extend with customer fields */\n userCollection?: string\n}\n\nexport type ResolvedReservationPluginConfig = {\n access: {\n customers?: CollectionConfig['access']\n reservations?: CollectionConfig['access']\n resources?: CollectionConfig['access']\n schedules?: CollectionConfig['access']\n services?: CollectionConfig['access']\n }\n adminGroup: string\n allowGuestBooking: boolean\n cancellationNoticePeriod: number\n collectionOverrides: {\n customers?: CollectionOverride\n reservations?: CollectionOverride\n resources?: CollectionOverride\n schedules?: CollectionOverride\n services?: CollectionOverride\n }\n defaultBufferTime: number\n disabled: boolean\n extraReservationFields: Field[]\n /** Whether the media collection (`slugs.media`) exists — set by the plugin; gates the image upload fields. */\n hasMediaCollection: boolean\n hooks: ReservationPluginHooks\n leaveTypes: string[]\n localized: boolean\n multiTenant: {\n cookieName: string\n tenantField: string\n timezoneField: string\n }\n resourceOwnerMode: ResolvedResourceOwnerModeConfig | undefined\n resourceTypes: string[]\n slugs: {\n customers: string\n media: string\n reservations: string\n resources: string\n schedules: string\n services: string\n }\n staffProvisioning: ResolvedStaffProvisioningConfig | undefined\n statusMachine: StatusMachineConfig\n timezone: string\n userCollection: string | undefined\n}\n\nexport type ReservationStatus = 'cancelled' | 'completed' | 'confirmed' | 'no-show' | 'pending'\n\nexport type DayOfWeek = 'fri' | 'mon' | 'sat' | 'sun' | 'thu' | 'tue' | 'wed'\n\nexport type ScheduleType = 'manual' | 'recurring'\n\n/** @deprecated Use DEFAULT_STATUS_MACHINE.transitions instead */\nexport const VALID_STATUS_TRANSITIONS: Record<ReservationStatus, ReservationStatus[]> =\n DEFAULT_STATUS_MACHINE.transitions as Record<ReservationStatus, ReservationStatus[]>\n"],"names":["DEFAULT_STATUS_MACHINE","blockingStatuses","cancelStatus","confirmStatus","defaultStatus","statuses","terminalStatuses","transitions","cancelled","completed","confirmed","pending","VALID_STATUS_TRANSITIONS"],"mappings":"AAsBA,OAAO,MAAMA,yBAA8C;IACzDC,kBAAkB;QAAC;QAAW;KAAY;IAC1CC,cAAc;IACdC,eAAe;IACfC,eAAe;IACfC,UAAU;QAAC;QAAW;QAAa;QAAa;QAAa;KAAU;IACvEC,kBAAkB;QAAC;QAAa;QAAa;KAAU;IACvDC,aAAa;QACXC,WAAW,EAAE;QACbC,WAAW,EAAE;QACbC,WAAW;YAAC;YAAa;YAAa;SAAU;QAChD,WAAW,EAAE;QACbC,SAAS;YAAC;YAAa;SAAY;IACrC;AACF,EAAC;AAsPD,+DAA+D,GAC/D,OAAO,MAAMC,2BACXZ,uBAAuBO,WAAW,CAAkD"}
@@ -0,0 +1,14 @@
1
+ import type { CollectionConfig } from 'payload';
2
+ import type { CollectionOverride } from '../types.js';
3
+ /**
4
+ * Apply a per-collection override to a generated collection, protecting the
5
+ * plugin's load-bearing behavior:
6
+ * - `fields`: a function receiving the plugin's default fields, returning the
7
+ * final list (append/reorder/replace — covers the issue #4 join-field case).
8
+ * - `hooks`: MERGED per event array (plugin hooks run first, then the user's) —
9
+ * an override can add hooks but never clobber conflict detection / status hooks.
10
+ * - `access`: composed per operation (rules the override omits survive).
11
+ * - `slug`: ignored (the `slugs` option owns slugs).
12
+ * - everything else (admin, labels, custom, …): shallow-merged.
13
+ */
14
+ export declare function applyCollectionOverride(collection: CollectionConfig, override?: CollectionOverride): CollectionConfig;
@@ -0,0 +1,47 @@
1
+ import { composeAccess } from './ownerAccess.js';
2
+ /**
3
+ * Apply a per-collection override to a generated collection, protecting the
4
+ * plugin's load-bearing behavior:
5
+ * - `fields`: a function receiving the plugin's default fields, returning the
6
+ * final list (append/reorder/replace — covers the issue #4 join-field case).
7
+ * - `hooks`: MERGED per event array (plugin hooks run first, then the user's) —
8
+ * an override can add hooks but never clobber conflict detection / status hooks.
9
+ * - `access`: composed per operation (rules the override omits survive).
10
+ * - `slug`: ignored (the `slugs` option owns slugs).
11
+ * - everything else (admin, labels, custom, …): shallow-merged.
12
+ */ export function applyCollectionOverride(collection, override) {
13
+ if (!override) {
14
+ return collection;
15
+ }
16
+ // Pull out the specially-handled keys; the rest shallow-merges. `slug` is
17
+ // omitted from CollectionOverride's type, but strip it defensively in case a
18
+ // caller cast around the type — the `slugs` option owns slugs.
19
+ const { access, fields, hooks, ...rest } = override;
20
+ delete rest.slug;
21
+ const mergedFields = fields ? fields({
22
+ defaultFields: collection.fields
23
+ }) : collection.fields;
24
+ // Merge hooks per event array: plugin's first, then the override's.
25
+ const mergedHooks = {
26
+ ...collection.hooks
27
+ };
28
+ if (hooks) {
29
+ for (const key of Object.keys(hooks)){
30
+ const pluginHooks = collection.hooks?.[key] ?? [];
31
+ const overrideHooks = hooks[key] ?? [];
32
+ mergedHooks[key] = [
33
+ ...pluginHooks,
34
+ ...overrideHooks
35
+ ];
36
+ }
37
+ }
38
+ return {
39
+ ...collection,
40
+ ...rest,
41
+ access: composeAccess(collection.access ?? {}, access),
42
+ fields: mergedFields,
43
+ hooks: mergedHooks
44
+ };
45
+ }
46
+
47
+ //# sourceMappingURL=collectionOverrides.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utilities/collectionOverrides.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload'\n\nimport type { CollectionOverride } from '../types.js'\n\nimport { composeAccess } from './ownerAccess.js'\n\n/**\n * Apply a per-collection override to a generated collection, protecting the\n * plugin's load-bearing behavior:\n * - `fields`: a function receiving the plugin's default fields, returning the\n * final list (append/reorder/replace — covers the issue #4 join-field case).\n * - `hooks`: MERGED per event array (plugin hooks run first, then the user's) —\n * an override can add hooks but never clobber conflict detection / status hooks.\n * - `access`: composed per operation (rules the override omits survive).\n * - `slug`: ignored (the `slugs` option owns slugs).\n * - everything else (admin, labels, custom, …): shallow-merged.\n */\nexport function applyCollectionOverride(\n collection: CollectionConfig,\n override?: CollectionOverride,\n): CollectionConfig {\n if (!override) {\n return collection\n }\n\n // Pull out the specially-handled keys; the rest shallow-merges. `slug` is\n // omitted from CollectionOverride's type, but strip it defensively in case a\n // caller cast around the type — the `slugs` option owns slugs.\n const { access, fields, hooks, ...rest } = override\n delete (rest as { slug?: unknown }).slug\n\n const mergedFields = fields ? fields({ defaultFields: collection.fields }) : collection.fields\n\n // Merge hooks per event array: plugin's first, then the override's.\n const mergedHooks: CollectionConfig['hooks'] = { ...collection.hooks }\n if (hooks) {\n for (const key of Object.keys(hooks) as Array<keyof NonNullable<CollectionConfig['hooks']>>) {\n const pluginHooks = (collection.hooks?.[key] ?? []) as unknown[]\n const overrideHooks = (hooks[key] ?? []) as unknown[]\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ;(mergedHooks as any)[key] = [...pluginHooks, ...overrideHooks]\n }\n }\n\n return {\n ...collection,\n ...rest,\n access: composeAccess(collection.access ?? {}, access),\n fields: mergedFields,\n hooks: mergedHooks,\n }\n}\n"],"names":["composeAccess","applyCollectionOverride","collection","override","access","fields","hooks","rest","slug","mergedFields","defaultFields","mergedHooks","key","Object","keys","pluginHooks","overrideHooks"],"mappings":"AAIA,SAASA,aAAa,QAAQ,mBAAkB;AAEhD;;;;;;;;;;CAUC,GACD,OAAO,SAASC,wBACdC,UAA4B,EAC5BC,QAA6B;IAE7B,IAAI,CAACA,UAAU;QACb,OAAOD;IACT;IAEA,0EAA0E;IAC1E,6EAA6E;IAC7E,+DAA+D;IAC/D,MAAM,EAAEE,MAAM,EAAEC,MAAM,EAAEC,KAAK,EAAE,GAAGC,MAAM,GAAGJ;IAC3C,OAAO,AAACI,KAA4BC,IAAI;IAExC,MAAMC,eAAeJ,SAASA,OAAO;QAAEK,eAAeR,WAAWG,MAAM;IAAC,KAAKH,WAAWG,MAAM;IAE9F,oEAAoE;IACpE,MAAMM,cAAyC;QAAE,GAAGT,WAAWI,KAAK;IAAC;IACrE,IAAIA,OAAO;QACT,KAAK,MAAMM,OAAOC,OAAOC,IAAI,CAACR,OAA+D;YAC3F,MAAMS,cAAeb,WAAWI,KAAK,EAAE,CAACM,IAAI,IAAI,EAAE;YAClD,MAAMI,gBAAiBV,KAAK,CAACM,IAAI,IAAI,EAAE;YAErCD,WAAmB,CAACC,IAAI,GAAG;mBAAIG;mBAAgBC;aAAc;QACjE;IACF;IAEA,OAAO;QACL,GAAGd,UAAU;QACb,GAAGK,IAAI;QACPH,QAAQJ,cAAcE,WAAWE,MAAM,IAAI,CAAC,GAAGA;QAC/CC,QAAQI;QACRH,OAAOK;IACT;AACF"}
@@ -1,6 +1,12 @@
1
1
  import type { CollectionConfig } from 'payload';
2
2
  import type { ResolvedResourceOwnerModeConfig } from '../types.js';
3
3
  type CollectionAccess = NonNullable<CollectionConfig['access']>;
4
+ /**
5
+ * Overlay app-provided access overrides onto a base (owner-mode) access object,
6
+ * per operation. Specifying only `read` keeps the base's create/update/delete
7
+ * rules intact, instead of replacing them wholesale (review C9).
8
+ */
9
+ export declare function composeAccess(base: CollectionAccess, override: CollectionConfig['access']): CollectionAccess;
4
10
  /**
5
11
  * Access factories for Resources collection.
6
12
  * Owners may read/update/delete their own resources; anyone authenticated may create.
@@ -1,13 +1,26 @@
1
+ /**
2
+ * Overlay app-provided access overrides onto a base (owner-mode) access object,
3
+ * per operation. Specifying only `read` keeps the base's create/update/delete
4
+ * rules intact, instead of replacing them wholesale (review C9).
5
+ */ export function composeAccess(base, override) {
6
+ return {
7
+ ...base,
8
+ ...override ?? {}
9
+ };
10
+ }
1
11
  /**
2
12
  * Returns true if the requesting user is considered an "admin" for resource-owner mode:
3
13
  * - No user → deny
4
- * - adminRoles provided → user.role must be in that list
14
+ * - adminRoles provided → the user's `roleField` value must be in that list
5
15
  * - adminRoles empty → no bypass role; all authenticated users are treated as owners
6
- */ function isAdmin(user, adminRoles) {
16
+ *
17
+ * Reads the configured `roleField` (not a hardcoded `user.role`) so apps using a
18
+ * `roles: string[]` field — or any custom role field — aren't silently demoted.
19
+ */ function isAdmin(user, adminRoles, roleField) {
7
20
  if (!adminRoles.length) {
8
21
  return false;
9
22
  }
10
- const role = user.role;
23
+ const role = user[roleField];
11
24
  if (!role) {
12
25
  return false;
13
26
  }
@@ -17,13 +30,13 @@
17
30
  * Access factories for Resources collection.
18
31
  * Owners may read/update/delete their own resources; anyone authenticated may create.
19
32
  */ export function makeResourceOwnerAccess(rom) {
20
- const { adminRoles, ownerField } = rom;
33
+ const { adminRoles, ownerField, roleField } = rom;
21
34
  const ownerOrAdmin = ({ req })=>{
22
35
  if (!req.user) {
23
36
  return false;
24
37
  }
25
38
  const user = req.user;
26
- if (isAdmin(user, adminRoles)) {
39
+ if (isAdmin(user, adminRoles, roleField)) {
27
40
  return true;
28
41
  }
29
42
  return {
@@ -43,13 +56,13 @@
43
56
  * Access factories for Schedules collection.
44
57
  * A schedule's ownership is determined through its `resource.owner` relationship.
45
58
  */ export function makeScheduleOwnerAccess(rom) {
46
- const { adminRoles, ownerField } = rom;
59
+ const { adminRoles, ownerField, roleField } = rom;
47
60
  const ownerOrAdmin = ({ req })=>{
48
61
  if (!req.user) {
49
62
  return false;
50
63
  }
51
64
  const user = req.user;
52
- if (isAdmin(user, adminRoles)) {
65
+ if (isAdmin(user, adminRoles, roleField)) {
53
66
  return true;
54
67
  }
55
68
  return {
@@ -70,13 +83,13 @@
70
83
  * Resource owners can see reservations for their resources (read-only);
71
84
  * mutations are admin-only to prevent owners from unilaterally cancelling guest bookings.
72
85
  */ export function makeReservationOwnerAccess(rom) {
73
- const { adminRoles, ownerField } = rom;
86
+ const { adminRoles, ownerField, roleField } = rom;
74
87
  const readAccess = ({ req })=>{
75
88
  if (!req.user) {
76
89
  return false;
77
90
  }
78
91
  const user = req.user;
79
- if (isAdmin(user, adminRoles)) {
92
+ if (isAdmin(user, adminRoles, roleField)) {
80
93
  return true;
81
94
  }
82
95
  return {
@@ -90,7 +103,7 @@
90
103
  return false;
91
104
  }
92
105
  const user = req.user;
93
- return isAdmin(user, adminRoles);
106
+ return isAdmin(user, adminRoles, roleField);
94
107
  };
95
108
  return {
96
109
  create: adminOnly,
@@ -102,13 +115,13 @@
102
115
  /**
103
116
  * Access factories for Services collection when `ownedServices: true`.
104
117
  */ export function makeServiceOwnerAccess(rom, ownerField) {
105
- const { adminRoles } = rom;
118
+ const { adminRoles, roleField } = rom;
106
119
  const ownerOrAdmin = ({ req })=>{
107
120
  if (!req.user) {
108
121
  return false;
109
122
  }
110
123
  const user = req.user;
111
- if (isAdmin(user, adminRoles)) {
124
+ if (isAdmin(user, adminRoles, roleField)) {
112
125
  return true;
113
126
  }
114
127
  return {