payload-reserve 2.4.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +217 -4
  2. package/dist/collections/Holds.d.ts +18 -0
  3. package/dist/collections/Holds.js +113 -0
  4. package/dist/collections/Holds.js.map +1 -0
  5. package/dist/collections/Reservations.js +8 -0
  6. package/dist/collections/Reservations.js.map +1 -1
  7. package/dist/collections/Resources.js +35 -1
  8. package/dist/collections/Resources.js.map +1 -1
  9. package/dist/collections/Services.js +41 -0
  10. package/dist/collections/Services.js.map +1 -1
  11. package/dist/components/CalendarView/LaneTimelineView.js +4 -5
  12. package/dist/components/CalendarView/LaneTimelineView.js.map +1 -1
  13. package/dist/components/CalendarView/index.js +55 -71
  14. package/dist/components/CalendarView/index.js.map +1 -1
  15. package/dist/components/DashboardWidget/DashboardWidgetServer.js +11 -74
  16. package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
  17. package/dist/components/DashboardWidget/fetchDashboardStats.d.ts +42 -0
  18. package/dist/components/DashboardWidget/fetchDashboardStats.js +113 -0
  19. package/dist/components/DashboardWidget/fetchDashboardStats.js.map +1 -0
  20. package/dist/defaults.d.ts +2 -0
  21. package/dist/defaults.js +11 -0
  22. package/dist/defaults.js.map +1 -1
  23. package/dist/endpoints/cancelBooking.js +43 -14
  24. package/dist/endpoints/cancelBooking.js.map +1 -1
  25. package/dist/endpoints/checkAvailability.js +12 -1
  26. package/dist/endpoints/checkAvailability.js.map +1 -1
  27. package/dist/endpoints/createBooking.js +108 -7
  28. package/dist/endpoints/createBooking.js.map +1 -1
  29. package/dist/endpoints/customerSearch.js +6 -0
  30. package/dist/endpoints/customerSearch.js.map +1 -1
  31. package/dist/endpoints/effectiveTimezone.js +1 -0
  32. package/dist/endpoints/effectiveTimezone.js.map +1 -1
  33. package/dist/endpoints/getSlots.js +12 -1
  34. package/dist/endpoints/getSlots.js.map +1 -1
  35. package/dist/endpoints/holdSlot.d.ts +12 -0
  36. package/dist/endpoints/holdSlot.js +126 -0
  37. package/dist/endpoints/holdSlot.js.map +1 -0
  38. package/dist/endpoints/releaseSlot.d.ts +8 -0
  39. package/dist/endpoints/releaseSlot.js +34 -0
  40. package/dist/endpoints/releaseSlot.js.map +1 -0
  41. package/dist/endpoints/resourceAvailability.d.ts +2 -0
  42. package/dist/endpoints/resourceAvailability.js +79 -3
  43. package/dist/endpoints/resourceAvailability.js.map +1 -1
  44. package/dist/hooks/holds/validateHoldSlot.d.ts +14 -0
  45. package/dist/hooks/holds/validateHoldSlot.js +90 -0
  46. package/dist/hooks/holds/validateHoldSlot.js.map +1 -0
  47. package/dist/hooks/reservations/acquireBookingLock.d.ts +30 -0
  48. package/dist/hooks/reservations/acquireBookingLock.js +71 -0
  49. package/dist/hooks/reservations/acquireBookingLock.js.map +1 -0
  50. package/dist/hooks/reservations/calculateEndTime.js +13 -1
  51. package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
  52. package/dist/hooks/reservations/expandRequiredResources.js +3 -0
  53. package/dist/hooks/reservations/expandRequiredResources.js.map +1 -1
  54. package/dist/hooks/reservations/validateActive.d.ts +11 -0
  55. package/dist/hooks/reservations/validateActive.js +103 -0
  56. package/dist/hooks/reservations/validateActive.js.map +1 -0
  57. package/dist/hooks/reservations/validateConflicts.js +7 -1
  58. package/dist/hooks/reservations/validateConflicts.js.map +1 -1
  59. package/dist/hooks/reservations/validateGuestBooking.js +3 -0
  60. package/dist/hooks/reservations/validateGuestBooking.js.map +1 -1
  61. package/dist/hooks/shared/preventDeleteWhenReferenced.d.ts +42 -0
  62. package/dist/hooks/shared/preventDeleteWhenReferenced.js +87 -0
  63. package/dist/hooks/shared/preventDeleteWhenReferenced.js.map +1 -0
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.js.map +1 -1
  66. package/dist/plugin.js +179 -3
  67. package/dist/plugin.js.map +1 -1
  68. package/dist/services/AvailabilityService.d.ts +46 -5
  69. package/dist/services/AvailabilityService.js +168 -12
  70. package/dist/services/AvailabilityService.js.map +1 -1
  71. package/dist/services/HoldService.d.ts +53 -0
  72. package/dist/services/HoldService.js +169 -0
  73. package/dist/services/HoldService.js.map +1 -0
  74. package/dist/services/index.d.ts +1 -0
  75. package/dist/services/index.js.map +1 -1
  76. package/dist/translations/ar.json +5 -1
  77. package/dist/translations/de.json +5 -1
  78. package/dist/translations/en.json +5 -1
  79. package/dist/translations/es.json +5 -1
  80. package/dist/translations/fa.json +5 -1
  81. package/dist/translations/fr.json +5 -1
  82. package/dist/translations/hi.json +5 -1
  83. package/dist/translations/id.json +5 -1
  84. package/dist/translations/index.js +36 -12
  85. package/dist/translations/index.js.map +1 -1
  86. package/dist/translations/pl.json +5 -1
  87. package/dist/translations/ru.json +5 -1
  88. package/dist/translations/tr.json +5 -1
  89. package/dist/translations/zh.json +5 -1
  90. package/dist/types.d.ts +29 -0
  91. package/dist/types.js.map +1 -1
  92. package/dist/utilities/calendarGrid.d.ts +44 -0
  93. package/dist/utilities/calendarGrid.js +67 -0
  94. package/dist/utilities/calendarGrid.js.map +1 -0
  95. package/dist/utilities/reservationChanges.d.ts +6 -1
  96. package/dist/utilities/reservationChanges.js +1 -1
  97. package/dist/utilities/reservationChanges.js.map +1 -1
  98. package/dist/utilities/resolveReservationItems.d.ts +13 -2
  99. package/dist/utilities/resolveReservationItems.js +87 -2
  100. package/dist/utilities/resolveReservationItems.js.map +1 -1
  101. package/dist/utilities/retryOnWriteConflict.d.ts +66 -0
  102. package/dist/utilities/retryOnWriteConflict.js +135 -0
  103. package/dist/utilities/retryOnWriteConflict.js.map +1 -0
  104. package/dist/utilities/tenantTimezone.d.ts +77 -1
  105. package/dist/utilities/tenantTimezone.js +163 -2
  106. package/dist/utilities/tenantTimezone.js.map +1 -1
  107. package/dist/utilities/transactionSupport.d.ts +13 -0
  108. package/dist/utilities/transactionSupport.js +30 -0
  109. package/dist/utilities/transactionSupport.js.map +1 -0
  110. 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":"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"}
@@ -1 +1,2 @@
1
1
  export { buildOverlapQuery, checkAvailability, computeEndTime, getAvailableSlots, isBlockingStatus, validateTransition, } from './AvailabilityService.js';
2
+ export type { EmptyReason } from './AvailabilityService.js';
@@ -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": "Additional resource pools every booking of this service occupies (e.g. a chair). Bookings are auto-expanded to include these and are blocked if any pool is full.",
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",
@@ -1,15 +1,39 @@
1
- import ar from './ar.json';
2
- import de from './de.json';
3
- import en from './en.json';
4
- import es from './es.json';
5
- import fa from './fa.json';
6
- import fr from './fr.json';
7
- import hi from './hi.json';
8
- import id from './id.json';
9
- import pl from './pl.json';
10
- import ru from './ru.json';
11
- import tr from './tr.json';
12
- import zh from './zh.json';
1
+ import ar from './ar.json' with {
2
+ type: 'json'
3
+ };
4
+ import de from './de.json' with {
5
+ type: 'json'
6
+ };
7
+ import en from './en.json' with {
8
+ type: 'json'
9
+ };
10
+ import es from './es.json' with {
11
+ type: 'json'
12
+ };
13
+ import fa from './fa.json' with {
14
+ type: 'json'
15
+ };
16
+ import fr from './fr.json' with {
17
+ type: 'json'
18
+ };
19
+ import hi from './hi.json' with {
20
+ type: 'json'
21
+ };
22
+ import id from './id.json' with {
23
+ type: 'json'
24
+ };
25
+ import pl from './pl.json' with {
26
+ type: 'json'
27
+ };
28
+ import ru from './ru.json' with {
29
+ type: 'json'
30
+ };
31
+ import tr from './tr.json' with {
32
+ type: 'json'
33
+ };
34
+ import zh from './zh.json' with {
35
+ type: 'json'
36
+ };
13
37
  export const translations = {
14
38
  id: {
15
39
  reservation: id
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/translations/index.ts"],"sourcesContent":["import ar from './ar.json' with { type: 'json' }\nimport de from './de.json' with { type: 'json' }\nimport en from './en.json' with { type: 'json' }\nimport es from './es.json' with { type: 'json' }\nimport fa from './fa.json' with { type: 'json' }\nimport fr from './fr.json' with { type: 'json' }\nimport hi from './hi.json' with { type: 'json' }\nimport id from './id.json' with { type: 'json' }\nimport pl from './pl.json' with { type: 'json' }\nimport ru from './ru.json' with { type: 'json' }\nimport tr from './tr.json' with { type: 'json' }\nimport zh from './zh.json' with { type: 'json' }\n\nexport const translations: Record<string, Record<string, unknown>> = {\n id: { reservation: id },\n ar: { reservation: ar },\n de: { reservation: de },\n en: { reservation: en },\n es: { reservation: es },\n fa: { reservation: fa },\n fr: { reservation: fr },\n hi: { reservation: hi },\n pl: { reservation: pl },\n ru: { reservation: ru },\n tr: { reservation: tr },\n zh: { reservation: zh },\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":["ar","de","en","es","fa","fr","hi","id","pl","ru","tr","zh","translations","reservation"],"mappings":"AAAA,OAAOA,QAAQ,YAAiC;AAChD,OAAOC,QAAQ,YAAiC;AAChD,OAAOC,QAAQ,YAAiC;AAChD,OAAOC,QAAQ,YAAiC;AAChD,OAAOC,QAAQ,YAAiC;AAChD,OAAOC,QAAQ,YAAiC;AAChD,OAAOC,QAAQ,YAAiC;AAChD,OAAOC,QAAQ,YAAiC;AAChD,OAAOC,QAAQ,YAAiC;AAChD,OAAOC,QAAQ,YAAiC;AAChD,OAAOC,QAAQ,YAAiC;AAChD,OAAOC,QAAQ,YAAiC;AAEhD,OAAO,MAAMC,eAAwD;IACnEL,IAAI;QAAEM,aAAaN;IAAG;IACtBP,IAAI;QAAEa,aAAab;IAAG;IACtBC,IAAI;QAAEY,aAAaZ;IAAG;IACtBC,IAAI;QAAEW,aAAaX;IAAG;IACtBC,IAAI;QAAEU,aAAaV;IAAG;IACtBC,IAAI;QAAES,aAAaT;IAAG;IACtBC,IAAI;QAAEQ,aAAaR;IAAG;IACtBC,IAAI;QAAEO,aAAaP;IAAG;IACtBE,IAAI;QAAEK,aAAaL;IAAG;IACtBC,IAAI;QAAEI,aAAaJ;IAAG;IACtBC,IAAI;QAAEG,aAAaH;IAAG;IACtBC,IAAI;QAAEE,aAAaF;IAAG;AACxB,EAAC"}
1
+ {"version":3,"sources":["../../src/translations/index.ts"],"sourcesContent":["import ar from './ar.json' with { type: 'json' }\nimport de from './de.json' with { type: 'json' }\nimport en from './en.json' with { type: 'json' }\nimport es from './es.json' with { type: 'json' }\nimport fa from './fa.json' with { type: 'json' }\nimport fr from './fr.json' with { type: 'json' }\nimport hi from './hi.json' with { type: 'json' }\nimport id from './id.json' with { type: 'json' }\nimport pl from './pl.json' with { type: 'json' }\nimport ru from './ru.json' with { type: 'json' }\nimport tr from './tr.json' with { type: 'json' }\nimport zh from './zh.json' with { type: 'json' }\n\nexport const translations: Record<string, Record<string, unknown>> = {\n id: { reservation: id },\n ar: { reservation: ar },\n de: { reservation: de },\n en: { reservation: en },\n es: { reservation: es },\n fa: { reservation: fa },\n fr: { reservation: fr },\n hi: { reservation: hi },\n pl: { reservation: pl },\n ru: { reservation: ru },\n tr: { reservation: tr },\n zh: { reservation: zh },\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":["ar","type","de","en","es","fa","fr","hi","id","pl","ru","tr","zh","translations","reservation"],"mappings":"AAAA,OAAOA,QAAQ,iBAAiB;IAAEC,MAAM;AAAO,EAAC;AAChD,OAAOC,QAAQ,iBAAiB;IAAED,MAAM;AAAO,EAAC;AAChD,OAAOE,QAAQ,iBAAiB;IAAEF,MAAM;AAAO,EAAC;AAChD,OAAOG,QAAQ,iBAAiB;IAAEH,MAAM;AAAO,EAAC;AAChD,OAAOI,QAAQ,iBAAiB;IAAEJ,MAAM;AAAO,EAAC;AAChD,OAAOK,QAAQ,iBAAiB;IAAEL,MAAM;AAAO,EAAC;AAChD,OAAOM,QAAQ,iBAAiB;IAAEN,MAAM;AAAO,EAAC;AAChD,OAAOO,QAAQ,iBAAiB;IAAEP,MAAM;AAAO,EAAC;AAChD,OAAOQ,QAAQ,iBAAiB;IAAER,MAAM;AAAO,EAAC;AAChD,OAAOS,QAAQ,iBAAiB;IAAET,MAAM;AAAO,EAAC;AAChD,OAAOU,QAAQ,iBAAiB;IAAEV,MAAM;AAAO,EAAC;AAChD,OAAOW,QAAQ,iBAAiB;IAAEX,MAAM;AAAO,EAAC;AAEhD,OAAO,MAAMY,eAAwD;IACnEL,IAAI;QAAEM,aAAaN;IAAG;IACtBR,IAAI;QAAEc,aAAad;IAAG;IACtBE,IAAI;QAAEY,aAAaZ;IAAG;IACtBC,IAAI;QAAEW,aAAaX;IAAG;IACtBC,IAAI;QAAEU,aAAaV;IAAG;IACtBC,IAAI;QAAES,aAAaT;IAAG;IACtBC,IAAI;QAAEQ,aAAaR;IAAG;IACtBC,IAAI;QAAEO,aAAaP;IAAG;IACtBE,IAAI;QAAEK,aAAaL;IAAG;IACtBC,IAAI;QAAEI,aAAaJ;IAAG;IACtBC,IAAI;QAAEG,aAAaH;IAAG;IACtBC,IAAI;QAAEE,aAAaF;IAAG;AACxB,EAAC"}
@@ -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;