mesauth-angular 1.2.6 → 1.3.1

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.
@@ -1,700 +0,0 @@
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">{{ selectedNotificationDate }}</span>
109
- </div>
110
- <div class="modal-body" [innerHTML]="selectedNotificationHtml"></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
- selectedNotificationHtml: SafeHtml | null = null;
502
- selectedNotificationDate: string = '';
503
-
504
- // Returns plain text message for list display
505
- getNotificationMessage(notification: NotificationDto): string {
506
- return notification.message || '';
507
- }
508
-
509
- private readonly sanitizer = inject(DomSanitizer);
510
-
511
- constructor(private authService: MesAuthService, private toastService: ToastService, private themeService: ThemeService) {}
512
-
513
- ngOnInit() {
514
- this.themeService.currentTheme$
515
- .pipe(takeUntil(this.destroy$))
516
- .subscribe(theme => {
517
- this.currentTheme = theme;
518
- });
519
-
520
- this.loadNotifications();
521
-
522
- // Listen for new real-time notifications
523
- this.authService.notifications$
524
- .pipe(takeUntil(this.destroy$))
525
- .subscribe((notification: RealTimeNotificationDto) => {
526
- // Show toast for new notification
527
- this.toastService.show(
528
- notification.messageHtml || notification.message || '',
529
- notification.title,
530
- 'info',
531
- 5000
532
- );
533
- // Reload notifications list
534
- this.loadNotifications();
535
- });
536
- }
537
-
538
- ngOnDestroy() {
539
- this.destroy$.next();
540
- this.destroy$.complete();
541
- }
542
-
543
- private loadNotifications() {
544
- this.authService.getNotifications(1, 50, true).subscribe({ // includeRead = true to get both read and unread
545
- next: (response: PagedList<NotificationDto>) => {
546
- this.notifications = response.items || [];
547
- },
548
- error: (err) => {}
549
- });
550
- }
551
-
552
- open() {
553
- this.isOpen = true;
554
- this.activeTab = 'unread'; // Reset to unread tab when opening
555
- }
556
-
557
- close() {
558
- this.isOpen = false;
559
- }
560
-
561
- switchTab(tab: 'unread' | 'read') {
562
- this.activeTab = tab;
563
- }
564
-
565
- openDetails(notification: NotificationDto) {
566
- this.selectedNotification = notification;
567
- // Cache computed values to avoid re-rendering on every change detection cycle
568
- const html = notification.messageHtml || notification.message || '';
569
- this.selectedNotificationHtml = this.sanitizer.bypassSecurityTrustHtml(html);
570
- this.selectedNotificationDate = this.formatDate(notification.createdAt);
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
- this.selectedNotificationHtml = null;
586
- this.selectedNotificationDate = '';
587
- }
588
-
589
- markAsRead(notificationId: string, event?: Event) {
590
- if (event) {
591
- event.stopPropagation();
592
- }
593
- this.authService.markAsRead(notificationId).subscribe({
594
- next: () => {
595
- const notification = this.notifications.find(n => n.id === notificationId);
596
- if (notification) {
597
- notification.isRead = true;
598
- this.notificationRead.emit();
599
- }
600
- },
601
- error: (err) => {}
602
- });
603
- }
604
-
605
- markAllAsRead() {
606
- this.authService.markAllAsRead().subscribe({
607
- next: () => {
608
- this.notifications.forEach(n => n.isRead = true);
609
- this.notificationRead.emit();
610
- },
611
- error: (err) => {}
612
- });
613
- }
614
-
615
- deleteAllRead() {
616
- const readNotificationIds = this.notifications
617
- .filter(n => n.isRead)
618
- .map(n => n.id);
619
-
620
- // Delete all read notifications
621
- const deletePromises = readNotificationIds.map(id =>
622
- this.authService.deleteNotification(id).toPromise()
623
- );
624
-
625
- Promise.all(deletePromises).then(() => {
626
- // Remove all read notifications from the local array
627
- this.notifications = this.notifications.filter(n => !n.isRead);
628
- }).catch((err) => {
629
- // If bulk delete fails, reload notifications to get current state
630
- this.loadNotifications();
631
- });
632
- }
633
-
634
- deleteAllUnread() {
635
- const unreadNotificationIds = this.notifications
636
- .filter(n => !n.isRead)
637
- .map(n => n.id);
638
-
639
- // Delete all unread notifications
640
- const deletePromises = unreadNotificationIds.map(id =>
641
- this.authService.deleteNotification(id).toPromise()
642
- );
643
-
644
- Promise.all(deletePromises).then(() => {
645
- // Remove all unread notifications from the local array
646
- this.notifications = this.notifications.filter(n => n.isRead);
647
- }).catch((err) => {
648
- // If bulk delete fails, reload notifications to get current state
649
- this.loadNotifications();
650
- });
651
- }
652
-
653
- delete(notificationId: string, event: Event) {
654
- event.stopPropagation();
655
- this.authService.deleteNotification(notificationId).subscribe({
656
- next: () => {
657
- this.notifications = this.notifications.filter(n => n.id !== notificationId);
658
- },
659
- error: (err) => {}
660
- });
661
- }
662
-
663
- formatDate(dateString: string): string {
664
- // Parse date string from server (stored in UTC but without 'Z' suffix or 'T' separator)
665
- const normalizedDateString = this.parseUtcDate(dateString);
666
- const date = new Date(normalizedDateString);
667
-
668
- // Check if the date is valid
669
- if (isNaN(date.getTime())) {
670
- return 'Invalid date';
671
- }
672
-
673
- const now = new Date();
674
- const diffMs = now.getTime() - date.getTime();
675
- const diffMins = Math.floor(diffMs / 60000);
676
- const diffHours = Math.floor(diffMs / 3600000);
677
- const diffDays = Math.floor(diffMs / 86400000);
678
-
679
- if (diffMins < 1) return 'Now';
680
- if (diffMins < 60) return `${diffMins}m ago`;
681
- if (diffHours < 24) return `${diffHours}h ago`;
682
- if (diffDays < 7) return `${diffDays}d ago`;
683
-
684
- return date.toLocaleDateString();
685
- }
686
-
687
- // Parse date string from server (stored in UTC but without 'Z' suffix or 'T' separator)
688
- private parseUtcDate(dateStr: string): string {
689
- // Handle date strings that might be missing the 'T' separator
690
- // Convert formats like "2023-12-01 12:30:45" to "2023-12-01T12:30:45"
691
- let normalized = dateStr.includes('T') ? dateStr : dateStr.replace(' ', 'T');
692
-
693
- // If no timezone indicator, assume UTC by appending 'Z'
694
- if (!normalized.endsWith('Z') && !normalized.includes('+') && !normalized.includes('-', 10)) {
695
- normalized += 'Z';
696
- }
697
-
698
- return normalized;
699
- }
700
- }