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.
Files changed (81) hide show
  1. package/README.md +61 -1141
  2. package/dist/collections/Customers.d.ts +3 -0
  3. package/dist/collections/Customers.js +58 -0
  4. package/dist/collections/Customers.js.map +1 -0
  5. package/dist/collections/Reservations.js +129 -35
  6. package/dist/collections/Reservations.js.map +1 -1
  7. package/dist/collections/Resources.js +70 -2
  8. package/dist/collections/Resources.js.map +1 -1
  9. package/dist/collections/Schedules.js +4 -1
  10. package/dist/collections/Schedules.js.map +1 -1
  11. package/dist/collections/Services.js +59 -2
  12. package/dist/collections/Services.js.map +1 -1
  13. package/dist/components/AvailabilityOverview/AvailabilityOverview.module.css +31 -0
  14. package/dist/components/AvailabilityOverview/index.js +59 -8
  15. package/dist/components/AvailabilityOverview/index.js.map +1 -1
  16. package/dist/components/CalendarView/CalendarView.module.css +171 -0
  17. package/dist/components/CalendarView/index.js +547 -38
  18. package/dist/components/CalendarView/index.js.map +1 -1
  19. package/dist/components/CustomerField/index.js +24 -10
  20. package/dist/components/CustomerField/index.js.map +1 -1
  21. package/dist/components/DashboardWidget/DashboardWidgetServer.js +21 -11
  22. package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
  23. package/dist/defaults.d.ts +1 -2
  24. package/dist/defaults.js +22 -4
  25. package/dist/defaults.js.map +1 -1
  26. package/dist/endpoints/cancelBooking.d.ts +3 -0
  27. package/dist/endpoints/cancelBooking.js +37 -0
  28. package/dist/endpoints/cancelBooking.js.map +1 -0
  29. package/dist/endpoints/checkAvailability.d.ts +3 -0
  30. package/dist/endpoints/checkAvailability.js +37 -0
  31. package/dist/endpoints/checkAvailability.js.map +1 -0
  32. package/dist/endpoints/createBooking.d.ts +3 -0
  33. package/dist/endpoints/createBooking.js +32 -0
  34. package/dist/endpoints/createBooking.js.map +1 -0
  35. package/dist/endpoints/customerSearch.js +76 -58
  36. package/dist/endpoints/customerSearch.js.map +1 -1
  37. package/dist/endpoints/getSlots.d.ts +3 -0
  38. package/dist/endpoints/getSlots.js +51 -0
  39. package/dist/endpoints/getSlots.js.map +1 -0
  40. package/dist/hooks/index.d.ts +2 -0
  41. package/dist/hooks/index.js +2 -0
  42. package/dist/hooks/index.js.map +1 -1
  43. package/dist/hooks/reservations/calculateEndTime.js +75 -9
  44. package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
  45. package/dist/hooks/reservations/checkIdempotency.d.ts +3 -0
  46. package/dist/hooks/reservations/checkIdempotency.js +32 -0
  47. package/dist/hooks/reservations/checkIdempotency.js.map +1 -0
  48. package/dist/hooks/reservations/onStatusChange.d.ts +3 -0
  49. package/dist/hooks/reservations/onStatusChange.js +41 -0
  50. package/dist/hooks/reservations/onStatusChange.js.map +1 -0
  51. package/dist/hooks/reservations/validateCancellation.js +1 -1
  52. package/dist/hooks/reservations/validateCancellation.js.map +1 -1
  53. package/dist/hooks/reservations/validateConflicts.js +34 -56
  54. package/dist/hooks/reservations/validateConflicts.js.map +1 -1
  55. package/dist/hooks/reservations/validateStatusTransition.d.ts +2 -1
  56. package/dist/hooks/reservations/validateStatusTransition.js +29 -6
  57. package/dist/hooks/reservations/validateStatusTransition.js.map +1 -1
  58. package/dist/index.d.ts +5 -1
  59. package/dist/index.js +3 -0
  60. package/dist/index.js.map +1 -1
  61. package/dist/plugin.js +49 -54
  62. package/dist/plugin.js.map +1 -1
  63. package/dist/services/AvailabilityService.d.ts +57 -0
  64. package/dist/services/AvailabilityService.js +232 -0
  65. package/dist/services/AvailabilityService.js.map +1 -0
  66. package/dist/services/index.d.ts +1 -0
  67. package/dist/services/index.js +3 -0
  68. package/dist/services/index.js.map +1 -0
  69. package/dist/translations/en.json +33 -1
  70. package/dist/translations/index.d.ts +5 -0
  71. package/dist/translations/index.js.map +1 -1
  72. package/dist/types.d.ts +84 -6
  73. package/dist/types.js +29 -9
  74. package/dist/types.js.map +1 -1
  75. package/dist/utilities/ownerAccess.d.ts +24 -0
  76. package/dist/utilities/ownerAccess.js +128 -0
  77. package/dist/utilities/ownerAccess.js.map +1 -0
  78. package/dist/utilities/resolveReservationItems.d.ts +18 -0
  79. package/dist/utilities/resolveReservationItems.js +45 -0
  80. package/dist/utilities/resolveReservationItems.js.map +1 -0
  81. 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
- const STATUS_COLORS = {
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
- config.routes.api,
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 = getResName(r.customer);
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 = getResName(r.customer) || t('reservation:calendarUnknownCustomer');
185
- const resourceName = getResName(r.resource) || t('reservation:calendarUnknownResource');
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 `${serviceName}\n${startStr} - ${endStr}\n${t('reservation:tooltipCustomer')} ${customerName}\n${t('reservation:tooltipResource')} ${resourceName}\n${t('reservation:tooltipStatus')} ${status}`;
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)=>/*#__PURE__*/ _jsx("div", {
190
- className: `${styles.eventItem} ${STATUS_CLASS_MAP[r.status] ?? ''}`,
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
- const renderStatusLegend = ()=>/*#__PURE__*/ _jsx("div", {
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: Object.entries(STATUS_LABELS).map(([key, label])=>/*#__PURE__*/ _jsxs("div", {
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
- label
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
- if (loading) {
426
- return /*#__PURE__*/ _jsx("div", {
427
- className: styles.loading,
428
- children: t('reservation:calendarLoading')
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__*/ _jsx("button", {
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: label
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 === 'month' && renderMonthView(),
498
- viewMode === 'week' && renderWeekView(),
499
- viewMode === 'day' && renderDayView(),
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: fetchReservations
1011
+ onSave: handleDrawerSave
503
1012
  })
504
1013
  ]
505
1014
  });