payload-reserve 2.4.0 → 3.0.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 +217 -4
- package/dist/collections/Holds.d.ts +18 -0
- package/dist/collections/Holds.js +113 -0
- package/dist/collections/Holds.js.map +1 -0
- package/dist/collections/Reservations.js +8 -0
- package/dist/collections/Reservations.js.map +1 -1
- package/dist/collections/Resources.js +35 -1
- package/dist/collections/Resources.js.map +1 -1
- package/dist/collections/Services.js +41 -0
- package/dist/collections/Services.js.map +1 -1
- package/dist/components/CalendarView/LaneTimelineView.js +4 -5
- package/dist/components/CalendarView/LaneTimelineView.js.map +1 -1
- package/dist/components/CalendarView/index.js +55 -71
- package/dist/components/CalendarView/index.js.map +1 -1
- package/dist/components/DashboardWidget/DashboardWidgetServer.js +11 -74
- package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
- package/dist/components/DashboardWidget/fetchDashboardStats.d.ts +42 -0
- package/dist/components/DashboardWidget/fetchDashboardStats.js +113 -0
- package/dist/components/DashboardWidget/fetchDashboardStats.js.map +1 -0
- package/dist/defaults.d.ts +2 -0
- package/dist/defaults.js +11 -0
- package/dist/defaults.js.map +1 -1
- package/dist/endpoints/cancelBooking.js +43 -14
- package/dist/endpoints/cancelBooking.js.map +1 -1
- package/dist/endpoints/checkAvailability.js +12 -1
- package/dist/endpoints/checkAvailability.js.map +1 -1
- package/dist/endpoints/createBooking.js +108 -7
- package/dist/endpoints/createBooking.js.map +1 -1
- package/dist/endpoints/customerSearch.js +6 -0
- package/dist/endpoints/customerSearch.js.map +1 -1
- package/dist/endpoints/effectiveTimezone.js +1 -0
- package/dist/endpoints/effectiveTimezone.js.map +1 -1
- package/dist/endpoints/getSlots.js +12 -1
- package/dist/endpoints/getSlots.js.map +1 -1
- package/dist/endpoints/holdSlot.d.ts +12 -0
- package/dist/endpoints/holdSlot.js +126 -0
- package/dist/endpoints/holdSlot.js.map +1 -0
- package/dist/endpoints/releaseSlot.d.ts +8 -0
- package/dist/endpoints/releaseSlot.js +34 -0
- package/dist/endpoints/releaseSlot.js.map +1 -0
- package/dist/endpoints/resourceAvailability.d.ts +2 -0
- package/dist/endpoints/resourceAvailability.js +79 -3
- package/dist/endpoints/resourceAvailability.js.map +1 -1
- package/dist/hooks/holds/validateHoldSlot.d.ts +14 -0
- package/dist/hooks/holds/validateHoldSlot.js +90 -0
- package/dist/hooks/holds/validateHoldSlot.js.map +1 -0
- package/dist/hooks/reservations/acquireBookingLock.d.ts +30 -0
- package/dist/hooks/reservations/acquireBookingLock.js +71 -0
- package/dist/hooks/reservations/acquireBookingLock.js.map +1 -0
- package/dist/hooks/reservations/calculateEndTime.js +13 -1
- package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
- package/dist/hooks/reservations/expandRequiredResources.js +3 -0
- package/dist/hooks/reservations/expandRequiredResources.js.map +1 -1
- package/dist/hooks/reservations/validateActive.d.ts +11 -0
- package/dist/hooks/reservations/validateActive.js +103 -0
- package/dist/hooks/reservations/validateActive.js.map +1 -0
- package/dist/hooks/reservations/validateConflicts.js +7 -1
- package/dist/hooks/reservations/validateConflicts.js.map +1 -1
- package/dist/hooks/reservations/validateGuestBooking.js +3 -0
- package/dist/hooks/reservations/validateGuestBooking.js.map +1 -1
- package/dist/hooks/shared/preventDeleteWhenReferenced.d.ts +42 -0
- package/dist/hooks/shared/preventDeleteWhenReferenced.js +87 -0
- package/dist/hooks/shared/preventDeleteWhenReferenced.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin.js +179 -3
- package/dist/plugin.js.map +1 -1
- package/dist/services/AvailabilityService.d.ts +46 -5
- package/dist/services/AvailabilityService.js +168 -12
- package/dist/services/AvailabilityService.js.map +1 -1
- package/dist/services/HoldService.d.ts +53 -0
- package/dist/services/HoldService.js +169 -0
- package/dist/services/HoldService.js.map +1 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js.map +1 -1
- package/dist/translations/ar.json +5 -1
- package/dist/translations/de.json +5 -1
- package/dist/translations/en.json +5 -1
- package/dist/translations/es.json +5 -1
- package/dist/translations/fa.json +5 -1
- package/dist/translations/fr.json +5 -1
- package/dist/translations/hi.json +5 -1
- package/dist/translations/id.json +5 -1
- package/dist/translations/pl.json +5 -1
- package/dist/translations/ru.json +5 -1
- package/dist/translations/tr.json +5 -1
- package/dist/translations/zh.json +5 -1
- package/dist/types.d.ts +29 -0
- package/dist/types.js.map +1 -1
- package/dist/utilities/calendarGrid.d.ts +44 -0
- package/dist/utilities/calendarGrid.js +67 -0
- package/dist/utilities/calendarGrid.js.map +1 -0
- package/dist/utilities/reservationChanges.d.ts +6 -1
- package/dist/utilities/reservationChanges.js +1 -1
- package/dist/utilities/reservationChanges.js.map +1 -1
- package/dist/utilities/resolveReservationItems.d.ts +13 -2
- package/dist/utilities/resolveReservationItems.js +87 -2
- package/dist/utilities/resolveReservationItems.js.map +1 -1
- package/dist/utilities/retryOnWriteConflict.d.ts +66 -0
- package/dist/utilities/retryOnWriteConflict.js +135 -0
- package/dist/utilities/retryOnWriteConflict.js.map +1 -0
- package/dist/utilities/tenantTimezone.d.ts +77 -1
- package/dist/utilities/tenantTimezone.js +163 -2
- package/dist/utilities/tenantTimezone.js.map +1 -1
- package/dist/utilities/transactionSupport.d.ts +13 -0
- package/dist/utilities/transactionSupport.js +30 -0
- package/dist/utilities/transactionSupport.js.map +1 -0
- package/package.json +13 -13
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { ValidationError } from 'payload';
|
|
2
|
+
import { isTransientWriteConflict } from '../utilities/retryOnWriteConflict.js';
|
|
3
|
+
import { computeEndTime } from './AvailabilityService.js';
|
|
4
|
+
/**
|
|
5
|
+
* Claim a slot for `config.slotHolds.ttlMinutes` while the caller completes an
|
|
6
|
+
* external step (typically payment).
|
|
7
|
+
*
|
|
8
|
+
* This function only assembles the row. The parts that make a hold trustworthy
|
|
9
|
+
* — claiming the resource lock and running the same `checkAvailability` a
|
|
10
|
+
* booking runs — live in the `validateHoldSlot` beforeChange hook, because they
|
|
11
|
+
* must share the transaction Payload opens around `create`. Doing them here
|
|
12
|
+
* instead granted 3 of 8 simultaneous holds for one slot.
|
|
13
|
+
*/ export async function takeHold(params) {
|
|
14
|
+
const { config, guestCount = 1, req, resourceId, serviceId, startTime } = params;
|
|
15
|
+
const { payload } = req;
|
|
16
|
+
// `disableErrors` is what makes the `!service` guard below reachable at all:
|
|
17
|
+
// without it `findByID` THROWS `NotFound`, and this call sits outside the
|
|
18
|
+
// try/catch, so a bad service id escaped as a raw 404 while a bad resource id
|
|
19
|
+
// came back as a 409 with an internal message. The trailing `.catch` covers
|
|
20
|
+
// the adapter's own cast error for a malformed id (the same treatment
|
|
21
|
+
// `/reserve/availability` and `/reserve/slots` already give it) — to a caller
|
|
22
|
+
// both mean exactly "no such service".
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
+
const service = await payload.findByID({
|
|
25
|
+
id: serviceId,
|
|
26
|
+
collection: config.slugs.services,
|
|
27
|
+
depth: 0,
|
|
28
|
+
disableErrors: true,
|
|
29
|
+
joins: false,
|
|
30
|
+
req
|
|
31
|
+
}).catch(()=>null);
|
|
32
|
+
if (!service) {
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
reason: 'service_not_found'
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (config.enforceActive && service.active === false) {
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
reason: 'service_inactive'
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// Same treatment for the resource. `validateHoldSlot` writes the resource's
|
|
45
|
+
// booking lock through `payload.db.updateOne`, which for an unknown or
|
|
46
|
+
// malformed id throws an adapter-level error — previously echoed verbatim as
|
|
47
|
+
// a 409. Resolving it here turns it into a clean 404.
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
+
const resource = await payload.findByID({
|
|
50
|
+
id: resourceId,
|
|
51
|
+
collection: config.slugs.resources,
|
|
52
|
+
depth: 0,
|
|
53
|
+
disableErrors: true,
|
|
54
|
+
req
|
|
55
|
+
}).catch(()=>null);
|
|
56
|
+
if (!resource) {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
reason: 'resource_not_found'
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const { endTime } = computeEndTime({
|
|
63
|
+
durationType: service.durationType ?? 'fixed',
|
|
64
|
+
endTime: params.endTime,
|
|
65
|
+
serviceDuration: service.duration ?? 0,
|
|
66
|
+
startTime,
|
|
67
|
+
timeZone: config.timezone
|
|
68
|
+
});
|
|
69
|
+
// Expired-row sweep. Purely hygienic — every read already filters on
|
|
70
|
+
// expiresAt — so it must never fail the hold.
|
|
71
|
+
try {
|
|
72
|
+
await payload.delete({
|
|
73
|
+
collection: config.slugs.holds,
|
|
74
|
+
req,
|
|
75
|
+
where: {
|
|
76
|
+
expiresAt: {
|
|
77
|
+
less_than: new Date().toISOString()
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
} catch {
|
|
82
|
+
// Ignored by design: expiry is enforced at read time, not by this sweep.
|
|
83
|
+
//
|
|
84
|
+
// But swallowing the error is not enough to keep that promise. If this
|
|
85
|
+
// delete's `beginTransaction` REJECTED, `initTransaction` already stored the
|
|
86
|
+
// rejected promise on `req.transactionID` and `killTransaction`'s guard
|
|
87
|
+
// skips promises — so the `create` below would short-circuit inside
|
|
88
|
+
// `initTransaction` and rethrow THE SWEEP'S error, failing the hold on
|
|
89
|
+
// behalf of a step documented as never able to. `retryOnWriteConflict`'s
|
|
90
|
+
// between-attempts clearing cannot reach this: the poisoning happens
|
|
91
|
+
// mid-attempt. Clearing it here restores the invariant.
|
|
92
|
+
//
|
|
93
|
+
// This cannot drop a live transaction. A sweep that COMMITTED cleared the id
|
|
94
|
+
// itself; a sweep that failed any other way had `killTransaction` clear it
|
|
95
|
+
// (Payload does that unconditionally, even for an id it only joined). The
|
|
96
|
+
// poisoned promise is the one shape that can still be sitting here.
|
|
97
|
+
if (req.transactionID !== undefined) {
|
|
98
|
+
delete req.transactionID;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const token = crypto.randomUUID();
|
|
102
|
+
const expiresAt = new Date(Date.now() + config.slotHolds.ttlMinutes * 60_000).toISOString();
|
|
103
|
+
// The lock, the availability check and this insert must share one
|
|
104
|
+
// transaction for the lock to serialize anything — Payload opens that
|
|
105
|
+
// transaction around `create`, and validateHoldSlot runs inside it.
|
|
106
|
+
let created;
|
|
107
|
+
try {
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
109
|
+
created = await payload.create({
|
|
110
|
+
collection: config.slugs.holds,
|
|
111
|
+
data: {
|
|
112
|
+
customer: req.user?.id,
|
|
113
|
+
endTime: endTime.toISOString(),
|
|
114
|
+
expiresAt,
|
|
115
|
+
guestCount,
|
|
116
|
+
resource: resourceId,
|
|
117
|
+
service: serviceId,
|
|
118
|
+
startTime: startTime.toISOString(),
|
|
119
|
+
token
|
|
120
|
+
},
|
|
121
|
+
req
|
|
122
|
+
});
|
|
123
|
+
} catch (error) {
|
|
124
|
+
// A lost lock race must REJECT, not resolve: `retryOnWriteConflict` (which
|
|
125
|
+
// wraps every caller of this function) only retries a rejected promise, so
|
|
126
|
+
// returning here made the whole retry wrapper inert and collapsed a
|
|
127
|
+
// `quantity: 3` resource to one hold under a burst.
|
|
128
|
+
if (isTransientWriteConflict(error)) {
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
// validateHoldSlot signals genuine unavailability — the slot is booked,
|
|
132
|
+
// held, out of capacity, or the service went inactive — as a
|
|
133
|
+
// ValidationError. That is the only refusal this catch recognises.
|
|
134
|
+
if (error instanceof ValidationError) {
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
reason: 'slot_taken'
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// Anything else is a real failure, not a refusal. Propagate it rather than
|
|
141
|
+
// dressing it up as a 409 carrying its own message.
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
hold: {
|
|
146
|
+
id: created.id,
|
|
147
|
+
expiresAt,
|
|
148
|
+
token
|
|
149
|
+
},
|
|
150
|
+
ok: true
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/** Release a hold early. Idempotent: releasing an unknown token is not an error. */ export async function releaseHold(params) {
|
|
154
|
+
const { config, req, token } = params;
|
|
155
|
+
const { docs } = await req.payload.delete({
|
|
156
|
+
collection: config.slugs.holds,
|
|
157
|
+
req,
|
|
158
|
+
where: {
|
|
159
|
+
token: {
|
|
160
|
+
equals: token
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
return {
|
|
165
|
+
released: docs.length
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
//# sourceMappingURL=HoldService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/services/HoldService.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { isTransientWriteConflict } from '../utilities/retryOnWriteConflict.js'\nimport { computeEndTime } from './AvailabilityService.js'\n\n/**\n * Why a hold was refused — a CLOSED set, deliberately.\n *\n * `/reserve/hold` is reachable without authentication, so the refusal reason is\n * attacker-visible output. An earlier version echoed `error.message` from any\n * failed write, which leaked internal detail (DB errors, `Forbidden`, genuine\n * bugs) and reported all of it as `409 slot_taken` — a status that tells a\n * well-behaved client the slot is gone and it should stop, for conditions that\n * are nothing of the sort. Everything not enumerated here now propagates as a\n * thrown error, so it surfaces as a 500 the operator can see rather than a\n * plausible-looking 409 the caller cannot act on.\n */\nexport type HoldRefusalReason =\n | 'resource_not_found'\n | 'service_inactive'\n | 'service_not_found'\n | 'slot_taken'\n\nexport type TakeHoldResult =\n | { hold: { expiresAt: string; id: number | string; token: string }; ok: true }\n | { ok: false; reason: HoldRefusalReason }\n\n/**\n * Claim a slot for `config.slotHolds.ttlMinutes` while the caller completes an\n * external step (typically payment).\n *\n * This function only assembles the row. The parts that make a hold trustworthy\n * — claiming the resource lock and running the same `checkAvailability` a\n * booking runs — live in the `validateHoldSlot` beforeChange hook, because they\n * must share the transaction Payload opens around `create`. Doing them here\n * instead granted 3 of 8 simultaneous holds for one slot.\n */\nexport async function takeHold(params: {\n config: ResolvedReservationPluginConfig\n endTime?: Date\n guestCount?: number\n req: PayloadRequest\n resourceId: number | string\n serviceId: number | string\n startTime: Date\n}): Promise<TakeHoldResult> {\n const { config, guestCount = 1, req, resourceId, serviceId, startTime } = params\n const { payload } = req\n\n // `disableErrors` is what makes the `!service` guard below reachable at all:\n // without it `findByID` THROWS `NotFound`, and this call sits outside the\n // try/catch, so a bad service id escaped as a raw 404 while a bad resource id\n // came back as a 409 with an internal message. The trailing `.catch` covers\n // the adapter's own cast error for a malformed id (the same treatment\n // `/reserve/availability` and `/reserve/slots` already give it) — to a caller\n // both mean exactly \"no such service\".\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (payload.findByID as any)({\n id: serviceId,\n collection: config.slugs.services,\n depth: 0,\n disableErrors: true,\n joins: false,\n req,\n }).catch(() => null)\n\n if (!service) {\n return { ok: false, reason: 'service_not_found' }\n }\n\n if (config.enforceActive && service.active === false) {\n return { ok: false, reason: 'service_inactive' }\n }\n\n // Same treatment for the resource. `validateHoldSlot` writes the resource's\n // booking lock through `payload.db.updateOne`, which for an unknown or\n // malformed id throws an adapter-level error — previously echoed verbatim as\n // a 409. Resolving it here turns it into a clean 404.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const resource = await (payload.findByID as any)({\n id: resourceId,\n collection: config.slugs.resources,\n depth: 0,\n disableErrors: true,\n req,\n }).catch(() => null)\n\n if (!resource) {\n return { ok: false, reason: 'resource_not_found' }\n }\n\n const { endTime } = computeEndTime({\n durationType: (service.durationType as 'fixed') ?? 'fixed',\n endTime: params.endTime,\n serviceDuration: (service.duration as number) ?? 0,\n startTime,\n timeZone: config.timezone,\n })\n\n // Expired-row sweep. Purely hygienic — every read already filters on\n // expiresAt — so it must never fail the hold.\n try {\n await payload.delete({\n collection: config.slugs.holds,\n req,\n where: { expiresAt: { less_than: new Date().toISOString() } },\n })\n } catch {\n // Ignored by design: expiry is enforced at read time, not by this sweep.\n //\n // But swallowing the error is not enough to keep that promise. If this\n // delete's `beginTransaction` REJECTED, `initTransaction` already stored the\n // rejected promise on `req.transactionID` and `killTransaction`'s guard\n // skips promises — so the `create` below would short-circuit inside\n // `initTransaction` and rethrow THE SWEEP'S error, failing the hold on\n // behalf of a step documented as never able to. `retryOnWriteConflict`'s\n // between-attempts clearing cannot reach this: the poisoning happens\n // mid-attempt. Clearing it here restores the invariant.\n //\n // This cannot drop a live transaction. A sweep that COMMITTED cleared the id\n // itself; a sweep that failed any other way had `killTransaction` clear it\n // (Payload does that unconditionally, even for an id it only joined). The\n // poisoned promise is the one shape that can still be sitting here.\n if (req.transactionID !== undefined) {\n delete req.transactionID\n }\n }\n\n const token = crypto.randomUUID()\n const expiresAt = new Date(Date.now() + config.slotHolds.ttlMinutes * 60_000).toISOString()\n\n // The lock, the availability check and this insert must share one\n // transaction for the lock to serialize anything — Payload opens that\n // transaction around `create`, and validateHoldSlot runs inside it.\n let created: { id: number | string }\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n created = await (payload.create as any)({\n collection: config.slugs.holds,\n data: {\n customer: req.user?.id,\n endTime: endTime.toISOString(),\n expiresAt,\n guestCount,\n resource: resourceId,\n service: serviceId,\n startTime: startTime.toISOString(),\n token,\n },\n req,\n })\n } catch (error) {\n // A lost lock race must REJECT, not resolve: `retryOnWriteConflict` (which\n // wraps every caller of this function) only retries a rejected promise, so\n // returning here made the whole retry wrapper inert and collapsed a\n // `quantity: 3` resource to one hold under a burst.\n if (isTransientWriteConflict(error)) {\n throw error\n }\n // validateHoldSlot signals genuine unavailability — the slot is booked,\n // held, out of capacity, or the service went inactive — as a\n // ValidationError. That is the only refusal this catch recognises.\n if (error instanceof ValidationError) {\n return { ok: false, reason: 'slot_taken' }\n }\n // Anything else is a real failure, not a refusal. Propagate it rather than\n // dressing it up as a 409 carrying its own message.\n throw error\n }\n\n return { hold: { id: created.id, expiresAt, token }, ok: true }\n}\n\n/** Release a hold early. Idempotent: releasing an unknown token is not an error. */\nexport async function releaseHold(params: {\n config: ResolvedReservationPluginConfig\n req: PayloadRequest\n token: string\n}): Promise<{ released: number }> {\n const { config, req, token } = params\n\n const { docs } = await req.payload.delete({\n collection: config.slugs.holds,\n req,\n where: { token: { equals: token } },\n })\n\n return { released: docs.length }\n}\n"],"names":["ValidationError","isTransientWriteConflict","computeEndTime","takeHold","params","config","guestCount","req","resourceId","serviceId","startTime","payload","service","findByID","id","collection","slugs","services","depth","disableErrors","joins","catch","ok","reason","enforceActive","active","resource","resources","endTime","durationType","serviceDuration","duration","timeZone","timezone","delete","holds","where","expiresAt","less_than","Date","toISOString","transactionID","undefined","token","crypto","randomUUID","now","slotHolds","ttlMinutes","created","create","data","customer","user","error","hold","releaseHold","docs","equals","released","length"],"mappings":"AAEA,SAASA,eAAe,QAAQ,UAAS;AAIzC,SAASC,wBAAwB,QAAQ,uCAAsC;AAC/E,SAASC,cAAc,QAAQ,2BAA0B;AAwBzD;;;;;;;;;CASC,GACD,OAAO,eAAeC,SAASC,MAQ9B;IACC,MAAM,EAAEC,MAAM,EAAEC,aAAa,CAAC,EAAEC,GAAG,EAAEC,UAAU,EAAEC,SAAS,EAAEC,SAAS,EAAE,GAAGN;IAC1E,MAAM,EAAEO,OAAO,EAAE,GAAGJ;IAEpB,6EAA6E;IAC7E,0EAA0E;IAC1E,8EAA8E;IAC9E,4EAA4E;IAC5E,sEAAsE;IACtE,8EAA8E;IAC9E,uCAAuC;IACvC,8DAA8D;IAC9D,MAAMK,UAAU,MAAM,AAACD,QAAQE,QAAQ,CAAS;QAC9CC,IAAIL;QACJM,YAAYV,OAAOW,KAAK,CAACC,QAAQ;QACjCC,OAAO;QACPC,eAAe;QACfC,OAAO;QACPb;IACF,GAAGc,KAAK,CAAC,IAAM;IAEf,IAAI,CAACT,SAAS;QACZ,OAAO;YAAEU,IAAI;YAAOC,QAAQ;QAAoB;IAClD;IAEA,IAAIlB,OAAOmB,aAAa,IAAIZ,QAAQa,MAAM,KAAK,OAAO;QACpD,OAAO;YAAEH,IAAI;YAAOC,QAAQ;QAAmB;IACjD;IAEA,4EAA4E;IAC5E,uEAAuE;IACvE,6EAA6E;IAC7E,sDAAsD;IACtD,8DAA8D;IAC9D,MAAMG,WAAW,MAAM,AAACf,QAAQE,QAAQ,CAAS;QAC/CC,IAAIN;QACJO,YAAYV,OAAOW,KAAK,CAACW,SAAS;QAClCT,OAAO;QACPC,eAAe;QACfZ;IACF,GAAGc,KAAK,CAAC,IAAM;IAEf,IAAI,CAACK,UAAU;QACb,OAAO;YAAEJ,IAAI;YAAOC,QAAQ;QAAqB;IACnD;IAEA,MAAM,EAAEK,OAAO,EAAE,GAAG1B,eAAe;QACjC2B,cAAc,AAACjB,QAAQiB,YAAY,IAAgB;QACnDD,SAASxB,OAAOwB,OAAO;QACvBE,iBAAiB,AAAClB,QAAQmB,QAAQ,IAAe;QACjDrB;QACAsB,UAAU3B,OAAO4B,QAAQ;IAC3B;IAEA,qEAAqE;IACrE,8CAA8C;IAC9C,IAAI;QACF,MAAMtB,QAAQuB,MAAM,CAAC;YACnBnB,YAAYV,OAAOW,KAAK,CAACmB,KAAK;YAC9B5B;YACA6B,OAAO;gBAAEC,WAAW;oBAAEC,WAAW,IAAIC,OAAOC,WAAW;gBAAG;YAAE;QAC9D;IACF,EAAE,OAAM;QACN,yEAAyE;QACzE,EAAE;QACF,uEAAuE;QACvE,6EAA6E;QAC7E,wEAAwE;QACxE,oEAAoE;QACpE,uEAAuE;QACvE,yEAAyE;QACzE,qEAAqE;QACrE,wDAAwD;QACxD,EAAE;QACF,6EAA6E;QAC7E,2EAA2E;QAC3E,0EAA0E;QAC1E,oEAAoE;QACpE,IAAIjC,IAAIkC,aAAa,KAAKC,WAAW;YACnC,OAAOnC,IAAIkC,aAAa;QAC1B;IACF;IAEA,MAAME,QAAQC,OAAOC,UAAU;IAC/B,MAAMR,YAAY,IAAIE,KAAKA,KAAKO,GAAG,KAAKzC,OAAO0C,SAAS,CAACC,UAAU,GAAG,QAAQR,WAAW;IAEzF,kEAAkE;IAClE,sEAAsE;IACtE,oEAAoE;IACpE,IAAIS;IACJ,IAAI;QACF,8DAA8D;QAC9DA,UAAU,MAAM,AAACtC,QAAQuC,MAAM,CAAS;YACtCnC,YAAYV,OAAOW,KAAK,CAACmB,KAAK;YAC9BgB,MAAM;gBACJC,UAAU7C,IAAI8C,IAAI,EAAEvC;gBACpBc,SAASA,QAAQY,WAAW;gBAC5BH;gBACA/B;gBACAoB,UAAUlB;gBACVI,SAASH;gBACTC,WAAWA,UAAU8B,WAAW;gBAChCG;YACF;YACApC;QACF;IACF,EAAE,OAAO+C,OAAO;QACd,2EAA2E;QAC3E,2EAA2E;QAC3E,oEAAoE;QACpE,oDAAoD;QACpD,IAAIrD,yBAAyBqD,QAAQ;YACnC,MAAMA;QACR;QACA,wEAAwE;QACxE,6DAA6D;QAC7D,mEAAmE;QACnE,IAAIA,iBAAiBtD,iBAAiB;YACpC,OAAO;gBAAEsB,IAAI;gBAAOC,QAAQ;YAAa;QAC3C;QACA,2EAA2E;QAC3E,oDAAoD;QACpD,MAAM+B;IACR;IAEA,OAAO;QAAEC,MAAM;YAAEzC,IAAImC,QAAQnC,EAAE;YAAEuB;YAAWM;QAAM;QAAGrB,IAAI;IAAK;AAChE;AAEA,kFAAkF,GAClF,OAAO,eAAekC,YAAYpD,MAIjC;IACC,MAAM,EAAEC,MAAM,EAAEE,GAAG,EAAEoC,KAAK,EAAE,GAAGvC;IAE/B,MAAM,EAAEqD,IAAI,EAAE,GAAG,MAAMlD,IAAII,OAAO,CAACuB,MAAM,CAAC;QACxCnB,YAAYV,OAAOW,KAAK,CAACmB,KAAK;QAC9B5B;QACA6B,OAAO;YAAEO,OAAO;gBAAEe,QAAQf;YAAM;QAAE;IACpC;IAEA,OAAO;QAAEgB,UAAUF,KAAKG,MAAM;IAAC;AACjC"}
|
package/dist/services/index.d.ts
CHANGED
|
@@ -1 +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"}
|
|
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'\nexport type { EmptyReason } 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"}
|
|
@@ -61,6 +61,8 @@
|
|
|
61
61
|
"errorInvalidTransition": "لا يمكن الانتقال من \"{{from}}\" إلى \"{{to}}\".",
|
|
62
62
|
"errorCancellationNotice": "يتطلب الإلغاء إشعاراً مسبقاً لا يقل عن {{period}} ساعة. لم يتبقَّ سوى {{hours}} ساعة على الموعد.",
|
|
63
63
|
"errorConflict": "يتعارض هذا الموعد الزمني مع حجز قائم لهذا المورد.",
|
|
64
|
+
"errorServiceInactive": "الخدمة \"{{name}}\" غير نشطة ولا يمكن حجزها.",
|
|
65
|
+
"errorResourceInactive": "المورد \"{{name}}\" غير نشط ولا يمكن حجزه.",
|
|
64
66
|
"calendarLoading": "جارٍ تحميل الحجوزات...",
|
|
65
67
|
"calendarToday": "اليوم",
|
|
66
68
|
"calendarCreateNew": "إنشاء جديد",
|
|
@@ -128,8 +130,10 @@
|
|
|
128
130
|
"filterByResource": "التصفية حسب المورد",
|
|
129
131
|
"fieldOwner": "المالك",
|
|
130
132
|
"fieldResourceType": "نوع المورد",
|
|
133
|
+
"fieldResources": "الموارد",
|
|
134
|
+
"fieldResourcesDesc": "الموارد التي يمكنها تقديم هذه الخدمة. لتغيير ذلك، افتح المورد وعدّل حقل الخدمات الخاص به.",
|
|
131
135
|
"fieldRequiredResources": "الموارد المطلوبة",
|
|
132
|
-
"fieldRequiredResourcesDesc": "مجموعات الموارد الإضافية التي يشغلها كل حجز لهذه الخدمة (مثل الكرسي). يتم توسيع الحجوزات تلقائياً لتشمل هذه الموارد، وتُحظر إذا كانت أي مجموعة ممتلئة.",
|
|
136
|
+
"fieldRequiredResourcesDesc": "مجموعات الموارد الإضافية التي يشغلها كل حجز لهذه الخدمة (مثل الكرسي). يتم توسيع الحجوزات تلقائياً لتشمل هذه الموارد، وتُحظر إذا كانت أي مجموعة ممتلئة. وهذا يختلف عن الموارد التي يمكنها تقديم هذه الخدمة — راجع الموارد أعلاه.",
|
|
133
137
|
"fieldAllowGuestBooking": "حجز الضيف",
|
|
134
138
|
"fieldAllowGuestBookingDesc": "السماح بالحجوزات دون حساب عميل. \"وراثة\" تستخدم الإعداد الافتراضي على مستوى الإضافة.",
|
|
135
139
|
"guestBookingInherit": "وراثة الإعداد الافتراضي للإضافة",
|
|
@@ -61,6 +61,8 @@
|
|
|
61
61
|
"errorInvalidTransition": "Wechsel von \"{{from}}\" zu \"{{to}}\" nicht möglich.",
|
|
62
62
|
"errorCancellationNotice": "Stornierungen erfordern eine Vorlaufzeit von mindestens {{period}} Stunden. Nur noch {{hours}} Stunden bis zum Termin.",
|
|
63
63
|
"errorConflict": "Dieses Zeitfenster überschneidet sich mit einer bestehenden Reservierung für diese Ressource.",
|
|
64
|
+
"errorServiceInactive": "Die Leistung „{{name}}\" ist nicht aktiv und kann nicht gebucht werden.",
|
|
65
|
+
"errorResourceInactive": "Die Ressource „{{name}}\" ist nicht aktiv und kann nicht gebucht werden.",
|
|
64
66
|
"calendarLoading": "Reservierungen werden geladen...",
|
|
65
67
|
"calendarToday": "Heute",
|
|
66
68
|
"calendarCreateNew": "Neu erstellen",
|
|
@@ -128,8 +130,10 @@
|
|
|
128
130
|
"filterByResource": "Nach Ressource filtern",
|
|
129
131
|
"fieldOwner": "Inhaber",
|
|
130
132
|
"fieldResourceType": "Ressourcentyp",
|
|
133
|
+
"fieldResources": "Ressourcen",
|
|
134
|
+
"fieldResourcesDesc": "Ressourcen, die diese Leistung erbringen können. Zum Ändern die Ressource öffnen und dort das Feld „Leistungen\" bearbeiten.",
|
|
131
135
|
"fieldRequiredResources": "Erforderliche Ressourcen",
|
|
132
|
-
"fieldRequiredResourcesDesc": "Zusätzliche Ressourcenpools, die jede Buchung dieser Leistung belegt (z. B. ein Stuhl). Buchungen werden automatisch um diese erweitert und blockiert, wenn ein Pool ausgelastet ist.",
|
|
136
|
+
"fieldRequiredResourcesDesc": "Zusätzliche Ressourcenpools, die jede Buchung dieser Leistung belegt (z. B. ein Stuhl). Buchungen werden automatisch um diese erweitert und blockiert, wenn ein Pool ausgelastet ist. Das ist nicht dasselbe wie die Ressourcen, die diese Leistung erbringen können – siehe „Ressourcen\" oben.",
|
|
133
137
|
"fieldAllowGuestBooking": "Gastbuchung",
|
|
134
138
|
"fieldAllowGuestBookingDesc": "Buchungen ohne Kundenkonto zulassen. \"Übernehmen\" verwendet die Plugin-Standardeinstellung.",
|
|
135
139
|
"guestBookingInherit": "Plugin-Standard übernehmen",
|
|
@@ -61,6 +61,8 @@
|
|
|
61
61
|
"errorInvalidTransition": "Cannot transition from \"{{from}}\" to \"{{to}}\".",
|
|
62
62
|
"errorCancellationNotice": "Cancellations require at least {{period}} hours notice. Only {{hours}} hours until the appointment.",
|
|
63
63
|
"errorConflict": "This time slot conflicts with an existing reservation for this resource.",
|
|
64
|
+
"errorServiceInactive": "Service \"{{name}}\" is not active and cannot be booked.",
|
|
65
|
+
"errorResourceInactive": "Resource \"{{name}}\" is not active and cannot be booked.",
|
|
64
66
|
"calendarLoading": "Loading reservations...",
|
|
65
67
|
"calendarToday": "Today",
|
|
66
68
|
"calendarCreateNew": "Create New",
|
|
@@ -128,8 +130,10 @@
|
|
|
128
130
|
"filterByResource": "Filter by resource",
|
|
129
131
|
"fieldOwner": "Owner",
|
|
130
132
|
"fieldResourceType": "Resource type",
|
|
133
|
+
"fieldResources": "Resources",
|
|
134
|
+
"fieldResourcesDesc": "Resources that can perform this service. To change this, open the resource and edit its Services field.",
|
|
131
135
|
"fieldRequiredResources": "Required resources",
|
|
132
|
-
"fieldRequiredResourcesDesc": "
|
|
136
|
+
"fieldRequiredResourcesDesc": "Extra resource pools every booking of this service also occupies (e.g. a chair). Bookings are auto-expanded to include these and are blocked if any pool is full. This is not the same as which resources perform the service — see Resources above.",
|
|
133
137
|
"fieldAllowGuestBooking": "Guest booking",
|
|
134
138
|
"fieldAllowGuestBookingDesc": "Allow bookings without a customer account. \"Inherit\" uses the plugin-level default.",
|
|
135
139
|
"guestBookingInherit": "Inherit plugin default",
|
|
@@ -61,6 +61,8 @@
|
|
|
61
61
|
"errorInvalidTransition": "No se puede cambiar de \"{{from}}\" a \"{{to}}\".",
|
|
62
62
|
"errorCancellationNotice": "Las cancelaciones requieren al menos {{period}} horas de antelación. Solo quedan {{hours}} horas hasta la cita.",
|
|
63
63
|
"errorConflict": "Esta franja horaria entra en conflicto con una reserva existente para este recurso.",
|
|
64
|
+
"errorServiceInactive": "El servicio \"{{name}}\" no está activo y no se puede reservar.",
|
|
65
|
+
"errorResourceInactive": "El recurso \"{{name}}\" no está activo y no se puede reservar.",
|
|
64
66
|
"calendarLoading": "Cargando reservas...",
|
|
65
67
|
"calendarToday": "Hoy",
|
|
66
68
|
"calendarCreateNew": "Crear nueva",
|
|
@@ -128,8 +130,10 @@
|
|
|
128
130
|
"filterByResource": "Filtrar por recurso",
|
|
129
131
|
"fieldOwner": "Propietario",
|
|
130
132
|
"fieldResourceType": "Tipo de recurso",
|
|
133
|
+
"fieldResources": "Recursos",
|
|
134
|
+
"fieldResourcesDesc": "Recursos que pueden realizar este servicio. Para cambiarlo, abre el recurso y edita su campo Servicios.",
|
|
131
135
|
"fieldRequiredResources": "Recursos necesarios",
|
|
132
|
-
"fieldRequiredResourcesDesc": "Grupos de recursos adicionales que ocupa cada reserva de este servicio (p. ej. una silla). Las reservas se amplían automáticamente para incluirlos y se bloquean si algún grupo está completo.",
|
|
136
|
+
"fieldRequiredResourcesDesc": "Grupos de recursos adicionales que ocupa cada reserva de este servicio (p. ej. una silla). Las reservas se amplían automáticamente para incluirlos y se bloquean si algún grupo está completo. Esto no es lo mismo que los recursos que pueden realizar este servicio. Consulta Recursos arriba.",
|
|
133
137
|
"fieldAllowGuestBooking": "Reserva de invitado",
|
|
134
138
|
"fieldAllowGuestBookingDesc": "Permitir reservas sin una cuenta de cliente. \"Heredar\" usa el valor predeterminado del plugin.",
|
|
135
139
|
"guestBookingInherit": "Heredar valor predeterminado del plugin",
|
|
@@ -61,6 +61,8 @@
|
|
|
61
61
|
"errorInvalidTransition": "امکان تغییر از «{{from}}» به «{{to}}» وجود ندارد.",
|
|
62
62
|
"errorCancellationNotice": "لغو رزرو به حداقل {{period}} ساعت اطلاعرسانی پیش نیاز دارد. تنها {{hours}} ساعت تا زمان قرار باقی مانده است.",
|
|
63
63
|
"errorConflict": "این بازه زمانی با یک رزرو موجود برای این منبع تداخل دارد.",
|
|
64
|
+
"errorServiceInactive": "سرویس «{{name}}» فعال نیست و قابل رزرو نیست.",
|
|
65
|
+
"errorResourceInactive": "منبع «{{name}}» فعال نیست و قابل رزرو نیست.",
|
|
64
66
|
"calendarLoading": "در حال بارگذاری رزروها...",
|
|
65
67
|
"calendarToday": "امروز",
|
|
66
68
|
"calendarCreateNew": "ایجاد جدید",
|
|
@@ -128,8 +130,10 @@
|
|
|
128
130
|
"filterByResource": "فیلتر بر اساس منبع",
|
|
129
131
|
"fieldOwner": "مالک",
|
|
130
132
|
"fieldResourceType": "نوع منبع",
|
|
133
|
+
"fieldResources": "منابع",
|
|
134
|
+
"fieldResourcesDesc": "منابعی که میتوانند این سرویس را ارائه دهند. برای تغییر، منبع را باز کنید و فیلد سرویسهای آن را ویرایش کنید.",
|
|
131
135
|
"fieldRequiredResources": "منابع موردنیاز",
|
|
132
|
-
"fieldRequiredResourcesDesc": "مخازن منابع اضافی که هر رزرو این خدمت آنها را اشغال میکند (مثلاً یک صندلی). رزروها بهطور خودکار برای دربرگرفتن این موارد گسترش مییابند و در صورت پر بودن هر مخزن مسدود میشوند.",
|
|
136
|
+
"fieldRequiredResourcesDesc": "مخازن منابع اضافی که هر رزرو این خدمت آنها را اشغال میکند (مثلاً یک صندلی). رزروها بهطور خودکار برای دربرگرفتن این موارد گسترش مییابند و در صورت پر بودن هر مخزن مسدود میشوند. این با منابعی که میتوانند این خدمت را ارائه دهند تفاوت دارد — منابع را در بالا ببینید.",
|
|
133
137
|
"fieldAllowGuestBooking": "رزرو مهمان",
|
|
134
138
|
"fieldAllowGuestBookingDesc": "اجازه رزرو بدون حساب مشتری. «ارثبری» از پیشفرض سطح افزونه استفاده میکند.",
|
|
135
139
|
"guestBookingInherit": "ارثبری از پیشفرض افزونه",
|
|
@@ -61,6 +61,8 @@
|
|
|
61
61
|
"errorInvalidTransition": "Impossible de passer de « {{from}} » à « {{to}} ».",
|
|
62
62
|
"errorCancellationNotice": "Les annulations nécessitent un préavis d'au moins {{period}} heures. Il ne reste que {{hours}} heures avant le rendez-vous.",
|
|
63
63
|
"errorConflict": "Ce créneau horaire est en conflit avec une réservation existante pour cette ressource.",
|
|
64
|
+
"errorServiceInactive": "Le service « {{name}} » n'est pas actif et ne peut pas être réservé.",
|
|
65
|
+
"errorResourceInactive": "La ressource « {{name}} » n'est pas active et ne peut pas être réservée.",
|
|
64
66
|
"calendarLoading": "Chargement des réservations...",
|
|
65
67
|
"calendarToday": "Aujourd'hui",
|
|
66
68
|
"calendarCreateNew": "Créer",
|
|
@@ -128,8 +130,10 @@
|
|
|
128
130
|
"filterByResource": "Filtrer par ressource",
|
|
129
131
|
"fieldOwner": "Propriétaire",
|
|
130
132
|
"fieldResourceType": "Type de ressource",
|
|
133
|
+
"fieldResources": "Ressources",
|
|
134
|
+
"fieldResourcesDesc": "Ressources pouvant réaliser ce service. Pour le modifier, ouvrez la ressource et modifiez son champ Services.",
|
|
131
135
|
"fieldRequiredResources": "Ressources requises",
|
|
132
|
-
"fieldRequiredResourcesDesc": "Pools de ressources supplémentaires occupés par chaque réservation de cette prestation (par ex. un fauteuil). Les réservations sont automatiquement étendues pour les inclure et sont bloquées si l'un des pools est complet.",
|
|
136
|
+
"fieldRequiredResourcesDesc": "Pools de ressources supplémentaires occupés par chaque réservation de cette prestation (par ex. un fauteuil). Les réservations sont automatiquement étendues pour les inclure et sont bloquées si l'un des pools est complet. Ce n'est pas la même chose que les ressources pouvant réaliser cette prestation — voir Ressources ci-dessus.",
|
|
133
137
|
"fieldAllowGuestBooking": "Réservation invité",
|
|
134
138
|
"fieldAllowGuestBookingDesc": "Autoriser les réservations sans compte client. « Hériter » utilise la valeur par défaut définie au niveau du plugin.",
|
|
135
139
|
"guestBookingInherit": "Hériter de la valeur par défaut du plugin",
|
|
@@ -61,6 +61,8 @@
|
|
|
61
61
|
"errorInvalidTransition": "\"{{from}}\" से \"{{to}}\" में नहीं बदला जा सकता।",
|
|
62
62
|
"errorCancellationNotice": "रद्दीकरण के लिए कम से कम {{period}} घंटे की सूचना आवश्यक है। अपॉइंटमेंट में केवल {{hours}} घंटे शेष हैं।",
|
|
63
63
|
"errorConflict": "यह समय स्लॉट इस संसाधन के किसी मौजूदा आरक्षण से टकराता है।",
|
|
64
|
+
"errorServiceInactive": "सेवा \"{{name}}\" सक्रिय नहीं है और बुक नहीं की जा सकती।",
|
|
65
|
+
"errorResourceInactive": "संसाधन \"{{name}}\" सक्रिय नहीं है और बुक नहीं किया जा सकता।",
|
|
64
66
|
"calendarLoading": "आरक्षण लोड हो रहे हैं...",
|
|
65
67
|
"calendarToday": "आज",
|
|
66
68
|
"calendarCreateNew": "नया बनाएँ",
|
|
@@ -128,8 +130,10 @@
|
|
|
128
130
|
"filterByResource": "संसाधन के अनुसार फ़िल्टर करें",
|
|
129
131
|
"fieldOwner": "स्वामी",
|
|
130
132
|
"fieldResourceType": "संसाधन प्रकार",
|
|
133
|
+
"fieldResources": "संसाधन",
|
|
134
|
+
"fieldResourcesDesc": "वे संसाधन जो यह सेवा कर सकते हैं। बदलने के लिए संसाधन खोलें और उसका सेवाएँ फ़ील्ड संपादित करें।",
|
|
131
135
|
"fieldRequiredResources": "आवश्यक संसाधन",
|
|
132
|
-
"fieldRequiredResourcesDesc": "अतिरिक्त संसाधन पूल जो इस सेवा की हर बुकिंग में उपयोग होते हैं (उदाहरण के लिए एक कुर्सी)। बुकिंग को इन्हें शामिल करने के लिए स्वतः विस्तारित किया जाता है और यदि कोई पूल भरा हो तो बुकिंग अवरुद्ध हो जाती है।",
|
|
136
|
+
"fieldRequiredResourcesDesc": "अतिरिक्त संसाधन पूल जो इस सेवा की हर बुकिंग में उपयोग होते हैं (उदाहरण के लिए एक कुर्सी)। बुकिंग को इन्हें शामिल करने के लिए स्वतः विस्तारित किया जाता है और यदि कोई पूल भरा हो तो बुकिंग अवरुद्ध हो जाती है। यह इससे अलग है कि कौन-से संसाधन यह सेवा कर सकते हैं — ऊपर संसाधन देखें।",
|
|
133
137
|
"fieldAllowGuestBooking": "अतिथि बुकिंग",
|
|
134
138
|
"fieldAllowGuestBookingDesc": "ग्राहक खाते के बिना बुकिंग की अनुमति दें। \"इनहेरिट\" प्लगइन-स्तरीय डिफ़ॉल्ट का उपयोग करता है।",
|
|
135
139
|
"guestBookingInherit": "प्लगइन डिफ़ॉल्ट इनहेरिट करें",
|
|
@@ -61,6 +61,8 @@
|
|
|
61
61
|
"errorInvalidTransition": "Tidak dapat berpindah dari \"{{from}}\" ke \"{{to}}\".",
|
|
62
62
|
"errorCancellationNotice": "Pembatalan memerlukan pemberitahuan minimal {{period}} jam. Hanya tersisa {{hours}} jam sebelum janji temu.",
|
|
63
63
|
"errorConflict": "Slot waktu ini berbenturan dengan reservasi yang sudah ada untuk sumber daya ini.",
|
|
64
|
+
"errorServiceInactive": "Layanan \"{{name}}\" tidak aktif dan tidak dapat dipesan.",
|
|
65
|
+
"errorResourceInactive": "Sumber daya \"{{name}}\" tidak aktif dan tidak dapat dipesan.",
|
|
64
66
|
"calendarLoading": "Memuat reservasi...",
|
|
65
67
|
"calendarToday": "Hari Ini",
|
|
66
68
|
"calendarCreateNew": "Buat Baru",
|
|
@@ -128,8 +130,10 @@
|
|
|
128
130
|
"filterByResource": "Filter berdasarkan sumber daya",
|
|
129
131
|
"fieldOwner": "Pemilik",
|
|
130
132
|
"fieldResourceType": "Tipe sumber daya",
|
|
133
|
+
"fieldResources": "Sumber Daya",
|
|
134
|
+
"fieldResourcesDesc": "Sumber daya yang dapat melakukan layanan ini. Untuk mengubahnya, buka sumber daya dan edit bidang Layanan-nya.",
|
|
131
135
|
"fieldRequiredResources": "Sumber daya yang dibutuhkan",
|
|
132
|
-
"fieldRequiredResourcesDesc": "Kumpulan sumber daya tambahan yang ditempati oleh setiap pemesanan layanan ini (mis. sebuah kursi). Pemesanan otomatis diperluas untuk menyertakan ini dan diblokir jika ada kumpulan yang penuh.",
|
|
136
|
+
"fieldRequiredResourcesDesc": "Kumpulan sumber daya tambahan yang ditempati oleh setiap pemesanan layanan ini (mis. sebuah kursi). Pemesanan otomatis diperluas untuk menyertakan ini dan diblokir jika ada kumpulan yang penuh. Ini berbeda dari sumber daya yang dapat melakukan layanan ini — lihat Sumber Daya di atas.",
|
|
133
137
|
"fieldAllowGuestBooking": "Pemesanan tamu",
|
|
134
138
|
"fieldAllowGuestBookingDesc": "Izinkan pemesanan tanpa akun pelanggan. \"Warisi\" menggunakan default tingkat plugin.",
|
|
135
139
|
"guestBookingInherit": "Warisi default plugin",
|
|
@@ -61,6 +61,8 @@
|
|
|
61
61
|
"errorInvalidTransition": "Nie można przejść ze stanu \"{{from}}\" do \"{{to}}\".",
|
|
62
62
|
"errorCancellationNotice": "Anulowanie wymaga wyprzedzenia co najmniej {{period}} godzin. Do wizyty pozostało tylko {{hours}} godzin.",
|
|
63
63
|
"errorConflict": "Ten przedział czasowy koliduje z istniejącą rezerwacją dla tego zasobu.",
|
|
64
|
+
"errorServiceInactive": "Usługa „{{name}}\" nie jest aktywna i nie można jej zarezerwować.",
|
|
65
|
+
"errorResourceInactive": "Zasób „{{name}}\" nie jest aktywny i nie można go zarezerwować.",
|
|
64
66
|
"calendarLoading": "Ładowanie rezerwacji...",
|
|
65
67
|
"calendarToday": "Dziś",
|
|
66
68
|
"calendarCreateNew": "Utwórz nową",
|
|
@@ -128,8 +130,10 @@
|
|
|
128
130
|
"filterByResource": "Filtruj według zasobu",
|
|
129
131
|
"fieldOwner": "Właściciel",
|
|
130
132
|
"fieldResourceType": "Typ zasobu",
|
|
133
|
+
"fieldResources": "Zasoby",
|
|
134
|
+
"fieldResourcesDesc": "Zasoby, które mogą wykonać tę usługę. Aby to zmienić, otwórz zasób i edytuj jego pole Usługi.",
|
|
131
135
|
"fieldRequiredResources": "Wymagane zasoby",
|
|
132
|
-
"fieldRequiredResourcesDesc": "Dodatkowe pule zasobów zajmowane przez każdą rezerwację tej usługi (np. fotel). Rezerwacje są automatycznie rozszerzane o nie i są blokowane, jeśli którakolwiek pula jest pełna.",
|
|
136
|
+
"fieldRequiredResourcesDesc": "Dodatkowe pule zasobów zajmowane przez każdą rezerwację tej usługi (np. fotel). Rezerwacje są automatycznie rozszerzane o nie i są blokowane, jeśli którakolwiek pula jest pełna. To nie to samo co zasoby, które mogą wykonać tę usługę — zobacz Zasoby powyżej.",
|
|
133
137
|
"fieldAllowGuestBooking": "Rezerwacja gościnna",
|
|
134
138
|
"fieldAllowGuestBookingDesc": "Zezwalaj na rezerwacje bez konta klienta. \"Dziedzicz\" używa domyślnego ustawienia wtyczki.",
|
|
135
139
|
"guestBookingInherit": "Dziedzicz domyślne ustawienie wtyczki",
|
|
@@ -61,6 +61,8 @@
|
|
|
61
61
|
"errorInvalidTransition": "Невозможно перейти из «{{from}}» в «{{to}}».",
|
|
62
62
|
"errorCancellationNotice": "Для отмены требуется уведомление не менее чем за {{period}} часов. До приёма осталось всего {{hours}} часов.",
|
|
63
63
|
"errorConflict": "Этот временной слот конфликтует с существующим бронированием для данного ресурса.",
|
|
64
|
+
"errorServiceInactive": "Услуга «{{name}}» неактивна и недоступна для бронирования.",
|
|
65
|
+
"errorResourceInactive": "Ресурс «{{name}}» неактивен и недоступен для бронирования.",
|
|
64
66
|
"calendarLoading": "Загрузка бронирований...",
|
|
65
67
|
"calendarToday": "Сегодня",
|
|
66
68
|
"calendarCreateNew": "Создать",
|
|
@@ -128,8 +130,10 @@
|
|
|
128
130
|
"filterByResource": "Фильтровать по ресурсу",
|
|
129
131
|
"fieldOwner": "Владелец",
|
|
130
132
|
"fieldResourceType": "Тип ресурса",
|
|
133
|
+
"fieldResources": "Ресурсы",
|
|
134
|
+
"fieldResourcesDesc": "Ресурсы, которые могут оказывать эту услугу. Чтобы изменить, откройте ресурс и отредактируйте его поле «Услуги».",
|
|
131
135
|
"fieldRequiredResources": "Необходимые ресурсы",
|
|
132
|
-
"fieldRequiredResourcesDesc": "Дополнительные пулы ресурсов, которые занимает каждое бронирование этой услуги (например, кресло). Бронирования автоматически расширяются для их включения и блокируются, если какой-либо пул заполнен.",
|
|
136
|
+
"fieldRequiredResourcesDesc": "Дополнительные пулы ресурсов, которые занимает каждое бронирование этой услуги (например, кресло). Бронирования автоматически расширяются для их включения и блокируются, если какой-либо пул заполнен. Это не то же самое, что ресурсы, которые могут оказывать эту услугу — см. «Ресурсы» выше.",
|
|
133
137
|
"fieldAllowGuestBooking": "Гостевое бронирование",
|
|
134
138
|
"fieldAllowGuestBookingDesc": "Разрешить бронирования без учётной записи клиента. «Наследовать» использует значение по умолчанию на уровне плагина.",
|
|
135
139
|
"guestBookingInherit": "Наследовать значение плагина по умолчанию",
|
|
@@ -61,6 +61,8 @@
|
|
|
61
61
|
"errorInvalidTransition": "\"{{from}}\" durumundan \"{{to}}\" durumuna geçiş yapılamaz.",
|
|
62
62
|
"errorCancellationNotice": "İptaller en az {{period}} saat önceden bildirim gerektirir. Randevuya yalnızca {{hours}} saat kaldı.",
|
|
63
63
|
"errorConflict": "Bu zaman aralığı, bu kaynak için mevcut bir rezervasyonla çakışıyor.",
|
|
64
|
+
"errorServiceInactive": "\"{{name}}\" hizmeti aktif değil ve rezerve edilemez.",
|
|
65
|
+
"errorResourceInactive": "\"{{name}}\" kaynağı aktif değil ve rezerve edilemez.",
|
|
64
66
|
"calendarLoading": "Rezervasyonlar yükleniyor...",
|
|
65
67
|
"calendarToday": "Bugün",
|
|
66
68
|
"calendarCreateNew": "Yeni Oluştur",
|
|
@@ -128,8 +130,10 @@
|
|
|
128
130
|
"filterByResource": "Kaynağa göre filtrele",
|
|
129
131
|
"fieldOwner": "Sahip",
|
|
130
132
|
"fieldResourceType": "Kaynak türü",
|
|
133
|
+
"fieldResources": "Kaynaklar",
|
|
134
|
+
"fieldResourcesDesc": "Bu hizmeti verebilecek kaynaklar. Değiştirmek için kaynağı açın ve Hizmetler alanını düzenleyin.",
|
|
131
135
|
"fieldRequiredResources": "Gerekli kaynaklar",
|
|
132
|
-
"fieldRequiredResourcesDesc": "Bu hizmetin her rezervasyonunun kullandığı ek kaynak havuzları (ör. bir koltuk). Rezervasyonlar bunları içerecek şekilde otomatik genişletilir ve herhangi bir havuz dolarsa engellenir.",
|
|
136
|
+
"fieldRequiredResourcesDesc": "Bu hizmetin her rezervasyonunun kullandığı ek kaynak havuzları (ör. bir koltuk). Rezervasyonlar bunları içerecek şekilde otomatik genişletilir ve herhangi bir havuz dolarsa engellenir. Bu, hizmeti hangi kaynakların verebileceğiyle aynı şey değildir — yukarıdaki Kaynaklar'a bakın.",
|
|
133
137
|
"fieldAllowGuestBooking": "Misafir rezervasyonu",
|
|
134
138
|
"fieldAllowGuestBookingDesc": "Müşteri hesabı olmadan rezervasyona izin ver. \"Devral\" eklenti düzeyindeki varsayılanı kullanır.",
|
|
135
139
|
"guestBookingInherit": "Eklenti varsayılanını devral",
|
|
@@ -61,6 +61,8 @@
|
|
|
61
61
|
"errorInvalidTransition": "无法从“{{from}}”转换为“{{to}}”。",
|
|
62
62
|
"errorCancellationNotice": "取消预约至少需要提前 {{period}} 小时通知。距离预约仅剩 {{hours}} 小时。",
|
|
63
63
|
"errorConflict": "此时间段与该资源的现有预约冲突。",
|
|
64
|
+
"errorServiceInactive": "服务\"{{name}}\"未启用,无法预订。",
|
|
65
|
+
"errorResourceInactive": "资源\"{{name}}\"未启用,无法预订。",
|
|
64
66
|
"calendarLoading": "正在加载预约……",
|
|
65
67
|
"calendarToday": "今天",
|
|
66
68
|
"calendarCreateNew": "新建",
|
|
@@ -128,8 +130,10 @@
|
|
|
128
130
|
"filterByResource": "按资源筛选",
|
|
129
131
|
"fieldOwner": "负责人",
|
|
130
132
|
"fieldResourceType": "资源类型",
|
|
133
|
+
"fieldResources": "资源",
|
|
134
|
+
"fieldResourcesDesc": "可以提供此服务的资源。如需更改,请打开该资源并编辑其\"服务\"字段。",
|
|
131
135
|
"fieldRequiredResources": "必需资源",
|
|
132
|
-
"fieldRequiredResourcesDesc": "
|
|
136
|
+
"fieldRequiredResourcesDesc": "此服务的每次预约都会占用的额外资源池(例如一把座椅)。预约会自动扩展以包含这些资源,若任一资源池已满则会被阻止。这与可以提供此服务的资源不是同一回事 — 请参见上方的\"资源\"。",
|
|
133
137
|
"fieldAllowGuestBooking": "访客预约",
|
|
134
138
|
"fieldAllowGuestBookingDesc": "允许无客户账户的预约。“继承”将使用插件级别的默认设置。",
|
|
135
139
|
"guestBookingInherit": "继承插件默认设置",
|
package/dist/types.d.ts
CHANGED
|
@@ -168,6 +168,12 @@ export type ReservationPluginConfig = {
|
|
|
168
168
|
defaultBufferTime?: number;
|
|
169
169
|
/** Disable the plugin entirely */
|
|
170
170
|
disabled?: boolean;
|
|
171
|
+
/**
|
|
172
|
+
* Reject bookings against an inactive service or resource, and omit them from
|
|
173
|
+
* availability. Default `true`. Set `false` to restore the previous behaviour
|
|
174
|
+
* where `active` was advisory and only filtered in the admin UI.
|
|
175
|
+
*/
|
|
176
|
+
enforceActive?: boolean;
|
|
171
177
|
/** @deprecated Use `collectionOverrides.reservations.fields` instead. Extra fields appended to Reservations. */
|
|
172
178
|
extraReservationFields?: Field[];
|
|
173
179
|
/** Resolve external busy intervals (calendar sync etc.) folded into availability + calendar display. */
|
|
@@ -193,9 +199,23 @@ export type ReservationPluginConfig = {
|
|
|
193
199
|
resourceOwnerMode?: ResourceOwnerModeConfig;
|
|
194
200
|
/** Configurable resourceType vocabulary (default: staff/equipment/room) */
|
|
195
201
|
resourceTypes?: string[];
|
|
202
|
+
/**
|
|
203
|
+
* Short-lived slot claims taken while a customer completes checkout.
|
|
204
|
+
*
|
|
205
|
+
* Opt-in: when absent, no holds collection is created and availability
|
|
206
|
+
* behaviour is byte-identical to before. Enabling it makes unexpired holds
|
|
207
|
+
* occupy their resource in every availability check.
|
|
208
|
+
*/
|
|
209
|
+
slotHolds?: {
|
|
210
|
+
enabled?: boolean;
|
|
211
|
+
/** Minutes a hold survives before it stops occupying its slot. Default 10. */
|
|
212
|
+
ttlMinutes?: number;
|
|
213
|
+
};
|
|
196
214
|
/** Override collection slugs */
|
|
197
215
|
slugs?: {
|
|
198
216
|
customers?: string;
|
|
217
|
+
/** Slug for the slot-holds collection (only created when `slotHolds.enabled`). */
|
|
218
|
+
holds?: string;
|
|
199
219
|
media?: string;
|
|
200
220
|
reservations?: string;
|
|
201
221
|
resources?: string;
|
|
@@ -232,10 +252,14 @@ export type ResolvedReservationPluginConfig = {
|
|
|
232
252
|
debug: boolean;
|
|
233
253
|
defaultBufferTime: number;
|
|
234
254
|
disabled: boolean;
|
|
255
|
+
enforceActive: boolean;
|
|
235
256
|
extraReservationFields: Field[];
|
|
236
257
|
getExternalBusy: GetExternalBusy | undefined;
|
|
237
258
|
/** Whether the media collection (`slugs.media`) exists — set by the plugin; gates the image upload fields. */
|
|
238
259
|
hasMediaCollection: boolean;
|
|
260
|
+
/** Set by the plugin once Resources is built: does it still expose a
|
|
261
|
+
* top-level `services` relationship for the Services join to target? */
|
|
262
|
+
hasResourceServicesField: boolean;
|
|
239
263
|
hooks: ReservationPluginHooks;
|
|
240
264
|
leaveTypes: string[];
|
|
241
265
|
localized: boolean;
|
|
@@ -246,8 +270,13 @@ export type ResolvedReservationPluginConfig = {
|
|
|
246
270
|
};
|
|
247
271
|
resourceOwnerMode: ResolvedResourceOwnerModeConfig | undefined;
|
|
248
272
|
resourceTypes: string[];
|
|
273
|
+
slotHolds: {
|
|
274
|
+
enabled: boolean;
|
|
275
|
+
ttlMinutes: number;
|
|
276
|
+
};
|
|
249
277
|
slugs: {
|
|
250
278
|
customers: string;
|
|
279
|
+
holds: string;
|
|
251
280
|
media: string;
|
|
252
281
|
reservations: string;
|
|
253
282
|
resources: string;
|
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 /** 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\n/** One external busy interval returned by `getExternalBusy` (ISO date strings). */\nexport type ExternalBusyInterval = { end: string; label?: string; start: string }\n\n/**\n * App-supplied resolver mapping an external source (e.g. Google/Outlook sync\n * tables) to busy intervals for one resource over [start, end). Wired into\n * checkAvailability (bookings overlapping an interval are unavailable — the\n * interval blocks the WHOLE resource, all units) and into the\n * resource-availability endpoint (returned as `external` for distinct\n * rendering). The plugin calls it inside try/catch and treats an error as []\n * (fail-open): a calendar/sync failure must never block a real booking.\n * Called once per candidate window during slot computation (N calls per\n * request) — keep it cheap: read a local sync table or a per-request cache,\n * never a remote API directly.\n */\nexport type GetExternalBusy = (args: {\n end: Date\n req: PayloadRequest\n resourceId: number | string\n start: Date\n}) => Promise<ExternalBusyInterval[]>\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 /** Emit info-level `reserve_debug` traces for slot generation and conflict detection (default false) */\n debug?: boolean\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 /** Resolve external busy intervals (calendar sync etc.) folded into availability + calendar display. */\n getExternalBusy?: GetExternalBusy\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 debug: boolean\n defaultBufferTime: number\n disabled: boolean\n extraReservationFields: Field[]\n getExternalBusy: GetExternalBusy | undefined\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;AAkRD,+DAA+D,GAC/D,OAAO,MAAMC,2BACXZ,uBAAuBO,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\n/** One external busy interval returned by `getExternalBusy` (ISO date strings). */\nexport type ExternalBusyInterval = { end: string; label?: string; start: string }\n\n/**\n * App-supplied resolver mapping an external source (e.g. Google/Outlook sync\n * tables) to busy intervals for one resource over [start, end). Wired into\n * checkAvailability (bookings overlapping an interval are unavailable — the\n * interval blocks the WHOLE resource, all units) and into the\n * resource-availability endpoint (returned as `external` for distinct\n * rendering). The plugin calls it inside try/catch and treats an error as []\n * (fail-open): a calendar/sync failure must never block a real booking.\n * Called once per candidate window during slot computation (N calls per\n * request) — keep it cheap: read a local sync table or a per-request cache,\n * never a remote API directly.\n */\nexport type GetExternalBusy = (args: {\n end: Date\n req: PayloadRequest\n resourceId: number | string\n start: Date\n}) => Promise<ExternalBusyInterval[]>\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 /** Emit info-level `reserve_debug` traces for slot generation and conflict detection (default false) */\n debug?: boolean\n /** Default buffer time in minutes between reservations */\n defaultBufferTime?: number\n /** Disable the plugin entirely */\n disabled?: boolean\n /**\n * Reject bookings against an inactive service or resource, and omit them from\n * availability. Default `true`. Set `false` to restore the previous behaviour\n * where `active` was advisory and only filtered in the admin UI.\n */\n enforceActive?: boolean\n /** @deprecated Use `collectionOverrides.reservations.fields` instead. Extra fields appended to Reservations. */\n extraReservationFields?: Field[]\n /** Resolve external busy intervals (calendar sync etc.) folded into availability + calendar display. */\n getExternalBusy?: GetExternalBusy\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 /**\n * Short-lived slot claims taken while a customer completes checkout.\n *\n * Opt-in: when absent, no holds collection is created and availability\n * behaviour is byte-identical to before. Enabling it makes unexpired holds\n * occupy their resource in every availability check.\n */\n slotHolds?: {\n enabled?: boolean\n /** Minutes a hold survives before it stops occupying its slot. Default 10. */\n ttlMinutes?: number\n }\n /** Override collection slugs */\n slugs?: {\n customers?: string\n /** Slug for the slot-holds collection (only created when `slotHolds.enabled`). */\n holds?: 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 debug: boolean\n defaultBufferTime: number\n disabled: boolean\n enforceActive: boolean\n extraReservationFields: Field[]\n getExternalBusy: GetExternalBusy | undefined\n /** Whether the media collection (`slugs.media`) exists — set by the plugin; gates the image upload fields. */\n hasMediaCollection: boolean\n /** Set by the plugin once Resources is built: does it still expose a\n * top-level `services` relationship for the Services join to target? */\n hasResourceServicesField: 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 slotHolds: {\n enabled: boolean\n ttlMinutes: number\n }\n slugs: {\n customers: string\n holds: 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;AA+SD,+DAA+D,GAC/D,OAAO,MAAMC,2BACXZ,uBAAuBO,WAAW,CAAkD"}
|