payload-reserve 1.6.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -3
- package/dist/collections/Reservations.js +19 -7
- package/dist/collections/Reservations.js.map +1 -1
- package/dist/collections/Resources.js +11 -8
- package/dist/collections/Resources.js.map +1 -1
- package/dist/collections/Schedules.js +12 -6
- package/dist/collections/Schedules.js.map +1 -1
- package/dist/collections/Services.js +19 -10
- package/dist/collections/Services.js.map +1 -1
- package/dist/components/AvailabilityOverview/index.js +76 -18
- package/dist/components/AvailabilityOverview/index.js.map +1 -1
- package/dist/components/CalendarView/CalendarView.module.css +9 -0
- package/dist/components/CalendarView/LaneTimelineView.d.ts +4 -1
- package/dist/components/CalendarView/LaneTimelineView.js +17 -12
- package/dist/components/CalendarView/LaneTimelineView.js.map +1 -1
- package/dist/components/CalendarView/index.js +166 -44
- package/dist/components/CalendarView/index.js.map +1 -1
- package/dist/components/CustomerField/index.js +8 -3
- package/dist/components/CustomerField/index.js.map +1 -1
- package/dist/components/DashboardWidget/DashboardWidgetServer.js +91 -18
- package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
- package/dist/defaults.js +44 -9
- package/dist/defaults.js.map +1 -1
- package/dist/endpoints/cancelBooking.js +1 -1
- package/dist/endpoints/cancelBooking.js.map +1 -1
- package/dist/endpoints/checkAvailability.js +56 -7
- package/dist/endpoints/checkAvailability.js.map +1 -1
- package/dist/endpoints/createBooking.js +19 -10
- package/dist/endpoints/createBooking.js.map +1 -1
- package/dist/endpoints/customerSearch.js +5 -2
- package/dist/endpoints/customerSearch.js.map +1 -1
- package/dist/endpoints/effectiveTimezone.d.ts +13 -0
- package/dist/endpoints/effectiveTimezone.js +41 -0
- package/dist/endpoints/effectiveTimezone.js.map +1 -0
- package/dist/endpoints/getSlots.js +56 -7
- package/dist/endpoints/getSlots.js.map +1 -1
- package/dist/endpoints/resourceAvailability.d.ts +4 -1
- package/dist/endpoints/resourceAvailability.js +102 -26
- package/dist/endpoints/resourceAvailability.js.map +1 -1
- package/dist/hooks/reservations/calculateEndTime.js +48 -20
- package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
- package/dist/hooks/reservations/enforceCustomerOwnership.d.ts +11 -0
- package/dist/hooks/reservations/enforceCustomerOwnership.js +30 -0
- package/dist/hooks/reservations/enforceCustomerOwnership.js.map +1 -0
- package/dist/hooks/reservations/onStatusChange.js +10 -4
- package/dist/hooks/reservations/onStatusChange.js.map +1 -1
- package/dist/hooks/reservations/validateCancellation.js +3 -2
- package/dist/hooks/reservations/validateCancellation.js.map +1 -1
- package/dist/hooks/reservations/validateConflicts.js +23 -4
- package/dist/hooks/reservations/validateConflicts.js.map +1 -1
- package/dist/hooks/reservations/validateGuestBooking.js +3 -4
- package/dist/hooks/reservations/validateGuestBooking.js.map +1 -1
- package/dist/hooks/reservations/validateStatusTransition.js +2 -2
- package/dist/hooks/reservations/validateStatusTransition.js.map +1 -1
- package/dist/hooks/users/provisionStaffResource.js +5 -8
- package/dist/hooks/users/provisionStaffResource.js.map +1 -1
- package/dist/plugin.js +83 -14
- package/dist/plugin.js.map +1 -1
- package/dist/services/AvailabilityService.d.ts +54 -2
- package/dist/services/AvailabilityService.js +180 -46
- package/dist/services/AvailabilityService.js.map +1 -1
- package/dist/translations/ar.json +1 -0
- package/dist/translations/de.json +1 -0
- package/dist/translations/en.json +1 -0
- package/dist/translations/es.json +1 -0
- package/dist/translations/fa.json +1 -0
- package/dist/translations/fr.json +1 -0
- package/dist/translations/hi.json +1 -0
- package/dist/translations/id.json +1 -0
- package/dist/translations/pl.json +1 -0
- package/dist/translations/ru.json +1 -0
- package/dist/translations/tr.json +1 -0
- package/dist/translations/zh.json +1 -0
- package/dist/types.d.ts +46 -1
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/dist/utilities/collectionOverrides.d.ts +14 -0
- package/dist/utilities/collectionOverrides.js +47 -0
- package/dist/utilities/collectionOverrides.js.map +1 -0
- package/dist/utilities/ownerAccess.d.ts +6 -0
- package/dist/utilities/ownerAccess.js +25 -12
- package/dist/utilities/ownerAccess.js.map +1 -1
- package/dist/utilities/reservationChanges.d.ts +17 -0
- package/dist/utilities/reservationChanges.js +88 -0
- package/dist/utilities/reservationChanges.js.map +1 -0
- package/dist/utilities/scheduleUtils.d.ts +14 -8
- package/dist/utilities/scheduleUtils.js +26 -19
- package/dist/utilities/scheduleUtils.js.map +1 -1
- package/dist/utilities/tenantTimezone.d.ts +41 -0
- package/dist/utilities/tenantTimezone.js +77 -0
- package/dist/utilities/tenantTimezone.js.map +1 -0
- package/dist/utilities/timezoneUtils.d.ts +44 -0
- package/dist/utilities/timezoneUtils.js +146 -0
- package/dist/utilities/timezoneUtils.js.map +1 -0
- package/package.json +1 -1
|
@@ -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
|
|
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
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
|
|
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
|
-
|
|
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
|
|
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 {
|