@dashadmin/dash-components 1.3.24 → 1.3.26

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 (111) hide show
  1. package/dist/components/AuditLog/AuditLog.js +345 -1
  2. package/dist/components/AuditLog/index.js +4 -1
  3. package/dist/components/AutoCompleteArrayInput/AutoCompleteCheckBoxInput.js +630 -3
  4. package/dist/components/AutoCompleteArrayInput/AutocompleteCheckBoxArrayInput.js +53 -1
  5. package/dist/components/AutoCompleteArrayInputAutoSearch/AutoCompleteCheckBoxInput.js +647 -3
  6. package/dist/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayInput.js +53 -1
  7. package/dist/components/Json/Json.js +235 -1
  8. package/dist/components/JsonColorSelector/JsonColorSelector.js +1172 -1
  9. package/dist/components/JsonColorSelector/JsonColorSelectorEnhanced.js +863 -2
  10. package/dist/components/JsonColorSelector/JsonCssVarValues.js +738 -1
  11. package/dist/components/JsonColorSelector/components/ColorEditDialog.js +426 -1
  12. package/dist/components/JsonColorSelector/components/ColorPaletteItem.js +75 -1
  13. package/dist/components/JsonColorSelector/components/ImageColorExtractor.js +283 -1
  14. package/dist/components/JsonColorSelector/helpers/functions.js +101 -1
  15. package/dist/components/JsonColorSelector/useAsyncColorThief.js +105 -1
  16. package/dist/components/ListActive/ListActive.js +126 -1
  17. package/dist/components/ListActive/ListBoolean.js +124 -1
  18. package/dist/components/NotificationPreferences/NotificationPreferences.js +317 -1
  19. package/dist/components/NotificationPreferences/index.js +7 -1
  20. package/dist/components/SearchableSelects/RASearchableSelect.js +236 -1
  21. package/dist/components/SearchableSelects/RASearchableSelectCheckboxes.js +479 -1
  22. package/dist/components/SearchableSelects/RASearchableSelectChips.js +465 -1
  23. package/dist/components/SearchableSelects/RASearchableSelectChipsRecordContext.js +295 -1
  24. package/dist/components/SearchableSelects/SearchableSelect.js +202 -1
  25. package/dist/components/SearchableSelects/SearchableSelectCheckboxes.js +290 -1
  26. package/dist/components/SortableDataGrid/SortableDataGrid.js +72 -1
  27. package/dist/components/SortableDataGrid/SortableDataGridBody.js +167 -1
  28. package/dist/components/SortableDataGrid/SortableDataGridHeader.js +109 -1
  29. package/dist/components/SortableDataGrid/SortableDataGridRow.js +283 -1
  30. package/dist/components/Upload/SingleImageUploader.js +40 -1
  31. package/dist/components/custom/PackageCopyMethod.js +78 -1
  32. package/dist/components/custom/PackageRadioButtonGroup.js +45 -1
  33. package/dist/components/dialog/Basic.js +8 -1
  34. package/dist/components/dialog/Correct.js +14 -1
  35. package/dist/components/dialog/Error.js +14 -1
  36. package/dist/components/dialog/Info.js +14 -1
  37. package/dist/components/dialog/InfoV2.js +21 -1
  38. package/dist/components/dialog/NotFound.js +14 -1
  39. package/dist/components/dialog/QuickSearch.js +117 -1
  40. package/dist/components/notifications/NotificationsCenter.js +597 -1
  41. package/dist/components/notifications/index.js +24 -1
  42. package/dist/components/tables/PaginationComponent.js +36 -1
  43. package/dist/components/theme/AppLoadingFallback.js +38 -2
  44. package/dist/components/theme/ErrorBoundary.js +52 -1
  45. package/dist/components/theme/InitialLoader.js +13 -1
  46. package/dist/components/theme/NotFound.js +136 -1
  47. package/dist/components/theme/NotResults.js +38 -1
  48. package/dist/hooks/useDraggable.js +6 -1
  49. package/dist/hooks/useNotifications.js +8 -1
  50. package/dist/hooks/useQuickSearch.js +26 -1
  51. package/dist/index.js +66 -5567
  52. package/dist/utils/setNativeValue.js +17 -1
  53. package/package.json +149 -132
  54. package/src/components/AuditLog/AuditLog.tsx +476 -0
  55. package/src/components/AuditLog/index.ts +2 -0
  56. package/src/components/AutoCompleteArrayInput/AutoCompleteCheckBoxInput.tsx +917 -0
  57. package/src/components/AutoCompleteArrayInput/AutocompleteCheckBoxArrayInput.tsx +39 -0
  58. package/src/components/AutoCompleteArrayInputAutoSearch/AutoCompleteCheckBoxInput.tsx +916 -0
  59. package/src/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayInput.tsx +44 -0
  60. package/src/components/Json/Json.tsx +312 -0
  61. package/src/components/JsonColorSelector/JsonColorSelector.tsx +1435 -0
  62. package/src/components/JsonColorSelector/JsonColorSelectorEnhanced.tsx +1029 -0
  63. package/src/components/JsonColorSelector/JsonCssVarValues.tsx +863 -0
  64. package/src/components/JsonColorSelector/components/ColorEditDialog.tsx +491 -0
  65. package/src/components/JsonColorSelector/components/ColorPaletteItem.tsx +80 -0
  66. package/src/components/JsonColorSelector/components/ImageColorExtractor.tsx +328 -0
  67. package/src/components/JsonColorSelector/helpers/functions.tsx +123 -0
  68. package/src/components/JsonColorSelector/interfaces/interfaces.tsx +22 -0
  69. package/src/components/JsonColorSelector/useAsyncColorThief.tsx +97 -0
  70. package/src/components/ListActive/ListActive.tsx +106 -0
  71. package/src/components/ListActive/ListBoolean.tsx +96 -0
  72. package/src/components/NotificationPreferences/NotificationPreferences.tsx +463 -0
  73. package/src/components/NotificationPreferences/index.tsx +2 -0
  74. package/src/components/SearchableSelects/RASearchableSelect.tsx +300 -0
  75. package/src/components/SearchableSelects/RASearchableSelectCheckboxes.tsx +604 -0
  76. package/src/components/SearchableSelects/RASearchableSelectChips.tsx +558 -0
  77. package/src/components/SearchableSelects/RASearchableSelectChipsRecordContext.tsx +362 -0
  78. package/src/components/SearchableSelects/SearchableSelect.tsx +321 -0
  79. package/src/components/SearchableSelects/SearchableSelectCheckboxes.tsx +355 -0
  80. package/src/components/SortableDataGrid/SortableDataGrid.tsx +56 -0
  81. package/src/components/SortableDataGrid/SortableDataGridBody.tsx +175 -0
  82. package/src/components/SortableDataGrid/SortableDataGridHeader.tsx +171 -0
  83. package/src/components/SortableDataGrid/SortableDataGridRow.tsx +247 -0
  84. package/src/components/Upload/SingleImageUploader.tsx +55 -0
  85. package/src/components/custom/PackageCopyMethod.tsx +109 -0
  86. package/src/components/custom/PackageRadioButtonGroup.tsx +47 -0
  87. package/src/components/dialog/Basic.tsx +11 -0
  88. package/src/components/dialog/Correct.tsx +16 -0
  89. package/src/components/dialog/Error.tsx +15 -0
  90. package/src/components/dialog/Info.tsx +15 -0
  91. package/src/components/dialog/InfoV2.tsx +35 -0
  92. package/src/components/dialog/NotFound.tsx +18 -0
  93. package/src/components/dialog/QuickSearch.tsx +115 -0
  94. package/src/components/notifications/NotificationsCenter.tsx +792 -0
  95. package/src/components/notifications/index.ts +42 -0
  96. package/src/components/tables/PaginationComponent.tsx +17 -0
  97. package/src/components/theme/AppLoadingFallback.tsx +42 -0
  98. package/src/components/theme/ErrorBoundary.tsx +57 -0
  99. package/src/components/theme/InitialLoader.tsx +16 -0
  100. package/src/components/theme/NotFound.tsx +124 -0
  101. package/src/components/theme/NotResults.tsx +54 -0
  102. package/src/hooks/useDraggable.ts +11 -0
  103. package/src/hooks/useNotifications.ts +14 -0
  104. package/src/hooks/useQuickSearch.tsx +29 -0
  105. package/src/index.tsx +77 -0
  106. package/src/utils/setNativeValue.tsx +23 -0
  107. package/dist/color-thief-CTwGFhOB.js +0 -272
  108. /package/{dist → src}/components/AutoCompleteArrayInputAutoSearch/AutocompleteCheckBoxArrayAutoSearch.tsx.wip +0 -0
  109. /package/{dist → src}/components/JsonColorSelector/JsonColorSelector.tsx.bkup +0 -0
  110. /package/{dist → src}/components/SortableDataGrid/README.md +0 -0
  111. /package/{dist → src}/styles/index.less +0 -0
@@ -0,0 +1,792 @@
1
+ import React, { useState, useCallback, useEffect } from 'react';
2
+ import {
3
+ Button,
4
+ Badge,
5
+ Menu,
6
+ MenuItem,
7
+ Typography,
8
+ Divider,
9
+ Box,
10
+ IconButton,
11
+ Chip,
12
+ Snackbar,
13
+ Alert,
14
+ Stack,
15
+ Paper,
16
+ Portal
17
+ } from '@mui/material';
18
+ import {
19
+ NotificationsNone,
20
+ NotificationsActive,
21
+ Close as CloseIcon,
22
+ Clear as ClearIcon,
23
+ Person as PersonIcon,
24
+ Store as StoreIcon,
25
+ DragIndicator as DragIcon
26
+ } from '@mui/icons-material';
27
+
28
+ // ============================================================================
29
+ // TYPES & INTERFACES
30
+ // ============================================================================
31
+
32
+ export interface NotificationData {
33
+ id: string;
34
+ title: string;
35
+ message: string;
36
+ customer?: string;
37
+ store?: string;
38
+ timestamp: string;
39
+ isRead: boolean;
40
+ data?: any;
41
+ }
42
+
43
+ export interface ToastNotification {
44
+ id: string;
45
+ title: string;
46
+ message: string;
47
+ severity: 'info' | 'warning' | 'error' | 'success';
48
+ }
49
+
50
+ export interface DialogNotification extends NotificationData {
51
+ position: { x: number; y: number };
52
+ zIndex: number;
53
+ }
54
+
55
+ export interface NotificationsCenterConfig {
56
+ /** Maximum number of notifications to keep */
57
+ maxNotifications?: number;
58
+ /** Auto-hide duration for toast notifications (ms) */
59
+ toastAutoHideDuration?: number;
60
+ /** Base z-index for notifications */
61
+ baseZIndex?: number;
62
+ /** Menu max height */
63
+ menuMaxHeight?: number;
64
+ /** Menu width */
65
+ menuWidth?: number;
66
+ /** Custom labels for localization */
67
+ labels?: NotificationsCenterLabels;
68
+ /** Custom icons */
69
+ icons?: NotificationsCenterIcons;
70
+ }
71
+
72
+ export interface NotificationsCenterLabels {
73
+ title?: string;
74
+ markAll?: string;
75
+ noNotifications?: string;
76
+ close?: string;
77
+ acknowledge?: string;
78
+ customer?: string;
79
+ store?: string;
80
+ received?: string;
81
+ now?: string;
82
+ minutesAgo?: string;
83
+ hoursAgo?: string;
84
+ daysAgo?: string;
85
+ }
86
+
87
+ export interface NotificationsCenterIcons {
88
+ empty?: React.ReactNode;
89
+ active?: React.ReactNode;
90
+ customer?: React.ReactNode;
91
+ store?: React.ReactNode;
92
+ }
93
+
94
+ // Default configuration
95
+ const defaultConfig: Required<NotificationsCenterConfig> = {
96
+ maxNotifications: 50,
97
+ toastAutoHideDuration: 8000,
98
+ baseZIndex: 100000,
99
+ menuMaxHeight: 480,
100
+ menuWidth: 360,
101
+ labels: {
102
+ title: 'Notifications',
103
+ markAll: 'Mark all',
104
+ noNotifications: 'No notifications',
105
+ close: 'Close',
106
+ acknowledge: 'Got it',
107
+ customer: 'Customer:',
108
+ store: 'Store:',
109
+ received: 'Received:',
110
+ now: 'Now',
111
+ minutesAgo: 'm',
112
+ hoursAgo: 'h',
113
+ daysAgo: 'd',
114
+ },
115
+ icons: {},
116
+ };
117
+
118
+ // ============================================================================
119
+ // UTILITIES
120
+ // ============================================================================
121
+
122
+ /**
123
+ * Format timestamp to relative time (e.g., "2m", "1h", "3d")
124
+ */
125
+ export const formatTimeAgo = (timestamp: string, labels?: NotificationsCenterLabels): string => {
126
+ const now = new Date();
127
+ const notificationTime = new Date(timestamp);
128
+ const diffInMinutes = Math.floor((now.getTime() - notificationTime.getTime()) / (1000 * 60));
129
+
130
+ const l = { ...defaultConfig.labels, ...labels };
131
+
132
+ if (diffInMinutes < 1) return l.now || 'Now';
133
+ if (diffInMinutes < 60) return `${diffInMinutes}${l.minutesAgo}`;
134
+ if (diffInMinutes < 1440) return `${Math.floor(diffInMinutes / 60)}${l.hoursAgo}`;
135
+ return `${Math.floor(diffInMinutes / 1440)}${l.daysAgo}`;
136
+ };
137
+
138
+ /**
139
+ * Generate unique notification ID
140
+ */
141
+ export const generateNotificationId = (): string => {
142
+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
143
+ };
144
+
145
+ // ============================================================================
146
+ // HOOKS
147
+ // ============================================================================
148
+
149
+ export interface UseNotificationsCenterReturn {
150
+ notifications: NotificationData[];
151
+ toastNotifications: ToastNotification[];
152
+ dialogNotifications: DialogNotification[];
153
+ unreadCount: number;
154
+ addNotification: (notification: Omit<NotificationData, 'id' | 'timestamp' | 'isRead'>, options?: { showDialog?: boolean; showToast?: boolean; toastSeverity?: ToastNotification['severity'] }) => void;
155
+ markAsRead: (notificationId: string) => void;
156
+ markAllAsRead: () => void;
157
+ removeNotification: (notificationId: string) => void;
158
+ clearAllNotifications: () => void;
159
+ closeToast: (toastId: string) => void;
160
+ closeDialog: (notificationId: string) => void;
161
+ acknowledgeDialog: (notificationId: string) => void;
162
+ bringDialogToFront: (notificationId: string) => void;
163
+ updateDialogPosition: (notificationId: string, position: { x: number; y: number }) => void;
164
+ }
165
+
166
+ /**
167
+ * Hook for managing notifications state
168
+ */
169
+ export const useNotificationsCenter = (
170
+ config: NotificationsCenterConfig = {}
171
+ ): UseNotificationsCenterReturn => {
172
+ const mergedConfig = { ...defaultConfig, ...config };
173
+
174
+ const [notifications, setNotifications] = useState<NotificationData[]>([]);
175
+ const [toastNotifications, setToastNotifications] = useState<ToastNotification[]>([]);
176
+ const [dialogNotifications, setDialogNotifications] = useState<DialogNotification[]>([]);
177
+
178
+ const unreadCount = notifications.filter(n => !n.isRead).length;
179
+
180
+ const addNotification = useCallback((
181
+ notification: Omit<NotificationData, 'id' | 'timestamp' | 'isRead'>,
182
+ options: { showDialog?: boolean; showToast?: boolean; toastSeverity?: ToastNotification['severity'] } = {}
183
+ ) => {
184
+ const { showDialog = true, showToast = true, toastSeverity = 'warning' } = options;
185
+
186
+ const processedNotification: NotificationData = {
187
+ ...notification,
188
+ id: generateNotificationId(),
189
+ timestamp: new Date().toISOString(),
190
+ isRead: false,
191
+ };
192
+
193
+ // Add to main notifications list
194
+ setNotifications(prev => [processedNotification, ...prev.slice(0, mergedConfig.maxNotifications - 1)]);
195
+
196
+ // Add dialog notification
197
+ if (showDialog) {
198
+ setDialogNotifications(prev => {
199
+ const dialogCount = prev.length;
200
+ const baseX = 20;
201
+ const baseY = typeof window !== 'undefined' ? window.innerHeight - 250 : 600;
202
+ const offsetY = dialogCount * -220;
203
+
204
+ const dialogNotification: DialogNotification = {
205
+ ...processedNotification,
206
+ position: { x: baseX, y: baseY + offsetY },
207
+ zIndex: mergedConfig.baseZIndex + dialogCount + 1,
208
+ };
209
+ return [...prev, dialogNotification];
210
+ });
211
+ }
212
+
213
+ // Add toast notification
214
+ if (showToast) {
215
+ const toastId = `toast-${processedNotification.id}`;
216
+ setToastNotifications(prev => [...prev, {
217
+ id: toastId,
218
+ title: processedNotification.title,
219
+ message: `${processedNotification.customer || ''} ${processedNotification.store ? `- ${processedNotification.store}` : ''}`.trim(),
220
+ severity: toastSeverity,
221
+ }]);
222
+ }
223
+ }, [mergedConfig.maxNotifications, mergedConfig.baseZIndex]);
224
+
225
+ const markAsRead = useCallback((notificationId: string) => {
226
+ setNotifications(prev =>
227
+ prev.map(n => n.id === notificationId ? { ...n, isRead: true } : n)
228
+ );
229
+ }, []);
230
+
231
+ const markAllAsRead = useCallback(() => {
232
+ setNotifications(prev => prev.map(n => ({ ...n, isRead: true })));
233
+ }, []);
234
+
235
+ const removeNotification = useCallback((notificationId: string) => {
236
+ setNotifications(prev => prev.filter(n => n.id !== notificationId));
237
+ }, []);
238
+
239
+ const clearAllNotifications = useCallback(() => {
240
+ setNotifications([]);
241
+ }, []);
242
+
243
+ const closeToast = useCallback((toastId: string) => {
244
+ setToastNotifications(prev => prev.filter(t => t.id !== toastId));
245
+ }, []);
246
+
247
+ const closeDialog = useCallback((notificationId: string) => {
248
+ setDialogNotifications(prev => prev.filter(d => d.id !== notificationId));
249
+ markAsRead(notificationId);
250
+ }, [markAsRead]);
251
+
252
+ const acknowledgeDialog = useCallback((notificationId: string) => {
253
+ markAsRead(notificationId);
254
+ setDialogNotifications(prev => prev.filter(d => d.id !== notificationId));
255
+ }, [markAsRead]);
256
+
257
+ const bringDialogToFront = useCallback((notificationId: string) => {
258
+ setDialogNotifications(prev => {
259
+ const maxZ = Math.max(...prev.map(p => p.zIndex), mergedConfig.baseZIndex);
260
+ return prev.map(d =>
261
+ d.id === notificationId
262
+ ? { ...d, zIndex: maxZ + 1 }
263
+ : d
264
+ );
265
+ });
266
+ }, [mergedConfig.baseZIndex]);
267
+
268
+ const updateDialogPosition = useCallback((notificationId: string, newPosition: { x: number; y: number }) => {
269
+ setDialogNotifications(prev =>
270
+ prev.map(d =>
271
+ d.id === notificationId
272
+ ? { ...d, position: newPosition }
273
+ : d
274
+ )
275
+ );
276
+ }, []);
277
+
278
+ return {
279
+ notifications,
280
+ toastNotifications,
281
+ dialogNotifications,
282
+ unreadCount,
283
+ addNotification,
284
+ markAsRead,
285
+ markAllAsRead,
286
+ removeNotification,
287
+ clearAllNotifications,
288
+ closeToast,
289
+ closeDialog,
290
+ acknowledgeDialog,
291
+ bringDialogToFront,
292
+ updateDialogPosition,
293
+ };
294
+ };
295
+
296
+ // ============================================================================
297
+ // COMPONENTS
298
+ // ============================================================================
299
+
300
+ // Notification Button Component
301
+ export interface NotificationButtonProps {
302
+ unreadCount: number;
303
+ onClick: (event: React.MouseEvent<HTMLElement>) => void;
304
+ icons?: NotificationsCenterIcons;
305
+ }
306
+
307
+ export const NotificationButton: React.FC<NotificationButtonProps> = ({
308
+ unreadCount,
309
+ onClick,
310
+ icons,
311
+ }) => {
312
+ return (
313
+ <IconButton
314
+ onClick={onClick}
315
+ aria-label="notifications"
316
+ color="inherit"
317
+ >
318
+ <Badge badgeContent={unreadCount} color="error" max={99}>
319
+ {unreadCount > 0
320
+ ? (icons?.active || <NotificationsActive />)
321
+ : (icons?.empty || <NotificationsNone />)
322
+ }
323
+ </Badge>
324
+ </IconButton>
325
+ );
326
+ };
327
+
328
+ // Notification Menu Item Component
329
+ export interface NotificationMenuItemProps {
330
+ notification: NotificationData;
331
+ onMarkAsRead: (id: string) => void;
332
+ onRemove: (id: string) => void;
333
+ formatTimeAgo: (timestamp: string) => string;
334
+ }
335
+
336
+ export const NotificationMenuItem: React.FC<NotificationMenuItemProps> = ({
337
+ notification,
338
+ onMarkAsRead,
339
+ onRemove,
340
+ formatTimeAgo,
341
+ }) => {
342
+ return (
343
+ <MenuItem
344
+ onClick={() => onMarkAsRead(notification.id)}
345
+ sx={{
346
+ backgroundColor: notification.isRead ? 'transparent' : 'action.hover',
347
+ borderLeft: notification.isRead ? 'none' : '4px solid #ff9800',
348
+ whiteSpace: 'normal',
349
+ alignItems: 'flex-start',
350
+ py: 1.5,
351
+ }}
352
+ >
353
+ <Box sx={{ width: '100%' }}>
354
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 0.5 }}>
355
+ <Typography variant="subtitle2" sx={{ fontWeight: notification.isRead ? 'normal' : 'bold' }}>
356
+ {notification.title}
357
+ </Typography>
358
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
359
+ <Typography variant="caption" color="text.secondary">
360
+ {formatTimeAgo(notification.timestamp)}
361
+ </Typography>
362
+ <IconButton
363
+ size="small"
364
+ onClick={(e) => {
365
+ e.stopPropagation();
366
+ onRemove(notification.id);
367
+ }}
368
+ >
369
+ <CloseIcon fontSize="small" />
370
+ </IconButton>
371
+ </Box>
372
+ </Box>
373
+ <Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
374
+ {notification.message}
375
+ </Typography>
376
+ <Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
377
+ {notification.customer && (
378
+ <Chip
379
+ label={notification.customer}
380
+ size="small"
381
+ variant="outlined"
382
+ color="primary"
383
+ />
384
+ )}
385
+ {notification.store && (
386
+ <Chip
387
+ label={notification.store}
388
+ size="small"
389
+ variant="outlined"
390
+ color="secondary"
391
+ />
392
+ )}
393
+ </Box>
394
+ </Box>
395
+ </MenuItem>
396
+ );
397
+ };
398
+
399
+ // Notifications Menu Component
400
+ export interface NotificationsMenuProps {
401
+ anchorEl: HTMLElement | null;
402
+ notifications: NotificationData[];
403
+ onClose: () => void;
404
+ onMarkAsRead: (id: string) => void;
405
+ onMarkAllAsRead: () => void;
406
+ onRemove: (id: string) => void;
407
+ onClearAll: () => void;
408
+ config?: NotificationsCenterConfig;
409
+ }
410
+
411
+ export const NotificationsMenu: React.FC<NotificationsMenuProps> = ({
412
+ anchorEl,
413
+ notifications,
414
+ onClose,
415
+ onMarkAsRead,
416
+ onMarkAllAsRead,
417
+ onRemove,
418
+ onClearAll,
419
+ config = {},
420
+ }) => {
421
+ const mergedConfig = { ...defaultConfig, ...config };
422
+ const labels = { ...defaultConfig.labels, ...config.labels };
423
+ const open = Boolean(anchorEl);
424
+ const unreadCount = notifications.filter(n => !n.isRead).length;
425
+
426
+ return (
427
+ <Menu
428
+ anchorEl={anchorEl}
429
+ open={open}
430
+ onClose={onClose}
431
+ PaperProps={{
432
+ style: {
433
+ maxHeight: mergedConfig.menuMaxHeight,
434
+ width: mergedConfig.menuWidth,
435
+ },
436
+ }}
437
+ transformOrigin={{ horizontal: 'right', vertical: 'top' }}
438
+ anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
439
+ >
440
+ <Box sx={{ p: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
441
+ <Typography variant="h6">{labels.title}</Typography>
442
+ {notifications.length > 0 && (
443
+ <Box>
444
+ {unreadCount > 0 && (
445
+ <Button size="small" onClick={onMarkAllAsRead} sx={{ mr: 1 }}>
446
+ {labels.markAll}
447
+ </Button>
448
+ )}
449
+ <IconButton size="small" onClick={onClearAll}>
450
+ <ClearIcon fontSize="small" />
451
+ </IconButton>
452
+ </Box>
453
+ )}
454
+ </Box>
455
+ <Divider />
456
+ {notifications.length === 0 ? (
457
+ <MenuItem disabled>
458
+ <Typography color="text.secondary">{labels.noNotifications}</Typography>
459
+ </MenuItem>
460
+ ) : (
461
+ notifications.map((notification) => (
462
+ <NotificationMenuItem
463
+ key={notification.id}
464
+ notification={notification}
465
+ onMarkAsRead={onMarkAsRead}
466
+ onRemove={onRemove}
467
+ formatTimeAgo={(ts) => formatTimeAgo(ts, labels)}
468
+ />
469
+ ))
470
+ )}
471
+ </Menu>
472
+ );
473
+ };
474
+
475
+ // Toast Notifications Stack Component
476
+ export interface ToastNotificationsStackProps {
477
+ toasts: ToastNotification[];
478
+ onClose: (toastId: string) => void;
479
+ autoHideDuration?: number;
480
+ baseZIndex?: number;
481
+ }
482
+
483
+ export const ToastNotificationsStack: React.FC<ToastNotificationsStackProps> = ({
484
+ toasts,
485
+ onClose,
486
+ autoHideDuration = 8000,
487
+ baseZIndex = 100000,
488
+ }) => {
489
+ return (
490
+ <Stack
491
+ spacing={1}
492
+ sx={{
493
+ position: 'fixed',
494
+ top: 16,
495
+ right: 16,
496
+ zIndex: baseZIndex,
497
+ maxWidth: 400,
498
+ }}
499
+ >
500
+ {toasts.map((toast) => (
501
+ <Snackbar
502
+ key={toast.id}
503
+ open={true}
504
+ autoHideDuration={autoHideDuration}
505
+ onClose={() => onClose(toast.id)}
506
+ anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
507
+ sx={{ position: 'relative', zIndex: baseZIndex }}
508
+ >
509
+ <Alert
510
+ onClose={() => onClose(toast.id)}
511
+ severity={toast.severity}
512
+ variant="filled"
513
+ sx={{ width: '100%' }}
514
+ >
515
+ <Typography variant="subtitle2">{toast.title}</Typography>
516
+ <Typography variant="body2">{toast.message}</Typography>
517
+ </Alert>
518
+ </Snackbar>
519
+ ))}
520
+ </Stack>
521
+ );
522
+ };
523
+
524
+ // Draggable Notification Dialog Component
525
+ export interface DraggableNotificationDialogProps {
526
+ notification: DialogNotification;
527
+ onClose: () => void;
528
+ onAcknowledge: () => void;
529
+ onFocus: () => void;
530
+ onPositionChange: (position: { x: number; y: number }) => void;
531
+ config?: NotificationsCenterConfig;
532
+ }
533
+
534
+ export const DraggableNotificationDialog: React.FC<DraggableNotificationDialogProps> = ({
535
+ notification,
536
+ onClose,
537
+ onAcknowledge,
538
+ onFocus,
539
+ onPositionChange,
540
+ config = {},
541
+ }) => {
542
+ const labels = { ...defaultConfig.labels, ...config.labels };
543
+ const icons = { ...defaultConfig.icons, ...config.icons };
544
+
545
+ const [isDragging, setIsDragging] = useState(false);
546
+ const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
547
+
548
+ const handleMouseDown = useCallback((e: React.MouseEvent) => {
549
+ if ((e.target as HTMLElement).closest('.drag-handle')) {
550
+ e.preventDefault();
551
+ onFocus();
552
+ setIsDragging(true);
553
+ setDragOffset({
554
+ x: e.clientX - notification.position.x,
555
+ y: e.clientY - notification.position.y,
556
+ });
557
+ }
558
+ }, [onFocus, notification.position]);
559
+
560
+ const handleMouseMove = useCallback((e: MouseEvent) => {
561
+ if (isDragging) {
562
+ e.preventDefault();
563
+ const newX = Math.max(0, Math.min(window.innerWidth - 400, e.clientX - dragOffset.x));
564
+ const newY = Math.max(0, Math.min(window.innerHeight - 200, e.clientY - dragOffset.y));
565
+ onPositionChange({ x: newX, y: newY });
566
+ }
567
+ }, [isDragging, dragOffset, onPositionChange]);
568
+
569
+ const handleMouseUp = useCallback(() => {
570
+ if (isDragging) {
571
+ setIsDragging(false);
572
+ }
573
+ }, [isDragging]);
574
+
575
+ useEffect(() => {
576
+ if (isDragging) {
577
+ document.addEventListener('mousemove', handleMouseMove);
578
+ document.addEventListener('mouseup', handleMouseUp);
579
+ document.body.style.userSelect = 'none';
580
+ return () => {
581
+ document.removeEventListener('mousemove', handleMouseMove);
582
+ document.removeEventListener('mouseup', handleMouseUp);
583
+ document.body.style.userSelect = '';
584
+ };
585
+ }
586
+ }, [isDragging, handleMouseMove, handleMouseUp]);
587
+
588
+ return (
589
+ <Portal>
590
+ <div
591
+ style={{
592
+ position: 'fixed',
593
+ left: notification.position.x,
594
+ top: notification.position.y,
595
+ zIndex: notification.zIndex,
596
+ pointerEvents: 'auto',
597
+ }}
598
+ onMouseDown={handleMouseDown}
599
+ >
600
+ <Paper
601
+ elevation={8}
602
+ sx={{
603
+ width: 400,
604
+ borderRadius: 2,
605
+ overflow: 'hidden',
606
+ boxShadow: '0 8px 32px rgba(0,0,0,0.24)',
607
+ cursor: isDragging ? 'grabbing' : 'default',
608
+ '&:hover': {
609
+ boxShadow: '0 12px 40px rgba(0,0,0,0.3)',
610
+ },
611
+ }}
612
+ >
613
+ {/* Header */}
614
+ <Box
615
+ className="drag-handle"
616
+ sx={{
617
+ backgroundColor: 'warning.main',
618
+ color: 'warning.contrastText',
619
+ p: 2,
620
+ display: 'flex',
621
+ alignItems: 'center',
622
+ gap: 1,
623
+ cursor: isDragging ? 'grabbing' : 'grab',
624
+ userSelect: 'none',
625
+ }}
626
+ >
627
+ <DragIcon sx={{ mr: 1 }} />
628
+ <NotificationsActive />
629
+ <Typography variant="h6" sx={{ flexGrow: 1, fontSize: '1rem' }}>
630
+ {notification.title}
631
+ </Typography>
632
+ <IconButton
633
+ size="small"
634
+ onClick={(e) => {
635
+ e.stopPropagation();
636
+ onClose();
637
+ }}
638
+ sx={{ color: 'warning.contrastText' }}
639
+ >
640
+ <CloseIcon fontSize="small" />
641
+ </IconButton>
642
+ </Box>
643
+
644
+ {/* Content */}
645
+ <Box sx={{ p: 2 }}>
646
+ <Typography variant="body1" sx={{ mb: 2 }}>
647
+ {notification.message}
648
+ </Typography>
649
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
650
+ {notification.customer && (
651
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
652
+ {icons.customer || <PersonIcon color="primary" />}
653
+ <Typography variant="body2" color="text.secondary">
654
+ {labels.customer}
655
+ </Typography>
656
+ <Chip
657
+ label={notification.customer}
658
+ size="small"
659
+ color="primary"
660
+ variant="outlined"
661
+ />
662
+ </Box>
663
+ )}
664
+ {notification.store && (
665
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
666
+ {icons.store || <StoreIcon color="secondary" />}
667
+ <Typography variant="body2" color="text.secondary">
668
+ {labels.store}
669
+ </Typography>
670
+ <Chip
671
+ label={notification.store}
672
+ size="small"
673
+ color="secondary"
674
+ variant="outlined"
675
+ />
676
+ </Box>
677
+ )}
678
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 1 }}>
679
+ <Typography variant="caption" color="text.secondary">
680
+ {labels.received} {formatTimeAgo(notification.timestamp, labels)}
681
+ </Typography>
682
+ </Box>
683
+ </Box>
684
+ </Box>
685
+
686
+ {/* Footer */}
687
+ <Box sx={{ p: 2, pt: 0, display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
688
+ <Button
689
+ onClick={(e) => {
690
+ e.stopPropagation();
691
+ onClose();
692
+ }}
693
+ color="inherit"
694
+ variant="outlined"
695
+ size="small"
696
+ >
697
+ {labels.close}
698
+ </Button>
699
+ <Button
700
+ onClick={(e) => {
701
+ e.stopPropagation();
702
+ onAcknowledge();
703
+ }}
704
+ color="primary"
705
+ variant="contained"
706
+ size="small"
707
+ >
708
+ {labels.acknowledge}
709
+ </Button>
710
+ </Box>
711
+ </Paper>
712
+ </div>
713
+ </Portal>
714
+ );
715
+ };
716
+
717
+ // Main NotificationsCenter Component
718
+ export interface NotificationsCenterProps {
719
+ /** Configuration options */
720
+ config?: NotificationsCenterConfig;
721
+ /** External notifications hook (if managed externally) */
722
+ notificationsHook?: UseNotificationsCenterReturn;
723
+ /** Called when a notification is added (for external integration) */
724
+ onNotificationReceived?: (notification: NotificationData) => void;
725
+ }
726
+
727
+ export const NotificationsCenter: React.FC<NotificationsCenterProps> = ({
728
+ config = {},
729
+ notificationsHook,
730
+ }) => {
731
+ const internalHook = useNotificationsCenter(config);
732
+ const hook = notificationsHook || internalHook;
733
+
734
+ const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
735
+
736
+ const handleNotificationClick = (event: React.MouseEvent<HTMLElement>) => {
737
+ setAnchorEl(event.currentTarget);
738
+ };
739
+
740
+ const handleClose = () => {
741
+ setAnchorEl(null);
742
+ };
743
+
744
+ const handleClearAll = () => {
745
+ hook.clearAllNotifications();
746
+ handleClose();
747
+ };
748
+
749
+ return (
750
+ <>
751
+ <NotificationButton
752
+ unreadCount={hook.unreadCount}
753
+ onClick={handleNotificationClick}
754
+ icons={config.icons}
755
+ />
756
+
757
+ <NotificationsMenu
758
+ anchorEl={anchorEl}
759
+ notifications={hook.notifications}
760
+ onClose={handleClose}
761
+ onMarkAsRead={hook.markAsRead}
762
+ onMarkAllAsRead={hook.markAllAsRead}
763
+ onRemove={hook.removeNotification}
764
+ onClearAll={handleClearAll}
765
+ config={config}
766
+ />
767
+
768
+ {/* Stackable Draggable Notification Dialogs */}
769
+ {hook.dialogNotifications.map((dialogNotification) => (
770
+ <DraggableNotificationDialog
771
+ key={dialogNotification.id}
772
+ notification={dialogNotification}
773
+ onClose={() => hook.closeDialog(dialogNotification.id)}
774
+ onAcknowledge={() => hook.acknowledgeDialog(dialogNotification.id)}
775
+ onFocus={() => hook.bringDialogToFront(dialogNotification.id)}
776
+ onPositionChange={(newPosition) => hook.updateDialogPosition(dialogNotification.id, newPosition)}
777
+ config={config}
778
+ />
779
+ ))}
780
+
781
+ {/* Toast Notifications Stack */}
782
+ <ToastNotificationsStack
783
+ toasts={hook.toastNotifications}
784
+ onClose={hook.closeToast}
785
+ autoHideDuration={config.toastAutoHideDuration}
786
+ baseZIndex={config.baseZIndex}
787
+ />
788
+ </>
789
+ );
790
+ };
791
+
792
+ export default NotificationsCenter;