mesauth-angular 1.3.1 → 1.3.3
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.
- package/README.md +305 -305
- package/fesm2022/mesauth-angular.mjs +433 -383
- package/fesm2022/mesauth-angular.mjs.map +1 -1
- package/index.d.ts +11 -2
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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.
|
|
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(() => { });
|
|
@@ -259,10 +267,10 @@ class MesAuthService {
|
|
|
259
267
|
refreshUser() {
|
|
260
268
|
return this.fetchCurrentUser();
|
|
261
269
|
}
|
|
262
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
263
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.
|
|
270
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: MesAuthService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
271
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: MesAuthService });
|
|
264
272
|
}
|
|
265
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
273
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: MesAuthService, decorators: [{
|
|
266
274
|
type: Injectable
|
|
267
275
|
}], ctorParameters: () => [] });
|
|
268
276
|
|
|
@@ -317,13 +325,13 @@ const mesAuthInterceptor = (req, next) => {
|
|
|
317
325
|
};
|
|
318
326
|
|
|
319
327
|
class MesAuthModule {
|
|
320
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
321
|
-
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.
|
|
322
|
-
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.
|
|
328
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: MesAuthModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
329
|
+
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.18", ngImport: i0, type: MesAuthModule });
|
|
330
|
+
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: MesAuthModule, providers: [
|
|
323
331
|
MesAuthService
|
|
324
332
|
] });
|
|
325
333
|
}
|
|
326
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
334
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: MesAuthModule, decorators: [{
|
|
327
335
|
type: NgModule,
|
|
328
336
|
args: [{
|
|
329
337
|
providers: [
|
|
@@ -382,10 +390,10 @@ class ThemeService {
|
|
|
382
390
|
refreshTheme() {
|
|
383
391
|
this.detectTheme();
|
|
384
392
|
}
|
|
385
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
386
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.
|
|
393
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
394
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ThemeService, providedIn: 'root' });
|
|
387
395
|
}
|
|
388
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
396
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ThemeService, decorators: [{
|
|
389
397
|
type: Injectable,
|
|
390
398
|
args: [{
|
|
391
399
|
providedIn: 'root'
|
|
@@ -532,101 +540,101 @@ class UserProfileComponent {
|
|
|
532
540
|
onNotificationClick() {
|
|
533
541
|
this.notificationClick.emit();
|
|
534
542
|
}
|
|
535
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
536
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.
|
|
537
|
-
<div class="user-profile-container">
|
|
538
|
-
<!-- Not logged in -->
|
|
539
|
-
<ng-container *ngIf="!currentUser()">
|
|
540
|
-
<button class="login-btn" (click)="onLogin()">
|
|
541
|
-
Login
|
|
542
|
-
</button>
|
|
543
|
-
</ng-container>
|
|
544
|
-
|
|
545
|
-
<!-- Logged in -->
|
|
546
|
-
<ng-container *ngIf="currentUser()">
|
|
547
|
-
<div class="user-header">
|
|
548
|
-
<button class="notification-btn" (click)="onNotificationClick()" title="Notifications">
|
|
549
|
-
<span class="icon">🔔</span>
|
|
550
|
-
<span class="badge" *ngIf="unreadCount > 0">{{ unreadCount }}</span>
|
|
551
|
-
</button>
|
|
552
|
-
|
|
553
|
-
<div class="user-menu-wrapper">
|
|
554
|
-
<button class="user-menu-btn" (click)="toggleDropdown()">
|
|
555
|
-
<img
|
|
556
|
-
*ngIf="currentUser().fullName || currentUser().userName"
|
|
557
|
-
[src]="getAvatarUrl(currentUser())"
|
|
558
|
-
[alt]="currentUser().fullName || currentUser().userName"
|
|
559
|
-
class="avatar"
|
|
560
|
-
/>
|
|
561
|
-
<span *ngIf="!(currentUser().fullName || currentUser().userName)" class="avatar-initial">
|
|
562
|
-
{{ getLastNameInitial(currentUser()) }}
|
|
563
|
-
</span>
|
|
564
|
-
</button>
|
|
565
|
-
|
|
566
|
-
<div class="mes-dropdown-menu" *ngIf="dropdownOpen">
|
|
567
|
-
<div class="mes-dropdown-header">
|
|
568
|
-
{{ currentUser().fullName || currentUser().userName }}
|
|
569
|
-
</div>
|
|
570
|
-
<button class="mes-dropdown-item profile-link" (click)="onViewProfile()">
|
|
571
|
-
View Profile
|
|
572
|
-
</button>
|
|
573
|
-
<button class="mes-dropdown-item logout-item" (click)="onLogout()">
|
|
574
|
-
Logout
|
|
575
|
-
</button>
|
|
576
|
-
</div>
|
|
577
|
-
</div>
|
|
578
|
-
</div>
|
|
579
|
-
</ng-container>
|
|
580
|
-
</div>
|
|
543
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: UserProfileComponent, deps: [{ token: MesAuthService }, { token: i2.Router }, { token: ThemeService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
544
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: UserProfileComponent, isStandalone: true, selector: "ma-user-profile", outputs: { notificationClick: "notificationClick" }, host: { listeners: { "document:click": "onDocumentClick($event)" }, properties: { "class": "this.themeClass" } }, ngImport: i0, template: `
|
|
545
|
+
<div class="user-profile-container">
|
|
546
|
+
<!-- Not logged in -->
|
|
547
|
+
<ng-container *ngIf="!currentUser()">
|
|
548
|
+
<button class="login-btn" (click)="onLogin()">
|
|
549
|
+
Login
|
|
550
|
+
</button>
|
|
551
|
+
</ng-container>
|
|
552
|
+
|
|
553
|
+
<!-- Logged in -->
|
|
554
|
+
<ng-container *ngIf="currentUser()">
|
|
555
|
+
<div class="user-header">
|
|
556
|
+
<button class="notification-btn" (click)="onNotificationClick()" title="Notifications">
|
|
557
|
+
<span class="icon">🔔</span>
|
|
558
|
+
<span class="badge" *ngIf="unreadCount > 0">{{ unreadCount }}</span>
|
|
559
|
+
</button>
|
|
560
|
+
|
|
561
|
+
<div class="user-menu-wrapper">
|
|
562
|
+
<button class="user-menu-btn" (click)="toggleDropdown()">
|
|
563
|
+
<img
|
|
564
|
+
*ngIf="currentUser().fullName || currentUser().userName"
|
|
565
|
+
[src]="getAvatarUrl(currentUser())"
|
|
566
|
+
[alt]="currentUser().fullName || currentUser().userName"
|
|
567
|
+
class="avatar"
|
|
568
|
+
/>
|
|
569
|
+
<span *ngIf="!(currentUser().fullName || currentUser().userName)" class="avatar-initial">
|
|
570
|
+
{{ getLastNameInitial(currentUser()) }}
|
|
571
|
+
</span>
|
|
572
|
+
</button>
|
|
573
|
+
|
|
574
|
+
<div class="mes-dropdown-menu" *ngIf="dropdownOpen">
|
|
575
|
+
<div class="mes-dropdown-header">
|
|
576
|
+
{{ currentUser().fullName || currentUser().userName }}
|
|
577
|
+
</div>
|
|
578
|
+
<button class="mes-dropdown-item profile-link" (click)="onViewProfile()">
|
|
579
|
+
View Profile
|
|
580
|
+
</button>
|
|
581
|
+
<button class="mes-dropdown-item logout-item" (click)="onLogout()">
|
|
582
|
+
Logout
|
|
583
|
+
</button>
|
|
584
|
+
</div>
|
|
585
|
+
</div>
|
|
586
|
+
</div>
|
|
587
|
+
</ng-container>
|
|
588
|
+
</div>
|
|
581
589
|
`, isInline: true, styles: [":host{--primary-color: #1976d2;--primary-hover: #1565c0;--primary-light: rgba(25, 118, 210, .1);--error-color: #f44336;--error-light: #ffebee;--text-primary: #333;--text-secondary: #666;--text-muted: #999;--bg-primary: white;--bg-secondary: #f5f5f5;--bg-tertiary: #fafafa;--bg-hover: #f5f5f5;--border-color: #e0e0e0;--border-light: #f0f0f0;--shadow: rgba(0, 0, 0, .15);--shadow-light: rgba(0, 0, 0, .1)}:host(.theme-dark){--primary-color: #90caf9;--primary-hover: #64b5f6;--primary-light: rgba(144, 202, 249, .1);--error-color: #ef5350;--error-light: rgba(239, 83, 80, .1);--text-primary: #e0e0e0;--text-secondary: #b0b0b0;--text-muted: #888;--bg-primary: #1e1e1e;--bg-secondary: #2d2d2d;--bg-tertiary: #252525;--bg-hover: #333;--border-color: #404040;--border-light: #333;--shadow: rgba(0, 0, 0, .3);--shadow-light: rgba(0, 0, 0, .2)}.user-profile-container{display:flex;align-items:center;gap:16px;padding:0 16px}.login-btn{padding:8px 16px;background-color:var(--primary-color);color:#fff;border:none;border-radius:4px;cursor:pointer;font-weight:500;transition:background-color .3s}.login-btn:hover{background-color:var(--primary-hover)}.user-header{display:flex;align-items:center;gap:16px}.notification-btn{position:relative;background:none;border:none;font-size:24px;cursor:pointer;padding:8px;transition:opacity .2s}.notification-btn:hover{opacity:.7}.icon{display:inline-block}.badge{position:absolute;top:0;right:0;background-color:var(--error-color);color:#fff;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700}.user-menu-wrapper{position:relative}.user-menu-btn{background:none;border:none;cursor:pointer;padding:4px;border-radius:50%;transition:background-color .2s;display:flex;align-items:center;justify-content:center}.user-menu-btn:hover{background-color:var(--primary-light)}.avatar{width:40px;height:40px;border-radius:50%;object-fit:cover;background-color:#e0e0e0}.avatar-initial{width:40px;height:40px;border-radius:50%;background-color:var(--primary-color);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:16px}.mes-dropdown-menu{position:absolute;top:calc(100% + 8px);right:0;background:var(--bg-primary);border:1px solid var(--border-color);border-radius:4px;box-shadow:0 2px 8px var(--shadow);min-width:200px;z-index:1000;overflow:hidden}.mes-dropdown-header{padding:12px 16px;border-bottom:1px solid var(--border-light);font-weight:600;color:var(--text-primary);font-size:14px}.mes-dropdown-item{display:block;width:100%;padding:12px 16px;border:none;background:none;text-align:left;cursor:pointer;font-size:14px;color:var(--text-primary);text-decoration:none;transition:background-color .2s}.mes-dropdown-item:hover{background-color:var(--bg-hover)}.profile-link{color:var(--primary-color)}.logout-item{border-top:1px solid var(--border-light);color:var(--error-color)}.logout-item:hover{background-color:var(--error-light)}.user-info{display:flex;flex-direction:column;gap:2px}.user-name{font-weight:500;font-size:14px;color:var(--text-primary)}.user-position{font-size:12px;color:var(--text-secondary)}.logout-btn{background:none;border:none;font-size:20px;cursor:pointer;color:var(--text-secondary);padding:4px 8px;transition:color .2s}.logout-btn:hover{color:var(--primary-color)}@media(max-width:768px){.user-info{display:none}.avatar{width:32px;height:32px}}\n"], dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
582
590
|
}
|
|
583
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
591
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: UserProfileComponent, decorators: [{
|
|
584
592
|
type: Component,
|
|
585
|
-
args: [{ selector: 'ma-user-profile', standalone: true, imports: [NgIf], template: `
|
|
586
|
-
<div class="user-profile-container">
|
|
587
|
-
<!-- Not logged in -->
|
|
588
|
-
<ng-container *ngIf="!currentUser()">
|
|
589
|
-
<button class="login-btn" (click)="onLogin()">
|
|
590
|
-
Login
|
|
591
|
-
</button>
|
|
592
|
-
</ng-container>
|
|
593
|
-
|
|
594
|
-
<!-- Logged in -->
|
|
595
|
-
<ng-container *ngIf="currentUser()">
|
|
596
|
-
<div class="user-header">
|
|
597
|
-
<button class="notification-btn" (click)="onNotificationClick()" title="Notifications">
|
|
598
|
-
<span class="icon">🔔</span>
|
|
599
|
-
<span class="badge" *ngIf="unreadCount > 0">{{ unreadCount }}</span>
|
|
600
|
-
</button>
|
|
601
|
-
|
|
602
|
-
<div class="user-menu-wrapper">
|
|
603
|
-
<button class="user-menu-btn" (click)="toggleDropdown()">
|
|
604
|
-
<img
|
|
605
|
-
*ngIf="currentUser().fullName || currentUser().userName"
|
|
606
|
-
[src]="getAvatarUrl(currentUser())"
|
|
607
|
-
[alt]="currentUser().fullName || currentUser().userName"
|
|
608
|
-
class="avatar"
|
|
609
|
-
/>
|
|
610
|
-
<span *ngIf="!(currentUser().fullName || currentUser().userName)" class="avatar-initial">
|
|
611
|
-
{{ getLastNameInitial(currentUser()) }}
|
|
612
|
-
</span>
|
|
613
|
-
</button>
|
|
614
|
-
|
|
615
|
-
<div class="mes-dropdown-menu" *ngIf="dropdownOpen">
|
|
616
|
-
<div class="mes-dropdown-header">
|
|
617
|
-
{{ currentUser().fullName || currentUser().userName }}
|
|
618
|
-
</div>
|
|
619
|
-
<button class="mes-dropdown-item profile-link" (click)="onViewProfile()">
|
|
620
|
-
View Profile
|
|
621
|
-
</button>
|
|
622
|
-
<button class="mes-dropdown-item logout-item" (click)="onLogout()">
|
|
623
|
-
Logout
|
|
624
|
-
</button>
|
|
625
|
-
</div>
|
|
626
|
-
</div>
|
|
627
|
-
</div>
|
|
628
|
-
</ng-container>
|
|
629
|
-
</div>
|
|
593
|
+
args: [{ selector: 'ma-user-profile', standalone: true, imports: [NgIf], template: `
|
|
594
|
+
<div class="user-profile-container">
|
|
595
|
+
<!-- Not logged in -->
|
|
596
|
+
<ng-container *ngIf="!currentUser()">
|
|
597
|
+
<button class="login-btn" (click)="onLogin()">
|
|
598
|
+
Login
|
|
599
|
+
</button>
|
|
600
|
+
</ng-container>
|
|
601
|
+
|
|
602
|
+
<!-- Logged in -->
|
|
603
|
+
<ng-container *ngIf="currentUser()">
|
|
604
|
+
<div class="user-header">
|
|
605
|
+
<button class="notification-btn" (click)="onNotificationClick()" title="Notifications">
|
|
606
|
+
<span class="icon">🔔</span>
|
|
607
|
+
<span class="badge" *ngIf="unreadCount > 0">{{ unreadCount }}</span>
|
|
608
|
+
</button>
|
|
609
|
+
|
|
610
|
+
<div class="user-menu-wrapper">
|
|
611
|
+
<button class="user-menu-btn" (click)="toggleDropdown()">
|
|
612
|
+
<img
|
|
613
|
+
*ngIf="currentUser().fullName || currentUser().userName"
|
|
614
|
+
[src]="getAvatarUrl(currentUser())"
|
|
615
|
+
[alt]="currentUser().fullName || currentUser().userName"
|
|
616
|
+
class="avatar"
|
|
617
|
+
/>
|
|
618
|
+
<span *ngIf="!(currentUser().fullName || currentUser().userName)" class="avatar-initial">
|
|
619
|
+
{{ getLastNameInitial(currentUser()) }}
|
|
620
|
+
</span>
|
|
621
|
+
</button>
|
|
622
|
+
|
|
623
|
+
<div class="mes-dropdown-menu" *ngIf="dropdownOpen">
|
|
624
|
+
<div class="mes-dropdown-header">
|
|
625
|
+
{{ currentUser().fullName || currentUser().userName }}
|
|
626
|
+
</div>
|
|
627
|
+
<button class="mes-dropdown-item profile-link" (click)="onViewProfile()">
|
|
628
|
+
View Profile
|
|
629
|
+
</button>
|
|
630
|
+
<button class="mes-dropdown-item logout-item" (click)="onLogout()">
|
|
631
|
+
Logout
|
|
632
|
+
</button>
|
|
633
|
+
</div>
|
|
634
|
+
</div>
|
|
635
|
+
</div>
|
|
636
|
+
</ng-container>
|
|
637
|
+
</div>
|
|
630
638
|
`, styles: [":host{--primary-color: #1976d2;--primary-hover: #1565c0;--primary-light: rgba(25, 118, 210, .1);--error-color: #f44336;--error-light: #ffebee;--text-primary: #333;--text-secondary: #666;--text-muted: #999;--bg-primary: white;--bg-secondary: #f5f5f5;--bg-tertiary: #fafafa;--bg-hover: #f5f5f5;--border-color: #e0e0e0;--border-light: #f0f0f0;--shadow: rgba(0, 0, 0, .15);--shadow-light: rgba(0, 0, 0, .1)}:host(.theme-dark){--primary-color: #90caf9;--primary-hover: #64b5f6;--primary-light: rgba(144, 202, 249, .1);--error-color: #ef5350;--error-light: rgba(239, 83, 80, .1);--text-primary: #e0e0e0;--text-secondary: #b0b0b0;--text-muted: #888;--bg-primary: #1e1e1e;--bg-secondary: #2d2d2d;--bg-tertiary: #252525;--bg-hover: #333;--border-color: #404040;--border-light: #333;--shadow: rgba(0, 0, 0, .3);--shadow-light: rgba(0, 0, 0, .2)}.user-profile-container{display:flex;align-items:center;gap:16px;padding:0 16px}.login-btn{padding:8px 16px;background-color:var(--primary-color);color:#fff;border:none;border-radius:4px;cursor:pointer;font-weight:500;transition:background-color .3s}.login-btn:hover{background-color:var(--primary-hover)}.user-header{display:flex;align-items:center;gap:16px}.notification-btn{position:relative;background:none;border:none;font-size:24px;cursor:pointer;padding:8px;transition:opacity .2s}.notification-btn:hover{opacity:.7}.icon{display:inline-block}.badge{position:absolute;top:0;right:0;background-color:var(--error-color);color:#fff;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700}.user-menu-wrapper{position:relative}.user-menu-btn{background:none;border:none;cursor:pointer;padding:4px;border-radius:50%;transition:background-color .2s;display:flex;align-items:center;justify-content:center}.user-menu-btn:hover{background-color:var(--primary-light)}.avatar{width:40px;height:40px;border-radius:50%;object-fit:cover;background-color:#e0e0e0}.avatar-initial{width:40px;height:40px;border-radius:50%;background-color:var(--primary-color);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:16px}.mes-dropdown-menu{position:absolute;top:calc(100% + 8px);right:0;background:var(--bg-primary);border:1px solid var(--border-color);border-radius:4px;box-shadow:0 2px 8px var(--shadow);min-width:200px;z-index:1000;overflow:hidden}.mes-dropdown-header{padding:12px 16px;border-bottom:1px solid var(--border-light);font-weight:600;color:var(--text-primary);font-size:14px}.mes-dropdown-item{display:block;width:100%;padding:12px 16px;border:none;background:none;text-align:left;cursor:pointer;font-size:14px;color:var(--text-primary);text-decoration:none;transition:background-color .2s}.mes-dropdown-item:hover{background-color:var(--bg-hover)}.profile-link{color:var(--primary-color)}.logout-item{border-top:1px solid var(--border-light);color:var(--error-color)}.logout-item:hover{background-color:var(--error-light)}.user-info{display:flex;flex-direction:column;gap:2px}.user-name{font-weight:500;font-size:14px;color:var(--text-primary)}.user-position{font-size:12px;color:var(--text-secondary)}.logout-btn{background:none;border:none;font-size:20px;cursor:pointer;color:var(--text-secondary);padding:4px 8px;transition:color .2s}.logout-btn:hover{color:var(--primary-color)}@media(max-width:768px){.user-info{display:none}.avatar{width:32px;height:32px}}\n"] }]
|
|
631
639
|
}], ctorParameters: () => [{ type: MesAuthService }, { type: i2.Router }, { type: ThemeService }, { type: i0.ChangeDetectorRef }], propDecorators: { notificationClick: [{
|
|
632
640
|
type: Output
|
|
@@ -666,10 +674,10 @@ class ToastService {
|
|
|
666
674
|
clear() {
|
|
667
675
|
this.toasts$.next([]);
|
|
668
676
|
}
|
|
669
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
670
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.
|
|
677
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ToastService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
678
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ToastService, providedIn: 'root' });
|
|
671
679
|
}
|
|
672
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
680
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ToastService, decorators: [{
|
|
673
681
|
type: Injectable,
|
|
674
682
|
args: [{ providedIn: 'root' }]
|
|
675
683
|
}] });
|
|
@@ -706,45 +714,45 @@ class ToastContainerComponent {
|
|
|
706
714
|
close(id) {
|
|
707
715
|
this.toastService.remove(id);
|
|
708
716
|
}
|
|
709
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
710
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.
|
|
711
|
-
<div class="toast-container">
|
|
712
|
-
<div
|
|
713
|
-
*ngFor="let toast of toasts"
|
|
714
|
-
class="toast"
|
|
715
|
-
[class]="'toast-' + toast.type"
|
|
716
|
-
[@slideIn]
|
|
717
|
-
>
|
|
718
|
-
<div class="toast-content">
|
|
719
|
-
<div *ngIf="toast.title" class="toast-title">{{ toast.title }}</div>
|
|
720
|
-
<div class="toast-message" [innerHTML]="toast.message"></div>
|
|
721
|
-
</div>
|
|
722
|
-
<button class="toast-close" (click)="close(toast.id)" aria-label="Close">
|
|
723
|
-
✕
|
|
724
|
-
</button>
|
|
725
|
-
</div>
|
|
726
|
-
</div>
|
|
717
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ToastContainerComponent, deps: [{ token: ToastService }, { token: ThemeService }], target: i0.ɵɵFactoryTarget.Component });
|
|
718
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: ToastContainerComponent, isStandalone: true, selector: "ma-toast-container", host: { properties: { "class": "this.themeClass" } }, ngImport: i0, template: `
|
|
719
|
+
<div class="toast-container">
|
|
720
|
+
<div
|
|
721
|
+
*ngFor="let toast of toasts"
|
|
722
|
+
class="toast"
|
|
723
|
+
[class]="'toast-' + toast.type"
|
|
724
|
+
[@slideIn]
|
|
725
|
+
>
|
|
726
|
+
<div class="toast-content">
|
|
727
|
+
<div *ngIf="toast.title" class="toast-title">{{ toast.title }}</div>
|
|
728
|
+
<div class="toast-message" [innerHTML]="toast.message"></div>
|
|
729
|
+
</div>
|
|
730
|
+
<button class="toast-close" (click)="close(toast.id)" aria-label="Close">
|
|
731
|
+
✕
|
|
732
|
+
</button>
|
|
733
|
+
</div>
|
|
734
|
+
</div>
|
|
727
735
|
`, isInline: true, styles: [":host{--info-color: #2196f3;--success-color: #4caf50;--warning-color: #ff9800;--error-color: #f44336;--text-primary: #333;--bg-primary: white;--shadow: rgba(0, 0, 0, .15);--text-secondary: #999;--border-color: rgba(0, 0, 0, .1)}:host(.theme-dark){--info-color: #64b5f6;--success-color: #81c784;--warning-color: #ffb74d;--error-color: #ef5350;--text-primary: #e0e0e0;--bg-primary: #1e1e1e;--shadow: rgba(0, 0, 0, .3);--text-secondary: #888;--border-color: rgba(255, 255, 255, .1)}.toast-container{position:fixed;top:20px;right:20px;z-index:9999;pointer-events:none}.toast{display:flex;align-items:flex-start;gap:12px;padding:12px 16px;margin-bottom:12px;border-radius:4px;background:var(--bg-primary);border:1px solid var(--border-color);box-shadow:0 4px 12px var(--shadow);pointer-events:auto;min-width:280px;max-width:400px;animation:slideIn .3s ease-out}.toast-content{flex:1}.toast-title{font-weight:600;font-size:14px;margin-bottom:4px}.toast-message{font-size:13px;line-height:1.4}.toast-close{background:none;border:none;cursor:pointer;font-size:18px;color:var(--text-secondary);padding:0;width:24px;height:24px;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:color .2s}.toast-close:hover{color:var(--text-primary)}.toast-info{border-left:4px solid var(--info-color)}.toast-info .toast-title{color:var(--info-color)}.toast-info .toast-message{color:var(--text-primary)}.toast-success{border-left:4px solid var(--success-color)}.toast-success .toast-title{color:var(--success-color)}.toast-success .toast-message{color:var(--text-primary)}.toast-warning{border-left:4px solid var(--warning-color)}.toast-warning .toast-title{color:var(--warning-color)}.toast-warning .toast-message{color:var(--text-primary)}.toast-error{border-left:4px solid var(--error-color)}.toast-error .toast-title{color:var(--error-color)}.toast-error .toast-message{color:var(--text-primary)}@keyframes slideIn{0%{transform:translate(400px);opacity:0}to{transform:translate(0);opacity:1}}@media(max-width:600px){.toast-container{top:10px;right:10px;left:10px}.toast{min-width:auto;max-width:100%}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
728
736
|
}
|
|
729
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
737
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ToastContainerComponent, decorators: [{
|
|
730
738
|
type: Component,
|
|
731
|
-
args: [{ selector: 'ma-toast-container', standalone: true, imports: [CommonModule], template: `
|
|
732
|
-
<div class="toast-container">
|
|
733
|
-
<div
|
|
734
|
-
*ngFor="let toast of toasts"
|
|
735
|
-
class="toast"
|
|
736
|
-
[class]="'toast-' + toast.type"
|
|
737
|
-
[@slideIn]
|
|
738
|
-
>
|
|
739
|
-
<div class="toast-content">
|
|
740
|
-
<div *ngIf="toast.title" class="toast-title">{{ toast.title }}</div>
|
|
741
|
-
<div class="toast-message" [innerHTML]="toast.message"></div>
|
|
742
|
-
</div>
|
|
743
|
-
<button class="toast-close" (click)="close(toast.id)" aria-label="Close">
|
|
744
|
-
✕
|
|
745
|
-
</button>
|
|
746
|
-
</div>
|
|
747
|
-
</div>
|
|
739
|
+
args: [{ selector: 'ma-toast-container', standalone: true, imports: [CommonModule], template: `
|
|
740
|
+
<div class="toast-container">
|
|
741
|
+
<div
|
|
742
|
+
*ngFor="let toast of toasts"
|
|
743
|
+
class="toast"
|
|
744
|
+
[class]="'toast-' + toast.type"
|
|
745
|
+
[@slideIn]
|
|
746
|
+
>
|
|
747
|
+
<div class="toast-content">
|
|
748
|
+
<div *ngIf="toast.title" class="toast-title">{{ toast.title }}</div>
|
|
749
|
+
<div class="toast-message" [innerHTML]="toast.message"></div>
|
|
750
|
+
</div>
|
|
751
|
+
<button class="toast-close" (click)="close(toast.id)" aria-label="Close">
|
|
752
|
+
✕
|
|
753
|
+
</button>
|
|
754
|
+
</div>
|
|
755
|
+
</div>
|
|
748
756
|
`, styles: [":host{--info-color: #2196f3;--success-color: #4caf50;--warning-color: #ff9800;--error-color: #f44336;--text-primary: #333;--bg-primary: white;--shadow: rgba(0, 0, 0, .15);--text-secondary: #999;--border-color: rgba(0, 0, 0, .1)}:host(.theme-dark){--info-color: #64b5f6;--success-color: #81c784;--warning-color: #ffb74d;--error-color: #ef5350;--text-primary: #e0e0e0;--bg-primary: #1e1e1e;--shadow: rgba(0, 0, 0, .3);--text-secondary: #888;--border-color: rgba(255, 255, 255, .1)}.toast-container{position:fixed;top:20px;right:20px;z-index:9999;pointer-events:none}.toast{display:flex;align-items:flex-start;gap:12px;padding:12px 16px;margin-bottom:12px;border-radius:4px;background:var(--bg-primary);border:1px solid var(--border-color);box-shadow:0 4px 12px var(--shadow);pointer-events:auto;min-width:280px;max-width:400px;animation:slideIn .3s ease-out}.toast-content{flex:1}.toast-title{font-weight:600;font-size:14px;margin-bottom:4px}.toast-message{font-size:13px;line-height:1.4}.toast-close{background:none;border:none;cursor:pointer;font-size:18px;color:var(--text-secondary);padding:0;width:24px;height:24px;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:color .2s}.toast-close:hover{color:var(--text-primary)}.toast-info{border-left:4px solid var(--info-color)}.toast-info .toast-title{color:var(--info-color)}.toast-info .toast-message{color:var(--text-primary)}.toast-success{border-left:4px solid var(--success-color)}.toast-success .toast-title{color:var(--success-color)}.toast-success .toast-message{color:var(--text-primary)}.toast-warning{border-left:4px solid var(--warning-color)}.toast-warning .toast-title{color:var(--warning-color)}.toast-warning .toast-message{color:var(--text-primary)}.toast-error{border-left:4px solid var(--error-color)}.toast-error .toast-title{color:var(--error-color)}.toast-error .toast-message{color:var(--text-primary)}@keyframes slideIn{0%{transform:translate(400px);opacity:0}to{transform:translate(0);opacity:1}}@media(max-width:600px){.toast-container{top:10px;right:10px;left:10px}.toast{min-width:auto;max-width:100%}}\n"] }]
|
|
749
757
|
}], ctorParameters: () => [{ type: ToastService }, { type: ThemeService }], propDecorators: { themeClass: [{
|
|
750
758
|
type: HostBinding,
|
|
@@ -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.
|
|
782
|
+
return this._unreadNotifications;
|
|
769
783
|
}
|
|
770
784
|
get readNotifications() {
|
|
771
|
-
return this.
|
|
785
|
+
return this._readNotifications;
|
|
772
786
|
}
|
|
773
787
|
get currentNotifications() {
|
|
774
|
-
return this.activeTab === 'unread' ? this.
|
|
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
|
-
|
|
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
|
|
@@ -942,215 +992,215 @@ class NotificationPanelComponent {
|
|
|
942
992
|
}
|
|
943
993
|
return normalized;
|
|
944
994
|
}
|
|
945
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
946
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.
|
|
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">{{
|
|
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>
|
|
995
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: NotificationPanelComponent, deps: [{ token: MesAuthService }, { token: ToastService }, { token: ThemeService }], target: i0.ɵɵFactoryTarget.Component });
|
|
996
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", 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
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
1100
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", 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">{{
|
|
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
|
|
@@ -1172,23 +1222,23 @@ class MaUserComponent {
|
|
|
1172
1222
|
this.userProfile.loadUnreadCount();
|
|
1173
1223
|
}
|
|
1174
1224
|
}
|
|
1175
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
1176
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.
|
|
1177
|
-
<ma-toast-container></ma-toast-container>
|
|
1178
|
-
<div class="user-header">
|
|
1179
|
-
<ma-user-profile (notificationClick)="notificationPanel.open()"></ma-user-profile>
|
|
1180
|
-
</div>
|
|
1181
|
-
<ma-notification-panel #notificationPanel (notificationRead)="onNotificationRead()"></ma-notification-panel>
|
|
1225
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: MaUserComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1226
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: MaUserComponent, isStandalone: true, selector: "ma-user", viewQueries: [{ propertyName: "userProfile", first: true, predicate: UserProfileComponent, descendants: true }], ngImport: i0, template: `
|
|
1227
|
+
<ma-toast-container></ma-toast-container>
|
|
1228
|
+
<div class="user-header">
|
|
1229
|
+
<ma-user-profile (notificationClick)="notificationPanel.open()"></ma-user-profile>
|
|
1230
|
+
</div>
|
|
1231
|
+
<ma-notification-panel #notificationPanel (notificationRead)="onNotificationRead()"></ma-notification-panel>
|
|
1182
1232
|
`, isInline: true, styles: [".user-header{display:flex;justify-content:flex-end}\n"], dependencies: [{ kind: "component", type: ToastContainerComponent, selector: "ma-toast-container" }, { kind: "component", type: UserProfileComponent, selector: "ma-user-profile", outputs: ["notificationClick"] }, { kind: "component", type: NotificationPanelComponent, selector: "ma-notification-panel", outputs: ["notificationRead"] }] });
|
|
1183
1233
|
}
|
|
1184
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
1234
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: MaUserComponent, decorators: [{
|
|
1185
1235
|
type: Component,
|
|
1186
|
-
args: [{ selector: 'ma-user', standalone: true, imports: [ToastContainerComponent, UserProfileComponent, NotificationPanelComponent], template: `
|
|
1187
|
-
<ma-toast-container></ma-toast-container>
|
|
1188
|
-
<div class="user-header">
|
|
1189
|
-
<ma-user-profile (notificationClick)="notificationPanel.open()"></ma-user-profile>
|
|
1190
|
-
</div>
|
|
1191
|
-
<ma-notification-panel #notificationPanel (notificationRead)="onNotificationRead()"></ma-notification-panel>
|
|
1236
|
+
args: [{ selector: 'ma-user', standalone: true, imports: [ToastContainerComponent, UserProfileComponent, NotificationPanelComponent], template: `
|
|
1237
|
+
<ma-toast-container></ma-toast-container>
|
|
1238
|
+
<div class="user-header">
|
|
1239
|
+
<ma-user-profile (notificationClick)="notificationPanel.open()"></ma-user-profile>
|
|
1240
|
+
</div>
|
|
1241
|
+
<ma-notification-panel #notificationPanel (notificationRead)="onNotificationRead()"></ma-notification-panel>
|
|
1192
1242
|
`, styles: [".user-header{display:flex;justify-content:flex-end}\n"] }]
|
|
1193
1243
|
}], propDecorators: { userProfile: [{
|
|
1194
1244
|
type: ViewChild,
|
|
@@ -1254,21 +1304,21 @@ class NotificationBadgeComponent {
|
|
|
1254
1304
|
onNotificationClick() {
|
|
1255
1305
|
this.notificationClick.emit();
|
|
1256
1306
|
}
|
|
1257
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
1258
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.
|
|
1259
|
-
<button class="notification-btn" (click)="onNotificationClick()" title="Notifications">
|
|
1260
|
-
<span class="icon">🔔</span>
|
|
1261
|
-
<span class="badge" *ngIf="unreadCount > 0">{{ unreadCount }}</span>
|
|
1262
|
-
</button>
|
|
1307
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: NotificationBadgeComponent, deps: [{ token: MesAuthService }, { token: ThemeService }], target: i0.ɵɵFactoryTarget.Component });
|
|
1308
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: NotificationBadgeComponent, isStandalone: true, selector: "ma-notification-badge", outputs: { notificationClick: "notificationClick" }, host: { properties: { "class": "this.themeClass" } }, ngImport: i0, template: `
|
|
1309
|
+
<button class="notification-btn" (click)="onNotificationClick()" title="Notifications">
|
|
1310
|
+
<span class="icon">🔔</span>
|
|
1311
|
+
<span class="badge" *ngIf="unreadCount > 0">{{ unreadCount }}</span>
|
|
1312
|
+
</button>
|
|
1263
1313
|
`, isInline: true, styles: [":host{--error-color: #f44336}:host(.theme-dark){--error-color: #ef5350}.notification-btn{position:relative;background:none;border:none;font-size:24px;cursor:pointer;padding:8px;transition:opacity .2s}.notification-btn:hover{opacity:.7}.icon{display:inline-block}.badge{position:absolute;top:0;right:0;background-color:var(--error-color);color:#fff;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700}\n"], dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
|
|
1264
1314
|
}
|
|
1265
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
1315
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: NotificationBadgeComponent, decorators: [{
|
|
1266
1316
|
type: Component,
|
|
1267
|
-
args: [{ selector: 'ma-notification-badge', standalone: true, imports: [NgIf], template: `
|
|
1268
|
-
<button class="notification-btn" (click)="onNotificationClick()" title="Notifications">
|
|
1269
|
-
<span class="icon">🔔</span>
|
|
1270
|
-
<span class="badge" *ngIf="unreadCount > 0">{{ unreadCount }}</span>
|
|
1271
|
-
</button>
|
|
1317
|
+
args: [{ selector: 'ma-notification-badge', standalone: true, imports: [NgIf], template: `
|
|
1318
|
+
<button class="notification-btn" (click)="onNotificationClick()" title="Notifications">
|
|
1319
|
+
<span class="icon">🔔</span>
|
|
1320
|
+
<span class="badge" *ngIf="unreadCount > 0">{{ unreadCount }}</span>
|
|
1321
|
+
</button>
|
|
1272
1322
|
`, styles: [":host{--error-color: #f44336}:host(.theme-dark){--error-color: #ef5350}.notification-btn{position:relative;background:none;border:none;font-size:24px;cursor:pointer;padding:8px;transition:opacity .2s}.notification-btn:hover{opacity:.7}.icon{display:inline-block}.badge{position:absolute;top:0;right:0;background-color:var(--error-color);color:#fff;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700}\n"] }]
|
|
1273
1323
|
}], ctorParameters: () => [{ type: MesAuthService }, { type: ThemeService }], propDecorators: { notificationClick: [{
|
|
1274
1324
|
type: Output
|