payload-reserve 2.1.1 → 2.2.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 +58 -1
- package/dist/components/CalendarView/CalendarView.module.css +11 -0
- package/dist/components/CalendarView/LaneTimelineView.js +3 -1
- package/dist/components/CalendarView/LaneTimelineView.js.map +1 -1
- package/dist/components/CalendarView/index.js +8 -0
- package/dist/components/CalendarView/index.js.map +1 -1
- package/dist/defaults.js +1 -0
- package/dist/defaults.js.map +1 -1
- package/dist/endpoints/checkAvailability.js +1 -0
- package/dist/endpoints/checkAvailability.js.map +1 -1
- package/dist/endpoints/getSlots.js +1 -0
- package/dist/endpoints/getSlots.js.map +1 -1
- package/dist/endpoints/resourceAvailability.d.ts +6 -2
- package/dist/endpoints/resourceAvailability.js +19 -1
- package/dist/endpoints/resourceAvailability.js.map +1 -1
- package/dist/hooks/reservations/validateConflicts.js +1 -0
- package/dist/hooks/reservations/validateConflicts.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/services/AvailabilityService.d.ts +9 -2
- package/dist/services/AvailabilityService.js +26 -3
- package/dist/services/AvailabilityService.js.map +1 -1
- package/dist/translations/ar.json +1 -0
- package/dist/translations/de.json +1 -0
- package/dist/translations/en.json +1 -0
- package/dist/translations/es.json +1 -0
- package/dist/translations/fa.json +1 -0
- package/dist/translations/fr.json +1 -0
- package/dist/translations/hi.json +1 -0
- package/dist/translations/id.json +1 -0
- package/dist/translations/pl.json +1 -0
- package/dist/translations/ru.json +1 -0
- package/dist/translations/tr.json +1 -0
- package/dist/translations/zh.json +1 -0
- package/dist/types.d.ts +27 -0
- package/dist/types.js.map +1 -1
- package/dist/utilities/computeSlotStates.d.ts +2 -1
- package/dist/utilities/computeSlotStates.js +5 -1
- package/dist/utilities/computeSlotStates.js.map +1 -1
- package/package.json +1 -1
|
@@ -150,6 +150,7 @@
|
|
|
150
150
|
"pickLoading": "Ładowanie dostępnych terminów…",
|
|
151
151
|
"pickNone": "Brak dostępnych terminów na ten dzień.",
|
|
152
152
|
"laneNoResources": "Brak aktywnych zasobów do wyświetlenia.",
|
|
153
|
+
"slotExternal": "Wydarzenie zewnętrzne",
|
|
153
154
|
"slotFree": "Wolny",
|
|
154
155
|
"slotFull": "Pełny",
|
|
155
156
|
"slotOffShift": "Poza zmianą",
|
|
@@ -150,6 +150,7 @@
|
|
|
150
150
|
"pickLoading": "Загрузка доступного времени…",
|
|
151
151
|
"pickNone": "Нет доступного времени на этот день.",
|
|
152
152
|
"laneNoResources": "Нет активных ресурсов для отображения.",
|
|
153
|
+
"slotExternal": "Внешнее событие",
|
|
153
154
|
"slotFree": "Свободно",
|
|
154
155
|
"slotFull": "Занято",
|
|
155
156
|
"slotOffShift": "Не в смене",
|
|
@@ -150,6 +150,7 @@
|
|
|
150
150
|
"pickLoading": "Müsait saatler yükleniyor…",
|
|
151
151
|
"pickNone": "Bu gün için müsait saat yok.",
|
|
152
152
|
"laneNoResources": "Gösterilecek aktif kaynak yok.",
|
|
153
|
+
"slotExternal": "Harici etkinlik",
|
|
153
154
|
"slotFree": "Boş",
|
|
154
155
|
"slotFull": "Dolu",
|
|
155
156
|
"slotOffShift": "Mesai dışı",
|
package/dist/types.d.ts
CHANGED
|
@@ -115,6 +115,30 @@ export type CollectionOverride = {
|
|
|
115
115
|
defaultFields: Field[];
|
|
116
116
|
}) => Field[];
|
|
117
117
|
} & Omit<Partial<CollectionConfig>, 'fields' | 'slug'>;
|
|
118
|
+
/** One external busy interval returned by `getExternalBusy` (ISO date strings). */
|
|
119
|
+
export type ExternalBusyInterval = {
|
|
120
|
+
end: string;
|
|
121
|
+
label?: string;
|
|
122
|
+
start: string;
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* App-supplied resolver mapping an external source (e.g. Google/Outlook sync
|
|
126
|
+
* tables) to busy intervals for one resource over [start, end). Wired into
|
|
127
|
+
* checkAvailability (bookings overlapping an interval are unavailable — the
|
|
128
|
+
* interval blocks the WHOLE resource, all units) and into the
|
|
129
|
+
* resource-availability endpoint (returned as `external` for distinct
|
|
130
|
+
* rendering). The plugin calls it inside try/catch and treats an error as []
|
|
131
|
+
* (fail-open): a calendar/sync failure must never block a real booking.
|
|
132
|
+
* Called once per candidate window during slot computation (N calls per
|
|
133
|
+
* request) — keep it cheap: read a local sync table or a per-request cache,
|
|
134
|
+
* never a remote API directly.
|
|
135
|
+
*/
|
|
136
|
+
export type GetExternalBusy = (args: {
|
|
137
|
+
end: Date;
|
|
138
|
+
req: PayloadRequest;
|
|
139
|
+
resourceId: number | string;
|
|
140
|
+
start: Date;
|
|
141
|
+
}) => Promise<ExternalBusyInterval[]>;
|
|
118
142
|
export type ReservationPluginConfig = {
|
|
119
143
|
/** Override access control per collection */
|
|
120
144
|
access?: {
|
|
@@ -144,6 +168,8 @@ export type ReservationPluginConfig = {
|
|
|
144
168
|
disabled?: boolean;
|
|
145
169
|
/** @deprecated Use `collectionOverrides.reservations.fields` instead. Extra fields appended to Reservations. */
|
|
146
170
|
extraReservationFields?: Field[];
|
|
171
|
+
/** Resolve external busy intervals (calendar sync etc.) folded into availability + calendar display. */
|
|
172
|
+
getExternalBusy?: GetExternalBusy;
|
|
147
173
|
/** Plugin hooks for external integrations */
|
|
148
174
|
hooks?: ReservationPluginHooks;
|
|
149
175
|
/** Configurable leave/exception type vocabulary (default: vacation/sick/personal/closure/other) */
|
|
@@ -204,6 +230,7 @@ export type ResolvedReservationPluginConfig = {
|
|
|
204
230
|
defaultBufferTime: number;
|
|
205
231
|
disabled: boolean;
|
|
206
232
|
extraReservationFields: Field[];
|
|
233
|
+
getExternalBusy: GetExternalBusy | undefined;
|
|
207
234
|
/** Whether the media collection (`slugs.media`) exists — set by the plugin; gates the image upload fields. */
|
|
208
235
|
hasMediaCollection: boolean;
|
|
209
236
|
hooks: ReservationPluginHooks;
|
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\nexport type ReservationPluginConfig = {\n /** Override access control per collection */\n access?: {\n customers?: CollectionConfig['access']\n reservations?: CollectionConfig['access']\n resources?: CollectionConfig['access']\n schedules?: CollectionConfig['access']\n services?: CollectionConfig['access']\n }\n /** Admin group name for all reservation collections */\n adminGroup?: string\n /** Allow bookings without a customer account by default (per-service override available) */\n allowGuestBooking?: boolean\n /** Hours of notice required before cancellation */\n cancellationNoticePeriod?: number\n /** Per-collection overrides applied to the generated collections (issue #4) */\n collectionOverrides?: {\n customers?: CollectionOverride\n reservations?: CollectionOverride\n resources?: CollectionOverride\n schedules?: CollectionOverride\n services?: CollectionOverride\n }\n /** Default buffer time in minutes between reservations */\n defaultBufferTime?: number\n /** Disable the plugin entirely */\n disabled?: boolean\n /** @deprecated Use `collectionOverrides.reservations.fields` instead. Extra fields appended to Reservations. */\n extraReservationFields?: Field[]\n /** Plugin hooks for external integrations */\n hooks?: ReservationPluginHooks\n /** Configurable leave/exception type vocabulary (default: vacation/sick/personal/closure/other) */\n leaveTypes?: string[]\n /** Tenant scoping for the custom admin views (calendar, availability, dashboard). Applied only when the scoped collection has the tenant field AND the tenant cookie is set. */\n multiTenant?: {\n /** Cookie written by the tenant-selector (default 'payload-tenant'). */\n cookieName?: string\n /** Tenant field name on scoped collections (default 'tenant'). */\n tenantField?: string\n /**\n * Field on the tenant document holding its IANA timezone (default 'timezone').\n * When set and the selected tenant has a valid value, the admin views resolve\n * day-boundaries in that tenant's zone instead of the global `timezone`.\n */\n timezoneField?: string\n }\n /** Enable resource-owner multi-tenancy (opt-in) */\n resourceOwnerMode?: ResourceOwnerModeConfig\n /** Configurable resourceType vocabulary (default: staff/equipment/room) */\n resourceTypes?: string[]\n /** Override collection slugs */\n slugs?: {\n customers?: string\n media?: string\n reservations?: string\n resources?: string\n schedules?: string\n services?: string\n }\n /** Auto-provision a Resource from staff-role users (opt-in; requires resourceOwnerMode) */\n staffProvisioning?: StaffProvisioningConfig\n /** Configurable status machine (defaults to current behavior) */\n statusMachine?: Partial<StatusMachineConfig>\n /** IANA business timezone governing schedules and day boundaries (default 'UTC') */\n timezone?: string\n /** Which existing auth collection to extend with customer fields */\n userCollection?: string\n}\n\nexport type ResolvedReservationPluginConfig = {\n access: {\n customers?: CollectionConfig['access']\n reservations?: CollectionConfig['access']\n resources?: CollectionConfig['access']\n schedules?: CollectionConfig['access']\n services?: CollectionConfig['access']\n }\n adminGroup: string\n allowGuestBooking: boolean\n cancellationNoticePeriod: number\n collectionOverrides: {\n customers?: CollectionOverride\n reservations?: CollectionOverride\n resources?: CollectionOverride\n schedules?: CollectionOverride\n services?: CollectionOverride\n }\n defaultBufferTime: number\n disabled: boolean\n extraReservationFields: Field[]\n /** Whether the media collection (`slugs.media`) exists — set by the plugin; gates the image upload fields. */\n hasMediaCollection: boolean\n hooks: ReservationPluginHooks\n leaveTypes: string[]\n localized: boolean\n multiTenant: {\n cookieName: string\n tenantField: string\n timezoneField: string\n }\n resourceOwnerMode: ResolvedResourceOwnerModeConfig | undefined\n resourceTypes: string[]\n slugs: {\n customers: string\n media: string\n reservations: string\n resources: string\n schedules: string\n services: string\n }\n staffProvisioning: ResolvedStaffProvisioningConfig | undefined\n statusMachine: StatusMachineConfig\n timezone: string\n userCollection: string | undefined\n}\n\nexport type ReservationStatus = 'cancelled' | 'completed' | 'confirmed' | 'no-show' | 'pending'\n\nexport type DayOfWeek = 'fri' | 'mon' | 'sat' | 'sun' | 'thu' | 'tue' | 'wed'\n\nexport type ScheduleType = 'manual' | 'recurring'\n\n/** @deprecated Use DEFAULT_STATUS_MACHINE.transitions instead */\nexport const VALID_STATUS_TRANSITIONS: Record<ReservationStatus, ReservationStatus[]> =\n DEFAULT_STATUS_MACHINE.transitions as Record<ReservationStatus, ReservationStatus[]>\n"],"names":["DEFAULT_STATUS_MACHINE","blockingStatuses","cancelStatus","confirmStatus","defaultStatus","statuses","terminalStatuses","transitions","cancelled","completed","confirmed","pending","VALID_STATUS_TRANSITIONS"],"mappings":"AAsBA,OAAO,MAAMA,yBAA8C;IACzDC,kBAAkB;QAAC;QAAW;KAAY;IAC1CC,cAAc;IACdC,eAAe;IACfC,eAAe;IACfC,UAAU;QAAC;QAAW;QAAa;QAAa;QAAa;KAAU;IACvEC,kBAAkB;QAAC;QAAa;QAAa;KAAU;IACvDC,aAAa;QACXC,WAAW,EAAE;QACbC,WAAW,EAAE;QACbC,WAAW;YAAC;YAAa;YAAa;SAAU;QAChD,WAAW,EAAE;QACbC,SAAS;YAAC;YAAa;SAAY;IACrC;AACF,EAAC;AAsPD,+DAA+D,GAC/D,OAAO,MAAMC,2BACXZ,uBAAuBO,WAAW,CAAkD"}
|
|
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 /** 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 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;AA+QD,+DAA+D,GAC/D,OAAO,MAAMC,2BACXZ,uBAAuBO,WAAW,CAAkD"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type SlotState = 'free' | 'full' | 'off-shift' | 'time-off';
|
|
1
|
+
export type SlotState = 'external' | 'free' | 'full' | 'off-shift' | 'time-off';
|
|
2
2
|
export type SlotInfo = {
|
|
3
3
|
end: Date;
|
|
4
4
|
occupancy: number;
|
|
@@ -30,6 +30,7 @@ export declare function computeSlotStates(params: {
|
|
|
30
30
|
capacityMode: 'per-guest' | 'per-reservation';
|
|
31
31
|
dayEnd: Date;
|
|
32
32
|
dayStart: Date;
|
|
33
|
+
external?: Interval[];
|
|
33
34
|
quantity: number;
|
|
34
35
|
requiredPools?: RequiredPool[];
|
|
35
36
|
shiftWindows: Interval[];
|
|
@@ -16,7 +16,7 @@ const within = (slotStart, slotEnd, windows)=>windows.some((w)=>doRangesOverlap(
|
|
|
16
16
|
* capacity — so the grid reflects true bookability, not just the stylist.
|
|
17
17
|
* Pure — no DB, no clock.
|
|
18
18
|
*/ export function computeSlotStates(params) {
|
|
19
|
-
const { busy, dayEnd, dayStart, quantity, requiredPools, shiftWindows, step, timeOff } = params;
|
|
19
|
+
const { busy, dayEnd, dayStart, external, quantity, requiredPools, shiftWindows, step, timeOff } = params;
|
|
20
20
|
const pools = requiredPools ?? [];
|
|
21
21
|
const slots = [];
|
|
22
22
|
let cursor = new Date(dayStart);
|
|
@@ -32,6 +32,10 @@ const within = (slotStart, slotEnd, windows)=>windows.some((w)=>doRangesOverlap(
|
|
|
32
32
|
state = 'time-off';
|
|
33
33
|
} else if (occupancy >= quantity || poolFull) {
|
|
34
34
|
state = 'full';
|
|
35
|
+
} else if (within(slotStart, slotEnd, external ?? [])) {
|
|
36
|
+
// External (synced calendar) busy — after `full` so a real booking's
|
|
37
|
+
// state wins when both overlap; enforcement already blocks either way.
|
|
38
|
+
state = 'external';
|
|
35
39
|
} else {
|
|
36
40
|
state = 'free';
|
|
37
41
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utilities/computeSlotStates.ts"],"sourcesContent":["import { addMinutes, doRangesOverlap } from './slotUtils.js'\n\nexport type SlotState = 'free' | 'full' | 'off-shift' | 'time-off'\n\nexport type SlotInfo = {\n end: Date\n occupancy: number\n start: Date\n state: SlotState\n}\n\ntype Busy = Array<{ end: string; start: string; units: number }>\ntype Interval = { end: string; start: string }\ntype RequiredPool = { busy: Busy; quantity: number }\n\nconst within = (slotStart: Date, slotEnd: Date, windows: Interval[]): boolean =>\n windows.some((w) => doRangesOverlap(slotStart, slotEnd, new Date(w.start), new Date(w.end)))\n\n/** Sum the `units` of busy intervals overlapping [slotStart, slotEnd). */\nconst occupancyAt = (slotStart: Date, slotEnd: Date, busy: Busy): number => {\n let occ = 0\n for (const b of busy) {\n if (doRangesOverlap(slotStart, slotEnd, new Date(b.start), new Date(b.end))) {\n occ += b.units\n }\n }\n return occ\n}\n\n/**\n * Classify each step-sized slot in [dayStart, dayEnd) for a single resource.\n * A slot is `full` when the resource itself is at capacity OR any of its\n * `requiredPools` (e.g. a shared chair pool a service also needs) is at\n * capacity — so the grid reflects true bookability, not just the stylist.\n * Pure — no DB, no clock.\n */\nexport function computeSlotStates(params: {\n busy: Busy\n capacityMode: 'per-guest' | 'per-reservation'\n dayEnd: Date\n dayStart: Date\n quantity: number\n requiredPools?: RequiredPool[]\n shiftWindows: Interval[]\n step: number\n timeOff: Interval[]\n}): SlotInfo[] {\n const { busy, dayEnd, dayStart, quantity, requiredPools, shiftWindows, step, timeOff }
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/computeSlotStates.ts"],"sourcesContent":["import { addMinutes, doRangesOverlap } from './slotUtils.js'\n\nexport type SlotState = 'external' | 'free' | 'full' | 'off-shift' | 'time-off'\n\nexport type SlotInfo = {\n end: Date\n occupancy: number\n start: Date\n state: SlotState\n}\n\ntype Busy = Array<{ end: string; start: string; units: number }>\ntype Interval = { end: string; start: string }\ntype RequiredPool = { busy: Busy; quantity: number }\n\nconst within = (slotStart: Date, slotEnd: Date, windows: Interval[]): boolean =>\n windows.some((w) => doRangesOverlap(slotStart, slotEnd, new Date(w.start), new Date(w.end)))\n\n/** Sum the `units` of busy intervals overlapping [slotStart, slotEnd). */\nconst occupancyAt = (slotStart: Date, slotEnd: Date, busy: Busy): number => {\n let occ = 0\n for (const b of busy) {\n if (doRangesOverlap(slotStart, slotEnd, new Date(b.start), new Date(b.end))) {\n occ += b.units\n }\n }\n return occ\n}\n\n/**\n * Classify each step-sized slot in [dayStart, dayEnd) for a single resource.\n * A slot is `full` when the resource itself is at capacity OR any of its\n * `requiredPools` (e.g. a shared chair pool a service also needs) is at\n * capacity — so the grid reflects true bookability, not just the stylist.\n * Pure — no DB, no clock.\n */\nexport function computeSlotStates(params: {\n busy: Busy\n capacityMode: 'per-guest' | 'per-reservation'\n dayEnd: Date\n dayStart: Date\n external?: Interval[]\n quantity: number\n requiredPools?: RequiredPool[]\n shiftWindows: Interval[]\n step: number\n timeOff: Interval[]\n}): SlotInfo[] {\n const { busy, dayEnd, dayStart, external, quantity, requiredPools, shiftWindows, step, timeOff } =\n params\n const pools = requiredPools ?? []\n const slots: SlotInfo[] = []\n\n let cursor = new Date(dayStart)\n while (cursor < dayEnd) {\n const slotStart = new Date(cursor)\n const slotEnd = addMinutes(slotStart, step)\n\n const occupancy = occupancyAt(slotStart, slotEnd, busy)\n const poolFull = pools.some((p) => occupancyAt(slotStart, slotEnd, p.busy) >= p.quantity)\n\n let state: SlotState\n if (!within(slotStart, slotEnd, shiftWindows)) {\n state = 'off-shift'\n } else if (within(slotStart, slotEnd, timeOff)) {\n state = 'time-off'\n } else if (occupancy >= quantity || poolFull) {\n state = 'full'\n } else if (within(slotStart, slotEnd, external ?? [])) {\n // External (synced calendar) busy — after `full` so a real booking's\n // state wins when both overlap; enforcement already blocks either way.\n state = 'external'\n } else {\n state = 'free'\n }\n\n slots.push({ end: slotEnd, occupancy, start: slotStart, state })\n cursor = slotEnd\n }\n\n return slots\n}\n"],"names":["addMinutes","doRangesOverlap","within","slotStart","slotEnd","windows","some","w","Date","start","end","occupancyAt","busy","occ","b","units","computeSlotStates","params","dayEnd","dayStart","external","quantity","requiredPools","shiftWindows","step","timeOff","pools","slots","cursor","occupancy","poolFull","p","state","push"],"mappings":"AAAA,SAASA,UAAU,EAAEC,eAAe,QAAQ,iBAAgB;AAe5D,MAAMC,SAAS,CAACC,WAAiBC,SAAeC,UAC9CA,QAAQC,IAAI,CAAC,CAACC,IAAMN,gBAAgBE,WAAWC,SAAS,IAAII,KAAKD,EAAEE,KAAK,GAAG,IAAID,KAAKD,EAAEG,GAAG;AAE3F,wEAAwE,GACxE,MAAMC,cAAc,CAACR,WAAiBC,SAAeQ;IACnD,IAAIC,MAAM;IACV,KAAK,MAAMC,KAAKF,KAAM;QACpB,IAAIX,gBAAgBE,WAAWC,SAAS,IAAII,KAAKM,EAAEL,KAAK,GAAG,IAAID,KAAKM,EAAEJ,GAAG,IAAI;YAC3EG,OAAOC,EAAEC,KAAK;QAChB;IACF;IACA,OAAOF;AACT;AAEA;;;;;;CAMC,GACD,OAAO,SAASG,kBAAkBC,MAWjC;IACC,MAAM,EAAEL,IAAI,EAAEM,MAAM,EAAEC,QAAQ,EAAEC,QAAQ,EAAEC,QAAQ,EAAEC,aAAa,EAAEC,YAAY,EAAEC,IAAI,EAAEC,OAAO,EAAE,GAC9FR;IACF,MAAMS,QAAQJ,iBAAiB,EAAE;IACjC,MAAMK,QAAoB,EAAE;IAE5B,IAAIC,SAAS,IAAIpB,KAAKW;IACtB,MAAOS,SAASV,OAAQ;QACtB,MAAMf,YAAY,IAAIK,KAAKoB;QAC3B,MAAMxB,UAAUJ,WAAWG,WAAWqB;QAEtC,MAAMK,YAAYlB,YAAYR,WAAWC,SAASQ;QAClD,MAAMkB,WAAWJ,MAAMpB,IAAI,CAAC,CAACyB,IAAMpB,YAAYR,WAAWC,SAAS2B,EAAEnB,IAAI,KAAKmB,EAAEV,QAAQ;QAExF,IAAIW;QACJ,IAAI,CAAC9B,OAAOC,WAAWC,SAASmB,eAAe;YAC7CS,QAAQ;QACV,OAAO,IAAI9B,OAAOC,WAAWC,SAASqB,UAAU;YAC9CO,QAAQ;QACV,OAAO,IAAIH,aAAaR,YAAYS,UAAU;YAC5CE,QAAQ;QACV,OAAO,IAAI9B,OAAOC,WAAWC,SAASgB,YAAY,EAAE,GAAG;YACrD,qEAAqE;YACrE,uEAAuE;YACvEY,QAAQ;QACV,OAAO;YACLA,QAAQ;QACV;QAEAL,MAAMM,IAAI,CAAC;YAAEvB,KAAKN;YAASyB;YAAWpB,OAAON;YAAW6B;QAAM;QAC9DJ,SAASxB;IACX;IAEA,OAAOuB;AACT"}
|
package/package.json
CHANGED