mesauth-angular 1.1.6 → 1.1.7
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/dist/README.md +291 -0
- package/dist/fesm2022/mesauth-angular.mjs +1237 -0
- package/dist/fesm2022/mesauth-angular.mjs.map +1 -0
- package/dist/index.d.ts +382 -0
- package/package.json +45 -44
|
@@ -0,0 +1,1237 @@
|
|
|
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';
|
|
3
|
+
import { HttpClient } from '@angular/common/http';
|
|
4
|
+
import { HubConnectionBuilder, LogLevel } from '@microsoft/signalr';
|
|
5
|
+
import { BehaviorSubject, Subject, throwError } from 'rxjs';
|
|
6
|
+
import { filter, debounceTime, tap, catchError, takeUntil } from 'rxjs/operators';
|
|
7
|
+
import * as i2 from '@angular/router';
|
|
8
|
+
import { Router, NavigationEnd } from '@angular/router';
|
|
9
|
+
import * as i3 from '@angular/common';
|
|
10
|
+
import { NgIf, CommonModule, NgFor } from '@angular/common';
|
|
11
|
+
|
|
12
|
+
/** Injection token for MesAuth configuration */
|
|
13
|
+
const MES_AUTH_CONFIG = new InjectionToken('MES_AUTH_CONFIG');
|
|
14
|
+
/**
|
|
15
|
+
* Provides MesAuth with configuration.
|
|
16
|
+
* This is the recommended way to set up mesauth-angular in standalone apps.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* // app.config.ts
|
|
21
|
+
* export const appConfig: ApplicationConfig = {
|
|
22
|
+
* providers: [
|
|
23
|
+
* provideHttpClient(withInterceptors([mesAuthInterceptor])),
|
|
24
|
+
* provideMesAuth({
|
|
25
|
+
* apiBaseUrl: 'https://auth.example.com',
|
|
26
|
+
* userBaseUrl: 'https://app.example.com'
|
|
27
|
+
* })
|
|
28
|
+
* ]
|
|
29
|
+
* };
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
function provideMesAuth(config) {
|
|
33
|
+
return makeEnvironmentProviders([
|
|
34
|
+
{ provide: MES_AUTH_CONFIG, useValue: config },
|
|
35
|
+
MesAuthService,
|
|
36
|
+
provideAppInitializer(() => {
|
|
37
|
+
const mesAuthService = inject(MesAuthService);
|
|
38
|
+
const httpClient = inject(HttpClient);
|
|
39
|
+
const router = inject(Router);
|
|
40
|
+
mesAuthService.init(config, httpClient, router);
|
|
41
|
+
})
|
|
42
|
+
]);
|
|
43
|
+
}
|
|
44
|
+
var NotificationType;
|
|
45
|
+
(function (NotificationType) {
|
|
46
|
+
NotificationType["Info"] = "Info";
|
|
47
|
+
NotificationType["Warning"] = "Warning";
|
|
48
|
+
NotificationType["Error"] = "Error";
|
|
49
|
+
NotificationType["Success"] = "Success";
|
|
50
|
+
})(NotificationType || (NotificationType = {}));
|
|
51
|
+
class MesAuthService {
|
|
52
|
+
hubConnection = null;
|
|
53
|
+
_currentUser = new BehaviorSubject(null);
|
|
54
|
+
currentUser$ = this._currentUser.asObservable();
|
|
55
|
+
_notifications = new Subject();
|
|
56
|
+
notifications$ = this._notifications.asObservable();
|
|
57
|
+
apiBase = '';
|
|
58
|
+
config = null;
|
|
59
|
+
http;
|
|
60
|
+
router;
|
|
61
|
+
constructor() {
|
|
62
|
+
// Empty constructor - all dependencies passed to init()
|
|
63
|
+
}
|
|
64
|
+
isProtectedRoute(url) {
|
|
65
|
+
// Consider routes protected if they don't include auth-related paths
|
|
66
|
+
return !url.includes('/login') && !url.includes('/auth') && !url.includes('/signin') && !url.includes('/logout');
|
|
67
|
+
}
|
|
68
|
+
init(config, httpClient, router) {
|
|
69
|
+
this.config = config;
|
|
70
|
+
this.http = httpClient;
|
|
71
|
+
this.router = router;
|
|
72
|
+
this.apiBase = config.apiBaseUrl.replace(/\/$/, '');
|
|
73
|
+
// Listen for route changes - only refresh user data if needed for SPA navigation
|
|
74
|
+
// This helps maintain authentication state in single-page applications
|
|
75
|
+
if (this.router) {
|
|
76
|
+
this.router.events
|
|
77
|
+
.pipe(filter(event => event instanceof NavigationEnd), debounceTime(1000) // Longer debounce to avoid interfering with login flow
|
|
78
|
+
)
|
|
79
|
+
.subscribe((event) => {
|
|
80
|
+
// Only refresh if user is logged in and navigating to protected routes
|
|
81
|
+
// Avoid refreshing during login/logout flows
|
|
82
|
+
if (this._currentUser.value && this.isProtectedRoute(event.url)) {
|
|
83
|
+
// Small delay to ensure any login/logout operations complete
|
|
84
|
+
setTimeout(() => {
|
|
85
|
+
if (this._currentUser.value) {
|
|
86
|
+
this.refreshUser();
|
|
87
|
+
}
|
|
88
|
+
}, 100);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
this.fetchCurrentUser();
|
|
93
|
+
}
|
|
94
|
+
getConfig() {
|
|
95
|
+
return this.config;
|
|
96
|
+
}
|
|
97
|
+
fetchCurrentUser() {
|
|
98
|
+
if (!this.apiBase)
|
|
99
|
+
return;
|
|
100
|
+
const url = `${this.apiBase}/auth/me`;
|
|
101
|
+
this.http.get(url, { withCredentials: this.config?.withCredentials ?? true }).subscribe({
|
|
102
|
+
next: (u) => {
|
|
103
|
+
this._currentUser.next(u);
|
|
104
|
+
if (u && this.config) {
|
|
105
|
+
this.startConnection(this.config);
|
|
106
|
+
// Only fetch notifications after confirming user is logged in
|
|
107
|
+
this.fetchInitialNotifications();
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
error: (err) => {
|
|
111
|
+
// Silently handle auth errors (401/403) - user is not logged in
|
|
112
|
+
if (err.status === 401 || err.status === 403) {
|
|
113
|
+
this._currentUser.next(null);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
fetchInitialNotifications() {
|
|
119
|
+
// Skip if no user is logged in or apiBase not set
|
|
120
|
+
if (!this.apiBase || !this._currentUser.value)
|
|
121
|
+
return;
|
|
122
|
+
this.http.get(`${this.apiBase}/notif/me`, { withCredentials: this.config?.withCredentials ?? true }).subscribe({
|
|
123
|
+
next: (notifications) => {
|
|
124
|
+
if (Array.isArray(notifications?.items)) {
|
|
125
|
+
notifications.items.forEach((n) => this._notifications.next(n));
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
error: (err) => {
|
|
129
|
+
// Silently handle auth errors (401/403) - user is not logged in
|
|
130
|
+
// No need to emit anything
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
getUnreadCount() {
|
|
135
|
+
return this.http.get(`${this.apiBase}/notif/me/unread-count`, { withCredentials: this.config?.withCredentials ?? true });
|
|
136
|
+
}
|
|
137
|
+
getNotifications(page = 1, pageSize = 20, includeRead = false, type) {
|
|
138
|
+
let url = `${this.apiBase}/notif/me?page=${page}&pageSize=${pageSize}&includeRead=${includeRead}`;
|
|
139
|
+
if (type) {
|
|
140
|
+
url += `&type=${type}`;
|
|
141
|
+
}
|
|
142
|
+
return this.http.get(url, { withCredentials: this.config?.withCredentials ?? true });
|
|
143
|
+
}
|
|
144
|
+
markAsRead(notificationId) {
|
|
145
|
+
return this.http.patch(`${this.apiBase}/notif/${notificationId}/read`, {}, { withCredentials: this.config?.withCredentials ?? true });
|
|
146
|
+
}
|
|
147
|
+
markAllAsRead() {
|
|
148
|
+
return this.http.patch(`${this.apiBase}/notif/me/read-all`, {}, { withCredentials: this.config?.withCredentials ?? true });
|
|
149
|
+
}
|
|
150
|
+
deleteNotification(notificationId) {
|
|
151
|
+
return this.http.delete(`${this.apiBase}/notif/${notificationId}`, { withCredentials: this.config?.withCredentials ?? true });
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Get frontend routes assigned to the current user
|
|
155
|
+
* Returns routes grouped by application
|
|
156
|
+
*/
|
|
157
|
+
getFrontEndRoutes() {
|
|
158
|
+
if (!this.apiBase)
|
|
159
|
+
throw new Error('MesAuth not initialized');
|
|
160
|
+
return this.http.get(`${this.apiBase}/fe-routes/me`, { withCredentials: this.config?.withCredentials ?? true });
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Get master routes for a specific application
|
|
164
|
+
* @param appId - The application ID
|
|
165
|
+
*/
|
|
166
|
+
getRouteMasters(appId) {
|
|
167
|
+
if (!this.apiBase)
|
|
168
|
+
throw new Error('MesAuth not initialized');
|
|
169
|
+
return this.http.get(`${this.apiBase}/fe-routes/masters/${appId}`, { withCredentials: this.config?.withCredentials ?? true });
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Register/sync frontend routes for an application
|
|
173
|
+
* This is typically called on app startup to sync routes from the frontend app
|
|
174
|
+
* @param appId - The application ID (passed via X-App-Id header)
|
|
175
|
+
* @param routes - Array of route definitions
|
|
176
|
+
*/
|
|
177
|
+
registerFrontEndRoutes(appId, routes) {
|
|
178
|
+
if (!this.apiBase)
|
|
179
|
+
throw new Error('MesAuth not initialized');
|
|
180
|
+
const headers = { 'X-App-Id': appId };
|
|
181
|
+
return this.http.post(`${this.apiBase}/fe-routes/register`, routes, {
|
|
182
|
+
headers,
|
|
183
|
+
withCredentials: this.config?.withCredentials ?? true
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Create a new route master
|
|
188
|
+
* @param appId - The application ID
|
|
189
|
+
* @param route - Route details
|
|
190
|
+
*/
|
|
191
|
+
createRouteMaster(appId, route) {
|
|
192
|
+
if (!this.apiBase)
|
|
193
|
+
throw new Error('MesAuth not initialized');
|
|
194
|
+
return this.http.post(`${this.apiBase}/fe-routes/masters`, {
|
|
195
|
+
appId,
|
|
196
|
+
...route
|
|
197
|
+
}, { withCredentials: this.config?.withCredentials ?? true });
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Update an existing route master
|
|
201
|
+
* @param routeId - The route master ID
|
|
202
|
+
* @param route - Updated route details
|
|
203
|
+
*/
|
|
204
|
+
updateRouteMaster(routeId, route) {
|
|
205
|
+
if (!this.apiBase)
|
|
206
|
+
throw new Error('MesAuth not initialized');
|
|
207
|
+
return this.http.put(`${this.apiBase}/fe-routes/masters/${routeId}`, route, {
|
|
208
|
+
withCredentials: this.config?.withCredentials ?? true
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Delete a route master
|
|
213
|
+
* @param routeId - The route master ID
|
|
214
|
+
*/
|
|
215
|
+
deleteRouteMaster(routeId) {
|
|
216
|
+
if (!this.apiBase)
|
|
217
|
+
throw new Error('MesAuth not initialized');
|
|
218
|
+
return this.http.delete(`${this.apiBase}/fe-routes/masters/${routeId}`, {
|
|
219
|
+
withCredentials: this.config?.withCredentials ?? true
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Assign a route to a role
|
|
224
|
+
* @param routeMasterId - The route master ID
|
|
225
|
+
* @param roleId - The role ID (GUID)
|
|
226
|
+
*/
|
|
227
|
+
assignRouteToRole(routeMasterId, roleId) {
|
|
228
|
+
if (!this.apiBase)
|
|
229
|
+
throw new Error('MesAuth not initialized');
|
|
230
|
+
return this.http.post(`${this.apiBase}/fe-routes/mappings`, {
|
|
231
|
+
routeMasterId,
|
|
232
|
+
roleId
|
|
233
|
+
}, { withCredentials: this.config?.withCredentials ?? true });
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Remove a route assignment from a role
|
|
237
|
+
* @param mappingId - The mapping ID
|
|
238
|
+
*/
|
|
239
|
+
removeRouteFromRole(mappingId) {
|
|
240
|
+
if (!this.apiBase)
|
|
241
|
+
throw new Error('MesAuth not initialized');
|
|
242
|
+
return this.http.delete(`${this.apiBase}/fe-routes/mappings/${mappingId}`, {
|
|
243
|
+
withCredentials: this.config?.withCredentials ?? true
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Get route-to-role mappings for a specific role
|
|
248
|
+
* @param roleId - The role ID (GUID)
|
|
249
|
+
*/
|
|
250
|
+
getRouteMappingsByRole(roleId) {
|
|
251
|
+
if (!this.apiBase)
|
|
252
|
+
throw new Error('MesAuth not initialized');
|
|
253
|
+
return this.http.get(`${this.apiBase}/fe-routes/mappings?roleId=${roleId}`, {
|
|
254
|
+
withCredentials: this.config?.withCredentials ?? true
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
startConnection(config) {
|
|
258
|
+
if (this.hubConnection)
|
|
259
|
+
return;
|
|
260
|
+
const signalrUrl = config.apiBaseUrl.replace(/\/$/, '') + '/hub/notification';
|
|
261
|
+
const builder = new HubConnectionBuilder()
|
|
262
|
+
.withUrl(signalrUrl, { withCredentials: config.withCredentials ?? true })
|
|
263
|
+
.withAutomaticReconnect()
|
|
264
|
+
.configureLogging(LogLevel.Warning);
|
|
265
|
+
this.hubConnection = builder.build();
|
|
266
|
+
this.hubConnection.on('ReceiveNotification', (n) => {
|
|
267
|
+
this._notifications.next(n);
|
|
268
|
+
});
|
|
269
|
+
this.hubConnection.start().then(() => { }).catch((err) => { });
|
|
270
|
+
this.hubConnection.onclose(() => { });
|
|
271
|
+
this.hubConnection.onreconnecting(() => { });
|
|
272
|
+
this.hubConnection.onreconnected(() => { });
|
|
273
|
+
}
|
|
274
|
+
stop() {
|
|
275
|
+
if (!this.hubConnection)
|
|
276
|
+
return;
|
|
277
|
+
this.hubConnection.stop().catch(() => { });
|
|
278
|
+
this.hubConnection = null;
|
|
279
|
+
}
|
|
280
|
+
logout() {
|
|
281
|
+
const url = `${this.apiBase}/auth/logout`;
|
|
282
|
+
return this.http.post(url, {}, { withCredentials: this.config?.withCredentials ?? true }).pipe(tap(() => {
|
|
283
|
+
this._currentUser.next(null);
|
|
284
|
+
this.stop();
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
get currentUser() {
|
|
288
|
+
return this._currentUser.value;
|
|
289
|
+
}
|
|
290
|
+
get isAuthenticated() {
|
|
291
|
+
return this._currentUser.value !== null;
|
|
292
|
+
}
|
|
293
|
+
refreshUser() {
|
|
294
|
+
this.fetchCurrentUser();
|
|
295
|
+
}
|
|
296
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MesAuthService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
297
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MesAuthService });
|
|
298
|
+
}
|
|
299
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MesAuthService, decorators: [{
|
|
300
|
+
type: Injectable
|
|
301
|
+
}], ctorParameters: () => [] });
|
|
302
|
+
|
|
303
|
+
// Track if we're currently redirecting to prevent loopback
|
|
304
|
+
let isRedirecting = false;
|
|
305
|
+
/**
|
|
306
|
+
* Functional HTTP interceptor for handling 401/403 auth errors.
|
|
307
|
+
* Redirects to login page on 401, and to 403 page on 403.
|
|
308
|
+
* Includes loopback prevention to avoid infinite redirects.
|
|
309
|
+
*/
|
|
310
|
+
const mesAuthInterceptor = (req, next) => {
|
|
311
|
+
const authService = inject(MesAuthService);
|
|
312
|
+
const router = inject(Router);
|
|
313
|
+
return next(req).pipe(catchError((error) => {
|
|
314
|
+
const status = error.status;
|
|
315
|
+
// Check if we should handle this error and prevent loopback
|
|
316
|
+
if ((status === 401 || status === 403) && !isRedirecting) {
|
|
317
|
+
const config = authService.getConfig();
|
|
318
|
+
const baseUrl = config?.userBaseUrl || '';
|
|
319
|
+
// Use router URL for internal navigation (cleaner URLs)
|
|
320
|
+
// Falls back to window.location for full URL if needed
|
|
321
|
+
const currentUrl = router.url + (window.location.hash || '');
|
|
322
|
+
const returnUrl = encodeURIComponent(currentUrl);
|
|
323
|
+
// Avoid loops if already on auth/unauth pages
|
|
324
|
+
const isLoginPage = currentUrl.includes('/login');
|
|
325
|
+
const is403Page = currentUrl.includes('/403');
|
|
326
|
+
const isAuthPage = currentUrl.includes('/auth');
|
|
327
|
+
const isMeAuthPage = req.url.includes('/auth/me');
|
|
328
|
+
// Check if user is authenticated
|
|
329
|
+
const isAuthenticated = authService.isAuthenticated;
|
|
330
|
+
if (status === 401 && !isLoginPage && !isAuthPage && !isAuthenticated && !isMeAuthPage) {
|
|
331
|
+
isRedirecting = true;
|
|
332
|
+
// Reset flag after a delay to allow future redirects after user returns
|
|
333
|
+
setTimeout(() => { isRedirecting = false; }, 5000);
|
|
334
|
+
window.location.href = `${baseUrl}/login?returnUrl=${returnUrl}`;
|
|
335
|
+
}
|
|
336
|
+
else if (status === 403 && !is403Page) {
|
|
337
|
+
isRedirecting = true;
|
|
338
|
+
setTimeout(() => { isRedirecting = false; }, 5000);
|
|
339
|
+
let redirectUrl = `${baseUrl}/403?returnUrl=${returnUrl}`;
|
|
340
|
+
if (error.error && error.error.required) {
|
|
341
|
+
redirectUrl += `&required=${encodeURIComponent(error.error.required)}`;
|
|
342
|
+
}
|
|
343
|
+
window.location.href = redirectUrl;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return throwError(() => error);
|
|
347
|
+
}));
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
class MesAuthModule {
|
|
351
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MesAuthModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
352
|
+
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.16", ngImport: i0, type: MesAuthModule });
|
|
353
|
+
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MesAuthModule, providers: [
|
|
354
|
+
MesAuthService
|
|
355
|
+
] });
|
|
356
|
+
}
|
|
357
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MesAuthModule, decorators: [{
|
|
358
|
+
type: NgModule,
|
|
359
|
+
args: [{
|
|
360
|
+
providers: [
|
|
361
|
+
MesAuthService
|
|
362
|
+
]
|
|
363
|
+
}]
|
|
364
|
+
}] });
|
|
365
|
+
|
|
366
|
+
class ThemeService {
|
|
367
|
+
_currentTheme = new BehaviorSubject('light');
|
|
368
|
+
currentTheme$ = this._currentTheme.asObservable();
|
|
369
|
+
observer = null;
|
|
370
|
+
constructor() {
|
|
371
|
+
this.detectTheme();
|
|
372
|
+
this.startWatching();
|
|
373
|
+
}
|
|
374
|
+
ngOnDestroy() {
|
|
375
|
+
this.stopWatching();
|
|
376
|
+
}
|
|
377
|
+
detectTheme() {
|
|
378
|
+
const html = document.documentElement;
|
|
379
|
+
const isDark = html.classList.contains('dark') ||
|
|
380
|
+
html.getAttribute('data-theme') === 'dark' ||
|
|
381
|
+
html.getAttribute('theme') === 'dark' ||
|
|
382
|
+
html.getAttribute('data-coreui-theme') === 'dark';
|
|
383
|
+
this._currentTheme.next(isDark ? 'dark' : 'light');
|
|
384
|
+
}
|
|
385
|
+
startWatching() {
|
|
386
|
+
if (typeof MutationObserver === 'undefined') {
|
|
387
|
+
// Fallback for older browsers - check periodically
|
|
388
|
+
setInterval(() => this.detectTheme(), 1000);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
this.observer = new MutationObserver(() => {
|
|
392
|
+
this.detectTheme();
|
|
393
|
+
});
|
|
394
|
+
this.observer.observe(document.documentElement, {
|
|
395
|
+
attributes: true,
|
|
396
|
+
attributeFilter: ['class', 'data-theme', 'theme', 'data-coreui-theme']
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
stopWatching() {
|
|
400
|
+
if (this.observer) {
|
|
401
|
+
this.observer.disconnect();
|
|
402
|
+
this.observer = null;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
get currentTheme() {
|
|
406
|
+
return this._currentTheme.value;
|
|
407
|
+
}
|
|
408
|
+
// Method to manually set theme if needed
|
|
409
|
+
setTheme(theme) {
|
|
410
|
+
this._currentTheme.next(theme);
|
|
411
|
+
}
|
|
412
|
+
// Re-detect theme from DOM
|
|
413
|
+
refreshTheme() {
|
|
414
|
+
this.detectTheme();
|
|
415
|
+
}
|
|
416
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
417
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ThemeService, providedIn: 'root' });
|
|
418
|
+
}
|
|
419
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ThemeService, decorators: [{
|
|
420
|
+
type: Injectable,
|
|
421
|
+
args: [{
|
|
422
|
+
providedIn: 'root'
|
|
423
|
+
}]
|
|
424
|
+
}], ctorParameters: () => [] });
|
|
425
|
+
|
|
426
|
+
class UserProfileComponent {
|
|
427
|
+
authService;
|
|
428
|
+
router;
|
|
429
|
+
themeService;
|
|
430
|
+
cdr;
|
|
431
|
+
notificationClick = new EventEmitter();
|
|
432
|
+
get themeClass() {
|
|
433
|
+
return `theme-${this.currentTheme}`;
|
|
434
|
+
}
|
|
435
|
+
currentUser = signal(null, ...(ngDevMode ? [{ debugName: "currentUser" }] : []));
|
|
436
|
+
currentTheme = 'light';
|
|
437
|
+
unreadCount = 0;
|
|
438
|
+
dropdownOpen = false;
|
|
439
|
+
hasUser = false;
|
|
440
|
+
destroy$ = new Subject();
|
|
441
|
+
// Signal to force avatar refresh
|
|
442
|
+
avatarRefresh = signal(Date.now(), ...(ngDevMode ? [{ debugName: "avatarRefresh" }] : []));
|
|
443
|
+
constructor(authService, router, themeService, cdr) {
|
|
444
|
+
this.authService = authService;
|
|
445
|
+
this.router = router;
|
|
446
|
+
this.themeService = themeService;
|
|
447
|
+
this.cdr = cdr;
|
|
448
|
+
}
|
|
449
|
+
ngOnInit() {
|
|
450
|
+
this.authService.currentUser$
|
|
451
|
+
.pipe(takeUntil(this.destroy$))
|
|
452
|
+
.subscribe(user => {
|
|
453
|
+
this.currentUser.set(user);
|
|
454
|
+
this.hasUser = !!user;
|
|
455
|
+
// Force avatar refresh when user changes
|
|
456
|
+
this.avatarRefresh.set(Date.now());
|
|
457
|
+
if (!this.hasUser) {
|
|
458
|
+
this.unreadCount = 0;
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
this.loadUnreadCount();
|
|
462
|
+
}
|
|
463
|
+
this.cdr.markForCheck();
|
|
464
|
+
});
|
|
465
|
+
this.themeService.currentTheme$
|
|
466
|
+
.pipe(takeUntil(this.destroy$))
|
|
467
|
+
.subscribe(theme => {
|
|
468
|
+
this.currentTheme = theme;
|
|
469
|
+
});
|
|
470
|
+
// Listen for new notifications
|
|
471
|
+
this.authService.notifications$
|
|
472
|
+
.pipe(takeUntil(this.destroy$))
|
|
473
|
+
.subscribe(() => {
|
|
474
|
+
console.log('Notification received, updating unread count');
|
|
475
|
+
if (this.hasUser) {
|
|
476
|
+
this.loadUnreadCount();
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
ngOnDestroy() {
|
|
481
|
+
this.destroy$.next();
|
|
482
|
+
this.destroy$.complete();
|
|
483
|
+
}
|
|
484
|
+
loadUnreadCount() {
|
|
485
|
+
if (!this.hasUser) {
|
|
486
|
+
this.unreadCount = 0;
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
this.authService.getUnreadCount().subscribe({
|
|
490
|
+
next: (response) => {
|
|
491
|
+
this.unreadCount = response.unreadCount || 0;
|
|
492
|
+
},
|
|
493
|
+
error: (err) => { }
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
getAvatarUrl(user) {
|
|
497
|
+
// Use the refresh signal to force update
|
|
498
|
+
const refresh = this.avatarRefresh();
|
|
499
|
+
const config = this.authService.getConfig();
|
|
500
|
+
const baseUrl = config?.apiBaseUrl || '';
|
|
501
|
+
// If user has avatarPath, use it directly
|
|
502
|
+
if (user.avatarPath) {
|
|
503
|
+
// If avatarPath is already a full URL, use it as-is
|
|
504
|
+
if (user.avatarPath.startsWith('http://') || user.avatarPath.startsWith('https://')) {
|
|
505
|
+
return user.avatarPath;
|
|
506
|
+
}
|
|
507
|
+
// If it's a relative path, construct full URL with refresh timestamp
|
|
508
|
+
return `${baseUrl.replace(/\/$/, '')}${user.avatarPath}?t=${refresh}`;
|
|
509
|
+
}
|
|
510
|
+
// Fallback: construct URL using userId
|
|
511
|
+
const userId = user.userId;
|
|
512
|
+
if (userId && baseUrl) {
|
|
513
|
+
return `${baseUrl.replace(/\/$/, '')}/auth/${userId}/avatar?t=${refresh}`;
|
|
514
|
+
}
|
|
515
|
+
// Fallback to UI avatars service if no userId or baseUrl
|
|
516
|
+
const displayName = user.userName || user.userId || 'User';
|
|
517
|
+
return `https://ui-avatars.com/api/?name=${encodeURIComponent(displayName)}&background=1976d2&color=fff`;
|
|
518
|
+
}
|
|
519
|
+
getLastNameInitial(user) {
|
|
520
|
+
const fullName = user.fullName || user.userName || 'U';
|
|
521
|
+
const parts = fullName.split(' ');
|
|
522
|
+
const lastPart = parts[parts.length - 1];
|
|
523
|
+
return lastPart.charAt(0).toUpperCase();
|
|
524
|
+
}
|
|
525
|
+
toggleDropdown() {
|
|
526
|
+
this.dropdownOpen = !this.dropdownOpen;
|
|
527
|
+
}
|
|
528
|
+
onDocumentClick(event) {
|
|
529
|
+
const target = event.target;
|
|
530
|
+
const clickedInside = target.closest('.user-menu-wrapper');
|
|
531
|
+
if (!clickedInside) {
|
|
532
|
+
this.dropdownOpen = false;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
onLogin() {
|
|
536
|
+
const config = this.authService.getConfig();
|
|
537
|
+
const baseUrl = config?.userBaseUrl || '';
|
|
538
|
+
const returnUrl = encodeURIComponent(this.router.url);
|
|
539
|
+
window.location.href = `${baseUrl}/login?returnUrl=${returnUrl}`;
|
|
540
|
+
}
|
|
541
|
+
onViewProfile() {
|
|
542
|
+
this.router.navigate(['/profile']);
|
|
543
|
+
this.dropdownOpen = false;
|
|
544
|
+
}
|
|
545
|
+
onLogout() {
|
|
546
|
+
this.authService.logout().subscribe({
|
|
547
|
+
next: () => {
|
|
548
|
+
// Clear current user after successful logout
|
|
549
|
+
this.dropdownOpen = false;
|
|
550
|
+
// Navigate to login with return URL
|
|
551
|
+
const config = this.authService.getConfig();
|
|
552
|
+
const baseUrl = config?.userBaseUrl || '';
|
|
553
|
+
const returnUrl = encodeURIComponent(window.location.href);
|
|
554
|
+
window.location.href = `${baseUrl}/login?returnUrl=${returnUrl}`;
|
|
555
|
+
},
|
|
556
|
+
error: (err) => {
|
|
557
|
+
// Still navigate to login even if logout fails
|
|
558
|
+
const config = this.authService.getConfig();
|
|
559
|
+
const baseUrl = config?.userBaseUrl || '';
|
|
560
|
+
window.location.href = `${baseUrl}/login`;
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
onNotificationClick() {
|
|
565
|
+
this.notificationClick.emit();
|
|
566
|
+
}
|
|
567
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: UserProfileComponent, deps: [{ token: MesAuthService }, { token: i2.Router }, { token: ThemeService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
568
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: UserProfileComponent, isStandalone: true, selector: "ma-user-profile", outputs: { notificationClick: "notificationClick" }, host: { listeners: { "document:click": "onDocumentClick($event)" }, properties: { "class": "this.themeClass" } }, ngImport: i0, template: `
|
|
569
|
+
<div class="user-profile-container">
|
|
570
|
+
<!-- Not logged in -->
|
|
571
|
+
<ng-container *ngIf="!currentUser()">
|
|
572
|
+
<button class="login-btn" (click)="onLogin()">
|
|
573
|
+
Login
|
|
574
|
+
</button>
|
|
575
|
+
</ng-container>
|
|
576
|
+
|
|
577
|
+
<!-- Logged in -->
|
|
578
|
+
<ng-container *ngIf="currentUser()">
|
|
579
|
+
<div class="user-header">
|
|
580
|
+
<button class="notification-btn" (click)="onNotificationClick()" title="Notifications">
|
|
581
|
+
<span class="icon">🔔</span>
|
|
582
|
+
<span class="badge" *ngIf="unreadCount > 0">{{ unreadCount }}</span>
|
|
583
|
+
</button>
|
|
584
|
+
|
|
585
|
+
<div class="user-menu-wrapper">
|
|
586
|
+
<button class="user-menu-btn" (click)="toggleDropdown()">
|
|
587
|
+
<img
|
|
588
|
+
*ngIf="currentUser().fullName || currentUser().userName"
|
|
589
|
+
[src]="getAvatarUrl(currentUser())"
|
|
590
|
+
[alt]="currentUser().fullName || currentUser().userName"
|
|
591
|
+
class="avatar"
|
|
592
|
+
/>
|
|
593
|
+
<span *ngIf="!(currentUser().fullName || currentUser().userName)" class="avatar-initial">
|
|
594
|
+
{{ getLastNameInitial(currentUser()) }}
|
|
595
|
+
</span>
|
|
596
|
+
</button>
|
|
597
|
+
|
|
598
|
+
<div class="mes-dropdown-menu" *ngIf="dropdownOpen">
|
|
599
|
+
<div class="mes-dropdown-header">
|
|
600
|
+
{{ currentUser().fullName || currentUser().userName }}
|
|
601
|
+
</div>
|
|
602
|
+
<button class="mes-dropdown-item profile-link" (click)="onViewProfile()">
|
|
603
|
+
View Profile
|
|
604
|
+
</button>
|
|
605
|
+
<button class="mes-dropdown-item logout-item" (click)="onLogout()">
|
|
606
|
+
Logout
|
|
607
|
+
</button>
|
|
608
|
+
</div>
|
|
609
|
+
</div>
|
|
610
|
+
</div>
|
|
611
|
+
</ng-container>
|
|
612
|
+
</div>
|
|
613
|
+
`, 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"] }] });
|
|
614
|
+
}
|
|
615
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: UserProfileComponent, decorators: [{
|
|
616
|
+
type: Component,
|
|
617
|
+
args: [{ selector: 'ma-user-profile', standalone: true, imports: [NgIf], template: `
|
|
618
|
+
<div class="user-profile-container">
|
|
619
|
+
<!-- Not logged in -->
|
|
620
|
+
<ng-container *ngIf="!currentUser()">
|
|
621
|
+
<button class="login-btn" (click)="onLogin()">
|
|
622
|
+
Login
|
|
623
|
+
</button>
|
|
624
|
+
</ng-container>
|
|
625
|
+
|
|
626
|
+
<!-- Logged in -->
|
|
627
|
+
<ng-container *ngIf="currentUser()">
|
|
628
|
+
<div class="user-header">
|
|
629
|
+
<button class="notification-btn" (click)="onNotificationClick()" title="Notifications">
|
|
630
|
+
<span class="icon">🔔</span>
|
|
631
|
+
<span class="badge" *ngIf="unreadCount > 0">{{ unreadCount }}</span>
|
|
632
|
+
</button>
|
|
633
|
+
|
|
634
|
+
<div class="user-menu-wrapper">
|
|
635
|
+
<button class="user-menu-btn" (click)="toggleDropdown()">
|
|
636
|
+
<img
|
|
637
|
+
*ngIf="currentUser().fullName || currentUser().userName"
|
|
638
|
+
[src]="getAvatarUrl(currentUser())"
|
|
639
|
+
[alt]="currentUser().fullName || currentUser().userName"
|
|
640
|
+
class="avatar"
|
|
641
|
+
/>
|
|
642
|
+
<span *ngIf="!(currentUser().fullName || currentUser().userName)" class="avatar-initial">
|
|
643
|
+
{{ getLastNameInitial(currentUser()) }}
|
|
644
|
+
</span>
|
|
645
|
+
</button>
|
|
646
|
+
|
|
647
|
+
<div class="mes-dropdown-menu" *ngIf="dropdownOpen">
|
|
648
|
+
<div class="mes-dropdown-header">
|
|
649
|
+
{{ currentUser().fullName || currentUser().userName }}
|
|
650
|
+
</div>
|
|
651
|
+
<button class="mes-dropdown-item profile-link" (click)="onViewProfile()">
|
|
652
|
+
View Profile
|
|
653
|
+
</button>
|
|
654
|
+
<button class="mes-dropdown-item logout-item" (click)="onLogout()">
|
|
655
|
+
Logout
|
|
656
|
+
</button>
|
|
657
|
+
</div>
|
|
658
|
+
</div>
|
|
659
|
+
</div>
|
|
660
|
+
</ng-container>
|
|
661
|
+
</div>
|
|
662
|
+
`, 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"] }]
|
|
663
|
+
}], ctorParameters: () => [{ type: MesAuthService }, { type: i2.Router }, { type: ThemeService }, { type: i0.ChangeDetectorRef }], propDecorators: { notificationClick: [{
|
|
664
|
+
type: Output
|
|
665
|
+
}], themeClass: [{
|
|
666
|
+
type: HostBinding,
|
|
667
|
+
args: ['class']
|
|
668
|
+
}], onDocumentClick: [{
|
|
669
|
+
type: HostListener,
|
|
670
|
+
args: ['document:click', ['$event']]
|
|
671
|
+
}] } });
|
|
672
|
+
|
|
673
|
+
class ToastService {
|
|
674
|
+
toasts$ = new BehaviorSubject([]);
|
|
675
|
+
toasts = this.toasts$.asObservable();
|
|
676
|
+
show(message, title, type = 'info', duration = 5000) {
|
|
677
|
+
const id = Math.random().toString(36).substr(2, 9);
|
|
678
|
+
const toast = {
|
|
679
|
+
id,
|
|
680
|
+
message,
|
|
681
|
+
title,
|
|
682
|
+
type,
|
|
683
|
+
duration
|
|
684
|
+
};
|
|
685
|
+
const currentToasts = this.toasts$.value;
|
|
686
|
+
this.toasts$.next([...currentToasts, toast]);
|
|
687
|
+
if (duration > 0) {
|
|
688
|
+
setTimeout(() => {
|
|
689
|
+
this.remove(id);
|
|
690
|
+
}, duration);
|
|
691
|
+
}
|
|
692
|
+
return id;
|
|
693
|
+
}
|
|
694
|
+
remove(id) {
|
|
695
|
+
const currentToasts = this.toasts$.value;
|
|
696
|
+
this.toasts$.next(currentToasts.filter(t => t.id !== id));
|
|
697
|
+
}
|
|
698
|
+
clear() {
|
|
699
|
+
this.toasts$.next([]);
|
|
700
|
+
}
|
|
701
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ToastService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
702
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ToastService, providedIn: 'root' });
|
|
703
|
+
}
|
|
704
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ToastService, decorators: [{
|
|
705
|
+
type: Injectable,
|
|
706
|
+
args: [{ providedIn: 'root' }]
|
|
707
|
+
}] });
|
|
708
|
+
|
|
709
|
+
class ToastContainerComponent {
|
|
710
|
+
toastService;
|
|
711
|
+
themeService;
|
|
712
|
+
get themeClass() {
|
|
713
|
+
return `theme-${this.currentTheme}`;
|
|
714
|
+
}
|
|
715
|
+
toasts = [];
|
|
716
|
+
currentTheme = 'light';
|
|
717
|
+
destroy$ = new Subject();
|
|
718
|
+
constructor(toastService, themeService) {
|
|
719
|
+
this.toastService = toastService;
|
|
720
|
+
this.themeService = themeService;
|
|
721
|
+
}
|
|
722
|
+
ngOnInit() {
|
|
723
|
+
this.toastService.toasts
|
|
724
|
+
.pipe(takeUntil(this.destroy$))
|
|
725
|
+
.subscribe(toasts => {
|
|
726
|
+
this.toasts = toasts;
|
|
727
|
+
});
|
|
728
|
+
this.themeService.currentTheme$
|
|
729
|
+
.pipe(takeUntil(this.destroy$))
|
|
730
|
+
.subscribe(theme => {
|
|
731
|
+
this.currentTheme = theme;
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
ngOnDestroy() {
|
|
735
|
+
this.destroy$.next();
|
|
736
|
+
this.destroy$.complete();
|
|
737
|
+
}
|
|
738
|
+
close(id) {
|
|
739
|
+
this.toastService.remove(id);
|
|
740
|
+
}
|
|
741
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ToastContainerComponent, deps: [{ token: ToastService }, { token: ThemeService }], target: i0.ɵɵFactoryTarget.Component });
|
|
742
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: ToastContainerComponent, isStandalone: true, selector: "ma-toast-container", host: { properties: { "class": "this.themeClass" } }, ngImport: i0, template: `
|
|
743
|
+
<div class="toast-container">
|
|
744
|
+
<div
|
|
745
|
+
*ngFor="let toast of toasts"
|
|
746
|
+
class="toast"
|
|
747
|
+
[class]="'toast-' + toast.type"
|
|
748
|
+
[@slideIn]
|
|
749
|
+
>
|
|
750
|
+
<div class="toast-content">
|
|
751
|
+
<div *ngIf="toast.title" class="toast-title">{{ toast.title }}</div>
|
|
752
|
+
<div class="toast-message" [innerHTML]="toast.message"></div>
|
|
753
|
+
</div>
|
|
754
|
+
<button class="toast-close" (click)="close(toast.id)" aria-label="Close">
|
|
755
|
+
✕
|
|
756
|
+
</button>
|
|
757
|
+
</div>
|
|
758
|
+
</div>
|
|
759
|
+
`, 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"] }] });
|
|
760
|
+
}
|
|
761
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ToastContainerComponent, decorators: [{
|
|
762
|
+
type: Component,
|
|
763
|
+
args: [{ selector: 'ma-toast-container', standalone: true, imports: [CommonModule], template: `
|
|
764
|
+
<div class="toast-container">
|
|
765
|
+
<div
|
|
766
|
+
*ngFor="let toast of toasts"
|
|
767
|
+
class="toast"
|
|
768
|
+
[class]="'toast-' + toast.type"
|
|
769
|
+
[@slideIn]
|
|
770
|
+
>
|
|
771
|
+
<div class="toast-content">
|
|
772
|
+
<div *ngIf="toast.title" class="toast-title">{{ toast.title }}</div>
|
|
773
|
+
<div class="toast-message" [innerHTML]="toast.message"></div>
|
|
774
|
+
</div>
|
|
775
|
+
<button class="toast-close" (click)="close(toast.id)" aria-label="Close">
|
|
776
|
+
✕
|
|
777
|
+
</button>
|
|
778
|
+
</div>
|
|
779
|
+
</div>
|
|
780
|
+
`, 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"] }]
|
|
781
|
+
}], ctorParameters: () => [{ type: ToastService }, { type: ThemeService }], propDecorators: { themeClass: [{
|
|
782
|
+
type: HostBinding,
|
|
783
|
+
args: ['class']
|
|
784
|
+
}] } });
|
|
785
|
+
|
|
786
|
+
class NotificationPanelComponent {
|
|
787
|
+
authService;
|
|
788
|
+
toastService;
|
|
789
|
+
themeService;
|
|
790
|
+
notificationRead = new EventEmitter();
|
|
791
|
+
get themeClass() {
|
|
792
|
+
return `theme-${this.currentTheme}`;
|
|
793
|
+
}
|
|
794
|
+
isOpen = false;
|
|
795
|
+
notifications = [];
|
|
796
|
+
currentTheme = 'light';
|
|
797
|
+
activeTab = 'unread'; // Default to unread tab
|
|
798
|
+
destroy$ = new Subject();
|
|
799
|
+
get unreadNotifications() {
|
|
800
|
+
return this.notifications.filter(n => !n.isRead);
|
|
801
|
+
}
|
|
802
|
+
get readNotifications() {
|
|
803
|
+
return this.notifications.filter(n => n.isRead);
|
|
804
|
+
}
|
|
805
|
+
get currentNotifications() {
|
|
806
|
+
return this.activeTab === 'unread' ? this.unreadNotifications : this.readNotifications;
|
|
807
|
+
}
|
|
808
|
+
getNotificationMessage(notification) {
|
|
809
|
+
return notification.messageHtml || notification.message || '';
|
|
810
|
+
}
|
|
811
|
+
constructor(authService, toastService, themeService) {
|
|
812
|
+
this.authService = authService;
|
|
813
|
+
this.toastService = toastService;
|
|
814
|
+
this.themeService = themeService;
|
|
815
|
+
}
|
|
816
|
+
ngOnInit() {
|
|
817
|
+
this.themeService.currentTheme$
|
|
818
|
+
.pipe(takeUntil(this.destroy$))
|
|
819
|
+
.subscribe(theme => {
|
|
820
|
+
this.currentTheme = theme;
|
|
821
|
+
});
|
|
822
|
+
this.loadNotifications();
|
|
823
|
+
// Listen for new real-time notifications
|
|
824
|
+
this.authService.notifications$
|
|
825
|
+
.pipe(takeUntil(this.destroy$))
|
|
826
|
+
.subscribe((notification) => {
|
|
827
|
+
// Show toast for new notification
|
|
828
|
+
this.toastService.show(notification.messageHtml || notification.message || '', notification.title, 'info', 5000);
|
|
829
|
+
// Reload notifications list
|
|
830
|
+
this.loadNotifications();
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
ngOnDestroy() {
|
|
834
|
+
this.destroy$.next();
|
|
835
|
+
this.destroy$.complete();
|
|
836
|
+
}
|
|
837
|
+
loadNotifications() {
|
|
838
|
+
this.authService.getNotifications(1, 50, true).subscribe({
|
|
839
|
+
next: (response) => {
|
|
840
|
+
this.notifications = response.items || [];
|
|
841
|
+
},
|
|
842
|
+
error: (err) => { }
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
open() {
|
|
846
|
+
this.isOpen = true;
|
|
847
|
+
this.activeTab = 'unread'; // Reset to unread tab when opening
|
|
848
|
+
}
|
|
849
|
+
close() {
|
|
850
|
+
this.isOpen = false;
|
|
851
|
+
}
|
|
852
|
+
switchTab(tab) {
|
|
853
|
+
this.activeTab = tab;
|
|
854
|
+
}
|
|
855
|
+
markAsRead(notificationId, event) {
|
|
856
|
+
if (event) {
|
|
857
|
+
event.stopPropagation();
|
|
858
|
+
}
|
|
859
|
+
this.authService.markAsRead(notificationId).subscribe({
|
|
860
|
+
next: () => {
|
|
861
|
+
const notification = this.notifications.find(n => n.id === notificationId);
|
|
862
|
+
if (notification) {
|
|
863
|
+
notification.isRead = true;
|
|
864
|
+
this.notificationRead.emit();
|
|
865
|
+
}
|
|
866
|
+
},
|
|
867
|
+
error: (err) => { }
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
markAllAsRead() {
|
|
871
|
+
this.authService.markAllAsRead().subscribe({
|
|
872
|
+
next: () => {
|
|
873
|
+
this.notifications.forEach(n => n.isRead = true);
|
|
874
|
+
this.notificationRead.emit();
|
|
875
|
+
},
|
|
876
|
+
error: (err) => { }
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
deleteAllRead() {
|
|
880
|
+
const readNotificationIds = this.notifications
|
|
881
|
+
.filter(n => n.isRead)
|
|
882
|
+
.map(n => n.id);
|
|
883
|
+
// Delete all read notifications
|
|
884
|
+
const deletePromises = readNotificationIds.map(id => this.authService.deleteNotification(id).toPromise());
|
|
885
|
+
Promise.all(deletePromises).then(() => {
|
|
886
|
+
// Remove all read notifications from the local array
|
|
887
|
+
this.notifications = this.notifications.filter(n => !n.isRead);
|
|
888
|
+
}).catch((err) => {
|
|
889
|
+
// If bulk delete fails, reload notifications to get current state
|
|
890
|
+
this.loadNotifications();
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
deleteAllUnread() {
|
|
894
|
+
const unreadNotificationIds = this.notifications
|
|
895
|
+
.filter(n => !n.isRead)
|
|
896
|
+
.map(n => n.id);
|
|
897
|
+
// Delete all unread notifications
|
|
898
|
+
const deletePromises = unreadNotificationIds.map(id => this.authService.deleteNotification(id).toPromise());
|
|
899
|
+
Promise.all(deletePromises).then(() => {
|
|
900
|
+
// Remove all unread notifications from the local array
|
|
901
|
+
this.notifications = this.notifications.filter(n => n.isRead);
|
|
902
|
+
}).catch((err) => {
|
|
903
|
+
// If bulk delete fails, reload notifications to get current state
|
|
904
|
+
this.loadNotifications();
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
delete(notificationId, event) {
|
|
908
|
+
event.stopPropagation();
|
|
909
|
+
this.authService.deleteNotification(notificationId).subscribe({
|
|
910
|
+
next: () => {
|
|
911
|
+
this.notifications = this.notifications.filter(n => n.id !== notificationId);
|
|
912
|
+
},
|
|
913
|
+
error: (err) => { }
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
formatDate(dateString) {
|
|
917
|
+
const date = new Date(dateString);
|
|
918
|
+
const now = new Date();
|
|
919
|
+
const diffMs = now.getTime() - date.getTime();
|
|
920
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
921
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
922
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
923
|
+
if (diffMins < 1)
|
|
924
|
+
return 'Now';
|
|
925
|
+
if (diffMins < 60)
|
|
926
|
+
return `${diffMins}m ago`;
|
|
927
|
+
if (diffHours < 24)
|
|
928
|
+
return `${diffHours}h ago`;
|
|
929
|
+
if (diffDays < 7)
|
|
930
|
+
return `${diffDays}d ago`;
|
|
931
|
+
return date.toLocaleDateString();
|
|
932
|
+
}
|
|
933
|
+
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 });
|
|
934
|
+
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: `
|
|
935
|
+
<div class="notification-panel" [class.open]="isOpen">
|
|
936
|
+
<!-- Header -->
|
|
937
|
+
<div class="panel-header">
|
|
938
|
+
<h3>Notifications</h3>
|
|
939
|
+
<button class="close-btn" (click)="close()" title="Close">✕</button>
|
|
940
|
+
</div>
|
|
941
|
+
|
|
942
|
+
<!-- Tabs -->
|
|
943
|
+
<div class="tabs">
|
|
944
|
+
<button
|
|
945
|
+
class="tab-btn"
|
|
946
|
+
[class.active]="activeTab === 'unread'"
|
|
947
|
+
(click)="switchTab('unread')"
|
|
948
|
+
>
|
|
949
|
+
Unread ({{ unreadNotifications.length }})
|
|
950
|
+
</button>
|
|
951
|
+
<button
|
|
952
|
+
class="tab-btn"
|
|
953
|
+
[class.active]="activeTab === 'read'"
|
|
954
|
+
(click)="switchTab('read')"
|
|
955
|
+
>
|
|
956
|
+
Read ({{ readNotifications.length }})
|
|
957
|
+
</button>
|
|
958
|
+
</div>
|
|
959
|
+
|
|
960
|
+
<!-- Notifications List -->
|
|
961
|
+
<div class="notifications-list">
|
|
962
|
+
<ng-container *ngIf="currentNotifications.length > 0">
|
|
963
|
+
<div
|
|
964
|
+
*ngFor="let notification of currentNotifications"
|
|
965
|
+
class="notification-item"
|
|
966
|
+
[class.unread]="!notification.isRead"
|
|
967
|
+
(click)="markAsRead(notification.id)"
|
|
968
|
+
>
|
|
969
|
+
<div class="notification-content">
|
|
970
|
+
<div class="notification-title">{{ notification.title }}</div>
|
|
971
|
+
<div class="notification-message" [innerHTML]="getNotificationMessage(notification)"></div>
|
|
972
|
+
<div class="notification-meta">
|
|
973
|
+
<span class="app-name">{{ notification.sourceAppName }}</span>
|
|
974
|
+
<span class="time">{{ formatDate(notification.createdAt) }}</span>
|
|
975
|
+
</div>
|
|
976
|
+
</div>
|
|
977
|
+
<button
|
|
978
|
+
class="read-btn"
|
|
979
|
+
(click)="markAsRead(notification.id, $event)"
|
|
980
|
+
title="Mark as read"
|
|
981
|
+
*ngIf="!notification.isRead"
|
|
982
|
+
>
|
|
983
|
+
✓
|
|
984
|
+
</button>
|
|
985
|
+
<button
|
|
986
|
+
class="delete-btn"
|
|
987
|
+
(click)="delete(notification.id, $event)"
|
|
988
|
+
title="Delete notification"
|
|
989
|
+
*ngIf="notification.isRead"
|
|
990
|
+
>
|
|
991
|
+
✓
|
|
992
|
+
</button>
|
|
993
|
+
</div>
|
|
994
|
+
</ng-container>
|
|
995
|
+
|
|
996
|
+
<ng-container *ngIf="currentNotifications.length === 0">
|
|
997
|
+
<div class="empty-state">
|
|
998
|
+
No {{ activeTab }} notifications
|
|
999
|
+
</div>
|
|
1000
|
+
</ng-container>
|
|
1001
|
+
</div>
|
|
1002
|
+
|
|
1003
|
+
<!-- Footer Actions -->
|
|
1004
|
+
<div class="panel-footer" *ngIf="currentNotifications.length > 0">
|
|
1005
|
+
<div class="footer-actions" *ngIf="activeTab === 'unread'">
|
|
1006
|
+
<button class="action-btn" (click)="markAllAsRead()" *ngIf="unreadNotifications.length > 0">
|
|
1007
|
+
Mark all as read
|
|
1008
|
+
</button>
|
|
1009
|
+
<button class="action-btn delete-all-btn" (click)="deleteAllUnread()" *ngIf="unreadNotifications.length > 0">
|
|
1010
|
+
Delete all
|
|
1011
|
+
</button>
|
|
1012
|
+
</div>
|
|
1013
|
+
<button class="action-btn delete-all-btn" (click)="deleteAllRead()" *ngIf="activeTab === 'read' && readNotifications.length > 0">
|
|
1014
|
+
Delete all
|
|
1015
|
+
</button>
|
|
1016
|
+
</div>
|
|
1017
|
+
</div>
|
|
1018
|
+
`, isInline: true, styles: [":host{--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){--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:1000;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:13px;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}@media(max-width:600px){.notification-panel{width:100%;right:-100%}}\n"], dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }] });
|
|
1019
|
+
}
|
|
1020
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: NotificationPanelComponent, decorators: [{
|
|
1021
|
+
type: Component,
|
|
1022
|
+
args: [{ selector: 'ma-notification-panel', standalone: true, imports: [NgIf, NgFor], template: `
|
|
1023
|
+
<div class="notification-panel" [class.open]="isOpen">
|
|
1024
|
+
<!-- Header -->
|
|
1025
|
+
<div class="panel-header">
|
|
1026
|
+
<h3>Notifications</h3>
|
|
1027
|
+
<button class="close-btn" (click)="close()" title="Close">✕</button>
|
|
1028
|
+
</div>
|
|
1029
|
+
|
|
1030
|
+
<!-- Tabs -->
|
|
1031
|
+
<div class="tabs">
|
|
1032
|
+
<button
|
|
1033
|
+
class="tab-btn"
|
|
1034
|
+
[class.active]="activeTab === 'unread'"
|
|
1035
|
+
(click)="switchTab('unread')"
|
|
1036
|
+
>
|
|
1037
|
+
Unread ({{ unreadNotifications.length }})
|
|
1038
|
+
</button>
|
|
1039
|
+
<button
|
|
1040
|
+
class="tab-btn"
|
|
1041
|
+
[class.active]="activeTab === 'read'"
|
|
1042
|
+
(click)="switchTab('read')"
|
|
1043
|
+
>
|
|
1044
|
+
Read ({{ readNotifications.length }})
|
|
1045
|
+
</button>
|
|
1046
|
+
</div>
|
|
1047
|
+
|
|
1048
|
+
<!-- Notifications List -->
|
|
1049
|
+
<div class="notifications-list">
|
|
1050
|
+
<ng-container *ngIf="currentNotifications.length > 0">
|
|
1051
|
+
<div
|
|
1052
|
+
*ngFor="let notification of currentNotifications"
|
|
1053
|
+
class="notification-item"
|
|
1054
|
+
[class.unread]="!notification.isRead"
|
|
1055
|
+
(click)="markAsRead(notification.id)"
|
|
1056
|
+
>
|
|
1057
|
+
<div class="notification-content">
|
|
1058
|
+
<div class="notification-title">{{ notification.title }}</div>
|
|
1059
|
+
<div class="notification-message" [innerHTML]="getNotificationMessage(notification)"></div>
|
|
1060
|
+
<div class="notification-meta">
|
|
1061
|
+
<span class="app-name">{{ notification.sourceAppName }}</span>
|
|
1062
|
+
<span class="time">{{ formatDate(notification.createdAt) }}</span>
|
|
1063
|
+
</div>
|
|
1064
|
+
</div>
|
|
1065
|
+
<button
|
|
1066
|
+
class="read-btn"
|
|
1067
|
+
(click)="markAsRead(notification.id, $event)"
|
|
1068
|
+
title="Mark as read"
|
|
1069
|
+
*ngIf="!notification.isRead"
|
|
1070
|
+
>
|
|
1071
|
+
✓
|
|
1072
|
+
</button>
|
|
1073
|
+
<button
|
|
1074
|
+
class="delete-btn"
|
|
1075
|
+
(click)="delete(notification.id, $event)"
|
|
1076
|
+
title="Delete notification"
|
|
1077
|
+
*ngIf="notification.isRead"
|
|
1078
|
+
>
|
|
1079
|
+
✓
|
|
1080
|
+
</button>
|
|
1081
|
+
</div>
|
|
1082
|
+
</ng-container>
|
|
1083
|
+
|
|
1084
|
+
<ng-container *ngIf="currentNotifications.length === 0">
|
|
1085
|
+
<div class="empty-state">
|
|
1086
|
+
No {{ activeTab }} notifications
|
|
1087
|
+
</div>
|
|
1088
|
+
</ng-container>
|
|
1089
|
+
</div>
|
|
1090
|
+
|
|
1091
|
+
<!-- Footer Actions -->
|
|
1092
|
+
<div class="panel-footer" *ngIf="currentNotifications.length > 0">
|
|
1093
|
+
<div class="footer-actions" *ngIf="activeTab === 'unread'">
|
|
1094
|
+
<button class="action-btn" (click)="markAllAsRead()" *ngIf="unreadNotifications.length > 0">
|
|
1095
|
+
Mark all as read
|
|
1096
|
+
</button>
|
|
1097
|
+
<button class="action-btn delete-all-btn" (click)="deleteAllUnread()" *ngIf="unreadNotifications.length > 0">
|
|
1098
|
+
Delete all
|
|
1099
|
+
</button>
|
|
1100
|
+
</div>
|
|
1101
|
+
<button class="action-btn delete-all-btn" (click)="deleteAllRead()" *ngIf="activeTab === 'read' && readNotifications.length > 0">
|
|
1102
|
+
Delete all
|
|
1103
|
+
</button>
|
|
1104
|
+
</div>
|
|
1105
|
+
</div>
|
|
1106
|
+
`, styles: [":host{--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){--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:1000;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:13px;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}@media(max-width:600px){.notification-panel{width:100%;right:-100%}}\n"] }]
|
|
1107
|
+
}], ctorParameters: () => [{ type: MesAuthService }, { type: ToastService }, { type: ThemeService }], propDecorators: { notificationRead: [{
|
|
1108
|
+
type: Output
|
|
1109
|
+
}], themeClass: [{
|
|
1110
|
+
type: HostBinding,
|
|
1111
|
+
args: ['class']
|
|
1112
|
+
}] } });
|
|
1113
|
+
|
|
1114
|
+
class MaUserComponent {
|
|
1115
|
+
userProfile;
|
|
1116
|
+
ngAfterViewInit() {
|
|
1117
|
+
// Ensure proper initialization
|
|
1118
|
+
if (this.userProfile) {
|
|
1119
|
+
this.userProfile.loadUnreadCount();
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
onNotificationRead() {
|
|
1123
|
+
if (this.userProfile) {
|
|
1124
|
+
this.userProfile.loadUnreadCount();
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MaUserComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1128
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: MaUserComponent, isStandalone: true, selector: "ma-user", viewQueries: [{ propertyName: "userProfile", first: true, predicate: UserProfileComponent, descendants: true }], ngImport: i0, template: `
|
|
1129
|
+
<ma-toast-container></ma-toast-container>
|
|
1130
|
+
<div class="user-header">
|
|
1131
|
+
<ma-user-profile (notificationClick)="notificationPanel.open()"></ma-user-profile>
|
|
1132
|
+
</div>
|
|
1133
|
+
<ma-notification-panel #notificationPanel (notificationRead)="onNotificationRead()"></ma-notification-panel>
|
|
1134
|
+
`, 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"] }] });
|
|
1135
|
+
}
|
|
1136
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MaUserComponent, decorators: [{
|
|
1137
|
+
type: Component,
|
|
1138
|
+
args: [{ selector: 'ma-user', standalone: true, imports: [ToastContainerComponent, UserProfileComponent, NotificationPanelComponent], template: `
|
|
1139
|
+
<ma-toast-container></ma-toast-container>
|
|
1140
|
+
<div class="user-header">
|
|
1141
|
+
<ma-user-profile (notificationClick)="notificationPanel.open()"></ma-user-profile>
|
|
1142
|
+
</div>
|
|
1143
|
+
<ma-notification-panel #notificationPanel (notificationRead)="onNotificationRead()"></ma-notification-panel>
|
|
1144
|
+
`, styles: [".user-header{display:flex;justify-content:flex-end}\n"] }]
|
|
1145
|
+
}], propDecorators: { userProfile: [{
|
|
1146
|
+
type: ViewChild,
|
|
1147
|
+
args: [UserProfileComponent]
|
|
1148
|
+
}] } });
|
|
1149
|
+
|
|
1150
|
+
class NotificationBadgeComponent {
|
|
1151
|
+
authService;
|
|
1152
|
+
themeService;
|
|
1153
|
+
notificationClick = new EventEmitter();
|
|
1154
|
+
get themeClass() {
|
|
1155
|
+
return `theme-${this.currentTheme}`;
|
|
1156
|
+
}
|
|
1157
|
+
unreadCount = 0;
|
|
1158
|
+
currentTheme = 'light';
|
|
1159
|
+
hasUser = false;
|
|
1160
|
+
destroy$ = new Subject();
|
|
1161
|
+
constructor(authService, themeService) {
|
|
1162
|
+
this.authService = authService;
|
|
1163
|
+
this.themeService = themeService;
|
|
1164
|
+
}
|
|
1165
|
+
ngOnInit() {
|
|
1166
|
+
this.themeService.currentTheme$
|
|
1167
|
+
.pipe(takeUntil(this.destroy$))
|
|
1168
|
+
.subscribe(theme => {
|
|
1169
|
+
this.currentTheme = theme;
|
|
1170
|
+
});
|
|
1171
|
+
this.authService.currentUser$
|
|
1172
|
+
.pipe(takeUntil(this.destroy$))
|
|
1173
|
+
.subscribe(user => {
|
|
1174
|
+
this.hasUser = !!user;
|
|
1175
|
+
if (!this.hasUser) {
|
|
1176
|
+
this.unreadCount = 0;
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
this.loadUnreadCount();
|
|
1180
|
+
});
|
|
1181
|
+
// Listen for new notifications
|
|
1182
|
+
this.authService.notifications$
|
|
1183
|
+
.pipe(takeUntil(this.destroy$))
|
|
1184
|
+
.subscribe(() => {
|
|
1185
|
+
if (this.hasUser) {
|
|
1186
|
+
this.loadUnreadCount();
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
ngOnDestroy() {
|
|
1191
|
+
this.destroy$.next();
|
|
1192
|
+
this.destroy$.complete();
|
|
1193
|
+
}
|
|
1194
|
+
loadUnreadCount() {
|
|
1195
|
+
if (!this.hasUser) {
|
|
1196
|
+
this.unreadCount = 0;
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
this.authService.getUnreadCount().subscribe({
|
|
1200
|
+
next: (response) => {
|
|
1201
|
+
this.unreadCount = response.unreadCount || 0;
|
|
1202
|
+
},
|
|
1203
|
+
error: (err) => console.error('Error loading unread count:', err)
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
onNotificationClick() {
|
|
1207
|
+
this.notificationClick.emit();
|
|
1208
|
+
}
|
|
1209
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: NotificationBadgeComponent, deps: [{ token: MesAuthService }, { token: ThemeService }], target: i0.ɵɵFactoryTarget.Component });
|
|
1210
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: NotificationBadgeComponent, isStandalone: true, selector: "ma-notification-badge", outputs: { notificationClick: "notificationClick" }, host: { properties: { "class": "this.themeClass" } }, ngImport: i0, template: `
|
|
1211
|
+
<button class="notification-btn" (click)="onNotificationClick()" title="Notifications">
|
|
1212
|
+
<span class="icon">🔔</span>
|
|
1213
|
+
<span class="badge" *ngIf="unreadCount > 0">{{ unreadCount }}</span>
|
|
1214
|
+
</button>
|
|
1215
|
+
`, 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"] }] });
|
|
1216
|
+
}
|
|
1217
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: NotificationBadgeComponent, decorators: [{
|
|
1218
|
+
type: Component,
|
|
1219
|
+
args: [{ selector: 'ma-notification-badge', standalone: true, imports: [NgIf], template: `
|
|
1220
|
+
<button class="notification-btn" (click)="onNotificationClick()" title="Notifications">
|
|
1221
|
+
<span class="icon">🔔</span>
|
|
1222
|
+
<span class="badge" *ngIf="unreadCount > 0">{{ unreadCount }}</span>
|
|
1223
|
+
</button>
|
|
1224
|
+
`, 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"] }]
|
|
1225
|
+
}], ctorParameters: () => [{ type: MesAuthService }, { type: ThemeService }], propDecorators: { notificationClick: [{
|
|
1226
|
+
type: Output
|
|
1227
|
+
}], themeClass: [{
|
|
1228
|
+
type: HostBinding,
|
|
1229
|
+
args: ['class']
|
|
1230
|
+
}] } });
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Generated bundle index. Do not edit.
|
|
1234
|
+
*/
|
|
1235
|
+
|
|
1236
|
+
export { MES_AUTH_CONFIG, MaUserComponent, MesAuthModule, MesAuthService, NotificationBadgeComponent, NotificationPanelComponent, NotificationType, ThemeService, ToastContainerComponent, ToastService, UserProfileComponent, mesAuthInterceptor, provideMesAuth };
|
|
1237
|
+
//# sourceMappingURL=mesauth-angular.mjs.map
|