mesauth-angular 1.2.3 → 1.2.4

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,698 @@
1
+ import { Component, OnInit, OnDestroy, HostBinding, Output, EventEmitter, inject } from '@angular/core';
2
+ import { NgIf, NgFor } from '@angular/common';
3
+ import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
4
+ import { MesAuthService, NotificationDto, PagedList, RealTimeNotificationDto } from './mes-auth.service';
5
+ import { ToastService } from './toast.service';
6
+ import { ThemeService, Theme } from './theme.service';
7
+ import { Subject } from 'rxjs';
8
+ import { takeUntil } from 'rxjs/operators';
9
+
10
+ @Component({
11
+ selector: 'ma-notification-panel',
12
+ standalone: true,
13
+ imports: [NgIf, NgFor],
14
+ template: `
15
+ <div class="notification-panel" [class.open]="isOpen">
16
+ <!-- Header -->
17
+ <div class="panel-header">
18
+ <h3>Notifications</h3>
19
+ <button class="close-btn" (click)="close()" title="Close">✕</button>
20
+ </div>
21
+
22
+ <!-- Tabs -->
23
+ <div class="tabs">
24
+ <button
25
+ class="tab-btn"
26
+ [class.active]="activeTab === 'unread'"
27
+ (click)="switchTab('unread')"
28
+ >
29
+ Unread ({{ unreadNotifications.length }})
30
+ </button>
31
+ <button
32
+ class="tab-btn"
33
+ [class.active]="activeTab === 'read'"
34
+ (click)="switchTab('read')"
35
+ >
36
+ Read ({{ readNotifications.length }})
37
+ </button>
38
+ </div>
39
+
40
+ <!-- Notifications List -->
41
+ <div class="notifications-list">
42
+ <ng-container *ngIf="currentNotifications.length > 0">
43
+ <div
44
+ *ngFor="let notification of currentNotifications"
45
+ class="notification-item"
46
+ [class.unread]="!notification.isRead"
47
+ (click)="openDetails(notification)"
48
+ >
49
+ <div class="notification-content">
50
+ <div class="notification-title">{{ notification.title }}</div>
51
+ <div class="notification-message">{{ getNotificationMessage(notification) }}</div>
52
+ <div class="notification-meta">
53
+ <span class="app-name">{{ notification.sourceAppName }}</span>
54
+ <span class="time">{{ formatDate(notification.createdAt) }}</span>
55
+ </div>
56
+ </div>
57
+ <button
58
+ class="read-btn"
59
+ (click)="markAsRead(notification.id, $event)"
60
+ title="Mark as read"
61
+ *ngIf="!notification.isRead"
62
+ >
63
+
64
+ </button>
65
+ <button
66
+ class="delete-btn"
67
+ (click)="delete(notification.id, $event)"
68
+ title="Delete notification"
69
+ *ngIf="notification.isRead"
70
+ >
71
+ 🗑
72
+ </button>
73
+ </div>
74
+ </ng-container>
75
+
76
+ <ng-container *ngIf="currentNotifications.length === 0">
77
+ <div class="empty-state">
78
+ No {{ activeTab }} notifications
79
+ </div>
80
+ </ng-container>
81
+ </div>
82
+
83
+ <!-- Footer Actions -->
84
+ <div class="panel-footer" *ngIf="currentNotifications.length > 0">
85
+ <div class="footer-actions" *ngIf="activeTab === 'unread'">
86
+ <button class="action-btn" (click)="markAllAsRead()" *ngIf="unreadNotifications.length > 0">
87
+ Mark all as read
88
+ </button>
89
+ <button class="action-btn delete-all-btn" (click)="deleteAllUnread()" *ngIf="unreadNotifications.length > 0">
90
+ Delete all
91
+ </button>
92
+ </div>
93
+ <button class="action-btn delete-all-btn" (click)="deleteAllRead()" *ngIf="activeTab === 'read' && readNotifications.length > 0">
94
+ Delete all
95
+ </button>
96
+ </div>
97
+ </div>
98
+
99
+ <!-- Details Modal -->
100
+ <div class="modal-overlay" *ngIf="selectedNotification" (click)="closeDetails()">
101
+ <div class="modal-container" (click)="$event.stopPropagation()">
102
+ <div class="modal-header">
103
+ <h3>{{ selectedNotification.title }}</h3>
104
+ <button class="close-btn" (click)="closeDetails()" title="Close">✕</button>
105
+ </div>
106
+ <div class="modal-meta">
107
+ <span class="app-name">{{ selectedNotification.sourceAppName }}</span>
108
+ <span class="time">{{ formatDate(selectedNotification.createdAt) }}</span>
109
+ </div>
110
+ <div class="modal-body" [innerHTML]="getHtmlMessage(selectedNotification)"></div>
111
+ <div class="modal-footer">
112
+ <button class="action-btn" (click)="closeDetails()">Close</button>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ `,
117
+ styles: [`
118
+ :host {
119
+ display: block;
120
+ position: relative;
121
+ --primary-color: #1976d2;
122
+ --primary-hover: #1565c0;
123
+ --success-color: #4caf50;
124
+ --error-color: #f44336;
125
+ --text-primary: #333;
126
+ --text-secondary: #666;
127
+ --text-muted: #999;
128
+ --bg-primary: white;
129
+ --bg-secondary: #f5f5f5;
130
+ --bg-tertiary: #fafafa;
131
+ --bg-hover: #f5f5f5;
132
+ --bg-unread: #e3f2fd;
133
+ --border-color: #e0e0e0;
134
+ --border-light: #f0f0f0;
135
+ --shadow: rgba(0, 0, 0, 0.1);
136
+ }
137
+
138
+ :host(.theme-dark) {
139
+ display: block;
140
+ position: relative;
141
+ --primary-color: #90caf9;
142
+ --primary-hover: #64b5f6;
143
+ --success-color: #81c784;
144
+ --error-color: #ef5350;
145
+ --text-primary: #e0e0e0;
146
+ --text-secondary: #b0b0b0;
147
+ --text-muted: #888;
148
+ --bg-primary: #1e1e1e;
149
+ --bg-secondary: #2d2d2d;
150
+ --bg-tertiary: #252525;
151
+ --bg-hover: #333;
152
+ --bg-unread: rgba(144, 202, 249, 0.1);
153
+ --border-color: #404040;
154
+ --border-light: #333;
155
+ --shadow: rgba(0, 0, 0, 0.3);
156
+ }
157
+
158
+ .notification-panel {
159
+ position: fixed;
160
+ top: 0;
161
+ right: -350px;
162
+ width: 350px;
163
+ height: 100vh;
164
+ background: var(--bg-primary);
165
+ box-shadow: -2px 0 8px var(--shadow);
166
+ display: flex;
167
+ flex-direction: column;
168
+ z-index: 1030;
169
+ transition: right 0.3s ease;
170
+ }
171
+
172
+ .notification-panel.open {
173
+ right: 0;
174
+ }
175
+
176
+ .panel-header {
177
+ display: flex;
178
+ justify-content: space-between;
179
+ align-items: center;
180
+ padding: 16px;
181
+ border-bottom: 1px solid var(--border-color);
182
+ background-color: var(--bg-secondary);
183
+ }
184
+
185
+ .panel-header h3 {
186
+ margin: 0;
187
+ font-size: 18px;
188
+ color: var(--text-primary);
189
+ }
190
+
191
+ .close-btn {
192
+ background: none;
193
+ border: none;
194
+ font-size: 20px;
195
+ cursor: pointer;
196
+ color: var(--text-secondary);
197
+ padding: 0;
198
+ width: 32px;
199
+ height: 32px;
200
+ display: flex;
201
+ align-items: center;
202
+ justify-content: center;
203
+ transition: color 0.2s;
204
+ }
205
+
206
+ .close-btn:hover {
207
+ color: var(--text-primary);
208
+ }
209
+
210
+ .tabs {
211
+ display: flex;
212
+ border-bottom: 1px solid var(--border-color);
213
+ background-color: var(--bg-secondary);
214
+ }
215
+
216
+ .tab-btn {
217
+ flex: 1;
218
+ padding: 12px 16px;
219
+ background: none;
220
+ border: none;
221
+ color: var(--text-secondary);
222
+ cursor: pointer;
223
+ font-size: 14px;
224
+ font-weight: 500;
225
+ transition: all 0.2s;
226
+ border-bottom: 2px solid transparent;
227
+ }
228
+
229
+ .tab-btn:hover {
230
+ background-color: var(--bg-hover);
231
+ color: var(--text-primary);
232
+ }
233
+
234
+ .tab-btn.active {
235
+ color: var(--primary-color);
236
+ border-bottom-color: var(--primary-color);
237
+ background-color: var(--bg-primary);
238
+ }
239
+
240
+ .notifications-list {
241
+ flex: 1;
242
+ overflow-y: auto;
243
+ }
244
+
245
+ .notification-item {
246
+ display: flex;
247
+ gap: 12px;
248
+ padding: 12px 16px;
249
+ border-bottom: 1px solid var(--border-light);
250
+ cursor: pointer;
251
+ background-color: var(--bg-tertiary);
252
+ transition: background-color 0.2s;
253
+ }
254
+
255
+ .notification-item:hover {
256
+ background-color: var(--bg-hover);
257
+ }
258
+
259
+ .notification-item.unread {
260
+ background-color: var(--bg-unread);
261
+ }
262
+
263
+ .notification-content {
264
+ flex: 1;
265
+ min-width: 0;
266
+ }
267
+
268
+ .notification-title {
269
+ font-weight: 600;
270
+ color: var(--text-primary);
271
+ font-size: 14px;
272
+ margin-bottom: 4px;
273
+ }
274
+
275
+ .notification-message {
276
+ color: var(--text-secondary);
277
+ font-size: 12px;
278
+ line-height: 1.4;
279
+ margin-bottom: 6px;
280
+ display: -webkit-box;
281
+ -webkit-line-clamp: 2;
282
+ -webkit-box-orient: vertical;
283
+ overflow: hidden;
284
+ }
285
+
286
+ .notification-meta {
287
+ display: flex;
288
+ justify-content: space-between;
289
+ font-size: 12px;
290
+ color: var(--text-muted);
291
+ }
292
+
293
+ .app-name {
294
+ font-weight: 500;
295
+ color: var(--primary-color);
296
+ }
297
+
298
+ .read-btn {
299
+ background: none;
300
+ border: none;
301
+ color: var(--text-muted);
302
+ cursor: pointer;
303
+ font-size: 14px;
304
+ padding: 0;
305
+ width: 24px;
306
+ height: 24px;
307
+ display: flex;
308
+ align-items: center;
309
+ justify-content: center;
310
+ flex-shrink: 0;
311
+ transition: color 0.2s;
312
+ }
313
+
314
+ .read-btn:hover {
315
+ color: var(--success-color);
316
+ }
317
+
318
+ .delete-btn {
319
+ background: none;
320
+ border: none;
321
+ color: var(--text-muted);
322
+ cursor: pointer;
323
+ font-size: 14px;
324
+ padding: 0;
325
+ width: 24px;
326
+ height: 24px;
327
+ display: flex;
328
+ align-items: center;
329
+ justify-content: center;
330
+ flex-shrink: 0;
331
+ transition: color 0.2s;
332
+ }
333
+
334
+ .delete-btn:hover {
335
+ color: var(--error-color);
336
+ }
337
+
338
+ .empty-state {
339
+ display: flex;
340
+ align-items: center;
341
+ justify-content: center;
342
+ height: 100%;
343
+ color: var(--text-muted);
344
+ font-size: 14px;
345
+ }
346
+
347
+ .panel-footer {
348
+ padding: 12px 16px;
349
+ border-top: 1px solid var(--border-color);
350
+ background-color: var(--bg-secondary);
351
+ }
352
+
353
+ .footer-actions {
354
+ display: flex;
355
+ gap: 8px;
356
+ }
357
+
358
+ .footer-actions .action-btn {
359
+ flex: 1;
360
+ }
361
+
362
+ .action-btn {
363
+ width: 100%;
364
+ padding: 8px;
365
+ background-color: var(--primary-color);
366
+ color: white;
367
+ border: none;
368
+ border-radius: 4px;
369
+ cursor: pointer;
370
+ font-weight: 500;
371
+ transition: background-color 0.2s;
372
+ }
373
+
374
+ .action-btn:hover {
375
+ background-color: var(--primary-hover);
376
+ }
377
+
378
+ .delete-all-btn {
379
+ background-color: var(--error-color);
380
+ color: white;
381
+ }
382
+
383
+ .delete-all-btn:hover {
384
+ background-color: #d32f2f; /* Darker red for hover */
385
+ }
386
+
387
+ /* Modal Overlay */
388
+ .modal-overlay {
389
+ position: fixed;
390
+ top: 0;
391
+ left: 0;
392
+ right: 0;
393
+ bottom: 0;
394
+ width: 100vw;
395
+ height: 100vh;
396
+ background-color: rgba(0, 0, 0, 0.5);
397
+ display: flex;
398
+ align-items: center;
399
+ justify-content: center;
400
+ z-index: 1060;
401
+ }
402
+
403
+ .modal-container {
404
+ background: var(--bg-primary);
405
+ border-radius: 8px;
406
+ width: 90%;
407
+ max-width: 600px;
408
+ max-height: 80vh;
409
+ display: flex;
410
+ flex-direction: column;
411
+ box-shadow: 0 4px 20px var(--shadow);
412
+ }
413
+
414
+ .modal-header {
415
+ display: flex;
416
+ justify-content: space-between;
417
+ align-items: center;
418
+ padding: 16px 20px;
419
+ border-bottom: 1px solid var(--border-color);
420
+ background-color: var(--bg-secondary);
421
+ border-radius: 8px 8px 0 0;
422
+ }
423
+
424
+ .modal-header h3 {
425
+ margin: 0;
426
+ font-size: 18px;
427
+ color: var(--text-primary);
428
+ }
429
+
430
+ .modal-meta {
431
+ display: flex;
432
+ justify-content: space-between;
433
+ padding: 8px 20px;
434
+ font-size: 12px;
435
+ color: var(--text-muted);
436
+ background-color: var(--bg-tertiary);
437
+ border-bottom: 1px solid var(--border-light);
438
+ }
439
+
440
+ .modal-body {
441
+ padding: 20px;
442
+ overflow-y: auto;
443
+ flex: 1;
444
+ color: var(--text-primary);
445
+ font-size: 14px;
446
+ line-height: 1.6;
447
+ }
448
+
449
+ .modal-footer {
450
+ padding: 12px 20px;
451
+ border-top: 1px solid var(--border-color);
452
+ background-color: var(--bg-secondary);
453
+ border-radius: 0 0 8px 8px;
454
+ display: flex;
455
+ justify-content: flex-end;
456
+ }
457
+
458
+ .modal-footer .action-btn {
459
+ width: auto;
460
+ padding: 8px 24px;
461
+ }
462
+
463
+ @media (max-width: 600px) {
464
+ .notification-panel {
465
+ width: 100%;
466
+ right: -100%;
467
+ }
468
+
469
+ .modal-container {
470
+ width: 95%;
471
+ max-height: 90vh;
472
+ }
473
+ }
474
+ `]
475
+ })
476
+ export class NotificationPanelComponent implements OnInit, OnDestroy {
477
+ @Output() notificationRead = new EventEmitter<void>();
478
+ @HostBinding('class') get themeClass(): string {
479
+ return `theme-${this.currentTheme}`;
480
+ }
481
+
482
+ isOpen = false;
483
+ notifications: NotificationDto[] = [];
484
+ currentTheme: Theme = 'light';
485
+ activeTab: 'unread' | 'read' = 'unread'; // Default to unread tab
486
+ private destroy$ = new Subject<void>();
487
+
488
+ get unreadNotifications(): NotificationDto[] {
489
+ return this.notifications.filter(n => !n.isRead);
490
+ }
491
+
492
+ get readNotifications(): NotificationDto[] {
493
+ return this.notifications.filter(n => n.isRead);
494
+ }
495
+
496
+ get currentNotifications(): NotificationDto[] {
497
+ return this.activeTab === 'unread' ? this.unreadNotifications : this.readNotifications;
498
+ }
499
+
500
+ selectedNotification: NotificationDto | null = null;
501
+
502
+ // Returns plain text message for list display
503
+ getNotificationMessage(notification: NotificationDto): string {
504
+ return notification.message || '';
505
+ }
506
+
507
+ private readonly sanitizer = inject(DomSanitizer);
508
+
509
+ // Returns HTML message for modal display (bypasses Angular sanitizer to preserve inline styles)
510
+ getHtmlMessage(notification: NotificationDto): SafeHtml {
511
+ const html = notification.messageHtml || notification.message || '';
512
+ return this.sanitizer.bypassSecurityTrustHtml(html);
513
+ }
514
+
515
+ constructor(private authService: MesAuthService, private toastService: ToastService, private themeService: ThemeService) {}
516
+
517
+ ngOnInit() {
518
+ this.themeService.currentTheme$
519
+ .pipe(takeUntil(this.destroy$))
520
+ .subscribe(theme => {
521
+ this.currentTheme = theme;
522
+ });
523
+
524
+ this.loadNotifications();
525
+
526
+ // Listen for new real-time notifications
527
+ this.authService.notifications$
528
+ .pipe(takeUntil(this.destroy$))
529
+ .subscribe((notification: RealTimeNotificationDto) => {
530
+ // Show toast for new notification
531
+ this.toastService.show(
532
+ notification.messageHtml || notification.message || '',
533
+ notification.title,
534
+ 'info',
535
+ 5000
536
+ );
537
+ // Reload notifications list
538
+ this.loadNotifications();
539
+ });
540
+ }
541
+
542
+ ngOnDestroy() {
543
+ this.destroy$.next();
544
+ this.destroy$.complete();
545
+ }
546
+
547
+ private loadNotifications() {
548
+ this.authService.getNotifications(1, 50, true).subscribe({ // includeRead = true to get both read and unread
549
+ next: (response: PagedList<NotificationDto>) => {
550
+ this.notifications = response.items || [];
551
+ },
552
+ error: (err) => {}
553
+ });
554
+ }
555
+
556
+ open() {
557
+ this.isOpen = true;
558
+ this.activeTab = 'unread'; // Reset to unread tab when opening
559
+ }
560
+
561
+ close() {
562
+ this.isOpen = false;
563
+ }
564
+
565
+ switchTab(tab: 'unread' | 'read') {
566
+ this.activeTab = tab;
567
+ }
568
+
569
+ openDetails(notification: NotificationDto) {
570
+ this.selectedNotification = notification;
571
+ // Mark as read when opening details (if not already read)
572
+ if (!notification.isRead) {
573
+ this.authService.markAsRead(notification.id).subscribe({
574
+ next: () => {
575
+ notification.isRead = true;
576
+ this.notificationRead.emit();
577
+ },
578
+ error: () => {}
579
+ });
580
+ }
581
+ }
582
+
583
+ closeDetails() {
584
+ this.selectedNotification = null;
585
+ }
586
+
587
+ markAsRead(notificationId: string, event?: Event) {
588
+ if (event) {
589
+ event.stopPropagation();
590
+ }
591
+ this.authService.markAsRead(notificationId).subscribe({
592
+ next: () => {
593
+ const notification = this.notifications.find(n => n.id === notificationId);
594
+ if (notification) {
595
+ notification.isRead = true;
596
+ this.notificationRead.emit();
597
+ }
598
+ },
599
+ error: (err) => {}
600
+ });
601
+ }
602
+
603
+ markAllAsRead() {
604
+ this.authService.markAllAsRead().subscribe({
605
+ next: () => {
606
+ this.notifications.forEach(n => n.isRead = true);
607
+ this.notificationRead.emit();
608
+ },
609
+ error: (err) => {}
610
+ });
611
+ }
612
+
613
+ deleteAllRead() {
614
+ const readNotificationIds = this.notifications
615
+ .filter(n => n.isRead)
616
+ .map(n => n.id);
617
+
618
+ // Delete all read notifications
619
+ const deletePromises = readNotificationIds.map(id =>
620
+ this.authService.deleteNotification(id).toPromise()
621
+ );
622
+
623
+ Promise.all(deletePromises).then(() => {
624
+ // Remove all read notifications from the local array
625
+ this.notifications = this.notifications.filter(n => !n.isRead);
626
+ }).catch((err) => {
627
+ // If bulk delete fails, reload notifications to get current state
628
+ this.loadNotifications();
629
+ });
630
+ }
631
+
632
+ deleteAllUnread() {
633
+ const unreadNotificationIds = this.notifications
634
+ .filter(n => !n.isRead)
635
+ .map(n => n.id);
636
+
637
+ // Delete all unread notifications
638
+ const deletePromises = unreadNotificationIds.map(id =>
639
+ this.authService.deleteNotification(id).toPromise()
640
+ );
641
+
642
+ Promise.all(deletePromises).then(() => {
643
+ // Remove all unread notifications from the local array
644
+ this.notifications = this.notifications.filter(n => n.isRead);
645
+ }).catch((err) => {
646
+ // If bulk delete fails, reload notifications to get current state
647
+ this.loadNotifications();
648
+ });
649
+ }
650
+
651
+ delete(notificationId: string, event: Event) {
652
+ event.stopPropagation();
653
+ this.authService.deleteNotification(notificationId).subscribe({
654
+ next: () => {
655
+ this.notifications = this.notifications.filter(n => n.id !== notificationId);
656
+ },
657
+ error: (err) => {}
658
+ });
659
+ }
660
+
661
+ formatDate(dateString: string): string {
662
+ // Parse date string from server (stored in UTC but without 'Z' suffix or 'T' separator)
663
+ const normalizedDateString = this.parseUtcDate(dateString);
664
+ const date = new Date(normalizedDateString);
665
+
666
+ // Check if the date is valid
667
+ if (isNaN(date.getTime())) {
668
+ return 'Invalid date';
669
+ }
670
+
671
+ const now = new Date();
672
+ const diffMs = now.getTime() - date.getTime();
673
+ const diffMins = Math.floor(diffMs / 60000);
674
+ const diffHours = Math.floor(diffMs / 3600000);
675
+ const diffDays = Math.floor(diffMs / 86400000);
676
+
677
+ if (diffMins < 1) return 'Now';
678
+ if (diffMins < 60) return `${diffMins}m ago`;
679
+ if (diffHours < 24) return `${diffHours}h ago`;
680
+ if (diffDays < 7) return `${diffDays}d ago`;
681
+
682
+ return date.toLocaleDateString();
683
+ }
684
+
685
+ // Parse date string from server (stored in UTC but without 'Z' suffix or 'T' separator)
686
+ private parseUtcDate(dateStr: string): string {
687
+ // Handle date strings that might be missing the 'T' separator
688
+ // Convert formats like "2023-12-01 12:30:45" to "2023-12-01T12:30:45"
689
+ let normalized = dateStr.includes('T') ? dateStr : dateStr.replace(' ', 'T');
690
+
691
+ // If no timezone indicator, assume UTC by appending 'Z'
692
+ if (!normalized.endsWith('Z') && !normalized.includes('+') && !normalized.includes('-', 10)) {
693
+ normalized += 'Z';
694
+ }
695
+
696
+ return normalized;
697
+ }
698
+ }