mesauth-angular 1.1.6 → 1.1.8

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.
@@ -0,0 +1,1295 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, makeEnvironmentProviders, provideAppInitializer, inject, Injectable, NgModule, EventEmitter, signal, HostListener, HostBinding, Output, Component, ViewChild } from '@angular/core';
3
+ import { HttpClient } from '@angular/common/http';
4
+ import { HubConnectionBuilder, LogLevel } from '@microsoft/signalr';
5
+ import { BehaviorSubject, Subject, throwError } from 'rxjs';
6
+ import { filter, debounceTime, tap, catchError, takeUntil } from 'rxjs/operators';
7
+ import * as i2 from '@angular/router';
8
+ import { Router, NavigationEnd } from '@angular/router';
9
+ import * as i3 from '@angular/common';
10
+ import { NgIf, CommonModule, NgFor } from '@angular/common';
11
+
12
+ /** Injection token for MesAuth configuration */
13
+ const MES_AUTH_CONFIG = new InjectionToken('MES_AUTH_CONFIG');
14
+ /**
15
+ * Provides MesAuth with configuration.
16
+ * This is the recommended way to set up mesauth-angular in standalone apps.
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * // app.config.ts
21
+ * export const appConfig: ApplicationConfig = {
22
+ * providers: [
23
+ * provideHttpClient(withInterceptors([mesAuthInterceptor])),
24
+ * provideMesAuth({
25
+ * apiBaseUrl: 'https://auth.example.com',
26
+ * userBaseUrl: 'https://app.example.com'
27
+ * })
28
+ * ]
29
+ * };
30
+ * ```
31
+ */
32
+ function provideMesAuth(config) {
33
+ return makeEnvironmentProviders([
34
+ { provide: MES_AUTH_CONFIG, useValue: config },
35
+ MesAuthService,
36
+ provideAppInitializer(() => {
37
+ const mesAuthService = inject(MesAuthService);
38
+ const httpClient = inject(HttpClient);
39
+ const router = inject(Router);
40
+ mesAuthService.init(config, httpClient, router);
41
+ })
42
+ ]);
43
+ }
44
+ var NotificationType;
45
+ (function (NotificationType) {
46
+ NotificationType["Info"] = "Info";
47
+ NotificationType["Warning"] = "Warning";
48
+ NotificationType["Error"] = "Error";
49
+ NotificationType["Success"] = "Success";
50
+ })(NotificationType || (NotificationType = {}));
51
+ class MesAuthService {
52
+ hubConnection = null;
53
+ _currentUser = new BehaviorSubject(null);
54
+ currentUser$ = this._currentUser.asObservable();
55
+ _notifications = new Subject();
56
+ notifications$ = this._notifications.asObservable();
57
+ apiBase = '';
58
+ config = null;
59
+ http;
60
+ router;
61
+ constructor() {
62
+ // Empty constructor - all dependencies passed to init()
63
+ }
64
+ isProtectedRoute(url) {
65
+ // Consider routes protected if they don't include auth-related paths
66
+ return !url.includes('/login') && !url.includes('/auth') && !url.includes('/signin') && !url.includes('/logout');
67
+ }
68
+ init(config, httpClient, router) {
69
+ this.config = config;
70
+ this.http = httpClient;
71
+ this.router = router;
72
+ this.apiBase = config.apiBaseUrl.replace(/\/$/, '');
73
+ // Listen for route changes - only refresh user data if needed for SPA navigation
74
+ // This helps maintain authentication state in single-page applications
75
+ if (this.router) {
76
+ this.router.events
77
+ .pipe(filter(event => event instanceof NavigationEnd), debounceTime(1000) // Longer debounce to avoid interfering with login flow
78
+ )
79
+ .subscribe((event) => {
80
+ // Only refresh if user is logged in and navigating to protected routes
81
+ // Avoid refreshing during login/logout flows
82
+ if (this._currentUser.value && this.isProtectedRoute(event.url)) {
83
+ // Small delay to ensure any login/logout operations complete
84
+ setTimeout(() => {
85
+ if (this._currentUser.value) {
86
+ this.refreshUser();
87
+ }
88
+ }, 100);
89
+ }
90
+ });
91
+ }
92
+ this.fetchCurrentUser();
93
+ }
94
+ getConfig() {
95
+ return this.config;
96
+ }
97
+ fetchCurrentUser() {
98
+ if (!this.apiBase)
99
+ return;
100
+ const url = `${this.apiBase}/auth/me`;
101
+ this.http.get(url, { withCredentials: this.config?.withCredentials ?? true }).subscribe({
102
+ next: (u) => {
103
+ this._currentUser.next(u);
104
+ if (u && this.config) {
105
+ this.startConnection(this.config);
106
+ // Only fetch notifications after confirming user is logged in
107
+ this.fetchInitialNotifications();
108
+ }
109
+ },
110
+ error: (err) => {
111
+ // Silently handle auth errors (401/403) - user is not logged in
112
+ if (err.status === 401 || err.status === 403) {
113
+ this._currentUser.next(null);
114
+ }
115
+ }
116
+ });
117
+ }
118
+ fetchInitialNotifications() {
119
+ // Skip if no user is logged in or apiBase not set
120
+ if (!this.apiBase || !this._currentUser.value)
121
+ return;
122
+ this.http.get(`${this.apiBase}/notif/me`, { withCredentials: this.config?.withCredentials ?? true }).subscribe({
123
+ next: (notifications) => {
124
+ if (Array.isArray(notifications?.items)) {
125
+ notifications.items.forEach((n) => this._notifications.next(n));
126
+ }
127
+ },
128
+ error: (err) => {
129
+ // Silently handle auth errors (401/403) - user is not logged in
130
+ // No need to emit anything
131
+ }
132
+ });
133
+ }
134
+ getUnreadCount() {
135
+ return this.http.get(`${this.apiBase}/notif/me/unread-count`, { withCredentials: this.config?.withCredentials ?? true });
136
+ }
137
+ getNotifications(page = 1, pageSize = 20, includeRead = false, type) {
138
+ let url = `${this.apiBase}/notif/me?page=${page}&pageSize=${pageSize}&includeRead=${includeRead}`;
139
+ if (type) {
140
+ url += `&type=${type}`;
141
+ }
142
+ return this.http.get(url, { withCredentials: this.config?.withCredentials ?? true });
143
+ }
144
+ markAsRead(notificationId) {
145
+ return this.http.patch(`${this.apiBase}/notif/${notificationId}/read`, {}, { withCredentials: this.config?.withCredentials ?? true });
146
+ }
147
+ markAllAsRead() {
148
+ return this.http.patch(`${this.apiBase}/notif/me/read-all`, {}, { withCredentials: this.config?.withCredentials ?? true });
149
+ }
150
+ deleteNotification(notificationId) {
151
+ return this.http.delete(`${this.apiBase}/notif/${notificationId}`, { withCredentials: this.config?.withCredentials ?? true });
152
+ }
153
+ /**
154
+ * Get frontend routes assigned to the current user
155
+ * Returns routes grouped by application
156
+ */
157
+ getFrontEndRoutes() {
158
+ if (!this.apiBase)
159
+ throw new Error('MesAuth not initialized');
160
+ return this.http.get(`${this.apiBase}/fe-routes/me`, { withCredentials: this.config?.withCredentials ?? true });
161
+ }
162
+ /**
163
+ * Get master routes for a specific application
164
+ * @param appId - The application ID
165
+ */
166
+ getRouteMasters(appId) {
167
+ if (!this.apiBase)
168
+ throw new Error('MesAuth not initialized');
169
+ return this.http.get(`${this.apiBase}/fe-routes/masters/${appId}`, { withCredentials: this.config?.withCredentials ?? true });
170
+ }
171
+ /**
172
+ * Register/sync frontend routes for an application
173
+ * This is typically called on app startup to sync routes from the frontend app
174
+ * @param appId - The application ID (passed via X-App-Id header)
175
+ * @param routes - Array of route definitions
176
+ */
177
+ registerFrontEndRoutes(appId, routes) {
178
+ if (!this.apiBase)
179
+ throw new Error('MesAuth not initialized');
180
+ const headers = { 'X-App-Id': appId };
181
+ return this.http.post(`${this.apiBase}/fe-routes/register`, routes, {
182
+ headers,
183
+ withCredentials: this.config?.withCredentials ?? true
184
+ });
185
+ }
186
+ /**
187
+ * Create a new route master
188
+ * @param appId - The application ID
189
+ * @param route - Route details
190
+ */
191
+ createRouteMaster(appId, route) {
192
+ if (!this.apiBase)
193
+ throw new Error('MesAuth not initialized');
194
+ return this.http.post(`${this.apiBase}/fe-routes/masters`, {
195
+ appId,
196
+ ...route
197
+ }, { withCredentials: this.config?.withCredentials ?? true });
198
+ }
199
+ /**
200
+ * Update an existing route master
201
+ * @param routeId - The route master ID
202
+ * @param route - Updated route details
203
+ */
204
+ updateRouteMaster(routeId, route) {
205
+ if (!this.apiBase)
206
+ throw new Error('MesAuth not initialized');
207
+ return this.http.put(`${this.apiBase}/fe-routes/masters/${routeId}`, route, {
208
+ withCredentials: this.config?.withCredentials ?? true
209
+ });
210
+ }
211
+ /**
212
+ * Delete a route master
213
+ * @param routeId - The route master ID
214
+ */
215
+ deleteRouteMaster(routeId) {
216
+ if (!this.apiBase)
217
+ throw new Error('MesAuth not initialized');
218
+ return this.http.delete(`${this.apiBase}/fe-routes/masters/${routeId}`, {
219
+ withCredentials: this.config?.withCredentials ?? true
220
+ });
221
+ }
222
+ /**
223
+ * Assign a route to a role
224
+ * @param routeMasterId - The route master ID
225
+ * @param roleId - The role ID (GUID)
226
+ */
227
+ assignRouteToRole(routeMasterId, roleId) {
228
+ if (!this.apiBase)
229
+ throw new Error('MesAuth not initialized');
230
+ return this.http.post(`${this.apiBase}/fe-routes/mappings`, {
231
+ routeMasterId,
232
+ roleId
233
+ }, { withCredentials: this.config?.withCredentials ?? true });
234
+ }
235
+ /**
236
+ * Remove a route assignment from a role
237
+ * @param mappingId - The mapping ID
238
+ */
239
+ removeRouteFromRole(mappingId) {
240
+ if (!this.apiBase)
241
+ throw new Error('MesAuth not initialized');
242
+ return this.http.delete(`${this.apiBase}/fe-routes/mappings/${mappingId}`, {
243
+ withCredentials: this.config?.withCredentials ?? true
244
+ });
245
+ }
246
+ /**
247
+ * Get route-to-role mappings for a specific role
248
+ * @param roleId - The role ID (GUID)
249
+ */
250
+ getRouteMappingsByRole(roleId) {
251
+ if (!this.apiBase)
252
+ throw new Error('MesAuth not initialized');
253
+ return this.http.get(`${this.apiBase}/fe-routes/mappings?roleId=${roleId}`, {
254
+ withCredentials: this.config?.withCredentials ?? true
255
+ });
256
+ }
257
+ startConnection(config) {
258
+ if (this.hubConnection)
259
+ return;
260
+ const signalrUrl = config.apiBaseUrl.replace(/\/$/, '') + '/hub/notification';
261
+ const builder = new HubConnectionBuilder()
262
+ .withUrl(signalrUrl, { withCredentials: config.withCredentials ?? true })
263
+ .withAutomaticReconnect()
264
+ .configureLogging(LogLevel.Warning);
265
+ this.hubConnection = builder.build();
266
+ this.hubConnection.on('ReceiveNotification', (n) => {
267
+ this._notifications.next(n);
268
+ });
269
+ this.hubConnection.start().then(() => { }).catch((err) => { });
270
+ this.hubConnection.onclose(() => { });
271
+ this.hubConnection.onreconnecting(() => { });
272
+ this.hubConnection.onreconnected(() => { });
273
+ }
274
+ stop() {
275
+ if (!this.hubConnection)
276
+ return;
277
+ this.hubConnection.stop().catch(() => { });
278
+ this.hubConnection = null;
279
+ }
280
+ logout() {
281
+ const url = `${this.apiBase}/auth/logout`;
282
+ return this.http.post(url, {}, { withCredentials: this.config?.withCredentials ?? true }).pipe(tap(() => {
283
+ this._currentUser.next(null);
284
+ this.stop();
285
+ }));
286
+ }
287
+ get currentUser() {
288
+ return this._currentUser.value;
289
+ }
290
+ get isAuthenticated() {
291
+ return this._currentUser.value !== null;
292
+ }
293
+ refreshUser() {
294
+ this.fetchCurrentUser();
295
+ }
296
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MesAuthService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
297
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MesAuthService });
298
+ }
299
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MesAuthService, decorators: [{
300
+ type: Injectable
301
+ }], ctorParameters: () => [] });
302
+
303
+ // Track if we're currently redirecting to prevent loopback
304
+ let isRedirecting = false;
305
+ /**
306
+ * Functional HTTP interceptor for handling 401/403 auth errors.
307
+ * Redirects to login page on 401, and to 403 page on 403.
308
+ * Includes loopback prevention to avoid infinite redirects.
309
+ */
310
+ const mesAuthInterceptor = (req, next) => {
311
+ const authService = inject(MesAuthService);
312
+ const router = inject(Router);
313
+ return next(req).pipe(catchError((error) => {
314
+ const status = error.status;
315
+ // Check if we should handle this error and prevent loopback
316
+ if ((status === 401 || status === 403) && !isRedirecting) {
317
+ const config = authService.getConfig();
318
+ const baseUrl = config?.userBaseUrl || '';
319
+ // Use router URL for internal navigation (cleaner URLs)
320
+ // Falls back to window.location for full URL if needed
321
+ const currentUrl = router.url + (window.location.hash || '');
322
+ const returnUrl = encodeURIComponent(currentUrl);
323
+ // Avoid loops if already on auth/unauth pages
324
+ const isLoginPage = currentUrl.includes('/login');
325
+ const is403Page = currentUrl.includes('/403');
326
+ const isAuthPage = currentUrl.includes('/auth');
327
+ const isMeAuthPage = req.url.includes('/auth/me');
328
+ // Check if user is authenticated
329
+ const isAuthenticated = authService.isAuthenticated;
330
+ if (status === 401 && !isLoginPage && !isAuthPage && !isAuthenticated && !isMeAuthPage) {
331
+ isRedirecting = true;
332
+ // Reset flag after a delay to allow future redirects after user returns
333
+ setTimeout(() => { isRedirecting = false; }, 5000);
334
+ window.location.href = `${baseUrl}/login?returnUrl=${returnUrl}`;
335
+ }
336
+ else if (status === 403 && !is403Page) {
337
+ isRedirecting = true;
338
+ setTimeout(() => { isRedirecting = false; }, 5000);
339
+ let redirectUrl = `${baseUrl}/403?returnUrl=${returnUrl}`;
340
+ if (error.error && error.error.required) {
341
+ redirectUrl += `&required=${encodeURIComponent(error.error.required)}`;
342
+ }
343
+ window.location.href = redirectUrl;
344
+ }
345
+ }
346
+ return throwError(() => error);
347
+ }));
348
+ };
349
+
350
+ class MesAuthModule {
351
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MesAuthModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
352
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.16", ngImport: i0, type: MesAuthModule });
353
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MesAuthModule, providers: [
354
+ MesAuthService
355
+ ] });
356
+ }
357
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MesAuthModule, decorators: [{
358
+ type: NgModule,
359
+ args: [{
360
+ providers: [
361
+ MesAuthService
362
+ ]
363
+ }]
364
+ }] });
365
+
366
+ class ThemeService {
367
+ _currentTheme = new BehaviorSubject('light');
368
+ currentTheme$ = this._currentTheme.asObservable();
369
+ observer = null;
370
+ constructor() {
371
+ this.detectTheme();
372
+ this.startWatching();
373
+ }
374
+ ngOnDestroy() {
375
+ this.stopWatching();
376
+ }
377
+ detectTheme() {
378
+ const html = document.documentElement;
379
+ const isDark = html.classList.contains('dark') ||
380
+ html.getAttribute('data-theme') === 'dark' ||
381
+ html.getAttribute('theme') === 'dark' ||
382
+ html.getAttribute('data-coreui-theme') === 'dark';
383
+ this._currentTheme.next(isDark ? 'dark' : 'light');
384
+ }
385
+ startWatching() {
386
+ if (typeof MutationObserver === 'undefined') {
387
+ // Fallback for older browsers - check periodically
388
+ setInterval(() => this.detectTheme(), 1000);
389
+ return;
390
+ }
391
+ this.observer = new MutationObserver(() => {
392
+ this.detectTheme();
393
+ });
394
+ this.observer.observe(document.documentElement, {
395
+ attributes: true,
396
+ attributeFilter: ['class', 'data-theme', 'theme', 'data-coreui-theme']
397
+ });
398
+ }
399
+ stopWatching() {
400
+ if (this.observer) {
401
+ this.observer.disconnect();
402
+ this.observer = null;
403
+ }
404
+ }
405
+ get currentTheme() {
406
+ return this._currentTheme.value;
407
+ }
408
+ // Method to manually set theme if needed
409
+ setTheme(theme) {
410
+ this._currentTheme.next(theme);
411
+ }
412
+ // Re-detect theme from DOM
413
+ refreshTheme() {
414
+ this.detectTheme();
415
+ }
416
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
417
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ThemeService, providedIn: 'root' });
418
+ }
419
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ThemeService, decorators: [{
420
+ type: Injectable,
421
+ args: [{
422
+ providedIn: 'root'
423
+ }]
424
+ }], ctorParameters: () => [] });
425
+
426
+ class UserProfileComponent {
427
+ authService;
428
+ router;
429
+ themeService;
430
+ cdr;
431
+ notificationClick = new EventEmitter();
432
+ get themeClass() {
433
+ return `theme-${this.currentTheme}`;
434
+ }
435
+ currentUser = signal(null, ...(ngDevMode ? [{ debugName: "currentUser" }] : []));
436
+ currentTheme = 'light';
437
+ unreadCount = 0;
438
+ dropdownOpen = false;
439
+ hasUser = false;
440
+ destroy$ = new Subject();
441
+ // Signal to force avatar refresh
442
+ avatarRefresh = signal(Date.now(), ...(ngDevMode ? [{ debugName: "avatarRefresh" }] : []));
443
+ constructor(authService, router, themeService, cdr) {
444
+ this.authService = authService;
445
+ this.router = router;
446
+ this.themeService = themeService;
447
+ this.cdr = cdr;
448
+ }
449
+ ngOnInit() {
450
+ this.authService.currentUser$
451
+ .pipe(takeUntil(this.destroy$))
452
+ .subscribe(user => {
453
+ this.currentUser.set(user);
454
+ this.hasUser = !!user;
455
+ // Force avatar refresh when user changes
456
+ this.avatarRefresh.set(Date.now());
457
+ if (!this.hasUser) {
458
+ this.unreadCount = 0;
459
+ }
460
+ else {
461
+ this.loadUnreadCount();
462
+ }
463
+ this.cdr.markForCheck();
464
+ });
465
+ this.themeService.currentTheme$
466
+ .pipe(takeUntil(this.destroy$))
467
+ .subscribe(theme => {
468
+ this.currentTheme = theme;
469
+ });
470
+ // Listen for new notifications
471
+ this.authService.notifications$
472
+ .pipe(takeUntil(this.destroy$))
473
+ .subscribe(() => {
474
+ console.log('Notification received, updating unread count');
475
+ if (this.hasUser) {
476
+ this.loadUnreadCount();
477
+ }
478
+ });
479
+ }
480
+ ngOnDestroy() {
481
+ this.destroy$.next();
482
+ this.destroy$.complete();
483
+ }
484
+ loadUnreadCount() {
485
+ if (!this.hasUser) {
486
+ this.unreadCount = 0;
487
+ return;
488
+ }
489
+ this.authService.getUnreadCount().subscribe({
490
+ next: (response) => {
491
+ this.unreadCount = response.unreadCount || 0;
492
+ },
493
+ error: (err) => { }
494
+ });
495
+ }
496
+ getAvatarUrl(user) {
497
+ // Use the refresh signal to force update
498
+ const refresh = this.avatarRefresh();
499
+ const config = this.authService.getConfig();
500
+ const baseUrl = config?.apiBaseUrl || '';
501
+ // If user has avatarPath, use it directly
502
+ if (user.avatarPath) {
503
+ // If avatarPath is already a full URL, use it as-is
504
+ if (user.avatarPath.startsWith('http://') || user.avatarPath.startsWith('https://')) {
505
+ return user.avatarPath;
506
+ }
507
+ // If it's a relative path, construct full URL with refresh timestamp
508
+ return `${baseUrl.replace(/\/$/, '')}${user.avatarPath}?t=${refresh}`;
509
+ }
510
+ // Fallback: construct URL using userId
511
+ const userId = user.userId;
512
+ if (userId && baseUrl) {
513
+ return `${baseUrl.replace(/\/$/, '')}/auth/${userId}/avatar?t=${refresh}`;
514
+ }
515
+ // Fallback to UI avatars service if no userId or baseUrl
516
+ const displayName = user.userName || user.userId || 'User';
517
+ return `https://ui-avatars.com/api/?name=${encodeURIComponent(displayName)}&background=1976d2&color=fff`;
518
+ }
519
+ getLastNameInitial(user) {
520
+ const fullName = user.fullName || user.userName || 'U';
521
+ const parts = fullName.split(' ');
522
+ const lastPart = parts[parts.length - 1];
523
+ return lastPart.charAt(0).toUpperCase();
524
+ }
525
+ toggleDropdown() {
526
+ this.dropdownOpen = !this.dropdownOpen;
527
+ }
528
+ onDocumentClick(event) {
529
+ const target = event.target;
530
+ const clickedInside = target.closest('.user-menu-wrapper');
531
+ if (!clickedInside) {
532
+ this.dropdownOpen = false;
533
+ }
534
+ }
535
+ onLogin() {
536
+ const config = this.authService.getConfig();
537
+ const baseUrl = config?.userBaseUrl || '';
538
+ const returnUrl = encodeURIComponent(this.router.url);
539
+ window.location.href = `${baseUrl}/login?returnUrl=${returnUrl}`;
540
+ }
541
+ onViewProfile() {
542
+ this.router.navigate(['/profile']);
543
+ this.dropdownOpen = false;
544
+ }
545
+ onLogout() {
546
+ this.authService.logout().subscribe({
547
+ next: () => {
548
+ // Clear current user after successful logout
549
+ this.dropdownOpen = false;
550
+ // Navigate to login with return URL
551
+ const config = this.authService.getConfig();
552
+ const baseUrl = config?.userBaseUrl || '';
553
+ const returnUrl = encodeURIComponent(window.location.href);
554
+ window.location.href = `${baseUrl}/login?returnUrl=${returnUrl}`;
555
+ },
556
+ error: (err) => {
557
+ // Still navigate to login even if logout fails
558
+ const config = this.authService.getConfig();
559
+ const baseUrl = config?.userBaseUrl || '';
560
+ window.location.href = `${baseUrl}/login`;
561
+ }
562
+ });
563
+ }
564
+ onNotificationClick() {
565
+ this.notificationClick.emit();
566
+ }
567
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: UserProfileComponent, deps: [{ token: MesAuthService }, { token: i2.Router }, { token: ThemeService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
568
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: UserProfileComponent, isStandalone: true, selector: "ma-user-profile", outputs: { notificationClick: "notificationClick" }, host: { listeners: { "document:click": "onDocumentClick($event)" }, properties: { "class": "this.themeClass" } }, ngImport: i0, template: `
569
+ <div class="user-profile-container">
570
+ <!-- Not logged in -->
571
+ <ng-container *ngIf="!currentUser()">
572
+ <button class="login-btn" (click)="onLogin()">
573
+ Login
574
+ </button>
575
+ </ng-container>
576
+
577
+ <!-- Logged in -->
578
+ <ng-container *ngIf="currentUser()">
579
+ <div class="user-header">
580
+ <button class="notification-btn" (click)="onNotificationClick()" title="Notifications">
581
+ <span class="icon">🔔</span>
582
+ <span class="badge" *ngIf="unreadCount > 0">{{ unreadCount }}</span>
583
+ </button>
584
+
585
+ <div class="user-menu-wrapper">
586
+ <button class="user-menu-btn" (click)="toggleDropdown()">
587
+ <img
588
+ *ngIf="currentUser().fullName || currentUser().userName"
589
+ [src]="getAvatarUrl(currentUser())"
590
+ [alt]="currentUser().fullName || currentUser().userName"
591
+ class="avatar"
592
+ />
593
+ <span *ngIf="!(currentUser().fullName || currentUser().userName)" class="avatar-initial">
594
+ {{ getLastNameInitial(currentUser()) }}
595
+ </span>
596
+ </button>
597
+
598
+ <div class="mes-dropdown-menu" *ngIf="dropdownOpen">
599
+ <div class="mes-dropdown-header">
600
+ {{ currentUser().fullName || currentUser().userName }}
601
+ </div>
602
+ <button class="mes-dropdown-item profile-link" (click)="onViewProfile()">
603
+ View Profile
604
+ </button>
605
+ <button class="mes-dropdown-item logout-item" (click)="onLogout()">
606
+ Logout
607
+ </button>
608
+ </div>
609
+ </div>
610
+ </div>
611
+ </ng-container>
612
+ </div>
613
+ `, isInline: true, styles: [":host{--primary-color: #1976d2;--primary-hover: #1565c0;--primary-light: rgba(25, 118, 210, .1);--error-color: #f44336;--error-light: #ffebee;--text-primary: #333;--text-secondary: #666;--text-muted: #999;--bg-primary: white;--bg-secondary: #f5f5f5;--bg-tertiary: #fafafa;--bg-hover: #f5f5f5;--border-color: #e0e0e0;--border-light: #f0f0f0;--shadow: rgba(0, 0, 0, .15);--shadow-light: rgba(0, 0, 0, .1)}:host(.theme-dark){--primary-color: #90caf9;--primary-hover: #64b5f6;--primary-light: rgba(144, 202, 249, .1);--error-color: #ef5350;--error-light: rgba(239, 83, 80, .1);--text-primary: #e0e0e0;--text-secondary: #b0b0b0;--text-muted: #888;--bg-primary: #1e1e1e;--bg-secondary: #2d2d2d;--bg-tertiary: #252525;--bg-hover: #333;--border-color: #404040;--border-light: #333;--shadow: rgba(0, 0, 0, .3);--shadow-light: rgba(0, 0, 0, .2)}.user-profile-container{display:flex;align-items:center;gap:16px;padding:0 16px}.login-btn{padding:8px 16px;background-color:var(--primary-color);color:#fff;border:none;border-radius:4px;cursor:pointer;font-weight:500;transition:background-color .3s}.login-btn:hover{background-color:var(--primary-hover)}.user-header{display:flex;align-items:center;gap:16px}.notification-btn{position:relative;background:none;border:none;font-size:24px;cursor:pointer;padding:8px;transition:opacity .2s}.notification-btn:hover{opacity:.7}.icon{display:inline-block}.badge{position:absolute;top:0;right:0;background-color:var(--error-color);color:#fff;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700}.user-menu-wrapper{position:relative}.user-menu-btn{background:none;border:none;cursor:pointer;padding:4px;border-radius:50%;transition:background-color .2s;display:flex;align-items:center;justify-content:center}.user-menu-btn:hover{background-color:var(--primary-light)}.avatar{width:40px;height:40px;border-radius:50%;object-fit:cover;background-color:#e0e0e0}.avatar-initial{width:40px;height:40px;border-radius:50%;background-color:var(--primary-color);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:16px}.mes-dropdown-menu{position:absolute;top:calc(100% + 8px);right:0;background:var(--bg-primary);border:1px solid var(--border-color);border-radius:4px;box-shadow:0 2px 8px var(--shadow);min-width:200px;z-index:1000;overflow:hidden}.mes-dropdown-header{padding:12px 16px;border-bottom:1px solid var(--border-light);font-weight:600;color:var(--text-primary);font-size:14px}.mes-dropdown-item{display:block;width:100%;padding:12px 16px;border:none;background:none;text-align:left;cursor:pointer;font-size:14px;color:var(--text-primary);text-decoration:none;transition:background-color .2s}.mes-dropdown-item:hover{background-color:var(--bg-hover)}.profile-link{color:var(--primary-color)}.logout-item{border-top:1px solid var(--border-light);color:var(--error-color)}.logout-item:hover{background-color:var(--error-light)}.user-info{display:flex;flex-direction:column;gap:2px}.user-name{font-weight:500;font-size:14px;color:var(--text-primary)}.user-position{font-size:12px;color:var(--text-secondary)}.logout-btn{background:none;border:none;font-size:20px;cursor:pointer;color:var(--text-secondary);padding:4px 8px;transition:color .2s}.logout-btn:hover{color:var(--primary-color)}@media(max-width:768px){.user-info{display:none}.avatar{width:32px;height:32px}}\n"], dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
614
+ }
615
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: UserProfileComponent, decorators: [{
616
+ type: Component,
617
+ args: [{ selector: 'ma-user-profile', standalone: true, imports: [NgIf], template: `
618
+ <div class="user-profile-container">
619
+ <!-- Not logged in -->
620
+ <ng-container *ngIf="!currentUser()">
621
+ <button class="login-btn" (click)="onLogin()">
622
+ Login
623
+ </button>
624
+ </ng-container>
625
+
626
+ <!-- Logged in -->
627
+ <ng-container *ngIf="currentUser()">
628
+ <div class="user-header">
629
+ <button class="notification-btn" (click)="onNotificationClick()" title="Notifications">
630
+ <span class="icon">🔔</span>
631
+ <span class="badge" *ngIf="unreadCount > 0">{{ unreadCount }}</span>
632
+ </button>
633
+
634
+ <div class="user-menu-wrapper">
635
+ <button class="user-menu-btn" (click)="toggleDropdown()">
636
+ <img
637
+ *ngIf="currentUser().fullName || currentUser().userName"
638
+ [src]="getAvatarUrl(currentUser())"
639
+ [alt]="currentUser().fullName || currentUser().userName"
640
+ class="avatar"
641
+ />
642
+ <span *ngIf="!(currentUser().fullName || currentUser().userName)" class="avatar-initial">
643
+ {{ getLastNameInitial(currentUser()) }}
644
+ </span>
645
+ </button>
646
+
647
+ <div class="mes-dropdown-menu" *ngIf="dropdownOpen">
648
+ <div class="mes-dropdown-header">
649
+ {{ currentUser().fullName || currentUser().userName }}
650
+ </div>
651
+ <button class="mes-dropdown-item profile-link" (click)="onViewProfile()">
652
+ View Profile
653
+ </button>
654
+ <button class="mes-dropdown-item logout-item" (click)="onLogout()">
655
+ Logout
656
+ </button>
657
+ </div>
658
+ </div>
659
+ </div>
660
+ </ng-container>
661
+ </div>
662
+ `, styles: [":host{--primary-color: #1976d2;--primary-hover: #1565c0;--primary-light: rgba(25, 118, 210, .1);--error-color: #f44336;--error-light: #ffebee;--text-primary: #333;--text-secondary: #666;--text-muted: #999;--bg-primary: white;--bg-secondary: #f5f5f5;--bg-tertiary: #fafafa;--bg-hover: #f5f5f5;--border-color: #e0e0e0;--border-light: #f0f0f0;--shadow: rgba(0, 0, 0, .15);--shadow-light: rgba(0, 0, 0, .1)}:host(.theme-dark){--primary-color: #90caf9;--primary-hover: #64b5f6;--primary-light: rgba(144, 202, 249, .1);--error-color: #ef5350;--error-light: rgba(239, 83, 80, .1);--text-primary: #e0e0e0;--text-secondary: #b0b0b0;--text-muted: #888;--bg-primary: #1e1e1e;--bg-secondary: #2d2d2d;--bg-tertiary: #252525;--bg-hover: #333;--border-color: #404040;--border-light: #333;--shadow: rgba(0, 0, 0, .3);--shadow-light: rgba(0, 0, 0, .2)}.user-profile-container{display:flex;align-items:center;gap:16px;padding:0 16px}.login-btn{padding:8px 16px;background-color:var(--primary-color);color:#fff;border:none;border-radius:4px;cursor:pointer;font-weight:500;transition:background-color .3s}.login-btn:hover{background-color:var(--primary-hover)}.user-header{display:flex;align-items:center;gap:16px}.notification-btn{position:relative;background:none;border:none;font-size:24px;cursor:pointer;padding:8px;transition:opacity .2s}.notification-btn:hover{opacity:.7}.icon{display:inline-block}.badge{position:absolute;top:0;right:0;background-color:var(--error-color);color:#fff;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700}.user-menu-wrapper{position:relative}.user-menu-btn{background:none;border:none;cursor:pointer;padding:4px;border-radius:50%;transition:background-color .2s;display:flex;align-items:center;justify-content:center}.user-menu-btn:hover{background-color:var(--primary-light)}.avatar{width:40px;height:40px;border-radius:50%;object-fit:cover;background-color:#e0e0e0}.avatar-initial{width:40px;height:40px;border-radius:50%;background-color:var(--primary-color);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:16px}.mes-dropdown-menu{position:absolute;top:calc(100% + 8px);right:0;background:var(--bg-primary);border:1px solid var(--border-color);border-radius:4px;box-shadow:0 2px 8px var(--shadow);min-width:200px;z-index:1000;overflow:hidden}.mes-dropdown-header{padding:12px 16px;border-bottom:1px solid var(--border-light);font-weight:600;color:var(--text-primary);font-size:14px}.mes-dropdown-item{display:block;width:100%;padding:12px 16px;border:none;background:none;text-align:left;cursor:pointer;font-size:14px;color:var(--text-primary);text-decoration:none;transition:background-color .2s}.mes-dropdown-item:hover{background-color:var(--bg-hover)}.profile-link{color:var(--primary-color)}.logout-item{border-top:1px solid var(--border-light);color:var(--error-color)}.logout-item:hover{background-color:var(--error-light)}.user-info{display:flex;flex-direction:column;gap:2px}.user-name{font-weight:500;font-size:14px;color:var(--text-primary)}.user-position{font-size:12px;color:var(--text-secondary)}.logout-btn{background:none;border:none;font-size:20px;cursor:pointer;color:var(--text-secondary);padding:4px 8px;transition:color .2s}.logout-btn:hover{color:var(--primary-color)}@media(max-width:768px){.user-info{display:none}.avatar{width:32px;height:32px}}\n"] }]
663
+ }], ctorParameters: () => [{ type: MesAuthService }, { type: i2.Router }, { type: ThemeService }, { type: i0.ChangeDetectorRef }], propDecorators: { notificationClick: [{
664
+ type: Output
665
+ }], themeClass: [{
666
+ type: HostBinding,
667
+ args: ['class']
668
+ }], onDocumentClick: [{
669
+ type: HostListener,
670
+ args: ['document:click', ['$event']]
671
+ }] } });
672
+
673
+ class ToastService {
674
+ toasts$ = new BehaviorSubject([]);
675
+ toasts = this.toasts$.asObservable();
676
+ show(message, title, type = 'info', duration = 5000) {
677
+ const id = Math.random().toString(36).substr(2, 9);
678
+ const toast = {
679
+ id,
680
+ message,
681
+ title,
682
+ type,
683
+ duration
684
+ };
685
+ const currentToasts = this.toasts$.value;
686
+ this.toasts$.next([...currentToasts, toast]);
687
+ if (duration > 0) {
688
+ setTimeout(() => {
689
+ this.remove(id);
690
+ }, duration);
691
+ }
692
+ return id;
693
+ }
694
+ remove(id) {
695
+ const currentToasts = this.toasts$.value;
696
+ this.toasts$.next(currentToasts.filter(t => t.id !== id));
697
+ }
698
+ clear() {
699
+ this.toasts$.next([]);
700
+ }
701
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ToastService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
702
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ToastService, providedIn: 'root' });
703
+ }
704
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ToastService, decorators: [{
705
+ type: Injectable,
706
+ args: [{ providedIn: 'root' }]
707
+ }] });
708
+
709
+ class ToastContainerComponent {
710
+ toastService;
711
+ themeService;
712
+ get themeClass() {
713
+ return `theme-${this.currentTheme}`;
714
+ }
715
+ toasts = [];
716
+ currentTheme = 'light';
717
+ destroy$ = new Subject();
718
+ constructor(toastService, themeService) {
719
+ this.toastService = toastService;
720
+ this.themeService = themeService;
721
+ }
722
+ ngOnInit() {
723
+ this.toastService.toasts
724
+ .pipe(takeUntil(this.destroy$))
725
+ .subscribe(toasts => {
726
+ this.toasts = toasts;
727
+ });
728
+ this.themeService.currentTheme$
729
+ .pipe(takeUntil(this.destroy$))
730
+ .subscribe(theme => {
731
+ this.currentTheme = theme;
732
+ });
733
+ }
734
+ ngOnDestroy() {
735
+ this.destroy$.next();
736
+ this.destroy$.complete();
737
+ }
738
+ close(id) {
739
+ this.toastService.remove(id);
740
+ }
741
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ToastContainerComponent, deps: [{ token: ToastService }, { token: ThemeService }], target: i0.ɵɵFactoryTarget.Component });
742
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: ToastContainerComponent, isStandalone: true, selector: "ma-toast-container", host: { properties: { "class": "this.themeClass" } }, ngImport: i0, template: `
743
+ <div class="toast-container">
744
+ <div
745
+ *ngFor="let toast of toasts"
746
+ class="toast"
747
+ [class]="'toast-' + toast.type"
748
+ [@slideIn]
749
+ >
750
+ <div class="toast-content">
751
+ <div *ngIf="toast.title" class="toast-title">{{ toast.title }}</div>
752
+ <div class="toast-message" [innerHTML]="toast.message"></div>
753
+ </div>
754
+ <button class="toast-close" (click)="close(toast.id)" aria-label="Close">
755
+
756
+ </button>
757
+ </div>
758
+ </div>
759
+ `, isInline: true, styles: [":host{--info-color: #2196f3;--success-color: #4caf50;--warning-color: #ff9800;--error-color: #f44336;--text-primary: #333;--bg-primary: white;--shadow: rgba(0, 0, 0, .15);--text-secondary: #999;--border-color: rgba(0, 0, 0, .1)}:host(.theme-dark){--info-color: #64b5f6;--success-color: #81c784;--warning-color: #ffb74d;--error-color: #ef5350;--text-primary: #e0e0e0;--bg-primary: #1e1e1e;--shadow: rgba(0, 0, 0, .3);--text-secondary: #888;--border-color: rgba(255, 255, 255, .1)}.toast-container{position:fixed;top:20px;right:20px;z-index:9999;pointer-events:none}.toast{display:flex;align-items:flex-start;gap:12px;padding:12px 16px;margin-bottom:12px;border-radius:4px;background:var(--bg-primary);border:1px solid var(--border-color);box-shadow:0 4px 12px var(--shadow);pointer-events:auto;min-width:280px;max-width:400px;animation:slideIn .3s ease-out}.toast-content{flex:1}.toast-title{font-weight:600;font-size:14px;margin-bottom:4px}.toast-message{font-size:13px;line-height:1.4}.toast-close{background:none;border:none;cursor:pointer;font-size:18px;color:var(--text-secondary);padding:0;width:24px;height:24px;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:color .2s}.toast-close:hover{color:var(--text-primary)}.toast-info{border-left:4px solid var(--info-color)}.toast-info .toast-title{color:var(--info-color)}.toast-info .toast-message{color:var(--text-primary)}.toast-success{border-left:4px solid var(--success-color)}.toast-success .toast-title{color:var(--success-color)}.toast-success .toast-message{color:var(--text-primary)}.toast-warning{border-left:4px solid var(--warning-color)}.toast-warning .toast-title{color:var(--warning-color)}.toast-warning .toast-message{color:var(--text-primary)}.toast-error{border-left:4px solid var(--error-color)}.toast-error .toast-title{color:var(--error-color)}.toast-error .toast-message{color:var(--text-primary)}@keyframes slideIn{0%{transform:translate(400px);opacity:0}to{transform:translate(0);opacity:1}}@media(max-width:600px){.toast-container{top:10px;right:10px;left:10px}.toast{min-width:auto;max-width:100%}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
760
+ }
761
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ToastContainerComponent, decorators: [{
762
+ type: Component,
763
+ args: [{ selector: 'ma-toast-container', standalone: true, imports: [CommonModule], template: `
764
+ <div class="toast-container">
765
+ <div
766
+ *ngFor="let toast of toasts"
767
+ class="toast"
768
+ [class]="'toast-' + toast.type"
769
+ [@slideIn]
770
+ >
771
+ <div class="toast-content">
772
+ <div *ngIf="toast.title" class="toast-title">{{ toast.title }}</div>
773
+ <div class="toast-message" [innerHTML]="toast.message"></div>
774
+ </div>
775
+ <button class="toast-close" (click)="close(toast.id)" aria-label="Close">
776
+
777
+ </button>
778
+ </div>
779
+ </div>
780
+ `, styles: [":host{--info-color: #2196f3;--success-color: #4caf50;--warning-color: #ff9800;--error-color: #f44336;--text-primary: #333;--bg-primary: white;--shadow: rgba(0, 0, 0, .15);--text-secondary: #999;--border-color: rgba(0, 0, 0, .1)}:host(.theme-dark){--info-color: #64b5f6;--success-color: #81c784;--warning-color: #ffb74d;--error-color: #ef5350;--text-primary: #e0e0e0;--bg-primary: #1e1e1e;--shadow: rgba(0, 0, 0, .3);--text-secondary: #888;--border-color: rgba(255, 255, 255, .1)}.toast-container{position:fixed;top:20px;right:20px;z-index:9999;pointer-events:none}.toast{display:flex;align-items:flex-start;gap:12px;padding:12px 16px;margin-bottom:12px;border-radius:4px;background:var(--bg-primary);border:1px solid var(--border-color);box-shadow:0 4px 12px var(--shadow);pointer-events:auto;min-width:280px;max-width:400px;animation:slideIn .3s ease-out}.toast-content{flex:1}.toast-title{font-weight:600;font-size:14px;margin-bottom:4px}.toast-message{font-size:13px;line-height:1.4}.toast-close{background:none;border:none;cursor:pointer;font-size:18px;color:var(--text-secondary);padding:0;width:24px;height:24px;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:color .2s}.toast-close:hover{color:var(--text-primary)}.toast-info{border-left:4px solid var(--info-color)}.toast-info .toast-title{color:var(--info-color)}.toast-info .toast-message{color:var(--text-primary)}.toast-success{border-left:4px solid var(--success-color)}.toast-success .toast-title{color:var(--success-color)}.toast-success .toast-message{color:var(--text-primary)}.toast-warning{border-left:4px solid var(--warning-color)}.toast-warning .toast-title{color:var(--warning-color)}.toast-warning .toast-message{color:var(--text-primary)}.toast-error{border-left:4px solid var(--error-color)}.toast-error .toast-title{color:var(--error-color)}.toast-error .toast-message{color:var(--text-primary)}@keyframes slideIn{0%{transform:translate(400px);opacity:0}to{transform:translate(0);opacity:1}}@media(max-width:600px){.toast-container{top:10px;right:10px;left:10px}.toast{min-width:auto;max-width:100%}}\n"] }]
781
+ }], ctorParameters: () => [{ type: ToastService }, { type: ThemeService }], propDecorators: { themeClass: [{
782
+ type: HostBinding,
783
+ args: ['class']
784
+ }] } });
785
+
786
+ class NotificationPanelComponent {
787
+ authService;
788
+ toastService;
789
+ themeService;
790
+ notificationRead = new EventEmitter();
791
+ get themeClass() {
792
+ return `theme-${this.currentTheme}`;
793
+ }
794
+ isOpen = false;
795
+ notifications = [];
796
+ currentTheme = 'light';
797
+ activeTab = 'unread'; // Default to unread tab
798
+ destroy$ = new Subject();
799
+ get unreadNotifications() {
800
+ return this.notifications.filter(n => !n.isRead);
801
+ }
802
+ get readNotifications() {
803
+ return this.notifications.filter(n => n.isRead);
804
+ }
805
+ get currentNotifications() {
806
+ return this.activeTab === 'unread' ? this.unreadNotifications : this.readNotifications;
807
+ }
808
+ selectedNotification = null;
809
+ // Returns plain text message for list display
810
+ getNotificationMessage(notification) {
811
+ return notification.message || '';
812
+ }
813
+ // Returns HTML message for modal display
814
+ getHtmlMessage(notification) {
815
+ return notification.messageHtml || notification.message || '';
816
+ }
817
+ constructor(authService, toastService, themeService) {
818
+ this.authService = authService;
819
+ this.toastService = toastService;
820
+ this.themeService = themeService;
821
+ }
822
+ ngOnInit() {
823
+ this.themeService.currentTheme$
824
+ .pipe(takeUntil(this.destroy$))
825
+ .subscribe(theme => {
826
+ this.currentTheme = theme;
827
+ });
828
+ this.loadNotifications();
829
+ // Listen for new real-time notifications
830
+ this.authService.notifications$
831
+ .pipe(takeUntil(this.destroy$))
832
+ .subscribe((notification) => {
833
+ // Show toast for new notification
834
+ this.toastService.show(notification.messageHtml || notification.message || '', notification.title, 'info', 5000);
835
+ // Reload notifications list
836
+ this.loadNotifications();
837
+ });
838
+ }
839
+ ngOnDestroy() {
840
+ this.destroy$.next();
841
+ this.destroy$.complete();
842
+ }
843
+ loadNotifications() {
844
+ this.authService.getNotifications(1, 50, true).subscribe({
845
+ next: (response) => {
846
+ this.notifications = response.items || [];
847
+ },
848
+ error: (err) => { }
849
+ });
850
+ }
851
+ open() {
852
+ this.isOpen = true;
853
+ this.activeTab = 'unread'; // Reset to unread tab when opening
854
+ }
855
+ close() {
856
+ this.isOpen = false;
857
+ }
858
+ switchTab(tab) {
859
+ this.activeTab = tab;
860
+ }
861
+ openDetails(notification) {
862
+ this.selectedNotification = notification;
863
+ // Mark as read when opening details (if not already read)
864
+ if (!notification.isRead) {
865
+ this.authService.markAsRead(notification.id).subscribe({
866
+ next: () => {
867
+ notification.isRead = true;
868
+ this.notificationRead.emit();
869
+ },
870
+ error: () => { }
871
+ });
872
+ }
873
+ }
874
+ closeDetails() {
875
+ this.selectedNotification = null;
876
+ }
877
+ markAsRead(notificationId, event) {
878
+ if (event) {
879
+ event.stopPropagation();
880
+ }
881
+ this.authService.markAsRead(notificationId).subscribe({
882
+ next: () => {
883
+ const notification = this.notifications.find(n => n.id === notificationId);
884
+ if (notification) {
885
+ notification.isRead = true;
886
+ this.notificationRead.emit();
887
+ }
888
+ },
889
+ error: (err) => { }
890
+ });
891
+ }
892
+ markAllAsRead() {
893
+ this.authService.markAllAsRead().subscribe({
894
+ next: () => {
895
+ this.notifications.forEach(n => n.isRead = true);
896
+ this.notificationRead.emit();
897
+ },
898
+ error: (err) => { }
899
+ });
900
+ }
901
+ deleteAllRead() {
902
+ const readNotificationIds = this.notifications
903
+ .filter(n => n.isRead)
904
+ .map(n => n.id);
905
+ // Delete all read notifications
906
+ const deletePromises = readNotificationIds.map(id => this.authService.deleteNotification(id).toPromise());
907
+ Promise.all(deletePromises).then(() => {
908
+ // Remove all read notifications from the local array
909
+ this.notifications = this.notifications.filter(n => !n.isRead);
910
+ }).catch((err) => {
911
+ // If bulk delete fails, reload notifications to get current state
912
+ this.loadNotifications();
913
+ });
914
+ }
915
+ deleteAllUnread() {
916
+ const unreadNotificationIds = this.notifications
917
+ .filter(n => !n.isRead)
918
+ .map(n => n.id);
919
+ // Delete all unread notifications
920
+ const deletePromises = unreadNotificationIds.map(id => this.authService.deleteNotification(id).toPromise());
921
+ Promise.all(deletePromises).then(() => {
922
+ // Remove all unread notifications from the local array
923
+ this.notifications = this.notifications.filter(n => n.isRead);
924
+ }).catch((err) => {
925
+ // If bulk delete fails, reload notifications to get current state
926
+ this.loadNotifications();
927
+ });
928
+ }
929
+ delete(notificationId, event) {
930
+ event.stopPropagation();
931
+ this.authService.deleteNotification(notificationId).subscribe({
932
+ next: () => {
933
+ this.notifications = this.notifications.filter(n => n.id !== notificationId);
934
+ },
935
+ error: (err) => { }
936
+ });
937
+ }
938
+ formatDate(dateString) {
939
+ const date = new Date(dateString);
940
+ const now = new Date();
941
+ const diffMs = now.getTime() - date.getTime();
942
+ const diffMins = Math.floor(diffMs / 60000);
943
+ const diffHours = Math.floor(diffMs / 3600000);
944
+ const diffDays = Math.floor(diffMs / 86400000);
945
+ if (diffMins < 1)
946
+ return 'Now';
947
+ if (diffMins < 60)
948
+ return `${diffMins}m ago`;
949
+ if (diffHours < 24)
950
+ return `${diffHours}h ago`;
951
+ if (diffDays < 7)
952
+ return `${diffDays}d ago`;
953
+ return date.toLocaleDateString();
954
+ }
955
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: NotificationPanelComponent, deps: [{ token: MesAuthService }, { token: ToastService }, { token: ThemeService }], target: i0.ɵɵFactoryTarget.Component });
956
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: NotificationPanelComponent, isStandalone: true, selector: "ma-notification-panel", outputs: { notificationRead: "notificationRead" }, host: { properties: { "class": "this.themeClass" } }, ngImport: i0, template: `
957
+ <div class="notification-panel" [class.open]="isOpen">
958
+ <!-- Header -->
959
+ <div class="panel-header">
960
+ <h3>Notifications</h3>
961
+ <button class="close-btn" (click)="close()" title="Close">✕</button>
962
+ </div>
963
+
964
+ <!-- Tabs -->
965
+ <div class="tabs">
966
+ <button
967
+ class="tab-btn"
968
+ [class.active]="activeTab === 'unread'"
969
+ (click)="switchTab('unread')"
970
+ >
971
+ Unread ({{ unreadNotifications.length }})
972
+ </button>
973
+ <button
974
+ class="tab-btn"
975
+ [class.active]="activeTab === 'read'"
976
+ (click)="switchTab('read')"
977
+ >
978
+ Read ({{ readNotifications.length }})
979
+ </button>
980
+ </div>
981
+
982
+ <!-- Notifications List -->
983
+ <div class="notifications-list">
984
+ <ng-container *ngIf="currentNotifications.length > 0">
985
+ <div
986
+ *ngFor="let notification of currentNotifications"
987
+ class="notification-item"
988
+ [class.unread]="!notification.isRead"
989
+ (click)="openDetails(notification)"
990
+ >
991
+ <div class="notification-content">
992
+ <div class="notification-title">{{ notification.title }}</div>
993
+ <div class="notification-message">{{ getNotificationMessage(notification) }}</div>
994
+ <div class="notification-meta">
995
+ <span class="app-name">{{ notification.sourceAppName }}</span>
996
+ <span class="time">{{ formatDate(notification.createdAt) }}</span>
997
+ </div>
998
+ </div>
999
+ <button
1000
+ class="read-btn"
1001
+ (click)="markAsRead(notification.id, $event)"
1002
+ title="Mark as read"
1003
+ *ngIf="!notification.isRead"
1004
+ >
1005
+
1006
+ </button>
1007
+ <button
1008
+ class="delete-btn"
1009
+ (click)="delete(notification.id, $event)"
1010
+ title="Delete notification"
1011
+ *ngIf="notification.isRead"
1012
+ >
1013
+ 🗑
1014
+ </button>
1015
+ </div>
1016
+ </ng-container>
1017
+
1018
+ <ng-container *ngIf="currentNotifications.length === 0">
1019
+ <div class="empty-state">
1020
+ No {{ activeTab }} notifications
1021
+ </div>
1022
+ </ng-container>
1023
+ </div>
1024
+
1025
+ <!-- Footer Actions -->
1026
+ <div class="panel-footer" *ngIf="currentNotifications.length > 0">
1027
+ <div class="footer-actions" *ngIf="activeTab === 'unread'">
1028
+ <button class="action-btn" (click)="markAllAsRead()" *ngIf="unreadNotifications.length > 0">
1029
+ Mark all as read
1030
+ </button>
1031
+ <button class="action-btn delete-all-btn" (click)="deleteAllUnread()" *ngIf="unreadNotifications.length > 0">
1032
+ Delete all
1033
+ </button>
1034
+ </div>
1035
+ <button class="action-btn delete-all-btn" (click)="deleteAllRead()" *ngIf="activeTab === 'read' && readNotifications.length > 0">
1036
+ Delete all
1037
+ </button>
1038
+ </div>
1039
+ </div>
1040
+
1041
+ <!-- Details Modal -->
1042
+ <div class="modal-overlay" *ngIf="selectedNotification" (click)="closeDetails()">
1043
+ <div class="modal-container" (click)="$event.stopPropagation()">
1044
+ <div class="modal-header">
1045
+ <h3>{{ selectedNotification.title }}</h3>
1046
+ <button class="close-btn" (click)="closeDetails()" title="Close">✕</button>
1047
+ </div>
1048
+ <div class="modal-meta">
1049
+ <span class="app-name">{{ selectedNotification.sourceAppName }}</span>
1050
+ <span class="time">{{ formatDate(selectedNotification.createdAt) }}</span>
1051
+ </div>
1052
+ <div class="modal-body" [innerHTML]="getHtmlMessage(selectedNotification)"></div>
1053
+ <div class="modal-footer">
1054
+ <button class="action-btn" (click)="closeDetails()">Close</button>
1055
+ </div>
1056
+ </div>
1057
+ </div>
1058
+ `, isInline: true, styles: [":host{--primary-color: #1976d2;--primary-hover: #1565c0;--success-color: #4caf50;--error-color: #f44336;--text-primary: #333;--text-secondary: #666;--text-muted: #999;--bg-primary: white;--bg-secondary: #f5f5f5;--bg-tertiary: #fafafa;--bg-hover: #f5f5f5;--bg-unread: #e3f2fd;--border-color: #e0e0e0;--border-light: #f0f0f0;--shadow: rgba(0, 0, 0, .1)}:host(.theme-dark){--primary-color: #90caf9;--primary-hover: #64b5f6;--success-color: #81c784;--error-color: #ef5350;--text-primary: #e0e0e0;--text-secondary: #b0b0b0;--text-muted: #888;--bg-primary: #1e1e1e;--bg-secondary: #2d2d2d;--bg-tertiary: #252525;--bg-hover: #333;--bg-unread: rgba(144, 202, 249, .1);--border-color: #404040;--border-light: #333;--shadow: rgba(0, 0, 0, .3)}.notification-panel{position:fixed;top:0;right:-350px;width:350px;height:100vh;background:var(--bg-primary);box-shadow:-2px 0 8px var(--shadow);display:flex;flex-direction:column;z-index:1000;transition:right .3s ease}.notification-panel.open{right:0}.panel-header{display:flex;justify-content:space-between;align-items:center;padding:16px;border-bottom:1px solid var(--border-color);background-color:var(--bg-secondary)}.panel-header h3{margin:0;font-size:18px;color:var(--text-primary)}.close-btn{background:none;border:none;font-size:20px;cursor:pointer;color:var(--text-secondary);padding:0;width:32px;height:32px;display:flex;align-items:center;justify-content:center;transition:color .2s}.close-btn:hover{color:var(--text-primary)}.tabs{display:flex;border-bottom:1px solid var(--border-color);background-color:var(--bg-secondary)}.tab-btn{flex:1;padding:12px 16px;background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:14px;font-weight:500;transition:all .2s;border-bottom:2px solid transparent}.tab-btn:hover{background-color:var(--bg-hover);color:var(--text-primary)}.tab-btn.active{color:var(--primary-color);border-bottom-color:var(--primary-color);background-color:var(--bg-primary)}.notifications-list{flex:1;overflow-y:auto}.notification-item{display:flex;gap:12px;padding:12px 16px;border-bottom:1px solid var(--border-light);cursor:pointer;background-color:var(--bg-tertiary);transition:background-color .2s}.notification-item:hover{background-color:var(--bg-hover)}.notification-item.unread{background-color:var(--bg-unread)}.notification-content{flex:1;min-width:0}.notification-title{font-weight:600;color:var(--text-primary);font-size:14px;margin-bottom:4px}.notification-message{color:var(--text-secondary);font-size:13px;line-height:1.4;margin-bottom:6px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.notification-meta{display:flex;justify-content:space-between;font-size:12px;color:var(--text-muted)}.app-name{font-weight:500;color:var(--primary-color)}.read-btn{background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:14px;padding:0;width:24px;height:24px;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:color .2s}.read-btn:hover{color:var(--success-color)}.delete-btn{background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:14px;padding:0;width:24px;height:24px;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:color .2s}.delete-btn:hover{color:var(--error-color)}.empty-state{display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-muted);font-size:14px}.panel-footer{padding:12px 16px;border-top:1px solid var(--border-color);background-color:var(--bg-secondary)}.footer-actions{display:flex;gap:8px}.footer-actions .action-btn{flex:1}.action-btn{width:100%;padding:8px;background-color:var(--primary-color);color:#fff;border:none;border-radius:4px;cursor:pointer;font-weight:500;transition:background-color .2s}.action-btn:hover{background-color:var(--primary-hover)}.delete-all-btn{background-color:var(--error-color);color:#fff}.delete-all-btn:hover{background-color:#d32f2f}.modal-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background-color:#00000080;display:flex;align-items:center;justify-content:center;z-index:1100}.modal-container{background:var(--bg-primary);border-radius:8px;width:90%;max-width:600px;max-height:80vh;display:flex;flex-direction:column;box-shadow:0 4px 20px var(--shadow)}.modal-header{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid var(--border-color);background-color:var(--bg-secondary);border-radius:8px 8px 0 0}.modal-header h3{margin:0;font-size:18px;color:var(--text-primary)}.modal-meta{display:flex;justify-content:space-between;padding:8px 20px;font-size:12px;color:var(--text-muted);background-color:var(--bg-tertiary);border-bottom:1px solid var(--border-light)}.modal-body{padding:20px;overflow-y:auto;flex:1;color:var(--text-primary);font-size:14px;line-height:1.6}.modal-footer{padding:12px 20px;border-top:1px solid var(--border-color);background-color:var(--bg-secondary);border-radius:0 0 8px 8px;display:flex;justify-content:flex-end}.modal-footer .action-btn{width:auto;padding:8px 24px}@media(max-width:600px){.notification-panel{width:100%;right:-100%}.modal-container{width:95%;max-height:90vh}}\n"], dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }] });
1059
+ }
1060
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: NotificationPanelComponent, decorators: [{
1061
+ type: Component,
1062
+ args: [{ selector: 'ma-notification-panel', standalone: true, imports: [NgIf, NgFor], template: `
1063
+ <div class="notification-panel" [class.open]="isOpen">
1064
+ <!-- Header -->
1065
+ <div class="panel-header">
1066
+ <h3>Notifications</h3>
1067
+ <button class="close-btn" (click)="close()" title="Close">✕</button>
1068
+ </div>
1069
+
1070
+ <!-- Tabs -->
1071
+ <div class="tabs">
1072
+ <button
1073
+ class="tab-btn"
1074
+ [class.active]="activeTab === 'unread'"
1075
+ (click)="switchTab('unread')"
1076
+ >
1077
+ Unread ({{ unreadNotifications.length }})
1078
+ </button>
1079
+ <button
1080
+ class="tab-btn"
1081
+ [class.active]="activeTab === 'read'"
1082
+ (click)="switchTab('read')"
1083
+ >
1084
+ Read ({{ readNotifications.length }})
1085
+ </button>
1086
+ </div>
1087
+
1088
+ <!-- Notifications List -->
1089
+ <div class="notifications-list">
1090
+ <ng-container *ngIf="currentNotifications.length > 0">
1091
+ <div
1092
+ *ngFor="let notification of currentNotifications"
1093
+ class="notification-item"
1094
+ [class.unread]="!notification.isRead"
1095
+ (click)="openDetails(notification)"
1096
+ >
1097
+ <div class="notification-content">
1098
+ <div class="notification-title">{{ notification.title }}</div>
1099
+ <div class="notification-message">{{ getNotificationMessage(notification) }}</div>
1100
+ <div class="notification-meta">
1101
+ <span class="app-name">{{ notification.sourceAppName }}</span>
1102
+ <span class="time">{{ formatDate(notification.createdAt) }}</span>
1103
+ </div>
1104
+ </div>
1105
+ <button
1106
+ class="read-btn"
1107
+ (click)="markAsRead(notification.id, $event)"
1108
+ title="Mark as read"
1109
+ *ngIf="!notification.isRead"
1110
+ >
1111
+
1112
+ </button>
1113
+ <button
1114
+ class="delete-btn"
1115
+ (click)="delete(notification.id, $event)"
1116
+ title="Delete notification"
1117
+ *ngIf="notification.isRead"
1118
+ >
1119
+ 🗑
1120
+ </button>
1121
+ </div>
1122
+ </ng-container>
1123
+
1124
+ <ng-container *ngIf="currentNotifications.length === 0">
1125
+ <div class="empty-state">
1126
+ No {{ activeTab }} notifications
1127
+ </div>
1128
+ </ng-container>
1129
+ </div>
1130
+
1131
+ <!-- Footer Actions -->
1132
+ <div class="panel-footer" *ngIf="currentNotifications.length > 0">
1133
+ <div class="footer-actions" *ngIf="activeTab === 'unread'">
1134
+ <button class="action-btn" (click)="markAllAsRead()" *ngIf="unreadNotifications.length > 0">
1135
+ Mark all as read
1136
+ </button>
1137
+ <button class="action-btn delete-all-btn" (click)="deleteAllUnread()" *ngIf="unreadNotifications.length > 0">
1138
+ Delete all
1139
+ </button>
1140
+ </div>
1141
+ <button class="action-btn delete-all-btn" (click)="deleteAllRead()" *ngIf="activeTab === 'read' && readNotifications.length > 0">
1142
+ Delete all
1143
+ </button>
1144
+ </div>
1145
+ </div>
1146
+
1147
+ <!-- Details Modal -->
1148
+ <div class="modal-overlay" *ngIf="selectedNotification" (click)="closeDetails()">
1149
+ <div class="modal-container" (click)="$event.stopPropagation()">
1150
+ <div class="modal-header">
1151
+ <h3>{{ selectedNotification.title }}</h3>
1152
+ <button class="close-btn" (click)="closeDetails()" title="Close">✕</button>
1153
+ </div>
1154
+ <div class="modal-meta">
1155
+ <span class="app-name">{{ selectedNotification.sourceAppName }}</span>
1156
+ <span class="time">{{ formatDate(selectedNotification.createdAt) }}</span>
1157
+ </div>
1158
+ <div class="modal-body" [innerHTML]="getHtmlMessage(selectedNotification)"></div>
1159
+ <div class="modal-footer">
1160
+ <button class="action-btn" (click)="closeDetails()">Close</button>
1161
+ </div>
1162
+ </div>
1163
+ </div>
1164
+ `, styles: [":host{--primary-color: #1976d2;--primary-hover: #1565c0;--success-color: #4caf50;--error-color: #f44336;--text-primary: #333;--text-secondary: #666;--text-muted: #999;--bg-primary: white;--bg-secondary: #f5f5f5;--bg-tertiary: #fafafa;--bg-hover: #f5f5f5;--bg-unread: #e3f2fd;--border-color: #e0e0e0;--border-light: #f0f0f0;--shadow: rgba(0, 0, 0, .1)}:host(.theme-dark){--primary-color: #90caf9;--primary-hover: #64b5f6;--success-color: #81c784;--error-color: #ef5350;--text-primary: #e0e0e0;--text-secondary: #b0b0b0;--text-muted: #888;--bg-primary: #1e1e1e;--bg-secondary: #2d2d2d;--bg-tertiary: #252525;--bg-hover: #333;--bg-unread: rgba(144, 202, 249, .1);--border-color: #404040;--border-light: #333;--shadow: rgba(0, 0, 0, .3)}.notification-panel{position:fixed;top:0;right:-350px;width:350px;height:100vh;background:var(--bg-primary);box-shadow:-2px 0 8px var(--shadow);display:flex;flex-direction:column;z-index:1000;transition:right .3s ease}.notification-panel.open{right:0}.panel-header{display:flex;justify-content:space-between;align-items:center;padding:16px;border-bottom:1px solid var(--border-color);background-color:var(--bg-secondary)}.panel-header h3{margin:0;font-size:18px;color:var(--text-primary)}.close-btn{background:none;border:none;font-size:20px;cursor:pointer;color:var(--text-secondary);padding:0;width:32px;height:32px;display:flex;align-items:center;justify-content:center;transition:color .2s}.close-btn:hover{color:var(--text-primary)}.tabs{display:flex;border-bottom:1px solid var(--border-color);background-color:var(--bg-secondary)}.tab-btn{flex:1;padding:12px 16px;background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:14px;font-weight:500;transition:all .2s;border-bottom:2px solid transparent}.tab-btn:hover{background-color:var(--bg-hover);color:var(--text-primary)}.tab-btn.active{color:var(--primary-color);border-bottom-color:var(--primary-color);background-color:var(--bg-primary)}.notifications-list{flex:1;overflow-y:auto}.notification-item{display:flex;gap:12px;padding:12px 16px;border-bottom:1px solid var(--border-light);cursor:pointer;background-color:var(--bg-tertiary);transition:background-color .2s}.notification-item:hover{background-color:var(--bg-hover)}.notification-item.unread{background-color:var(--bg-unread)}.notification-content{flex:1;min-width:0}.notification-title{font-weight:600;color:var(--text-primary);font-size:14px;margin-bottom:4px}.notification-message{color:var(--text-secondary);font-size:13px;line-height:1.4;margin-bottom:6px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.notification-meta{display:flex;justify-content:space-between;font-size:12px;color:var(--text-muted)}.app-name{font-weight:500;color:var(--primary-color)}.read-btn{background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:14px;padding:0;width:24px;height:24px;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:color .2s}.read-btn:hover{color:var(--success-color)}.delete-btn{background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:14px;padding:0;width:24px;height:24px;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:color .2s}.delete-btn:hover{color:var(--error-color)}.empty-state{display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-muted);font-size:14px}.panel-footer{padding:12px 16px;border-top:1px solid var(--border-color);background-color:var(--bg-secondary)}.footer-actions{display:flex;gap:8px}.footer-actions .action-btn{flex:1}.action-btn{width:100%;padding:8px;background-color:var(--primary-color);color:#fff;border:none;border-radius:4px;cursor:pointer;font-weight:500;transition:background-color .2s}.action-btn:hover{background-color:var(--primary-hover)}.delete-all-btn{background-color:var(--error-color);color:#fff}.delete-all-btn:hover{background-color:#d32f2f}.modal-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background-color:#00000080;display:flex;align-items:center;justify-content:center;z-index:1100}.modal-container{background:var(--bg-primary);border-radius:8px;width:90%;max-width:600px;max-height:80vh;display:flex;flex-direction:column;box-shadow:0 4px 20px var(--shadow)}.modal-header{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid var(--border-color);background-color:var(--bg-secondary);border-radius:8px 8px 0 0}.modal-header h3{margin:0;font-size:18px;color:var(--text-primary)}.modal-meta{display:flex;justify-content:space-between;padding:8px 20px;font-size:12px;color:var(--text-muted);background-color:var(--bg-tertiary);border-bottom:1px solid var(--border-light)}.modal-body{padding:20px;overflow-y:auto;flex:1;color:var(--text-primary);font-size:14px;line-height:1.6}.modal-footer{padding:12px 20px;border-top:1px solid var(--border-color);background-color:var(--bg-secondary);border-radius:0 0 8px 8px;display:flex;justify-content:flex-end}.modal-footer .action-btn{width:auto;padding:8px 24px}@media(max-width:600px){.notification-panel{width:100%;right:-100%}.modal-container{width:95%;max-height:90vh}}\n"] }]
1165
+ }], ctorParameters: () => [{ type: MesAuthService }, { type: ToastService }, { type: ThemeService }], propDecorators: { notificationRead: [{
1166
+ type: Output
1167
+ }], themeClass: [{
1168
+ type: HostBinding,
1169
+ args: ['class']
1170
+ }] } });
1171
+
1172
+ class MaUserComponent {
1173
+ userProfile;
1174
+ ngAfterViewInit() {
1175
+ // Ensure proper initialization
1176
+ if (this.userProfile) {
1177
+ this.userProfile.loadUnreadCount();
1178
+ }
1179
+ }
1180
+ onNotificationRead() {
1181
+ if (this.userProfile) {
1182
+ this.userProfile.loadUnreadCount();
1183
+ }
1184
+ }
1185
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MaUserComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1186
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: MaUserComponent, isStandalone: true, selector: "ma-user", viewQueries: [{ propertyName: "userProfile", first: true, predicate: UserProfileComponent, descendants: true }], ngImport: i0, template: `
1187
+ <ma-toast-container></ma-toast-container>
1188
+ <div class="user-header">
1189
+ <ma-user-profile (notificationClick)="notificationPanel.open()"></ma-user-profile>
1190
+ </div>
1191
+ <ma-notification-panel #notificationPanel (notificationRead)="onNotificationRead()"></ma-notification-panel>
1192
+ `, isInline: true, styles: [".user-header{display:flex;justify-content:flex-end}\n"], dependencies: [{ kind: "component", type: ToastContainerComponent, selector: "ma-toast-container" }, { kind: "component", type: UserProfileComponent, selector: "ma-user-profile", outputs: ["notificationClick"] }, { kind: "component", type: NotificationPanelComponent, selector: "ma-notification-panel", outputs: ["notificationRead"] }] });
1193
+ }
1194
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MaUserComponent, decorators: [{
1195
+ type: Component,
1196
+ args: [{ selector: 'ma-user', standalone: true, imports: [ToastContainerComponent, UserProfileComponent, NotificationPanelComponent], template: `
1197
+ <ma-toast-container></ma-toast-container>
1198
+ <div class="user-header">
1199
+ <ma-user-profile (notificationClick)="notificationPanel.open()"></ma-user-profile>
1200
+ </div>
1201
+ <ma-notification-panel #notificationPanel (notificationRead)="onNotificationRead()"></ma-notification-panel>
1202
+ `, styles: [".user-header{display:flex;justify-content:flex-end}\n"] }]
1203
+ }], propDecorators: { userProfile: [{
1204
+ type: ViewChild,
1205
+ args: [UserProfileComponent]
1206
+ }] } });
1207
+
1208
+ class NotificationBadgeComponent {
1209
+ authService;
1210
+ themeService;
1211
+ notificationClick = new EventEmitter();
1212
+ get themeClass() {
1213
+ return `theme-${this.currentTheme}`;
1214
+ }
1215
+ unreadCount = 0;
1216
+ currentTheme = 'light';
1217
+ hasUser = false;
1218
+ destroy$ = new Subject();
1219
+ constructor(authService, themeService) {
1220
+ this.authService = authService;
1221
+ this.themeService = themeService;
1222
+ }
1223
+ ngOnInit() {
1224
+ this.themeService.currentTheme$
1225
+ .pipe(takeUntil(this.destroy$))
1226
+ .subscribe(theme => {
1227
+ this.currentTheme = theme;
1228
+ });
1229
+ this.authService.currentUser$
1230
+ .pipe(takeUntil(this.destroy$))
1231
+ .subscribe(user => {
1232
+ this.hasUser = !!user;
1233
+ if (!this.hasUser) {
1234
+ this.unreadCount = 0;
1235
+ return;
1236
+ }
1237
+ this.loadUnreadCount();
1238
+ });
1239
+ // Listen for new notifications
1240
+ this.authService.notifications$
1241
+ .pipe(takeUntil(this.destroy$))
1242
+ .subscribe(() => {
1243
+ if (this.hasUser) {
1244
+ this.loadUnreadCount();
1245
+ }
1246
+ });
1247
+ }
1248
+ ngOnDestroy() {
1249
+ this.destroy$.next();
1250
+ this.destroy$.complete();
1251
+ }
1252
+ loadUnreadCount() {
1253
+ if (!this.hasUser) {
1254
+ this.unreadCount = 0;
1255
+ return;
1256
+ }
1257
+ this.authService.getUnreadCount().subscribe({
1258
+ next: (response) => {
1259
+ this.unreadCount = response.unreadCount || 0;
1260
+ },
1261
+ error: (err) => console.error('Error loading unread count:', err)
1262
+ });
1263
+ }
1264
+ onNotificationClick() {
1265
+ this.notificationClick.emit();
1266
+ }
1267
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: NotificationBadgeComponent, deps: [{ token: MesAuthService }, { token: ThemeService }], target: i0.ɵɵFactoryTarget.Component });
1268
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: NotificationBadgeComponent, isStandalone: true, selector: "ma-notification-badge", outputs: { notificationClick: "notificationClick" }, host: { properties: { "class": "this.themeClass" } }, ngImport: i0, template: `
1269
+ <button class="notification-btn" (click)="onNotificationClick()" title="Notifications">
1270
+ <span class="icon">🔔</span>
1271
+ <span class="badge" *ngIf="unreadCount > 0">{{ unreadCount }}</span>
1272
+ </button>
1273
+ `, isInline: true, styles: [":host{--error-color: #f44336}:host(.theme-dark){--error-color: #ef5350}.notification-btn{position:relative;background:none;border:none;font-size:24px;cursor:pointer;padding:8px;transition:opacity .2s}.notification-btn:hover{opacity:.7}.icon{display:inline-block}.badge{position:absolute;top:0;right:0;background-color:var(--error-color);color:#fff;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700}\n"], dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
1274
+ }
1275
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: NotificationBadgeComponent, decorators: [{
1276
+ type: Component,
1277
+ args: [{ selector: 'ma-notification-badge', standalone: true, imports: [NgIf], template: `
1278
+ <button class="notification-btn" (click)="onNotificationClick()" title="Notifications">
1279
+ <span class="icon">🔔</span>
1280
+ <span class="badge" *ngIf="unreadCount > 0">{{ unreadCount }}</span>
1281
+ </button>
1282
+ `, styles: [":host{--error-color: #f44336}:host(.theme-dark){--error-color: #ef5350}.notification-btn{position:relative;background:none;border:none;font-size:24px;cursor:pointer;padding:8px;transition:opacity .2s}.notification-btn:hover{opacity:.7}.icon{display:inline-block}.badge{position:absolute;top:0;right:0;background-color:var(--error-color);color:#fff;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700}\n"] }]
1283
+ }], ctorParameters: () => [{ type: MesAuthService }, { type: ThemeService }], propDecorators: { notificationClick: [{
1284
+ type: Output
1285
+ }], themeClass: [{
1286
+ type: HostBinding,
1287
+ args: ['class']
1288
+ }] } });
1289
+
1290
+ /**
1291
+ * Generated bundle index. Do not edit.
1292
+ */
1293
+
1294
+ export { MES_AUTH_CONFIG, MaUserComponent, MesAuthModule, MesAuthService, NotificationBadgeComponent, NotificationPanelComponent, NotificationType, ThemeService, ToastContainerComponent, ToastService, UserProfileComponent, mesAuthInterceptor, provideMesAuth };
1295
+ //# sourceMappingURL=mesauth-angular.mjs.map