@dashadmin/dash-admin 1.3.45 → 1.3.46

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 (43) hide show
  1. package/dist/components/i18n/BridgedLocalesMenuButton.js +1 -2
  2. package/dist/components/menu/DarkToggleMode.js +1 -1
  3. package/dist/components/subscription/PlanPricesSettings.js +4 -2
  4. package/dist/components/tenancy/GlobalTenantWrapper.js +64 -0
  5. package/dist/components/tenancy/TenantSwitcher.js +389 -0
  6. package/dist/contexts/com/components/EchoNotificationsCenter.js +40 -0
  7. package/dist/default-theme/menu/AppMenuComponents/expanded/SidebarItem.js +1 -0
  8. package/dist/index.js +60 -40
  9. package/dist/pages/Profile.js +32 -38
  10. package/dist/providers/i18n/en.js +33 -0
  11. package/dist/providers/i18n/es.js +33 -0
  12. package/dist/resources/User/components/AvatarUploadField.js +61 -0
  13. package/dist/resources/User/components/PasswordFields.js +67 -0
  14. package/dist/resources/User/getProfileMenu.js +36 -0
  15. package/dist/resources/User/profileResource.js +33 -0
  16. package/dist/resources/User/userLayout.js +58 -0
  17. package/dist/resources/User/userResource.js +90 -0
  18. package/dist/resources/User/userSchema.js +206 -0
  19. package/dist/resources/User/usersFilters.js +22 -0
  20. package/dist/systemResources.js +30 -19
  21. package/package.json +1 -1
  22. package/src/components/i18n/BridgedLocalesMenuButton.tsx +1 -2
  23. package/src/components/menu/DarkToggleMode.tsx +1 -1
  24. package/src/components/subscription/PlanPricesSettings.tsx +2 -2
  25. package/src/components/tenancy/GlobalTenantWrapper.tsx +84 -0
  26. package/src/components/tenancy/TenantSwitcher.tsx +485 -0
  27. package/src/contexts/com/components/EchoNotificationsCenter.tsx +65 -0
  28. package/src/default-theme/menu/AppMaterialMenu.tsx +1 -1
  29. package/src/default-theme/menu/AppMenuComponents/expanded/SidebarItem.tsx +1 -0
  30. package/src/default-theme/menu/SidebarActions.tsx +1 -1
  31. package/src/index.tsx +15 -0
  32. package/src/pages/Profile.tsx +69 -107
  33. package/src/providers/i18n/en.ts +33 -0
  34. package/src/providers/i18n/es.ts +33 -0
  35. package/src/resources/User/components/AvatarUploadField.tsx +57 -0
  36. package/src/resources/User/components/PasswordFields.tsx +73 -0
  37. package/src/resources/User/getProfileMenu.ts +57 -0
  38. package/src/resources/User/profileResource.tsx +51 -0
  39. package/src/resources/User/userLayout.tsx +62 -0
  40. package/src/resources/User/userResource.tsx +111 -0
  41. package/src/resources/User/userSchema.tsx +213 -0
  42. package/src/resources/User/usersFilters.tsx +21 -0
  43. package/src/systemResources.tsx +31 -20
@@ -0,0 +1,485 @@
1
+ /**
2
+ * TenantSwitcher
3
+ *
4
+ * A dropdown component that allows switching between the Tenancy Account and
5
+ * individual Tenants. It reads the available tenants from the persisted
6
+ * systemValues (populated by the tenancyAuth endpoint) and allows the user
7
+ * to select one, triggering a full environment switch:
8
+ *
9
+ * - Persists the selected tenant_id to dashStorage
10
+ * - Dispatches a 'tenant_switch' CustomEvent so the PrivateAppLoader can
11
+ * reload the correct resources, routes, and auth data
12
+ * - Re-fetches the appropriate getAuth endpoint (tenancyAuth vs getauth)
13
+ * - Updates CSS variables and logos from the new tenant's settings
14
+ *
15
+ * Consolidated 2026-08-03 from four byte-identical app-level copies
16
+ * (kitchntabs-app, kitchntabs-web, vanexa-app, vanexa-web). The only per-app
17
+ * difference was which local brand assets each app fell back to when
18
+ * switching back to the tenancy level — that's now the defaultHorizontalLogo/
19
+ * defaultSquaredLogo/defaultLoginBackground props instead of hardcoded
20
+ * relative imports (a relative image import moved into this package would
21
+ * have resolved against dash-admin's own assets, not each app's brand).
22
+ */
23
+ import React, { useState, useRef, useEffect, useCallback } from 'react';
24
+ import {
25
+ Avatar,
26
+ Box,
27
+ Typography,
28
+ CircularProgress,
29
+ } from '@mui/material';
30
+ import BusinessIcon from '@mui/icons-material/Business';
31
+ import StoreIcon from '@mui/icons-material/Store';
32
+ import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
33
+ import ReactDOM from 'react-dom';
34
+ import { dashStorage } from 'dash-utils';
35
+ import { AuthPersistenceService } from 'dash-auth';
36
+ import { DASHAdminSystemConstants, getEnv } from 'dash-constants';
37
+ import { useAxios } from 'dash-axios-hook';
38
+ import { updateDomCssVariables } from 'dash-utils';
39
+ import DASHLayoutSettings from '../../theme/AppLayoutSetting';
40
+ import { useDispatch, useSelector } from 'react-redux';
41
+ import { setPanelSettings } from 'dash-admin-state/redux/actions/Common';
42
+ import { IDASHAppState } from 'dash-admin-state';
43
+ import { useWindowSize } from 'dash-utils';
44
+ import { setAuthEvent } from '../../contexts/auth';
45
+
46
+ export interface TenantSwitcherProps {
47
+ /** Fallback horizontal logo used when switching back to the tenancy level */
48
+ defaultHorizontalLogo?: string;
49
+ /** Fallback squared logo used when switching back to the tenancy level */
50
+ defaultSquaredLogo?: string;
51
+ /** Fallback login background used when switching back to the tenancy level */
52
+ defaultLoginBackground?: string;
53
+ }
54
+
55
+ /** Shape of a tenant from systemValues.tenants */
56
+ interface TenantOption {
57
+ id: string;
58
+ name: string;
59
+ slug: string | null;
60
+ }
61
+
62
+ /** The value emitted in the tenant_switch CustomEvent */
63
+ export interface TenantSwitchEventDetail {
64
+ tenantId: string | null; // null = tenancy account level
65
+ tenantName: string;
66
+ isTenanancyLevel: boolean;
67
+ }
68
+
69
+ /**
70
+ * Dispatch a tenant_switch event so other parts of the app can react.
71
+ */
72
+ export const dispatchTenantSwitchEvent = (detail: TenantSwitchEventDetail) => {
73
+ window.dispatchEvent(new CustomEvent('tenant_switch', { detail }));
74
+ };
75
+
76
+ /**
77
+ * Get the currently active tenant_id from storage (null = tenancy level).
78
+ */
79
+ export const getActiveTenantId = (): string | null => {
80
+ if (!isTenantImpersonationEnabled()) {
81
+ return null;
82
+ }
83
+ return dashStorage.getItem('active_tenant_id') || null;
84
+ };
85
+
86
+ /**
87
+ * Check whether tenant impersonation is enabled.
88
+ */
89
+ export const isTenantImpersonationEnabled = (): boolean => {
90
+ const val = DASHAdminSystemConstants.system.ENABLE_TENANT_IMPERSONATION;
91
+ if (typeof val === 'boolean') return val;
92
+ if (typeof val === 'string') return val === 'true' || val === '1';
93
+ return !!val;
94
+ };
95
+
96
+
97
+
98
+ const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
99
+ defaultHorizontalLogo,
100
+ defaultSquaredLogo,
101
+ defaultLoginBackground,
102
+ }) => {
103
+ const axios = useAxios();
104
+ const dispatch = useDispatch();
105
+ const windowSize = useWindowSize();
106
+
107
+
108
+
109
+ const [open, setOpen] = useState(false);
110
+ const [switching, setSwitching] = useState(false);
111
+ const avatarRef = useRef<HTMLDivElement>(null);
112
+ const menuRef = useRef<HTMLDivElement>(null);
113
+ const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0 });
114
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
115
+ const [webView, setWebView] = useState(false);
116
+
117
+ // Current active tenant id
118
+ const [activeTenantId, setActiveTenantId] = useState<string | null>(
119
+ getActiveTenantId()
120
+ );
121
+
122
+ // Read panel settings for logos
123
+ const panelSettings = useSelector(
124
+ (state: IDASHAppState<any, any, any>) => state.common?.panelSettings
125
+ );
126
+
127
+ // Get primary and secondary sidebar positions from Redux panelSettings
128
+ const sidebarPosition = panelSettings?.sidebarPosition || 'left';
129
+
130
+ // Get tenants from persisted systemValues
131
+ const systemValues = AuthPersistenceService.getSystemValues();
132
+ const tenants: TenantOption[] = systemValues?.tenants || [];
133
+ const tenancy = systemValues?.tenancy;
134
+
135
+ // Get current tenant auth data for display
136
+ const auth = AuthPersistenceService.getAuth();
137
+ const currentTenantName = activeTenantId
138
+ ? tenants.find(t => t.id === activeTenantId)?.name || 'Tenant'
139
+ : tenancy?.public_name || auth?.tenant?.name || 'Tenancy Account';
140
+
141
+ useEffect(() => {
142
+ if (document.body.classList.contains('webview')) {
143
+ setWebView(true);
144
+ }
145
+ }, []);
146
+
147
+ // Don't render if impersonation is disabled or no tenants available
148
+ if (!isTenantImpersonationEnabled() || tenants.length === 0) {
149
+ return null;
150
+ }
151
+
152
+ const calculateMenuPosition = () => {
153
+ if (avatarRef.current && windowSize.width && windowSize.height) {
154
+ const rect = avatarRef.current.getBoundingClientRect();
155
+ const menuWidth = 230; // minWidth from styles
156
+ const menuHeight = 120; // approximate menu height
157
+
158
+ let left = rect.left + window.scrollX;
159
+ let top = rect.top + window.scrollY;
160
+
161
+ // Ensure menu doesn't go off-screen horizontally
162
+ if (left + menuWidth > windowSize.width) {
163
+ left = windowSize.width - menuWidth - 10; // 10px margin
164
+ }
165
+ if (left < 10) {
166
+ left = 10; // 10px margin
167
+ }
168
+
169
+ // For bottom sidebar position, open menu above the avatar
170
+ if (sidebarPosition === 'bottom') {
171
+ top = rect.top + window.scrollY - menuHeight - 10; // Open above
172
+ }
173
+
174
+ setMenuPosition({
175
+ top: top,
176
+ left: left + 40
177
+ });
178
+ }
179
+ };
180
+
181
+ const handleMouseEnter = (e: React.MouseEvent<HTMLDivElement>) => {
182
+ e.preventDefault();
183
+ if (timeoutRef.current) {
184
+ clearTimeout(timeoutRef.current);
185
+ timeoutRef.current = null;
186
+ }
187
+ setOpen(true);
188
+ calculateMenuPosition();
189
+ };
190
+
191
+ const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
192
+ e.preventDefault();
193
+ setOpen(prev => !prev);
194
+ calculateMenuPosition();
195
+ };
196
+
197
+ const handleMouseLeave = () => {
198
+ if (!webView) {
199
+ timeoutRef.current = setTimeout(() => setOpen(false), 150);
200
+ }
201
+ };
202
+
203
+ const handleMenuMouseEnter = () => {
204
+ if (timeoutRef.current) {
205
+ clearTimeout(timeoutRef.current);
206
+ timeoutRef.current = null;
207
+ }
208
+ };
209
+
210
+ const handleMenuMouseLeave = () => {
211
+ if (!webView) {
212
+ setOpen(false);
213
+ }
214
+ };
215
+
216
+ useEffect(() => {
217
+ return () => {
218
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
219
+ };
220
+ }, []);
221
+
222
+ useEffect(() => {
223
+ if (open) calculateMenuPosition();
224
+ }, [open, windowSize.width]);
225
+
226
+ /**
227
+ * Core switching logic.
228
+ * - Always uses tenancyAuth endpoint (preserves tenancy/tenants in response)
229
+ * - Passes X-Tenant-Id header when switching to a specific tenant
230
+ * - Persists auth data
231
+ * - Updates CSS variables, logos
232
+ * - Dispatches tenant_switch event for PrivateAppLoader
233
+ */
234
+ const handleSwitch = useCallback(async (tenantId: string | null) => {
235
+ if (switching) return;
236
+ if (tenantId === activeTenantId) {
237
+ setOpen(false);
238
+ return;
239
+ }
240
+
241
+ setSwitching(true);
242
+ setOpen(false);
243
+
244
+ try {
245
+ const isTenancyLevel = tenantId === null;
246
+ // Always use tenancyAuth - it returns tenancy/tenants list + respects X-Tenant-Id header
247
+ const authEndpoint = getEnv('APP_GETAUTH_ENDPOINT') || 'auth/tenancyAuth';
248
+
249
+ // If switching to a specific tenant, set the header so backend knows
250
+ const headers: Record<string, string> = {};
251
+ if (!isTenancyLevel && tenantId) {
252
+ headers['X-Tenant-Id'] = tenantId;
253
+ }
254
+
255
+ // Persist active_tenant_id BEFORE the request so the axios interceptor picks it up
256
+ if (tenantId) {
257
+ dashStorage.setItem('active_tenant_id', tenantId);
258
+ dashStorage.setItem('tenant_id', tenantId);
259
+ } else {
260
+ dashStorage.removeItem('active_tenant_id');
261
+ dashStorage.removeItem('tenant_id');
262
+ }
263
+
264
+ // Fetch new auth data from the correct endpoint
265
+ const { data: authData } = await axios.get(authEndpoint, { headers });
266
+
267
+ // Persist auth data using the standard service
268
+ AuthPersistenceService.saveAuth(authData);
269
+
270
+ // Update basic localStorage for react-admin compatibility
271
+ dashStorage.setItem('authenticated', 'true');
272
+ dashStorage.setItem('user', JSON.stringify(authData.user));
273
+ if (authData.user?.roles) {
274
+ dashStorage.setItem('roles', JSON.stringify(authData.user.roles));
275
+ }
276
+
277
+ // Trigger auth context update
278
+ setAuthEvent({
279
+ authenticated: true,
280
+ user: authData.user,
281
+ auth: authData.auth,
282
+ token: dashStorage.getItem('token'),
283
+ roles: authData.user?.roles,
284
+ });
285
+
286
+ // Update CSS variables from new tenant settings
287
+ const tenantSettings = authData.auth?.tenantSettings;
288
+ try {
289
+
290
+ // Read current theme mode instead of hardcoding 'dark'
291
+ const currentThemeMode = document.documentElement.getAttribute('data-theme') || DASHLayoutSettings.THEME_TYPE_DARK;
292
+ // Always call updateDomCssVariables — when colors is undefined,
293
+ // it correctly resets to compiled LESS defaults for tenants without custom colors.
294
+ updateDomCssVariables(currentThemeMode, tenantSettings?.colors, tenantSettings?.values);
295
+ } catch (err) {
296
+ console.error('[TenantSwitcher] Error updating CSS variables:', err);
297
+ }
298
+
299
+ // Update logos in Redux panel settings
300
+ const tenantImages = authData.auth?.tenantImages;
301
+ if (tenantImages) {
302
+ AuthPersistenceService.setTenantImages(tenantImages);
303
+ dispatch(setPanelSettings({
304
+ ...(panelSettings || {}),
305
+ horizontalLogo: tenantImages.horizontal_logo?.original || panelSettings?.horizontalLogo,
306
+ squaredLogo: tenantImages.squared_logo?.original || panelSettings?.squaredLogo,
307
+ loginBackground: tenantImages.banner?.original || panelSettings?.loginBackground,
308
+ }));
309
+ } else {
310
+ // Switching to tenancy level — clear stale tenant images
311
+ // and reset Redux logos to system defaults
312
+ AuthPersistenceService.clearTenantImages();
313
+ dispatch(setPanelSettings({
314
+ ...(panelSettings || {}),
315
+ horizontalLogo: defaultHorizontalLogo,
316
+ squaredLogo: defaultSquaredLogo,
317
+ loginBackground: defaultLoginBackground,
318
+ }));
319
+ }
320
+
321
+ // Update tenant settings in persistence
322
+ if (tenantSettings) {
323
+ AuthPersistenceService.setTenantSettings(tenantSettings);
324
+ } else {
325
+ // Switching to tenancy level — clear stale tenant settings
326
+ // so DashThemeProvider reads null on remount and uses defaults
327
+ AuthPersistenceService.clearTenantSettings();
328
+ }
329
+
330
+ setActiveTenantId(tenantId);
331
+
332
+ // Find tenant name for event
333
+ const tenantName = isTenancyLevel
334
+ ? (tenancy?.public_name || 'Tenancy Account')
335
+ : (tenants.find(t => t.id === tenantId)?.name || 'Tenant');
336
+
337
+ // Dispatch switch event so PrivateAppLoader can reload resources
338
+ dispatchTenantSwitchEvent({
339
+ tenantId,
340
+ tenantName,
341
+ isTenanancyLevel: isTenancyLevel,
342
+ });
343
+
344
+ console.log(`[TenantSwitcher] Switched to ${isTenancyLevel ? 'Tenancy Account' : tenantName} (${tenantId})`);
345
+
346
+ } catch (error) {
347
+ console.error('[TenantSwitcher] Switch failed:', error);
348
+ // Revert storage on failure
349
+ if (activeTenantId) {
350
+ dashStorage.setItem('active_tenant_id', activeTenantId);
351
+ dashStorage.setItem('tenant_id', activeTenantId);
352
+ } else {
353
+ dashStorage.removeItem('active_tenant_id');
354
+ dashStorage.removeItem('tenant_id');
355
+ }
356
+ } finally {
357
+ setSwitching(false);
358
+ }
359
+ }, [activeTenantId, switching, axios, dispatch, panelSettings, tenants, tenancy, defaultHorizontalLogo, defaultSquaredLogo, defaultLoginBackground]);
360
+
361
+ const isSelected = (id: string | null) => id === activeTenantId;
362
+
363
+ return (
364
+ <>
365
+ <div
366
+ ref={avatarRef}
367
+ className="dash-tenant-switcher-trigger"
368
+ {...(!webView
369
+ ? { onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave }
370
+ : { onClick: handleClick }
371
+ )}
372
+ style={{ cursor: 'pointer', display: 'flex', alignItems: 'center' }}
373
+ >
374
+ {switching ? (
375
+ <CircularProgress
376
+ className='dash-icon-button-color dash-icon-button-bg'
377
+ size={30} />
378
+ ) : (
379
+ <Avatar
380
+ className='dash-icon-button-color dash-icon-button-bg'
381
+ sx={{
382
+ width: 30,
383
+ height: 30,
384
+ fontSize: '0.8rem',
385
+ }}
386
+ >
387
+ {activeTenantId ? <StoreIcon sx={{ fontSize: 18 }} /> : <BusinessIcon sx={{ fontSize: 18 }} />}
388
+ </Avatar>
389
+ )}
390
+ </div>
391
+
392
+ {open && ReactDOM.createPortal(
393
+ <div
394
+ ref={menuRef}
395
+ className="dash-tenant-switcher-portal"
396
+ onMouseEnter={!webView ? handleMenuMouseEnter : undefined}
397
+ onMouseLeave={!webView ? handleMenuMouseLeave : undefined}
398
+ >
399
+ <div
400
+ className="dash-tenant-switcher-menu"
401
+ style={{
402
+ top: menuPosition.top,
403
+ left: menuPosition.left,
404
+ zIndex: 10000,
405
+ position: 'absolute',
406
+ }}
407
+ >
408
+ {/* Header */}
409
+ <Box sx={{ px: 2, py: 1.5, borderBottom: '1px solid rgba(255,255,255,0.08)' }}>
410
+ <Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5 }}>
411
+ <SwapHorizIcon sx={{ fontSize: 14, mr: 0.5, verticalAlign: 'middle' }} />
412
+ Switch Environment
413
+ </Typography>
414
+ </Box>
415
+
416
+ {/* Tenancy Account option */}
417
+ <div
418
+ onClick={() => handleSwitch(null)}
419
+ style={{
420
+ padding: '10px 16px',
421
+ cursor: 'pointer',
422
+ display: 'flex',
423
+ alignItems: 'center',
424
+ gap: 10,
425
+ backgroundColor: isSelected(null) ? 'rgba(var(--dash-primary-rgb, 25, 118, 210), 0.15)' : 'transparent',
426
+ borderLeft: isSelected(null) ? '3px solid var(--dash-primary, #1976d2)' : '3px solid transparent',
427
+ transition: 'background-color 0.15s ease',
428
+ }}
429
+ onMouseOver={(e) => { if (!isSelected(null)) (e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.05)'); }}
430
+ onMouseOut={(e) => { if (!isSelected(null)) (e.currentTarget.style.backgroundColor = 'transparent'); }}
431
+ >
432
+ <BusinessIcon sx={{ fontSize: 20, color: isSelected(null) ? 'primary.main' : 'text.secondary' }} />
433
+ <Box>
434
+ <Typography variant="body2" sx={{ fontWeight: isSelected(null) ? 600 : 400, color: isSelected(null) ? 'primary.main' : 'text.primary' }}>
435
+ {tenancy?.public_name || 'Tenancy Account'}
436
+ </Typography>
437
+ <Typography variant="caption" sx={{ color: 'text.secondary' }}>
438
+ Manager Account
439
+ </Typography>
440
+ </Box>
441
+ </div>
442
+
443
+ {/* Divider */}
444
+ <Box sx={{ borderTop: '1px solid rgba(255,255,255,0.06)', mx: 1 }} />
445
+
446
+ {/* Tenant options */}
447
+ {tenants.map((tenant) => (
448
+ <div
449
+ key={tenant.id}
450
+ onClick={() => handleSwitch(tenant.id)}
451
+ style={{
452
+ padding: '10px 16px',
453
+ cursor: 'pointer',
454
+ display: 'flex',
455
+ alignItems: 'center',
456
+ gap: 10,
457
+ backgroundColor: isSelected(tenant.id) ? 'rgba(var(--dash-primary-rgb, 25, 118, 210), 0.15)' : 'transparent',
458
+ borderLeft: isSelected(tenant.id) ? '3px solid var(--dash-primary, #1976d2)' : '3px solid transparent',
459
+ transition: 'background-color 0.15s ease',
460
+ }}
461
+ onMouseOver={(e) => { if (!isSelected(tenant.id)) (e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.05)'); }}
462
+ onMouseOut={(e) => { if (!isSelected(tenant.id)) (e.currentTarget.style.backgroundColor = 'transparent'); }}
463
+ >
464
+ <StoreIcon sx={{ fontSize: 20, color: isSelected(tenant.id) ? 'primary.main' : 'text.secondary' }} />
465
+ <Box>
466
+ <Typography variant="body2" sx={{ fontWeight: isSelected(tenant.id) ? 600 : 400, color: isSelected(tenant.id) ? 'primary.main' : 'text.primary' }}>
467
+ {tenant.name}
468
+ </Typography>
469
+ {tenant.slug && (
470
+ <Typography variant="caption" sx={{ color: 'text.secondary' }}>
471
+ {tenant.slug}
472
+ </Typography>
473
+ )}
474
+ </Box>
475
+ </div>
476
+ ))}
477
+ </div>
478
+ </div>,
479
+ document.body
480
+ )}
481
+ </>
482
+ );
483
+ };
484
+
485
+ export default TenantSwitcher;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * EchoNotificationsCenter
3
+ *
4
+ * Wires LaravelEchoContext's real-time "urgency-alert" events into
5
+ * dash-components' generic NotificationsCenter (bell button + menu + toast
6
+ * stack + draggable dialogs). This replaces what used to be an ~500-line
7
+ * NotificationsCenter.tsx hand-copied into every domain app — kitchntabs-app,
8
+ * kitchntabs-system, kitchntabs-web, vanexa-app, vanexa-system, vanexa-web all
9
+ * had byte-identical copies of this exact wiring. Each of those apps now just
10
+ * renders <EchoNotificationsCenter /> instead of maintaining its own copy.
11
+ *
12
+ * kitchntabs-mall intentionally keeps its own NotificationsCenter — it also
13
+ * handles mall-specific "mall_order_status_update" events with event
14
+ * deduplication and translated messages, which is real business logic, not
15
+ * duplicated boilerplate. Folding it into this shared component would need a
16
+ * proper event-transformer API, not a copy/paste consolidation.
17
+ */
18
+ import React, { useContext, useEffect } from 'react';
19
+ import { NotificationsCenter, useNotificationsCenter, type NotificationsCenterLabels } from 'dash-components';
20
+ import LaravelEchoContext, { ILaravelEchoContext } from '../LaravelEchoContext';
21
+
22
+ // Matches the hardcoded Spanish strings every duplicated app copy used to
23
+ // render (dash-components' defaults are English).
24
+ const spanishLabels: NotificationsCenterLabels = {
25
+ title: 'Notificaciones',
26
+ markAll: 'Marcar todas',
27
+ noNotifications: 'No hay notificaciones',
28
+ close: 'Cerrar',
29
+ acknowledge: 'Entendido',
30
+ customer: 'Cliente:',
31
+ store: 'Tienda:',
32
+ received: 'Recibido:',
33
+ now: 'Ahora',
34
+ minutesAgo: 'm',
35
+ hoursAgo: 'h',
36
+ daysAgo: 'd',
37
+ };
38
+
39
+ const EchoNotificationsCenter: React.FC = () => {
40
+ const { lastEvent } = useContext<ILaravelEchoContext>(LaravelEchoContext);
41
+ const hook = useNotificationsCenter({ labels: spanishLabels });
42
+
43
+ useEffect(() => {
44
+ if (lastEvent && lastEvent.type === 'urgency-alert') {
45
+ const notification: any = lastEvent?.notificationPayload;
46
+ hook.addNotification({
47
+ title: notification?.title || 'Alerta',
48
+ message: notification?.message || '',
49
+ customer: notification?.customer_info?.name
50
+ ? `${notification.customer_info.name} - Mesa ${notification.customer_info.table}`
51
+ : '',
52
+ store: notification?.store_info?.name || '',
53
+ data: notification,
54
+ }, { showDialog: true, showToast: true, toastSeverity: 'warning' });
55
+ }
56
+ // Only re-run when a new event arrives, matching the original per-app
57
+ // implementations — addNotification's own setState calls are all
58
+ // functional updates, so a stale hook reference here is harmless.
59
+ // eslint-disable-next-line react-hooks/exhaustive-deps
60
+ }, [lastEvent]);
61
+
62
+ return <NotificationsCenter notificationsHook={hook} />;
63
+ };
64
+
65
+ export default EchoNotificationsCenter;
@@ -82,7 +82,7 @@ const GenerateItems: React.FC<{ items: IMenuItem[]; navExpanded: boolean, navSiz
82
82
  />
83
83
  ) : (
84
84
 
85
- <SidebarItem level={level} navExpanded={navExpanded} navSize={navSize} item={item} key={index} />
85
+ <SidebarItem level={level} navExpanded={navExpanded} navSize={navSize} item={item} key={index} />
86
86
  );
87
87
  })}
88
88
  </List>
@@ -63,6 +63,7 @@ const SidebarItem = ({
63
63
 
64
64
  return (
65
65
  <ListItemButton
66
+ className={className}
66
67
  selected={isCurrent}
67
68
  onClick={(e) => {
68
69
 
@@ -71,7 +71,7 @@ const SidebarActions: React.FC<SidebarActionsProps> = ({
71
71
  <HeaderToolBar />
72
72
  ) : (
73
73
  // Default mode: Render all actions
74
- <>
74
+ <>
75
75
  {HeaderToolBar && <HeaderToolBar />}
76
76
  {authContext?.authenticated ? <BridgedLocalesMenuButton /> : <LangSwitcher />}
77
77
  <DarkToggleMode />
package/src/index.tsx CHANGED
@@ -32,6 +32,7 @@ export { default as CacheInvalidatorListenerComponent } from './utils/cache/Cach
32
32
  export { AuthContext } from './contexts/auth';
33
33
  export { NotificationComponent } from './contexts/com/components/NotificationRenderer'
34
34
  export { NotificationWrapper } from './contexts/com/components/NotificationsWidget'
35
+ export { default as EchoNotificationsCenter } from './contexts/com/components/EchoNotificationsCenter'
35
36
 
36
37
  export { default as useLaravelEcho } from './contexts/com/useLaravelEcho';
37
38
  export { default as WSMessagesManager } from './hooks/notifications/WSMessagesManager';
@@ -84,7 +85,21 @@ export { default as useFormPersistence } from "./hooks/useFormPersistance";
84
85
  export { default as AvatarComponent } from './components/user/AvatarComponent';
85
86
  export { default as TenantAvatarComponent } from './components/user/TenantAvatarComponent';
86
87
  export { default as UserPreferences } from './components/user/UserPreferences';
88
+ export { default as Profile } from './pages/Profile';
87
89
  export { default as LanguageSwitcher } from './components/misc/LanguageSwitcher';
90
+ export { default as LangSwitcher } from './components/i18n/LangSwitcher';
91
+ export {
92
+ default as TenantSwitcher,
93
+ dispatchTenantSwitchEvent,
94
+ getActiveTenantId,
95
+ isTenantImpersonationEnabled,
96
+ type TenantSwitcherProps,
97
+ type TenantSwitchEventDetail,
98
+ } from './components/tenancy/TenantSwitcher';
99
+ export {
100
+ default as GlobalTenantWrapper,
101
+ type IGlobalTenantWrapper,
102
+ } from './components/tenancy/GlobalTenantWrapper';
88
103
  export { default as DarkModeSwitcher } from './components/misc/DarkModeSwitcher';
89
104
 
90
105
  // Navigation Components