mesauth-angular 1.3.0 → 1.3.2

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,5 +1,5 @@
1
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';
2
+ import { InjectionToken, makeEnvironmentProviders, provideAppInitializer, inject, NgZone, Injectable, NgModule, EventEmitter, signal, HostListener, HostBinding, Output, Component, ViewChild } from '@angular/core';
3
3
  import { HttpClient } from '@angular/common/http';
4
4
  import { HubConnectionBuilder, LogLevel } from '@microsoft/signalr';
5
5
  import { BehaviorSubject, Subject, EMPTY, of, throwError } from 'rxjs';
@@ -38,7 +38,8 @@ function provideMesAuth(config) {
38
38
  const mesAuthService = inject(MesAuthService);
39
39
  const httpClient = inject(HttpClient);
40
40
  const router = inject(Router);
41
- mesAuthService.init(config, httpClient, router);
41
+ const ngZone = inject(NgZone);
42
+ mesAuthService.init(config, httpClient, router, ngZone);
42
43
  })
43
44
  ]);
44
45
  }
@@ -59,13 +60,15 @@ class MesAuthService {
59
60
  config = null;
60
61
  http;
61
62
  router;
63
+ ngZone = null;
62
64
  constructor() {
63
65
  // Empty constructor - all dependencies passed to init()
64
66
  }
65
- init(config, httpClient, router) {
67
+ init(config, httpClient, router, ngZone) {
66
68
  this.config = config;
67
69
  this.http = httpClient;
68
70
  this.router = router;
71
+ this.ngZone = ngZone ?? null;
69
72
  this.apiBase = config.apiBaseUrl.replace(/\/$/, '');
70
73
  // Fetch user once on init. Route changes do NOT re-fetch the user.
71
74
  // Auth state is maintained via cookies; 401 errors are handled by HTTP interceptors.
@@ -225,7 +228,12 @@ class MesAuthService {
225
228
  .configureLogging(LogLevel.Warning);
226
229
  this.hubConnection = builder.build();
227
230
  this.hubConnection.on('ReceiveNotification', (n) => {
228
- this._notifications.next(n);
231
+ if (this.ngZone) {
232
+ this.ngZone.run(() => this._notifications.next(n));
233
+ }
234
+ else {
235
+ this._notifications.next(n);
236
+ }
229
237
  });
230
238
  this.hubConnection.start().then(() => { }).catch((err) => { });
231
239
  this.hubConnection.onclose(() => { });
@@ -764,14 +772,20 @@ class NotificationPanelComponent {
764
772
  currentTheme = 'light';
765
773
  activeTab = 'unread'; // Default to unread tab
766
774
  destroy$ = new Subject();
775
+ // Cached filtered lists — updated explicitly to avoid re-filtering on every CD cycle
776
+ _unreadNotifications = [];
777
+ _readNotifications = [];
778
+ // Stable time-ago strings keyed by notification id — refreshed every 30s
779
+ dateLabels = new Map();
780
+ dateTimer = null;
767
781
  get unreadNotifications() {
768
- return this.notifications.filter(n => !n.isRead);
782
+ return this._unreadNotifications;
769
783
  }
770
784
  get readNotifications() {
771
- return this.notifications.filter(n => n.isRead);
785
+ return this._readNotifications;
772
786
  }
773
787
  get currentNotifications() {
774
- return this.activeTab === 'unread' ? this.unreadNotifications : this.readNotifications;
788
+ return this.activeTab === 'unread' ? this._unreadNotifications : this._readNotifications;
775
789
  }
776
790
  selectedNotification = null;
777
791
  selectedNotificationHtml = null;
@@ -793,6 +807,8 @@ class NotificationPanelComponent {
793
807
  this.currentTheme = theme;
794
808
  });
795
809
  this.loadNotifications();
810
+ // Refresh time-ago labels every 30s to avoid NG0100 from live new Date() in template
811
+ this.dateTimer = setInterval(() => this.refreshDateLabels(), 30000);
796
812
  // Listen for new real-time notifications
797
813
  this.authService.notifications$
798
814
  .pipe(takeUntil(this.destroy$))
@@ -804,6 +820,10 @@ class NotificationPanelComponent {
804
820
  });
805
821
  }
806
822
  ngOnDestroy() {
823
+ if (this.dateTimer !== null) {
824
+ clearInterval(this.dateTimer);
825
+ this.dateTimer = null;
826
+ }
807
827
  this.destroy$.next();
808
828
  this.destroy$.complete();
809
829
  }
@@ -811,6 +831,7 @@ class NotificationPanelComponent {
811
831
  this.authService.getNotifications(1, 50, true).subscribe({
812
832
  next: (response) => {
813
833
  this.notifications = response.items || [];
834
+ this.onNotificationsChanged();
814
835
  },
815
836
  error: (err) => { }
816
837
  });
@@ -837,6 +858,7 @@ class NotificationPanelComponent {
837
858
  next: () => {
838
859
  notification.isRead = true;
839
860
  this.notificationRead.emit();
861
+ this.onNotificationsChanged();
840
862
  },
841
863
  error: () => { }
842
864
  });
@@ -857,6 +879,7 @@ class NotificationPanelComponent {
857
879
  if (notification) {
858
880
  notification.isRead = true;
859
881
  this.notificationRead.emit();
882
+ this.onNotificationsChanged();
860
883
  }
861
884
  },
862
885
  error: (err) => { }
@@ -867,6 +890,7 @@ class NotificationPanelComponent {
867
890
  next: () => {
868
891
  this.notifications.forEach(n => n.isRead = true);
869
892
  this.notificationRead.emit();
893
+ this.onNotificationsChanged();
870
894
  },
871
895
  error: (err) => { }
872
896
  });
@@ -880,6 +904,7 @@ class NotificationPanelComponent {
880
904
  Promise.all(deletePromises).then(() => {
881
905
  // Remove all read notifications from the local array
882
906
  this.notifications = this.notifications.filter(n => !n.isRead);
907
+ this.onNotificationsChanged();
883
908
  }).catch((err) => {
884
909
  // If bulk delete fails, reload notifications to get current state
885
910
  this.loadNotifications();
@@ -894,6 +919,8 @@ class NotificationPanelComponent {
894
919
  Promise.all(deletePromises).then(() => {
895
920
  // Remove all unread notifications from the local array
896
921
  this.notifications = this.notifications.filter(n => n.isRead);
922
+ this.notificationRead.emit();
923
+ this.onNotificationsChanged();
897
924
  }).catch((err) => {
898
925
  // If bulk delete fails, reload notifications to get current state
899
926
  this.loadNotifications();
@@ -901,22 +928,28 @@ class NotificationPanelComponent {
901
928
  }
902
929
  delete(notificationId, event) {
903
930
  event.stopPropagation();
931
+ const wasUnread = this.notifications.find(n => n.id === notificationId && !n.isRead) !== undefined;
904
932
  this.authService.deleteNotification(notificationId).subscribe({
905
933
  next: () => {
906
934
  this.notifications = this.notifications.filter(n => n.id !== notificationId);
935
+ if (wasUnread) {
936
+ this.notificationRead.emit();
937
+ }
938
+ this.onNotificationsChanged();
907
939
  },
908
940
  error: (err) => { }
909
941
  });
910
942
  }
911
943
  formatDate(dateString) {
912
- // Parse date string from server (stored in UTC but without 'Z' suffix or 'T' separator)
944
+ return this.computeTimeAgo(dateString, new Date());
945
+ }
946
+ // Pure computation — takes now as param so it never calls new Date() internally
947
+ computeTimeAgo(dateString, now) {
913
948
  const normalizedDateString = this.parseUtcDate(dateString);
914
949
  const date = new Date(normalizedDateString);
915
- // Check if the date is valid
916
950
  if (isNaN(date.getTime())) {
917
951
  return 'Invalid date';
918
952
  }
919
- const now = new Date();
920
953
  const diffMs = now.getTime() - date.getTime();
921
954
  const diffMins = Math.floor(diffMs / 60000);
922
955
  const diffHours = Math.floor(diffMs / 3600000);
@@ -931,6 +964,23 @@ class NotificationPanelComponent {
931
964
  return `${diffDays}d ago`;
932
965
  return date.toLocaleDateString();
933
966
  }
967
+ // Rebuild dateLabels map using a single shared now — prevents mid-loop clock drift
968
+ refreshDateLabels() {
969
+ const now = new Date();
970
+ for (const n of this.notifications) {
971
+ this.dateLabels.set(n.id, this.computeTimeAgo(n.createdAt, now));
972
+ }
973
+ }
974
+ // Re-run filter once and store results in stable arrays
975
+ recomputeFilteredLists() {
976
+ this._unreadNotifications = this.notifications.filter(n => !n.isRead);
977
+ this._readNotifications = this.notifications.filter(n => n.isRead);
978
+ }
979
+ // Single call-site after every notification mutation
980
+ onNotificationsChanged() {
981
+ this.recomputeFilteredLists();
982
+ this.refreshDateLabels();
983
+ }
934
984
  // Parse date string from server (stored in UTC but without 'Z' suffix or 'T' separator)
935
985
  parseUtcDate(dateStr) {
936
986
  // Handle date strings that might be missing the 'T' separator
@@ -943,214 +993,214 @@ class NotificationPanelComponent {
943
993
  return normalized;
944
994
  }
945
995
  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 });
946
- 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: `
947
- <div class="notification-panel" [class.open]="isOpen">
948
- <!-- Header -->
949
- <div class="panel-header">
950
- <h3>Notifications</h3>
951
- <button class="close-btn" (click)="close()" title="Close">✕</button>
952
- </div>
953
-
954
- <!-- Tabs -->
955
- <div class="tabs">
956
- <button
957
- class="tab-btn"
958
- [class.active]="activeTab === 'unread'"
959
- (click)="switchTab('unread')"
960
- >
961
- Unread ({{ unreadNotifications.length }})
962
- </button>
963
- <button
964
- class="tab-btn"
965
- [class.active]="activeTab === 'read'"
966
- (click)="switchTab('read')"
967
- >
968
- Read ({{ readNotifications.length }})
969
- </button>
970
- </div>
971
-
972
- <!-- Notifications List -->
973
- <div class="notifications-list">
974
- <ng-container *ngIf="currentNotifications.length > 0">
975
- <div
976
- *ngFor="let notification of currentNotifications"
977
- class="notification-item"
978
- [class.unread]="!notification.isRead"
979
- (click)="openDetails(notification)"
980
- >
981
- <div class="notification-content">
982
- <div class="notification-title">{{ notification.title }}</div>
983
- <div class="notification-message">{{ getNotificationMessage(notification) }}</div>
984
- <div class="notification-meta">
985
- <span class="app-name">{{ notification.sourceAppName }}</span>
986
- <span class="time">{{ formatDate(notification.createdAt) }}</span>
987
- </div>
988
- </div>
989
- <button
990
- class="read-btn"
991
- (click)="markAsRead(notification.id, $event)"
992
- title="Mark as read"
993
- *ngIf="!notification.isRead"
994
- >
995
-
996
- </button>
997
- <button
998
- class="delete-btn"
999
- (click)="delete(notification.id, $event)"
1000
- title="Delete notification"
1001
- *ngIf="notification.isRead"
1002
- >
1003
- 🗑
1004
- </button>
1005
- </div>
1006
- </ng-container>
1007
-
1008
- <ng-container *ngIf="currentNotifications.length === 0">
1009
- <div class="empty-state">
1010
- No {{ activeTab }} notifications
1011
- </div>
1012
- </ng-container>
1013
- </div>
1014
-
1015
- <!-- Footer Actions -->
1016
- <div class="panel-footer" *ngIf="currentNotifications.length > 0">
1017
- <div class="footer-actions" *ngIf="activeTab === 'unread'">
1018
- <button class="action-btn" (click)="markAllAsRead()" *ngIf="unreadNotifications.length > 0">
1019
- Mark all as read
1020
- </button>
1021
- <button class="action-btn delete-all-btn" (click)="deleteAllUnread()" *ngIf="unreadNotifications.length > 0">
1022
- Delete all
1023
- </button>
1024
- </div>
1025
- <button class="action-btn delete-all-btn" (click)="deleteAllRead()" *ngIf="activeTab === 'read' && readNotifications.length > 0">
1026
- Delete all
1027
- </button>
1028
- </div>
1029
- </div>
1030
-
1031
- <!-- Details Modal -->
1032
- <div class="modal-overlay" *ngIf="selectedNotification" (click)="closeDetails()">
1033
- <div class="modal-container" (click)="$event.stopPropagation()">
1034
- <div class="modal-header">
1035
- <h3>{{ selectedNotification.title }}</h3>
1036
- <button class="close-btn" (click)="closeDetails()" title="Close">✕</button>
1037
- </div>
1038
- <div class="modal-meta">
1039
- <span class="app-name">{{ selectedNotification.sourceAppName }}</span>
1040
- <span class="time">{{ selectedNotificationDate }}</span>
1041
- </div>
1042
- <div class="modal-body" [innerHTML]="selectedNotificationHtml"></div>
1043
- <div class="modal-footer">
1044
- <button class="action-btn" (click)="closeDetails()">Close</button>
1045
- </div>
1046
- </div>
1047
- </div>
996
+ 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: `
997
+ <div class="notification-panel" [class.open]="isOpen">
998
+ <!-- Header -->
999
+ <div class="panel-header">
1000
+ <h3>Notifications</h3>
1001
+ <button class="close-btn" (click)="close()" title="Close">✕</button>
1002
+ </div>
1003
+
1004
+ <!-- Tabs -->
1005
+ <div class="tabs">
1006
+ <button
1007
+ class="tab-btn"
1008
+ [class.active]="activeTab === 'unread'"
1009
+ (click)="switchTab('unread')"
1010
+ >
1011
+ Unread ({{ unreadNotifications.length }})
1012
+ </button>
1013
+ <button
1014
+ class="tab-btn"
1015
+ [class.active]="activeTab === 'read'"
1016
+ (click)="switchTab('read')"
1017
+ >
1018
+ Read ({{ readNotifications.length }})
1019
+ </button>
1020
+ </div>
1021
+
1022
+ <!-- Notifications List -->
1023
+ <div class="notifications-list">
1024
+ <ng-container *ngIf="currentNotifications.length > 0">
1025
+ <div
1026
+ *ngFor="let notification of currentNotifications"
1027
+ class="notification-item"
1028
+ [class.unread]="!notification.isRead"
1029
+ (click)="openDetails(notification)"
1030
+ >
1031
+ <div class="notification-content">
1032
+ <div class="notification-title">{{ notification.title }}</div>
1033
+ <div class="notification-message">{{ getNotificationMessage(notification) }}</div>
1034
+ <div class="notification-meta">
1035
+ <span class="app-name">{{ notification.sourceAppName }}</span>
1036
+ <span class="time">{{ dateLabels.get(notification.id) }}</span>
1037
+ </div>
1038
+ </div>
1039
+ <button
1040
+ class="read-btn"
1041
+ (click)="markAsRead(notification.id, $event)"
1042
+ title="Mark as read"
1043
+ *ngIf="!notification.isRead"
1044
+ >
1045
+
1046
+ </button>
1047
+ <button
1048
+ class="delete-btn"
1049
+ (click)="delete(notification.id, $event)"
1050
+ title="Delete notification"
1051
+ *ngIf="notification.isRead"
1052
+ >
1053
+ 🗑
1054
+ </button>
1055
+ </div>
1056
+ </ng-container>
1057
+
1058
+ <ng-container *ngIf="currentNotifications.length === 0">
1059
+ <div class="empty-state">
1060
+ No {{ activeTab }} notifications
1061
+ </div>
1062
+ </ng-container>
1063
+ </div>
1064
+
1065
+ <!-- Footer Actions -->
1066
+ <div class="panel-footer" *ngIf="currentNotifications.length > 0">
1067
+ <div class="footer-actions" *ngIf="activeTab === 'unread'">
1068
+ <button class="action-btn" (click)="markAllAsRead()" *ngIf="unreadNotifications.length > 0">
1069
+ Mark all as read
1070
+ </button>
1071
+ <button class="action-btn delete-all-btn" (click)="deleteAllUnread()" *ngIf="unreadNotifications.length > 0">
1072
+ Delete all
1073
+ </button>
1074
+ </div>
1075
+ <button class="action-btn delete-all-btn" (click)="deleteAllRead()" *ngIf="activeTab === 'read' && readNotifications.length > 0">
1076
+ Delete all
1077
+ </button>
1078
+ </div>
1079
+ </div>
1080
+
1081
+ <!-- Details Modal -->
1082
+ <div class="modal-overlay" *ngIf="selectedNotification" (click)="closeDetails()">
1083
+ <div class="modal-container" (click)="$event.stopPropagation()">
1084
+ <div class="modal-header">
1085
+ <h3>{{ selectedNotification.title }}</h3>
1086
+ <button class="close-btn" (click)="closeDetails()" title="Close">✕</button>
1087
+ </div>
1088
+ <div class="modal-meta">
1089
+ <span class="app-name">{{ selectedNotification.sourceAppName }}</span>
1090
+ <span class="time">{{ selectedNotificationDate }}</span>
1091
+ </div>
1092
+ <div class="modal-body" [innerHTML]="selectedNotificationHtml"></div>
1093
+ <div class="modal-footer">
1094
+ <button class="action-btn" (click)="closeDetails()">Close</button>
1095
+ </div>
1096
+ </div>
1097
+ </div>
1048
1098
  `, isInline: true, styles: [":host{display:block;position:relative;--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){display:block;position:relative;--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:1030;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:12px;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;inset:0;width:100vw;height:100vh;background-color:#00000080;display:flex;align-items:center;justify-content:center;z-index:1060}.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"] }] });
1049
1099
  }
1050
1100
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: NotificationPanelComponent, decorators: [{
1051
1101
  type: Component,
1052
- args: [{ selector: 'ma-notification-panel', standalone: true, imports: [NgIf, NgFor], template: `
1053
- <div class="notification-panel" [class.open]="isOpen">
1054
- <!-- Header -->
1055
- <div class="panel-header">
1056
- <h3>Notifications</h3>
1057
- <button class="close-btn" (click)="close()" title="Close">✕</button>
1058
- </div>
1059
-
1060
- <!-- Tabs -->
1061
- <div class="tabs">
1062
- <button
1063
- class="tab-btn"
1064
- [class.active]="activeTab === 'unread'"
1065
- (click)="switchTab('unread')"
1066
- >
1067
- Unread ({{ unreadNotifications.length }})
1068
- </button>
1069
- <button
1070
- class="tab-btn"
1071
- [class.active]="activeTab === 'read'"
1072
- (click)="switchTab('read')"
1073
- >
1074
- Read ({{ readNotifications.length }})
1075
- </button>
1076
- </div>
1077
-
1078
- <!-- Notifications List -->
1079
- <div class="notifications-list">
1080
- <ng-container *ngIf="currentNotifications.length > 0">
1081
- <div
1082
- *ngFor="let notification of currentNotifications"
1083
- class="notification-item"
1084
- [class.unread]="!notification.isRead"
1085
- (click)="openDetails(notification)"
1086
- >
1087
- <div class="notification-content">
1088
- <div class="notification-title">{{ notification.title }}</div>
1089
- <div class="notification-message">{{ getNotificationMessage(notification) }}</div>
1090
- <div class="notification-meta">
1091
- <span class="app-name">{{ notification.sourceAppName }}</span>
1092
- <span class="time">{{ formatDate(notification.createdAt) }}</span>
1093
- </div>
1094
- </div>
1095
- <button
1096
- class="read-btn"
1097
- (click)="markAsRead(notification.id, $event)"
1098
- title="Mark as read"
1099
- *ngIf="!notification.isRead"
1100
- >
1101
-
1102
- </button>
1103
- <button
1104
- class="delete-btn"
1105
- (click)="delete(notification.id, $event)"
1106
- title="Delete notification"
1107
- *ngIf="notification.isRead"
1108
- >
1109
- 🗑
1110
- </button>
1111
- </div>
1112
- </ng-container>
1113
-
1114
- <ng-container *ngIf="currentNotifications.length === 0">
1115
- <div class="empty-state">
1116
- No {{ activeTab }} notifications
1117
- </div>
1118
- </ng-container>
1119
- </div>
1120
-
1121
- <!-- Footer Actions -->
1122
- <div class="panel-footer" *ngIf="currentNotifications.length > 0">
1123
- <div class="footer-actions" *ngIf="activeTab === 'unread'">
1124
- <button class="action-btn" (click)="markAllAsRead()" *ngIf="unreadNotifications.length > 0">
1125
- Mark all as read
1126
- </button>
1127
- <button class="action-btn delete-all-btn" (click)="deleteAllUnread()" *ngIf="unreadNotifications.length > 0">
1128
- Delete all
1129
- </button>
1130
- </div>
1131
- <button class="action-btn delete-all-btn" (click)="deleteAllRead()" *ngIf="activeTab === 'read' && readNotifications.length > 0">
1132
- Delete all
1133
- </button>
1134
- </div>
1135
- </div>
1136
-
1137
- <!-- Details Modal -->
1138
- <div class="modal-overlay" *ngIf="selectedNotification" (click)="closeDetails()">
1139
- <div class="modal-container" (click)="$event.stopPropagation()">
1140
- <div class="modal-header">
1141
- <h3>{{ selectedNotification.title }}</h3>
1142
- <button class="close-btn" (click)="closeDetails()" title="Close">✕</button>
1143
- </div>
1144
- <div class="modal-meta">
1145
- <span class="app-name">{{ selectedNotification.sourceAppName }}</span>
1146
- <span class="time">{{ selectedNotificationDate }}</span>
1147
- </div>
1148
- <div class="modal-body" [innerHTML]="selectedNotificationHtml"></div>
1149
- <div class="modal-footer">
1150
- <button class="action-btn" (click)="closeDetails()">Close</button>
1151
- </div>
1152
- </div>
1153
- </div>
1102
+ args: [{ selector: 'ma-notification-panel', standalone: true, imports: [NgIf, NgFor], template: `
1103
+ <div class="notification-panel" [class.open]="isOpen">
1104
+ <!-- Header -->
1105
+ <div class="panel-header">
1106
+ <h3>Notifications</h3>
1107
+ <button class="close-btn" (click)="close()" title="Close">✕</button>
1108
+ </div>
1109
+
1110
+ <!-- Tabs -->
1111
+ <div class="tabs">
1112
+ <button
1113
+ class="tab-btn"
1114
+ [class.active]="activeTab === 'unread'"
1115
+ (click)="switchTab('unread')"
1116
+ >
1117
+ Unread ({{ unreadNotifications.length }})
1118
+ </button>
1119
+ <button
1120
+ class="tab-btn"
1121
+ [class.active]="activeTab === 'read'"
1122
+ (click)="switchTab('read')"
1123
+ >
1124
+ Read ({{ readNotifications.length }})
1125
+ </button>
1126
+ </div>
1127
+
1128
+ <!-- Notifications List -->
1129
+ <div class="notifications-list">
1130
+ <ng-container *ngIf="currentNotifications.length > 0">
1131
+ <div
1132
+ *ngFor="let notification of currentNotifications"
1133
+ class="notification-item"
1134
+ [class.unread]="!notification.isRead"
1135
+ (click)="openDetails(notification)"
1136
+ >
1137
+ <div class="notification-content">
1138
+ <div class="notification-title">{{ notification.title }}</div>
1139
+ <div class="notification-message">{{ getNotificationMessage(notification) }}</div>
1140
+ <div class="notification-meta">
1141
+ <span class="app-name">{{ notification.sourceAppName }}</span>
1142
+ <span class="time">{{ dateLabels.get(notification.id) }}</span>
1143
+ </div>
1144
+ </div>
1145
+ <button
1146
+ class="read-btn"
1147
+ (click)="markAsRead(notification.id, $event)"
1148
+ title="Mark as read"
1149
+ *ngIf="!notification.isRead"
1150
+ >
1151
+
1152
+ </button>
1153
+ <button
1154
+ class="delete-btn"
1155
+ (click)="delete(notification.id, $event)"
1156
+ title="Delete notification"
1157
+ *ngIf="notification.isRead"
1158
+ >
1159
+ 🗑
1160
+ </button>
1161
+ </div>
1162
+ </ng-container>
1163
+
1164
+ <ng-container *ngIf="currentNotifications.length === 0">
1165
+ <div class="empty-state">
1166
+ No {{ activeTab }} notifications
1167
+ </div>
1168
+ </ng-container>
1169
+ </div>
1170
+
1171
+ <!-- Footer Actions -->
1172
+ <div class="panel-footer" *ngIf="currentNotifications.length > 0">
1173
+ <div class="footer-actions" *ngIf="activeTab === 'unread'">
1174
+ <button class="action-btn" (click)="markAllAsRead()" *ngIf="unreadNotifications.length > 0">
1175
+ Mark all as read
1176
+ </button>
1177
+ <button class="action-btn delete-all-btn" (click)="deleteAllUnread()" *ngIf="unreadNotifications.length > 0">
1178
+ Delete all
1179
+ </button>
1180
+ </div>
1181
+ <button class="action-btn delete-all-btn" (click)="deleteAllRead()" *ngIf="activeTab === 'read' && readNotifications.length > 0">
1182
+ Delete all
1183
+ </button>
1184
+ </div>
1185
+ </div>
1186
+
1187
+ <!-- Details Modal -->
1188
+ <div class="modal-overlay" *ngIf="selectedNotification" (click)="closeDetails()">
1189
+ <div class="modal-container" (click)="$event.stopPropagation()">
1190
+ <div class="modal-header">
1191
+ <h3>{{ selectedNotification.title }}</h3>
1192
+ <button class="close-btn" (click)="closeDetails()" title="Close">✕</button>
1193
+ </div>
1194
+ <div class="modal-meta">
1195
+ <span class="app-name">{{ selectedNotification.sourceAppName }}</span>
1196
+ <span class="time">{{ selectedNotificationDate }}</span>
1197
+ </div>
1198
+ <div class="modal-body" [innerHTML]="selectedNotificationHtml"></div>
1199
+ <div class="modal-footer">
1200
+ <button class="action-btn" (click)="closeDetails()">Close</button>
1201
+ </div>
1202
+ </div>
1203
+ </div>
1154
1204
  `, styles: [":host{display:block;position:relative;--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){display:block;position:relative;--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:1030;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:12px;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;inset:0;width:100vw;height:100vh;background-color:#00000080;display:flex;align-items:center;justify-content:center;z-index:1060}.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"] }]
1155
1205
  }], ctorParameters: () => [{ type: MesAuthService }, { type: ToastService }, { type: ThemeService }], propDecorators: { notificationRead: [{
1156
1206
  type: Output