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