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