payload-reserve 1.0.2 → 1.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 +61 -1141
- package/dist/collections/Customers.d.ts +3 -0
- package/dist/collections/Customers.js +58 -0
- package/dist/collections/Customers.js.map +1 -0
- package/dist/collections/Reservations.js +129 -35
- package/dist/collections/Reservations.js.map +1 -1
- package/dist/collections/Resources.js +70 -2
- package/dist/collections/Resources.js.map +1 -1
- package/dist/collections/Schedules.js +4 -1
- package/dist/collections/Schedules.js.map +1 -1
- package/dist/collections/Services.js +59 -2
- package/dist/collections/Services.js.map +1 -1
- package/dist/components/AvailabilityOverview/AvailabilityOverview.module.css +31 -0
- package/dist/components/AvailabilityOverview/index.js +59 -8
- package/dist/components/AvailabilityOverview/index.js.map +1 -1
- package/dist/components/CalendarView/CalendarView.module.css +171 -0
- package/dist/components/CalendarView/index.js +547 -38
- package/dist/components/CalendarView/index.js.map +1 -1
- package/dist/components/CustomerField/index.js +24 -10
- package/dist/components/CustomerField/index.js.map +1 -1
- package/dist/components/DashboardWidget/DashboardWidgetServer.js +21 -11
- package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
- package/dist/defaults.d.ts +1 -2
- package/dist/defaults.js +22 -4
- package/dist/defaults.js.map +1 -1
- package/dist/endpoints/cancelBooking.d.ts +3 -0
- package/dist/endpoints/cancelBooking.js +37 -0
- package/dist/endpoints/cancelBooking.js.map +1 -0
- package/dist/endpoints/checkAvailability.d.ts +3 -0
- package/dist/endpoints/checkAvailability.js +37 -0
- package/dist/endpoints/checkAvailability.js.map +1 -0
- package/dist/endpoints/createBooking.d.ts +3 -0
- package/dist/endpoints/createBooking.js +32 -0
- package/dist/endpoints/createBooking.js.map +1 -0
- package/dist/endpoints/customerSearch.js +76 -58
- package/dist/endpoints/customerSearch.js.map +1 -1
- package/dist/endpoints/getSlots.d.ts +3 -0
- package/dist/endpoints/getSlots.js +51 -0
- package/dist/endpoints/getSlots.js.map +1 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/reservations/calculateEndTime.js +75 -9
- package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
- package/dist/hooks/reservations/checkIdempotency.d.ts +3 -0
- package/dist/hooks/reservations/checkIdempotency.js +32 -0
- package/dist/hooks/reservations/checkIdempotency.js.map +1 -0
- package/dist/hooks/reservations/onStatusChange.d.ts +3 -0
- package/dist/hooks/reservations/onStatusChange.js +41 -0
- package/dist/hooks/reservations/onStatusChange.js.map +1 -0
- package/dist/hooks/reservations/validateCancellation.js +1 -1
- package/dist/hooks/reservations/validateCancellation.js.map +1 -1
- package/dist/hooks/reservations/validateConflicts.js +34 -56
- package/dist/hooks/reservations/validateConflicts.js.map +1 -1
- package/dist/hooks/reservations/validateStatusTransition.d.ts +2 -1
- package/dist/hooks/reservations/validateStatusTransition.js +29 -6
- package/dist/hooks/reservations/validateStatusTransition.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin.js +49 -54
- package/dist/plugin.js.map +1 -1
- package/dist/services/AvailabilityService.d.ts +57 -0
- package/dist/services/AvailabilityService.js +232 -0
- package/dist/services/AvailabilityService.js.map +1 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +3 -0
- package/dist/services/index.js.map +1 -0
- package/dist/translations/en.json +33 -1
- package/dist/translations/index.d.ts +5 -0
- package/dist/translations/index.js.map +1 -1
- package/dist/types.d.ts +84 -6
- package/dist/types.js +29 -9
- package/dist/types.js.map +1 -1
- package/dist/utilities/ownerAccess.d.ts +24 -0
- package/dist/utilities/ownerAccess.js +128 -0
- package/dist/utilities/ownerAccess.js.map +1 -0
- package/dist/utilities/resolveReservationItems.d.ts +18 -0
- package/dist/utilities/resolveReservationItems.js +45 -0
- package/dist/utilities/resolveReservationItems.js.map +1 -0
- package/package.json +12 -9
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Payload, PayloadRequest, Where } from 'payload';
|
|
2
|
+
import type { DurationType, StatusMachineConfig } from '../types.js';
|
|
3
|
+
export declare function computeEndTime(params: {
|
|
4
|
+
durationType: DurationType;
|
|
5
|
+
endTime?: Date;
|
|
6
|
+
serviceDuration: number;
|
|
7
|
+
startTime: Date;
|
|
8
|
+
}): {
|
|
9
|
+
durationMinutes: number;
|
|
10
|
+
endTime: Date;
|
|
11
|
+
};
|
|
12
|
+
export declare function buildOverlapQuery(params: {
|
|
13
|
+
blockingStatuses: string[];
|
|
14
|
+
effectiveEnd: Date;
|
|
15
|
+
effectiveStart: Date;
|
|
16
|
+
excludeReservationId?: string;
|
|
17
|
+
resourceId: string;
|
|
18
|
+
}): Where;
|
|
19
|
+
export declare function isBlockingStatus(status: string, statusMachine: StatusMachineConfig): boolean;
|
|
20
|
+
export declare function validateTransition(fromStatus: string, toStatus: string, statusMachine: StatusMachineConfig): {
|
|
21
|
+
reason?: string;
|
|
22
|
+
valid: boolean;
|
|
23
|
+
};
|
|
24
|
+
export declare function checkAvailability(params: {
|
|
25
|
+
blockingStatuses: string[];
|
|
26
|
+
bufferAfter: number;
|
|
27
|
+
bufferBefore: number;
|
|
28
|
+
endTime: Date;
|
|
29
|
+
excludeReservationId?: string;
|
|
30
|
+
guestCount: number;
|
|
31
|
+
payload: Payload;
|
|
32
|
+
req: PayloadRequest;
|
|
33
|
+
reservationSlug: string;
|
|
34
|
+
resourceId: string;
|
|
35
|
+
resourceSlug: string;
|
|
36
|
+
startTime: Date;
|
|
37
|
+
}): Promise<{
|
|
38
|
+
available: boolean;
|
|
39
|
+
currentCount: number;
|
|
40
|
+
reason?: string;
|
|
41
|
+
totalCapacity: number;
|
|
42
|
+
}>;
|
|
43
|
+
export declare function getAvailableSlots(params: {
|
|
44
|
+
blockingStatuses: string[];
|
|
45
|
+
date: Date;
|
|
46
|
+
payload: Payload;
|
|
47
|
+
req: PayloadRequest;
|
|
48
|
+
reservationSlug: string;
|
|
49
|
+
resourceId: string;
|
|
50
|
+
resourceSlug: string;
|
|
51
|
+
scheduleSlug: string;
|
|
52
|
+
serviceId: string;
|
|
53
|
+
serviceSlug: string;
|
|
54
|
+
}): Promise<Array<{
|
|
55
|
+
end: Date;
|
|
56
|
+
start: Date;
|
|
57
|
+
}>>;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { resolveScheduleForDate } from '../utilities/scheduleUtils.js';
|
|
2
|
+
import { addMinutes, computeBlockedWindow } from '../utilities/slotUtils.js';
|
|
3
|
+
// --- Pure functions (no DB) ---
|
|
4
|
+
export function computeEndTime(params) {
|
|
5
|
+
const { durationType, serviceDuration, startTime } = params;
|
|
6
|
+
if (durationType === 'full-day') {
|
|
7
|
+
const end = new Date(startTime);
|
|
8
|
+
end.setHours(23, 59, 59, 999);
|
|
9
|
+
const durationMinutes = Math.round((end.getTime() - startTime.getTime()) / 60_000);
|
|
10
|
+
return {
|
|
11
|
+
durationMinutes,
|
|
12
|
+
endTime: end
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
if (durationType === 'flexible' && params.endTime) {
|
|
16
|
+
const durationMinutes = Math.round((params.endTime.getTime() - startTime.getTime()) / 60_000);
|
|
17
|
+
return {
|
|
18
|
+
durationMinutes,
|
|
19
|
+
endTime: params.endTime
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
// fixed duration (default)
|
|
23
|
+
const endTime = addMinutes(startTime, serviceDuration);
|
|
24
|
+
return {
|
|
25
|
+
durationMinutes: serviceDuration,
|
|
26
|
+
endTime
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function buildOverlapQuery(params) {
|
|
30
|
+
const { blockingStatuses, effectiveEnd, effectiveStart, excludeReservationId, resourceId } = params;
|
|
31
|
+
const conditions = [
|
|
32
|
+
{
|
|
33
|
+
resource: {
|
|
34
|
+
equals: resourceId
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
status: {
|
|
39
|
+
in: blockingStatuses
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
startTime: {
|
|
44
|
+
less_than: effectiveEnd.toISOString()
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
endTime: {
|
|
49
|
+
greater_than: effectiveStart.toISOString()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
];
|
|
53
|
+
if (excludeReservationId) {
|
|
54
|
+
conditions.push({
|
|
55
|
+
id: {
|
|
56
|
+
not_equals: excludeReservationId
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
and: conditions
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function isBlockingStatus(status, statusMachine) {
|
|
65
|
+
return statusMachine.blockingStatuses.includes(status);
|
|
66
|
+
}
|
|
67
|
+
export function validateTransition(fromStatus, toStatus, statusMachine) {
|
|
68
|
+
const allowed = statusMachine.transitions[fromStatus];
|
|
69
|
+
if (!allowed) {
|
|
70
|
+
return {
|
|
71
|
+
reason: `Unknown status: ${fromStatus}`,
|
|
72
|
+
valid: false
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (!allowed.includes(toStatus)) {
|
|
76
|
+
return {
|
|
77
|
+
reason: `Cannot transition from "${fromStatus}" to "${toStatus}"`,
|
|
78
|
+
valid: false
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
valid: true
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// --- DB functions (use Payload Local API only) ---
|
|
86
|
+
export async function checkAvailability(params) {
|
|
87
|
+
const { blockingStatuses, bufferAfter, bufferBefore, endTime, excludeReservationId, guestCount, payload, req, reservationSlug, resourceId, resourceSlug, startTime } = params;
|
|
88
|
+
// Fetch resource for quantity and capacity mode
|
|
89
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
90
|
+
const resource = await payload.findByID({
|
|
91
|
+
id: resourceId,
|
|
92
|
+
collection: resourceSlug,
|
|
93
|
+
depth: 0,
|
|
94
|
+
req
|
|
95
|
+
});
|
|
96
|
+
const quantity = resource.quantity ?? 1;
|
|
97
|
+
const capacityMode = resource.capacityMode ?? 'per-reservation';
|
|
98
|
+
// Compute effective window with buffers
|
|
99
|
+
const { effectiveEnd, effectiveStart } = computeBlockedWindow(startTime, endTime, bufferBefore, bufferAfter);
|
|
100
|
+
// Build overlap query
|
|
101
|
+
const where = buildOverlapQuery({
|
|
102
|
+
blockingStatuses,
|
|
103
|
+
effectiveEnd,
|
|
104
|
+
effectiveStart,
|
|
105
|
+
excludeReservationId,
|
|
106
|
+
resourceId
|
|
107
|
+
});
|
|
108
|
+
if (capacityMode === 'per-guest') {
|
|
109
|
+
// Must fetch docs to sum guestCount
|
|
110
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
111
|
+
const { docs } = await payload.find({
|
|
112
|
+
collection: reservationSlug,
|
|
113
|
+
depth: 0,
|
|
114
|
+
limit: 0,
|
|
115
|
+
req,
|
|
116
|
+
select: {
|
|
117
|
+
guestCount: true
|
|
118
|
+
},
|
|
119
|
+
where
|
|
120
|
+
});
|
|
121
|
+
const currentGuests = docs.reduce((sum, doc)=>sum + (doc.guestCount ?? 1), 0);
|
|
122
|
+
return {
|
|
123
|
+
available: currentGuests + guestCount <= quantity,
|
|
124
|
+
currentCount: currentGuests,
|
|
125
|
+
reason: currentGuests + guestCount > quantity ? 'Guest capacity exceeded' : undefined,
|
|
126
|
+
totalCapacity: quantity
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// per-reservation mode: count is sufficient
|
|
130
|
+
// TODO: batch queries — linear per-item cost acceptable for 2-5 items
|
|
131
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
132
|
+
const { totalDocs } = await payload.count({
|
|
133
|
+
collection: reservationSlug,
|
|
134
|
+
req,
|
|
135
|
+
where
|
|
136
|
+
});
|
|
137
|
+
return {
|
|
138
|
+
available: totalDocs + 1 <= quantity,
|
|
139
|
+
currentCount: totalDocs,
|
|
140
|
+
reason: totalDocs + 1 > quantity ? 'All units are booked for this time' : undefined,
|
|
141
|
+
totalCapacity: quantity
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
export async function getAvailableSlots(params) {
|
|
145
|
+
const { blockingStatuses, date, payload, req, reservationSlug, resourceId, resourceSlug, scheduleSlug, serviceId, serviceSlug } = params;
|
|
146
|
+
// 1. Fetch service for duration + buffer times
|
|
147
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
148
|
+
const service = await payload.findByID({
|
|
149
|
+
id: serviceId,
|
|
150
|
+
collection: serviceSlug,
|
|
151
|
+
depth: 0,
|
|
152
|
+
req
|
|
153
|
+
});
|
|
154
|
+
const duration = service.duration ?? 60;
|
|
155
|
+
const bufferBefore = service.bufferTimeBefore ?? 0;
|
|
156
|
+
const bufferAfter = service.bufferTimeAfter ?? 0;
|
|
157
|
+
const durationType = service.durationType ?? 'fixed';
|
|
158
|
+
// 2. Fetch resource's schedules for the date
|
|
159
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
160
|
+
const { docs: schedules } = await payload.find({
|
|
161
|
+
collection: scheduleSlug,
|
|
162
|
+
depth: 0,
|
|
163
|
+
limit: 100,
|
|
164
|
+
req,
|
|
165
|
+
where: {
|
|
166
|
+
and: [
|
|
167
|
+
{
|
|
168
|
+
resource: {
|
|
169
|
+
equals: resourceId
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
active: {
|
|
174
|
+
equals: true
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
]
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
// 3. Resolve schedules to time ranges for the date
|
|
181
|
+
const timeRanges = [];
|
|
182
|
+
for (const schedule of schedules){
|
|
183
|
+
const ranges = resolveScheduleForDate(schedule, date);
|
|
184
|
+
timeRanges.push(...ranges);
|
|
185
|
+
}
|
|
186
|
+
if (timeRanges.length === 0) {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
// 4. Generate candidate slots from schedule ranges
|
|
190
|
+
const { endTime: slotEndOffset } = computeEndTime({
|
|
191
|
+
durationType,
|
|
192
|
+
serviceDuration: duration,
|
|
193
|
+
startTime: new Date(0)
|
|
194
|
+
});
|
|
195
|
+
const slotDuration = Math.round(slotEndOffset.getTime() / 60_000);
|
|
196
|
+
const effectiveDuration = durationType === 'fixed' ? duration : slotDuration;
|
|
197
|
+
const availableSlots = [];
|
|
198
|
+
for (const range of timeRanges){
|
|
199
|
+
let candidateStart = new Date(range.start);
|
|
200
|
+
while(true){
|
|
201
|
+
const candidateEnd = addMinutes(candidateStart, effectiveDuration);
|
|
202
|
+
if (candidateEnd > range.end) {
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
// 5. Check availability for each candidate slot
|
|
206
|
+
const result = await checkAvailability({
|
|
207
|
+
blockingStatuses,
|
|
208
|
+
bufferAfter,
|
|
209
|
+
bufferBefore,
|
|
210
|
+
endTime: candidateEnd,
|
|
211
|
+
guestCount: 1,
|
|
212
|
+
payload,
|
|
213
|
+
req,
|
|
214
|
+
reservationSlug,
|
|
215
|
+
resourceId,
|
|
216
|
+
resourceSlug,
|
|
217
|
+
startTime: candidateStart
|
|
218
|
+
});
|
|
219
|
+
if (result.available) {
|
|
220
|
+
availableSlots.push({
|
|
221
|
+
end: candidateEnd,
|
|
222
|
+
start: new Date(candidateStart)
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
// Move to next slot (service duration as step)
|
|
226
|
+
candidateStart = addMinutes(candidateStart, effectiveDuration);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return availableSlots;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
//# sourceMappingURL=AvailabilityService.js.map
|
|
@@ -0,0 +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 } 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?: string\n resourceId: string\n}): Where {\n const { blockingStatuses, effectiveEnd, effectiveStart, excludeReservationId, resourceId } =\n params\n\n const conditions: Where[] = [\n { resource: { equals: resourceId } },\n { status: { in: blockingStatuses } },\n { startTime: { less_than: effectiveEnd.toISOString() } },\n { endTime: { greater_than: effectiveStart.toISOString() } },\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?: string\n guestCount: number\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId: 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 payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId: string\n resourceSlug: string\n scheduleSlug: string\n serviceId: string\n serviceSlug: string\n}): Promise<Array<{ end: Date; start: Date }>> {\n const {\n blockingStatuses,\n date,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n scheduleSlug,\n serviceId,\n serviceSlug,\n } = params\n\n // 1. Fetch service for duration + buffer times\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. Fetch resource's schedules for the date\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: resourceId } }, { active: { equals: true } }],\n },\n })\n\n // 3. Resolve schedules to time ranges for the date\n const timeRanges: Array<{ end: Date; start: Date }> = []\n for (const schedule of schedules) {\n const ranges = resolveScheduleForDate(\n schedule as unknown as Parameters<typeof resolveScheduleForDate>[0],\n date,\n )\n timeRanges.push(...ranges)\n }\n\n if (timeRanges.length === 0) {\n return []\n }\n\n // 4. Generate candidate slots from schedule ranges\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 const availableSlots: Array<{ end: Date; start: Date }> = []\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) {break}\n\n // 5. Check availability for each candidate slot\n const result = await checkAvailability({\n blockingStatuses,\n bufferAfter,\n bufferBefore,\n endTime: candidateEnd,\n guestCount: 1,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n startTime: candidateStart,\n })\n\n if (result.available) {\n availableSlots.push({ end: candidateEnd, start: new Date(candidateStart) })\n }\n\n // Move to next slot (service duration as step)\n candidateStart = addMinutes(candidateStart, effectiveDuration)\n }\n }\n\n return availableSlots\n}\n"],"names":["resolveScheduleForDate","addMinutes","computeBlockedWindow","computeEndTime","params","durationType","serviceDuration","startTime","end","Date","setHours","durationMinutes","Math","round","getTime","endTime","buildOverlapQuery","blockingStatuses","effectiveEnd","effectiveStart","excludeReservationId","resourceId","conditions","resource","equals","status","in","less_than","toISOString","greater_than","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","scheduleSlug","serviceId","serviceSlug","service","duration","bufferTimeBefore","bufferTimeAfter","schedules","active","timeRanges","schedule","ranges","length","slotEndOffset","slotDuration","effectiveDuration","availableSlots","range","candidateStart","start","candidateEnd","result"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { buildOverlapQuery, checkAvailability, computeEndTime, getAvailableSlots, isBlockingStatus, validateTransition, } from './AvailabilityService.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/services/index.ts"],"sourcesContent":["export {\n buildOverlapQuery,\n checkAvailability,\n computeEndTime,\n getAvailableSlots,\n isBlockingStatus,\n validateTransition,\n} from './AvailabilityService.js'\n"],"names":["buildOverlapQuery","checkAvailability","computeEndTime","getAvailableSlots","isBlockingStatus","validateTransition"],"mappings":"AAAA,SACEA,iBAAiB,EACjBC,iBAAiB,EACjBC,cAAc,EACdC,iBAAiB,EACjBC,gBAAgB,EAChBC,kBAAkB,QACb,2BAA0B"}
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
"collectionResources": "Resources",
|
|
4
4
|
"collectionSchedules": "Schedules",
|
|
5
5
|
"collectionReservations": "Reservations",
|
|
6
|
+
"collectionCustomers": "Customers",
|
|
7
|
+
"collectionCustomer": "Customer",
|
|
8
|
+
"fieldFirstName": "First Name",
|
|
9
|
+
"fieldLastName": "Last Name",
|
|
6
10
|
"fieldName": "Name",
|
|
7
11
|
"fieldDescription": "Description",
|
|
8
12
|
"fieldImage": "Image",
|
|
@@ -73,11 +77,14 @@
|
|
|
73
77
|
"availabilityNoResources": "No active resources found.",
|
|
74
78
|
"availabilityNotConfigured": "Reservation plugin not configured.",
|
|
75
79
|
"availabilityBooked": "Booked",
|
|
80
|
+
"availabilityXofYBooked": "{{booked}}/{{total}} booked",
|
|
76
81
|
"availabilityUnavailable": "Unavailable",
|
|
77
82
|
"availabilityResource": "Resource",
|
|
78
83
|
"dashboardTitle": "Today's Reservations",
|
|
79
84
|
"dashboardTotal": "Total",
|
|
85
|
+
"dashboardActive": "Active",
|
|
80
86
|
"dashboardUpcoming": "Upcoming",
|
|
87
|
+
"dashboardTerminal": "Closed",
|
|
81
88
|
"dashboardCompleted": "Completed",
|
|
82
89
|
"dashboardCancelled": "Cancelled",
|
|
83
90
|
"dashboardNextAppointment": "Next Appointment",
|
|
@@ -87,5 +94,30 @@
|
|
|
87
94
|
"fieldCustomerSearch": "Search customers...",
|
|
88
95
|
"fieldCustomerNoResults": "No customers found",
|
|
89
96
|
"fieldCustomerCreateNew": "Create new customer",
|
|
90
|
-
"fieldCustomerClear": "Clear selection"
|
|
97
|
+
"fieldCustomerClear": "Clear selection",
|
|
98
|
+
"calendarPending": "Pending",
|
|
99
|
+
"pendingDateTime": "Date / Time",
|
|
100
|
+
"pendingActions": "Actions",
|
|
101
|
+
"pendingSelectAll": "Select all",
|
|
102
|
+
"pendingConfirmSelected": "Confirm Selected ({{count}})",
|
|
103
|
+
"pendingConfirming": "Confirming...",
|
|
104
|
+
"pendingConfirm": "Confirm",
|
|
105
|
+
"pendingCancel": "Cancel",
|
|
106
|
+
"pendingConfirmSuccess": "Reservation confirmed",
|
|
107
|
+
"pendingConfirmError": "Failed to confirm reservation",
|
|
108
|
+
"pendingCancelSuccess": "Reservation cancelled",
|
|
109
|
+
"pendingCancelError": "Failed to cancel reservation",
|
|
110
|
+
"pendingBulkConfirmSuccess": "{{succeeded}} confirmed, {{failed}} failed",
|
|
111
|
+
"pendingEmpty": "No pending reservations",
|
|
112
|
+
"fieldQuantity": "Quantity",
|
|
113
|
+
"fieldCapacityMode": "Capacity Mode",
|
|
114
|
+
"fieldTimezone": "Timezone",
|
|
115
|
+
"fieldDurationType": "Duration Type",
|
|
116
|
+
"capacityPerReservation": "Per Reservation",
|
|
117
|
+
"capacityPerGuest": "Per Guest",
|
|
118
|
+
"durationFixed": "Fixed",
|
|
119
|
+
"durationFlexible": "Flexible",
|
|
120
|
+
"durationFullDay": "Full Day",
|
|
121
|
+
"fieldItems": "Items",
|
|
122
|
+
"fieldGuestCount": "Guest Count"
|
|
91
123
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import en from './en.json';
|
|
1
2
|
export declare const translations: Record<string, Record<string, unknown>>;
|
|
3
|
+
type EnKeys = keyof typeof en;
|
|
4
|
+
/** Union of all valid plugin translation keys, e.g. `"reservation:fieldName"` */
|
|
5
|
+
export type PluginTranslationKeys = `reservation:${EnKeys}`;
|
|
2
6
|
/** Helper type for plugin translation calls (bypasses DefaultTranslationKeys constraint) */
|
|
3
7
|
export type PluginT = (key: string, vars?: Record<string, unknown>) => string;
|
|
8
|
+
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/translations/index.ts"],"sourcesContent":["import en from './en.json' with { type: 'json' }\n\nexport const translations: Record<string, Record<string, unknown>> = {\n en: { reservation: en },\n}\n\n/** Helper type for plugin translation calls (bypasses DefaultTranslationKeys constraint) */\nexport type PluginT = (key: string, vars?: Record<string, unknown>) => string\n"],"names":["en","translations","reservation"],"mappings":"AAAA,OAAOA,QAAQ,YAAiC;AAEhD,OAAO,MAAMC,eAAwD;IACnED,IAAI;QAAEE,aAAaF;IAAG;AACxB,EAAC"}
|
|
1
|
+
{"version":3,"sources":["../../src/translations/index.ts"],"sourcesContent":["import en from './en.json' with { type: 'json' }\n\nexport const translations: Record<string, Record<string, unknown>> = {\n en: { reservation: en },\n}\n\ntype EnKeys = keyof typeof en\n/** Union of all valid plugin translation keys, e.g. `\"reservation:fieldName\"` */\nexport type PluginTranslationKeys = `reservation:${EnKeys}`\n\n/** Helper type for plugin translation calls (bypasses DefaultTranslationKeys constraint) */\nexport type PluginT = (key: string, vars?: Record<string, unknown>) => string\n"],"names":["en","translations","reservation"],"mappings":"AAAA,OAAOA,QAAQ,YAAiC;AAEhD,OAAO,MAAMC,eAAwD;IACnED,IAAI;QAAEE,aAAaF;IAAG;AACxB,EAAC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,7 +1,72 @@
|
|
|
1
|
-
import type { CollectionConfig } from 'payload';
|
|
1
|
+
import type { CollectionConfig, Field, PayloadRequest } from 'payload';
|
|
2
|
+
export type DurationType = 'fixed' | 'flexible' | 'full-day';
|
|
3
|
+
export type CapacityMode = 'per-guest' | 'per-reservation';
|
|
4
|
+
export type StatusMachineConfig = {
|
|
5
|
+
blockingStatuses: string[];
|
|
6
|
+
defaultStatus: string;
|
|
7
|
+
statuses: string[];
|
|
8
|
+
terminalStatuses: string[];
|
|
9
|
+
transitions: Record<string, string[]>;
|
|
10
|
+
};
|
|
11
|
+
export declare const DEFAULT_STATUS_MACHINE: StatusMachineConfig;
|
|
12
|
+
export type ReservationItemConfig = {
|
|
13
|
+
endTime?: string;
|
|
14
|
+
guestCount?: number;
|
|
15
|
+
resource: string;
|
|
16
|
+
service?: string;
|
|
17
|
+
startTime?: string;
|
|
18
|
+
};
|
|
19
|
+
export type ReservationPluginHooks = {
|
|
20
|
+
afterBookingCancel?: Array<(args: {
|
|
21
|
+
doc: Record<string, unknown>;
|
|
22
|
+
req: PayloadRequest;
|
|
23
|
+
}) => Promise<void> | void>;
|
|
24
|
+
afterBookingConfirm?: Array<(args: {
|
|
25
|
+
doc: Record<string, unknown>;
|
|
26
|
+
req: PayloadRequest;
|
|
27
|
+
}) => Promise<void> | void>;
|
|
28
|
+
afterBookingCreate?: Array<(args: {
|
|
29
|
+
doc: Record<string, unknown>;
|
|
30
|
+
req: PayloadRequest;
|
|
31
|
+
}) => Promise<void> | void>;
|
|
32
|
+
afterStatusChange?: Array<(args: {
|
|
33
|
+
doc: Record<string, unknown>;
|
|
34
|
+
newStatus: string;
|
|
35
|
+
previousStatus: string;
|
|
36
|
+
req: PayloadRequest;
|
|
37
|
+
}) => Promise<void> | void>;
|
|
38
|
+
beforeBookingCancel?: Array<(args: {
|
|
39
|
+
doc: Record<string, unknown>;
|
|
40
|
+
reason?: string;
|
|
41
|
+
req: PayloadRequest;
|
|
42
|
+
}) => Promise<void> | void>;
|
|
43
|
+
beforeBookingConfirm?: Array<(args: {
|
|
44
|
+
doc: Record<string, unknown>;
|
|
45
|
+
newStatus: string;
|
|
46
|
+
req: PayloadRequest;
|
|
47
|
+
}) => Promise<void> | void>;
|
|
48
|
+
beforeBookingCreate?: Array<(args: {
|
|
49
|
+
data: Record<string, unknown>;
|
|
50
|
+
req: PayloadRequest;
|
|
51
|
+
}) => Promise<Record<string, unknown>> | Record<string, unknown>>;
|
|
52
|
+
};
|
|
53
|
+
export type ResourceOwnerModeConfig = {
|
|
54
|
+
/** Roles that can see all records (default: check req.user.collection === adminCollection) */
|
|
55
|
+
adminRoles?: string[];
|
|
56
|
+
/** Whether Services also get an owner field (default: false — Services are platform-managed) */
|
|
57
|
+
ownedServices?: boolean;
|
|
58
|
+
/** Field name for the owner relationship on Resources (default: 'owner') */
|
|
59
|
+
ownerField?: string;
|
|
60
|
+
};
|
|
61
|
+
export type ResolvedResourceOwnerModeConfig = {
|
|
62
|
+
adminRoles: string[];
|
|
63
|
+
ownedServices: boolean;
|
|
64
|
+
ownerField: string;
|
|
65
|
+
};
|
|
2
66
|
export type ReservationPluginConfig = {
|
|
3
67
|
/** Override access control per collection */
|
|
4
68
|
access?: {
|
|
69
|
+
customers?: CollectionConfig['access'];
|
|
5
70
|
reservations?: CollectionConfig['access'];
|
|
6
71
|
resources?: CollectionConfig['access'];
|
|
7
72
|
schedules?: CollectionConfig['access'];
|
|
@@ -11,25 +76,33 @@ export type ReservationPluginConfig = {
|
|
|
11
76
|
adminGroup?: string;
|
|
12
77
|
/** Hours of notice required before cancellation */
|
|
13
78
|
cancellationNoticePeriod?: number;
|
|
14
|
-
/** Role to filter customers by in the reservation form. Set false to disable filtering. (default: 'customer') */
|
|
15
|
-
customerRole?: false | string;
|
|
16
79
|
/** Default buffer time in minutes between reservations */
|
|
17
80
|
defaultBufferTime?: number;
|
|
18
81
|
/** Disable the plugin entirely */
|
|
19
82
|
disabled?: boolean;
|
|
83
|
+
/** Extra fields to append to the Reservations collection */
|
|
84
|
+
extraReservationFields?: Field[];
|
|
85
|
+
/** Plugin hooks for external integrations */
|
|
86
|
+
hooks?: ReservationPluginHooks;
|
|
87
|
+
/** Enable resource-owner multi-tenancy (opt-in) */
|
|
88
|
+
resourceOwnerMode?: ResourceOwnerModeConfig;
|
|
20
89
|
/** Override collection slugs */
|
|
21
90
|
slugs?: {
|
|
91
|
+
customers?: string;
|
|
22
92
|
media?: string;
|
|
23
93
|
reservations?: string;
|
|
24
94
|
resources?: string;
|
|
25
95
|
schedules?: string;
|
|
26
96
|
services?: string;
|
|
27
97
|
};
|
|
28
|
-
/**
|
|
98
|
+
/** Configurable status machine (defaults to current behavior) */
|
|
99
|
+
statusMachine?: Partial<StatusMachineConfig>;
|
|
100
|
+
/** Which existing auth collection to extend with customer fields */
|
|
29
101
|
userCollection?: string;
|
|
30
102
|
};
|
|
31
103
|
export type ResolvedReservationPluginConfig = {
|
|
32
104
|
access: {
|
|
105
|
+
customers?: CollectionConfig['access'];
|
|
33
106
|
reservations?: CollectionConfig['access'];
|
|
34
107
|
resources?: CollectionConfig['access'];
|
|
35
108
|
schedules?: CollectionConfig['access'];
|
|
@@ -37,20 +110,25 @@ export type ResolvedReservationPluginConfig = {
|
|
|
37
110
|
};
|
|
38
111
|
adminGroup: string;
|
|
39
112
|
cancellationNoticePeriod: number;
|
|
40
|
-
customerRole: false | string;
|
|
41
113
|
defaultBufferTime: number;
|
|
42
114
|
disabled: boolean;
|
|
115
|
+
extraReservationFields: Field[];
|
|
116
|
+
hooks: ReservationPluginHooks;
|
|
43
117
|
localized: boolean;
|
|
118
|
+
resourceOwnerMode: ResolvedResourceOwnerModeConfig | undefined;
|
|
44
119
|
slugs: {
|
|
120
|
+
customers: string;
|
|
45
121
|
media: string;
|
|
46
122
|
reservations: string;
|
|
47
123
|
resources: string;
|
|
48
124
|
schedules: string;
|
|
49
125
|
services: string;
|
|
50
126
|
};
|
|
51
|
-
|
|
127
|
+
statusMachine: StatusMachineConfig;
|
|
128
|
+
userCollection: string | undefined;
|
|
52
129
|
};
|
|
53
130
|
export type ReservationStatus = 'cancelled' | 'completed' | 'confirmed' | 'no-show' | 'pending';
|
|
54
131
|
export type DayOfWeek = 'fri' | 'mon' | 'sat' | 'sun' | 'thu' | 'tue' | 'wed';
|
|
55
132
|
export type ScheduleType = 'manual' | 'recurring';
|
|
133
|
+
/** @deprecated Use DEFAULT_STATUS_MACHINE.transitions instead */
|
|
56
134
|
export declare const VALID_STATUS_TRANSITIONS: Record<ReservationStatus, ReservationStatus[]>;
|
package/dist/types.js
CHANGED
|
@@ -1,16 +1,36 @@
|
|
|
1
|
-
export const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
export const DEFAULT_STATUS_MACHINE = {
|
|
2
|
+
blockingStatuses: [
|
|
3
|
+
'pending',
|
|
4
|
+
'confirmed'
|
|
5
|
+
],
|
|
6
|
+
defaultStatus: 'pending',
|
|
7
|
+
statuses: [
|
|
8
|
+
'pending',
|
|
9
|
+
'confirmed',
|
|
5
10
|
'completed',
|
|
6
11
|
'cancelled',
|
|
7
12
|
'no-show'
|
|
8
13
|
],
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
'
|
|
12
|
-
'
|
|
13
|
-
]
|
|
14
|
+
terminalStatuses: [
|
|
15
|
+
'completed',
|
|
16
|
+
'cancelled',
|
|
17
|
+
'no-show'
|
|
18
|
+
],
|
|
19
|
+
transitions: {
|
|
20
|
+
cancelled: [],
|
|
21
|
+
completed: [],
|
|
22
|
+
confirmed: [
|
|
23
|
+
'completed',
|
|
24
|
+
'cancelled',
|
|
25
|
+
'no-show'
|
|
26
|
+
],
|
|
27
|
+
'no-show': [],
|
|
28
|
+
pending: [
|
|
29
|
+
'confirmed',
|
|
30
|
+
'cancelled'
|
|
31
|
+
]
|
|
32
|
+
}
|
|
14
33
|
};
|
|
34
|
+
/** @deprecated Use DEFAULT_STATUS_MACHINE.transitions instead */ export const VALID_STATUS_TRANSITIONS = DEFAULT_STATUS_MACHINE.transitions;
|
|
15
35
|
|
|
16
36
|
//# sourceMappingURL=types.js.map
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload'\n\nexport type ReservationPluginConfig = {\n /** Override access control per collection */\n 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 /** Hours of notice required before cancellation */\n cancellationNoticePeriod?: number\n /**
|
|
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 /** 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 ownerField: 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 /** 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 /** Enable resource-owner multi-tenancy (opt-in) */\n resourceOwnerMode?: ResourceOwnerModeConfig\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 /** 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 cancellationNoticePeriod: number\n defaultBufferTime: number\n disabled: boolean\n extraReservationFields: Field[]\n hooks: ReservationPluginHooks\n localized: boolean\n resourceOwnerMode: ResolvedResourceOwnerModeConfig | undefined\n slugs: {\n customers: string\n media: string\n reservations: string\n resources: string\n schedules: string\n services: string\n }\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;AAiJD,+DAA+D,GAC/D,OAAO,MAAMC,2BACXV,uBAAuBK,WAAW,CAAkD"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CollectionConfig } from 'payload';
|
|
2
|
+
import type { ResolvedResourceOwnerModeConfig } from '../types.js';
|
|
3
|
+
type CollectionAccess = NonNullable<CollectionConfig['access']>;
|
|
4
|
+
/**
|
|
5
|
+
* Access factories for Resources collection.
|
|
6
|
+
* Owners may read/update/delete their own resources; anyone authenticated may create.
|
|
7
|
+
*/
|
|
8
|
+
export declare function makeResourceOwnerAccess(rom: ResolvedResourceOwnerModeConfig): CollectionAccess;
|
|
9
|
+
/**
|
|
10
|
+
* Access factories for Schedules collection.
|
|
11
|
+
* A schedule's ownership is determined through its `resource.owner` relationship.
|
|
12
|
+
*/
|
|
13
|
+
export declare function makeScheduleOwnerAccess(rom: ResolvedResourceOwnerModeConfig): CollectionAccess;
|
|
14
|
+
/**
|
|
15
|
+
* Access factories for Reservations collection.
|
|
16
|
+
* Resource owners can see reservations for their resources (read-only);
|
|
17
|
+
* mutations are admin-only to prevent owners from unilaterally cancelling guest bookings.
|
|
18
|
+
*/
|
|
19
|
+
export declare function makeReservationOwnerAccess(rom: ResolvedResourceOwnerModeConfig): CollectionAccess;
|
|
20
|
+
/**
|
|
21
|
+
* Access factories for Services collection when `ownedServices: true`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function makeServiceOwnerAccess(rom: ResolvedResourceOwnerModeConfig, ownerField: string): CollectionAccess;
|
|
24
|
+
export {};
|