@masterteam/audit-logs 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,59 +1,87 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, Component, Injectable, computed, viewChild, linkedSignal } from '@angular/core';
2
+ import { inject, ChangeDetectionStrategy, Component, Injectable, computed, input, output, signal, effect, viewChild, linkedSignal } from '@angular/core';
3
+ import { toSignal } from '@angular/core/rxjs-interop';
3
4
  import { Router, RouterOutlet } from '@angular/router';
4
5
  import { TranslocoService, TranslocoDirective } from '@jsverse/transloco';
5
6
  import { Breadcrumb } from '@masterteam/components/breadcrumb';
6
7
  import { Page } from '@masterteam/components/page';
8
+ import * as i2 from '@angular/common';
7
9
  import { CommonModule } from '@angular/common';
10
+ import { Button } from '@masterteam/components/button';
11
+ import { Chip } from '@masterteam/components/chip';
12
+ import { StatisticCard } from '@masterteam/components/statistic-card';
8
13
  import { Table } from '@masterteam/components/table';
9
- import { SkeletonModule } from 'primeng/skeleton';
10
- import { Avatar } from '@masterteam/components/avatar';
11
- import { Action, Selector, State, Store, select } from '@ngxs/store';
12
14
  import { HttpClient, HttpContext } from '@angular/common/http';
13
- import { CrudStateBase, handleApiRequest, REQUEST_CONTEXT } from '@masterteam/components';
15
+ import { CrudStateBase, REQUEST_CONTEXT, handleApiRequest } from '@masterteam/components';
16
+ import { Action, Selector, State, Store, select } from '@ngxs/store';
17
+ import { Card } from '@masterteam/components/card';
18
+ import { Drawer } from '@masterteam/components/drawer';
19
+ import { EntityStatus, EntityUser } from '@masterteam/components/entities';
20
+ import { ModalService } from '@masterteam/components/modal';
21
+ import * as i1 from 'primeng/skeleton';
22
+ import { SkeletonModule } from 'primeng/skeleton';
14
23
 
15
24
  class AuditLogs {
16
25
  router = inject(Router);
17
26
  translocoService = inject(TranslocoService);
18
- breadcrumbItems = [
19
- {
20
- label: '',
21
- icon: 'general.home-line',
22
- routerLink: '/control-panel/workspaces',
23
- },
24
- {
25
- label: this.translocoService.translate('product-settings.product-settings'),
26
- routerLink: '/control-panel/product-settings',
27
- },
28
- { label: this.translocoService.translate('audit-logs.audit-logs') },
29
- ];
27
+ activeLang = toSignal(this.translocoService.langChanges$, {
28
+ initialValue: this.translocoService.getActiveLang(),
29
+ });
30
+ get breadcrumbItems() {
31
+ this.activeLang();
32
+ return [
33
+ {
34
+ label: '',
35
+ icon: 'general.home-line',
36
+ routerLink: '/control-panel/workspaces',
37
+ },
38
+ {
39
+ label: this.translocoService.translate('product-settings.product-settings'),
40
+ routerLink: '/control-panel/product-settings',
41
+ },
42
+ { label: this.translocoService.translate('audit-logs.audit-logs') },
43
+ ];
44
+ }
30
45
  goBack() {
31
46
  this.router.navigate(['control-panel/product-settings']);
32
47
  }
33
48
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: AuditLogs, deps: [], target: i0.ɵɵFactoryTarget.Component });
34
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.3", type: AuditLogs, isStandalone: true, selector: "mt-audit-logs", ngImport: i0, template: "<ng-container *transloco=\"let t\">\r\n <mt-page\r\n [title]=\"t('audit-logs.audit-logs')\"\r\n [avatarIcon]=\"'time.clock-fast-forward'\"\r\n [avatarStyle]=\"{\r\n '--p-avatar-background': 'var(--p-lime-50)',\r\n '--p-avatar-color': 'var(--p-lime-700)',\r\n }\"\r\n (backButtonClick)=\"goBack()\"\r\n backButton\r\n >\r\n <h3 class=\"font-bold text-xl\">{{ t(\"audit-logs.audit-logs\") }}</h3>\r\n <mt-breadcrumb\r\n [items]=\"breadcrumbItems\"\r\n [styleClass]=\"'flex justify-start mx-1 mb-4'\"\r\n ></mt-breadcrumb>\r\n <router-outlet />\r\n </mt-page>\r\n</ng-container>\r\n", styles: [""], dependencies: [{ kind: "component", type: Page, selector: "mt-page", inputs: ["backButton", "backButtonIcon", "avatarIcon", "avatarStyle", "avatarShape", "title", "tabs", "activeTab", "contentClass", "contentId"], outputs: ["backButtonClick", "tabChange"] }, { kind: "directive", type: RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Breadcrumb, selector: "mt-breadcrumb", inputs: ["items", "styleClass"], outputs: ["onItemClick"] }] });
49
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.3", type: AuditLogs, isStandalone: true, selector: "mt-audit-logs", ngImport: i0, template: "<ng-container *transloco=\"let t\">\n <mt-page\n [title]=\"t('audit-logs.audit-logs')\"\n [contentClass]=\"'max-[1025px]:p-4 max-[640px]:p-3'\"\n [avatarIcon]=\"'time.clock-fast-forward'\"\n [avatarStyle]=\"{\n '--p-avatar-background': 'var(--p-lime-50)',\n '--p-avatar-color': 'var(--p-lime-700)',\n }\"\n (backButtonClick)=\"goBack()\"\n backButton\n >\n <div class=\"flex min-h-0 flex-col gap-4\">\n <mt-breadcrumb\n [items]=\"breadcrumbItems\"\n [styleClass]=\"'mx-1 flex justify-start'\"\n />\n\n <router-outlet />\n </div>\n </mt-page>\n</ng-container>\n", styles: [":host{display:block;height:100%}\n"], dependencies: [{ kind: "component", type: Page, selector: "mt-page", inputs: ["backButton", "backButtonIcon", "avatarIcon", "avatarStyle", "avatarShape", "title", "tabs", "activeTab", "contentClass", "contentId"], outputs: ["backButtonClick", "tabChange"] }, { kind: "directive", type: RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Breadcrumb, selector: "mt-breadcrumb", inputs: ["items", "styleClass"], outputs: ["onItemClick"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
35
50
  }
36
51
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: AuditLogs, decorators: [{
37
52
  type: Component,
38
- args: [{ selector: 'mt-audit-logs', imports: [Page, RouterOutlet, TranslocoDirective, Breadcrumb], template: "<ng-container *transloco=\"let t\">\r\n <mt-page\r\n [title]=\"t('audit-logs.audit-logs')\"\r\n [avatarIcon]=\"'time.clock-fast-forward'\"\r\n [avatarStyle]=\"{\r\n '--p-avatar-background': 'var(--p-lime-50)',\r\n '--p-avatar-color': 'var(--p-lime-700)',\r\n }\"\r\n (backButtonClick)=\"goBack()\"\r\n backButton\r\n >\r\n <h3 class=\"font-bold text-xl\">{{ t(\"audit-logs.audit-logs\") }}</h3>\r\n <mt-breadcrumb\r\n [items]=\"breadcrumbItems\"\r\n [styleClass]=\"'flex justify-start mx-1 mb-4'\"\r\n ></mt-breadcrumb>\r\n <router-outlet />\r\n </mt-page>\r\n</ng-container>\r\n" }]
53
+ args: [{ selector: 'mt-audit-logs', imports: [Page, RouterOutlet, TranslocoDirective, Breadcrumb], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t\">\n <mt-page\n [title]=\"t('audit-logs.audit-logs')\"\n [contentClass]=\"'max-[1025px]:p-4 max-[640px]:p-3'\"\n [avatarIcon]=\"'time.clock-fast-forward'\"\n [avatarStyle]=\"{\n '--p-avatar-background': 'var(--p-lime-50)',\n '--p-avatar-color': 'var(--p-lime-700)',\n }\"\n (backButtonClick)=\"goBack()\"\n backButton\n >\n <div class=\"flex min-h-0 flex-col gap-4\">\n <mt-breadcrumb\n [items]=\"breadcrumbItems\"\n [styleClass]=\"'mx-1 flex justify-start'\"\n />\n\n <router-outlet />\n </div>\n </mt-page>\n</ng-container>\n", styles: [":host{display:block;height:100%}\n"] }]
39
54
  }] });
40
55
 
41
56
  var AuditLogsActionKey;
42
57
  (function (AuditLogsActionKey) {
43
58
  AuditLogsActionKey["GetAuditLogs"] = "getAuditLogs";
59
+ AuditLogsActionKey["GetAuditLogDetail"] = "getAuditLogDetail";
44
60
  })(AuditLogsActionKey || (AuditLogsActionKey = {}));
45
61
 
46
62
  class GetAuditLogs {
47
63
  page;
48
64
  pageSize;
49
65
  filters;
50
- static type = '[AuditLogs] Get Audit Logs';
51
- constructor(page, pageSize, filters) {
66
+ static type = '[AuditLogs] Get Audit Trail Search';
67
+ constructor(page = 1, pageSize = 20, filters = {}) {
52
68
  this.page = page;
53
69
  this.pageSize = pageSize;
54
70
  this.filters = filters;
55
71
  }
56
72
  }
73
+ class GetAuditLogDetail {
74
+ eventId;
75
+ language;
76
+ static type = '[AuditLogs] Get Audit Trail Detail';
77
+ constructor(eventId, language) {
78
+ this.eventId = eventId;
79
+ this.language = language;
80
+ }
81
+ }
82
+ class ClearAuditLogDetail {
83
+ static type = '[AuditLogs] Clear Audit Trail Detail';
84
+ }
57
85
 
58
86
  var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
59
87
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -61,67 +89,135 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
61
89
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
62
90
  return c > 3 && r && Object.defineProperty(target, key, r), r;
63
91
  };
92
+ const DEFAULT_SEARCH_RESPONSE = {
93
+ totalCount: 0,
94
+ page: 1,
95
+ pageSize: 20,
96
+ items: [],
97
+ };
64
98
  let AuditLogsState = class AuditLogsState extends CrudStateBase {
65
99
  http = inject(HttpClient);
66
- // ============================================================================
67
- // Selectors - Individual data selectors for fine-grained reactivity
68
- // ============================================================================
100
+ context = new HttpContext().set(REQUEST_CONTEXT, {
101
+ useBaseUrl: false,
102
+ });
103
+ baseUrl = 'control-panel/audit-trail';
69
104
  static getAuditLogs(state) {
70
105
  return state.auditLogs;
71
106
  }
72
- static getPage(state) {
73
- return state.page;
107
+ static getFilters(state) {
108
+ return state.filters;
74
109
  }
75
- static getPageSize(state) {
76
- return state.pageSize;
110
+ static getSelectedEventId(state) {
111
+ return state.selectedEventId;
112
+ }
113
+ static getSelectedEventDetail(state) {
114
+ return state.selectedEventDetail;
77
115
  }
78
- // ============================================================================
79
- // Loading/Error Slice Selectors - REQUIRED for optimal performance
80
- // ============================================================================
81
116
  static getLoadingActive(state) {
82
117
  return state.loadingActive;
83
118
  }
84
119
  static getErrors(state) {
85
120
  return state.errors;
86
121
  }
87
- // ============================================================================
88
- // Actions
89
- // ============================================================================
90
122
  getAuditLogs(ctx, { page, pageSize, filters }) {
91
- const params = {
92
- page: page.toString(),
93
- pageSize: pageSize.toString(),
123
+ const query = this.buildQueryParams({
94
124
  ...filters,
95
- };
96
- const req$ = this.http.get(`app/audittrail`, {
97
- params: params,
125
+ page: String(page),
126
+ pageSize: String(pageSize),
98
127
  });
99
128
  return handleApiRequest({
100
129
  ctx,
101
130
  key: AuditLogsActionKey.GetAuditLogs,
102
- request$: req$,
131
+ request$: this.http.get(`${this.baseUrl}/search`, {
132
+ params: query,
133
+ context: this.context,
134
+ }),
103
135
  onSuccess: (response) => ({
104
- auditLogs: response.data ?? [],
105
- page,
106
- pageSize,
136
+ auditLogs: response.data
137
+ ? {
138
+ ...DEFAULT_SEARCH_RESPONSE,
139
+ ...response.data,
140
+ items: response.data.items ?? [],
141
+ }
142
+ : {
143
+ ...DEFAULT_SEARCH_RESPONSE,
144
+ page,
145
+ pageSize,
146
+ },
147
+ filters: this.sanitizeFilters(filters),
148
+ }),
149
+ onError: () => ({
150
+ filters: this.sanitizeFilters(filters),
107
151
  }),
108
152
  });
109
153
  }
154
+ getAuditLogDetail(ctx, { eventId, language }) {
155
+ ctx.patchState({
156
+ selectedEventId: eventId,
157
+ selectedEventDetail: null,
158
+ });
159
+ return handleApiRequest({
160
+ ctx,
161
+ key: AuditLogsActionKey.GetAuditLogDetail,
162
+ request$: this.http.get(`${this.baseUrl}/events/${encodeURIComponent(eventId)}`, {
163
+ params: this.buildQueryParams(language ? { language } : undefined),
164
+ context: this.context,
165
+ }),
166
+ onSuccess: (response, state) => state.selectedEventId === eventId
167
+ ? {
168
+ selectedEventDetail: response.data ?? null,
169
+ }
170
+ : {},
171
+ onError: (_error, state) => state.selectedEventId === eventId
172
+ ? {
173
+ selectedEventDetail: null,
174
+ }
175
+ : {},
176
+ });
177
+ }
178
+ clearAuditLogDetail(ctx) {
179
+ ctx.patchState({
180
+ selectedEventId: null,
181
+ selectedEventDetail: null,
182
+ });
183
+ }
184
+ buildQueryParams(query) {
185
+ return Object.entries(query ?? {}).reduce((params, [key, value]) => {
186
+ if (value === null || value === undefined || value.trim() === '') {
187
+ return params;
188
+ }
189
+ params[key] = value;
190
+ return params;
191
+ }, {});
192
+ }
193
+ sanitizeFilters(filters) {
194
+ const { language: _language, ...rest } = filters;
195
+ return rest;
196
+ }
110
197
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: AuditLogsState, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
111
198
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: AuditLogsState });
112
199
  };
113
200
  __decorate([
114
201
  Action(GetAuditLogs)
115
202
  ], AuditLogsState.prototype, "getAuditLogs", null);
203
+ __decorate([
204
+ Action(GetAuditLogDetail)
205
+ ], AuditLogsState.prototype, "getAuditLogDetail", null);
206
+ __decorate([
207
+ Action(ClearAuditLogDetail)
208
+ ], AuditLogsState.prototype, "clearAuditLogDetail", null);
116
209
  __decorate([
117
210
  Selector()
118
211
  ], AuditLogsState, "getAuditLogs", null);
119
212
  __decorate([
120
213
  Selector()
121
- ], AuditLogsState, "getPage", null);
214
+ ], AuditLogsState, "getFilters", null);
215
+ __decorate([
216
+ Selector()
217
+ ], AuditLogsState, "getSelectedEventId", null);
122
218
  __decorate([
123
219
  Selector()
124
- ], AuditLogsState, "getPageSize", null);
220
+ ], AuditLogsState, "getSelectedEventDetail", null);
125
221
  __decorate([
126
222
  Selector()
127
223
  ], AuditLogsState, "getLoadingActive", null);
@@ -132,9 +228,10 @@ AuditLogsState = __decorate([
132
228
  State({
133
229
  name: 'auditLogs',
134
230
  defaults: {
135
- auditLogs: { items: [] },
136
- page: 1,
137
- pageSize: 10,
231
+ auditLogs: DEFAULT_SEARCH_RESPONSE,
232
+ filters: {},
233
+ selectedEventId: null,
234
+ selectedEventDetail: null,
138
235
  loadingActive: [],
139
236
  errors: {},
140
237
  },
@@ -142,35 +239,30 @@ AuditLogsState = __decorate([
142
239
  ], AuditLogsState);
143
240
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: AuditLogsState, decorators: [{
144
241
  type: Injectable
145
- }], propDecorators: { getAuditLogs: [] } });
242
+ }], propDecorators: { getAuditLogs: [], getAuditLogDetail: [], clearAuditLogDetail: [] } });
146
243
 
147
244
  class AuditLogsFacade {
148
245
  store = inject(Store);
149
- // ============================================================================
150
- // Data Selectors - Memoized by NGXS (fine-grained reactivity)
151
- // ============================================================================
152
246
  auditLogs = select(AuditLogsState.getAuditLogs);
153
- page = select(AuditLogsState.getPage);
154
- pageSize = select(AuditLogsState.getPageSize);
155
- // ============================================================================
156
- // Loading/Error Slices - Memoized by NGXS
157
- // ============================================================================
247
+ filters = select(AuditLogsState.getFilters);
248
+ selectedEventId = select(AuditLogsState.getSelectedEventId);
249
+ selectedEventDetail = select(AuditLogsState.getSelectedEventDetail);
158
250
  loadingActive = select(AuditLogsState.getLoadingActive);
159
251
  errors = select(AuditLogsState.getErrors);
160
- // ============================================================================
161
- // Loading Signals - Computed from slice (minimal reactivity)
162
- // ============================================================================
163
252
  isLoadingAuditLogs = computed(() => this.loadingActive().includes(AuditLogsActionKey.GetAuditLogs), ...(ngDevMode ? [{ debugName: "isLoadingAuditLogs" }] : []));
164
- // ============================================================================
165
- // Error Signals - Computed from slice (minimal reactivity)
166
- // ============================================================================
253
+ isLoadingAuditLogDetail = computed(() => this.loadingActive().includes(AuditLogsActionKey.GetAuditLogDetail), ...(ngDevMode ? [{ debugName: "isLoadingAuditLogDetail" }] : []));
167
254
  getAuditLogsError = computed(() => this.errors()[AuditLogsActionKey.GetAuditLogs] ?? null, ...(ngDevMode ? [{ debugName: "getAuditLogsError" }] : []));
168
- // ============================================================================
169
- // Action Dispatchers
170
- // ============================================================================
171
- getAuditLogs(page = 1, pageSize = 10, filters) {
255
+ getAuditLogDetailError = computed(() => this.errors()[AuditLogsActionKey.GetAuditLogDetail] ?? null, ...(ngDevMode ? [{ debugName: "getAuditLogDetailError" }] : []));
256
+ activeFilterCount = computed(() => Object.values(this.filters()).filter((value) => value !== null && value !== undefined && value !== '').length, ...(ngDevMode ? [{ debugName: "activeFilterCount" }] : []));
257
+ getAuditLogs(page = 1, pageSize = 20, filters = {}) {
172
258
  return this.store.dispatch(new GetAuditLogs(page, pageSize, filters));
173
259
  }
260
+ getAuditLogDetail(eventId, language) {
261
+ return this.store.dispatch(new GetAuditLogDetail(eventId, language));
262
+ }
263
+ clearAuditLogDetail() {
264
+ return this.store.dispatch(new ClearAuditLogDetail());
265
+ }
174
266
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: AuditLogsFacade, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
175
267
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: AuditLogsFacade, providedIn: 'root' });
176
268
  }
@@ -179,6 +271,82 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImpor
179
271
  args: [{ providedIn: 'root' }]
180
272
  }] });
181
273
 
274
+ const FALLBACK_STATUS_COLOR = '#475569';
275
+ const STATUS_COLOR_MAPS = {
276
+ category: {
277
+ Runtime: '#047857',
278
+ ControlPanel: '#0369a1',
279
+ Discussion: '#7c3aed',
280
+ Process: '#0f766e',
281
+ Escalation: '#c2410c',
282
+ PhaseGate: '#0e7490',
283
+ Governance: '#b45309',
284
+ System: '#475569',
285
+ },
286
+ operation: {
287
+ Create: '#047857',
288
+ Update: '#0369a1',
289
+ Delete: '#be123c',
290
+ Close: '#475569',
291
+ Move: '#b45309',
292
+ Progress: '#0e7490',
293
+ Configure: '#4338ca',
294
+ Comment: '#7c3aed',
295
+ BulkUpdate: '#1d4ed8',
296
+ Change: '#0369a1',
297
+ Unknown: '#475569',
298
+ },
299
+ importance: {
300
+ Low: '#475569',
301
+ Medium: '#0369a1',
302
+ High: '#b45309',
303
+ Critical: '#be123c',
304
+ },
305
+ surface: {
306
+ Runtime: '#047857',
307
+ ControlPanel: '#4338ca',
308
+ Background: '#b45309',
309
+ System: '#475569',
310
+ },
311
+ };
312
+ const STATUS_LABELS = {
313
+ category: 'Category',
314
+ operation: 'Operation',
315
+ importance: 'Importance',
316
+ surface: 'Surface',
317
+ };
318
+ function getAuditStatusMap(kind) {
319
+ return Object.fromEntries(Object.entries(STATUS_COLOR_MAPS[kind]).map(([key, color]) => [
320
+ key,
321
+ { color },
322
+ ]));
323
+ }
324
+ function getAuditStatusColor(kind, code) {
325
+ if (!code) {
326
+ return FALLBACK_STATUS_COLOR;
327
+ }
328
+ return STATUS_COLOR_MAPS[kind][code] ?? FALLBACK_STATUS_COLOR;
329
+ }
330
+ function getAuditStatusEntity(kind, item) {
331
+ const display = item?.label || item?.code;
332
+ const code = item?.code || display;
333
+ if (!display || !code) {
334
+ return null;
335
+ }
336
+ return {
337
+ name: STATUS_LABELS[kind],
338
+ viewType: 'Status',
339
+ value: {
340
+ key: code,
341
+ display,
342
+ color: getAuditStatusColor(kind, code),
343
+ },
344
+ configuration: {
345
+ hideName: true,
346
+ },
347
+ };
348
+ }
349
+
182
350
  /**
183
351
  * Recursively normalizes Date objects to YYYY-MM-DDTHH:mm:ss format.
184
352
  * Prevents UTC timezone leaks by converting dates to local datetime strings.
@@ -210,82 +378,422 @@ function normalizeDates(value) {
210
378
  return value;
211
379
  }
212
380
 
381
+ class AuditLogDetailDrawer {
382
+ modal = inject(ModalService);
383
+ visible = input(false, ...(ngDevMode ? [{ debugName: "visible" }] : []));
384
+ detail = input(null, ...(ngDevMode ? [{ debugName: "detail" }] : []));
385
+ loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
386
+ error = input(null, ...(ngDevMode ? [{ debugName: "error" }] : []));
387
+ visibleChange = output();
388
+ expandedEvidence = signal({}, ...(ngDevMode ? [{ debugName: "expandedEvidence" }] : []));
389
+ drawerTitle = computed(() => this.detail()?.summary ?? '', ...(ngDevMode ? [{ debugName: "drawerTitle" }] : []));
390
+ constructor() {
391
+ effect(() => {
392
+ this.detail()?.eventId;
393
+ this.expandedEvidence.set({});
394
+ });
395
+ }
396
+ onDrawerVisibleChange(visible) {
397
+ this.visibleChange.emit(visible);
398
+ }
399
+ getStatusEntity(kind, item) {
400
+ return getAuditStatusEntity(kind, item);
401
+ }
402
+ isEmptyValue(value) {
403
+ return value === null || value === undefined || value === '';
404
+ }
405
+ formatValue(value, valueKind) {
406
+ if (this.isEmptyValue(value)) {
407
+ return '';
408
+ }
409
+ if (this.isJsonValue(valueKind, value)) {
410
+ return this.formatJson(value);
411
+ }
412
+ if (valueKind === 'Boolean') {
413
+ if (value === 'true') {
414
+ return 'True';
415
+ }
416
+ if (value === 'false') {
417
+ return 'False';
418
+ }
419
+ }
420
+ return value ?? '';
421
+ }
422
+ isJsonValue(valueKind, value) {
423
+ if (valueKind === 'Json') {
424
+ return true;
425
+ }
426
+ const trimmed = value?.trim();
427
+ return !!trimmed && (trimmed.startsWith('{') || trimmed.startsWith('['));
428
+ }
429
+ formatJson(value) {
430
+ if (!value) {
431
+ return '';
432
+ }
433
+ try {
434
+ return JSON.stringify(JSON.parse(value), null, 2);
435
+ }
436
+ catch {
437
+ return value;
438
+ }
439
+ }
440
+ getFieldLabel(change) {
441
+ return change.fieldLabel || this.humanizeKey(change.fieldKey);
442
+ }
443
+ getChangeSource(change) {
444
+ return change.changeSource ? this.humanizeKey(change.changeSource) : null;
445
+ }
446
+ getSubjectLabel(subject) {
447
+ return (subject.label ||
448
+ `${this.humanizeKey(subject.subjectType)} ${subject.subjectId}`.trim());
449
+ }
450
+ getActorUserEntity(value) {
451
+ if (!value) {
452
+ return null;
453
+ }
454
+ return {
455
+ viewType: 'User',
456
+ value,
457
+ };
458
+ }
459
+ isEvidenceExpanded(index) {
460
+ return !!this.expandedEvidence()[String(index)];
461
+ }
462
+ toggleEvidence(index) {
463
+ const key = String(index);
464
+ this.expandedEvidence.update((state) => ({
465
+ ...state,
466
+ [key]: !state[key],
467
+ }));
468
+ }
469
+ humanizeKey(value) {
470
+ if (!value) {
471
+ return '';
472
+ }
473
+ return value
474
+ .replace(/[_-]+/g, ' ')
475
+ .replace(/\s+/g, ' ')
476
+ .trim()
477
+ .replace(/\b\w/g, (char) => char.toUpperCase());
478
+ }
479
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: AuditLogDetailDrawer, deps: [], target: i0.ɵɵFactoryTarget.Component });
480
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.3", type: AuditLogDetailDrawer, isStandalone: true, selector: "mt-audit-log-detail-drawer", inputs: { visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: false, transformFunction: null }, detail: { classPropertyName: "detail", publicName: "detail", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { visibleChange: "visibleChange" }, host: { classAttribute: "block h-full" }, ngImport: i0, template: "<mt-drawer\n [visible]=\"visible()\"\n [title]=\"drawerTitle()\"\n [loadingHeader]=\"loading()\"\n styleClass=\"mt-audit-log-drawer !absolute !w-[90%] xl:!w-[68rem]\"\n appendTo=\"page-content\"\n position=\"right\"\n (visibleChange)=\"onDrawerVisibleChange($event)\"\n *transloco=\"let t; prefix: 'audit-logs'\"\n>\n <ng-container content>\n @if (loading()) {\n <div [class]=\"modal.contentClass + ' h-full overflow-y-auto p-5'\">\n <div class=\"flex flex-col gap-5\">\n <div class=\"flex flex-wrap gap-3\">\n @for (_ of [1, 2, 3, 4]; track $index) {\n <p-skeleton width=\"7rem\" height=\"2rem\" borderRadius=\"0.5rem\" />\n }\n </div>\n\n <div class=\"grid gap-5 xl:grid-cols-[minmax(0,1.35fr)_minmax(18rem,0.85fr)]\">\n <div class=\"flex flex-col gap-5\">\n <mt-card [title]=\"t('drawer.change-groups')\">\n <div class=\"flex flex-col gap-4\">\n <p-skeleton height=\"12rem\" borderRadius=\"1rem\" />\n <p-skeleton height=\"14rem\" borderRadius=\"1rem\" />\n </div>\n </mt-card>\n\n <mt-card [title]=\"t('drawer.evidence')\">\n <div class=\"flex flex-col gap-4\">\n <p-skeleton height=\"9rem\" borderRadius=\"1rem\" />\n </div>\n </mt-card>\n </div>\n\n <div class=\"flex flex-col gap-5\">\n <mt-card [title]=\"t('drawer.request-context')\">\n <div class=\"flex flex-col gap-4\">\n <p-skeleton height=\"1rem\" borderRadius=\"0.5rem\" />\n <p-skeleton height=\"1rem\" borderRadius=\"0.5rem\" />\n <p-skeleton height=\"1rem\" borderRadius=\"0.5rem\" />\n <p-skeleton height=\"1rem\" borderRadius=\"0.5rem\" />\n <p-skeleton height=\"3rem\" borderRadius=\"0.75rem\" />\n </div>\n </mt-card>\n\n <mt-card [title]=\"t('drawer.subjects')\">\n <div class=\"flex flex-col gap-4\">\n <p-skeleton height=\"8rem\" borderRadius=\"1rem\" />\n </div>\n </mt-card>\n </div>\n </div>\n </div>\n </div>\n } @else if (error(); as errorMessage) {\n <div [class]=\"modal.contentClass + ' h-full overflow-y-auto p-5'\">\n <mt-card class=\"min-h-[28rem]\">\n <div\n class=\"flex min-h-[28rem] items-center justify-center p-6 text-center\"\n >\n <p class=\"max-w-2xl text-sm font-medium text-rose-700\">\n {{ errorMessage }}\n </p>\n </div>\n </mt-card>\n </div>\n } @else if (detail(); as event) {\n <div [class]=\"modal.contentClass + ' h-full overflow-y-auto p-5'\">\n <div class=\"flex flex-col gap-5\">\n <div\n class=\"grid gap-5 xl:grid-cols-[minmax(0,1.35fr)_minmax(18rem,0.85fr)]\"\n >\n <div class=\"flex min-w-0 flex-col gap-5\">\n <mt-card [title]=\"t('drawer.change-groups')\">\n @if (event.changeGroups.length > 0) {\n @let hasMultipleChangeGroups = event.changeGroups.length > 1;\n <div class=\"flex flex-col gap-5\">\n @for (group of event.changeGroups; track group.key) {\n @if (hasMultipleChangeGroups) {\n <div\n class=\"overflow-hidden rounded-xl border border-surface-200\"\n >\n <div class=\"border-b border-surface-200 px-4 py-3\">\n <h5 class=\"font-semibold text-surface-900\">\n {{ group.label || humanizeKey(group.key) }}\n </h5>\n </div>\n\n <div class=\"divide-y divide-surface-100\">\n @for (\n change of group.changes;\n track change.fieldKey + \"-\" + $index\n ) {\n <div class=\"flex flex-col gap-3 px-4 py-4\">\n <div\n class=\"flex flex-wrap items-start justify-between gap-2\"\n >\n <div class=\"flex flex-col gap-1\">\n <span class=\"font-medium text-surface-900\">\n {{ getFieldLabel(change) }}\n </span>\n\n <div\n class=\"flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-surface-500\"\n >\n @if (\n getChangeSource(change);\n as changeSource\n ) {\n <span>\n {{ t(\"drawer.change-source\") }}:\n {{ changeSource }}\n </span>\n }\n\n <span>\n {{ t(\"drawer.value-kind\") }}:\n {{ change.valueKind }}\n </span>\n </div>\n </div>\n </div>\n\n <div\n class=\"grid gap-3 lg:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)]\"\n >\n <div\n class=\"rounded-xl border border-surface-200 bg-surface-50 p-4\"\n >\n <p\n class=\"mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-surface-500\"\n >\n {{ t(\"drawer.from\") }}\n </p>\n\n @if (isEmptyValue(change.oldValue)) {\n <span class=\"text-sm text-surface-400\">\n {{ t(\"drawer.empty-value\") }}\n </span>\n } @else if (\n isJsonValue(\n change.valueKind,\n change.oldValue\n )\n ) {\n <pre class=\"audit-json-value\">{{\n formatValue(\n change.oldValue,\n change.valueKind\n )\n }}</pre>\n } @else {\n <span class=\"text-sm text-surface-700\">\n {{\n formatValue(\n change.oldValue,\n change.valueKind\n )\n }}\n </span>\n }\n </div>\n\n <div\n class=\"flex items-center justify-center text-xl text-surface-300\"\n >\n -&gt;\n </div>\n\n <div\n class=\"rounded-xl border border-surface-200 bg-surface-0 p-4\"\n >\n <p\n class=\"mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-surface-500\"\n >\n {{ t(\"drawer.to\") }}\n </p>\n\n @if (isEmptyValue(change.newValue)) {\n <span class=\"text-sm text-surface-500\">\n {{ t(\"drawer.empty-value\") }}\n </span>\n } @else if (\n isJsonValue(\n change.valueKind,\n change.newValue\n )\n ) {\n <pre class=\"audit-json-value\">{{\n formatValue(\n change.newValue,\n change.valueKind\n )\n }}</pre>\n } @else {\n <span class=\"text-sm text-surface-700\">\n {{\n formatValue(\n change.newValue,\n change.valueKind\n )\n }}\n </span>\n }\n </div>\n </div>\n </div>\n }\n </div>\n </div>\n } @else {\n <div class=\"flex flex-col gap-4\">\n @if (group.label || group.key) {\n <h5 class=\"font-semibold text-surface-900\">\n {{ group.label || humanizeKey(group.key) }}\n </h5>\n }\n\n <div class=\"flex flex-col gap-5\">\n @for (\n change of group.changes;\n track change.fieldKey + \"-\" + $index\n ) {\n <div class=\"flex flex-col gap-3\">\n <div\n class=\"flex flex-wrap items-start justify-between gap-2\"\n >\n <div class=\"flex flex-col gap-1\">\n <span class=\"font-medium text-surface-900\">\n {{ getFieldLabel(change) }}\n </span>\n\n <div\n class=\"flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-surface-500\"\n >\n @if (\n getChangeSource(change);\n as changeSource\n ) {\n <span>\n {{ t(\"drawer.change-source\") }}:\n {{ changeSource }}\n </span>\n }\n\n <span>\n {{ t(\"drawer.value-kind\") }}:\n {{ change.valueKind }}\n </span>\n </div>\n </div>\n </div>\n\n <div\n class=\"grid gap-3 lg:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)]\"\n >\n <div\n class=\"rounded-xl border border-surface-200 bg-surface-50 p-4\"\n >\n <p\n class=\"mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-surface-500\"\n >\n {{ t(\"drawer.from\") }}\n </p>\n\n @if (isEmptyValue(change.oldValue)) {\n <span class=\"text-sm text-surface-400\">\n {{ t(\"drawer.empty-value\") }}\n </span>\n } @else if (\n isJsonValue(\n change.valueKind,\n change.oldValue\n )\n ) {\n <pre class=\"audit-json-value\">{{\n formatValue(\n change.oldValue,\n change.valueKind\n )\n }}</pre>\n } @else {\n <span class=\"text-sm text-surface-700\">\n {{\n formatValue(\n change.oldValue,\n change.valueKind\n )\n }}\n </span>\n }\n </div>\n\n <div\n class=\"flex items-center justify-center text-xl text-surface-300\"\n >\n -&gt;\n </div>\n\n <div\n class=\"rounded-xl border border-surface-200 bg-surface-0 p-4\"\n >\n <p\n class=\"mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-surface-500\"\n >\n {{ t(\"drawer.to\") }}\n </p>\n\n @if (isEmptyValue(change.newValue)) {\n <span class=\"text-sm text-surface-500\">\n {{ t(\"drawer.empty-value\") }}\n </span>\n } @else if (\n isJsonValue(\n change.valueKind,\n change.newValue\n )\n ) {\n <pre class=\"audit-json-value\">{{\n formatValue(\n change.newValue,\n change.valueKind\n )\n }}</pre>\n } @else {\n <span class=\"text-sm text-surface-700\">\n {{\n formatValue(\n change.newValue,\n change.valueKind\n )\n }}\n </span>\n }\n </div>\n </div>\n </div>\n }\n </div>\n </div>\n }\n }\n </div>\n } @else {\n <div class=\"text-sm text-surface-500\">\n {{ t(\"drawer.no-changes\") }}\n </div>\n }\n </mt-card>\n </div>\n\n <div class=\"flex min-w-0 flex-col gap-5\">\n <mt-card [title]=\"t('drawer.request-context')\">\n <dl class=\"grid gap-4\">\n <div class=\"grid gap-1\">\n <dt class=\"text-xs font-semibold uppercase text-surface-500\">\n {{ t(\"drawer.actor\") }}\n </dt>\n <dd class=\"text-sm text-surface-800\">\n @if (\n getActorUserEntity(event.requestContext.actorUser);\n as actorUserEntity\n ) {\n <mt-entity-user [data]=\"actorUserEntity\" />\n } @else {\n <span class=\"break-all\">\n {{ event.requestContext.actorUserId || \"system\" }}\n </span>\n }\n </dd>\n </div>\n\n <div class=\"grid gap-1\">\n <dt class=\"text-xs font-semibold uppercase text-surface-500\">\n {{ t(\"drawer.occurred-at\") }}\n </dt>\n <dd class=\"text-sm text-surface-800\">\n {{ event.requestContext.occurredAtUtc | date: \"medium\" }}\n </dd>\n </div>\n\n <div class=\"grid gap-1\">\n <dt class=\"text-xs font-semibold uppercase text-surface-500\">\n {{ t(\"category\") }}\n </dt>\n <dd>\n @if (getStatusEntity('category', event.category); as status) {\n <mt-entity-status [data]=\"status\" />\n }\n </dd>\n </div>\n\n <div class=\"grid gap-1\">\n <dt class=\"text-xs font-semibold uppercase text-surface-500\">\n {{ t(\"operation\") }}\n </dt>\n <dd>\n @if (\n getStatusEntity('operation', event.operation);\n as status\n ) {\n <mt-entity-status [data]=\"status\" />\n }\n </dd>\n </div>\n\n <div class=\"grid gap-1\">\n <dt class=\"text-xs font-semibold uppercase text-surface-500\">\n {{ t(\"importance\") }}\n </dt>\n <dd>\n @if (\n getStatusEntity('importance', event.importance);\n as status\n ) {\n <mt-entity-status [data]=\"status\" />\n }\n </dd>\n </div>\n\n <div class=\"grid gap-1\">\n <dt class=\"text-xs font-semibold uppercase text-surface-500\">\n {{ t(\"surface\") }}\n </dt>\n <dd>\n @if (getStatusEntity('surface', event.surface); as status) {\n <mt-entity-status [data]=\"status\" />\n }\n </dd>\n </div>\n </dl>\n </mt-card>\n\n <mt-card [title]=\"t('drawer.subjects')\">\n @if (event.subjects.length > 0) {\n <div\n class=\"overflow-hidden rounded-xl border border-surface-200\"\n >\n <div class=\"divide-y divide-surface-100\">\n @for (\n subject of event.subjects;\n track subject.subjectType + \"-\" + subject.subjectId\n ) {\n <div class=\"flex flex-col gap-3 px-4 py-4\">\n <div\n class=\"flex flex-wrap items-start justify-between gap-3\"\n >\n <div class=\"flex flex-col gap-1\">\n <span class=\"font-medium text-surface-900\">\n {{ getSubjectLabel(subject) }}\n </span>\n <span class=\"text-xs text-surface-500\">\n {{ humanizeKey(subject.subjectType) }}\n </span>\n </div>\n\n <span\n class=\"rounded-full bg-surface-100 px-3 py-1 text-xs font-medium text-surface-700\"\n >\n {{ humanizeKey(subject.relation) }}\n </span>\n </div>\n\n <div\n class=\"flex flex-wrap gap-2 text-xs text-surface-500\"\n >\n <span class=\"rounded-full bg-surface-100 px-3 py-1\">\n {{ t(\"drawer.scope\") }}: {{ subject.subjectId }}\n </span>\n @if (\n subject.levelId !== null &&\n subject.levelId !== undefined\n ) {\n <span class=\"rounded-full bg-surface-100 px-3 py-1\">\n {{ t(\"drawer.level-id\") }}:\n {{ subject.levelId }}\n </span>\n }\n @if (\n subject.levelDataId !== null &&\n subject.levelDataId !== undefined\n ) {\n <span class=\"rounded-full bg-surface-100 px-3 py-1\">\n {{ t(\"drawer.level-data-id\") }}:\n {{ subject.levelDataId }}\n </span>\n }\n @if (\n subject.moduleDataId !== null &&\n subject.moduleDataId !== undefined\n ) {\n <span class=\"rounded-full bg-surface-100 px-3 py-1\">\n {{ t(\"drawer.module-data-id\") }}:\n {{ subject.moduleDataId }}\n </span>\n }\n @if (subject.moduleKey) {\n <span class=\"rounded-full bg-surface-100 px-3 py-1\">\n {{ t(\"drawer.module-key\") }}:\n {{ subject.moduleKey }}\n </span>\n }\n </div>\n </div>\n }\n </div>\n </div>\n } @else {\n <div class=\"text-sm text-surface-500\">\n {{ t(\"drawer.no-subjects\") }}\n </div>\n }\n </mt-card>\n </div>\n </div>\n\n <mt-card [title]=\"t('drawer.evidence')\">\n <p class=\"mb-4 text-sm text-surface-500\">\n {{ t(\"drawer.advanced-description\") }}\n </p>\n\n @if (event.evidence.length > 0) {\n <div class=\"overflow-hidden rounded-xl border border-surface-200\">\n <div class=\"divide-y divide-surface-100\">\n @for (\n evidence of event.evidence;\n track evidence.type + \"-\" + $index\n ) {\n <section>\n <div\n class=\"flex flex-wrap items-center justify-between gap-3 px-4 py-3\"\n >\n <span class=\"font-medium text-surface-900\">\n {{ evidence.type || t(\"drawer.technical-evidence\") }}\n </span>\n\n <mt-button\n [label]=\"t('drawer.view-json')\"\n severity=\"secondary\"\n variant=\"text\"\n size=\"small\"\n (onClick)=\"toggleEvidence($index)\"\n />\n </div>\n\n @if (isEvidenceExpanded($index)) {\n <pre class=\"audit-json-panel\">{{\n formatJson(evidence.contentJson)\n }}</pre>\n }\n </section>\n }\n </div>\n </div>\n } @else {\n <div class=\"text-sm text-surface-500\">\n {{ t(\"drawer.no-evidence\") }}\n </div>\n }\n </mt-card>\n </div>\n </div>\n } @else {\n <div [class]=\"modal.contentClass + ' h-full overflow-y-auto p-5'\">\n <mt-card class=\"min-h-[28rem]\">\n <div\n class=\"flex min-h-[28rem] items-center justify-center p-6 text-center\"\n >\n <p class=\"max-w-2xl text-sm text-surface-500\">\n {{ t(\"drawer.no-event-selected\") }}\n </p>\n </div>\n </mt-card>\n </div>\n }\n </ng-container>\n</mt-drawer>\n", styles: [":host{display:block}.audit-json-panel,.audit-json-value{margin:0;overflow-x:auto;white-space:pre-wrap;word-break:break-word;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.audit-json-panel{border-top:1px solid var(--p-surface-100);background:color-mix(in srgb,var(--p-surface-50) 80%,white);padding:1rem;font-size:.8rem;line-height:1.55}.audit-json-value{font-size:.78rem;line-height:1.55;color:var(--p-surface-700)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Drawer, selector: "mt-drawer", inputs: ["visible", "position", "fullScreen", "closeOnEscape", "blockScroll", "dismissible", "title", "subtitle", "loadingHeader", "styleClass", "transitionOptions", "appendTo", "modal"], outputs: ["visibleChange", "onShow", "onHide"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: Card, selector: "mt-card", inputs: ["class", "title", "paddingless"] }, { kind: "component", type: EntityStatus, selector: "mt-entity-status", inputs: ["data", "name", "value"] }, { kind: "component", type: EntityUser, selector: "mt-entity-user", inputs: ["data"] }, { kind: "ngmodule", type: SkeletonModule }, { kind: "component", type: i1.Skeleton, selector: "p-skeleton", inputs: ["styleClass", "shape", "animation", "borderRadius", "size", "width", "height"] }, { kind: "pipe", type: i2.DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
481
+ }
482
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: AuditLogDetailDrawer, decorators: [{
483
+ type: Component,
484
+ args: [{ selector: 'mt-audit-log-detail-drawer', imports: [
485
+ CommonModule,
486
+ TranslocoDirective,
487
+ Drawer,
488
+ Button,
489
+ Card,
490
+ EntityStatus,
491
+ EntityUser,
492
+ SkeletonModule,
493
+ ], changeDetection: ChangeDetectionStrategy.OnPush, host: {
494
+ class: 'block h-full',
495
+ }, template: "<mt-drawer\n [visible]=\"visible()\"\n [title]=\"drawerTitle()\"\n [loadingHeader]=\"loading()\"\n styleClass=\"mt-audit-log-drawer !absolute !w-[90%] xl:!w-[68rem]\"\n appendTo=\"page-content\"\n position=\"right\"\n (visibleChange)=\"onDrawerVisibleChange($event)\"\n *transloco=\"let t; prefix: 'audit-logs'\"\n>\n <ng-container content>\n @if (loading()) {\n <div [class]=\"modal.contentClass + ' h-full overflow-y-auto p-5'\">\n <div class=\"flex flex-col gap-5\">\n <div class=\"flex flex-wrap gap-3\">\n @for (_ of [1, 2, 3, 4]; track $index) {\n <p-skeleton width=\"7rem\" height=\"2rem\" borderRadius=\"0.5rem\" />\n }\n </div>\n\n <div class=\"grid gap-5 xl:grid-cols-[minmax(0,1.35fr)_minmax(18rem,0.85fr)]\">\n <div class=\"flex flex-col gap-5\">\n <mt-card [title]=\"t('drawer.change-groups')\">\n <div class=\"flex flex-col gap-4\">\n <p-skeleton height=\"12rem\" borderRadius=\"1rem\" />\n <p-skeleton height=\"14rem\" borderRadius=\"1rem\" />\n </div>\n </mt-card>\n\n <mt-card [title]=\"t('drawer.evidence')\">\n <div class=\"flex flex-col gap-4\">\n <p-skeleton height=\"9rem\" borderRadius=\"1rem\" />\n </div>\n </mt-card>\n </div>\n\n <div class=\"flex flex-col gap-5\">\n <mt-card [title]=\"t('drawer.request-context')\">\n <div class=\"flex flex-col gap-4\">\n <p-skeleton height=\"1rem\" borderRadius=\"0.5rem\" />\n <p-skeleton height=\"1rem\" borderRadius=\"0.5rem\" />\n <p-skeleton height=\"1rem\" borderRadius=\"0.5rem\" />\n <p-skeleton height=\"1rem\" borderRadius=\"0.5rem\" />\n <p-skeleton height=\"3rem\" borderRadius=\"0.75rem\" />\n </div>\n </mt-card>\n\n <mt-card [title]=\"t('drawer.subjects')\">\n <div class=\"flex flex-col gap-4\">\n <p-skeleton height=\"8rem\" borderRadius=\"1rem\" />\n </div>\n </mt-card>\n </div>\n </div>\n </div>\n </div>\n } @else if (error(); as errorMessage) {\n <div [class]=\"modal.contentClass + ' h-full overflow-y-auto p-5'\">\n <mt-card class=\"min-h-[28rem]\">\n <div\n class=\"flex min-h-[28rem] items-center justify-center p-6 text-center\"\n >\n <p class=\"max-w-2xl text-sm font-medium text-rose-700\">\n {{ errorMessage }}\n </p>\n </div>\n </mt-card>\n </div>\n } @else if (detail(); as event) {\n <div [class]=\"modal.contentClass + ' h-full overflow-y-auto p-5'\">\n <div class=\"flex flex-col gap-5\">\n <div\n class=\"grid gap-5 xl:grid-cols-[minmax(0,1.35fr)_minmax(18rem,0.85fr)]\"\n >\n <div class=\"flex min-w-0 flex-col gap-5\">\n <mt-card [title]=\"t('drawer.change-groups')\">\n @if (event.changeGroups.length > 0) {\n @let hasMultipleChangeGroups = event.changeGroups.length > 1;\n <div class=\"flex flex-col gap-5\">\n @for (group of event.changeGroups; track group.key) {\n @if (hasMultipleChangeGroups) {\n <div\n class=\"overflow-hidden rounded-xl border border-surface-200\"\n >\n <div class=\"border-b border-surface-200 px-4 py-3\">\n <h5 class=\"font-semibold text-surface-900\">\n {{ group.label || humanizeKey(group.key) }}\n </h5>\n </div>\n\n <div class=\"divide-y divide-surface-100\">\n @for (\n change of group.changes;\n track change.fieldKey + \"-\" + $index\n ) {\n <div class=\"flex flex-col gap-3 px-4 py-4\">\n <div\n class=\"flex flex-wrap items-start justify-between gap-2\"\n >\n <div class=\"flex flex-col gap-1\">\n <span class=\"font-medium text-surface-900\">\n {{ getFieldLabel(change) }}\n </span>\n\n <div\n class=\"flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-surface-500\"\n >\n @if (\n getChangeSource(change);\n as changeSource\n ) {\n <span>\n {{ t(\"drawer.change-source\") }}:\n {{ changeSource }}\n </span>\n }\n\n <span>\n {{ t(\"drawer.value-kind\") }}:\n {{ change.valueKind }}\n </span>\n </div>\n </div>\n </div>\n\n <div\n class=\"grid gap-3 lg:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)]\"\n >\n <div\n class=\"rounded-xl border border-surface-200 bg-surface-50 p-4\"\n >\n <p\n class=\"mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-surface-500\"\n >\n {{ t(\"drawer.from\") }}\n </p>\n\n @if (isEmptyValue(change.oldValue)) {\n <span class=\"text-sm text-surface-400\">\n {{ t(\"drawer.empty-value\") }}\n </span>\n } @else if (\n isJsonValue(\n change.valueKind,\n change.oldValue\n )\n ) {\n <pre class=\"audit-json-value\">{{\n formatValue(\n change.oldValue,\n change.valueKind\n )\n }}</pre>\n } @else {\n <span class=\"text-sm text-surface-700\">\n {{\n formatValue(\n change.oldValue,\n change.valueKind\n )\n }}\n </span>\n }\n </div>\n\n <div\n class=\"flex items-center justify-center text-xl text-surface-300\"\n >\n -&gt;\n </div>\n\n <div\n class=\"rounded-xl border border-surface-200 bg-surface-0 p-4\"\n >\n <p\n class=\"mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-surface-500\"\n >\n {{ t(\"drawer.to\") }}\n </p>\n\n @if (isEmptyValue(change.newValue)) {\n <span class=\"text-sm text-surface-500\">\n {{ t(\"drawer.empty-value\") }}\n </span>\n } @else if (\n isJsonValue(\n change.valueKind,\n change.newValue\n )\n ) {\n <pre class=\"audit-json-value\">{{\n formatValue(\n change.newValue,\n change.valueKind\n )\n }}</pre>\n } @else {\n <span class=\"text-sm text-surface-700\">\n {{\n formatValue(\n change.newValue,\n change.valueKind\n )\n }}\n </span>\n }\n </div>\n </div>\n </div>\n }\n </div>\n </div>\n } @else {\n <div class=\"flex flex-col gap-4\">\n @if (group.label || group.key) {\n <h5 class=\"font-semibold text-surface-900\">\n {{ group.label || humanizeKey(group.key) }}\n </h5>\n }\n\n <div class=\"flex flex-col gap-5\">\n @for (\n change of group.changes;\n track change.fieldKey + \"-\" + $index\n ) {\n <div class=\"flex flex-col gap-3\">\n <div\n class=\"flex flex-wrap items-start justify-between gap-2\"\n >\n <div class=\"flex flex-col gap-1\">\n <span class=\"font-medium text-surface-900\">\n {{ getFieldLabel(change) }}\n </span>\n\n <div\n class=\"flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-surface-500\"\n >\n @if (\n getChangeSource(change);\n as changeSource\n ) {\n <span>\n {{ t(\"drawer.change-source\") }}:\n {{ changeSource }}\n </span>\n }\n\n <span>\n {{ t(\"drawer.value-kind\") }}:\n {{ change.valueKind }}\n </span>\n </div>\n </div>\n </div>\n\n <div\n class=\"grid gap-3 lg:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)]\"\n >\n <div\n class=\"rounded-xl border border-surface-200 bg-surface-50 p-4\"\n >\n <p\n class=\"mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-surface-500\"\n >\n {{ t(\"drawer.from\") }}\n </p>\n\n @if (isEmptyValue(change.oldValue)) {\n <span class=\"text-sm text-surface-400\">\n {{ t(\"drawer.empty-value\") }}\n </span>\n } @else if (\n isJsonValue(\n change.valueKind,\n change.oldValue\n )\n ) {\n <pre class=\"audit-json-value\">{{\n formatValue(\n change.oldValue,\n change.valueKind\n )\n }}</pre>\n } @else {\n <span class=\"text-sm text-surface-700\">\n {{\n formatValue(\n change.oldValue,\n change.valueKind\n )\n }}\n </span>\n }\n </div>\n\n <div\n class=\"flex items-center justify-center text-xl text-surface-300\"\n >\n -&gt;\n </div>\n\n <div\n class=\"rounded-xl border border-surface-200 bg-surface-0 p-4\"\n >\n <p\n class=\"mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-surface-500\"\n >\n {{ t(\"drawer.to\") }}\n </p>\n\n @if (isEmptyValue(change.newValue)) {\n <span class=\"text-sm text-surface-500\">\n {{ t(\"drawer.empty-value\") }}\n </span>\n } @else if (\n isJsonValue(\n change.valueKind,\n change.newValue\n )\n ) {\n <pre class=\"audit-json-value\">{{\n formatValue(\n change.newValue,\n change.valueKind\n )\n }}</pre>\n } @else {\n <span class=\"text-sm text-surface-700\">\n {{\n formatValue(\n change.newValue,\n change.valueKind\n )\n }}\n </span>\n }\n </div>\n </div>\n </div>\n }\n </div>\n </div>\n }\n }\n </div>\n } @else {\n <div class=\"text-sm text-surface-500\">\n {{ t(\"drawer.no-changes\") }}\n </div>\n }\n </mt-card>\n </div>\n\n <div class=\"flex min-w-0 flex-col gap-5\">\n <mt-card [title]=\"t('drawer.request-context')\">\n <dl class=\"grid gap-4\">\n <div class=\"grid gap-1\">\n <dt class=\"text-xs font-semibold uppercase text-surface-500\">\n {{ t(\"drawer.actor\") }}\n </dt>\n <dd class=\"text-sm text-surface-800\">\n @if (\n getActorUserEntity(event.requestContext.actorUser);\n as actorUserEntity\n ) {\n <mt-entity-user [data]=\"actorUserEntity\" />\n } @else {\n <span class=\"break-all\">\n {{ event.requestContext.actorUserId || \"system\" }}\n </span>\n }\n </dd>\n </div>\n\n <div class=\"grid gap-1\">\n <dt class=\"text-xs font-semibold uppercase text-surface-500\">\n {{ t(\"drawer.occurred-at\") }}\n </dt>\n <dd class=\"text-sm text-surface-800\">\n {{ event.requestContext.occurredAtUtc | date: \"medium\" }}\n </dd>\n </div>\n\n <div class=\"grid gap-1\">\n <dt class=\"text-xs font-semibold uppercase text-surface-500\">\n {{ t(\"category\") }}\n </dt>\n <dd>\n @if (getStatusEntity('category', event.category); as status) {\n <mt-entity-status [data]=\"status\" />\n }\n </dd>\n </div>\n\n <div class=\"grid gap-1\">\n <dt class=\"text-xs font-semibold uppercase text-surface-500\">\n {{ t(\"operation\") }}\n </dt>\n <dd>\n @if (\n getStatusEntity('operation', event.operation);\n as status\n ) {\n <mt-entity-status [data]=\"status\" />\n }\n </dd>\n </div>\n\n <div class=\"grid gap-1\">\n <dt class=\"text-xs font-semibold uppercase text-surface-500\">\n {{ t(\"importance\") }}\n </dt>\n <dd>\n @if (\n getStatusEntity('importance', event.importance);\n as status\n ) {\n <mt-entity-status [data]=\"status\" />\n }\n </dd>\n </div>\n\n <div class=\"grid gap-1\">\n <dt class=\"text-xs font-semibold uppercase text-surface-500\">\n {{ t(\"surface\") }}\n </dt>\n <dd>\n @if (getStatusEntity('surface', event.surface); as status) {\n <mt-entity-status [data]=\"status\" />\n }\n </dd>\n </div>\n </dl>\n </mt-card>\n\n <mt-card [title]=\"t('drawer.subjects')\">\n @if (event.subjects.length > 0) {\n <div\n class=\"overflow-hidden rounded-xl border border-surface-200\"\n >\n <div class=\"divide-y divide-surface-100\">\n @for (\n subject of event.subjects;\n track subject.subjectType + \"-\" + subject.subjectId\n ) {\n <div class=\"flex flex-col gap-3 px-4 py-4\">\n <div\n class=\"flex flex-wrap items-start justify-between gap-3\"\n >\n <div class=\"flex flex-col gap-1\">\n <span class=\"font-medium text-surface-900\">\n {{ getSubjectLabel(subject) }}\n </span>\n <span class=\"text-xs text-surface-500\">\n {{ humanizeKey(subject.subjectType) }}\n </span>\n </div>\n\n <span\n class=\"rounded-full bg-surface-100 px-3 py-1 text-xs font-medium text-surface-700\"\n >\n {{ humanizeKey(subject.relation) }}\n </span>\n </div>\n\n <div\n class=\"flex flex-wrap gap-2 text-xs text-surface-500\"\n >\n <span class=\"rounded-full bg-surface-100 px-3 py-1\">\n {{ t(\"drawer.scope\") }}: {{ subject.subjectId }}\n </span>\n @if (\n subject.levelId !== null &&\n subject.levelId !== undefined\n ) {\n <span class=\"rounded-full bg-surface-100 px-3 py-1\">\n {{ t(\"drawer.level-id\") }}:\n {{ subject.levelId }}\n </span>\n }\n @if (\n subject.levelDataId !== null &&\n subject.levelDataId !== undefined\n ) {\n <span class=\"rounded-full bg-surface-100 px-3 py-1\">\n {{ t(\"drawer.level-data-id\") }}:\n {{ subject.levelDataId }}\n </span>\n }\n @if (\n subject.moduleDataId !== null &&\n subject.moduleDataId !== undefined\n ) {\n <span class=\"rounded-full bg-surface-100 px-3 py-1\">\n {{ t(\"drawer.module-data-id\") }}:\n {{ subject.moduleDataId }}\n </span>\n }\n @if (subject.moduleKey) {\n <span class=\"rounded-full bg-surface-100 px-3 py-1\">\n {{ t(\"drawer.module-key\") }}:\n {{ subject.moduleKey }}\n </span>\n }\n </div>\n </div>\n }\n </div>\n </div>\n } @else {\n <div class=\"text-sm text-surface-500\">\n {{ t(\"drawer.no-subjects\") }}\n </div>\n }\n </mt-card>\n </div>\n </div>\n\n <mt-card [title]=\"t('drawer.evidence')\">\n <p class=\"mb-4 text-sm text-surface-500\">\n {{ t(\"drawer.advanced-description\") }}\n </p>\n\n @if (event.evidence.length > 0) {\n <div class=\"overflow-hidden rounded-xl border border-surface-200\">\n <div class=\"divide-y divide-surface-100\">\n @for (\n evidence of event.evidence;\n track evidence.type + \"-\" + $index\n ) {\n <section>\n <div\n class=\"flex flex-wrap items-center justify-between gap-3 px-4 py-3\"\n >\n <span class=\"font-medium text-surface-900\">\n {{ evidence.type || t(\"drawer.technical-evidence\") }}\n </span>\n\n <mt-button\n [label]=\"t('drawer.view-json')\"\n severity=\"secondary\"\n variant=\"text\"\n size=\"small\"\n (onClick)=\"toggleEvidence($index)\"\n />\n </div>\n\n @if (isEvidenceExpanded($index)) {\n <pre class=\"audit-json-panel\">{{\n formatJson(evidence.contentJson)\n }}</pre>\n }\n </section>\n }\n </div>\n </div>\n } @else {\n <div class=\"text-sm text-surface-500\">\n {{ t(\"drawer.no-evidence\") }}\n </div>\n }\n </mt-card>\n </div>\n </div>\n } @else {\n <div [class]=\"modal.contentClass + ' h-full overflow-y-auto p-5'\">\n <mt-card class=\"min-h-[28rem]\">\n <div\n class=\"flex min-h-[28rem] items-center justify-center p-6 text-center\"\n >\n <p class=\"max-w-2xl text-sm text-surface-500\">\n {{ t(\"drawer.no-event-selected\") }}\n </p>\n </div>\n </mt-card>\n </div>\n }\n </ng-container>\n</mt-drawer>\n", styles: [":host{display:block}.audit-json-panel,.audit-json-value{margin:0;overflow-x:auto;white-space:pre-wrap;word-break:break-word;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.audit-json-panel{border-top:1px solid var(--p-surface-100);background:color-mix(in srgb,var(--p-surface-50) 80%,white);padding:1rem;font-size:.8rem;line-height:1.55}.audit-json-value{font-size:.78rem;line-height:1.55;color:var(--p-surface-700)}\n"] }]
496
+ }], ctorParameters: () => [], propDecorators: { visible: [{ type: i0.Input, args: [{ isSignal: true, alias: "visible", required: false }] }], detail: [{ type: i0.Input, args: [{ isSignal: true, alias: "detail", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], visibleChange: [{ type: i0.Output, args: ["visibleChange"] }] } });
497
+
498
+ const CATEGORY_OPTIONS = [
499
+ 'Runtime',
500
+ 'ControlPanel',
501
+ 'Discussion',
502
+ 'Process',
503
+ 'Escalation',
504
+ 'PhaseGate',
505
+ 'Governance',
506
+ 'System',
507
+ ].map((value) => ({
508
+ label: value,
509
+ value,
510
+ }));
511
+ const OPERATION_OPTIONS = [
512
+ 'Create',
513
+ 'Update',
514
+ 'Delete',
515
+ 'Close',
516
+ 'Move',
517
+ 'Progress',
518
+ 'Configure',
519
+ 'Comment',
520
+ 'BulkUpdate',
521
+ 'Change',
522
+ 'Unknown',
523
+ ].map((value) => ({
524
+ label: value,
525
+ value,
526
+ }));
527
+ const IMPORTANCE_OPTIONS = [
528
+ 'Low',
529
+ 'Medium',
530
+ 'High',
531
+ 'Critical',
532
+ ].map((value) => ({
533
+ label: value,
534
+ value,
535
+ }));
213
536
  class AuditLogsList {
214
- userCol = viewChild.required('userCol');
537
+ summaryCol = viewChild.required('summaryCol');
538
+ actorCol = viewChild.required('actorCol');
215
539
  facade = inject(AuditLogsFacade);
216
540
  translocoService = inject(TranslocoService);
217
- context = new HttpContext().set(REQUEST_CONTEXT, {
218
- useBaseUrl: true,
541
+ activeLang = toSignal(this.translocoService.langChanges$, {
542
+ initialValue: this.translocoService.getActiveLang(),
219
543
  });
220
- auditLogs = computed(() => this.facade.auditLogs()?.items, ...(ngDevMode ? [{ debugName: "auditLogs" }] : []));
221
- fullDataAuditLogs = computed(() => this.facade.auditLogs(), ...(ngDevMode ? [{ debugName: "fullDataAuditLogs" }] : []));
222
- tableColumns = linkedSignal(() => [
223
- {
224
- key: 'name',
225
- label: this.translocoService.translate('audit-logs.title'),
226
- },
227
- {
228
- key: 'module',
229
- label: this.translocoService.translate('audit-logs.module'),
230
- },
231
- {
232
- key: 'originalUserId.userName',
233
- label: this.translocoService.translate('audit-logs.user'),
234
- type: 'custom',
235
- customCellTpl: this.userCol(),
236
- filterConfig: {
544
+ auditLogs = this.facade.auditLogs;
545
+ appliedFilters = this.facade.filters;
546
+ loading = this.facade.isLoadingAuditLogs;
547
+ error = this.facade.getAuditLogsError;
548
+ detail = this.facade.selectedEventDetail;
549
+ detailError = this.facade.getAuditLogDetailError;
550
+ loadingDetail = this.facade.isLoadingAuditLogDetail;
551
+ drawerVisible = signal(false, ...(ngDevMode ? [{ debugName: "drawerVisible" }] : []));
552
+ tableFilters = signal({}, ...(ngDevMode ? [{ debugName: "tableFilters" }] : []));
553
+ summaryCards = computed(() => {
554
+ this.activeLang();
555
+ const response = this.auditLogs();
556
+ const items = response.items ?? [];
557
+ const elevatedCount = items.filter((item) => ['High', 'Critical'].includes(item.importance?.code ?? '')).length;
558
+ const uniqueActors = new Set(items
559
+ .map((item) => item.actorUserId?.trim())
560
+ .filter((value) => !!value)).size;
561
+ return [
562
+ {
563
+ title: String(response.totalCount ?? 0),
564
+ subTitle: this.translate('total-events'),
565
+ icon: 'file.file-05',
566
+ color: 'blue',
567
+ },
568
+ {
569
+ title: String(items.length),
570
+ subTitle: this.translate('visible-results'),
571
+ icon: 'general.building-02',
572
+ color: 'emerald',
573
+ },
574
+ {
575
+ title: String(elevatedCount),
576
+ subTitle: this.translate('high-impact'),
577
+ icon: 'alert.bell-02',
578
+ color: 'amber',
579
+ },
580
+ {
581
+ title: String(uniqueActors),
582
+ subTitle: this.translate('unique-actors'),
583
+ icon: 'custom.user-pp',
584
+ color: 'violet',
585
+ },
586
+ ];
587
+ }, ...(ngDevMode ? [{ debugName: "summaryCards" }] : []));
588
+ activeFilterBadges = computed(() => {
589
+ this.activeLang();
590
+ const filters = this.appliedFilters();
591
+ const badges = [];
592
+ this.pushFilterBadge(badges, this.translate('search'), filters.search);
593
+ this.pushFilterBadge(badges, this.translate('category'), filters.category);
594
+ this.pushFilterBadge(badges, this.translate('operation'), filters.operation);
595
+ this.pushFilterBadge(badges, this.translate('importance'), filters.importance);
596
+ this.pushFilterBadge(badges, this.translate('surface'), filters.surface);
597
+ this.pushFilterBadge(badges, this.translate('actor'), filters.actorUserId);
598
+ if (filters.fromUtc) {
599
+ badges.push(`${this.translate('from')}: ${this.formatFilterDate(filters.fromUtc)}`);
600
+ }
601
+ if (filters.toUtc) {
602
+ badges.push(`${this.translate('to')}: ${this.formatFilterDate(filters.toUtc)}`);
603
+ }
604
+ return badges;
605
+ }, ...(ngDevMode ? [{ debugName: "activeFilterBadges" }] : []));
606
+ tableColumns = linkedSignal(() => {
607
+ this.activeLang();
608
+ return [
609
+ {
610
+ key: 'summary',
611
+ label: this.translate('summary'),
612
+ type: 'custom',
613
+ customCellTpl: this.summaryCol(),
614
+ width: '27rem',
615
+ },
616
+ {
617
+ key: 'category.code',
618
+ label: this.translate('category'),
619
+ type: 'status',
620
+ statusMap: getAuditStatusMap('category'),
621
+ filterConfig: {
622
+ type: 'select',
623
+ label: this.translate('category'),
624
+ options: CATEGORY_OPTIONS,
625
+ },
626
+ width: '10rem',
627
+ },
628
+ {
629
+ key: 'operation.code',
630
+ label: this.translate('operation'),
631
+ type: 'status',
632
+ statusMap: getAuditStatusMap('operation'),
633
+ filterConfig: {
634
+ type: 'select',
635
+ label: this.translate('operation'),
636
+ options: OPERATION_OPTIONS,
637
+ },
638
+ width: '10rem',
639
+ },
640
+ {
641
+ key: 'importance.code',
642
+ label: this.translate('importance'),
643
+ type: 'status',
644
+ statusMap: getAuditStatusMap('importance'),
645
+ filterConfig: {
646
+ type: 'select',
647
+ label: this.translate('importance'),
648
+ options: IMPORTANCE_OPTIONS,
649
+ },
650
+ width: '10rem',
651
+ },
652
+ {
653
+ key: 'actorUser',
654
+ label: this.translate('actor'),
237
655
  type: 'user',
238
- label: 'User',
239
- context: this.context,
240
- apiUrl: 'Identity/users',
656
+ filterConfig: {
657
+ type: 'user',
658
+ label: this.translate('actor'),
659
+ },
660
+ width: '14rem',
241
661
  },
242
- },
243
- {
244
- key: 'description',
245
- label: this.translocoService.translate('audit-logs.activity'),
246
- },
247
- {
248
- key: 'userIpAddress',
249
- label: this.translocoService.translate('audit-logs.ip-address'),
250
- },
251
- {
252
- key: 'date.displayValue',
253
- label: this.translocoService.translate('audit-logs.date'),
254
- filterConfig: { type: 'date', label: 'Start Date' },
255
- },
256
- ], ...(ngDevMode ? [{ debugName: "tableColumns" }] : []));
257
- loading = this.facade.isLoadingAuditLogs;
662
+ {
663
+ key: 'occurredAtUtc',
664
+ label: this.translate('occurred-at'),
665
+ type: 'date',
666
+ filterConfig: {
667
+ type: 'date',
668
+ label: this.translate('occurred-at'),
669
+ },
670
+ width: '12rem',
671
+ },
672
+ ];
673
+ }, ...(ngDevMode ? [{ debugName: "tableColumns" }] : []));
258
674
  onLazyLoad(event) {
259
- // Transform filters to API format
260
- const apiFilters = {};
261
- if (event.filters) {
262
- // Handle date filter transformation
263
- if (event.filters['date.displayValue']) {
264
- const dateFilter = event.filters['date.displayValue'];
265
- if (dateFilter.from) {
266
- apiFilters['startDate'] = normalizeDates(dateFilter.from);
267
- }
268
- if (dateFilter.to) {
269
- apiFilters['endDate'] = normalizeDates(dateFilter.to);
270
- }
271
- }
272
- if (event.filters['generalSearch']) {
273
- apiFilters['searchTerm'] = event.filters['generalSearch'];
274
- }
275
- if (event.filters['originalUserId.userName']) {
276
- const userFilter = event.filters['originalUserId.userName'];
277
- apiFilters['userId'] = userFilter?.userId;
278
- }
675
+ const filters = event.filters ?? {};
676
+ const apiFilters = {
677
+ language: this.currentLanguage(),
678
+ };
679
+ const generalSearch = this.readFilterValue(filters['generalSearch']);
680
+ if (generalSearch) {
681
+ apiFilters.search = generalSearch;
682
+ }
683
+ const category = this.readFilterValue(filters['category.code']);
684
+ if (category) {
685
+ apiFilters.category = category;
686
+ }
687
+ const operation = this.readFilterValue(filters['operation.code']);
688
+ if (operation) {
689
+ apiFilters.operation = operation;
690
+ }
691
+ const importance = this.readFilterValue(filters['importance.code']);
692
+ if (importance) {
693
+ apiFilters.importance = importance;
694
+ }
695
+ const surface = this.readFilterValue(filters['surface.code']);
696
+ if (surface) {
697
+ apiFilters.surface = surface;
698
+ }
699
+ const actorUserId = this.readFilterValue(filters['actorUserId']);
700
+ if (actorUserId) {
701
+ apiFilters.actorUserId = actorUserId;
702
+ }
703
+ const occurredAtRange = filters['occurredAtUtc'];
704
+ if (occurredAtRange?.from) {
705
+ apiFilters.fromUtc = this.normalizeDateBoundary(occurredAtRange.from, 'start');
706
+ }
707
+ if (occurredAtRange?.to) {
708
+ apiFilters.toUtc = this.normalizeDateBoundary(occurredAtRange.to, 'end');
709
+ }
710
+ this.facade.getAuditLogs(event.currentPage ?? 1, event.pageSize ?? this.auditLogs().pageSize ?? 20, apiFilters);
711
+ }
712
+ openEventDetail(row) {
713
+ this.drawerVisible.set(true);
714
+ this.facade.getAuditLogDetail(row.eventId, this.currentLanguage());
715
+ }
716
+ onDrawerVisibleChange(visible) {
717
+ this.drawerVisible.set(visible);
718
+ if (!visible) {
719
+ this.facade.clearAuditLogDetail();
720
+ }
721
+ }
722
+ retry() {
723
+ const response = this.auditLogs();
724
+ this.facade.getAuditLogs(response.page || 1, response.pageSize || 20, {
725
+ ...this.appliedFilters(),
726
+ language: this.currentLanguage(),
727
+ });
728
+ }
729
+ formatSubjectType(row) {
730
+ return row.primarySubjectType || this.translate('subject-type-unknown');
731
+ }
732
+ currentLanguage() {
733
+ return this.activeLang() === 'ar' ? 'ar' : 'en';
734
+ }
735
+ translate(key) {
736
+ this.activeLang();
737
+ return this.translocoService.translate(`audit-logs.${key}`);
738
+ }
739
+ pushFilterBadge(badges, label, value) {
740
+ if (!value) {
741
+ return;
742
+ }
743
+ badges.push(`${label}: ${value}`);
744
+ }
745
+ formatFilterDate(value) {
746
+ const date = new Date(value);
747
+ if (Number.isNaN(date.getTime())) {
748
+ return value;
749
+ }
750
+ return new Intl.DateTimeFormat(this.currentLanguage() === 'ar' ? 'ar' : undefined, {
751
+ dateStyle: 'medium',
752
+ }).format(date);
753
+ }
754
+ readFilterValue(value) {
755
+ if (typeof value === 'string' && value.trim().length > 0) {
756
+ return value.trim();
757
+ }
758
+ if (typeof value === 'number') {
759
+ return String(value);
760
+ }
761
+ if (value &&
762
+ typeof value === 'object' &&
763
+ 'value' in value &&
764
+ typeof value.value === 'string' &&
765
+ value.value.trim().length > 0) {
766
+ return value.value.trim();
767
+ }
768
+ return undefined;
769
+ }
770
+ normalizeDateBoundary(value, boundary) {
771
+ const normalizedDate = new Date(value);
772
+ if (boundary === 'start') {
773
+ normalizedDate.setHours(0, 0, 0, 0);
774
+ }
775
+ else {
776
+ normalizedDate.setHours(23, 59, 59, 999);
279
777
  }
280
- this.facade.getAuditLogs(event.currentPage, event.pageSize, apiFilters);
778
+ return normalizeDates(normalizedDate);
281
779
  }
282
780
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: AuditLogsList, deps: [], target: i0.ɵɵFactoryTarget.Component });
283
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.0.3", type: AuditLogsList, isStandalone: true, selector: "mt-audit-logs-list", viewQueries: [{ propertyName: "userCol", first: true, predicate: ["userCol"], descendants: true, isSignal: true }], ngImport: i0, template: "<ng-container *transloco=\"let t\">\r\n <mt-table\r\n [data]=\"auditLogs()\"\r\n [columns]=\"tableColumns()\"\r\n [loading]=\"loading()\"\r\n [lazy]=\"true\"\r\n [showFilters]=\"true\"\r\n [exportable]=\"true\"\r\n [generalSearch]=\"true\"\r\n [lazyTotalRecords]=\"fullDataAuditLogs().totalCount\"\r\n (lazyLoad)=\"onLazyLoad($event)\"\r\n >\r\n </mt-table>\r\n\r\n <ng-template #userCol let-row>\r\n <div class=\"flex items-center gap-2\">\r\n <mt-avatar [icon]=\"'custom.user-pp'\"> </mt-avatar>\r\n {{ row.originalUserId?.displayName }}\r\n </div>\r\n </ng-template>\r\n</ng-container>\r\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: SkeletonModule }, { kind: "component", type: Table, selector: "mt-table", inputs: ["filters", "data", "columns", "rowActions", "size", "showGridlines", "stripedRows", "selectableRows", "clickableRows", "generalSearch", "showFilters", "loading", "updating", "lazy", "lazyTotalRecords", "reorderableColumns", "reorderableRows", "dataKey", "exportable", "exportFilename", "actionShape", "tabs", "tabsOptionLabel", "tabsOptionValue", "activeTab", "actions", "paginatorPosition", "pageSize", "currentPage", "first", "filterTerm"], outputs: ["selectionChange", "cellChange", "lazyLoad", "columnReorder", "rowReorder", "rowClick", "filtersChange", "activeTabChange", "onTabChange", "pageSizeChange", "currentPageChange", "firstChange", "filterTermChange"] }, { kind: "component", type: Avatar, selector: "mt-avatar", inputs: ["label", "icon", "image", "styleClass", "size", "shape", "badge", "badgeSize", "badgeSeverity"], outputs: ["onImageError"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }] });
781
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.3", type: AuditLogsList, isStandalone: true, selector: "mt-audit-logs-list", host: { classAttribute: "block h-full" }, viewQueries: [{ propertyName: "summaryCol", first: true, predicate: ["summaryCol"], descendants: true, isSignal: true }, { propertyName: "actorCol", first: true, predicate: ["actorCol"], descendants: true, isSignal: true }], ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'audit-logs'\">\n <div class=\"flex flex-col gap-4\">\n @if (activeFilterBadges().length > 0) {\n <div class=\"flex flex-wrap gap-2\">\n @for (badge of activeFilterBadges(); track badge) {\n <mt-chip\n [label]=\"badge\"\n styleClass=\"border border-surface-200 !bg-surface-0 !text-surface-700 text-xs\"\n />\n }\n </div>\n }\n\n <div class=\"grid gap-3 md:grid-cols-2 xl:grid-cols-4\">\n @for (card of summaryCards(); track card.subTitle) {\n <mt-statistic-card [data]=\"card\" cardClass=\"shadow-sm\" />\n }\n </div>\n\n @if (error(); as errorMessage) {\n <section\n class=\"flex flex-col gap-3 rounded-xl border border-rose-200 bg-rose-50 px-5 py-4 lg:flex-row lg:items-center lg:justify-between\"\n >\n <div class=\"space-y-1\">\n <p class=\"text-sm font-semibold text-rose-700\">\n {{ t(\"load-error-title\") }}\n </p>\n <p class=\"text-sm text-rose-600\">\n {{ errorMessage }}\n </p>\n </div>\n\n <mt-button\n severity=\"danger\"\n variant=\"outlined\"\n icon=\"general.refresh-cw-05\"\n [label]=\"t('retry')\"\n (onClick)=\"retry()\"\n />\n </section>\n }\n\n <mt-table\n [(filters)]=\"tableFilters\"\n [data]=\"auditLogs().items\"\n [columns]=\"tableColumns()\"\n [loading]=\"loading()\"\n [lazy]=\"true\"\n [showFilters]=\"true\"\n [generalSearch]=\"true\"\n [clickableRows]=\"true\"\n [lazyTotalRecords]=\"auditLogs().totalCount\"\n dataKey=\"eventId\"\n (lazyLoad)=\"onLazyLoad($event)\"\n (rowClick)=\"openEventDetail($event)\"\n />\n </div>\n\n <ng-template #summaryCol let-row>\n <div class=\"flex flex-col gap-2 py-1\">\n <p class=\"line-clamp-2 text-sm font-semibold text-surface-900\">\n {{ row.summary }}\n </p>\n <div\n class=\"flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-surface-500\"\n >\n <span>{{ t(\"event-id\") }}: {{ row.eventId }}</span>\n @if (row.primarySubjectType) {\n <span>{{ formatSubjectType(row) }}</span>\n }\n </div>\n </div>\n </ng-template>\n\n <ng-template #actorCol let-row>\n <div class=\"flex flex-col gap-1 py-1\">\n <span class=\"break-all text-sm font-medium text-surface-800\">\n {{ row.actorUserId || t(\"actor-unknown\") }}\n </span>\n <span class=\"text-xs text-surface-500\">\n {{ row.moduleKey || row.levelDataId || row.moduleDataId || \"audit\" }}\n </span>\n </div>\n </ng-template>\n\n <mt-audit-log-detail-drawer\n [visible]=\"drawerVisible()\"\n [detail]=\"detail()\"\n [loading]=\"loadingDetail()\"\n [error]=\"detailError()\"\n (visibleChange)=\"onDrawerVisibleChange($event)\"\n />\n</ng-container>\n", styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: Table, selector: "mt-table", inputs: ["filters", "data", "columns", "rowActions", "size", "showGridlines", "stripedRows", "selectableRows", "clickableRows", "generalSearch", "lazyLocalSearch", "showFilters", "loading", "updating", "lazy", "lazyLocalSort", "lazyTotalRecords", "reorderableColumns", "reorderableRows", "dataKey", "exportable", "exportFilename", "actionShape", "tabs", "tabsOptionLabel", "tabsOptionValue", "activeTab", "actions", "paginatorPosition", "pageSize", "currentPage", "first", "filterTerm"], outputs: ["selectionChange", "cellChange", "lazyLoad", "columnReorder", "rowReorder", "rowClick", "filtersChange", "activeTabChange", "onTabChange", "pageSizeChange", "currentPageChange", "firstChange", "filterTermChange"] }, { kind: "component", type: Chip, selector: "mt-chip", inputs: ["label", "icon", "image", "removable", "removeIcon", "styleClass"], outputs: ["onRemove", "onImageError"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: StatisticCard, selector: "mt-statistic-card", inputs: ["data", "cardClass"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: AuditLogDetailDrawer, selector: "mt-audit-log-detail-drawer", inputs: ["visible", "detail", "loading", "error"], outputs: ["visibleChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
284
782
  }
285
783
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: AuditLogsList, decorators: [{
286
784
  type: Component,
287
- args: [{ selector: 'mt-audit-logs-list', standalone: true, imports: [CommonModule, SkeletonModule, Table, Avatar, TranslocoDirective], template: "<ng-container *transloco=\"let t\">\r\n <mt-table\r\n [data]=\"auditLogs()\"\r\n [columns]=\"tableColumns()\"\r\n [loading]=\"loading()\"\r\n [lazy]=\"true\"\r\n [showFilters]=\"true\"\r\n [exportable]=\"true\"\r\n [generalSearch]=\"true\"\r\n [lazyTotalRecords]=\"fullDataAuditLogs().totalCount\"\r\n (lazyLoad)=\"onLazyLoad($event)\"\r\n >\r\n </mt-table>\r\n\r\n <ng-template #userCol let-row>\r\n <div class=\"flex items-center gap-2\">\r\n <mt-avatar [icon]=\"'custom.user-pp'\"> </mt-avatar>\r\n {{ row.originalUserId?.displayName }}\r\n </div>\r\n </ng-template>\r\n</ng-container>\r\n" }]
288
- }], propDecorators: { userCol: [{ type: i0.ViewChild, args: ['userCol', { isSignal: true }] }] } });
785
+ args: [{ selector: 'mt-audit-logs-list', imports: [
786
+ CommonModule,
787
+ Table,
788
+ Chip,
789
+ Button,
790
+ StatisticCard,
791
+ TranslocoDirective,
792
+ AuditLogDetailDrawer,
793
+ ], changeDetection: ChangeDetectionStrategy.OnPush, host: {
794
+ class: 'block h-full',
795
+ }, template: "<ng-container *transloco=\"let t; prefix: 'audit-logs'\">\n <div class=\"flex flex-col gap-4\">\n @if (activeFilterBadges().length > 0) {\n <div class=\"flex flex-wrap gap-2\">\n @for (badge of activeFilterBadges(); track badge) {\n <mt-chip\n [label]=\"badge\"\n styleClass=\"border border-surface-200 !bg-surface-0 !text-surface-700 text-xs\"\n />\n }\n </div>\n }\n\n <div class=\"grid gap-3 md:grid-cols-2 xl:grid-cols-4\">\n @for (card of summaryCards(); track card.subTitle) {\n <mt-statistic-card [data]=\"card\" cardClass=\"shadow-sm\" />\n }\n </div>\n\n @if (error(); as errorMessage) {\n <section\n class=\"flex flex-col gap-3 rounded-xl border border-rose-200 bg-rose-50 px-5 py-4 lg:flex-row lg:items-center lg:justify-between\"\n >\n <div class=\"space-y-1\">\n <p class=\"text-sm font-semibold text-rose-700\">\n {{ t(\"load-error-title\") }}\n </p>\n <p class=\"text-sm text-rose-600\">\n {{ errorMessage }}\n </p>\n </div>\n\n <mt-button\n severity=\"danger\"\n variant=\"outlined\"\n icon=\"general.refresh-cw-05\"\n [label]=\"t('retry')\"\n (onClick)=\"retry()\"\n />\n </section>\n }\n\n <mt-table\n [(filters)]=\"tableFilters\"\n [data]=\"auditLogs().items\"\n [columns]=\"tableColumns()\"\n [loading]=\"loading()\"\n [lazy]=\"true\"\n [showFilters]=\"true\"\n [generalSearch]=\"true\"\n [clickableRows]=\"true\"\n [lazyTotalRecords]=\"auditLogs().totalCount\"\n dataKey=\"eventId\"\n (lazyLoad)=\"onLazyLoad($event)\"\n (rowClick)=\"openEventDetail($event)\"\n />\n </div>\n\n <ng-template #summaryCol let-row>\n <div class=\"flex flex-col gap-2 py-1\">\n <p class=\"line-clamp-2 text-sm font-semibold text-surface-900\">\n {{ row.summary }}\n </p>\n <div\n class=\"flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-surface-500\"\n >\n <span>{{ t(\"event-id\") }}: {{ row.eventId }}</span>\n @if (row.primarySubjectType) {\n <span>{{ formatSubjectType(row) }}</span>\n }\n </div>\n </div>\n </ng-template>\n\n <ng-template #actorCol let-row>\n <div class=\"flex flex-col gap-1 py-1\">\n <span class=\"break-all text-sm font-medium text-surface-800\">\n {{ row.actorUserId || t(\"actor-unknown\") }}\n </span>\n <span class=\"text-xs text-surface-500\">\n {{ row.moduleKey || row.levelDataId || row.moduleDataId || \"audit\" }}\n </span>\n </div>\n </ng-template>\n\n <mt-audit-log-detail-drawer\n [visible]=\"drawerVisible()\"\n [detail]=\"detail()\"\n [loading]=\"loadingDetail()\"\n [error]=\"detailError()\"\n (visibleChange)=\"onDrawerVisibleChange($event)\"\n />\n</ng-container>\n", styles: [":host{display:block}\n"] }]
796
+ }], propDecorators: { summaryCol: [{ type: i0.ViewChild, args: ['summaryCol', { isSignal: true }] }], actorCol: [{ type: i0.ViewChild, args: ['actorCol', { isSignal: true }] }] } });
289
797
 
290
798
  // store/index.ts
291
799
 
@@ -293,5 +801,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImpor
293
801
  * Generated bundle index. Do not edit.
294
802
  */
295
803
 
296
- export { AuditLogs, AuditLogsActionKey, AuditLogsFacade, AuditLogsList, AuditLogsState, GetAuditLogs };
804
+ export { AuditLogs, AuditLogsActionKey, AuditLogsFacade, AuditLogsList, AuditLogsState, ClearAuditLogDetail, GetAuditLogDetail, GetAuditLogs };
297
805
  //# sourceMappingURL=masterteam-audit-logs.mjs.map