payload-reserve 1.6.0 → 2.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 +40 -3
- package/dist/collections/Reservations.js +19 -7
- package/dist/collections/Reservations.js.map +1 -1
- package/dist/collections/Resources.js +11 -8
- package/dist/collections/Resources.js.map +1 -1
- package/dist/collections/Schedules.js +12 -6
- package/dist/collections/Schedules.js.map +1 -1
- package/dist/collections/Services.js +19 -10
- package/dist/collections/Services.js.map +1 -1
- package/dist/components/AvailabilityOverview/index.js +43 -18
- package/dist/components/AvailabilityOverview/index.js.map +1 -1
- package/dist/components/CalendarView/CalendarView.module.css +9 -0
- package/dist/components/CalendarView/LaneTimelineView.d.ts +4 -1
- package/dist/components/CalendarView/LaneTimelineView.js +17 -12
- package/dist/components/CalendarView/LaneTimelineView.js.map +1 -1
- package/dist/components/CalendarView/index.js +134 -44
- package/dist/components/CalendarView/index.js.map +1 -1
- package/dist/components/CustomerField/index.js +8 -3
- package/dist/components/CustomerField/index.js.map +1 -1
- package/dist/components/DashboardWidget/DashboardWidgetServer.js +79 -17
- package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
- package/dist/defaults.js +42 -8
- package/dist/defaults.js.map +1 -1
- package/dist/endpoints/cancelBooking.js +1 -1
- package/dist/endpoints/cancelBooking.js.map +1 -1
- package/dist/endpoints/checkAvailability.js +56 -7
- package/dist/endpoints/checkAvailability.js.map +1 -1
- package/dist/endpoints/createBooking.js +19 -10
- package/dist/endpoints/createBooking.js.map +1 -1
- package/dist/endpoints/customerSearch.js +5 -2
- package/dist/endpoints/customerSearch.js.map +1 -1
- package/dist/endpoints/getSlots.js +56 -7
- package/dist/endpoints/getSlots.js.map +1 -1
- package/dist/endpoints/resourceAvailability.d.ts +2 -1
- package/dist/endpoints/resourceAvailability.js +85 -25
- package/dist/endpoints/resourceAvailability.js.map +1 -1
- package/dist/hooks/reservations/calculateEndTime.js +48 -20
- package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
- package/dist/hooks/reservations/enforceCustomerOwnership.d.ts +11 -0
- package/dist/hooks/reservations/enforceCustomerOwnership.js +30 -0
- package/dist/hooks/reservations/enforceCustomerOwnership.js.map +1 -0
- package/dist/hooks/reservations/onStatusChange.js +10 -4
- package/dist/hooks/reservations/onStatusChange.js.map +1 -1
- package/dist/hooks/reservations/validateCancellation.js +3 -2
- package/dist/hooks/reservations/validateCancellation.js.map +1 -1
- package/dist/hooks/reservations/validateConflicts.js +23 -4
- package/dist/hooks/reservations/validateConflicts.js.map +1 -1
- package/dist/hooks/reservations/validateGuestBooking.js +3 -4
- package/dist/hooks/reservations/validateGuestBooking.js.map +1 -1
- package/dist/hooks/reservations/validateStatusTransition.js +2 -2
- package/dist/hooks/reservations/validateStatusTransition.js.map +1 -1
- package/dist/hooks/users/provisionStaffResource.js +5 -8
- package/dist/hooks/users/provisionStaffResource.js.map +1 -1
- package/dist/plugin.js +81 -13
- package/dist/plugin.js.map +1 -1
- package/dist/services/AvailabilityService.d.ts +54 -2
- package/dist/services/AvailabilityService.js +180 -46
- package/dist/services/AvailabilityService.js.map +1 -1
- package/dist/translations/ar.json +1 -0
- package/dist/translations/de.json +1 -0
- package/dist/translations/en.json +1 -0
- package/dist/translations/es.json +1 -0
- package/dist/translations/fa.json +1 -0
- package/dist/translations/fr.json +1 -0
- package/dist/translations/hi.json +1 -0
- package/dist/translations/id.json +1 -0
- package/dist/translations/pl.json +1 -0
- package/dist/translations/ru.json +1 -0
- package/dist/translations/tr.json +1 -0
- package/dist/translations/zh.json +1 -0
- package/dist/types.d.ts +39 -1
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/dist/utilities/collectionOverrides.d.ts +14 -0
- package/dist/utilities/collectionOverrides.js +47 -0
- package/dist/utilities/collectionOverrides.js.map +1 -0
- package/dist/utilities/ownerAccess.d.ts +6 -0
- package/dist/utilities/ownerAccess.js +25 -12
- package/dist/utilities/ownerAccess.js.map +1 -1
- package/dist/utilities/reservationChanges.d.ts +17 -0
- package/dist/utilities/reservationChanges.js +88 -0
- package/dist/utilities/reservationChanges.js.map +1 -0
- package/dist/utilities/scheduleUtils.d.ts +14 -8
- package/dist/utilities/scheduleUtils.js +26 -19
- package/dist/utilities/scheduleUtils.js.map +1 -1
- package/dist/utilities/timezoneUtils.d.ts +39 -0
- package/dist/utilities/timezoneUtils.js +134 -0
- package/dist/utilities/timezoneUtils.js.map +1 -0
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useConfig, useTranslation } from '@payloadcms/ui';
|
|
4
4
|
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
|
5
|
+
import { getDayKeyInTimezone, getDayOfWeekFromDayKey } from '../../utilities/timezoneUtils.js';
|
|
5
6
|
import { useTenantFilter } from '../../utilities/useTenantFilter.js';
|
|
6
7
|
import styles from './AvailabilityOverview.module.css';
|
|
7
8
|
const DAY_MAP = {
|
|
@@ -32,6 +33,7 @@ export const AvailabilityOverview = ()=>{
|
|
|
32
33
|
'pending',
|
|
33
34
|
'confirmed'
|
|
34
35
|
];
|
|
36
|
+
const reservationTimezone = config.admin?.custom?.reservationTimezone ?? 'UTC';
|
|
35
37
|
const resourcesTenantParams = useTenantFilter(slugs?.resources ?? 'resources');
|
|
36
38
|
const schedulesTenantParams = useTenantFilter(slugs?.schedules ?? 'schedules');
|
|
37
39
|
const reservationsTenantParams = useTenantFilter(slugs?.reservations ?? 'reservations');
|
|
@@ -56,6 +58,8 @@ export const AvailabilityOverview = ()=>{
|
|
|
56
58
|
const [schedules, setSchedules] = useState([]);
|
|
57
59
|
const [reservations, setReservations] = useState([]);
|
|
58
60
|
const [loading, setLoading] = useState(true);
|
|
61
|
+
// Cells are browser-local midnights by design; every key derived from them
|
|
62
|
+
// (day keys, weekday matching) is computed in the business timezone.
|
|
59
63
|
const weekDays = useMemo(()=>{
|
|
60
64
|
return Array.from({
|
|
61
65
|
length: 7
|
|
@@ -79,6 +83,8 @@ export const AvailabilityOverview = ()=>{
|
|
|
79
83
|
if (!slugs) {
|
|
80
84
|
return;
|
|
81
85
|
}
|
|
86
|
+
// Ignore a slow earlier fetch if the week changed before it resolved (D5)
|
|
87
|
+
let stale = false;
|
|
82
88
|
const fetchData = async ()=>{
|
|
83
89
|
setLoading(true);
|
|
84
90
|
const apiBase = `${config.serverURL ?? ''}${config.routes.api}`;
|
|
@@ -93,13 +99,13 @@ export const AvailabilityOverview = ()=>{
|
|
|
93
99
|
...resourcesTenantParams
|
|
94
100
|
});
|
|
95
101
|
const schedulesParams = new URLSearchParams({
|
|
96
|
-
limit: '
|
|
102
|
+
limit: '1000',
|
|
97
103
|
'where[active][equals]': 'true',
|
|
98
104
|
...schedulesTenantParams
|
|
99
105
|
});
|
|
100
106
|
const reservationsParams = new URLSearchParams({
|
|
101
107
|
depth: '0',
|
|
102
|
-
limit: '
|
|
108
|
+
limit: '2000',
|
|
103
109
|
'where[startTime][greater_than_equal]': weekStart.toISOString(),
|
|
104
110
|
'where[startTime][less_than_equal]': weekEnd.toISOString(),
|
|
105
111
|
'where[status][in]': blockingIn,
|
|
@@ -115,17 +121,28 @@ export const AvailabilityOverview = ()=>{
|
|
|
115
121
|
schedulesRes.json(),
|
|
116
122
|
reservationsRes.json()
|
|
117
123
|
]);
|
|
124
|
+
if (stale) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
118
127
|
setResources(rData.docs ?? []);
|
|
119
128
|
setSchedules(sData.docs ?? []);
|
|
120
129
|
setReservations(resData.docs ?? []);
|
|
121
130
|
} catch {
|
|
131
|
+
if (stale) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
122
134
|
setResources([]);
|
|
123
135
|
setSchedules([]);
|
|
124
136
|
setReservations([]);
|
|
125
137
|
}
|
|
126
|
-
|
|
138
|
+
if (!stale) {
|
|
139
|
+
setLoading(false);
|
|
140
|
+
}
|
|
127
141
|
};
|
|
128
142
|
void fetchData();
|
|
143
|
+
return ()=>{
|
|
144
|
+
stale = true;
|
|
145
|
+
};
|
|
129
146
|
// blockingStatuses is derived from config which is stable; stringify to
|
|
130
147
|
// avoid object-reference churn causing infinite loops.
|
|
131
148
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -156,14 +173,16 @@ export const AvailabilityOverview = ()=>{
|
|
|
156
173
|
const getResourceId = (r)=>typeof r === 'object' ? r.id : r;
|
|
157
174
|
const getSlotsForResourceDay = (resourceId, day)=>{
|
|
158
175
|
const resourceSchedules = schedules.filter((s)=>getResourceId(s.resource) === resourceId);
|
|
159
|
-
const dateStr = day
|
|
160
|
-
const dayOfWeek =
|
|
176
|
+
const dateStr = getDayKeyInTimezone(day, reservationTimezone);
|
|
177
|
+
const dayOfWeek = getDayOfWeekFromDayKey(dateStr);
|
|
161
178
|
const slots = [];
|
|
162
179
|
for (const schedule of resourceSchedules){
|
|
163
|
-
// Check for exceptions
|
|
180
|
+
// Check for exceptions — match the full [date, endDate] range inclusively
|
|
181
|
+
// (review D4), keyed in the business timezone, like the server does.
|
|
164
182
|
const exception = schedule.exceptions?.find((e)=>{
|
|
165
|
-
const
|
|
166
|
-
|
|
183
|
+
const start = getDayKeyInTimezone(new Date(e.date), reservationTimezone);
|
|
184
|
+
const end = e.endDate ? getDayKeyInTimezone(new Date(e.endDate), reservationTimezone) : start;
|
|
185
|
+
return dateStr >= start && dateStr <= end;
|
|
167
186
|
});
|
|
168
187
|
if (exception) {
|
|
169
188
|
slots.push({
|
|
@@ -174,7 +193,7 @@ export const AvailabilityOverview = ()=>{
|
|
|
174
193
|
}
|
|
175
194
|
if (schedule.scheduleType === 'recurring') {
|
|
176
195
|
for (const slot of schedule.recurringSlots ?? []){
|
|
177
|
-
if (
|
|
196
|
+
if (slot.day === dayOfWeek) {
|
|
178
197
|
slots.push({
|
|
179
198
|
type: 'available',
|
|
180
199
|
label: `${slot.startTime}-${slot.endTime}`
|
|
@@ -183,7 +202,7 @@ export const AvailabilityOverview = ()=>{
|
|
|
183
202
|
}
|
|
184
203
|
} else if (schedule.scheduleType === 'manual') {
|
|
185
204
|
for (const slot of schedule.manualSlots ?? []){
|
|
186
|
-
const slotDate = new Date(slot.date)
|
|
205
|
+
const slotDate = getDayKeyInTimezone(new Date(slot.date), reservationTimezone);
|
|
187
206
|
if (slotDate === dateStr) {
|
|
188
207
|
slots.push({
|
|
189
208
|
type: 'available',
|
|
@@ -196,9 +215,9 @@ export const AvailabilityOverview = ()=>{
|
|
|
196
215
|
return slots;
|
|
197
216
|
};
|
|
198
217
|
/** Returns all blocking-status reservations for a resource on a given day. */ const getBookingsForResourceDay = (resourceId, day)=>{
|
|
218
|
+
const dayKey = getDayKeyInTimezone(day, reservationTimezone);
|
|
199
219
|
return reservations.filter((r)=>{
|
|
200
|
-
|
|
201
|
-
return getResourceId(r.resource) === resourceId && rDate.getFullYear() === day.getFullYear() && rDate.getMonth() === day.getMonth() && rDate.getDate() === day.getDate();
|
|
220
|
+
return getResourceId(r.resource) === resourceId && getDayKeyInTimezone(new Date(r.startTime), reservationTimezone) === dayKey;
|
|
202
221
|
});
|
|
203
222
|
};
|
|
204
223
|
if (!slugs) {
|
|
@@ -215,10 +234,12 @@ export const AvailabilityOverview = ()=>{
|
|
|
215
234
|
}
|
|
216
235
|
const weekLabel = `${weekDays[0].toLocaleDateString([], {
|
|
217
236
|
day: 'numeric',
|
|
218
|
-
month: 'short'
|
|
237
|
+
month: 'short',
|
|
238
|
+
timeZone: reservationTimezone
|
|
219
239
|
})} - ${weekDays[6].toLocaleDateString([], {
|
|
220
240
|
day: 'numeric',
|
|
221
241
|
month: 'short',
|
|
242
|
+
timeZone: reservationTimezone,
|
|
222
243
|
year: 'numeric'
|
|
223
244
|
})}`;
|
|
224
245
|
const gridColumns = `150px repeat(7, 1fr)`;
|
|
@@ -269,14 +290,17 @@ export const AvailabilityOverview = ()=>{
|
|
|
269
290
|
className: styles.headerCell,
|
|
270
291
|
children: t('reservation:availabilityResource')
|
|
271
292
|
}),
|
|
272
|
-
weekDays.map((day, i)
|
|
293
|
+
weekDays.map((day, i)=>{
|
|
294
|
+
const dayKey = getDayKeyInTimezone(day, reservationTimezone);
|
|
295
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
273
296
|
className: styles.headerCell,
|
|
274
297
|
children: [
|
|
275
|
-
DAY_NAMES[
|
|
298
|
+
DAY_NAMES[DAY_MAP[getDayOfWeekFromDayKey(dayKey)]],
|
|
276
299
|
" ",
|
|
277
|
-
|
|
300
|
+
Number(dayKey.slice(8, 10))
|
|
278
301
|
]
|
|
279
|
-
}, i)
|
|
302
|
+
}, i);
|
|
303
|
+
}),
|
|
280
304
|
resources.map((resource)=>{
|
|
281
305
|
const quantity = resource.quantity ?? 1;
|
|
282
306
|
return /*#__PURE__*/ _jsxs(Fragment, {
|
|
@@ -326,7 +350,8 @@ export const AvailabilityOverview = ()=>{
|
|
|
326
350
|
children: [
|
|
327
351
|
new Date(b.startTime).toLocaleTimeString([], {
|
|
328
352
|
hour: '2-digit',
|
|
329
|
-
minute: '2-digit'
|
|
353
|
+
minute: '2-digit',
|
|
354
|
+
timeZone: reservationTimezone
|
|
330
355
|
}),
|
|
331
356
|
' ',
|
|
332
357
|
t('reservation:availabilityBooked')
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/AvailabilityOverview/index.tsx"],"sourcesContent":["'use client'\nimport type { AdminViewServerProps } from 'payload'\n\nimport { useConfig, useTranslation } from '@payloadcms/ui'\nimport { Fragment, useCallback, useEffect, useMemo, useState } from 'react'\n\nimport type { PluginT } from '../../translations/index.js'\n\nimport { useTenantFilter } from '../../utilities/useTenantFilter.js'\nimport styles from './AvailabilityOverview.module.css'\n\ntype Resource = {\n active?: boolean\n capacityMode?: 'per-guest' | 'per-reservation'\n id: string\n name: string\n quantity?: number\n}\n\ntype Schedule = {\n active?: boolean\n exceptions?: Array<{ date: string; reason?: string }>\n id: string\n manualSlots?: Array<{ date: string; endTime: string; startTime: string }>\n recurringSlots?: Array<{ day: string; endTime: string; startTime: string }>\n resource: { id: string } | string\n scheduleType: 'manual' | 'recurring'\n}\n\ntype Reservation = {\n endTime?: string\n id: string\n resource: { id: string } | string\n startTime: string\n status: string\n}\n\nconst DAY_MAP: Record<string, number> = {\n fri: 5,\n mon: 1,\n sat: 6,\n sun: 0,\n thu: 4,\n tue: 2,\n wed: 3,\n}\n\n/** Return the CSS class for a capacity badge based on utilization ratio. */\nfunction capacityClass(booked: number, total: number): string {\n if (booked >= total) {return styles.slotCapacityFull}\n if (booked / total >= 0.5) {return styles.slotCapacityMid}\n return styles.slotCapacityLow\n}\n\nexport const AvailabilityOverview: React.FC<AdminViewServerProps> = () => {\n const { config } = useConfig()\n const { t: _t } = useTranslation()\n const t = _t as PluginT\n const slugs = config.admin?.custom?.reservationSlugs\n const statusMachine = config.admin?.custom?.reservationStatusMachine\n const blockingStatuses: string[] = statusMachine?.blockingStatuses ?? ['pending', 'confirmed']\n\n const resourcesTenantParams = useTenantFilter(slugs?.resources ?? 'resources')\n const schedulesTenantParams = useTenantFilter(slugs?.schedules ?? 'schedules')\n const reservationsTenantParams = useTenantFilter(slugs?.reservations ?? 'reservations')\n\n const DAY_NAMES = useMemo(\n () => [\n t('reservation:dayShortSun'),\n t('reservation:dayShortMon'),\n t('reservation:dayShortTue'),\n t('reservation:dayShortWed'),\n t('reservation:dayShortThu'),\n t('reservation:dayShortFri'),\n t('reservation:dayShortSat'),\n ],\n [t],\n )\n\n const [weekStart, setWeekStart] = useState(() => {\n const now = new Date()\n const d = new Date(now.getFullYear(), now.getMonth(), now.getDate())\n d.setDate(d.getDate() - d.getDay())\n return d\n })\n\n const [resources, setResources] = useState<Resource[]>([])\n const [schedules, setSchedules] = useState<Schedule[]>([])\n const [reservations, setReservations] = useState<Reservation[]>([])\n const [loading, setLoading] = useState(true)\n\n const weekDays = useMemo(() => {\n return Array.from({ length: 7 }, (_, i) => {\n const d = new Date(weekStart)\n d.setDate(d.getDate() + i)\n return d\n })\n }, [weekStart])\n\n const weekEnd = useMemo(() => {\n const d = new Date(weekStart)\n d.setDate(d.getDate() + 6)\n d.setHours(23, 59, 59, 999)\n return d\n }, [weekStart])\n\n useEffect(() => {\n if (!slugs) {return}\n\n const fetchData = async () => {\n setLoading(true)\n const apiBase = `${config.serverURL ?? ''}${config.routes.api}`\n\n // Build a query that fetches only blocking-status reservations so the\n // component doesn't need to filter client-side. The `in` operator on\n // Payload's REST API accepts a comma-separated list.\n const blockingIn = blockingStatuses.join(',')\n\n try {\n const resourcesParams = new URLSearchParams({\n limit: '100',\n 'where[active][equals]': 'true',\n ...resourcesTenantParams,\n })\n const schedulesParams = new URLSearchParams({\n limit: '500',\n 'where[active][equals]': 'true',\n ...schedulesTenantParams,\n })\n const reservationsParams = new URLSearchParams({\n depth: '0',\n limit: '500',\n 'where[startTime][greater_than_equal]': weekStart.toISOString(),\n 'where[startTime][less_than_equal]': weekEnd.toISOString(),\n 'where[status][in]': blockingIn,\n ...reservationsTenantParams,\n })\n const [resourcesRes, schedulesRes, reservationsRes] = await Promise.all([\n fetch(`${apiBase}/${slugs.resources}?${resourcesParams}`),\n fetch(`${apiBase}/${slugs.schedules}?${schedulesParams}`),\n fetch(`${apiBase}/${slugs.reservations}?${reservationsParams}`),\n ])\n\n const [rData, sData, resData] = await Promise.all([\n resourcesRes.json(),\n schedulesRes.json(),\n reservationsRes.json(),\n ])\n\n setResources(rData.docs ?? [])\n setSchedules(sData.docs ?? [])\n setReservations(resData.docs ?? [])\n } catch {\n setResources([])\n setSchedules([])\n setReservations([])\n }\n setLoading(false)\n }\n\n void fetchData()\n // blockingStatuses is derived from config which is stable; stringify to\n // avoid object-reference churn causing infinite loops.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [weekStart, weekEnd, config.routes.api, config.serverURL, slugs, blockingStatuses.join(','), resourcesTenantParams, schedulesTenantParams, reservationsTenantParams])\n\n const navigateWeek = useCallback((direction: -1 | 1) => {\n setWeekStart((prev) => {\n const next = new Date(prev)\n next.setDate(next.getDate() + 7 * direction)\n return next\n })\n }, [])\n\n const goToThisWeek = useCallback(() => {\n const now = new Date()\n const d = new Date(now.getFullYear(), now.getMonth(), now.getDate())\n d.setDate(d.getDate() - d.getDay())\n setWeekStart(d)\n }, [])\n\n const getResourceId = (r: { id: string } | string) =>\n typeof r === 'object' ? r.id : r\n\n const getSlotsForResourceDay = (resourceId: string, day: Date) => {\n const resourceSchedules = schedules.filter(\n (s) => getResourceId(s.resource) === resourceId,\n )\n const dateStr = day.toISOString().split('T')[0]\n const dayOfWeek = day.getDay()\n\n const slots: Array<{ label: string; type: 'available' | 'exception' }> = []\n\n for (const schedule of resourceSchedules) {\n // Check for exceptions\n const exception = schedule.exceptions?.find((e) => {\n const excDate = new Date(e.date).toISOString().split('T')[0]\n return excDate === dateStr\n })\n\n if (exception) {\n slots.push({\n type: 'exception',\n label: exception.reason || t('reservation:availabilityUnavailable'),\n })\n continue\n }\n\n if (schedule.scheduleType === 'recurring') {\n for (const slot of schedule.recurringSlots ?? []) {\n if (DAY_MAP[slot.day] === dayOfWeek) {\n slots.push({\n type: 'available',\n label: `${slot.startTime}-${slot.endTime}`,\n })\n }\n }\n } else if (schedule.scheduleType === 'manual') {\n for (const slot of schedule.manualSlots ?? []) {\n const slotDate = new Date(slot.date).toISOString().split('T')[0]\n if (slotDate === dateStr) {\n slots.push({\n type: 'available',\n label: `${slot.startTime}-${slot.endTime}`,\n })\n }\n }\n }\n }\n\n return slots\n }\n\n /** Returns all blocking-status reservations for a resource on a given day. */\n const getBookingsForResourceDay = (resourceId: string, day: Date) => {\n return reservations.filter((r) => {\n const rDate = new Date(r.startTime)\n return (\n getResourceId(r.resource) === resourceId &&\n rDate.getFullYear() === day.getFullYear() &&\n rDate.getMonth() === day.getMonth() &&\n rDate.getDate() === day.getDate()\n )\n })\n }\n\n if (!slugs) {\n return <div className={styles.noResources}>{t('reservation:availabilityNotConfigured')}</div>\n }\n\n if (loading) {\n return <div className={styles.loading}>{t('reservation:availabilityLoading')}</div>\n }\n\n const weekLabel = `${weekDays[0].toLocaleDateString([], { day: 'numeric', month: 'short' })} - ${weekDays[6].toLocaleDateString([], { day: 'numeric', month: 'short', year: 'numeric' })}`\n\n const gridColumns = `150px repeat(7, 1fr)`\n\n return (\n <div className={styles.wrapper}>\n <h2 className={styles.title}>{t('reservation:availabilityTitle')}</h2>\n <div className={styles.navigation}>\n <button className={styles.navButton} onClick={() => navigateWeek(-1)} type=\"button\">\n ←\n </button>\n <button className={styles.navButton} onClick={goToThisWeek} type=\"button\">\n {t('reservation:availabilityThisWeek')}\n </button>\n <button className={styles.navButton} onClick={() => navigateWeek(1)} type=\"button\">\n →\n </button>\n <span className={styles.weekLabel}>{weekLabel}</span>\n </div>\n\n {resources.length === 0 ? (\n <div className={styles.noResources}>{t('reservation:availabilityNoResources')}</div>\n ) : (\n <div className={styles.grid} style={{ gridTemplateColumns: gridColumns }}>\n {/* Header row */}\n <div className={styles.headerCell}>{t('reservation:availabilityResource')}</div>\n {weekDays.map((day, i) => (\n <div className={styles.headerCell} key={i}>\n {DAY_NAMES[day.getDay()]} {day.getDate()}\n </div>\n ))}\n\n {/* Resource rows */}\n {resources.map((resource) => {\n const quantity = resource.quantity ?? 1\n return (\n <Fragment key={resource.id}>\n <div className={styles.resourceName}>\n {resource.name}\n {quantity > 1 && (\n <span style={{ fontWeight: 400, marginLeft: 4, opacity: 0.6 }}>\n {' '}(×{quantity})\n </span>\n )}\n </div>\n {weekDays.map((day, di) => {\n const slots = getSlotsForResourceDay(resource.id, day)\n const bookings = getBookingsForResourceDay(resource.id, day)\n const bookedCount = bookings.length\n\n return (\n <div className={styles.cell} key={`cell-${resource.id}-${di}`}>\n {slots.map((slot, si) => (\n <div\n className={\n slot.type === 'exception'\n ? styles.slotException\n : styles.slotAvailable\n }\n key={`slot-${si}`}\n >\n {slot.label}\n </div>\n ))}\n {quantity > 1 ? (\n /* Multi-unit resource: show X/Y booked with graduated color */\n bookedCount > 0 && (\n <div\n className={capacityClass(bookedCount, quantity)}\n title={t('reservation:availabilityXofYBooked', {\n booked: bookedCount,\n total: quantity,\n })}\n >\n {t('reservation:availabilityXofYBooked', {\n booked: bookedCount,\n total: quantity,\n })}\n </div>\n )\n ) : (\n /* Single-unit resource: show individual booking times */\n bookings.map((b) => (\n <div className={styles.slotBooked} key={b.id}>\n {new Date(b.startTime).toLocaleTimeString([], {\n hour: '2-digit',\n minute: '2-digit',\n })}{' '}\n {t('reservation:availabilityBooked')}\n </div>\n ))\n )}\n </div>\n )\n })}\n </Fragment>\n )\n })}\n </div>\n )}\n </div>\n )\n}\n"],"names":["useConfig","useTranslation","Fragment","useCallback","useEffect","useMemo","useState","useTenantFilter","styles","DAY_MAP","fri","mon","sat","sun","thu","tue","wed","capacityClass","booked","total","slotCapacityFull","slotCapacityMid","slotCapacityLow","AvailabilityOverview","config","t","_t","slugs","admin","custom","reservationSlugs","statusMachine","reservationStatusMachine","blockingStatuses","resourcesTenantParams","resources","schedulesTenantParams","schedules","reservationsTenantParams","reservations","DAY_NAMES","weekStart","setWeekStart","now","Date","d","getFullYear","getMonth","getDate","setDate","getDay","setResources","setSchedules","setReservations","loading","setLoading","weekDays","Array","from","length","_","i","weekEnd","setHours","fetchData","apiBase","serverURL","routes","api","blockingIn","join","resourcesParams","URLSearchParams","limit","schedulesParams","reservationsParams","depth","toISOString","resourcesRes","schedulesRes","reservationsRes","Promise","all","fetch","rData","sData","resData","json","docs","navigateWeek","direction","prev","next","goToThisWeek","getResourceId","r","id","getSlotsForResourceDay","resourceId","day","resourceSchedules","filter","s","resource","dateStr","split","dayOfWeek","slots","schedule","exception","exceptions","find","e","excDate","date","push","type","label","reason","scheduleType","slot","recurringSlots","startTime","endTime","manualSlots","slotDate","getBookingsForResourceDay","rDate","div","className","noResources","weekLabel","toLocaleDateString","month","year","gridColumns","wrapper","h2","title","navigation","button","navButton","onClick","span","grid","style","gridTemplateColumns","headerCell","map","quantity","resourceName","name","fontWeight","marginLeft","opacity","di","bookings","bookedCount","cell","si","slotException","slotAvailable","b","slotBooked","toLocaleTimeString","hour","minute"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/AvailabilityOverview/index.tsx"],"sourcesContent":["'use client'\nimport type { AdminViewServerProps } from 'payload'\n\nimport { useConfig, useTranslation } from '@payloadcms/ui'\nimport { Fragment, useCallback, useEffect, useMemo, useState } from 'react'\n\nimport type { PluginT } from '../../translations/index.js'\n\nimport { getDayKeyInTimezone, getDayOfWeekFromDayKey } from '../../utilities/timezoneUtils.js'\nimport { useTenantFilter } from '../../utilities/useTenantFilter.js'\nimport styles from './AvailabilityOverview.module.css'\n\ntype Resource = {\n active?: boolean\n capacityMode?: 'per-guest' | 'per-reservation'\n id: string\n name: string\n quantity?: number\n}\n\ntype Schedule = {\n active?: boolean\n exceptions?: Array<{ date: string; endDate?: string; reason?: string }>\n id: string\n manualSlots?: Array<{ date: string; endTime: string; startTime: string }>\n recurringSlots?: Array<{ day: string; endTime: string; startTime: string }>\n resource: { id: string } | string\n scheduleType: 'manual' | 'recurring'\n}\n\ntype Reservation = {\n endTime?: string\n id: string\n resource: { id: string } | string\n startTime: string\n status: string\n}\n\nconst DAY_MAP: Record<string, number> = {\n fri: 5,\n mon: 1,\n sat: 6,\n sun: 0,\n thu: 4,\n tue: 2,\n wed: 3,\n}\n\n/** Return the CSS class for a capacity badge based on utilization ratio. */\nfunction capacityClass(booked: number, total: number): string {\n if (booked >= total) {return styles.slotCapacityFull}\n if (booked / total >= 0.5) {return styles.slotCapacityMid}\n return styles.slotCapacityLow\n}\n\nexport const AvailabilityOverview: React.FC<AdminViewServerProps> = () => {\n const { config } = useConfig()\n const { t: _t } = useTranslation()\n const t = _t as PluginT\n const slugs = config.admin?.custom?.reservationSlugs\n const statusMachine = config.admin?.custom?.reservationStatusMachine\n const blockingStatuses: string[] = statusMachine?.blockingStatuses ?? ['pending', 'confirmed']\n const reservationTimezone: string = config.admin?.custom?.reservationTimezone ?? 'UTC'\n\n const resourcesTenantParams = useTenantFilter(slugs?.resources ?? 'resources')\n const schedulesTenantParams = useTenantFilter(slugs?.schedules ?? 'schedules')\n const reservationsTenantParams = useTenantFilter(slugs?.reservations ?? 'reservations')\n\n const DAY_NAMES = useMemo(\n () => [\n t('reservation:dayShortSun'),\n t('reservation:dayShortMon'),\n t('reservation:dayShortTue'),\n t('reservation:dayShortWed'),\n t('reservation:dayShortThu'),\n t('reservation:dayShortFri'),\n t('reservation:dayShortSat'),\n ],\n [t],\n )\n\n const [weekStart, setWeekStart] = useState(() => {\n const now = new Date()\n const d = new Date(now.getFullYear(), now.getMonth(), now.getDate())\n d.setDate(d.getDate() - d.getDay())\n return d\n })\n\n const [resources, setResources] = useState<Resource[]>([])\n const [schedules, setSchedules] = useState<Schedule[]>([])\n const [reservations, setReservations] = useState<Reservation[]>([])\n const [loading, setLoading] = useState(true)\n\n // Cells are browser-local midnights by design; every key derived from them\n // (day keys, weekday matching) is computed in the business timezone.\n const weekDays = useMemo(() => {\n return Array.from({ length: 7 }, (_, i) => {\n const d = new Date(weekStart)\n d.setDate(d.getDate() + i)\n return d\n })\n }, [weekStart])\n\n const weekEnd = useMemo(() => {\n const d = new Date(weekStart)\n d.setDate(d.getDate() + 6)\n d.setHours(23, 59, 59, 999)\n return d\n }, [weekStart])\n\n useEffect(() => {\n if (!slugs) {return}\n\n // Ignore a slow earlier fetch if the week changed before it resolved (D5)\n let stale = false\n\n const fetchData = async () => {\n setLoading(true)\n const apiBase = `${config.serverURL ?? ''}${config.routes.api}`\n\n // Build a query that fetches only blocking-status reservations so the\n // component doesn't need to filter client-side. The `in` operator on\n // Payload's REST API accepts a comma-separated list.\n const blockingIn = blockingStatuses.join(',')\n\n try {\n const resourcesParams = new URLSearchParams({\n limit: '100',\n 'where[active][equals]': 'true',\n ...resourcesTenantParams,\n })\n const schedulesParams = new URLSearchParams({\n limit: '1000',\n 'where[active][equals]': 'true',\n ...schedulesTenantParams,\n })\n const reservationsParams = new URLSearchParams({\n depth: '0',\n limit: '2000',\n 'where[startTime][greater_than_equal]': weekStart.toISOString(),\n 'where[startTime][less_than_equal]': weekEnd.toISOString(),\n 'where[status][in]': blockingIn,\n ...reservationsTenantParams,\n })\n const [resourcesRes, schedulesRes, reservationsRes] = await Promise.all([\n fetch(`${apiBase}/${slugs.resources}?${resourcesParams}`),\n fetch(`${apiBase}/${slugs.schedules}?${schedulesParams}`),\n fetch(`${apiBase}/${slugs.reservations}?${reservationsParams}`),\n ])\n\n const [rData, sData, resData] = await Promise.all([\n resourcesRes.json(),\n schedulesRes.json(),\n reservationsRes.json(),\n ])\n\n if (stale) {return}\n setResources(rData.docs ?? [])\n setSchedules(sData.docs ?? [])\n setReservations(resData.docs ?? [])\n } catch {\n if (stale) {return}\n setResources([])\n setSchedules([])\n setReservations([])\n }\n if (!stale) {setLoading(false)}\n }\n\n void fetchData()\n return () => {\n stale = true\n }\n // blockingStatuses is derived from config which is stable; stringify to\n // avoid object-reference churn causing infinite loops.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [weekStart, weekEnd, config.routes.api, config.serverURL, slugs, blockingStatuses.join(','), resourcesTenantParams, schedulesTenantParams, reservationsTenantParams])\n\n const navigateWeek = useCallback((direction: -1 | 1) => {\n setWeekStart((prev) => {\n const next = new Date(prev)\n next.setDate(next.getDate() + 7 * direction)\n return next\n })\n }, [])\n\n const goToThisWeek = useCallback(() => {\n const now = new Date()\n const d = new Date(now.getFullYear(), now.getMonth(), now.getDate())\n d.setDate(d.getDate() - d.getDay())\n setWeekStart(d)\n }, [])\n\n const getResourceId = (r: { id: string } | string) =>\n typeof r === 'object' ? r.id : r\n\n const getSlotsForResourceDay = (resourceId: string, day: Date) => {\n const resourceSchedules = schedules.filter(\n (s) => getResourceId(s.resource) === resourceId,\n )\n const dateStr = getDayKeyInTimezone(day, reservationTimezone)\n const dayOfWeek = getDayOfWeekFromDayKey(dateStr)\n\n const slots: Array<{ label: string; type: 'available' | 'exception' }> = []\n\n for (const schedule of resourceSchedules) {\n // Check for exceptions — match the full [date, endDate] range inclusively\n // (review D4), keyed in the business timezone, like the server does.\n const exception = schedule.exceptions?.find((e) => {\n const start = getDayKeyInTimezone(new Date(e.date), reservationTimezone)\n const end = e.endDate ? getDayKeyInTimezone(new Date(e.endDate), reservationTimezone) : start\n return dateStr >= start && dateStr <= end\n })\n\n if (exception) {\n slots.push({\n type: 'exception',\n label: exception.reason || t('reservation:availabilityUnavailable'),\n })\n continue\n }\n\n if (schedule.scheduleType === 'recurring') {\n for (const slot of schedule.recurringSlots ?? []) {\n if (slot.day === dayOfWeek) {\n slots.push({\n type: 'available',\n label: `${slot.startTime}-${slot.endTime}`,\n })\n }\n }\n } else if (schedule.scheduleType === 'manual') {\n for (const slot of schedule.manualSlots ?? []) {\n const slotDate = getDayKeyInTimezone(new Date(slot.date), reservationTimezone)\n if (slotDate === dateStr) {\n slots.push({\n type: 'available',\n label: `${slot.startTime}-${slot.endTime}`,\n })\n }\n }\n }\n }\n\n return slots\n }\n\n /** Returns all blocking-status reservations for a resource on a given day. */\n const getBookingsForResourceDay = (resourceId: string, day: Date) => {\n const dayKey = getDayKeyInTimezone(day, reservationTimezone)\n return reservations.filter((r) => {\n return (\n getResourceId(r.resource) === resourceId &&\n getDayKeyInTimezone(new Date(r.startTime), reservationTimezone) === dayKey\n )\n })\n }\n\n if (!slugs) {\n return <div className={styles.noResources}>{t('reservation:availabilityNotConfigured')}</div>\n }\n\n if (loading) {\n return <div className={styles.loading}>{t('reservation:availabilityLoading')}</div>\n }\n\n const weekLabel = `${weekDays[0].toLocaleDateString([], { day: 'numeric', month: 'short', timeZone: reservationTimezone })} - ${weekDays[6].toLocaleDateString([], { day: 'numeric', month: 'short', timeZone: reservationTimezone, year: 'numeric' })}`\n\n const gridColumns = `150px repeat(7, 1fr)`\n\n return (\n <div className={styles.wrapper}>\n <h2 className={styles.title}>{t('reservation:availabilityTitle')}</h2>\n <div className={styles.navigation}>\n <button className={styles.navButton} onClick={() => navigateWeek(-1)} type=\"button\">\n ←\n </button>\n <button className={styles.navButton} onClick={goToThisWeek} type=\"button\">\n {t('reservation:availabilityThisWeek')}\n </button>\n <button className={styles.navButton} onClick={() => navigateWeek(1)} type=\"button\">\n →\n </button>\n <span className={styles.weekLabel}>{weekLabel}</span>\n </div>\n\n {resources.length === 0 ? (\n <div className={styles.noResources}>{t('reservation:availabilityNoResources')}</div>\n ) : (\n <div className={styles.grid} style={{ gridTemplateColumns: gridColumns }}>\n {/* Header row */}\n <div className={styles.headerCell}>{t('reservation:availabilityResource')}</div>\n {weekDays.map((day, i) => {\n const dayKey = getDayKeyInTimezone(day, reservationTimezone)\n return (\n <div className={styles.headerCell} key={i}>\n {DAY_NAMES[DAY_MAP[getDayOfWeekFromDayKey(dayKey)]]} {Number(dayKey.slice(8, 10))}\n </div>\n )\n })}\n\n {/* Resource rows */}\n {resources.map((resource) => {\n const quantity = resource.quantity ?? 1\n return (\n <Fragment key={resource.id}>\n <div className={styles.resourceName}>\n {resource.name}\n {quantity > 1 && (\n <span style={{ fontWeight: 400, marginLeft: 4, opacity: 0.6 }}>\n {' '}(×{quantity})\n </span>\n )}\n </div>\n {weekDays.map((day, di) => {\n const slots = getSlotsForResourceDay(resource.id, day)\n const bookings = getBookingsForResourceDay(resource.id, day)\n const bookedCount = bookings.length\n\n return (\n <div className={styles.cell} key={`cell-${resource.id}-${di}`}>\n {slots.map((slot, si) => (\n <div\n className={\n slot.type === 'exception'\n ? styles.slotException\n : styles.slotAvailable\n }\n key={`slot-${si}`}\n >\n {slot.label}\n </div>\n ))}\n {quantity > 1 ? (\n /* Multi-unit resource: show X/Y booked with graduated color */\n bookedCount > 0 && (\n <div\n className={capacityClass(bookedCount, quantity)}\n title={t('reservation:availabilityXofYBooked', {\n booked: bookedCount,\n total: quantity,\n })}\n >\n {t('reservation:availabilityXofYBooked', {\n booked: bookedCount,\n total: quantity,\n })}\n </div>\n )\n ) : (\n /* Single-unit resource: show individual booking times */\n bookings.map((b) => (\n <div className={styles.slotBooked} key={b.id}>\n {new Date(b.startTime).toLocaleTimeString([], {\n hour: '2-digit',\n minute: '2-digit',\n timeZone: reservationTimezone,\n })}{' '}\n {t('reservation:availabilityBooked')}\n </div>\n ))\n )}\n </div>\n )\n })}\n </Fragment>\n )\n })}\n </div>\n )}\n </div>\n )\n}\n"],"names":["useConfig","useTranslation","Fragment","useCallback","useEffect","useMemo","useState","getDayKeyInTimezone","getDayOfWeekFromDayKey","useTenantFilter","styles","DAY_MAP","fri","mon","sat","sun","thu","tue","wed","capacityClass","booked","total","slotCapacityFull","slotCapacityMid","slotCapacityLow","AvailabilityOverview","config","t","_t","slugs","admin","custom","reservationSlugs","statusMachine","reservationStatusMachine","blockingStatuses","reservationTimezone","resourcesTenantParams","resources","schedulesTenantParams","schedules","reservationsTenantParams","reservations","DAY_NAMES","weekStart","setWeekStart","now","Date","d","getFullYear","getMonth","getDate","setDate","getDay","setResources","setSchedules","setReservations","loading","setLoading","weekDays","Array","from","length","_","i","weekEnd","setHours","stale","fetchData","apiBase","serverURL","routes","api","blockingIn","join","resourcesParams","URLSearchParams","limit","schedulesParams","reservationsParams","depth","toISOString","resourcesRes","schedulesRes","reservationsRes","Promise","all","fetch","rData","sData","resData","json","docs","navigateWeek","direction","prev","next","goToThisWeek","getResourceId","r","id","getSlotsForResourceDay","resourceId","day","resourceSchedules","filter","s","resource","dateStr","dayOfWeek","slots","schedule","exception","exceptions","find","e","start","date","end","endDate","push","type","label","reason","scheduleType","slot","recurringSlots","startTime","endTime","manualSlots","slotDate","getBookingsForResourceDay","dayKey","div","className","noResources","weekLabel","toLocaleDateString","month","timeZone","year","gridColumns","wrapper","h2","title","navigation","button","navButton","onClick","span","grid","style","gridTemplateColumns","headerCell","map","Number","slice","quantity","resourceName","name","fontWeight","marginLeft","opacity","di","bookings","bookedCount","cell","si","slotException","slotAvailable","b","slotBooked","toLocaleTimeString","hour","minute"],"mappings":"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"}
|
|
@@ -461,6 +461,15 @@
|
|
|
461
461
|
margin-bottom: 12px;
|
|
462
462
|
}
|
|
463
463
|
|
|
464
|
+
.truncationNotice {
|
|
465
|
+
margin-bottom: 12px;
|
|
466
|
+
padding: 8px 12px;
|
|
467
|
+
border-radius: 4px;
|
|
468
|
+
background: var(--theme-warning-100, #fef3c7);
|
|
469
|
+
color: var(--theme-warning-700, #92400e);
|
|
470
|
+
font-size: 0.85rem;
|
|
471
|
+
}
|
|
472
|
+
|
|
464
473
|
.resourceFilter {
|
|
465
474
|
appearance: none;
|
|
466
475
|
background: var(--theme-bg);
|
|
@@ -3,10 +3,13 @@ type LaneResource = {
|
|
|
3
3
|
id: string;
|
|
4
4
|
name: string;
|
|
5
5
|
};
|
|
6
|
-
export declare function LaneTimelineView({ apiBase, day, onBook, resources, }: {
|
|
6
|
+
export declare function LaneTimelineView({ apiBase, day, endHour, onBook, resources, startHour, timeZone, }: {
|
|
7
7
|
apiBase: string;
|
|
8
8
|
day: Date;
|
|
9
|
+
endHour: number;
|
|
9
10
|
onBook: (resourceId: string, startIso: string) => void;
|
|
10
11
|
resources: LaneResource[];
|
|
12
|
+
startHour: number;
|
|
13
|
+
timeZone: string;
|
|
11
14
|
}): React.JSX.Element;
|
|
12
15
|
export {};
|
|
@@ -3,26 +3,24 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useTranslation } from '@payloadcms/ui';
|
|
4
4
|
import React from 'react';
|
|
5
5
|
import { computeSlotStates } from '../../utilities/computeSlotStates.js';
|
|
6
|
-
import {
|
|
6
|
+
import { getDayKeyInTimezone } from '../../utilities/timezoneUtils.js';
|
|
7
7
|
import styles from './CalendarView.module.css';
|
|
8
8
|
import { useResourceAvailability } from './useResourceAvailability.js';
|
|
9
|
-
const HOUR_START = 7;
|
|
10
|
-
const HOUR_END = 20;
|
|
11
9
|
const SLOT_STATE_KEYS = {
|
|
12
10
|
free: 'reservation:slotFree',
|
|
13
11
|
full: 'reservation:slotFull',
|
|
14
12
|
'off-shift': 'reservation:slotOffShift',
|
|
15
13
|
'time-off': 'reservation:slotTimeOff'
|
|
16
14
|
};
|
|
17
|
-
function Lane({ apiBase, day, onBook, resource }) {
|
|
15
|
+
function Lane({ apiBase, day, endHour, onBook, resource, startHour, timeZone }) {
|
|
18
16
|
const { t: _t } = useTranslation();
|
|
19
17
|
const t = _t;
|
|
20
18
|
const dayStart = new Date(day);
|
|
21
|
-
dayStart.setHours(
|
|
19
|
+
dayStart.setHours(startHour, 0, 0, 0);
|
|
22
20
|
const dayEnd = new Date(day);
|
|
23
|
-
dayEnd.setHours(
|
|
21
|
+
dayEnd.setHours(endHour, 0, 0, 0);
|
|
24
22
|
const { data } = useResourceAvailability(apiBase, resource.id, dayStart, dayEnd);
|
|
25
|
-
const isoDay =
|
|
23
|
+
const isoDay = getDayKeyInTimezone(day, timeZone);
|
|
26
24
|
const dayAvail = data?.days.find((d)=>d.date === isoDay);
|
|
27
25
|
const slots = dayAvail ? computeSlotStates({
|
|
28
26
|
busy: data.busy,
|
|
@@ -47,7 +45,11 @@ function Lane({ apiBase, day, onBook, resource }) {
|
|
|
47
45
|
children: slots.map((s)=>{
|
|
48
46
|
const cls = s.state === 'off-shift' ? styles.slotOffShift : s.state === 'time-off' ? styles.slotTimeOff : s.state === 'full' ? styles.slotFull : styles.slotFree;
|
|
49
47
|
const isFree = s.state === 'free';
|
|
50
|
-
const slotLabel = `${s.start.toLocaleTimeString(
|
|
48
|
+
const slotLabel = `${s.start.toLocaleTimeString([], {
|
|
49
|
+
hour: '2-digit',
|
|
50
|
+
minute: '2-digit',
|
|
51
|
+
timeZone
|
|
52
|
+
})} — ${t(SLOT_STATE_KEYS[s.state])}`;
|
|
51
53
|
return isFree ? /*#__PURE__*/ _jsx("div", {
|
|
52
54
|
"aria-label": slotLabel,
|
|
53
55
|
className: `${styles.laneCell} ${cls}`,
|
|
@@ -70,7 +72,7 @@ function Lane({ apiBase, day, onBook, resource }) {
|
|
|
70
72
|
]
|
|
71
73
|
});
|
|
72
74
|
}
|
|
73
|
-
export function LaneTimelineView({ apiBase, day, onBook, resources }) {
|
|
75
|
+
export function LaneTimelineView({ apiBase, day, endHour, onBook, resources, startHour, timeZone }) {
|
|
74
76
|
const { t: _t } = useTranslation();
|
|
75
77
|
const t = _t;
|
|
76
78
|
if (resources.length === 0) {
|
|
@@ -80,8 +82,8 @@ export function LaneTimelineView({ apiBase, day, onBook, resources }) {
|
|
|
80
82
|
});
|
|
81
83
|
}
|
|
82
84
|
const hours = Array.from({
|
|
83
|
-
length:
|
|
84
|
-
}, (_, i)=>
|
|
85
|
+
length: endHour - startHour
|
|
86
|
+
}, (_, i)=>startHour + i);
|
|
85
87
|
return /*#__PURE__*/ _jsxs("div", {
|
|
86
88
|
className: styles.lanes,
|
|
87
89
|
children: [
|
|
@@ -106,8 +108,11 @@ export function LaneTimelineView({ apiBase, day, onBook, resources }) {
|
|
|
106
108
|
resources.map((r)=>/*#__PURE__*/ _jsx(Lane, {
|
|
107
109
|
apiBase: apiBase,
|
|
108
110
|
day: day,
|
|
111
|
+
endHour: endHour,
|
|
109
112
|
onBook: onBook,
|
|
110
|
-
resource: r
|
|
113
|
+
resource: r,
|
|
114
|
+
startHour: startHour,
|
|
115
|
+
timeZone: timeZone
|
|
111
116
|
}, r.id))
|
|
112
117
|
]
|
|
113
118
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/CalendarView/LaneTimelineView.tsx"],"sourcesContent":["'use client'\nimport { useTranslation } from '@payloadcms/ui'\nimport React from 'react'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { SlotState } from '../../utilities/computeSlotStates.js'\n\nimport { computeSlotStates } from '../../utilities/computeSlotStates.js'\nimport {
|
|
1
|
+
{"version":3,"sources":["../../../src/components/CalendarView/LaneTimelineView.tsx"],"sourcesContent":["'use client'\nimport { useTranslation } from '@payloadcms/ui'\nimport React from 'react'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { SlotState } from '../../utilities/computeSlotStates.js'\n\nimport { computeSlotStates } from '../../utilities/computeSlotStates.js'\nimport { getDayKeyInTimezone } from '../../utilities/timezoneUtils.js'\nimport styles from './CalendarView.module.css'\nimport { useResourceAvailability } from './useResourceAvailability.js'\n\ntype LaneResource = { id: string; name: string }\n\nconst SLOT_STATE_KEYS: Record<SlotState, string> = {\n free: 'reservation:slotFree',\n full: 'reservation:slotFull',\n 'off-shift': 'reservation:slotOffShift',\n 'time-off': 'reservation:slotTimeOff',\n}\n\nfunction Lane({\n apiBase,\n day,\n endHour,\n onBook,\n resource,\n startHour,\n timeZone,\n}: {\n apiBase: string\n day: Date\n endHour: number\n onBook: (resourceId: string, startIso: string) => void\n resource: LaneResource\n startHour: number\n timeZone: string\n}) {\n const { t: _t } = useTranslation()\n const t = _t as PluginT\n\n const dayStart = new Date(day)\n dayStart.setHours(startHour, 0, 0, 0)\n const dayEnd = new Date(day)\n dayEnd.setHours(endHour, 0, 0, 0)\n\n const { data } = useResourceAvailability(apiBase, resource.id, dayStart, dayEnd)\n const isoDay = getDayKeyInTimezone(day, timeZone)\n const dayAvail = data?.days.find((d) => d.date === isoDay)\n\n const slots = dayAvail\n ? computeSlotStates({\n busy: data!.busy,\n capacityMode: data!.capacityMode,\n dayEnd,\n dayStart,\n quantity: data!.quantity,\n requiredPools: data!.requiredPools,\n shiftWindows: dayAvail.shiftWindows,\n step: 60,\n timeOff: dayAvail.timeOff,\n })\n : []\n\n return (\n <div className={styles.lane}>\n <div className={styles.laneLabel}>{resource.name}</div>\n <div className={styles.laneTrack}>\n {slots.map((s) => {\n const cls =\n s.state === 'off-shift'\n ? styles.slotOffShift\n : s.state === 'time-off'\n ? styles.slotTimeOff\n : s.state === 'full'\n ? styles.slotFull\n : styles.slotFree\n const isFree = s.state === 'free'\n const slotLabel = `${s.start.toLocaleTimeString([], {\n hour: '2-digit',\n minute: '2-digit',\n timeZone,\n })} — ${t(SLOT_STATE_KEYS[s.state])}`\n return isFree ? (\n <div\n aria-label={slotLabel}\n className={`${styles.laneCell} ${cls}`}\n key={s.start.toISOString()}\n onClick={() => onBook(resource.id, s.start.toISOString())}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n onBook(resource.id, s.start.toISOString())\n }\n }}\n role=\"button\"\n tabIndex={0}\n title={slotLabel}\n />\n ) : (\n <div\n className={`${styles.laneCell} ${cls}`}\n key={s.start.toISOString()}\n title={slotLabel}\n />\n )\n })}\n </div>\n </div>\n )\n}\n\nexport function LaneTimelineView({\n apiBase,\n day,\n endHour,\n onBook,\n resources,\n startHour,\n timeZone,\n}: {\n apiBase: string\n day: Date\n endHour: number\n onBook: (resourceId: string, startIso: string) => void\n resources: LaneResource[]\n startHour: number\n timeZone: string\n}) {\n const { t: _t } = useTranslation()\n const t = _t as PluginT\n if (resources.length === 0) {\n return <p className={styles.hint}>{t('reservation:laneNoResources')}</p>\n }\n const hours = Array.from({ length: endHour - startHour }, (_, i) => startHour + i)\n return (\n <div className={styles.lanes}>\n <div className={styles.laneHeader}>\n <div className={styles.laneLabel} />\n <div className={styles.laneTrack}>\n {hours.map((h) => (\n <div className={styles.laneTime} key={h}>\n {String(h).padStart(2, '0')}:00\n </div>\n ))}\n </div>\n </div>\n {resources.map((r) => (\n <Lane\n apiBase={apiBase}\n day={day}\n endHour={endHour}\n key={r.id}\n onBook={onBook}\n resource={r}\n startHour={startHour}\n timeZone={timeZone}\n />\n ))}\n </div>\n )\n}\n"],"names":["useTranslation","React","computeSlotStates","getDayKeyInTimezone","styles","useResourceAvailability","SLOT_STATE_KEYS","free","full","Lane","apiBase","day","endHour","onBook","resource","startHour","timeZone","t","_t","dayStart","Date","setHours","dayEnd","data","id","isoDay","dayAvail","days","find","d","date","slots","busy","capacityMode","quantity","requiredPools","shiftWindows","step","timeOff","div","className","lane","laneLabel","name","laneTrack","map","s","cls","state","slotOffShift","slotTimeOff","slotFull","slotFree","isFree","slotLabel","start","toLocaleTimeString","hour","minute","aria-label","laneCell","onClick","toISOString","onKeyDown","e","key","preventDefault","role","tabIndex","title","LaneTimelineView","resources","length","p","hint","hours","Array","from","_","i","lanes","laneHeader","h","laneTime","String","padStart","r"],"mappings":"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"}
|