payload-reserve 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -1141
- package/dist/collections/Customers.d.ts +3 -0
- package/dist/collections/Customers.js +58 -0
- package/dist/collections/Customers.js.map +1 -0
- package/dist/collections/Reservations.js +129 -35
- package/dist/collections/Reservations.js.map +1 -1
- package/dist/collections/Resources.js +70 -2
- package/dist/collections/Resources.js.map +1 -1
- package/dist/collections/Schedules.js +4 -1
- package/dist/collections/Schedules.js.map +1 -1
- package/dist/collections/Services.js +59 -2
- package/dist/collections/Services.js.map +1 -1
- package/dist/components/AvailabilityOverview/AvailabilityOverview.module.css +31 -0
- package/dist/components/AvailabilityOverview/index.js +59 -8
- package/dist/components/AvailabilityOverview/index.js.map +1 -1
- package/dist/components/CalendarView/CalendarView.module.css +171 -0
- package/dist/components/CalendarView/index.js +547 -38
- package/dist/components/CalendarView/index.js.map +1 -1
- package/dist/components/CustomerField/index.js +24 -10
- package/dist/components/CustomerField/index.js.map +1 -1
- package/dist/components/DashboardWidget/DashboardWidgetServer.js +21 -11
- package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
- package/dist/defaults.d.ts +1 -2
- package/dist/defaults.js +22 -4
- package/dist/defaults.js.map +1 -1
- package/dist/endpoints/cancelBooking.d.ts +3 -0
- package/dist/endpoints/cancelBooking.js +37 -0
- package/dist/endpoints/cancelBooking.js.map +1 -0
- package/dist/endpoints/checkAvailability.d.ts +3 -0
- package/dist/endpoints/checkAvailability.js +37 -0
- package/dist/endpoints/checkAvailability.js.map +1 -0
- package/dist/endpoints/createBooking.d.ts +3 -0
- package/dist/endpoints/createBooking.js +32 -0
- package/dist/endpoints/createBooking.js.map +1 -0
- package/dist/endpoints/customerSearch.js +76 -58
- package/dist/endpoints/customerSearch.js.map +1 -1
- package/dist/endpoints/getSlots.d.ts +3 -0
- package/dist/endpoints/getSlots.js +51 -0
- package/dist/endpoints/getSlots.js.map +1 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/reservations/calculateEndTime.js +75 -9
- package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
- package/dist/hooks/reservations/checkIdempotency.d.ts +3 -0
- package/dist/hooks/reservations/checkIdempotency.js +32 -0
- package/dist/hooks/reservations/checkIdempotency.js.map +1 -0
- package/dist/hooks/reservations/onStatusChange.d.ts +3 -0
- package/dist/hooks/reservations/onStatusChange.js +41 -0
- package/dist/hooks/reservations/onStatusChange.js.map +1 -0
- package/dist/hooks/reservations/validateCancellation.js +1 -1
- package/dist/hooks/reservations/validateCancellation.js.map +1 -1
- package/dist/hooks/reservations/validateConflicts.js +34 -56
- package/dist/hooks/reservations/validateConflicts.js.map +1 -1
- package/dist/hooks/reservations/validateStatusTransition.d.ts +2 -1
- package/dist/hooks/reservations/validateStatusTransition.js +29 -6
- package/dist/hooks/reservations/validateStatusTransition.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin.js +49 -54
- package/dist/plugin.js.map +1 -1
- package/dist/services/AvailabilityService.d.ts +57 -0
- package/dist/services/AvailabilityService.js +232 -0
- package/dist/services/AvailabilityService.js.map +1 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +3 -0
- package/dist/services/index.js.map +1 -0
- package/dist/translations/en.json +33 -1
- package/dist/translations/index.d.ts +5 -0
- package/dist/translations/index.js.map +1 -1
- package/dist/types.d.ts +84 -6
- package/dist/types.js +29 -9
- package/dist/types.js.map +1 -1
- package/dist/utilities/ownerAccess.d.ts +24 -0
- package/dist/utilities/ownerAccess.js +128 -0
- package/dist/utilities/ownerAccess.js.map +1 -0
- package/dist/utilities/resolveReservationItems.d.ts +18 -0
- package/dist/utilities/resolveReservationItems.js +45 -0
- package/dist/utilities/resolveReservationItems.js.map +1 -0
- package/package.json +12 -9
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useConfig, useDocumentDrawer, useTranslation } from '@payloadcms/ui';
|
|
4
4
|
import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
5
5
|
import styles from './CalendarView.module.css';
|
|
6
|
+
// Built-in status → CSS class map (for known statuses; custom statuses use inline style)
|
|
6
7
|
const STATUS_CLASS_MAP = {
|
|
7
8
|
cancelled: styles.statusCancelled,
|
|
8
9
|
completed: styles.statusCompleted,
|
|
@@ -10,34 +11,101 @@ const STATUS_CLASS_MAP = {
|
|
|
10
11
|
'no-show': styles.statusNoShow,
|
|
11
12
|
pending: styles.statusPending
|
|
12
13
|
};
|
|
13
|
-
|
|
14
|
+
// Built-in default colors for known statuses
|
|
15
|
+
const BUILTIN_STATUS_COLORS = {
|
|
14
16
|
cancelled: '#e5e7eb',
|
|
15
17
|
completed: '#d1fae5',
|
|
16
18
|
confirmed: '#dbeafe',
|
|
17
19
|
'no-show': '#fee2e2',
|
|
18
20
|
pending: '#fef3c7'
|
|
19
21
|
};
|
|
22
|
+
// Palette for auto-assigning colors to custom statuses
|
|
23
|
+
const CUSTOM_STATUS_PALETTE = [
|
|
24
|
+
'#fde68a',
|
|
25
|
+
'#c7d2fe',
|
|
26
|
+
'#a7f3d0',
|
|
27
|
+
'#fca5a5',
|
|
28
|
+
'#fdba74'
|
|
29
|
+
];
|
|
20
30
|
export const CalendarView = ()=>{
|
|
21
31
|
const { config } = useConfig();
|
|
22
32
|
const { t: _t } = useTranslation();
|
|
23
33
|
const t = _t;
|
|
24
|
-
const STATUS_LABELS = useMemo(()=>({
|
|
25
|
-
cancelled: t('reservation:statusCancelled'),
|
|
26
|
-
completed: t('reservation:statusCompleted'),
|
|
27
|
-
confirmed: t('reservation:statusConfirmed'),
|
|
28
|
-
'no-show': t('reservation:statusNoShowLabel'),
|
|
29
|
-
pending: t('reservation:statusPending')
|
|
30
|
-
}), [
|
|
31
|
-
t
|
|
32
|
-
]);
|
|
33
34
|
const slugs = config.admin?.custom?.reservationSlugs;
|
|
34
35
|
const reservationSlug = slugs?.reservations ?? 'reservations';
|
|
36
|
+
const apiUrl = `${config.serverURL ?? ''}${config.routes.api}/${reservationSlug}`;
|
|
37
|
+
const statusMachine = config.admin?.custom?.reservationStatusMachine;
|
|
38
|
+
// The initial/pending status (what "pending" view shows)
|
|
39
|
+
const defaultStatus = statusMachine?.defaultStatus ?? 'pending';
|
|
40
|
+
// Build STATUS_COLORS dynamically: built-ins first, then auto-assign palette for custom statuses
|
|
41
|
+
const STATUS_COLORS = useMemo(()=>{
|
|
42
|
+
const colors = {
|
|
43
|
+
...BUILTIN_STATUS_COLORS
|
|
44
|
+
};
|
|
45
|
+
const statuses = statusMachine?.statuses ?? [];
|
|
46
|
+
statuses.forEach((s, i)=>{
|
|
47
|
+
if (!colors[s]) {
|
|
48
|
+
colors[s] = CUSTOM_STATUS_PALETTE[i % CUSTOM_STATUS_PALETTE.length];
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
return colors;
|
|
52
|
+
}, [
|
|
53
|
+
statusMachine
|
|
54
|
+
]);
|
|
55
|
+
// Derive confirm/cancel target statuses from config transitions
|
|
56
|
+
// "confirm" = first non-terminal transition from defaultStatus
|
|
57
|
+
// "cancel" = first terminal transition from defaultStatus (or fallback: 'cancelled')
|
|
58
|
+
const { cancelStatus, confirmStatus } = useMemo(()=>{
|
|
59
|
+
const terminalStatuses = statusMachine?.terminalStatuses ?? [
|
|
60
|
+
'completed',
|
|
61
|
+
'cancelled',
|
|
62
|
+
'no-show'
|
|
63
|
+
];
|
|
64
|
+
const transitions = statusMachine?.transitions ?? {};
|
|
65
|
+
const defaultTransitions = transitions[defaultStatus] ?? [];
|
|
66
|
+
const nonTerminal = defaultTransitions.find((s)=>!terminalStatuses.includes(s));
|
|
67
|
+
const terminal = defaultTransitions.find((s)=>terminalStatuses.includes(s));
|
|
68
|
+
return {
|
|
69
|
+
cancelStatus: terminal ?? 'cancelled',
|
|
70
|
+
confirmStatus: nonTerminal ?? 'confirmed'
|
|
71
|
+
};
|
|
72
|
+
}, [
|
|
73
|
+
statusMachine,
|
|
74
|
+
defaultStatus
|
|
75
|
+
]);
|
|
76
|
+
const STATUS_LABELS = useMemo(()=>{
|
|
77
|
+
const statuses = statusMachine?.statuses ?? [
|
|
78
|
+
'pending',
|
|
79
|
+
'confirmed',
|
|
80
|
+
'completed',
|
|
81
|
+
'cancelled',
|
|
82
|
+
'no-show'
|
|
83
|
+
];
|
|
84
|
+
const labels = {};
|
|
85
|
+
for (const s of statuses){
|
|
86
|
+
// Attempt to look up a translation key, e.g. reservation:statusPending
|
|
87
|
+
const key = `reservation:status${s.charAt(0).toUpperCase() + s.slice(1).replace(/-./g, (m)=>m[1].toUpperCase())}`;
|
|
88
|
+
const translated = t(key);
|
|
89
|
+
// If translation returns the key itself, it's missing — fall back to capitalized status name
|
|
90
|
+
labels[s] = translated !== key ? translated : s.charAt(0).toUpperCase() + s.slice(1);
|
|
91
|
+
}
|
|
92
|
+
return labels;
|
|
93
|
+
}, [
|
|
94
|
+
statusMachine,
|
|
95
|
+
t
|
|
96
|
+
]);
|
|
35
97
|
const [currentDate, setCurrentDate] = useState(()=>new Date());
|
|
36
98
|
const [viewMode, setViewMode] = useState('month');
|
|
37
99
|
const [reservations, setReservations] = useState([]);
|
|
38
100
|
const [loading, setLoading] = useState(true);
|
|
39
101
|
const [drawerDocId, setDrawerDocId] = useState(null);
|
|
40
102
|
const [initialData, setInitialData] = useState(undefined);
|
|
103
|
+
// Pending tab state
|
|
104
|
+
const [pendingReservations, setPendingReservations] = useState([]);
|
|
105
|
+
const [pendingCount, setPendingCount] = useState(0);
|
|
106
|
+
const [selectedIds, setSelectedIds] = useState(()=>new Set());
|
|
107
|
+
const [confirmingIds, setConfirmingIds] = useState(()=>new Set());
|
|
108
|
+
const [actionFeedback, setActionFeedback] = useState(null);
|
|
41
109
|
const [DocumentDrawer, , { openDrawer }] = useDocumentDrawer({
|
|
42
110
|
id: drawerDocId ?? undefined,
|
|
43
111
|
collectionSlug: reservationSlug
|
|
@@ -75,7 +143,6 @@ export const CalendarView = ()=>{
|
|
|
75
143
|
const fetchReservations = useCallback(async ()=>{
|
|
76
144
|
setLoading(true);
|
|
77
145
|
try {
|
|
78
|
-
const apiUrl = `${config.serverURL ?? ''}${config.routes.api}/${reservationSlug}`;
|
|
79
146
|
const params = new URLSearchParams({
|
|
80
147
|
depth: '1',
|
|
81
148
|
limit: '500',
|
|
@@ -93,15 +160,205 @@ export const CalendarView = ()=>{
|
|
|
93
160
|
}, [
|
|
94
161
|
rangeStart,
|
|
95
162
|
rangeEnd,
|
|
96
|
-
|
|
97
|
-
config.serverURL,
|
|
98
|
-
reservationSlug
|
|
163
|
+
apiUrl
|
|
99
164
|
]);
|
|
100
165
|
useEffect(()=>{
|
|
101
166
|
void fetchReservations();
|
|
102
167
|
}, [
|
|
103
168
|
fetchReservations
|
|
104
169
|
]);
|
|
170
|
+
// Fetch pending count (always, for badge) — uses defaultStatus from config
|
|
171
|
+
const fetchPendingCount = useCallback(async ()=>{
|
|
172
|
+
try {
|
|
173
|
+
const params = new URLSearchParams({
|
|
174
|
+
limit: '0',
|
|
175
|
+
'where[status][equals]': defaultStatus
|
|
176
|
+
});
|
|
177
|
+
const response = await fetch(`${apiUrl}?${params}`);
|
|
178
|
+
const result = await response.json();
|
|
179
|
+
setPendingCount(result.totalDocs ?? 0);
|
|
180
|
+
} catch {
|
|
181
|
+
// silently ignore
|
|
182
|
+
}
|
|
183
|
+
}, [
|
|
184
|
+
apiUrl,
|
|
185
|
+
defaultStatus
|
|
186
|
+
]);
|
|
187
|
+
useEffect(()=>{
|
|
188
|
+
void fetchPendingCount();
|
|
189
|
+
}, [
|
|
190
|
+
fetchPendingCount
|
|
191
|
+
]);
|
|
192
|
+
// Fetch pending reservations when tab is active — uses defaultStatus from config
|
|
193
|
+
const fetchPendingReservations = useCallback(async ()=>{
|
|
194
|
+
try {
|
|
195
|
+
const params = new URLSearchParams({
|
|
196
|
+
depth: '1',
|
|
197
|
+
limit: '500',
|
|
198
|
+
sort: 'startTime',
|
|
199
|
+
'where[status][equals]': defaultStatus
|
|
200
|
+
});
|
|
201
|
+
const response = await fetch(`${apiUrl}?${params}`);
|
|
202
|
+
const result = await response.json();
|
|
203
|
+
setPendingReservations(result.docs ?? []);
|
|
204
|
+
} catch {
|
|
205
|
+
setPendingReservations([]);
|
|
206
|
+
}
|
|
207
|
+
}, [
|
|
208
|
+
apiUrl,
|
|
209
|
+
defaultStatus
|
|
210
|
+
]);
|
|
211
|
+
useEffect(()=>{
|
|
212
|
+
if (viewMode === 'pending') {
|
|
213
|
+
void fetchPendingReservations();
|
|
214
|
+
}
|
|
215
|
+
}, [
|
|
216
|
+
viewMode,
|
|
217
|
+
fetchPendingReservations
|
|
218
|
+
]);
|
|
219
|
+
// Clear selection when leaving pending view
|
|
220
|
+
useEffect(()=>{
|
|
221
|
+
if (viewMode !== 'pending') {
|
|
222
|
+
setSelectedIds(new Set());
|
|
223
|
+
setActionFeedback(null);
|
|
224
|
+
}
|
|
225
|
+
}, [
|
|
226
|
+
viewMode
|
|
227
|
+
]);
|
|
228
|
+
// Auto-clear feedback toast
|
|
229
|
+
useEffect(()=>{
|
|
230
|
+
if (!actionFeedback) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const timer = setTimeout(()=>setActionFeedback(null), 4000);
|
|
234
|
+
return ()=>clearTimeout(timer);
|
|
235
|
+
}, [
|
|
236
|
+
actionFeedback
|
|
237
|
+
]);
|
|
238
|
+
const patchReservation = useCallback(async (id, data)=>{
|
|
239
|
+
try {
|
|
240
|
+
const response = await fetch(`${apiUrl}/${id}`, {
|
|
241
|
+
body: JSON.stringify(data),
|
|
242
|
+
headers: {
|
|
243
|
+
'Content-Type': 'application/json'
|
|
244
|
+
},
|
|
245
|
+
method: 'PATCH'
|
|
246
|
+
});
|
|
247
|
+
return response.ok;
|
|
248
|
+
} catch {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}, [
|
|
252
|
+
apiUrl
|
|
253
|
+
]);
|
|
254
|
+
// Uses confirmStatus derived from config transitions
|
|
255
|
+
const handleQuickConfirm = useCallback(async (id)=>{
|
|
256
|
+
setConfirmingIds((prev)=>new Set(prev).add(id));
|
|
257
|
+
const ok = await patchReservation(id, {
|
|
258
|
+
status: confirmStatus
|
|
259
|
+
});
|
|
260
|
+
setConfirmingIds((prev)=>{
|
|
261
|
+
const next = new Set(prev);
|
|
262
|
+
next.delete(id);
|
|
263
|
+
return next;
|
|
264
|
+
});
|
|
265
|
+
setActionFeedback({
|
|
266
|
+
type: ok ? 'success' : 'error',
|
|
267
|
+
message: ok ? t('reservation:pendingConfirmSuccess') : t('reservation:pendingConfirmError')
|
|
268
|
+
});
|
|
269
|
+
if (ok) {
|
|
270
|
+
setSelectedIds((prev)=>{
|
|
271
|
+
const next = new Set(prev);
|
|
272
|
+
next.delete(id);
|
|
273
|
+
return next;
|
|
274
|
+
});
|
|
275
|
+
void fetchPendingReservations();
|
|
276
|
+
void fetchPendingCount();
|
|
277
|
+
}
|
|
278
|
+
}, [
|
|
279
|
+
patchReservation,
|
|
280
|
+
fetchPendingReservations,
|
|
281
|
+
fetchPendingCount,
|
|
282
|
+
t,
|
|
283
|
+
confirmStatus
|
|
284
|
+
]);
|
|
285
|
+
// Uses cancelStatus derived from config transitions
|
|
286
|
+
const handleQuickCancel = useCallback(async (id)=>{
|
|
287
|
+
setConfirmingIds((prev)=>new Set(prev).add(id));
|
|
288
|
+
const ok = await patchReservation(id, {
|
|
289
|
+
status: cancelStatus
|
|
290
|
+
});
|
|
291
|
+
setConfirmingIds((prev)=>{
|
|
292
|
+
const next = new Set(prev);
|
|
293
|
+
next.delete(id);
|
|
294
|
+
return next;
|
|
295
|
+
});
|
|
296
|
+
setActionFeedback({
|
|
297
|
+
type: ok ? 'success' : 'error',
|
|
298
|
+
message: ok ? t('reservation:pendingCancelSuccess') : t('reservation:pendingCancelError')
|
|
299
|
+
});
|
|
300
|
+
if (ok) {
|
|
301
|
+
setSelectedIds((prev)=>{
|
|
302
|
+
const next = new Set(prev);
|
|
303
|
+
next.delete(id);
|
|
304
|
+
return next;
|
|
305
|
+
});
|
|
306
|
+
void fetchPendingReservations();
|
|
307
|
+
void fetchPendingCount();
|
|
308
|
+
}
|
|
309
|
+
}, [
|
|
310
|
+
patchReservation,
|
|
311
|
+
fetchPendingReservations,
|
|
312
|
+
fetchPendingCount,
|
|
313
|
+
t,
|
|
314
|
+
cancelStatus
|
|
315
|
+
]);
|
|
316
|
+
const confirmSelected = useCallback(async ()=>{
|
|
317
|
+
const ids = Array.from(selectedIds);
|
|
318
|
+
if (ids.length === 0) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
setConfirmingIds((prev)=>{
|
|
322
|
+
const next = new Set(prev);
|
|
323
|
+
for (const id of ids){
|
|
324
|
+
next.add(id);
|
|
325
|
+
}
|
|
326
|
+
return next;
|
|
327
|
+
});
|
|
328
|
+
const results = await Promise.allSettled(ids.map((id)=>patchReservation(id, {
|
|
329
|
+
status: confirmStatus
|
|
330
|
+
})));
|
|
331
|
+
setConfirmingIds((prev)=>{
|
|
332
|
+
const next = new Set(prev);
|
|
333
|
+
for (const id of ids){
|
|
334
|
+
next.delete(id);
|
|
335
|
+
}
|
|
336
|
+
return next;
|
|
337
|
+
});
|
|
338
|
+
const succeeded = results.filter((r)=>r.status === 'fulfilled' && r.value).length;
|
|
339
|
+
const failed = ids.length - succeeded;
|
|
340
|
+
if (failed === 0) {
|
|
341
|
+
setActionFeedback({
|
|
342
|
+
type: 'success',
|
|
343
|
+
message: `${succeeded} ${t('reservation:pendingConfirmSuccess').toLowerCase()}`
|
|
344
|
+
});
|
|
345
|
+
} else {
|
|
346
|
+
setActionFeedback({
|
|
347
|
+
type: failed === ids.length ? 'error' : 'success',
|
|
348
|
+
message: t('reservation:pendingBulkConfirmSuccess').replace('{{succeeded}}', String(succeeded)).replace('{{failed}}', String(failed))
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
setSelectedIds(new Set());
|
|
352
|
+
void fetchPendingReservations();
|
|
353
|
+
void fetchPendingCount();
|
|
354
|
+
}, [
|
|
355
|
+
selectedIds,
|
|
356
|
+
patchReservation,
|
|
357
|
+
fetchPendingReservations,
|
|
358
|
+
fetchPendingCount,
|
|
359
|
+
t,
|
|
360
|
+
confirmStatus
|
|
361
|
+
]);
|
|
105
362
|
const handleEventClick = useCallback((e, id)=>{
|
|
106
363
|
e.stopPropagation();
|
|
107
364
|
setDrawerDocId(id);
|
|
@@ -129,6 +386,11 @@ export const CalendarView = ()=>{
|
|
|
129
386
|
});
|
|
130
387
|
pendingDrawerOpen.current = true;
|
|
131
388
|
}, []);
|
|
389
|
+
const openDocDrawer = useCallback((id)=>{
|
|
390
|
+
setDrawerDocId(id);
|
|
391
|
+
setInitialData(undefined);
|
|
392
|
+
pendingDrawerOpen.current = true;
|
|
393
|
+
}, []);
|
|
132
394
|
const navigate = useCallback((direction)=>{
|
|
133
395
|
setCurrentDate((prev)=>{
|
|
134
396
|
const next = new Date(prev);
|
|
@@ -154,6 +416,19 @@ export const CalendarView = ()=>{
|
|
|
154
416
|
}
|
|
155
417
|
return field.name ?? '';
|
|
156
418
|
};
|
|
419
|
+
const getCustomerName = (field)=>{
|
|
420
|
+
if (!field) {
|
|
421
|
+
return '';
|
|
422
|
+
}
|
|
423
|
+
if (typeof field === 'string') {
|
|
424
|
+
return '';
|
|
425
|
+
}
|
|
426
|
+
const parts = [
|
|
427
|
+
field.firstName,
|
|
428
|
+
field.lastName
|
|
429
|
+
].filter(Boolean);
|
|
430
|
+
return parts.length > 0 ? parts.join(' ') : field.name ?? '';
|
|
431
|
+
};
|
|
157
432
|
const getEventLabel = (r, compact)=>{
|
|
158
433
|
const time = new Date(r.startTime).toLocaleTimeString([], {
|
|
159
434
|
hour: '2-digit',
|
|
@@ -163,7 +438,7 @@ export const CalendarView = ()=>{
|
|
|
163
438
|
if (compact) {
|
|
164
439
|
return `${time} ${serviceName}`.trim();
|
|
165
440
|
}
|
|
166
|
-
const customerName =
|
|
441
|
+
const customerName = getCustomerName(r.customer);
|
|
167
442
|
const parts = [
|
|
168
443
|
time,
|
|
169
444
|
serviceName,
|
|
@@ -171,6 +446,19 @@ export const CalendarView = ()=>{
|
|
|
171
446
|
].filter(Boolean);
|
|
172
447
|
return parts.join(' - ');
|
|
173
448
|
};
|
|
449
|
+
// Returns all resource names for a reservation — from items array if present, otherwise top-level resource
|
|
450
|
+
const getResourceNames = (r)=>{
|
|
451
|
+
if (r.items && r.items.length > 0) {
|
|
452
|
+
const names = r.items.map((item)=>getResName(item.resource)).filter((name)=>name.length > 0);
|
|
453
|
+
if (names.length > 0) {
|
|
454
|
+
return names;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const single = getResName(r.resource);
|
|
458
|
+
return single ? [
|
|
459
|
+
single
|
|
460
|
+
] : [];
|
|
461
|
+
};
|
|
174
462
|
const getEventTooltip = (r)=>{
|
|
175
463
|
const serviceName = getResName(r.service) || t('reservation:calendarUnknownService');
|
|
176
464
|
const startStr = new Date(r.startTime).toLocaleTimeString([], {
|
|
@@ -181,23 +469,43 @@ export const CalendarView = ()=>{
|
|
|
181
469
|
hour: '2-digit',
|
|
182
470
|
minute: '2-digit'
|
|
183
471
|
}) : '?';
|
|
184
|
-
const customerName =
|
|
185
|
-
const
|
|
472
|
+
const customerName = getCustomerName(r.customer) || t('reservation:calendarUnknownCustomer');
|
|
473
|
+
const resourceNames = getResourceNames(r);
|
|
474
|
+
const resourceStr = resourceNames.length > 0 ? resourceNames.join(', ') : t('reservation:calendarUnknownResource');
|
|
186
475
|
const status = STATUS_LABELS[r.status] ?? r.status;
|
|
187
|
-
return
|
|
476
|
+
return [
|
|
477
|
+
serviceName,
|
|
478
|
+
`${startStr} - ${endStr}`,
|
|
479
|
+
`${t('reservation:tooltipCustomer')} ${customerName}`,
|
|
480
|
+
`${t('reservation:tooltipResource')} ${resourceStr}`,
|
|
481
|
+
`${t('reservation:tooltipStatus')} ${status}`
|
|
482
|
+
].join('\n');
|
|
188
483
|
};
|
|
189
|
-
const renderEventItem = (r, compact)
|
|
190
|
-
|
|
484
|
+
const renderEventItem = (r, compact)=>{
|
|
485
|
+
// Use CSS class for built-in statuses; also apply inline background for custom statuses
|
|
486
|
+
const cssClass = STATUS_CLASS_MAP[r.status] ?? '';
|
|
487
|
+
const color = STATUS_COLORS[r.status];
|
|
488
|
+
// Only apply inline style when there's no CSS class (custom statuses) or as a supplement
|
|
489
|
+
const inlineStyle = cssClass ? undefined : {
|
|
490
|
+
background: color
|
|
491
|
+
};
|
|
492
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
493
|
+
className: `${styles.eventItem} ${cssClass}`,
|
|
191
494
|
onClick: (e)=>handleEventClick(e, r.id),
|
|
192
495
|
onKeyDown: (e)=>handleEventKeyDown(e, r.id),
|
|
193
496
|
role: "button",
|
|
497
|
+
style: inlineStyle,
|
|
194
498
|
tabIndex: 0,
|
|
195
499
|
title: getEventTooltip(r),
|
|
196
500
|
children: getEventLabel(r, compact)
|
|
197
501
|
}, r.id);
|
|
198
|
-
|
|
502
|
+
};
|
|
503
|
+
// Dynamic legend: iterates all statuses from the status machine config
|
|
504
|
+
const renderStatusLegend = ()=>{
|
|
505
|
+
const statuses = statusMachine?.statuses ?? Object.keys(BUILTIN_STATUS_COLORS);
|
|
506
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
199
507
|
className: styles.statusLegend,
|
|
200
|
-
children:
|
|
508
|
+
children: statuses.map((key)=>/*#__PURE__*/ _jsxs("div", {
|
|
201
509
|
className: styles.legendItem,
|
|
202
510
|
children: [
|
|
203
511
|
/*#__PURE__*/ _jsx("span", {
|
|
@@ -206,10 +514,11 @@ export const CalendarView = ()=>{
|
|
|
206
514
|
background: STATUS_COLORS[key]
|
|
207
515
|
}
|
|
208
516
|
}),
|
|
209
|
-
|
|
517
|
+
STATUS_LABELS[key] ?? key
|
|
210
518
|
]
|
|
211
519
|
}, key))
|
|
212
520
|
});
|
|
521
|
+
};
|
|
213
522
|
const renderCurrentTimeLine = (cellDate, cellHour)=>{
|
|
214
523
|
const now = new Date();
|
|
215
524
|
if (now.getFullYear() !== cellDate.getFullYear() || now.getMonth() !== cellDate.getMonth() || now.getDate() !== cellDate.getDate() || now.getHours() !== cellHour) {
|
|
@@ -391,6 +700,181 @@ export const CalendarView = ()=>{
|
|
|
391
700
|
})
|
|
392
701
|
});
|
|
393
702
|
};
|
|
703
|
+
const renderPendingView = ()=>{
|
|
704
|
+
if (pendingReservations.length === 0) {
|
|
705
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
706
|
+
className: styles.pendingEmpty,
|
|
707
|
+
children: t('reservation:pendingEmpty')
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
const allSelected = pendingReservations.length > 0 && pendingReservations.every((r)=>selectedIds.has(r.id));
|
|
711
|
+
const toggleSelectAll = ()=>{
|
|
712
|
+
if (allSelected) {
|
|
713
|
+
setSelectedIds(new Set());
|
|
714
|
+
} else {
|
|
715
|
+
setSelectedIds(new Set(pendingReservations.map((r)=>r.id)));
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
const toggleSelect = (id)=>{
|
|
719
|
+
setSelectedIds((prev)=>{
|
|
720
|
+
const next = new Set(prev);
|
|
721
|
+
if (next.has(id)) {
|
|
722
|
+
next.delete(id);
|
|
723
|
+
} else {
|
|
724
|
+
next.add(id);
|
|
725
|
+
}
|
|
726
|
+
return next;
|
|
727
|
+
});
|
|
728
|
+
};
|
|
729
|
+
const formatDateTime = (iso)=>{
|
|
730
|
+
const d = new Date(iso);
|
|
731
|
+
return d.toLocaleString([], {
|
|
732
|
+
day: 'numeric',
|
|
733
|
+
hour: '2-digit',
|
|
734
|
+
minute: '2-digit',
|
|
735
|
+
month: 'short',
|
|
736
|
+
year: 'numeric'
|
|
737
|
+
});
|
|
738
|
+
};
|
|
739
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
740
|
+
className: styles.pendingView,
|
|
741
|
+
children: [
|
|
742
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
743
|
+
className: styles.pendingToolbar,
|
|
744
|
+
children: [
|
|
745
|
+
/*#__PURE__*/ _jsxs("label", {
|
|
746
|
+
className: styles.selectAllLabel,
|
|
747
|
+
children: [
|
|
748
|
+
/*#__PURE__*/ _jsx("input", {
|
|
749
|
+
"aria-label": t('reservation:pendingSelectAll'),
|
|
750
|
+
checked: allSelected,
|
|
751
|
+
onChange: toggleSelectAll,
|
|
752
|
+
type: "checkbox"
|
|
753
|
+
}),
|
|
754
|
+
t('reservation:pendingSelectAll')
|
|
755
|
+
]
|
|
756
|
+
}),
|
|
757
|
+
selectedIds.size > 0 && /*#__PURE__*/ _jsx("button", {
|
|
758
|
+
className: styles.bulkConfirmButton,
|
|
759
|
+
disabled: confirmingIds.size > 0,
|
|
760
|
+
onClick: ()=>void confirmSelected(),
|
|
761
|
+
type: "button",
|
|
762
|
+
children: confirmingIds.size > 0 ? t('reservation:pendingConfirming') : t('reservation:pendingConfirmSelected').replace('{{count}}', String(selectedIds.size))
|
|
763
|
+
})
|
|
764
|
+
]
|
|
765
|
+
}),
|
|
766
|
+
actionFeedback && /*#__PURE__*/ _jsx("div", {
|
|
767
|
+
className: `${styles.feedbackToast} ${actionFeedback.type === 'success' ? styles.feedbackSuccess : styles.feedbackError}`,
|
|
768
|
+
children: actionFeedback.message
|
|
769
|
+
}),
|
|
770
|
+
/*#__PURE__*/ _jsxs("table", {
|
|
771
|
+
className: styles.pendingTable,
|
|
772
|
+
children: [
|
|
773
|
+
/*#__PURE__*/ _jsx("thead", {
|
|
774
|
+
children: /*#__PURE__*/ _jsxs("tr", {
|
|
775
|
+
children: [
|
|
776
|
+
/*#__PURE__*/ _jsx("th", {
|
|
777
|
+
"aria-label": t('reservation:pendingSelectAll'),
|
|
778
|
+
className: styles.pendingTh
|
|
779
|
+
}),
|
|
780
|
+
/*#__PURE__*/ _jsx("th", {
|
|
781
|
+
className: styles.pendingTh,
|
|
782
|
+
children: t('reservation:fieldCustomer')
|
|
783
|
+
}),
|
|
784
|
+
/*#__PURE__*/ _jsx("th", {
|
|
785
|
+
className: styles.pendingTh,
|
|
786
|
+
children: t('reservation:fieldService')
|
|
787
|
+
}),
|
|
788
|
+
/*#__PURE__*/ _jsx("th", {
|
|
789
|
+
className: styles.pendingTh,
|
|
790
|
+
children: t('reservation:fieldResource')
|
|
791
|
+
}),
|
|
792
|
+
/*#__PURE__*/ _jsx("th", {
|
|
793
|
+
className: styles.pendingTh,
|
|
794
|
+
children: t('reservation:pendingDateTime')
|
|
795
|
+
}),
|
|
796
|
+
/*#__PURE__*/ _jsx("th", {
|
|
797
|
+
className: styles.pendingTh,
|
|
798
|
+
children: t('reservation:pendingActions')
|
|
799
|
+
})
|
|
800
|
+
]
|
|
801
|
+
})
|
|
802
|
+
}),
|
|
803
|
+
/*#__PURE__*/ _jsx("tbody", {
|
|
804
|
+
children: pendingReservations.map((r)=>{
|
|
805
|
+
const isConfirming = confirmingIds.has(r.id);
|
|
806
|
+
// Show all resources from items array if present, else top-level resource
|
|
807
|
+
const resourceDisplay = getResourceNames(r).join(', ') || t('reservation:calendarUnknownResource');
|
|
808
|
+
return /*#__PURE__*/ _jsxs("tr", {
|
|
809
|
+
className: styles.pendingRow,
|
|
810
|
+
children: [
|
|
811
|
+
/*#__PURE__*/ _jsx("td", {
|
|
812
|
+
className: styles.pendingTd,
|
|
813
|
+
children: /*#__PURE__*/ _jsx("input", {
|
|
814
|
+
"aria-label": getCustomerName(r.customer) || r.id,
|
|
815
|
+
checked: selectedIds.has(r.id),
|
|
816
|
+
onChange: ()=>toggleSelect(r.id),
|
|
817
|
+
type: "checkbox"
|
|
818
|
+
})
|
|
819
|
+
}),
|
|
820
|
+
/*#__PURE__*/ _jsx("td", {
|
|
821
|
+
className: styles.pendingTd,
|
|
822
|
+
children: /*#__PURE__*/ _jsx("span", {
|
|
823
|
+
className: styles.pendingCustomerLink,
|
|
824
|
+
onClick: ()=>openDocDrawer(r.id),
|
|
825
|
+
onKeyDown: (e)=>{
|
|
826
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
827
|
+
e.preventDefault();
|
|
828
|
+
openDocDrawer(r.id);
|
|
829
|
+
}
|
|
830
|
+
},
|
|
831
|
+
role: "button",
|
|
832
|
+
tabIndex: 0,
|
|
833
|
+
children: getCustomerName(r.customer) || t('reservation:calendarUnknownCustomer')
|
|
834
|
+
})
|
|
835
|
+
}),
|
|
836
|
+
/*#__PURE__*/ _jsx("td", {
|
|
837
|
+
className: styles.pendingTd,
|
|
838
|
+
children: getResName(r.service) || t('reservation:calendarUnknownService')
|
|
839
|
+
}),
|
|
840
|
+
/*#__PURE__*/ _jsx("td", {
|
|
841
|
+
className: styles.pendingTd,
|
|
842
|
+
children: resourceDisplay
|
|
843
|
+
}),
|
|
844
|
+
/*#__PURE__*/ _jsx("td", {
|
|
845
|
+
className: styles.pendingTd,
|
|
846
|
+
children: formatDateTime(r.startTime)
|
|
847
|
+
}),
|
|
848
|
+
/*#__PURE__*/ _jsxs("td", {
|
|
849
|
+
className: styles.pendingTd,
|
|
850
|
+
children: [
|
|
851
|
+
/*#__PURE__*/ _jsx("button", {
|
|
852
|
+
className: styles.confirmButton,
|
|
853
|
+
disabled: isConfirming,
|
|
854
|
+
onClick: ()=>void handleQuickConfirm(r.id),
|
|
855
|
+
title: t('reservation:pendingConfirm'),
|
|
856
|
+
type: "button",
|
|
857
|
+
children: "✓"
|
|
858
|
+
}),
|
|
859
|
+
/*#__PURE__*/ _jsx("button", {
|
|
860
|
+
className: styles.cancelButton,
|
|
861
|
+
disabled: isConfirming,
|
|
862
|
+
onClick: ()=>void handleQuickCancel(r.id),
|
|
863
|
+
title: t('reservation:pendingCancel'),
|
|
864
|
+
type: "button",
|
|
865
|
+
children: "✗"
|
|
866
|
+
})
|
|
867
|
+
]
|
|
868
|
+
})
|
|
869
|
+
]
|
|
870
|
+
}, r.id);
|
|
871
|
+
})
|
|
872
|
+
})
|
|
873
|
+
]
|
|
874
|
+
})
|
|
875
|
+
]
|
|
876
|
+
});
|
|
877
|
+
};
|
|
394
878
|
const dateLabel = useMemo(()=>{
|
|
395
879
|
if (viewMode === 'month') {
|
|
396
880
|
return currentDate.toLocaleDateString([], {
|
|
@@ -422,19 +906,25 @@ export const CalendarView = ()=>{
|
|
|
422
906
|
currentDate,
|
|
423
907
|
viewMode
|
|
424
908
|
]);
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
909
|
+
const handleDrawerSave = useCallback(()=>{
|
|
910
|
+
void fetchReservations();
|
|
911
|
+
void fetchPendingCount();
|
|
912
|
+
if (viewMode === 'pending') {
|
|
913
|
+
void fetchPendingReservations();
|
|
914
|
+
}
|
|
915
|
+
}, [
|
|
916
|
+
fetchReservations,
|
|
917
|
+
fetchPendingCount,
|
|
918
|
+
fetchPendingReservations,
|
|
919
|
+
viewMode
|
|
920
|
+
]);
|
|
431
921
|
return /*#__PURE__*/ _jsxs("div", {
|
|
432
922
|
className: styles.wrapper,
|
|
433
923
|
children: [
|
|
434
924
|
/*#__PURE__*/ _jsxs("div", {
|
|
435
925
|
className: styles.header,
|
|
436
926
|
children: [
|
|
437
|
-
/*#__PURE__*/ _jsxs("div", {
|
|
927
|
+
viewMode !== 'pending' && /*#__PURE__*/ _jsxs("div", {
|
|
438
928
|
className: styles.navButtons,
|
|
439
929
|
children: [
|
|
440
930
|
/*#__PURE__*/ _jsx("button", {
|
|
@@ -461,6 +951,7 @@ export const CalendarView = ()=>{
|
|
|
461
951
|
})
|
|
462
952
|
]
|
|
463
953
|
}),
|
|
954
|
+
viewMode === 'pending' && /*#__PURE__*/ _jsx("div", {}),
|
|
464
955
|
/*#__PURE__*/ _jsxs("div", {
|
|
465
956
|
className: styles.viewToggle,
|
|
466
957
|
children: [
|
|
@@ -482,24 +973,42 @@ export const CalendarView = ()=>{
|
|
|
482
973
|
{
|
|
483
974
|
key: 'day',
|
|
484
975
|
label: t('reservation:calendarDay')
|
|
976
|
+
},
|
|
977
|
+
{
|
|
978
|
+
key: 'pending',
|
|
979
|
+
label: t('reservation:calendarPending')
|
|
485
980
|
}
|
|
486
|
-
].map(({ key, label })=>/*#__PURE__*/
|
|
981
|
+
].map(({ key, label })=>/*#__PURE__*/ _jsxs("button", {
|
|
487
982
|
className: `${styles.viewToggleButton} ${viewMode === key ? styles.viewToggleButtonActive : ''}`,
|
|
488
983
|
onClick: ()=>setViewMode(key),
|
|
489
984
|
type: "button",
|
|
490
|
-
children:
|
|
985
|
+
children: [
|
|
986
|
+
label,
|
|
987
|
+
key === 'pending' && pendingCount > 0 && /*#__PURE__*/ _jsx("span", {
|
|
988
|
+
className: styles.pendingBadge,
|
|
989
|
+
children: pendingCount
|
|
990
|
+
})
|
|
991
|
+
]
|
|
491
992
|
}, key))
|
|
492
993
|
]
|
|
493
994
|
})
|
|
494
995
|
]
|
|
495
996
|
}),
|
|
496
|
-
renderStatusLegend(),
|
|
497
|
-
viewMode
|
|
498
|
-
|
|
499
|
-
|
|
997
|
+
viewMode !== 'pending' && renderStatusLegend(),
|
|
998
|
+
loading && viewMode !== 'pending' ? /*#__PURE__*/ _jsx("div", {
|
|
999
|
+
className: styles.loading,
|
|
1000
|
+
children: t('reservation:calendarLoading')
|
|
1001
|
+
}) : /*#__PURE__*/ _jsxs(_Fragment, {
|
|
1002
|
+
children: [
|
|
1003
|
+
viewMode === 'month' && renderMonthView(),
|
|
1004
|
+
viewMode === 'week' && renderWeekView(),
|
|
1005
|
+
viewMode === 'day' && renderDayView()
|
|
1006
|
+
]
|
|
1007
|
+
}),
|
|
1008
|
+
viewMode === 'pending' && renderPendingView(),
|
|
500
1009
|
/*#__PURE__*/ _jsx(DocumentDrawer, {
|
|
501
1010
|
initialData: initialData,
|
|
502
|
-
onSave:
|
|
1011
|
+
onSave: handleDrawerSave
|
|
503
1012
|
})
|
|
504
1013
|
]
|
|
505
1014
|
});
|